befly 3.2.0 → 3.3.0
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/bin/index.ts +138 -0
- package/checks/conflict.ts +35 -25
- package/checks/table.ts +6 -6
- package/commands/addon.ts +57 -0
- package/commands/build.ts +74 -0
- package/commands/dev.ts +94 -0
- package/commands/index.ts +252 -0
- package/commands/script.ts +308 -0
- package/commands/start.ts +80 -0
- package/commands/syncApi.ts +328 -0
- package/{scripts → commands}/syncDb/apply.ts +2 -2
- package/{scripts → commands}/syncDb/constants.ts +13 -7
- package/{scripts → commands}/syncDb/ddl.ts +7 -5
- package/{scripts → commands}/syncDb/helpers.ts +18 -18
- package/{scripts → commands}/syncDb/index.ts +37 -23
- package/{scripts → commands}/syncDb/sqlite.ts +1 -1
- package/{scripts → commands}/syncDb/state.ts +10 -4
- package/{scripts → commands}/syncDb/table.ts +7 -7
- package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
- package/{scripts → commands}/syncDb/types.ts +5 -5
- package/{scripts → commands}/syncDb/version.ts +1 -1
- package/commands/syncDb.ts +35 -0
- package/commands/syncDev.ts +174 -0
- package/commands/syncMenu.ts +368 -0
- package/config/env.ts +4 -4
- package/config/menu.json +67 -0
- package/{utils/crypto.ts → lib/cipher.ts} +16 -67
- package/lib/database.ts +296 -0
- package/{utils → lib}/dbHelper.ts +102 -56
- package/{utils → lib}/jwt.ts +124 -151
- package/{utils → lib}/logger.ts +47 -24
- package/lib/middleware.ts +271 -0
- package/{utils → lib}/redisHelper.ts +4 -4
- package/{utils/validate.ts → lib/validator.ts} +101 -78
- package/lifecycle/bootstrap.ts +63 -0
- package/lifecycle/checker.ts +165 -0
- package/lifecycle/cluster.ts +241 -0
- package/lifecycle/lifecycle.ts +139 -0
- package/lifecycle/loader.ts +513 -0
- package/main.ts +14 -12
- package/package.json +21 -9
- package/paths.ts +34 -0
- package/plugins/cache.ts +187 -0
- package/plugins/db.ts +4 -4
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/router/api.ts +155 -0
- package/router/root.ts +53 -0
- package/router/static.ts +76 -0
- package/types/api.d.ts +0 -36
- package/types/befly.d.ts +8 -6
- package/types/common.d.ts +1 -1
- package/types/context.d.ts +3 -3
- package/types/util.d.ts +45 -0
- package/util.ts +301 -0
- package/config/fields.ts +0 -55
- package/config/regexAliases.ts +0 -51
- package/config/reserved.ts +0 -96
- package/scripts/syncDb/tests/constants.test.ts +0 -105
- package/scripts/syncDb/tests/ddl.test.ts +0 -134
- package/scripts/syncDb/tests/helpers.test.ts +0 -70
- package/scripts/syncDb.ts +0 -10
- package/types/index.d.ts +0 -450
- package/types/index.ts +0 -438
- package/types/validator.ts +0 -43
- package/utils/colors.ts +0 -221
- package/utils/database.ts +0 -348
- package/utils/helper.ts +0 -812
- package/utils/index.ts +0 -33
- package/utils/requestContext.ts +0 -167
- /package/{scripts → commands}/syncDb/schema.ts +0 -0
- /package/{utils → lib}/sqlBuilder.ts +0 -0
- /package/{utils → lib}/xml.ts +0 -0
package/lib/database.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库连接管理器
|
|
3
|
+
* 统一管理 SQL 和 Redis 连接
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SQL, RedisClient } from 'bun';
|
|
7
|
+
import { Env } from '../config/env.js';
|
|
8
|
+
import { Logger } from './logger.js';
|
|
9
|
+
import { DbHelper } from './dbHelper.js';
|
|
10
|
+
import { RedisHelper } from './redisHelper.js';
|
|
11
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
12
|
+
import type { SqlClientOptions } from '../types/database.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 数据库连接管理器
|
|
16
|
+
* 使用静态方法管理全局单例连接
|
|
17
|
+
*/
|
|
18
|
+
export class Database {
|
|
19
|
+
private static sqlClient: SQL | null = null;
|
|
20
|
+
private static redisClient: RedisClient | null = null;
|
|
21
|
+
private static dbHelper: DbHelper | null = null;
|
|
22
|
+
|
|
23
|
+
// ========================================
|
|
24
|
+
// SQL 连接管理
|
|
25
|
+
// ========================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 连接 SQL 数据库
|
|
29
|
+
* @param options - SQL 客户端配置选项
|
|
30
|
+
* @returns SQL 客户端实例
|
|
31
|
+
*/
|
|
32
|
+
static async connectSql(options: SqlClientOptions = {}): Promise<SQL> {
|
|
33
|
+
// 构建数据库连接字符串
|
|
34
|
+
const type = Env.DB_TYPE || '';
|
|
35
|
+
const host = Env.DB_HOST || '';
|
|
36
|
+
const port = Env.DB_PORT;
|
|
37
|
+
const user = encodeURIComponent(Env.DB_USER || '');
|
|
38
|
+
const password = encodeURIComponent(Env.DB_PASS || '');
|
|
39
|
+
const database = encodeURIComponent(Env.DB_NAME || '');
|
|
40
|
+
|
|
41
|
+
let finalUrl: string;
|
|
42
|
+
if (type === 'sqlite') {
|
|
43
|
+
finalUrl = database;
|
|
44
|
+
} else {
|
|
45
|
+
if (!host || !database) {
|
|
46
|
+
throw new Error('数据库配置不完整,请检查环境变量');
|
|
47
|
+
}
|
|
48
|
+
finalUrl = `${type}://${user}:${password}@${host}:${port}/${database}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let sql: SQL;
|
|
52
|
+
|
|
53
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
54
|
+
sql = new SQL(finalUrl);
|
|
55
|
+
} else {
|
|
56
|
+
sql = new SQL({
|
|
57
|
+
url: finalUrl,
|
|
58
|
+
max: options.max ?? 1,
|
|
59
|
+
bigint: false,
|
|
60
|
+
...options
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const timeout = options.connectionTimeout ?? 5000;
|
|
66
|
+
|
|
67
|
+
const healthCheckPromise = (async () => {
|
|
68
|
+
let version = '';
|
|
69
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
70
|
+
const v = await sql`SELECT sqlite_version() AS version`;
|
|
71
|
+
version = v?.[0]?.version;
|
|
72
|
+
} else if (Env.DB_TYPE === 'postgresql' || Env.DB_TYPE === 'postgres') {
|
|
73
|
+
const v = await sql`SELECT version() AS version`;
|
|
74
|
+
version = v?.[0]?.version;
|
|
75
|
+
} else {
|
|
76
|
+
const v = await sql`SELECT VERSION() AS version`;
|
|
77
|
+
version = v?.[0]?.version;
|
|
78
|
+
}
|
|
79
|
+
return version;
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
reject(new Error(`数据库连接超时 (${timeout}ms)`));
|
|
85
|
+
}, timeout);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const version = await Promise.race([healthCheckPromise, timeoutPromise]);
|
|
89
|
+
|
|
90
|
+
Logger.info(`数据库连接成功,version: ${version}`);
|
|
91
|
+
|
|
92
|
+
this.sqlClient = sql;
|
|
93
|
+
return sql;
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
Logger.error('数据库连接测试失败', error);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await sql.close();
|
|
99
|
+
} catch (cleanupError) {}
|
|
100
|
+
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 断开 SQL 连接
|
|
107
|
+
*/
|
|
108
|
+
static async disconnectSql(): Promise<void> {
|
|
109
|
+
if (this.sqlClient) {
|
|
110
|
+
try {
|
|
111
|
+
await this.sqlClient.close();
|
|
112
|
+
Logger.info('SQL 连接已关闭');
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
Logger.warn('关闭 SQL 连接时出错:', error.message);
|
|
115
|
+
}
|
|
116
|
+
this.sqlClient = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.dbHelper) {
|
|
120
|
+
this.dbHelper = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取 SQL 客户端实例
|
|
126
|
+
* @throws 如果未连接则抛出错误
|
|
127
|
+
*/
|
|
128
|
+
static getSql(): SQL {
|
|
129
|
+
if (!this.sqlClient) {
|
|
130
|
+
throw new Error('SQL 客户端未连接,请先调用 Database.connectSql()');
|
|
131
|
+
}
|
|
132
|
+
return this.sqlClient;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取 DbHelper 实例
|
|
137
|
+
* @throws 如果未连接则抛出错误
|
|
138
|
+
*/
|
|
139
|
+
static getDbHelper(befly?: BeflyContext): DbHelper {
|
|
140
|
+
if (!this.dbHelper) {
|
|
141
|
+
if (!this.sqlClient) {
|
|
142
|
+
throw new Error('SQL 客户端未连接,请先调用 Database.connectSql()');
|
|
143
|
+
}
|
|
144
|
+
// 创建临时 befly 上下文(仅用于 DbHelper)
|
|
145
|
+
const ctx = befly || {
|
|
146
|
+
redis: RedisHelper,
|
|
147
|
+
db: null as any,
|
|
148
|
+
tool: null as any,
|
|
149
|
+
logger: null as any
|
|
150
|
+
};
|
|
151
|
+
this.dbHelper = new DbHelper(ctx, this.sqlClient);
|
|
152
|
+
}
|
|
153
|
+
return this.dbHelper;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ========================================
|
|
157
|
+
// Redis 连接管理
|
|
158
|
+
// ========================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 连接 Redis
|
|
162
|
+
* @returns Redis 客户端实例
|
|
163
|
+
*/
|
|
164
|
+
static async connectRedis(): Promise<RedisClient> {
|
|
165
|
+
try {
|
|
166
|
+
// 构建 Redis URL
|
|
167
|
+
const { REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB } = Env;
|
|
168
|
+
|
|
169
|
+
let auth = '';
|
|
170
|
+
if (REDIS_USERNAME && REDIS_PASSWORD) {
|
|
171
|
+
auth = `${REDIS_USERNAME}:${REDIS_PASSWORD}@`;
|
|
172
|
+
} else if (REDIS_PASSWORD) {
|
|
173
|
+
auth = `:${REDIS_PASSWORD}@`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const url = `redis://${auth}${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}`;
|
|
177
|
+
|
|
178
|
+
const redis = new RedisClient(url, {
|
|
179
|
+
connectionTimeout: 10000,
|
|
180
|
+
idleTimeout: 0,
|
|
181
|
+
autoReconnect: true,
|
|
182
|
+
maxRetries: 0,
|
|
183
|
+
enableOfflineQueue: true,
|
|
184
|
+
enableAutoPipelining: true
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await redis.ping();
|
|
188
|
+
Logger.info('Redis 连接成功');
|
|
189
|
+
|
|
190
|
+
this.redisClient = redis;
|
|
191
|
+
return redis;
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
Logger.error('Redis 连接失败', error);
|
|
194
|
+
throw new Error(`Redis 连接失败: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 断开 Redis 连接
|
|
200
|
+
*/
|
|
201
|
+
static async disconnectRedis(): Promise<void> {
|
|
202
|
+
if (this.redisClient) {
|
|
203
|
+
try {
|
|
204
|
+
this.redisClient.close();
|
|
205
|
+
Logger.info('Redis 连接已关闭');
|
|
206
|
+
} catch (error: any) {
|
|
207
|
+
Logger.warn('关闭 Redis 连接时出错:', error);
|
|
208
|
+
}
|
|
209
|
+
this.redisClient = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 获取 Redis 客户端实例
|
|
215
|
+
* @throws 如果未连接则抛出错误
|
|
216
|
+
*/
|
|
217
|
+
static getRedis(): RedisClient {
|
|
218
|
+
if (!this.redisClient) {
|
|
219
|
+
throw new Error('Redis 客户端未连接,请先调用 Database.connectRedis()');
|
|
220
|
+
}
|
|
221
|
+
return this.redisClient;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ========================================
|
|
225
|
+
// 统一连接管理
|
|
226
|
+
// ========================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 连接所有数据库(SQL + Redis)
|
|
230
|
+
* @param options - 配置选项
|
|
231
|
+
*/
|
|
232
|
+
static async connect(options?: { sql?: SqlClientOptions; redis?: boolean }): Promise<void> {
|
|
233
|
+
try {
|
|
234
|
+
if (options?.sql !== false) {
|
|
235
|
+
Logger.info('正在初始化 SQL 连接...');
|
|
236
|
+
await this.connectSql(options?.sql);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (options?.redis !== false) {
|
|
240
|
+
Logger.info('正在初始化 Redis 连接...');
|
|
241
|
+
await this.connectRedis();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Logger.info('数据库连接初始化完成');
|
|
245
|
+
} catch (error: any) {
|
|
246
|
+
Logger.error('数据库初始化失败', error);
|
|
247
|
+
await this.disconnect();
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 断开所有数据库连接
|
|
254
|
+
*/
|
|
255
|
+
static async disconnect(): Promise<void> {
|
|
256
|
+
await this.disconnectSql();
|
|
257
|
+
await this.disconnectRedis();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 检查连接状态
|
|
262
|
+
*/
|
|
263
|
+
static isConnected(): { sql: boolean; redis: boolean } {
|
|
264
|
+
return {
|
|
265
|
+
sql: this.sqlClient !== null,
|
|
266
|
+
redis: this.redisClient !== null
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ========================================
|
|
271
|
+
// 测试辅助方法
|
|
272
|
+
// ========================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 设置 mock SQL 客户端(仅用于测试)
|
|
276
|
+
*/
|
|
277
|
+
static __setMockSql(mockClient: SQL): void {
|
|
278
|
+
this.sqlClient = mockClient;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 设置 mock Redis 客户端(仅用于测试)
|
|
283
|
+
*/
|
|
284
|
+
static __setMockRedis(mockClient: RedisClient): void {
|
|
285
|
+
this.redisClient = mockClient;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 重置所有连接状态(仅用于测试)
|
|
290
|
+
*/
|
|
291
|
+
static __reset(): void {
|
|
292
|
+
this.sqlClient = null;
|
|
293
|
+
this.redisClient = null;
|
|
294
|
+
this.dbHelper = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 提供数据库 CRUD 操作的封装
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { snakeCase } from 'es-toolkit/string';
|
|
6
7
|
import { SqlBuilder } from './sqlBuilder.js';
|
|
7
|
-
import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear
|
|
8
|
-
import { Logger } from '
|
|
8
|
+
import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear } from '../util.js';
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
9
10
|
import type { WhereConditions } from '../types/common.js';
|
|
10
11
|
import type { BeflyContext } from '../types/befly.js';
|
|
11
12
|
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from '../types/database.js';
|
|
@@ -39,7 +40,7 @@ export class DbHelper {
|
|
|
39
40
|
if (field === '*' || field.includes('(') || field.includes(' ')) {
|
|
40
41
|
return field;
|
|
41
42
|
}
|
|
42
|
-
return
|
|
43
|
+
return snakeCase(field);
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -51,51 +52,10 @@ export class DbHelper {
|
|
|
51
52
|
return orderBy.map((item) => {
|
|
52
53
|
if (typeof item !== 'string' || !item.includes('#')) return item;
|
|
53
54
|
const [field, direction] = item.split('#');
|
|
54
|
-
return `${
|
|
55
|
+
return `${snakeCase(field.trim())}#${direction.trim()}`;
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
* where 条件键名转下划线格式(私有方法,递归处理嵌套)
|
|
60
|
-
*/
|
|
61
|
-
private whereKeysToSnake(where: any): any {
|
|
62
|
-
if (!where || typeof where !== 'object') return where;
|
|
63
|
-
|
|
64
|
-
// 处理数组($or, $and 等)
|
|
65
|
-
if (Array.isArray(where)) {
|
|
66
|
-
return where.map((item) => this.whereKeysToSnake(item));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const result: any = {};
|
|
70
|
-
for (const [key, value] of Object.entries(where)) {
|
|
71
|
-
// 保留 $or, $and 等逻辑操作符
|
|
72
|
-
if (key === '$or' || key === '$and') {
|
|
73
|
-
result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 处理带操作符的字段名(如 userId$gt)
|
|
78
|
-
if (key.includes('$')) {
|
|
79
|
-
const lastDollarIndex = key.lastIndexOf('$');
|
|
80
|
-
const fieldName = key.substring(0, lastDollarIndex);
|
|
81
|
-
const operator = key.substring(lastDollarIndex);
|
|
82
|
-
const snakeKey = toSnakeCase(fieldName) + operator;
|
|
83
|
-
result[snakeKey] = value;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 普通字段:转换键名,递归处理值(支持嵌套对象)
|
|
88
|
-
const snakeKey = toSnakeCase(key);
|
|
89
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
90
|
-
result[snakeKey] = this.whereKeysToSnake(value);
|
|
91
|
-
} else {
|
|
92
|
-
result[snakeKey] = value;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
59
|
/**
|
|
100
60
|
* 统一的查询参数预处理方法
|
|
101
61
|
*/
|
|
@@ -103,7 +63,7 @@ export class DbHelper {
|
|
|
103
63
|
const cleanWhere = this.cleanFields(options.where);
|
|
104
64
|
|
|
105
65
|
return {
|
|
106
|
-
table:
|
|
66
|
+
table: snakeCase(options.table),
|
|
107
67
|
fields: this.fieldsToSnake(options.fields || ['*']),
|
|
108
68
|
where: this.whereKeysToSnake(cleanWhere),
|
|
109
69
|
orderBy: this.orderByToSnake(options.orderBy || []),
|
|
@@ -138,6 +98,92 @@ export class DbHelper {
|
|
|
138
98
|
return fieldClear(data || ({} as T), excludeValues, keepValues);
|
|
139
99
|
}
|
|
140
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 转换数据库 BIGINT 字段为数字类型(私有方法)
|
|
103
|
+
* 当 bigint: false 时,Bun SQL 会将大于 u32 的 BIGINT 返回为字符串,此方法将其转换为 number
|
|
104
|
+
*
|
|
105
|
+
* 转换规则:
|
|
106
|
+
* 1. 白名单中的字段会被转换
|
|
107
|
+
* 2. 所有以 'Id' 或 '_id' 结尾的字段会被自动转换
|
|
108
|
+
* 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
|
|
109
|
+
* 4. 其他字段保持不变
|
|
110
|
+
*/
|
|
111
|
+
private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ['id', 'pid', 'sort']): T[] {
|
|
112
|
+
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
113
|
+
|
|
114
|
+
return arr.map((item) => {
|
|
115
|
+
const converted = { ...item };
|
|
116
|
+
|
|
117
|
+
// 遍历对象的所有字段
|
|
118
|
+
for (const [key, value] of Object.entries(converted)) {
|
|
119
|
+
// 跳过 undefined 和 null
|
|
120
|
+
if (value === undefined || value === null) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 判断是否需要转换:
|
|
125
|
+
// 1. 在白名单中
|
|
126
|
+
// 2. 以 'Id' 结尾(如 userId, roleId, categoryId)
|
|
127
|
+
// 3. 以 '_id' 结尾(如 user_id, role_id)
|
|
128
|
+
// 4. 以 'At' 结尾(如 createdAt, updatedAt)
|
|
129
|
+
// 5. 以 '_at' 结尾(如 created_at, updated_at)
|
|
130
|
+
const shouldConvert = fields.includes(key) || key.endsWith('Id') || key.endsWith('_id') || key.endsWith('At') || key.endsWith('_at');
|
|
131
|
+
|
|
132
|
+
if (shouldConvert && typeof value === 'string') {
|
|
133
|
+
const num = Number(value);
|
|
134
|
+
if (!isNaN(num)) {
|
|
135
|
+
converted[key] = num;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// number 类型保持不变(小于 u32 的值)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return converted as T;
|
|
142
|
+
}) as T[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
|
|
147
|
+
* 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
|
|
148
|
+
*/
|
|
149
|
+
private whereKeysToSnake(where: any): any {
|
|
150
|
+
if (!where || typeof where !== 'object') return where;
|
|
151
|
+
|
|
152
|
+
// 处理数组($or, $and 等)
|
|
153
|
+
if (Array.isArray(where)) {
|
|
154
|
+
return where.map((item) => this.whereKeysToSnake(item));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result: any = {};
|
|
158
|
+
for (const [key, value] of Object.entries(where)) {
|
|
159
|
+
// 保留 $or, $and 等逻辑操作符
|
|
160
|
+
if (key === '$or' || key === '$and') {
|
|
161
|
+
result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 处理带操作符的字段名(如 userId$gt)
|
|
166
|
+
if (key.includes('$')) {
|
|
167
|
+
const lastDollarIndex = key.lastIndexOf('$');
|
|
168
|
+
const fieldName = key.substring(0, lastDollarIndex);
|
|
169
|
+
const operator = key.substring(lastDollarIndex);
|
|
170
|
+
const snakeKey = snakeCase(fieldName) + operator;
|
|
171
|
+
result[snakeKey] = value;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 普通字段:转换键名,递归处理值(支持嵌套对象)
|
|
176
|
+
const snakeKey = snakeCase(key);
|
|
177
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
178
|
+
result[snakeKey] = this.whereKeysToSnake(value);
|
|
179
|
+
} else {
|
|
180
|
+
result[snakeKey] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
141
187
|
/**
|
|
142
188
|
* 执行 SQL(使用 sql.unsafe,带慢查询日志)
|
|
143
189
|
*/
|
|
@@ -176,7 +222,7 @@ export class DbHelper {
|
|
|
176
222
|
*/
|
|
177
223
|
async tableExists(tableName: string): Promise<boolean> {
|
|
178
224
|
// 将表名转换为下划线格式
|
|
179
|
-
const snakeTableName =
|
|
225
|
+
const snakeTableName = snakeCase(tableName);
|
|
180
226
|
|
|
181
227
|
const result = await this.executeWithConn('SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', [snakeTableName]);
|
|
182
228
|
|
|
@@ -228,7 +274,7 @@ export class DbHelper {
|
|
|
228
274
|
const camelRow = keysToCamel<T>(row);
|
|
229
275
|
|
|
230
276
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
231
|
-
return convertBigIntFields<T>([camelRow])[0];
|
|
277
|
+
return this.convertBigIntFields<T>([camelRow])[0];
|
|
232
278
|
}
|
|
233
279
|
|
|
234
280
|
/**
|
|
@@ -288,7 +334,7 @@ export class DbHelper {
|
|
|
288
334
|
|
|
289
335
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
290
336
|
return {
|
|
291
|
-
lists: convertBigIntFields<T>(camelList),
|
|
337
|
+
lists: this.convertBigIntFields<T>(camelList),
|
|
292
338
|
total: total,
|
|
293
339
|
page: prepared.page,
|
|
294
340
|
limit: prepared.limit,
|
|
@@ -335,7 +381,7 @@ export class DbHelper {
|
|
|
335
381
|
const camelResult = arrayKeysToCamel<T>(result);
|
|
336
382
|
|
|
337
383
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
338
|
-
return convertBigIntFields<T>(camelResult);
|
|
384
|
+
return this.convertBigIntFields<T>(camelResult);
|
|
339
385
|
}
|
|
340
386
|
|
|
341
387
|
/**
|
|
@@ -349,7 +395,7 @@ export class DbHelper {
|
|
|
349
395
|
const cleanData = this.cleanFields(data);
|
|
350
396
|
|
|
351
397
|
// 转换表名:小驼峰 → 下划线
|
|
352
|
-
const snakeTable =
|
|
398
|
+
const snakeTable = snakeCase(table);
|
|
353
399
|
|
|
354
400
|
// 处理数据(自动添加必要字段)
|
|
355
401
|
// 字段名转换:小驼峰 → 下划线
|
|
@@ -405,7 +451,7 @@ export class DbHelper {
|
|
|
405
451
|
}
|
|
406
452
|
|
|
407
453
|
// 转换表名:小驼峰 → 下划线
|
|
408
|
-
const snakeTable =
|
|
454
|
+
const snakeTable = snakeCase(table);
|
|
409
455
|
|
|
410
456
|
// 批量生成 ID(一次性从 Redis 获取 N 个 ID)
|
|
411
457
|
const ids = await this.befly.redis.genTimeIDBatch(dataList.length);
|
|
@@ -459,7 +505,7 @@ export class DbHelper {
|
|
|
459
505
|
const cleanWhere = this.cleanFields(where);
|
|
460
506
|
|
|
461
507
|
// 转换表名:小驼峰 → 下划线
|
|
462
|
-
const snakeTable =
|
|
508
|
+
const snakeTable = snakeCase(table);
|
|
463
509
|
|
|
464
510
|
// 字段名转换:小驼峰 → 下划线
|
|
465
511
|
const snakeData = keysToSnake(cleanData);
|
|
@@ -507,7 +553,7 @@ export class DbHelper {
|
|
|
507
553
|
const { table, where } = options;
|
|
508
554
|
|
|
509
555
|
// 转换表名:小驼峰 → 下划线
|
|
510
|
-
const snakeTable =
|
|
556
|
+
const snakeTable = snakeCase(table);
|
|
511
557
|
|
|
512
558
|
// 清理条件字段
|
|
513
559
|
const cleanWhere = this.cleanFields(where);
|
|
@@ -666,8 +712,8 @@ export class DbHelper {
|
|
|
666
712
|
*/
|
|
667
713
|
async increment(table: string, field: string, where: WhereConditions, value: number = 1): Promise<number> {
|
|
668
714
|
// 转换表名和字段名:小驼峰 → 下划线
|
|
669
|
-
const snakeTable =
|
|
670
|
-
const snakeField =
|
|
715
|
+
const snakeTable = snakeCase(table);
|
|
716
|
+
const snakeField = snakeCase(field);
|
|
671
717
|
|
|
672
718
|
// 验证表名格式(只允许字母、数字、下划线)
|
|
673
719
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(snakeTable)) {
|