wok-server 0.7.1 → 0.8.0
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 +101 -14
- package/dist/http-client/index.js +5 -4
- package/dist/lock/index.js +5 -2
- package/dist/mvc/config.js +4 -2
- package/dist/mvc/exchange.js +32 -7
- package/dist/mvc/index.js +1 -1
- package/dist/mvc/server.js +12 -4
- package/dist/mysql/manager/base.js +5 -8
- package/dist/mysql/manager/ops/delete.js +2 -1
- package/dist/mysql/manager/ops/find.js +8 -4
- package/dist/mysql/manager/ops/insert.js +15 -40
- package/dist/mysql/manager/ops/update.js +2 -1
- package/dist/mysql/manager/ops/upsert.js +11 -31
- package/dist/mysql/migration.js +10 -3
- package/documentation/en/mysql.md +26 -33
- package/documentation/zh-cn/mysql.md +27 -34
- package/documentation/zh-cn/philosophy.md +433 -0
- package/package.json +1 -1
- package/skills/wok-server-api-rules/SKILL.md +113 -0
- package/skills/wok-server-code-navigation/SKILL.md +169 -95
- package/skills/wok-server-mysql/SKILL.md +16 -10
- package/skills/wok-server-mysql/references/version-control.md +7 -5
- package/src/http-client/index.ts +8 -7
- package/src/lock/index.ts +5 -2
- package/src/mvc/config.ts +9 -2
- package/src/mvc/exchange.ts +31 -6
- package/src/mvc/index.ts +1 -1
- package/src/mvc/server.ts +11 -5
- package/src/mysql/manager/base.ts +17 -17
- package/src/mysql/manager/ops/delete.ts +2 -1
- package/src/mysql/manager/ops/find.ts +8 -4
- package/src/mysql/manager/ops/insert.ts +23 -61
- package/src/mysql/manager/ops/update.ts +2 -1
- package/src/mysql/manager/ops/upsert.ts +31 -51
- package/src/mysql/migration.ts +14 -3
- package/types/http-client/index.d.ts +4 -4
- package/types/lock/index.d.ts +3 -0
- package/types/mvc/config.d.ts +5 -0
- package/types/mvc/exchange.d.ts +13 -3
- package/types/mysql/manager/base.d.ts +12 -12
- package/types/mysql/manager/ops/insert.d.ts +2 -16
- package/types/mysql/manager/ops/upsert.d.ts +3 -4
|
@@ -242,6 +242,119 @@ await startWebServer({
|
|
|
242
242
|
|
|
243
243
|
---
|
|
244
244
|
|
|
245
|
+
## 规则 5:MySQL 更新方法传参——利用框架自动忽略 `undefined`
|
|
246
|
+
|
|
247
|
+
wok-server MySQL 组件会自动忽略值为 `undefined` 的字段,**不要**手动构建 `updateData` 对象:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// ❌ 啰嗦,不推荐
|
|
251
|
+
const updateData: Partial<Knowledge> = {}
|
|
252
|
+
if (body.name !== undefined) updateData.name = body.name
|
|
253
|
+
if (body.desc !== undefined) updateData.desc = body.desc
|
|
254
|
+
|
|
255
|
+
await session.updateOne(tableKnowledge, where, updateData)
|
|
256
|
+
|
|
257
|
+
// ✅ 简洁直观,推荐
|
|
258
|
+
await session.updateOne(tableKnowledge, where, {
|
|
259
|
+
name: body.name,
|
|
260
|
+
desc: body.desc,
|
|
261
|
+
question_prompt: body.question_prompt
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 规则 6:MySQL 更新接口参数必填性——与实体类保持一致
|
|
268
|
+
|
|
269
|
+
更新接口中出现的字段,其必填性应与实体类一致。实体中必填的字段,在更新接口中不应设为可选:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// ❌ 不推荐:name 在实体中是必填,这里却设成了可选
|
|
273
|
+
interface UpdateForm {
|
|
274
|
+
id: string
|
|
275
|
+
name?: string
|
|
276
|
+
desc?: string
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ✅ 推荐:出现的字段与实体类保持一致的必填性
|
|
280
|
+
interface UpdateForm {
|
|
281
|
+
id: string
|
|
282
|
+
name: string
|
|
283
|
+
desc?: string
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 规则 7:Entity 状态字段约束——禁止 `string`,必须使用联合类型
|
|
290
|
+
|
|
291
|
+
Entity 中的**状态字段禁止使用 `string` 类型**,必须使用**联合类型**精确约束所有合法值:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// ❌ 错误:使用 string 类型,没有任何约束
|
|
295
|
+
interface ChatbotSession {
|
|
296
|
+
status: string // 任何字符串都可以通过编译
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ✅ 正确:使用联合类型,精确约束
|
|
300
|
+
interface ChatbotSession {
|
|
301
|
+
/**
|
|
302
|
+
* 会话状态
|
|
303
|
+
*/
|
|
304
|
+
status: '临时' | '活跃'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface Chatbot {
|
|
308
|
+
/**
|
|
309
|
+
* 状态
|
|
310
|
+
*/
|
|
311
|
+
status: '未发布' | '已发布'
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**为什么必须这样做**:联合类型提供编译期检查,`string` 类型允许任何值,一旦写错(如 `'temprary'` vs `'临时'`)不会报错,运行时难以排查。
|
|
316
|
+
|
|
317
|
+
**检查方法**:定义或修改 Entity 时,检查每个 `status` 字段,确保它是联合类型而非 `string`。
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 规则 8:API Response 接口中枚举/联合类型必须引用实体类型
|
|
322
|
+
|
|
323
|
+
在 API 的 Response 接口中定义字段时:
|
|
324
|
+
- **枚举/联合类型字段**(如 `status`、`type`)**必须引用实体的类型定义**,禁止手写字面量值或使用 `number`/`string`
|
|
325
|
+
- **原始类型字段**(如 `number`、`string`、`boolean`)直接用原始类型,不要过度引用
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
// ❌ 错误:手写字面量值,与实体定义脱节
|
|
329
|
+
interface Resp {
|
|
330
|
+
status?: number // number 无法约束合法值
|
|
331
|
+
learning_task?: {
|
|
332
|
+
type: '视频' | '文档' | '考试' // 实体变了这里不会同步
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ❌ 错误:原始类型也去引用实体,过度设计
|
|
337
|
+
interface Resp {
|
|
338
|
+
duration: LearningTask['duration'] // duration 本来就是 number,没必要
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ✅ 正确:枚举/联合类型引用实体,原始类型直接用
|
|
342
|
+
import { User } from '../../user'
|
|
343
|
+
import { LearningTask } from '../learning_task/learning-task'
|
|
344
|
+
|
|
345
|
+
interface Resp {
|
|
346
|
+
status?: User['status'] // 引用实体的联合类型
|
|
347
|
+
learning_task?: {
|
|
348
|
+
type: LearningTask['type'] // 引用实体的联合类型
|
|
349
|
+
duration: number // 原始类型直接用
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**为什么**:联合类型的手写字面量会与实体定义脱节。当实体新增/修改合法值时,Response 接口不会被同步更新,导致类型泄露。引用实体类型确保两者始终一致。
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
245
358
|
## 附录 A:ServerExchange 速查表
|
|
246
359
|
|
|
247
360
|
所有路由 handler 和拦截器都通过 `ServerExchange` 与请求/响应交互。以下是其完整方法列表,禁止使用表中未列出的方法。
|
|
@@ -11,143 +11,217 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## 概述
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
面对一个基于 wok-server 的项目时,按以下路径快速理解项目结构、定位目标代码。核心原则:**项目按功能划分目录,路由集中配置,每个接口/拦截器独立一个文件**。
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## 通用
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### 找到后端代码位置
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
2. `startWebServer()` 调用,配置 routers 和 interceptors
|
|
22
|
-
3. 任务调度(`scheduleWithFixedDelay` 等)
|
|
20
|
+
一个项目可能前后端分离,也可能前端项目合并在同一个仓库。按以下顺序排查:
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
#### 1. 检查根目录
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
查看项目根目录是否有 `package.json`:
|
|
27
25
|
|
|
26
|
+
```bash
|
|
27
|
+
ls package.json
|
|
28
28
|
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
|
|
30
|
+
如果有,搜索其中是否依赖了 `wok-server`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
grep -o '"wok-server"' package.json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
如果匹配,说明根目录就是后端项目,后端代码就在根目录。
|
|
37
|
+
|
|
38
|
+
#### 2. 检查 backend / server 目录
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
如果根目录没有 `package.json` 或没有安装 wok-server,查找 `backend/` 或 `server/` 子目录:
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| 路由配置(大项目) | `src/router.ts` 或 `src/router/` 目录 |
|
|
44
|
-
| 某个接口的处理逻辑 | `src/{功能名}/{接口名}.ts`(如 `src/user/create-user.ts`) |
|
|
45
|
-
| 拦截器 | `src/{功能名}/{拦截器名}-interceptor.ts` 或 `src/exception.ts` |
|
|
46
|
-
| 数据库实体与表定义 | `src/{功能名}/{功能名}.ts`(如 `user/user.ts`) |
|
|
47
|
-
| MySQL 迁移脚本 | `db_migration/` 目录 |
|
|
48
|
-
| 环境变量 | 项目根目录 `.env` |
|
|
42
|
+
```bash
|
|
43
|
+
ls backend/package.json
|
|
44
|
+
```
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
搜索其中是否依赖了 `wok-server`。例如一个前后端分离的项目,后端代码可能在 `backend/` 目录:
|
|
51
47
|
|
|
52
48
|
```
|
|
53
|
-
|
|
54
|
-
├──
|
|
49
|
+
backend/
|
|
50
|
+
├── package.json # 包含 "wok-server": "^0.5.0"
|
|
55
51
|
├── src/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
│ │ ├── auth-interceptor.ts # 授权拦截器
|
|
59
|
-
│ │ ├── create-auth.ts # 创建授权接口
|
|
60
|
-
│ │ └── index.ts # 包入口,聚合导出
|
|
61
|
-
│ ├── user/ # 用户功能
|
|
62
|
-
│ │ ├── user.ts
|
|
63
|
-
│ │ ├── create-user.ts
|
|
64
|
-
│ │ └── index.ts
|
|
65
|
-
│ ├── exception.ts # 全局异常定义 + 异常拦截器
|
|
66
|
-
│ ├── router.ts # 路由集中配置(大项目)
|
|
67
|
-
│ └── main.ts # 入口文件(小项目路由也在这里)
|
|
68
|
-
├── .env
|
|
69
|
-
├── package.json
|
|
70
|
-
└── tsconfig.json
|
|
52
|
+
├── db-migration/
|
|
53
|
+
└── docker/
|
|
71
54
|
```
|
|
72
55
|
|
|
73
|
-
|
|
56
|
+
#### 3. 如果以上都行不通
|
|
74
57
|
|
|
75
|
-
|
|
58
|
+
- 搜索项目中的 `wok-server` 关键词(如 `from 'wok-server'`)
|
|
59
|
+
- 搜索 `startWebServer` 函数调用,这是 wok-server 的入口 API
|
|
60
|
+
- 搜索 `createJsonHandler`、`Table<`、`Routers` 等 wok-server 特有类型
|
|
76
61
|
|
|
77
|
-
|
|
62
|
+
> **确定了后端代码目录后**,后续所有文件路径都相对于该目录(以下称为**项目根目录**)。
|
|
78
63
|
|
|
79
|
-
|
|
80
|
-
2. **startWebServer() 调用** — 了解有哪些路由和拦截器
|
|
81
|
-
3. **任务调度调用** — 了解有哪些定时任务
|
|
64
|
+
### 真实项目结构示例
|
|
82
65
|
|
|
83
|
-
|
|
66
|
+
一个典型 wok-server 中型项目的目录结构(仅作参考,实际项目可能不同):
|
|
84
67
|
|
|
85
|
-
|
|
68
|
+
```
|
|
69
|
+
backend/
|
|
70
|
+
├── db-migration/ # MySQL 迁移文件
|
|
71
|
+
└── src/
|
|
72
|
+
├── main.ts # 入口文件
|
|
73
|
+
├── exception.ts # 全局异常定义 + 异常拦截器
|
|
74
|
+
├── router/
|
|
75
|
+
│ ├── json-handler.ts # 自定义 handler 工厂(二次封装)
|
|
76
|
+
│ └── rules/ # 按业务模块拆分的路由规则
|
|
77
|
+
│ ├── index.ts # 聚合所有模块路由
|
|
78
|
+
│ ├── contact.ts # 联系人模块路由
|
|
79
|
+
│ └── order.ts # 订单模块路由
|
|
80
|
+
├── contact/ # 联系人业务模块
|
|
81
|
+
│ ├── contact.ts # 数据实体 & 表定义
|
|
82
|
+
│ ├── contact-service.ts # 业务逻辑层
|
|
83
|
+
│ ├── bs-contact-create.ts
|
|
84
|
+
│ ├── bs-contact-get.ts
|
|
85
|
+
│ ├── field/ # 子功能:自定义字段
|
|
86
|
+
│ └── log/ # 子功能:操作日志
|
|
87
|
+
├── order/ # 订单模块
|
|
88
|
+
├── auth/ # 授权模块
|
|
89
|
+
├── redis/ # Redis 基础设施(项目扩展)
|
|
90
|
+
└── ali-oss/ # OSS 基础设施(项目扩展)
|
|
91
|
+
```
|
|
86
92
|
|
|
87
|
-
###
|
|
93
|
+
### 搜索技巧
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
如果以上方法定位不到文件,全文搜索以下关键词:
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
1. **路由路径字符串** — 一定能找到 handler 的 import 来源(因为 wok-server 路由使用完整路径作为 key)
|
|
98
|
+
2. `createJsonHandler` —— 列出直接使用内置工厂的 JSON 接口
|
|
99
|
+
3. `create.*Handler` —— 发现自定义封装的 handler 工厂及使用位置
|
|
100
|
+
4. `Table<` —— 列出所有数据表定义
|
|
101
|
+
5. `Interceptor` —— 列出所有拦截器
|
|
102
|
+
6. `schedule` —— 列出所有定时任务
|
|
103
|
+
7. `from 'wok-server'` —— 列出所有使用 wok-server API 的文件
|
|
94
104
|
|
|
95
|
-
|
|
105
|
+
## MVC(路由 / 拦截器 / Handler)
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
### 项目入口 — main.ts
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
- `{功能}-interceptor.ts` — 了解功能级拦截逻辑
|
|
101
|
-
- 其他 handler 文件 — 了解该功能的完整接口
|
|
109
|
+
入口文件在 `src/main.ts`,包含以下典型结构(以真实项目为例):
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
```ts
|
|
112
|
+
// 1. 全局初始化
|
|
113
|
+
Date.prototype.toJSON = function () { return this.getTime() as any }
|
|
114
|
+
process.on('uncaughtException', err => getLogger().error('未捕获的异常', err))
|
|
115
|
+
|
|
116
|
+
// 2. 启用基础设施
|
|
117
|
+
await enableRedis() // 项目扩展的,非框架内置
|
|
118
|
+
await enableMysql() // wok-server 框架自带的
|
|
119
|
+
await enableAliOss() // 项目扩展的,非框架内置
|
|
120
|
+
|
|
121
|
+
// 3. 启动 Web 服务 —— 配置路由和拦截器
|
|
122
|
+
await startWebServer({
|
|
123
|
+
interceptors: [globalErrorInterceptor],
|
|
124
|
+
routers: { '/': homepage, ...routers }
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 4. 定时任务
|
|
128
|
+
scheduleWithFixedDelay(30, 30, new MessageQueueTask({...}))
|
|
129
|
+
scheduleDailyTask(3, 0, new CorpFileCleanTask())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**所以 `main.ts` 是读懂项目的第一个文件**。从它可以看到:
|
|
133
|
+
- 项目用了哪些基础设施(框架自带的如 `enableMysql()`,项目扩展的如 `enableRedis()`、`enableAliOss()`)
|
|
134
|
+
- **拦截器链**:`interceptors` 数组定义了请求处理流程,例如 `请求 → globalErrorInterceptor → handler`
|
|
135
|
+
- **路由入口**:`routers` 对象指明了路由配置的位置
|
|
136
|
+
- **定时任务**:`scheduleWithFixedDelay`、`scheduleDailyTask` 等
|
|
104
137
|
|
|
105
|
-
###
|
|
138
|
+
### 从接口路径定位 handler(最可靠的方法)
|
|
106
139
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
wok-server 的路由配置使用接口完整路径作为 key,所以**在 `src/` 目录下搜索接口路径字符串,就能直接定位到 handler 的 import 位置**。不需要依赖固定的文件名或目录结构。
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
要找一个接口的处理逻辑?
|
|
144
|
+
→ 在 src/ 目录下搜索接口完整路径(如 '/bs/contact/create')
|
|
145
|
+
→ 找到匹配的路由配置位置
|
|
146
|
+
→ 在该行看到 handler 的 import 来源
|
|
147
|
+
→ Ctrl/Cmd + 点击跳转到 handler 文件
|
|
148
|
+
```
|
|
110
149
|
|
|
111
|
-
|
|
150
|
+
#### 关键原则
|
|
112
151
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
- **最可靠的方式**:从 `startWebServer()`(在 `src/main.ts`)的 `routers` 属性出发,这是路由配置的入口锚点,永远有效。
|
|
153
|
+
- **不要依赖固定的文件或目录名**:项目可能将路由写在 `src/main.ts`、`src/router.ts` 或 `src/router/rules/` 目录下,具体位置取决于项目规模。
|
|
154
|
+
- **wok-server 路由使用完整路径作为 key**,不会分散配置,所以搜索路径字符串一定能找到。
|
|
116
155
|
|
|
117
|
-
|
|
156
|
+
例如搜索 `/bs/contact/create` 就能在路由文件中找到:
|
|
118
157
|
|
|
119
|
-
|
|
120
|
-
|
|
158
|
+
```ts
|
|
159
|
+
'/bs/contact/create': bsContactCreate, // ← 直接看到 handler 名及其 import 来源
|
|
160
|
+
```
|
|
121
161
|
|
|
122
|
-
|
|
162
|
+
路由文件的形态取决于项目规模:
|
|
123
163
|
|
|
124
|
-
|
|
164
|
+
- **小型项目**:路由可能直接写在 `src/main.ts` 里
|
|
165
|
+
- **中型项目**:每个业务模块一个路由文件,通过 `...` 展开符合并导出
|
|
125
166
|
|
|
126
167
|
```ts
|
|
127
|
-
//
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return opts.handle(body, exchange, auth)
|
|
134
|
-
}
|
|
135
|
-
})
|
|
168
|
+
// router/rules/contact.ts
|
|
169
|
+
export const contactRouters: Routers = {
|
|
170
|
+
'/bs/contact/create': bsContactCreate,
|
|
171
|
+
'/bs/contact/delete': bsContactDelete,
|
|
172
|
+
'/bs/contact/get': bsContactGet,
|
|
173
|
+
// ...
|
|
136
174
|
}
|
|
137
175
|
```
|
|
138
176
|
|
|
139
|
-
**面对一个项目时,先搜索以下关键词,判断项目是否有自定义封装:**
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
- 搜 `create.*Handler` 或 `create.*Json` —— 发现自定义的 handler 工厂函数
|
|
143
|
-
- 搜 `from 'wok-server'` 中的 `createJsonHandler` —— 确认哪些文件直接用了内置版本
|
|
144
|
-
- 后续搜索接口文件时,用自定义函数名而非内置名
|
|
178
|
+
## MySQL
|
|
145
179
|
|
|
146
|
-
###
|
|
180
|
+
### 通过 SQL 迁移文件了解全局业务
|
|
181
|
+
|
|
182
|
+
SQL 迁移文件记录了完整的建表语句,可以从中了解项目涉及的所有业务表。查找方式如下:
|
|
183
|
+
|
|
184
|
+
1. 打开项目根目录下的 `.env` 文件
|
|
185
|
+
2. 查看 `MYSQL_VERSION_CONTROL_ENABLED` 变量是否为 `true`
|
|
186
|
+
3. 如果为 `true`,再看 `MYSQL_VERSION_CONTROL_DIR` 变量配置的迁移文件目录(如未设置,默认为 `db_migration`)
|
|
187
|
+
4. 如果 `.env` 不存在或 `MYSQL_VERSION_CONTROL_ENABLED` 不为 `true`,说明项目没有使用版本控制,即没有 SQL 迁移文件
|
|
188
|
+
|
|
189
|
+
通过迁移文件的建表语句,可以看到每个表的完整字段定义、索引、外键等,比实体 `interface` 更全面,有助于先对项目业务有个大致的了解。
|
|
190
|
+
|
|
191
|
+
### 通过实体文件查表结构
|
|
192
|
+
|
|
193
|
+
知道表名查配置,最快的方式是搜索 `tableName`,但不同项目的代码格式化风格可能不同(空格、引号),建议用正则搜索提高容错:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
搜索正则可匹配 tableName\s*:\s*['"]表名['"]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
例如搜索 `tableName:\s*['"]contact['"]`(注意正则中 `\s*` 可以匹配任意数量的空格,`['"]` 可以匹配单引号或双引号),找到匹配位置后,确认满足以下两个条件就是表的配置:
|
|
200
|
+
|
|
201
|
+
1. 该 `tableName` 所属的常量声明是 `Table<表类型>` 类型,例如 `export const tableContact: Table<Contact>`
|
|
202
|
+
2. `Table` 导入自 `wok-server`(即 `import { Table } from 'wok-server'`)
|
|
203
|
+
|
|
204
|
+
满足这两点,就找到了完整的表配置,从中可以看到表名、主键 ID、字段列表、时间戳配置等信息。
|
|
205
|
+
|
|
206
|
+
示例代码结构:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { Table } from 'wok-server'
|
|
210
|
+
|
|
211
|
+
export interface Contact {
|
|
212
|
+
id: string
|
|
213
|
+
corp_id: string
|
|
214
|
+
name: string
|
|
215
|
+
// ...
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const tableContact: Table<Contact> = {
|
|
219
|
+
tableName: 'contact', // ← 搜这个字符串找到这里
|
|
220
|
+
id: 'id', // 主键字段
|
|
221
|
+
columns: ['corp_id', 'name', /* ... */],
|
|
222
|
+
createdDate: { type: 'date', column: 'create_at' },
|
|
223
|
+
updatedDate: { type: 'date', column: 'update_at' }
|
|
224
|
+
}
|
|
225
|
+
```
|
|
147
226
|
|
|
148
|
-
|
|
149
|
-
2. 全文搜索 `createJsonHandler` —— 列出直接使用内置工厂的 JSON 接口
|
|
150
|
-
3. 全文搜索 `create.*Handler` —— 发现自定义封装的 handler 工厂及使用位置
|
|
151
|
-
4. 全文搜索 `Table<` —— 列出所有数据表定义
|
|
152
|
-
5. 全文搜索 `Interceptor` —— 列出所有拦截器
|
|
153
|
-
6. 全文搜索 `schedule` —— 列出所有定时任务
|
|
227
|
+
业务逻辑层通常在 `src/{功能名}/{功能名}-service.ts`。
|
|
@@ -106,7 +106,6 @@ export const tableUser: Table<User> = {
|
|
|
106
106
|
|
|
107
107
|
| JS 类型 | MySQL 字段类型 |
|
|
108
108
|
| :------------- | :------------------------------------------------------------------- |
|
|
109
|
-
| `boolean` | TINYINT |
|
|
110
109
|
| `number` | TINYINT, SMALLINT, INT, MEDIUMINT, YEAR, FLOAT, DOUBLE, BIGINT |
|
|
111
110
|
| `Date` | TIMESTAMP, DATE, DATETIME |
|
|
112
111
|
| `Buffer` | TINYBLOB, MEDIUMBLOB, LONGBLOB, BLOB, BINARY, VARBINARY, BIT |
|
|
@@ -115,6 +114,21 @@ export const tableUser: Table<User> = {
|
|
|
115
114
|
|
|
116
115
|
实体字段类型必须与数据库列类型匹配对应的 JS 原生类型,否则查询结果会不正确。可空字段在实体中也定义为可选(`?`)。
|
|
117
116
|
|
|
117
|
+
> **⚠️ `boolean` 类型安全警告**
|
|
118
|
+
>
|
|
119
|
+
> MySQL 的 `BOOLEAN` 实际是 `TINYINT(1)`,驱动返回 `0`/`1`(number),**不是** `true`/`false`。
|
|
120
|
+
> 实体类中将字段声明为 `boolean` 会导致 `===` 全等比较出现 bug,且手写 SQL 查询同样存在此问题。
|
|
121
|
+
>
|
|
122
|
+
> 框架**不提供**类型自动映射。最安全的做法是使用 `0 | 1` 类型,与数据库真实返回值保持一致:
|
|
123
|
+
>
|
|
124
|
+
> ```ts
|
|
125
|
+
> export interface User {
|
|
126
|
+
> is_active: 0 | 1 // 推荐
|
|
127
|
+
> }
|
|
128
|
+
> ```
|
|
129
|
+
>
|
|
130
|
+
> `0 | 1` 在条件判断中与 `boolean` 行为一致,仅在 `=== true/false` 时有差异。
|
|
131
|
+
|
|
118
132
|
---
|
|
119
133
|
|
|
120
134
|
## CRUD 操作
|
|
@@ -197,17 +211,9 @@ await manager.insertMany(tableUser, [
|
|
|
197
211
|
{ id: 'im001', nickname: '张飞' },
|
|
198
212
|
{ id: 'im002', nickname: '关羽' }
|
|
199
213
|
])
|
|
200
|
-
|
|
201
|
-
// 插入时使用表达式(InsertValue)
|
|
202
|
-
await manager.insert(tableUser, {
|
|
203
|
-
id: 'in002',
|
|
204
|
-
nickname: '小红',
|
|
205
|
-
balance: ['expr', '?? * ?', ['score', 2]],
|
|
206
|
-
createAt: ['now']
|
|
207
|
-
})
|
|
208
214
|
```
|
|
209
215
|
|
|
210
|
-
|
|
216
|
+
`insert` 要求传入完整的实体对象,必填字段必须存在,返回的对象是真实的 `T` 类型。
|
|
211
217
|
|
|
212
218
|
### Upsert
|
|
213
219
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# MySQL 版本管理
|
|
2
2
|
|
|
3
|
-
开启 `MYSQL_VERSION_CONTROL_ENABLED=true`,在 `db_migration/` 目录下创建 SQL
|
|
3
|
+
开启 `MYSQL_VERSION_CONTROL_ENABLED=true`,在 `db_migration/` 目录下创建 SQL 文件,文件名必须以数字开头,数字后可加描述文字:
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
db_migration/
|
|
7
|
-
1.sql
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1.sql # 等价于版本号 1
|
|
8
|
+
2_init.sql # 等价于版本号 2
|
|
9
|
+
3_add_user_index.sql # 等价于版本号 3
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
版本号必须从 1 开始连续递增。文件名示例:`1.sql`、`2_init.sql`、`3_add_user_index.sql`。不支持 `v1.sql` 或 `init_v1.sql` 这类不以数字开头的命名。
|
|
13
|
+
|
|
14
|
+
> **⚠️ 重复版本号校验**:如果有两个文件解析出版本号相同(如 `1_init.sql` 和 `1_create_table.sql` 都对应版本 1),启动时会抛异常,并明确指出冲突的文件路径。
|
|
13
15
|
|
|
14
16
|
启动时自动在事务中检测当前版本,顺序执行未执行的 SQL 并更新版本号。
|
|
15
17
|
|
package/src/http-client/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { IncomingHttpHeaders, request as requestHttp
|
|
2
|
-
import {
|
|
3
|
-
import { URL } from 'url'
|
|
1
|
+
import { Agent as HttpAgent, IncomingHttpHeaders, request as requestHttp } from 'http'
|
|
2
|
+
import { Agent as HttpsAgent, request as requestHttps } from 'https'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* 请求选项.
|
|
@@ -141,7 +140,7 @@ export function doRequest(opts: HttpRequestOpts): Promise<HttpResponseInfo> {
|
|
|
141
140
|
* @param opts
|
|
142
141
|
*/
|
|
143
142
|
export async function postJson<T>(
|
|
144
|
-
opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'> & { body:
|
|
143
|
+
opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'> & { body: unknown }
|
|
145
144
|
): Promise<T> {
|
|
146
145
|
const headers = Object.assign({}, opts.headers || {}, {
|
|
147
146
|
'Content-Type': 'application/json; charset=utf-8'
|
|
@@ -153,7 +152,8 @@ export async function postJson<T>(
|
|
|
153
152
|
timeout: opts.timeout,
|
|
154
153
|
method: 'POST',
|
|
155
154
|
body: JSON.stringify(opts.body),
|
|
156
|
-
followRedirect: false
|
|
155
|
+
followRedirect: false,
|
|
156
|
+
agent: opts.agent
|
|
157
157
|
})
|
|
158
158
|
if (res.status !== 200) {
|
|
159
159
|
if (res.body.byteLength < 1024) {
|
|
@@ -178,7 +178,7 @@ export async function postJson<T>(
|
|
|
178
178
|
* @param opts
|
|
179
179
|
*/
|
|
180
180
|
export async function getJson<T>(
|
|
181
|
-
opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout'>
|
|
181
|
+
opts: Pick<HttpRequestOpts, 'url' | 'query' | 'headers' | 'timeout' | 'agent'>
|
|
182
182
|
): Promise<T> {
|
|
183
183
|
const res = await doRequest({
|
|
184
184
|
url: opts.url,
|
|
@@ -186,7 +186,8 @@ export async function getJson<T>(
|
|
|
186
186
|
headers: opts.headers,
|
|
187
187
|
timeout: opts.timeout,
|
|
188
188
|
method: 'GET',
|
|
189
|
-
followRedirect: true
|
|
189
|
+
followRedirect: true,
|
|
190
|
+
agent: opts.agent
|
|
190
191
|
})
|
|
191
192
|
if (res.status !== 200) {
|
|
192
193
|
if (res.body.byteLength < 1024) {
|
package/src/lock/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface LockInfo {
|
|
|
14
14
|
/**
|
|
15
15
|
* 锁管理器,主要用于将不确定的顺序且有冲突的异步操作顺序执行,
|
|
16
16
|
* 防止异步流程庞大穿插执行造成的数据混乱和错误,常见于请求的处理。
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 注意:这是一个本地内存锁,仅在当前进程内生效,不支持分布式场景。
|
|
19
|
+
* 如果需要在多进程、多服务器环境下使用,请使用 Redis 等分布式锁方案。
|
|
17
20
|
*/
|
|
18
21
|
class ServerLockManager {
|
|
19
22
|
/**
|
|
@@ -23,7 +26,7 @@ class ServerLockManager {
|
|
|
23
26
|
|
|
24
27
|
constructor() {
|
|
25
28
|
// 定期清理,将过期的信息移除,防止内存泄漏
|
|
26
|
-
|
|
29
|
+
setInterval(() => {
|
|
27
30
|
const keysToBeDeleted: string[] = []
|
|
28
31
|
const now = Date.now()
|
|
29
32
|
for (const entry of this.lockMap.entries()) {
|
|
@@ -144,7 +147,7 @@ class ServerLockManager {
|
|
|
144
147
|
*/
|
|
145
148
|
private sleep() {
|
|
146
149
|
return new Promise<void>((resolve, reject) => {
|
|
147
|
-
setTimeout(resolve,
|
|
150
|
+
setTimeout(resolve, 50)
|
|
148
151
|
})
|
|
149
152
|
}
|
|
150
153
|
}
|
package/src/mvc/config.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface WebConfig {
|
|
|
41
41
|
* pem 格式证书私钥文件路径
|
|
42
42
|
*/
|
|
43
43
|
tlsKey: string
|
|
44
|
+
/**
|
|
45
|
+
* 请求体最大大小(字节),超出返回 413 Payload Too Large。
|
|
46
|
+
* 默认 10MB,设置为 0 表示不限制。
|
|
47
|
+
*/
|
|
48
|
+
maxBodySize: number
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export function getConfig() {
|
|
@@ -54,7 +59,8 @@ export function getConfig() {
|
|
|
54
59
|
corsAllowOrigin: '*',
|
|
55
60
|
tlsEnable: false,
|
|
56
61
|
tlsKey: '',
|
|
57
|
-
tlsCert: ''
|
|
62
|
+
tlsCert: '',
|
|
63
|
+
maxBodySize: 10 * 1024 * 1024
|
|
58
64
|
},
|
|
59
65
|
'SERVER',
|
|
60
66
|
{
|
|
@@ -64,7 +70,8 @@ export function getConfig() {
|
|
|
64
70
|
corsAllowOrigin: [notBlank()],
|
|
65
71
|
corsAllowHeaders: [notBlank()],
|
|
66
72
|
corsAllowMethods: [notBlank()],
|
|
67
|
-
tlsEnable: [notNull()]
|
|
73
|
+
tlsEnable: [notNull()],
|
|
74
|
+
maxBodySize: [notNull(), min(0)]
|
|
68
75
|
}
|
|
69
76
|
)
|
|
70
77
|
}
|