@lovrabet/cli 1.2.4 → 1.2.5-beta.2
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/lib/add-page/input-page-router.js +1 -1
- package/lib/add-page/main.js +1 -1
- package/lib/add-page/select-page-template.js +1 -1
- package/lib/api/api-doc-ui.js +1 -1
- package/lib/api/api-doc.js +1 -1
- package/lib/api/api-pull-ui.js +1 -1
- package/lib/api/fetch-model-list.js +1 -1
- package/lib/api/generate-api-file.js +1 -1
- package/lib/api/main.js +1 -1
- package/lib/api/pull-silent.js +1 -1
- package/lib/app-menu/app-menu-sync-ui.js +1 -1
- package/lib/app-menu/create-menu.js +1 -1
- package/lib/app-menu/get-local-pages.js +1 -1
- package/lib/app-menu/get-online-menu-list.js +1 -1
- package/lib/app-menu/use-get-online-menu-list.js +1 -1
- package/lib/app-menu/utils.js +1 -1
- package/lib/app-menu/valid-url.js +1 -1
- package/lib/app-menu-update-cdn/current-content.js +1 -1
- package/lib/app-menu-update-cdn/input-cdn-asset.js +1 -1
- package/lib/app-menu-update-cdn/main.js +1 -1
- package/lib/app-menu-update-cdn/update-menu-cdn-url.js +1 -1
- package/lib/auth/auth-server-ui.js +1 -1
- package/lib/auth/auth-server.js +1 -1
- package/lib/auth/constant.js +1 -1
- package/lib/auth/get-cookie.js +1 -1
- package/lib/auth/is-session-valid.js +1 -1
- package/lib/auth/logout.js +1 -1
- package/lib/cli.js +1 -1
- package/lib/cmd/build-watch.js +1 -1
- package/lib/cmd/build.js +1 -1
- package/lib/cmd/logs.js +1 -1
- package/lib/cmd/preview.js +1 -1
- package/lib/cmd/start.js +1 -1
- package/lib/config/config-help.js +1 -1
- package/lib/config/main.js +1 -1
- package/lib/constant/domain.js +1 -1
- package/lib/constant/env.js +1 -1
- package/lib/create-app/enhanced-guided-create.js +1 -1
- package/lib/create-app/format-elapsed.js +1 -1
- package/lib/create-app/main.js +1 -1
- package/lib/create-app/task-finished.js +1 -1
- package/lib/create-app/task-loading.js +1 -1
- package/lib/create-app/task-running.js +1 -1
- package/lib/create-app/task-time.js +1 -1
- package/lib/create-app/use-copy-project-template.js +1 -1
- package/lib/create-app/use-format-code.js +1 -1
- package/lib/create-app/use-install-dependencies.js +1 -1
- package/lib/help.js +1 -1
- package/lib/init/main.js +1 -1
- package/lib/mcp/claude.js +1 -0
- package/lib/mcp/cursor.js +1 -1
- package/lib/mcp/main.js +1 -1
- package/lib/skills/main.js +1 -0
- package/lib/utils/check-sdk-version.js +1 -1
- package/lib/utils/config.js +1 -1
- package/lib/utils/copy-directory.js +1 -1
- package/lib/utils/http-client.js +1 -1
- package/lib/utils/logger.js +1 -1
- package/lib/utils/router-updater.js +1 -1
- package/lib/utils/sleep.js +1 -1
- package/lib/utils/template-replacer.js +1 -1
- package/package.json +1 -1
- package/templates/projects/sub-app-react-demo/index.html +22 -34
- package/templates/projects/sub-app-react-demo/public/logo.svg +1 -0
- package/templates/projects/sub-app-react-demo/src/api/api.ts +1 -1
- package/templates/projects/sub-app-react-demo/src/api/client.ts +1 -1
- package/templates/projects/sub-app-react-demo/src/layouts/MainLayout.tsx +44 -71
- package/templates/projects/sub-app-react-demo/src/pages/index.tsx +387 -927
- package/templates/projects/sub-app-react-demo/src/pages/sdk-demo/index.tsx +1 -1
- package/templates/projects/sub-app-react-demo/src/pages/workbench/index.module.css +293 -0
- package/templates/projects/sub-app-react-demo/src/pages/workbench/index.tsx +100 -414
- package/templates/projects/sub-app-react-demo/src/style.css +21 -15
- package/templates/projects/sub-app-react-demo/vite.config.ts +18 -13
- package/templates/rules/lovrabet_rules.mdc.tpl +636 -43
- package/templates/skills/.claude/skills/lovrabet/SKILL.md +258 -0
- package/templates/skills/.cursor/commands/lovrabet.md +248 -0
- package/templates/skills/.cursorrules +109 -0
- package/templates/skills/.shared/README.md +45 -0
- package/templates/skills/.shared/guides/01-filter-query/guide.md +300 -0
- package/templates/skills/.shared/guides/02-mcp-sql-workflow/guide.md +272 -0
- package/templates/skills/.shared/guides/03-antd-style/guide.md +227 -0
- package/templates/skills/.shared/guides/04-troubleshooting/guide.md +426 -0
- package/templates/skills/.shared/guides/05-api-integration/guide.md +327 -0
- package/templates/skills/.shared/guides/06-menu-management/guide.md +305 -0
- package/templates/skills/.shared/guides/07-backend-function/guide.md +679 -0
- package/templates/skills/.windsurf/workflows/lovrabet.md +257 -0
- package/templates/projects/sub-app-react-demo/.vscode/extensions.json +0 -3
- package/templates/projects/sub-app-react-demo/.vscode/settings.json +0 -57
- package/templates/projects/sub-app-react-demo/src/pages/intro/index.tsx +0 -560
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# Backend Function 脚本编写规范
|
|
2
|
+
|
|
3
|
+
> **目标**:学会编写符合 lovrabet-runtime 平台规范的动态脚本,用于数据权限过滤、动态脱敏、数据增强和复杂业务逻辑。
|
|
4
|
+
|
|
5
|
+
## 核心概念
|
|
6
|
+
|
|
7
|
+
Backend Function 是运行在 lovrabet-runtime 平台上的动态脚本,分为两类:
|
|
8
|
+
|
|
9
|
+
| 类型 | 说明 | 触发方式 |
|
|
10
|
+
|------|------|----------|
|
|
11
|
+
| **HOOK 脚本** | 依附于标准数据接口 | Before(前置)/ After(后置) |
|
|
12
|
+
| **ENDPOINT 脚本** | 独立业务端点 | POST `/api/{appCode}/endpoint/{scriptName}` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 5 步强制工作流程
|
|
17
|
+
|
|
18
|
+
**重要**:编写 Backend Function 脚本时,必须严格按照以下顺序执行,**禁止跳过任何步骤**。
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
需求分析 → 字段分析 → 编写脚本 → 字段验证 → 添加注释
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 步骤 1: 需求分析与数据集获取
|
|
25
|
+
|
|
26
|
+
**目标**:理解用户需求,确定涉及的数据集。
|
|
27
|
+
|
|
28
|
+
**操作**:
|
|
29
|
+
1. 分解用户需求,识别关键信息:
|
|
30
|
+
- 需要操作哪个数据集?
|
|
31
|
+
- 是 Before 还是 After 脚本?
|
|
32
|
+
- 是哪个接口操作(filter、getList、create 等)?
|
|
33
|
+
- 业务逻辑是什么(权限过滤、数据脱敏、字段计算等)?
|
|
34
|
+
|
|
35
|
+
2. 使用 MCP 工具获取数据集详情:
|
|
36
|
+
```
|
|
37
|
+
使用 get_dataset_detail 获取数据集详情
|
|
38
|
+
datasetCode: <数据集代码>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**示例**:
|
|
42
|
+
```
|
|
43
|
+
用户需求:"在用户列表查询前,非管理员只能看到自己创建的数据"
|
|
44
|
+
|
|
45
|
+
分析结果:
|
|
46
|
+
- 数据集:users(用户信息)
|
|
47
|
+
- 脚本类型:Before
|
|
48
|
+
- 接口操作:filter
|
|
49
|
+
- 业务逻辑:权限过滤(根据 created_by 字段)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 步骤 2: 字段详细分析与推理
|
|
53
|
+
|
|
54
|
+
**目标**:深入分析数据集的字段结构,并根据业务需求推理出需要使用的字段。
|
|
55
|
+
|
|
56
|
+
**操作**:
|
|
57
|
+
1. 从 MCP 返回的数据集详情中,提取关键信息:
|
|
58
|
+
- `fields` - 所有字段列表
|
|
59
|
+
- 每个字段的 `name`(字段名)、`type`(类型)、`comment`(语义说明)
|
|
60
|
+
- 字段之间的关联关系(如外键)
|
|
61
|
+
|
|
62
|
+
2. **根据业务需求推理所需字段**(关键步骤):
|
|
63
|
+
- 分析业务场景,识别涉及的业务实体
|
|
64
|
+
- 推理需要读取或修改的字段
|
|
65
|
+
- 推理需要用于条件判断的字段
|
|
66
|
+
- 推理需要关联查询的字段
|
|
67
|
+
|
|
68
|
+
3. 记录脚本中需要用到的字段:
|
|
69
|
+
- 字段名称(精确匹配,区分大小写)
|
|
70
|
+
- 字段类型(STRING、NUMBER、BOOLEAN 等)
|
|
71
|
+
- 字段语义(理解字段的业务含义)
|
|
72
|
+
- 使用场景(条件判断、数据修改、关联查询等)
|
|
73
|
+
|
|
74
|
+
**推理示例**:
|
|
75
|
+
|
|
76
|
+
**场景 1:权限过滤**
|
|
77
|
+
```
|
|
78
|
+
业务需求:"非管理员只能查看自己创建的数据"
|
|
79
|
+
|
|
80
|
+
推理过程:
|
|
81
|
+
1. 需要判断用户角色 → 需要 context.userInfo.role
|
|
82
|
+
2. 需要过滤创建人 → 需要 created_by 字段
|
|
83
|
+
3. 需要当前用户 ID → 需要 context.userInfo.id
|
|
84
|
+
|
|
85
|
+
需要的字段:
|
|
86
|
+
- created_by (STRING) - 创建人 ID(用于过滤条件)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**场景 2:数据脱敏**
|
|
90
|
+
```
|
|
91
|
+
业务需求:"手机号中间四位脱敏显示"
|
|
92
|
+
|
|
93
|
+
推理过程:
|
|
94
|
+
1. 需要处理手机号 → 需要 phone 字段
|
|
95
|
+
2. 脱敏是在返回数据后处理 → After 脚本
|
|
96
|
+
3. 需要遍历列表数据 → 操作 params.tableData
|
|
97
|
+
|
|
98
|
+
需要的字段:
|
|
99
|
+
- phone (STRING) - 手机号(用于脱敏处理)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**场景 3:自动填充**
|
|
103
|
+
```
|
|
104
|
+
业务需求:"创建订单时自动填充用户信息和时间"
|
|
105
|
+
|
|
106
|
+
推理过程:
|
|
107
|
+
1. 需要填充用户 ID → 需要 user_id 字段
|
|
108
|
+
2. 需要填充租户编码 → 需要 tenant_code 字段
|
|
109
|
+
3. 需要填充创建时间 → 需要 create_time 字段
|
|
110
|
+
4. 需要当前用户信息 → 需要 context.userInfo
|
|
111
|
+
5. 需要当前租户信息 → 需要 context.tenantCode
|
|
112
|
+
|
|
113
|
+
需要的字段:
|
|
114
|
+
- user_id (STRING) - 用户 ID(自动填充)
|
|
115
|
+
- tenant_code (STRING) - 租户编码(自动填充)
|
|
116
|
+
- create_time (DATETIME) - 创建时间(自动填充)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**场景 4:关联查询增强**
|
|
120
|
+
```
|
|
121
|
+
业务需求:"订单列表显示用户名称和等级"
|
|
122
|
+
|
|
123
|
+
推理过程:
|
|
124
|
+
1. 订单表有用户 ID → 需要 user_id 字段
|
|
125
|
+
2. 需要查询用户表 → 需要用户数据集的 id 字段
|
|
126
|
+
3. 需要获取用户名称 → 需要用户数据集的 username 字段
|
|
127
|
+
4. 需要获取用户等级 → 需要用户数据集的 vip_level 字段
|
|
128
|
+
5. 需要将用户信息追加到订单数据 → 需要在订单对象上添加新字段
|
|
129
|
+
|
|
130
|
+
需要的字段:
|
|
131
|
+
订单表:
|
|
132
|
+
- user_id (STRING) - 用户 ID(用于关联查询)
|
|
133
|
+
|
|
134
|
+
用户表:
|
|
135
|
+
- id (STRING) - 用户 ID(关联条件)
|
|
136
|
+
- username (STRING) - 用户名称(追加到订单数据)
|
|
137
|
+
- vip_level (NUMBER) - VIP 等级(追加到订单数据)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 步骤 3: 编写脚本
|
|
141
|
+
|
|
142
|
+
**目标**:根据需求和字段分析,编写符合规范的脚本代码。
|
|
143
|
+
|
|
144
|
+
**操作**:
|
|
145
|
+
1. 确定脚本类型(HOOK 或 ENDPOINT)
|
|
146
|
+
2. 确定接口类型(普通接口 或 filter/aggregate)
|
|
147
|
+
3. 编写函数签名:`export default async function`
|
|
148
|
+
4. 实现业务逻辑
|
|
149
|
+
5. 确保返回 params 或业务对象
|
|
150
|
+
|
|
151
|
+
**注意**:
|
|
152
|
+
- 普通接口:直接设置 `params.fieldName = value`
|
|
153
|
+
- filter 接口:操作 `params.ytWhere`,使用操作符语法
|
|
154
|
+
- 所有数据库操作必须 `await`
|
|
155
|
+
|
|
156
|
+
### 步骤 4: 字段二次验证
|
|
157
|
+
|
|
158
|
+
**目标**:检查脚本中使用的所有字段,确保没有编造或用错字段。
|
|
159
|
+
|
|
160
|
+
**操作**:
|
|
161
|
+
1. 列出脚本中使用的所有字段名
|
|
162
|
+
2. 逐一对照步骤 2 中记录的字段列表
|
|
163
|
+
3. 检查:
|
|
164
|
+
- 字段名是否精确匹配(区分大小写)
|
|
165
|
+
- 字段类型是否正确使用
|
|
166
|
+
- 是否有拼写错误(如 `create_by` vs `created_by`)
|
|
167
|
+
- 是否使用了不存在的字段
|
|
168
|
+
|
|
169
|
+
**常见错误**:
|
|
170
|
+
- ❌ `params.createBy` - 错误:驼峰命名,应为 `created_by`
|
|
171
|
+
- ❌ `params.userId` - 错误:字段不存在,应为 `user_id`
|
|
172
|
+
- ❌ `params.userName` - 错误:字段不存在,应为 `user_name`
|
|
173
|
+
|
|
174
|
+
### 步骤 5: 添加顶部注释
|
|
175
|
+
|
|
176
|
+
**目标**:在脚本顶部添加清晰的注释,描述入参和出参格式。
|
|
177
|
+
|
|
178
|
+
**格式**:
|
|
179
|
+
```javascript
|
|
180
|
+
/**
|
|
181
|
+
* [脚本功能描述]
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} params - 入参说明
|
|
184
|
+
* @param {string} params.fieldName - 字段说明
|
|
185
|
+
* @param {Object} context - 执行上下文
|
|
186
|
+
* @returns {Object} 出参说明
|
|
187
|
+
*/
|
|
188
|
+
export default async function functionName(params, context) {
|
|
189
|
+
// 业务逻辑
|
|
190
|
+
return params;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**示例**:
|
|
195
|
+
```javascript
|
|
196
|
+
/**
|
|
197
|
+
* 用户列表查询前置脚本 - 权限过滤
|
|
198
|
+
* 非管理员用户只能查询自己创建的数据
|
|
199
|
+
*
|
|
200
|
+
* @param {Object} params - filter 接口请求参数
|
|
201
|
+
* @param {Object} params.ytWhere - 查询条件对象
|
|
202
|
+
* @param {Object} context - 执行上下文
|
|
203
|
+
* @param {Object} context.userInfo - 当前用户信息
|
|
204
|
+
* @param {string} context.userInfo.id - 用户 ID
|
|
205
|
+
* @param {string} context.userInfo.role - 用户角色
|
|
206
|
+
* @returns {Object} 修改后的 params 对象
|
|
207
|
+
*/
|
|
208
|
+
export default async function beforeFilter(params, context) {
|
|
209
|
+
if (context.userInfo.role !== "admin") {
|
|
210
|
+
params.ytWhere = params.ytWhere || {};
|
|
211
|
+
const originalWhere = { ...params.ytWhere };
|
|
212
|
+
params.ytWhere = {
|
|
213
|
+
$and: [
|
|
214
|
+
originalWhere,
|
|
215
|
+
{ created_by: { $eq: context.userInfo.id } }
|
|
216
|
+
]
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return params;
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 函数签名规范
|
|
226
|
+
|
|
227
|
+
所有脚本必须导出一个**异步函数**,接收两个固定参数:
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
export default async function functionName(params, context) {
|
|
231
|
+
// 业务逻辑
|
|
232
|
+
return params;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**函数名格式**:`[stage][OperationName]`,例如:`beforeGetList`、`afterFilter`、`createOrder`
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 参数说明
|
|
241
|
+
|
|
242
|
+
### params(业务数据)
|
|
243
|
+
|
|
244
|
+
| 阶段 | params 内容 |
|
|
245
|
+
|------|-------------|
|
|
246
|
+
| **Before** | API 请求参数(requestBody),如查询条件、表单数据 |
|
|
247
|
+
| **After** | API 返回结果(responseBody.data),通常包含 `tableData`、`tableColumns` |
|
|
248
|
+
| **Endpoint** | HTTP 请求体中的 JSON 数据 |
|
|
249
|
+
|
|
250
|
+
### context(执行上下文)
|
|
251
|
+
|
|
252
|
+
| 属性 | 类型 | 说明 |
|
|
253
|
+
|------|------|------|
|
|
254
|
+
| `context.userInfo` | Object | 当前登录用户信息(id, username, role, tenantCode 等) |
|
|
255
|
+
| `context.appCode` | String | 当前应用编码 |
|
|
256
|
+
| `context.tenantCode` | String | 当前租户编码 |
|
|
257
|
+
| `context.client` | Object | 数据库操作入口,提供 models 方法访问数据集 |
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 数据库操作 API
|
|
262
|
+
|
|
263
|
+
通过 `context.client.models.dataset_{code}` 访问数据集(`{code}` 是 32 位数据集编码):
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
const ds = context.client.models.dataset_XXXXXXXXXX;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
| 方法 | 说明 | 参数示例 | 返回值 |
|
|
270
|
+
|------|------|----------|--------|
|
|
271
|
+
| `findOne(params)` | 查询单条 | `{ id: 1 }` | Object |
|
|
272
|
+
| `getList(params)` | 分页查询 | `{ page: 1, size: 20 }` | List |
|
|
273
|
+
| `filter(params)` | 高级过滤 | `{ ytWhere: { age: { $eq: 18 } } }` | List |
|
|
274
|
+
| `create(data)` | 创建数据 | `{ name: "John", age: 20 }` | ID |
|
|
275
|
+
| `update(data)` | 更新数据 | `{ id: 1, name: "New" }` | Boolean |
|
|
276
|
+
| `delete(params)` | 删除数据 | `{ id: 1 }` | Boolean |
|
|
277
|
+
|
|
278
|
+
**注意**:所有数据库操作都是异步的,**必须使用 `await`**。
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 接口返回规范
|
|
283
|
+
|
|
284
|
+
### filter 接口返回格式
|
|
285
|
+
|
|
286
|
+
filter 接口返回的数据格式固定为:
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
{
|
|
290
|
+
"tableData": [], // 数据列表
|
|
291
|
+
"paging": {
|
|
292
|
+
"currentPage": 1, // 当前页码
|
|
293
|
+
"totalCount": 0, // 总记录数
|
|
294
|
+
"pageSize": 30 // 每页数量
|
|
295
|
+
},
|
|
296
|
+
"tableColumns": [] // 列定义
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 关联查询时的数据提取
|
|
301
|
+
|
|
302
|
+
**重要**:调用其他数据集的 filter 接口时,返回数据需要从 `tableData` 数组中获取。
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
// ✅ 正确:从 tableData 中提取数据
|
|
306
|
+
const filterResult = await context.client.models.dataset_XXXXXXXXXX.filter({
|
|
307
|
+
ytWhere: { user_id: { $in: userIds } }
|
|
308
|
+
});
|
|
309
|
+
const dataList = filterResult.tableData || [];
|
|
310
|
+
|
|
311
|
+
// ❌ 错误:直接使用返回结果
|
|
312
|
+
const dataList = await context.client.models.dataset_XXXXXXXXXX.filter({
|
|
313
|
+
ytWhere: { user_id: { $in: userIds } }
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 接口分类与调用规范
|
|
320
|
+
|
|
321
|
+
**重要**:以下规范适用于所有 Backend Function 脚本(Before、After、ENDPOINT)中调用数据集接口时。
|
|
322
|
+
|
|
323
|
+
### 接口分类
|
|
324
|
+
|
|
325
|
+
lovrabet-runtime 平台的数据集接口分为两大类:
|
|
326
|
+
|
|
327
|
+
| 分类 | 接口 | 特点 |
|
|
328
|
+
|------|------|------|
|
|
329
|
+
| **普通接口** | getList / create / update / delete / findOne | 参数直接设置字段值 |
|
|
330
|
+
| **高级查询接口** | filter / aggregate | 支持复杂查询条件和操作符 |
|
|
331
|
+
|
|
332
|
+
**关键区别**:
|
|
333
|
+
- **普通接口**:参数中直接设置字段值,如 `{ status: "active" }`
|
|
334
|
+
- **高级查询接口**:必须使用操作符语法,如 `{ ytWhere: { status: { $eq: "active" } } }`
|
|
335
|
+
|
|
336
|
+
### Filter / Aggregate 接口参数
|
|
337
|
+
|
|
338
|
+
| 参数 | filter | aggregate | 说明 |
|
|
339
|
+
|------|:------:|:---------:|------|
|
|
340
|
+
| `ytWhere` | Y | Y | 查询条件(使用操作符语法) |
|
|
341
|
+
| `ytSelect` | Y | Y | 返回字段列表 |
|
|
342
|
+
| `ytOrderBy` | Y | Y | 排序规则 `[{"columnName": "desc"}]` |
|
|
343
|
+
| `ytHaving` | N | Y | having 条件 |
|
|
344
|
+
| `ytGroupBy` | N | Y | 分组字段 |
|
|
345
|
+
| `ytAggregate` | N | Y | 聚合函数 |
|
|
346
|
+
| `currentPage` | Y | Y | 当前页码 |
|
|
347
|
+
| `pageSize` | Y | Y | 每页数量 |
|
|
348
|
+
|
|
349
|
+
### ytAggregate 聚合函数格式(仅 aggregate 接口)
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
[
|
|
353
|
+
{
|
|
354
|
+
type: "SUM", // 聚合类型:SUM / COUNT / AVG / MAX / MIN
|
|
355
|
+
field: "amount", // 聚合字段
|
|
356
|
+
alias: "sumAmount", // 结果别名
|
|
357
|
+
distinct: true, // 可选:是否去重
|
|
358
|
+
round: true, // 可选:是否四舍五入
|
|
359
|
+
precision: 2, // 可选:小数精度
|
|
360
|
+
},
|
|
361
|
+
]
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**聚合类型说明**:
|
|
365
|
+
- `SUM` - 求和
|
|
366
|
+
- `COUNT` - 计数
|
|
367
|
+
- `AVG` - 平均值
|
|
368
|
+
- `MAX` - 最大值
|
|
369
|
+
- `MIN` - 最小值
|
|
370
|
+
|
|
371
|
+
**可选参数**:
|
|
372
|
+
- `distinct` - 是否对字段值去重后再聚合
|
|
373
|
+
- `round` - 是否对结果四舍五入
|
|
374
|
+
- `precision` - 保留小数位数
|
|
375
|
+
|
|
376
|
+
### ytWhere 查询操作符
|
|
377
|
+
|
|
378
|
+
| 操作符 | 说明 | 示例 |
|
|
379
|
+
|--------|------|------|
|
|
380
|
+
| `$eq` | 等于 | `{ field: { $eq: value } }` |
|
|
381
|
+
| `$ne` | 不等于 | `{ field: { $ne: value } }` |
|
|
382
|
+
| `$gte` / `$gteq` | 大于等于 | `{ field: { $gte: value } }` |
|
|
383
|
+
| `$lte` / `$lteq` | 小于等于 | `{ field: { $lte: value } }` |
|
|
384
|
+
| `$in` | 在集合内 | `{ field: { $in: [v1, v2] } }` |
|
|
385
|
+
| `$contain` | 包含(模糊匹配) | `{ field: { $contain: "keyword" } }` |
|
|
386
|
+
| `$startWith` | 以...开头 | `{ field: { $startWith: "prefix" } }` |
|
|
387
|
+
| `$endWith` | 以...结尾 | `{ field: { $endWith: "suffix" } }` |
|
|
388
|
+
| `$and` | 且(所有条件满足) | `{ $and: [条件1, 条件2] }` |
|
|
389
|
+
| `$or` | 或(任一条件满足) | `{ $or: [条件1, 条件2] }` |
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Before 脚本编写规则
|
|
394
|
+
|
|
395
|
+
**关键**:**必须识别用户需求中的「操作类型」**,根据操作类型选择正确的写法!
|
|
396
|
+
|
|
397
|
+
Before 脚本用于在接口执行前修改请求参数。根据接口类型不同,修改方式也不同:
|
|
398
|
+
|
|
399
|
+
### 普通接口 Before
|
|
400
|
+
|
|
401
|
+
直接设置字段值:
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
export default async function beforeGetList(params, context) {
|
|
405
|
+
if (context.userInfo.role !== "admin") {
|
|
406
|
+
params.created_by = context.userInfo.id;
|
|
407
|
+
}
|
|
408
|
+
return params;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**限制**:普通接口不支持操作符(如 `$gte`、`$contain`)。如需高级查询,请使用 filter 接口。
|
|
413
|
+
|
|
414
|
+
### Filter / Aggregate 接口 Before
|
|
415
|
+
|
|
416
|
+
对于 filter 和 aggregate 接口,Before 脚本必须操作 `params.ytWhere` 来修改查询条件。
|
|
417
|
+
|
|
418
|
+
**关键规则**:
|
|
419
|
+
- 必须使用操作符语法(参见"接口分类与调用规范"章节)
|
|
420
|
+
- 必须保留原有的 `ytWhere` 条件,使用 `$and` 组合
|
|
421
|
+
- 不能直接覆盖 `params.ytWhere`
|
|
422
|
+
|
|
423
|
+
#### 示例
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
export default async function beforeFilter(params, context) {
|
|
427
|
+
if (context.userInfo.role !== "admin") {
|
|
428
|
+
params.ytWhere = params.ytWhere || {};
|
|
429
|
+
const originalWhere = { ...params.ytWhere };
|
|
430
|
+
params.ytWhere = {
|
|
431
|
+
$and: [
|
|
432
|
+
originalWhere,
|
|
433
|
+
{ created_by: { $eq: context.userInfo.id } }
|
|
434
|
+
]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return params;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### 编写要点
|
|
442
|
+
|
|
443
|
+
1. **必须操作 `params.ytWhere`**:不能直接 `params.fieldName = value`
|
|
444
|
+
2. **必须使用操作符语法**:条件值用 `{ $eq: value }` 包裹
|
|
445
|
+
3. **必须保留原有条件**:使用 `$and` 组合,不能直接覆盖
|
|
446
|
+
4. **必须初始化 ytWhere**:`params.ytWhere = params.ytWhere || {}`
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## After 脚本编写规则
|
|
451
|
+
|
|
452
|
+
After 阶段的 params 通常包含:
|
|
453
|
+
|
|
454
|
+
- `params.tableData`:数据列表(数组)
|
|
455
|
+
- `params.tableColumns`:列定义(数组)
|
|
456
|
+
|
|
457
|
+
### 示例:数据脱敏
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
export default async function afterFilter(params, context) {
|
|
461
|
+
params.tableData?.forEach((record) => {
|
|
462
|
+
if (record.phone) {
|
|
463
|
+
record.phone = record.phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return params;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 示例:敏感字段权限控制
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
export default async function afterFilter(params, context) {
|
|
474
|
+
if (context.userInfo.role !== "admin") {
|
|
475
|
+
params.tableColumns = params.tableColumns?.filter(
|
|
476
|
+
(col) => !["salary", "bank_account"].includes(col.dataIndex)
|
|
477
|
+
);
|
|
478
|
+
params.tableData?.forEach((record) => {
|
|
479
|
+
delete record.salary;
|
|
480
|
+
delete record.bank_account;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return params;
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### 示例:动态追加计算字段
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
export default async function afterFilter(params, context) {
|
|
491
|
+
params.tableData?.forEach((record) => {
|
|
492
|
+
record.total_amount = record.price * record.quantity;
|
|
493
|
+
record.is_vip = record.order_count > 10;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const extraColumns = [
|
|
497
|
+
{ dataIndex: "total_amount", title: "总金额", type: "NUMBER" },
|
|
498
|
+
{ dataIndex: "is_vip", title: "VIP客户", type: "BOOLEAN" }
|
|
499
|
+
];
|
|
500
|
+
extraColumns.forEach((col) => params.tableColumns?.push(col));
|
|
501
|
+
|
|
502
|
+
return params;
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## ENDPOINT 脚本编写规则
|
|
509
|
+
|
|
510
|
+
独立端点脚本用于复杂的事务性业务逻辑。
|
|
511
|
+
|
|
512
|
+
- **API 路径**:`POST /api/{appCode}/endpoint/{scriptName}`
|
|
513
|
+
- **请求体**:JSON 对象,作为 `params` 传入
|
|
514
|
+
- **返回值**:业务结果对象(不是 params)
|
|
515
|
+
|
|
516
|
+
### 示例:库存校验与下单
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
export default async function createOrder(params, context) {
|
|
520
|
+
// 1. 校验库存
|
|
521
|
+
const product = await context.client.models.dataset_XXXXXXXXXX.findOne({
|
|
522
|
+
id: params.productId
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (!product || product.stock < params.quantity) {
|
|
526
|
+
throw new Error(`库存不足,当前库存: ${product ? product.stock : 0}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 2. 计算价格
|
|
530
|
+
const user = await context.client.models.dataset_XXXXXXXXXX.findOne({
|
|
531
|
+
id: context.userInfo.id
|
|
532
|
+
});
|
|
533
|
+
const discount = user.vip_level >= 3 ? 0.8 : 1.0;
|
|
534
|
+
const finalPrice = product.price * params.quantity * discount;
|
|
535
|
+
|
|
536
|
+
// 3. 创建订单
|
|
537
|
+
const orderId = await context.client.models.dataset_XXXXXXXXXX.create({
|
|
538
|
+
user_id: context.userInfo.id,
|
|
539
|
+
product_id: params.productId,
|
|
540
|
+
quantity: params.quantity,
|
|
541
|
+
amount: finalPrice,
|
|
542
|
+
status: "pending",
|
|
543
|
+
create_time: new Date()
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// 4. 扣减库存
|
|
547
|
+
await context.client.models.dataset_XXXXXXXXXX.update({
|
|
548
|
+
id: product.id,
|
|
549
|
+
stock: product.stock - params.quantity
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
success: true,
|
|
554
|
+
orderId: orderId,
|
|
555
|
+
payAmount: finalPrice,
|
|
556
|
+
discountRate: discount
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 文件命名与存储规范
|
|
564
|
+
|
|
565
|
+
### 存储位置
|
|
566
|
+
|
|
567
|
+
所有 Backend Function 脚本存放在 `src/BackendFunction/` 目录下(仅存储,不参与实际调用)。
|
|
568
|
+
|
|
569
|
+
### 命名规范
|
|
570
|
+
|
|
571
|
+
| 脚本类型 | 命名格式 | 示例 |
|
|
572
|
+
|----------|----------|------|
|
|
573
|
+
| **HOOK 脚本** | `{alias}_{operation}_{before/after}.js` | `users_filter_before.js`、`orders_create_after.js` |
|
|
574
|
+
| **ENDPOINT 脚本** | `endpoint_{description}.js` | `endpoint_create_order.js` |
|
|
575
|
+
|
|
576
|
+
**alias 获取方式**:从项目的 `api.ts` 文件中 `registerModels` 函数获取数据集别名。
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 代码生成输出格式
|
|
581
|
+
|
|
582
|
+
生成 Backend Function 代码时,必须按以下 JSON 格式输出:
|
|
583
|
+
|
|
584
|
+
```json
|
|
585
|
+
{
|
|
586
|
+
"description": "功能描述(10-30字)",
|
|
587
|
+
"code": "export default async function..."
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### 代码质量要求
|
|
592
|
+
|
|
593
|
+
1. **必须导出默认异步函数**:`export default async function`
|
|
594
|
+
2. **必须返回结果**:函数末尾一定要 `return params`(HOOK)或业务对象(ENDPOINT)
|
|
595
|
+
3. **数据库操作必须 await**:所有 `context.client.models` 调用必须使用 await
|
|
596
|
+
4. **错误处理**:使用 `throw new Error("友好的错误信息")` 抛出业务异常
|
|
597
|
+
5. **代码简洁**:只生成必要的逻辑,不要添加多余的注释(除了顶部的 JSDoc 注释)
|
|
598
|
+
6. **性能优化**:
|
|
599
|
+
- 查询其他表数据,尽可能使用 filter 接口,**禁止使用 getList**
|
|
600
|
+
- 尽最大可能避免循环调用 findOne,而是使用 filter 的 `$in` 一次查询多条再合并
|
|
601
|
+
|
|
602
|
+
### 无法实现的需求处理
|
|
603
|
+
|
|
604
|
+
如果用户提出的需求在当前接口类型下无法实现,**不要乱写代码**,而是在 `description` 中告知用户原因和建议。
|
|
605
|
+
|
|
606
|
+
**示例**:
|
|
607
|
+
```json
|
|
608
|
+
{
|
|
609
|
+
"description": "getList接口不支持大于运算,请使用filter接口",
|
|
610
|
+
"code": "// getList 接口不支持运算操作符(如 $gte),无法实现\"金额大于1000\"的查询条件。\n// 建议:请将此数据集的查询接口更换为 filter 接口,filter 支持 $gte、$lte 等操作符。\nexport default async function beforeGetList(params, context) {\n return params;\n}"
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### 现有代码处理
|
|
615
|
+
|
|
616
|
+
如果用户提供了现有代码:
|
|
617
|
+
1. **理解现有代码的逻辑**:分析代码的功能和实现方式
|
|
618
|
+
2. **在现有代码基础上修改或增强**:根据用户需求进行调整
|
|
619
|
+
3. **保持代码风格一致**:遵循原有的命名规范和代码结构
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## 注意事项
|
|
624
|
+
|
|
625
|
+
### 必须遵守
|
|
626
|
+
|
|
627
|
+
1. **必须返回结果**:函数末尾一定要 `return params`(HOOK)或业务对象(ENDPOINT)
|
|
628
|
+
2. **必须 await 数据库操作**:所有 `context.client.models` 调用必须使用 await
|
|
629
|
+
3. **必须使用 filter 查询其他表**:禁止使用 getList,尽量避免循环调用 findOne,改用 filter 的 `$in` 批量查询
|
|
630
|
+
|
|
631
|
+
### 禁止事项
|
|
632
|
+
|
|
633
|
+
1. **禁止递归调用接口本身**:Before 和 After 脚本不能调用当前数据集的接口本身,防止死循环
|
|
634
|
+
2. **禁止网络请求**:Backend Function 不支持任何网络请求(HTTP、WebSocket 等)
|
|
635
|
+
3. **禁止遗漏 await**:否则无法获取数据库结果
|
|
636
|
+
4. **禁止直接覆盖 ytWhere**:必须用 `$and` 组合原有条件
|
|
637
|
+
|
|
638
|
+
### 限制
|
|
639
|
+
|
|
640
|
+
- 单次脚本执行的数据库调用次数限制:**50 次**(超出会抛出异常)
|
|
641
|
+
- 不支持网络请求:无法调用外部 API 或发起 HTTP 请求
|
|
642
|
+
|
|
643
|
+
### 错误处理
|
|
644
|
+
|
|
645
|
+
使用 `throw new Error("友好的错误信息")` 抛出业务异常,系统会捕获并返回给前端(状态码 500)。
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 快速对照表
|
|
650
|
+
|
|
651
|
+
### 普通接口 vs Filter 接口
|
|
652
|
+
|
|
653
|
+
| 场景 | 普通接口(getList 等) | Filter 接口 |
|
|
654
|
+
|------|------------------------|-------------|
|
|
655
|
+
| 权限过滤 | `params.created_by = userId` | `params.ytWhere = { $and: [..., { created_by: { $eq: userId } }] }` |
|
|
656
|
+
| 支持操作符 | 不支持 | 支持 `$eq`、`$gte`、`$contain` 等 |
|
|
657
|
+
| 复杂条件 | 不支持 | 支持 `$and`、`$or` 组合 |
|
|
658
|
+
|
|
659
|
+
### Before vs After
|
|
660
|
+
|
|
661
|
+
| 阶段 | params 内容 | 典型用途 |
|
|
662
|
+
|------|-------------|----------|
|
|
663
|
+
| **Before** | 请求参数 | 权限过滤、参数校验、自动填充 |
|
|
664
|
+
| **After** | 响应数据 | 数据脱敏、字段计算、敏感列隐藏 |
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## 自检清单
|
|
669
|
+
|
|
670
|
+
编写 Backend Function 时,检查:
|
|
671
|
+
|
|
672
|
+
- [ ] 函数签名正确:`export default async function`
|
|
673
|
+
- [ ] 返回了 params 或业务对象
|
|
674
|
+
- [ ] 数据库操作使用了 await
|
|
675
|
+
- [ ] Filter 接口使用了 ytWhere 和操作符
|
|
676
|
+
- [ ] 保留了原有 ytWhere 条件(使用 $and 组合)
|
|
677
|
+
- [ ] 没有递归调用数据集本身
|
|
678
|
+
- [ ] 查询其他表使用 filter 而非 getList
|
|
679
|
+
- [ ] 错误信息对用户友好
|