Files
nl-admin-api/app/Http/Middleware/ApiOpLogMiddleware.php

157 lines
5.0 KiB
PHP
Raw Normal View History

2026-08-10 18:32:06 +08:00
<?php
namespace App\Http\Middleware;
use App\Models\ApiEndpointModel;
use App\Models\ApiOpLogModel;
use App\Service\common\UserAgentService;
use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* API 操作日志中间件:响应结束后按接口注册表决定是否落库
*/
class ApiOpLogMiddleware
{
/**
* 透传请求;真正写日志在 terminate避免拖慢接口响应
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
/**
* 响应结束后写操作日志
* 匹配:去掉 /api/ 前缀的 url method=0(ANY) 或等于请求方法is_log=1、status=1
*/
public function terminate(Request $request, Response $response): void
{
try {
$path = $this->normalizePath($request->path());
if ($path === '') {
return;
}
$noInsert = config('nl.log.no_insert', []);
if (is_array($noInsert)) {
$full = 'api/' . $path;
if (in_array($path, $noInsert, true)
|| in_array($full, $noInsert, true)
|| in_array('/' . $full, $noInsert, true)
) {
return;
}
}
$methodCode = $this->mapMethod($request->method());
$endpoint = ApiEndpointModel::where('url', $path)
->where('deleted_at', 0)
->where('status', 1)
->where('is_log', 1)
->where(function ($q) use ($methodCode) {
$q->where('method', 0)->orWhere('method', $methodCode);
})
->orderByDesc('method')
->first();
if (!$endpoint) {
return;
}
$userId = $this->resolveUserId($request);
$body = $response->getContent();
$result = json_decode((string) $body, true);
if (!is_array($result)) {
$result = ['raw' => mb_substr((string) $body, 0, 2000)];
}
$code = $result['code'] ?? null;
$type = ((int) $code === 0) ? 0 : 1;
$ua = UserAgentService::getInstance();
ApiOpLogModel::insert([
'user_id' => $userId,
'url' => $path,
'method' => $methodCode,
'controller' => (string) ($endpoint->controller ?? ''),
'ip' => (string) get_ip(),
'param' => json_encode($this->collectParams($request), JSON_UNESCAPED_UNICODE),
'result' => json_encode($result, JSON_UNESCAPED_UNICODE),
'type' => $type,
'result_code' => (string) ($code ?? ''),
'platform_type' => 0,
'belong_id' => 0,
'user_type' => 0,
'equipment' => $ua->parseEquipment(),
'browser' => $ua->parseBrowser(),
'created_at' => time(),
]);
} catch (\Throwable $e) {
// 日志失败不影响主流程
}
}
/**
* 规范化路径:去掉 api/ 前缀
*/
private function normalizePath(string $path): string
{
$path = trim($path, '/');
if (str_starts_with($path, 'api/')) {
$path = substr($path, 4);
}
return trim($path, '/');
}
/**
* HTTP 方法映射GET=1 POST=2 其他=0
*/
private function mapMethod(string $method): int
{
return match (strtoupper($method)) {
'GET' => 1,
'POST' => 2,
default => 0,
};
}
/**
* Bearer JWT 解析 user_id失败返回 0(不抛鉴权异常)
*/
private function resolveUserId(Request $request): int
{
try {
$token = $request->bearerToken();
if (empty($token)) {
return 0;
}
$secret = (string) config('nl.jwt.secret', '');
if (strlen($secret) < 32) {
$secret = hash('sha256', $secret !== '' ? $secret : 'nl_admin_jwt_fallback_secret');
}
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
return (int) ($decoded->data->id ?? 0);
} catch (\Throwable $e) {
return 0;
}
}
/**
* 收集请求参数并脱敏密码类字段
*
* @return array<string,mixed>
*/
private function collectParams(Request $request): array
{
$all = array_merge($request->query(), $request->request->all());
$maskKeys = [
'password', 'old_password', 'new_password', 'confirm_password',
'access_key', 'secret_key', 'api_key', 'token',
];
foreach ($maskKeys as $k) {
if (array_key_exists($k, $all) && $all[$k] !== '' && $all[$k] !== null) {
$all[$k] = '******';
}
}
return $all;
}
}