浏览代码

feat(middleware): 添加 API 请求日志记录功能

- 新增 ApiRequestLog 模型用于存储请求日志
- 实现 ApiRequestLogMiddleware 中间件记录请求信息
- 在全局中间件配置中添加 ApiRequestLogMiddleware
- 添加 api_log 别名方便配置和使用
shichen 10 月之前
父节点
当前提交
e6026b85ae
共有 4 个文件被更改,包括 413 次插入1 次删除
  1. 128 0
      app/common/model/system/ApiRequestLog.php
  2. 3 0
      app/middleware.php
  3. 279 0
      app/middleware/ApiRequestLogMiddleware.php
  4. 3 1
      config/middleware.php

+ 128 - 0
app/common/model/system/ApiRequestLog.php

@@ -0,0 +1,128 @@
1
+<?php
2
+/**
3
+ * @package merchant
4
+ *
5
+ * @author xaboy
6
+ * @day 2025-09-15
7
+ *
8
+ * 
9
+ */
10
+
11
+namespace app\common\model\system;
12
+
13
+use app\common\model\BaseModel;
14
+
15
+class ApiRequestLog extends BaseModel
16
+{
17
+
18
+    /**
19
+     * @return string
20
+     * @author xaboy
21
+     * @day 2025-09-15
22
+     */
23
+    public static function tablePk(): string
24
+    {
25
+        return 'log_id';
26
+    }
27
+
28
+    /**
29
+     * @return string
30
+     * @author xaboy
31
+     * @day 2025-09-15
32
+     */
33
+    public static function tableName(): string
34
+    {
35
+        return 'api_request_log';
36
+    }
37
+
38
+    /**
39
+     * 记录接口请求日志
40
+     * @param array $data
41
+     * @return bool
42
+     */
43
+    public static function record(array $data): bool
44
+    {
45
+        try {
46
+            $logData = [
47
+                'request_id' => $data['request_id'] ?? self::generateRequestId(),
48
+                'app_type' => $data['app_type'] ?? '',
49
+                'request_method' => $data['request_method'] ?? '',
50
+                'request_url' => $data['request_url'] ?? '',
51
+                'request_params' => isset($data['request_params']) ? json_encode($data['request_params'], JSON_UNESCAPED_UNICODE) : null,
52
+                'request_headers' => isset($data['request_headers']) ? json_encode($data['request_headers'], JSON_UNESCAPED_UNICODE) : null,
53
+                'request_ip' => $data['request_ip'] ?? '',
54
+                'user_agent' => $data['user_agent'] ?? '',
55
+                'user_id' => $data['user_id'] ?? 0,
56
+                'merchant_id' => $data['merchant_id'] ?? 0,
57
+                'admin_id' => $data['admin_id'] ?? 0,
58
+                'response_code' => $data['response_code'] ?? 0,
59
+                'response_data' => isset($data['response_data']) ? json_encode($data['response_data'], JSON_UNESCAPED_UNICODE) : null,
60
+                'response_time' => $data['response_time'] ?? 0,
61
+                'error_message' => $data['error_message'] ?? '',
62
+            ];
63
+
64
+            return self::create($logData) !== false;
65
+        } catch (\Exception $e) {
66
+            // 记录日志失败时不中断主流程
67
+            return false;
68
+        }
69
+    }
70
+
71
+    /**
72
+     * 生成请求唯一标识
73
+     * @return string
74
+     */
75
+    private static function generateRequestId(): string
76
+    {
77
+        return md5(uniqid('api_request_', true) . mt_rand(1000, 9999));
78
+    }
79
+
80
+    /**
81
+     * 根据请求ID查找日志
82
+     * @param string $requestId
83
+     * @return array|null
84
+     */
85
+    public static function findByRequestId(string $requestId): ?array
86
+    {
87
+        $log = self::getDB()->where('request_id', $requestId)->find();
88
+        return $log ? $log->toArray() : null;
89
+    }
90
+
91
+    /**
92
+     * 获取指定时间范围内的日志列表
93
+     * @param string $startTime
94
+     * @param string $endTime
95
+     * @param int $page
96
+     * @param int $limit
97
+     * @return array
98
+     */
99
+    public static function getListByTimeRange(string $startTime, string $endTime, int $page = 1, int $limit = 20): array
100
+    {
101
+        $query = self::getDB()
102
+            ->where('create_time', '>=', $startTime)
103
+            ->where('create_time', '<=', $endTime)
104
+            ->order('log_id', 'desc');
105
+
106
+        $total = $query->count();
107
+        $list = $query->page($page, $limit)->select()->toArray();
108
+
109
+        return [
110
+            'list' => $list,
111
+            'total' => $total,
112
+            'page' => $page,
113
+            'limit' => $limit,
114
+            'total_page' => ceil($total / $limit)
115
+        ];
116
+    }
117
+
118
+    /**
119
+     * 清理过期日志
120
+     * @param int $days 保留天数
121
+     * @return int
122
+     */
123
+    public static function clearExpiredLogs(int $days = 30): int
124
+    {
125
+        $expireTime = date('Y-m-d H:i:s', strtotime("-$days days"));
126
+        return self::getDB()->where('create_time', '<', $expireTime)->delete();
127
+    }
128
+}

+ 3 - 0
app/middleware.php

@@ -1,6 +1,9 @@
1 1
 <?php
2 2
 // 全局中间件定义文件
3 3
 return [
4
+    // 接口请求记录中间件
5
+    \app\middleware\ApiRequestLogMiddleware::class,
6
+    
4 7
     // 全局请求缓存
5 8
     // \think\middleware\CheckRequestCache::class,
6 9
     // 多语言加载

+ 279 - 0
app/middleware/ApiRequestLogMiddleware.php

@@ -0,0 +1,279 @@
1
+<?php
2
+declare (strict_types=1);
3
+
4
+namespace app\middleware;
5
+
6
+use app\common\model\system\ApiRequestLog;
7
+use crmeb\services\JwtTokenService;
8
+use think\facade\Request;
9
+use think\Response;
10
+
11
+class ApiRequestLogMiddleware
12
+{
13
+    /**
14
+     * 处理请求
15
+     *
16
+     * @param \think\Request $request
17
+     * @param \Closure       $next
18
+     * @return Response
19
+     */
20
+    public function handle($request, \Closure $next)
21
+    {
22
+        // 记录请求开始时间
23
+        $startTime = microtime(true);
24
+        
25
+        // 执行请求
26
+        $response = $next($request);
27
+        
28
+        // 记录请求结束时间和计算响应时间
29
+        $endTime = microtime(true);
30
+        $responseTime = (int)(($endTime - $startTime) * 1000); // 转换为毫秒
31
+        
32
+        try {
33
+            // 获取应用类型
34
+            $appType = $this->getAppType($request);
35
+            
36
+            // 从token中获取所有用户信息
37
+            $userInfo = $this->getUserInfoFromToken($request);
38
+            $userId = $userInfo['userId'] ?? 0;
39
+            $merchantId = $userInfo['merchantId'] ?? 0;
40
+            $adminId = $userInfo['adminId'] ?? 0;
41
+
42
+            // 记录请求日志
43
+            ApiRequestLog::record([
44
+                'app_type' => $appType,
45
+                'request_method' => $request->method(),
46
+                'request_url' => $request->url(),
47
+                'request_params' => $this->getRequestParams($request),
48
+                'request_headers' => $this->getRequestHeaders($request),
49
+                'request_ip' => $request->ip(),
50
+                'user_agent' => $request->header('user-agent', ''),
51
+                'user_id' => $userId,
52
+                'merchant_id' => $merchantId,
53
+                'admin_id' => $adminId,
54
+                'response_code' => $response->getCode(),
55
+                'response_data' => $this->getResponseData($response),
56
+                'response_time' => $responseTime,
57
+                'error_message' => $this->getErrorMessage($response),
58
+            ]);
59
+        } catch (\Exception $e) {
60
+            // 记录日志失败时不中断主流程
61
+            // 可以在这里添加日志记录失败的日志
62
+        }
63
+        
64
+        return $response;
65
+    }
66
+
67
+    /**
68
+     * 获取应用类型
69
+     * @param \think\Request $request
70
+     * @return string
71
+     */
72
+    private function getAppType($request): string
73
+    {
74
+        // 获取请求路径
75
+        $path = $request->pathinfo();
76
+
77
+        $position = strpos($path, '/');
78
+        if ($position === false) {
79
+            return 'unknown';
80
+        }
81
+
82
+        return substr($path, 0, $position);
83
+    }
84
+
85
+    /**
86
+     * 获取请求参数
87
+     * @param \think\Request $request
88
+     * @return array
89
+     */
90
+    private function getRequestParams($request): array
91
+    {
92
+        $params = $request->param();
93
+        
94
+        // 过滤敏感信息
95
+        $sensitiveFields = ['password', 'pwd', 'token', 'secret', 'key', 'authorization'];
96
+        foreach ($sensitiveFields as $field) {
97
+            if (isset($params[$field])) {
98
+                $params[$field] = '***';
99
+            }
100
+        }
101
+        
102
+        return $params;
103
+    }
104
+
105
+    /**
106
+     * 获取请求头信息
107
+     * @param \think\Request $request
108
+     * @return array
109
+     */
110
+    private function getRequestHeaders($request): array
111
+    {
112
+        $headers = $request->header();
113
+        
114
+        // 过滤敏感头信息
115
+        $sensitiveHeaders = ['authorization', 'cookie', 'token'];
116
+        foreach ($sensitiveHeaders as $header) {
117
+            if (isset($headers[$header])) {
118
+                $headers[$header] = '***';
119
+            }
120
+        }
121
+        
122
+        return $headers;
123
+    }
124
+
125
+    /**
126
+     * 获取响应数据
127
+     * @param Response $response
128
+     * @return array|null
129
+     */
130
+    private function getResponseData(Response $response): ?array
131
+    {
132
+        $content = $response->getContent();
133
+        
134
+        try {
135
+            $data = json_decode($content, true);
136
+            if (json_last_error() === JSON_ERROR_NONE) {
137
+                return $data;
138
+            }
139
+        } catch (\Exception $e) {
140
+            // JSON解析失败,返回原始内容的前1000个字符
141
+            return ['raw_content' => substr($content, 0, 1000)];
142
+        }
143
+        
144
+        return null;
145
+    }
146
+
147
+    /**
148
+     * 获取错误信息
149
+     * @param Response $response
150
+     * @return string
151
+     */
152
+    private function getErrorMessage(Response $response): string
153
+    {
154
+        if ($response->getCode() >= 400) {
155
+            $content = $response->getContent();
156
+            try {
157
+                $data = json_decode($content, true);
158
+                if (json_last_error() === JSON_ERROR_NONE && isset($data['msg'])) {
159
+                    return $data['msg'];
160
+                }
161
+            } catch (\Exception $e) {
162
+                return substr($content, 0, 500);
163
+            }
164
+        }
165
+        
166
+        return '';
167
+    }
168
+
169
+    /**
170
+     * 从token中解析adminId
171
+     * @param \think\Request $request
172
+     * @return int
173
+     */
174
+    private function getAdminIdFromToken($request): int
175
+    {
176
+        try {
177
+            $token = $this->getTokenFromRequest($request);
178
+            if (!$token) {
179
+                return 0;
180
+            }
181
+            
182
+            $jwtService = new JwtTokenService();
183
+            
184
+            try {
185
+                // 尝试解析token
186
+                $payload = $jwtService->parseToken($token);
187
+            } catch (\Exception $e) {
188
+                // 如果解析失败,尝试解码(不验证签名)
189
+                $payload = $jwtService->decode($token);
190
+            }
191
+            
192
+            // 检查token类型是否为admin,并且包含有效的ID
193
+            if (isset($payload->jti) && is_array($payload->jti) && count($payload->jti) >= 2) {
194
+                if ($payload->jti[1] === 'admin') {
195
+                    return (int)$payload->jti[0];
196
+                }
197
+            }
198
+        } catch (\Exception $e) {
199
+            // token解析失败,返回0
200
+            return 0;
201
+        }
202
+        
203
+        return 0;
204
+    }
205
+
206
+    /**
207
+     * 从请求中获取token
208
+     * @param \think\Request $request
209
+     * @return string|null
210
+     */
211
+    private function getTokenFromRequest($request): ?string
212
+    {
213
+        // 从header中获取token
214
+        $token = $request->header('X-Token');
215
+        if ($token && strpos($token, 'Bearer ') === 0) {
216
+            $token = substr($token, 7);
217
+        }
218
+        
219
+        // 如果header中没有,尝试从参数中获取
220
+        if (!$token) {
221
+            $token = $request->param('token');
222
+        }
223
+        
224
+        return $token ? trim($token) : null;
225
+    }
226
+
227
+    /**
228
+     * 从token中获取所有用户信息
229
+     * @param \think\Request $request
230
+     * @return array
231
+     */
232
+    private function getUserInfoFromToken($request): array
233
+    {
234
+        $result = [
235
+            'userId' => 0,
236
+            'merchantId' => 0,
237
+            'adminId' => 0
238
+        ];
239
+        
240
+        try {
241
+            $token = $this->getTokenFromRequest($request);
242
+            if (!$token) {
243
+                return $result;
244
+            }
245
+            
246
+            $jwtService = new JwtTokenService();
247
+            
248
+            try {
249
+                // 尝试解析token
250
+                $payload = $jwtService->parseToken($token);
251
+            } catch (\Exception $e) {
252
+                // 如果解析失败,尝试解码(不验证签名)
253
+                $payload = $jwtService->decode($token);
254
+            }
255
+            
256
+            // 检查token类型并获取对应的ID
257
+            if (isset($payload->jti) && is_array($payload->jti) && count($payload->jti) >= 2) {
258
+                $userId = (int)$payload->jti[0];
259
+                $userType = $payload->jti[1];
260
+                
261
+                switch ($userType) {
262
+                    case 'user':
263
+                        $result['userId'] = $userId;
264
+                        break;
265
+                    case 'mer':
266
+                        $result['merchantId'] = $userId;
267
+                        break;
268
+                    case 'admin':
269
+                        $result['adminId'] = $userId;
270
+                        break;
271
+                }
272
+            }
273
+        } catch (\Exception $e) {
274
+            // token解析失败,返回默认值
275
+        }
276
+        
277
+        return $result;
278
+    }
279
+}

+ 3 - 1
config/middleware.php

@@ -2,7 +2,9 @@
2 2
 // 中间件配置
3 3
 return [
4 4
     // 别名或分组
5
-    'alias'    => [],
5
+    'alias'    => [
6
+        'api_log' => \app\middleware\ApiRequestLogMiddleware::class,
7
+    ],
6 8
     // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
7 9
     'priority' => [],
8 10
 ];