diff --git a/app/Http/Controllers/Api/RoomSongController.php b/app/Http/Controllers/Api/RoomSongController.php index a626d03..c581ffc 100644 --- a/app/Http/Controllers/Api/RoomSongController.php +++ b/app/Http/Controllers/Api/RoomSongController.php @@ -4,8 +4,10 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Requests\OrderSongRequest; +use App\Http\Requests\SyncOrderSongRequest; use App\Http\Requests\RoomSongRequest; use Illuminate\Http\Request; +use App\Services\MachineStatusForwarder; use App\Models\Room; use App\Models\Song; use App\Models\OrderedSong; @@ -56,6 +58,10 @@ class RoomSongController extends Controller // 取得對應的 RoomSession(透過 api_token) $roomSession = $this->getRoomSession($request->api_token) ; + // 找這個 session 的最大 order_number,下一首加 1 + $lastOrder = OrderedSong::where('room_session_id', $roomSession->id)->max('order_number'); + $orderNumber = $lastOrder ? $lastOrder + 1 : 1; + // 取得歌曲名稱 $song = Song::findOrFail($request->song_id); @@ -63,6 +69,7 @@ class RoomSongController extends Controller $orderedSong = OrderedSong::create([ 'room_session_id' => $roomSession->id, 'from_by' => $request->from_by, + 'order_number' => $orderNumber, 'status' => $request->status, 'song_id' => $request->song_id, 'song_name' => $song->name, @@ -78,7 +85,7 @@ class RoomSongController extends Controller if ($countInSession === 1) { // 第一次點才加 $song->increment('song_counts'); } - + $this->sys($roomSession,$orderedSong); return ApiResponse::success([ 'ordered_song' => $orderedSong, 'next_song_name' => $this->nextSongName($roomSession), @@ -156,125 +163,6 @@ class RoomSongController extends Controller 'not_played' => $not_played, ]); } - /** - * @OA\Post( - * path="/api/room/current-song", - * summary="取得目前播放中的歌曲", - * description="回傳當前房間正在播放的歌曲資訊 (包含部分 song 欄位)", - * tags={"Room Control Song"}, - * security={{"Authorization":{}}}, - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/RoomSongRequest") - * ), - * @OA\Response( - * response=200, - * description="成功回傳目前播放的歌曲", - * @OA\JsonContent( - * type="object", - * @OA\Property( - * property="success", - * type="boolean", - * example=true - * ), - * @OA\Property( - * property="data", - * type="object", - * @OA\Property( - * property="current", - * ref="#/components/schemas/OrderedSongWithPartialSong" - * ) - * ) - * ) - * ) - * ) - */ - public function currentSong(RoomSongRequest $request) - { - $roomSession = $this->getRoomSession($request->api_token) ; - $current = OrderedSong::where('room_session_id', $roomSession->id) - ->where('status', 'Playing') - ->withPartialSong() - ->first(); - return ApiResponse::success([ - 'current' => $current , - ]); - } - /** - * @OA\Post( - * path="/api/room/next-song", - * summary="播放下一首歌曲", - * description="將目前播放的歌標記為結束,並將下一首設為播放中,回傳下首與下下首資訊。", - * tags={"Room Control Song"}, - * security={{"Authorization":{}}}, - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/RoomSongRequest") - * ), - * @OA\Response( - * response=200, - * description="成功回傳歌曲資訊", - * @OA\JsonContent( - * type="object", - * @OA\Property( - * property="current", - * type="object", - * nullable=true, - * ref="#/components/schemas/OrderedSongWithPartialSong" - * ), - * @OA\Property( - * property="next", - * type="object", - * nullable=true, - * ref="#/components/schemas/OrderedSongWithPartialSong" - * ) - * ) - * ), - * @OA\Response( - * response=401, - * description="未授權或 Token 錯誤" - * ), - * @OA\Response( - * response=404, - * description="找不到房間或歌曲" - * ) - * ) - */ - public function nextSong(RoomSongRequest $request) - { - $roomSession = $this->getRoomSession($request->api_token) ; - // 找目前正在播 - $current = OrderedSong::where('room_session_id', $roomSession->id) - ->where('status', 'Playing') - ->first(); - if ($current) { - // 把當前設為 finished,並記錄結束時間 - $current->update([ - 'status' => 'Played', - 'finished_at' => now(), - ]); - } - // 撈出候播清單(下首 + 下下首) - $queue = OrderedSong::where('room_session_id', $roomSession->id) - ->whereIn('status', ['InsertPlayback', 'NotPlayed']) - ->withPartialSong() - ->orderByRaw("FIELD(status, 'InsertPlayback', 'NotPlayed')") // InsertPlayback > NotPlayed - ->orderByRaw("CASE WHEN status = 'InsertPlayback' THEN ordered_at END DESC") // InsertPlayback 後插播的先播 - ->orderByRaw("CASE WHEN status = 'NotPlayed' THEN ordered_at END ASC") // NotPlayed 先點的先播 - ->limit(2) - ->get(); - $current =$queue->get(0); - if ($current) { - $current->update([ - 'status' => 'Playing', - 'started_at' => now(), - ]); - } - return ApiResponse::success([ - 'current' => $current,// 下首 - 'next' => $queue->get(1) ?? null,// 下下首 - ]); - } private function nextSongName(RoomSession $roomSession) { @@ -298,4 +186,60 @@ class RoomSongController extends Controller ->whereIn('status', ['active', 'maintain']) ->firstOrFail(); } + + private function sys($roomSession,$orderedSong) + { + $validated = [ + 'api_token' => $roomSession->api_token, + 'order_number' => $orderedSong->order_number, + 'from_by' => $orderedSong->from_by, + 'song_id' => $orderedSong->song_id, + 'song_name' => $orderedSong->song_name, + 'artist_name' => $orderedSong->artist_name, + 'status' => $orderedSong->status, + 'ordered_at' => $orderedSong->ordered_at, + 'started_at' => $orderedSong->started_at, + 'finished_at' => $orderedSong->finished_at, + ]; + $response = (new MachineStatusForwarder( + $roomSession->room->branch->external_ip, + "/api/room/sync-order-song", + $validated + ))->forward(); + } + public function syncOrderSong(SyncOrderSongRequest $request) + { + $roomSession = $this->getRoomSession($request->api_token) ; + + // 建立或更新 OrderedSong + $orderedSong = OrderedSong::updateOrCreate( + [ + 'room_session_id' => $roomSession->id, + 'order_number' => $request->order_number, + ], + [ + 'from_by' => $request->from_by, + 'song_id' => $request->song_id, + 'song_name' => $request->song_name, + 'artist_name' => $request->artist_name, + 'status' => $request->status, + 'ordered_at' => $request->ordered_at, + 'started_at' => $request->started_at, + 'finished_at' => $request->finished_at, + ] + ); + + // 檢查這首歌在此 session 是否第一次點 + if ($orderedSong->wasRecentlyCreated) { + $countInSession = OrderedSong::where('room_session_id', $roomSession->id) + ->where('song_id', $request->song_id)->count(); + if ($countInSession === 1) { // 第一次點才加 + $song->increment('song_counts'); + } + } + + return ApiResponse::success([ + 'ordered_song' => $orderedSong + ]); + } } \ No newline at end of file diff --git a/app/Http/Controllers/RoomControlController.php b/app/Http/Controllers/RoomControlController.php index 0d635bc..06b4e0c 100644 --- a/app/Http/Controllers/RoomControlController.php +++ b/app/Http/Controllers/RoomControlController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Http\Requests\SendRoomSwitchCommandRequest; use App\Http\Requests\ReceiveSwitchRequest; +use App\Http\Requests\SessionRequest; use App\Services\TcpSocketClient; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; @@ -22,6 +23,81 @@ use Illuminate\Support\Facades\Log; */ class RoomControlController extends Controller { + /** + * @OA\Post( + * path="/api/room/session", + * summary="取得包廂狀態", + * description="依據 hostname 與 branch_name 取得或建立房間,並回傳房間資料與最新 session", + * tags={"Room Control"}, + * security={{"Authorization":{}}}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent(ref="#/components/schemas/SessionRequest") + * ), + * @OA\Response( + * response=200, + * description="成功取得房間狀態", + * @OA\JsonContent( + * type="object", + * @OA\Property( + * property="room", + * type="object", + * ref="#/components/schemas/Room" + * ) + * ) + * ), + * @OA\Response( + * response=422, + * description="驗證失敗", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="message", type="string", example="The given data was invalid."), + * @OA\Property(property="errors", type="object") + * ) + * ) + * ) + */ + public function session(SessionRequest $request): JsonResponse + { + $validated = $request->validated(); + $roomType = null; + $roomName = null; + $floor = null; + // 從 room_name(例如 PC101, SVR01)中擷取 type 與 name + if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['hostname'], $matches)) { + $roomType = strtolower($matches[1]); // 'PC' → 'pc' + $roomName = $matches[2]; // '101' + if($roomType =='svr'){ + $floor = '0'; + } else{ + $floor = (int) substr($roomName, 0, 1); + } + } + $branch=Branch::where('name',$validated['branch_name'])->first(); + $room = Room::firstOrNew([ + 'branch_id' => $branch->id, + 'floor' => $floor, + 'name' => $roomName, + 'type' => $roomType, + ]); + $room->internal_ip = $validated['ip']; + $room->port = 1000; + $room->is_online=1; + $room->touch(); // 更新 updated_at + $room->save(); + $room->load('latestSession'); + if ($room->latestSession) { + $room->latestSession->api_token = !empty($validated['token']) + ? $validated['token'] + : bin2hex(random_bytes(32)); + $room->latestSession->save(); + $validated['token']=$room->latestSession->api_token; + } + return ApiResponse::success([ + 'room' => $room->latestSession, + ]); + + } /** * @OA\Post( * path="/api/room/sendSwitch", diff --git a/app/Http/Requests/SessionRequest.php b/app/Http/Requests/SessionRequest.php new file mode 100644 index 0000000..6ce2097 --- /dev/null +++ b/app/Http/Requests/SessionRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'branch_name' =>'required|string|exists:branches,name', + 'hostname' => 'required|string', + 'ip' => 'required|string', + 'token' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/SyncOrderSongRequest.php b/app/Http/Requests/SyncOrderSongRequest.php new file mode 100644 index 0000000..1d5404b --- /dev/null +++ b/app/Http/Requests/SyncOrderSongRequest.php @@ -0,0 +1,25 @@ + 'required|exists:room_sessions,api_token', + 'from_by' => 'nullable', + 'order_number' => 'required|integer', + 'song_id' => 'required|exists:songs,id', + 'song_name' => 'nullable', + 'artist_name' => 'nullable', + 'status' => 'required|in:NotPlayed,Playing,Played,NoFile,Skipped,InsertPlayback', + 'ordered_at' => 'required', + 'started_at' => 'nullable', + 'finished_at' => 'nullable', + ]; + } +} \ No newline at end of file diff --git a/app/Models/OrderedSong.php b/app/Models/OrderedSong.php index fb21747..0c92bca 100644 --- a/app/Models/OrderedSong.php +++ b/app/Models/OrderedSong.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Model; * @OA\Property(property="id", type="integer", example=123), * @OA\Property(property="room_session_id", type="integer", example=1), * @OA\Property(property="from_by", type="string", example="remote"), + * @OA\Property(property="order_number", type="integer", example=1), * @OA\Property(property="song_id", type="integer", example=5), * @OA\Property(property="song_name", type="string", example="歌名"), * @OA\Property(property="artist_name", type="string", example="歌手名稱"), @@ -49,6 +50,7 @@ class OrderedSong extends Model protected $fillable = [ 'room_session_id', 'from_by', + 'order_number', 'song_id', 'song_name', 'artist_name', diff --git a/app/Models/Room.php b/app/Models/Room.php index 20d79d2..0d2e748 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -2,6 +2,8 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Traits\LogsModelActivity; @@ -89,11 +91,27 @@ class Room extends Model return $this->type->labelPowergridFilter().$this->name; } - public function statusLogs() { + public function statusLogs() : HasMany + { return $this->hasMany(RoomStatusLog::class); } - public function sessions() + public function sessions() : HasMany { return $this->hasMany(RoomSession::class); } + /** + * 最新的一筆 session + */ + public function latestSession() : HasOne + { + return $this->hasOne(RoomSession::class)->latestOfMany(); + } + + /** + * 方便取得最新狀態 + */ + public function getLatestStatusAttribute() : string + { + return $this->latestSession?->status ?? 'error'; + } } diff --git a/app/Observers/RoomObserver.php b/app/Observers/RoomObserver.php index e7bd0b9..4ebfefc 100644 --- a/app/Observers/RoomObserver.php +++ b/app/Observers/RoomObserver.php @@ -49,7 +49,6 @@ class RoomObserver 'mode' => $mode, 'status' => $room->status->value, 'started_at' => $now, - 'api_token' => bin2hex(random_bytes(32)), ]); } } diff --git a/app/Services/MachineStatusForwarder.php b/app/Services/MachineStatusForwarder.php new file mode 100644 index 0000000..e91f0ed --- /dev/null +++ b/app/Services/MachineStatusForwarder.php @@ -0,0 +1,46 @@ +externalUrl = $externalUrl; + $this->endpoint = $endpoint; + $this->validated = $validated; + } + + public function forward(): ?Response + { + $response = null; + + $this->user = User::find(2); // 或用 dependency injection 把 User 帶進來 + + if ($this->user && $this->user->api_plain_token) { + $client = new ApiClient($this->externalUrl, $this->user->api_plain_token); + $response = $client->post($this->endpoint, $this->validated); + + Log::info('✅ Machine status forwarded', [ + 'endpoint' => $this->endpoint, + 'request' => $this->validated, + 'status' => $response->status(), + 'body' => $response->json(), + ]); + } else { + Log::warning("🔒 User with ID 2 not found or missing token"); + } + + return $response; + } +} \ No newline at end of file diff --git a/database/migrations/2025_08_18_104152_create_ordered_songs_table.php b/database/migrations/2025_08_18_104152_create_ordered_songs_table.php index c596df4..2dfe301 100644 --- a/database/migrations/2025_08_18_104152_create_ordered_songs_table.php +++ b/database/migrations/2025_08_18_104152_create_ordered_songs_table.php @@ -15,6 +15,7 @@ return new class extends Migration $table->id(); $table->foreignId('room_session_id')->constrained('room_sessions')->cascadeOnDelete(); $table->string('from_by')->nullable(); + $table->integer('order_number')->default(0); // 歌曲資訊 $table->foreignId('song_id')->constrained()->cascadeOnDelete(); @@ -30,7 +31,7 @@ return new class extends Migration $table->timestamp('started_at')->nullable(); // 開始播放時間 $table->timestamp('finished_at')->nullable(); // 播放結束時間 - + $table->unique(['room_session_id', 'order_number']); }); } diff --git a/routes/api.php b/routes/api.php index d77ed88..bfe2531 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,11 +16,11 @@ Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegi Route::middleware('auth:sanctum')->group(function () { Route::get ('/profile', [AuthController::class, 'profile']); Route::get ('/branches',[BranchControlController::class, 'Branches']); + Route::post('/room/sendSwitch', [RoomControlController::class, 'sendSwitch']); Route::post('/room/receiveSwitch', [RoomControlController::class, 'receiveSwitch']); - Route::post('/room/heartbeat', [RoomControlController::class, 'StatusReport']); + Route::post('/room/session',[RoomControlController::class, 'session']); Route::post('/room/order-song', [RoomSongController::class, 'orderSong']); Route::get ('/room/ordered-songs', [RoomSongController::class, 'listOrderedSongs']); - Route::post ('/room/current-song', [RoomSongController::class, 'currentSong']); - Route::post ('/room/next-song', [RoomSongController::class, 'nextSong']); + Route::post ('/room/sync-order-song', [RoomSongController::class, 'syncOrderSong']); }); \ No newline at end of file