shouldLog($request)) { return $response; } $user = $request->user(); try { AuditLog::create([ 'user_id' => $user?->id, 'username' => $user?->username ?: $user?->name, 'role' => $user?->role, 'method' => strtoupper($request->method()), 'path' => '/'.ltrim($request->path(), '/'), 'action' => strtoupper($request->method()).' '.$request->path(), 'status_code' => (int) $response->getStatusCode(), 'ip' => $request->ip(), 'user_agent' => substr((string) $request->userAgent(), 0, 500), 'request_payload' => $this->sanitizePayload($request->all()), ]); } catch (\Throwable $e) { Log::warning('audit_log_write_failed', [ 'path' => $request->path(), 'message' => $e->getMessage(), ]); } return $response; } private function shouldLog(Request $request): bool { if ($request->isMethod('GET')) { return false; } if ($request->is('api/audit-logs*')) { return false; } // multipart 上传不参与审计,避免大 payload 与序列化问题 if ($request->is('api/upload') || $request->is('api/h5/upload')) { return false; } return true; } private function sanitizePayload(array $payload): array { $sensitive = ['password', 'password_confirmation', 'token', 'access_token']; $walk = function ($value) use (&$walk, $sensitive) { if ($value instanceof SymfonyUploadedFile) { return [ '_upload' => true, 'client_name' => $value->getClientOriginalName(), 'size' => $value->getSize(), 'mime' => $value->getClientMimeType(), ]; } if (is_object($value)) { if ($value instanceof \DateTimeInterface) { return $value->format('c'); } if ($value instanceof \JsonSerializable) { return $walk($value->jsonSerialize()); } return ['_object' => $value::class]; } if (!is_array($value)) { return $value; } $result = []; foreach ($value as $k => $v) { if (is_string($k) && in_array(strtolower($k), $sensitive, true)) { $result[$k] = '***'; } else { $result[$k] = $walk($v); } } return $result; }; return $walk($payload); } }