handle($request); $duration_ms = (int) ((microtime(true) - $start) * 1000); // 在父协程中提取 user_id(子协程不继承 Swoole Context) $user_id = null; try { $user_id = $this->auth->guard('jwt')->user()?->getId(); } catch (\Throwable) { // 未认证请求,user_id 保持 null } $method = $request->getMethod(); $path = $request->getUri()->getPath(); $status_code = $response->getStatusCode(); $ip = RequestHelper::getClientIp($request); $user_agent = $request->getHeaderLine('User-Agent') ?: null; // GET/DELETE 请求不记录请求体 $request_body = null; if (!in_array($method, ['GET', 'DELETE', 'HEAD', 'OPTIONS'], true)) { $parsed_body = $request->getParsedBody(); if (is_array($parsed_body) && !empty($parsed_body)) { $request_body = self::sanitizeBody($parsed_body); } } $response_code = self::extractResponseCode($response); // defer 写库:在当前协程退出时执行,响应已返回客户端,同时保证协程内 DB 连接可用 Coroutine::defer(static function () use ( $user_id, $method, $path, $status_code, $ip, $user_agent, $request_body, $response_code, $duration_ms ): void { try { ApiRequestLog::query()->create([ 'user_id' => $user_id, 'method' => $method, 'path' => $path, 'status_code' => $status_code, 'ip' => $ip, 'user_agent' => $user_agent, 'request_body' => $request_body, 'response_code' => $response_code, 'duration_ms' => $duration_ms, ]); } catch (\Throwable $e) { Log::get()->error('RequestLogMiddleware: 写入请求日志失败', [ 'error' => $e->getMessage(), 'path' => $path, ]); } }); return $response; } /** * 递归替换敏感字段为 *** */ public static function sanitizeBody(array $body): array { $sensitive_keys = [ 'password', 'old_password', 'new_password', 'password_confirmation', 'token', 'secret', 'api_key', 'access_token', 'refresh_token', ]; foreach ($body as $key => $value) { if (in_array($key, $sensitive_keys, true)) { $body[$key] = '***'; } elseif (is_array($value)) { $body[$key] = self::sanitizeBody($value); } } return $body; } /** * 从 JSON 响应体中提取 code 字段 */ public static function extractResponseCode(ResponseInterface $response): ?int { $content_type = $response->getHeaderLine('Content-Type'); if (stripos($content_type, 'application/json') === false) { return null; } try { $stream = $response->getBody(); $body = (string) $stream; // 回退流指针,避免后续读取得到空内容 if ($stream->isSeekable()) { $stream->rewind(); } $data = json_decode($body, true); if (is_array($data) && isset($data['code'])) { return (int) $data['code']; } } catch (\Throwable) { // 解析失败返回 null } return null; } }