chanjs 2.7.8 → 2.7.10

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 (39) 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 +4 -8
  24. package/package.json +20 -2
  25. package/response/code.js +0 -12
  26. package/response/response.js +8 -2
  27. package/security/keywords.js +2 -3
  28. package/utils/logger.js +60 -91
  29. package/utils/signal.js +21 -2
  30. package/USAGE.md +0 -533
  31. package/doc/Cache.md +0 -333
  32. package/doc/Common.md +0 -638
  33. package/doc/Controller.md +0 -223
  34. package/doc/Help.md +0 -390
  35. package/doc/QuickStart.md +0 -116
  36. package/doc/Repository.md +0 -560
  37. package/doc/Service.md +0 -240
  38. package/publish.bat +0 -4
  39. package/todo.md +0 -1
@@ -0,0 +1,208 @@
1
+ # Chanjs 框架开发教程(对外 API 全解)
2
+
3
+ > 目标读者:刚接触 Node.js / Express 的小白,以及需要在 ChanCMS 上做二次开发的工程师。
4
+ > 本文档只讲**框架对外提供的函数与类**,所有签名均来自 `chanjs` 源码(`node_modules/chanjs`),可放心照抄。
5
+ > 配套示例尽量贴近真实业务(`chancms-vip` 项目)。
6
+
7
+ ---
8
+
9
+ ## 一、Chanjs 是什么
10
+
11
+ Chanjs 是一个基于 **Node.js + Express 5** 的 **标准 HMVC 框架**(可称 NHMVC,New-generation HMVC)。
12
+
13
+ 它的内核不是"轻量 CMS",而是**模块化 + 模块自治 + 跨模块复用**:
14
+
15
+ - **模块自治**:每个业务模块(`app/modules/<module>/`)是独立的 MVC 单元,自带 `controller/`、`service/`、`middleware/`、`router.js`
16
+ - **跨模块双通道协作**:`this.get("模块","Controller")` 取其它模块的 Controller、`this.get("模块","Service","service")` 取其它模块的 Service,按需加载、永久缓存、低耦合
17
+ - **分层职责**:Controller(接口)→ Service(业务逻辑)→ Repository(数据库 CRUD)
18
+
19
+ 技术底座:
20
+
21
+ - **Node.js** + **Express 5**(HTTP 服务与路由)
22
+ - **Knex**(数据库查询构造器,支持 MySQL / PostgreSQL / SQLite)
23
+ - **art-template**(模板引擎,用于前台页面渲染)
24
+
25
+ 它帮你把"建表、写接口、做鉴权、渲染页面"这些重复劳动封装好,你只需写**业务层**:Controller(接口)、Service(业务逻辑)、Repository(数据库 CRUD)。
26
+
27
+ ---
28
+
29
+ ## 二、整体架构与代码关系图
30
+
31
+ ```mermaid
32
+ graph TD
33
+ subgraph 启动层["启动层"]
34
+ Chan["Chan 应用核心类<br/>(core/App.js)"]
35
+ Config["配置加载<br/>(core/loader.js)"]
36
+ StoreInit["存储初始化<br/>(storage)"]
37
+ DBInit["数据库初始化<br/>(core/Database.js)"]
38
+ Routes["模块路由加载<br/>(bootstrap/router-loader.js)"]
39
+ ErrH["全局错误处理器"]
40
+ Hooks["启动钩子 beforeStart"]
41
+ end
42
+
43
+ subgraph 业务层["业务层(你写的地方)"]
44
+ Ctrl["Controller 基类<br/>(core/Controller.js)"]
45
+ Svc["Service 基类<br/>(core/Service.js)"]
46
+ Repo["Repository 基类<br/>(core/Repository.js)"]
47
+ Base["BaseComponent 基类"]
48
+ end
49
+
50
+ subgraph 支撑层["框架支撑 API"]
51
+ Resp["响应 success / fail<br/>(response)"]
52
+ Err["错误 AppError 族<br/>(core/errors.js)"]
53
+ Sec["安全 jwt / filterXSS /<br/>checkKeywords / rate-limit / sign"]
54
+ Sto["存储 store / cache"]
55
+ Log["日志 logger / createLogger"]
56
+ Val["校验 validate / validateAll"]
57
+ Path["路径 utils.paths / file"]
58
+ end
59
+
60
+ DB[("数据库 MySQL")]
61
+ Redis[("Redis / 内存")]
62
+
63
+ Chan --> Config
64
+ Chan --> StoreInit
65
+ Chan --> DBInit
66
+ Chan --> Routes
67
+ Chan --> ErrH
68
+ Chan --> Hooks
69
+
70
+ Routes --> Ctrl
71
+ Ctrl --> Svc
72
+ Svc --> Repo
73
+ Repo --> DB
74
+ Ctrl --> Resp
75
+ Ctrl --> Err
76
+ Ctrl --> Sec
77
+ Ctrl --> Val
78
+ Sec --> Sto
79
+ Sto --> Redis
80
+ Log -.记录.-> Chan
81
+ Path --> Ctrl
82
+ ```
83
+
84
+ **一句话理解调用链:**
85
+ `HTTP 请求` → `路由 (router.js)` → `Controller.方法()` → `Service.方法()` → `Repository.方法()` → `数据库`。
86
+ 过程中用 `this.success()` 返回数据,用 `AppError` 抛错,用 `store/cache` 做缓存,用 `logger` 打日志。
87
+
88
+ ### HMVC 架构定位
89
+
90
+ 教科书 HMVC 的"标准"是 **Controller 嵌套调用 Controller + 运行时子请求**,这其实是把**实现手段**当成了标准,是片面的。
91
+ HMVC 的本质是**分层 + 模块自治 + 跨模块复用**。Chanjs 抓住本质,实现**标准 HMVC**:
92
+
93
+ - 每个业务模块是自治的 MVC 单元(`controller/` + `service/` + `middleware/` + `router.js`)
94
+ - 支持**双通道跨模块协作**:`this.get("模块","Controller")` 拿 Controller、`this.get("模块","Service","service")` 拿 Service
95
+ - 模块间按需加载、永久缓存、低耦合
96
+
97
+ | 维度 | 教科书 HMVC(过时) | Chanjs(标准 HMVC) |
98
+ |---|---|---|
99
+ | HMVC 定义依据 | 把"Controller 嵌套 + 子请求"当标准 | 以"模块自治 + 跨模块复用"本质为标准 |
100
+ | 跨模块协作 | Controller 嵌套调用 Controller | **双通道**:`get(Controller)` 或 `get(Service)` |
101
+ | 典型调用链 | Controller → 嵌套 Controller → 子 View | Controller → 跨模块 `get()` → 复用模块能力 |
102
+ | 复杂度 | 高(子请求开销、栈深、职责混乱) | 低(一层 get、层级简单) |
103
+
104
+ 结论:教科书把实现手段当标准,是过时的变体;Chanjs 还原 HMVC 本质,才是更贴近定义的**标准实现**。
105
+
106
+ ---
107
+
108
+ ## 三、怎么拿到这些 API
109
+
110
+ 业务代码里统一这样引入(全部从根包 `"chanjs"` 导出):
111
+
112
+ ```js
113
+ import Controller, { Service, Repository } from "chanjs";
114
+ import { success, fail } from "chanjs"; // 也可从 Controller 里用 this.success
115
+ import { AppError, ValidationError } from "chanjs";
116
+ import { setToken, getToken, verifyToken, revokeToken } from "chanjs";
117
+ import { filterXSS, checkKeywords, createRateLimitMiddleware } from "chanjs";
118
+ import { store, cache } from "chanjs";
119
+ import logger, { createLogger } from "chanjs";
120
+ import { validate, validateAll } from "chanjs";
121
+ import { utils, Paths } from "chanjs"; // utils 工具集 / Paths 路径常量
122
+ import Chan from "chanjs"; // 应用核心类(入口 app.js 用)
123
+ ```
124
+
125
+ > ⚠️ **重要**:`chanjs/index.js` 的导出列表是唯一权威来源。若你 `import` 了某个名字却报
126
+ > `does not provide an export named 'xxx'`,先去 `chanjs/index.js` 看是否真的导出了——**新增 import 前务必核对导出列表**。
127
+
128
+ ---
129
+
130
+ ## 四、最小可运行示例(先有个整体印象)
131
+
132
+ `app.js`(项目入口):
133
+
134
+ ```js
135
+ import Chan from "chanjs";
136
+
137
+ const app = new Chan();
138
+
139
+ // 启动前钩子:可做一次性初始化
140
+ app.beforeStart(async () => {
141
+ console.log("准备启动...");
142
+ });
143
+
144
+ app.start().then(() => {
145
+ app.run(port => console.log("监听", port));
146
+ });
147
+ ```
148
+
149
+ `modules/demo/router.js`:
150
+
151
+ ```js
152
+ import { loader } from "chanjs";
153
+
154
+ // 框架自动扫描 app/modules/*/router.js 并调用本函数完成路由注册
155
+ export default async (app, router, config) => {
156
+ const controller = await loader.loadController("demo");
157
+ router.get("/demo/list", controller.Demo.list);
158
+ };
159
+ ```
160
+
161
+ `modules/demo/controller/DemoController.js`:
162
+
163
+ ```js
164
+ import { Controller, Service, Repository } from "chanjs";
165
+
166
+ class DemoRepo extends Repository {
167
+ constructor() { super("demo_table"); } // 表名
168
+ }
169
+
170
+ class DemoService extends Service {
171
+ constructor() { super(); this.repo = new DemoRepo(); }
172
+ async list() { return this.repo.all(); }
173
+ }
174
+
175
+ export class DemoController extends Controller {
176
+ constructor() { super(); this.service = new DemoService(); }
177
+ async list(req, res) {
178
+ const data = await this.service.list();
179
+ this.success(data); // → { success:true, code:0, msg:"操作成功", data }
180
+ }
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 五、文档导航(按学习顺序)
187
+
188
+ | 文件 | 讲什么 | 适合谁 |
189
+ |---|---|---|
190
+ | [01-核心类.md](./01-核心类Controller-Service-Repository.md) | `Controller` / `Service` / `Repository` / `BaseComponent` 三个基类怎么用 | 所有人,最先看 |
191
+ | [02-响应与错误.md](./02-响应与错误.md) | `success` / `fail` / `AppError` 族 / `CODE` 错误码 | 写接口必看 |
192
+ | [03-安全模块.md](./03-安全模块.md) | JWT 登录态、XSS 过滤、关键词校验、限流、加解密 | 做登录/评论/表单必看 |
193
+ | [04-存储与缓存.md](./04-存储与缓存.md) | `store`(Redis/内存)、`cache`(内存缓存) | 做缓存/限流/黑名单必看 |
194
+ | [05-工具与校验.md](./05-工具与校验.md) | `logger` / `createLogger`、`Paths`、`safePath`、`validate` / `validateAll` | 日常工具 |
195
+ | [06-应用生命周期.md](./06-应用生命周期.md) | `Chan` 类、`beforeStart`、`loader`、`registry` | 想理解启动流程/写启动钩子 |
196
+ | [07-事件系统EventBus.md](./07-事件系统EventBus.md) | `EventBus` / `event` 事件总线 | 做模块解耦 / 异步通知 |
197
+ | [08-定时任务Task.md](./08-定时任务Task.md) | `Task` 定时任务(cron) | 做定时清理 / 同步 / 报表 |
198
+ | [09-国际化Lang.md](./09-国际化Lang.md) | `initLang` / i18next 多语言 | 做多语言站点 |
199
+
200
+ ---
201
+
202
+ ## 六、约定与名词
203
+
204
+ - **业务码 `code`**:`0` 成功;`1xxx` 业务错误(1001 未登录、1003 无权限…);`5xxx` 系统;`6xxx` 数据库。
205
+ - **响应信封**:所有 JSON 接口统一返回 `{ success, code, msg, data }`。
206
+ - **`this.app`**:业务对象上能拿到 `app`(Chan 实例)、`app.db`(默认数据库连接)、`app.config`、`app.dbManager`。
207
+ - **`req.validated`**:经过 `validate` / `validateAll` 中间件后,校验通过的参数会挂在这里。
208
+ - **多数据库**:Repository 构造传第二个参数 `dbName` 即可切换连接(`app.dbManager.get(dbName)`)。
@@ -0,0 +1,432 @@
1
+ # 01 · 核心类:Controller / Service / Repository / BaseComponent / Container
2
+
3
+ Chanjs 推荐**三层结构**:Controller 收请求、Service 写业务逻辑、Repository 管数据库。
4
+ 三者都通过继承基类自动获得 `this.app`、`this.config`、`this.db` 等运行时能力。
5
+
6
+ ---
7
+
8
+ ## 0. 类继承关系(务必看清)
9
+
10
+ | 类 | 直接父类 | 拥有的能力 |
11
+ |---|---|---|
12
+ | `BaseComponent` | — | `this.app` / `this.config` / `this.db` |
13
+ | `Container` | `BaseComponent` | 上面三个 **+ `this.get()` 跨模块按需加载** |
14
+ | `Controller` | `Container` | 上面全部 **+ `this.success()` / `this.fail()`** |
15
+ | `Service` | `Container` | 上面全部(`this.get()` 可用) |
16
+ | `Repository` | `BaseComponent` | **只有** `this.app`/`this.config`/`this.db`,**没有 `this.get()`** |
17
+
18
+ > ⚠️ **重要**:`Repository` 直接继承 `BaseComponent`,**不继承 `Container`**,因此 Repository 里**不能** `this.get()`。
19
+ > 跨模块取组件(`this.get()`)只在 **Controller / Service** 上可用。Repository 需要别的模块数据时,
20
+ > 由上层 Service 取好后传入,或 Repository 直接用 `this.db("其它表")` 跨表查。
21
+
22
+ ---
23
+
24
+ ## 1. BaseComponent(最底层基类)
25
+
26
+ 所有业务组件的"祖父类"。它把 `app / config / db` 这些运行时对象挂到 `this` 上。
27
+
28
+ ```js
29
+ class BaseComponent {
30
+ get app() { return getApp(); } // 全局 Chan 实例
31
+ get config() { return this.app?.config ?? {}; }
32
+ get db() { return this.app?.db ?? null; } // 默认数据库连接(knex 实例)
33
+ }
34
+ ```
35
+
36
+ **你能直接用到的:**
37
+ - `this.app` —— 整个应用实例(含 `dbManager`、`config`、`paths`、`event`、`task`、`lang`)。
38
+ - `this.db` —— 默认数据库连接(knex 查询构造器入口),例如 `this.db("user").where({id})`。
39
+ - `this.config` —— 配置对象(来自 `.env` 与 `config/`),未初始化时返回 `{}` 避免 `undefined.xxx` 报错。
40
+
41
+ > 实际开发中你几乎不直接继承 `BaseComponent`,而是继承它更具体的子类 `Service` / `Repository` / `Controller`。
42
+ > **注意**:早期文档提到 `BaseComponent` 有 `on()` 生命周期钩子,这是**错误**的,源码里 `BaseComponent` 没有 `on()`。
43
+
44
+ ---
45
+
46
+ ## 2. Repository(数据访问层)
47
+
48
+ 文件:`core/Repository.js`。封装了通用 CRUD,**你一般只需 `extends Repository` 并传入表名**。
49
+ 注意:**Repository 没有 `this.get()`**,只能访问 `this.db`(默认库)或通过构造第二参切换库。
50
+
51
+ ### 2.1 构造
52
+
53
+ ```js
54
+ class UserRepo extends Repository {
55
+ constructor() {
56
+ // super(表名, 可选db名, 可选{dateFields})
57
+ super("cms_user", null, { dateFields: ["created_at", "updated_at"] });
58
+ }
59
+ }
60
+ ```
61
+
62
+ | 参数 | 类型 | 必填 | 说明 |
63
+ |---|---|---|---|
64
+ | `table` | string | ✅ | 数据库表名 |
65
+ | `dbName` | string \| null | ❌ | 指定数据库连接 key;不传用默认库(`app.db`)。传了则每次取 `app.dbManager.get(dbName)` |
66
+ | `opts.dateFields` | string[] | ❌ | 这些字段在写入时若传字符串会被自动转成 `Date` 对象 |
67
+
68
+ > 实例属性 `this.limit`:返回 `config.LIMIT_MAX || 300`,是分页每页上限。
69
+
70
+ ### 2.2 查询类方法
71
+
72
+ 所有方法返回统一信封 `{ success, code, msg, data }`(失败信封见 02 文档)。
73
+
74
+ | 方法 | 签名 | 返回 `data` | 说明 |
75
+ |---|---|---|---|
76
+ | `all` | `all({ query={}, sort={}, fields=[], limit=1000 })` | `Array` | 查全部,默认上限 1000;`limit<=0` 表示不限 |
77
+ | `find` | `find({ query={}, sort={}, fields=[], limit, offset })` | `Array` | 偏移分页查询(`limit`/`offset` 为数字时生效)|
78
+ | `findOne` | `findOne({ query={}, fields=[] })` | 行对象 | 查单条;无则 `code=1004` 且 `data=null` |
79
+ | `findById` | `findById(id, { fields=[] })` | 行对象 | 等价于 `findOne({ query:{id}, fields })` |
80
+ | `query` | `query({ current=1, pageSize=10, query={}, sort={}, field=[] })` | 分页对象(见下)| **标准分页查询** |
81
+ | `count` | `count(query={})` | `{ count:number }` | 统计符合条件行数 |
82
+ | `exists` | `exists(query={})` | `{ exists:boolean }` | 是否存在 |
83
+ | `join` | `join({ joinTable, localField, foreignField, fields=[], query={}, sort={} })` | `Array` | 联表查询(表名/字段有白名单校验)|
84
+ | `stats` | `stats()` | `{ total, today }` | 表总记录数 + 今日(`created_at/createdAt` 字段)新增数 |
85
+
86
+ **分页 `query()` 的返回结构(前端最爱):**
87
+ ```json
88
+ {
89
+ "success": true, "code": 0, "msg": "查询成功",
90
+ "data": {
91
+ "list": [ /* 当前页数据 */ ],
92
+ "total": 135,
93
+ "current": 1,
94
+ "pageSize": 10,
95
+ "totalPages": 14
96
+ }
97
+ }
98
+ ```
99
+
100
+ **`query` / `sort` / `fields` 怎么写:**
101
+ ```js
102
+ await repo.query({
103
+ current: 1,
104
+ pageSize: 10,
105
+ query: { status: 1, cid: 5 }, // 等值条件
106
+ // query 还支持操作符:
107
+ // { pv: { $gt: 100 } } 大于
108
+ // { title: { $like: "%foo%" } } 模糊
109
+ // { id: { $in: [1,2,3] } } in
110
+ // { deleted_at: { $null: true } } 为空(true→IS NULL,false→IS NOT NULL)
111
+ // { deleted_at: { $notNull: true } } 不为空
112
+ sort: { created_at: "desc" }, // 排序,字段有白名单校验(asc/desc)
113
+ field: ["id", "title", "created_at"], // 只取这些列("*" 也行)
114
+ });
115
+ ```
116
+
117
+ > ⚠️ **安全机制**:所有 `query` / `sort` / `field` 的字段名都必须匹配正则 `/^[a-zA-Z_][a-zA-Z0-9_]*$/`,
118
+ > 非法字段会被**静默忽略**(并在日志 warn)。这是防 SQL 注入的第一道关。
119
+
120
+ ### 2.3 写入类方法
121
+
122
+ | 方法 | 签名 | 返回 `data` | 说明 |
123
+ |---|---|---|---|
124
+ | `insert` | `insert(data={})` | `{ insertId, affectedRows:1 }` | 单条插入;`data` 为空返回 `code=1007` |
125
+ | `insertMany` | `insertMany(records=[])` | `{ insertId, affectedRows:N }` | 批量插入 |
126
+ | `update` | `update({ query, data })` | `{ affectedRows }` | 条件更新;缺 `query`/`data` 返回 `code=1006` |
127
+ | `updateById` | `updateById(id, data={})` | **更新后的完整行** | 按主键更新并返回新行 |
128
+ | `updateMany` | `updateMany([{query,data}])` | `{ affectedRows }` | **事务**批量更新,失败自动回滚 |
129
+ | `del` | `del(query={})` | `{ affectedRows }` | 条件删除;`query` 为空返回 `code=1007` |
130
+ | `delById` | `delById(id)` | `{ affectedRows }` | 按主键删(即 `del({ id })`)|
131
+ | `delMany` | `delMany(ids=[])` | `{ affectedRows }` | 按 ID 数组批量删(`whereIn('id', ids)`)|
132
+
133
+ **写操作示例:**
134
+ ```js
135
+ const r1 = await repo.insert({ name: "张三", age: 18 });
136
+ console.log(r1.data.insertId);
137
+
138
+ const r2 = await repo.updateById(1, { name: "李四" });
139
+ console.log(r2.data.name); // "李四"
140
+
141
+ await repo.del({ id: 1 }); // 或 repo.delById(1)
142
+ ```
143
+
144
+ > 🛡️ **fail-closed 保护**:`del` 和 `update` 在「查询条件字段全部非法 / 为空」时会**拒绝执行**
145
+ > (返回 `code=参数无效`),避免生成不带 WHERE 的「全表删除/更新」灾难。所以删除一定要带有效条件,例如 `del({ id })`。
146
+
147
+ ### 2.4 完整示例(Repository)
148
+
149
+ ```js
150
+ import { Repository } from "chanjs";
151
+
152
+ export class ArticleRepo extends Repository {
153
+ constructor() {
154
+ super("cms_article", null, { dateFields: ["created_at", "updated_at"] });
155
+ }
156
+
157
+ async getHot(limit = 10) {
158
+ return this.db("cms_article")
159
+ .where({ status: 1 })
160
+ .orderBy("pv", "desc")
161
+ .limit(limit);
162
+ }
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 3. Service(业务逻辑层)
169
+
170
+ 文件:`core/Service.js`。它**继承 `Container`(不是 `BaseComponent`)**,本身没有强制方法,
171
+ 作用是为你定义一个"持有 Repository、聚合多个 Repository、写业务规则"的标准位置。
172
+ 因为继承 Container,Service 里 **可以 `this.get()` 跨模块取组件**。
173
+
174
+ ```js
175
+ import { Service, Repository } from "chanjs";
176
+
177
+ class ArticleRepo extends Repository {
178
+ constructor() { super("cms_article"); }
179
+ }
180
+
181
+ export class ArticleService extends Service {
182
+ constructor() {
183
+ super();
184
+ this.repo = new ArticleRepo();
185
+ }
186
+
187
+ // 业务方法:组合 Repository + 规则
188
+ async listPage(page = 1, pageSize = 10) {
189
+ const res = await this.repo.query({ current: page, pageSize, query: { status: 1 } });
190
+ // 这里可以加缓存、加格式化、调别的 repo……
191
+ return res;
192
+ }
193
+
194
+ async createArticle(data) {
195
+ if (!data.title) throw new ValidationError("标题不能为空");
196
+ return this.repo.insert(data);
197
+ }
198
+
199
+ // 跨模块取别的模块的 Service(type 默认就是 "service",可省略)
200
+ async relatedCategory(id) {
201
+ const categorySvc = await this.get("cms", "Category", "service");
202
+ return categorySvc?.findById(id);
203
+ }
204
+ }
205
+ ```
206
+
207
+ **约定:**
208
+ - Service 里 `new` 出它需要的 Repository,挂到 `this.repo` / `this.xxxRepo`。
209
+ - 复杂业务(如"发文章同时更新栏目计数")在 Service 里编排多个 Repository 调用。
210
+ - Service 不直接碰 `req/res`,只处理数据和规则,便于单元测试。
211
+
212
+ ---
213
+
214
+ ## 4. Controller(接口层)
215
+
216
+ 文件:`core/Controller.js`。继承 `Container`,**最关键的是内置了 `this.success()` / `this.fail()`**,
217
+ 且**可以 `this.get()` 跨模块取组件**。
218
+
219
+ ### 4.1 构造与签名
220
+
221
+ ```js
222
+ import { Controller, Service } from "chanjs";
223
+
224
+ export class ArticleController extends Controller {
225
+ constructor() {
226
+ super(); // 必调
227
+ this.service = new ArticleService();
228
+ }
229
+
230
+ // 每个接口方法:(req, res) => Promise<void>
231
+ async list(req, res) { /* ... */ }
232
+ }
233
+ ```
234
+
235
+ ### 4.2 `this.success(opts)` —— 成功返回
236
+
237
+ ```js
238
+ this.success({ data: {...}, msg: "操作成功" });
239
+ // 实际输出:
240
+ // { success: true, code: 0, msg: "操作成功", data: {...} }
241
+ ```
242
+
243
+ | 参数 | 类型 | 必填 | 说明 |
244
+ |---|---|---|---|
245
+ | `opts.data` | any | ❌ | 业务数据,默认 `{}` |
246
+ | `opts.msg` | string | ❌ | 提示文案,默认 `"操作成功"` |
247
+
248
+ > 不需要手写 `res.json(...)`,框架会自动处理。你也可以直接 `return this.success(...)`。
249
+
250
+ ### 4.3 `this.fail(opts)` —— 失败返回
251
+
252
+ ```js
253
+ // 方式一:只给字符串,当提示文案(默认 code=1008)
254
+ this.fail("用户名已存在");
255
+
256
+ // 方式二:对象,可指定 code / msg
257
+ this.fail({ code: 1008, msg: "验证码错误" });
258
+ ```
259
+
260
+ | 参数 | 类型 | 说明 |
261
+ |---|---|---|
262
+ | `opts` | string | 当成 `msg`;code 默认 1008 |
263
+ | `opts.msg` | string | 提示文案 |
264
+ | `opts.code` | number | 业务码,如 1008 业务失败 / 1006 参数错误 |
265
+
266
+ ### 4.4 标准 Controller 写法
267
+
268
+ ```js
269
+ import { Controller } from "chanjs";
270
+ import { ArticleService } from "../service/ArticleService.js";
271
+ import { ValidationError } from "chanjs";
272
+
273
+ export class ArticleController extends Controller {
274
+ constructor() {
275
+ super();
276
+ this.service = new ArticleService();
277
+ }
278
+
279
+ async list(req, res) {
280
+ const { current = 1, pageSize = 10 } = req.query;
281
+ const data = await this.service.listPage(Number(current), Number(pageSize));
282
+ this.success({ data });
283
+ }
284
+
285
+ async create(req, res) {
286
+ const { title } = req.body;
287
+ if (!title) throw new ValidationError("标题不能为空"); // 抛错会被全局错误处理器接住
288
+ const result = await this.service.createArticle(req.body);
289
+ this.success({ data: result.data, msg: "发布成功" });
290
+ }
291
+ }
292
+ ```
293
+
294
+ ### 4.5 路由里怎么挂
295
+
296
+ ```js
297
+ import express from "express";
298
+ import { ArticleController } from "./controller/ArticleController.js";
299
+
300
+ const router = express.Router();
301
+ const ctrl = new ArticleController();
302
+
303
+ router.get("/article/list", ctrl.list.bind(ctrl));
304
+ router.post("/article/create", ctrl.create.bind(ctrl));
305
+
306
+ export default router;
307
+ ```
308
+
309
+ > ⚠️ 一定要 `.bind(ctrl)`,否则方法里 `this` 会丢失(拿不到 `this.service` / `this.success`)。
310
+
311
+ ---
312
+
313
+ ## 5. 三层协作流程图
314
+
315
+ ```mermaid
316
+ sequenceDiagram
317
+ participant R as 路由 router.js
318
+ participant C as Controller
319
+ participant S as Service
320
+ participant Rep as Repository
321
+ participant DB as 数据库
322
+
323
+ R->>C: ctrl.list(req,res)
324
+ C->>S: this.service.listPage(...)
325
+ S->>Rep: this.repo.query(...)
326
+ Rep->>DB: knex SQL
327
+ DB-->>Rep: rows
328
+ Rep-->>S: {list,total,...}
329
+ S-->>C: data
330
+ C-->>R: this.success({data})
331
+ ```
332
+
333
+ ---
334
+
335
+ ## 6. 常见坑
336
+
337
+ 1. **忘记 `super()`**:Controller / Service / Repository 构造里第一句必须 `super()`,否则拿不到 `this.app`。
338
+ 2. **`this.success` 不是静态方法**:必须在 `extends Controller` 的实例方法里用 `this.success(...)`。
339
+ 3. **Repository 表名写错**:`super("cms_article")` 的表名必须和数据库一致(注意复数/前缀)。
340
+ 4. **删除不带条件**:`del({})` 会被框架拒绝(防全表删),务必 `del({ id })`。
341
+ 5. **路由没 bind**:`router.get("/x", ctrl.list)` 少了 `.bind(ctrl)` 会运行时报 `this is undefined`。
342
+ 6. **多库切换**:非默认库要在 Repository 构造传 `dbName`,且确保 `config` 里配了该连接。
343
+ 7. **`Repository` 没有 `this.get()`**:跨模块数据协作请放在 Service 层,`Repository` 只能直接用 `this.db`。
344
+
345
+ ---
346
+
347
+ ## 7. 跨模块调用(容器 `this.get()` / HMVC)
348
+
349
+ Chanjs 采用**标准 HMVC 架构**:每个业务模块自治,且 **Controller / Service 都继承 `Container`**,
350
+ 自带 `this.get()` 能力,**无需手写相对路径 import,即可按需获取任意模块的组件**。
351
+ (Repository 不继承 Container,所以没有这个方法。)
352
+
353
+ ### 7.1 get 签名
354
+
355
+ ```js
356
+ await this.get(moduleName, fileName, type?)
357
+ ```
358
+
359
+ | 参数 | 类型 | 必填 | 默认 | 说明 |
360
+ |---|---|---|---|---|
361
+ | `moduleName` | string | ✅ | — | 目标模块名,对应 `app/modules/<moduleName>/` |
362
+ | `fileName` | string | ✅ | — | 组件文件名(不含 `.js`)|
363
+ | `type` | `"controller"\|"service"` | ❌ | 本容器类型 | 组件类型 |
364
+
365
+ - 成功加载(存在)→ 返回实例,并**永久缓存**(进程内不失效)。
366
+ - 缺失 → 返回 `null`,不缓存,文件新增后下次立即感知。
367
+ - Controller 容器默认 `type="controller"`,Service 容器默认 `type="service"`。
368
+
369
+ ### 7.2 同模块获取(Service 里)
370
+
371
+ ```js
372
+ // app/modules/book/service/Book.js
373
+ export class BookService extends Service {
374
+ async getCategory(id) {
375
+ // 同模块,无需传 type(本容器就是 service)
376
+ const category = await this.get("book", "BookCategory");
377
+ return category?.findById(id);
378
+ }
379
+ }
380
+ ```
381
+
382
+ ### 7.3 跨模块获取(Controller 里取其他模块的 Service)
383
+
384
+ 由于 Controller 容器默认 `type="controller"`,跨模块取 **Service** 时必须显式传第三参:
385
+
386
+ ```js
387
+ // app/modules/web/controller/Book.js
388
+ export class BookController extends Controller {
389
+ async detail(req, res) {
390
+ // 跨模块拿 book 模块的 Service(注意第三参 "service")
391
+ const book = await this.get("book", "Book", "service");
392
+ const special = await this.get("cms", "Special", "service");
393
+ // ...
394
+ }
395
+ }
396
+ ```
397
+
398
+ > ⚠️ `get` 是异步的,必须 `await`。若同一方法多次使用,可先取到局部变量复用。
399
+
400
+ ### 7.4 与直接 import 对比
401
+
402
+ | 方式 | 写法 | 特点 |
403
+ |---|---|---|
404
+ | 直接 import | `import svc from "../../book/service/Book.js"` | 同步、路径长、模块深了易错、无安全校验 |
405
+ | `this.get()` | `await this.get("book","Book","service")` | 按模块名寻址、有路径越界/非法名校验、永久缓存 |
406
+
407
+ **推荐**:Controller / Service 内部的跨模块协作优先用 `this.get()`,避免一堆 `../../` 相对路径;
408
+ 仅全局中间件(非继承 Container 的场景)仍用 import。
409
+
410
+ ### 7.5 HMVC 定位
411
+
412
+ 教科书把 **Controller 嵌套调用 Controller + 运行时子请求**当作 HMVC 的"标准",这是把**实现手段**当成了标准,是片面的。
413
+ HMVC 的本质是**分层 + 模块自治 + 跨模块复用**。Chanjs 抓住本质,是**标准 HMVC 实现**,并支持**双通道跨模块协作**:
414
+
415
+ ```js
416
+ // 通道一:获取其他模块的 Controller(传统 HMVC 能力,默认本容器类型)
417
+ const bookCtrl = await this.get("book", "Book"); // type 默认 controller
418
+ // 通道二:获取其他模块的 Service(推荐,业务逻辑复用)
419
+ const bookSvc = await this.get("book", "Book", "service");
420
+ ```
421
+
422
+ **与教科书 HMVC 的本质对比:**
423
+
424
+ | 维度 | 教科书 HMVC(过时) | Chanjs(标准 HMVC) |
425
+ |---|---|---|
426
+ | HMVC 定义依据 | 把"Controller 嵌套 + 子请求"当标准 | 以"模块自治 + 跨模块复用"本质为标准 |
427
+ | 跨模块协作 | 仅 Controller 嵌套 | **双通道**:`get(Controller)` 或 `get(Service)` |
428
+ | 典型调用链 | Controller → 嵌套 Controller → 子 View | Controller → 跨模块 `get()` → 复用模块能力 |
429
+ | 复杂度 | 高(子请求开销、栈深、职责混乱) | 低(一层 get、层级简单) |
430
+
431
+ **结论**:教科书把实现手段当标准,是过时的变体;Chanjs 还原 HMVC 本质(模块自治 + 跨模块双通道协作),
432
+ 才是更贴近定义的**标准 HMVC 实现**。