befly 3.9.36 → 3.9.38
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 +1 -1
- package/docs/api.md +113 -5
- package/docs/database.md +14 -14
- package/docs/table.md +6 -6
- package/hooks/validator.ts +8 -0
- package/lib/cacheHelper.ts +2 -6
- package/lib/dbHelper.ts +31 -10
- package/loader/loadApis.ts +13 -11
- package/package.json +4 -4
- package/plugins/tool.ts +77 -1
- package/sync/syncApi.ts +1 -3
- package/sync/syncDev.ts +0 -1
- package/sync/syncMenu.ts +1 -3
package/README.md
CHANGED
|
@@ -293,7 +293,7 @@ export const beflyConfig = { ... };
|
|
|
293
293
|
|
|
294
294
|
- **`packages/core`** - Befly 核心框架包(发布到 npm)
|
|
295
295
|
- **`packages/tpl`** - API 项目模板示例
|
|
296
|
-
- **`packages/admin`** - 后台管理系统(Vue3 +
|
|
296
|
+
- **`packages/admin`** - 后台管理系统(Vue3 + TDesign Vue Next + 自动导入)
|
|
297
297
|
|
|
298
298
|
## 🚀 快速启动
|
|
299
299
|
|
package/docs/api.md
CHANGED
|
@@ -282,6 +282,116 @@ befly.tool.No(msg: string, data?: any, other?: Record<string, any>)
|
|
|
282
282
|
}
|
|
283
283
|
```
|
|
284
284
|
|
|
285
|
+
### Raw - 原始响应
|
|
286
|
+
|
|
287
|
+
用于第三方回调等需要自定义响应格式的场景,直接返回 `Response` 对象。自动识别数据类型并设置正确的 Content-Type。
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
befly.tool.Raw(ctx: RequestContext, data: Record<string, any> | string, options?: ResponseOptions)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**参数说明:**
|
|
294
|
+
|
|
295
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
296
|
+
| ------- | ----------------------------- | ---- | ------ | ------------------------ |
|
|
297
|
+
| ctx | RequestContext | 是 | - | 请求上下文 |
|
|
298
|
+
| data | Record<string, any> \| string | 是 | - | 响应数据(对象或字符串) |
|
|
299
|
+
| options | ResponseOptions | 否 | {} | 响应选项 |
|
|
300
|
+
|
|
301
|
+
**ResponseOptions 选项:**
|
|
302
|
+
|
|
303
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
304
|
+
| ----------- | ---------------------- | -------- | ---------------------------------------- |
|
|
305
|
+
| status | number | 200 | HTTP 状态码 |
|
|
306
|
+
| contentType | string | 自动判断 | Content-Type,默认根据 data 类型自动判断 |
|
|
307
|
+
| headers | Record<string, string> | {} | 额外的响应头 |
|
|
308
|
+
|
|
309
|
+
**Content-Type 自动判断规则:**
|
|
310
|
+
|
|
311
|
+
| data 类型 | 自动 Content-Type |
|
|
312
|
+
| --------------------- | ----------------- |
|
|
313
|
+
| 对象 | application/json |
|
|
314
|
+
| 字符串(以 `<` 开头) | application/xml |
|
|
315
|
+
| 字符串(其他) | text/plain |
|
|
316
|
+
|
|
317
|
+
**使用示例:**
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// JSON 响应(自动)
|
|
321
|
+
return befly.tool.Raw(ctx, { code: 'SUCCESS', message: '成功' });
|
|
322
|
+
|
|
323
|
+
// 纯文本响应(自动)- 支付宝回调
|
|
324
|
+
return befly.tool.Raw(ctx, 'success');
|
|
325
|
+
|
|
326
|
+
// XML 响应(自动判断)
|
|
327
|
+
return befly.tool.Raw(ctx, '<xml><return_code>SUCCESS</return_code></xml>');
|
|
328
|
+
|
|
329
|
+
// XML 响应(手动指定)
|
|
330
|
+
return befly.tool.Raw(ctx, xmlString, { contentType: 'application/xml' });
|
|
331
|
+
|
|
332
|
+
// 自定义状态码
|
|
333
|
+
return befly.tool.Raw(ctx, { error: 'Not Found' }, { status: 404 });
|
|
334
|
+
|
|
335
|
+
// 自定义响应头
|
|
336
|
+
return befly.tool.Raw(
|
|
337
|
+
ctx,
|
|
338
|
+
{ code: 'SUCCESS' },
|
|
339
|
+
{
|
|
340
|
+
headers: { 'X-Custom-Header': 'value' }
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**完整回调示例:**
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// 微信支付回调
|
|
349
|
+
export default {
|
|
350
|
+
name: '微信支付回调',
|
|
351
|
+
auth: false,
|
|
352
|
+
rawBody: true,
|
|
353
|
+
handler: async (befly, ctx) => {
|
|
354
|
+
if (!befly.weixin) {
|
|
355
|
+
return befly.tool.Raw(ctx, { code: 'SYSTEM_ERROR', message: 'weixin 插件未配置' });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 处理成功
|
|
359
|
+
return befly.tool.Raw(ctx, { code: 'SUCCESS', message: '' });
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// 支付宝回调
|
|
364
|
+
export default {
|
|
365
|
+
name: '支付宝回调',
|
|
366
|
+
auth: false,
|
|
367
|
+
rawBody: true,
|
|
368
|
+
handler: async (befly, ctx) => {
|
|
369
|
+
// 支付宝要求返回纯文本 "success"
|
|
370
|
+
return befly.tool.Raw(ctx, 'success');
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// 微信公众号 XML 回调
|
|
375
|
+
export default {
|
|
376
|
+
name: '微信公众号回调',
|
|
377
|
+
auth: false,
|
|
378
|
+
rawBody: true,
|
|
379
|
+
handler: async (befly, ctx) => {
|
|
380
|
+
// 返回 XML 格式响应
|
|
381
|
+
const xml = `<xml>
|
|
382
|
+
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
|
383
|
+
<FromUserName><![CDATA[${toUser}]]></FromUserName>
|
|
384
|
+
<CreateTime>${Date.now()}</CreateTime>
|
|
385
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
386
|
+
<Content><![CDATA[收到]]></Content>
|
|
387
|
+
</xml>`;
|
|
388
|
+
return befly.tool.Raw(ctx, xml);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**注意**:`Raw` 返回的是 `Response` 对象,会直接作为 HTTP 响应返回,不经过 `FinalResponse` 处理。
|
|
394
|
+
|
|
285
395
|
### ErrorResponse - Hook 中断响应
|
|
286
396
|
|
|
287
397
|
在 Hook 中使用,用于提前拦截请求:
|
|
@@ -889,7 +999,7 @@ import adminTable from '../../tables/admin.json';
|
|
|
889
999
|
export default {
|
|
890
1000
|
name: '添加管理员',
|
|
891
1001
|
fields: adminTable,
|
|
892
|
-
required: ['username', 'password', '
|
|
1002
|
+
required: ['username', 'password', 'roleCode'],
|
|
893
1003
|
handler: async (befly, ctx) => {
|
|
894
1004
|
// 检查用户名是否已存在
|
|
895
1005
|
const existing = await befly.db.getOne({
|
|
@@ -904,11 +1014,10 @@ export default {
|
|
|
904
1014
|
// 查询角色信息
|
|
905
1015
|
const role = await befly.db.getOne({
|
|
906
1016
|
table: 'addon_admin_role',
|
|
907
|
-
where: {
|
|
908
|
-
columns: ['code']
|
|
1017
|
+
where: { code: ctx.body.roleCode }
|
|
909
1018
|
});
|
|
910
1019
|
|
|
911
|
-
if (!role
|
|
1020
|
+
if (!role) {
|
|
912
1021
|
return befly.tool.No('角色不存在');
|
|
913
1022
|
}
|
|
914
1023
|
|
|
@@ -922,7 +1031,6 @@ export default {
|
|
|
922
1031
|
username: ctx.body.username,
|
|
923
1032
|
password: hashedPassword,
|
|
924
1033
|
nickname: ctx.body.nickname,
|
|
925
|
-
roleId: ctx.body.roleId,
|
|
926
1034
|
roleCode: role.code
|
|
927
1035
|
}
|
|
928
1036
|
});
|
package/docs/database.md
CHANGED
|
@@ -153,7 +153,7 @@ export default {
|
|
|
153
153
|
name: '用户列表',
|
|
154
154
|
fields: {
|
|
155
155
|
keyword: { name: '关键词', type: 'string', max: 50 },
|
|
156
|
-
|
|
156
|
+
status: { name: '状态', type: 'number' },
|
|
157
157
|
state: { name: '状态', type: 'number' }
|
|
158
158
|
},
|
|
159
159
|
handler: async (befly, ctx) => {
|
|
@@ -162,7 +162,7 @@ export default {
|
|
|
162
162
|
const result = await befly.db.getList({
|
|
163
163
|
table: 'user',
|
|
164
164
|
where: {
|
|
165
|
-
|
|
165
|
+
status: ctx.body.status, // 未传时为 undefined,自动忽略
|
|
166
166
|
state: ctx.body.state, // 未传时为 undefined,自动忽略
|
|
167
167
|
username$like: ctx.body.keyword ? `%${ctx.body.keyword}%` : undefined // 无关键词时忽略
|
|
168
168
|
},
|
|
@@ -441,7 +441,7 @@ const userId = await befly.db.insData({
|
|
|
441
441
|
username: 'john',
|
|
442
442
|
email: 'john@example.com',
|
|
443
443
|
password: hashedPassword,
|
|
444
|
-
|
|
444
|
+
categoryId: 1
|
|
445
445
|
}
|
|
446
446
|
});
|
|
447
447
|
// 返回新记录的 ID
|
|
@@ -502,7 +502,7 @@ const affected = await befly.db.updData({
|
|
|
502
502
|
await befly.db.updData({
|
|
503
503
|
table: 'user',
|
|
504
504
|
data: { state: 2 },
|
|
505
|
-
where: {
|
|
505
|
+
where: { status: 5 }
|
|
506
506
|
});
|
|
507
507
|
```
|
|
508
508
|
|
|
@@ -794,8 +794,8 @@ where: { id: 1 }
|
|
|
794
794
|
// → WHERE id = 1
|
|
795
795
|
|
|
796
796
|
// 多条件(AND)
|
|
797
|
-
where: { state: 1,
|
|
798
|
-
// → WHERE state = 1 AND
|
|
797
|
+
where: { state: 1, status: 2 }
|
|
798
|
+
// → WHERE state = 1 AND status = 2
|
|
799
799
|
```
|
|
800
800
|
|
|
801
801
|
### 比较操作符
|
|
@@ -842,9 +842,9 @@ where: {
|
|
|
842
842
|
```typescript
|
|
843
843
|
// 显式 AND(通常不需要,多条件默认就是 AND)
|
|
844
844
|
where: {
|
|
845
|
-
$and: [{ state: 1 }, {
|
|
845
|
+
$and: [{ state: 1 }, { status: 2 }];
|
|
846
846
|
}
|
|
847
|
-
// → WHERE state = 1 AND
|
|
847
|
+
// → WHERE state = 1 AND status = 2
|
|
848
848
|
```
|
|
849
849
|
|
|
850
850
|
**组合使用**
|
|
@@ -867,9 +867,9 @@ where: {
|
|
|
867
867
|
|
|
868
868
|
```typescript
|
|
869
869
|
where: {
|
|
870
|
-
|
|
870
|
+
categoryId$in: [1, 2, 3];
|
|
871
871
|
}
|
|
872
|
-
// → WHERE
|
|
872
|
+
// → WHERE category_id IN (1, 2, 3)
|
|
873
873
|
|
|
874
874
|
where: {
|
|
875
875
|
status$in: ['pending', 'processing'];
|
|
@@ -1075,7 +1075,7 @@ export default {
|
|
|
1075
1075
|
name: '用户列表',
|
|
1076
1076
|
fields: {
|
|
1077
1077
|
keyword: { name: '关键词', type: 'string', max: 50 },
|
|
1078
|
-
|
|
1078
|
+
departmentId: { name: '部门ID', type: 'number' },
|
|
1079
1079
|
page: Fields.page,
|
|
1080
1080
|
limit: Fields.limit
|
|
1081
1081
|
},
|
|
@@ -1087,9 +1087,9 @@ export default {
|
|
|
1087
1087
|
where.$or = [{ username$like: `%${ctx.body.keyword}%` }, { nickname$like: `%${ctx.body.keyword}%` }, { email$like: `%${ctx.body.keyword}%` }];
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
-
//
|
|
1091
|
-
if (ctx.body.
|
|
1092
|
-
where.
|
|
1090
|
+
// 部门过滤
|
|
1091
|
+
if (ctx.body.departmentId) {
|
|
1092
|
+
where.departmentId = ctx.body.departmentId;
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
1095
|
const result = await befly.db.getList({
|
package/docs/table.md
CHANGED
|
@@ -184,8 +184,8 @@ interface FieldDefinition {
|
|
|
184
184
|
"max": 150,
|
|
185
185
|
"default": 0
|
|
186
186
|
},
|
|
187
|
-
"
|
|
188
|
-
"name": "
|
|
187
|
+
"userId": {
|
|
188
|
+
"name": "用户ID",
|
|
189
189
|
"type": "number",
|
|
190
190
|
"min": 1,
|
|
191
191
|
"index": true
|
|
@@ -287,8 +287,8 @@ interface FieldDefinition {
|
|
|
287
287
|
|
|
288
288
|
```json
|
|
289
289
|
{
|
|
290
|
-
"
|
|
291
|
-
"name": "
|
|
290
|
+
"categoryId": {
|
|
291
|
+
"name": "分类ID",
|
|
292
292
|
"type": "number",
|
|
293
293
|
"index": true
|
|
294
294
|
},
|
|
@@ -620,8 +620,8 @@ bun run sync:db --force
|
|
|
620
620
|
"type": "string",
|
|
621
621
|
"max": 500
|
|
622
622
|
},
|
|
623
|
-
"
|
|
624
|
-
"name": "
|
|
623
|
+
"departmentId": {
|
|
624
|
+
"name": "部门ID",
|
|
625
625
|
"type": "number",
|
|
626
626
|
"min": 1,
|
|
627
627
|
"index": true
|
package/hooks/validator.ts
CHANGED
|
@@ -19,6 +19,14 @@ const hook: Hook = {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// 应用字段默认值
|
|
23
|
+
for (const [field, fieldDef] of Object.entries(ctx.api.fields)) {
|
|
24
|
+
// 字段未传值且定义了默认值时,应用默认值
|
|
25
|
+
if (ctx.body[field] === undefined && (fieldDef as any)?.default !== undefined) {
|
|
26
|
+
ctx.body[field] = (fieldDef as any).default;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
// 验证参数
|
|
23
31
|
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
24
32
|
|
package/lib/cacheHelper.ts
CHANGED
|
@@ -37,7 +37,6 @@ export class CacheHelper {
|
|
|
37
37
|
// 从数据库查询所有接口
|
|
38
38
|
const apiList = await this.befly.db.getAll({
|
|
39
39
|
table: 'addon_admin_api',
|
|
40
|
-
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
41
40
|
orderBy: ['addonName#ASC', 'path#ASC']
|
|
42
41
|
});
|
|
43
42
|
|
|
@@ -67,7 +66,6 @@ export class CacheHelper {
|
|
|
67
66
|
// 从数据库查询所有菜单
|
|
68
67
|
const menus = await this.befly.db.getAll({
|
|
69
68
|
table: 'addon_admin_menu',
|
|
70
|
-
fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
|
|
71
69
|
orderBy: ['sort#ASC', 'id#ASC']
|
|
72
70
|
});
|
|
73
71
|
|
|
@@ -100,12 +98,10 @@ export class CacheHelper {
|
|
|
100
98
|
// 并行查询角色和接口(利用自动 pipeline)
|
|
101
99
|
const [roles, allApis] = await Promise.all([
|
|
102
100
|
this.befly.db.getAll({
|
|
103
|
-
table: 'addon_admin_role'
|
|
104
|
-
fields: ['id', 'code', 'apis']
|
|
101
|
+
table: 'addon_admin_role'
|
|
105
102
|
}),
|
|
106
103
|
this.befly.db.getAll({
|
|
107
|
-
table: 'addon_admin_api'
|
|
108
|
-
fields: ['id', 'path', 'method']
|
|
104
|
+
table: 'addon_admin_api'
|
|
109
105
|
})
|
|
110
106
|
]);
|
|
111
107
|
|
package/lib/dbHelper.ts
CHANGED
|
@@ -283,7 +283,7 @@ export class DbHelper {
|
|
|
283
283
|
|
|
284
284
|
// 联查时使用特殊处理逻辑
|
|
285
285
|
if (hasJoins) {
|
|
286
|
-
//
|
|
286
|
+
// 联查时字段直接处理(支持表名.字段名格式)
|
|
287
287
|
const processedFields = (options.fields || []).map((f) => this.processJoinField(f));
|
|
288
288
|
|
|
289
289
|
return {
|
|
@@ -339,15 +339,26 @@ export class DbHelper {
|
|
|
339
339
|
/**
|
|
340
340
|
* 添加默认的 state 过滤条件
|
|
341
341
|
* 默认查询 state > 0 的数据(排除已删除和特殊状态)
|
|
342
|
+
* @param where - where 条件
|
|
343
|
+
* @param table - 主表名(JOIN 查询时需要,用于添加表名前缀避免歧义)
|
|
344
|
+
* @param hasJoins - 是否有 JOIN 查询
|
|
342
345
|
*/
|
|
343
|
-
private addDefaultStateFilter(where: WhereConditions = {}): WhereConditions {
|
|
346
|
+
private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
|
|
344
347
|
// 如果用户已经指定了 state 条件,优先使用用户的条件
|
|
345
|
-
const hasStateCondition = Object.keys(where).some((key) => key.startsWith('state'));
|
|
348
|
+
const hasStateCondition = Object.keys(where).some((key) => key.startsWith('state') || key.includes('.state'));
|
|
346
349
|
|
|
347
350
|
if (hasStateCondition) {
|
|
348
351
|
return where;
|
|
349
352
|
}
|
|
350
353
|
|
|
354
|
+
// JOIN 查询时需要指定主表名前缀避免歧义
|
|
355
|
+
if (hasJoins && table) {
|
|
356
|
+
return {
|
|
357
|
+
...where,
|
|
358
|
+
[`${table}.state$gt`]: 0
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
351
362
|
// 默认查询 state > 0 的数据
|
|
352
363
|
return {
|
|
353
364
|
...where,
|
|
@@ -541,7 +552,10 @@ export class DbHelper {
|
|
|
541
552
|
async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
|
|
542
553
|
const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
|
|
543
554
|
|
|
544
|
-
const builder = new SqlBuilder()
|
|
555
|
+
const builder = new SqlBuilder()
|
|
556
|
+
.select(['COUNT(*) as count'])
|
|
557
|
+
.from(table)
|
|
558
|
+
.where(this.addDefaultStateFilter(where, table, !!joins));
|
|
545
559
|
|
|
546
560
|
// 添加 JOIN
|
|
547
561
|
this.applyJoins(builder, joins);
|
|
@@ -571,7 +585,10 @@ export class DbHelper {
|
|
|
571
585
|
async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
|
|
572
586
|
const { table, fields, where, joins } = await this.prepareQueryOptions(options);
|
|
573
587
|
|
|
574
|
-
const builder = new SqlBuilder()
|
|
588
|
+
const builder = new SqlBuilder()
|
|
589
|
+
.select(fields)
|
|
590
|
+
.from(table)
|
|
591
|
+
.where(this.addDefaultStateFilter(where, table, !!joins));
|
|
575
592
|
|
|
576
593
|
// 添加 JOIN
|
|
577
594
|
this.applyJoins(builder, joins);
|
|
@@ -623,7 +640,7 @@ export class DbHelper {
|
|
|
623
640
|
}
|
|
624
641
|
|
|
625
642
|
// 构建查询
|
|
626
|
-
const whereFiltered = this.addDefaultStateFilter(prepared.where);
|
|
643
|
+
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
627
644
|
|
|
628
645
|
// 查询总数
|
|
629
646
|
const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
|
|
@@ -698,7 +715,7 @@ export class DbHelper {
|
|
|
698
715
|
|
|
699
716
|
const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
700
717
|
|
|
701
|
-
const whereFiltered = this.addDefaultStateFilter(prepared.where);
|
|
718
|
+
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
702
719
|
|
|
703
720
|
// 查询真实总数
|
|
704
721
|
const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
|
|
@@ -893,7 +910,7 @@ export class DbHelper {
|
|
|
893
910
|
};
|
|
894
911
|
|
|
895
912
|
// 构建 SQL
|
|
896
|
-
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
913
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
897
914
|
const builder = new SqlBuilder().where(whereFiltered);
|
|
898
915
|
const { sql, params } = builder.toUpdateSql(snakeTable, processed);
|
|
899
916
|
|
|
@@ -1003,7 +1020,11 @@ export class DbHelper {
|
|
|
1003
1020
|
const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
1004
1021
|
|
|
1005
1022
|
// 使用 COUNT(1) 性能更好
|
|
1006
|
-
const builder = new SqlBuilder()
|
|
1023
|
+
const builder = new SqlBuilder()
|
|
1024
|
+
.select(['COUNT(1) as cnt'])
|
|
1025
|
+
.from(table)
|
|
1026
|
+
.where(this.addDefaultStateFilter(where, table, false))
|
|
1027
|
+
.limit(1);
|
|
1007
1028
|
|
|
1008
1029
|
const { sql, params } = builder.toSelectSql();
|
|
1009
1030
|
const result = await this.executeWithConn(sql, params);
|
|
@@ -1084,7 +1105,7 @@ export class DbHelper {
|
|
|
1084
1105
|
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
1085
1106
|
|
|
1086
1107
|
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
|
1087
|
-
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
1108
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
1088
1109
|
const builder = new SqlBuilder().where(whereFiltered);
|
|
1089
1110
|
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
1090
1111
|
|
package/loader/loadApis.ts
CHANGED
|
@@ -24,16 +24,16 @@ import type { ApiRoute } from '../types/api.js';
|
|
|
24
24
|
*/
|
|
25
25
|
const PRESET_FIELDS: Record<string, any> = {
|
|
26
26
|
'@id': { name: 'ID', type: 'number', min: 1, max: null },
|
|
27
|
-
'@page': { name: '页码', type: 'number', min: 1, max: 9999 },
|
|
28
|
-
'@limit': { name: '每页数量', type: 'number', min: 1, max: 100 },
|
|
29
|
-
'@keyword': { name: '关键词', type: 'string', min:
|
|
30
|
-
'@state': { name: '状态', type: 'number',
|
|
27
|
+
'@page': { name: '页码', type: 'number', min: 1, max: 9999, default: 1 },
|
|
28
|
+
'@limit': { name: '每页数量', type: 'number', min: 1, max: 100, default: 30 },
|
|
29
|
+
'@keyword': { name: '关键词', type: 'string', min: 0, max: 50 },
|
|
30
|
+
'@state': { name: '状态', type: 'number', regex: '^[0-2]$' }
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* 处理字段定义,将 @ 符号引用替换为实际字段定义
|
|
35
35
|
*/
|
|
36
|
-
function processFields(fields: Record<string, any
|
|
36
|
+
function processFields(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any> {
|
|
37
37
|
if (!fields || typeof fields !== 'object') return fields;
|
|
38
38
|
|
|
39
39
|
const processed: Record<string, any> = {};
|
|
@@ -43,8 +43,9 @@ function processFields(fields: Record<string, any>): Record<string, any> {
|
|
|
43
43
|
if (PRESET_FIELDS[value]) {
|
|
44
44
|
processed[key] = PRESET_FIELDS[value];
|
|
45
45
|
} else {
|
|
46
|
-
//
|
|
47
|
-
|
|
46
|
+
// 未找到预定义字段,抛出错误
|
|
47
|
+
const validKeys = Object.keys(PRESET_FIELDS).join(', ');
|
|
48
|
+
throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
|
|
48
49
|
}
|
|
49
50
|
} else {
|
|
50
51
|
// 普通字段定义,保持原样
|
|
@@ -110,13 +111,14 @@ export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
|
|
|
110
111
|
// 设置默认值
|
|
111
112
|
const methodStr = (api.method || 'POST').toUpperCase();
|
|
112
113
|
api.auth = api.auth !== undefined ? api.auth : true;
|
|
113
|
-
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
114
|
-
api.fields = processFields(api.fields || {});
|
|
115
|
-
api.required = api.required || [];
|
|
116
114
|
|
|
117
|
-
//
|
|
115
|
+
// 构建路由路径(用于错误提示)
|
|
118
116
|
const routePath = `/api${apiFile.routePrefix}${apiFile.relativePath}`;
|
|
119
117
|
|
|
118
|
+
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
119
|
+
api.fields = processFields(api.fields || {}, api.name || apiFile.relativePath, routePath);
|
|
120
|
+
api.required = api.required || [];
|
|
121
|
+
|
|
120
122
|
// 支持逗号分隔的多方法,拆分后分别注册
|
|
121
123
|
const methods = methodStr
|
|
122
124
|
.split(',')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.38",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -65,16 +65,16 @@
|
|
|
65
65
|
"bun": ">=1.3.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"befly-shared": "^1.2.
|
|
68
|
+
"befly-shared": "^1.2.8",
|
|
69
69
|
"chalk": "^5.6.2",
|
|
70
|
-
"es-toolkit": "^1.
|
|
70
|
+
"es-toolkit": "^1.43.0",
|
|
71
71
|
"fast-jwt": "^6.1.0",
|
|
72
72
|
"fast-xml-parser": "^5.3.3",
|
|
73
73
|
"pathe": "^2.0.3",
|
|
74
74
|
"pino": "^10.1.0",
|
|
75
75
|
"pino-roll": "^4.0.0"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "8cb371928961c63d6c3c416c6dd667f8fe285376",
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"typescript": "^5.9.3"
|
|
80
80
|
}
|
package/plugins/tool.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// 类型导入
|
|
7
7
|
import type { Plugin } from '../types/plugin.js';
|
|
8
|
+
import type { RequestContext } from '../types/context.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 成功响应
|
|
@@ -38,11 +39,86 @@ export function No(msg: string, data: any = null, other: Record<string, any> = {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 响应选项
|
|
44
|
+
*/
|
|
45
|
+
interface ResponseOptions {
|
|
46
|
+
/** HTTP 状态码,默认 200 */
|
|
47
|
+
status?: number;
|
|
48
|
+
/** Content-Type,默认根据 data 类型自动判断 */
|
|
49
|
+
contentType?: string;
|
|
50
|
+
/** 额外的响应头 */
|
|
51
|
+
headers?: Record<string, string>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 统一响应函数
|
|
56
|
+
*
|
|
57
|
+
* 自动识别数据类型并设置正确的 Content-Type:
|
|
58
|
+
* - 对象 → application/json
|
|
59
|
+
* - 字符串 → text/plain
|
|
60
|
+
* - 可通过 options.contentType 手动指定
|
|
61
|
+
*
|
|
62
|
+
* @param ctx 请求上下文
|
|
63
|
+
* @param data 响应数据(对象或字符串)
|
|
64
|
+
* @param options 响应选项
|
|
65
|
+
* @returns Response 对象
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // JSON 响应(自动)
|
|
69
|
+
* return Raw(ctx, { code: 'SUCCESS', message: '成功' });
|
|
70
|
+
*
|
|
71
|
+
* // 纯文本响应(自动)
|
|
72
|
+
* return Raw(ctx, 'success');
|
|
73
|
+
*
|
|
74
|
+
* // XML 响应(手动指定)
|
|
75
|
+
* return Raw(ctx, xmlString, { contentType: 'application/xml' });
|
|
76
|
+
*
|
|
77
|
+
* // 自定义状态码和额外头
|
|
78
|
+
* return Raw(ctx, { error: 'Not Found' }, {
|
|
79
|
+
* status: 404,
|
|
80
|
+
* headers: { 'X-Custom': 'value' }
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
export function Raw(ctx: RequestContext, data: Record<string, any> | string, options: ResponseOptions = {}): Response {
|
|
84
|
+
const { status = 200, contentType, headers = {} } = options;
|
|
85
|
+
|
|
86
|
+
// 自动判断 Content-Type
|
|
87
|
+
let finalContentType = contentType;
|
|
88
|
+
let body: string;
|
|
89
|
+
|
|
90
|
+
if (typeof data === 'string') {
|
|
91
|
+
// 字符串类型
|
|
92
|
+
body = data;
|
|
93
|
+
if (!finalContentType) {
|
|
94
|
+
// 自动判断:XML 或纯文本
|
|
95
|
+
finalContentType = data.trim().startsWith('<') ? 'application/xml' : 'text/plain';
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// 对象类型,JSON 序列化
|
|
99
|
+
body = JSON.stringify(data);
|
|
100
|
+
finalContentType = finalContentType || 'application/json';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 合并响应头
|
|
104
|
+
const responseHeaders = {
|
|
105
|
+
...ctx.corsHeaders,
|
|
106
|
+
'Content-Type': finalContentType,
|
|
107
|
+
...headers
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return new Response(body, {
|
|
111
|
+
status: status,
|
|
112
|
+
headers: responseHeaders
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
41
116
|
const plugin: Plugin = {
|
|
42
117
|
handler: () => {
|
|
43
118
|
return {
|
|
44
119
|
Yes: Yes,
|
|
45
|
-
No: No
|
|
120
|
+
No: No,
|
|
121
|
+
Raw: Raw
|
|
46
122
|
};
|
|
47
123
|
}
|
|
48
124
|
};
|
package/sync/syncApi.ts
CHANGED
|
@@ -199,7 +199,6 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
199
199
|
async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<void> {
|
|
200
200
|
const allRecords = await helper.getAll({
|
|
201
201
|
table: 'addon_admin_api',
|
|
202
|
-
fields: ['id', 'addonName', 'path', 'method'],
|
|
203
202
|
where: { state$gte: 0 }
|
|
204
203
|
});
|
|
205
204
|
|
|
@@ -250,8 +249,7 @@ export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void
|
|
|
250
249
|
try {
|
|
251
250
|
const apiList = await helper.getAll({
|
|
252
251
|
table: 'addon_admin_api',
|
|
253
|
-
|
|
254
|
-
orderBy: ['addonName#ASC', 'path#ASC']
|
|
252
|
+
orderBy: ['id#ASC']
|
|
255
253
|
});
|
|
256
254
|
|
|
257
255
|
const redisHelper = new RedisHelper();
|
package/sync/syncDev.ts
CHANGED
package/sync/syncMenu.ts
CHANGED
|
@@ -231,8 +231,7 @@ async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, exi
|
|
|
231
231
|
*/
|
|
232
232
|
async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
233
233
|
const allExistingMenus = await helper.getAll({
|
|
234
|
-
table: 'addon_admin_menu'
|
|
235
|
-
fields: ['id', 'pid', 'name', 'path', 'sort']
|
|
234
|
+
table: 'addon_admin_menu'
|
|
236
235
|
});
|
|
237
236
|
const existingMenuMap = new Map<string, any>();
|
|
238
237
|
for (const menu of allExistingMenus.lists) {
|
|
@@ -368,7 +367,6 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
368
367
|
// 7. 获取最终菜单数据(用于缓存)
|
|
369
368
|
const allMenusData = await helper.getAll({
|
|
370
369
|
table: 'addon_admin_menu',
|
|
371
|
-
fields: ['id', 'pid', 'name', 'path', 'sort'],
|
|
372
370
|
orderBy: ['sort#ASC', 'id#ASC']
|
|
373
371
|
});
|
|
374
372
|
|