diff --git a/app/Http/Controllers/Api/RoomSongController.php b/app/Http/Controllers/Api/RoomSongController.php index a626d03..f59831c 100644 --- a/app/Http/Controllers/Api/RoomSongController.php +++ b/app/Http/Controllers/Api/RoomSongController.php @@ -5,9 +5,11 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Requests\OrderSongRequest; use App\Http\Requests\RoomSongRequest; +use App\Http\Requests\SyncOrderSongRequest; use Illuminate\Http\Request; +use App\Services\MachineStatusForwarder; use App\Models\Room; -use App\Models\Song; +use App\Models\SongLibraryCache; use App\Models\OrderedSong; use App\Models\RoomSession; use Illuminate\Support\Facades\Auth; @@ -56,17 +58,22 @@ 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); + $song = SongLibraryCache::findOrFail($request->song_id); // 建立 OrderedSong $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, - 'artist_name' => $song->str_artists_plus(), + 'song_name' => $song->song_name, + 'artist_name' => $song->str_artists_plus(), 'ordered_at' => now(), ]); @@ -78,12 +85,66 @@ 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), ]); } + 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 + ]); + } /** * @OA\Get( * path="/api/room/ordered-songs", @@ -253,6 +314,7 @@ class RoomSongController extends Controller 'status' => 'Played', 'finished_at' => now(), ]); + $this->sys($roomSession,$current); } // 撈出候播清單(下首 + 下下首) $queue = OrderedSong::where('room_session_id', $roomSession->id) @@ -269,6 +331,7 @@ class RoomSongController extends Controller 'status' => 'Playing', 'started_at' => now(), ]); + $this->sys($roomSession,$current); } return ApiResponse::success([ 'current' => $current,// 下首 diff --git a/app/Http/Controllers/RoomControlController.php b/app/Http/Controllers/RoomControlController.php index 6cf1dc1..00ae616 100644 --- a/app/Http/Controllers/RoomControlController.php +++ b/app/Http/Controllers/RoomControlController.php @@ -4,7 +4,8 @@ namespace App\Http\Controllers; use App\Http\Requests\SendRoomSwitchCommandRequest; use App\Http\Requests\ReceiveRoomRegisterRequest; -use App\Http\Requests\ReceiveRoomStatusDataRequest; +use App\Http\Requests\HeartBeatRequest; +use App\Http\Requests\SessionRequest; use App\Services\TcpSocketClient; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; @@ -102,6 +103,87 @@ class RoomControlController extends Controller 'other_set' => $otherSet ]); } + /** + * @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; + } + + $response = (new MachineStatusForwarder( + $branch->external_ip, + "/api/room/session", + $validated + ))->forward(); + return ApiResponse::success([ + 'room' => $room->latestSession, + ]); + + } /** * @OA\Post( @@ -113,7 +195,7 @@ class RoomControlController extends Controller * security={{"Authorization":{}}}, * @OA\RequestBody( * required=true, - * @OA\JsonContent(ref="#/components/schemas/ReceiveRoomStatusDataRequest") + * @OA\JsonContent(ref="#/components/schemas/HeartBeatRequest") * ), * @OA\Response( * response=200, @@ -149,7 +231,7 @@ class RoomControlController extends Controller * ) * ) */ - public function StatusReport(ReceiveRoomStatusDataRequest $request) + public function HeartBeat(HeartBeatRequest $request) { $validated = $request->validated(); $roomType = null; diff --git a/app/Http/Requests/ReceiveRoomStatusDataRequest.php b/app/Http/Requests/HeartBeatRequest.php similarity index 91% rename from app/Http/Requests/ReceiveRoomStatusDataRequest.php rename to app/Http/Requests/HeartBeatRequest.php index 7e62797..caba528 100644 --- a/app/Http/Requests/ReceiveRoomStatusDataRequest.php +++ b/app/Http/Requests/HeartBeatRequest.php @@ -6,7 +6,7 @@ use Illuminate\Foundation\Http\FormRequest; /** * @OA\Schema( - * schema="ReceiveRoomStatusDataRequest", + * schema="HeartBeatRequest", * required={"branch_name","hostname", "ip"}, * @OA\Property(property="branch_name", type="string", example="測試"), * @OA\Property(property="hostname", type="string", example="PC101"), @@ -16,7 +16,7 @@ use Illuminate\Foundation\Http\FormRequest; * @OA\Property(property="disk", type="numeric", example="158266.49"), * ) */ -class ReceiveRoomStatusDataRequest extends ApiRequest +class HeartBeatRequest extends ApiRequest { /** * Get the validation rules that apply to the request. diff --git a/app/Http/Requests/OrderSongRequest.php b/app/Http/Requests/OrderSongRequest.php index 7beb1bc..b0b3f07 100644 --- a/app/Http/Requests/OrderSongRequest.php +++ b/app/Http/Requests/OrderSongRequest.php @@ -25,7 +25,7 @@ class OrderSongRequest extends ApiRequest Rule::exists('room_sessions', 'api_token') ->where(fn ($q) => $q->whereIn('status', ['active', 'maintain'])) ], - 'song_id' => 'required|exists:songs,id', + 'song_id' => 'required|exists:song_library_cache,song_id', 'status' => 'required|in:NotPlayed,InsertPlayback,Skipped', 'from_by' => 'nullable', ]; 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..580c573 --- /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:song_library_cache,song_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..552088c 100644 --- a/app/Models/OrderedSong.php +++ b/app/Models/OrderedSong.php @@ -49,6 +49,7 @@ class OrderedSong extends Model protected $fillable = [ 'room_session_id', 'from_by', + 'order_number', 'song_id', 'song_name', 'artist_name', @@ -74,7 +75,7 @@ class OrderedSong extends Model public function song() { - return $this->belongsTo(Song::class); + return $this->belongsTo(SongLibraryCache::class); } public function scopeWithPartialSong($query) { diff --git a/app/Models/Room.php b/app/Models/Room.php index 6acbb83..0c690f3 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; @@ -92,8 +94,23 @@ class Room extends Model public function statusLogs() { 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/Models/SongLibraryCache.php b/app/Models/SongLibraryCache.php index 16ddd10..4f208c2 100644 --- a/app/Models/SongLibraryCache.php +++ b/app/Models/SongLibraryCache.php @@ -39,4 +39,8 @@ class SongLibraryCache extends Model 'updated_at', ]; + public function str_artists_plus(): string + { + return ($this->artistB!=null) ? $this->artistA ." + ".$this->artistB :$this->artistA; + } } 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/database/migrations/2025_08_18_104152_create_ordered_songs_table.php b/database/migrations/2025_08_18_104152_create_ordered_songs_table.php index eb3cf77..0fee13f 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->unsignedBigInteger('song_id'); @@ -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 bc8c465..a5e8d6a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\ArtistController; use App\Http\Controllers\AuthController; use App\Http\Controllers\RoomControlController; +use App\Http\Controllers\Api\RoomSongController; use App\Http\Controllers\SqliteUploadController; Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegister']); @@ -11,12 +12,14 @@ Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegi Route::middleware('auth:sanctum')->group(function () { Route::get('/profile', [AuthController::class, 'profile']); Route::post('/room/sendSwitch', [RoomControlController::class, 'sendSwitch']); - Route::post('/room/heartbeat', [RoomControlController::class, 'StatusReport']); + Route::post('/room/heartbeat', [RoomControlController::class, 'HeartBeat']); + 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']); Route::post('/upload-sqlite', [SqliteUploadController::class, 'upload']); }); \ No newline at end of file