---
description: Node Core（Koa2+Sequelize）启动、路由、鉴权与 Context 严格约束（详尽版，对照 node_core）
globs: "{app.js,api/**/*.js,config/**/*.js,middleware/**/*.js,services/**/*.js,models/**/*.js,node_core/**/*.js}"
alwaysApply: false
---

# Node Core 严格约束（详尽）

适用于基于 **`node_core`** 的业务后端；行为以 **`node_core/framework.js`**、**`middleware/baseRequest.js`**、**`router/baseController.js`** 及分发 **`node-core-framework.md`** 为准。

---

## 1. 静态入口 `Framework.init(config)` 执行顺序

模块导出 **`init`**（`framework.js` 末尾），顺序**固定**：

1. **`new Framework(config)`**  
   - 构造内 **`_validateConfig(config)`**：缺 `db` 必填子项、`apiPaths` 非法等会 **抛错终止**；`baseUrl` 缺失、`allowUrls` 非数组、`redis` 不完整等多为 **警告**。  
   - 创建 **`ServiceManager`**、**`RequestManager`**；挂载 **`initDb`**、**`initApi`** 方法引用。

2. **`_validatePackages()`**  
   - 若 **`config.skipPackageValidation === true`** 则跳过。  
   - 否则 **`PackageValidator.validate`**；若 **`strictPackageValidation === true`** 且失败则 **抛错**；否则仅 **warn**。

3. **`await initDb(config.modelPaths, config.businessAssociations)`** → 内部 **`_initDb`**：  
   - **`await _validateLicense(licensePath)`**：无授权码会 **`process.exit(1)`**（硬退出）。  
   - **`ModelManager`** 初始化库、**`initSysModels`**、**`_loadBusinessModels`**（目录下 `*.js`，函数则 `(db) => model(db)`）、**`mergeModels`**、**`setupBusinessModelAssociations`**（若传入）。  
   - 设置 **`dbInitialized = true`**。

4. **`await initServices()`**  
   - **`serviceManager.initServices()`**（含 Redis 等异步）；默认 **`servicesRequired !== false`** 时失败 **抛错**；若为 **`false`** 则 warn 并继续。

5. **`await initApi(config.beforeInitApi)`** → 内部 **`_initApi`**：  
   - 若 **`apiInitialized`** 已 true：warn 并 return。  
   - 若 **`!dbInitialized`**：**抛错** `Database must be initialized before API`。  
   - 然后进入 **§2** 管线。

6. **`return global_framework`**。

**业务侧**：`const framework = await Framework.init({ ... }); await framework.start(port);`；不得在 **`init` resolve 之前** 假设 DB/API 已就绪。

---

## 2. `_initApi` 内部顺序（中间件与路由）

1. **`_setupMiddleware()`**（顺序即 Koa 洋葱外层 → 内层）  
   - **`koa-static`**：`path.resolve(__dirname, "../upload")`（相对 **`node_core` 包内位置**），`extensions: ["html"]`。  
   - **`@koa/cors`**：`exposeHeaders: ["*"]`。  
   - **`koa-body`**：`multipart: true`，**`maxFileSize: 200 * 1024 * 1024`**（200MB）。  
   - **`this.requestManager.getBaseRequest()`**：Token 校验、**`ctx.*` 扩展**、日志等（见 §4）。

2. **`await beforeInitApi(this)`**（若存在）  
   - **仅此阶段**应调用 **`framework.addRoutes`**、**`framework.use`** 等，保证在 **Swagger 与路由表注册之前** 插入中间件或收集自定义路由。

3. **`_setupSwagger()`**（当构造时存在 `apiPaths` 即会调用；与 `_initApi` 内注释一致）  
   - 使用 **`modelManager.getAllModels()`** 等创建 Swagger 服务并挂载文档路由。

4. **`_setupRoutes(apiPaths)`**  
   - **`_registerSystemControllers`**：固定加载 **`controller/admin`** 下 **sys_*** 模块，前缀 **`/admin_api`**（见 §3）。  
   - **`baseController.loadControllers(apiPaths)`**：扫描业务目录。  
   - 将 **`this.customRoutes`**（由 **`addRoutes`** 累积）**push** 到 **`baseController.routes`**。  
   - **`baseController.init(router)`** 注册到 **koa-router**，再 **`app.use(router.routes())`**、**`router.allowedMethods()`**。

5. **`apiInitialized = true`**。

---

## 3. 系统控制器与业务控制器

### 3.1 系统模块（`_registerSystemControllers`）

手动 `require` 并注册（避免 webpack 动态路径问题）：

- **`sys_user`**、**`sys_menu`**、**`sys_role`**、**`sys_tenant`**、**`sys_parameter`**、**`sys_log`**

路由前缀：**`/admin_api`** + 各导出键路径。

### 3.2 业务 `apiPaths`

- 元素为 **字符串**：仅 **`path`**（目录），**无前缀**。  
- 元素为 **对象**：**`path`**（必填），**`prefix`**（可选），**`authType`**（可选，见 §5）。

### 3.3 `loadControllers` 文件规则

- 目录内 **`*.js`** 或 **`*_controller.js`** 会加载；其他文件忽略。  
- 每个文件 `require` 后的导出对象由 **`_parseController`** 解析。

### 3.4 导出键与处理器

- 键必须匹配 **`/^(GET|POST)\s+(.+)$/`**；否则忽略。  
- 处理器须为 **function**；签名 **`async (ctx, next) => {}`**，按需 **`await next()`**（系统控制器多为 **`async (ctx) =>`**）。  
- 注册路径：**`routePrefix`（来自 apiPaths） + 键中 path 段**；系统侧 **`routePrefix` 为 `/admin_api`**。

---

## 4. `addRoutes(prefix, routes)`（仅 `beforeInitApi` 内）

- **`routes`** 须为对象；否则 warn 并 return。  
- 对每个键：非 function 跳过；正则提取 **method** 与 **path**；最终 **`path: prefix ? prefix + match[2] : match[2]`**，**`module: 'custom'`**，push 到 **`this.customRoutes`**。  
- **`prefix` 为空字符串**时路径为键内裸 path（常用于与文档路径对齐）。

---

## 5. 认证、白名单与 `apiPaths` 匹配（`baseRequest.js`）

### 5.1 日志

- 进入中间件后：**`console.log('Process API', method, path)`** — **仅 path**，**不打印 query**（防敏感信息）。

### 5.2 白名单

- **`defaultAllowUrls`**：`/login`、`/register`、`/health`、`/docs`、`/swagger.json` 等（以源码为准）。  
- **`allowUrls`**：`[...defaultAllowUrls, ...(config.allowUrls || [])]`。  
- 匹配：**`ctx.request.path.indexOf(url) > -1`**（**子串**）。**禁止**配置过宽片段；新增白名单须为**明确路径片段**。

### 5.3 需鉴权时的 `apiPaths` 匹配

- 遍历 **`config.apiPaths`**：取 **`prefix`**（对象）或空（字符串形式无 prefix）。  
- 条件：**`ctx.request.path.indexOf(prefix + '/') > -1`**。  
- **注意**：`prefix` 建议带前导 **`/`** 且**无尾斜杠**；否则与业务 path 拼接易不匹配。  
- 命中后：**`authType === 'admin'`** → **`ctx.getAdminUserId()`** 非 0 视为通过；否则 **`ctx.getPappletUserId()`**。  
- 任一条 `apiPaths` 命中且 userId 有效 → **`next()`**；否则 **`ctx.tokenFail()`**。

### 5.4 `ctx.getPappletUserId`（小程序）

- 测试：**`ctx.request.query.userIdTest45`** 优先。  
- 否则解析头 **`applet-token`**。

---

## 6. Context 扩展详解

### 6.1 `ctx.get(id)`

- 合并 **`ctx.request.query`** 与 **`ctx.request.body`** 到一对象再取值。  
- 若合并结果为 **string**，会 **`JSON.parse`**（异常未在片段中捕获时需知悉风险）。  
- 取值：**`obj[id] === undefined ? "" : obj[id]`**（缺省为 **空字符串**）。

### 6.2 `ctx.getBody()`

- **`Object.assign({}, ctx.body, ctx.request.body)`**；若整体为 string 则 **`JSON.parse`**。  
- **不含 query**；写操作、JSON 体优先。

### 6.3 `ctx.getQuery()`

- 返回 **`ctx.request.query`**。

### 6.4 `ctx.getAdminUserId()` / `ctx.getAdminTenantId()`

- 读头 **`admin-token`**，**`tokenService.parse`**。  
- **租户**：若 payload 无 **`tenant_id`**（或 null/空串）则返回 **默认 `1`**；解析失败或无效为 **`0`**（与「未登录」区分以业务为准，源码为数字返回值）。

### 6.5 `ctx.getPageSize()`

- 读 **`ctx.get("pageOption")`**；若为字符串则 **`JSON.parse`**。  
- 返回 **`{ limit: pageSize || 20, offset: pageSize * (page - 1) }`**（**`page` 默认 1**）。

### 6.6 `ctx.getOrder(key)`

- **`key`** 默认 **`"order"`**；先 **`ctx.get(key)`**。  
- 若已是数组则直接返回；否则 **`eval("(" + order + ")")`** — **有注入风险**；新代码应避免把不可信字符串传入，或改为 JSON.parse + 校验。

### 6.7 `ctx.getIp()`

- **`x-forwarded-for`** 取第一段；否则 socket 地址。

### 6.8 响应辅助

| 方法 | 行为 |
|------|------|
| **`ctx.json(code, message, data)`** | `type: application/json`，`body: { code, message, data }` |
| **`ctx.success(data, msg)`** | **`ctx.json(0, msg \|\| '请求成功', data)`** |
| **`ctx.fail(msg)`** | **`ctx.json(-1, message, {})`**；若存在 **`ctx.errorCallback`** 则先调用 |
| **`ctx.tokenFail(data)`** | **`ctx.json(-2, '非法请求,或登录已超时', data)`** |
| **`ctx.downFile({ title, rows, cols })`** | CSV，**UTF-8 BOM**，`Content-Type: text/csv; charset=utf-8` |

### 6.9 中间件错误

- **`try/catch`** 包裹鉴权逻辑：异常时 **`logsService.ctxError`**，**`return ctx.fail(e.message)`**。

---

## 7. `start(port)`

- 若 **`!apiInitialized`**：**抛错** `API not initialized. Call initApi() first.`  
- **`this.app.listen(port)`** 返回 server。  
- **定时任务、后台循环**等须在 **`start` 成功之后**再启动（与框架规则一致）。

---

## 8. 静态资源与上传目录

- 默认静态目录指向包内 **`../upload`**；业务部署若需对外提供文件，应明确该目录在磁盘上的位置与权限，而非在控制器里随意 `sendFile` 到任意路径。

---

## 9. 数据模型：`DatabaseManager.define` 自动注入字段（`node_core/database/db.js`）

业务在 **`db.define(name, attributes)`**（或经 `ModelManager` 等价入口）里**只写业务字段**即可；框架会在 **`define()` 内统一合并**下列字段与行为，**无需、也不应在每个模型文件里重复声明** `create_time` / `last_modify_time` / `is_delete`（若 `attributes` 里写了同名键，会被框架定义**覆盖**）。

### 9.1 自动追加的三列

| 字段 | 类型与默认 | 说明 |
|------|------------|------|
| **`create_time`** | `DATE`，库默认 `CURRENT_TIMESTAMP`，`allowNull: false` | **创建时间**。`getter` 用 dayjs 格式化为 **`YYYY-MM-DD HH:mm:ss`** 字符串返回；`setter` 接受 `Date` 或可解析字符串。 |
| **`last_modify_time`** | `DATE`，库默认 `CURRENT_TIMESTAMP`，`allowNull: false` | **最后修改时间**。读写格式与 `create_time` 相同。 |
| **`is_delete`** | `INTEGER(1)`，默认 **`0`**，`allowNull: false` | **软删标记**：`0` 正常，`1` 已删；`is_swagger: false`（可按生成器策略处理）。 |

### 9.2 `define` 选项与默认作用域

- **`tableName`**：与模型名 **`name`** 一致（即表名与 `define` 第一个参数相同）。  
- **`timestamps: false`**：不使用 Sequelize 内置 `createdAt`/`updatedAt`，改由上述 **`create_time` / `last_modify_time`** 维护。  
- **`defaultScope`**：  
  - **`where: { is_delete: 0 }`** — 普通 **`findAll` / `findOne` / `findByPk`** 等**默认只查未删除行**。  
  - **`attributes: { exclude: ['is_delete'] }`** — 默认查询结果里**不选出 `is_delete` 列**（行仍受 `where` 约束）。  
- 需要查已删数据或显式带出 **`is_delete`** 时，须使用 **Sequelize 作用域 API**（如 **`Model.unscoped()`** / **`Model.scope(null)`** 等，以项目所用 Sequelize 版本文档为准）**临时去掉 `defaultScope`**，再自行写 **`where`**；业务仓库内若未封装，不要随意全表无 `where` 查询。

### 9.3 模型钩子（与操作日志、时间戳）

- **`beforeCreate`**：若传入的 `create_time` / `last_modify_time` 无效则删除让库/默认值处理；未设置则填 **`new Date()`**。  
- **`afterCreate`**：在排除表之外写 **`sys_log`**「新增」日志；必要时补全空的时间字段。  
- **`beforeUpdate`**：对非 **`sys_log`** 表强制 **`last_modify_time = new Date()`**；并取更新前一行与当前值对比写 **`sys_log`**「修改」日志（字段 diff 时**忽略** `create_time` / `last_modify_time` 的变化展示规则见 `_logEditField`）。  
- **`afterDestroy`**：写 **`sys_log`**「删除」日志（若业务走物理 `destroy`）；**业务侧软删推荐**与系统控制器一致：使用 **`update({ is_delete: 1 }, { where: { id, is_delete: 0 } })`**，而不是依赖 `destroy()`。

### 9.4 列表与统计查询约定

- **列表/详情**：在 `defaultScope` 下已隐含 **`is_delete: 0`**；若手写 **`raw: true`** 或复杂 **`include`**，仍应显式带上 **`is_delete: 0`**（或与产品确认的可删数据范围），避免误查出已删行。  
- **Swagger / 文档**：框架侧会把 **`create_time` / `last_modify_time` / `is_delete`** 纳入 schema 相关约定（见 **`services/swagger.js`** 等），与表结构保持一致即可。

---

## 10. 控制器编写规范（与既有严格条目一致）

- **入参**：**`snake_case`**；**禁止** `body.a ?? body.b` 双字段兼容。  
- **异常**：**不要**在控制器外层包一层「万能 try/catch」吞栈；业务分支用 **`ctx.fail`**；未捕获异常交给全局（若项目有统一错误中间件）。  
- **`if`**：**必须**使用大括号，**禁止** `if (cond) return ctx.fail(...);` 单行无括号写法。  
- **模型/服务**：**`Framework.getModels()`**、**`Framework.getServices()`**（或 **`Framework.getInstance()`**）须在 **`init` 完成之后**调用。

---

## 11. 配置校验摘要（`_validateConfig`）

| 配置 | 严重级别 |
|------|-----------|
| 缺 `db` / 缺 `host|username|password|database|dialect` | **error** |
| `apiPaths` 非非空数组 / 项缺 `path` | **error** |
| 缺 `baseUrl` | **warning**（Swagger 等可能异常） |
| `allowUrls` 非数组 | **warning** |
| `redis` 缺 host/port | **warning** |

---

## 12. 提交前自检（后端）

- [ ] 新增路由是否在 **`beforeInitApi`** 内 **`addRoutes`** 或业务控制器导出 **`GET`/`POST`** 键？  
- [ ] **`apiPaths`** 的 **`prefix`** 是否与 **`indexOf(prefix + '/')`** 匹配方式一致？  
- [ ] **`allowUrls`** 是否尽量短且明确、避免误放行？  
- [ ] 读 body 是否优先 **`ctx.getBody()`**，避免与 query 混用？  
- [ ] 是否避免 **`ctx.getOrder`** 处理不可信字符串？  
- [ ] 响应是否统一 **`ctx.success` / `ctx.fail` / `ctx.tokenFail`**？  
- [ ] 控制器是否无万能 try/catch、且 **if 均带大括号**？  
- [ ] 列表/更新/删除是否默认带上 **`is_delete: 0`**（或与 `defaultScope` 一致），软删是否用 **`update({ is_delete: 1 })`**？
