Files
nl-admin-api/app/Http/Middleware/ApiOpLogMiddleware.php
2026-08-10 18:32:06 +08:00

157 lines
5.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}