befly 3.8.29 → 3.8.30
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 +8 -6
- package/checks/checkApi.ts +2 -1
- package/checks/checkTable.ts +3 -2
- package/hooks/parser.ts +5 -3
- package/hooks/permission.ts +12 -5
- package/lib/cacheHelper.ts +76 -62
- package/lib/connect.ts +8 -35
- package/lib/dbHelper.ts +14 -11
- package/lib/jwt.ts +58 -437
- package/lib/logger.ts +76 -197
- package/lib/redisHelper.ts +163 -1
- package/lib/sqlBuilder.ts +2 -1
- package/lib/validator.ts +9 -8
- package/loader/loadApis.ts +4 -7
- package/loader/loadHooks.ts +2 -2
- package/loader/loadPlugins.ts +4 -4
- package/main.ts +4 -17
- package/package.json +9 -8
- package/paths.ts +0 -6
- package/plugins/db.ts +2 -2
- package/plugins/jwt.ts +5 -5
- package/plugins/redis.ts +1 -1
- package/router/api.ts +2 -2
- package/router/static.ts +1 -2
- package/sync/syncAll.ts +2 -2
- package/sync/syncApi.ts +10 -7
- package/sync/syncDb/apply.ts +1 -2
- package/sync/syncDb.ts +6 -10
- package/sync/syncDev.ts +10 -48
- package/sync/syncMenu.ts +11 -8
- package/tests/cacheHelper.test.ts +327 -0
- package/tests/dbHelper-columns.test.ts +5 -20
- package/tests/dbHelper-execute.test.ts +14 -68
- package/tests/fields-redis-cache.test.ts +5 -3
- package/tests/integration.test.ts +15 -26
- package/tests/jwt.test.ts +36 -94
- package/tests/logger.test.ts +32 -34
- package/tests/redisHelper.test.ts +270 -0
- package/tests/redisKeys.test.ts +76 -0
- package/tests/sync-connection.test.ts +0 -6
- package/tests/syncDb-constants.test.ts +12 -12
- package/tests/util.test.ts +5 -1
- package/types/befly.d.ts +2 -15
- package/types/common.d.ts +11 -93
- package/types/database.d.ts +216 -5
- package/types/index.ts +1 -0
- package/types/logger.d.ts +11 -41
- package/types/table.d.ts +213 -0
- package/hooks/_rateLimit.ts +0 -64
- package/lib/regexAliases.ts +0 -59
- package/lib/xml.ts +0 -383
- package/tests/xml.test.ts +0 -101
package/README.md
CHANGED
|
@@ -37,12 +37,14 @@ bunx befly init
|
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
39
|
// main.ts
|
|
40
|
-
import {
|
|
40
|
+
import { Befly } from 'befly';
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const app = new Befly({
|
|
43
|
+
appName: 'My API',
|
|
44
|
+
appPort: 3000
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
await app.start();
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
运行项目:
|
|
@@ -55,7 +57,7 @@ bun run main.ts
|
|
|
55
57
|
|
|
56
58
|
```typescript
|
|
57
59
|
// apis/user/hello.ts
|
|
58
|
-
import type { ApiRoute } from 'befly';
|
|
60
|
+
import type { ApiRoute } from 'befly/types/index';
|
|
59
61
|
|
|
60
62
|
export default {
|
|
61
63
|
name: '问候接口',
|
|
@@ -79,7 +81,7 @@ export default {
|
|
|
79
81
|
### TypeScript 全面支持
|
|
80
82
|
|
|
81
83
|
```typescript
|
|
82
|
-
import type { ApiRoute, BeflyContext } from 'befly';
|
|
84
|
+
import type { ApiRoute, BeflyContext } from 'befly/types/index';
|
|
83
85
|
import type { User } from './types/models';
|
|
84
86
|
|
|
85
87
|
export default {
|
package/checks/checkApi.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
|
|
4
4
|
// 外部依赖
|
|
5
5
|
import { isPlainObject } from 'es-toolkit/compat';
|
|
6
|
-
import {
|
|
6
|
+
import { scanFiles } from 'befly-shared/scanFiles';
|
|
7
|
+
import { scanAddons, getAddonDir, addonDirExists } from 'befly-shared/addonHelper';
|
|
7
8
|
|
|
8
9
|
// 相对导入
|
|
9
10
|
import { Logger } from '../lib/logger.js';
|
package/checks/checkTable.ts
CHANGED
|
@@ -3,14 +3,15 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
|
|
4
4
|
// 外部依赖
|
|
5
5
|
import { basename } from 'pathe';
|
|
6
|
-
import {
|
|
6
|
+
import { scanFiles } from 'befly-shared/scanFiles';
|
|
7
|
+
import { scanAddons, getAddonDir } from 'befly-shared/addonHelper';
|
|
7
8
|
|
|
8
9
|
// 相对导入
|
|
9
10
|
import { Logger } from '../lib/logger.js';
|
|
10
11
|
import { projectTableDir } from '../paths.js';
|
|
11
12
|
|
|
12
13
|
// 类型导入
|
|
13
|
-
import type { FieldDefinition } from '
|
|
14
|
+
import type { FieldDefinition } from 'befly-shared/types';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 表文件信息接口
|
package/hooks/parser.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// 外部依赖
|
|
2
2
|
import { isPlainObject, isEmpty } from 'es-toolkit/compat';
|
|
3
|
-
import { pickFields } from 'befly-
|
|
3
|
+
import { pickFields } from 'befly-shared/pickFields';
|
|
4
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
4
5
|
|
|
5
6
|
// 相对导入
|
|
6
|
-
import { Xml } from '../lib/xml.js';
|
|
7
7
|
import { ErrorResponse } from '../util.js';
|
|
8
8
|
|
|
9
9
|
// 类型导入
|
|
10
10
|
import type { Hook } from '../types/hook.js';
|
|
11
11
|
|
|
12
|
+
const xmlParser = new XMLParser();
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* 请求参数解析钩子
|
|
14
16
|
* - GET 请求:解析 URL 查询参数
|
|
@@ -43,7 +45,7 @@ const hook: Hook = {
|
|
|
43
45
|
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
|
|
44
46
|
// XML 格式
|
|
45
47
|
const text = await ctx.req.text();
|
|
46
|
-
const body =
|
|
48
|
+
const body = xmlParser.parse(text);
|
|
47
49
|
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
48
50
|
ctx.body = pickFields(body, Object.keys(ctx.api.fields));
|
|
49
51
|
} else {
|
package/hooks/permission.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 相对导入
|
|
2
2
|
import { ErrorResponse } from '../util.js';
|
|
3
|
+
import { RedisKeys } from 'befly-shared/redisKeys';
|
|
3
4
|
|
|
4
5
|
// 类型导入
|
|
5
6
|
import type { Hook } from '../types/hook.js';
|
|
@@ -35,11 +36,17 @@ const hook: Hook = {
|
|
|
35
36
|
// 4. 角色权限检查
|
|
36
37
|
let hasPermission = false;
|
|
37
38
|
if (ctx.user.roleCode && befly.redis) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
try {
|
|
40
|
+
// 验证角色权限
|
|
41
|
+
const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
|
|
42
|
+
const roleApisKey = RedisKeys.roleApis(ctx.user.roleCode);
|
|
43
|
+
const isMember = await befly.redis.sismember(roleApisKey, apiPath);
|
|
44
|
+
hasPermission = isMember === 1;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Redis 异常时降级为拒绝访问
|
|
47
|
+
befly.logger.warn({ err: error, route: ctx.route }, 'Redis 权限检查失败');
|
|
48
|
+
hasPermission = false;
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
if (!hasPermission) {
|
package/lib/cacheHelper.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Logger } from './logger.js';
|
|
7
|
+
import { RedisKeys } from 'befly-shared/redisKeys';
|
|
8
|
+
|
|
7
9
|
import type { BeflyContext } from '../types/befly.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -26,29 +28,29 @@ export class CacheHelper {
|
|
|
26
28
|
async cacheApis(): Promise<void> {
|
|
27
29
|
try {
|
|
28
30
|
// 检查表是否存在
|
|
29
|
-
const tableExists = await this.befly.db.tableExists('
|
|
31
|
+
const tableExists = await this.befly.db.tableExists('addon_admin_api');
|
|
30
32
|
if (!tableExists) {
|
|
31
33
|
Logger.warn('⚠️ 接口表不存在,跳过接口缓存');
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
//
|
|
37
|
+
// 从数据库查询所有接口
|
|
36
38
|
const apiList = await this.befly.db.getAll({
|
|
37
|
-
table: '
|
|
39
|
+
table: 'addon_admin_api',
|
|
38
40
|
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
39
41
|
orderBy: ['addonName#ASC', 'path#ASC']
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
// 缓存到 Redis
|
|
43
|
-
const result = await this.befly.redis.setObject(
|
|
45
|
+
const result = await this.befly.redis.setObject(RedisKeys.apisAll(), apiList);
|
|
44
46
|
|
|
45
47
|
if (result === null) {
|
|
46
48
|
Logger.warn('⚠️ 接口缓存失败');
|
|
47
49
|
} else {
|
|
48
|
-
Logger.info(`✅ 已缓存 ${apiList.length} 个接口到 Redis (Key:
|
|
50
|
+
Logger.info(`✅ 已缓存 ${apiList.length} 个接口到 Redis (Key: ${RedisKeys.apisAll()})`);
|
|
49
51
|
}
|
|
50
52
|
} catch (error: any) {
|
|
51
|
-
Logger.error('⚠️
|
|
53
|
+
Logger.error({ err: error }, '⚠️ 接口缓存异常');
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -58,7 +60,7 @@ export class CacheHelper {
|
|
|
58
60
|
async cacheMenus(): Promise<void> {
|
|
59
61
|
try {
|
|
60
62
|
// 检查表是否存在
|
|
61
|
-
const tableExists = await this.befly.db.tableExists('
|
|
63
|
+
const tableExists = await this.befly.db.tableExists('addon_admin_menu');
|
|
62
64
|
if (!tableExists) {
|
|
63
65
|
Logger.warn('⚠️ 菜单表不存在,跳过菜单缓存');
|
|
64
66
|
return;
|
|
@@ -66,86 +68,101 @@ export class CacheHelper {
|
|
|
66
68
|
|
|
67
69
|
// 从数据库查询所有菜单
|
|
68
70
|
const menus = await this.befly.db.getAll({
|
|
69
|
-
table: '
|
|
71
|
+
table: 'addon_admin_menu',
|
|
70
72
|
fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
|
|
71
73
|
orderBy: ['sort#ASC', 'id#ASC']
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
// 缓存到 Redis
|
|
75
|
-
const result = await this.befly.redis.setObject(
|
|
77
|
+
const result = await this.befly.redis.setObject(RedisKeys.menusAll(), menus);
|
|
76
78
|
|
|
77
79
|
if (result === null) {
|
|
78
80
|
Logger.warn('⚠️ 菜单缓存失败');
|
|
79
81
|
} else {
|
|
80
|
-
Logger.info(`✅ 已缓存 ${menus.length} 个菜单到 Redis (Key:
|
|
82
|
+
Logger.info(`✅ 已缓存 ${menus.length} 个菜单到 Redis (Key: ${RedisKeys.menusAll()})`);
|
|
81
83
|
}
|
|
82
84
|
} catch (error: any) {
|
|
83
|
-
|
|
84
|
-
Logger.warn('⚠️ 菜单缓存异常:', errorMessage);
|
|
85
|
+
Logger.warn({ err: error }, '⚠️ 菜单缓存异常');
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* 缓存所有角色的接口权限到 Redis
|
|
91
|
+
* 优化:使用 Promise.all 利用 Bun Redis 自动 pipeline 特性
|
|
90
92
|
*/
|
|
91
93
|
async cacheRolePermissions(): Promise<void> {
|
|
92
94
|
try {
|
|
93
95
|
// 检查表是否存在
|
|
94
|
-
const apiTableExists = await this.befly.db.tableExists('
|
|
95
|
-
const roleTableExists = await this.befly.db.tableExists('
|
|
96
|
+
const apiTableExists = await this.befly.db.tableExists('addon_admin_api');
|
|
97
|
+
const roleTableExists = await this.befly.db.tableExists('addon_admin_role');
|
|
96
98
|
|
|
97
99
|
if (!apiTableExists || !roleTableExists) {
|
|
98
100
|
Logger.warn('⚠️ 接口或角色表不存在,跳过角色权限缓存');
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
//
|
|
103
|
-
const roles = await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// 并行查询角色和接口(利用自动 pipeline)
|
|
105
|
+
const [roles, allApis] = await Promise.all([
|
|
106
|
+
this.befly.db.getAll({
|
|
107
|
+
table: 'addon_admin_role',
|
|
108
|
+
fields: ['id', 'code', 'apis']
|
|
109
|
+
}),
|
|
110
|
+
this.befly.db.getAll({
|
|
111
|
+
table: 'addon_admin_api',
|
|
112
|
+
fields: ['id', 'path', 'method']
|
|
113
|
+
})
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// 构建接口 ID -> 路径的映射(避免重复过滤)
|
|
117
|
+
const apiMap = new Map<number, string>();
|
|
118
|
+
for (const api of allApis) {
|
|
119
|
+
apiMap.set(api.id, `${api.method}${api.path}`);
|
|
120
|
+
}
|
|
107
121
|
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
table: 'core_api',
|
|
111
|
-
fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
|
|
112
|
-
});
|
|
122
|
+
// 收集需要缓存的角色权限
|
|
123
|
+
const cacheOperations: Array<{ roleCode: string; apiPaths: string[] }> = [];
|
|
113
124
|
|
|
114
|
-
// 为每个角色缓存接口权限
|
|
115
|
-
let cachedRoles = 0;
|
|
116
125
|
for (const role of roles) {
|
|
117
126
|
if (!role.apis) continue;
|
|
118
127
|
|
|
119
|
-
// 解析角色的接口 ID
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
// 解析角色的接口 ID 列表并映射到路径
|
|
129
|
+
const apiPaths: string[] = [];
|
|
130
|
+
const apiIds = role.apis.split(',');
|
|
131
|
+
|
|
132
|
+
for (const idStr of apiIds) {
|
|
133
|
+
const id = parseInt(idStr.trim());
|
|
134
|
+
if (!isNaN(id)) {
|
|
135
|
+
const path = apiMap.get(id);
|
|
136
|
+
if (path) {
|
|
137
|
+
apiPaths.push(path);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
127
141
|
|
|
128
|
-
if (
|
|
142
|
+
if (apiPaths.length > 0) {
|
|
143
|
+
cacheOperations.push({ roleCode: role.code, apiPaths: apiPaths });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
if (cacheOperations.length === 0) {
|
|
148
|
+
Logger.info('✅ 没有需要缓存的角色权限');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
132
151
|
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
// 批量删除旧缓存(利用自动 pipeline)
|
|
153
|
+
const deletePromises = cacheOperations.map((op) => this.befly.redis.del(RedisKeys.roleApis(op.roleCode)));
|
|
154
|
+
await Promise.all(deletePromises);
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
// 批量添加新缓存(利用自动 pipeline)
|
|
157
|
+
const addPromises = cacheOperations.map((op) => this.befly.redis.sadd(RedisKeys.roleApis(op.roleCode), op.apiPaths));
|
|
158
|
+
const results = await Promise.all(addPromises);
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Logger.debug(` └ 角色 ${role.code}: ${result} 个接口`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
160
|
+
// 统计成功缓存的角色数
|
|
161
|
+
const cachedRoles = results.filter((r) => r > 0).length;
|
|
144
162
|
|
|
145
|
-
Logger.info(`✅ 已缓存 ${cachedRoles}
|
|
163
|
+
Logger.info(`✅ 已缓存 ${cachedRoles} 个角色的接口权限(共 ${cacheOperations.reduce((sum, op) => sum + op.apiPaths.length, 0)} 个接口)`);
|
|
146
164
|
} catch (error: any) {
|
|
147
|
-
|
|
148
|
-
Logger.warn('⚠️ 角色权限缓存异常:', errorMessage);
|
|
165
|
+
Logger.warn({ err: error }, '⚠️ 角色权限缓存异常');
|
|
149
166
|
}
|
|
150
167
|
}
|
|
151
168
|
|
|
@@ -169,10 +186,10 @@ export class CacheHelper {
|
|
|
169
186
|
*/
|
|
170
187
|
async getApis(): Promise<any[]> {
|
|
171
188
|
try {
|
|
172
|
-
const apis = await this.befly.redis.getObject<any[]>(
|
|
189
|
+
const apis = await this.befly.redis.getObject<any[]>(RedisKeys.apisAll());
|
|
173
190
|
return apis || [];
|
|
174
191
|
} catch (error: any) {
|
|
175
|
-
Logger.error('
|
|
192
|
+
Logger.error({ err: error }, '获取接口缓存失败');
|
|
176
193
|
return [];
|
|
177
194
|
}
|
|
178
195
|
}
|
|
@@ -183,10 +200,10 @@ export class CacheHelper {
|
|
|
183
200
|
*/
|
|
184
201
|
async getMenus(): Promise<any[]> {
|
|
185
202
|
try {
|
|
186
|
-
const menus = await this.befly.redis.getObject<any[]>(
|
|
203
|
+
const menus = await this.befly.redis.getObject<any[]>(RedisKeys.menusAll());
|
|
187
204
|
return menus || [];
|
|
188
205
|
} catch (error: any) {
|
|
189
|
-
Logger.error('
|
|
206
|
+
Logger.error({ err: error }, '获取菜单缓存失败');
|
|
190
207
|
return [];
|
|
191
208
|
}
|
|
192
209
|
}
|
|
@@ -198,11 +215,10 @@ export class CacheHelper {
|
|
|
198
215
|
*/
|
|
199
216
|
async getRolePermissions(roleCode: string): Promise<string[]> {
|
|
200
217
|
try {
|
|
201
|
-
const
|
|
202
|
-
const permissions = await this.befly.redis.smembers(redisKey);
|
|
218
|
+
const permissions = await this.befly.redis.smembers(RedisKeys.roleApis(roleCode));
|
|
203
219
|
return permissions || [];
|
|
204
220
|
} catch (error: any) {
|
|
205
|
-
Logger.error(
|
|
221
|
+
Logger.error({ err: error, roleCode: roleCode }, '获取角色权限缓存失败');
|
|
206
222
|
return [];
|
|
207
223
|
}
|
|
208
224
|
}
|
|
@@ -215,11 +231,10 @@ export class CacheHelper {
|
|
|
215
231
|
*/
|
|
216
232
|
async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
|
|
217
233
|
try {
|
|
218
|
-
const
|
|
219
|
-
const result = await this.befly.redis.sismember(redisKey, apiPath);
|
|
234
|
+
const result = await this.befly.redis.sismember(RedisKeys.roleApis(roleCode), apiPath);
|
|
220
235
|
return result === 1;
|
|
221
236
|
} catch (error: any) {
|
|
222
|
-
Logger.error(
|
|
237
|
+
Logger.error({ err: error, roleCode: roleCode }, '检查角色权限失败');
|
|
223
238
|
return false;
|
|
224
239
|
}
|
|
225
240
|
}
|
|
@@ -231,15 +246,14 @@ export class CacheHelper {
|
|
|
231
246
|
*/
|
|
232
247
|
async deleteRolePermissions(roleCode: string): Promise<boolean> {
|
|
233
248
|
try {
|
|
234
|
-
const
|
|
235
|
-
const result = await this.befly.redis.del(redisKey);
|
|
249
|
+
const result = await this.befly.redis.del(RedisKeys.roleApis(roleCode));
|
|
236
250
|
if (result > 0) {
|
|
237
251
|
Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
|
|
238
252
|
return true;
|
|
239
253
|
}
|
|
240
254
|
return false;
|
|
241
255
|
} catch (error: any) {
|
|
242
|
-
Logger.error(
|
|
256
|
+
Logger.error({ err: error, roleCode: roleCode }, '删除角色权限缓存失败');
|
|
243
257
|
return false;
|
|
244
258
|
}
|
|
245
259
|
}
|
package/lib/connect.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { SQL, RedisClient } from 'bun';
|
|
7
|
+
|
|
7
8
|
import { Logger } from './logger.js';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import type { BeflyContext, BeflyOptions, DatabaseConfig, RedisConfig } from '../types/befly.js';
|
|
9
|
+
|
|
10
|
+
import type { BeflyOptions, DatabaseConfig, RedisConfig } from '../types/befly.js';
|
|
11
11
|
import type { SqlClientOptions } from '../types/database.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -17,7 +17,6 @@ import type { SqlClientOptions } from '../types/database.js';
|
|
|
17
17
|
export class Connect {
|
|
18
18
|
private static sqlClient: SQL | null = null;
|
|
19
19
|
private static redisClient: RedisClient | null = null;
|
|
20
|
-
private static dbHelper: DbHelper | null = null;
|
|
21
20
|
|
|
22
21
|
// 连接统计信息
|
|
23
22
|
private static sqlConnectedAt: number | null = null;
|
|
@@ -95,7 +94,7 @@ export class Connect {
|
|
|
95
94
|
this.sqlPoolMax = config.poolMax ?? 1;
|
|
96
95
|
return sql;
|
|
97
96
|
} catch (error: any) {
|
|
98
|
-
Logger.error('[Connect] SQL 连接失败'
|
|
97
|
+
Logger.error({ err: error }, '[Connect] SQL 连接失败');
|
|
99
98
|
try {
|
|
100
99
|
await sql?.close();
|
|
101
100
|
} catch (cleanupError) {}
|
|
@@ -112,15 +111,11 @@ export class Connect {
|
|
|
112
111
|
try {
|
|
113
112
|
await this.sqlClient.close();
|
|
114
113
|
} catch (error: any) {
|
|
115
|
-
Logger.error('[Connect] 关闭 SQL 连接时出错'
|
|
114
|
+
Logger.error({ err: error }, '[Connect] 关闭 SQL 连接时出错');
|
|
116
115
|
}
|
|
117
116
|
this.sqlClient = null;
|
|
118
117
|
this.sqlConnectedAt = null;
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
if (this.dbHelper) {
|
|
122
|
-
this.dbHelper = null;
|
|
123
|
-
}
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
/**
|
|
@@ -134,27 +129,6 @@ export class Connect {
|
|
|
134
129
|
return this.sqlClient;
|
|
135
130
|
}
|
|
136
131
|
|
|
137
|
-
/**
|
|
138
|
-
* 获取 DbHelper 实例
|
|
139
|
-
* @throws 如果未连接则抛出错误
|
|
140
|
-
*/
|
|
141
|
-
static getDbHelper(befly?: BeflyContext): DbHelper {
|
|
142
|
-
if (!this.dbHelper) {
|
|
143
|
-
if (!this.sqlClient) {
|
|
144
|
-
throw new Error('SQL 客户端未连接,请先调用 Connect.connectSql()');
|
|
145
|
-
}
|
|
146
|
-
// 创建临时 befly 上下文(仅用于 DbHelper)
|
|
147
|
-
const ctx = befly || {
|
|
148
|
-
redis: new RedisHelper(),
|
|
149
|
-
db: null as any,
|
|
150
|
-
tool: null as any,
|
|
151
|
-
logger: null as any
|
|
152
|
-
};
|
|
153
|
-
this.dbHelper = new DbHelper(ctx, this.sqlClient);
|
|
154
|
-
}
|
|
155
|
-
return this.dbHelper;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
132
|
// ========================================
|
|
159
133
|
// Redis 连接管理
|
|
160
134
|
// ========================================
|
|
@@ -197,7 +171,7 @@ export class Connect {
|
|
|
197
171
|
this.redisConnectedAt = Date.now();
|
|
198
172
|
return redis;
|
|
199
173
|
} catch (error: any) {
|
|
200
|
-
Logger.error('[Connect] Redis 连接失败'
|
|
174
|
+
Logger.error({ err: error }, '[Connect] Redis 连接失败');
|
|
201
175
|
throw new Error(`Redis 连接失败: ${error.message}`);
|
|
202
176
|
}
|
|
203
177
|
}
|
|
@@ -211,7 +185,7 @@ export class Connect {
|
|
|
211
185
|
this.redisClient.close();
|
|
212
186
|
this.redisConnectedAt = null;
|
|
213
187
|
} catch (error: any) {
|
|
214
|
-
Logger.error('[Connect] 关闭 Redis 连接时出错'
|
|
188
|
+
Logger.error({ err: error }, '[Connect] 关闭 Redis 连接时出错');
|
|
215
189
|
}
|
|
216
190
|
this.redisClient = null;
|
|
217
191
|
}
|
|
@@ -250,7 +224,7 @@ export class Connect {
|
|
|
250
224
|
const redisConfig = config?.redis || {};
|
|
251
225
|
await this.connectRedis(redisConfig);
|
|
252
226
|
} catch (error: any) {
|
|
253
|
-
Logger.error('数据库初始化失败'
|
|
227
|
+
Logger.error({ err: error }, '数据库初始化失败');
|
|
254
228
|
await this.disconnect();
|
|
255
229
|
throw error;
|
|
256
230
|
}
|
|
@@ -330,7 +304,6 @@ export class Connect {
|
|
|
330
304
|
static __reset(): void {
|
|
331
305
|
this.sqlClient = null;
|
|
332
306
|
this.redisClient = null;
|
|
333
|
-
this.dbHelper = null;
|
|
334
307
|
this.sqlConnectedAt = null;
|
|
335
308
|
this.redisConnectedAt = null;
|
|
336
309
|
this.sqlPoolMax = 1;
|
package/lib/dbHelper.ts
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { snakeCase } from 'es-toolkit/string';
|
|
7
7
|
import { SqlBuilder } from './sqlBuilder.js';
|
|
8
|
-
import { keysToCamel
|
|
9
|
-
import {
|
|
8
|
+
import { keysToCamel } from 'befly-shared/keysToCamel';
|
|
9
|
+
import { arrayKeysToCamel } from 'befly-shared/arrayKeysToCamel';
|
|
10
|
+
import { keysToSnake } from 'befly-shared/keysToSnake';
|
|
11
|
+
import { fieldClear } from 'befly-shared/fieldClear';
|
|
12
|
+
import { RedisTTL, RedisKeys } from 'befly-shared/redisKeys';
|
|
13
|
+
import { Logger } from './logger.js';
|
|
10
14
|
import type { WhereConditions } from '../types/common.js';
|
|
11
15
|
import type { BeflyContext } from '../types/befly.js';
|
|
12
16
|
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from '../types/database.js';
|
|
@@ -81,7 +85,7 @@ export class DbHelper {
|
|
|
81
85
|
*/
|
|
82
86
|
private async getTableColumns(table: string): Promise<string[]> {
|
|
83
87
|
// 1. 先查 Redis 缓存
|
|
84
|
-
const cacheKey =
|
|
88
|
+
const cacheKey = RedisKeys.tableColumns(table);
|
|
85
89
|
let columns = await this.befly.redis.getObject<string[]>(cacheKey);
|
|
86
90
|
|
|
87
91
|
if (columns && columns.length > 0) {
|
|
@@ -98,8 +102,8 @@ export class DbHelper {
|
|
|
98
102
|
|
|
99
103
|
columns = result.map((row: any) => row.Field);
|
|
100
104
|
|
|
101
|
-
// 3. 写入 Redis
|
|
102
|
-
await this.befly.redis.setObject(cacheKey, columns,
|
|
105
|
+
// 3. 写入 Redis 缓存
|
|
106
|
+
await this.befly.redis.setObject(cacheKey, columns, RedisTTL.tableColumns);
|
|
103
107
|
|
|
104
108
|
return columns;
|
|
105
109
|
}
|
|
@@ -507,12 +511,12 @@ export class DbHelper {
|
|
|
507
511
|
|
|
508
512
|
// 警告日志:返回数据超过警告阈值
|
|
509
513
|
if (result.length >= WARNING_LIMIT) {
|
|
510
|
-
Logger.warn(
|
|
514
|
+
Logger.warn({ table: options.table, count: result.length }, 'getAll 返回数据过多,建议使用 getList 分页查询');
|
|
511
515
|
}
|
|
512
516
|
|
|
513
517
|
// 如果达到上限,额外警告
|
|
514
518
|
if (result.length >= MAX_LIMIT) {
|
|
515
|
-
Logger.warn(
|
|
519
|
+
Logger.warn({ table: options.table, limit: MAX_LIMIT }, 'getAll 达到最大限制,可能还有更多数据');
|
|
516
520
|
}
|
|
517
521
|
|
|
518
522
|
// 字段名转换:下划线 → 小驼峰
|
|
@@ -625,8 +629,7 @@ export class DbHelper {
|
|
|
625
629
|
await this.executeWithConn(sql, params);
|
|
626
630
|
return ids;
|
|
627
631
|
} catch (error: any) {
|
|
628
|
-
|
|
629
|
-
Logger.error(`表 \`${table}\` 批量插入失败`, error);
|
|
632
|
+
Logger.error({ err: error, table: table }, '批量插入失败');
|
|
630
633
|
throw error;
|
|
631
634
|
}
|
|
632
635
|
}
|
|
@@ -759,7 +762,7 @@ export class DbHelper {
|
|
|
759
762
|
await conn.query('COMMIT');
|
|
760
763
|
committed = true;
|
|
761
764
|
} catch (commitError: any) {
|
|
762
|
-
Logger.error('事务提交失败,正在回滚'
|
|
765
|
+
Logger.error({ err: commitError }, '事务提交失败,正在回滚');
|
|
763
766
|
await conn.query('ROLLBACK');
|
|
764
767
|
throw new Error(`事务提交失败: ${commitError.message}`);
|
|
765
768
|
}
|
|
@@ -772,7 +775,7 @@ export class DbHelper {
|
|
|
772
775
|
await conn.query('ROLLBACK');
|
|
773
776
|
Logger.warn('事务已回滚');
|
|
774
777
|
} catch (rollbackError: any) {
|
|
775
|
-
Logger.error('事务回滚失败'
|
|
778
|
+
Logger.error({ err: rollbackError }, '事务回滚失败');
|
|
776
779
|
}
|
|
777
780
|
}
|
|
778
781
|
throw error;
|