chanjs 2.7.7 → 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.
- package/README.md +261 -363
- package/config/index.js +4 -2
- package/core/App.js +35 -0
- package/core/Container.js +56 -29
- package/core/Database.js +58 -8
- package/core/EventBus.js +88 -0
- package/core/Lang.js +56 -0
- package/core/Repository.js +34 -2
- package/core/Task.js +87 -0
- package/core/errors.js +0 -5
- package/doc/00-README.md +208 -0
- package/doc/01-/346/240/270/345/277/203/347/261/273Controller-Service-Repository.md +432 -0
- package/doc/02-/345/223/215/345/272/224/344/270/216/351/224/231/350/257/257.md +255 -0
- package/doc/03-/345/256/211/345/205/250/346/250/241/345/235/227.md +264 -0
- package/doc/04-/345/255/230/345/202/250/344/270/216/347/274/223/345/255/230.md +157 -0
- package/doc/05-/345/267/245/345/205/267/344/270/216/346/240/241/351/252/214.md +309 -0
- package/doc/06-/345/272/224/347/224/250/347/224/237/345/221/275/345/221/250/346/234/237.md +207 -0
- package/doc/07-/344/272/213/344/273/266/347/263/273/347/273/237EventBus.md +324 -0
- package/doc/08-/345/256/232/346/227/266/344/273/273/345/212/241Task.md +262 -0
- package/doc/09-/345/233/275/351/231/205/345/214/226Lang.md +220 -0
- package/index.js +31 -2
- package/middleware/log.js +48 -31
- package/middleware/waf.js +4 -8
- package/package.json +21 -3
- package/response/code.js +0 -12
- package/response/response.js +8 -2
- package/security/keywords.js +2 -3
- package/utils/logger.js +60 -91
- package/utils/signal.js +21 -2
- package/USAGE.md +0 -533
- package/doc/Cache.md +0 -333
- package/doc/Common.md +0 -638
- package/doc/Controller.md +0 -223
- package/doc/Help.md +0 -390
- package/doc/QuickStart.md +0 -116
- package/doc/Repository.md +0 -560
- package/doc/Service.md +0 -240
- package/publish.bat +0 -4
- package/todo.md +0 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# 国际化 i18n(Lang)
|
|
2
|
+
|
|
3
|
+
> 基于 `i18next` 的多语言支持,启动时扫描 `lang/` 目录加载全部 JSON 语言包,运行时 O(1) 查词。
|
|
4
|
+
> 对外通过 `import { initLang } from "chanjs"` 或 `this.app.lang`(Chan 应用实例已内置)使用。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、设计理念
|
|
9
|
+
|
|
10
|
+
| 决策 | 说明 |
|
|
11
|
+
|------|------|
|
|
12
|
+
| 成熟库 | 采用 `i18next`,复用其插值、复数、命名空间等成熟能力,不重复造轮子 |
|
|
13
|
+
| 启动加载 | 启动时扫描 `lang/` 目录,把全部语言包读进内存,运行时零 I/O |
|
|
14
|
+
| 全局单例 | `initLang` 返回的就是 i18next 单例,`this.app.lang` 指向同一实例 |
|
|
15
|
+
| 后端不转义 | `interpolation.escapeValue: false`,JSON 输出 / 邮件等后端场景不自动转义 |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 二、语言包目录结构
|
|
20
|
+
|
|
21
|
+
在项目**根目录**下建 `lang/`,每个语言一个子目录,每个命名空间一个 JSON 文件:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
lang/
|
|
25
|
+
├── zh-CN/
|
|
26
|
+
│ ├── common.json # 默认命名空间
|
|
27
|
+
│ └── user.json # 也可按模块拆命名空间
|
|
28
|
+
├── en/
|
|
29
|
+
│ ├── common.json
|
|
30
|
+
│ └── user.json
|
|
31
|
+
└── ja/
|
|
32
|
+
└── common.json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`lang/zh-CN/common.json` 示例:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"user": {
|
|
40
|
+
"welcome": "欢迎,{{name}}",
|
|
41
|
+
"login": "登录",
|
|
42
|
+
"logout": "退出"
|
|
43
|
+
},
|
|
44
|
+
"order": {
|
|
45
|
+
"created": "订单创建成功,编号 {{id}}"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> 语言目录名需匹配 `^[a-zA-Z_-]+$`(如 `zh-CN`、`en`、`ja`)。命名空间 = JSON 文件名(如 `common`)。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 三、初始化 initLang
|
|
55
|
+
|
|
56
|
+
### 应用实例内置(推荐)
|
|
57
|
+
|
|
58
|
+
`Chan` 应用启动时会自动调用 `initLang`,并把结果挂到 `this.app.lang`:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// app/app.js
|
|
62
|
+
import Chan from "chanjs";
|
|
63
|
+
|
|
64
|
+
const chan = new Chan();
|
|
65
|
+
await chan.start(); // 内部已初始化 i18n,默认语言来自 config
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
启动后业务代码直接用 `this.app.lang`:
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const msg = this.app.lang.t("user.welcome", { name: "张三" }); // → "欢迎,张三"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 手动初始化(中间件 / 独立脚本)
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import { initLang } from "chanjs";
|
|
78
|
+
|
|
79
|
+
const i18n = await initLang("zh-CN"); // 返回 i18next 实例
|
|
80
|
+
i18n.t("user.welcome", { name: "张三" }); // → "欢迎,张三"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| 参数 | 类型 | 说明 |
|
|
84
|
+
|------|------|------|
|
|
85
|
+
| `locale` | `string` | 默认语言,默认 `"zh-CN"` |
|
|
86
|
+
| **返回** | `Promise<i18next>` | i18next 实例 |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 四、翻译 `t(key, opts?)`
|
|
91
|
+
|
|
92
|
+
### 4.1 基础查词
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
this.app.lang.t("user.login"); // → "登录"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4.2 插值(`{{变量}}`)
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
this.app.lang.t("user.welcome", { name: "张三" }); // → "欢迎,张三"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4.3 复数(i18next 原生支持)
|
|
105
|
+
|
|
106
|
+
语言包:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "item": {
|
|
110
|
+
"one": "有 {{count}} 个商品",
|
|
111
|
+
"other": "有 {{count}} 个商品"
|
|
112
|
+
} }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
this.app.lang.t("item", { count: 1 }); // → "有 1 个商品"
|
|
117
|
+
this.app.lang.t("item", { count: 5 }); // → "有 5 个商品"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4.4 指定命名空间
|
|
121
|
+
|
|
122
|
+
语言包拆了多个命名空间时,用 `ns` 指定:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// lang/user.json: { "title": "用户中心" }
|
|
126
|
+
this.app.lang.t("title", { ns: "user" }); // → "用户中心"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> 未指定 `ns` 时默认用 `common` 命名空间。
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 五、动态切换语言 changeLanguage
|
|
134
|
+
|
|
135
|
+
业务运行时(登录后 / 用户偏好 / 请求头)可随时切换:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// 切换语言并等待资源生效
|
|
139
|
+
await this.app.lang.changeLanguage("en");
|
|
140
|
+
|
|
141
|
+
// 切换后立即按新语言取词
|
|
142
|
+
const msg = this.app.lang.t("user.welcome", { name: "Zhang San" }); // 英文包
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 按请求动态切换(多语言站点)
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// app/middleware/i18n.js —— 每个请求按 Accept-Language 切换
|
|
149
|
+
import { event } from "chanjs";
|
|
150
|
+
|
|
151
|
+
export function i18nMw(app) {
|
|
152
|
+
app.use(async (req, res, next) => {
|
|
153
|
+
const lang = req.headers["accept-language"]?.split(",")[0] || "zh-CN";
|
|
154
|
+
await app.lang.changeLanguage(lang);
|
|
155
|
+
next();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> 注意:`changeLanguage` 是异步的,切换后记得 `await` 再取词。
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 六、使用场景
|
|
165
|
+
|
|
166
|
+
### 场景一:接口返回多语言文案
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// Controller 里
|
|
170
|
+
async greet(req, res) {
|
|
171
|
+
const name = req.query.name || "朋友";
|
|
172
|
+
return this.success({
|
|
173
|
+
data: {
|
|
174
|
+
msg: this.app.lang.t("user.welcome", { name }),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 场景二:邮件 / 短信模板
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
const subject = this.app.lang.t("email.subject", { ns: "mail" });
|
|
184
|
+
const body = this.app.lang.t("email.reset_body", { name: user.name, link });
|
|
185
|
+
await sendMail(user.email, subject, body);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 场景三:错误提示本地化
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const KeyedError = new AppError(this.app.lang.t("error.order_not_found"));
|
|
192
|
+
throw KeyedError; // → 按当前语言给出错误提示
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 七、注意事项
|
|
198
|
+
|
|
199
|
+
### 1. 语言包必须存在,否则回退 zh-CN
|
|
200
|
+
|
|
201
|
+
`fallbackLng` 固定为 `zh-CN`。某语言缺词时自动回退到中文包,不会报错。
|
|
202
|
+
|
|
203
|
+
### 2. `escapeValue: false`(后端不转义)
|
|
204
|
+
|
|
205
|
+
这是后端场景特意关闭的。若用于**前端渲染用户输入**,请自行 `XSS` 过滤(见 `filterXSS`),不要直接拼接。
|
|
206
|
+
|
|
207
|
+
### 3. 单文件解析失败不阻断
|
|
208
|
+
|
|
209
|
+
某个语言包的 JSON 解析失败会被跳过,不影响其它语言和应用启动。
|
|
210
|
+
|
|
211
|
+
### 4. 与 `changeLanguage` 配合 requestId
|
|
212
|
+
|
|
213
|
+
`changeLanguage` 是异步切换,多语言站点建议在请求期间保持语言一致,避免并发切换串语言。
|
|
214
|
+
|
|
215
|
+
### 5. 新增语言步骤
|
|
216
|
+
|
|
217
|
+
1. 在 `lang/` 下建语言目录(如 `lang/fr/`)
|
|
218
|
+
2. 放入 `common.json`(及需要的命名空间文件)
|
|
219
|
+
3. 重启应用(启动时扫描加载)
|
|
220
|
+
4. 用 `changeLanguage("fr")` 切换后按 `fr` 渲染
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* import { Controller, Repository, Service, cache, Paths, loader, utils, getApp } from "chanjs";
|
|
11
11
|
* import { validate, validateAll } from "chanjs"; // zod 校验中间件
|
|
12
12
|
* import { AppError, NotFoundError, ValidationError } from "chanjs"; // 错误类
|
|
13
|
+
* import { event, EventBus, Task, initLang } from "chanjs"; // 基础设施
|
|
13
14
|
*
|
|
14
15
|
* 多实例:
|
|
15
16
|
* 每个 Chan 实例持有独立 config/db/paths/dbManager(注册表管理),
|
|
@@ -22,6 +23,11 @@ export { default as Controller } from "./core/Controller.js";
|
|
|
22
23
|
export { default as Repository } from "./core/Repository.js";
|
|
23
24
|
export { Service } from "./core/Service.js";
|
|
24
25
|
|
|
26
|
+
// ===================== 基础设施(事件总线 / 定时任务 / i18n)=====================
|
|
27
|
+
export { EventBus, event } from "./core/EventBus.js";
|
|
28
|
+
export { default as Task } from "./core/Task.js";
|
|
29
|
+
export { initLang } from "./core/Lang.js";
|
|
30
|
+
|
|
25
31
|
// ===================== 错误体系 =====================
|
|
26
32
|
export {
|
|
27
33
|
AppError, AuthError, TokenExpiredError, ForbiddenError,
|
|
@@ -33,8 +39,31 @@ export {
|
|
|
33
39
|
// ===================== 统一响应工具(纯函数,可复用于接口 / 定时任务 / RPC)=====================
|
|
34
40
|
export { success, fail, routeNotFound, serializeError, buildErrorHtml, respondError } from "./response/index.js";
|
|
35
41
|
|
|
42
|
+
// ===================== 业务状态码 =====================
|
|
43
|
+
export {
|
|
44
|
+
CODE,
|
|
45
|
+
getCodeMsg,
|
|
46
|
+
CODE_OK,
|
|
47
|
+
CODE_AUTH_FAILED,
|
|
48
|
+
CODE_TOKEN_EXPIRED,
|
|
49
|
+
CODE_FORBIDDEN,
|
|
50
|
+
CODE_NOT_FOUND,
|
|
51
|
+
CODE_CONFLICT,
|
|
52
|
+
CODE_PARAM_INVALID,
|
|
53
|
+
CODE_PARAM_MISSING,
|
|
54
|
+
CODE_BUSINESS_FAIL,
|
|
55
|
+
CODE_RATE_LIMIT,
|
|
56
|
+
CODE_DEVICE_ERROR,
|
|
57
|
+
CODE_BLOCKED,
|
|
58
|
+
CODE_SYSTEM_ERROR,
|
|
59
|
+
CODE_SERVICE_BUSY,
|
|
60
|
+
CODE_DB_CONNECTION_ERROR,
|
|
61
|
+
CODE_DB_ACCESS_DENIED,
|
|
62
|
+
CODE_DB_OPERATION_TIMEOUT,
|
|
63
|
+
} from "./response/code.js";
|
|
64
|
+
|
|
36
65
|
// ===================== 安全工具 =====================
|
|
37
|
-
export { setToken, getToken, verifyToken } from "./security/jwt.js";
|
|
66
|
+
export { setToken, getToken, verifyToken, revokeToken } from "./security/jwt.js";
|
|
38
67
|
export { aesEncrypt, aesDecrypt } from "./security/sign.js";
|
|
39
68
|
export { createRateLimitMiddleware } from "./security/rate-limit.js";
|
|
40
69
|
export { filterXSS, checkKeywords } from "./security/index.js";
|
|
@@ -58,4 +87,4 @@ export { Paths } from "./utils/paths.js";
|
|
|
58
87
|
|
|
59
88
|
// ===================== 默认导出应用主类 =====================
|
|
60
89
|
import Chan from "./core/App.js";
|
|
61
|
-
export default Chan;
|
|
90
|
+
export default Chan;
|
package/middleware/log.js
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import pinoHttp from "pino-http";
|
|
2
3
|
import { getIp } from "../utils/ip.js";
|
|
3
|
-
|
|
4
|
-
// 日志格式白名单,防止非法格式注入
|
|
5
|
-
const ALLOWED_FORMATS = new Set(["chancms", "combined", "common", "dev", "short", "tiny"]);
|
|
6
|
-
|
|
7
|
-
// 自定义token
|
|
8
|
-
morgan.token("ip", req => getIp(req));
|
|
9
|
-
morgan.token("user", req => req.user ? `${req.user.uid}:${req.user.username}` : "-");
|
|
10
|
-
morgan.token("datetime", () => new Date().toISOString());
|
|
11
|
-
|
|
12
|
-
// 自定义ChanCMS日志输出模板
|
|
13
|
-
morgan.format("chancms", (tokens, req, res) => [
|
|
14
|
-
tokens.datetime(req, res),
|
|
15
|
-
tokens.ip(req, res),
|
|
16
|
-
tokens.user(req, res),
|
|
17
|
-
tokens.method(req, res),
|
|
18
|
-
tokens.url(req, res),
|
|
19
|
-
tokens.status(req, res),
|
|
20
|
-
tokens.res(req, res, "content-length") || "-",
|
|
21
|
-
"-",
|
|
22
|
-
tokens["response-time"](req, res),
|
|
23
|
-
"ms"
|
|
24
|
-
].join(" "));
|
|
4
|
+
import { root } from "../utils/logger.js";
|
|
25
5
|
|
|
26
6
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
7
|
+
* 请求日志中间件(pino-http)
|
|
8
|
+
*
|
|
9
|
+
* 自动为每个请求生成 requestId,挂载 req.log(带 requestId 的子 logger)。
|
|
10
|
+
* 业务 Controller 中可通过 req.log.info("xxx") 输出带 requestId 的日志。
|
|
11
|
+
*
|
|
12
|
+
* @param {object} app - Express 实例
|
|
13
|
+
* @param {object} [config={}] - 配置对象
|
|
30
14
|
*/
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
export function log(app, config = {}) {
|
|
16
|
+
app.use(pinoHttp({
|
|
17
|
+
logger: root,
|
|
18
|
+
// 自动记录请求日志(静态资源跳过)
|
|
19
|
+
autoLogging: {
|
|
20
|
+
ignore: (req) => {
|
|
21
|
+
const url = req.url || "";
|
|
22
|
+
return /\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2)$/.test(url);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
// requestId 生成:优先复用上游 X-Request-Id
|
|
26
|
+
genReqId: (req, res) => {
|
|
27
|
+
const id = req.headers["x-request-id"] || randomUUID();
|
|
28
|
+
res.setHeader("X-Request-Id", id);
|
|
29
|
+
return id;
|
|
30
|
+
},
|
|
31
|
+
// 日志级别按状态码区分
|
|
32
|
+
customLogLevel: (req, res, err) => {
|
|
33
|
+
if (err || res.statusCode >= 500) return "error";
|
|
34
|
+
if (res.statusCode >= 400) return "warn";
|
|
35
|
+
return "info";
|
|
36
|
+
},
|
|
37
|
+
customSuccessMessage: (req, res) => {
|
|
38
|
+
return `${req.method} ${req.originalUrl || req.url} ${res.statusCode}`;
|
|
39
|
+
},
|
|
40
|
+
customErrorMessage: (req, res, err) => {
|
|
41
|
+
return `${req.method} ${req.originalUrl || req.url} ${res.statusCode} ${err.message}`;
|
|
42
|
+
},
|
|
43
|
+
serializers: {
|
|
44
|
+
req: (req) => ({
|
|
45
|
+
method: req.method,
|
|
46
|
+
url: req.originalUrl || req.url,
|
|
47
|
+
ip: getIp(req),
|
|
48
|
+
}),
|
|
49
|
+
res: (res) => ({ statusCode: res.statusCode }),
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
}
|
package/middleware/waf.js
CHANGED
|
@@ -29,17 +29,13 @@ const DEFAULT_BLOCK = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* 判断本机回环可信IP:仅 127.0.0.1 / ::1 无条件放行 WAF。
|
|
33
|
+
* 不再放行整个内网段(如 10.x、192.168.x、172.16-31.x),
|
|
34
|
+
* 否则当 Nginx 部署在另一台内网机、Express 未解析 XFF 时,全站 WAF 会静默失效。
|
|
33
35
|
*/
|
|
34
36
|
const isTrustedIp = ip => {
|
|
35
37
|
if (!ip) return false;
|
|
36
|
-
|
|
37
|
-
if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
|
|
38
|
-
if (ip.startsWith("172.")) {
|
|
39
|
-
const seg = Number(ip.split(".")[1]);
|
|
40
|
-
return seg >= 16 && seg <= 31;
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
38
|
+
return TRUSTED_IPS.has(ip);
|
|
43
39
|
};
|
|
44
40
|
|
|
45
41
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "chanjs",
|
|
4
|
-
"version": "2.7.
|
|
4
|
+
"version": "2.7.10",
|
|
5
5
|
"description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"module": "index.js",
|
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
},
|
|
19
19
|
"author": "明空",
|
|
20
20
|
"license": "ISC",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"app/",
|
|
24
|
+
"config/",
|
|
25
|
+
"core/",
|
|
26
|
+
"middleware/",
|
|
27
|
+
"response/",
|
|
28
|
+
"security/",
|
|
29
|
+
"storage/",
|
|
30
|
+
"utils/",
|
|
31
|
+
"doc/"
|
|
32
|
+
],
|
|
21
33
|
"dependencies": {
|
|
22
34
|
"art-template": "^4.13.4",
|
|
23
35
|
"cookie-parser": "^1.4.7",
|
|
@@ -26,17 +38,23 @@
|
|
|
26
38
|
"dotenv": "^17.4.2",
|
|
27
39
|
"express": "^5.2.1",
|
|
28
40
|
"express-art-template": "^1.0.1",
|
|
41
|
+
"i18next": "^24.2.0",
|
|
29
42
|
"jsonwebtoken": "^9.0.3",
|
|
30
43
|
"knex": "^3.2.10",
|
|
31
44
|
"marked": "^18.0.3",
|
|
32
|
-
"morgan": "^1.10.1",
|
|
33
45
|
"mysql2": "^3.22.3",
|
|
46
|
+
"node-cron": "^3.0.3",
|
|
47
|
+
"pino": "^9.5.0",
|
|
48
|
+
"pino-http": "^10.3.0",
|
|
34
49
|
"serve-favicon": "^2.5.1",
|
|
35
50
|
"xss": "^1.0.15",
|
|
36
51
|
"ioredis": "^5.4.6"
|
|
37
52
|
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"pino-pretty": "^11.3.0"
|
|
55
|
+
},
|
|
38
56
|
"peerDependencies": {
|
|
39
|
-
"zod": "^
|
|
57
|
+
"zod": "^4.4.3"
|
|
40
58
|
},
|
|
41
59
|
"peerDependenciesMeta": {
|
|
42
60
|
"zod": {
|
package/response/code.js
CHANGED
|
@@ -52,18 +52,6 @@ export const CODE_DB_CONNECTION_ERROR = 6001;
|
|
|
52
52
|
export const CODE_DB_ACCESS_DENIED = 6002;
|
|
53
53
|
export const CODE_DB_OPERATION_TIMEOUT = 6007;
|
|
54
54
|
|
|
55
|
-
// 数据库原生错误标识 → 业务码映射
|
|
56
|
-
export const DB_ERROR = Object.freeze({
|
|
57
|
-
ECONNREFUSED: 6001,
|
|
58
|
-
ER_ACCESS_DENIED_ERROR: 6002,
|
|
59
|
-
ER_ROW_IS_REFERENCED_2: 6003,
|
|
60
|
-
ER_BAD_FIELD_ERROR: 6004,
|
|
61
|
-
ER_DUP_ENTRY: 6005,
|
|
62
|
-
ER_NO_SUCH_TABLE: 6006,
|
|
63
|
-
ETIMEDOUT: 6007,
|
|
64
|
-
ER_TABLE_EXISTS_ERROR: 1005,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
55
|
/**
|
|
68
56
|
* 根据业务码获取默认提示文案
|
|
69
57
|
* @param {number} code 业务状态码
|
package/response/response.js
CHANGED
|
@@ -31,7 +31,12 @@ export function serializeError(err, exposeDetail = false) {
|
|
|
31
31
|
if (isAppError(err)) {
|
|
32
32
|
const result = { success: false, code: err.code, msg: err.message };
|
|
33
33
|
const extra = {};
|
|
34
|
+
// 框架内部字段永远不外泄;stack/cause(可能含SQL) 仅 dev(exposeDetail=true) 时附带
|
|
35
|
+
const NEVER_EXPOSE = new Set(["code", "message", "name", "meta", "httpStatus"]);
|
|
36
|
+
const SENSITIVE = new Set(["stack", "cause"]);
|
|
34
37
|
for (const key of Object.getOwnPropertyNames(err)) {
|
|
38
|
+
if (NEVER_EXPOSE.has(key)) continue;
|
|
39
|
+
if (SENSITIVE.has(key) && !exposeDetail) continue; // 生产环境不泄露堆栈/根因(SQL)
|
|
35
40
|
const val = err[key];
|
|
36
41
|
if (val === undefined || val === null) continue;
|
|
37
42
|
if (typeof val === "function" || typeof val === "symbol") continue;
|
|
@@ -42,11 +47,12 @@ export function serializeError(err, exposeDetail = false) {
|
|
|
42
47
|
return result;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
// 原生未知系统错误:生产环境强制脱敏,不泄露 SQL / 堆栈
|
|
50
|
+
// 原生未知系统错误:生产环境强制脱敏,不泄露 SQL / 堆栈 / 根因
|
|
46
51
|
return {
|
|
47
52
|
success: false,
|
|
48
53
|
code: CODE_SYSTEM_ERROR,
|
|
49
|
-
|
|
54
|
+
// 生产环境不暴露原始 message(可能含 SQL 片段),仅 dev 透出
|
|
55
|
+
msg: exposeDetail ? (err?.message || "系统内部错误") : "系统内部错误",
|
|
50
56
|
...(exposeDetail && err?.stack ? { data: { stack: err.stack } } : {}),
|
|
51
57
|
};
|
|
52
58
|
}
|
package/security/keywords.js
CHANGED
|
@@ -22,11 +22,10 @@ const KEYWORD_RULES = Object.freeze({
|
|
|
22
22
|
commandInjection: [
|
|
23
23
|
"cmd=", "system(", "exec(", "shell_exec(", "passthru(",
|
|
24
24
|
"eval(", "assert(", "preg_replace", "bash -i", "rm -rf",
|
|
25
|
-
"wget ", "
|
|
25
|
+
"wget ", "base64_decode", "phpinfo()",
|
|
26
26
|
"killall", "shutdown", "fdisk", "mkfs", "dd ",
|
|
27
27
|
"scp ", "rsync", "nc ", "netcat", "nmap", "iptables",
|
|
28
|
-
"systemctl"
|
|
29
|
-
"userdel", "usermod", "groupadd", "groupdel", "passwd", "chpasswd"
|
|
28
|
+
"systemctl"
|
|
30
29
|
],
|
|
31
30
|
pathTraversal: [
|
|
32
31
|
"../", "..\\", "/etc/passwd", "/etc/shadow", "/etc/hosts",
|
package/utils/logger.js
CHANGED
|
@@ -1,117 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
* 统一日志工具
|
|
3
|
-
* 1. 本地dev:彩色友好打印
|
|
4
|
-
* 2. 线上prd:JSON单行字符串输出,供日志采集器解析
|
|
5
|
-
* 3. 日志分级控制 LOG_LEVEL,线上关闭调试日志减少磁盘占用
|
|
6
|
-
* 4. error/fatal 自动走 stderr
|
|
7
|
-
* 5. 支持自定义模块标签、本地文件落盘、错误堆栈结构化解析
|
|
8
|
-
*/
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import { parseStack } from "../core/errors.js";
|
|
12
|
-
|
|
13
|
-
const LOG_LEVELS = Object.freeze({
|
|
14
|
-
trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, silent: 99,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const COLORS = Object.freeze({
|
|
18
|
-
trace: "\x1b[90m",
|
|
19
|
-
debug: "\x1b[34m",
|
|
20
|
-
info: "\x1b[32m",
|
|
21
|
-
warn: "\x1b[33m",
|
|
22
|
-
error: "\x1b[31m",
|
|
23
|
-
fatal: "\x1b[41m\x1b[37m",
|
|
24
|
-
reset: "\x1b[0m",
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const ERROR_LEVELS = new Set(["error", "fatal"]);
|
|
1
|
+
import pino from "pino";
|
|
28
2
|
|
|
29
3
|
const NODE_ENV = (process.env.NODE_ENV || "dev").toLowerCase();
|
|
30
4
|
const IS_PROD = ["prd", "prod", "production"].includes(NODE_ENV);
|
|
31
|
-
const
|
|
32
|
-
const CURRENT_LEVEL = LOG_LEVELS[(process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL).toLowerCase()] ?? LOG_LEVELS.info;
|
|
5
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || (IS_PROD ? "info" : "debug");
|
|
33
6
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
7
|
+
/**
|
|
8
|
+
* pino 基础配置
|
|
9
|
+
* - dev:pino-pretty 彩色输出,单行易读
|
|
10
|
+
* - prod:纯 JSON,pm2 捕获 stdout
|
|
11
|
+
*/
|
|
12
|
+
const baseOptions = {
|
|
13
|
+
level: LOG_LEVEL,
|
|
14
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
15
|
+
};
|
|
39
16
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
17
|
+
if (!IS_PROD) {
|
|
18
|
+
baseOptions.transport = {
|
|
19
|
+
target: "pino-pretty",
|
|
20
|
+
options: {
|
|
21
|
+
colorize: true,
|
|
22
|
+
translateTime: "yyyy-mm-dd HH:mm:ss.l",
|
|
23
|
+
// dev 终端只做单进程开发,module 恒为 "Chan" 无区分意义,隐藏以精简输出
|
|
24
|
+
// (prod 仍保留 module 字段,便于日志聚合时按模块过滤)
|
|
25
|
+
ignore: "pid,hostname,module",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
45
28
|
}
|
|
46
29
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
30
|
+
/** pino 根实例 */
|
|
31
|
+
export const root = pino(baseOptions);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将多参数调用适配为 pino 格式
|
|
35
|
+
* 兼容现有用法:logger.info("msg")、logger.error("msg", err)、logger.warn("msg", obj)
|
|
36
|
+
*
|
|
37
|
+
* @param {string} level - 日志级别
|
|
38
|
+
* @param {import("pino").Logger} instance - pino 实例
|
|
39
|
+
* @param {...any} args - 业务传入的参数
|
|
40
|
+
*/
|
|
41
|
+
function emit(level, instance, ...args) {
|
|
42
|
+
const msgParts = [];
|
|
43
|
+
const mergeObj = {};
|
|
61
44
|
|
|
62
|
-
/** 解析日志入参,分离普通文本与错误堆栈 */
|
|
63
|
-
function parseArgs(args) {
|
|
64
|
-
let stackInfo = null;
|
|
65
|
-
const parts = [];
|
|
66
45
|
for (const arg of args) {
|
|
67
46
|
if (arg instanceof Error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
47
|
+
// Error 对象转为 pino 的 err 字段(自动序列化 stack)
|
|
48
|
+
mergeObj.err = arg;
|
|
49
|
+
msgParts.push(arg.message);
|
|
71
50
|
} else if (arg !== null && typeof arg === "object") {
|
|
72
|
-
|
|
73
|
-
catch { parts.push(String(arg)); }
|
|
51
|
+
Object.assign(mergeObj, arg);
|
|
74
52
|
} else {
|
|
75
|
-
|
|
53
|
+
msgParts.push(String(arg));
|
|
76
54
|
}
|
|
77
55
|
}
|
|
78
|
-
return { message: parts.join(" "), stackInfo };
|
|
79
|
-
}
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isLevelEnable(level)) return;
|
|
57
|
+
const msg = msgParts.join(" ");
|
|
58
|
+
const hasMerge = Object.keys(mergeObj).length > 0;
|
|
84
59
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (IS_PROD) {
|
|
91
|
-
const logLine = JSON.stringify({ time, env: NODE_ENV, level, label, message, stack: stackInfo, pid: process.pid });
|
|
92
|
-
printFn(logLine);
|
|
93
|
-
return;
|
|
60
|
+
if (hasMerge) {
|
|
61
|
+
instance[level](mergeObj, msg);
|
|
62
|
+
} else {
|
|
63
|
+
instance[level](msg);
|
|
94
64
|
}
|
|
95
|
-
|
|
96
|
-
const color = COLORS[level] ?? "";
|
|
97
|
-
printFn(`${color}[${time}] [${level.toUpperCase()}] [${label}]${COLORS.reset}`, ...args);
|
|
98
|
-
if (stackInfo) console.error(`${color}>>>> 错误堆栈详情:${COLORS.reset}`, stackInfo);
|
|
99
|
-
writeFile(`[${time}] [${level.toUpperCase()}] [${label}] ${message}`);
|
|
100
65
|
}
|
|
101
66
|
|
|
102
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 创建带固定标签的日志实例
|
|
69
|
+
* @param {string} [label="Chan"] - 模块标签
|
|
70
|
+
* @returns {{trace: Function, debug: Function, info: Function, warn: Function, error: Function, fatal: Function}}
|
|
71
|
+
*/
|
|
103
72
|
export function createLogger(label = "Chan") {
|
|
104
|
-
const
|
|
105
|
-
const make = level => (...args) => emit(level, safeLabel, args);
|
|
73
|
+
const instance = root.child({ module: label });
|
|
106
74
|
return {
|
|
107
|
-
trace:
|
|
108
|
-
debug:
|
|
109
|
-
info:
|
|
110
|
-
warn:
|
|
111
|
-
error:
|
|
112
|
-
fatal:
|
|
75
|
+
trace: (...args) => emit("trace", instance, ...args),
|
|
76
|
+
debug: (...args) => emit("debug", instance, ...args),
|
|
77
|
+
info: (...args) => emit("info", instance, ...args),
|
|
78
|
+
warn: (...args) => emit("warn", instance, ...args),
|
|
79
|
+
error: (...args) => emit("error", instance, ...args),
|
|
80
|
+
fatal: (...args) => emit("fatal", instance, ...args),
|
|
113
81
|
};
|
|
114
82
|
}
|
|
115
83
|
|
|
84
|
+
/** 默认导出:全局 logger 实例(标签 "Chan") */
|
|
116
85
|
const logger = createLogger("Chan");
|
|
117
86
|
export default logger;
|