only('email', 'password'); if (!Auth::attempt($credentials)) { return ApiResponse::unauthorized(); } // 2. 取得登入使用者 $user = Auth::user(); // 3. 產生或取得 Token if (empty($user->api_plain_token)) { $token = $user->createToken('pc-heartbeat')->plainTextToken; $user->api_plain_token = $token; $user->save(); } else { $token = $user->api_plain_token; } $branch = Branch::first(); $otherSet = OtherSet::pluck('value', 'name')->toArray(); return ApiResponse::success([ 'token' => $token, 'branch_name' => $branch->name, 'other_set' => $otherSet ]); } /** * @OA\Post( * path="/api/room/heartbeat", * summary="包廂心跳封包指令", * description="記錄設備連線狀況", * operationId="heartbeatRoomCommand", * tags={"Room Control"}, * security={{"Authorization":{}}}, * @OA\RequestBody( * required=true, * @OA\JsonContent(ref="#/components/schemas/ReceiveRoomStatusDataRequest") * ), * @OA\Response( * response=200, * description="成功傳送指令並回傳 TCP 回應", * @OA\JsonContent( * allOf={ * @OA\Schema(ref="#/components/schemas/ApiResponse"), * @OA\Schema( * @OA\Property(property="data", ref="#/components/schemas/MachineStatus") * ) * } * ) * ), * @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 StatusReport(ReceiveRoomStatusDataRequest $request) { $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, ]); if ($room->exists && $room->internal_ip !== $validated['ip']) { $validated['status'] = 'error'; } else { $validated['status'] = 'online'; } $room->internal_ip = $validated['ip']; $room->port = 1000; $room->is_online=1; $room->touch(); // 更新 updated_at $room->save(); $response = ( new MachineStatusForwarder( $branch->external_ip ?? '', '/api/room/receiveSwitch', (new RoomResource($room->refresh()))->toArray(request()) ) )->forward(); return ApiResponse::success([ 'data' => MachineStatus::create($validated), ]); } /** * @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(); $branch = Branch::where('name',$validated['branch_name'])->first(); $roomType = null; $roomName = null; // 從 room_name(例如 PC101, SVR01)中擷取 type 與 name if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['room_name'], $matches)) { $roomType = strtolower($matches[1]); // 'PC' → 'pc' $roomName = $matches[2]; // '101' } $room = Room::where('branch_id', $branch->id) ->where('name', $roomName) ->where('type', $roomType) ->first(); if (!$room) { return ApiResponse::error('房間不存在'); } // 檢查必要欄位是否缺失或狀態為錯誤 if (empty($room->internal_ip) || empty($room->port)) { return ApiResponse::error('房間未設定 IP 或 Port'); } $room->status=$validated['command']; $room->log_source='api'; $room->log_message='sendSwitch'; $room->started_at=$validated['started_at']; $room->ended_at=$validated['ended_at']; $room->save(); $suffix = substr($room->name, -3) ?: $room->name; $signal = match ($validated['command']) { 'maintain' => 'O', 'active' => 'O', 'closed' => 'X', 'fire' => 'F', default => 'X', // fallback 保險起見 }; $data = $suffix . "," . $signal; $client = new TcpSocketClient($room->internal_ip, $room->port); try { $response = $client->send($data); } catch (\Throwable $e) { logger()->error('❌ TCP 傳送失敗: ' . $e->getMessage(), [ 'room_id' => $room->id, 'ip' => $room->internal_ip, 'port' => $room->port, ]); } $response = (new MachineStatusForwarder( $branch->external_ip, "/api/room/receiveSwitch", (new RoomResource($room->refresh()))->toArray(request()) ))->forward(); return $validated['command']==='error' ? ApiResponse::error('機房控制失敗') : ApiResponse::success($room); } }