chanjs 2.7.8 → 2.7.11

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 (41) hide show
  1. package/README.md +261 -363
  2. package/config/index.js +4 -2
  3. package/core/App.js +35 -0
  4. package/core/Container.js +56 -29
  5. package/core/Database.js +58 -8
  6. package/core/EventBus.js +88 -0
  7. package/core/Lang.js +56 -0
  8. package/core/Repository.js +34 -2
  9. package/core/Task.js +87 -0
  10. package/core/errors.js +0 -5
  11. package/doc/00-README.md +208 -0
  12. package/doc/01-/346/240/270/345/277/203/347/261/273Controller-Service-Repository.md +432 -0
  13. package/doc/02-/345/223/215/345/272/224/344/270/216/351/224/231/350/257/257.md +255 -0
  14. package/doc/03-/345/256/211/345/205/250/346/250/241/345/235/227.md +264 -0
  15. package/doc/04-/345/255/230/345/202/250/344/270/216/347/274/223/345/255/230.md +157 -0
  16. package/doc/05-/345/267/245/345/205/267/344/270/216/346/240/241/351/252/214.md +309 -0
  17. package/doc/06-/345/272/224/347/224/250/347/224/237/345/221/275/345/221/250/346/234/237.md +207 -0
  18. package/doc/07-/344/272/213/344/273/266/347/263/273/347/273/237EventBus.md +324 -0
  19. package/doc/08-/345/256/232/346/227/266/344/273/273/345/212/241Task.md +262 -0
  20. package/doc/09-/345/233/275/351/231/205/345/214/226Lang.md +220 -0
  21. package/index.js +30 -1
  22. package/middleware/log.js +48 -31
  23. package/middleware/waf.js +22 -90
  24. package/package.json +20 -2
  25. package/response/code.js +0 -12
  26. package/response/response.js +8 -2
  27. package/security/checker.js +14 -7
  28. package/security/keywords.js +2 -3
  29. package/utils/logger.js +60 -91
  30. package/utils/pages.js +13 -12
  31. package/utils/signal.js +21 -2
  32. package/USAGE.md +0 -533
  33. package/doc/Cache.md +0 -333
  34. package/doc/Common.md +0 -638
  35. package/doc/Controller.md +0 -223
  36. package/doc/Help.md +0 -390
  37. package/doc/QuickStart.md +0 -116
  38. package/doc/Repository.md +0 -560
  39. package/doc/Service.md +0 -240
  40. package/publish.bat +0 -4
  41. package/todo.md +0 -1
@@ -0,0 +1,324 @@
1
+ # 事件系统 EventBus
2
+
3
+ > 基于 Node 内置 EventEmitter 的轻量事件总线,零第三方依赖。
4
+
5
+ ---
6
+
7
+ ## 一、设计理念
8
+
9
+ ChanJS 事件总线提供**进程内异步解耦**能力:模块间通过事件名通信,不直接持有对方引用。
10
+
11
+ **核心决策**:
12
+
13
+ | 决策 | 说明 |
14
+ |------|------|
15
+ | 全局单例 | 导出共享实例 `event`,任何模块 `import { event } from "chanjs"` 即可用 |
16
+ | 同时导出类 | `EventBus` 类供需要隔离的场景 `new EventBus()` 创建独立实例 |
17
+ | App 实例引用 | `this.app.event` 指向全局实例,与 `import { event }` 等价 |
18
+ | 优雅停机 | `event.destroy()` 在进程退出时移除全部监听器,防内存泄漏 |
19
+
20
+ ---
21
+
22
+ ## 二、API 文档
23
+
24
+ ### `event.on(event, listener)`
25
+
26
+ 注册监听器,返回取消函数。
27
+
28
+ ```javascript
29
+ import { event } from "chanjs";
30
+
31
+ const off = event.on("user.login", (uid) => {
32
+ console.log(`用户 ${uid} 登录`);
33
+ });
34
+
35
+ // 需要时取消监听
36
+ off();
37
+ ```
38
+
39
+ | 参数 | 类型 | 说明 |
40
+ |------|------|------|
41
+ | `event` | `string` | 事件名 |
42
+ | `listener` | `(...args) => void` | 监听回调 |
43
+ | **返回** | `() => void` | 取消监听函数 |
44
+
45
+ ---
46
+
47
+ ### `event.once(event, listener)`
48
+
49
+ 只监听一次,触发后自动移除。
50
+
51
+ ```javascript
52
+ event.once("app.ready", () => {
53
+ console.log("应用启动完成(只触发一次)");
54
+ });
55
+ ```
56
+
57
+ ---
58
+
59
+ ### `event.emit(event, ...args)`
60
+
61
+ 触发事件,同步执行全部监听器。
62
+
63
+ ```javascript
64
+ event.emit("order.created", { orderId: 1001, amount: 299.0 });
65
+ ```
66
+
67
+ | 参数 | 类型 | 说明 |
68
+ |------|------|------|
69
+ | `event` | `string` | 事件名 |
70
+ | `...args` | `any[]` | 传递给监听器的参数(支持多参数) |
71
+
72
+ ---
73
+
74
+ ### `event.off(event, listener)`
75
+
76
+ 移除指定监听器。
77
+
78
+ ```javascript
79
+ const handler = (uid) => console.log(uid);
80
+ event.on("user.login", handler);
81
+ event.off("user.login", handler);
82
+ ```
83
+
84
+ > 注意:`off` 需传入与 `on` 相同的函数引用。匿名函数无法移除。
85
+
86
+ ---
87
+
88
+ ### `event.removeAll(event)`
89
+
90
+ 移除某事件的全部监听器。
91
+
92
+ ```javascript
93
+ event.removeAll("user.login");
94
+ ```
95
+
96
+ ---
97
+
98
+ ### `event.count(event)`
99
+
100
+ 获取某事件的监听器数量。
101
+
102
+ ```javascript
103
+ const n = event.count("user.login");
104
+ console.log(`当前有 ${n} 个监听器`);
105
+ ```
106
+
107
+ ---
108
+
109
+ ### `event.destroy()`
110
+
111
+ 移除所有事件的全部监听器。优雅停机时由框架自动调用,业务通常不需要手动调用。
112
+
113
+ ```javascript
114
+ event.destroy();
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 三、使用场景
120
+
121
+ ### 场景一:中间件中使用(无 app 实例)
122
+
123
+ 中间件函数签名是 `(app, config) => void`,但有时在工具函数里没有 app 引用,直接 import 全局实例即可。
124
+
125
+ ```javascript
126
+ // app/middleware/audit.js
127
+ import { event } from "chanjs";
128
+
129
+ export function audit(app, config) {
130
+ app.use((req, res, next) => {
131
+ res.on("finish", () => {
132
+ event.emit("audit.log", {
133
+ method: req.method,
134
+ url: req.originalUrl,
135
+ status: res.statusCode,
136
+ ip: req.ip,
137
+ });
138
+ });
139
+ next();
140
+ });
141
+ }
142
+ ```
143
+
144
+ ### 场景二:定时任务中使用
145
+
146
+ 定时任务回调中没有 HTTP 请求上下文,但可以通过 `event` 触发或监听事件。
147
+
148
+ ```javascript
149
+ // app/index/service/SyncTask.js
150
+ import { event } from "chanjs";
151
+
152
+ export default class SyncTask extends Service {
153
+ async register() {
154
+ this.app.task.add("data-sync", "0 */6 * * *", async () => {
155
+ event.emit("sync.started");
156
+ try {
157
+ const data = await this.fetchRemote();
158
+ await this.save(data);
159
+ event.emit("sync.completed", { count: data.length });
160
+ } catch (err) {
161
+ event.emit("sync.failed", { error: err.message });
162
+ }
163
+ });
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### 场景三:Controller / Service 中使用(有 app 实例)
169
+
170
+ ```javascript
171
+ // app/modules/user/controller/User.js
172
+ export default class User extends Controller {
173
+ async login(req, res) {
174
+ const user = await this.service.user.verify(req.body);
175
+
176
+ // 方式一:通过 app 实例
177
+ this.app.event.emit("user.login", user.id);
178
+
179
+ // 方式二:直接 import(等价)
180
+ // event.emit("user.login", user.id);
181
+
182
+ return this.success({ data: user });
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### 场景四:启动钩子中注册监听
188
+
189
+ ```javascript
190
+ // app/index.js
191
+ import Chan from "chanjs";
192
+
193
+ const chan = new Chan();
194
+
195
+ chan.beforeStart(() => {
196
+ // 监听用户登录事件
197
+ const off = chan.event.on("user.login", (uid) => {
198
+ logger.info(`[Hook] 用户登录: ${uid}`);
199
+ });
200
+
201
+ // 监听订单创建事件
202
+ chan.event.on("order.created", ({ orderId, amount }) => {
203
+ logger.info(`[Hook] 新订单: ${orderId}, 金额: ${amount}`);
204
+ });
205
+ });
206
+
207
+ await chan.start();
208
+ ```
209
+
210
+ ### 场景五:跨模块通信
211
+
212
+ 模块 A 发出事件,模块 B 监听,互不依赖。
213
+
214
+ ```javascript
215
+ // 模块 A:订单服务发出事件
216
+ // app/modules/order/service/Order.js
217
+ export default class OrderService extends Service {
218
+ async create(data) {
219
+ const order = await this.repository.order.create(data);
220
+ this.app.event.emit("order.created", { orderId: order.id, amount: order.amount });
221
+ return order;
222
+ }
223
+ }
224
+
225
+ // 模块 B:积分服务监听事件(无需 import OrderService)
226
+ // app/modules/points/service/Points.js
227
+ export default class PointsService extends Service {
228
+ async init() {
229
+ this.app.event.on("order.created", ({ orderId, amount }) => {
230
+ // 根据订单金额计算积分
231
+ const points = Math.floor(amount);
232
+ this.repository.points.add(orderId, points);
233
+ });
234
+ }
235
+ }
236
+ ```
237
+
238
+ ### 场景六:使用独立实例(隔离场景)
239
+
240
+ 需要事件隔离时,创建独立实例,与全局实例互不干扰。
241
+
242
+ ```javascript
243
+ import { EventBus } from "chanjs";
244
+
245
+ const localBus = new EventBus();
246
+
247
+ localBus.on("local.event", () => { /* ... */ });
248
+ localBus.emit("local.event");
249
+
250
+ // 全局实例不受影响
251
+ event.count("local.event"); // 0
252
+ ```
253
+
254
+ ---
255
+
256
+ ## 四、事件命名规范
257
+
258
+ 推荐使用**点分层命名**,格式:`<模块>.<动作>` 或 `<模块>.<资源>.<动作>`。
259
+
260
+ | 事件名 | 说明 |
261
+ |--------|------|
262
+ | `user.login` | 用户登录 |
263
+ | `user.logout` | 用户退出 |
264
+ | `user.register` | 用户注册 |
265
+ | `order.created` | 订单创建 |
266
+ | `order.paid` | 订单支付 |
267
+ | `order.cancelled` | 订单取消 |
268
+ | `sync.started` | 同步开始 |
269
+ | `sync.completed` | 同步完成 |
270
+ | `sync.failed` | 同步失败 |
271
+ | `app.ready` | 应用启动完成 |
272
+ | `audit.log` | 审计日志 |
273
+
274
+ ---
275
+
276
+ ## 五、注意事项
277
+
278
+ ### 1. 监听器异常不会阻断 emit
279
+
280
+ `emit()` 是同步的,但单个监听器抛异常会影响后续监听器执行。建议在监听器内部 try-catch。
281
+
282
+ ```javascript
283
+ event.on("order.created", async ({ orderId }) => {
284
+ try {
285
+ await sendNotification(orderId);
286
+ } catch (err) {
287
+ logger.error(`通知发送失败: ${err.message}`);
288
+ }
289
+ });
290
+ ```
291
+
292
+ ### 2. 异步监听器
293
+
294
+ `emit()` 不会 await 异步监听器。如果需要等待异步操作完成,在监听器内部自行处理。
295
+
296
+ ```javascript
297
+ event.on("order.created", async ({ orderId }) => {
298
+ // emit 不会 await 这个 Promise
299
+ await processOrder(orderId);
300
+ });
301
+ ```
302
+
303
+ ### 3. 监听器上限
304
+
305
+ 默认上限 50 个监听器/事件。超出时 Node 会打印警告。如需调整:
306
+
307
+ ```javascript
308
+ import { EventBus } from "chanjs";
309
+
310
+ const bus = new EventBus();
311
+ // 独立实例可自行调整(全局实例不建议改)
312
+ ```
313
+
314
+ ### 4. 内存泄漏防护
315
+
316
+ - `on()` 返回的取消函数务必在不需要时调用。
317
+ - 优雅停机时框架自动调用 `event.destroy()` 移除全部监听器。
318
+ - 定时任务中注册的监听器如果只在任务生命周期内有效,任务结束时调用 `off()`。
319
+
320
+ ### 5. 性能说明
321
+
322
+ - `EventEmitter` 是 Node 原生 V8 级实现,`emit/on` 时间复杂度 O(1)~O(n)。
323
+ - 全局单例避免重复实例化,零额外内存开销。
324
+ - `emit()` 是同步操作,监听器不宜执行耗时逻辑(应异步化或放入定时任务)。
@@ -0,0 +1,262 @@
1
+ # 定时任务 Task
2
+
3
+ > 基于 `node-cron` 的定时任务管理器,处理"到点自动执行"的业务,如数据清理、定时同步、缓存刷新、报表生成。
4
+ > 对外通过 `import { Task } from "chanjs"` 或 `this.app.task`(Chan 应用实例已内置)使用。
5
+
6
+ ---
7
+
8
+ ## 一、设计理念
9
+
10
+ Channel 把 `node-cron` 封装成 `Task` 类,解决三件事:
11
+
12
+ | 决策 | 说明 |
13
+ |------|------|
14
+ | 校验不阻断 | 任务名非法 / cron 非法 / 任务重名 / 回调非函数 → 只打 error 日志并返回 `null`,**不抛异常、不阻断应用启动** |
15
+ | 异常不崩溃 | 任务回调内部异常自动捕获并记录日志,**不会导致进程崩溃** |
16
+ | 优雅停机 | 进程退出时由框架调用 `stopAll()` 停止全部定时器,防资源泄漏 |
17
+
18
+ ---
19
+
20
+ ## 二、怎么拿到 Task
21
+
22
+ ### 方式一:应用实例内置(推荐)
23
+
24
+ `Chan` 应用在启动时已创建 `this.task`,业务代码直接使用:
25
+
26
+ ```javascript
27
+ // 任意 Controller / Service 里
28
+ async syncData(req, res) {
29
+ this.app.task.add("data-sync", "0 */6 * * *", async () => {
30
+ // 每 6 小时同步一次
31
+ });
32
+ }
33
+ ```
34
+
35
+ ### 方式二:直接 import(中间件 / 工具函数等无 app 实例处)
36
+
37
+ ```javascript
38
+ import { Task } from "chanjs";
39
+
40
+ const task = new Task();
41
+ task.add("clear-log", "0 3 * * *", async () => { /* ... */ });
42
+ ```
43
+
44
+ > 项目里通常用方式一(`this.app.task`),与 EventBus 单例的理念一致。
45
+
46
+ ---
47
+
48
+ ## 三、API 文档
49
+
50
+ ### `task.add(name, expr, fn, opts?)`
51
+
52
+ 注册一个定时任务。**校验失败返回 `null`,成功返回 `node-cron` 的任务实例。**
53
+
54
+ ```javascript
55
+ const task = this.app.task.add(
56
+ "clear-log", // 任务名(唯一标识)
57
+ "0 3 * * *", // cron 表达式
58
+ async () => { // 回调
59
+ await this.app.db.raw("DELETE FROM logs WHERE created_at < NOW() - INTERVAL 7 DAY");
60
+ },
61
+ { start: true } // 选项(可选)
62
+ );
63
+ ```
64
+
65
+ | 参数 | 类型 | 说明 |
66
+ |------|------|------|
67
+ | `name` | `string` | 任务名,`^[a-zA-Z0-9_-]{1,50}$`,必须唯一 |
68
+ | `expr` | `string` | cron 表达式(5 段,如 `"0 3 * * *"`) |
69
+ | `fn` | `(task) => Promise<void> \| void` | 任务回调,可选异步 |
70
+ | `opts.start` | `boolean` | 注册后是否立即启动,默认 `true` |
71
+ | `opts.timezone` | `string` | 时区,如 `"Asia/Shanghai"` |
72
+ | **返回** | `ScheduledTask \| null` | 任务实例;校验失败返回 `null` |
73
+
74
+ **校验失败的情况**(返回 `null`,仅打 error 日志):
75
+ - 任务名非法
76
+ - cron 表达式非法
77
+ - 任务名已存在(重复注册)
78
+ - 回调不是函数
79
+
80
+ ```javascript
81
+ // 重复注册同名任务 → 返回 null,不报错
82
+ const t1 = this.app.task.add("daily", "0 0 * * *", fn1); // 成功
83
+ const t2 = this.app.task.add("daily", "0 0 * * *", fn2); // null(已存在)
84
+ ```
85
+
86
+ ---
87
+
88
+ ### `task.start(name)` / `task.stop(name)`
89
+
90
+ 启动 / 停止指定任务(`add` 时 `start: false` 注册的懒启动任务,或暂停后恢复)。
91
+
92
+ ```javascript
93
+ // 注册但不立即启动
94
+ this.app.task.add("report", "30 8 * * *", genReport, { start: false });
95
+
96
+ // 需要时手动启动 / 停止
97
+ this.app.task.start("report");
98
+ this.app.task.stop("report");
99
+ ```
100
+
101
+ ---
102
+
103
+ ### `task.list()`
104
+
105
+ 返回全部已注册任务名数组。
106
+
107
+ ```javascript
108
+ const names = this.app.task.list(); // ["clear-log", "data-sync", ...]
109
+ ```
110
+
111
+ ---
112
+
113
+ ### `task.stopAll()`
114
+
115
+ 停止全部任务并清空注册表。**优雅停机时框架自动调用,业务通常不需要手动调用。**
116
+
117
+ ```javascript
118
+ this.app.task.stopAll();
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 四、cron 表达式速查
124
+
125
+ `node-cron` 使用 5 段标准 cron:`分钟 小时 日 月 星期`。
126
+
127
+ | 表达式 | 含义 |
128
+ |--------|------|
129
+ | `0 3 * * *` | 每天 03:00 |
130
+ | `0 */6 * * *` | 每 6 小时整点 |
131
+ | `30 8 * * 1-5` | 工作日 08:30 |
132
+ | `0 0 1 * *` | 每月 1 日 00:00 |
133
+ | `*/5 * * * *` | 每 5 分钟 |
134
+ | `0 0 * * 0` | 每周日 00:00 |
135
+
136
+ > 支持 `*`(任意)、`*/n`(每 n)、`a-b`(范围)、`a,b`(列表)。
137
+
138
+ ---
139
+
140
+ ## 五、使用场景
141
+
142
+ ### 场景一:定时清理过期数据
143
+
144
+ ```javascript
145
+ // app/modules/sys/service/CleanTask.js
146
+ import { Service } from "chanjs";
147
+
148
+ export default class CleanTask extends Service {
149
+ register() {
150
+ this.app.task.add("clean-expired", "0 4 * * *", async () => {
151
+ // 删除 7 天前的登录日志
152
+ await this.repo.logs.deleteExpired(7);
153
+ // 清理过期的临时验证码(store 无按前缀删除,需遍历或存集合后逐个 del)
154
+ // 例:已知要清的 key 列表
155
+ for (const key of expiredCodeKeys) {
156
+ await this.app.store.del(key);
157
+ }
158
+ }, { timezone: "Asia/Shanghai" });
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### 场景二:定时同步外部数据
164
+
165
+ ```javascript
166
+ this.app.task.add("sync-wx-token", "0 */30 * * *", async () => {
167
+ const token = await fetchWechatToken();
168
+ await this.app.store.set("wx:token", token, 60 * 60 * 1000);
169
+ });
170
+ ```
171
+
172
+ ### 场景三:定时刷新热点缓存
173
+
174
+ ```javascript
175
+ this.app.task.add("refresh-home-cache", "0 */10 * * *", async () => {
176
+ await clearHomeCache(); // 业务缓存清理函数
177
+ });
178
+ ```
179
+
180
+ ### 场景四:生成日报 / 周报
181
+
182
+ ```javascript
183
+ this.app.task.add("daily-report", "0 8 * * *", async () => {
184
+ const report = await buildReport();
185
+ await sendToAdmin(report);
186
+ });
187
+ ```
188
+
189
+ ### 场景五:与事件总线配合(任务触发事件)
190
+
191
+ 任务完成后 `emit` 事件,其他模块监听处理:
192
+
193
+ ```javascript
194
+ this.app.task.add("data-sync", "0 */6 * * *", async () => {
195
+ this.app.event.emit("sync.started");
196
+ try {
197
+ const data = await fetchRemote();
198
+ await saveAll(data);
199
+ this.app.event.emit("sync.completed", { count: data.length });
200
+ } catch (err) {
201
+ this.app.event.emit("sync.failed", { error: err.message });
202
+ }
203
+ });
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 六、注册时机
209
+
210
+ 定时任务通常在**启动钩子**里注册最合适:
211
+
212
+ ```javascript
213
+ // app/app.js
214
+ import Chan from "chanjs";
215
+
216
+ const chan = new Chan();
217
+
218
+ chan.beforeStart(async () => {
219
+ // 注册全部定时任务
220
+ const syncTask = new SyncTask();
221
+ syncTask.register();
222
+ });
223
+
224
+ await chan.start();
225
+ ```
226
+
227
+ > 或写在某个 `Service` 的 `register()` 方法里,在启动钩子中调用。
228
+
229
+ ---
230
+
231
+ ## 七、注意事项
232
+
233
+ ### 1. 回调内部务必 try-catch(或依赖框架自动捕获)
234
+
235
+ 框架会自动捕获回调抛出的异常并打 error 日志,不会崩溃。但若你希望失败时做补偿,可自行 try-catch:
236
+
237
+ ```javascript
238
+ this.app.task.add("sync", "0 */6 * * *", async () => {
239
+ try {
240
+ await doSync();
241
+ } catch (err) {
242
+ logger.error("同步失败,已跳过本次", err);
243
+ // 可在此告警
244
+ }
245
+ });
246
+ ```
247
+
248
+ ### 2. 回调不传 `req` / `res`
249
+
250
+ 定时任务没有 HTTP 请求上下文,需要访问数据请用 `this.repo` / `this.app.db` / `this.app.store`。
251
+
252
+ ### 3. 任务名全局唯一
253
+
254
+ 重复注册同名任务会返回 `null`。建议按 `<模块>-<动作>` 命名,如 `data-sync`、`clean-log`。
255
+
256
+ ### 4. 时区
257
+
258
+ 默认使用服务器本地时区。跨时区定时请显式传 `{ timezone: "Asia/Shanghai" }`。
259
+
260
+ ### 5. 优雅停机
261
+
262
+ 进程退出时 `signal.js` 会自动调用 `stopAll()`,你无需处理。但若在插件 / 独立脚本里手动 `new Task()`,记得自己 `stopAll()`。