# Node Core 框架 API 文档2

本文档与仓库源码一致，说明 **HTTP 接口**、**业务控制器约定**、**Koa Context 扩展**、**框架模块导出（`init` / `getModels` 等）** 及 **内置服务**。

---

## 目录

1. [基础约定](#1-基础约定)
2. [认证与白名单](#2-认证与白名单)
3. [Swagger 与 OpenAPI](#3-swagger-与-openapi)
4. [内置管理端 REST API（`/admin_api`）](#4-内置管理端-rest-apiadmin_api)
5. [自定义业务控制器](#5-自定义业务控制器)（含 [手动添加路由](#54-手动添加路由addroutes)）
6. [Context 方法参考](#6-context-方法参考)
7. [框架 Node 模块 API](#7-框架-node-模块-api)
8. [内置服务（`getServices()`）](#8-内置服务getservices)
9. [业务项目集成与配置](#9-业务项目集成与配置)
10. [内部方法与外部注入详解](#10-内部方法与外部注入详解)
11. [注意事项](#11-注意事项)
12. [常见问题排查（FAQ）](#12-常见问题排查faq)

---

## 1. 基础约定

### 1.1 完整 URL

业务项目在配置里通过 `apiPaths` 为「控制器目录」指定路由前缀 `prefix`。最终地址为：

```text
http(s)://<host>:<port><prefix><控制器内定义的路径>
```

框架**内置系统管理控制器**固定注册在前缀 **`/admin_api`** 下（见 `framework.js` 中 `_registerSystemControllers`）。例如控制器键名为 `"POST /sys_user/login"` 时，完整路径为：

```text
POST /admin_api/sys_user/login
```

业务项目自己的控制器由 `apiPaths[].path` 加载，键名同样为 `"GET /xxx"` / `"POST /xxx"`，再拼上所配置的 `prefix`。

### 1.2 统一 JSON 响应

成功与业务失败均可能返回下列结构（`middleware/baseRequest.js` 中 `ctx.json` / `ctx.success` / `ctx.fail` / `ctx.tokenFail`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | `number` | `0` 成功；`-1` 业务失败；`-2` 未登录或 token 无效 |
| `message` | `string` | 提示文案 |
| `data` | `any` | 载荷；失败时可能为 `{}` |

示例：

```json
{
  "code": 0,
  "message": "请求成功",
  "data": {}
}
```

**特例：** 部分接口直接输出文件流（如用户导出 CSV），不返回上述 JSON。

### 1.3 请求参数合并规则

`ctx.get(name)` 将 **Query** 与 **Body** 合并后按字段名读取（实现为 `Object.assign({}, query, body)`）。同名字段以 **body 覆盖 query**。

### 1.4 HTTP 方法

控制器导出对象的键必须为 **`GET`** 或 **`POST`**，格式：

```text
<GET|POST> <空格> <路径>
```

路径中可含动态段（由 `koa-router` 解析）。删除、更新类操作在业务上常用 `POST`。

---

## 2. 认证与白名单

### 2.1 默认免登录路径（子串匹配）

请求 **path** 任意位置包含下列子串之一时，**不校验** token（`baseRequest.js`）：

- `/login`
- `/register`
- `/health`
- `/docs`
- `/swagger.json`

### 2.2 配置 `allowUrls`

配置项 `allowUrls: ['/your/public', ...]` 与上述默认列表合并；命中规则同样是 **path 包含子串**。

### 2.3 `apiPaths` 与 `authType`

`apiPaths` 每一项为**字符串**（仅作前缀，旧用法）或**对象**：

```js
{
  path: './controller',
  prefix: '/admin_api',
  authType: 'admin' // 可选：'admin' | 省略时默认 'applet'
}
```

- **`authType === 'admin'`**：从请求头 **`admin-token`** 解析 JWT，得到后台用户；`ctx.getAdminUserId()` 返回用户 id，无效则为 `0`。
- **默认 `applet`**：从 **`applet-token`** 解析；`ctx.getPappletUserId()` 返回用户 id。开发时若 query 带 **`userIdTest45`**，会优先返回该值（便于联调）。

未命中白名单且未通过对应前缀的鉴权时，返回 **`code: -2`**，`message: "非法请求,或登录已超时"`（`ctx.tokenFail`）。

**管理端请求头示例：**

```http
admin-token: <登录返回的 token>
Content-Type: application/json
```

---

## 3. Swagger 与 OpenAPI

| 路径 | 说明 |
|------|------|
| `GET /api/docs` | Swagger UI 页面（HTML） |
| `GET /api/swagger.json` | OpenAPI JSON（由 `SwaggerService.generateSpecs` 生成） |

**Swagger 页相关辅助接口**（`lib/swaggerUIManager.js`，响应格式与业务 JSON 约定独立）：

| 路径 | 方法 | 说明 |
|------|------|------|
| `/api/swagger/security-config` | GET | 读取 Cookie `swagger_security_config` 中的安全配置 |
| `/api/swagger/security-config` | POST | Body：`{ securityConfig: Array }`，写入 Cookie |
| `/api/swagger/security-config` | DELETE | 清除该 Cookie |

---

## 4. 内置管理端 REST API（`/admin_api`）

下文 **`BASE`** = `http(s)://<host>:<port>`。系统接口完整路径为 **`BASE + /admin_api + 下表路径`**。

**通用请求约定：**

- 需登录接口：请求头带 **`admin-token`**。
- 参数可通过 **Query** 或 **JSON Body** 传递（见 §1.3）。
- 成功时 `code === 0`，具体数据在 **`data`**（除非另有说明）。

---

### 4.1 用户模块 `sys_user`

源码：`controller/admin/sys_user.js`。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_user/index` | GET | admin | 用户列表。当前登录租户为平台租户时看全部；普通租户仅看本租户。 |
| `/sys_user/login` | POST | 免登录 | 登录：`name`、`password`，可选 `tenant_code`。成功返回 `token`、`user`、`authorityMenus`、`tenant`。 |
| `/sys_user/export` | POST | admin | **CSV 文件流**：列为模型字段注释或字段名，行为未删除用户；非 JSON。 |
| `/sys_user/authorityMenus` | POST | admin | 当前登录用户可见菜单。`role.type=0` 按 `menus` 过滤；`role.type=1` 返回全部未删菜单。 |
| `/sys_user/add` | POST | admin | 新增用户。平台租户可指定 `tenant_id`；普通租户固定写入当前租户。 |
| `/sys_user/edit` | POST | admin | 编辑用户。支持 `name`、`roleId`、`password`、`tenant_id(平台租户)`。 |
| `/sys_user/del` | POST | admin | 软删除：`id`，`is_delete` 置 1。 |

**`POST /sys_user/login` 请求 body（完整示例）：**

```json
{
  "name": "admin",
  "password": "123456",
  "tenant_code": "default"
}
```

**`POST /sys_user/login` 响应 `data`（成功示例）：**

```json
{
  "token": "<jwt>",
  "user": {
    "id": 1,
    "name": "admin",
    "roleId": 1,
    "tenant_id": 1
  },
  "authorityMenus": [1, 2, 3, 10],
  "tenant": { "id": 1, "name": "", "code": "default", "is_platform": 1 }
}
```

**`POST /sys_user/add` 请求 body（完整示例）：**

```json
{
  "name": "ops_user",
  "password": "123456",
  "roleId": 2,
  "tenant_id": 2
}
```

**`POST /sys_user/edit` 请求 body（完整示例）：**

```json
{
  "id": 12,
  "name": "ops_user_2",
  "password": "12345678",
  "roleId": 3,
  "tenant_id": 2
}
```

**`POST /sys_user/del` 请求 body（完整示例）：**

```json
{
  "id": 12
}
```

**`POST /sys_user/authorityMenus` 请求 body（完整示例）：**

```json
{}
```

**`POST /sys_user/export` 请求 body（完整示例）：**

```json
{}
```

**响应示例（成功）：**

```json
{
  "/sys_user/index": { "code": 0, "message": "请求成功", "data": [{ "id": 1, "name": "admin", "role": { "id": 1, "name": "管理员" }, "tenant": { "id": 1, "code": "default" } }] },
  "/sys_user/authorityMenus": { "code": 0, "message": "请求成功", "data": [{ "id": 10, "name": "用户管理", "path": "/sys/user" }] },
  "/sys_user/export": "CSV 文件流（非 JSON，Content-Disposition: attachment）",
  "/sys_user/add": { "code": 0, "message": "请求成功", "data": { "id": 12, "name": "ops_user", "roleId": 2, "tenant_id": 2 } },
  "/sys_user/edit": { "code": 0, "message": "请求成功", "data": [1] },
  "/sys_user/del": { "code": 0, "message": "请求成功", "data": [1] }
}
```

**curl 示例：**

```bash
curl -X POST "$BASE/admin_api/sys_user/login" -H "Content-Type: application/json" \
  -d "{\"name\":\"admin\",\"password\":\"your_password\",\"tenant_code\":\"default\"}"

curl "$BASE/admin_api/sys_user/index" -H "admin-token: <token>"
```

---

### 4.2 菜单与低代码 `sys_menu` / `model` / `form`

源码：`controller/admin/sys_menu.js`，依赖 `platformProjectService`（远程平台与 `config.project_key`）。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_menu/index` | GET | admin | 菜单列表，`sort` 升序；每条增加 `parent_node_ids`。 |
| `/sys_menu/add` | POST | admin | 创建菜单，并尝试触发表单创建/代码下载。 |
| `/sys_menu/edit` | POST | admin | `id` + `body` 更新字段。 |
| `/sys_menu/del` | POST | admin | 软删除：`id`。 |
| `/model/all` | POST | admin | 平台模型列表（内部 `projectKey`）；失败返回 `[]`。 |
| `/form/generate` | POST | admin | 参数 **`id`**（菜单 id）。按菜单关联表单更新并生成，下载资源；成功 `frontApiUrl`、`frontVueUrl`；失败「生成失败」。 |
| `/model/generate` | POST | admin | 参数 **`id`**（菜单 id）。按关联 `model_id` 生成后端代码并下载；成功返回 `controllerPath`、`modelPath` 等；失败「生成失败」。 |
| `/model/interface` | POST | admin | Body：`model_key`（模型名，须已在 `getModels()` 中存在）、`map_option: { key, value }`。返回 `[{ key, value }, ...]`；模型不存在则业务失败「数据模型不存在」。 |

**`POST /sys_menu/add` 请求 body（完整示例）：**

```json
{
  "name": "订单管理",
  "parent_id": 0,
  "icon": "ios-list-box",
  "path": "/order/index",
  "type": "页面",
  "model_id": 5,
  "form_id": 0,
  "component": "views/order/index.vue",
  "api_path": "/api/order",
  "is_show_menu": 1,
  "is_show": 1,
  "sort": 10
}
```

**`POST /sys_menu/edit` 请求 body（完整示例）：**

```json
{
  "id": 20,
  "name": "订单中心",
  "icon": "ios-paper",
  "path": "/order/list",
  "sort": 11
}
```

**`POST /sys_menu/del` / `/form/generate` / `/model/generate` 请求 body（完整示例）：**

```json
{
  "id": 20
}
```

**`POST /model/all` 请求 body（完整示例）：**

```json
{}
```

**`POST /model/interface` 请求 body（完整示例）：**

```json
{
  "model_key": "sys_role",
  "map_option": {
    "key": "id",
    "value": "name"
  }
}
```

**响应示例（成功）：**

```json
{
  "/sys_menu/index": { "code": 0, "message": "请求成功", "data": [{ "id": 20, "name": "订单管理", "parent_node_ids": [0] }] },
  "/sys_menu/add": { "code": 0, "message": "请求成功", "data": { "frontApiUrl": "https://xx/api.js", "frontVueUrl": "https://xx/index.vue" } },
  "/sys_menu/edit": { "code": 0, "message": "请求成功", "data": [1] },
  "/sys_menu/del": { "code": 0, "message": "请求成功", "data": [1] },
  "/model/all": { "code": 0, "message": "请求成功", "data": [{ "id": 5, "name": "order" }] },
  "/form/generate": { "code": 0, "message": "请求成功", "data": { "frontApiUrl": "https://xx/api.js", "frontVueUrl": "https://xx/index.vue" } },
  "/model/generate": { "code": 0, "message": "请求成功", "data": { "controllerPath": "https://xx/controller.js", "modelPath": "https://xx/model.js" } },
  "/model/interface": { "code": 0, "message": "请求成功", "data": [{ "key": 1, "value": "管理员" }] }
}
```

---

### 4.3 角色模块 `sys_role`

源码：`controller/admin/sys_role.js`。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_role/index` | GET | admin | 全部未删除角色。 |
| `/sys_role/add` | POST | admin | 新增角色，至少传 `name`。 |
| `/sys_role/edit` | POST | admin | 编辑角色，传 `id` 和更新字段。 |
| `/sys_role/del` | POST | admin | 软删除：`id`。 |

**`POST /sys_role/add` 请求 body（完整示例）：**

```json
{
  "name": "运营角色",
  "menus": [1, 2, 3, 20]
}
```

**`POST /sys_role/edit` 请求 body（完整示例）：**

```json
{
  "id": 3,
  "name": "运营管理员",
  "menus": [1, 2, 3, 4, 20]
}
```

**`POST /sys_role/del` 请求 body（完整示例）：**

```json
{
  "id": 3
}
```

**响应示例（成功）：**

```json
{
  "/sys_role/index": { "code": 0, "message": "请求成功", "data": [{ "id": 1, "name": "管理员", "menus": [1, 2, 3] }] },
  "/sys_role/add": { "code": 0, "message": "请求成功", "data": { "id": 3, "name": "运营角色" } },
  "/sys_role/edit": { "code": 0, "message": "请求成功", "data": [1] },
  "/sys_role/del": { "code": 0, "message": "请求成功", "data": [1] }
}
```

---

### 4.4 系统参数 `sys_parameter`

源码：`controller/admin/sys_parameter.js`。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_parameter/index` | GET | admin | `is_modified=0` 且 `is_delete=0` 的参数列表。 |
| `/sys_parameter/key` | GET | admin | 查询参数 **`key`**，返回单条或未命中。 |
| `/sys_parameter/setSysConfig` | POST | admin | Body：`title`、`logoUrl`，分别更新 `key=sys_title`、`key=sys_logo` 的 `value`。 |
| `/sys_parameter/add` | POST | admin | 新增：整表 `body`。 |
| `/sys_parameter/edit` | POST | admin | `id` + `body` 更新。 |
| `/sys_parameter/del` | POST | admin | 软删除：`id`。 |

**`POST /sys_parameter/setSysConfig` 请求 body（完整示例）：**

```json
{
  "title": "Node Core 管理后台",
  "logoUrl": "https://cdn.example.com/logo.png"
}
```

**`POST /sys_parameter/add` 请求 body（完整示例）：**

```json
{
  "key": "sys_theme",
  "value": "dark",
  "name": "系统主题",
  "is_modified": 0
}
```

**`POST /sys_parameter/edit` 请求 body（完整示例）：**

```json
{
  "id": 8,
  "value": "light",
  "name": "系统主题(浅色)"
}
```

**`POST /sys_parameter/del` 请求 body（完整示例）：**

```json
{
  "id": 8
}
```

**响应示例（成功）：**

```json
{
  "/sys_parameter/index": { "code": 0, "message": "请求成功", "data": [{ "id": 1, "key": "sys_title", "value": "管理后台" }] },
  "/sys_parameter/key": { "code": 0, "message": "请求成功", "data": { "id": 1, "key": "sys_title", "value": "管理后台" } },
  "/sys_parameter/setSysConfig": { "code": 0, "message": "请求成功", "data": {} },
  "/sys_parameter/add": { "code": 0, "message": "请求成功", "data": { "id": 8, "key": "sys_theme", "value": "dark" } },
  "/sys_parameter/edit": { "code": 0, "message": "请求成功", "data": [1] },
  "/sys_parameter/del": { "code": 0, "message": "请求成功", "data": [1] }
}
```

---

### 4.5 日志模块 `sys_log`

源码：`controller/admin/sys_log.js`。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_log/all` | GET | admin | 日志目录下文件名列表（倒序），项为 `{ title: 文件名 }`。 |
| `/sys_log/detail` | GET | admin | Query **`title`**：文件名，返回文件文本内容。 |
| `/sys_log/delete` | GET | admin | 删除单个文件：`title`。 |
| `/sys_log/delete_all` | GET | admin | 清空日志目录下所有文件（慎用）。 |
| `/sys_log/operates` | GET | admin | 数据库操作记录分页：`ctx.getPageSize()`，需传 **`pageOption`**（JSON 字符串或对象），如 `{"page":1,"pageSize":20}`。返回 Sequelize `findAndCountAll` 结果（含 `rows`、`count`）。 |

**操作记录写入规则**：`database/db.js` 在 `define` 模型的 Sequelize 钩子中，对符合条件的表在**新增、修改、删除**时写入 `sys_log`。`sys_log` 表本身**从不**记操作日志。若需在业务库中排除其它表（如高频临时表、会话表），在 **`db.operateLogExcludeTableNames`** 中传入与模型 **`tableName`** 一致的字符串数组即可，命中表名将不写入操作日志（修改时仍会更新 `last_modify_time`）。详见 [§9.1.1](#911-db-可选字段扩展)。

**GET 请求示例：**

```bash
curl "$BASE/admin_api/sys_log/all" -H "admin-token: <token>"
curl "$BASE/admin_api/sys_log/detail?title=app-2026-04-15.log" -H "admin-token: <token>"
curl "$BASE/admin_api/sys_log/delete?title=app-2026-04-15.log" -H "admin-token: <token>"
curl "$BASE/admin_api/sys_log/delete_all" -H "admin-token: <token>"
curl "$BASE/admin_api/sys_log/operates?pageOption=%7B%22page%22%3A1%2C%22pageSize%22%3A20%7D" -H "admin-token: <token>"
```

**响应示例（成功）：**

```json
{
  "/sys_log/all": { "code": 0, "message": "请求成功", "data": [{ "title": "app-2026-04-15.log" }] },
  "/sys_log/detail": { "code": 0, "message": "请求成功", "data": "日志文件文本内容..." },
  "/sys_log/delete": { "code": 0, "message": "请求成功", "data": {} },
  "/sys_log/delete_all": { "code": 0, "message": "请求成功", "data": {} },
  "/sys_log/operates": { "code": 0, "message": "请求成功", "data": { "count": 100, "rows": [{ "id": 1, "table_name": "sys_user", "operate": "登录" }] } }
}
```


---

### 4.7 租户模块 `sys_tenant`

源码：`controller/admin/sys_tenant.js`。

| 路径 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/sys_tenant/index` | GET | admin | 平台租户看全部租户；普通租户仅看自身。 |
| `/sys_tenant/add` | POST | admin | 仅平台租户可新增。`code` 可不传，不传时自动生成。 |
| `/sys_tenant/edit` | POST | admin | 仅平台租户可编辑。 |
| `/sys_tenant/del` | POST | admin | 仅平台租户可删除，且禁止删除默认租户（`id=1`）。 |

**`POST /sys_tenant/add` 请求 body（完整示例）：**

```json
{
  "name": "华东租户A",
  "code": "tenant_east_a",
  "remark": "华东区域项目",
  "status": 1
}
```

**`POST /sys_tenant/edit` 请求 body（完整示例）：**

```json
{
  "id": 2,
  "name": "华东租户A-正式",
  "code": "tenant_east_a_prod",
  "remark": "正式环境",
  "status": 1
}
```

**`POST /sys_tenant/del` 请求 body（完整示例）：**

```json
{
  "id": 2
}
```

**响应示例（成功）：**

```json
{
  "/sys_tenant/index": { "code": 0, "message": "请求成功", "data": [{ "id": 1, "name": "默认租户", "code": "default", "status": 1 }] },
  "/sys_tenant/add": { "code": 0, "message": "请求成功", "data": { "id": 2, "name": "华东租户A", "code": "tenant_east_a" } },
  "/sys_tenant/edit": { "code": 0, "message": "请求成功", "data": 1 },
  "/sys_tenant/del": { "code": 0, "message": "请求成功", "data": [1] }
}
```

---

## 5. 自定义业务控制器

### 5.1 路由约定

- 目录内 `*.js` 被加载；导出对象为 **`"GET /path"` / `"POST /path"`** → 处理器 **`async (ctx, next) => {}`**。
- 完整路径 = `prefix + 路径`（`router/baseController.js`）。
- 在处理器中通过 **`Framework.getModels()`**（需在 `init` 完成 DB 后）访问模型；通过 **`Framework.getServices()`** 访问内置服务。

### 5.2 与内置 `/admin_api` 的关系

系统管理控制器由核心包**内置注册**，不依赖业务 `apiPaths`。业务可再配置其它 `prefix`（如 `/app_api`）挂载自己的控制器。

### 5.3 控制器文件示例

```js
const Framework = require('@project/core'); // 或实际包名 / 相对路径

module.exports = {
  'GET /goods/list': async (ctx) => {
    const models = Framework.getModels();
    const rows = await models.goods.findAll({ where: { is_delete: 0 } });
    return ctx.success(rows);
  },
  'POST /goods/save': async (ctx) => {
    const row = ctx.getBody();
    const id = ctx.get('id');
    return ctx.success({ id });
  }
};
```

### 5.4 手动添加路由（`addRoutes`）

除了通过 `apiPaths` 配置控制器目录自动扫描之外，还可以在 `beforeInitApi` 回调中调用 `framework.addRoutes(prefix, routes)` **手动注册路由**。

手动注册的路由与控制器目录扫描的路由**统一注册到 `koa-router`**，支持路径参数（`:id`），并会出现在启动时的路由列表打印中。

**方法签名：**

```js
framework.addRoutes(prefix, routes)
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `prefix` | `string` | 路由前缀，如 `'/api'`；不需要前缀时传 `''` |
| `routes` | `object` | 路由定义对象，键名格式 `"GET /path"` 或 `"POST /path"`，值为 `async (ctx) => {}` 处理函数 |

**使用示例：**

```js
const framework = await Framework.init({
  // ...其他配置
  beforeInitApi: async (framework) => {
    // 手动添加带前缀的路由
    framework.addRoutes('/api', {
      'GET /custom/list': async (ctx) => {
        ctx.success([]);
      },
      'POST /custom/create': async (ctx) => {
        const { name } = ctx.request.body;
        ctx.success({ id: 1, name });
      }
    });

    // 不带前缀的路由
    framework.addRoutes('', {
      'GET /health': async (ctx) => {
        ctx.body = { status: 'ok', timestamp: new Date().toISOString() };
      }
    });
  }
});
```

上述示例将注册以下路由：

| 方法 | 路径 |
|------|------|
| GET | `/api/custom/list` |
| POST | `/api/custom/create` |
| GET | `/health` |

> **注意：** `addRoutes` 必须在 `beforeInitApi` 回调中调用，不能在 `init()` 完成之后调用，否则路由不会被注册。

---

## 6. Context 方法参考

由 `middleware/baseRequest.js` 注入（业务控制器与系统控制器均可使用）：

| 方法 | 说明 |
|------|------|
| `ctx.get(name)` | 合并 query + body 后取字段；缺失返回 `""`。 |
| `ctx.getQuery()` | 仅 query 对象。 |
| `ctx.getBody()` | body（与部分兼容字段合并）。 |
| `ctx.getIp()` | 客户端 IP，支持 `x-forwarded-for` 第一段。 |
| `ctx.getAdminUserId()` | 解析 `admin-token`，返回用户 id 或 `0`。 |
| `ctx.getPappletUserId()` | 解析 `applet-token` 或调试 query `userIdTest45`。 |
| `ctx.getPageSize()` | 从 `pageOption` 解析 `limit`、`offset`（默认 `page=1`，`pageSize=20`）。 |
| `ctx.getOrder(key)` | 默认从 `order` 字段解析排序数组（支持字符串 eval 解析，异常则 `[]`）。 |
| `ctx.downFile({ title, rows, cols })` | 导出 CSV，UTF-8 BOM，`cols[].caption` 为表头。 |
| `ctx.json(code, message, data)` | 原始 JSON 输出。 |
| `ctx.success(data, msg)` | `code: 0`。 |
| `ctx.fail(msg)` | `code: -1`；若存在 `ctx.errorCallback` 会调用。 |
| `ctx.tokenFail(data)` | `code: -2`，默认提示「非法请求,或登录已超时」。 |

---

## 7. 框架 Node 模块 API

入口：`index.js` → 导出 `framework.js` 的聚合对象。

### 7.1 顶层导出

| 导出 | 说明 |
|------|------|
| `init(config)` | **异步**：校验配置 → `_validatePackages` → `initDb` → `initServices` → `initApi`；返回 **Framework 实例**。 |
| `getInstance()` | 当前全局 Framework 实例（`init` 之后）。 |
| `getModels()` | 等价于实例 `getModels()`；未初始化则为 `null`。 |
| `getSequelize()` | Sequelize 实例。 |
| `getDB()` | 与实例 `getDB()` 一致（见下）。 |
| `getServices()` | `initServices` 返回的对象（含 `tokenService`、`logsService` 等）。 |
| `getPlatformProjectService()` | `platformProjectService` 快捷方式。 |
| `Framework` | 类，可 `new Framework(config)`（一般业务用 `init`）。 |

### 7.2 Framework 实例方法

| 方法 | 说明 |
|------|------|
| `start(port)` | **异步**：监听端口；**必须先** `initApi` 完成（即通过 `init()` 一次性跑完）。 |
| `addRoutes(prefix, routes)` | 手动注册路由（须在 `beforeInitApi` 中调用），详见 [§5.4](#54-手动添加路由addroutes)。 |
| `use(middleware)` | 向 Koa `app` 追加中间件。 |
| `getApp()` | Koa 应用实例。 |
| `getModels()` | 合并后的模型对象（系统表 + 业务表）。 |
| `getSequelize()` | Sequelize 实例。 |
| `getDB()` | 当前 `framework.js` 实现返回 `this.db`，但构造函数未给 `this.db` 赋值，**可能为 `undefined`**；请优先使用 **`getSequelize()`** 或 **`getModels()`**，或在业务层封装修复后再用 `getDB()`。 |
| `getServices()` | 见 §8。 |
| `getPlatformProjectService()` | 平台项目服务实例。 |
| `query(sql)` | 执行原始 SQL（`modelManager.core.querySql`）。 |
| `syncDatabase()` | 同步数据库结构（慎用生产环境）。 |

**推荐启动顺序（与 `examples/index.js` 一致）：**

```js
const core = require('@project/core'); // 包名以实际为准

const framework = await core.init({
  db: {
    /* host, username, password, database, dialect */
    // operateLogExcludeTableNames: ['cache_row', 'user_session'], // 可选：这些 tableName 不写 sys_log 操作记录
  },
  baseUrl: 'http://localhost:3000',
  apiPaths: [{ path: require('path').join(__dirname, 'controller'), prefix: '/api', authType: 'admin' }],
  allowUrls: ['/health'],
  licensePath: '...',
  modelPaths: '...',
  beforeInitApi: async (fw) => { /* 在路由前挂中间件 */ }
});

await framework.start(3000);
```

---

## 8. 内置服务（`getServices()`）

`initServices()` 完成后，`getServices()` 返回对象包含（`services/index.js`）：

| 键 | 类型 | 说明 |
|----|------|------|
| `tokenService` | `TokenService` | JWT 签发/解析、`getMd5`、`decryptWxData` 等。 |
| `logsService` | `LogsService` | 文件日志目录、错误记录等。 |
| `redisService` | `RedisService` | 未配置 `redis` 时可能未完整初始化。 |
| `platformProjectService` | `PlatformProjectService` | 低代码平台 HTTP 调用、文件下载。 |
| `createSwaggerService` | `function` | `createSwaggerService(models)` → Swagger 文档服务实例。 |

**TokenService（`services/token.js`）常用方法：**

| 方法 | 说明 |
|------|------|
| `getMd5(str)` | MD5 十六进制字符串。 |
| `create(userInfo)` | `jwt.sign`，载荷写入 token。 |
| `parse(token)` | `jwt.verify`，失败返回 `null`。 |
| `verify(token)` | 布尔是否有效。 |

### 8.1 LogsService 用法（`services/logs.js`）

| 方法 | 说明 | 示例 |
|------|------|------|
| `log(msg, data?)` | 记录普通日志 | `logsService.log('用户登录', { id: 1 })` |
| `error(msg, err?)` | 记录错误日志 | `logsService.error('下单失败', error)` |
| `ctxError(error, ctx)` | 记录请求上下文错误 | `logsService.ctxError(error, ctx)` |
| `writeSlowSQL(sql, timing)` | 记录慢 SQL | `logsService.writeSlowSQL(sql, 850)` |

日志文件说明：

- 普通日志：`log_YYYY.MM.DD.log`
- 错误日志：`logError_YYYY.MM.DD.log`
- 慢 SQL：`slowSQL_YYYY.MM.DD.log`
- 单文件超过 5MB 自动滚动为 `_1.log`、`_2.log`...

### 8.2 RedisService 用法（`services/redis.js`）

| 方法 | 返回 | 说明 |
|------|------|------|
| `await init()` | `boolean` | 初始化并连接 Redis |
| `await set(key, value, expire?)` | `void` | 设置缓存（默认 12 小时） |
| `await get(key)` | `string \| null` | 读取缓存 |
| `await del(key)` | `number` | 删除缓存 |
| `await setIfAbsent(key, value, expire?)` | `boolean` | 原子写入（NX + EX） |
| `await waitForConnection(timeout?)` | `boolean` | 等待连接就绪 |
| `close()` | `void` | 关闭连接 |

示例：

```js
const { redisService } = Framework.getServices();
await redisService.set('order_lock_1001', '1', 30);
const locked = await redisService.get('order_lock_1001');
if (locked) {
  // ... 业务逻辑
}
await redisService.del('order_lock_1001');
```

### 8.3 PlatformProjectService 用法（`services/platformProject.js`）

| 方法 | 说明 |
|------|------|
| `modelAll()` | 获取平台模型列表 |
| `createForm(row)` / `updateForm(row)` | 创建/更新表单 |
| `formGenerate({ id })` | 生成前端资源地址 |
| `modelGenerate({ id })` | 生成后端资源地址 |
| `downloadPlatformFile(url)` | 下载平台文件到本地项目 |

示例：

```js
const { platformProjectService } = Framework.getServices();
const models = await platformProjectService.modelAll();
const form = await platformProjectService.createForm({
  name: '订单管理',
  model_id: 5,
  api_path: '/api/order',
  component: 'views/order/index.vue',
});
const { frontApiUrl, frontVueUrl } = await platformProjectService.formGenerate({ id: form.id });
await platformProjectService.downloadPlatformFile(frontApiUrl);
await platformProjectService.downloadPlatformFile(frontVueUrl);
```

### 8.4 Swagger 服务用法（`createSwaggerService`）

`getServices().createSwaggerService(models)` 可手动创建 SwaggerService（通常框架自动创建即可）：

```js
const services = Framework.getServices();
const models = Framework.getModels();
const swaggerService = services.createSwaggerService(models);
const specs = swaggerService.generateSpecs();
```

### 8.5 服务层注意事项

- `ServiceManager.getServices()` 在未完成初始化时会触发异步初始化，业务不要在 `init()` 前调用。
- Redis 配置存在但连接失败时：
  - `servicesRequired !== false`：启动失败；
  - `servicesRequired === false`：仅告警并继续启动。
- `TokenService.secret` 目前是代码内常量，生产建议在你们业务层封装替换（环境变量注入）。

---

## 9. 业务项目集成与配置

### 9.1 必填与常用配置

| 配置项 | 说明 |
|--------|------|
| `db` | **必填**：`host`、`username`、`password`、`database`、`dialect` 等；可选扩展字段见 [§9.1.1](#911-db-可选字段扩展)。 |
| `apiPaths` | **必填**：非空数组；每项为字符串或 `{ path, prefix?, authType? }`。 |
| `baseUrl` | 强烈建议：Swagger 等依赖。 |
| `allowUrls` | 白名单子串数组；未配置时非白名单路径均需登录（见中间件逻辑）。 |
| `logPath` | 日志服务目录。 |
| `redis` | 可选；不配则跳过 Redis 初始化（`initServices` 内不抛错则继续）。 |
| `licensePath` | 授权文件路径；`initDb` 时会做离线授权校验。 |
| `modelPaths` | 业务模型目录。 |
| `businessAssociations` | `(models) => void`，业务模型关联。 |
| `beforeInitApi` | `(framework) => Promise`，在路由注册前执行（适合挂 `/health`、额外中间件）。 |
| `customSchemas` | 传入 Swagger 服务，扩展 Schema。 |
| `project_key` | 低代码平台项目标识。 |
| `skipPackageValidation` | `true` 跳过依赖版本校验。 |
| `strictPackageValidation` | `true` 校验失败则中止启动。 |
| `servicesRequired` | 默认 `true`：`initServices` 失败会中止；`false` 时仅警告。 |
| `env` | 如 `development` / `production`，影响部分日志与路由打印行为。 |

#### 9.1.1 db 可选字段扩展

除连接与 Sequelize 常用项外，框架会读取：

| 字段 | 类型 | 说明 |
|------|------|------|
| `slowSQLThreshold` | `number` | 慢 SQL 阈值（毫秒），默认 `300`；超过则写入文件日志。 |
| `logging` | `boolean` \| `function` | SQL 控制台输出；为函数时可自定义并仍参与慢 SQL 检测。 |
| `pool` | `object` | 连接池，会与内置默认合并。 |
| `operateLogExcludeTableNames` | `string[]` | **操作日志排除表**：元素为模型的 **`tableName`**（与 `db.define` 时一致，默认常为模型名）。命中则**不**向 `sys_log` 写入新增/修改/删除操作记录；`sys_log` 本身始终排除。更新钩子仍会维护 `last_modify_time`。 |

示例：

```js
db: {
  host: '127.0.0.1',
  username: 'root',
  password: '***',
  database: 'app',
  dialect: 'mysql',
  operateLogExcludeTableNames: ['user_session', 'api_rate_limit'],
},
```

### 9.2 静态与上传

`framework.js` 中 `koa-static` 指向 **`path.resolve(__dirname, '../upload')`**（相对框架包内 `framework.js` 所在目录），用于静态资源；上传目录以部署为准。

### 9.3 前端调用流程简述

1. `POST /admin_api/sys_user/login` 取 `data.token`。
2. 后续请求头：`admin-token: <token>`。
3. 小程序接口：配置 `authType: 'applet'`，使用 `applet-token`。

### 9.4 配置模板（开发 / 生产）

**开发环境模板：**

```js
{
  env: 'development',
  db: {
    host: '127.0.0.1',
    port: 3306,
    username: 'root',
    password: '***',
    database: 'demo_dev',
    dialect: 'mysql',
    logging: true,
    slowSQLThreshold: 300,
  },
  redis: { host: '127.0.0.1', port: 6379, pwd: '' },
  baseUrl: 'http://127.0.0.1:3000',
  apiPaths: [{ path: './controller', prefix: '/api', authType: 'admin' }],
  allowUrls: ['/health'],
  servicesRequired: true,
}
```

**生产环境模板：**

```js
{
  env: 'production',
  db: {
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT || 3306),
    username: process.env.DB_USER,
    password: process.env.DB_PWD,
    database: process.env.DB_NAME,
    dialect: 'mysql',
    logging: false,
    slowSQLThreshold: 500,
    operateLogExcludeTableNames: ['sys_log', 'sys_session'],
  },
  redis: {
    host: process.env.REDIS_HOST,
    port: Number(process.env.REDIS_PORT || 6379),
    pwd: process.env.REDIS_PWD,
  },
  baseUrl: process.env.BASE_URL,
  apiPaths: [{ path: './controller', prefix: '/api', authType: 'admin' }],
  allowUrls: ['/health', '/api/public'],
  servicesRequired: true,
  strictPackageValidation: true,
}
```

### 9.5 `apiPaths` 组合示例（多前缀）

```js
apiPaths: [
  { path: path.join(__dirname, 'controller/admin'), prefix: '/admin_api', authType: 'admin' },
  { path: path.join(__dirname, 'controller/applet'), prefix: '/api', authType: 'applet' },
]
```

### 9.6 业务注入最小模板（模型 + 关联 + 路由）

```js
{
  modelPaths: path.join(__dirname, 'models'),
  businessAssociations: (models) => {
    if (models.order && models.user) {
      models.order.belongsTo(models.user, { foreignKey: 'user_id', as: 'creator' });
    }
  },
  beforeInitApi: async (fw) => {
    fw.addRoutes('/api', {
      'GET /health': async (ctx) => ctx.success({ ok: true }),
    });
  },
}
```

---

## 10. 内部方法与外部注入详解

本节聚焦两个问题：

1. 框架内部方法是按什么顺序调用、各自负责什么。
2. 业务项目可以从哪些入口注入自定义逻辑（模型、关联、路由、中间件、Swagger 扩展等）。

### 10.1 `init(config)` 内部执行顺序

`index.js` 导出 `framework.init(config)`，其内部固定按以下顺序执行：

1. `new Framework(config)`：校验配置、构造 Koa、初始化 `ServiceManager` 与 `RequestManager`。
2. `_validatePackages()`：依赖版本检查（可被 `skipPackageValidation` 跳过）。
3. `initDb(modelPaths, businessAssociations)`：校验授权、初始化 DB、加载系统模型、加载业务模型、合并模型并执行关联注入。
4. `initServices()`：初始化 `logs/token/redis/platformProject`。
5. `initApi(beforeInitApi)`：安装中间件，执行外部注入回调，注册 Swagger 与路由。

最小可运行示例：

```js
const path = require('path');
const core = require('@project/core');

async function bootstrap() {
  const framework = await core.init({
    db: {
      host: '127.0.0.1',
      port: 3306,
      username: 'root',
      password: '***',
      database: 'demo',
      dialect: 'mysql',
    },
    baseUrl: 'http://127.0.0.1:3000',
    apiPaths: [{ path: path.join(__dirname, 'controller'), prefix: '/api', authType: 'admin' }],
    modelPaths: path.join(__dirname, 'models'),
    allowUrls: ['/health'],
  });

  await framework.start(3000);
}
```

### 10.2 Framework 实例核心方法（内部职责 + 推荐用法）

| 方法 | 内部职责 | 业务侧何时用 |
|------|----------|--------------|
| `start(port)` | `app.listen(port)` 启动 HTTP 服务 | `init` 成功后调用一次 |
| `addRoutes(prefix, routes)` | 缓存手动路由，最终在 `_setupRoutes` 注册到 `koa-router` | 仅在 `beforeInitApi` 里动态加路由 |
| `use(middleware)` | 对 Koa `app` 执行 `app.use` | 优先在 `beforeInitApi` 中注入全局中间件 |
| `getApp()` | 返回 Koa 实例 | 需要访问底层 Koa 能力时 |
| `getModels()` | 返回合并后的模型对象（系统 + 业务） | 控制器 / Service 查询数据库 |
| `getServices()` | 返回 `ServiceFactory.initServices()` 产物 | 获取 `tokenService`、`logsService` 等 |
| `getPlatformProjectService()` | 返回低代码平台服务 | 需要调用平台模型/表单生成能力 |
| `query(sql)` | 原生 SQL 查询（通过 ModelManager） | 临时查询或复杂 SQL |
| `syncDatabase()` | 触发结构同步 | 仅开发期使用，生产慎用 |

> `getDB()`、`getSequelize()` 当前源码存在不一致实现，业务建议优先使用 `getModels()` 与 `getServices()`，必要时自行封装。

### 10.3 外部注入点总览（最重要）

| 注入点 | 配置项 / 方法 | 触发时机 | 适合注入什么 |
|--------|----------------|----------|--------------|
| 业务模型目录 | `config.modelPaths` | `initDb` 中 `_loadBusinessModels` | 业务 Sequelize 模型 |
| 业务模型关联 | `config.businessAssociations(models)` | 系统模型与业务模型合并后 | `belongsTo/hasMany/belongsToMany` |
| API 前置注入 | `config.beforeInitApi(framework)` | `_setupMiddleware` 后、`_setupRoutes` 前 | 手动路由、全局中间件、桥接逻辑 |
| 手动路由 | `framework.addRoutes(prefix, routes)` | `beforeInitApi` 内调用 | 动态生成路由、代理路由 |
| 中间件 | `framework.use(middleware)` 或 `framework.app.use` | 推荐在 `beforeInitApi` | 请求日志、租户注入、灰度开关 |
| Swagger 扩展 | `config.customSchemas` | `_setupSwagger` 时 | 业务模型额外 schema |

### 10.4 业务模型注入：`modelPaths`

模型文件建议导出工厂函数，框架会调用 `model(db)`：

```js
// models/order.js
const Sequelize = require('sequelize');

module.exports = (db) => {
  return db.define('order', {
    title: { type: Sequelize.STRING(100), allowNull: false, defaultValue: '' },
    amount: { type: Sequelize.DECIMAL(10, 2), allowNull: false, defaultValue: 0 },
  });
};
```

配置：

```js
modelPaths: path.join(__dirname, 'models')
```

### 10.5 关联注入：`businessAssociations(models)`

框架会在模型合并后调用 `businessAssociations`，这里写跨模型关联最稳妥：

```js
businessAssociations: (models) => {
  const { order, user } = models;
  if (order && user) {
    order.belongsTo(user, { foreignKey: 'user_id', targetKey: 'id', as: 'creator' });
    user.hasMany(order, { foreignKey: 'user_id', sourceKey: 'id', as: 'orders' });
  }
}
```

### 10.6 路由注入：`beforeInitApi` + `addRoutes`

`beforeInitApi` 是最关键的扩展点，执行时机早于路由扫描注册，可同时注入中间件与手动路由：

```js
beforeInitApi: async (framework) => {
  // 1) 注入中间件
  framework.use(async (ctx, next) => {
    ctx.set('x-request-source', 'node-core');
    await next();
  });

  // 2) 注入手动路由
  framework.addRoutes('/api', {
    'GET /health': async (ctx) => ctx.success({ ok: true }),
    'POST /tenant/switch': async (ctx) => {
      const tenantId = ctx.get('tenant_id');
      return ctx.success({ tenant_id: tenantId });
    },
  });
}
```

### 10.7 Swagger 扩展注入：`customSchemas`

业务侧可把自定义 schema 注入 Swagger 生成器：

```js
customSchemas: {
  OrderCreateBody: {
    type: 'object',
    properties: {
      title: { type: 'string' },
      amount: { type: 'number' },
    },
    required: ['title', 'amount'],
  },
}
```

注入后可在业务 OpenAPI 注释或生成逻辑中引用该 schema。

### 10.8 服务注入关系（内部）

`ServiceFactory.initServices()` 内部依赖顺序如下：

1. `logsService`
2. `tokenService`
3. `redisService`（有配置才连接）
4. `platformProjectService`
5. `tokenService.setLogsService(logsService)`（日志依赖回注）

业务侧通常只需：

```js
const { tokenService, redisService, logsService } = Framework.getServices();
```

### 10.9 内部模块分层说明（源码导读）

这一节按「启动层 -> 数据层 -> 请求层 -> 服务层」说明内部模块，方便你二开时快速定位。

#### 10.9.1 启动编排层：`framework.js`

**定位**：框架总调度器，负责把 DB、Service、API 串起来。

**关键职责：**

- 配置校验：`_validateConfig(config)` 检查 `db`、`apiPaths` 等必填项。
- 启动顺序：`init -> initDb -> initServices -> initApi -> start`。
- 系统控制器注册：`_registerSystemControllers()` 固定挂载在 `/admin_api`。
- 手动路由缓存：`addRoutes()` 先存入 `customRoutes`，最后在 `_setupRoutes()` 统一注册。

**关键扩展点：**

- `beforeInitApi(framework)`：最推荐的业务注入位置。
- `addRoutes(prefix, routes)`：动态路由注入。
- `use(middleware)`：追加 Koa 中间件。

#### 10.9.2 数据模型层：`lib/modelManager.js` + `database/db.js`

**`ModelManager`（模型编排）：**

- `initSysModels()`：初始化系统模型（`sys_user/sys_role/...`）。
- `_loadBusinessModels`（在 `framework.js` 内调用）：按 `modelPaths` 加载业务模型。
- `mergeModels()`：合并系统模型与业务模型，顺序为：
  - `allModels = { ...sysModels, ...businessModels }`
  - **同名时业务模型覆盖系统模型**（优先级更高）。
- `setupBusinessModelAssociations(models => {})`：注入业务关联关系。

**`DatabaseManager`（Sequelize 封装）：**

- `initDatabase(config)`：连接池、SQL 日志、慢 SQL 记录、`operateLogExcludeTableNames` 解析。
- `define(name, attrs)`：统一追加 `create_time/last_modify_time/is_delete` 字段。
- 钩子日志：`afterCreate/beforeUpdate/afterDestroy` 自动写 `sys_log`（可按表名排除）。

#### 10.9.3 请求路由层：`lib/requestManager.js` + `middleware/baseRequest.js` + `router/baseController.js`

**`RequestManager`：**

- `getBaseRequest()`：构建基础中间件（鉴权 + Context 扩展）。
- `getBaseController()`：构建/复用路由加载器。

**`baseRequest`（全局中间件）做了三件事：**

1. 注入 `ctx.get/ctx.getBody/ctx.success/ctx.fail/...` 等便捷 API。
2. 执行白名单 + token 鉴权（`admin-token` / `applet-token`）。
3. 统一异常捕获并写错误日志。

**`BaseController`（控制器解析）：**

- 扫描 `apiPaths[].path` 下的控制器文件。
- 仅识别导出键格式：`GET /path`、`POST /path`。
- 合并前缀后注册到 `koa-router`，并打印路由清单。

#### 10.9.4 服务层：`lib/serviceManager.js` + `services/*`

**初始化入口**：`ServiceFactory.initServices()`，顺序固定：

1. `LogsService`
2. `TokenService`
3. `RedisService`（配置了 `redis` 才连接）
4. `PlatformProjectService`
5. `tokenService.setLogsService(logsService)`

**核心服务职责：**

- `services/token.js`
  - `getMd5(str)`、`create(payload)`、`parse(token)`、`verify(token)`。
- `services/logs.js`
  - 统一日志写入，按类型分文件（普通/错误/慢 SQL），支持文件超过 5MB 自动滚动后缀。
- `services/redis.js`
  - 封装 `set/get/del/setIfAbsent`，带重连和连接状态管理。
- `services/platformProject.js`
  - 对接低代码平台：`modelAll/createForm/updateForm/formGenerate/modelGenerate`。
- `services/swagger.js` + `lib/swaggerUIManager.js`
  - 生成 OpenAPI 文档、挂载 `/api/docs` `/api/swagger.json`，并支持安全配置 Cookie 持久化。

#### 10.9.5 模块调用关系（简图）

```text
Framework.init
  -> ModelManager (db + models)
  -> ServiceManager (logs/token/redis/platform/swagger)
  -> RequestManager
       -> baseRequest (ctx扩展 + 鉴权)
       -> BaseController (控制器解析注册)
  -> Koa app.listen
```

#### 10.9.6 二开建议（内部模块维度）

- 需要改鉴权策略：优先改 `middleware/baseRequest.js`。
- 需要改控制器扫描规则：改 `router/baseController.js` 的 `_parseController` / `loadControllers`。
- 需要改 DB 公共字段与日志策略：改 `database/db.js` 的 `define` 与 hooks。
- 需要新增通用服务：放到 `services/`，并在 `services/index.js` + `lib/serviceManager.js` 暴露。

### 10.10 建议的集成模板（可直接复制）

```js
const path = require('path');
const Framework = require('@project/core');

async function start() {
  const framework = await Framework.init({
    env: process.env.NODE_ENV || 'development',
    db: {
      host: '127.0.0.1',
      port: 3306,
      username: 'root',
      password: '***',
      database: 'demo',
      dialect: 'mysql',
      operateLogExcludeTableNames: ['sys_log', 'user_session'],
    },
    redis: { host: '127.0.0.1', port: 6379, pwd: '' },
    baseUrl: 'http://127.0.0.1:3000',
    allowUrls: ['/health', '/api/public'],
    apiPaths: [{ path: path.join(__dirname, 'controller'), prefix: '/api', authType: 'admin' }],
    modelPaths: path.join(__dirname, 'models'),
    businessAssociations: (models) => {
      // 写业务模型关联
    },
    beforeInitApi: async (fw) => {
      // 写中间件与手动路由
    },
  });

  await framework.start(3000);
}

start().catch((error) => {
  console.error('启动失败:', error);
  process.exit(1);
});
```

---

## 11. 注意事项

1. **白名单**为子串匹配，自定义公开接口时避免与 `/login` 等敏感片段无意冲突。
2. **系统管理接口**路径前缀为 **`/admin_api`**；业务前缀在 `apiPaths` 中单独配置。
3. **`sys_user/edit`** 源码对 `password` 直接取长度，调用时请始终传入 `password`（或按你们分支修复后再依赖可选密码）。
5. **生产环境**授权、依赖版本与数据库同步策略以部署与发行说明为准。
6. 更完整的 OpenAPI 可通过 **`GET /api/swagger.json`** 与 Swagger UI 查看（与手写路由并行存在时，以生成内容为准）。

如需扩展接口文档，可在业务侧增加 swagger-jsdoc 注释或维护独立 OpenAPI 文件，并按项目约定合并到 `customSchemas`。

---

## 12. 常见问题排查（FAQ）

### 12.1 启动时报 “配置验证失败”

优先检查：

- `db.host/username/password/database/dialect` 是否完整；
- `apiPaths` 是否为非空数组，且对象项有 `path`；
- `baseUrl` 建议填写，否则 Swagger 相关能力受影响。

### 12.2 接口总是返回 `code: -2`

排查顺序：

1. 请求路径是否命中 `allowUrls` 或默认白名单；
2. 路径前缀是否与 `apiPaths[].prefix` 匹配；
3. `authType` 对应请求头是否正确：
   - `admin` -> `admin-token`
   - `applet` -> `applet-token`

### 12.3 `getModels()` 里模型找不到

- 确认 `modelPaths` 指向真实目录；
- 模型文件是否导出工厂函数 `module.exports = (db) => db.define(...)`；
- 文件后缀是否为 `.js`；
- 初始化顺序必须是先 `init` 再在业务代码中调用 `Framework.getModels()`。

### 12.4 Redis 连接失败导致启动中断

- 如果希望 Redis 故障不阻断服务，把 `servicesRequired` 设为 `false`；
- 检查 Redis 地址、端口、密码、网络策略；
- 查看 `logsService` 与控制台日志中的 `RedisService` 错误信息。

### 12.5 Swagger 页面可打开但接口调试失败

- 检查 `baseUrl` 是否正确；
- 检查 `/api/swagger.json` 是否可访问；
- 在 `/api/docs` 里确认安全配置（`admin-token` / `applet-token`）是否已填写。

### 12.6 业务模型与系统模型同名时用哪个？

`mergeModels()` 顺序是 `{ ...sysModels, ...businessModels }`，所以**业务模型优先级更高**（会覆盖系统同名模型）。
