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/api.md ADDED
@@ -0,0 +1,1604 @@
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
+ 框架提供了一套预定义字段系统,通过 `@` 符号引用常用字段,避免重复定义。
314
+
315
+ #### 可用预定义字段
316
+
317
+ ```typescript
318
+ const PRESET_FIELDS = {
319
+ '@id': {
320
+ name: 'ID',
321
+ type: 'number',
322
+ min: 1,
323
+ max: null
324
+ },
325
+ '@page': {
326
+ name: '页码',
327
+ type: 'number',
328
+ min: 1,
329
+ max: 9999
330
+ },
331
+ '@limit': {
332
+ name: '每页数量',
333
+ type: 'number',
334
+ min: 1,
335
+ max: 100
336
+ },
337
+ '@keyword': {
338
+ name: '关键词',
339
+ type: 'string',
340
+ min: 1,
341
+ max: 50
342
+ },
343
+ '@state': {
344
+ name: '状态',
345
+ type: 'number',
346
+ min: 0,
347
+ max: 2
348
+ }
349
+ };
350
+ ```
351
+
352
+ #### 预定义字段说明
353
+
354
+ | 字段 | 类型 | 范围 | 说明 |
355
+ | ---------- | -------- | --------- | ------------------------------------ |
356
+ | `@id` | `number` | >= 1 | 通用 ID 字段,用于详情/删除等 |
357
+ | `@page` | `number` | 1-9999 | 分页页码,默认从 1 开始 |
358
+ | `@limit` | `number` | 1-100 | 每页数量,最大 100 条 |
359
+ | `@keyword` | `string` | 1-50 字符 | 搜索关键词 |
360
+ | `@state` | `number` | 0-2 | 状态字段(0=软删除,1=正常,2=禁用) |
361
+
362
+ #### 使用方式
363
+
364
+ 在 `fields` 中使用 `@` 符号引用预定义字段:
365
+
366
+ ```typescript
367
+ // 方式一:直接字符串引用
368
+ fields: {
369
+ id: '@id',
370
+ page: '@page',
371
+ limit: '@limit',
372
+ keyword: '@keyword',
373
+ state: '@state'
374
+ }
375
+
376
+ // 方式二:与自定义字段混用
377
+ fields: {
378
+ page: '@page',
379
+ limit: '@limit',
380
+ categoryId: { name: '分类ID', type: 'number', min: 0 }
381
+ }
382
+ ```
383
+
384
+ #### 加载机制
385
+
386
+ 1. **按需引用**:只有在 `fields` 中显式声明的预定义字段才会生效
387
+ 2. **自动替换**:在 API 加载时,`@` 引用会被自动替换为完整的字段定义
388
+ 3. **验证生效**:引用的预定义字段会自动应用验证规则
389
+
390
+ #### 使用预定义字段示例
391
+
392
+ **列表查询接口**
393
+
394
+ ```typescript
395
+ // apis/article/list.ts
396
+ export default {
397
+ name: '文章列表',
398
+ auth: true,
399
+ fields: {
400
+ page: '@page',
401
+ limit: '@limit',
402
+ keyword: '@keyword',
403
+ state: '@state',
404
+ categoryId: { name: '分类ID', type: 'number', min: 0 }
405
+ },
406
+ handler: async (befly, ctx) => {
407
+ const { page, limit, keyword, categoryId } = ctx.body;
408
+
409
+ const where: Record<string, any> = { state: 1 };
410
+ if (categoryId) where.categoryId = categoryId;
411
+ if (keyword) where.title = { $like: `%${keyword}%` };
412
+
413
+ const result = await befly.db.getList({
414
+ table: 'article',
415
+ columns: ['id', 'title', 'summary', 'createdAt'],
416
+ where: where,
417
+ page: page || 1,
418
+ limit: limit || 10,
419
+ orderBy: { id: 'desc' }
420
+ });
421
+
422
+ return befly.tool.Yes('获取成功', result);
423
+ }
424
+ } as ApiRoute;
425
+ ```
426
+
427
+ **详情/删除接口**
428
+
429
+ ```typescript
430
+ // apis/article/detail.ts
431
+ export default {
432
+ name: '文章详情',
433
+ auth: false,
434
+ fields: {
435
+ id: '@id'
436
+ },
437
+ required: ['id'],
438
+ handler: async (befly, ctx) => {
439
+ const article = await befly.db.getDetail({
440
+ table: 'article',
441
+ where: { id: ctx.body.id, state: 1 }
442
+ });
443
+
444
+ if (!article?.id) {
445
+ return befly.tool.No('文章不存在');
446
+ }
447
+
448
+ return befly.tool.Yes('获取成功', article);
449
+ }
450
+ } as ApiRoute;
451
+ ```
452
+
453
+ ```typescript
454
+ // apis/article/delete.ts
455
+ export default {
456
+ name: '删除文章',
457
+ auth: true,
458
+ fields: {
459
+ id: '@id'
460
+ },
461
+ required: ['id'],
462
+ handler: async (befly, ctx) => {
463
+ await befly.db.delData({
464
+ table: 'article',
465
+ where: { id: ctx.body.id }
466
+ });
467
+
468
+ return befly.tool.Yes('删除成功');
469
+ }
470
+ } as ApiRoute;
471
+ ```
472
+
473
+ #### 覆盖预定义字段
474
+
475
+ 如需修改预定义字段的验证规则,在 `fields` 中重新定义即可:
476
+
477
+ ```typescript
478
+ export default {
479
+ name: '大数据列表',
480
+ fields: {
481
+ page: '@page',
482
+ // 覆盖默认的 @limit,允许更大的分页
483
+ limit: {
484
+ name: '每页数量',
485
+ type: 'number',
486
+ min: 1,
487
+ max: 500 // 修改最大值为 500
488
+ }
489
+ },
490
+ handler: async (befly, ctx) => {
491
+ // ctx.body.limit 最大可以是 500
492
+ return befly.tool.Yes('获取成功');
493
+ }
494
+ } as ApiRoute;
495
+ ```
496
+
497
+ #### 预定义字段最佳实践
498
+
499
+ **推荐使用场景**
500
+
501
+ | API 类型 | 推荐字段 | 说明 |
502
+ | -------- | ----------------------------------- | ------------------ |
503
+ | 列表查询 | `page`, `limit`, `keyword`, `state` | 完整的查询字段组合 |
504
+ | 获取详情 | `id` | 只需 ID 参数 |
505
+ | 删除操作 | `id` | 只需 ID 参数 |
506
+ | 更新操作 | `id` + 表字段 | ID + 业务字段 |
507
+ | 添加操作 | 表字段(无需预定义字段) | 只需业务字段 |
508
+
509
+ **使用建议**
510
+
511
+ ```typescript
512
+ // ✅ 推荐:列表查询使用完整预定义字段
513
+ fields: {
514
+ page: '@page',
515
+ limit: '@limit',
516
+ keyword: '@keyword',
517
+ state: '@state'
518
+ }
519
+
520
+ // ✅ 推荐:详情/删除只使用 id
521
+ fields: {
522
+ id: '@id'
523
+ }
524
+
525
+ // ✅ 推荐:更新接口混用预定义和表字段
526
+ fields: {
527
+ id: '@id',
528
+ ...articleTable
529
+ }
530
+
531
+ // ❌ 避免:添加接口不需要预定义字段
532
+ fields: {
533
+ page: '@page', // 添加操作不需要分页
534
+ ...articleTable
535
+ }
536
+ ```
537
+
538
+ ### 字段定义格式
539
+
540
+ ```typescript
541
+ fields: {
542
+ // 方式一:引用表字段
543
+ email: adminTable.email,
544
+
545
+ // 方式二:自定义字段
546
+ account: {
547
+ name: '账号',
548
+ type: 'string',
549
+ min: 3,
550
+ max: 100
551
+ },
552
+
553
+ // 方式三:字符串格式
554
+ // "字段标签|类型|最小|最大|默认|必填|正则"
555
+ username: '用户名|string|3|20'
556
+ }
557
+ ```
558
+
559
+ ### 字段类型
560
+
561
+ | 类型 | 说明 | 数据库映射 |
562
+ | -------------- | ---------- | ----------------- |
563
+ | `string` | 字符串 | VARCHAR |
564
+ | `number` | 数字 | BIGINT |
565
+ | `text` | 长文本 | MEDIUMTEXT / TEXT |
566
+ | `array_string` | 字符串数组 | VARCHAR (JSON) |
567
+ | `array_text` | 文本数组 | MEDIUMTEXT (JSON) |
568
+
569
+ ### 验证规则
570
+
571
+ ```typescript
572
+ interface FieldDefinition {
573
+ name: string; // 字段名称(用于错误提示)
574
+ type: string; // 字段类型
575
+ min?: number; // 最小值/最小长度
576
+ max?: number; // 最大值/最大长度
577
+ default?: any; // 默认值
578
+ required?: boolean; // 是否必填
579
+ regex?: string; // 正则表达式
580
+ }
581
+ ```
582
+
583
+ ---
584
+
585
+ ## 实际案例
586
+
587
+ ### 案例一:公开接口(无需认证)
588
+
589
+ ```typescript
590
+ // apis/auth/login.ts
591
+ import adminTable from '../../tables/admin.json';
592
+
593
+ export default {
594
+ name: '管理员登录',
595
+ auth: false, // 公开接口
596
+ fields: {
597
+ account: {
598
+ name: '账号',
599
+ type: 'string',
600
+ min: 3,
601
+ max: 100
602
+ },
603
+ password: adminTable.password
604
+ },
605
+ required: ['account', 'password'],
606
+ handler: async (befly, ctx) => {
607
+ // 查询用户
608
+ const admin = await befly.db.getOne({
609
+ table: 'addon_admin_admin',
610
+ where: {
611
+ $or: [{ username: ctx.body.account }, { email: ctx.body.account }]
612
+ }
613
+ });
614
+
615
+ if (!admin?.id) {
616
+ return befly.tool.No('账号或密码错误');
617
+ }
618
+
619
+ // 验证密码
620
+ const isValid = await befly.cipher.verifyPassword(ctx.body.password, admin.password);
621
+
622
+ if (!isValid) {
623
+ return befly.tool.No('账号或密码错误');
624
+ }
625
+
626
+ // 生成 Token
627
+ const token = await befly.jwt.sign({
628
+ id: admin.id,
629
+ roleCode: admin.roleCode
630
+ });
631
+
632
+ return befly.tool.Yes('登录成功', {
633
+ token: token,
634
+ userInfo: admin
635
+ });
636
+ }
637
+ };
638
+ ```
639
+
640
+ ### 案例二:列表查询(需要认证)
641
+
642
+ ```typescript
643
+ // apis/admin/list.ts
644
+ export default {
645
+ name: '获取管理员列表',
646
+ // auth: true, // 默认需要认证
647
+ handler: async (befly, ctx) => {
648
+ const result = await befly.db.getList({
649
+ table: 'addon_admin_admin',
650
+ page: ctx.body.page || 1,
651
+ limit: ctx.body.limit || 10,
652
+ where: {
653
+ roleCode: { $ne: 'dev' }
654
+ },
655
+ orderBy: ['createdAt#DESC']
656
+ });
657
+
658
+ return befly.tool.Yes('获取成功', result);
659
+ }
660
+ };
661
+ ```
662
+
663
+ ### 案例三:新增数据
664
+
665
+ ```typescript
666
+ // apis/admin/ins.ts
667
+ import adminTable from '../../tables/admin.json';
668
+
669
+ export default {
670
+ name: '添加管理员',
671
+ fields: adminTable,
672
+ required: ['username', 'password', 'roleId'],
673
+ handler: async (befly, ctx) => {
674
+ // 检查用户名是否已存在
675
+ const existing = await befly.db.getOne({
676
+ table: 'addon_admin_admin',
677
+ where: { username: ctx.body.username }
678
+ });
679
+
680
+ if (existing) {
681
+ return befly.tool.No('用户名已被使用');
682
+ }
683
+
684
+ // 查询角色信息
685
+ const role = await befly.db.getOne({
686
+ table: 'addon_admin_role',
687
+ where: { id: ctx.body.roleId },
688
+ columns: ['code']
689
+ });
690
+
691
+ if (!role?.code) {
692
+ return befly.tool.No('角色不存在');
693
+ }
694
+
695
+ // 加密密码
696
+ const hashedPassword = await befly.cipher.hashPassword(ctx.body.password);
697
+
698
+ // 创建管理员
699
+ const adminId = await befly.db.insData({
700
+ table: 'addon_admin_admin',
701
+ data: {
702
+ username: ctx.body.username,
703
+ password: hashedPassword,
704
+ nickname: ctx.body.nickname,
705
+ roleId: ctx.body.roleId,
706
+ roleCode: role.code
707
+ }
708
+ });
709
+
710
+ return befly.tool.Yes('添加成功', {
711
+ id: adminId
712
+ });
713
+ }
714
+ };
715
+ ```
716
+
717
+ ### 案例四:更新数据
718
+
719
+ ```typescript
720
+ // apis/admin/upd.ts
721
+ import adminTable from '../../tables/admin.json';
722
+
723
+ export default {
724
+ name: '更新管理员',
725
+ fields: adminTable,
726
+ required: ['id'],
727
+ handler: async (befly, ctx) => {
728
+ const { id, ...updateData } = ctx.body;
729
+
730
+ // 检查管理员是否存在
731
+ const admin = await befly.db.getOne({
732
+ table: 'addon_admin_admin',
733
+ where: { id: id }
734
+ });
735
+
736
+ if (!admin?.id) {
737
+ return befly.tool.No('管理员不存在');
738
+ }
739
+
740
+ // 更新管理员信息
741
+ await befly.db.updData({
742
+ table: 'addon_admin_admin',
743
+ data: updateData,
744
+ where: { id: id }
745
+ });
746
+
747
+ return befly.tool.Yes('更新成功');
748
+ }
749
+ };
750
+ ```
751
+
752
+ ### 案例五:删除数据
753
+
754
+ ```typescript
755
+ // apis/admin/del.ts
756
+ export default {
757
+ name: '删除管理员',
758
+ fields: {},
759
+ required: ['id'],
760
+ handler: async (befly, ctx) => {
761
+ // 检查管理员是否存在
762
+ const admin = await befly.db.getOne({
763
+ table: 'addon_admin_admin',
764
+ where: { id: ctx.body.id }
765
+ });
766
+
767
+ if (!admin) {
768
+ return befly.tool.No('管理员不存在');
769
+ }
770
+
771
+ // 业务检查:不能删除开发者账号
772
+ if (admin.roleCode === 'dev') {
773
+ return befly.tool.No('不能删除开发者账号');
774
+ }
775
+
776
+ // 删除管理员
777
+ await befly.db.delData({
778
+ table: 'addon_admin_admin',
779
+ where: { id: ctx.body.id }
780
+ });
781
+
782
+ return befly.tool.Yes('删除成功');
783
+ }
784
+ };
785
+ ```
786
+
787
+ ### 案例六:获取详情
788
+
789
+ ```typescript
790
+ // apis/admin/detail.ts
791
+ export default {
792
+ name: '获取用户信息',
793
+ handler: async (befly, ctx) => {
794
+ const userId = ctx.user?.id;
795
+
796
+ if (!userId) {
797
+ return befly.tool.No('未授权');
798
+ }
799
+
800
+ // 查询用户信息
801
+ const admin = await befly.db.getOne({
802
+ table: 'addon_admin_admin',
803
+ where: { id: userId }
804
+ });
805
+
806
+ if (!admin) {
807
+ return befly.tool.No('用户不存在');
808
+ }
809
+
810
+ // 查询角色信息
811
+ let roleInfo = null;
812
+ if (admin.roleCode) {
813
+ roleInfo = await befly.db.getOne({
814
+ table: 'addon_admin_role',
815
+ where: { code: admin.roleCode }
816
+ });
817
+ }
818
+
819
+ // 返回用户信息(不包含密码)
820
+ const { password: _, ...userWithoutPassword } = admin;
821
+
822
+ return befly.tool.Yes('获取成功', {
823
+ ...userWithoutPassword,
824
+ role: roleInfo
825
+ });
826
+ }
827
+ };
828
+ ```
829
+
830
+ ### 案例七:支持 GET 和 POST
831
+
832
+ ```typescript
833
+ // apis/article/search.ts
834
+ export default {
835
+ name: '搜索文章',
836
+ method: 'GET,POST', // 同时支持 GET 和 POST
837
+ auth: false,
838
+ fields: {
839
+ keyword: {
840
+ name: '关键词',
841
+ type: 'string',
842
+ min: 1,
843
+ max: 100
844
+ }
845
+ },
846
+ required: ['keyword'],
847
+ handler: async (befly, ctx) => {
848
+ const result = await befly.db.getList({
849
+ table: 'article',
850
+ where: {
851
+ title: { $like: `%${ctx.body.keyword}%` }
852
+ }
853
+ });
854
+
855
+ return befly.tool.Yes('搜索成功', result);
856
+ }
857
+ };
858
+ ```
859
+
860
+ ### 案例八:保留原始请求体(webhook)
861
+
862
+ ```typescript
863
+ // apis/webhook/wechat.ts
864
+ export default {
865
+ name: '微信回调',
866
+ method: 'POST',
867
+ auth: false,
868
+ rawBody: true, // 不过滤字段,保留完整请求体
869
+ handler: async (befly, ctx) => {
870
+ // ctx.body 包含完整的微信回调数据
871
+ const { ToUserName, FromUserName, MsgType, Content } = ctx.body;
872
+
873
+ // 处理微信消息
874
+ befly.logger.info({ msg: Content }, '收到微信消息');
875
+
876
+ return befly.tool.Yes('处理成功');
877
+ }
878
+ };
879
+ ```
880
+
881
+ ### 案例九:预处理函数
882
+
883
+ ```typescript
884
+ // apis/data/import.ts
885
+ export default {
886
+ name: '导入数据',
887
+ preprocess: async (befly, ctx) => {
888
+ // 在 handler 之前执行
889
+ // 可以解密、转换数据
890
+ if (ctx.body.encryptedData) {
891
+ ctx.body.data = await befly.cipher.decrypt(ctx.body.encryptedData);
892
+ }
893
+ },
894
+ handler: async (befly, ctx) => {
895
+ // 使用预处理后的数据
896
+ const data = ctx.body.data;
897
+
898
+ // 处理导入逻辑...
899
+
900
+ return befly.tool.Yes('导入成功');
901
+ }
902
+ };
903
+ ```
904
+
905
+ ---
906
+
907
+ ## 请求处理流程
908
+
909
+ ### Hook 执行顺序(洋葱模型)
910
+
911
+ ```
912
+ 请求进入
913
+
914
+ ┌─────────────────────────────────────────────┐
915
+ │ 1. cors (order: 2) │
916
+ │ - 设置 CORS 响应头 │
917
+ │ - 处理 OPTIONS 预检请求 │
918
+ ├─────────────────────────────────────────────┤
919
+ │ 2. auth (order: 3) │
920
+ │ - 解析 Authorization Header │
921
+ │ - 验证 JWT Token │
922
+ │ - 设置 ctx.user │
923
+ ├─────────────────────────────────────────────┤
924
+ │ 3. parser (order: 4) │
925
+ │ - 解析 GET 查询参数 │
926
+ │ - 解析 POST JSON/XML 请求体 │
927
+ │ - 根据 fields 过滤字段 │
928
+ │ - 设置 ctx.body │
929
+ ├─────────────────────────────────────────────┤
930
+ │ 4. validator (order: 6) │
931
+ │ - 验证 ctx.body 参数 │
932
+ │ - 检查必填字段 │
933
+ │ - 验证类型、长度、正则 │
934
+ ├─────────────────────────────────────────────┤
935
+ │ 5. permission (order: 6) │
936
+ │ - 检查 auth 配置 │
937
+ │ - 验证用户登录状态 │
938
+ │ - 检查角色权限 │
939
+ ├─────────────────────────────────────────────┤
940
+ │ 6. preprocess (如果定义) │
941
+ │ - 执行 API 预处理函数 │
942
+ ├─────────────────────────────────────────────┤
943
+ │ 7. handler │
944
+ │ - 执行 API 处理函数 │
945
+ │ - 返回结果 │
946
+ ├─────────────────────────────────────────────┤
947
+ │ 8. FinalResponse │
948
+ │ - 格式化响应 │
949
+ │ - 记录请求日志 │
950
+ └─────────────────────────────────────────────┘
951
+
952
+ 响应返回
953
+ ```
954
+
955
+ ### 中断请求
956
+
957
+ 在任何 Hook 或 preprocess 中设置 `ctx.response` 可以中断请求处理:
958
+
959
+ ```typescript
960
+ // 在 Hook 中中断
961
+ if (!ctx.user?.id) {
962
+ ctx.response = ErrorResponse(ctx, '未登录');
963
+ return; // 后续 Hook 和 handler 不会执行
964
+ }
965
+ ```
966
+
967
+ ---
968
+
969
+ ## 路由加载机制
970
+
971
+ ### 加载顺序
972
+
973
+ 1. **项目 API**:`tpl/apis/**/*.ts` → `/api/...`
974
+ 2. **Addon API**:`addonXxx/apis/**/*.ts` → `/api/addon/addonXxx/...`
975
+
976
+ ### 路由映射规则
977
+
978
+ | 文件路径 | 生成路由 |
979
+ | ------------------------------- | --------------------------------------- |
980
+ | `tpl/apis/user/login.ts` | `POST /api/user/login` |
981
+ | `tpl/apis/article/list.ts` | `POST /api/article/list` |
982
+ | `addonAdmin/apis/auth/login.ts` | `POST /api/addon/addonAdmin/auth/login` |
983
+ | `addonAdmin/apis/admin/list.ts` | `POST /api/addon/addonAdmin/admin/list` |
984
+
985
+ ### 多方法注册
986
+
987
+ 当 `method: 'GET,POST'` 时,会同时注册两个路由:
988
+
989
+ - `GET /api/user/search`
990
+ - `POST /api/user/search`
991
+
992
+ ---
993
+
994
+ ## BeflyContext 对象
995
+
996
+ handler 函数的第一个参数 `befly` 提供框架核心功能:
997
+
998
+ ```typescript
999
+ interface BeflyContext {
1000
+ // 数据库操作
1001
+ db: DbHelper;
1002
+
1003
+ // Redis 操作
1004
+ redis: RedisHelper;
1005
+
1006
+ // 缓存操作
1007
+ cache: CacheHelper;
1008
+
1009
+ // JWT 操作
1010
+ jwt: Jwt;
1011
+
1012
+ // 加密操作
1013
+ cipher: Cipher;
1014
+
1015
+ // 日志
1016
+ logger: Logger;
1017
+
1018
+ // 工具函数
1019
+ tool: {
1020
+ Yes: (msg: string, data?: any, other?: object) => object;
1021
+ No: (msg: string, data?: any, other?: object) => object;
1022
+ };
1023
+
1024
+ // 配置
1025
+ config: BeflyConfig;
1026
+ }
1027
+ ```
1028
+
1029
+ ### 常用工具方法
1030
+
1031
+ #### befly.db.cleanFields - 清理数据字段
1032
+
1033
+ 清理对象中的 `null` 和 `undefined` 值,适用于处理可选参数:
1034
+
1035
+ ```typescript
1036
+ // 方法签名
1037
+ befly.db.cleanFields<T>(
1038
+ data: T, // 要清理的数据对象
1039
+ excludeValues?: any[], // 要排除的值,默认 [null, undefined]
1040
+ keepValues?: Record<string, any> // 强制保留的键值对
1041
+ ): Partial<T>
1042
+ ```
1043
+
1044
+ **基本用法:**
1045
+
1046
+ ```typescript
1047
+ // 默认排除 null 和 undefined
1048
+ const cleanData = befly.db.cleanFields({
1049
+ name: 'John',
1050
+ age: null,
1051
+ email: undefined,
1052
+ phone: ''
1053
+ });
1054
+ // 结果: { name: 'John', phone: '' }
1055
+ ```
1056
+
1057
+ **自定义排除值:**
1058
+
1059
+ ```typescript
1060
+ // 同时排除 null、undefined 和空字符串
1061
+ const cleanData = befly.db.cleanFields({ name: 'John', phone: '', age: null }, [null, undefined, '']);
1062
+ // 结果: { name: 'John' }
1063
+ ```
1064
+
1065
+ **保留特定字段的特定值:**
1066
+
1067
+ ```typescript
1068
+ // 即使值在排除列表中,也保留 status 字段的 null 值
1069
+ const cleanData = befly.db.cleanFields({ name: 'John', status: null, count: 0 }, [null, undefined], { status: null });
1070
+ // 结果: { name: 'John', status: null, count: 0 }
1071
+ ```
1072
+
1073
+ > **注意**:`insData`、`updData` 和 `where` 条件会自动调用 `cleanFields`,通常无需手动调用。
1074
+
1075
+ ---
1076
+
1077
+ ## 最佳实践
1078
+
1079
+ ### 1. 字段引用优先级
1080
+
1081
+ ```typescript
1082
+ fields: {
1083
+ // 1. 首选:使用预定义字段(@id, @page, @limit, @keyword, @state)
1084
+ page: '@page',
1085
+ limit: '@limit',
1086
+ keyword: '@keyword',
1087
+
1088
+ // 2. 次选:引用表字段
1089
+ email: adminTable.email,
1090
+ password: adminTable.password,
1091
+
1092
+ // 3. 最后:自定义字段
1093
+ customField: {
1094
+ name: '自定义字段',
1095
+ type: 'string',
1096
+ min: 1,
1097
+ max: 100
1098
+ }
1099
+ }
1100
+ ```
1101
+
1102
+ ### 2. 直接使用 ctx.body
1103
+
1104
+ ```typescript
1105
+ // ✅ 推荐:直接使用
1106
+ const result = await befly.db.insData({
1107
+ table: 'user',
1108
+ data: {
1109
+ username: ctx.body.username,
1110
+ email: ctx.body.email
1111
+ }
1112
+ });
1113
+
1114
+ // ❌ 避免:不必要的解构
1115
+ const { username, email } = ctx.body;
1116
+ ```
1117
+
1118
+ ### 3. 明确字段赋值
1119
+
1120
+ ```typescript
1121
+ // ✅ 推荐:明确每个字段
1122
+ await befly.db.insData({
1123
+ table: 'user',
1124
+ data: {
1125
+ username: ctx.body.username,
1126
+ email: ctx.body.email,
1127
+ password: hashedPassword
1128
+ }
1129
+ });
1130
+
1131
+ // ❌ 避免:扩展运算符
1132
+ await befly.db.insData({
1133
+ table: 'user',
1134
+ data: { ...ctx.body } // 危险!可能写入未预期的字段
1135
+ });
1136
+ ```
1137
+
1138
+ ### 4. 错误处理
1139
+
1140
+ ```typescript
1141
+ handler: async (befly, ctx) => {
1142
+ try {
1143
+ // 业务逻辑
1144
+ const result = await someOperation();
1145
+ return befly.tool.Yes('成功', result);
1146
+ } catch (error: any) {
1147
+ // 记录错误日志
1148
+ befly.logger.error({ err: error }, '操作失败');
1149
+ // 返回友好错误信息
1150
+ return befly.tool.No('操作失败,请稍后重试');
1151
+ }
1152
+ };
1153
+ ```
1154
+
1155
+ ### 5. 时间字段使用 Date.now()
1156
+
1157
+ ```typescript
1158
+ // ✅ 推荐:使用 Date.now()
1159
+ await befly.db.updData({
1160
+ table: 'user',
1161
+ data: {
1162
+ lastLoginTime: Date.now(), // number 类型
1163
+ lastLoginIp: ctx.ip
1164
+ },
1165
+ where: { id: ctx.user.id }
1166
+ });
1167
+
1168
+ // ❌ 避免:使用 new Date()
1169
+ lastLoginTime: new Date(); // 类型不一致
1170
+ ```
1171
+
1172
+ ---
1173
+
1174
+ ## 常见问题
1175
+
1176
+ ### Q1: 如何设置公开接口?
1177
+
1178
+ ```typescript
1179
+ export default {
1180
+ name: '公开接口',
1181
+ auth: false, // 设置为 false
1182
+ handler: async (befly, ctx) => {
1183
+ // ...
1184
+ }
1185
+ };
1186
+ ```
1187
+
1188
+ ### Q2: 如何获取当前用户?
1189
+
1190
+ ```typescript
1191
+ handler: async (befly, ctx) => {
1192
+ const userId = ctx.user?.id;
1193
+ const roleCode = ctx.user?.roleCode;
1194
+
1195
+ if (!userId) {
1196
+ return befly.tool.No('未登录');
1197
+ }
1198
+ };
1199
+ ```
1200
+
1201
+ ### Q3: 如何处理文件上传?
1202
+
1203
+ 文件上传需要使用 `rawBody: true` 保留原始请求体,然后手动解析。
1204
+
1205
+ ### Q4: 如何添加自定义 Hook?
1206
+
1207
+ 在 `tpl/hooks/` 目录创建 Hook 文件:
1208
+
1209
+ ```typescript
1210
+ // tpl/hooks/requestLog.ts
1211
+ import type { Hook } from 'befly-core/types/hook';
1212
+
1213
+ const hook: Hook = {
1214
+ order: 100, // 执行顺序
1215
+ handler: async (befly, ctx) => {
1216
+ befly.logger.info({ route: ctx.route }, '请求开始');
1217
+ }
1218
+ };
1219
+ export default hook;
1220
+ ```
1221
+
1222
+ ### Q5: 为什么参数验证失败?
1223
+
1224
+ 1. 检查字段是否在 `fields` 中定义
1225
+ 2. 检查必填字段是否在 `required` 中
1226
+ 3. 检查参数类型是否匹配
1227
+ 4. 检查参数值是否在 min/max 范围内
1228
+
1229
+ ---
1230
+
1231
+ ## 高级用法
1232
+
1233
+ ### 事务处理
1234
+
1235
+ 在需要保证数据一致性的场景中使用事务:
1236
+
1237
+ ```typescript
1238
+ // apis/order/create.ts
1239
+ export default {
1240
+ name: '创建订单',
1241
+ fields: {
1242
+ productId: {
1243
+ name: '商品ID',
1244
+ type: 'number',
1245
+ min: 1
1246
+ },
1247
+ quantity: {
1248
+ name: '数量',
1249
+ type: 'number',
1250
+ min: 1,
1251
+ max: 999
1252
+ }
1253
+ },
1254
+ required: ['productId', 'quantity'],
1255
+ handler: async (befly, ctx) => {
1256
+ // 使用事务确保库存扣减和订单创建的原子性
1257
+ const result = await befly.db.transaction(async (trx) => {
1258
+ // 1. 查询商品信息(带锁)
1259
+ const product = await trx.getOne({
1260
+ table: 'product',
1261
+ where: { id: ctx.body.productId },
1262
+ forUpdate: true // 行锁
1263
+ });
1264
+
1265
+ if (!product) {
1266
+ throw new Error('商品不存在');
1267
+ }
1268
+
1269
+ if (product.stock < ctx.body.quantity) {
1270
+ throw new Error('库存不足');
1271
+ }
1272
+
1273
+ // 2. 扣减库存
1274
+ await trx.updData({
1275
+ table: 'product',
1276
+ data: {
1277
+ stock: product.stock - ctx.body.quantity
1278
+ },
1279
+ where: { id: ctx.body.productId }
1280
+ });
1281
+
1282
+ // 3. 创建订单
1283
+ const orderId = await trx.insData({
1284
+ table: 'order',
1285
+ data: {
1286
+ userId: ctx.user.id,
1287
+ productId: ctx.body.productId,
1288
+ quantity: ctx.body.quantity,
1289
+ totalPrice: product.price * ctx.body.quantity,
1290
+ status: 'pending'
1291
+ }
1292
+ });
1293
+
1294
+ // 4. 创建订单明细
1295
+ await trx.insData({
1296
+ table: 'order_item',
1297
+ data: {
1298
+ orderId: orderId,
1299
+ productId: ctx.body.productId,
1300
+ productName: product.name,
1301
+ price: product.price,
1302
+ quantity: ctx.body.quantity
1303
+ }
1304
+ });
1305
+
1306
+ return { orderId: orderId };
1307
+ });
1308
+
1309
+ return befly.tool.Yes('订单创建成功', result);
1310
+ }
1311
+ };
1312
+ ```
1313
+
1314
+ ### 批量操作
1315
+
1316
+ #### 批量插入
1317
+
1318
+ ```typescript
1319
+ // apis/user/batchImport.ts
1320
+ export default {
1321
+ name: '批量导入用户',
1322
+ rawBody: true, // 保留原始请求体
1323
+ handler: async (befly, ctx) => {
1324
+ const users = ctx.body.users;
1325
+
1326
+ if (!Array.isArray(users) || users.length === 0) {
1327
+ return befly.tool.No('用户列表不能为空');
1328
+ }
1329
+
1330
+ if (users.length > 100) {
1331
+ return befly.tool.No('单次导入不能超过100条');
1332
+ }
1333
+
1334
+ // 批量插入
1335
+ const result = await befly.db.batchInsert({
1336
+ table: 'user',
1337
+ data: users.map((user: any) => ({
1338
+ username: user.username,
1339
+ email: user.email,
1340
+ nickname: user.nickname || user.username,
1341
+ state: 1
1342
+ }))
1343
+ });
1344
+
1345
+ return befly.tool.Yes('导入成功', {
1346
+ total: users.length,
1347
+ inserted: result.affectedRows
1348
+ });
1349
+ }
1350
+ };
1351
+ ```
1352
+
1353
+ #### 批量更新
1354
+
1355
+ ```typescript
1356
+ // apis/article/batchUpdate.ts
1357
+ export default {
1358
+ name: '批量更新文章状态',
1359
+ rawBody: true,
1360
+ handler: async (befly, ctx) => {
1361
+ const { ids, state } = ctx.body;
1362
+
1363
+ if (!Array.isArray(ids) || ids.length === 0) {
1364
+ return befly.tool.No('文章ID列表不能为空');
1365
+ }
1366
+
1367
+ // 批量更新
1368
+ const result = await befly.db.updData({
1369
+ table: 'article',
1370
+ data: { state: state },
1371
+ where: {
1372
+ id: { $in: ids }
1373
+ }
1374
+ });
1375
+
1376
+ return befly.tool.Yes('更新成功', {
1377
+ updated: result.affectedRows
1378
+ });
1379
+ }
1380
+ };
1381
+ ```
1382
+
1383
+ #### 批量删除
1384
+
1385
+ ```typescript
1386
+ // apis/log/batchDelete.ts
1387
+ export default {
1388
+ name: '批量删除日志',
1389
+ rawBody: true,
1390
+ handler: async (befly, ctx) => {
1391
+ const { ids } = ctx.body;
1392
+
1393
+ if (!Array.isArray(ids) || ids.length === 0) {
1394
+ return befly.tool.No('日志ID列表不能为空');
1395
+ }
1396
+
1397
+ // 批量软删除
1398
+ const result = await befly.db.delData({
1399
+ table: 'operate_log',
1400
+ where: {
1401
+ id: { $in: ids }
1402
+ }
1403
+ });
1404
+
1405
+ return befly.tool.Yes('删除成功', {
1406
+ deleted: result.affectedRows
1407
+ });
1408
+ }
1409
+ };
1410
+ ```
1411
+
1412
+ ### 复杂查询
1413
+
1414
+ #### 多表关联查询
1415
+
1416
+ ```typescript
1417
+ // apis/order/detail.ts
1418
+ export default {
1419
+ name: '订单详情',
1420
+ required: ['id'],
1421
+ handler: async (befly, ctx) => {
1422
+ // 查询订单基本信息
1423
+ const order = await befly.db.getOne({
1424
+ table: 'order',
1425
+ where: { id: ctx.body.id }
1426
+ });
1427
+
1428
+ if (!order) {
1429
+ return befly.tool.No('订单不存在');
1430
+ }
1431
+
1432
+ // 查询订单明细
1433
+ const items = await befly.db.getAll({
1434
+ table: 'order_item',
1435
+ where: { orderId: order.id }
1436
+ });
1437
+
1438
+ // 查询用户信息
1439
+ const user = await befly.db.getOne({
1440
+ table: 'user',
1441
+ where: { id: order.userId },
1442
+ columns: ['id', 'username', 'nickname', 'phone']
1443
+ });
1444
+
1445
+ return befly.tool.Yes('获取成功', {
1446
+ ...order,
1447
+ items: items,
1448
+ user: user
1449
+ });
1450
+ }
1451
+ };
1452
+ ```
1453
+
1454
+ #### 使用 JOIN 查询
1455
+
1456
+ ```typescript
1457
+ // apis/article/listWithAuthor.ts
1458
+ export default {
1459
+ name: '文章列表(含作者)',
1460
+ handler: async (befly, ctx) => {
1461
+ const result = await befly.db.getList({
1462
+ table: 'article',
1463
+ joins: [
1464
+ {
1465
+ type: 'LEFT',
1466
+ table: 'user',
1467
+ alias: 'author',
1468
+ on: { 'article.authorId': 'author.id' }
1469
+ }
1470
+ ],
1471
+ columns: ['article.id', 'article.title', 'article.createdAt', 'author.nickname AS authorName'],
1472
+ page: ctx.body.page || 1,
1473
+ limit: ctx.body.limit || 10,
1474
+ orderBy: ['article.createdAt#DESC']
1475
+ });
1476
+
1477
+ return befly.tool.Yes('获取成功', result);
1478
+ }
1479
+ };
1480
+ ```
1481
+
1482
+ ### 缓存策略
1483
+
1484
+ ```typescript
1485
+ // apis/config/getSiteConfig.ts
1486
+ export default {
1487
+ name: '获取站点配置',
1488
+ auth: false,
1489
+ cache: 300, // 缓存 5 分钟
1490
+ handler: async (befly, ctx) => {
1491
+ // 先从缓存获取
1492
+ const cacheKey = 'site:config';
1493
+ let config = await befly.redis.get(cacheKey);
1494
+
1495
+ if (!config) {
1496
+ // 缓存不存在,从数据库查询
1497
+ config = await befly.db.getAll({
1498
+ table: 'sys_config',
1499
+ where: { state: 1 }
1500
+ });
1501
+
1502
+ // 写入缓存
1503
+ await befly.redis.set(cacheKey, JSON.stringify(config), 'EX', 300);
1504
+ } else {
1505
+ config = JSON.parse(config);
1506
+ }
1507
+
1508
+ return befly.tool.Yes('获取成功', config);
1509
+ }
1510
+ };
1511
+ ```
1512
+
1513
+ ### 分布式锁
1514
+
1515
+ ```typescript
1516
+ // apis/task/execute.ts
1517
+ export default {
1518
+ name: '执行定时任务',
1519
+ handler: async (befly, ctx) => {
1520
+ const lockKey = `lock:task:${ctx.body.taskId}`;
1521
+
1522
+ // 尝试获取锁(30秒超时)
1523
+ const locked = await befly.redis.set(lockKey, ctx.requestId, 'EX', 30, 'NX');
1524
+
1525
+ if (!locked) {
1526
+ return befly.tool.No('任务正在执行中,请稍后');
1527
+ }
1528
+
1529
+ try {
1530
+ // 执行任务逻辑
1531
+ await executeTask(ctx.body.taskId);
1532
+
1533
+ return befly.tool.Yes('任务执行成功');
1534
+ } finally {
1535
+ // 释放锁
1536
+ await befly.redis.del(lockKey);
1537
+ }
1538
+ }
1539
+ };
1540
+ ```
1541
+
1542
+ ### 数据导出
1543
+
1544
+ ```typescript
1545
+ // apis/report/exportUsers.ts
1546
+ export default {
1547
+ name: '导出用户数据',
1548
+ handler: async (befly, ctx) => {
1549
+ // 查询所有用户(不分页)
1550
+ const users = await befly.db.getAll({
1551
+ table: 'user',
1552
+ columns: ['id', 'username', 'nickname', 'email', 'phone', 'createdAt'],
1553
+ where: { state: 1 },
1554
+ orderBy: ['createdAt#DESC']
1555
+ });
1556
+
1557
+ // 转换为 CSV 格式
1558
+ const headers = ['ID', '用户名', '昵称', '邮箱', '手机', '注册时间'];
1559
+ const rows = users.map((u: any) => [u.id, u.username, u.nickname, u.email, u.phone, new Date(u.createdAt).toLocaleString()]);
1560
+
1561
+ const csv = [headers.join(','), ...rows.map((r: any[]) => r.join(','))].join('\n');
1562
+
1563
+ // 返回 CSV 文件
1564
+ return new Response(csv, {
1565
+ headers: {
1566
+ 'Content-Type': 'text/csv; charset=utf-8',
1567
+ 'Content-Disposition': 'attachment; filename="users.csv"'
1568
+ }
1569
+ });
1570
+ }
1571
+ };
1572
+ ```
1573
+
1574
+ ### 文件流处理
1575
+
1576
+ ```typescript
1577
+ // apis/file/download.ts
1578
+ export default {
1579
+ name: '文件下载',
1580
+ required: ['fileId'],
1581
+ handler: async (befly, ctx) => {
1582
+ // 查询文件信息
1583
+ const file = await befly.db.getOne({
1584
+ table: 'file',
1585
+ where: { id: ctx.body.fileId }
1586
+ });
1587
+
1588
+ if (!file) {
1589
+ return befly.tool.No('文件不存在');
1590
+ }
1591
+
1592
+ // 读取文件并返回流
1593
+ const fileStream = Bun.file(file.path);
1594
+
1595
+ return new Response(fileStream, {
1596
+ headers: {
1597
+ 'Content-Type': file.mimeType,
1598
+ 'Content-Disposition': `attachment; filename="${encodeURIComponent(file.name)}"`,
1599
+ 'Content-Length': String(file.size)
1600
+ }
1601
+ });
1602
+ }
1603
+ };
1604
+ ```