befly 3.0.0 → 3.0.1
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/checks/conflict.ts +35 -114
- package/checks/table.ts +31 -63
- package/config/env.ts +3 -3
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +1 -1
- package/main.ts +17 -71
- package/package.json +7 -28
- package/plugins/db.ts +11 -10
- package/plugins/redis.ts +5 -9
- package/scripts/syncDb/apply.ts +3 -3
- package/scripts/syncDb/constants.ts +2 -1
- package/scripts/syncDb/ddl.ts +15 -8
- package/scripts/syncDb/helpers.ts +3 -2
- package/scripts/syncDb/index.ts +23 -35
- package/scripts/syncDb/state.ts +8 -6
- package/scripts/syncDb/table.ts +32 -22
- package/scripts/syncDb/tableCreate.ts +9 -3
- package/scripts/syncDb/tests/constants.test.ts +2 -1
- package/scripts/syncDb.ts +10 -9
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +17 -14
- package/types/befly.d.ts +2 -6
- package/types/context.d.ts +7 -0
- package/types/database.d.ts +9 -14
- package/types/index.d.ts +442 -8
- package/types/index.ts +35 -56
- package/types/redis.d.ts +2 -0
- package/types/validator.d.ts +0 -2
- package/types/validator.ts +43 -0
- package/utils/colors.ts +117 -37
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +687 -116
- package/utils/helper.ts +812 -0
- package/utils/index.ts +10 -23
- package/utils/logger.ts +78 -171
- package/utils/redisHelper.ts +135 -152
- package/{types/context.ts → utils/requestContext.ts} +3 -3
- package/utils/sqlBuilder.ts +142 -165
- package/utils/validate.ts +51 -9
- package/apis/health/info.ts +0 -64
- package/apis/tool/tokenCheck.ts +0 -51
- package/bin/befly.ts +0 -202
- package/bunfig.toml +0 -3
- package/plugins/tool.ts +0 -34
- package/scripts/syncDev.ts +0 -112
- package/system.ts +0 -149
- package/tables/_common.json +0 -21
- package/tables/admin.json +0 -10
- package/utils/addonHelper.ts +0 -60
- package/utils/api.ts +0 -23
- package/utils/datetime.ts +0 -51
- package/utils/errorHandler.ts +0 -68
- package/utils/objectHelper.ts +0 -68
- package/utils/pluginHelper.ts +0 -62
- package/utils/response.ts +0 -38
- package/utils/sqlHelper.ts +0 -447
- package/utils/tableHelper.ts +0 -167
- package/utils/tool.ts +0 -230
- package/utils/typeHelper.ts +0 -101
package/utils/dbHelper.ts
CHANGED
|
@@ -1,142 +1,713 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 数据库助手 - TypeScript 版本
|
|
3
|
+
* 提供数据库 CRUD 操作的封装
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { SqlBuilder } from './sqlBuilder.js';
|
|
7
|
+
import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear, convertBigIntFields, toSnakeCase } from './helper.js';
|
|
8
8
|
import { Logger } from './logger.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { WhereConditions } from '../types/common.js';
|
|
10
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
11
|
+
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from '../types/database.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
* 用于将小驼峰命名的表名转换为数据库约定的蛇形命名
|
|
14
|
-
* @param name - 小驼峰命名的字符串
|
|
15
|
-
* @returns 蛇形命名的字符串
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* toSnakeTableName('userTable') // 'user_table'
|
|
19
|
-
* toSnakeTableName('testNewFormat') // 'test_new_format'
|
|
20
|
-
* toSnakeTableName('common') // 'common'
|
|
21
|
-
* toSnakeTableName('APIKey') // 'api_key'
|
|
14
|
+
* 数据库助手类
|
|
22
15
|
*/
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, '$1_$2')
|
|
28
|
-
.toLowerCase();
|
|
29
|
-
};
|
|
16
|
+
export class DbHelper {
|
|
17
|
+
private befly: BeflyContext;
|
|
18
|
+
private sql: any = null;
|
|
19
|
+
private isTransaction: boolean = false;
|
|
30
20
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* // SQLite 文件数据库
|
|
42
|
-
* buildDatabaseUrl() // 'sqlite://database.db'
|
|
43
|
-
* buildDatabaseUrl() // 'sqlite://./data/app.db'
|
|
44
|
-
* buildDatabaseUrl() // 'sqlite:///absolute/path/db.sqlite'
|
|
45
|
-
*
|
|
46
|
-
* // PostgreSQL
|
|
47
|
-
* buildDatabaseUrl() // 'postgres://user:pass@localhost:5432/dbname'
|
|
48
|
-
*
|
|
49
|
-
* // MySQL
|
|
50
|
-
* buildDatabaseUrl() // 'mysql://user:pass@localhost:3306/dbname'
|
|
51
|
-
*/
|
|
52
|
-
export const buildDatabaseUrl = (): string => {
|
|
53
|
-
const type = Env.DB_TYPE || '';
|
|
54
|
-
const host = Env.DB_HOST || '';
|
|
55
|
-
const port = Env.DB_PORT;
|
|
56
|
-
const user = encodeURIComponent(Env.DB_USER || '');
|
|
57
|
-
const pass = encodeURIComponent(Env.DB_PASS || '');
|
|
58
|
-
const name = Env.DB_NAME || '';
|
|
21
|
+
/**
|
|
22
|
+
* 构造函数
|
|
23
|
+
* @param befly - Befly 上下文
|
|
24
|
+
* @param sql - Bun SQL 客户端(可选,用于事务)
|
|
25
|
+
*/
|
|
26
|
+
constructor(befly: BeflyContext, sql: any = null) {
|
|
27
|
+
this.befly = befly;
|
|
28
|
+
this.sql = sql;
|
|
29
|
+
this.isTransaction = !!sql;
|
|
30
|
+
}
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 字段数组转下划线格式(私有方法)
|
|
34
|
+
*/
|
|
35
|
+
private fieldsToSnake(fields: string[]): string[] {
|
|
36
|
+
if (!fields || !Array.isArray(fields)) return fields;
|
|
37
|
+
return fields.map((field) => {
|
|
38
|
+
// 保留通配符和特殊字段
|
|
39
|
+
if (field === '*' || field.includes('(') || field.includes(' ')) {
|
|
40
|
+
return field;
|
|
41
|
+
}
|
|
42
|
+
return toSnakeCase(field);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
/**
|
|
47
|
+
* orderBy 数组转下划线格式(私有方法)
|
|
48
|
+
*/
|
|
49
|
+
private orderByToSnake(orderBy: string[]): string[] {
|
|
50
|
+
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
51
|
+
return orderBy.map((item) => {
|
|
52
|
+
if (typeof item !== 'string' || !item.includes('#')) return item;
|
|
53
|
+
const [field, direction] = item.split('#');
|
|
54
|
+
return `${toSnakeCase(field.trim())}#${direction.trim()}`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
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
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
94
|
}
|
|
72
|
-
|
|
73
|
-
return
|
|
95
|
+
|
|
96
|
+
return result;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
/**
|
|
100
|
+
* 统一的查询参数预处理方法
|
|
101
|
+
*/
|
|
102
|
+
private prepareQueryOptions(options: QueryOptions) {
|
|
103
|
+
const cleanWhere = this.cleanFields(options.where);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
table: toSnakeCase(options.table),
|
|
107
|
+
fields: this.fieldsToSnake(options.fields || ['*']),
|
|
108
|
+
where: this.whereKeysToSnake(cleanWhere),
|
|
109
|
+
orderBy: this.orderByToSnake(options.orderBy || []),
|
|
110
|
+
page: options.page || 1,
|
|
111
|
+
limit: options.limit || 10
|
|
112
|
+
};
|
|
80
113
|
}
|
|
81
114
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
/**
|
|
116
|
+
* 添加默认的 state 过滤条件
|
|
117
|
+
* 默认查询 state > 0 的数据(排除已删除和特殊状态)
|
|
118
|
+
*/
|
|
119
|
+
private addDefaultStateFilter(where: WhereConditions = {}): WhereConditions {
|
|
120
|
+
// 如果用户已经指定了 state 条件,优先使用用户的条件
|
|
121
|
+
const hasStateCondition = Object.keys(where).some((key) => key.startsWith('state'));
|
|
122
|
+
|
|
123
|
+
if (hasStateCondition) {
|
|
124
|
+
return where;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 默认查询 state > 0 的数据
|
|
128
|
+
return {
|
|
129
|
+
...where,
|
|
130
|
+
state$gt: 0
|
|
131
|
+
};
|
|
86
132
|
}
|
|
87
133
|
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
/**
|
|
135
|
+
* 清理数据或 where 条件(默认排除 null 和 undefined)
|
|
136
|
+
*/
|
|
137
|
+
private cleanFields<T extends Record<string, any>>(data: T | undefined | null, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> {
|
|
138
|
+
return fieldClear(data || ({} as T), excludeValues, keepValues);
|
|
139
|
+
}
|
|
90
140
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
/**
|
|
142
|
+
* 执行 SQL(使用 sql.unsafe,带慢查询日志)
|
|
143
|
+
*/
|
|
144
|
+
private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
|
|
145
|
+
if (!this.sql) {
|
|
146
|
+
throw new Error('数据库连接未初始化');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 记录开始时间
|
|
150
|
+
const startTime = Date.now();
|
|
151
|
+
|
|
152
|
+
// 使用 sql.unsafe 执行查询
|
|
153
|
+
let result;
|
|
154
|
+
if (params && params.length > 0) {
|
|
155
|
+
result = await this.sql.unsafe(sqlStr, params);
|
|
156
|
+
} else {
|
|
157
|
+
result = await this.sql.unsafe(sqlStr);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 计算执行时间
|
|
161
|
+
const duration = Date.now() - startTime;
|
|
162
|
+
|
|
163
|
+
// 慢查询警告(超过 1000ms)
|
|
164
|
+
if (duration > 1000) {
|
|
165
|
+
const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
|
|
166
|
+
Logger.warn(`🐌 检测到慢查询 (${duration}ms): ${sqlPreview}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 检查表是否存在
|
|
174
|
+
* @param tableName - 表名(支持小驼峰,会自动转换为下划线)
|
|
175
|
+
* @returns 表是否存在
|
|
176
|
+
*/
|
|
177
|
+
async tableExists(tableName: string): Promise<boolean> {
|
|
178
|
+
// 将表名转换为下划线格式
|
|
179
|
+
const snakeTableName = toSnakeCase(tableName);
|
|
180
|
+
|
|
181
|
+
const result = await this.executeWithConn('SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', [snakeTableName]);
|
|
182
|
+
|
|
183
|
+
return result?.[0]?.count > 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 查询记录数
|
|
188
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
189
|
+
* @param options.where - 查询条件
|
|
190
|
+
* @example
|
|
191
|
+
* // 查询总数
|
|
192
|
+
* const count = await db.getCount({ table: 'user' });
|
|
193
|
+
* // 查询指定条件的记录数
|
|
194
|
+
* const activeCount = await db.getCount({ table: 'user', where: { state: 1 } });
|
|
195
|
+
*/
|
|
196
|
+
async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
|
|
197
|
+
const { table, where } = this.prepareQueryOptions(options as QueryOptions);
|
|
198
|
+
|
|
199
|
+
const builder = new SqlBuilder().select(['COUNT(*) as count']).from(table).where(this.addDefaultStateFilter(where));
|
|
200
|
+
|
|
201
|
+
const { sql, params } = builder.toSelectSql();
|
|
202
|
+
const result = await this.executeWithConn(sql, params);
|
|
203
|
+
|
|
204
|
+
return result?.[0]?.count || 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 查询单条数据
|
|
209
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
210
|
+
* @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
|
|
211
|
+
* @example
|
|
212
|
+
* // 以下两种写法等效:
|
|
213
|
+
* getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
214
|
+
* getOne({ table: 'user_profile', fields: ['user_id', 'user_name'] })
|
|
215
|
+
*/
|
|
216
|
+
async getOne<T = any>(options: QueryOptions): Promise<T | null> {
|
|
217
|
+
const { table, fields, where } = this.prepareQueryOptions(options);
|
|
218
|
+
|
|
219
|
+
const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
|
|
220
|
+
|
|
221
|
+
const { sql, params } = builder.toSelectSql();
|
|
222
|
+
const result = await this.executeWithConn(sql, params);
|
|
223
|
+
|
|
224
|
+
// 字段名转换:下划线 → 小驼峰
|
|
225
|
+
const row = result?.[0] || null;
|
|
226
|
+
if (!row) return null;
|
|
227
|
+
|
|
228
|
+
const camelRow = keysToCamel<T>(row);
|
|
229
|
+
|
|
230
|
+
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
231
|
+
return convertBigIntFields<T>([camelRow])[0];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 查询列表(带分页)
|
|
236
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
237
|
+
* @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
|
|
238
|
+
* @example
|
|
239
|
+
* // 使用小驼峰格式(推荐)
|
|
240
|
+
* getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
|
|
241
|
+
*/
|
|
242
|
+
async getList<T = any>(options: QueryOptions): Promise<ListResult<T>> {
|
|
243
|
+
const prepared = this.prepareQueryOptions(options);
|
|
244
|
+
|
|
245
|
+
// 参数上限校验
|
|
246
|
+
if (prepared.page < 1 || prepared.page > 10000) {
|
|
247
|
+
throw new Error(`页码必须在 1 到 10000 之间 (table: ${options.table}, page: ${prepared.page}, limit: ${prepared.limit})`);
|
|
248
|
+
}
|
|
249
|
+
if (prepared.limit < 1 || prepared.limit > 1000) {
|
|
250
|
+
throw new Error(`每页数量必须在 1 到 1000 之间 (table: ${options.table}, page: ${prepared.page}, limit: ${prepared.limit})`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 构建查询
|
|
254
|
+
const whereFiltered = this.addDefaultStateFilter(prepared.where);
|
|
255
|
+
|
|
256
|
+
// 查询总数
|
|
257
|
+
const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
|
|
258
|
+
|
|
259
|
+
const { sql: countSql, params: countParams } = countBuilder.toSelectSql();
|
|
260
|
+
const countResult = await this.executeWithConn(countSql, countParams);
|
|
261
|
+
const total = countResult?.[0]?.total || 0;
|
|
262
|
+
|
|
263
|
+
// 如果总数为 0,直接返回,不执行第二次查询
|
|
264
|
+
if (total === 0) {
|
|
265
|
+
return {
|
|
266
|
+
lists: [],
|
|
267
|
+
total: 0,
|
|
268
|
+
page: prepared.page,
|
|
269
|
+
limit: prepared.limit,
|
|
270
|
+
pages: 0
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 查询数据
|
|
275
|
+
const offset = (prepared.page - 1) * prepared.limit;
|
|
276
|
+
const dataBuilder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
|
|
277
|
+
|
|
278
|
+
// 只有用户明确指定了 orderBy 才添加排序
|
|
279
|
+
if (prepared.orderBy && prepared.orderBy.length > 0) {
|
|
280
|
+
dataBuilder.orderBy(prepared.orderBy);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
284
|
+
const list = (await this.executeWithConn(dataSql, dataParams)) || [];
|
|
285
|
+
|
|
286
|
+
// 字段名转换:下划线 → 小驼峰
|
|
287
|
+
const camelList = arrayKeysToCamel<T>(list);
|
|
288
|
+
|
|
289
|
+
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
290
|
+
return {
|
|
291
|
+
lists: convertBigIntFields<T>(camelList),
|
|
292
|
+
total: total,
|
|
293
|
+
page: prepared.page,
|
|
294
|
+
limit: prepared.limit,
|
|
295
|
+
pages: Math.ceil(total / prepared.limit)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 查询所有数据(不分页,有上限保护)
|
|
301
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
302
|
+
* @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
|
|
303
|
+
* ⚠️ 警告:此方法会查询大量数据,建议使用 getList 分页查询
|
|
304
|
+
* @example
|
|
305
|
+
* // 使用小驼峰格式(推荐)
|
|
306
|
+
* getAll({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
307
|
+
*/
|
|
308
|
+
async getAll<T = any>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]> {
|
|
309
|
+
// 添加硬性上限保护,防止内存溢出
|
|
310
|
+
const MAX_LIMIT = 10000;
|
|
311
|
+
const WARNING_LIMIT = 1000;
|
|
312
|
+
|
|
313
|
+
const prepared = this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
314
|
+
|
|
315
|
+
const builder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(this.addDefaultStateFilter(prepared.where)).limit(MAX_LIMIT);
|
|
316
|
+
|
|
317
|
+
if (prepared.orderBy && prepared.orderBy.length > 0) {
|
|
318
|
+
builder.orderBy(prepared.orderBy);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const { sql, params } = builder.toSelectSql();
|
|
322
|
+
const result = (await this.executeWithConn(sql, params)) || [];
|
|
323
|
+
|
|
324
|
+
// 警告日志:返回数据超过警告阈值
|
|
325
|
+
if (result.length >= WARNING_LIMIT) {
|
|
326
|
+
Logger.warn(`⚠️ getAll 从表 \`${options.table}\` 返回了 ${result.length} 行数据,建议使用 getList 分页查询以获得更好的性能。`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 如果达到上限,额外警告
|
|
330
|
+
if (result.length >= MAX_LIMIT) {
|
|
331
|
+
Logger.warn(`🚨 getAll 达到了最大限制 (${MAX_LIMIT}),可能还有更多数据。请使用 getList 分页查询。`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 字段名转换:下划线 → 小驼峰
|
|
335
|
+
const camelResult = arrayKeysToCamel<T>(result);
|
|
336
|
+
|
|
337
|
+
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
338
|
+
return convertBigIntFields<T>(camelResult);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 插入数据(自动生成 ID、时间戳、state)
|
|
343
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
344
|
+
*/
|
|
345
|
+
async insData(options: InsertOptions): Promise<number> {
|
|
346
|
+
const { table, data } = options;
|
|
347
|
+
|
|
348
|
+
// 清理数据(排除 null 和 undefined)
|
|
349
|
+
const cleanData = this.cleanFields(data);
|
|
350
|
+
|
|
351
|
+
// 转换表名:小驼峰 → 下划线
|
|
352
|
+
const snakeTable = toSnakeCase(table);
|
|
353
|
+
|
|
354
|
+
// 处理数据(自动添加必要字段)
|
|
355
|
+
// 字段名转换:小驼峰 → 下划线
|
|
356
|
+
const snakeData = keysToSnake(cleanData);
|
|
357
|
+
|
|
358
|
+
// 复制用户数据,但移除系统字段(防止用户尝试覆盖)
|
|
359
|
+
const { id, created_at, updated_at, deleted_at, state, ...userData } = snakeData;
|
|
360
|
+
|
|
361
|
+
const processed: Record<string, any> = { ...userData };
|
|
362
|
+
|
|
363
|
+
// 强制生成 ID(不可被用户覆盖)
|
|
364
|
+
try {
|
|
365
|
+
processed.id = await this.befly.redis.genTimeID();
|
|
366
|
+
} catch (error: any) {
|
|
367
|
+
throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table}): ${error.message}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 强制生成时间戳(不可被用户覆盖)
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
processed.created_at = now;
|
|
373
|
+
processed.updated_at = now;
|
|
374
|
+
|
|
375
|
+
// 强制设置 state 为 1(激活状态,不可被用户覆盖)
|
|
376
|
+
processed.state = 1;
|
|
377
|
+
|
|
378
|
+
// 注意:deleted_at 字段不在插入时生成,只在软删除时设置
|
|
379
|
+
|
|
380
|
+
// 构建 SQL
|
|
381
|
+
const builder = new SqlBuilder();
|
|
382
|
+
const { sql, params } = builder.toInsertSql(snakeTable, processed);
|
|
383
|
+
|
|
384
|
+
// 执行
|
|
385
|
+
const result = await this.executeWithConn(sql, params);
|
|
386
|
+
return processed.id || result?.lastInsertRowid || 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 批量插入数据(真正的批量操作)
|
|
391
|
+
* 使用 INSERT INTO ... VALUES (...), (...), (...) 语法
|
|
392
|
+
* 自动生成系统字段并包装在事务中
|
|
393
|
+
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
394
|
+
*/
|
|
395
|
+
async insBatch(table: string, dataList: Record<string, any>[]): Promise<number[]> {
|
|
396
|
+
// 空数组直接返回
|
|
397
|
+
if (dataList.length === 0) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 限制批量大小
|
|
402
|
+
const MAX_BATCH_SIZE = 1000;
|
|
403
|
+
if (dataList.length > MAX_BATCH_SIZE) {
|
|
404
|
+
throw new Error(`批量插入数量 ${dataList.length} 超过最大限制 ${MAX_BATCH_SIZE}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 转换表名:小驼峰 → 下划线
|
|
408
|
+
const snakeTable = toSnakeCase(table);
|
|
409
|
+
|
|
410
|
+
// 批量生成 ID(一次性从 Redis 获取 N 个 ID)
|
|
411
|
+
const ids = await this.befly.redis.genTimeIDBatch(dataList.length);
|
|
412
|
+
const now = Date.now();
|
|
413
|
+
|
|
414
|
+
// 处理所有数据(自动添加系统字段)
|
|
415
|
+
const processedList = dataList.map((data, index) => {
|
|
416
|
+
// 清理数据(排除 null 和 undefined)
|
|
417
|
+
const cleanData = this.cleanFields(data);
|
|
418
|
+
|
|
419
|
+
// 字段名转换:小驼峰 → 下划线
|
|
420
|
+
const snakeData = keysToSnake(cleanData);
|
|
421
|
+
|
|
422
|
+
// 移除系统字段(防止用户尝试覆盖)
|
|
423
|
+
const { id, created_at, updated_at, deleted_at, state, ...userData } = snakeData;
|
|
424
|
+
|
|
425
|
+
// 强制生成系统字段(不可被用户覆盖)
|
|
426
|
+
return {
|
|
427
|
+
...userData,
|
|
428
|
+
id: ids[index],
|
|
429
|
+
created_at: now,
|
|
430
|
+
updated_at: now,
|
|
431
|
+
state: 1
|
|
432
|
+
};
|
|
117
433
|
});
|
|
434
|
+
|
|
435
|
+
// 构建批量插入 SQL
|
|
436
|
+
const builder = new SqlBuilder();
|
|
437
|
+
const { sql, params } = builder.toInsertSql(snakeTable, processedList);
|
|
438
|
+
|
|
439
|
+
// 在事务中执行批量插入
|
|
440
|
+
try {
|
|
441
|
+
await this.executeWithConn(sql, params);
|
|
442
|
+
return ids;
|
|
443
|
+
} catch (error: any) {
|
|
444
|
+
// 批量插入失败,记录错误
|
|
445
|
+
Logger.error(`表 \`${table}\` 批量插入失败`, error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
118
448
|
}
|
|
119
449
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
450
|
+
/**
|
|
451
|
+
* 更新数据(强制更新时间戳,系统字段不可修改)
|
|
452
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
453
|
+
*/
|
|
454
|
+
async updData(options: UpdateOptions): Promise<number> {
|
|
455
|
+
const { table, data, where } = options;
|
|
456
|
+
|
|
457
|
+
// 清理数据和条件(排除 null 和 undefined)
|
|
458
|
+
const cleanData = this.cleanFields(data);
|
|
459
|
+
const cleanWhere = this.cleanFields(where);
|
|
460
|
+
|
|
461
|
+
// 转换表名:小驼峰 → 下划线
|
|
462
|
+
const snakeTable = toSnakeCase(table);
|
|
463
|
+
|
|
464
|
+
// 字段名转换:小驼峰 → 下划线
|
|
465
|
+
const snakeData = keysToSnake(cleanData);
|
|
466
|
+
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
467
|
+
|
|
468
|
+
// 移除系统字段(防止用户尝试修改)
|
|
469
|
+
// 注意:state 允许用户修改(用于设置禁用状态 state=2)
|
|
470
|
+
const { id, created_at, updated_at, deleted_at, ...userData } = snakeData;
|
|
471
|
+
|
|
472
|
+
// 强制更新时间戳(不可被用户覆盖)
|
|
473
|
+
const processed: Record<string, any> = {
|
|
474
|
+
...userData,
|
|
475
|
+
updated_at: Date.now()
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// 构建 SQL
|
|
479
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
480
|
+
const builder = new SqlBuilder().where(whereFiltered);
|
|
481
|
+
const { sql, params } = builder.toUpdateSql(snakeTable, processed);
|
|
482
|
+
|
|
483
|
+
// 执行
|
|
484
|
+
const result = await this.executeWithConn(sql, params);
|
|
485
|
+
return result?.changes || 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 软删除数据(deleted_at 设置为当前时间,state 设置为 0)
|
|
490
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
491
|
+
*/
|
|
492
|
+
async delData(options: DeleteOptions): Promise<number> {
|
|
493
|
+
const { table, where } = options;
|
|
494
|
+
|
|
495
|
+
return await this.updData({
|
|
496
|
+
table: table,
|
|
497
|
+
data: { state: 0, deleted_at: Date.now() },
|
|
498
|
+
where: where
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 硬删除数据(物理删除,不可恢复)
|
|
504
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
505
|
+
*/
|
|
506
|
+
async delForce(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
|
|
507
|
+
const { table, where } = options;
|
|
508
|
+
|
|
509
|
+
// 转换表名:小驼峰 → 下划线
|
|
510
|
+
const snakeTable = toSnakeCase(table);
|
|
511
|
+
|
|
512
|
+
// 清理条件字段
|
|
513
|
+
const cleanWhere = this.cleanFields(where);
|
|
514
|
+
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
515
|
+
|
|
516
|
+
// 物理删除
|
|
517
|
+
const builder = new SqlBuilder().where(snakeWhere);
|
|
518
|
+
const { sql, params } = builder.toDeleteSql(snakeTable);
|
|
519
|
+
|
|
520
|
+
const result = await this.executeWithConn(sql, params);
|
|
521
|
+
return result?.changes || 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 禁用数据(设置 state=2)
|
|
526
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
527
|
+
*/
|
|
528
|
+
async disableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
|
|
529
|
+
const { table, where } = options;
|
|
530
|
+
|
|
531
|
+
return await this.updData({
|
|
532
|
+
table: table,
|
|
533
|
+
data: {
|
|
534
|
+
state: 2
|
|
535
|
+
},
|
|
536
|
+
where: where
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 启用数据(设置 state=1)
|
|
542
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
543
|
+
*/
|
|
544
|
+
async enableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
|
|
545
|
+
const { table, where } = options;
|
|
546
|
+
|
|
547
|
+
return await this.updData({
|
|
548
|
+
table: table,
|
|
549
|
+
data: {
|
|
550
|
+
state: 1
|
|
551
|
+
},
|
|
552
|
+
where: where
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 执行事务
|
|
558
|
+
*/
|
|
559
|
+
async trans<T = any>(callback: TransactionCallback<T>): Promise<T> {
|
|
560
|
+
if (this.isTransaction) {
|
|
561
|
+
// 已经在事务中,直接执行回调
|
|
562
|
+
return await callback(this);
|
|
132
563
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
564
|
+
|
|
565
|
+
// 开启新事务
|
|
566
|
+
const conn = await this.befly.db.transaction();
|
|
567
|
+
let committed = false;
|
|
568
|
+
|
|
137
569
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
570
|
+
const trans = new DbHelper(this.befly, conn);
|
|
571
|
+
const result = await callback(trans);
|
|
572
|
+
|
|
573
|
+
// 提交事务
|
|
574
|
+
try {
|
|
575
|
+
await conn.query('COMMIT');
|
|
576
|
+
committed = true;
|
|
577
|
+
} catch (commitError: any) {
|
|
578
|
+
Logger.error('事务提交失败,正在回滚', commitError);
|
|
579
|
+
await conn.query('ROLLBACK');
|
|
580
|
+
throw new Error(`事务提交失败: ${commitError.message}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return result;
|
|
584
|
+
} catch (error: any) {
|
|
585
|
+
// 回调执行失败,回滚事务
|
|
586
|
+
if (!committed) {
|
|
587
|
+
try {
|
|
588
|
+
await conn.query('ROLLBACK');
|
|
589
|
+
Logger.warn('事务已回滚');
|
|
590
|
+
} catch (rollbackError: any) {
|
|
591
|
+
Logger.error('事务回滚失败:', rollbackError);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 执行原始 SQL
|
|
600
|
+
*/
|
|
601
|
+
async query(sql: string, params?: any[]): Promise<any> {
|
|
602
|
+
return await this.executeWithConn(sql, params);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 检查数据是否存在(优化性能)
|
|
607
|
+
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
608
|
+
*/
|
|
609
|
+
async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
|
|
610
|
+
const { table, where } = this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
611
|
+
|
|
612
|
+
// 使用 COUNT(1) 性能更好
|
|
613
|
+
const builder = new SqlBuilder().select(['COUNT(1) as cnt']).from(table).where(this.addDefaultStateFilter(where)).limit(1);
|
|
614
|
+
|
|
615
|
+
const { sql, params } = builder.toSelectSql();
|
|
616
|
+
const result = await this.executeWithConn(sql, params);
|
|
617
|
+
|
|
618
|
+
return (result?.[0]?.cnt || 0) > 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 查询单个字段值(带字段名验证)
|
|
623
|
+
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
624
|
+
*/
|
|
625
|
+
async getFieldValue<T = any>(options: Omit<QueryOptions, 'fields'> & { field: string }): Promise<T | null> {
|
|
626
|
+
const { field, ...queryOptions } = options;
|
|
627
|
+
|
|
628
|
+
// 验证字段名格式(只允许字母、数字、下划线)
|
|
629
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
630
|
+
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const result = await this.getOne({
|
|
634
|
+
...queryOptions,
|
|
635
|
+
fields: [field]
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!result) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 尝试直接访问字段(小驼峰)
|
|
643
|
+
if (field in result) {
|
|
644
|
+
return result[field];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 转换为小驼峰格式再尝试访问(支持用户传入下划线格式)
|
|
648
|
+
const camelField = field.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
649
|
+
if (camelField !== field && camelField in result) {
|
|
650
|
+
return result[camelField];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 转换为下划线格式再尝试访问(支持用户传入小驼峰格式)
|
|
654
|
+
const snakeField = field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
655
|
+
if (snakeField !== field && snakeField in result) {
|
|
656
|
+
return result[snakeField];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 自增字段(安全实现,防止 SQL 注入)
|
|
664
|
+
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
665
|
+
* @param field - 字段名(支持小驼峰或下划线格式,会自动转换)
|
|
666
|
+
*/
|
|
667
|
+
async increment(table: string, field: string, where: WhereConditions, value: number = 1): Promise<number> {
|
|
668
|
+
// 转换表名和字段名:小驼峰 → 下划线
|
|
669
|
+
const snakeTable = toSnakeCase(table);
|
|
670
|
+
const snakeField = toSnakeCase(field);
|
|
671
|
+
|
|
672
|
+
// 验证表名格式(只允许字母、数字、下划线)
|
|
673
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(snakeTable)) {
|
|
674
|
+
throw new Error(`无效的表名: ${snakeTable}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 验证字段名格式(只允许字母、数字、下划线)
|
|
678
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(snakeField)) {
|
|
679
|
+
throw new Error(`无效的字段名: ${field}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 验证 value 必须是数字
|
|
683
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
684
|
+
throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// 清理 where 条件(排除 null 和 undefined)
|
|
688
|
+
const cleanWhere = this.cleanFields(where);
|
|
689
|
+
|
|
690
|
+
// 转换 where 条件字段名:小驼峰 → 下划线
|
|
691
|
+
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
692
|
+
|
|
693
|
+
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
|
694
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
695
|
+
const builder = new SqlBuilder().where(whereFiltered);
|
|
696
|
+
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
697
|
+
|
|
698
|
+
// 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
|
|
699
|
+
const sql = whereClause ? `UPDATE \`${snakeTable}\` SET \`${snakeField}\` = \`${snakeField}\` + ? WHERE ${whereClause}` : `UPDATE \`${snakeTable}\` SET \`${snakeField}\` = \`${snakeField}\` + ?`;
|
|
700
|
+
|
|
701
|
+
const result = await this.executeWithConn(sql, [value, ...whereParams]);
|
|
702
|
+
return result?.changes || 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* 自减字段
|
|
707
|
+
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
708
|
+
* @param field - 字段名(支持小驼峰或下划线格式,会自动转换)
|
|
709
|
+
*/
|
|
710
|
+
async decrement(table: string, field: string, where: WhereConditions, value: number = 1): Promise<number> {
|
|
711
|
+
return await this.increment(table, field, where, -value);
|
|
141
712
|
}
|
|
142
713
|
}
|