befly 3.9.11 → 3.9.13

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/api.md ADDED
@@ -0,0 +1,1368 @@
1
+ # Befly API 接口文档
2
+
3
+ > 本文档详细介绍 Befly 框架的 API 接口开发规范,包括路由定义、参数验证、权限控制、请求处理流程等。
4
+
5
+ ## 目录
6
+
7
+ - [Befly API 接口文档](#befly-api-接口文档)
8
+ - [目录](#目录)
9
+ - [概述](#概述)
10
+ - [核心特性](#核心特性)
11
+ - [目录结构](#目录结构-1)
12
+ - [项目 API](#项目-api)
13
+ - [Addon API](#addon-api)
14
+ - [文件命名规范](#文件命名规范)
15
+ - [API 定义](#api-定义)
16
+ - [基础结构](#基础结构)
17
+ - [完整类型定义](#完整类型定义)
18
+ - [请求上下文 (RequestContext)](#请求上下文-requestcontext)
19
+ - [结构定义](#结构定义)
20
+ - [常用属性](#常用属性)
21
+ - [响应函数](#响应函数)
22
+ - [Yes - 成功响应](#yes---成功响应)
23
+ - [No - 失败响应](#no---失败响应)
24
+ - [ErrorResponse - Hook 中断响应](#errorresponse---hook-中断响应)
25
+ - [FinalResponse - 最终响应](#finalresponse---最终响应)
26
+ - [字段定义与验证](#字段定义与验证)
27
+ - [默认字段](#默认字段)
28
+ - [字段定义格式](#字段定义格式)
29
+ - [字段类型](#字段类型)
30
+ - [验证规则](#验证规则)
31
+ - [实际案例](#实际案例)
32
+ - [案例一:公开接口(无需认证)](#案例一公开接口无需认证)
33
+ - [案例二:列表查询(需要认证)](#案例二列表查询需要认证)
34
+ - [案例三:新增数据](#案例三新增数据)
35
+ - [案例四:更新数据](#案例四更新数据)
36
+ - [案例五:删除数据](#案例五删除数据)
37
+ - [案例六:获取详情](#案例六获取详情)
38
+ - [案例七:支持 GET 和 POST](#案例七支持-get-和-post)
39
+ - [案例八:保留原始请求体(webhook)](#案例八保留原始请求体webhook)
40
+ - [案例九:预处理函数](#案例九预处理函数)
41
+ - [请求处理流程](#请求处理流程)
42
+ - [Hook 执行顺序(洋葱模型)](#hook-执行顺序洋葱模型)
43
+ - [中断请求](#中断请求)
44
+ - [路由加载机制](#路由加载机制)
45
+ - [加载顺序](#加载顺序)
46
+ - [路由映射规则](#路由映射规则)
47
+ - [多方法注册](#多方法注册)
48
+ - [BeflyContext 对象](#beflycontext-对象)
49
+ - [最佳实践](#最佳实践)
50
+ - [1. 字段引用优先级](#1-字段引用优先级)
51
+ - [2. 直接使用 ctx.body](#2-直接使用-ctxbody)
52
+ - [3. 明确字段赋值](#3-明确字段赋值)
53
+ - [4. 错误处理](#4-错误处理)
54
+ - [5. 时间字段使用 Date.now()](#5-时间字段使用-datenow)
55
+ - [常见问题](#常见问题)
56
+ - [高级用法](#高级用法)
57
+ - [事务处理](#事务处理)
58
+ - [批量操作](#批量操作)
59
+ - [复杂查询](#复杂查询)
60
+ - [缓存策略](#缓存策略)
61
+ - [分布式锁](#分布式锁)
62
+ - [数据导出](#数据导出)
63
+ - [文件流处理](#文件流处理)
64
+
65
+ ---
66
+
67
+ ## 概述
68
+
69
+ Befly 框架的 API 系统是一套基于约定优于配置的接口开发体系。通过简洁的 JSON 配置定义接口,自动完成路由注册、参数解析、字段验证、权限控制等功能。
70
+
71
+ ### 核心特性
72
+
73
+ - **约定式路由**:文件路径自动映射为 API 路径
74
+ - **声明式配置**:通过简洁的配置定义接口行为
75
+ - **自动字段验证**:基于字段定义自动验证请求参数
76
+ - **权限控制**:支持认证和角色权限检查
77
+ - **洋葱模型**:Hook 中间件按顺序处理请求
78
+
79
+ ---
80
+
81
+ ## 目录结构
82
+
83
+ ### 项目 API
84
+
85
+ ```
86
+ tpl/apis/
87
+ ├── user/
88
+ │ ├── login.ts → POST /api/user/login
89
+ │ ├── register.ts → POST /api/user/register
90
+ │ └── info.ts → POST /api/user/info
91
+ └── article/
92
+ ├── list.ts → POST /api/article/list
93
+ └── detail.ts → POST /api/article/detail
94
+ ```
95
+
96
+ ### Addon API
97
+
98
+ ```
99
+ addonAdmin/apis/
100
+ ├── auth/
101
+ │ ├── login.ts → POST /api/addon/addonAdmin/auth/login
102
+ │ └── logout.ts → POST /api/addon/addonAdmin/auth/logout
103
+ └── admin/
104
+ ├── list.ts → POST /api/addon/addonAdmin/admin/list
105
+ └── ins.ts → POST /api/addon/addonAdmin/admin/ins
106
+ ```
107
+
108
+ ### 文件命名规范
109
+
110
+ | 动作 | 后缀 | 说明 | 示例 |
111
+ | ---- | -------- | ------ | --------------- |
112
+ | 添加 | `Ins` | Insert | `userIns.ts` |
113
+ | 更新 | `Upd` | Update | `userUpd.ts` |
114
+ | 删除 | `Del` | Delete | `userDel.ts` |
115
+ | 列表 | `List` | List | `userList.ts` |
116
+ | 全部 | `All` | All | `userAll.ts` |
117
+ | 详情 | `Detail` | Detail | `userDetail.ts` |
118
+
119
+ ---
120
+
121
+ ## API 定义
122
+
123
+ ### 基础结构
124
+
125
+ ```typescript
126
+ import type { ApiRoute } from 'befly-core/types/api';
127
+
128
+ export default {
129
+ // 必填字段
130
+ name: '接口名称', // 接口描述,用于日志和文档
131
+ handler: async (befly, ctx) => {
132
+ // 处理逻辑
133
+ return befly.tool.Yes('成功', { data });
134
+ },
135
+
136
+ // 可选字段
137
+ method: 'POST', // HTTP 方法,默认 POST
138
+ auth: true, // 是否需要认证,默认 true
139
+ fields: {}, // 字段定义(验证规则)
140
+ required: [], // 必填字段列表
141
+ rawBody: false, // 是否保留原始请求体
142
+ preprocess: undefined, // 预处理函数
143
+ cache: undefined, // 缓存时间(秒)
144
+ rateLimit: undefined // 限流配置
145
+ } as ApiRoute;
146
+ ```
147
+
148
+ ### 完整类型定义
149
+
150
+ ```typescript
151
+ interface ApiRoute<T = any, R = any> {
152
+ /** 接口名称(必填) */
153
+ name: string;
154
+
155
+ /** 处理器函数(必填) */
156
+ handler: ApiHandler<T, R>;
157
+
158
+ /** HTTP 方法(可选,默认 POST,支持逗号分隔多个方法) */
159
+ method?: 'GET' | 'POST' | 'GET,POST' | 'POST,GET';
160
+
161
+ /** 认证类型(可选,默认 true)
162
+ * - true: 需要登录
163
+ * - false: 公开访问(无需登录)
164
+ */
165
+ auth?: boolean;
166
+
167
+ /** 字段定义(验证规则)(可选,默认 {}) */
168
+ fields?: TableDefinition;
169
+
170
+ /** 必填字段(可选,默认 []) */
171
+ required?: string[];
172
+
173
+ /** 是否保留原始请求体(可选,默认 false)
174
+ * - true: 不过滤字段,保留完整请求体(适用于微信回调、webhook 等场景)
175
+ * - false: 根据 fields 定义过滤字段
176
+ */
177
+ rawBody?: boolean;
178
+
179
+ /** 请求预处理函数(可选,在 handler 之前执行)
180
+ * 用于解密、转换请求数据等场景
181
+ * 可以修改 ctx.body
182
+ */
183
+ preprocess?: ApiHandler<T, void>;
184
+
185
+ /** 缓存配置(可选,单位:秒) */
186
+ cache?: number;
187
+
188
+ /** 限流配置(可选,格式:次数/秒,如 "10/60" 表示 60秒内10次) */
189
+ rateLimit?: string;
190
+
191
+ /** 路由路径(运行时生成,无需手动设置) */
192
+ route?: string;
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## 请求上下文 (RequestContext)
199
+
200
+ ### 结构定义
201
+
202
+ ```typescript
203
+ interface RequestContext {
204
+ /** 请求方法 (GET/POST) */
205
+ method: string;
206
+
207
+ /** 请求体参数(已解析和过滤) */
208
+ body: Record<string, any>;
209
+
210
+ /** 用户信息(从 JWT 解析) */
211
+ user: Record<string, any>;
212
+
213
+ /** 原始请求对象 */
214
+ req: Request;
215
+
216
+ /** 请求开始时间(毫秒) */
217
+ now: number;
218
+
219
+ /** 客户端 IP 地址 */
220
+ ip: string;
221
+
222
+ /** 请求头 */
223
+ headers: Headers;
224
+
225
+ /** API 路由路径(如 POST/api/user/login) */
226
+ route: string;
227
+
228
+ /** 请求唯一 ID */
229
+ requestId: string;
230
+
231
+ /** CORS 响应头 */
232
+ corsHeaders: Record<string, string>;
233
+
234
+ /** 当前请求的 API 路由对象 */
235
+ api?: ApiRoute;
236
+
237
+ /** 响应对象(设置后将直接返回) */
238
+ response?: Response;
239
+
240
+ /** 原始处理结果 */
241
+ result?: any;
242
+ }
243
+ ```
244
+
245
+ ### 常用属性
246
+
247
+ | 属性 | 类型 | 说明 |
248
+ | ----------- | --------- | ---------------------- |
249
+ | `ctx.body` | `object` | 已解析的请求参数 |
250
+ | `ctx.user` | `object` | 当前登录用户信息 |
251
+ | `ctx.ip` | `string` | 客户端 IP |
252
+ | `ctx.now` | `number` | 请求开始时间戳(毫秒) |
253
+ | `ctx.route` | `string` | 完整路由路径 |
254
+ | `ctx.req` | `Request` | 原始 Request 对象 |
255
+
256
+ ---
257
+
258
+ ## 响应函数
259
+
260
+ ### Yes - 成功响应
261
+
262
+ ```typescript
263
+ befly.tool.Yes(msg: string, data?: any, other?: Record<string, any>)
264
+ ```
265
+
266
+ 返回格式:
267
+
268
+ ```json
269
+ {
270
+ "code": 0,
271
+ "msg": "成功消息",
272
+ "data": { ... }
273
+ }
274
+ ```
275
+
276
+ ### No - 失败响应
277
+
278
+ ```typescript
279
+ befly.tool.No(msg: string, data?: any, other?: Record<string, any>)
280
+ ```
281
+
282
+ 返回格式:
283
+
284
+ ```json
285
+ {
286
+ "code": 1,
287
+ "msg": "失败消息",
288
+ "data": null
289
+ }
290
+ ```
291
+
292
+ ### ErrorResponse - Hook 中断响应
293
+
294
+ 在 Hook 中使用,用于提前拦截请求:
295
+
296
+ ```typescript
297
+ import { ErrorResponse } from 'befly-core/util';
298
+
299
+ // 在 Hook 中使用
300
+ ctx.response = ErrorResponse(ctx, '未授权', 1, null);
301
+ ```
302
+
303
+ ### FinalResponse - 最终响应
304
+
305
+ 在 API 路由末尾自动调用,无需手动使用。自动处理 `ctx.result` 并记录请求日志。
306
+
307
+ ---
308
+
309
+ ## 字段定义与验证
310
+
311
+ ### 默认字段
312
+
313
+ 所有 API 自动包含以下默认字段,无需重复定义:
314
+
315
+ ```typescript
316
+ const DEFAULT_API_FIELDS = {
317
+ id: {
318
+ name: 'ID',
319
+ type: 'number',
320
+ min: 1,
321
+ max: null
322
+ },
323
+ page: {
324
+ name: '页码',
325
+ type: 'number',
326
+ min: 1,
327
+ max: 9999
328
+ },
329
+ limit: {
330
+ name: '每页数量',
331
+ type: 'number',
332
+ min: 1,
333
+ max: 100
334
+ },
335
+ keyword: {
336
+ name: '关键词',
337
+ type: 'string',
338
+ min: 1,
339
+ max: 50
340
+ },
341
+ state: {
342
+ name: '状态',
343
+ type: 'number',
344
+ min: 0,
345
+ max: 2
346
+ }
347
+ };
348
+ ```
349
+
350
+ ### 字段定义格式
351
+
352
+ ```typescript
353
+ fields: {
354
+ // 方式一:引用表字段
355
+ email: adminTable.email,
356
+
357
+ // 方式二:自定义字段
358
+ account: {
359
+ name: '账号',
360
+ type: 'string',
361
+ min: 3,
362
+ max: 100
363
+ },
364
+
365
+ // 方式三:字符串格式
366
+ // "字段标签|类型|最小|最大|默认|必填|正则"
367
+ username: '用户名|string|3|20'
368
+ }
369
+ ```
370
+
371
+ ### 字段类型
372
+
373
+ | 类型 | 说明 | 数据库映射 |
374
+ | -------------- | ---------- | ----------------- |
375
+ | `string` | 字符串 | VARCHAR |
376
+ | `number` | 数字 | BIGINT |
377
+ | `text` | 长文本 | MEDIUMTEXT / TEXT |
378
+ | `array_string` | 字符串数组 | VARCHAR (JSON) |
379
+ | `array_text` | 文本数组 | MEDIUMTEXT (JSON) |
380
+
381
+ ### 验证规则
382
+
383
+ ```typescript
384
+ interface FieldDefinition {
385
+ name: string; // 字段名称(用于错误提示)
386
+ type: string; // 字段类型
387
+ min?: number; // 最小值/最小长度
388
+ max?: number; // 最大值/最大长度
389
+ default?: any; // 默认值
390
+ required?: boolean; // 是否必填
391
+ regex?: string; // 正则表达式
392
+ }
393
+ ```
394
+
395
+ ---
396
+
397
+ ## 实际案例
398
+
399
+ ### 案例一:公开接口(无需认证)
400
+
401
+ ```typescript
402
+ // apis/auth/login.ts
403
+ import adminTable from '../../tables/admin.json';
404
+
405
+ export default {
406
+ name: '管理员登录',
407
+ auth: false, // 公开接口
408
+ fields: {
409
+ account: {
410
+ name: '账号',
411
+ type: 'string',
412
+ min: 3,
413
+ max: 100
414
+ },
415
+ password: adminTable.password
416
+ },
417
+ required: ['account', 'password'],
418
+ handler: async (befly, ctx) => {
419
+ // 查询用户
420
+ const admin = await befly.db.getOne({
421
+ table: 'addon_admin_admin',
422
+ where: {
423
+ $or: [{ username: ctx.body.account }, { email: ctx.body.account }]
424
+ }
425
+ });
426
+
427
+ if (!admin?.id) {
428
+ return befly.tool.No('账号或密码错误');
429
+ }
430
+
431
+ // 验证密码
432
+ const isValid = await befly.cipher.verifyPassword(ctx.body.password, admin.password);
433
+
434
+ if (!isValid) {
435
+ return befly.tool.No('账号或密码错误');
436
+ }
437
+
438
+ // 生成 Token
439
+ const token = await befly.jwt.sign({
440
+ id: admin.id,
441
+ roleCode: admin.roleCode
442
+ });
443
+
444
+ return befly.tool.Yes('登录成功', {
445
+ token: token,
446
+ userInfo: admin
447
+ });
448
+ }
449
+ };
450
+ ```
451
+
452
+ ### 案例二:列表查询(需要认证)
453
+
454
+ ```typescript
455
+ // apis/admin/list.ts
456
+ export default {
457
+ name: '获取管理员列表',
458
+ // auth: true, // 默认需要认证
459
+ handler: async (befly, ctx) => {
460
+ const result = await befly.db.getList({
461
+ table: 'addon_admin_admin',
462
+ page: ctx.body.page || 1,
463
+ limit: ctx.body.limit || 10,
464
+ where: {
465
+ roleCode: { $ne: 'dev' }
466
+ },
467
+ orderBy: ['createdAt#DESC']
468
+ });
469
+
470
+ return befly.tool.Yes('获取成功', result);
471
+ }
472
+ };
473
+ ```
474
+
475
+ ### 案例三:新增数据
476
+
477
+ ```typescript
478
+ // apis/admin/ins.ts
479
+ import adminTable from '../../tables/admin.json';
480
+
481
+ export default {
482
+ name: '添加管理员',
483
+ fields: adminTable,
484
+ required: ['username', 'password', 'roleId'],
485
+ handler: async (befly, ctx) => {
486
+ // 检查用户名是否已存在
487
+ const existing = await befly.db.getOne({
488
+ table: 'addon_admin_admin',
489
+ where: { username: ctx.body.username }
490
+ });
491
+
492
+ if (existing) {
493
+ return befly.tool.No('用户名已被使用');
494
+ }
495
+
496
+ // 查询角色信息
497
+ const role = await befly.db.getOne({
498
+ table: 'addon_admin_role',
499
+ where: { id: ctx.body.roleId },
500
+ columns: ['code']
501
+ });
502
+
503
+ if (!role?.code) {
504
+ return befly.tool.No('角色不存在');
505
+ }
506
+
507
+ // 加密密码
508
+ const hashedPassword = await befly.cipher.hashPassword(ctx.body.password);
509
+
510
+ // 创建管理员
511
+ const adminId = await befly.db.insData({
512
+ table: 'addon_admin_admin',
513
+ data: {
514
+ username: ctx.body.username,
515
+ password: hashedPassword,
516
+ nickname: ctx.body.nickname,
517
+ roleId: ctx.body.roleId,
518
+ roleCode: role.code
519
+ }
520
+ });
521
+
522
+ return befly.tool.Yes('添加成功', {
523
+ id: adminId
524
+ });
525
+ }
526
+ };
527
+ ```
528
+
529
+ ### 案例四:更新数据
530
+
531
+ ```typescript
532
+ // apis/admin/upd.ts
533
+ import adminTable from '../../tables/admin.json';
534
+
535
+ export default {
536
+ name: '更新管理员',
537
+ fields: adminTable,
538
+ required: ['id'],
539
+ handler: async (befly, ctx) => {
540
+ const { id, ...updateData } = ctx.body;
541
+
542
+ // 检查管理员是否存在
543
+ const admin = await befly.db.getOne({
544
+ table: 'addon_admin_admin',
545
+ where: { id: id }
546
+ });
547
+
548
+ if (!admin?.id) {
549
+ return befly.tool.No('管理员不存在');
550
+ }
551
+
552
+ // 更新管理员信息
553
+ await befly.db.updData({
554
+ table: 'addon_admin_admin',
555
+ data: updateData,
556
+ where: { id: id }
557
+ });
558
+
559
+ return befly.tool.Yes('更新成功');
560
+ }
561
+ };
562
+ ```
563
+
564
+ ### 案例五:删除数据
565
+
566
+ ```typescript
567
+ // apis/admin/del.ts
568
+ export default {
569
+ name: '删除管理员',
570
+ fields: {},
571
+ required: ['id'],
572
+ handler: async (befly, ctx) => {
573
+ // 检查管理员是否存在
574
+ const admin = await befly.db.getOne({
575
+ table: 'addon_admin_admin',
576
+ where: { id: ctx.body.id }
577
+ });
578
+
579
+ if (!admin) {
580
+ return befly.tool.No('管理员不存在');
581
+ }
582
+
583
+ // 业务检查:不能删除开发者账号
584
+ if (admin.roleCode === 'dev') {
585
+ return befly.tool.No('不能删除开发者账号');
586
+ }
587
+
588
+ // 删除管理员
589
+ await befly.db.delData({
590
+ table: 'addon_admin_admin',
591
+ where: { id: ctx.body.id }
592
+ });
593
+
594
+ return befly.tool.Yes('删除成功');
595
+ }
596
+ };
597
+ ```
598
+
599
+ ### 案例六:获取详情
600
+
601
+ ```typescript
602
+ // apis/admin/detail.ts
603
+ export default {
604
+ name: '获取用户信息',
605
+ handler: async (befly, ctx) => {
606
+ const userId = ctx.user?.id;
607
+
608
+ if (!userId) {
609
+ return befly.tool.No('未授权');
610
+ }
611
+
612
+ // 查询用户信息
613
+ const admin = await befly.db.getOne({
614
+ table: 'addon_admin_admin',
615
+ where: { id: userId }
616
+ });
617
+
618
+ if (!admin) {
619
+ return befly.tool.No('用户不存在');
620
+ }
621
+
622
+ // 查询角色信息
623
+ let roleInfo = null;
624
+ if (admin.roleCode) {
625
+ roleInfo = await befly.db.getOne({
626
+ table: 'addon_admin_role',
627
+ where: { code: admin.roleCode }
628
+ });
629
+ }
630
+
631
+ // 返回用户信息(不包含密码)
632
+ const { password: _, ...userWithoutPassword } = admin;
633
+
634
+ return befly.tool.Yes('获取成功', {
635
+ ...userWithoutPassword,
636
+ role: roleInfo
637
+ });
638
+ }
639
+ };
640
+ ```
641
+
642
+ ### 案例七:支持 GET 和 POST
643
+
644
+ ```typescript
645
+ // apis/article/search.ts
646
+ export default {
647
+ name: '搜索文章',
648
+ method: 'GET,POST', // 同时支持 GET 和 POST
649
+ auth: false,
650
+ fields: {
651
+ keyword: {
652
+ name: '关键词',
653
+ type: 'string',
654
+ min: 1,
655
+ max: 100
656
+ }
657
+ },
658
+ required: ['keyword'],
659
+ handler: async (befly, ctx) => {
660
+ const result = await befly.db.getList({
661
+ table: 'article',
662
+ where: {
663
+ title: { $like: `%${ctx.body.keyword}%` }
664
+ }
665
+ });
666
+
667
+ return befly.tool.Yes('搜索成功', result);
668
+ }
669
+ };
670
+ ```
671
+
672
+ ### 案例八:保留原始请求体(webhook)
673
+
674
+ ```typescript
675
+ // apis/webhook/wechat.ts
676
+ export default {
677
+ name: '微信回调',
678
+ method: 'POST',
679
+ auth: false,
680
+ rawBody: true, // 不过滤字段,保留完整请求体
681
+ handler: async (befly, ctx) => {
682
+ // ctx.body 包含完整的微信回调数据
683
+ const { ToUserName, FromUserName, MsgType, Content } = ctx.body;
684
+
685
+ // 处理微信消息
686
+ befly.logger.info({ msg: Content }, '收到微信消息');
687
+
688
+ return befly.tool.Yes('处理成功');
689
+ }
690
+ };
691
+ ```
692
+
693
+ ### 案例九:预处理函数
694
+
695
+ ```typescript
696
+ // apis/data/import.ts
697
+ export default {
698
+ name: '导入数据',
699
+ preprocess: async (befly, ctx) => {
700
+ // 在 handler 之前执行
701
+ // 可以解密、转换数据
702
+ if (ctx.body.encryptedData) {
703
+ ctx.body.data = await befly.cipher.decrypt(ctx.body.encryptedData);
704
+ }
705
+ },
706
+ handler: async (befly, ctx) => {
707
+ // 使用预处理后的数据
708
+ const data = ctx.body.data;
709
+
710
+ // 处理导入逻辑...
711
+
712
+ return befly.tool.Yes('导入成功');
713
+ }
714
+ };
715
+ ```
716
+
717
+ ---
718
+
719
+ ## 请求处理流程
720
+
721
+ ### Hook 执行顺序(洋葱模型)
722
+
723
+ ```
724
+ 请求进入
725
+
726
+ ┌─────────────────────────────────────────────┐
727
+ │ 1. cors (order: 2) │
728
+ │ - 设置 CORS 响应头 │
729
+ │ - 处理 OPTIONS 预检请求 │
730
+ ├─────────────────────────────────────────────┤
731
+ │ 2. auth (order: 3) │
732
+ │ - 解析 Authorization Header │
733
+ │ - 验证 JWT Token │
734
+ │ - 设置 ctx.user │
735
+ ├─────────────────────────────────────────────┤
736
+ │ 3. parser (order: 4) │
737
+ │ - 解析 GET 查询参数 │
738
+ │ - 解析 POST JSON/XML 请求体 │
739
+ │ - 根据 fields 过滤字段 │
740
+ │ - 设置 ctx.body │
741
+ ├─────────────────────────────────────────────┤
742
+ │ 4. validator (order: 6) │
743
+ │ - 验证 ctx.body 参数 │
744
+ │ - 检查必填字段 │
745
+ │ - 验证类型、长度、正则 │
746
+ ├─────────────────────────────────────────────┤
747
+ │ 5. permission (order: 6) │
748
+ │ - 检查 auth 配置 │
749
+ │ - 验证用户登录状态 │
750
+ │ - 检查角色权限 │
751
+ ├─────────────────────────────────────────────┤
752
+ │ 6. preprocess (如果定义) │
753
+ │ - 执行 API 预处理函数 │
754
+ ├─────────────────────────────────────────────┤
755
+ │ 7. handler │
756
+ │ - 执行 API 处理函数 │
757
+ │ - 返回结果 │
758
+ ├─────────────────────────────────────────────┤
759
+ │ 8. FinalResponse │
760
+ │ - 格式化响应 │
761
+ │ - 记录请求日志 │
762
+ └─────────────────────────────────────────────┘
763
+
764
+ 响应返回
765
+ ```
766
+
767
+ ### 中断请求
768
+
769
+ 在任何 Hook 或 preprocess 中设置 `ctx.response` 可以中断请求处理:
770
+
771
+ ```typescript
772
+ // 在 Hook 中中断
773
+ if (!ctx.user?.id) {
774
+ ctx.response = ErrorResponse(ctx, '未登录');
775
+ return; // 后续 Hook 和 handler 不会执行
776
+ }
777
+ ```
778
+
779
+ ---
780
+
781
+ ## 路由加载机制
782
+
783
+ ### 加载顺序
784
+
785
+ 1. **项目 API**:`tpl/apis/**/*.ts` → `/api/...`
786
+ 2. **Addon API**:`addonXxx/apis/**/*.ts` → `/api/addon/addonXxx/...`
787
+
788
+ ### 路由映射规则
789
+
790
+ | 文件路径 | 生成路由 |
791
+ | ------------------------------- | --------------------------------------- |
792
+ | `tpl/apis/user/login.ts` | `POST /api/user/login` |
793
+ | `tpl/apis/article/list.ts` | `POST /api/article/list` |
794
+ | `addonAdmin/apis/auth/login.ts` | `POST /api/addon/addonAdmin/auth/login` |
795
+ | `addonAdmin/apis/admin/list.ts` | `POST /api/addon/addonAdmin/admin/list` |
796
+
797
+ ### 多方法注册
798
+
799
+ 当 `method: 'GET,POST'` 时,会同时注册两个路由:
800
+
801
+ - `GET /api/user/search`
802
+ - `POST /api/user/search`
803
+
804
+ ---
805
+
806
+ ## BeflyContext 对象
807
+
808
+ handler 函数的第一个参数 `befly` 提供框架核心功能:
809
+
810
+ ```typescript
811
+ interface BeflyContext {
812
+ // 数据库操作
813
+ db: DbHelper;
814
+
815
+ // Redis 操作
816
+ redis: RedisHelper;
817
+
818
+ // 缓存操作
819
+ cache: CacheHelper;
820
+
821
+ // JWT 操作
822
+ jwt: Jwt;
823
+
824
+ // 加密操作
825
+ cipher: Cipher;
826
+
827
+ // 日志
828
+ logger: Logger;
829
+
830
+ // 工具函数
831
+ tool: {
832
+ Yes: (msg: string, data?: any, other?: object) => object;
833
+ No: (msg: string, data?: any, other?: object) => object;
834
+ };
835
+
836
+ // 配置
837
+ config: BeflyConfig;
838
+ }
839
+ ```
840
+
841
+ ---
842
+
843
+ ## 最佳实践
844
+
845
+ ### 1. 字段引用优先级
846
+
847
+ ```typescript
848
+ fields: {
849
+ // 1. 首选:使用默认字段(id, page, limit, keyword, state)
850
+ // 无需定义,自动包含
851
+
852
+ // 2. 次选:引用表字段
853
+ email: adminTable.email,
854
+ password: adminTable.password,
855
+
856
+ // 3. 最后:自定义字段
857
+ customField: {
858
+ name: '自定义字段',
859
+ type: 'string',
860
+ min: 1,
861
+ max: 100
862
+ }
863
+ }
864
+ ```
865
+
866
+ ### 2. 直接使用 ctx.body
867
+
868
+ ```typescript
869
+ // ✅ 推荐:直接使用
870
+ const result = await befly.db.insData({
871
+ table: 'user',
872
+ data: {
873
+ username: ctx.body.username,
874
+ email: ctx.body.email
875
+ }
876
+ });
877
+
878
+ // ❌ 避免:不必要的解构
879
+ const { username, email } = ctx.body;
880
+ ```
881
+
882
+ ### 3. 明确字段赋值
883
+
884
+ ```typescript
885
+ // ✅ 推荐:明确每个字段
886
+ await befly.db.insData({
887
+ table: 'user',
888
+ data: {
889
+ username: ctx.body.username,
890
+ email: ctx.body.email,
891
+ password: hashedPassword
892
+ }
893
+ });
894
+
895
+ // ❌ 避免:扩展运算符
896
+ await befly.db.insData({
897
+ table: 'user',
898
+ data: { ...ctx.body } // 危险!可能写入未预期的字段
899
+ });
900
+ ```
901
+
902
+ ### 4. 错误处理
903
+
904
+ ```typescript
905
+ handler: async (befly, ctx) => {
906
+ try {
907
+ // 业务逻辑
908
+ const result = await someOperation();
909
+ return befly.tool.Yes('成功', result);
910
+ } catch (error: any) {
911
+ // 记录错误日志
912
+ befly.logger.error({ err: error }, '操作失败');
913
+ // 返回友好错误信息
914
+ return befly.tool.No('操作失败,请稍后重试');
915
+ }
916
+ };
917
+ ```
918
+
919
+ ### 5. 时间字段使用 Date.now()
920
+
921
+ ```typescript
922
+ // ✅ 推荐:使用 Date.now()
923
+ await befly.db.updData({
924
+ table: 'user',
925
+ data: {
926
+ lastLoginTime: Date.now(), // number 类型
927
+ lastLoginIp: ctx.ip
928
+ },
929
+ where: { id: ctx.user.id }
930
+ });
931
+
932
+ // ❌ 避免:使用 new Date()
933
+ lastLoginTime: new Date(); // 类型不一致
934
+ ```
935
+
936
+ ---
937
+
938
+ ## 常见问题
939
+
940
+ ### Q1: 如何设置公开接口?
941
+
942
+ ```typescript
943
+ export default {
944
+ name: '公开接口',
945
+ auth: false, // 设置为 false
946
+ handler: async (befly, ctx) => {
947
+ // ...
948
+ }
949
+ };
950
+ ```
951
+
952
+ ### Q2: 如何获取当前用户?
953
+
954
+ ```typescript
955
+ handler: async (befly, ctx) => {
956
+ const userId = ctx.user?.id;
957
+ const roleCode = ctx.user?.roleCode;
958
+
959
+ if (!userId) {
960
+ return befly.tool.No('未登录');
961
+ }
962
+ };
963
+ ```
964
+
965
+ ### Q3: 如何处理文件上传?
966
+
967
+ 文件上传需要使用 `rawBody: true` 保留原始请求体,然后手动解析。
968
+
969
+ ### Q4: 如何添加自定义 Hook?
970
+
971
+ 在 `tpl/hooks/` 目录创建 Hook 文件:
972
+
973
+ ```typescript
974
+ // tpl/hooks/requestLog.ts
975
+ import type { Hook } from 'befly-core/types/hook';
976
+
977
+ const hook: Hook = {
978
+ order: 100, // 执行顺序
979
+ handler: async (befly, ctx) => {
980
+ befly.logger.info({ route: ctx.route }, '请求开始');
981
+ }
982
+ };
983
+ export default hook;
984
+ ```
985
+
986
+ ### Q5: 为什么参数验证失败?
987
+
988
+ 1. 检查字段是否在 `fields` 中定义
989
+ 2. 检查必填字段是否在 `required` 中
990
+ 3. 检查参数类型是否匹配
991
+ 4. 检查参数值是否在 min/max 范围内
992
+
993
+ ---
994
+
995
+ ## 高级用法
996
+
997
+ ### 事务处理
998
+
999
+ 在需要保证数据一致性的场景中使用事务:
1000
+
1001
+ ```typescript
1002
+ // apis/order/create.ts
1003
+ export default {
1004
+ name: '创建订单',
1005
+ fields: {
1006
+ productId: {
1007
+ name: '商品ID',
1008
+ type: 'number',
1009
+ min: 1
1010
+ },
1011
+ quantity: {
1012
+ name: '数量',
1013
+ type: 'number',
1014
+ min: 1,
1015
+ max: 999
1016
+ }
1017
+ },
1018
+ required: ['productId', 'quantity'],
1019
+ handler: async (befly, ctx) => {
1020
+ // 使用事务确保库存扣减和订单创建的原子性
1021
+ const result = await befly.db.transaction(async (trx) => {
1022
+ // 1. 查询商品信息(带锁)
1023
+ const product = await trx.getOne({
1024
+ table: 'product',
1025
+ where: { id: ctx.body.productId },
1026
+ forUpdate: true // 行锁
1027
+ });
1028
+
1029
+ if (!product) {
1030
+ throw new Error('商品不存在');
1031
+ }
1032
+
1033
+ if (product.stock < ctx.body.quantity) {
1034
+ throw new Error('库存不足');
1035
+ }
1036
+
1037
+ // 2. 扣减库存
1038
+ await trx.updData({
1039
+ table: 'product',
1040
+ data: {
1041
+ stock: product.stock - ctx.body.quantity
1042
+ },
1043
+ where: { id: ctx.body.productId }
1044
+ });
1045
+
1046
+ // 3. 创建订单
1047
+ const orderId = await trx.insData({
1048
+ table: 'order',
1049
+ data: {
1050
+ userId: ctx.user.id,
1051
+ productId: ctx.body.productId,
1052
+ quantity: ctx.body.quantity,
1053
+ totalPrice: product.price * ctx.body.quantity,
1054
+ status: 'pending'
1055
+ }
1056
+ });
1057
+
1058
+ // 4. 创建订单明细
1059
+ await trx.insData({
1060
+ table: 'order_item',
1061
+ data: {
1062
+ orderId: orderId,
1063
+ productId: ctx.body.productId,
1064
+ productName: product.name,
1065
+ price: product.price,
1066
+ quantity: ctx.body.quantity
1067
+ }
1068
+ });
1069
+
1070
+ return { orderId: orderId };
1071
+ });
1072
+
1073
+ return befly.tool.Yes('订单创建成功', result);
1074
+ }
1075
+ };
1076
+ ```
1077
+
1078
+ ### 批量操作
1079
+
1080
+ #### 批量插入
1081
+
1082
+ ```typescript
1083
+ // apis/user/batchImport.ts
1084
+ export default {
1085
+ name: '批量导入用户',
1086
+ rawBody: true, // 保留原始请求体
1087
+ handler: async (befly, ctx) => {
1088
+ const users = ctx.body.users;
1089
+
1090
+ if (!Array.isArray(users) || users.length === 0) {
1091
+ return befly.tool.No('用户列表不能为空');
1092
+ }
1093
+
1094
+ if (users.length > 100) {
1095
+ return befly.tool.No('单次导入不能超过100条');
1096
+ }
1097
+
1098
+ // 批量插入
1099
+ const result = await befly.db.batchInsert({
1100
+ table: 'user',
1101
+ data: users.map((user: any) => ({
1102
+ username: user.username,
1103
+ email: user.email,
1104
+ nickname: user.nickname || user.username,
1105
+ state: 1
1106
+ }))
1107
+ });
1108
+
1109
+ return befly.tool.Yes('导入成功', {
1110
+ total: users.length,
1111
+ inserted: result.affectedRows
1112
+ });
1113
+ }
1114
+ };
1115
+ ```
1116
+
1117
+ #### 批量更新
1118
+
1119
+ ```typescript
1120
+ // apis/article/batchUpdate.ts
1121
+ export default {
1122
+ name: '批量更新文章状态',
1123
+ rawBody: true,
1124
+ handler: async (befly, ctx) => {
1125
+ const { ids, state } = ctx.body;
1126
+
1127
+ if (!Array.isArray(ids) || ids.length === 0) {
1128
+ return befly.tool.No('文章ID列表不能为空');
1129
+ }
1130
+
1131
+ // 批量更新
1132
+ const result = await befly.db.updData({
1133
+ table: 'article',
1134
+ data: { state: state },
1135
+ where: {
1136
+ id: { $in: ids }
1137
+ }
1138
+ });
1139
+
1140
+ return befly.tool.Yes('更新成功', {
1141
+ updated: result.affectedRows
1142
+ });
1143
+ }
1144
+ };
1145
+ ```
1146
+
1147
+ #### 批量删除
1148
+
1149
+ ```typescript
1150
+ // apis/log/batchDelete.ts
1151
+ export default {
1152
+ name: '批量删除日志',
1153
+ rawBody: true,
1154
+ handler: async (befly, ctx) => {
1155
+ const { ids } = ctx.body;
1156
+
1157
+ if (!Array.isArray(ids) || ids.length === 0) {
1158
+ return befly.tool.No('日志ID列表不能为空');
1159
+ }
1160
+
1161
+ // 批量软删除
1162
+ const result = await befly.db.delData({
1163
+ table: 'operate_log',
1164
+ where: {
1165
+ id: { $in: ids }
1166
+ }
1167
+ });
1168
+
1169
+ return befly.tool.Yes('删除成功', {
1170
+ deleted: result.affectedRows
1171
+ });
1172
+ }
1173
+ };
1174
+ ```
1175
+
1176
+ ### 复杂查询
1177
+
1178
+ #### 多表关联查询
1179
+
1180
+ ```typescript
1181
+ // apis/order/detail.ts
1182
+ export default {
1183
+ name: '订单详情',
1184
+ required: ['id'],
1185
+ handler: async (befly, ctx) => {
1186
+ // 查询订单基本信息
1187
+ const order = await befly.db.getOne({
1188
+ table: 'order',
1189
+ where: { id: ctx.body.id }
1190
+ });
1191
+
1192
+ if (!order) {
1193
+ return befly.tool.No('订单不存在');
1194
+ }
1195
+
1196
+ // 查询订单明细
1197
+ const items = await befly.db.getAll({
1198
+ table: 'order_item',
1199
+ where: { orderId: order.id }
1200
+ });
1201
+
1202
+ // 查询用户信息
1203
+ const user = await befly.db.getOne({
1204
+ table: 'user',
1205
+ where: { id: order.userId },
1206
+ columns: ['id', 'username', 'nickname', 'phone']
1207
+ });
1208
+
1209
+ return befly.tool.Yes('获取成功', {
1210
+ ...order,
1211
+ items: items,
1212
+ user: user
1213
+ });
1214
+ }
1215
+ };
1216
+ ```
1217
+
1218
+ #### 使用 JOIN 查询
1219
+
1220
+ ```typescript
1221
+ // apis/article/listWithAuthor.ts
1222
+ export default {
1223
+ name: '文章列表(含作者)',
1224
+ handler: async (befly, ctx) => {
1225
+ const result = await befly.db.getList({
1226
+ table: 'article',
1227
+ joins: [
1228
+ {
1229
+ type: 'LEFT',
1230
+ table: 'user',
1231
+ alias: 'author',
1232
+ on: { 'article.authorId': 'author.id' }
1233
+ }
1234
+ ],
1235
+ columns: ['article.id', 'article.title', 'article.createdAt', 'author.nickname AS authorName'],
1236
+ page: ctx.body.page || 1,
1237
+ limit: ctx.body.limit || 10,
1238
+ orderBy: ['article.createdAt#DESC']
1239
+ });
1240
+
1241
+ return befly.tool.Yes('获取成功', result);
1242
+ }
1243
+ };
1244
+ ```
1245
+
1246
+ ### 缓存策略
1247
+
1248
+ ```typescript
1249
+ // apis/config/getSiteConfig.ts
1250
+ export default {
1251
+ name: '获取站点配置',
1252
+ auth: false,
1253
+ cache: 300, // 缓存 5 分钟
1254
+ handler: async (befly, ctx) => {
1255
+ // 先从缓存获取
1256
+ const cacheKey = 'site:config';
1257
+ let config = await befly.redis.get(cacheKey);
1258
+
1259
+ if (!config) {
1260
+ // 缓存不存在,从数据库查询
1261
+ config = await befly.db.getAll({
1262
+ table: 'sys_config',
1263
+ where: { state: 1 }
1264
+ });
1265
+
1266
+ // 写入缓存
1267
+ await befly.redis.set(cacheKey, JSON.stringify(config), 'EX', 300);
1268
+ } else {
1269
+ config = JSON.parse(config);
1270
+ }
1271
+
1272
+ return befly.tool.Yes('获取成功', config);
1273
+ }
1274
+ };
1275
+ ```
1276
+
1277
+ ### 分布式锁
1278
+
1279
+ ```typescript
1280
+ // apis/task/execute.ts
1281
+ export default {
1282
+ name: '执行定时任务',
1283
+ handler: async (befly, ctx) => {
1284
+ const lockKey = `lock:task:${ctx.body.taskId}`;
1285
+
1286
+ // 尝试获取锁(30秒超时)
1287
+ const locked = await befly.redis.set(lockKey, ctx.requestId, 'EX', 30, 'NX');
1288
+
1289
+ if (!locked) {
1290
+ return befly.tool.No('任务正在执行中,请稍后');
1291
+ }
1292
+
1293
+ try {
1294
+ // 执行任务逻辑
1295
+ await executeTask(ctx.body.taskId);
1296
+
1297
+ return befly.tool.Yes('任务执行成功');
1298
+ } finally {
1299
+ // 释放锁
1300
+ await befly.redis.del(lockKey);
1301
+ }
1302
+ }
1303
+ };
1304
+ ```
1305
+
1306
+ ### 数据导出
1307
+
1308
+ ```typescript
1309
+ // apis/report/exportUsers.ts
1310
+ export default {
1311
+ name: '导出用户数据',
1312
+ handler: async (befly, ctx) => {
1313
+ // 查询所有用户(不分页)
1314
+ const users = await befly.db.getAll({
1315
+ table: 'user',
1316
+ columns: ['id', 'username', 'nickname', 'email', 'phone', 'createdAt'],
1317
+ where: { state: 1 },
1318
+ orderBy: ['createdAt#DESC']
1319
+ });
1320
+
1321
+ // 转换为 CSV 格式
1322
+ const headers = ['ID', '用户名', '昵称', '邮箱', '手机', '注册时间'];
1323
+ const rows = users.map((u: any) => [u.id, u.username, u.nickname, u.email, u.phone, new Date(u.createdAt).toLocaleString()]);
1324
+
1325
+ const csv = [headers.join(','), ...rows.map((r: any[]) => r.join(','))].join('\n');
1326
+
1327
+ // 返回 CSV 文件
1328
+ return new Response(csv, {
1329
+ headers: {
1330
+ 'Content-Type': 'text/csv; charset=utf-8',
1331
+ 'Content-Disposition': 'attachment; filename="users.csv"'
1332
+ }
1333
+ });
1334
+ }
1335
+ };
1336
+ ```
1337
+
1338
+ ### 文件流处理
1339
+
1340
+ ```typescript
1341
+ // apis/file/download.ts
1342
+ export default {
1343
+ name: '文件下载',
1344
+ required: ['fileId'],
1345
+ handler: async (befly, ctx) => {
1346
+ // 查询文件信息
1347
+ const file = await befly.db.getOne({
1348
+ table: 'file',
1349
+ where: { id: ctx.body.fileId }
1350
+ });
1351
+
1352
+ if (!file) {
1353
+ return befly.tool.No('文件不存在');
1354
+ }
1355
+
1356
+ // 读取文件并返回流
1357
+ const fileStream = Bun.file(file.path);
1358
+
1359
+ return new Response(fileStream, {
1360
+ headers: {
1361
+ 'Content-Type': file.mimeType,
1362
+ 'Content-Disposition': `attachment; filename="${encodeURIComponent(file.name)}"`,
1363
+ 'Content-Length': String(file.size)
1364
+ }
1365
+ });
1366
+ }
1367
+ };
1368
+ ```