diff --git a/app/Http/Controllers/RoomControlController.php b/app/Http/Controllers/RoomControlController.php index fa8f6c6..d721664 100644 --- a/app/Http/Controllers/RoomControlController.php +++ b/app/Http/Controllers/RoomControlController.php @@ -200,26 +200,8 @@ class RoomControlController extends Controller $room->touch(); // 更新 updated_at $room->save(); } - $externalUrl = $branch->external_ip; - $parsed = parse_url($externalUrl); - $hostParts = explode('.', $parsed['host']); - - if (count($hostParts) >= 3) { - $mainDomain = implode('.', array_slice($hostParts, 1)); - } else { - $mainDomain = $parsed['host']; - } - - $mainDomainUrl = $parsed['scheme'] . '://' . $mainDomain; - try { - $user = \App\Models\User::find(2); // 你可改為 config 或動態決定 - if ($user && $user->api_plain_token) { - $client = new ApiClient($mainDomainUrl,$user->api_plain_token); - $client->post('/api/room/heartbeat', $validated); - } - } catch (\Throwable $e) { - logger()->error('❌ Failed to forward machine status: ' . $e->getMessage()); - } + + $this->receiveRequest($branch->external_ip,"'/api/room/heartbeat'", $validated); return ApiResponse::success([ @@ -307,19 +289,50 @@ class RoomControlController extends Controller $client = new TcpSocketClient($room->internal_ip, $room->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 ApiResponse::success($room); } catch (\Throwable $e) { - $room->status=RoomStatus::Error; - $room->started_at=null; - $room->ended_at=null; - $room->save(); - return ApiResponse::error($e->getMessage()); + logger()->error('❌ TCP 傳送失敗: ' . $e->getMessage(), [ + 'room_id' => $room->id, + 'ip' => $room->internal_ip, + 'port' => $room->port, + ]); + $validated['command']="error"; + $validated['started_at']=null; + $validated['ended_at']=null; } + + $room->status=$validated['command']; + $room->started_at=$validated['started_at']; + $room->ended_at=$validated['ended_at']; + $room->save(); + $this->receiveRequest($branch->external_ip,"/api/room/receiveSwitch",$validated); + return $validated['command']==='error' ? ApiResponse::error('機房控制失敗') : ApiResponse::success($room); + } + + function receiveRequest(string $externalUrl,string $endpoint, array $validated){ + $response=null; + $parsed = parse_url($externalUrl); + $hostParts = explode('.', $parsed['host']); + + if (count($hostParts) >= 3) { + $mainDomain = implode('.', array_slice($hostParts, 1)); + } else { + $mainDomain = $parsed['host']; + } + + $mainDomainUrl= $parsed['scheme'] . '://' . $mainDomain; + if ($user && $user->api_plain_token) { + $client = (new ApiClient($mainDomainUrl, $user->api_plain_token)); + $response = $client->post($endpoint, $validated); + + Log::info('✅ Machine status forwarded', [ + 'endpoint' => $endpoint, + 'request' => $validated, + 'status' => $response->status(), + 'body' => $response->json(), + ]); + } else { + Log::warning("🔒 User with ID {$userId} not found or missing token"); + } + return $response; } } diff --git a/app/Services/ApiClient.php b/app/Services/ApiClient.php index 68d6140..0da6c09 100644 --- a/app/Services/ApiClient.php +++ b/app/Services/ApiClient.php @@ -2,51 +2,184 @@ namespace App\Services; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\RequestException; +use Closure; +use App\Http\Responses\ApiResponse; class ApiClient { protected string $baseUrl; protected string $token; - public function __construct(string $baseUrl = null,string $token = null) + protected int $timeout = 10; + protected int $connectTimeout = 5; + protected int $retryTimes = 0; + protected int $retryDelay = 100; + + protected ?Closure $fallbackHandler = null; + protected bool $enableLogging = true; + + public function __construct(string $baseUrl = '', string $token = '') { - $this->baseUrl = $baseUrl; + $this->baseUrl = rtrim($baseUrl, '/'); $this->token = $token; } - public function set(string $baseUrl = null,string $token = null):self + + public function setBaseUrl(string $url): self { - $this->baseUrl = $baseUrl; - $this->token = $token; - return $this; - } - public function setBaseUrl():self - { - $this->baseUrl = $baseUrl; + $this->baseUrl = rtrim($url, '/'); return $this; } + public function setToken(string $token): self { $this->token = $token; return $this; } - public function withDefaultHeaders(): \Illuminate\Http\Client\PendingRequest + public function setTimeout(int $seconds): self { - return Http::withHeaders([ - 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . $this->token, - 'Content-Type' => 'application/json', - ]); + $this->timeout = $seconds; + return $this; + } + + public function setConnectTimeout(int $seconds): self + { + $this->connectTimeout = $seconds; + return $this; + } + + public function setRetry(int $times, int $delayMs = 100): self + { + $this->retryTimes = $times; + $this->retryDelay = $delayMs; + return $this; + } + + public function setFallbackHandler(?Closure $handler): self + { + $this->fallbackHandler = $handler; + return $this; + } + + public function enableLogging(bool $value = true): self + { + $this->enableLogging = $value; + return $this; + } + + protected function withDefaultHeaders(): PendingRequest + { + $request = Http::connectTimeout($this->connectTimeout) + ->timeout($this->timeout) + ->withHeaders([ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $this->token, + ]); + + if ($this->retryTimes > 0) { + $request = $request->retry($this->retryTimes, $this->retryDelay, function ($exception, $request) { + if ($this->enableLogging) { + Log::warning('API retrying...', [ + 'url' => $request->url(), + 'exception' => $exception->getMessage(), + ]); + } + return true; + }); + } + + return $request; + } + + public function requestWithCatch(callable $fn) +{ + try { + return $fn(); + } catch (RequestException $e) { + if ($this->enableLogging) { + Log::error('API Request Failed', [ + 'message' => $e->getMessage(), + 'url' => $e->request?->url(), + 'code' => $e->getCode(), + ]); + } + + if ($this->fallbackHandler instanceof \Closure) { + return call_user_func($this->fallbackHandler, $e); + } + + // 智能 fallback,直接寫在這裡 + $status = $e->response?->status(); + + if ($status >= 500) { + return ApiResponse::error('伺服器錯誤,請稍後再試','SERVER_ERROR',503); + } + + if ($status === 404) { + return ApiResponse::error('遠端 API 路徑不存在','API_NOT_FOUND',404); + } + + if ($status === 401 || $status === 403) { + return ApiResponse::unauthorized(); + } + + if ($e->getCode() === 0) { + return ApiResponse::error('無法連線遠端 API(可能超時或 DNS 錯誤)','CONNECTION_ERROR',504); + } + + return ApiResponse::error('API 請求失敗: ' . $e->getMessage(), 'HTTP_CLIENT_ERROR',$status ?? 400); + } +} + + public function get(string $endpoint, array $query = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->get("{$this->baseUrl}{$endpoint}", $query) + ); } public function post(string $endpoint, array $data = []) { - return $this->withDefaultHeaders()->post($this->baseUrl . $endpoint, $data); + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->post("{$this->baseUrl}{$endpoint}", $data) + ); } - public function get(string $endpoint, array $query = []) + public function put(string $endpoint, array $data = []) { - return $this->withDefaultHeaders()->get($this->baseUrl . $endpoint, $query); + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->put("{$this->baseUrl}{$endpoint}", $data) + ); } + public function patch(string $endpoint, array $data = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->patch("{$this->baseUrl}{$endpoint}", $data) + ); + } + + public function delete(string $endpoint, array $data = []) + { + return $this->requestWithCatch(fn () => + $this->withDefaultHeaders()->delete("{$this->baseUrl}{$endpoint}", $data) + ); + } + + public function upload(string $endpoint, array $files = [], array $data = []) + { + return $this->requestWithCatch(function () use ($endpoint, $files, $data) { + $request = $this->withDefaultHeaders(); + + foreach ($files as $key => $filePath) { + $filename = basename($filePath); + $request = $request->attach($key, fopen($filePath, 'r'), $filename); + } + + return $request->withoutVerifying()->post("{$this->baseUrl}{$endpoint}", $data); + }); + } } \ No newline at end of file diff --git a/app/Services/TcpSocketClient.php b/app/Services/TcpSocketClient.php index 45530b5..2bad12e 100644 --- a/app/Services/TcpSocketClient.php +++ b/app/Services/TcpSocketClient.php @@ -1,11 +1,14 @@ timeout = $timeout; } - public function send(string $data): string + public function send(string $data, string $breakOn = "\n"): string { $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($socket === false) { - throw new \Exception("Socket create failed: " . socket_strerror(socket_last_error())); + $err = socket_strerror(socket_last_error()); + Log::error("Socket create failed: {$err}"); + throw new Exception("Socket create failed: {$err}"); } + // 設定寫入超時(發送) + socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, [ + 'sec' => $this->timeout, + 'usec' => 0, + ]); + // 設定讀取超時(接收) + socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, [ + 'sec' => $this->timeout, + 'usec' => 0, + ]); + $result = socket_connect($socket, $this->ip, $this->port); if ($result === false) { - throw new \Exception("Socket connect failed: " . socket_strerror(socket_last_error($socket))); + $err = socket_strerror(socket_last_error($socket)); + Log::error("Socket connect failed: {$err}"); + throw new Exception("Socket connect failed: {$err}"); } - socket_write($socket, $data, strlen($data)); + $write = socket_write($socket, $data, strlen($data)); + if ($write === false) { + $err = socket_strerror(socket_last_error($socket)); + Log::error("Socket write failed: {$err}"); + throw new Exception("Socket write failed: {$err}"); + } $response = ''; while ($out = socket_read($socket, 2048)) { $response .= $out; - // 根據協議判斷是否結束接收,可以自行調整 - if (strpos($response, "\n") !== false) { + if ($breakOn && strpos($response, $breakOn) !== false) { break; } } socket_close($socket); + + if ($response === '') { + Log::warning('Socket read empty or timeout.'); + } + return $response; } } \ No newline at end of file