update request log
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Model\ApiRequestLog;
|
||||
use App\Utils\Log;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Qbhy\HyperfAuth\AuthManager;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
class RequestLogMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected AuthManager $auth
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$response = $handler->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 = self::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);
|
||||
|
||||
// 异步协程写库,不阻塞响应返回
|
||||
Coroutine::create(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'];
|
||||
|
||||
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 {
|
||||
$body = (string) $response->getBody();
|
||||
$data = json_decode($body, true);
|
||||
if (is_array($data) && isset($data['code'])) {
|
||||
return (int) $data['code'];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// 解析失败返回 null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP
|
||||
*
|
||||
* 优先级:X-Forwarded-For → X-Real-IP → ServerParams remote_addr
|
||||
*/
|
||||
public static function getClientIp(ServerRequestInterface $request): ?string
|
||||
{
|
||||
// X-Forwarded-For 可能包含多个 IP,取第一个
|
||||
$forwarded_for = $request->getHeaderLine('X-Forwarded-For');
|
||||
if ($forwarded_for !== '') {
|
||||
$ips = explode(',', $forwarded_for);
|
||||
return trim($ips[0]);
|
||||
}
|
||||
|
||||
$real_ip = $request->getHeaderLine('X-Real-IP');
|
||||
if ($real_ip !== '') {
|
||||
return trim($real_ip);
|
||||
}
|
||||
|
||||
$server_params = $request->getServerParams();
|
||||
return $server_params['remote_addr'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
/**
|
||||
* API 请求日志模型
|
||||
*
|
||||
* 记录所有 HTTP 请求的基本信息、耗时和响应状态
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $user_id
|
||||
* @property string $method
|
||||
* @property string $path
|
||||
* @property int $status_code
|
||||
* @property string|null $ip
|
||||
* @property string|null $user_agent
|
||||
* @property array|null $request_body
|
||||
* @property int|null $response_code
|
||||
* @property int $duration_ms
|
||||
* @property \Carbon\Carbon $created_at
|
||||
*/
|
||||
class ApiRequestLog extends Model
|
||||
{
|
||||
protected ?string $table = 'api_request_logs';
|
||||
|
||||
public bool $timestamps = true;
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected array $fillable = [
|
||||
'user_id', 'method', 'path', 'status_code', 'ip',
|
||||
'user_agent', 'request_body', 'response_code', 'duration_ms',
|
||||
];
|
||||
|
||||
protected array $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'status_code' => 'integer',
|
||||
'response_code' => 'integer',
|
||||
'duration_ms' => 'integer',
|
||||
'request_body' => 'json',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -121,6 +121,51 @@ use OpenApi\Attributes as OA;
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
]
|
||||
)]
|
||||
#[OA\Schema(
|
||||
schema: 'DashboardOverview',
|
||||
type: 'object',
|
||||
description: 'Dashboard 概览统计',
|
||||
properties: [
|
||||
new OA\Property(property: 'today', properties: [
|
||||
new OA\Property(property: 'success', type: 'integer', example: 120),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 3),
|
||||
], type: 'object', description: '今日统计'),
|
||||
new OA\Property(property: 'this_week', properties: [
|
||||
new OA\Property(property: 'success', type: 'integer', example: 850),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 15),
|
||||
], type: 'object', description: '本周统计'),
|
||||
new OA\Property(property: 'this_month', properties: [
|
||||
new OA\Property(property: 'success', type: 'integer', example: 3200),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 42),
|
||||
], type: 'object', description: '本月统计'),
|
||||
new OA\Property(property: 'by_type', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'data_type', type: 'string', enum: ['order', 'product', 'refund', 'inventory'], example: 'order'),
|
||||
new OA\Property(property: 'success', type: 'integer', example: 1000),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 10),
|
||||
], type: 'object'), description: '按数据类型分组统计(本月窗口)'),
|
||||
]
|
||||
)]
|
||||
#[OA\Schema(
|
||||
schema: 'DashboardTrendItem',
|
||||
type: 'object',
|
||||
description: '趋势数据点',
|
||||
properties: [
|
||||
new OA\Property(property: 'date', type: 'string', format: 'date', example: '2026-03-17'),
|
||||
new OA\Property(property: 'success', type: 'integer', example: 120),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 3),
|
||||
]
|
||||
)]
|
||||
#[OA\Schema(
|
||||
schema: 'DashboardBreakdownItem',
|
||||
type: 'object',
|
||||
description: '分组统计项',
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1, description: '维度 ID(公司/平台/店铺)'),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'Tmall', description: '维度名称'),
|
||||
new OA\Property(property: 'success', type: 'integer', example: 500),
|
||||
new OA\Property(property: 'failed', type: 'integer', example: 8),
|
||||
]
|
||||
)]
|
||||
class OpenApiSpec
|
||||
{
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user