diff --git a/src/Exception/Handler/ApiExceptionHandler.php b/src/Exception/Handler/ApiExceptionHandler.php deleted file mode 100644 index 2ffa306..0000000 --- a/src/Exception/Handler/ApiExceptionHandler.php +++ /dev/null @@ -1,253 +0,0 @@ -request = Util::get(RequestInterface::class); - $this->traceId = $this->getTraceId(); - } - - public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface - { - $this->stopPropagation(); // 阻止异常冒泡 - - $logData = $this->buildLogData($throwable); - - // 按异常类型记录日志(业务异常warning,系统异常error) - $this->logException($throwable, $logData); - - // 解析错误信息 - [$code, $message, $data] = $this->resolveErrorInfo($throwable); - - // 根据请求类型返回对应格式的响应(HTTP/RPC) - if ($this->isRpcRequest()) { - return $this->buildRpcResponse($response, $code, $message, $data); - } - - return $this->buildHttpResponse($response, $code, $message, $data); - } - - public function isValid(Throwable $throwable): bool - { - return true; - } - - /** - * 构建日志数据(包含完整上下文,生产环境脱敏) - */ - private function buildLogData(Throwable $throwable): array - { - $logData = [ - 'trace_id' => $this->traceId, - 'type' => get_class($throwable), - 'message' => $throwable->getMessage(), - 'code' => $throwable->getCode(), - 'file' => $throwable->getFile(), - 'line' => $throwable->getLine(), - 'trace' => $throwable->getTraceAsString(), - 'context' => [ - 'url' => (string)$this->request->getUri(), - 'method' => $this->request->getMethod(), - 'user_id' => $this->request->header('X-User-Id') ?? 'unknown', - 'timestamp' => date('Y-m-d H:i:s'), - ], - ]; - - // 生产环境脱敏敏感参数 - if (env('APP_ENV') === 'prod') { - $logData['context']['params'] = $this->desensitizeParams($this->request->all()); - } else { - $logData['context']['params'] = $this->request->all(); - } - - return $logData; - } - - /** - * 按异常类型记录日志 - */ - private function logException(Throwable $throwable, array $logData): void - { - if ($throwable instanceof BusinessException) { - Util::recordLog($throwable->getMessage(), $logData, $this->traceId); - } else { - // 系统异常额外标记级别,便于日志告警 - Util::recordLog('系统异常', $logData, $this->traceId); - } - } - - /** - * 解析错误信息(区分环境和异常类型) - */ - private function resolveErrorInfo(Throwable $throwable): array - { - // 业务异常:直接使用其错误码和消息 - if ($throwable instanceof BusinessException) { - return [ - $throwable->getCode(), - $throwable->getMessage(), - $throwable->getData() ?? [], - ]; - } - - // 微服务 JSON-RPC 调用异常:按 code 是否为系统异常保留值(-32000)区分业务错误与系统错误, - // 不要求业务码必须落在 400-499(各微服务有自己的业务状态码,不是 HTTP 状态码) - if ($throwable instanceof RequestException) { - $code = $throwable->getCode(); - if ($code !== ResponseBuilder::SERVER_ERROR) { - return [$code, $throwable->getMessage(), []]; - } - $isDev = env('APP_ENV') === 'dev'; - return [500, $isDev ? $throwable->getMessage() : '服务异常,请稍后重试', []]; - } - - // 系统异常:开发环境返回详细信息,生产环境返回通用消息 - $isDev = env('APP_ENV') === 'dev'; - $innerCode = $this->getInnerCode($throwable) ?? 500; - - return [ - $innerCode, - $throwable->getMessage() ?: '服务异常,请稍后重试', - $isDev ? [ - 'trace' => $throwable->getTraceAsString(), - 'file' => $throwable->getFile(), - 'line' => $throwable->getLine(), - 'trace_id' => $this->traceId, // 开发环境返回追踪ID便于调试 - ] : ['trace_id' => $this->traceId], // 生产环境仅返回追踪ID供排查 - ]; - } - - /** - * 判断是否为RPC请求(如JSON-RPC) - */ - private function isRpcRequest(): bool - { - $contentType = $this->request->getHeaderLine('Content-Type'); - $path = $this->request->getUri()->getPath(); - - // 适配常见RPC场景:Content-Type为json且路径包含/rpc - return str_contains($contentType, 'application/json') - && str_contains($path, '/rpc'); - } - - /** - * 构建RPC响应(JSON-RPC 2.0规范) - */ - private function buildRpcResponse(ResponseInterface $response, int $code, string $message, array $data): ResponseInterface - { - $requestBody = $this->request->getBody()->getContents(); - $requestData = json_decode($requestBody, true) ?? []; - $rpcId = $requestData['id'] ?? null; - - $responseData = [ - 'jsonrpc' => '2.0', - 'error' => [ - 'code' => $code, - 'data' => $data, - 'message' => $message, - ], - 'id' => $rpcId, - ]; - - return $response - ->withHeader('Content-Type', 'application/json') - ->withHeader('X-Trace-Id', $this->traceId) // 透传追踪ID - ->withStatus(200) // RPC错误必须返回200状态码 - ->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE))); - } - - /** - * 构建HTTP响应(通用JSON格式) - */ - private function buildHttpResponse(ResponseInterface $response, int $code, string $message, array $data): ResponseInterface - { - // HTTP状态码映射:业务码本身就是合法 HTTP 状态码时按原样用, - // 否则统一回落 200(body 里带真实业务码,与本项目 success()/error() 助手函数约定一致), - // 避免把非法状态码(如各服务自定义的业务码)发给真实 HTTP 客户端 - $statusCode = ($code >= 100 && $code <= 599) ? $code : 200; - - $responseData = [ - 'code' => $code, - 'data' => $data, - 'message' => $message, - ]; - - return $response - ->withHeader('Content-Type', 'application/json') - ->withHeader('X-Trace-Id', $this->traceId) - ->withStatus($statusCode) - ->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE))); - } - - /** - * 脱敏敏感参数(生产环境用) - */ - private function desensitizeParams(array $params): array - { - $sensitiveKeys = ['password', 'pwd', 'mobile', 'phone', 'id_card', 'credit_card']; - foreach ($params as $key => &$value) { - if (in_array(strtolower($key), $sensitiveKeys) && is_string($value)) { - $value = $this->maskValue($value, $key); - } - } - return $params; - } - - /** - * 具体字段脱敏逻辑 - */ - private function maskValue(string $value, string $key): string - { - return match (strtolower($key)) { - 'password', 'pwd' => '******', - 'mobile', 'phone' => substr($value, 0, 3) . '****' . substr($value, -4), - 'id_card' => substr($value, 0, 6) . '********' . substr($value, -4), - default => substr($value, 0, 2) . '****' . substr($value, -2), - }; - } - - /** - * 获取内部异常错误码 - */ - private function getInnerCode(Throwable $throwable): ?int - { - if (method_exists($throwable, 'getThrowable') && is_array($inner = $throwable->getThrowable())) { - return $inner['code'] ?? null; - } - // 兼容嵌套异常 - if ($throwable->getPrevious()) { - return $throwable->getPrevious()->getCode() ?: null; - } - return null; - } -} diff --git a/src/Exception/Handler/GatewayExceptionHandler.php b/src/Exception/Handler/GatewayExceptionHandler.php new file mode 100644 index 0000000..bd9980c --- /dev/null +++ b/src/Exception/Handler/GatewayExceptionHandler.php @@ -0,0 +1,64 @@ +stopPropagation(); // 阻止异常冒泡 + + $traceId = $this->getTraceId(); + + $code = 500; + $message = '服务器内部错误'; + + if (($throwable instanceof BusinessException)) { + Util::recordLog($throwable->getMessage(), $throwable, $traceId); + $code = $throwable->getCode(); + $message = $throwable->getMessage(); + } else if ($throwable instanceof ClientException) { + $code = $throwable->getCode() ?: 500; + $message = $throwable->getMessage() ?: '下游服务异常'; + Util::recordLog($message, $throwable, $traceId); + } + + $responseData = [ + 'code' => $code, + 'message' => $message, + 'data' => null, + ]; + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('X-Trace-Id', $traceId) + ->withStatus(200) + ->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE))); + } + + public function isValid(Throwable $throwable): bool + { + return true; + } +}