From ae5ed4aa1f5f45fb7444246ece34a471816e42ab Mon Sep 17 00:00:00 2001 From: "allen.yan" Date: Mon, 19 May 2025 16:08:35 +0800 Subject: [PATCH] =?UTF-8?q?Swagger=20=E9=83=BD=E6=94=B9=E5=88=B0ApiRespons?= =?UTF-8?q?e=20=E8=BC=B8=E5=87=BA=20Swagger=20Room=E5=8A=A0=E5=85=A5TcpSoc?= =?UTF-8?q?ketClient=20Swagger=20room=20=E5=8A=A0=E5=85=A5=20=E8=A8=AD?= =?UTF-8?q?=E5=82=99=E8=A8=BB=E5=86=8A=EF=BC=8C=E8=A8=AD=E5=82=99=E9=96=8B?= =?UTF-8?q?=E9=97=9C=20Swagger=20room=20=E6=9C=89=E7=95=B0=E5=8B=95?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=AF=AB=E8=A8=98=E9=8C=84=2020250519?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 10244 bytes app/Enums/RoomStatus.php | 10 + app/Http/Controllers/AuthController.php | 82 ++--- app/Http/Controllers/Controller.php | 14 - .../Controllers/RoomControlController.php | 157 ++++++++++ app/Http/Requests/ApiRequest.php | 31 ++ .../Requests/ReceiveRoomRegisterRequest.php | 31 ++ .../Requests/SendRoomSwitchCommandRequest.php | 35 +++ .../Traits/FailedValidationJsonResponse.php | 20 ++ app/Models/Room.php | 18 ++ app/Models/RoomStatusLog.php | 5 +- app/Observers/RoomObserver.php | 6 +- app/Providers/AppServiceProvider.php | 5 +- app/Services/TcpSocketClient.php | 43 +++ .../2025_05_06_055307_create_rooms_table.php | 2 +- ...6_055312_create_room_status_logs_table.php | 2 +- resources/lang/zh-tw/enums.php | 1 + routes/api.php | 3 + storage/api-docs/api-docs.json | 289 +++++++++++++++++- tests/Feature/TcpSocketClientTest.php | 43 +++ 開發手冊.ini | 5 +- 21 files changed, 737 insertions(+), 65 deletions(-) create mode 100644 app/Http/Controllers/RoomControlController.php create mode 100644 app/Http/Requests/ApiRequest.php create mode 100644 app/Http/Requests/ReceiveRoomRegisterRequest.php create mode 100644 app/Http/Requests/SendRoomSwitchCommandRequest.php create mode 100644 app/Http/Requests/Traits/FailedValidationJsonResponse.php create mode 100644 app/Services/TcpSocketClient.php create mode 100644 tests/Feature/TcpSocketClientTest.php diff --git a/.DS_Store b/.DS_Store index 92ed48d27371b7970cea20c067ba4968c675b67d..cf959a303d8d9bfd368abc9bbca0f42b628cc68c 100644 GIT binary patch delta 974 zcmbu7TWFL=6vxkha{E4qG_#wn*-UoV*r@Rmk_}DNNUJ8PrY6R$8a0Vk+~oVDA11qT zH;JYdVL{FVwmDtV$ZF>&TeY~pl%{$9ivr)&>9Z@UYQC*o+PGpYPq^Iq9?sR1 z!ipgsdkdv(>+f{7_B(xpqQo$gFZ=bYF58?a+>CmIn= z$y=9ufrrbsu4qq>9qEgw?-nkSQvD&lvR(%r@gKu6yJcI%j-~52B+~EYRheJJ_IOouk#DGEagdVBlR7@`AcOoAq&jM(jnqqnw2uzZF}g^X z=rY}*Kj|;}oBpG5dP%Qog5Dq#xyXZn3lgQ6h1r;cDlA4V>d=G`R$(<-u>qa1U}H0) z=*Q4DY{wvWV=wmOSNw*9IF1uIiF3G)8@P#ExQmB)gfTqB|9F9MOt4Ir#j;r*6U@!> z*$h_7f~Vn_X&Zilgp2!;0#wqfZ@>zz)CbCnq%vWw`rwEpmywdlzsr?`_QnJi9 z&ol&5Vgdig)Xor@De31b5GeReI``=zJ)wW-xu)|D88A?QV$4K2=3^liX&&`hf)!}N zN=+rfWFs~qj4pJeM|0^#3_oKBs&`@tzi2l5G@S&SLpY2hIEqs^jWallQC!4jT){O> Z>oy+XF`nYz(N_V1-*@>(bBf>Qd;m&!+Drfd delta 931 zcma))OH30{6o&7A5!xvi83fZ7dJz?am=-ExTAxu7pGZL<6hxpnV;F_DwDmJHRh%e3L1 z4r?E?$K$TK^D8~hq6)7bw-(^AJ}ytgkUnawENtis_h`|W+x;3eZBlxM$t+niSr%{h zx3sc0-Ir6PUv_x)3pwT1$<}Gbvt1RQ#mlPKZm55mE$2+h&0|b|W-m9{?D+)_&h^fz zwn6`nF0F5e7TY7wG@FfQMbkNJQvxw{LrBrKYN{gNGD}8e)^yG{B_dT3RoqG{p>z&s zjZv*%l>@1VW#x=DZr6f}9F>x(^C}r{>IrnIJ+dMt>v$J1-aNQdjYPFjP+p%>m*g?- z-xBKWRfDl;N?X2y^Y%pDnm{z>3#&o#Ph+X#t3<3d+8v0fzFk2zl2X?RwL{J}^h8p% zqQyc+Q1o6a+Gt69Zn>gl8C~St?Ag45H|dswd>VPETM8YzKR0WTlM1qu)R0aRA#pNH zj*~0oCV4_e$t&`fd?H`T4*(gEU_mDGkdIdRJLp?U40d44phHZ#qH~JCB z01o01hA@l~9K&gx#W|eEWn9Ni+`?@V15BQ3o_=Vpzom#1l+UX4H zq%K-UJ=9AV(nYkE`lz1<=r$UnVPYJLq$QhEHf87@-R*EoEX%Qa&DwRW=AXSO(<$!; z)3p~efOhtc8e*vu^<4OPk diff --git a/app/Enums/RoomStatus.php b/app/Enums/RoomStatus.php index b9cd372..74b27ec 100644 --- a/app/Enums/RoomStatus.php +++ b/app/Enums/RoomStatus.php @@ -4,11 +4,20 @@ namespace App\Enums; use App\Enums\Traits\HasLabels; +/** + * @OA\Schema( + * schema="RoomStatus", + * type="string", + * enum={"active", "closed", "fire", "error", "maintenance"}, + * example="error" + * ) + */ enum RoomStatus: string { use HasLabels; case Active = 'active'; case Closed = 'closed'; + case Fire ='fire'; case Error = 'error'; case Maintenance = 'maintenance'; @@ -18,6 +27,7 @@ enum RoomStatus: string { return match($this) { self::Active => __('enums.room.status.Active'), self::Closed => __('enums.room.status.Closed'), + self::Fire => __('enums.room.status.Fire'), self::Error => __('enums.room.status.Error'), self::Maintenance => __('enums.room.status.Maintenance'), }; diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index ea9faf8..5be3027 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -11,53 +11,53 @@ use OpenApi\Annotations as OA; /** * @OA\Tag( * name="Auth", - * description="用戶註冊、登入、登出與個人資料" + * description="用戶個人資料" * ) */ class AuthController extends Controller { /** - * @OA\Get( - * path="/api/profile", - * summary="Get current user profile", - * tags={"Auth"}, - * security={{"Authorization":{}}}, - * @OA\Response( - * response=200, - * description="User profile", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/User") - * ) - * } - * ) - * ), - * @OA\Response( - * response=401, - * description="Unauthorized", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="code", type="string", example="UNAUTHORIZED"), - * @OA\Property(property="message", type="string", example="Unauthorized"), - * @OA\Property(property="data", type="null") - * ) - * } - * ) - * ), - * @OA\Parameter( - * name="Accept", - * in="header", - * required=true, - * @OA\Schema(type="string", default="application/json") - * ) - * ) - */ + * @OA\Get( + * path="/api/profile", + * summary="Get current user profile", + * tags={"Auth"}, + * security={{"Authorization":{}}}, + * @OA\Response( + * response=200, + * description="User profile", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * @OA\Property(property="data", ref="#/components/schemas/User") + * ) + * } + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * @OA\Property(property="code", type="string", example="UNAUTHORIZED"), + * @OA\Property(property="message", type="string", example="Unauthorized"), + * @OA\Property(property="data", type="null") + * ) + * } + * ) + * ), + * @OA\Parameter( + * name="Accept", + * in="header", + * required=true, + * @OA\Schema(type="string", default="application/json") + * ) + * ) + */ public function profile(Request $request) { - return response()->json($request->user()); + return \App\Http\Responses\ApiResponse::success($request->user()); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 784ab4d..ad6fabf 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -29,18 +29,4 @@ use Illuminate\Routing\Controller as BaseController; class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; - - /** - * @OA\Schema( - * schema="UnauthorizedResponse", - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="code", type="string", example="UNAUTHORIZED"), - * @OA\Property(property="message", type="string", example="Unauthorized"), - * @OA\Property(property="data", type="null", nullable=true) - * ) - * } - * ) - */ } \ No newline at end of file diff --git a/app/Http/Controllers/RoomControlController.php b/app/Http/Controllers/RoomControlController.php new file mode 100644 index 0000000..9387c47 --- /dev/null +++ b/app/Http/Controllers/RoomControlController.php @@ -0,0 +1,157 @@ +validated(); + + $room= Room::where([['branch_id',$validated['branch_id']],['name',$validated['room_name']]])->first(); + if (!$room) { + return \App\Http\Responses\ApiResponse::error("'找不到對應包廂'"); + } + $room->internal_ip = $validated['room_ip']; + $room->port =1000; + $room->touch(); + $room->status=RoomStatus::Closed; + $room->save(); + return \App\Http\Responses\ApiResponse::success($room); + + } + /** + * @OA\Post( + * path="/api/room/sendSwitch", + * summary="送出包廂控制指令", + * description="依據傳入的 room_id 與 command,透過 TCP 傳送對應指令給包廂電腦。", + * operationId="sendRoomSwitchCommand", + * tags={"Room Control"}, + * security={{"Authorization":{}}}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent(ref="#/components/schemas/SendRoomSwitchCommandRequest") + * ), + * @OA\Response( + * response=200, + * description="成功傳送指令並回傳 TCP 回應", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * @OA\Property(property="data", ref="#/components/schemas/Room") + * ) + * } + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * @OA\Property(property="code", type="string", example="UNAUTHORIZED"), + * @OA\Property(property="message", type="string", example="Unauthorized"), + * @OA\Property(property="data", type="null") + * ) + * } + * ) + * ), + * @OA\Parameter( + * name="Accept", + * in="header", + * required=true, + * @OA\Schema(type="string", default="application/json") + * ) + * ) + */ + public function sendSwitch(SendRoomSwitchCommandRequest $request): JsonResponse + { + $validated = $request->validated(); + $room= Room::where([['branch_id',$validated['branch_id']],['name',$validated['room_name']]])->first(); + + $ip = $room->internal_ip; + $port = $room->port; + $data=(substr($room->name, -3) ?? $room->name).",".($validated['command']=='active' ? 'O':'X'); + + //dd($data); + $client = new TcpSocketClient($ip, $port); + try { + $response = $client->send($data); + + $room->status=$validated['command']; + $room->started_at=$validated['started_at']; + $room->ended_at=$validated['ended_at']; + $room->save(); + + return \App\Http\Responses\ApiResponse::success($room); + } catch (\Throwable $e) { + $room->status=RoomStatus::Error; + $room->started_at=null; + $room->ended_at=null; + $room->save(); + return \App\Http\Responses\ApiResponse::error($e->getMessage()); + } + } +} diff --git a/app/Http/Requests/ApiRequest.php b/app/Http/Requests/ApiRequest.php new file mode 100644 index 0000000..a05875b --- /dev/null +++ b/app/Http/Requests/ApiRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'branch_id' => 'required|integer|exists:branches,id', + 'room_name' => 'required|string', + 'room_ip' => 'nullable|ip', + ]; + } +} diff --git a/app/Http/Requests/SendRoomSwitchCommandRequest.php b/app/Http/Requests/SendRoomSwitchCommandRequest.php new file mode 100644 index 0000000..e002345 --- /dev/null +++ b/app/Http/Requests/SendRoomSwitchCommandRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'branch_id' => 'required|integer', + 'room_name' => 'required|string', + 'command' => 'required|string', + 'started_at' => 'nullable|date_format:Y-m-d H:i:s', + 'ended_at' => 'nullable|date_format:Y-m-d H:i:s', + ]; + } +} diff --git a/app/Http/Requests/Traits/FailedValidationJsonResponse.php b/app/Http/Requests/Traits/FailedValidationJsonResponse.php new file mode 100644 index 0000000..bd6c0ac --- /dev/null +++ b/app/Http/Requests/Traits/FailedValidationJsonResponse.php @@ -0,0 +1,20 @@ +json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + 'code' => 'ERROR', + 'token' => '' + ], Response::HTTP_UNPROCESSABLE_ENTITY)); + } +} \ No newline at end of file diff --git a/app/Models/Room.php b/app/Models/Room.php index 42ca611..2a07725 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -6,6 +6,19 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Traits\LogsModelActivity; +/** + * @OA\Schema( + * schema="Room", + * type="object", + * @OA\Property(property="id", type="integer", example=16), + * @OA\Property(property="name", type="string", example="pc102"), + * @OA\Property(property="internal_ip", type="string", example="192.168.11.7"), + * @OA\Property(property="port", type="int", example="9000"), + * @OA\Property(property="status", ref="#/components/schemas/RoomStatus"), + * @OA\Property(property="started_at", type="string", format="date-time", example="2025-05-11T16:00:00.000000Z"), + * @OA\Property(property="ended_at", type="string", format="date-time", example=null), + * ) + */ class Room extends Model { /** @use HasFactory<\Database\Factories\ArtistFactory> */ @@ -20,6 +33,11 @@ class Room extends Model 'ended_at', ]; + protected $hidden = [ + 'internal_ip', + 'port', + ]; + protected $casts = [ 'name' => 'string', 'internal_ip' =>'string', diff --git a/app/Models/RoomStatusLog.php b/app/Models/RoomStatusLog.php index d36c84a..7080beb 100644 --- a/app/Models/RoomStatusLog.php +++ b/app/Models/RoomStatusLog.php @@ -10,7 +10,7 @@ class RoomStatusLog extends Model /** @use HasFactory<\Database\Factories\ArtistFactory> */ use HasFactory; - public $timestamps = false; + public $timestamps = true; protected $fillable = [ @@ -18,11 +18,10 @@ class RoomStatusLog extends Model 'user_id', 'status', 'message', - 'logged_at' ]; protected $casts = [ - 'logged_at' => 'datetime', + 'status' => \App\Enums\RoomStatus::class, ]; public function room() { diff --git a/app/Observers/RoomObserver.php b/app/Observers/RoomObserver.php index 01a9775..d8a3649 100644 --- a/app/Observers/RoomObserver.php +++ b/app/Observers/RoomObserver.php @@ -2,7 +2,10 @@ namespace App\Observers; +use Illuminate\Support\Facades\Auth; + use App\Models\Room; +use App\Models\RoomStatusLog; class RoomObserver { @@ -25,8 +28,7 @@ class RoomObserver 'room_id' => $room->id, 'user_id' => Auth::id(), // 若是 console 或系統自動操作可能為 null 'status' => $room->status, - 'message' => '狀態自動變更紀錄', - 'logged_at' => now(), + 'message' => 'started_at:'.$room->started_at.',ended_at:'.$room->ended_at, ]); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..4828850 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,9 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use App\Models\Room; +use App\Observers\RoomObserver; + class AppServiceProvider extends ServiceProvider { /** @@ -19,6 +22,6 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Room::observe(RoomObserver::class); } } diff --git a/app/Services/TcpSocketClient.php b/app/Services/TcpSocketClient.php new file mode 100644 index 0000000..45530b5 --- /dev/null +++ b/app/Services/TcpSocketClient.php @@ -0,0 +1,43 @@ +ip = $ip; + $this->port = $port; + $this->timeout = $timeout; + } + + public function send(string $data): string + { + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket === false) { + throw new \Exception("Socket create failed: " . socket_strerror(socket_last_error())); + } + + $result = socket_connect($socket, $this->ip, $this->port); + if ($result === false) { + throw new \Exception("Socket connect failed: " . socket_strerror(socket_last_error($socket))); + } + + socket_write($socket, $data, strlen($data)); + + $response = ''; + while ($out = socket_read($socket, 2048)) { + $response .= $out; + // 根據協議判斷是否結束接收,可以自行調整 + if (strpos($response, "\n") !== false) { + break; + } + } + + socket_close($socket); + return $response; + } +} \ No newline at end of file diff --git a/database/migrations/2025_05_06_055307_create_rooms_table.php b/database/migrations/2025_05_06_055307_create_rooms_table.php index 5cbc491..81466a9 100644 --- a/database/migrations/2025_05_06_055307_create_rooms_table.php +++ b/database/migrations/2025_05_06_055307_create_rooms_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->string('name')->comment('包廂名稱'); $table->string('internal_ip')->nullable()->comment('內部 IP'); $table->unsignedSmallInteger('port')->nullable()->comment('通訊 Port'); - $table->enum('status', ['active', 'closed', 'error', 'maintenance'])->default('error')->comment('狀態'); // :啟用中 / 已結束 + $table->enum('status', ['active', 'closed','fire', 'error', 'maintenance'])->default('error')->comment('狀態'); // :啟用中 / 已結束 $table->dateTime('started_at')->nullable()->comment('開始時間'); // $table->dateTime('ended_at')->nullable()->comment('結束時間'); // $table->timestamps(); diff --git a/database/migrations/2025_05_06_055312_create_room_status_logs_table.php b/database/migrations/2025_05_06_055312_create_room_status_logs_table.php index 8c27e98..33c9743 100644 --- a/database/migrations/2025_05_06_055312_create_room_status_logs_table.php +++ b/database/migrations/2025_05_06_055312_create_room_status_logs_table.php @@ -15,7 +15,7 @@ return new class extends Migration $table->id(); $table->foreignId('room_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // 操作者,可為 null(系統) - $table->enum('status', ['active', 'closed', 'error', 'maintenance']); + $table->enum('status', ['active', 'closed','fire', 'error', 'maintenance']); $table->text('message')->nullable(); // 可填異常原因或操作說明 $table->timestamps(); }); diff --git a/resources/lang/zh-tw/enums.php b/resources/lang/zh-tw/enums.php index 46d6eab..a8f1c8b 100644 --- a/resources/lang/zh-tw/enums.php +++ b/resources/lang/zh-tw/enums.php @@ -26,6 +26,7 @@ return [ 'room.status.Active' => '已占用', 'room.status.Closed' => '可用', + 'room.status.Fire' => '火災', 'room.status.Error' => '異常', 'room.status.Maintenance' => '維修', ]; \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 081634a..4e5ad37 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,10 +3,13 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\ArtistController; use App\Http\Controllers\AuthController; +use App\Http\Controllers\RoomControlController; Route::get('/artists/search', [App\Http\Controllers\ArtistController::class, 'search'])->name('api.artists.search'); Route::middleware('auth:sanctum')->group(function () { Route::get('/profile', [AuthController::class, 'profile']); + Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegister']); + Route::post('/room/sendSwitch', [RoomControlController::class, 'sendSwitch']); }); \ No newline at end of file diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index 1643ba9..e43a1dd 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -127,10 +127,201 @@ } ] } + }, + "/api/room/receiveRegister": { + "post": { + "tags": [ + "Room Control" + ], + "summary": "包廂註冊控制指令", + "description": "依據傳入的 branch_id 與 room_name,知道過來的設備來之於那個IP設備。", + "operationId": "registerRoomCommand", + "parameters": [ + { + "name": "Accept", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "application/json" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReceiveRoomRegisterRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功傳送指令並回傳 TCP 回應", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/Room" + } + }, + "type": "object" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "code": { + "type": "string", + "example": "UNAUTHORIZED" + }, + "message": { + "type": "string", + "example": "Unauthorized" + }, + "data": { + "type": "null" + } + }, + "type": "object" + } + ] + } + } + } + } + }, + "security": [ + { + "Authorization": [] + } + ] + } + }, + "/api/room/sendSwitch": { + "post": { + "tags": [ + "Room Control" + ], + "summary": "送出包廂控制指令", + "description": "依據傳入的 room_id 與 command,透過 TCP 傳送對應指令給包廂電腦。", + "operationId": "sendRoomSwitchCommand", + "parameters": [ + { + "name": "Accept", + "in": "header", + "required": true, + "schema": { + "type": "string", + "default": "application/json" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendRoomSwitchCommandRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功傳送指令並回傳 TCP 回應", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/Room" + } + }, + "type": "object" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "code": { + "type": "string", + "example": "UNAUTHORIZED" + }, + "message": { + "type": "string", + "example": "Unauthorized" + }, + "data": { + "type": "null" + } + }, + "type": "object" + } + ] + } + } + } + } + }, + "security": [ + { + "Authorization": [] + } + ] + } } }, "components": { "schemas": { + "RoomStatus": { + "type": "string", + "enum": [ + "active", + "closed", + "fire", + "error", + "maintenance" + ], + "example": "error" + }, "UserGender": { "type": "string", "enum": [ @@ -151,6 +342,64 @@ ], "example": "0" }, + "ReceiveRoomRegisterRequest": { + "required": [ + "branch_id", + "room_id", + "ip" + ], + "properties": { + "branch_id": { + "type": "integer", + "example": "5" + }, + "room_name": { + "type": "string", + "example": "pc102" + }, + "ip": { + "type": "string", + "example": "192.168.x.x" + } + }, + "type": "object" + }, + "SendRoomSwitchCommandRequest": { + "required": [ + "room_id", + "command" + ], + "properties": { + "branch_id": { + "type": "integer", + "example": "5" + }, + "room_name": { + "type": "string", + "example": "pc102" + }, + "command": { + "type": "string", + "enum": [ + "active", + "closed", + "maintenance" + ], + "example": "active" + }, + "started_at": { + "type": "string", + "example": "2025-05-19 09:31:00", + "nullable": true + }, + "ended_at": { + "type": "string", + "example": "2025-05-19 09:31:00", + "nullable": true + } + }, + "type": "object" + }, "ApiResponse": { "properties": { "code": { @@ -168,6 +417,40 @@ }, "type": "object" }, + "Room": { + "properties": { + "id": { + "type": "integer", + "example": 16 + }, + "name": { + "type": "string", + "example": "pc102" + }, + "internal_ip": { + "type": "string", + "example": "192.168.11.7" + }, + "port": { + "type": "integer", + "example": "9000" + }, + "status": { + "$ref": "#/components/schemas/RoomStatus" + }, + "started_at": { + "type": "string", + "format": "date-time", + "example": "2025-05-11T16:00:00.000000Z" + }, + "ended_at": { + "type": "string", + "format": "date-time", + "example": null + } + }, + "type": "object" + }, "User": { "properties": { "id": { @@ -216,7 +499,11 @@ }, { "name": "Auth", - "description": "用戶註冊、登入、登出與個人資料" + "description": "包廂控制" + }, + { + "name": "Room Control", + "description": "Room Control" } ] } \ No newline at end of file diff --git a/tests/Feature/TcpSocketClientTest.php b/tests/Feature/TcpSocketClientTest.php new file mode 100644 index 0000000..bd3be91 --- /dev/null +++ b/tests/Feature/TcpSocketClientTest.php @@ -0,0 +1,43 @@ +fail("Unable to fork process for mock TCP server."); + } elseif ($pid == 0) { + // 子處理序:啟動簡單 TCP server + $socket = stream_socket_server("tcp://$host:$port", $errno, $errstr); + if (!$socket) { + exit(1); + } + $conn = stream_socket_accept($socket); + $data = fread($conn, 1024); // 收資料 + fwrite($conn, "Echo: " . $data); // 回傳資料 + fclose($conn); + fclose($socket); + exit(0); + } else { + // 父處理序:給 server 一點時間啟動 + usleep(100_000); // 0.1 秒 + + $client = new TcpSocketClient($host, $port); + $response = $client->send("hello test\n"); + + $this->assertEquals("Echo: hello test\n", $response); + + pcntl_wait($status); // 等子程序結束 + } + } +} \ No newline at end of file diff --git a/開發手冊.ini b/開發手冊.ini index dcc2f6d..d4bb37c 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -126,4 +126,7 @@ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate composer require "darkaonline/l5-swagger" -php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider" \ No newline at end of file +php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider" +php -d memory_limit=512M artisan l5-swagger:generate + +4|qdXjrZTvWzOE5kNhbmOXFO6d7DrFzGh1RqhVATLZf8475a6b \ No newline at end of file