202508211817

加入 與後台資料
This commit is contained in:
allen.yan 2025-08-21 18:20:37 +08:00
parent 00c4225987
commit f395da76ec
12 changed files with 243 additions and 16 deletions

View File

@ -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,16 +58,21 @@ 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,
'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,// 下首

View File

@ -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;

View File

@ -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.

View File

@ -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',
];

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* @OA\Schema(
* schema="SessionRequest",
* required={"branch_name","hostname", "ip"},
* @OA\Property(property="branch_name", type="string", example="測試"),
* @OA\Property(property="hostname", type="string", example="PC101"),
* @OA\Property(property="ip", type="string", example="192.168.XX.XX"),
* )
*/
class SessionRequest extends ApiRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'branch_name' =>'required|string|exists:branches,name',
'hostname' => 'required|string',
'ip' => 'required|string',
'token' => 'nullable|string',
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class SyncOrderSongRequest extends ApiRequest
{
public function rules(): array
{
return [
'api_token' => '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',
];
}
}

View File

@ -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)
{

View File

@ -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';
}
}

View File

@ -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;
}
}

View File

@ -49,7 +49,6 @@ class RoomObserver
'mode' => $mode,
'status' => $room->status->value,
'started_at' => $now,
'api_token' => bin2hex(random_bytes(32)),
]);
}
}

View File

@ -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']);
});
}

View File

@ -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']);
});