befly 3.9.12 → 3.9.14
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/docs/README.md +85 -0
- package/docs/addon.md +512 -0
- package/docs/api.md +1604 -0
- package/docs/cipher.md +580 -0
- package/docs/config.md +638 -0
- package/docs/database.md +147 -3
- package/docs/examples.md +892 -0
- package/docs/hook.md +754 -0
- package/docs/logger.md +495 -0
- package/docs/plugin.md +978 -0
- package/docs/quickstart.md +331 -0
- package/docs/sync.md +586 -0
- package/docs/table.md +765 -0
- package/docs/validator.md +618 -0
- package/loader/loadApis.ts +33 -36
- package/package.json +3 -3
- package/sync/syncDb/apply.ts +1 -1
- package/sync/syncDb/constants.ts +0 -11
- package/sync/syncDb/helpers.ts +0 -1
- package/sync/syncDb/table.ts +2 -2
- package/sync/syncDb/tableCreate.ts +3 -3
- package/tests/syncDb-constants.test.ts +1 -23
- package/tests/syncDb-helpers.test.ts +0 -1
- package/types/database.d.ts +0 -2
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# Validator 参数验证
|
|
2
|
+
|
|
3
|
+
> 请求参数验证与类型转换
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [概述](#概述)
|
|
8
|
+
- [验证器类](#验证器类)
|
|
9
|
+
- [验证规则](#验证规则)
|
|
10
|
+
- [类型转换](#类型转换)
|
|
11
|
+
- [正则别名](#正则别名)
|
|
12
|
+
- [验证钩子](#验证钩子)
|
|
13
|
+
- [API 字段定义](#api-字段定义)
|
|
14
|
+
- [验证结果](#验证结果)
|
|
15
|
+
- [使用示例](#使用示例)
|
|
16
|
+
- [FAQ](#faq)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 概述
|
|
21
|
+
|
|
22
|
+
Validator 是 Befly 的参数验证系统,提供:
|
|
23
|
+
|
|
24
|
+
- **数据验证**:根据字段定义验证数据
|
|
25
|
+
- **类型转换**:自动转换为目标类型
|
|
26
|
+
- **规则检查**:长度、范围、正则等
|
|
27
|
+
- **钩子集成**:自动验证 API 请求参数
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 验证器类
|
|
32
|
+
|
|
33
|
+
### 基本结构
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Validator } from '../lib/validator.js';
|
|
37
|
+
|
|
38
|
+
class Validator {
|
|
39
|
+
// 验证数据对象
|
|
40
|
+
static validate(data: Record<string, any>, rules: TableDefinition, required: string[]): ValidateResult;
|
|
41
|
+
|
|
42
|
+
// 验证单个值(带类型转换)
|
|
43
|
+
static single(value: any, fieldDef: FieldDefinition): SingleResult;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### validate 方法
|
|
48
|
+
|
|
49
|
+
批量验证数据对象:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const data = {
|
|
53
|
+
email: 'test@example.com',
|
|
54
|
+
age: 25,
|
|
55
|
+
name: 'John'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const rules = {
|
|
59
|
+
email: { name: '邮箱', type: 'string', min: 5, max: 100, regexp: '@email' },
|
|
60
|
+
age: { name: '年龄', type: 'number', min: 0, max: 150 },
|
|
61
|
+
name: { name: '姓名', type: 'string', min: 2, max: 50 }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = Validator.validate(data, rules, ['email', 'name']);
|
|
65
|
+
|
|
66
|
+
if (result.failed) {
|
|
67
|
+
console.log(result.firstError); // 第一条错误信息
|
|
68
|
+
console.log(result.errors); // 所有错误信息
|
|
69
|
+
console.log(result.errorFields); // 出错字段列表
|
|
70
|
+
console.log(result.fieldErrors); // 字段->错误映射
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### single 方法
|
|
75
|
+
|
|
76
|
+
验证单个值并进行类型转换:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const fieldDef = {
|
|
80
|
+
name: '年龄',
|
|
81
|
+
type: 'number',
|
|
82
|
+
min: 0,
|
|
83
|
+
max: 150,
|
|
84
|
+
default: 0
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = Validator.single('25', fieldDef);
|
|
88
|
+
|
|
89
|
+
if (!result.error) {
|
|
90
|
+
console.log(result.value); // 25 (已转换为 number)
|
|
91
|
+
} else {
|
|
92
|
+
console.log(result.error); // 错误信息
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 验证规则
|
|
99
|
+
|
|
100
|
+
### 字段定义格式
|
|
101
|
+
|
|
102
|
+
字段定义包含以下属性:
|
|
103
|
+
|
|
104
|
+
| 属性 | 类型 | 必填 | 说明 |
|
|
105
|
+
| ---------- | ------- | ---- | ------------------------ |
|
|
106
|
+
| `name` | string | 是 | 字段标签(用于错误提示) |
|
|
107
|
+
| `type` | string | 是 | 数据类型 |
|
|
108
|
+
| `min` | number | 否 | 最小值/长度 |
|
|
109
|
+
| `max` | number | 否 | 最大值/长度 |
|
|
110
|
+
| `default` | any | 否 | 默认值 |
|
|
111
|
+
| `regexp` | string | 否 | 正则表达式或别名 |
|
|
112
|
+
| `required` | boolean | 否 | 是否必填 |
|
|
113
|
+
|
|
114
|
+
### 支持的类型
|
|
115
|
+
|
|
116
|
+
| 类型 | 说明 | min/max 含义 |
|
|
117
|
+
| -------------- | ---------- | ------------ |
|
|
118
|
+
| `string` | 字符串 | 字符长度 |
|
|
119
|
+
| `text` | 长文本 | 字符长度 |
|
|
120
|
+
| `number` | 数字 | 数值范围 |
|
|
121
|
+
| `array_string` | 字符串数组 | 元素数量 |
|
|
122
|
+
| `array_text` | 文本数组 | 元素数量 |
|
|
123
|
+
|
|
124
|
+
### 验证逻辑
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
┌─────────────────────────────────────────────────────┐
|
|
128
|
+
│ 验证流程 │
|
|
129
|
+
├─────────────────────────────────────────────────────┤
|
|
130
|
+
│ 1. 参数检查 │
|
|
131
|
+
│ └── 确保 data 和 rules 是有效对象 │
|
|
132
|
+
│ ↓ │
|
|
133
|
+
│ 2. 必填字段检查 │
|
|
134
|
+
│ └── 检查 required 数组中的字段是否有值 │
|
|
135
|
+
│ ↓ │
|
|
136
|
+
│ 3. 类型转换 │
|
|
137
|
+
│ └── 将值转换为目标类型 │
|
|
138
|
+
│ ↓ │
|
|
139
|
+
│ 4. 规则验证 │
|
|
140
|
+
│ ├── 数字:检查 min/max 范围 │
|
|
141
|
+
│ ├── 字符串:检查长度 min/max │
|
|
142
|
+
│ ├── 数组:检查元素数量 │
|
|
143
|
+
│ └── 正则:检查格式是否匹配 │
|
|
144
|
+
│ ↓ │
|
|
145
|
+
│ 5. 构建结果 │
|
|
146
|
+
│ └── 返回 ValidateResult 对象 │
|
|
147
|
+
└─────────────────────────────────────────────────────┘
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 类型转换
|
|
153
|
+
|
|
154
|
+
### 转换规则
|
|
155
|
+
|
|
156
|
+
| 目标类型 | 输入 | 输出 |
|
|
157
|
+
| -------------- | ------------ | ------------ |
|
|
158
|
+
| `number` | `"123"` | `123` |
|
|
159
|
+
| `number` | `123` | `123` |
|
|
160
|
+
| `number` | `"abc"` | **错误** |
|
|
161
|
+
| `string` | `"hello"` | `"hello"` |
|
|
162
|
+
| `string` | `123` | **错误** |
|
|
163
|
+
| `array_string` | `["a", "b"]` | `["a", "b"]` |
|
|
164
|
+
| `array_string` | `"abc"` | **错误** |
|
|
165
|
+
|
|
166
|
+
### 空值处理
|
|
167
|
+
|
|
168
|
+
空值(`undefined`、`null`、`""`)时返回默认值:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// 有 default 属性时使用 default
|
|
172
|
+
{ type: 'number', default: 10 } → 10
|
|
173
|
+
{ type: 'string', default: '默认' } → '默认'
|
|
174
|
+
|
|
175
|
+
// 无 default 时使用类型默认值
|
|
176
|
+
{ type: 'number' } → 0
|
|
177
|
+
{ type: 'string' } → ''
|
|
178
|
+
{ type: 'array_string' } → []
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 数组默认值
|
|
182
|
+
|
|
183
|
+
数组类型的默认值可以是字符串格式:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// 字符串格式的数组默认值
|
|
187
|
+
{ type: 'array_string', default: '[]' } → []
|
|
188
|
+
{ type: 'array_string', default: '["a","b"]' } → ['a', 'b']
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 正则别名
|
|
194
|
+
|
|
195
|
+
### 使用方式
|
|
196
|
+
|
|
197
|
+
正则可以使用别名(以 `@` 开头)或直接写正则表达式:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// 使用别名
|
|
201
|
+
{
|
|
202
|
+
regexp: '@email';
|
|
203
|
+
} // 邮箱格式
|
|
204
|
+
{
|
|
205
|
+
regexp: '@phone';
|
|
206
|
+
} // 手机号格式
|
|
207
|
+
{
|
|
208
|
+
regexp: '@url';
|
|
209
|
+
} // URL 格式
|
|
210
|
+
|
|
211
|
+
// 直接使用正则
|
|
212
|
+
{
|
|
213
|
+
regexp: '^[a-z]+$';
|
|
214
|
+
} // 小写字母
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 内置别名
|
|
218
|
+
|
|
219
|
+
| 别名 | 说明 | 正则 |
|
|
220
|
+
| ----------- | --------------- | ------------------------ | -------- | ----- |
|
|
221
|
+
| `@number` | 正整数 | `^\d+$` |
|
|
222
|
+
| `@integer` | 整数(含负数) | `^-?\d+$` |
|
|
223
|
+
| `@float` | 浮点数 | `^-?\d+(\.\d+)?$` |
|
|
224
|
+
| `@positive` | 正整数(不含0) | `^[1-9]\d*$` |
|
|
225
|
+
| `@email` | 邮箱 | `^[a-zA-Z0-9._%+-]+@...` |
|
|
226
|
+
| `@phone` | 手机号 | `^1[3-9]\d{9}$` |
|
|
227
|
+
| `@url` | URL | `^https?://` |
|
|
228
|
+
| `@ip` | IPv4 | `^((25[0-5] | 2[0-4]\d | ...)` |
|
|
229
|
+
| `@uuid` | UUID | `^[0-9a-f]{8}-...` |
|
|
230
|
+
| `@date` | 日期 | `^\d{4}-\d{2}-\d{2}$` |
|
|
231
|
+
| `@time` | 时间 | `^\d{2}:\d{2}:\d{2}$` |
|
|
232
|
+
| `@datetime` | 日期时间 | `^\d{4}-\d{2}-\d{2}T...` |
|
|
233
|
+
|
|
234
|
+
### 完整别名列表
|
|
235
|
+
|
|
236
|
+
**数字类**:
|
|
237
|
+
|
|
238
|
+
- `@number` - 正整数
|
|
239
|
+
- `@integer` - 整数(含负数)
|
|
240
|
+
- `@float` - 浮点数
|
|
241
|
+
- `@positive` - 正整数(不含0)
|
|
242
|
+
- `@negative` - 负整数
|
|
243
|
+
- `@zero` - 零
|
|
244
|
+
|
|
245
|
+
**字符类**:
|
|
246
|
+
|
|
247
|
+
- `@word` - 纯字母
|
|
248
|
+
- `@alphanumeric` - 字母和数字
|
|
249
|
+
- `@alphanumeric_` - 字母、数字和下划线
|
|
250
|
+
- `@lowercase` - 小写字母
|
|
251
|
+
- `@uppercase` - 大写字母
|
|
252
|
+
- `@chinese` - 纯中文
|
|
253
|
+
- `@chineseWord` - 中文和字母
|
|
254
|
+
|
|
255
|
+
**网络类**:
|
|
256
|
+
|
|
257
|
+
- `@email` - 邮箱
|
|
258
|
+
- `@url` - URL
|
|
259
|
+
- `@ip` - IPv4
|
|
260
|
+
- `@ipv6` - IPv6
|
|
261
|
+
- `@domain` - 域名
|
|
262
|
+
|
|
263
|
+
**编码类**:
|
|
264
|
+
|
|
265
|
+
- `@uuid` - UUID
|
|
266
|
+
- `@hex` - 十六进制
|
|
267
|
+
- `@base64` - Base64
|
|
268
|
+
- `@md5` - MD5
|
|
269
|
+
- `@sha1` - SHA1
|
|
270
|
+
- `@sha256` - SHA256
|
|
271
|
+
|
|
272
|
+
**日期时间**:
|
|
273
|
+
|
|
274
|
+
- `@date` - 日期 YYYY-MM-DD
|
|
275
|
+
- `@time` - 时间 HH:MM:SS
|
|
276
|
+
- `@datetime` - ISO 日期时间
|
|
277
|
+
- `@year` - 年份
|
|
278
|
+
- `@month` - 月份
|
|
279
|
+
- `@day` - 日期
|
|
280
|
+
|
|
281
|
+
**标识符**:
|
|
282
|
+
|
|
283
|
+
- `@variable` - 变量名
|
|
284
|
+
- `@constant` - 常量名
|
|
285
|
+
- `@package` - 包名
|
|
286
|
+
- `@username` - 用户名
|
|
287
|
+
- `@nickname` - 昵称
|
|
288
|
+
|
|
289
|
+
**账号类**:
|
|
290
|
+
|
|
291
|
+
- `@phone` - 手机号
|
|
292
|
+
- `@telephone` - 固定电话
|
|
293
|
+
- `@idCard` - 身份证号
|
|
294
|
+
- `@bankCard` - 银行卡号
|
|
295
|
+
- `@qq` - QQ号
|
|
296
|
+
- `@wechat` - 微信号
|
|
297
|
+
|
|
298
|
+
**密码类**:
|
|
299
|
+
|
|
300
|
+
- `@passwordWeak` - 弱密码(6位以上)
|
|
301
|
+
- `@passwordMedium` - 中等密码(8位,含字母数字)
|
|
302
|
+
- `@passwordStrong` - 强密码(8位,含大小写、数字、特殊字符)
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 验证钩子
|
|
307
|
+
|
|
308
|
+
### 自动验证
|
|
309
|
+
|
|
310
|
+
Validator Hook 自动验证 API 请求参数:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// hooks/validator.ts
|
|
314
|
+
const hook: Hook = {
|
|
315
|
+
order: 6, // 在 parser 之后执行
|
|
316
|
+
handler: async (befly, ctx) => {
|
|
317
|
+
if (!ctx.api?.fields) return;
|
|
318
|
+
|
|
319
|
+
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
320
|
+
|
|
321
|
+
if (result.code !== 0) {
|
|
322
|
+
ctx.response = ErrorResponse(ctx, result.firstError || '参数验证失败', 1, null, result.fieldErrors);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 执行顺序
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
请求 → parser (解析) → validator (验证) → API handler
|
|
332
|
+
↓
|
|
333
|
+
验证失败则返回错误响应
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## API 字段定义
|
|
339
|
+
|
|
340
|
+
### 在 API 中定义字段
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// apis/user/login.ts
|
|
344
|
+
export default {
|
|
345
|
+
name: '用户登录',
|
|
346
|
+
method: 'POST',
|
|
347
|
+
auth: false,
|
|
348
|
+
fields: {
|
|
349
|
+
email: { name: '邮箱', type: 'string', min: 5, max: 100, regexp: '@email' },
|
|
350
|
+
password: { name: '密码', type: 'string', min: 6, max: 100 }
|
|
351
|
+
},
|
|
352
|
+
required: ['email', 'password'],
|
|
353
|
+
handler: async (befly, ctx) => {
|
|
354
|
+
// ctx.body.email 和 ctx.body.password 已验证
|
|
355
|
+
return Yes('登录成功');
|
|
356
|
+
}
|
|
357
|
+
} as ApiRoute;
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 引用表字段
|
|
361
|
+
|
|
362
|
+
可以引用表定义中的字段:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { adminTable } from '../../../tables/admin.js';
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
name: '创建管理员',
|
|
369
|
+
fields: {
|
|
370
|
+
email: adminTable.email, // 引用表字段
|
|
371
|
+
password: adminTable.password,
|
|
372
|
+
nickname: adminTable.nickname
|
|
373
|
+
},
|
|
374
|
+
required: ['email', 'password'],
|
|
375
|
+
handler: async (befly, ctx) => {
|
|
376
|
+
// ...
|
|
377
|
+
}
|
|
378
|
+
} as ApiRoute;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 使用公共字段
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { Fields } from '../../../config/fields.js';
|
|
385
|
+
|
|
386
|
+
export default {
|
|
387
|
+
name: '获取列表',
|
|
388
|
+
fields: {
|
|
389
|
+
...Fields.page, // 分页字段
|
|
390
|
+
...Fields.limit,
|
|
391
|
+
keyword: { name: '关键词', type: 'string', max: 50 }
|
|
392
|
+
},
|
|
393
|
+
handler: async (befly, ctx) => {
|
|
394
|
+
// ...
|
|
395
|
+
}
|
|
396
|
+
} as ApiRoute;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 验证结果
|
|
402
|
+
|
|
403
|
+
### ValidateResult 结构
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
interface ValidateResult {
|
|
407
|
+
code: number; // 0=成功,1=失败
|
|
408
|
+
failed: boolean; // 是否失败
|
|
409
|
+
firstError: string | null; // 第一条错误信息
|
|
410
|
+
errors: string[]; // 所有错误信息
|
|
411
|
+
errorFields: string[]; // 出错字段名列表
|
|
412
|
+
fieldErrors: Record<string, string>; // 字段->错误映射
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### SingleResult 结构
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
interface SingleResult {
|
|
420
|
+
value: any; // 转换后的值
|
|
421
|
+
error: string | null; // 错误信息
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 结果示例
|
|
426
|
+
|
|
427
|
+
**验证成功**:
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
{
|
|
431
|
+
code: 0,
|
|
432
|
+
failed: false,
|
|
433
|
+
firstError: null,
|
|
434
|
+
errors: [],
|
|
435
|
+
errorFields: [],
|
|
436
|
+
fieldErrors: {}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**验证失败**:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
{
|
|
444
|
+
code: 1,
|
|
445
|
+
failed: true,
|
|
446
|
+
firstError: '邮箱为必填项',
|
|
447
|
+
errors: ['邮箱为必填项', '密码长度不能少于6个字符'],
|
|
448
|
+
errorFields: ['email', 'password'],
|
|
449
|
+
fieldErrors: {
|
|
450
|
+
email: '邮箱为必填项',
|
|
451
|
+
password: '密码长度不能少于6个字符'
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 使用示例
|
|
459
|
+
|
|
460
|
+
### 示例 1:基本验证
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const data = {
|
|
464
|
+
username: 'john',
|
|
465
|
+
age: 25,
|
|
466
|
+
email: 'john@example.com'
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const rules = {
|
|
470
|
+
username: { name: '用户名', type: 'string', min: 2, max: 20 },
|
|
471
|
+
age: { name: '年龄', type: 'number', min: 0, max: 150 },
|
|
472
|
+
email: { name: '邮箱', type: 'string', regexp: '@email' }
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const result = Validator.validate(data, rules, ['username', 'email']);
|
|
476
|
+
// result.code === 0
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### 示例 2:类型转换
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const data = {
|
|
483
|
+
age: '25', // 字符串
|
|
484
|
+
score: '98.5' // 字符串
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const rules = {
|
|
488
|
+
age: { name: '年龄', type: 'number', min: 0 },
|
|
489
|
+
score: { name: '分数', type: 'number', min: 0, max: 100 }
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// 验证通过,'25' 会被转换为 25
|
|
493
|
+
const result = Validator.validate(data, rules);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 示例 3:数组验证
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const data = {
|
|
500
|
+
tags: ['vue', 'react', 'angular'],
|
|
501
|
+
ids: [1, 2, 3]
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const rules = {
|
|
505
|
+
tags: { name: '标签', type: 'array_string', min: 1, max: 10 },
|
|
506
|
+
ids: { name: 'ID列表', type: 'array_string', min: 1 }
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const result = Validator.validate(data, rules, ['tags']);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 示例 4:正则验证
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const data = {
|
|
516
|
+
phone: '13812345678',
|
|
517
|
+
email: 'test@example.com',
|
|
518
|
+
code: 'ABC123'
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const rules = {
|
|
522
|
+
phone: { name: '手机号', type: 'string', regexp: '@phone' },
|
|
523
|
+
email: { name: '邮箱', type: 'string', regexp: '@email' },
|
|
524
|
+
code: { name: '验证码', type: 'string', regexp: '^[A-Z0-9]{6}$' }
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const result = Validator.validate(data, rules);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 示例 5:单值验证
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// 验证并转换单个值
|
|
534
|
+
const ageResult = Validator.single('25', {
|
|
535
|
+
name: '年龄',
|
|
536
|
+
type: 'number',
|
|
537
|
+
min: 0,
|
|
538
|
+
max: 150
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (!ageResult.error) {
|
|
542
|
+
console.log(ageResult.value); // 25 (number)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 空值使用默认值
|
|
546
|
+
const emptyResult = Validator.single('', {
|
|
547
|
+
name: '数量',
|
|
548
|
+
type: 'number',
|
|
549
|
+
default: 10
|
|
550
|
+
});
|
|
551
|
+
console.log(emptyResult.value); // 10
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## FAQ
|
|
557
|
+
|
|
558
|
+
### Q: 如何自定义错误信息?
|
|
559
|
+
|
|
560
|
+
A: 目前错误信息由验证器自动生成,格式为 `{字段标签}{错误描述}`。可以在 API handler 中捕获验证结果后自定义处理。
|
|
561
|
+
|
|
562
|
+
### Q: 如何跳过某些字段的验证?
|
|
563
|
+
|
|
564
|
+
A: 不在 `fields` 中定义的字段不会被验证。如果字段不在 `required` 数组中且值为空,也会跳过验证。
|
|
565
|
+
|
|
566
|
+
### Q: 验证失败后请求参数还能用吗?
|
|
567
|
+
|
|
568
|
+
A: 验证失败时 Hook 会直接返回错误响应,不会执行 API handler。如果需要在 handler 中手动验证,可以禁用 validator hook。
|
|
569
|
+
|
|
570
|
+
### Q: 如何验证嵌套对象?
|
|
571
|
+
|
|
572
|
+
A: 目前只支持扁平对象验证。嵌套对象需要在 handler 中手动验证。
|
|
573
|
+
|
|
574
|
+
### Q: 正则别名可以扩展吗?
|
|
575
|
+
|
|
576
|
+
A: 正则别名定义在 `befly-shared/regex` 中,可以直接使用自定义正则字符串,不需要扩展别名。
|
|
577
|
+
|
|
578
|
+
### Q: 类型转换失败会怎样?
|
|
579
|
+
|
|
580
|
+
A: 类型转换失败会在 `errors` 中记录错误,如 "年龄必须是数字"。
|
|
581
|
+
|
|
582
|
+
### Q: 数组元素如何验证?
|
|
583
|
+
|
|
584
|
+
A: 数组类型会验证:
|
|
585
|
+
|
|
586
|
+
1. 值是否为数组
|
|
587
|
+
2. 元素数量是否在 min/max 范围内
|
|
588
|
+
3. 如果有 regexp,每个元素都会进行正则验证
|
|
589
|
+
|
|
590
|
+
### Q: 如何在验证前清理数据中的 null/undefined 值?
|
|
591
|
+
|
|
592
|
+
A: 使用 `befly.tool.cleanFields` 方法:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// 在 API handler 中使用
|
|
596
|
+
handler: async (befly, ctx) => {
|
|
597
|
+
const { nickname, phone, address } = ctx.body;
|
|
598
|
+
|
|
599
|
+
// 清理 null 和 undefined 值
|
|
600
|
+
const cleanData = befly.tool.cleanFields({
|
|
601
|
+
nickname: nickname,
|
|
602
|
+
phone: phone,
|
|
603
|
+
address: address
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// cleanData 只包含有效值
|
|
607
|
+
await befly.db.updData({
|
|
608
|
+
table: 'user',
|
|
609
|
+
data: cleanData,
|
|
610
|
+
where: { id: ctx.user.userId }
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return Yes('更新成功');
|
|
614
|
+
};
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
> **注意**:数据库操作(insData、updData 等)会自动过滤 null/undefined 值,通常不需要手动调用 cleanFields。
|
|
618
|
+
> 详见 [database.md](./database.md#nullundefined-值自动过滤)。
|
package/loader/loadApis.ts
CHANGED
|
@@ -20,42 +20,39 @@ import { projectApiDir } from '../paths.js';
|
|
|
20
20
|
import type { ApiRoute } from '../types/api.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* 这些字段会自动合并到所有 API 的 fields 中
|
|
25
|
-
* API 自定义的同名字段可以覆盖这些默认值
|
|
23
|
+
* 预定义的默认字段
|
|
26
24
|
*/
|
|
27
|
-
const
|
|
28
|
-
id: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
min: 0,
|
|
56
|
-
max: 2
|
|
25
|
+
const PRESET_FIELDS: Record<string, any> = {
|
|
26
|
+
'@id': { name: 'ID', type: 'number', min: 1, max: null },
|
|
27
|
+
'@page': { name: '页码', type: 'number', min: 1, max: 9999 },
|
|
28
|
+
'@limit': { name: '每页数量', type: 'number', min: 1, max: 100 },
|
|
29
|
+
'@keyword': { name: '关键词', type: 'string', min: 1, max: 50 },
|
|
30
|
+
'@state': { name: '状态', type: 'number', min: 0, max: 2 }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 处理字段定义,将 @ 符号引用替换为实际字段定义
|
|
35
|
+
*/
|
|
36
|
+
function processFields(fields: Record<string, any>): Record<string, any> {
|
|
37
|
+
if (!fields || typeof fields !== 'object') return fields;
|
|
38
|
+
|
|
39
|
+
const processed: Record<string, any> = {};
|
|
40
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
41
|
+
// 如果值是字符串且以 @ 开头,则查找预定义字段
|
|
42
|
+
if (typeof value === 'string' && value.startsWith('@')) {
|
|
43
|
+
if (PRESET_FIELDS[value]) {
|
|
44
|
+
processed[key] = PRESET_FIELDS[value];
|
|
45
|
+
} else {
|
|
46
|
+
// 未找到预定义字段,保持原值
|
|
47
|
+
processed[key] = value;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// 普通字段定义,保持原样
|
|
51
|
+
processed[key] = value;
|
|
52
|
+
}
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
return processed;
|
|
55
|
+
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
58
|
* 加载所有 API 路由
|
|
@@ -113,8 +110,8 @@ export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
|
|
|
113
110
|
// 设置默认值
|
|
114
111
|
const methodStr = (api.method || 'POST').toUpperCase();
|
|
115
112
|
api.auth = api.auth !== undefined ? api.auth : true;
|
|
116
|
-
//
|
|
117
|
-
api.fields =
|
|
113
|
+
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
114
|
+
api.fields = processFields(api.fields || {});
|
|
118
115
|
api.required = api.required || [];
|
|
119
116
|
|
|
120
117
|
// 构建路由路径(不含方法)
|