202509041808

加入RoomApiToken 驗証
加入通知顯示
This commit is contained in:
allen.yan 2025-09-04 18:12:15 +08:00
parent 355dde4481
commit d70da24a75
19 changed files with 249 additions and 79 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Models\RoomSession;
class RoomApiTokenMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// 優先從 query 拿,沒就 session
$roomCode = $request->query('room_code', session('room_code'));
if ($roomCode) {
$roomSession = RoomSession::validToken($roomCode)->first();
if (!$roomSession) {
session()->forget('room_code');
return redirect()->route('welcome')->with('error', '房間不存在或狀態不可用');
}else{
session(['room_code' => $roomCode]);
$request->merge(['roomSession' => $roomSession]); // 可選:直接注入 request
}
}
return $next($request);
}
}

View File

@ -2,22 +2,33 @@
namespace App\Livewire\Pages;
use App\Models\RoomSession;
use Livewire\Component;
class Home extends Component
{
public $roomCode;
{
public array $menus = [];
public function mount()
{
// 先從 URL 取得 room_code再存進 session
//session()->forget('room_code');
$this->roomCode = request()->query('room_code', session('room_code', null));
if ($this->roomCode) {
session(['room_code' => $this->roomCode]);
{
$this->menus = [
['route' => 'new-songs','image' => '手機點歌/首頁-新歌快報.png'],
['route' => 'top-ranking','image' => '手機點歌/首頁-熱門排行.png'],
['route' => 'search-song','image' => '手機點歌/首頁-歌名查詢.png'],
];
$roomMenus = [
['route' => 'clicked-song','image' => '手機點歌/首頁-已點歌曲.png'],
['route' => 'sound-control','image' => '手機點歌/首頁-聲音控制.png',],
['route' => 'love-message','image' => '手機點歌/首頁-真情告白.png',],
];
$roomCode = request()->query('room_code', session('room_code', null));
if ($roomCode) {
$roomSession = RoomSession::validToken($roomCode)->first();
if ($roomSession) {
session(['room_code' => $roomCode]);
$this->menus = array_merge($this->menus, $roomMenus);
}
}
}
public function render()
{
return view('livewire.pages.home');

View File

@ -3,12 +3,21 @@
namespace App\Livewire\Pages;
use Livewire\Component;
use App\Models\RoomSession;
use WireUi\Traits\WireUiActions;
use App\Services\TcpSocketClient;
class LoveMessage extends Component
{
use WireUiActions;
protected $listeners = ['stickerSelected' => 'handleSticker'];
public $roomSession;
public $message = '';
public $selectedSticker=null;
public function mount()
{
$this->roomSession = request()->roomSession;
}
public function handleSticker($sticker)
{

View File

@ -9,7 +9,7 @@ use App\Models\OrderedSong;
class OrderedSongList extends Component
{
public ?string $roomSessionId = null;
public $roomSession;
public EloquentCollection $playing ;
public EloquentCollection $nextSong ;
@ -21,32 +21,22 @@ class OrderedSongList extends Component
$this->playing = new EloquentCollection();
$this->nextSong = new EloquentCollection();
$this->finished = new EloquentCollection();
$roomSession = $this->getRoomSession(session('room_code'))?->id ;
if ($roomSession) {
$this->roomSessionId = $roomSession->id;
$this->loadSongs($this->roomSessionId);
}
}
private function getRoomSession($apiToken): ?RoomSession
{
if (!$apiToken) return null;
return RoomSession::where('api_token', $apiToken)
->whereIn('status', ['active', 'maintain'])
->first();
$this->roomSession = request()->roomSession;
$this->refreshSongs();
}
public function refreshSongs()
{
if ($this->roomSessionId) {
$this->loadSongs($this->roomSessionId);
$dbSession = $this->roomSession->refreshValid();
if (!$dbSession) {
session()->forget('room_code');
return $this->redirect(route('welcome'), navigate: true);
}
$this->playing = OrderedSong::forSession($this->roomSession->id)->playing()->get();
$this->nextSong = OrderedSong::forSession($this->roomSession->id)->nextSong()->get();
$this->finished = OrderedSong::forSession($this->roomSession->id)->finished()->get();
}
protected function loadSongs(string $roomSessionId)
{
$this->playing = OrderedSong::forSession($roomSessionId)->playing()->get();
$this->nextSong = OrderedSong::forSession($roomSessionId)->nextSong()->get();
$this->finished = OrderedSong::forSession($roomSessionId)->finished()->get();
}
public function render()
{
return view('livewire.pages.ordered-song-list', [

View File

@ -3,11 +3,15 @@
namespace App\Livewire\Pages;
use Livewire\Component;
use App\Models\RoomSession;
use App\Models\Artist;
use App\Models\SongLibraryCache;
use WireUi\Traits\WireUiActions;
class SearchSong extends Component
{
use WireUiActions;
public $roomSession;
public string $search = '';
public $searchCategory = '';
public $selectedLanguage = '國語'; // 預設語言
@ -21,6 +25,13 @@ class SearchSong extends Component
public function mount(string $searchCategory = '')
{
$this->searchCategory = $searchCategory;
$roomCode = request()->query('room_code', session('room_code', null));
if ($roomCode) {
$this->roomSession = RoomSession::validToken($roomCode)->first();
if ($this->roomSession) {
session(['room_code' => $roomCode]);
}
}
}
/**
@ -68,8 +79,10 @@ class SearchSong extends Component
// TODO: 加入已點歌曲邏輯,例如:
// auth()->user()->room->addSong($songId);
$this->dispatchBrowserEvent('notify', [
'message' => '已加入已點歌曲'
$this->notification()->send([
'icon' => 'success',
'title' => $songId,
'description' => '已加入已點歌曲',
]);
}

View File

@ -2,37 +2,76 @@
namespace App\Livewire\Pages;
use App\Models\RoomSession;
use Livewire\Component;
use App\Services\ApiClient;
use WireUi\Traits\WireUiActions;
use App\Services\TcpSocketClient;
class SoundControl extends Component
{
use WireUiActions;
public $roomSession;
public $buttons = [
['img'=>'音控-01.jpg', 'action'=>'pause'],
['img'=>'音控-02.jpg', 'action'=>'volume_up'],
['img'=>'音控-04.jpg', 'action'=>'mic_up'],
['img'=>'音控-06.jpg', 'action'=>'mute'],
['img'=>'音控-03.jpg', 'action'=>'volume_down'],
['img'=>'音控-05.jpg', 'action'=>'mic_down'],
['img'=>'音控-07.jpg', 'action'=>'original_song'],
['img'=>'音控-08.jpg', 'action'=>'service'],
['img'=>'音控-09.jpg', 'action'=>'replay'],
['img'=>'音控-11.jpg', 'action'=>'male_key'],
['img'=>'音控-12.jpg', 'action'=>'female_key'],
['img'=>'音控-10.jpg', 'action'=>'cut'],
['img'=>'音控-15.jpg', 'action'=>'lower_key'],
['img'=>'音控-14.jpg', 'action'=>'standard_key'],
['img'=>'音控-13.jpg', 'action'=>'raise_key'],
'pause' =>['img'=>'音控-01.jpg','label' =>'暫停'],
'volume_up' =>['img'=>'音控-02.jpg','label' =>'音量 ↑'],
'volume_down' =>['img'=>'音控-04.jpg','label' =>'音量 ↓'],
'mic_up' =>['img'=>'音控-06.jpg','label' =>'麥克風 ↑'],
'mic_down' =>['img'=>'音控-03.jpg','label' =>'麥克風 ↓'],
'mute' =>['img'=>'音控-05.jpg','label' =>'靜音'],
'original_song' =>['img'=>'音控-07.jpg','label' =>'原唱'],
'service' =>['img'=>'音控-08.jpg','label' =>'服務鈴'],
'replay' =>['img'=>'音控-09.jpg','label' =>'重播'],
'male_key' =>['img'=>'音控-11.jpg','label' =>'男調'],
'female_key' =>['img'=>'音控-12.jpg','label' =>'女調'],
'cut' =>['img'=>'音控-10.jpg','label' =>'切歌'],
'lower_key' =>['img'=>'音控-15.jpg','label' =>'降調'],
'standard_key' =>['img'=>'音控-14.jpg','label' =>'原調'],
'raise_key' =>['img'=>'音控-13.jpg','label' =>'升調'],
];
public function sendVolumeControl(string $action)
public function mount()
{
// 這裡可以加你的 API 或邏輯
// 範例:發送到後台控制音量
info("Sound control action: ".$action);
$this->dispatchBrowserEvent('notify', [
'message' => "已執行操作: {$action}"
]);
$this->roomSession = request()->roomSession;
}
public function sendControl(string $action)
{
$dbSession = $this->roomSession->refreshValid();
if (!$dbSession) {
session()->forget('room_code');
$this->notification()->send([
'icon' => 'error',
'title' => '驗證失敗',
'description' => 'Token 已失效,請重新登入',
]);
return $this->redirect(route('welcome'), navigate: true);
}else{
$room=$this->roomSession->room;
}
$client = new TcpSocketClient($room->internal_ip, $room->port);
try {
$response = $client->send($room->name . "," . $action);
} catch (\Throwable $e) {
logger()->error('❌ TCP 傳送失敗: ' . $e->getMessage(), [
'room_id' => $room->id,
'ip' => $room->internal_ip,
'port' => $room->port,
]);
}
$title = $this->buttons[$action]['label'] ?? $action;
$this->notification()->send(
(isset($response) && trim($response) === "OK")
?[
'icon' => 'success',
'title' => $title,
'description' => '已執行操作',
]:[
'icon' => 'error',
'title' => $title,
'description' => $data['message'] ?? '操作失敗',
]
);
}
public function render()

View File

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Enums\OrderedSongStatus;
/**
* @OA\Schema(
@ -85,4 +86,33 @@ class OrderedSong extends Model
}
]);
}
public function scopeForSession($query, $roomSessionId)
{
return $query->where('room_session_id', $roomSessionId);
}
public function scopePlaying($query)
{
return $query->where('status', OrderedSongStatus::Playing);
}
public function scopeNextSong($query)
{
return $query->whereIn('status', [OrderedSongStatus::InsertPlayback, OrderedSongStatus::NotPlayed])
->orderByRaw("FIELD(status, ?, ?)", [
OrderedSongStatus::InsertPlayback->value,
OrderedSongStatus::NotPlayed->value,
])
->orderByRaw("CASE WHEN status=? THEN ordered_at END DESC", [OrderedSongStatus::InsertPlayback->value])
->orderByRaw("CASE WHEN status=? THEN ordered_at END ASC", [OrderedSongStatus::NotPlayed->value]);
}
public function scopeFinished($query)
{
return $query->whereIn('status', [
OrderedSongStatus::Played,
OrderedSongStatus::Skipped,
OrderedSongStatus::NoFile,
])->orderByDesc('finished_at');
}
}

View File

@ -35,8 +35,22 @@ class RoomSession extends Model
public const MODE_VIP = 'vip';
public const MODE_TEST = 'test';
public function scopeValidToken($query, ?string $token)
{
return $query->with('room')
->where('api_token', $token)
->whereIn('status', ['active', 'maintain']);
}
public function refreshValid(): ?self
{
return self::validToken($this->api_token)
->where('id', $this->id)
->first();
}
public function room()
{
return $this->belongsTo(Room::class);
}
}

View File

@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'api_token' => \App\Http\Middleware\ApiTokenMiddleware::class,
'room_api_token' => \App\Http\Middleware\RoomApiTokenMiddleware::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class

View File

@ -5,6 +5,6 @@
<img src="{{ asset('手機點歌/BANNER-12.png') }}" alt="超級巨星 Banner">
</div>
</x-slot>
<x-notifications/>
<livewire:pages.love-message />
</x-app-layout>

View File

@ -5,6 +5,7 @@
<img src="{{ asset('手機點歌/BANNER-01.png') }}" alt="新歌快報">
</div>
</x-slot>
<x-notifications/>
<livewire:pages.search-song :searchCategory="'New'" />
<div class="image-container">
<img src="{{ asset('手機點歌/LOGO_800x400px.png') }}" alt="Wolf Fox Logo">

View File

@ -5,5 +5,6 @@
<img src="{{ asset('手機點歌/BANNER-06.png') }}" alt="歌名查詢">
</div>
</x-slot>
<x-notifications/>
<livewire:pages.search-song />
</x-app-layout>

View File

@ -5,6 +5,6 @@
<img src="{{ asset('手機點歌/BANNER-09.png') }}" alt="超級巨星 Banner">
</div>
</x-slot>
<x-notifications/>
<livewire:pages.sound-control />
</x-app-layout>

View File

@ -5,6 +5,7 @@
<img src="{{ asset('手機點歌/BANNER-02.png') }}" alt="熱門排行">
</div>
</x-slot>
<x-notifications/>
<livewire:pages.search-song :searchCategory="'Hot'" />
<div class="image-container">
<img src="{{ asset('手機點歌/LOGO_800x400px.png') }}" alt="Wolf Fox Logo">

View File

@ -1,15 +1,7 @@
<div class="py-12 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<x-button.flat-card image="{{ asset('手機點歌/首頁-新歌快報.png') }}" href="{{ route('new-songs') }}" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-熱門排行.png') }}" href="{{ route('top-ranking') }}" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-歌名查詢.png') }}" href="{{ route('search-song') }}" />
@if($roomCode)
<x-button.flat-card image="{{ asset('手機點歌/首頁-已點歌曲.png') }}" onclick="orderSongAndNavigate()" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-聲音控制.png') }}" href="{{ route('sound-control') }}" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-社群媒體.png') }}" href="{{ route('social-media') }}" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-真情告白.png') }}" href="{{ route('love-message') }}" />
<x-button.flat-card image="{{ asset('手機點歌/首頁-心情貼圖.png') }}" href="{{ route('mood-stickers') }}" />
@endif
@foreach($menus as $menu)
<x-button.flat-card image="{{ asset($menu['image']) }}" href="{{ route($menu['route']) }}" />
@endforeach
</div>
</div>

View File

@ -62,12 +62,14 @@
<tr>
<th class="px-4 py-2 w-16 text-center">編號</th>
<th class="px-4 py-2">歌曲</th>
<th class="px-4 py-2 w-24 text-center">操作</th>
@if($roomSession)
<th class="px-4 py-2 w-24 text-center">操作</th>
@endif
</tr>
</x-slot>
@forelse($songs as $song)
<tr wire:click="orderSong({{ $song->id }})" class="hover:bg-gray-50 cursor-pointer">
<tr @if($roomSession) wire:click="orderSong({{ $song->song_id }})" @endif class="hover:bg-gray-50 cursor-pointer">
<td class="px-4 py-2 text-center">{{ $song->song_id }}</td>
<td class="px-4 py-2">
<div class="flex flex-col">
@ -75,7 +77,9 @@
<span class="text-xs text-gray-500 self-end">{{ $song->str_artists_plus() }}</span>
</div>
</td>
<td class="px-4 py-2 text-blue-500 text-center">點歌</td>
@if($roomSession)
<td class="px-4 py-2 text-blue-500 text-center">點歌</td>
@endif
</tr>
@empty
<tr>

View File

@ -1,9 +1,10 @@
<div class="py-12 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-3 gap-6">
@foreach($buttons as $btn)
@foreach($buttons as $key => $btn)
<x-button.flat-card
image="{{ asset('手機點歌/'.$btn['img']) }}"
wire:click="sendVolumeControl('{{ $btn['action'] }}')"
wire:click="sendControl('{{ $key }}')"
label="{{ $btn['label'] ?? '' }}"
/>
@endforeach
</div>

View File

@ -11,9 +11,11 @@ Route::view('/welcome', 'livewire.app.welcome')->name('welcome');
Route::view('/new-songs', 'livewire.app.new-songs')->name('new-songs');
Route::view('/top-ranking', 'livewire.app.top-ranking')->name('top-ranking');
Route::view('/search-song', 'livewire.app.search-song')->name('search-song');
Route::view('/clicked-song', 'livewire.app.clicked-song')->name('clicked-song');
Route::view('/sound-control', 'livewire.app.sound-control')->name('sound-control');
Route::view('/love-message', 'livewire.app.love-message')->name('love-message');
Route::middleware('room_api_token')->group(function () {
Route::view('/clicked-song', 'livewire.app.clicked-song')->name('clicked-song');
Route::view('/sound-control', 'livewire.app.sound-control')->name('sound-control');
Route::view('/love-message', 'livewire.app.love-message')->name('love-message');
});
Route::middleware(['auth'])->group(function () {
// Profile

View File

@ -89,4 +89,32 @@ php artisan migrate:rollback
php artisan migrate
php artisan transfer:sqlite sqlite/tempUser.sqlite --sync
php artisan queue:work --daemon --timeout=3600 --tries=1 --queue=default
php artisan queue:work --daemon --timeout=3600 --tries=1 --queue=default
npm install && npm run build
https://ktvremote.test/?room_code=abc123
http://192.168.12.14:9090/wnknwl1f3yy/windows.html
php artisan storage:link
# 執行全部測試
php artisan test
# 執行特定檔案
php artisan test tests/Feature/InternalHtmlTest.php
# 執行特定測試方法
php artisan test --filter test_internal_html_page
# 執行全部測試
vendor/bin/phpunit
# 執行特定檔案
vendor/bin/phpunit tests/Feature/InternalHtmlTest.php
# 執行特定測試方法
vendor/bin/phpunit --filter test_internal_html_page
02 2171 1168 89843