mm_os 3.2.4 → 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/conf.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "runPath": "E:\\gitee\\1_doing\\mm\\mm_sos\\demo"
2
+ "runPath": "E:\\gitee\\1_doing\\mm\\mm_os"
3
3
  }
@@ -154,7 +154,7 @@ MQTT.prototype.use = function(func) {
154
154
  */
155
155
  MQTT.prototype.main = function(state) {
156
156
  var cg = this.config;
157
- sr = this.server;
157
+ var sr = this.server;
158
158
 
159
159
  var _this = this;
160
160
 
@@ -239,46 +239,144 @@ MQTT.prototype.published = function(packet, client) {
239
239
  };
240
240
 
241
241
  /**
242
- * 身份验证
242
+ * 身份验证 - 使用MySQL数据库验证
243
243
  * @param {Object} client 客户端
244
244
  * @param {String} username 用户名
245
245
  * @param {String} password 密码
246
246
  * @param {Function} callback 回调函数,回调返回true,则表示验证通过。
247
247
  */
248
248
  MQTT.prototype.auth = function(client, username, password, callback) {
249
- // console.log("连接授权", client.id, username, password ? password.toString() : '');
250
- var bl = true;
251
- // 回调第二个参数为true表示验证通过, 为false表示验证失败
252
- callback(null, bl);
249
+ // 密码可能是Buffer类型,需要转换为字符串
250
+ const passwordStr = password ? password.toString() : '';
251
+
252
+ // 检查必要参数
253
+ if (!username || !passwordStr) {
254
+ console.warn(`MQTT客户端 ${client.id} 身份验证失败:用户名或密码为空`);
255
+ callback(null, false);
256
+ return;
257
+ }
258
+
259
+ // 检查是否有可用的MySQL连接
260
+ if (!$.sql || typeof $.sql.query !== 'function') {
261
+ console.error(`MQTT客户端 ${client.id} 身份验证错误:MySQL连接不可用`);
262
+ callback(null, false);
263
+ return;
264
+ }
265
+
266
+ // 使用参数化查询以防止SQL注入
267
+ const sql = `SELECT password FROM mqtt_users WHERE username = ? LIMIT 1`;
268
+ const params = [username];
269
+
270
+ // 执行查询验证用户
271
+ $.sql.query(sql, params, (err, results) => {
272
+ if (err) {
273
+ console.error(`MQTT客户端 ${client.id} 身份验证数据库错误:`, err);
274
+ callback(null, false);
275
+ return;
276
+ }
277
+
278
+ // 检查用户是否存在
279
+ if (!results || results.length === 0) {
280
+ console.warn(`MQTT客户端 ${client.id} 身份验证失败:用户不存在,用户名: ${username}`);
281
+ callback(null, false);
282
+ return;
283
+ }
284
+
285
+ // 获取数据库中的密码
286
+ const dbPassword = results[0].password;
287
+
288
+ // 验证密码
289
+ // 注意:在实际应用中,应该使用bcrypt等密码哈希算法进行验证,而不是明文比较
290
+ // 如果数据库中存储的是哈希密码,需要使用相应的哈希算法进行验证
291
+ try {
292
+ // 尝试使用bcrypt验证(如果已安装bcrypt模块)
293
+ const bcrypt = require('bcrypt');
294
+ const isMatch = bcrypt.compareSync(passwordStr, dbPassword);
295
+
296
+ if (isMatch) {
297
+ console.info(`MQTT客户端 ${client.id} 身份验证成功,用户: ${username}`);
298
+ } else {
299
+ console.warn(`MQTT客户端 ${client.id} 身份验证失败:密码不匹配,用户名: ${username}`);
300
+ }
301
+
302
+ callback(null, isMatch);
303
+ } catch (err) {
304
+ // 如果bcrypt不可用,回退到简单的密码比较(不推荐在生产环境中使用)
305
+ console.warn('bcrypt模块不可用,使用简单密码比较');
306
+ const isMatch = dbPassword === passwordStr;
307
+
308
+ if (isMatch) {
309
+ console.info(`MQTT客户端 ${client.id} 身份验证成功,用户: ${username}`);
310
+ } else {
311
+ console.warn(`MQTT客户端 ${client.id} 身份验证失败:密码不匹配,用户名: ${username}`);
312
+ }
313
+
314
+ callback(null, isMatch);
315
+ }
316
+ });
253
317
  };
254
318
 
255
319
  /**
256
320
  * 验证发布,决定客户端可以发布哪些主题
257
321
  * @param {Object} client 客户端
258
- * @param {String} topic 用户名
322
+ * @param {String} topic 主题
259
323
  * @param {Object} payload 参数
260
324
  * @param {Function} callback 回调函数,回调返回true,则表示验证通过。
261
325
  */
262
326
  MQTT.prototype.authPublish = function(client, topic, payload, callback) {
263
- var bl = true;
264
- // if(topic == "temperature"){
265
- // bl = false;
266
- // }
267
- // console.log('收到推送', client.id, topic, payload.toString());
327
+ // 示例:可以基于客户端ID或用户名实现更细粒度的权限控制
328
+ // 在实际应用中,应该从数据库或配置中获取客户端的发布权限
329
+ let isAllowed = true;
330
+
331
+ // 例如:限制某些敏感主题只能由管理员发布
332
+ const sensitiveTopics = ['system/settings', 'admin/commands'];
333
+ const isSensitiveTopic = sensitiveTopics.some(sensitiveTopic =>
334
+ topic === sensitiveTopic || topic.startsWith(sensitiveTopic + '/')
335
+ );
336
+
337
+ // 简单示例:检查客户端ID是否表示管理员客户端
338
+ const isAdminClient = client.id === 'admin_client' || client.id.startsWith('admin_');
339
+
340
+ if (isSensitiveTopic && !isAdminClient) {
341
+ isAllowed = false;
342
+ console.warn(`MQTT客户端 ${client.id} 被拒绝发布到敏感主题: ${topic}`);
343
+ }
344
+
268
345
  // 回调第二个参数为true表示验证通过, 为false表示验证失败
269
- callback(null, bl);
346
+ callback(null, isAllowed);
270
347
  };
271
348
 
272
349
  /**
273
350
  * 验证订阅,决定客户端可以订阅哪些主题
274
351
  * @param {Object} client 客户端
275
- * @param {String} topic 用户名
352
+ * @param {String} topic 主题
276
353
  * @param {Function} callback 回调函数,回调返回true,则表示验证通过。
277
354
  */
278
355
  MQTT.prototype.authSubscribe = function(client, topic, callback) {
279
- var bl = true;
356
+ // 示例:可以基于客户端ID或用户名实现更细粒度的权限控制
357
+ // 在实际应用中,应该从数据库或配置中获取客户端的订阅权限
358
+ let isAllowed = true;
359
+
360
+ // 例如:限制某些敏感主题只能由特定客户端订阅
361
+ const restrictedTopics = ['private/data', 'user/+/profile'];
362
+ const isRestrictedTopic = restrictedTopics.some(restrictedTopic => {
363
+ // 处理通配符,例如:user/+/profile 匹配 user/123/profile, user/456/profile 等
364
+ const pattern = restrictedTopic.replace(/\+/g, '[^/]+');
365
+ const regex = new RegExp(`^${pattern}$`);
366
+ return regex.test(topic);
367
+ });
368
+
369
+ // 简单示例:允许特定客户端订阅受限主题
370
+ const allowedClients = ['trusted_client', 'monitoring_service'];
371
+ const isAllowedClient = allowedClients.includes(client.id);
372
+
373
+ if (isRestrictedTopic && !isAllowedClient) {
374
+ isAllowed = false;
375
+ console.warn(`MQTT客户端 ${client.id} 被拒绝订阅受限主题: ${topic}`);
376
+ }
377
+
280
378
  // 回调第二个参数为true表示验证通过, 为false表示验证失败
281
- callback(null, bl);
379
+ callback(null, isAllowed);
282
380
  };
283
381
 
284
382
  /**
@@ -50,6 +50,38 @@ WEB.prototype.init = function(config) {
50
50
  this.config = Object.assign(this.config, config);
51
51
  }
52
52
  this.server = new Koa();
53
+
54
+ // 添加统一错误处理中间件(确保是第一个被注册的中间件)
55
+ this.server.use(async (ctx, next) => {
56
+ try {
57
+ await next();
58
+ // 处理404错误
59
+ if (ctx.status === 404 && !ctx.body) {
60
+ ctx.status = 404;
61
+ ctx.body = { code: 404, message: 'Not Found' };
62
+ }
63
+ } catch (error) {
64
+ // 统一错误处理逻辑
65
+ const status = error.status || 500;
66
+ const message = error.message || 'Internal Server Error';
67
+
68
+ // 根据环境决定是否返回详细错误信息
69
+ const errorInfo = process.env.NODE_ENV === 'production'
70
+ ? { message }
71
+ : { message, stack: error.stack };
72
+
73
+ ctx.status = status;
74
+ ctx.body = { code: status, ...errorInfo };
75
+
76
+ // 记录错误日志
77
+ if ($.log && $.log.error) {
78
+ $.log.error(`请求错误 [${ctx.method} ${ctx.path}]:`, error);
79
+ } else {
80
+ console.error(`请求错误 [${ctx.method} ${ctx.path}]:`, error);
81
+ }
82
+ }
83
+ });
84
+
53
85
  return this;
54
86
  };
55
87
 
@@ -71,9 +103,23 @@ WEB.prototype.main = function(state) {
71
103
  if (host == '0.0.0.0') {
72
104
  host = '127.0.0.1'
73
105
  }
74
- this.server.listen(cg.port, cg.host, () => {
106
+
107
+ // 启动HTTP服务器
108
+ const httpServer = this.server.listen(cg.port, cg.host, () => {
75
109
  console.info(`HTTP访问 http://${host}:${cg.port}`);
76
110
  });
111
+
112
+ // 监听服务器错误事件
113
+ httpServer.on('error', (error) => {
114
+ if ($.log && $.log.error) {
115
+ $.log.error('Web服务器错误:', error);
116
+ } else {
117
+ console.error('Web服务器错误:', error);
118
+ }
119
+ });
120
+
121
+ // 保存服务器实例引用,方便后续操作
122
+ this.httpServer = httpServer;
77
123
  };
78
124
 
79
125
  /**
@@ -27,8 +27,8 @@ class MQTT extends Index {
27
27
  port: "1883",
28
28
  protocol: "mqtt",
29
29
  clientId: "iot_test",
30
- subscribe_qos: 1,
31
- publish_qos: 1,
30
+ subscribe_qos: 2,
31
+ publish_qos: 2,
32
32
  username: "iot_test",
33
33
  password: "asd123",
34
34
  table: "iot_device",
@@ -263,7 +263,7 @@ MQTT.prototype.receive = async function(push_topic, msg) {
263
263
  try {
264
264
  msg = JSON.parse(msg);
265
265
  } catch (error) {
266
- $.log.error("消息结构体不对", error);
266
+ $.log.error("消息结构体不对", push_topic, error);
267
267
  }
268
268
  }
269
269
  var ret;
package/index.js CHANGED
@@ -41,7 +41,13 @@ class OS {
41
41
  "static": true,
42
42
  "maxAge": 7200,
43
43
  "static_path": "./static",
44
- "proxy": {}
44
+ "proxy": {},
45
+ "rateLimit": {
46
+ "windowMs": 15 * 60 * 1000, // 时间窗口,默认15分钟
47
+ "maxRequests": 100, // 每个时间窗口内的最大请求数
48
+ "message": "请求过于频繁,请稍后再试", // 超过限制时的提示信息
49
+ "statusCode": 429 // 超过限制时的HTTP状态码
50
+ }
45
51
  },
46
52
  "mqtt": {
47
53
  "state": true,
@@ -158,6 +164,29 @@ OS.prototype.loadModule = function(cg) {
158
164
  }
159
165
  }
160
166
 
167
+ /**
168
+ * 初始化全局错误处理机制
169
+ */
170
+ OS.prototype.initErrorHandler = function() {
171
+ // 全局未捕获异常处理
172
+ process.on('uncaughtException', (error) => {
173
+ if ($.log && $.log.error) {
174
+ $.log.error('全局未捕获异常:', error);
175
+ } else {
176
+ console.error('全局未捕获异常:', error);
177
+ }
178
+ });
179
+
180
+ // 全局未处理Promise拒绝处理
181
+ process.on('unhandledRejection', (reason, promise) => {
182
+ if ($.log && $.log.error) {
183
+ $.log.error('全局未处理Promise拒绝:', reason);
184
+ } else {
185
+ console.error('全局未处理Promise拒绝:', reason);
186
+ }
187
+ });
188
+ };
189
+
161
190
  /**
162
191
  * 初始化
163
192
  * @param {Object} config - 配置参数
@@ -166,7 +195,19 @@ OS.prototype.init = function(config) {
166
195
  this.config = Object.assign(this.config, config);
167
196
  this.initData(this.config);
168
197
  this.loadModule(this.config);
198
+ // 提供简单的日志功能替代方案
199
+ if (!$.log) {
200
+ $.log = {
201
+ debug: console.debug,
202
+ info: console.info,
203
+ warn: console.warn,
204
+ error: console.error,
205
+ log: console.log
206
+ };
207
+ }
169
208
  this.initBase(this.config);
209
+ // 初始化全局错误处理机制
210
+ this.initErrorHandler();
170
211
  }
171
212
 
172
213
  /**
@@ -9,73 +9,91 @@ module.exports = function(server, config) {
9
9
  if (cos.headers && cos.origin !== "*") {
10
10
  /* 跨域限制(web防火墙) */
11
11
  server.use(async (ctx, next) => {
12
- var req = ctx.request;
12
+ try {
13
+ var req = ctx.request;
13
14
 
14
- // 允许来自所有域名请求
15
- ctx.set('Access-Control-Allow-Origin', cos.origin);
16
- // 这样就能只允许 http://localhost:8080 这个域名的请求了
17
- // ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");
15
+ // 允许来自所有域名请求
16
+ ctx.set('Access-Control-Allow-Origin', cos.origin);
17
+ // 这样就能只允许 http://localhost:8080 这个域名的请求了
18
+ // ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");
18
19
 
19
- // 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段.
20
- if (req.method == "OPTIONS") {
21
- ctx.set('Access-Control-Allow-Headers', cos.headers);
20
+ // 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段.
21
+ if (req.method == "OPTIONS") {
22
+ ctx.set('Access-Control-Allow-Headers', cos.headers);
22
23
 
23
- // 服务器收到请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
24
- // Content-Type表示具体请求中的媒体类型信息
25
- // ctx.set("Content-Type", "application/json;charset=utf-8");
26
- // 设置所允许的HTTP请求方法PUT,POST,GET,DELETE,HEAD,OPTIONS
27
- // ctx.set('Access-Control-Allow-Methods', 'GET,POST');
24
+ // 服务器收到请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
25
+ // Content-Type表示具体请求中的媒体类型信息
26
+ // ctx.set("Content-Type", "application/json;charset=utf-8");
27
+ // 设置所允许的HTTP请求方法PUT,POST,GET,DELETE,HEAD,OPTIONS
28
+ // ctx.set('Access-Control-Allow-Methods', 'GET,POST');
28
29
 
29
- // 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。
30
- // 当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*";
31
- // ctx.set("Access-Control-Allow-Credentials", 'false');
30
+ // 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。
31
+ // 当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*";
32
+ // ctx.set("Access-Control-Allow-Credentials", 'false');
32
33
 
33
- // 该字段可选,用来指定本次预检请求的有效期,单位为秒。
34
- // 当请求方法是PUT或DELETE等特殊方法或者Content-Type字段的类型是application/json时,服务器会提前发送一次请求进行验证
35
- // 下面的的设置只本次验证的有效时间,即在该时间段内服务端可以不用进行验证
36
- // ctx.set("Access-Control-Max-Age", '3600');
37
- /*
38
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
39
- Cache-Control、
40
- Content-Language、
41
- Content-Type、
42
- Expires、
43
- Last-Modified、
44
- Pragma
45
- */
46
- // 需要获取其他字段时,使用Access-Control-Expose-Headers,
47
- // getResponseHeader('myData')可以返回我们所需的值
48
- // ctx.set("Access-Control-Expose-Headers", "myData");
49
- ctx.status = 204;
50
- } else {
34
+ // 该字段可选,用来指定本次预检请求的有效期,单位为秒。
35
+ // 当请求方法是PUT或DELETE等特殊方法或者Content-Type字段的类型是application/json时,服务器会提前发送一次请求进行验证
36
+ // 下面的的设置只本次验证的有效时间,即在该时间段内服务端可以不用进行验证
37
+ // ctx.set("Access-Control-Max-Age", '3600');
38
+ /*
39
+ CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
40
+ Cache-Control、
41
+ Content-Language、
42
+ Content-Type、
43
+ Expires、
44
+ Last-Modified、
45
+ Pragma
46
+ */
47
+ // 需要获取其他字段时,使用Access-Control-Expose-Headers,
48
+ // getResponseHeader('myData')可以返回我们所需的值
49
+ // ctx.set("Access-Control-Expose-Headers", "myData");
50
+ ctx.status = 204;
51
+ } else {
52
+ await next();
53
+ }
54
+ } catch (error) {
55
+ $.log.error('CORS中间件错误(origin限制):', error);
56
+ // 出错时默认允许请求继续处理
51
57
  await next();
52
58
  }
53
59
  });
54
60
  } else if (cos.headers && cos.headers !== "*") {
55
61
  /* 跨域限制(web防火墙) */
56
62
  server.use(async (ctx, next) => {
57
- var req = ctx.request;
58
- // 允许来自所有域名请求
59
- ctx.set('Access-Control-Allow-Origin', '*');
60
- // ctx.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
61
- if (req.method === 'OPTIONS') {
62
- ctx.set('Access-Control-Allow-Headers', cos.headers);
63
- ctx.status = 204;
64
- } else {
63
+ try {
64
+ var req = ctx.request;
65
+ // 允许来自所有域名请求
66
+ ctx.set('Access-Control-Allow-Origin', '*');
67
+ // ctx.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
68
+ if (req.method === 'OPTIONS') {
69
+ ctx.set('Access-Control-Allow-Headers', cos.headers);
70
+ ctx.status = 204;
71
+ } else {
72
+ await next();
73
+ }
74
+ } catch (error) {
75
+ $.log.error('CORS中间件错误(headers限制):', error);
76
+ // 出错时默认允许请求继续处理
65
77
  await next();
66
78
  }
67
79
  });
68
80
  } else {
69
81
  /* 跨域限制(web防火墙) */
70
82
  server.use(async (ctx, next) => {
71
- var req = ctx.request;
72
- // 允许来自所有域名请求
73
- ctx.set('Access-Control-Allow-Origin', '*');
74
- // ctx.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
75
- if (req.method === 'OPTIONS') {
76
- ctx.set('Access-Control-Allow-Headers', '*');
77
- ctx.status = 204;
78
- } else {
83
+ try {
84
+ var req = ctx.request;
85
+ // 允许来自所有域名请求
86
+ ctx.set('Access-Control-Allow-Origin', '*');
87
+ // ctx.set('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
88
+ if (req.method === 'OPTIONS') {
89
+ ctx.set('Access-Control-Allow-Headers', '*');
90
+ ctx.status = 204;
91
+ } else {
92
+ await next();
93
+ }
94
+ } catch (error) {
95
+ $.log.error('CORS中间件错误(完全宽松):', error);
96
+ // 出错时默认允许请求继续处理
79
97
  await next();
80
98
  }
81
99
  });
@@ -6,16 +6,27 @@
6
6
  module.exports = function(server, config) {
7
7
  /* 跨域限制(web防火墙) */
8
8
  server.use(async (ctx, next) => {
9
- var url = ctx.path + ctx.querystring;
10
- var body = "";
11
- if (ctx.request.body) {
12
- body = JSON.stringify(ctx.request.body);
13
- if (body.length > 1024) {
14
- body = body.substring(0, 1024) + "..."
9
+ try {
10
+ var url = ctx.path + ctx.querystring;
11
+ var body = "";
12
+ if (ctx.request.body) {
13
+ try {
14
+ body = JSON.stringify(ctx.request.body);
15
+ if (body.length > 1024) {
16
+ body = body.substring(0, 1024) + "..."
17
+ }
18
+ } catch (jsonError) {
19
+ body = "[无法序列化请求体]";
20
+ $.log.error('日志中间件JSON序列化错误:', jsonError);
21
+ }
15
22
  }
23
+ $.log.http(`${ctx.method}\t${url}\t${body}`);
24
+ await next();
25
+ } catch (error) {
26
+ $.log.error('log中间件错误:', error);
27
+ // 确保请求可以继续处理
28
+ await next();
16
29
  }
17
- $.log.http(`${ctx.method} ${url} ${body}`);
18
- await next();
19
30
  });
20
31
  return server;
21
32
  };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * 请求性能监控中间件
3
+ * 记录请求响应时间,监控慢请求,提供性能数据收集
4
+ */
5
+ module.exports = function(server, config) {
6
+ if(config.web && !config.web.performance) {
7
+ return server;
8
+ }
9
+
10
+ // 默认配置
11
+ const cg = Object.assign({
12
+ slowThreshold: 1000, // 慢请求阈值,单位毫秒
13
+ enableMetrics: true, // 是否启用性能指标收集
14
+ ignorePaths: [] // 忽略监控的路径
15
+ }, config);
16
+
17
+ // 性能监控数据收集器
18
+ const performanceData = {
19
+ counters: new Map(), // 请求计数器
20
+ responseTimes: new Map(), // 响应时间数据
21
+ slowRequests: [] // 慢请求记录
22
+ };
23
+
24
+ // 全局性能监控对象
25
+ $.performanceMonitor = {
26
+ record: function(path, responseTime) {
27
+ if (!cg.enableMetrics) return;
28
+
29
+ // 更新请求计数
30
+ if (!performanceData.counters.has(path)) {
31
+ performanceData.counters.set(path, 1);
32
+ performanceData.responseTimes.set(path, { sum: responseTime, count: 1, min: responseTime, max: responseTime });
33
+ } else {
34
+ performanceData.counters.set(path, performanceData.counters.get(path) + 1);
35
+ const stats = performanceData.responseTimes.get(path);
36
+ stats.sum += responseTime;
37
+ stats.count += 1;
38
+ stats.min = Math.min(stats.min, responseTime);
39
+ stats.max = Math.max(stats.max, responseTime);
40
+ }
41
+
42
+ // 记录慢请求
43
+ if (responseTime > cg.slowThreshold) {
44
+ performanceData.slowRequests.push({
45
+ path: path,
46
+ time: new Date(),
47
+ responseTime: responseTime
48
+ });
49
+ // 限制慢请求记录数量
50
+ if (performanceData.slowRequests.length > 1000) {
51
+ performanceData.slowRequests.shift();
52
+ }
53
+ }
54
+ },
55
+
56
+ getStats: function() {
57
+ const result = {};
58
+ performanceData.counters.forEach((count, path) => {
59
+ const times = performanceData.responseTimes.get(path);
60
+ result[path] = {
61
+ count: count,
62
+ avg: times.sum / times.count,
63
+ min: times.min,
64
+ max: times.max
65
+ };
66
+ });
67
+ return result;
68
+ },
69
+
70
+ getSlowRequests: function(limit = 100) {
71
+ return performanceData.slowRequests.slice(-limit);
72
+ },
73
+
74
+ reset: function() {
75
+ performanceData.counters.clear();
76
+ performanceData.responseTimes.clear();
77
+ performanceData.slowRequests = [];
78
+ }
79
+ };
80
+
81
+ // 性能监控中间件
82
+ server.use(async (ctx, next) => {
83
+ // 检查是否需要忽略此路径
84
+ const shouldIgnore = cg.ignorePaths.some(pattern => {
85
+ if (typeof pattern === 'string') {
86
+ return ctx.path === pattern;
87
+ } else if (pattern instanceof RegExp) {
88
+ return pattern.test(ctx.path);
89
+ }
90
+ return false;
91
+ });
92
+
93
+ if (shouldIgnore) {
94
+ await next();
95
+ return;
96
+ }
97
+
98
+ const start = Date.now();
99
+ const startTime = process.hrtime();
100
+
101
+ try {
102
+ await next();
103
+ } finally {
104
+ const ms = Date.now() - start;
105
+ const [seconds, nanoseconds] = process.hrtime(startTime);
106
+ const executionTime = seconds * 1000 + nanoseconds / 1000000;
107
+
108
+ // 设置响应时间头
109
+ ctx.set('X-Response-Time', `${ms}ms`);
110
+
111
+ // 记录性能数据
112
+ $.performanceMonitor.record(ctx.path, ms);
113
+
114
+ // 慢请求报警
115
+ if (ms > cg.slowThreshold) {
116
+ const clientIP = ctx.headers['x-forwarded-for'] || ctx.ip;
117
+ $.log.warn(`【慢请求】 ${ctx.method} ${ctx.path} - ${ms}ms - ${clientIP}`);
118
+
119
+ // 如果是非常慢的请求(超过阈值的2倍),记录更多信息
120
+ if (ms > cg.slowThreshold * 2) {
121
+ const userAgent = ctx.headers['user-agent'] || 'unknown';
122
+ let requestData = '';
123
+ try {
124
+ if (ctx.method !== 'GET' && ctx.request.body) {
125
+ const bodyStr = JSON.stringify(ctx.request.body);
126
+ // 限制记录的请求体大小
127
+ requestData = bodyStr.length > 1024 ? bodyStr.substring(0, 1024) + '...' : bodyStr;
128
+ }
129
+ } catch (e) {
130
+ requestData = '[无法序列化请求体]';
131
+ }
132
+
133
+ $.log.warn(`【慢请求详情】路径: ${ctx.path}, 方法: ${ctx.method}, 耗时: ${ms}ms, IP: ${clientIP}, UA: ${userAgent.substring(0, 200)}`);
134
+ if (requestData) {
135
+ $.log.warn(`【慢请求数据】${requestData}`);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ });
141
+
142
+ return server;
143
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "performance",
3
+ "title": "性能监控",
4
+ "description": "记录请求响应时间,监控慢请求,提供性能数据收集",
5
+ "mode": "web",
6
+ "priority": 10,
7
+ "config": {
8
+ "slowThreshold": 1000,
9
+ "enableMetrics": true,
10
+ "ignorePaths": [
11
+ "/favicon.ico",
12
+ "/robots.txt",
13
+ "/static/"
14
+ ]
15
+ }
16
+ }