Compare commits
3 Commits
19eba46173
...
bb1defcc98
Author | SHA1 | Date | |
---|---|---|---|
bb1defcc98 | |||
f395da76ec | |||
00c4225987 |
35
app/Enums/OrderedSongStatus.php
Normal file
35
app/Enums/OrderedSongStatus.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="OrderedSongStatus",
|
||||||
|
* type="string",
|
||||||
|
* enum={"NotPlayed", "Playing", "Played", "NoFile", "Skipped", "InsertPlayback"},
|
||||||
|
* example="NotPlayed"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
enum OrderedSongStatus: string
|
||||||
|
{
|
||||||
|
case NotPlayed = 'NotPlayed';
|
||||||
|
case Playing = 'Playing';
|
||||||
|
case Played = 'Played';
|
||||||
|
case NoFile = 'NoFile';
|
||||||
|
case Skipped = 'Skipped';
|
||||||
|
case InsertPlayback = 'InsertPlayback';
|
||||||
|
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::NotPlayed => __('enums.NotPlayed'),
|
||||||
|
self::Playing => __('enums.Playing'),
|
||||||
|
self::Played => __('enums.Played'),
|
||||||
|
self::NoFile => __('enums.NoFile'),
|
||||||
|
self::Skipped => __('enums.Skipped'),
|
||||||
|
self::InsertPlayback => __('enums.InsertPlayback'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
364
app/Http/Controllers/Api/RoomSongController.php
Normal file
364
app/Http/Controllers/Api/RoomSongController.php
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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\SongLibraryCache;
|
||||||
|
use App\Models\OrderedSong;
|
||||||
|
use App\Models\RoomSession;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
|
||||||
|
class RoomSongController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/room/order-song",
|
||||||
|
* summary="點歌",
|
||||||
|
* description="在指定包廂點一首歌曲",
|
||||||
|
* tags={"Room Control Song"},
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/OrderSongRequest")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(
|
||||||
|
* property="ordered_song",
|
||||||
|
* ref="#/components/schemas/OrderedSong"
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="next_song_name", type="string", example="XXXSSSS")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @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 orderSong(OrderSongRequest $request)
|
||||||
|
{
|
||||||
|
// 取得對應的 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 = 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->song_name,
|
||||||
|
'artist_name' => $song->str_artists_plus(),
|
||||||
|
'ordered_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 檢查這首歌在此 session 是否第一次點
|
||||||
|
$countInSession = OrderedSong::where('room_session_id', $roomSession->id)
|
||||||
|
->where('song_id', $request->song_id)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
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",
|
||||||
|
* summary="取得包廂點歌列表",
|
||||||
|
* description="取得指定包廂的已播、正在播放、待播/插播歌曲列表",
|
||||||
|
* tags={"Room Control Song"},
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/ApiTokenQuery"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功取得點歌列表",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(
|
||||||
|
* property="played",
|
||||||
|
* type="array",
|
||||||
|
* description="已播放 (Played, Skipped, NoFile)",
|
||||||
|
* @OA\Items(ref="#/components/schemas/OrderedSong")
|
||||||
|
* ),
|
||||||
|
* @OA\Property(
|
||||||
|
* property="playing",
|
||||||
|
* type="array",
|
||||||
|
* description="正在播放中 (Playing)",
|
||||||
|
* @OA\Items(ref="#/components/schemas/OrderedSong")
|
||||||
|
* ),
|
||||||
|
* @OA\Property(
|
||||||
|
* property="not_played",
|
||||||
|
* type="array",
|
||||||
|
* description="未播放 (NotPlayed, InsertPlayback)",
|
||||||
|
* @OA\Items(ref="#/components/schemas/OrderedSong")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @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 listOrderedSongs(RoomSongRequest $request)
|
||||||
|
{
|
||||||
|
$roomSession = $this->getRoomSession($request->api_token) ;
|
||||||
|
// 已結束 (finished + canceled)
|
||||||
|
$played = OrderedSong::where('room_session_id', $roomSession->id)
|
||||||
|
->whereIn('status', ['Played', 'Skipped', 'NoFile'])
|
||||||
|
->orderByDesc('finished_at')
|
||||||
|
->get();
|
||||||
|
$playing = OrderedSong::where('room_session_id', $roomSession->id)
|
||||||
|
->whereIn('status', ['Playing'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 正在播 + 插播 + 待播
|
||||||
|
$not_played = OrderedSong::where('room_session_id', $roomSession->id)
|
||||||
|
->whereIn('status', ['Playing', 'InsertPlayback', 'NotPlayed'])
|
||||||
|
->orderByRaw("FIELD(status, 'Playing', 'InsertPlayback', 'NotPlayed')") // playing > InsertPlayback > NotPlayed
|
||||||
|
->orderByRaw("CASE
|
||||||
|
WHEN status = 'InsertPlayback' THEN ordered_at END DESC") // InsertPlayback 越後排越前
|
||||||
|
->orderByRaw("CASE
|
||||||
|
WHEN status = 'NotPlayed' THEN ordered_at END ASC") // NotPlayed 越後排越後
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'played' => $played,
|
||||||
|
'playing' => $playing,
|
||||||
|
'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(),
|
||||||
|
]);
|
||||||
|
$this->sys($roomSession,$current);
|
||||||
|
}
|
||||||
|
// 撈出候播清單(下首 + 下下首)
|
||||||
|
$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(),
|
||||||
|
]);
|
||||||
|
$this->sys($roomSession,$current);
|
||||||
|
}
|
||||||
|
return ApiResponse::success([
|
||||||
|
'current' => $current,// 下首
|
||||||
|
'next' => $queue->get(1) ?? null,// 下下首
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextSongName(RoomSession $roomSession)
|
||||||
|
{
|
||||||
|
// 找下首
|
||||||
|
$next = OrderedSong::where('room_session_id', $roomSession->id)
|
||||||
|
->whereIn('status', ['InsertPlayback', 'NotPlayed'])
|
||||||
|
->with('song')
|
||||||
|
->orderByRaw("FIELD(status, 'InsertPlayback', 'NotPlayed')")
|
||||||
|
->orderByRaw("CASE WHEN status = 'InsertPlayback' THEN ordered_at END DESC")
|
||||||
|
->orderByRaw("CASE WHEN status = 'NotPlayed' THEN ordered_at END ASC")
|
||||||
|
->first();
|
||||||
|
return $next?->song?->name;
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得對應的 RoomSession
|
||||||
|
*/
|
||||||
|
private function getRoomSession($api_token): RoomSession
|
||||||
|
{
|
||||||
|
return RoomSession::where('api_token', $api_token)
|
||||||
|
->whereIn('status', ['active', 'maintain'])
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,8 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Http\Requests\SendRoomSwitchCommandRequest;
|
use App\Http\Requests\SendRoomSwitchCommandRequest;
|
||||||
use App\Http\Requests\ReceiveRoomRegisterRequest;
|
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 App\Services\TcpSocketClient;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@ -102,6 +103,87 @@ class RoomControlController extends Controller
|
|||||||
'other_set' => $otherSet
|
'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(
|
* @OA\Post(
|
||||||
@ -113,7 +195,7 @@ class RoomControlController extends Controller
|
|||||||
* security={{"Authorization":{}}},
|
* security={{"Authorization":{}}},
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
* @OA\JsonContent(ref="#/components/schemas/ReceiveRoomStatusDataRequest")
|
* @OA\JsonContent(ref="#/components/schemas/HeartBeatRequest")
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
@ -149,7 +231,7 @@ class RoomControlController extends Controller
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function StatusReport(ReceiveRoomStatusDataRequest $request)
|
public function HeartBeat(HeartBeatRequest $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$roomType = null;
|
$roomType = null;
|
||||||
@ -265,6 +347,8 @@ class RoomControlController extends Controller
|
|||||||
return ApiResponse::error('房間未設定 IP 或 Port');
|
return ApiResponse::error('房間未設定 IP 或 Port');
|
||||||
}
|
}
|
||||||
$room->status=$validated['command'];
|
$room->status=$validated['command'];
|
||||||
|
$room->log_source='api';
|
||||||
|
$room->log_message='sendSwitch';
|
||||||
$room->started_at=$validated['started_at'];
|
$room->started_at=$validated['started_at'];
|
||||||
$room->ended_at=$validated['ended_at'];
|
$room->ended_at=$validated['ended_at'];
|
||||||
$room->save();
|
$room->save();
|
||||||
@ -279,7 +363,6 @@ class RoomControlController extends Controller
|
|||||||
};
|
};
|
||||||
$data = $suffix . "," . $signal;
|
$data = $suffix . "," . $signal;
|
||||||
|
|
||||||
//dd($data);
|
|
||||||
$client = new TcpSocketClient($room->internal_ip, $room->port);
|
$client = new TcpSocketClient($room->internal_ip, $room->port);
|
||||||
try {
|
try {
|
||||||
$response = $client->send($data);
|
$response = $client->send($data);
|
||||||
|
@ -6,7 +6,7 @@ use Illuminate\Foundation\Http\FormRequest;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Schema(
|
* @OA\Schema(
|
||||||
* schema="ReceiveRoomStatusDataRequest",
|
* schema="HeartBeatRequest",
|
||||||
* required={"branch_name","hostname", "ip"},
|
* required={"branch_name","hostname", "ip"},
|
||||||
* @OA\Property(property="branch_name", type="string", example="測試"),
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
* @OA\Property(property="hostname", type="string", example="PC101"),
|
* @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"),
|
* @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.
|
* Get the validation rules that apply to the request.
|
33
app/Http/Requests/OrderSongRequest.php
Normal file
33
app/Http/Requests/OrderSongRequest.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="OrderSongRequest",
|
||||||
|
* required={"song_id","status","api_token"},
|
||||||
|
* @OA\Property(property="song_id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="status", type="string", enum={"NotPlayed", "InsertPlayback", "Skipped"},example="NotPlayed"),
|
||||||
|
* @OA\Property(property="from_by", type="string", example="介面,ERP之類的說明"),
|
||||||
|
* @OA\Property(property="api_token", type="string", example="da9cdb88a60a377bba9fc53f09dd07838eb6f4f355d63f4927c711e7aaae3104"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class OrderSongRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
|
||||||
|
'api_token' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists('room_sessions', 'api_token')
|
||||||
|
->where(fn ($q) => $q->whereIn('status', ['active', 'maintain']))
|
||||||
|
],
|
||||||
|
'song_id' => 'required|exists:song_library_cache,song_id',
|
||||||
|
'status' => 'required|in:NotPlayed,InsertPlayback,Skipped',
|
||||||
|
'from_by' => 'nullable',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
43
app/Http/Requests/RoomSongRequest.php
Normal file
43
app/Http/Requests/RoomSongRequest.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Parameter(
|
||||||
|
* parameter="ApiTokenQuery",
|
||||||
|
* name="api_token",
|
||||||
|
* in="query",
|
||||||
|
* required=true,
|
||||||
|
* description="Room session 的 API token",
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* example="da9cdb88a60a377bba9fc53f09dd07838eb6f4f355d63f4927c711e7aaae3104"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="RoomSongRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"api_token"},
|
||||||
|
* @OA\Property(
|
||||||
|
* property="api_token",
|
||||||
|
* type="string",
|
||||||
|
* description="Room session 的 API token",
|
||||||
|
* example="da9cdb88a60a377bba9fc53f09dd07838eb6f4f355d63f4927c711e7aaae3104"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class RoomSongRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'api_token' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists('room_sessions', 'api_token')
|
||||||
|
->where(fn ($q) => $q->whereIn('status', ['active', 'maintain']))
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
32
app/Http/Requests/SessionRequest.php
Normal file
32
app/Http/Requests/SessionRequest.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
25
app/Http/Requests/SyncOrderSongRequest.php
Normal file
25
app/Http/Requests/SyncOrderSongRequest.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ namespace App\Livewire\Modals;
|
|||||||
|
|
||||||
use App\Models\Room;
|
use App\Models\Room;
|
||||||
use App\Models\Branch;
|
use App\Models\Branch;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use App\Services\ApiClient;
|
use App\Services\ApiClient;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@ -36,20 +36,20 @@ class RoomDetailModal extends Component
|
|||||||
}
|
}
|
||||||
public function startNotify()
|
public function startNotify()
|
||||||
{
|
{
|
||||||
$data = $this->buildNotifyData('maintain', null, null);
|
$data = $this->buildNotifyData('maintain', Carbon::now(), null);
|
||||||
$this->send($data);
|
$this->send($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stopNotify()
|
public function stopNotify()
|
||||||
{
|
{
|
||||||
$data = $this->buildNotifyData('closed', null, null);
|
$data = $this->buildNotifyData('closed', null, Carbon::now());
|
||||||
$chk =$this->send($data);
|
$chk =$this->send($data);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fireNotify()
|
public function fireNotify()
|
||||||
{
|
{
|
||||||
$data = $this->buildNotifyData('fire', null, null);
|
$data = $this->buildNotifyData('fire', null, Carbon::now());
|
||||||
$this->send($data);
|
$this->send($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Tables;
|
namespace App\Livewire\Tables;
|
||||||
|
|
||||||
|
use App\Models\Branch;
|
||||||
use App\Models\RoomStatusLog;
|
use App\Models\RoomStatusLog;
|
||||||
|
use App\Enums\RoomType;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use PowerComponents\LivewirePowerGrid\Button;
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
@ -37,28 +39,47 @@ final class RoomStatusLogTable extends PowerGridComponent
|
|||||||
|
|
||||||
public function datasource(): Builder
|
public function datasource(): Builder
|
||||||
{
|
{
|
||||||
return RoomStatusLog::query()->latest();;
|
return RoomStatusLog::with(['room', 'branch'])->latest();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function relationSearch(): array
|
public function relationSearch(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
'branch' => ['name'],
|
||||||
|
'room' => ['name'],
|
||||||
|
'user' => ['name'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fields(): PowerGridFields
|
public function fields(): PowerGridFields
|
||||||
{
|
{
|
||||||
return PowerGrid::fields()
|
return PowerGrid::fields()
|
||||||
->add('id')
|
->add('id')
|
||||||
|
->add('branch_name', function (RoomStatusLog $model) {
|
||||||
|
return $model->branch?->name;
|
||||||
|
})
|
||||||
|
->add('room_type', function (RoomStatusLog $model) {
|
||||||
|
return $model->room?->type->labelPowergridFilter();
|
||||||
|
})
|
||||||
->add('room_name', function (RoomStatusLog $model) {
|
->add('room_name', function (RoomStatusLog $model) {
|
||||||
return $model->room?->type->labelPowergridFilter().$model->room?->name;
|
return $model->room?->name;
|
||||||
})
|
})
|
||||||
->add('user_name', function (RoomStatusLog $model){
|
->add('user_name', function (RoomStatusLog $model){
|
||||||
return $model->user?->name;
|
return $model->user?->name;
|
||||||
})
|
})
|
||||||
|
->add('is_online_img', fn ($model) =>
|
||||||
|
[
|
||||||
|
$model->is_online ? 'check-circle' : 'x-circle' => [
|
||||||
|
'text-color' => $model->is_online ? 'text-green-600' : 'text-red-600',
|
||||||
|
],
|
||||||
|
])
|
||||||
->add('status_str',function (RoomStatusLog $model){
|
->add('status_str',function (RoomStatusLog $model){
|
||||||
return $model->status->labelPowergridFilter();
|
return $model->status->labelPowergridFilter();
|
||||||
})
|
})
|
||||||
|
->add('started_at')
|
||||||
|
->add('ended_at')
|
||||||
->add('message')
|
->add('message')
|
||||||
|
->add('source')
|
||||||
->add('created_at');
|
->add('created_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,17 +87,48 @@ final class RoomStatusLogTable extends PowerGridComponent
|
|||||||
{
|
{
|
||||||
$column=[];
|
$column=[];
|
||||||
$column[]=Column::make(__('room-status-log.id'), 'id');
|
$column[]=Column::make(__('room-status-log.id'), 'id');
|
||||||
$column[]=Column::make(__('room-status-log.room'), 'room_name');
|
$column[]=Column::make(__('room-status-log.branch'), 'branch_name');
|
||||||
|
$column[]=Column::make(__('room-status-log.room_type'), 'room_type');
|
||||||
|
$column[]=Column::make(__('room-status-log.room_name'), 'room_name');
|
||||||
$column[]=Column::make(__('room-status-log.user'), 'user_name');
|
$column[]=Column::make(__('room-status-log.user'), 'user_name');
|
||||||
|
$column[]=Column::make(__('room-status-log.is_online'), 'is_online_img')->template();
|
||||||
$column[]=Column::make(__('room-status-log.status'), 'status_str');
|
$column[]=Column::make(__('room-status-log.status'), 'status_str');
|
||||||
|
$column[]=Column::make(__('room-status-log.started_at'), 'started_at');
|
||||||
|
$column[]=Column::make(__('room-status-log.ended_at'), 'ended_at');
|
||||||
$column[]=Column::make(__('room-status-log.message'), 'message');
|
$column[]=Column::make(__('room-status-log.message'), 'message');
|
||||||
|
$column[]=Column::make(__('room-status-log.source'), 'source');
|
||||||
$column[]=Column::make(__('room-status-log.created_at'), 'created_at');
|
$column[]=Column::make(__('room-status-log.created_at'), 'created_at');
|
||||||
return $column;
|
return $column;
|
||||||
}
|
}
|
||||||
|
public function rowTemplates(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'check-circle' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 {{ text-color }}">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||||
|
</svg>',
|
||||||
|
'x-circle' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 {{ text-color }}">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||||
|
</svg>',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function filters(): array
|
public function filters(): array
|
||||||
{
|
{
|
||||||
|
$branches = Branch::query()->orderBy('name')->get()->map(fn($branch) => (object)[
|
||||||
|
'value' => $branch->id,
|
||||||
|
'label' => $branch->name,
|
||||||
|
]);
|
||||||
|
//dd($branches);
|
||||||
return [
|
return [
|
||||||
|
Filter::enumSelect('branch_name','branch_id')
|
||||||
|
->datasource($branches)
|
||||||
|
->optionLabel('label'),
|
||||||
|
Filter::inputText('room_type')
|
||||||
|
->placeholder('輸入"pc","svr"')
|
||||||
|
->filterRelation('room','type'),
|
||||||
|
Filter::inputText('room_name')
|
||||||
|
->placeholder('輸入包廂名稱')
|
||||||
|
->filterRelation('room','name')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
app/Models/OrderedSong.php
Normal file
88
app/Models/OrderedSong.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="OrderedSong",
|
||||||
|
* type="object",
|
||||||
|
* @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="song_id", type="integer", example=5),
|
||||||
|
* @OA\Property(property="song_name", type="string", example="歌名"),
|
||||||
|
* @OA\Property(property="artist_name", type="string", example="歌手名稱"),
|
||||||
|
* @OA\Property(property="status", ref="#/components/schemas/OrderedSongStatus"),
|
||||||
|
* @OA\Property(property="ordered_at", type="string", format="date-time", example="2025-08-18T14:00:00Z"),
|
||||||
|
* @OA\Property(property="started_at", type="string", format="date-time", nullable=true, example="2025-08-18T14:05:00Z"),
|
||||||
|
* @OA\Property(property="finished_at", type="string", format="date-time", nullable=true, example="2025-08-18T14:10:00Z"),
|
||||||
|
* )
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="OrderedSongWithPartialSong",
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/OrderedSong"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(
|
||||||
|
* property="song",
|
||||||
|
* type="object",
|
||||||
|
* nullable=true,
|
||||||
|
* @OA\Property(property="id", type="integer", example=5),
|
||||||
|
* @OA\Property(property="name", type="string", example="歌名"),
|
||||||
|
* @OA\Property(property="filename", type="string", example="song123.mp4"),
|
||||||
|
* @OA\Property(property="db_change", type="integer", example=-2),
|
||||||
|
* @OA\Property(property="vocal", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="situation", type="string", example="party")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class OrderedSong extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'room_session_id',
|
||||||
|
'from_by',
|
||||||
|
'order_number',
|
||||||
|
'song_id',
|
||||||
|
'song_name',
|
||||||
|
'artist_name',
|
||||||
|
'status',
|
||||||
|
'ordered_at',
|
||||||
|
'started_at',
|
||||||
|
'finished_at',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'ordered_at' => 'datetime',
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'finished_at' => 'datetime',
|
||||||
|
'status' => \App\Enums\OrderedSongStatus::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function session()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(RoomSession::class, 'room_session_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function song()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SongLibraryCache::class);
|
||||||
|
}
|
||||||
|
public function scopeWithPartialSong($query)
|
||||||
|
{
|
||||||
|
return $query->with([
|
||||||
|
'song' => function ($q) {
|
||||||
|
$q->select('id', 'name','filename','db_change','vocal','situation'); // 精簡版
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Traits\LogsModelActivity;
|
use App\Traits\LogsModelActivity;
|
||||||
@ -22,11 +23,15 @@ use App\Traits\LogsModelActivity;
|
|||||||
* @OA\Property(property="ended_at", type="string", format="date-time", example=null),
|
* @OA\Property(property="ended_at", type="string", format="date-time", example=null),
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Room extends Model
|
class Room extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||||
use HasFactory, LogsModelActivity;
|
use HasFactory, LogsModelActivity;
|
||||||
|
|
||||||
|
public string $log_message = 'BranchForm-add';
|
||||||
|
public string $log_source = 'manual';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'branch_id',
|
'branch_id',
|
||||||
'floor',
|
'floor',
|
||||||
@ -39,6 +44,12 @@ class Room extends Model
|
|||||||
'started_at',
|
'started_at',
|
||||||
'ended_at',
|
'ended_at',
|
||||||
];
|
];
|
||||||
|
protected $attributes = [
|
||||||
|
'type' => \App\Enums\RoomType::Unset,
|
||||||
|
'floor' => 1,
|
||||||
|
'is_online' => false,
|
||||||
|
'status' => \App\Enums\RoomStatus::Error,
|
||||||
|
];
|
||||||
|
|
||||||
//protected $hidden = [
|
//protected $hidden = [
|
||||||
// 'internal_ip',
|
// 'internal_ip',
|
||||||
@ -55,7 +66,6 @@ class Room extends Model
|
|||||||
'status' => \App\Enums\RoomStatus::class,
|
'status' => \App\Enums\RoomStatus::class,
|
||||||
'started_at' => 'datetime',
|
'started_at' => 'datetime',
|
||||||
'ended_at' => 'datetime',
|
'ended_at' => 'datetime',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function str_started_at(){
|
public function str_started_at(){
|
||||||
@ -77,8 +87,30 @@ class Room extends Model
|
|||||||
public function branch() {
|
public function branch() {
|
||||||
return $this->belongsTo(Branch::class);
|
return $this->belongsTo(Branch::class);
|
||||||
}
|
}
|
||||||
|
public function full_name(){
|
||||||
|
return $this->type->labelPowergridFilter().$this->name;
|
||||||
|
}
|
||||||
|
|
||||||
public function statusLogs() {
|
public function statusLogs() {
|
||||||
return $this->hasMany(RoomStatusLog::class);
|
return $this->hasMany(RoomStatusLog::class);
|
||||||
}
|
}
|
||||||
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
42
app/Models/RoomSession.php
Normal file
42
app/Models/RoomSession.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class RoomSession extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'room_id',
|
||||||
|
'started_at',
|
||||||
|
'ended_at',
|
||||||
|
'status',
|
||||||
|
'mode',
|
||||||
|
'close_reason',
|
||||||
|
'api_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'ended_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 狀態常數
|
||||||
|
public const STATUS_ACTIVE = 'active';
|
||||||
|
public const STATUS_CLOSED = 'closed';
|
||||||
|
public const STATUS_FORCE_CLOSED = 'force_closed';
|
||||||
|
public const STATUS_FIRE_CLOSED = 'fire_closed';
|
||||||
|
|
||||||
|
// 模式常數
|
||||||
|
public const MODE_NORMAL = 'normal';
|
||||||
|
public const MODE_VIP = 'vip';
|
||||||
|
public const MODE_TEST = 'test';
|
||||||
|
|
||||||
|
public function room()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Room::class);
|
||||||
|
}
|
||||||
|
}
|
@ -13,18 +13,30 @@ class RoomStatusLog extends Model
|
|||||||
|
|
||||||
protected $fillable =
|
protected $fillable =
|
||||||
[
|
[
|
||||||
|
'branch_id',
|
||||||
'room_id',
|
'room_id',
|
||||||
'user_id',
|
'user_id',
|
||||||
|
'is_online',
|
||||||
'status',
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'ended_at',
|
||||||
'message',
|
'message',
|
||||||
|
'source',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'is_online' => 'boolean',
|
||||||
'status' => \App\Enums\RoomStatus::class,
|
'status' => \App\Enums\RoomStatus::class,
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'ended_at' => 'datetime',
|
||||||
|
|
||||||
];
|
];
|
||||||
public function user(){
|
public function user(){
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
public function branch() {
|
||||||
|
return $this->belongsTo(Branch::class);
|
||||||
|
}
|
||||||
public function room() {
|
public function room() {
|
||||||
return $this->belongsTo(Room::class);
|
return $this->belongsTo(Room::class);
|
||||||
}
|
}
|
||||||
|
@ -39,4 +39,8 @@ class SongLibraryCache extends Model
|
|||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function str_artists_plus(): string
|
||||||
|
{
|
||||||
|
return ($this->artistB!=null) ? $this->artistA ." + ".$this->artistB :$this->artistA;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
use App\Models\Room;
|
use App\Models\Room;
|
||||||
use App\Models\RoomStatusLog;
|
use App\Models\RoomStatusLog;
|
||||||
@ -14,7 +15,8 @@ class RoomObserver
|
|||||||
*/
|
*/
|
||||||
public function created(Room $room): void
|
public function created(Room $room): void
|
||||||
{
|
{
|
||||||
//
|
// 建立初始 log
|
||||||
|
$this->createStatusLog($room);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,14 +25,33 @@ class RoomObserver
|
|||||||
public function updated(Room $room): void
|
public function updated(Room $room): void
|
||||||
{
|
{
|
||||||
// 檢查是否有變更狀態
|
// 檢查是否有變更狀態
|
||||||
if ($room->wasChanged('status')) {
|
if ($room->wasChanged()) {
|
||||||
RoomStatusLog::create([
|
$this->createStatusLog($room);
|
||||||
'room_id' => $room->id,
|
}
|
||||||
'user_id' => Auth::id(), // 若是 console 或系統自動操作可能為 null
|
if ($room->isDirty('status')) {
|
||||||
'status' => $room->status,
|
$now = now();
|
||||||
'message' => 'started_at:'.$room->started_at.',ended_at:'.$room->ended_at,
|
|
||||||
|
// 找到最後一筆未結束 session
|
||||||
|
$lastSession = $room->sessions()->whereNull('ended_at')->latest('started_at')->first();
|
||||||
|
|
||||||
|
if ($lastSession) {
|
||||||
|
// 結束上一筆 session
|
||||||
|
$lastSession->update([
|
||||||
|
'status' => $room->status->value,
|
||||||
|
'ended_at' => $now,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果狀態是 active 或 maintain,開新 session
|
||||||
|
if (in_array($room->status->value, ['active', 'maintain'])) {
|
||||||
|
$mode = $room->status->value === 'active' ? 'normal' : 'test';
|
||||||
|
$room->sessions()->create([
|
||||||
|
'mode' => $mode,
|
||||||
|
'status' => $room->status->value,
|
||||||
|
'started_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,22 +59,38 @@ class RoomObserver
|
|||||||
*/
|
*/
|
||||||
public function deleted(Room $room): void
|
public function deleted(Room $room): void
|
||||||
{
|
{
|
||||||
//
|
$message = sprintf(
|
||||||
|
"%s:%s%s (%s:%s) 已刪除",
|
||||||
|
$room->branch->name,
|
||||||
|
$room->type->value,
|
||||||
|
$room->name,
|
||||||
|
$room->internal_ip,
|
||||||
|
$room->port
|
||||||
|
);
|
||||||
|
$this->createStatusLog($room,$message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the Room "restored" event.
|
* 建立 RoomStatusLog
|
||||||
*/
|
*/
|
||||||
public function restored(Room $room): void
|
private function createStatusLog(Room $room,$log_message =null): void
|
||||||
{
|
{
|
||||||
//
|
$message=($log_message !=null)?$log_message:$room->log_message ?? '';
|
||||||
|
RoomStatusLog::create([
|
||||||
|
'branch_id' => $room->branch->id,
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'user_id' => Auth::id() ?? 0,
|
||||||
|
'is_online' => $room->is_online,
|
||||||
|
'status' => $room->status,
|
||||||
|
'started_at' => $room->started_at,
|
||||||
|
'ended_at' => $room->ended_at,
|
||||||
|
'message' => $message,
|
||||||
|
'source' => $this->getSource($room),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getSource(Room $room): string
|
||||||
* Handle the Room "force deleted" event.
|
|
||||||
*/
|
|
||||||
public function forceDeleted(Room $room): void
|
|
||||||
{
|
{
|
||||||
//
|
return app()->runningInConsole() ? 'system' : ($room->log_source ?? 'manual');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,19 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('room_status_logs', function (Blueprint $table) {
|
Schema::create('room_status_logs', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('room_id')->nullable();
|
$table->unsignedBigInteger('branch_id');
|
||||||
$table->foreignId('user_id')->nullable(); // 操作者,可為 null(系統)
|
$table->unsignedBigInteger('room_id');
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
$table->tinyInteger('is_online')->default(0);
|
||||||
$table->enum('status', ['active', 'closed','fire', 'error', 'maintain']);
|
$table->enum('status', ['active', 'closed','fire', 'error', 'maintain']);
|
||||||
|
$table->timestamp('started_at')->nullable();
|
||||||
|
$table->timestamp('ended_at')->nullable();
|
||||||
$table->text('message')->nullable(); // 可填異常原因或操作說明
|
$table->text('message')->nullable(); // 可填異常原因或操作說明
|
||||||
|
$table->enum('source', ['system', 'manual', 'api'])->default('manual');
|
||||||
|
$table->index('branch_id');
|
||||||
|
$table->index('room_id');
|
||||||
|
$table->index('user_id');
|
||||||
|
$table->index('started_at');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('room_sessions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('room_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->enum('status', ['active', 'closed','fire', 'error', 'maintain'])->default('error');
|
||||||
|
$table->timestamp('started_at')->nullable();
|
||||||
|
$table->timestamp('ended_at')->nullable();
|
||||||
|
$table->enum('mode', ['normal', 'test'])->default('normal');
|
||||||
|
$table->string('close_reason')->nullable();
|
||||||
|
$table->string('api_token', 64)->unique()->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('room_sessions');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('ordered_songs', function (Blueprint $table) {
|
||||||
|
$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');
|
||||||
|
$table->string('song_name')->nullable();
|
||||||
|
$table->string('artist_name')->nullable();
|
||||||
|
|
||||||
|
|
||||||
|
// 狀態:未播 / 播放中 / 已播 / 刪除
|
||||||
|
$table->enum('status', ['NotPlayed', 'Playing', 'Played', 'NoFile', 'Skipped' , 'InsertPlayback'])->default('NotPlayed');
|
||||||
|
|
||||||
|
// 播放流程
|
||||||
|
$table->timestamp('ordered_at')->useCurrent(); // 點歌時間
|
||||||
|
$table->timestamp('started_at')->nullable(); // 開始播放時間
|
||||||
|
$table->timestamp('finished_at')->nullable(); // 播放結束時間
|
||||||
|
|
||||||
|
$table->unique(['room_session_id', 'order_number']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ordered_songs');
|
||||||
|
}
|
||||||
|
};
|
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'Unset' => '未定義',
|
'Unset' => '未定義',
|
||||||
|
'NotPlayed' =>'未播放',
|
||||||
|
'Playing' =>'播放中',
|
||||||
|
'Played' =>'播畢',
|
||||||
|
'NoFile' =>'無文件',
|
||||||
|
'Skipped' =>'刪除',
|
||||||
|
'InsertPlayback' =>'插播',
|
||||||
'user.gender.Male' =>'男',
|
'user.gender.Male' =>'男',
|
||||||
'user.gender.Female' =>'女',
|
'user.gender.Female' =>'女',
|
||||||
'user.gender.Other' =>'其他',
|
'user.gender.Other' =>'其他',
|
||||||
|
@ -4,10 +4,17 @@ return [
|
|||||||
'list' => '包廂狀態紀錄',
|
'list' => '包廂狀態紀錄',
|
||||||
|
|
||||||
'id' => '編號',
|
'id' => '編號',
|
||||||
|
'branch' => '分店',
|
||||||
'room' => '包廂',
|
'room' => '包廂',
|
||||||
|
'room_type' => '包廂類別',
|
||||||
|
'room_name' => '包廂名稱',
|
||||||
'user' => '操成者',
|
'user' => '操成者',
|
||||||
|
'is_online' => '在線?',
|
||||||
'status' => '狀態',
|
'status' => '狀態',
|
||||||
|
'started_at' => '開始於',
|
||||||
|
'ended_at' => '結束於',
|
||||||
'message' => '紀錄',
|
'message' => '紀錄',
|
||||||
|
'source' => '來源',
|
||||||
'created_at' => '建立於'
|
'created_at' => '建立於'
|
||||||
|
|
||||||
];
|
];
|
@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use App\Http\Controllers\ArtistController;
|
use App\Http\Controllers\ArtistController;
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\RoomControlController;
|
use App\Http\Controllers\RoomControlController;
|
||||||
|
use App\Http\Controllers\Api\RoomSongController;
|
||||||
use App\Http\Controllers\SqliteUploadController;
|
use App\Http\Controllers\SqliteUploadController;
|
||||||
|
|
||||||
Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegister']);
|
Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegister']);
|
||||||
@ -11,6 +12,14 @@ Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegi
|
|||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::get('/profile', [AuthController::class, 'profile']);
|
Route::get('/profile', [AuthController::class, 'profile']);
|
||||||
Route::post('/room/sendSwitch', [RoomControlController::class, 'sendSwitch']);
|
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']);
|
Route::post('/upload-sqlite', [SqliteUploadController::class, 'upload']);
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user