chanjs 2.7.2 → 2.7.4

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.
Files changed (67) hide show
  1. package/App.js +232 -16
  2. package/base/Aop.js +20 -3
  3. package/base/Container.js +80 -3
  4. package/base/Controller.js +38 -9
  5. package/base/Database.js +50 -0
  6. package/base/Event.js +12 -0
  7. package/base/{Service.js → Repository.js} +644 -539
  8. package/common/api.js +18 -8
  9. package/common/code.js +25 -15
  10. package/common/email.js +98 -17
  11. package/common/index.js +1 -1
  12. package/config/code.js +138 -82
  13. package/global/index.js +1 -1
  14. package/helper/index.js +43 -41
  15. package/index.js +19 -6
  16. package/loader/index.js +6 -0
  17. package/{helper → loader}/loader.js +41 -27
  18. package/middleware/compress.js +185 -0
  19. package/middleware/cors.js +36 -24
  20. package/middleware/header.js +5 -10
  21. package/middleware/index.js +1 -0
  22. package/middleware/log.js +27 -3
  23. package/middleware/setBody.js +9 -1
  24. package/middleware/static.js +2 -1
  25. package/middleware/template.js +139 -4
  26. package/middleware/waf.js +136 -76
  27. package/package.json +4 -4
  28. package/realtime/index.js +7 -0
  29. package/realtime/sse.js +424 -0
  30. package/realtime/websocket.js +540 -0
  31. package/response/index.js +12 -0
  32. package/response/response.js +258 -0
  33. package/schedule/index.js +6 -0
  34. package/schedule/schedule.js +491 -0
  35. package/{helper → security}/checker.js +23 -8
  36. package/security/index.js +14 -0
  37. package/{helper → security}/jwt.js +175 -107
  38. package/security/keywords.js +179 -0
  39. package/security/rate-limit.js +105 -0
  40. package/security/sign.js +210 -0
  41. package/security/xss-filter.js +63 -0
  42. package/storage/cache.js +258 -0
  43. package/storage/index.js +9 -0
  44. package/storage/redis.js +258 -0
  45. package/storage/store.js +266 -0
  46. package/{helper → utils}/file.js +106 -15
  47. package/{helper → utils}/filter.js +2 -1
  48. package/{helper → utils}/html.js +19 -1
  49. package/utils/index.js +34 -0
  50. package/{helper → utils}/ip.js +25 -16
  51. package/utils/request.js +172 -0
  52. package/{helper → utils}/time.js +1 -1
  53. package/utils/tree.js +121 -0
  54. package/common/category.js +0 -22
  55. package/common/sms.js +0 -104
  56. package/extend/art-template.js +0 -129
  57. package/extend/index.js +0 -6
  58. package/global/global.js +0 -63
  59. package/helper/cache.js +0 -187
  60. package/helper/keywords.js +0 -132
  61. package/helper/rate-limit.js +0 -116
  62. package/helper/request.js +0 -47
  63. package/helper/response.js +0 -180
  64. package/helper/sign.js +0 -96
  65. package/helper/tree.js +0 -77
  66. package/helper/xss-filter.js +0 -42
  67. /package/{helper → utils}/data-parse.js +0 -0
package/App.js CHANGED
@@ -7,9 +7,13 @@ import DatabaseManager from "./base/Database.js";
7
7
 
8
8
  // 引入配置和工具
9
9
  import { Paths } from "./config/index.js";
10
- import { loadConfig, loaderSort, loadController } from "./helper/index.js";
11
- import { Cors, setBody, setCookie, setFavicon, setHeader, setStatic, setTemplate, waf, log } from "./middleware/index.js";
12
- import { notFoundResponse, parseDatabaseError, errorResponse } from "./helper/index.js";
10
+ import { loadConfig, loaderSort, loadController } from "./loader/index.js";
11
+ import { Cors, setBody, setCookie, setFavicon, setHeader, setStatic, setTemplate, waf, compress, log } from "./middleware/index.js";
12
+ import { notFoundResponse, parseDatabaseError, errorResponse } from "./response/index.js";
13
+ import { store } from "./storage/index.js";
14
+ import { schedule } from "./schedule/schedule.js";
15
+ import { sse } from "./realtime/sse.js";
16
+ import { websocket } from "./realtime/websocket.js";
13
17
  import { importFile } from "./global/import.js";
14
18
 
15
19
  import "./global/index.js";
@@ -47,6 +51,10 @@ class Chan {
47
51
  async start() {
48
52
  //加载配置
49
53
  await this.config();
54
+ //暴露 Paths 到 Chan 静态属性(DI:业务侧通过 Chan.paths 或 this.paths 访问)
55
+ Chan.paths = Paths;
56
+ //初始化 Redis 适配器(WAF 封禁 / Rate Limit 共享存储)
57
+ await this.loadRedis();
50
58
  //加载数据库
51
59
  await this.loadDB();
52
60
  //加载扩展方法
@@ -59,8 +67,140 @@ class Chan {
59
67
  await this.loadRouter();
60
68
  await this.loadCommonRouter();
61
69
  this.useRouter();
62
- //404 500 处理
70
+ //404 500 处理(放在最后,确保所有路由都已注册)
63
71
  this.setErrorHandler();
72
+ //执行 beforeStart 钩子(用户自定义的初始化逻辑)
73
+ await this._runBeforeStart();
74
+ }
75
+
76
+ /**
77
+ * 注册 beforeStart 钩子
78
+ * @param {Function} fn - 钩子函数(支持 async)
79
+ * @description 在 start() 流程末尾、run() 之前执行
80
+ * 适合做:缓存预热、定时任务注册、健康检查初始化等
81
+ * @example
82
+ * chan.beforeStart(async () => {
83
+ * schedule.every('clean-log', 60000, cleanLogs);
84
+ * });
85
+ */
86
+ beforeStart(fn) {
87
+ if (typeof fn !== 'function') {
88
+ throw new Error('beforeStart 参数必须是函数');
89
+ }
90
+ if (!this._beforeStartHooks) this._beforeStartHooks = [];
91
+ this._beforeStartHooks.push(fn);
92
+ }
93
+
94
+ /**
95
+ * 执行 beforeStart 钩子
96
+ * @private
97
+ */
98
+ async _runBeforeStart() {
99
+ if (!this._beforeStartHooks || !this._beforeStartHooks.length) return;
100
+ for (const fn of this._beforeStartHooks) {
101
+ try {
102
+ await fn();
103
+ } catch (err) {
104
+ console.error('[Chan] beforeStart 钩子执行失败:', err.message);
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 优雅停机
111
+ * @async
112
+ * @description 关闭 HTTP 服务、定时任务、数据库连接、Redis
113
+ * 监听 SIGTERM / SIGINT 信号自动触发
114
+ * @returns {Promise<void>}
115
+ */
116
+ async shutdown() {
117
+ if (this._shuttingDown) return;
118
+ this._shuttingDown = true;
119
+ console.log('[Chan] 开始优雅停机...');
120
+
121
+ // 5 秒强制退出兜底(原 30s 过长,连接已主动销毁,5s 足够收尾)
122
+ const forceExitTimer = setTimeout(() => {
123
+ console.error('[Chan] 优雅停机超时(5s),强制退出');
124
+ process.exit(1);
125
+ }, 5000);
126
+ forceExitTimer.unref?.();
127
+
128
+ try {
129
+ // 1. 停止接收新请求 + 主动销毁所有 keep-alive 连接
130
+ // 说明:server.close() 只停止 listen,会等所有现有连接关闭才回调,
131
+ // 浏览器默认 keep-alive 会导致 30s 超时,所以必须主动 destroy。
132
+ if (this.server) {
133
+ if (this._sockets && this._sockets.size > 0) {
134
+ for (const socket of this._sockets) {
135
+ try { socket.destroy(); } catch (e) { /* 忽略已关闭的 socket */ }
136
+ }
137
+ this._sockets.clear();
138
+ }
139
+ await new Promise(resolve => this.server.close(resolve));
140
+ console.log('[Chan] HTTP 服务已关闭');
141
+ }
142
+
143
+ // 2. 关闭实时通信(SSE / WebSocket,先于定时任务关闭,避免推送失败)
144
+ try {
145
+ sse.shutdown();
146
+ } catch (e) {
147
+ console.error('[Chan] 关闭 SSE 失败:', e.message);
148
+ }
149
+ try {
150
+ websocket.shutdown();
151
+ } catch (e) {
152
+ console.error('[Chan] 关闭 WebSocket 失败:', e.message);
153
+ }
154
+
155
+ // 3. 停止所有定时任务
156
+ try {
157
+ schedule.shutdown();
158
+ } catch (e) {
159
+ console.error('[Chan] 停止定时任务失败:', e.message);
160
+ }
161
+
162
+ // 4. 关闭数据库连接
163
+ try {
164
+ await this.dbManager.closeAll();
165
+ } catch (e) {
166
+ console.error('[Chan] 关闭数据库失败:', e.message);
167
+ }
168
+
169
+ // 5. 关闭 Redis / 存储适配层
170
+ try {
171
+ await store.close();
172
+ } catch (e) {
173
+ console.error('[Chan] 关闭存储适配层失败:', e.message);
174
+ }
175
+
176
+ console.log('[Chan] 优雅停机完成');
177
+ } finally {
178
+ clearTimeout(forceExitTimer);
179
+ process.exit(0);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 注册进程信号监听器(自动触发优雅停机)
185
+ * @private
186
+ */
187
+ _registerSignalHandlers() {
188
+ if (this._signalRegistered) return;
189
+ this._signalRegistered = true;
190
+
191
+ const handler = async (signal) => {
192
+ // 幂等守卫:首次信号触发停机,后续信号直接忽略,避免日志噪音
193
+ if (this._shuttingDown) {
194
+ // 第二次 ctrl+c:直接强制退出,让用户能快速中断
195
+ console.error(`[Chan] 再次收到 ${signal},强制退出`);
196
+ process.exit(1);
197
+ }
198
+ console.log(`[Chan] 收到 ${signal} 信号`);
199
+ await this.shutdown();
200
+ };
201
+
202
+ process.on('SIGTERM', () => handler('SIGTERM'));
203
+ process.on('SIGINT', () => handler('SIGINT'));
64
204
  }
65
205
 
66
206
  /**
@@ -74,6 +214,27 @@ class Chan {
74
214
  Chan.config = config;
75
215
  }
76
216
 
217
+ /**
218
+ * 初始化存储适配层
219
+ * @async
220
+ * @description
221
+ * 根据 config.REDIS_ENABLED 决定使用 Redis 还是内存模式
222
+ * - Redis 启用且连接成功 → 使用 Redis 后端
223
+ * - Redis 未启用或连接失败 → 降级到内存模式(Map+TTL,上限 10 万条)
224
+ * WAF 封禁状态、Rate Limit 计数都依赖此适配层
225
+ */
226
+ async loadRedis() {
227
+ const config = Chan.config || {};
228
+ try {
229
+ await store.init({
230
+ REDIS_ENABLED: config.REDIS_ENABLED,
231
+ REDIS: config.REDIS,
232
+ });
233
+ } catch (err) {
234
+ console.warn(`[Chan] 存储适配层初始化失败,使用内存模式: ${err.message}`);
235
+ }
236
+ }
237
+
77
238
  /**
78
239
  * 加载数据库
79
240
  * @async
@@ -85,12 +246,8 @@ class Chan {
85
246
  for (const [index, item] of dbList.entries()) {
86
247
  const key = item.key || String(index);
87
248
  try {
88
- const dbConfig = item;
89
- if (!dbConfig) {
90
- console.error(`[DB] 数据库 ${key} 未找到数据库配置`);
91
- return;
92
- };
93
- const connection = this.dbManager.add(key, dbConfig, { isDefault: index === 0 });
249
+ // 已通过 dbList.entries() 拿到 item,无需再做 if (!dbConfig) return 死代码判断
250
+ const connection = this.dbManager.add(key, item, { isDefault: index === 0 });
94
251
  if (index === 0) {
95
252
  Chan.db = this.dbManager.get();
96
253
  }
@@ -172,6 +329,9 @@ class Chan {
172
329
  await waf(this.app, wafConfig);
173
330
  setFavicon(this.app);
174
331
  setStatic(this.app, statics);
332
+ // 响应压缩中间件:放在 setStatic 之后(静态资源已由 express.static 处理流式响应)、Cors 之前
333
+ // 跳过 SSE / WebSocket 升级请求,仅对可压缩文本类型且 > 1KB 的响应体压缩
334
+ this.app.use(compress());
175
335
  setCookie(this.app, cookieKey);
176
336
  setBody(this.app, BODY_LIMIT);
177
337
  Cors(this.app, cors);
@@ -184,9 +344,20 @@ class Chan {
184
344
  * 应用配置
185
345
  * @private
186
346
  * @description 设置 Express 应用配置
347
+ * trust proxy 根据 config.PROXY 动态设置,避免默认 true 导致 X-Forwarded-For 伪造
187
348
  */
188
349
  setApp() {
189
- this.app.set("trust proxy", true);
350
+ const proxyConfig = Chan.config?.PROXY;
351
+ if (proxyConfig === 'true' || proxyConfig === true) {
352
+ // 信任所有代理(仅在反代可信时使用)
353
+ this.app.set("trust proxy", true);
354
+ } else if (proxyConfig === 'loopback' || !proxyConfig) {
355
+ // 默认仅信任本机回环代理
356
+ this.app.set("trust proxy", "loopback");
357
+ } else {
358
+ // 其他值按字面传入(IP / CIDR / 跳数)
359
+ this.app.set("trust proxy", proxyConfig);
360
+ }
190
361
  }
191
362
 
192
363
  /**
@@ -204,8 +375,8 @@ class Chan {
204
375
  * @description 配置全局错误处理中间件
205
376
  */
206
377
  setErrorHandler() {
207
- //404
208
- this.app.use((req, res) => {
378
+ //404(标准 Express 中间件签名,保留 next 参数)
379
+ this.app.use((req, res, next) => {
209
380
  res.status(404).json(notFoundResponse(req));
210
381
  });
211
382
 
@@ -302,16 +473,61 @@ class Chan {
302
473
  /**
303
474
  * 准备启动
304
475
  * @param {Function} [cb] - 回调函数
305
- * @description 在启动前执行回调,传递端口号
476
+ * @description 启动 HTTP 服务,并按配置挂载 WebSocket(SSE 由业务侧通过 sse.middleware() 注册路由)
306
477
  */
307
478
  run(cb) {
308
- this.app.listen(Chan.config.PORT, () => {
479
+ this.server = this.app.listen(Chan.config.PORT, () => {
309
480
  console.log(`Server running on port ${Chan.config.PORT}`);
310
481
  cb?.(Chan.config.PORT);
311
482
  });
483
+
484
+ // 跟踪所有 HTTP socket,用于优雅停机时主动销毁(避免 keep-alive 卡住 server.close)
485
+ this._sockets = new Set();
486
+ this.server.on('connection', (socket) => {
487
+ this._sockets.add(socket);
488
+ socket.on('close', () => this._sockets.delete(socket));
489
+ });
490
+
491
+ // 按配置挂载 WebSocket(config.WEBSOCKET.enabled = true 时启用)
492
+ this._attachWebSocket();
493
+ // 注册 SIGTERM / SIGINT 信号处理,支持优雅停机
494
+ this._registerSignalHandlers();
495
+ }
496
+
497
+ /**
498
+ * 挂载 WebSocket 服务到 HTTP server
499
+ * @private
500
+ * @description 根据 config.WEBSOCKET 配置自动挂载,业务侧通过 Chan.websocket 使用
501
+ * @example
502
+ * // config.js
503
+ * module.exports = {
504
+ * WEBSOCKET: { enabled: true, path: '/ws', heartbeatInterval: 30000 }
505
+ * };
506
+ * // 业务侧
507
+ * import { websocket } from 'chanjs';
508
+ * websocket.on('chat', (socket, payload) => { ... });
509
+ */
510
+ _attachWebSocket() {
511
+ const wsConfig = Chan.config?.WEBSOCKET || {};
512
+ if (!wsConfig.enabled) return;
513
+
514
+ try {
515
+ websocket.attach(this.server, {
516
+ path: wsConfig.path || '/ws',
517
+ heartbeatInterval: wsConfig.heartbeatInterval,
518
+ verifyClient: wsConfig.verifyClient,
519
+ onConnection: wsConfig.onConnection,
520
+ });
521
+ // 暴露到 Chan 静态属性,便于业务侧通过 import Chan 访问
522
+ Chan.websocket = websocket;
523
+ } catch (err) {
524
+ console.error('[Chan] WebSocket 挂载失败:', err.message);
525
+ }
312
526
  }
313
527
  }
314
528
 
315
- global.Chan = Chan;
529
+ // 不再使用 global.Chan 全局污染
530
+ // 业务侧通过 `import Chan from 'chanjs'` 获取同一个类引用(ESM 模块缓存保证单例)
531
+ // 框架内部直接使用类名 Chan(同一模块作用域)
316
532
 
317
533
  export default Chan;
package/base/Aop.js CHANGED
@@ -9,6 +9,8 @@ export class Aop {
9
9
  static TYPES = new Set(['type', 'enabled']);
10
10
  // 支持的切面类型(仅保留常用的3种)
11
11
  static SUPPORTED_TYPES = new Set(['before', 'after', 'error']);
12
+ // 幂等标记 Symbol,避免 wrap 多次嵌套包装
13
+ static WRAP_FLAG = Symbol('aop.wrapped');
12
14
 
13
15
  constructor() {
14
16
  this.aspects = new Map();
@@ -66,6 +68,9 @@ export class Aop {
66
68
  * @param {object} config - 切面配置 { methodName: [{ type: 'before', enabled: true, log: true }] }
67
69
  * @returns {object} - 绑定后的实例对象
68
70
  * @throws {TypeError} - 入参类型错误时抛出
71
+ * @description
72
+ * 幂等性:用 Symbol 标记已包装的方法,避免多次调用 wrap 导致嵌套
73
+ * 同一方法第二次 wrap 会跳过,防止切面函数被重复执行
69
74
  */
70
75
  wrap(instance, config) {
71
76
  if (typeof instance !== 'object' || instance === null) {
@@ -82,6 +87,12 @@ export class Aop {
82
87
  return;
83
88
  }
84
89
 
90
+ // 幂等检查:已包装过的方法直接跳过,避免多次 wrap 嵌套
91
+ if (originalMethod[Aop.WRAP_FLAG]) {
92
+ console.warn(`[AOP警告] 方法 ${methodName} 已包装过切面,跳过重复绑定`);
93
+ return;
94
+ }
95
+
85
96
  // 统一规则为数组 + 过滤禁用规则 + 过滤不支持的类型
86
97
  const ruleList = Array.isArray(rules) ? rules : [rules];
87
98
  const validRules = ruleList.filter(rule => {
@@ -92,17 +103,17 @@ export class Aop {
92
103
  if (validRules.length === 0) return;
93
104
 
94
105
  // 包装原方法(仅保留 before/after/error 逻辑)
95
- instance[methodName] = async (...args) => {
106
+ const wrapped = async (...args) => {
96
107
  const ctx = instance;
97
108
  let result;
98
109
 
99
110
  try {
100
111
  // 执行前置切面
101
112
  await this._executeAspectByType(ctx, validRules, 'before', args, originalMethod);
102
-
113
+
103
114
  // 执行原方法
104
115
  result = await Reflect.apply(originalMethod, ctx, args);
105
-
116
+
106
117
  // 执行后置切面(仅无异常时执行)
107
118
  await this._executeAspectByType(ctx, validRules, 'after', args, originalMethod, result);
108
119
  } catch (error) {
@@ -113,6 +124,12 @@ export class Aop {
113
124
 
114
125
  return result;
115
126
  };
127
+
128
+ // 打幂等标记,防止重复 wrap
129
+ wrapped[Aop.WRAP_FLAG] = true;
130
+ // 保留原方法名,便于调试
131
+ Object.defineProperty(wrapped, 'name', { value: originalMethod.name, configurable: true });
132
+ instance[methodName] = wrapped;
116
133
  });
117
134
 
118
135
  return instance;
package/base/Container.js CHANGED
@@ -1,7 +1,31 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { Paths } from '../config/paths.js';
4
+ import Chan from '../App.js';
4
5
 
6
+ /**
7
+ * 容器基类(依赖注入容器)
8
+ *
9
+ * ============================================================
10
+ * 设计说明
11
+ * ============================================================
12
+ * 不再使用 global.Chan 全局污染。
13
+ * 通过 import Chan 获取应用类引用,所有资源(config/db/paths 等)
14
+ * 都通过 Chan 类的静态属性动态访问,避免构造时快照导致的不同步问题。
15
+ *
16
+ * 子类(Controller/Repository)可通过以下方式获取应用资源:
17
+ * - this.app → Chan 类本身(静态属性容器)
18
+ * - this.app.config → 应用配置
19
+ * - this.app.db → 默认数据库连接
20
+ * - this.app.dbManager → 数据库连接管理器
21
+ * - this.app.paths → 路径配置(兼容 Chan.paths)
22
+ *
23
+ * ============================================================
24
+ * DI 方法
25
+ * ============================================================
26
+ * - get(moduleName, fileName) → 加载业务模块组件(service/controller)
27
+ * - getResource(name) → 获取应用级资源(config/db/dbManager/paths)
28
+ */
5
29
  export class Container {
6
30
  constructor(type = 'service') {
7
31
  this.map = new Map();
@@ -9,8 +33,41 @@ export class Container {
9
33
  this.type = type;
10
34
  // modules 目录路径
11
35
  this.baseDir = Paths.modulesPath;
12
- // 只添加 config 属性,全局默认的配置
13
- this.config = Chan.config;
36
+ // 应用类引用(DI 核心:通过它访问 config/db 等静态资源)
37
+ this.app = Chan;
38
+ }
39
+
40
+ /**
41
+ * 获取应用级资源(显式 DI)
42
+ * @param {string} name - 资源名:'config' | 'db' | 'dbManager' | 'paths'
43
+ * @returns {*} 对应的资源
44
+ * @example
45
+ * const config = this.getResource('config');
46
+ * const db = this.getResource('db');
47
+ */
48
+ getResource(name) {
49
+ return Chan[name];
50
+ }
51
+
52
+ /**
53
+ * config 快捷访问(getter 动态读取,避免快照不同步)
54
+ */
55
+ get config() {
56
+ return Chan.config;
57
+ }
58
+
59
+ /**
60
+ * db 快捷访问(getter 动态读取)
61
+ */
62
+ get db() {
63
+ return Chan.db;
64
+ }
65
+
66
+ /**
67
+ * paths 快捷访问(getter 动态读取)
68
+ */
69
+ get paths() {
70
+ return Chan.paths || Paths;
14
71
  }
15
72
 
16
73
  /**
@@ -58,8 +115,19 @@ export class Container {
58
115
  * @param {string} moduleName - 模块名 ,例如:'web','api'
59
116
  * @param {string} fileName - 文件名
60
117
  * @returns {Promise<any>} - 组件实例
118
+ * @description
119
+ * 安全改进:
120
+ * - 名称白名单校验(仅允许字母开头、字母数字下划线/横线),拒绝路径注入
121
+ * - 解析后路径必须仍在 baseDir 内,防止路径越界
122
+ * - 用 fs.promises.access 替代 existsSync,避免同步 IO 阻塞事件循环
61
123
  */
62
124
  async loadFile(moduleName, fileName) {
125
+ // 模块/文件名白名单:字母开头,允许字母数字下划线横线
126
+ const NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
127
+ if (!NAME_REGEX.test(moduleName) || !NAME_REGEX.test(fileName)) {
128
+ throw new Error(`非法模块/文件名: ${moduleName}/${fileName}`);
129
+ }
130
+
63
131
  const filePath = path.resolve(
64
132
  this.baseDir,
65
133
  moduleName,
@@ -67,7 +135,16 @@ export class Container {
67
135
  `${fileName}.js`
68
136
  );
69
137
 
70
- if (!fs.existsSync(filePath)) {
138
+ // 路径越界检查:解析后必须仍在 baseDir 内
139
+ const rel = path.relative(this.baseDir, filePath);
140
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
141
+ throw new Error(`路径越界: ${filePath}`);
142
+ }
143
+
144
+ // 异步检查文件存在性,避免同步 IO
145
+ try {
146
+ await fs.promises.access(filePath, fs.constants.R_OK);
147
+ } catch (e) {
71
148
  console.log(`模块 ${moduleName} 下 ${this.type} 目录中未找到文件:${fileName}.js`);
72
149
  return null;
73
150
  }
@@ -1,9 +1,23 @@
1
- import { success, fail } from "../helper/response.js";
1
+ import { success, fail } from "../response/response.js";
2
2
  import Container from "./Container.js";
3
3
 
4
4
  /**
5
5
  * 控制器基类
6
6
  * 提供统一的响应格式和常用方法
7
+ *
8
+ * ============================================================
9
+ * 响应格式(三层 code 体系)
10
+ * ============================================================
11
+ * 所有响应都遵循:{ success, code, msg, data }
12
+ * - code: 0 成功 / 1xxx 业务 / 5xxx 系统 / 6xxx 数据库
13
+ * - HTTP Status 由网关/Express 控制,与业务 code 解耦
14
+ *
15
+ * 使用示例:
16
+ * this.success(data) // → code: 0
17
+ * this.success({ data, msg: '创建成功' }) // → code: 0
18
+ * this.fail({ msg: '栏目下存在文章' }) // → code: 1008 业务失败
19
+ * this.fail({ msg: '参数缺失', code: 1007 }) // → code: 1007
20
+ * this.fail('密码错误') // 字符串简写,→ code: 1008
7
21
  */
8
22
  export default class Controller extends Container {
9
23
  constructor() {
@@ -15,8 +29,8 @@ export default class Controller extends Container {
15
29
  * 返回成功响应
16
30
  * @param {Object} options - 响应选项
17
31
  * @param {*} options.data - 响应数据
18
- * @param {string} options.msg - 响应消息,默认"操作成功"
19
- * @returns {Object} 标准成功响应
32
+ * @param {string} [options.msg="操作成功"] - 响应消息
33
+ * @returns {Object} 标准成功响应,code = 0
20
34
  */
21
35
  success({ data, msg = "操作成功" } = {}) {
22
36
  return success({ data, msg });
@@ -24,13 +38,28 @@ export default class Controller extends Container {
24
38
 
25
39
  /**
26
40
  * 返回失败响应
27
- * @param {Object} options - 响应选项
28
- * @param {string} options.msg - 失败消息,默认"操作失败"
29
- * @param {*} options.data - 响应数据
30
- * @param {number} options.code - 错误码,默认201
41
+ * @param {Object|string} options - 响应选项,或失败消息字符串(简写)
42
+ * @param {string} [options.msg="操作失败"] - 失败消息
43
+ * @param {*} [options.data={}] - 响应数据
44
+ * @param {number} [options.code=1008] - 业务 code,默认 1008(业务处理失败)
31
45
  * @returns {Object} 标准失败响应
46
+ * @description
47
+ * 兼容两种调用方式:
48
+ * - 对象:this.fail({ msg: '...', code: 1007 })
49
+ * - 字符串简写:this.fail('密码错误') // 等价于 this.fail({ msg: '密码错误' })
50
+ *
51
+ * 默认 code = 1008(业务处理失败)
52
+ * 业务方可指定 code:
53
+ * - 1001 认证失败 / 1002 token 过期 / 1003 权限不足
54
+ * - 1004 资源不存在 / 1005 资源已存在
55
+ * - 1006 参数无效 / 1007 参数缺失
56
+ * - 1008 业务处理失败(通用兜底)
32
57
  */
33
- fail({ msg = "操作失败", data = {}, code = 201 } = {}) {
34
- return fail({ msg, data, code });
58
+ fail(options = {}) {
59
+ // 字符串简写:this.fail('错误消息') → this.fail({ msg: '错误消息' })
60
+ if (typeof options === 'string') {
61
+ return fail({ msg: options });
62
+ }
63
+ return fail(options);
35
64
  }
36
65
  }
package/base/Database.js CHANGED
@@ -78,6 +78,56 @@ class DatabaseManager {
78
78
  }
79
79
  }
80
80
 
81
+ /**
82
+ * 关闭所有数据库连接(优雅停机使用)
83
+ * 单个连接关闭失败不影响其他连接
84
+ * @returns {Promise<Array<{name:string, success:boolean, error?:string}>>} 每个连接的关闭结果
85
+ */
86
+ async closeAll() {
87
+ const results = [];
88
+ for (const [name, conn] of this._connections) {
89
+ try {
90
+ await conn.destroy();
91
+ results.push({ name, success: true });
92
+ } catch (err) {
93
+ results.push({ name, success: false, error: err.message });
94
+ console.error(`[Database] 关闭连接 ${name} 失败:`, err.message);
95
+ }
96
+ }
97
+ this._connections.clear();
98
+ console.log(`[Database] 已关闭所有连接(共 ${results.length} 个)`);
99
+ return results;
100
+ }
101
+
102
+ /**
103
+ * 心跳检查(单个连接)
104
+ * @param {string} [name] - 连接名称,默认检查默认连接
105
+ * @returns {Promise<boolean>} 是否健康
106
+ */
107
+ async ping(name = this._defaultName) {
108
+ const conn = this._connections.get(name);
109
+ if (!conn) return false;
110
+ try {
111
+ // 用最简 SELECT 1 触发一次实际查询,验证连接可用
112
+ await conn.raw('SELECT 1');
113
+ return true;
114
+ } catch (err) {
115
+ console.error(`[Database] ping ${name} 失败:`, err.message);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 获取所有连接状态(健康检查 / 监控使用)
122
+ * @returns {Array<{name:string, isDefault:boolean}>} 连接列表
123
+ */
124
+ getStatus() {
125
+ return Array.from(this._connections.keys()).map(name => ({
126
+ name,
127
+ isDefault: name === this._defaultName,
128
+ }));
129
+ }
130
+
81
131
  }
82
132
 
83
133
  export default DatabaseManager;
package/base/Event.js CHANGED
@@ -1,8 +1,20 @@
1
1
  import { EventEmitter } from 'events';
2
2
 
3
+ /**
4
+ * 默认最大监听器数量上限,防止业务侧泄漏过多监听器导致内存警告
5
+ */
6
+ const DEFAULT_MAX_LISTENERS = 50;
7
+
3
8
  export class Event extends EventEmitter {
4
9
  constructor() {
5
10
  super();
11
+ // 设置最大监听器数量,避免业务侧泄漏导致 process 警告
12
+ this.setMaxListeners(DEFAULT_MAX_LISTENERS);
13
+ // 兜底 error 监听器:未注册 error 监听时 EventEmitter 默认会抛出并崩溃进程
14
+ // 这里注册一个仅打印日志的 error 处理器,确保 unhandled error 不会让进程退出
15
+ this.on('error', (err) => {
16
+ console.error('[Event] 未处理的 error 事件:', err?.message || err);
17
+ });
6
18
  }
7
19
 
8
20
  /**