befly 3.10.18 → 3.10.19
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 +83 -307
- package/dist/befly.config.d.ts +7 -0
- package/{befly.config.ts → dist/befly.config.js} +10 -33
- package/dist/checks/checkApi.d.ts +1 -0
- package/{checks/checkApi.ts → dist/checks/checkApi.js} +11 -28
- package/dist/checks/checkHook.d.ts +1 -0
- package/{checks/checkHook.ts → dist/checks/checkHook.js} +11 -20
- package/dist/checks/checkMenu.d.ts +7 -0
- package/{checks/checkMenu.ts → dist/checks/checkMenu.js} +18 -53
- package/dist/checks/checkPlugin.d.ts +1 -0
- package/{checks/checkPlugin.ts → dist/checks/checkPlugin.js} +11 -20
- package/dist/checks/checkTable.d.ts +6 -0
- package/{checks/checkTable.ts → dist/checks/checkTable.js} +17 -41
- package/dist/configs/presetFields.d.ts +4 -0
- package/{configs/presetFields.ts → dist/configs/presetFields.js} +1 -1
- package/dist/configs/presetRegexp.d.ts +145 -0
- package/{utils/regex.ts → dist/configs/presetRegexp.js} +8 -31
- package/dist/hooks/auth.d.ts +5 -0
- package/{hooks/auth.ts → dist/hooks/auth.js} +6 -10
- package/dist/hooks/cors.d.ts +9 -0
- package/{hooks/cors.ts → dist/hooks/cors.js} +3 -13
- package/dist/hooks/parser.d.ts +12 -0
- package/{hooks/parser.ts → dist/hooks/parser.js} +29 -44
- package/dist/hooks/permission.d.ts +12 -0
- package/{hooks/permission.ts → dist/hooks/permission.js} +14 -25
- package/dist/hooks/validator.d.ts +9 -0
- package/{hooks/validator.ts → dist/hooks/validator.js} +7 -14
- package/dist/lib/asyncContext.d.ts +21 -0
- package/dist/lib/asyncContext.js +27 -0
- package/dist/lib/cacheHelper.d.ts +95 -0
- package/{lib/cacheHelper.ts → dist/lib/cacheHelper.js} +45 -105
- package/dist/lib/cacheKeys.d.ts +23 -0
- package/{lib/cacheKeys.ts → dist/lib/cacheKeys.js} +5 -10
- package/dist/lib/cipher.d.ts +153 -0
- package/{lib/cipher.ts → dist/lib/cipher.js} +23 -44
- package/dist/lib/connect.d.ts +91 -0
- package/{lib/connect.ts → dist/lib/connect.js} +47 -88
- package/dist/lib/dbDialect.d.ts +87 -0
- package/{lib/dbDialect.ts → dist/lib/dbDialect.js} +32 -112
- package/dist/lib/dbHelper.d.ts +204 -0
- package/{lib/dbHelper.ts → dist/lib/dbHelper.js} +83 -240
- package/dist/lib/dbUtils.d.ts +68 -0
- package/{lib/dbUtils.ts → dist/lib/dbUtils.js} +51 -125
- package/dist/lib/jwt.d.ts +13 -0
- package/{lib/jwt.ts → dist/lib/jwt.js} +11 -32
- package/dist/lib/logger.d.ts +32 -0
- package/{lib/logger.ts → dist/lib/logger.js} +202 -279
- package/dist/lib/redisHelper.d.ts +185 -0
- package/{lib/redisHelper.ts → dist/lib/redisHelper.js} +97 -141
- package/dist/lib/sqlBuilder.d.ts +160 -0
- package/{lib/sqlBuilder.ts → dist/lib/sqlBuilder.js} +132 -278
- package/dist/lib/sqlCheck.d.ts +23 -0
- package/{lib/sqlCheck.ts → dist/lib/sqlCheck.js} +24 -41
- package/dist/lib/validator.d.ts +45 -0
- package/{lib/validator.ts → dist/lib/validator.js} +44 -61
- package/dist/loader/loadApis.d.ts +12 -0
- package/{loader/loadApis.ts → dist/loader/loadApis.js} +9 -19
- package/dist/loader/loadHooks.d.ts +8 -0
- package/{loader/loadHooks.ts → dist/loader/loadHooks.js} +7 -21
- package/dist/loader/loadPlugins.d.ts +8 -0
- package/{loader/loadPlugins.ts → dist/loader/loadPlugins.js} +10 -22
- package/dist/main.d.ts +26 -0
- package/{main.ts → dist/main.js} +60 -99
- package/dist/paths.d.ts +93 -0
- package/{paths.ts → dist/paths.js} +6 -19
- package/dist/plugins/cache.d.ts +14 -0
- package/{plugins/cache.ts → dist/plugins/cache.js} +5 -12
- package/dist/plugins/cipher.d.ts +10 -0
- package/{plugins/cipher.ts → dist/plugins/cipher.js} +2 -6
- package/dist/plugins/config.d.ts +10 -0
- package/dist/plugins/config.js +6 -0
- package/dist/plugins/db.d.ts +14 -0
- package/{plugins/db.ts → dist/plugins/db.js} +9 -17
- package/dist/plugins/jwt.d.ts +10 -0
- package/dist/plugins/jwt.js +10 -0
- package/dist/plugins/logger.d.ts +28 -0
- package/{plugins/logger.ts → dist/plugins/logger.js} +3 -8
- package/dist/plugins/redis.d.ts +14 -0
- package/{plugins/redis.ts → dist/plugins/redis.js} +7 -12
- package/dist/plugins/tool.d.ts +79 -0
- package/{plugins/tool.ts → dist/plugins/tool.js} +7 -30
- package/dist/router/api.d.ts +14 -0
- package/dist/router/api.js +107 -0
- package/dist/router/static.d.ts +9 -0
- package/{router/static.ts → dist/router/static.js} +20 -34
- package/dist/scripts/ensureDist.d.ts +1 -0
- package/dist/scripts/ensureDist.js +80 -0
- package/dist/sync/syncApi.d.ts +3 -0
- package/{sync/syncApi.ts → dist/sync/syncApi.js} +34 -54
- package/dist/sync/syncCache.d.ts +2 -0
- package/{sync/syncCache.ts → dist/sync/syncCache.js} +1 -6
- package/dist/sync/syncDev.d.ts +6 -0
- package/{sync/syncDev.ts → dist/sync/syncDev.js} +29 -62
- package/dist/sync/syncMenu.d.ts +14 -0
- package/{sync/syncMenu.ts → dist/sync/syncMenu.js} +65 -125
- package/dist/sync/syncTable.d.ts +151 -0
- package/{sync/syncTable.ts → dist/sync/syncTable.js} +171 -378
- package/{types → dist/types}/api.d.ts +8 -47
- package/dist/types/api.js +4 -0
- package/{types → dist/types}/befly.d.ts +31 -222
- package/dist/types/befly.js +4 -0
- package/{types → dist/types}/cache.d.ts +7 -15
- package/dist/types/cache.js +4 -0
- package/dist/types/cipher.d.ts +27 -0
- package/dist/types/cipher.js +7 -0
- package/{types → dist/types}/common.d.ts +8 -33
- package/dist/types/common.js +5 -0
- package/{types → dist/types}/context.d.ts +2 -4
- package/dist/types/context.js +4 -0
- package/{types → dist/types}/crypto.d.ts +0 -3
- package/dist/types/crypto.js +4 -0
- package/dist/types/database.d.ts +138 -0
- package/dist/types/database.js +4 -0
- package/dist/types/hook.d.ts +15 -0
- package/dist/types/hook.js +6 -0
- package/dist/types/jwt.d.ts +75 -0
- package/dist/types/jwt.js +4 -0
- package/dist/types/logger.d.ts +47 -0
- package/dist/types/logger.js +6 -0
- package/dist/types/plugin.d.ts +14 -0
- package/dist/types/plugin.js +6 -0
- package/dist/types/redis.d.ts +71 -0
- package/dist/types/redis.js +4 -0
- package/{types/roleApisCache.ts → dist/types/roleApisCache.d.ts} +0 -2
- package/dist/types/roleApisCache.js +8 -0
- package/dist/types/sync.d.ts +92 -0
- package/dist/types/sync.js +4 -0
- package/dist/types/table.d.ts +34 -0
- package/dist/types/table.js +4 -0
- package/dist/types/validate.d.ts +67 -0
- package/dist/types/validate.js +4 -0
- package/dist/utils/arrayKeysToCamel.d.ts +13 -0
- package/{utils/arrayKeysToCamel.ts → dist/utils/arrayKeysToCamel.js} +5 -5
- package/dist/utils/calcPerfTime.d.ts +4 -0
- package/{utils/calcPerfTime.ts → dist/utils/calcPerfTime.js} +3 -3
- package/dist/utils/configTypes.d.ts +1 -0
- package/dist/utils/configTypes.js +1 -0
- package/dist/utils/convertBigIntFields.d.ts +11 -0
- package/{utils/convertBigIntFields.ts → dist/utils/convertBigIntFields.js} +5 -9
- package/dist/utils/cors.d.ts +8 -0
- package/{utils/cors.ts → dist/utils/cors.js} +1 -3
- package/dist/utils/disableMenusGlob.d.ts +13 -0
- package/{utils/disableMenusGlob.ts → dist/utils/disableMenusGlob.js} +9 -29
- package/dist/utils/fieldClear.d.ts +11 -0
- package/{utils/fieldClear.ts → dist/utils/fieldClear.js} +15 -33
- package/dist/utils/genShortId.d.ts +10 -0
- package/{utils/genShortId.ts → dist/utils/genShortId.js} +1 -1
- package/dist/utils/getClientIp.d.ts +6 -0
- package/{utils/getClientIp.ts → dist/utils/getClientIp.js} +1 -7
- package/dist/utils/importDefault.d.ts +1 -0
- package/dist/utils/importDefault.js +29 -0
- package/dist/utils/isDirentDirectory.d.ts +2 -0
- package/{utils/isDirentDirectory.ts → dist/utils/isDirentDirectory.js} +3 -8
- package/dist/utils/keysToCamel.d.ts +10 -0
- package/{utils/keysToCamel.ts → dist/utils/keysToCamel.js} +4 -5
- package/dist/utils/keysToSnake.d.ts +10 -0
- package/{utils/keysToSnake.ts → dist/utils/keysToSnake.js} +4 -5
- package/dist/utils/loadMenuConfigs.d.ts +5 -0
- package/{utils/loadMenuConfigs.ts → dist/utils/loadMenuConfigs.js} +24 -51
- package/dist/utils/pickFields.d.ts +4 -0
- package/{utils/pickFields.ts → dist/utils/pickFields.js} +2 -5
- package/dist/utils/process.d.ts +24 -0
- package/{utils/process.ts → dist/utils/process.js} +2 -18
- package/dist/utils/processFields.d.ts +4 -0
- package/{utils/processFields.ts → dist/utils/processFields.js} +5 -9
- package/dist/utils/regex.d.ts +145 -0
- package/{configs/presetRegexp.ts → dist/utils/regex.js} +8 -31
- package/dist/utils/response.d.ts +20 -0
- package/{utils/response.ts → dist/utils/response.js} +28 -49
- package/dist/utils/scanAddons.d.ts +17 -0
- package/{utils/scanAddons.ts → dist/utils/scanAddons.js} +6 -40
- package/dist/utils/scanConfig.d.ts +26 -0
- package/{utils/scanConfig.ts → dist/utils/scanConfig.js} +22 -59
- package/dist/utils/scanFiles.d.ts +30 -0
- package/{utils/scanFiles.ts → dist/utils/scanFiles.js} +26 -66
- package/dist/utils/scanSources.d.ts +10 -0
- package/dist/utils/scanSources.js +41 -0
- package/dist/utils/sortModules.d.ts +28 -0
- package/{utils/sortModules.ts → dist/utils/sortModules.js} +25 -65
- package/dist/utils/sqlLog.d.ts +14 -0
- package/{utils/sqlLog.ts → dist/utils/sqlLog.js} +2 -14
- package/package.json +14 -28
- package/.gitignore +0 -0
- package/bunfig.toml +0 -3
- package/docs/README.md +0 -98
- package/docs/api/api.md +0 -1921
- package/docs/guide/examples.md +0 -926
- package/docs/guide/quickstart.md +0 -354
- package/docs/hooks/auth.md +0 -38
- package/docs/hooks/cors.md +0 -28
- package/docs/hooks/hook.md +0 -838
- package/docs/hooks/parser.md +0 -19
- package/docs/hooks/rateLimit.md +0 -47
- package/docs/infra/redis.md +0 -628
- package/docs/plugins/cipher.md +0 -61
- package/docs/plugins/database.md +0 -189
- package/docs/plugins/plugin.md +0 -986
- package/docs/reference/addon.md +0 -510
- package/docs/reference/config.md +0 -573
- package/docs/reference/logger.md +0 -495
- package/docs/reference/sync.md +0 -478
- package/docs/reference/table.md +0 -763
- package/docs/reference/validator.md +0 -620
- package/lib/asyncContext.ts +0 -43
- package/plugins/config.ts +0 -13
- package/plugins/jwt.ts +0 -15
- package/router/api.ts +0 -130
- package/tsconfig.json +0 -8
- package/types/database.d.ts +0 -541
- package/types/hook.d.ts +0 -25
- package/types/jwt.d.ts +0 -118
- package/types/logger.d.ts +0 -65
- package/types/plugin.d.ts +0 -19
- package/types/redis.d.ts +0 -83
- package/types/sync.d.ts +0 -398
- package/types/table.d.ts +0 -216
- package/types/validate.d.ts +0 -69
- package/utils/configTypes.ts +0 -3
- package/utils/importDefault.ts +0 -21
- package/utils/scanSources.ts +0 -64
package/docs/api/api.md
DELETED
|
@@ -1,1921 +0,0 @@
|
|
|
1
|
-
# Befly API 接口文档
|
|
2
|
-
|
|
3
|
-
> 本文档详细介绍 Befly 框架的 API 接口开发规范,包括路由定义、参数验证、权限控制、请求处理流程等。
|
|
4
|
-
|
|
5
|
-
## 目录
|
|
6
|
-
|
|
7
|
-
- [Befly API 接口文档](#befly-api-接口文档)
|
|
8
|
-
- [目录](#目录)
|
|
9
|
-
- [概述](#概述)
|
|
10
|
-
- [核心特性](#核心特性)
|
|
11
|
-
- [强约束清单](#强约束清单)
|
|
12
|
-
- [目录结构](#目录结构-1)
|
|
13
|
-
- [项目 API](#项目-api)
|
|
14
|
-
- [Addon API](#addon-api)
|
|
15
|
-
- [文件命名规范](#文件命名规范)
|
|
16
|
-
- [API 定义](#api-定义)
|
|
17
|
-
- [基础结构](#基础结构)
|
|
18
|
-
- [完整类型定义](#完整类型定义)
|
|
19
|
-
- [请求上下文 (RequestContext)](#请求上下文-requestcontext)
|
|
20
|
-
- [结构定义](#结构定义)
|
|
21
|
-
- [常用属性](#常用属性)
|
|
22
|
-
- [响应函数](#响应函数)
|
|
23
|
-
- [Yes - 成功响应](#yes---成功响应)
|
|
24
|
-
- [No - 失败响应](#no---失败响应)
|
|
25
|
-
- [ErrorResponse - Hook 中断响应](#errorresponse---hook-中断响应)
|
|
26
|
-
- [FinalResponse - 最终响应](#finalresponse---最终响应)
|
|
27
|
-
- [字段定义与验证](#字段定义与验证)
|
|
28
|
-
- [预定义字段](#预定义字段)
|
|
29
|
-
- [字段定义格式](#字段定义格式)
|
|
30
|
-
- [字段类型](#字段类型)
|
|
31
|
-
- [验证规则](#验证规则)
|
|
32
|
-
- [实际案例](#实际案例)
|
|
33
|
-
- [案例一:公开接口(无需认证)](#案例一公开接口无需认证)
|
|
34
|
-
- [案例二:列表查询(需要认证)](#案例二列表查询需要认证)
|
|
35
|
-
- [案例三:新增数据](#案例三新增数据)
|
|
36
|
-
- [案例四:更新数据](#案例四更新数据)
|
|
37
|
-
- [案例五:删除数据](#案例五删除数据)
|
|
38
|
-
- [案例六:获取详情](#案例六获取详情)
|
|
39
|
-
- [案例七:支持 GET 和 POST](#案例七支持-get-和-post)
|
|
40
|
-
- [案例八:保留原始请求体(webhook)](#案例八保留原始请求体webhook)
|
|
41
|
-
- [案例九:预处理函数](#案例九预处理函数)
|
|
42
|
-
- [请求处理流程](#请求处理流程)
|
|
43
|
-
- [Hook 执行顺序(洋葱模型)](#hook-执行顺序洋葱模型)
|
|
44
|
-
- [中断请求](#中断请求)
|
|
45
|
-
- [路由加载机制](#路由加载机制)
|
|
46
|
-
- [加载顺序](#加载顺序)
|
|
47
|
-
- [路由映射规则](#路由映射规则)
|
|
48
|
-
- [多方法注册](#多方法注册)
|
|
49
|
-
- [BeflyContext 对象](#beflycontext-对象)
|
|
50
|
-
- [最佳实践](#最佳实践)
|
|
51
|
-
- [1. 字段引用优先级](#1-字段引用优先级)
|
|
52
|
-
- [2. 直接使用 ctx.body](#2-直接使用-ctxbody)
|
|
53
|
-
- [3. 明确字段赋值](#3-明确字段赋值)
|
|
54
|
-
- [4. 错误处理](#4-错误处理)
|
|
55
|
-
- [5. 时间字段使用 Date.now()](#5-时间字段使用-datenow)
|
|
56
|
-
- [常见问题](#常见问题)
|
|
57
|
-
- [高级用法](#高级用法)
|
|
58
|
-
- [事务处理](#事务处理)
|
|
59
|
-
- [批量操作](#批量操作)
|
|
60
|
-
- [复杂查询](#复杂查询)
|
|
61
|
-
- [缓存策略](#缓存策略)
|
|
62
|
-
- [分布式锁](#分布式锁)
|
|
63
|
-
- [数据导出](#数据导出)
|
|
64
|
-
- [文件流处理](#文件流处理)
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## 概述
|
|
69
|
-
|
|
70
|
-
Befly 框架的 API 系统是一套基于约定优于配置的接口开发体系。通过简洁的 JSON 配置定义接口,自动完成路由注册、参数解析、字段验证、权限控制等功能。
|
|
71
|
-
|
|
72
|
-
### 核心特性
|
|
73
|
-
|
|
74
|
-
- **约定式路由**:文件路径自动映射为 API 路径
|
|
75
|
-
- **声明式配置**:通过简洁的配置定义接口行为
|
|
76
|
-
- **自动字段验证**:基于字段定义自动验证请求参数
|
|
77
|
-
- **权限控制**:支持认证和角色权限检查
|
|
78
|
-
- **洋葱模型**:Hook 中间件按顺序处理请求
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 强约束清单
|
|
83
|
-
|
|
84
|
-
- **权限/路由匹配只看 pathname**:系统内部用于路由匹配与权限判断的值均为 `url.pathname`(例如 `/api/user/login`),与 method 无关。
|
|
85
|
-
- **禁止写法**:禁止把权限或路由路径写成 `POST/api/...` 或 `POST /api/...`(这些只是一种“请求行展示写法”,不能进入任何存储/配置/权限集合)。
|
|
86
|
-
- **routePath 必须严格合法**:必须以 `/api/` 开头、不得包含空格、不得出现 `/api//`。
|
|
87
|
-
- **同一路径多方法共用权限**:同一个 pathname(例如 `/api/user/login`)即使同时注册 GET/POST,也共用同一套权限集合。
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## 目录结构
|
|
92
|
-
|
|
93
|
-
### 项目 API
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
tpl/apis/
|
|
97
|
-
├── user/
|
|
98
|
-
│ ├── login.ts → POST /api/user/login
|
|
99
|
-
│ ├── register.ts → POST /api/user/register
|
|
100
|
-
│ └── info.ts → POST /api/user/info
|
|
101
|
-
└── article/
|
|
102
|
-
├── list.ts → POST /api/article/list
|
|
103
|
-
└── detail.ts → POST /api/article/detail
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Addon API
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
addonAdmin/apis/
|
|
110
|
-
├── auth/
|
|
111
|
-
│ ├── login.ts → POST /api/addon/addonAdmin/auth/login
|
|
112
|
-
│ └── logout.ts → POST /api/addon/addonAdmin/auth/logout
|
|
113
|
-
└── admin/
|
|
114
|
-
├── list.ts → POST /api/addon/addonAdmin/admin/list
|
|
115
|
-
└── ins.ts → POST /api/addon/addonAdmin/admin/ins
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### 文件命名规范
|
|
119
|
-
|
|
120
|
-
| 动作 | 后缀 | 说明 | 示例 |
|
|
121
|
-
| ---- | -------- | ------ | --------------- |
|
|
122
|
-
| 添加 | `Ins` | Insert | `userIns.ts` |
|
|
123
|
-
| 更新 | `Upd` | Update | `userUpd.ts` |
|
|
124
|
-
| 删除 | `Del` | Delete | `userDel.ts` |
|
|
125
|
-
| 列表 | `List` | List | `userList.ts` |
|
|
126
|
-
| 全部 | `All` | All | `userAll.ts` |
|
|
127
|
-
| 详情 | `Detail` | Detail | `userDetail.ts` |
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## API 定义
|
|
132
|
-
|
|
133
|
-
### 基础结构
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
import type { ApiRoute } from "befly/types/api";
|
|
137
|
-
|
|
138
|
-
export default {
|
|
139
|
-
// 必填字段
|
|
140
|
-
name: "接口名称", // 接口描述,用于日志和文档
|
|
141
|
-
handler: async (befly, ctx) => {
|
|
142
|
-
// 处理逻辑
|
|
143
|
-
return befly.tool.Yes("成功", { data });
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
// 可选字段
|
|
147
|
-
method: "POST", // HTTP 方法,默认 POST
|
|
148
|
-
auth: true, // 是否需要认证,默认 true
|
|
149
|
-
fields: {}, // 字段定义(验证规则)
|
|
150
|
-
required: [], // 必填字段列表
|
|
151
|
-
rawBody: false // 是否保留原始请求体
|
|
152
|
-
// 缓存/限流:当前实现为 Hook 能力(见 hook 文档/配置),不再挂载在接口定义上
|
|
153
|
-
} as ApiRoute;
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### 完整类型定义
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
interface ApiRoute<T = any, R = any> {
|
|
160
|
-
/** 接口名称(必填) */
|
|
161
|
-
name: string;
|
|
162
|
-
|
|
163
|
-
/** 处理器函数(必填) */
|
|
164
|
-
handler: ApiHandler<T, R>;
|
|
165
|
-
|
|
166
|
-
/** HTTP 方法(可选,默认 POST,支持逗号分隔多个方法) */
|
|
167
|
-
method?: "GET" | "POST" | "GET,POST" | "POST,GET";
|
|
168
|
-
|
|
169
|
-
/** 认证类型(可选,默认 true)
|
|
170
|
-
* - true: 需要登录
|
|
171
|
-
* - false: 公开访问(无需登录)
|
|
172
|
-
*/
|
|
173
|
-
auth?: boolean;
|
|
174
|
-
|
|
175
|
-
/** 字段定义(验证规则)(可选,默认 {}) */
|
|
176
|
-
fields?: TableDefinition;
|
|
177
|
-
|
|
178
|
-
/** 必填字段(可选,默认 []) */
|
|
179
|
-
required?: string[];
|
|
180
|
-
|
|
181
|
-
/** 是否保留原始请求体(可选,默认 false)
|
|
182
|
-
* - true: 不过滤字段,保留完整请求体(适用于微信回调、webhook 等场景)
|
|
183
|
-
* - false: 根据 fields 定义过滤字段
|
|
184
|
-
*/
|
|
185
|
-
rawBody?: boolean;
|
|
186
|
-
|
|
187
|
-
/** 路由路径(运行时生成,无需手动设置) */
|
|
188
|
-
route?: string;
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## 请求上下文 (RequestContext)
|
|
195
|
-
|
|
196
|
-
### 结构定义
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
interface RequestContext {
|
|
200
|
-
/** 请求方法 (GET/POST) */
|
|
201
|
-
method: string;
|
|
202
|
-
|
|
203
|
-
/** 请求体参数(已解析和过滤) */
|
|
204
|
-
body: Record<string, any>;
|
|
205
|
-
|
|
206
|
-
/** 用户信息(从 JWT 解析) */
|
|
207
|
-
user: Record<string, any>;
|
|
208
|
-
|
|
209
|
-
/** 原始请求对象 */
|
|
210
|
-
req: Request;
|
|
211
|
-
|
|
212
|
-
/** 请求开始时间(毫秒) */
|
|
213
|
-
now: number;
|
|
214
|
-
|
|
215
|
-
/** 客户端 IP 地址 */
|
|
216
|
-
ip: string;
|
|
217
|
-
|
|
218
|
-
/** 请求头 */
|
|
219
|
-
headers: Headers;
|
|
220
|
-
|
|
221
|
-
/** API 路由路径(url.pathname,例如 /api/user/login;与 method 无关) */
|
|
222
|
-
route: string;
|
|
223
|
-
|
|
224
|
-
/** 请求唯一 ID */
|
|
225
|
-
requestId: string;
|
|
226
|
-
|
|
227
|
-
/** CORS 响应头 */
|
|
228
|
-
corsHeaders: Record<string, string>;
|
|
229
|
-
|
|
230
|
-
/** 当前请求的 API 路由对象 */
|
|
231
|
-
api?: ApiRoute;
|
|
232
|
-
|
|
233
|
-
/** 响应对象(设置后将直接返回) */
|
|
234
|
-
response?: Response;
|
|
235
|
-
|
|
236
|
-
/** 原始处理结果 */
|
|
237
|
-
result?: any;
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### 常用属性
|
|
242
|
-
|
|
243
|
-
| 属性 | 类型 | 说明 |
|
|
244
|
-
| ----------- | --------- | ---------------------- |
|
|
245
|
-
| `ctx.body` | `object` | 已解析的请求参数 |
|
|
246
|
-
| `ctx.user` | `object` | 当前登录用户信息 |
|
|
247
|
-
| `ctx.ip` | `string` | 客户端 IP |
|
|
248
|
-
| `ctx.now` | `number` | 请求开始时间戳(毫秒) |
|
|
249
|
-
| `ctx.route` | `string` | 完整路由路径 |
|
|
250
|
-
| `ctx.req` | `Request` | 原始 Request 对象 |
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## 响应函数
|
|
255
|
-
|
|
256
|
-
### Yes - 成功响应
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
befly.tool.Yes(msg: string, data?: any, other?: Record<string, any>)
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
返回格式:
|
|
263
|
-
|
|
264
|
-
```json
|
|
265
|
-
{
|
|
266
|
-
"code": 0,
|
|
267
|
-
"msg": "成功消息",
|
|
268
|
-
"data": { ... }
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### No - 失败响应
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
befly.tool.No(msg: string, data?: any, other?: Record<string, any>)
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
返回格式:
|
|
279
|
-
|
|
280
|
-
```json
|
|
281
|
-
{
|
|
282
|
-
"code": 1,
|
|
283
|
-
"msg": "失败消息",
|
|
284
|
-
"data": null
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Raw - 原始响应
|
|
289
|
-
|
|
290
|
-
用于第三方回调等需要自定义响应格式的场景,直接返回 `Response` 对象。自动识别数据类型并设置正确的 Content-Type。
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
befly.tool.Raw(ctx: RequestContext, data: Record<string, any> | string, options?: ResponseOptions)
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**参数说明:**
|
|
297
|
-
|
|
298
|
-
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
299
|
-
| ------- | ----------------------------- | ---- | ------ | ------------------------ |
|
|
300
|
-
| ctx | RequestContext | 是 | - | 请求上下文 |
|
|
301
|
-
| data | Record<string, any> \| string | 是 | - | 响应数据(对象或字符串) |
|
|
302
|
-
| options | ResponseOptions | 否 | {} | 响应选项 |
|
|
303
|
-
|
|
304
|
-
**ResponseOptions 选项:**
|
|
305
|
-
|
|
306
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
307
|
-
| ----------- | ---------------------- | -------- | ---------------------------------------- |
|
|
308
|
-
| status | number | 200 | HTTP 状态码 |
|
|
309
|
-
| contentType | string | 自动判断 | Content-Type,默认根据 data 类型自动判断 |
|
|
310
|
-
| headers | Record<string, string> | {} | 额外的响应头 |
|
|
311
|
-
|
|
312
|
-
**Content-Type 自动判断规则:**
|
|
313
|
-
|
|
314
|
-
| data 类型 | 自动 Content-Type |
|
|
315
|
-
| --------------------- | ----------------- |
|
|
316
|
-
| 对象 | application/json |
|
|
317
|
-
| 字符串(以 `<` 开头) | application/xml |
|
|
318
|
-
| 字符串(其他) | text/plain |
|
|
319
|
-
|
|
320
|
-
**使用示例:**
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// JSON 响应(自动)
|
|
324
|
-
return befly.tool.Raw(ctx, { code: "SUCCESS", message: "成功" });
|
|
325
|
-
|
|
326
|
-
// 纯文本响应(自动)- 支付宝回调
|
|
327
|
-
return befly.tool.Raw(ctx, "success");
|
|
328
|
-
|
|
329
|
-
// XML 响应(自动判断)
|
|
330
|
-
return befly.tool.Raw(ctx, "<xml><return_code>SUCCESS</return_code></xml>");
|
|
331
|
-
|
|
332
|
-
// XML 响应(手动指定)
|
|
333
|
-
return befly.tool.Raw(ctx, xmlString, { contentType: "application/xml" });
|
|
334
|
-
|
|
335
|
-
// 自定义状态码
|
|
336
|
-
return befly.tool.Raw(ctx, { error: "Not Found" }, { status: 404 });
|
|
337
|
-
|
|
338
|
-
// 自定义响应头
|
|
339
|
-
return befly.tool.Raw(
|
|
340
|
-
ctx,
|
|
341
|
-
{ code: "SUCCESS" },
|
|
342
|
-
{
|
|
343
|
-
headers: { "X-Custom-Header": "value" }
|
|
344
|
-
}
|
|
345
|
-
);
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
**完整回调示例:**
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
// 微信支付回调
|
|
352
|
-
export default {
|
|
353
|
-
name: "微信支付回调",
|
|
354
|
-
auth: false,
|
|
355
|
-
rawBody: true,
|
|
356
|
-
handler: async (befly, ctx) => {
|
|
357
|
-
if (!befly.weixin) {
|
|
358
|
-
return befly.tool.Raw(ctx, { code: "SYSTEM_ERROR", message: "weixin 插件未配置" });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// 处理成功
|
|
362
|
-
return befly.tool.Raw(ctx, { code: "SUCCESS", message: "" });
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// 支付宝回调
|
|
367
|
-
export default {
|
|
368
|
-
name: "支付宝回调",
|
|
369
|
-
auth: false,
|
|
370
|
-
rawBody: true,
|
|
371
|
-
handler: async (befly, ctx) => {
|
|
372
|
-
// 支付宝要求返回纯文本 "success"
|
|
373
|
-
return befly.tool.Raw(ctx, "success");
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
// 微信公众号 XML 回调
|
|
378
|
-
export default {
|
|
379
|
-
name: "微信公众号回调",
|
|
380
|
-
auth: false,
|
|
381
|
-
rawBody: true,
|
|
382
|
-
handler: async (befly, ctx) => {
|
|
383
|
-
// 返回 XML 格式响应
|
|
384
|
-
const xml = `<xml>
|
|
385
|
-
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
|
386
|
-
<FromUserName><![CDATA[${toUser}]]></FromUserName>
|
|
387
|
-
<CreateTime>${Date.now()}</CreateTime>
|
|
388
|
-
<MsgType><![CDATA[text]]></MsgType>
|
|
389
|
-
<Content><![CDATA[收到]]></Content>
|
|
390
|
-
</xml>`;
|
|
391
|
-
return befly.tool.Raw(ctx, xml);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
**注意**:`Raw` 返回的是 `Response` 对象,会直接作为 HTTP 响应返回,不经过 `FinalResponse` 处理。
|
|
397
|
-
|
|
398
|
-
### ErrorResponse - Hook 中断响应
|
|
399
|
-
|
|
400
|
-
在 Hook 中使用,用于提前拦截请求:
|
|
401
|
-
|
|
402
|
-
```typescript
|
|
403
|
-
import { ErrorResponse } from "befly/utils/response";
|
|
404
|
-
|
|
405
|
-
// 在 Hook 中使用
|
|
406
|
-
ctx.response = ErrorResponse(ctx, "未授权", 1, null);
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### FinalResponse - 最终响应
|
|
410
|
-
|
|
411
|
-
在 API 路由末尾自动调用,无需手动使用。自动处理 `ctx.result` 并记录请求日志。
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## 字段定义与验证
|
|
416
|
-
|
|
417
|
-
### 预定义字段
|
|
418
|
-
|
|
419
|
-
框架提供了一套预定义字段系统,通过 `@` 符号引用常用字段,避免重复定义。
|
|
420
|
-
|
|
421
|
-
#### 可用预定义字段
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
const PRESET_FIELDS = {
|
|
425
|
-
"@id": {
|
|
426
|
-
name: "ID",
|
|
427
|
-
type: "number",
|
|
428
|
-
min: 1,
|
|
429
|
-
max: null
|
|
430
|
-
},
|
|
431
|
-
"@page": {
|
|
432
|
-
name: "页码",
|
|
433
|
-
type: "number",
|
|
434
|
-
min: 1,
|
|
435
|
-
max: 9999
|
|
436
|
-
},
|
|
437
|
-
"@limit": {
|
|
438
|
-
name: "每页数量",
|
|
439
|
-
type: "number",
|
|
440
|
-
min: 1,
|
|
441
|
-
max: 100
|
|
442
|
-
},
|
|
443
|
-
"@keyword": {
|
|
444
|
-
name: "关键词",
|
|
445
|
-
type: "string",
|
|
446
|
-
min: 1,
|
|
447
|
-
max: 50
|
|
448
|
-
},
|
|
449
|
-
"@state": {
|
|
450
|
-
name: "状态",
|
|
451
|
-
type: "number",
|
|
452
|
-
min: 0,
|
|
453
|
-
max: 2
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
#### 预定义字段说明
|
|
459
|
-
|
|
460
|
-
| 字段 | 类型 | 范围 | 说明 |
|
|
461
|
-
| ---------- | -------- | --------- | ------------------------------------ |
|
|
462
|
-
| `@id` | `number` | >= 1 | 通用 ID 字段,用于详情/删除等 |
|
|
463
|
-
| `@page` | `number` | 1-9999 | 分页页码,默认从 1 开始 |
|
|
464
|
-
| `@limit` | `number` | 1-100 | 每页数量,最大 100 条 |
|
|
465
|
-
| `@keyword` | `string` | 1-50 字符 | 搜索关键词 |
|
|
466
|
-
| `@state` | `number` | 0-2 | 状态字段(0=软删除,1=正常,2=禁用) |
|
|
467
|
-
|
|
468
|
-
#### 使用方式
|
|
469
|
-
|
|
470
|
-
在 `fields` 中使用 `@` 符号引用预定义字段:
|
|
471
|
-
|
|
472
|
-
```typescript
|
|
473
|
-
// 方式一:直接字符串引用
|
|
474
|
-
fields: {
|
|
475
|
-
id: '@id',
|
|
476
|
-
page: '@page',
|
|
477
|
-
limit: '@limit',
|
|
478
|
-
keyword: '@keyword',
|
|
479
|
-
state: '@state'
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// 方式二:与自定义字段混用
|
|
483
|
-
fields: {
|
|
484
|
-
page: '@page',
|
|
485
|
-
limit: '@limit',
|
|
486
|
-
categoryId: { name: '分类ID', type: 'number', min: 0 }
|
|
487
|
-
}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
#### 加载机制
|
|
491
|
-
|
|
492
|
-
1. **按需引用**:只有在 `fields` 中显式声明的预定义字段才会生效
|
|
493
|
-
2. **自动替换**:在 API 加载时,`@` 引用会被自动替换为完整的字段定义
|
|
494
|
-
3. **验证生效**:引用的预定义字段会自动应用验证规则
|
|
495
|
-
|
|
496
|
-
#### 使用预定义字段示例
|
|
497
|
-
|
|
498
|
-
**列表查询接口**
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
// apis/article/list.ts
|
|
502
|
-
export default {
|
|
503
|
-
name: "文章列表",
|
|
504
|
-
auth: true,
|
|
505
|
-
fields: {
|
|
506
|
-
page: "@page",
|
|
507
|
-
limit: "@limit",
|
|
508
|
-
keyword: "@keyword",
|
|
509
|
-
state: "@state",
|
|
510
|
-
categoryId: { name: "分类ID", type: "number", min: 0 }
|
|
511
|
-
},
|
|
512
|
-
handler: async (befly, ctx) => {
|
|
513
|
-
const { page, limit, keyword, categoryId } = ctx.body;
|
|
514
|
-
|
|
515
|
-
const where: Record<string, any> = { state: 1 };
|
|
516
|
-
if (categoryId) where.categoryId = categoryId;
|
|
517
|
-
if (keyword) where.title = { $like: `%${keyword}%` };
|
|
518
|
-
|
|
519
|
-
const result = await befly.db.getList({
|
|
520
|
-
table: "article",
|
|
521
|
-
fields: ["id", "title", "summary", "createdAt"],
|
|
522
|
-
where: where,
|
|
523
|
-
page: page || 1,
|
|
524
|
-
limit: limit || 10,
|
|
525
|
-
orderBy: ["id#DESC"]
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
return befly.tool.Yes("获取成功", result.data);
|
|
529
|
-
}
|
|
530
|
-
} as ApiRoute;
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
**详情/删除接口**
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
// apis/article/detail.ts
|
|
537
|
-
export default {
|
|
538
|
-
name: "文章详情",
|
|
539
|
-
auth: false,
|
|
540
|
-
fields: {
|
|
541
|
-
id: "@id"
|
|
542
|
-
},
|
|
543
|
-
required: ["id"],
|
|
544
|
-
handler: async (befly, ctx) => {
|
|
545
|
-
const articleRes = await befly.db.getOne({
|
|
546
|
-
table: "article",
|
|
547
|
-
where: { id: ctx.body.id, state: 1 }
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
const article = articleRes.data;
|
|
551
|
-
|
|
552
|
-
if (!article?.id) {
|
|
553
|
-
return befly.tool.No("文章不存在");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return befly.tool.Yes("获取成功", article);
|
|
557
|
-
}
|
|
558
|
-
} as ApiRoute;
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
```typescript
|
|
562
|
-
// apis/article/delete.ts
|
|
563
|
-
export default {
|
|
564
|
-
name: "删除文章",
|
|
565
|
-
auth: true,
|
|
566
|
-
fields: {
|
|
567
|
-
id: "@id"
|
|
568
|
-
},
|
|
569
|
-
required: ["id"],
|
|
570
|
-
handler: async (befly, ctx) => {
|
|
571
|
-
await befly.db.delData({
|
|
572
|
-
table: "article",
|
|
573
|
-
where: { id: ctx.body.id }
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
return befly.tool.Yes("删除成功");
|
|
577
|
-
}
|
|
578
|
-
} as ApiRoute;
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
#### 覆盖预定义字段
|
|
582
|
-
|
|
583
|
-
如需修改预定义字段的验证规则,在 `fields` 中重新定义即可:
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
export default {
|
|
587
|
-
name: "大数据列表",
|
|
588
|
-
fields: {
|
|
589
|
-
page: "@page",
|
|
590
|
-
// 覆盖默认的 @limit,允许更大的分页
|
|
591
|
-
limit: {
|
|
592
|
-
name: "每页数量",
|
|
593
|
-
type: "number",
|
|
594
|
-
min: 1,
|
|
595
|
-
max: 500 // 修改最大值为 500
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
handler: async (befly, ctx) => {
|
|
599
|
-
// ctx.body.limit 最大可以是 500
|
|
600
|
-
return befly.tool.Yes("获取成功");
|
|
601
|
-
}
|
|
602
|
-
} as ApiRoute;
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
#### 预定义字段最佳实践
|
|
606
|
-
|
|
607
|
-
**推荐使用场景**
|
|
608
|
-
|
|
609
|
-
| API 类型 | 推荐字段 | 说明 |
|
|
610
|
-
| -------- | ----------------------------------- | ------------------ |
|
|
611
|
-
| 列表查询 | `page`, `limit`, `keyword`, `state` | 完整的查询字段组合 |
|
|
612
|
-
| 获取详情 | `id` | 只需 ID 参数 |
|
|
613
|
-
| 删除操作 | `id` | 只需 ID 参数 |
|
|
614
|
-
| 更新操作 | `id` + 表字段 | ID + 业务字段 |
|
|
615
|
-
| 添加操作 | 表字段(无需预定义字段) | 只需业务字段 |
|
|
616
|
-
|
|
617
|
-
**使用建议**
|
|
618
|
-
|
|
619
|
-
```typescript
|
|
620
|
-
// ✅ 推荐:列表查询使用完整预定义字段
|
|
621
|
-
fields: {
|
|
622
|
-
page: '@page',
|
|
623
|
-
limit: '@limit',
|
|
624
|
-
keyword: '@keyword',
|
|
625
|
-
state: '@state'
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// ✅ 推荐:详情/删除只使用 id
|
|
629
|
-
fields: {
|
|
630
|
-
id: '@id'
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// ✅ 推荐:更新接口混用预定义和表字段
|
|
634
|
-
fields: {
|
|
635
|
-
id: '@id',
|
|
636
|
-
...articleTable
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// ❌ 避免:添加接口不需要预定义字段
|
|
640
|
-
fields: {
|
|
641
|
-
page: '@page', // 添加操作不需要分页
|
|
642
|
-
...articleTable
|
|
643
|
-
}
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
### 字段定义格式
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
fields: {
|
|
650
|
-
// 方式一:引用表字段
|
|
651
|
-
email: adminTable.email,
|
|
652
|
-
|
|
653
|
-
// 方式二:自定义字段
|
|
654
|
-
account: {
|
|
655
|
-
name: '账号',
|
|
656
|
-
type: 'string',
|
|
657
|
-
min: 3,
|
|
658
|
-
max: 100
|
|
659
|
-
},
|
|
660
|
-
|
|
661
|
-
// 方式三:字符串格式
|
|
662
|
-
// "字段标签|类型|最小|最大|默认|必填|正则"
|
|
663
|
-
username: '用户名|string|3|20'
|
|
664
|
-
}
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### 字段类型
|
|
668
|
-
|
|
669
|
-
| 类型 | 说明 | 数据库映射 |
|
|
670
|
-
| -------------- | ---------- | ----------------- |
|
|
671
|
-
| `string` | 字符串 | VARCHAR |
|
|
672
|
-
| `number` | 数字 | BIGINT |
|
|
673
|
-
| `text` | 长文本 | MEDIUMTEXT / TEXT |
|
|
674
|
-
| `array_string` | 字符串数组 | VARCHAR (JSON) |
|
|
675
|
-
| `array_text` | 文本数组 | MEDIUMTEXT (JSON) |
|
|
676
|
-
|
|
677
|
-
### 验证规则
|
|
678
|
-
|
|
679
|
-
```typescript
|
|
680
|
-
interface FieldDefinition {
|
|
681
|
-
name: string; // 字段名称(用于错误提示)
|
|
682
|
-
type: string; // 字段类型
|
|
683
|
-
min?: number; // 最小值/最小长度
|
|
684
|
-
max?: number; // 最大值/最大长度
|
|
685
|
-
default?: any; // 默认值
|
|
686
|
-
required?: boolean; // 是否必填
|
|
687
|
-
regex?: string; // 正则表达式
|
|
688
|
-
}
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
---
|
|
692
|
-
|
|
693
|
-
## rawBody 原始请求体
|
|
694
|
-
|
|
695
|
-
### 概述
|
|
696
|
-
|
|
697
|
-
`rawBody` 参数用于控制 `parser` 钩子的行为。当设置 `rawBody: true` 时,框架会**完全跳过请求体解析**,`ctx.body` 为空对象 `{}`,handler 可以通过 `ctx.req` 获取原始请求自行处理。
|
|
698
|
-
|
|
699
|
-
这对于需要**手动验签、解密**的场景非常重要,如微信支付回调需要原始请求体来验证 RSA 签名。
|
|
700
|
-
|
|
701
|
-
### 工作原理
|
|
702
|
-
|
|
703
|
-
#### 默认行为(rawBody: false)
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
// API 定义
|
|
707
|
-
export default {
|
|
708
|
-
name: '更新用户',
|
|
709
|
-
fields: {
|
|
710
|
-
id: '@id',
|
|
711
|
-
username: { name: '用户名', type: 'string', max: 50 }
|
|
712
|
-
},
|
|
713
|
-
handler: async (befly, ctx) => {
|
|
714
|
-
// ctx.body 只包含 fields 中定义的字段
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
// 请求参数
|
|
719
|
-
{ id: 1, username: 'test', email: 'test@qq.com', role: 'admin' }
|
|
720
|
-
|
|
721
|
-
// ctx.body 实际值(过滤后)
|
|
722
|
-
{ id: 1, username: 'test' }
|
|
723
|
-
// email 和 role 被过滤掉了
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
#### rawBody 模式(rawBody: true)
|
|
727
|
-
|
|
728
|
-
```typescript
|
|
729
|
-
// API 定义
|
|
730
|
-
export default {
|
|
731
|
-
name: "微信支付回调",
|
|
732
|
-
method: "POST",
|
|
733
|
-
auth: false,
|
|
734
|
-
rawBody: true, // 跳过解析,保留原始请求
|
|
735
|
-
handler: async (befly, ctx) => {
|
|
736
|
-
// ctx.body 为空对象 {}
|
|
737
|
-
// 通过 ctx.req 获取原始请求自行处理
|
|
738
|
-
|
|
739
|
-
// 获取原始请求体(用于验签)
|
|
740
|
-
const rawBody = await ctx.req.text();
|
|
741
|
-
|
|
742
|
-
// 解析数据
|
|
743
|
-
const data = JSON.parse(rawBody);
|
|
744
|
-
|
|
745
|
-
// 处理业务逻辑...
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
### 处理流程
|
|
751
|
-
|
|
752
|
-
| rawBody | parser 钩子行为 | ctx.body | 使用场景 |
|
|
753
|
-
| -------------- | ------------------------------- | ------------ | ------------------ |
|
|
754
|
-
| `false` (默认) | 解析 JSON/XML,根据 fields 过滤 | 解析后的对象 | 普通 CRUD |
|
|
755
|
-
| `true` | **完全跳过**,不解析请求体 | `{}` 空对象 | 回调验签、手动解密 |
|
|
756
|
-
|
|
757
|
-
### 适用场景
|
|
758
|
-
|
|
759
|
-
| 场景 | rawBody | 说明 |
|
|
760
|
-
| ------------ | ------- | ------------------------------ |
|
|
761
|
-
| 微信支付回调 | `true` | 需要原始请求体验证 RSA 签名 |
|
|
762
|
-
| 支付宝回调 | `true` | 需要原始请求体验证签名 |
|
|
763
|
-
| Webhook | `true` | 需要原始请求体验证 HMAC 签名 |
|
|
764
|
-
| 加密数据接口 | `true` | 需要手动解密请求体 |
|
|
765
|
-
| 文件上传 | `true` | 需要原始请求体处理二进制数据 |
|
|
766
|
-
| 普通 CRUD | `false` | 标准增删改查(默认) |
|
|
767
|
-
| 批量操作 | `false` | 使用空 fields 即可保留所有字段 |
|
|
768
|
-
|
|
769
|
-
### 示例代码
|
|
770
|
-
|
|
771
|
-
#### 微信支付 V3 回调(需要验签和解密)
|
|
772
|
-
|
|
773
|
-
```typescript
|
|
774
|
-
// apis/webhook/wechatPayV3.ts
|
|
775
|
-
export default {
|
|
776
|
-
name: "微信支付V3回调",
|
|
777
|
-
method: "POST",
|
|
778
|
-
auth: false,
|
|
779
|
-
rawBody: true, // 跳过解析,保留原始请求
|
|
780
|
-
handler: async (befly, ctx) => {
|
|
781
|
-
// 1. 获取原始请求体(用于验签)
|
|
782
|
-
const rawBody = await ctx.req.text();
|
|
783
|
-
|
|
784
|
-
// 2. 获取微信签名头
|
|
785
|
-
const signature = ctx.req.headers.get("Wechatpay-Signature");
|
|
786
|
-
const timestamp = ctx.req.headers.get("Wechatpay-Timestamp");
|
|
787
|
-
const nonce = ctx.req.headers.get("Wechatpay-Nonce");
|
|
788
|
-
const serial = ctx.req.headers.get("Wechatpay-Serial");
|
|
789
|
-
|
|
790
|
-
// 3. 验证签名
|
|
791
|
-
const verifyMessage = `${timestamp}\n${nonce}\n${rawBody}\n`;
|
|
792
|
-
if (!verifyRsaSign(verifyMessage, signature, serial)) {
|
|
793
|
-
return { code: "FAIL", message: "签名验证失败" };
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// 4. 解析并解密数据
|
|
797
|
-
const notification = JSON.parse(rawBody);
|
|
798
|
-
const { ciphertext, nonce: decryptNonce, associated_data } = notification.resource;
|
|
799
|
-
const decrypted = decryptAesGcm(ciphertext, decryptNonce, associated_data);
|
|
800
|
-
const payResult = JSON.parse(decrypted);
|
|
801
|
-
|
|
802
|
-
// 5. 处理支付结果
|
|
803
|
-
if (payResult.trade_state === "SUCCESS") {
|
|
804
|
-
await befly.db.updData({
|
|
805
|
-
table: "order",
|
|
806
|
-
where: { orderNo: payResult.out_trade_no },
|
|
807
|
-
data: {
|
|
808
|
-
payStatus: 1,
|
|
809
|
-
payTime: Date.now(),
|
|
810
|
-
transactionId: payResult.transaction_id
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
return { code: "SUCCESS", message: "" };
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
#### GitHub Webhook(HMAC 签名验证)
|
|
821
|
-
|
|
822
|
-
```typescript
|
|
823
|
-
// apis/webhook/github.ts
|
|
824
|
-
import { createHmac } from "crypto";
|
|
825
|
-
|
|
826
|
-
export default {
|
|
827
|
-
name: "GitHub Webhook",
|
|
828
|
-
method: "POST",
|
|
829
|
-
auth: false,
|
|
830
|
-
rawBody: true,
|
|
831
|
-
handler: async (befly, ctx) => {
|
|
832
|
-
// 获取原始请求体
|
|
833
|
-
const rawBody = await ctx.req.text();
|
|
834
|
-
|
|
835
|
-
// 获取 GitHub 签名
|
|
836
|
-
const signature = ctx.req.headers.get("X-Hub-Signature-256");
|
|
837
|
-
if (!signature) {
|
|
838
|
-
return befly.tool.No("缺少签名");
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// 验证 HMAC 签名
|
|
842
|
-
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
|
843
|
-
const hmac = createHmac("sha256", secret);
|
|
844
|
-
const expectedSignature = "sha256=" + hmac.update(rawBody).digest("hex");
|
|
845
|
-
|
|
846
|
-
if (signature !== expectedSignature) {
|
|
847
|
-
return befly.tool.No("签名验证失败");
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// 解析数据
|
|
851
|
-
const payload = JSON.parse(rawBody);
|
|
852
|
-
const event = ctx.req.headers.get("X-GitHub-Event");
|
|
853
|
-
|
|
854
|
-
// 处理不同事件
|
|
855
|
-
if (event === "push") {
|
|
856
|
-
befly.logger.info({ ref: payload.ref }, "GitHub Push 事件");
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
return befly.tool.Yes("处理成功");
|
|
860
|
-
}
|
|
861
|
-
};
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
#### 加密数据接口
|
|
865
|
-
|
|
866
|
-
```typescript
|
|
867
|
-
// apis/secure/receive.ts
|
|
868
|
-
export default {
|
|
869
|
-
name: "接收加密数据",
|
|
870
|
-
auth: false,
|
|
871
|
-
rawBody: true,
|
|
872
|
-
handler: async (befly, ctx) => {
|
|
873
|
-
// 获取加密的请求体
|
|
874
|
-
const encryptedBody = await ctx.req.text();
|
|
875
|
-
|
|
876
|
-
// 解密数据
|
|
877
|
-
const decrypted = befly.cipher.decrypt(encryptedBody);
|
|
878
|
-
const data = JSON.parse(decrypted);
|
|
879
|
-
|
|
880
|
-
// 处理解密后的数据
|
|
881
|
-
// ...
|
|
882
|
-
|
|
883
|
-
return befly.tool.Yes("处理成功");
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
### 批量操作的替代方案
|
|
889
|
-
|
|
890
|
-
对于批量导入等场景,**不需要使用 rawBody**,使用空 `fields` 即可保留所有字段:
|
|
891
|
-
|
|
892
|
-
```typescript
|
|
893
|
-
// apis/user/batchImport.ts
|
|
894
|
-
export default {
|
|
895
|
-
name: "批量导入用户",
|
|
896
|
-
// 不定义 fields,或 fields: {},会保留所有请求参数
|
|
897
|
-
handler: async (befly, ctx) => {
|
|
898
|
-
const { users } = ctx.body; // 正常解析
|
|
899
|
-
|
|
900
|
-
if (!Array.isArray(users) || users.length === 0) {
|
|
901
|
-
return befly.tool.No("用户列表不能为空");
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// 批量插入...
|
|
905
|
-
return befly.tool.Yes("导入成功");
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
### 注意事项
|
|
911
|
-
|
|
912
|
-
1. **请求体只能读取一次**:`ctx.req.text()` 或 `ctx.req.json()` 只能调用一次,第二次调用会返回空
|
|
913
|
-
2. **ctx.body 为空**:`rawBody: true` 时 `ctx.body = {}`,所有数据需要从 `ctx.req` 获取
|
|
914
|
-
3. **validator 钩子**:如果定义了 `required` 字段,validator 仍会检查 `ctx.body`(此时为空),建议 rawBody 接口不定义 required
|
|
915
|
-
4. **安全性**:手动处理请求时务必做好验签和数据验证
|
|
916
|
-
5. **Content-Type 无限制**:rawBody 模式不检查 Content-Type,支持任意格式
|
|
917
|
-
|
|
918
|
-
---
|
|
919
|
-
|
|
920
|
-
## 实际案例
|
|
921
|
-
|
|
922
|
-
### 案例一:公开接口(无需认证)
|
|
923
|
-
|
|
924
|
-
```typescript
|
|
925
|
-
// apis/auth/login.ts
|
|
926
|
-
import adminTable from "../../tables/admin.json";
|
|
927
|
-
|
|
928
|
-
export default {
|
|
929
|
-
name: "管理员登录",
|
|
930
|
-
auth: false, // 公开接口
|
|
931
|
-
fields: {
|
|
932
|
-
account: {
|
|
933
|
-
name: "账号",
|
|
934
|
-
type: "string",
|
|
935
|
-
min: 3,
|
|
936
|
-
max: 100
|
|
937
|
-
},
|
|
938
|
-
password: adminTable.password
|
|
939
|
-
},
|
|
940
|
-
required: ["account", "password"],
|
|
941
|
-
handler: async (befly, ctx) => {
|
|
942
|
-
// 查询用户
|
|
943
|
-
const admin = await befly.db.getOne({
|
|
944
|
-
table: "addon_admin_admin",
|
|
945
|
-
where: {
|
|
946
|
-
$or: [{ username: ctx.body.account }, { email: ctx.body.account }]
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
if (!admin?.id) {
|
|
951
|
-
return befly.tool.No("账号或密码错误");
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// 验证密码
|
|
955
|
-
const isValid = await befly.cipher.verifyPassword(ctx.body.password, admin.password);
|
|
956
|
-
|
|
957
|
-
if (!isValid) {
|
|
958
|
-
return befly.tool.No("账号或密码错误");
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// 生成 Token
|
|
962
|
-
const token = await befly.jwt.sign({
|
|
963
|
-
id: admin.id,
|
|
964
|
-
roleCode: admin.roleCode
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
return befly.tool.Yes("登录成功", {
|
|
968
|
-
token: token,
|
|
969
|
-
userInfo: admin
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
};
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
### 案例二:列表查询(需要认证)
|
|
976
|
-
|
|
977
|
-
```typescript
|
|
978
|
-
// apis/admin/list.ts
|
|
979
|
-
export default {
|
|
980
|
-
name: "获取管理员列表",
|
|
981
|
-
// auth: true, // 默认需要认证
|
|
982
|
-
handler: async (befly, ctx) => {
|
|
983
|
-
const result = await befly.db.getList({
|
|
984
|
-
table: "addon_admin_admin",
|
|
985
|
-
page: ctx.body.page || 1,
|
|
986
|
-
limit: ctx.body.limit || 10,
|
|
987
|
-
where: {
|
|
988
|
-
roleCode: { $ne: "dev" }
|
|
989
|
-
},
|
|
990
|
-
orderBy: ["createdAt#DESC"]
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
return befly.tool.Yes("获取成功", result);
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
### 案例三:新增数据
|
|
999
|
-
|
|
1000
|
-
```typescript
|
|
1001
|
-
// apis/admin/ins.ts
|
|
1002
|
-
import adminTable from "../../tables/admin.json";
|
|
1003
|
-
|
|
1004
|
-
export default {
|
|
1005
|
-
name: "添加管理员",
|
|
1006
|
-
fields: adminTable,
|
|
1007
|
-
required: ["username", "password", "roleCode"],
|
|
1008
|
-
handler: async (befly, ctx) => {
|
|
1009
|
-
// 检查用户名是否已存在
|
|
1010
|
-
const existing = await befly.db.getOne({
|
|
1011
|
-
table: "addon_admin_admin",
|
|
1012
|
-
where: { username: ctx.body.username }
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
if (existing) {
|
|
1016
|
-
return befly.tool.No("用户名已被使用");
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// 查询角色信息
|
|
1020
|
-
const role = await befly.db.getOne({
|
|
1021
|
-
table: "addon_admin_role",
|
|
1022
|
-
where: { code: ctx.body.roleCode }
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
if (!role) {
|
|
1026
|
-
return befly.tool.No("角色不存在");
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// 加密密码
|
|
1030
|
-
const hashedPassword = await befly.cipher.hashPassword(ctx.body.password);
|
|
1031
|
-
|
|
1032
|
-
// 创建管理员
|
|
1033
|
-
const adminId = await befly.db.insData({
|
|
1034
|
-
table: "addon_admin_admin",
|
|
1035
|
-
data: {
|
|
1036
|
-
username: ctx.body.username,
|
|
1037
|
-
password: hashedPassword,
|
|
1038
|
-
nickname: ctx.body.nickname,
|
|
1039
|
-
roleCode: role.code
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
return befly.tool.Yes("添加成功", {
|
|
1044
|
-
id: adminId
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
};
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
### 案例四:更新数据
|
|
1051
|
-
|
|
1052
|
-
```typescript
|
|
1053
|
-
// apis/admin/upd.ts
|
|
1054
|
-
import adminTable from "../../tables/admin.json";
|
|
1055
|
-
|
|
1056
|
-
export default {
|
|
1057
|
-
name: "更新管理员",
|
|
1058
|
-
fields: adminTable,
|
|
1059
|
-
required: ["id"],
|
|
1060
|
-
handler: async (befly, ctx) => {
|
|
1061
|
-
const { id, ...updateData } = ctx.body;
|
|
1062
|
-
|
|
1063
|
-
// 检查管理员是否存在
|
|
1064
|
-
const admin = await befly.db.getOne({
|
|
1065
|
-
table: "addon_admin_admin",
|
|
1066
|
-
where: { id: id }
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
if (!admin?.id) {
|
|
1070
|
-
return befly.tool.No("管理员不存在");
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// 更新管理员信息
|
|
1074
|
-
await befly.db.updData({
|
|
1075
|
-
table: "addon_admin_admin",
|
|
1076
|
-
data: updateData,
|
|
1077
|
-
where: { id: id }
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
return befly.tool.Yes("更新成功");
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
### 案例五:删除数据
|
|
1086
|
-
|
|
1087
|
-
```typescript
|
|
1088
|
-
// apis/admin/del.ts
|
|
1089
|
-
export default {
|
|
1090
|
-
name: "删除管理员",
|
|
1091
|
-
fields: {},
|
|
1092
|
-
required: ["id"],
|
|
1093
|
-
handler: async (befly, ctx) => {
|
|
1094
|
-
// 检查管理员是否存在
|
|
1095
|
-
const admin = await befly.db.getOne({
|
|
1096
|
-
table: "addon_admin_admin",
|
|
1097
|
-
where: { id: ctx.body.id }
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
if (!admin) {
|
|
1101
|
-
return befly.tool.No("管理员不存在");
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// 业务检查:不能删除开发者账号
|
|
1105
|
-
if (admin.roleCode === "dev") {
|
|
1106
|
-
return befly.tool.No("不能删除开发者账号");
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// 删除管理员
|
|
1110
|
-
await befly.db.delData({
|
|
1111
|
-
table: "addon_admin_admin",
|
|
1112
|
-
where: { id: ctx.body.id }
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
return befly.tool.Yes("删除成功");
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1118
|
-
```
|
|
1119
|
-
|
|
1120
|
-
### 案例六:获取详情
|
|
1121
|
-
|
|
1122
|
-
```typescript
|
|
1123
|
-
// apis/admin/detail.ts
|
|
1124
|
-
export default {
|
|
1125
|
-
name: "获取用户信息",
|
|
1126
|
-
handler: async (befly, ctx) => {
|
|
1127
|
-
const userId = ctx.user?.id;
|
|
1128
|
-
|
|
1129
|
-
if (!userId) {
|
|
1130
|
-
return befly.tool.No("未授权");
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// 查询用户信息
|
|
1134
|
-
const admin = await befly.db.getOne({
|
|
1135
|
-
table: "addon_admin_admin",
|
|
1136
|
-
where: { id: userId }
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
if (!admin) {
|
|
1140
|
-
return befly.tool.No("用户不存在");
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// 查询角色信息
|
|
1144
|
-
let roleInfo = null;
|
|
1145
|
-
if (admin.roleCode) {
|
|
1146
|
-
roleInfo = await befly.db.getOne({
|
|
1147
|
-
table: "addon_admin_role",
|
|
1148
|
-
where: { code: admin.roleCode }
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// 返回用户信息(不包含密码)
|
|
1153
|
-
const { password: _, ...userWithoutPassword } = admin;
|
|
1154
|
-
|
|
1155
|
-
return befly.tool.Yes("获取成功", {
|
|
1156
|
-
...userWithoutPassword,
|
|
1157
|
-
role: roleInfo
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### 案例七:支持 GET 和 POST
|
|
1164
|
-
|
|
1165
|
-
```typescript
|
|
1166
|
-
// apis/article/search.ts
|
|
1167
|
-
export default {
|
|
1168
|
-
name: "搜索文章",
|
|
1169
|
-
method: "GET,POST", // 同时支持 GET 和 POST
|
|
1170
|
-
auth: false,
|
|
1171
|
-
fields: {
|
|
1172
|
-
keyword: {
|
|
1173
|
-
name: "关键词",
|
|
1174
|
-
type: "string",
|
|
1175
|
-
min: 1,
|
|
1176
|
-
max: 100
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
required: ["keyword"],
|
|
1180
|
-
handler: async (befly, ctx) => {
|
|
1181
|
-
const result = await befly.db.getList({
|
|
1182
|
-
table: "article",
|
|
1183
|
-
where: {
|
|
1184
|
-
title: { $like: `%${ctx.body.keyword}%` }
|
|
1185
|
-
}
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
return befly.tool.Yes("搜索成功", result);
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
1191
|
-
```
|
|
1192
|
-
|
|
1193
|
-
### 案例八:保留原始请求体(webhook)
|
|
1194
|
-
|
|
1195
|
-
```typescript
|
|
1196
|
-
// apis/webhook/wechat.ts
|
|
1197
|
-
export default {
|
|
1198
|
-
name: "微信回调",
|
|
1199
|
-
method: "POST",
|
|
1200
|
-
auth: false,
|
|
1201
|
-
rawBody: true, // 不过滤字段,保留完整请求体
|
|
1202
|
-
handler: async (befly, ctx) => {
|
|
1203
|
-
// ctx.body 包含完整的微信回调数据
|
|
1204
|
-
const { ToUserName, FromUserName, MsgType, Content } = ctx.body;
|
|
1205
|
-
|
|
1206
|
-
// 处理微信消息
|
|
1207
|
-
befly.logger.info({ msg: Content }, "收到微信消息");
|
|
1208
|
-
|
|
1209
|
-
return befly.tool.Yes("处理成功");
|
|
1210
|
-
}
|
|
1211
|
-
};
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
---
|
|
1215
|
-
|
|
1216
|
-
## 请求处理流程
|
|
1217
|
-
|
|
1218
|
-
### Hook 执行顺序(洋葱模型)
|
|
1219
|
-
|
|
1220
|
-
```
|
|
1221
|
-
请求进入
|
|
1222
|
-
↓
|
|
1223
|
-
┌─────────────────────────────────────────────┐
|
|
1224
|
-
│ 1. cors (order: 2) │
|
|
1225
|
-
│ - 设置 CORS 响应头 │
|
|
1226
|
-
│ - 处理 OPTIONS 预检请求 │
|
|
1227
|
-
├─────────────────────────────────────────────┤
|
|
1228
|
-
│ 2. auth (order: 3) │
|
|
1229
|
-
│ - 解析 Authorization Header │
|
|
1230
|
-
│ - 验证 JWT Token │
|
|
1231
|
-
│ - 设置 ctx.user │
|
|
1232
|
-
├─────────────────────────────────────────────┤
|
|
1233
|
-
│ 3. parser (order: 4) │
|
|
1234
|
-
│ - 解析 GET 查询参数 │
|
|
1235
|
-
│ - 解析 POST JSON/XML 请求体 │
|
|
1236
|
-
│ - 根据 fields 过滤字段 │
|
|
1237
|
-
│ - 设置 ctx.body │
|
|
1238
|
-
├─────────────────────────────────────────────┤
|
|
1239
|
-
│ 4. validator (order: 6) │
|
|
1240
|
-
│ - 验证 ctx.body 参数 │
|
|
1241
|
-
│ - 检查必填字段 │
|
|
1242
|
-
│ - 验证类型、长度、正则 │
|
|
1243
|
-
├─────────────────────────────────────────────┤
|
|
1244
|
-
│ 5. permission (order: 6) │
|
|
1245
|
-
│ - 检查 auth 配置 │
|
|
1246
|
-
│ - 验证用户登录状态 │
|
|
1247
|
-
│ - 检查角色权限 │
|
|
1248
|
-
├─────────────────────────────────────────────┤
|
|
1249
|
-
│ 6. handler │
|
|
1250
|
-
│ - 执行 API 处理函数 │
|
|
1251
|
-
│ - 返回结果 │
|
|
1252
|
-
├─────────────────────────────────────────────┤
|
|
1253
|
-
│ 7. FinalResponse │
|
|
1254
|
-
│ - 格式化响应 │
|
|
1255
|
-
│ - 记录请求日志 │
|
|
1256
|
-
└─────────────────────────────────────────────┘
|
|
1257
|
-
↓
|
|
1258
|
-
响应返回
|
|
1259
|
-
```
|
|
1260
|
-
|
|
1261
|
-
### 中断请求
|
|
1262
|
-
|
|
1263
|
-
在任何 Hook 中设置 `ctx.response` 可以中断请求处理:
|
|
1264
|
-
|
|
1265
|
-
```typescript
|
|
1266
|
-
// 在 Hook 中中断
|
|
1267
|
-
if (!ctx.user?.id) {
|
|
1268
|
-
ctx.response = ErrorResponse(ctx, "未登录");
|
|
1269
|
-
return; // 后续 Hook 和 handler 不会执行
|
|
1270
|
-
}
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
---
|
|
1274
|
-
|
|
1275
|
-
## 路由加载机制
|
|
1276
|
-
|
|
1277
|
-
### 加载顺序
|
|
1278
|
-
|
|
1279
|
-
1. **项目 API**:`tpl/apis/**/*.ts` → `/api/...`
|
|
1280
|
-
2. **Addon API**:`addonXxx/apis/**/*.ts` → `/api/addon/addonXxx/...`
|
|
1281
|
-
|
|
1282
|
-
### 路由映射规则
|
|
1283
|
-
|
|
1284
|
-
| 文件路径 | 生成路由 |
|
|
1285
|
-
| ------------------------------- | --------------------------------------- |
|
|
1286
|
-
| `tpl/apis/user/login.ts` | `POST /api/user/login` |
|
|
1287
|
-
| `tpl/apis/article/list.ts` | `POST /api/article/list` |
|
|
1288
|
-
| `addonAdmin/apis/auth/login.ts` | `POST /api/addon/addonAdmin/auth/login` |
|
|
1289
|
-
| `addonAdmin/apis/admin/list.ts` | `POST /api/addon/addonAdmin/admin/list` |
|
|
1290
|
-
|
|
1291
|
-
> 注意:上表中的 `POST /api/...` 是“请求行示意”。系统内部生成并存储的 `ctx.route` / 数据库 `routePath` / 角色权限 `role.apis` / Redis 权限缓存都只使用 `url.pathname`(例如 `/api/user/login`),与 method 无关;权限数据禁止写成 `POST /api/...` 或 `POST/api/...`。
|
|
1292
|
-
|
|
1293
|
-
### 多方法注册
|
|
1294
|
-
|
|
1295
|
-
当 `method: 'GET,POST'` 时,会同时注册两个路由:
|
|
1296
|
-
|
|
1297
|
-
- `GET /api/user/search`
|
|
1298
|
-
- `POST /api/user/search`
|
|
1299
|
-
|
|
1300
|
-
> 权限校验只看 pathname:如果同一个 pathname 同时注册了多个 method,它们共享同一套权限(以 `/api/user/search` 作为权限值)。
|
|
1301
|
-
|
|
1302
|
-
---
|
|
1303
|
-
|
|
1304
|
-
## BeflyContext 对象
|
|
1305
|
-
|
|
1306
|
-
handler 函数的第一个参数 `befly` 提供框架核心功能:
|
|
1307
|
-
|
|
1308
|
-
```typescript
|
|
1309
|
-
interface BeflyContext {
|
|
1310
|
-
// 数据库操作
|
|
1311
|
-
db: DbHelper;
|
|
1312
|
-
|
|
1313
|
-
// Redis 操作
|
|
1314
|
-
redis: RedisHelper;
|
|
1315
|
-
|
|
1316
|
-
// 缓存操作
|
|
1317
|
-
cache: CacheHelper;
|
|
1318
|
-
|
|
1319
|
-
// JWT 操作
|
|
1320
|
-
jwt: Jwt;
|
|
1321
|
-
|
|
1322
|
-
// 加密操作
|
|
1323
|
-
cipher: Cipher;
|
|
1324
|
-
|
|
1325
|
-
// 日志
|
|
1326
|
-
logger: Logger;
|
|
1327
|
-
|
|
1328
|
-
// 工具函数
|
|
1329
|
-
tool: {
|
|
1330
|
-
Yes: (msg: string, data?: any, other?: object) => object;
|
|
1331
|
-
No: (msg: string, data?: any, other?: object) => object;
|
|
1332
|
-
};
|
|
1333
|
-
|
|
1334
|
-
// 配置
|
|
1335
|
-
config: BeflyConfig;
|
|
1336
|
-
}
|
|
1337
|
-
```
|
|
1338
|
-
|
|
1339
|
-
### 常用工具方法
|
|
1340
|
-
|
|
1341
|
-
#### fieldClear - 清理数据字段
|
|
1342
|
-
|
|
1343
|
-
清理对象中的指定值(常用:`null/undefined`),适用于处理可选参数。
|
|
1344
|
-
|
|
1345
|
-
```typescript
|
|
1346
|
-
import { fieldClear } from "befly/utils/fieldClear";
|
|
1347
|
-
|
|
1348
|
-
// 常用:排除 null 和 undefined
|
|
1349
|
-
const cleanData = fieldClear(
|
|
1350
|
-
{
|
|
1351
|
-
name: "John",
|
|
1352
|
-
age: null,
|
|
1353
|
-
email: undefined,
|
|
1354
|
-
phone: ""
|
|
1355
|
-
},
|
|
1356
|
-
{ excludeValues: [null, undefined] }
|
|
1357
|
-
);
|
|
1358
|
-
// 结果: { name: 'John', phone: '' }
|
|
1359
|
-
```
|
|
1360
|
-
|
|
1361
|
-
**自定义排除值:**
|
|
1362
|
-
|
|
1363
|
-
```typescript
|
|
1364
|
-
// 同时排除 null、undefined 和空字符串
|
|
1365
|
-
const cleanData = fieldClear({ name: "John", phone: "", age: null }, { excludeValues: [null, undefined, ""] });
|
|
1366
|
-
// 结果: { name: 'John' }
|
|
1367
|
-
```
|
|
1368
|
-
|
|
1369
|
-
**保留特定字段的特定值:**
|
|
1370
|
-
|
|
1371
|
-
```typescript
|
|
1372
|
-
// 保留 count 的 0 值
|
|
1373
|
-
const cleanData = fieldClear(
|
|
1374
|
-
{ name: "John", status: null, count: 0 },
|
|
1375
|
-
{ excludeValues: [null, undefined], keepMap: { count: 0 } }
|
|
1376
|
-
);
|
|
1377
|
-
// 结果: { name: 'John', count: 0 }
|
|
1378
|
-
```
|
|
1379
|
-
|
|
1380
|
-
> **注意**:DbHelper 的写入(`insData/insBatch/updData`)与查询条件(`where`)会自动过滤 `null/undefined`,通常无需手动调用。
|
|
1381
|
-
|
|
1382
|
-
---
|
|
1383
|
-
|
|
1384
|
-
## 最佳实践
|
|
1385
|
-
|
|
1386
|
-
### 1. 字段引用优先级
|
|
1387
|
-
|
|
1388
|
-
```typescript
|
|
1389
|
-
fields: {
|
|
1390
|
-
// 1. 首选:使用预定义字段(@id, @page, @limit, @keyword, @state)
|
|
1391
|
-
page: '@page',
|
|
1392
|
-
limit: '@limit',
|
|
1393
|
-
keyword: '@keyword',
|
|
1394
|
-
|
|
1395
|
-
// 2. 次选:引用表字段
|
|
1396
|
-
email: adminTable.email,
|
|
1397
|
-
password: adminTable.password,
|
|
1398
|
-
|
|
1399
|
-
// 3. 最后:自定义字段
|
|
1400
|
-
customField: {
|
|
1401
|
-
name: '自定义字段',
|
|
1402
|
-
type: 'string',
|
|
1403
|
-
min: 1,
|
|
1404
|
-
max: 100
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
```
|
|
1408
|
-
|
|
1409
|
-
### 2. 直接使用 ctx.body
|
|
1410
|
-
|
|
1411
|
-
```typescript
|
|
1412
|
-
// ✅ 推荐:直接使用
|
|
1413
|
-
const result = await befly.db.insData({
|
|
1414
|
-
table: "user",
|
|
1415
|
-
data: {
|
|
1416
|
-
username: ctx.body.username,
|
|
1417
|
-
email: ctx.body.email
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
// ❌ 避免:不必要的解构
|
|
1422
|
-
const { username, email } = ctx.body;
|
|
1423
|
-
```
|
|
1424
|
-
|
|
1425
|
-
### 3. 明确字段赋值
|
|
1426
|
-
|
|
1427
|
-
```typescript
|
|
1428
|
-
// ✅ 推荐:明确每个字段
|
|
1429
|
-
await befly.db.insData({
|
|
1430
|
-
table: "user",
|
|
1431
|
-
data: {
|
|
1432
|
-
username: ctx.body.username,
|
|
1433
|
-
email: ctx.body.email,
|
|
1434
|
-
password: hashedPassword
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
// ❌ 避免:扩展运算符
|
|
1439
|
-
await befly.db.insData({
|
|
1440
|
-
table: "user",
|
|
1441
|
-
data: { ...ctx.body } // 危险!可能写入未预期的字段
|
|
1442
|
-
});
|
|
1443
|
-
```
|
|
1444
|
-
|
|
1445
|
-
### 4. 错误处理
|
|
1446
|
-
|
|
1447
|
-
```typescript
|
|
1448
|
-
handler: async (befly, ctx) => {
|
|
1449
|
-
try {
|
|
1450
|
-
// 业务逻辑
|
|
1451
|
-
const result = await someOperation();
|
|
1452
|
-
return befly.tool.Yes("成功", result);
|
|
1453
|
-
} catch (error: any) {
|
|
1454
|
-
// 记录错误日志
|
|
1455
|
-
befly.logger.error({ err: error }, "操作失败");
|
|
1456
|
-
// 返回友好错误信息
|
|
1457
|
-
return befly.tool.No("操作失败,请稍后重试");
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1460
|
-
```
|
|
1461
|
-
|
|
1462
|
-
### 5. 时间字段使用 Date.now()
|
|
1463
|
-
|
|
1464
|
-
```typescript
|
|
1465
|
-
// ✅ 推荐:使用 Date.now()
|
|
1466
|
-
await befly.db.updData({
|
|
1467
|
-
table: "user",
|
|
1468
|
-
data: {
|
|
1469
|
-
lastLoginTime: Date.now(), // number 类型
|
|
1470
|
-
lastLoginIp: ctx.ip
|
|
1471
|
-
},
|
|
1472
|
-
where: { id: ctx.user.id }
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
// ❌ 避免:使用 new Date()
|
|
1476
|
-
lastLoginTime: new Date(); // 类型不一致
|
|
1477
|
-
```
|
|
1478
|
-
|
|
1479
|
-
---
|
|
1480
|
-
|
|
1481
|
-
## 常见问题
|
|
1482
|
-
|
|
1483
|
-
### Q1: 如何设置公开接口?
|
|
1484
|
-
|
|
1485
|
-
```typescript
|
|
1486
|
-
export default {
|
|
1487
|
-
name: "公开接口",
|
|
1488
|
-
auth: false, // 设置为 false
|
|
1489
|
-
handler: async (befly, ctx) => {
|
|
1490
|
-
// ...
|
|
1491
|
-
}
|
|
1492
|
-
};
|
|
1493
|
-
```
|
|
1494
|
-
|
|
1495
|
-
### Q2: 如何获取当前用户?
|
|
1496
|
-
|
|
1497
|
-
```typescript
|
|
1498
|
-
handler: async (befly, ctx) => {
|
|
1499
|
-
const userId = ctx.user?.id;
|
|
1500
|
-
const roleCode = ctx.user?.roleCode;
|
|
1501
|
-
|
|
1502
|
-
if (!userId) {
|
|
1503
|
-
return befly.tool.No("未登录");
|
|
1504
|
-
}
|
|
1505
|
-
};
|
|
1506
|
-
```
|
|
1507
|
-
|
|
1508
|
-
### Q3: 如何处理文件上传?
|
|
1509
|
-
|
|
1510
|
-
文件上传需要使用 `rawBody: true` 保留原始请求体,然后手动解析。
|
|
1511
|
-
|
|
1512
|
-
### Q4: 如何添加自定义 Hook?
|
|
1513
|
-
|
|
1514
|
-
在 `tpl/hooks/` 目录创建 Hook 文件:
|
|
1515
|
-
|
|
1516
|
-
```typescript
|
|
1517
|
-
// tpl/hooks/requestLog.ts
|
|
1518
|
-
import type { Hook } from "befly/types/hook";
|
|
1519
|
-
|
|
1520
|
-
const hook: Hook = {
|
|
1521
|
-
order: 100, // 执行顺序
|
|
1522
|
-
handler: async (befly, ctx) => {
|
|
1523
|
-
befly.logger.info({ route: ctx.route }, "请求开始");
|
|
1524
|
-
}
|
|
1525
|
-
};
|
|
1526
|
-
export default hook;
|
|
1527
|
-
```
|
|
1528
|
-
|
|
1529
|
-
### Q5: 为什么参数验证失败?
|
|
1530
|
-
|
|
1531
|
-
1. 检查字段是否在 `fields` 中定义
|
|
1532
|
-
2. 检查必填字段是否在 `required` 中
|
|
1533
|
-
3. 检查参数类型是否匹配
|
|
1534
|
-
4. 检查参数值是否在 min/max 范围内
|
|
1535
|
-
|
|
1536
|
-
---
|
|
1537
|
-
|
|
1538
|
-
## 高级用法
|
|
1539
|
-
|
|
1540
|
-
### 事务处理
|
|
1541
|
-
|
|
1542
|
-
在需要保证数据一致性的场景中使用事务:
|
|
1543
|
-
|
|
1544
|
-
```typescript
|
|
1545
|
-
// apis/order/create.ts
|
|
1546
|
-
export default {
|
|
1547
|
-
name: "创建订单",
|
|
1548
|
-
fields: {
|
|
1549
|
-
productId: {
|
|
1550
|
-
name: "商品ID",
|
|
1551
|
-
type: "number",
|
|
1552
|
-
min: 1
|
|
1553
|
-
},
|
|
1554
|
-
quantity: {
|
|
1555
|
-
name: "数量",
|
|
1556
|
-
type: "number",
|
|
1557
|
-
min: 1,
|
|
1558
|
-
max: 999
|
|
1559
|
-
}
|
|
1560
|
-
},
|
|
1561
|
-
required: ["productId", "quantity"],
|
|
1562
|
-
handler: async (befly, ctx) => {
|
|
1563
|
-
// 使用事务确保库存扣减和订单创建的原子性
|
|
1564
|
-
const result = await befly.db.trans(async (trx) => {
|
|
1565
|
-
// 1. 查询商品信息(带锁)
|
|
1566
|
-
const productRes = await trx.getOne({
|
|
1567
|
-
table: "product",
|
|
1568
|
-
where: { id: ctx.body.productId }
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
const product = productRes.data;
|
|
1572
|
-
|
|
1573
|
-
if (!product?.id) {
|
|
1574
|
-
throw new Error("商品不存在");
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
if (product.stock < ctx.body.quantity) {
|
|
1578
|
-
throw new Error("库存不足");
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// 2. 扣减库存
|
|
1582
|
-
await trx.updData({
|
|
1583
|
-
table: "product",
|
|
1584
|
-
data: {
|
|
1585
|
-
stock: product.stock - ctx.body.quantity
|
|
1586
|
-
},
|
|
1587
|
-
where: { id: ctx.body.productId }
|
|
1588
|
-
});
|
|
1589
|
-
|
|
1590
|
-
// 3. 创建订单
|
|
1591
|
-
const orderIdRes = await trx.insData({
|
|
1592
|
-
table: "order",
|
|
1593
|
-
data: {
|
|
1594
|
-
userId: ctx.user.id,
|
|
1595
|
-
productId: ctx.body.productId,
|
|
1596
|
-
quantity: ctx.body.quantity,
|
|
1597
|
-
totalPrice: product.price * ctx.body.quantity,
|
|
1598
|
-
status: "pending"
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
const orderId = orderIdRes.data;
|
|
1603
|
-
|
|
1604
|
-
// 4. 创建订单明细
|
|
1605
|
-
await trx.insData({
|
|
1606
|
-
table: "order_item",
|
|
1607
|
-
data: {
|
|
1608
|
-
orderId: orderId,
|
|
1609
|
-
productId: ctx.body.productId,
|
|
1610
|
-
productName: product.name,
|
|
1611
|
-
price: product.price,
|
|
1612
|
-
quantity: ctx.body.quantity
|
|
1613
|
-
}
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
return { orderId: orderId };
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
return befly.tool.Yes("订单创建成功", result);
|
|
1620
|
-
}
|
|
1621
|
-
};
|
|
1622
|
-
```
|
|
1623
|
-
|
|
1624
|
-
### 批量操作
|
|
1625
|
-
|
|
1626
|
-
#### 批量插入
|
|
1627
|
-
|
|
1628
|
-
```typescript
|
|
1629
|
-
// apis/user/batchImport.ts
|
|
1630
|
-
export default {
|
|
1631
|
-
name: "批量导入用户",
|
|
1632
|
-
rawBody: true, // 保留原始请求体
|
|
1633
|
-
handler: async (befly, ctx) => {
|
|
1634
|
-
const users = ctx.body.users;
|
|
1635
|
-
|
|
1636
|
-
if (!Array.isArray(users) || users.length === 0) {
|
|
1637
|
-
return befly.tool.No("用户列表不能为空");
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
if (users.length > 100) {
|
|
1641
|
-
return befly.tool.No("单次导入不能超过100条");
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// 批量插入
|
|
1645
|
-
const idsRes = await befly.db.insBatch(
|
|
1646
|
-
"user",
|
|
1647
|
-
users.map((user: any) => {
|
|
1648
|
-
return {
|
|
1649
|
-
username: user.username,
|
|
1650
|
-
email: user.email,
|
|
1651
|
-
nickname: user.nickname || user.username,
|
|
1652
|
-
state: 1
|
|
1653
|
-
};
|
|
1654
|
-
})
|
|
1655
|
-
);
|
|
1656
|
-
|
|
1657
|
-
return befly.tool.Yes("导入成功", {
|
|
1658
|
-
total: users.length,
|
|
1659
|
-
ids: idsRes.data
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
```
|
|
1664
|
-
|
|
1665
|
-
#### 批量更新
|
|
1666
|
-
|
|
1667
|
-
```typescript
|
|
1668
|
-
// apis/article/batchUpdate.ts
|
|
1669
|
-
export default {
|
|
1670
|
-
name: "批量更新文章状态",
|
|
1671
|
-
rawBody: true,
|
|
1672
|
-
handler: async (befly, ctx) => {
|
|
1673
|
-
const { ids, state } = ctx.body;
|
|
1674
|
-
|
|
1675
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1676
|
-
return befly.tool.No("文章ID列表不能为空");
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// 批量更新
|
|
1680
|
-
const result = await befly.db.updData({
|
|
1681
|
-
table: "article",
|
|
1682
|
-
data: { state: state },
|
|
1683
|
-
where: {
|
|
1684
|
-
id: { $in: ids }
|
|
1685
|
-
}
|
|
1686
|
-
});
|
|
1687
|
-
|
|
1688
|
-
return befly.tool.Yes("更新成功", {
|
|
1689
|
-
updated: result.data
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
```
|
|
1694
|
-
|
|
1695
|
-
#### 批量删除
|
|
1696
|
-
|
|
1697
|
-
```typescript
|
|
1698
|
-
// apis/log/batchDelete.ts
|
|
1699
|
-
export default {
|
|
1700
|
-
name: "批量删除日志",
|
|
1701
|
-
rawBody: true,
|
|
1702
|
-
handler: async (befly, ctx) => {
|
|
1703
|
-
const { ids } = ctx.body;
|
|
1704
|
-
|
|
1705
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1706
|
-
return befly.tool.No("日志ID列表不能为空");
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// 批量软删除
|
|
1710
|
-
const result = await befly.db.delData({
|
|
1711
|
-
table: "operate_log",
|
|
1712
|
-
where: {
|
|
1713
|
-
id: { $in: ids }
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1716
|
-
|
|
1717
|
-
return befly.tool.Yes("删除成功", {
|
|
1718
|
-
deleted: result.data
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
};
|
|
1722
|
-
```
|
|
1723
|
-
|
|
1724
|
-
### 复杂查询
|
|
1725
|
-
|
|
1726
|
-
#### 多表关联查询
|
|
1727
|
-
|
|
1728
|
-
```typescript
|
|
1729
|
-
// apis/order/detail.ts
|
|
1730
|
-
export default {
|
|
1731
|
-
name: '订单详情',
|
|
1732
|
-
required: ['id'],
|
|
1733
|
-
handler: async (befly, ctx) => {
|
|
1734
|
-
// 查询订单基本信息
|
|
1735
|
-
const orderRes = await befly.db.getOne({
|
|
1736
|
-
table: 'order',
|
|
1737
|
-
where: { id: ctx.body.id }
|
|
1738
|
-
});
|
|
1739
|
-
|
|
1740
|
-
const order = orderRes.data;
|
|
1741
|
-
|
|
1742
|
-
if (!order?.id) {
|
|
1743
|
-
return befly.tool.No('订单不存在');
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
// 查询订单明细
|
|
1747
|
-
const itemsResult = await befly.db.getAll({
|
|
1748
|
-
table: 'order_item',
|
|
1749
|
-
where: { orderId: order.id }
|
|
1750
|
-
});
|
|
1751
|
-
|
|
1752
|
-
// 查询用户信息
|
|
1753
|
-
const userRes = await befly.db.getOne({
|
|
1754
|
-
table: 'user',
|
|
1755
|
-
fields: ['id', 'username', 'nickname', 'phone'],
|
|
1756
|
-
where: { id: order.userId }
|
|
1757
|
-
});
|
|
1758
|
-
|
|
1759
|
-
const user = userRes.data;
|
|
1760
|
-
|
|
1761
|
-
return befly.tool.Yes('查询成功', {
|
|
1762
|
-
order: order,
|
|
1763
|
-
items: itemsResult.data.lists, // 订单明细列表
|
|
1764
|
-
user: user
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
```
|
|
1769
|
-
|
|
1770
|
-
#### 使用 JOIN 查询
|
|
1771
|
-
|
|
1772
|
-
```typescript
|
|
1773
|
-
// apis/article/listWithAuthor.ts
|
|
1774
|
-
export default {
|
|
1775
|
-
name: "文章列表(含作者)",
|
|
1776
|
-
handler: async (befly, ctx) => {
|
|
1777
|
-
const result = await befly.db.getList({
|
|
1778
|
-
table: "article",
|
|
1779
|
-
joins: [
|
|
1780
|
-
{
|
|
1781
|
-
type: "LEFT",
|
|
1782
|
-
table: "user",
|
|
1783
|
-
alias: "author",
|
|
1784
|
-
on: { "article.authorId": "author.id" }
|
|
1785
|
-
}
|
|
1786
|
-
],
|
|
1787
|
-
fields: ["article.id", "article.title", "article.createdAt", "author.nickname AS authorName"],
|
|
1788
|
-
page: ctx.body.page || 1,
|
|
1789
|
-
limit: ctx.body.limit || 10,
|
|
1790
|
-
orderBy: ["article.createdAt#DESC"]
|
|
1791
|
-
});
|
|
1792
|
-
|
|
1793
|
-
return befly.tool.Yes("获取成功", result.data);
|
|
1794
|
-
}
|
|
1795
|
-
};
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
|
-
### 缓存策略
|
|
1799
|
-
|
|
1800
|
-
```typescript
|
|
1801
|
-
// apis/config/getSiteConfig.ts
|
|
1802
|
-
export default {
|
|
1803
|
-
name: "获取站点配置",
|
|
1804
|
-
auth: false,
|
|
1805
|
-
handler: async (befly, ctx) => {
|
|
1806
|
-
// 先从缓存获取
|
|
1807
|
-
const cacheKey = "site:config";
|
|
1808
|
-
let config = await befly.redis.get(cacheKey);
|
|
1809
|
-
|
|
1810
|
-
if (!config) {
|
|
1811
|
-
// 缓存不存在,从数据库查询
|
|
1812
|
-
const result = await befly.db.getAll({
|
|
1813
|
-
table: "sys_config",
|
|
1814
|
-
where: { state: 1 }
|
|
1815
|
-
});
|
|
1816
|
-
|
|
1817
|
-
config = result.lists; // 获取配置列表
|
|
1818
|
-
|
|
1819
|
-
// 写入缓存
|
|
1820
|
-
await befly.redis.set(cacheKey, JSON.stringify(config), "EX", 300);
|
|
1821
|
-
} else {
|
|
1822
|
-
config = JSON.parse(config);
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
return befly.tool.Yes("获取成功", config);
|
|
1826
|
-
}
|
|
1827
|
-
};
|
|
1828
|
-
```
|
|
1829
|
-
|
|
1830
|
-
### 分布式锁
|
|
1831
|
-
|
|
1832
|
-
```typescript
|
|
1833
|
-
// apis/task/execute.ts
|
|
1834
|
-
export default {
|
|
1835
|
-
name: "执行定时任务",
|
|
1836
|
-
handler: async (befly, ctx) => {
|
|
1837
|
-
const lockKey = `lock:task:${ctx.body.taskId}`;
|
|
1838
|
-
|
|
1839
|
-
// 尝试获取锁(30秒超时)
|
|
1840
|
-
const locked = await befly.redis.set(lockKey, ctx.requestId, "EX", 30, "NX");
|
|
1841
|
-
|
|
1842
|
-
if (!locked) {
|
|
1843
|
-
return befly.tool.No("任务正在执行中,请稍后");
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
try {
|
|
1847
|
-
// 执行任务逻辑
|
|
1848
|
-
await executeTask(ctx.body.taskId);
|
|
1849
|
-
|
|
1850
|
-
return befly.tool.Yes("任务执行成功");
|
|
1851
|
-
} finally {
|
|
1852
|
-
// 释放锁
|
|
1853
|
-
await befly.redis.del(lockKey);
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
```
|
|
1858
|
-
|
|
1859
|
-
### 数据导出
|
|
1860
|
-
|
|
1861
|
-
```typescript
|
|
1862
|
-
// apis/report/exportUsers.ts
|
|
1863
|
-
export default {
|
|
1864
|
-
name: "导出用户数据",
|
|
1865
|
-
handler: async (befly, ctx) => {
|
|
1866
|
-
// 查询所有用户(不分页,注意上限 10000 条)
|
|
1867
|
-
const result = await befly.db.getAll({
|
|
1868
|
-
table: "user",
|
|
1869
|
-
fields: ["id", "username", "nickname", "email", "phone", "createdAt"],
|
|
1870
|
-
where: { state: 1 },
|
|
1871
|
-
orderBy: ["createdAt#DESC"]
|
|
1872
|
-
});
|
|
1873
|
-
|
|
1874
|
-
// 转换为 CSV 格式
|
|
1875
|
-
const headers = ["ID", "用户名", "昵称", "邮箱", "手机", "注册时间"];
|
|
1876
|
-
const rows = result.data.lists.map((u: any) => [u.id, u.username, u.nickname, u.email, u.phone, new Date(u.createdAt).toLocaleString()]);
|
|
1877
|
-
|
|
1878
|
-
const csv = [headers.join(","), ...rows.map((r: any[]) => r.join(","))].join("\n");
|
|
1879
|
-
|
|
1880
|
-
// 返回 CSV 文件(注意:如果 total > 10000,只会导出前 10000 条)
|
|
1881
|
-
return new Response(csv, {
|
|
1882
|
-
headers: {
|
|
1883
|
-
"Content-Type": "text/csv; charset=utf-8",
|
|
1884
|
-
"Content-Disposition": 'attachment; filename="users.csv"'
|
|
1885
|
-
}
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
};
|
|
1889
|
-
```
|
|
1890
|
-
|
|
1891
|
-
### 文件流处理
|
|
1892
|
-
|
|
1893
|
-
```typescript
|
|
1894
|
-
// apis/file/download.ts
|
|
1895
|
-
export default {
|
|
1896
|
-
name: "文件下载",
|
|
1897
|
-
required: ["fileId"],
|
|
1898
|
-
handler: async (befly, ctx) => {
|
|
1899
|
-
// 查询文件信息
|
|
1900
|
-
const file = await befly.db.getOne({
|
|
1901
|
-
table: "file",
|
|
1902
|
-
where: { id: ctx.body.fileId }
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
if (!file) {
|
|
1906
|
-
return befly.tool.No("文件不存在");
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
// 读取文件并返回流
|
|
1910
|
-
const fileStream = Bun.file(file.path);
|
|
1911
|
-
|
|
1912
|
-
return new Response(fileStream, {
|
|
1913
|
-
headers: {
|
|
1914
|
-
"Content-Type": file.mimeType,
|
|
1915
|
-
"Content-Disposition": `attachment; filename="${encodeURIComponent(file.name)}"`,
|
|
1916
|
-
"Content-Length": String(file.size)
|
|
1917
|
-
}
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
};
|
|
1921
|
-
```
|