befly 3.16.10 → 3.16.11
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 +0 -129
- package/befly.js +12769 -0
- package/befly.min.js +47 -0
- package/package.json +18 -29
- package/dist/befly.config.d.ts +0 -7
- package/dist/befly.config.js +0 -128
- package/dist/befly.js +0 -17348
- package/dist/befly.min.js +0 -23
- package/dist/checks/checkApi.d.ts +0 -1
- package/dist/checks/checkApi.js +0 -139
- package/dist/checks/checkConfig.d.ts +0 -9
- package/dist/checks/checkConfig.js +0 -255
- package/dist/checks/checkHook.d.ts +0 -1
- package/dist/checks/checkHook.js +0 -132
- package/dist/checks/checkMenu.d.ts +0 -3
- package/dist/checks/checkMenu.js +0 -106
- package/dist/checks/checkPlugin.d.ts +0 -1
- package/dist/checks/checkPlugin.js +0 -132
- package/dist/checks/checkTable.d.ts +0 -7
- package/dist/checks/checkTable.js +0 -431
- package/dist/configs/presetRegexp.d.ts +0 -145
- package/dist/configs/presetRegexp.js +0 -218
- package/dist/hooks/auth.d.ts +0 -3
- package/dist/hooks/auth.js +0 -24
- package/dist/hooks/cors.d.ts +0 -7
- package/dist/hooks/cors.js +0 -36
- package/dist/hooks/parser.d.ts +0 -10
- package/dist/hooks/parser.js +0 -76
- package/dist/hooks/permission.d.ts +0 -11
- package/dist/hooks/permission.js +0 -78
- package/dist/hooks/validator.d.ts +0 -7
- package/dist/hooks/validator.js +0 -52
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -316
- package/dist/lib/asyncContext.d.ts +0 -21
- package/dist/lib/asyncContext.js +0 -27
- package/dist/lib/cacheHelper.d.ts +0 -128
- package/dist/lib/cacheHelper.js +0 -477
- package/dist/lib/cacheKeys.d.ts +0 -27
- package/dist/lib/cacheKeys.js +0 -37
- package/dist/lib/cipher.d.ts +0 -153
- package/dist/lib/cipher.js +0 -237
- package/dist/lib/connect.d.ts +0 -95
- package/dist/lib/connect.js +0 -313
- package/dist/lib/dbHelper.d.ts +0 -229
- package/dist/lib/dbHelper.js +0 -1099
- package/dist/lib/dbUtils.d.ts +0 -91
- package/dist/lib/dbUtils.js +0 -544
- package/dist/lib/jwt.d.ts +0 -13
- package/dist/lib/jwt.js +0 -77
- package/dist/lib/logger.d.ts +0 -46
- package/dist/lib/logger.js +0 -731
- package/dist/lib/redisHelper.d.ts +0 -193
- package/dist/lib/redisHelper.js +0 -598
- package/dist/lib/sqlBuilder.d.ts +0 -160
- package/dist/lib/sqlBuilder.js +0 -837
- package/dist/lib/sqlCheck.d.ts +0 -23
- package/dist/lib/sqlCheck.js +0 -119
- package/dist/lib/validator.d.ts +0 -45
- package/dist/lib/validator.js +0 -424
- package/dist/loader/loadApis.d.ts +0 -12
- package/dist/loader/loadApis.js +0 -71
- package/dist/loader/loadHooks.d.ts +0 -7
- package/dist/loader/loadHooks.js +0 -50
- package/dist/loader/loadPlugins.d.ts +0 -8
- package/dist/loader/loadPlugins.js +0 -69
- package/dist/paths.d.ts +0 -93
- package/dist/paths.js +0 -100
- package/dist/plugins/cache.d.ts +0 -10
- package/dist/plugins/cache.js +0 -24
- package/dist/plugins/cipher.d.ts +0 -7
- package/dist/plugins/cipher.js +0 -14
- package/dist/plugins/config.d.ts +0 -3
- package/dist/plugins/config.js +0 -9
- package/dist/plugins/db.d.ts +0 -10
- package/dist/plugins/db.js +0 -48
- package/dist/plugins/jwt.d.ts +0 -6
- package/dist/plugins/jwt.js +0 -13
- package/dist/plugins/logger.d.ts +0 -10
- package/dist/plugins/logger.js +0 -21
- package/dist/plugins/redis.d.ts +0 -10
- package/dist/plugins/redis.js +0 -40
- package/dist/plugins/tool.d.ts +0 -75
- package/dist/plugins/tool.js +0 -105
- package/dist/router/api.d.ts +0 -14
- package/dist/router/api.js +0 -109
- package/dist/router/static.d.ts +0 -9
- package/dist/router/static.js +0 -56
- package/dist/scripts/ensureDist.d.ts +0 -1
- package/dist/scripts/ensureDist.js +0 -296
- package/dist/sync/syncApi.d.ts +0 -3
- package/dist/sync/syncApi.js +0 -163
- package/dist/sync/syncCache.d.ts +0 -2
- package/dist/sync/syncCache.js +0 -14
- package/dist/sync/syncDev.d.ts +0 -6
- package/dist/sync/syncDev.js +0 -166
- package/dist/sync/syncMenu.d.ts +0 -14
- package/dist/sync/syncMenu.js +0 -308
- package/dist/sync/syncTable.d.ts +0 -126
- package/dist/sync/syncTable.js +0 -1129
- package/dist/types/api.d.ts +0 -177
- package/dist/types/api.js +0 -4
- package/dist/types/befly.d.ts +0 -231
- package/dist/types/befly.js +0 -4
- package/dist/types/cache.d.ts +0 -96
- package/dist/types/cache.js +0 -4
- package/dist/types/cipher.d.ts +0 -27
- package/dist/types/cipher.js +0 -7
- package/dist/types/common.d.ts +0 -127
- package/dist/types/common.js +0 -5
- package/dist/types/context.d.ts +0 -39
- package/dist/types/context.js +0 -4
- package/dist/types/coreError.d.ts +0 -31
- package/dist/types/coreError.js +0 -38
- package/dist/types/crypto.d.ts +0 -20
- package/dist/types/crypto.js +0 -4
- package/dist/types/database.d.ts +0 -182
- package/dist/types/database.js +0 -4
- package/dist/types/hook.d.ts +0 -30
- package/dist/types/hook.js +0 -19
- package/dist/types/jwt.d.ts +0 -76
- package/dist/types/jwt.js +0 -4
- package/dist/types/logger.d.ts +0 -95
- package/dist/types/logger.js +0 -6
- package/dist/types/plugin.d.ts +0 -27
- package/dist/types/plugin.js +0 -17
- package/dist/types/redis.d.ts +0 -80
- package/dist/types/redis.js +0 -4
- package/dist/types/roleApisCache.d.ts +0 -21
- package/dist/types/roleApisCache.js +0 -8
- package/dist/types/sync.d.ts +0 -93
- package/dist/types/sync.js +0 -4
- package/dist/types/table.d.ts +0 -34
- package/dist/types/table.js +0 -4
- package/dist/types/validate.d.ts +0 -113
- package/dist/types/validate.js +0 -4
- package/dist/utils/calcPerfTime.d.ts +0 -4
- package/dist/utils/calcPerfTime.js +0 -13
- package/dist/utils/cors.d.ts +0 -8
- package/dist/utils/cors.js +0 -17
- package/dist/utils/dbFieldRules.d.ts +0 -31
- package/dist/utils/dbFieldRules.js +0 -94
- package/dist/utils/fieldClear.d.ts +0 -11
- package/dist/utils/fieldClear.js +0 -57
- package/dist/utils/formatYmdHms.d.ts +0 -1
- package/dist/utils/formatYmdHms.js +0 -20
- package/dist/utils/getClientIp.d.ts +0 -6
- package/dist/utils/getClientIp.js +0 -39
- package/dist/utils/importDefault.d.ts +0 -1
- package/dist/utils/importDefault.js +0 -53
- package/dist/utils/isDirentDirectory.d.ts +0 -3
- package/dist/utils/isDirentDirectory.js +0 -18
- package/dist/utils/loadMenuConfigs.d.ts +0 -11
- package/dist/utils/loadMenuConfigs.js +0 -130
- package/dist/utils/loggerUtils.d.ts +0 -18
- package/dist/utils/loggerUtils.js +0 -171
- package/dist/utils/mergeAndConcat.d.ts +0 -7
- package/dist/utils/mergeAndConcat.js +0 -77
- package/dist/utils/normalizeFieldDefinition.d.ts +0 -18
- package/dist/utils/normalizeFieldDefinition.js +0 -27
- package/dist/utils/processInfo.d.ts +0 -26
- package/dist/utils/processInfo.js +0 -41
- package/dist/utils/response.d.ts +0 -20
- package/dist/utils/response.js +0 -96
- package/dist/utils/scanAddons.d.ts +0 -15
- package/dist/utils/scanAddons.js +0 -35
- package/dist/utils/scanCoreBuiltins.d.ts +0 -3
- package/dist/utils/scanCoreBuiltins.js +0 -72
- package/dist/utils/scanFiles.d.ts +0 -32
- package/dist/utils/scanFiles.js +0 -124
- package/dist/utils/scanSources.d.ts +0 -10
- package/dist/utils/scanSources.js +0 -46
- package/dist/utils/sortModules.d.ts +0 -28
- package/dist/utils/sortModules.js +0 -105
- package/dist/utils/sqlUtil.d.ts +0 -33
- package/dist/utils/sqlUtil.js +0 -146
- package/dist/utils/util.d.ts +0 -172
- package/dist/utils/util.js +0 -517
package/dist/lib/dbHelper.js
DELETED
|
@@ -1,1099 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 数据库助手 - TypeScript 版本
|
|
3
|
-
* 提供数据库 CRUD 操作的封装
|
|
4
|
-
*/
|
|
5
|
-
import { CoreError } from "../types/coreError";
|
|
6
|
-
import { toNumberFromSql, toSqlParams } from "../utils/sqlUtil";
|
|
7
|
-
import { arrayKeysToCamel, canConvertToNumber, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
|
|
8
|
-
import { DbUtils } from "./dbUtils";
|
|
9
|
-
import { Logger } from "./logger";
|
|
10
|
-
import { SqlBuilder } from "./sqlBuilder";
|
|
11
|
-
import { SqlCheck } from "./sqlCheck";
|
|
12
|
-
function quoteIdentMySql(identifier) {
|
|
13
|
-
if (typeof identifier !== "string") {
|
|
14
|
-
throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`);
|
|
15
|
-
}
|
|
16
|
-
const trimmed = identifier.trim();
|
|
17
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
18
|
-
throw new Error(`无效的 SQL 标识符: ${trimmed}`);
|
|
19
|
-
}
|
|
20
|
-
return `\`${trimmed}\``;
|
|
21
|
-
}
|
|
22
|
-
function hasBegin(sql) {
|
|
23
|
-
return typeof sql.begin === "function";
|
|
24
|
-
}
|
|
25
|
-
class DbSqlError extends Error {
|
|
26
|
-
originalError;
|
|
27
|
-
params;
|
|
28
|
-
duration;
|
|
29
|
-
sqlInfo;
|
|
30
|
-
constructor(message, options) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.originalError = options.originalError;
|
|
33
|
-
this.params = options.params;
|
|
34
|
-
this.duration = options.duration;
|
|
35
|
-
this.sqlInfo = options.sqlInfo;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
class TransAbortError extends Error {
|
|
39
|
-
payload;
|
|
40
|
-
constructor(payload) {
|
|
41
|
-
super("TRANSACTION_ABORT");
|
|
42
|
-
this.payload = payload;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function isBeflyResponse(value) {
|
|
46
|
-
if (!isPlainObject(value)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
const record = value;
|
|
50
|
-
return typeof record["code"] === "number" && typeof record["msg"] === "string";
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* 数据库助手类
|
|
54
|
-
*/
|
|
55
|
-
export class DbHelper {
|
|
56
|
-
redis;
|
|
57
|
-
dbName;
|
|
58
|
-
sql = null;
|
|
59
|
-
isTransaction = false;
|
|
60
|
-
idMode;
|
|
61
|
-
static convertBigIntFields(arr, fields) {
|
|
62
|
-
if (arr === null || arr === undefined) {
|
|
63
|
-
return arr;
|
|
64
|
-
}
|
|
65
|
-
const defaultFields = ["id", "pid", "sort"];
|
|
66
|
-
const buildFields = (userFields) => {
|
|
67
|
-
if (!userFields || userFields.length === 0) {
|
|
68
|
-
return defaultFields;
|
|
69
|
-
}
|
|
70
|
-
const merged = ["id", "pid", "sort"];
|
|
71
|
-
for (const f of userFields) {
|
|
72
|
-
if (typeof f !== "string") {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const trimmed = f.trim();
|
|
76
|
-
if (trimmed.length === 0) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!merged.includes(trimmed)) {
|
|
80
|
-
merged.push(trimmed);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return merged;
|
|
84
|
-
};
|
|
85
|
-
const effectiveFields = buildFields(fields);
|
|
86
|
-
const fieldSet = new Set(effectiveFields);
|
|
87
|
-
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
88
|
-
const MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
89
|
-
const convertRecord = (source) => {
|
|
90
|
-
const converted = {};
|
|
91
|
-
for (const [key, value] of Object.entries(source)) {
|
|
92
|
-
let nextValue = value;
|
|
93
|
-
if (value !== undefined && value !== null) {
|
|
94
|
-
const shouldConvert = fieldSet.has(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
|
|
95
|
-
if (shouldConvert) {
|
|
96
|
-
let bigintValue = null;
|
|
97
|
-
if (typeof value === "bigint") {
|
|
98
|
-
bigintValue = value;
|
|
99
|
-
}
|
|
100
|
-
else if (typeof value === "string") {
|
|
101
|
-
// BIGINT 字段应为整数;非整数/非数字字符串不做转换
|
|
102
|
-
if (/^-?\d+$/.test(value)) {
|
|
103
|
-
try {
|
|
104
|
-
bigintValue = BigInt(value);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
bigintValue = null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (bigintValue !== null) {
|
|
112
|
-
const convertedNumber = canConvertToNumber(bigintValue);
|
|
113
|
-
if (convertedNumber !== null) {
|
|
114
|
-
nextValue = convertedNumber;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
converted[key] = nextValue;
|
|
120
|
-
}
|
|
121
|
-
return converted;
|
|
122
|
-
};
|
|
123
|
-
if (Array.isArray(arr)) {
|
|
124
|
-
return arr.map((item) => convertRecord(item));
|
|
125
|
-
}
|
|
126
|
-
if (typeof arr === "object") {
|
|
127
|
-
return convertRecord(arr);
|
|
128
|
-
}
|
|
129
|
-
return arr;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* 构造函数
|
|
133
|
-
* @param redis - Redis 实例
|
|
134
|
-
* @param sql - Bun SQL 客户端(可选,用于事务)
|
|
135
|
-
*/
|
|
136
|
-
constructor(options) {
|
|
137
|
-
this.redis = options.redis;
|
|
138
|
-
if (typeof options.dbName !== "string" || options.dbName.trim() === "") {
|
|
139
|
-
throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串");
|
|
140
|
-
}
|
|
141
|
-
this.dbName = options.dbName;
|
|
142
|
-
this.sql = options.sql || null;
|
|
143
|
-
this.isTransaction = Boolean(options.sql);
|
|
144
|
-
// 默认保持历史行为:timeId
|
|
145
|
-
this.idMode = options.idMode === "autoId" ? "autoId" : "timeId";
|
|
146
|
-
}
|
|
147
|
-
createSqlBuilder() {
|
|
148
|
-
return new SqlBuilder({ quoteIdent: quoteIdentMySql });
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* 获取表的所有字段名
|
|
152
|
-
* @param table - 表名(下划线格式)
|
|
153
|
-
* @returns 字段名数组(下划线格式)
|
|
154
|
-
*/
|
|
155
|
-
async getTableColumns(table) {
|
|
156
|
-
// 查询数据库
|
|
157
|
-
const quotedTable = quoteIdentMySql(table);
|
|
158
|
-
const execRes = await this.executeSelect(`SHOW COLUMNS FROM ${quotedTable}`, []);
|
|
159
|
-
const result = execRes.data;
|
|
160
|
-
if (!result || result.length === 0) {
|
|
161
|
-
throw new Error(`表 ${table} 不存在或没有字段`);
|
|
162
|
-
}
|
|
163
|
-
const columnNames = [];
|
|
164
|
-
for (const row of result) {
|
|
165
|
-
const name = row["Field"];
|
|
166
|
-
if (typeof name === "string" && name.length > 0) {
|
|
167
|
-
columnNames.push(name);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return columnNames;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* 统一的查询参数预处理方法
|
|
174
|
-
*/
|
|
175
|
-
async prepareQueryOptions(options) {
|
|
176
|
-
const cleanWhere = DbUtils.clearDeep(options.where || {});
|
|
177
|
-
const hasJoins = options.joins && options.joins.length > 0;
|
|
178
|
-
// 联查时使用特殊处理逻辑
|
|
179
|
-
if (hasJoins) {
|
|
180
|
-
// 联查时字段直接处理(支持表名.字段名格式)
|
|
181
|
-
const processedFields = (options.fields || []).map((f) => DbUtils.processJoinField(f));
|
|
182
|
-
const normalizedTableRef = DbUtils.normalizeTableRef(options.table);
|
|
183
|
-
const mainQualifier = DbUtils.getJoinMainQualifier(options.table);
|
|
184
|
-
return {
|
|
185
|
-
table: normalizedTableRef,
|
|
186
|
-
tableQualifier: mainQualifier,
|
|
187
|
-
fields: processedFields.length > 0 ? processedFields : ["*"],
|
|
188
|
-
where: DbUtils.processJoinWhere(cleanWhere),
|
|
189
|
-
joins: options.joins,
|
|
190
|
-
orderBy: DbUtils.processJoinOrderBy(options.orderBy || []),
|
|
191
|
-
page: options.page || 1,
|
|
192
|
-
limit: options.limit || 10
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
// 单表查询使用原有逻辑
|
|
196
|
-
const processedFields = await DbUtils.fieldsToSnake(snakeCase(options.table), options.fields || [], this.getTableColumns.bind(this));
|
|
197
|
-
return {
|
|
198
|
-
table: snakeCase(options.table),
|
|
199
|
-
tableQualifier: snakeCase(options.table),
|
|
200
|
-
fields: processedFields,
|
|
201
|
-
where: DbUtils.whereKeysToSnake(cleanWhere),
|
|
202
|
-
joins: undefined,
|
|
203
|
-
orderBy: DbUtils.orderByToSnake(options.orderBy || []),
|
|
204
|
-
page: options.page || 1,
|
|
205
|
-
limit: options.limit || 10
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* 为 builder 添加 JOIN
|
|
210
|
-
*/
|
|
211
|
-
applyJoins(builder, joins) {
|
|
212
|
-
if (!joins || joins.length === 0)
|
|
213
|
-
return;
|
|
214
|
-
for (const join of joins) {
|
|
215
|
-
const processedTable = DbUtils.normalizeTableRef(join.table);
|
|
216
|
-
const type = join.type || "left";
|
|
217
|
-
switch (type) {
|
|
218
|
-
case "inner":
|
|
219
|
-
builder.innerJoin(processedTable, join.on);
|
|
220
|
-
break;
|
|
221
|
-
case "right":
|
|
222
|
-
builder.rightJoin(processedTable, join.on);
|
|
223
|
-
break;
|
|
224
|
-
case "left":
|
|
225
|
-
default:
|
|
226
|
-
builder.leftJoin(processedTable, join.on);
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
async executeSelect(sqlStr, params) {
|
|
232
|
-
return await this.executeWithConn(sqlStr, params);
|
|
233
|
-
}
|
|
234
|
-
async executeRun(sqlStr, params) {
|
|
235
|
-
return await this.executeWithConn(sqlStr, params);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* 执行 SQL(使用 sql.unsafe)
|
|
239
|
-
*
|
|
240
|
-
* - DbHelper 不再负责打印 SQL 调试日志
|
|
241
|
-
* - SQL 信息由调用方基于返回值中的 sql 自行输出
|
|
242
|
-
*/
|
|
243
|
-
async executeWithConn(sqlStr, params) {
|
|
244
|
-
if (!this.sql) {
|
|
245
|
-
throw new Error("数据库连接未初始化");
|
|
246
|
-
}
|
|
247
|
-
// 强制类型检查:只接受字符串类型的 SQL
|
|
248
|
-
if (typeof sqlStr !== "string") {
|
|
249
|
-
throw new Error(`executeWithConn 只接受字符串类型的 SQL,收到类型: ${typeof sqlStr},值: ${JSON.stringify(sqlStr)}`);
|
|
250
|
-
}
|
|
251
|
-
// 记录开始时间
|
|
252
|
-
const startTime = Date.now();
|
|
253
|
-
const safeParams = toSqlParams(params);
|
|
254
|
-
try {
|
|
255
|
-
// 使用 sql.unsafe 执行查询
|
|
256
|
-
let result;
|
|
257
|
-
if (safeParams.length > 0) {
|
|
258
|
-
result = await this.sql.unsafe(sqlStr, safeParams);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
result = await this.sql.unsafe(sqlStr);
|
|
262
|
-
}
|
|
263
|
-
// 计算执行时间
|
|
264
|
-
const duration = Date.now() - startTime;
|
|
265
|
-
const sql = {
|
|
266
|
-
sql: sqlStr,
|
|
267
|
-
params: safeParams,
|
|
268
|
-
duration: duration
|
|
269
|
-
};
|
|
270
|
-
return {
|
|
271
|
-
data: result,
|
|
272
|
-
sql: sql
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
const duration = Date.now() - startTime;
|
|
277
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
278
|
-
throw new DbSqlError(`SQL执行失败: ${msg}`, {
|
|
279
|
-
originalError: error,
|
|
280
|
-
params: safeParams,
|
|
281
|
-
duration: duration,
|
|
282
|
-
sqlInfo: {
|
|
283
|
-
sql: sqlStr,
|
|
284
|
-
params: safeParams,
|
|
285
|
-
duration: duration
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* 执行原生 SQL(内部工具/同步脚本专用)
|
|
292
|
-
*
|
|
293
|
-
* - 复用当前 DbHelper 持有的连接/事务
|
|
294
|
-
* - 统一走 executeWithConn,保持参数校验与错误行为一致
|
|
295
|
-
*/
|
|
296
|
-
async unsafe(sqlStr, params) {
|
|
297
|
-
return await this.executeWithConn(sqlStr, params);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* 检查表是否存在
|
|
301
|
-
* @param tableName - 表名(支持小驼峰,会自动转换为下划线)
|
|
302
|
-
* @returns 表是否存在
|
|
303
|
-
*/
|
|
304
|
-
async tableExists(tableName) {
|
|
305
|
-
// 将表名转换为下划线格式
|
|
306
|
-
const snakeTableName = snakeCase(tableName);
|
|
307
|
-
const execRes = await this.executeSelect("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
308
|
-
const exists = (execRes.data?.[0]?.count || 0) > 0;
|
|
309
|
-
return {
|
|
310
|
-
data: exists,
|
|
311
|
-
sql: execRes.sql
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* 查询记录数
|
|
316
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
317
|
-
* @param options.where - 查询条件
|
|
318
|
-
* @param options.joins - 多表联查选项
|
|
319
|
-
* @example
|
|
320
|
-
* // 查询总数
|
|
321
|
-
* const count = await db.getCount({ table: 'user' });
|
|
322
|
-
* // 查询指定条件的记录数
|
|
323
|
-
* const activeCount = await db.getCount({ table: 'user', where: { state: 1 } });
|
|
324
|
-
* // 联查计数
|
|
325
|
-
* const count = await db.getCount({
|
|
326
|
-
* table: 'order o',
|
|
327
|
-
* joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
|
|
328
|
-
* where: { 'o.state': 1 }
|
|
329
|
-
* });
|
|
330
|
-
*/
|
|
331
|
-
async getCount(options) {
|
|
332
|
-
const { table, where, joins, tableQualifier } = await this.prepareQueryOptions(options);
|
|
333
|
-
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
334
|
-
const builder = this.createSqlBuilder()
|
|
335
|
-
.selectRaw("COUNT(*) as count")
|
|
336
|
-
.from(table)
|
|
337
|
-
.where(DbUtils.addDefaultStateFilter(where, tableQualifier, hasJoins));
|
|
338
|
-
// 添加 JOIN
|
|
339
|
-
this.applyJoins(builder, joins);
|
|
340
|
-
const { sql, params } = builder.toSelectSql();
|
|
341
|
-
const execRes = await this.executeSelect(sql, params);
|
|
342
|
-
const count = execRes.data?.[0]?.count || 0;
|
|
343
|
-
return {
|
|
344
|
-
data: count,
|
|
345
|
-
sql: execRes.sql
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* 查询单条数据
|
|
350
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名如 'order o')
|
|
351
|
-
* @param options.fields - 字段列表(联查时需带表别名,如 'o.id', 'u.username')
|
|
352
|
-
* @param options.joins - 多表联查选项
|
|
353
|
-
* @returns DbResult<TItem>
|
|
354
|
-
*
|
|
355
|
-
* 语义说明(重要):
|
|
356
|
-
* - 本方法不再用 `null` 表示“未命中”。
|
|
357
|
-
* - 当查询未命中(或数据反序列化失败)时,`data` 将返回空对象 `{}`。
|
|
358
|
-
* - 因此业务侧应通过关键字段判断是否存在(例如 `if (!res.data?.id) { ... }`)。
|
|
359
|
-
* @example
|
|
360
|
-
* // 单表查询
|
|
361
|
-
* getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
362
|
-
* // 联查
|
|
363
|
-
* getOne({
|
|
364
|
-
* table: 'order o',
|
|
365
|
-
* joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
|
|
366
|
-
* fields: ['o.id', 'o.totalAmount', 'u.username'],
|
|
367
|
-
* where: { 'o.id': 1 }
|
|
368
|
-
* })
|
|
369
|
-
*/
|
|
370
|
-
async getOne(options) {
|
|
371
|
-
const { table, fields, where, joins, tableQualifier } = await this.prepareQueryOptions(options);
|
|
372
|
-
const hasJoins = Array.isArray(joins) && joins.length > 0;
|
|
373
|
-
const builder = this.createSqlBuilder()
|
|
374
|
-
.select(fields)
|
|
375
|
-
.from(table)
|
|
376
|
-
.where(DbUtils.addDefaultStateFilter(where, tableQualifier, hasJoins));
|
|
377
|
-
// 添加 JOIN
|
|
378
|
-
this.applyJoins(builder, joins);
|
|
379
|
-
const { sql, params } = builder.toSelectSql();
|
|
380
|
-
const execRes = await this.executeSelect(sql, params);
|
|
381
|
-
const result = execRes.data;
|
|
382
|
-
// 字段名转换:下划线 → 小驼峰
|
|
383
|
-
const row = result?.[0] || null;
|
|
384
|
-
if (!row) {
|
|
385
|
-
return {
|
|
386
|
-
data: {},
|
|
387
|
-
sql: execRes.sql
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const camelRow = keysToCamel(row);
|
|
391
|
-
// 反序列化数组字段(JSON 字符串 → 数组)
|
|
392
|
-
const deserialized = DbUtils.deserializeArrayFields(camelRow);
|
|
393
|
-
if (!deserialized) {
|
|
394
|
-
return {
|
|
395
|
-
data: {},
|
|
396
|
-
sql: execRes.sql
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
400
|
-
const convertedList = DbHelper.convertBigIntFields([deserialized], options.bigint);
|
|
401
|
-
const data = convertedList[0] ?? deserialized;
|
|
402
|
-
return {
|
|
403
|
-
data: data,
|
|
404
|
-
sql: execRes.sql
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* 语义化别名:getDetail(与 getOne 一致)
|
|
409
|
-
*
|
|
410
|
-
* 说明:Befly 早期业务侧习惯用 getDetail 表达“查详情”;这里不引入新的查询逻辑,直接复用 getOne。
|
|
411
|
-
*
|
|
412
|
-
* 语义说明:与 getOne 完全一致,未命中时 `data` 返回 `{}`。
|
|
413
|
-
*/
|
|
414
|
-
async getDetail(options) {
|
|
415
|
-
return await this.getOne(options);
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* 查询列表(带分页)
|
|
419
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
|
|
420
|
-
* @param options.fields - 字段列表(联查时需带表别名)
|
|
421
|
-
* @param options.joins - 多表联查选项
|
|
422
|
-
* @example
|
|
423
|
-
* // 单表分页
|
|
424
|
-
* getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
|
|
425
|
-
* // 联查分页
|
|
426
|
-
* getList({
|
|
427
|
-
* table: 'order o',
|
|
428
|
-
* joins: [
|
|
429
|
-
* { table: 'user u', on: 'o.user_id = u.id' },
|
|
430
|
-
* { table: 'product p', on: 'o.product_id = p.id' }
|
|
431
|
-
* ],
|
|
432
|
-
* fields: ['o.id', 'o.totalAmount', 'u.username', 'p.name AS productName'],
|
|
433
|
-
* where: { 'o.status': 'paid' },
|
|
434
|
-
* orderBy: ['o.createdAt#DESC'],
|
|
435
|
-
* page: 1,
|
|
436
|
-
* limit: 10
|
|
437
|
-
* })
|
|
438
|
-
*/
|
|
439
|
-
async getList(options) {
|
|
440
|
-
const prepared = await this.prepareQueryOptions(options);
|
|
441
|
-
// 参数上限校验
|
|
442
|
-
if (prepared.page < 1 || prepared.page > 10000) {
|
|
443
|
-
throw new Error(`页码必须在 1 到 10000 之间 (table: ${options.table}, page: ${prepared.page}, limit: ${prepared.limit})`);
|
|
444
|
-
}
|
|
445
|
-
if (prepared.limit < 1 || prepared.limit > 1000) {
|
|
446
|
-
throw new Error(`每页数量必须在 1 到 1000 之间 (table: ${options.table}, page: ${prepared.page}, limit: ${prepared.limit})`);
|
|
447
|
-
}
|
|
448
|
-
// 构建查询
|
|
449
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, Array.isArray(prepared.joins) && prepared.joins.length > 0);
|
|
450
|
-
// 查询总数
|
|
451
|
-
const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
|
|
452
|
-
// 添加 JOIN(计数也需要)
|
|
453
|
-
this.applyJoins(countBuilder, prepared.joins);
|
|
454
|
-
const { sql: countSql, params: countParams } = countBuilder.toSelectSql();
|
|
455
|
-
const countExecRes = await this.executeSelect(countSql, countParams);
|
|
456
|
-
const total = countExecRes.data?.[0]?.total || 0;
|
|
457
|
-
// 如果总数为 0,直接返回,不执行第二次查询
|
|
458
|
-
if (total === 0) {
|
|
459
|
-
const offset = (prepared.page - 1) * prepared.limit;
|
|
460
|
-
const dataBuilder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
|
|
461
|
-
// 添加 JOIN
|
|
462
|
-
this.applyJoins(dataBuilder, prepared.joins);
|
|
463
|
-
// 只有用户明确指定了 orderBy 才添加排序
|
|
464
|
-
if (prepared.orderBy && prepared.orderBy.length > 0) {
|
|
465
|
-
dataBuilder.orderBy(prepared.orderBy);
|
|
466
|
-
}
|
|
467
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
468
|
-
return {
|
|
469
|
-
data: {
|
|
470
|
-
lists: [],
|
|
471
|
-
total: 0,
|
|
472
|
-
page: prepared.page,
|
|
473
|
-
limit: prepared.limit,
|
|
474
|
-
pages: 0
|
|
475
|
-
},
|
|
476
|
-
sql: {
|
|
477
|
-
count: countExecRes.sql,
|
|
478
|
-
data: {
|
|
479
|
-
sql: dataSql,
|
|
480
|
-
params: dataParams,
|
|
481
|
-
duration: 0
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
// 查询数据
|
|
487
|
-
const offset = (prepared.page - 1) * prepared.limit;
|
|
488
|
-
const dataBuilder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
|
|
489
|
-
// 添加 JOIN
|
|
490
|
-
this.applyJoins(dataBuilder, prepared.joins);
|
|
491
|
-
// 只有用户明确指定了 orderBy 才添加排序
|
|
492
|
-
if (prepared.orderBy && prepared.orderBy.length > 0) {
|
|
493
|
-
dataBuilder.orderBy(prepared.orderBy);
|
|
494
|
-
}
|
|
495
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
496
|
-
const dataExecRes = await this.executeSelect(dataSql, dataParams);
|
|
497
|
-
const list = dataExecRes.data || [];
|
|
498
|
-
// 字段名转换:下划线 → 小驼峰
|
|
499
|
-
const camelList = arrayKeysToCamel(list);
|
|
500
|
-
// 反序列化数组字段
|
|
501
|
-
const deserializedList = camelList.map((item) => DbUtils.deserializeArrayFields(item)).filter((item) => item !== null);
|
|
502
|
-
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
503
|
-
return {
|
|
504
|
-
data: {
|
|
505
|
-
lists: DbHelper.convertBigIntFields(deserializedList, options.bigint),
|
|
506
|
-
total: total,
|
|
507
|
-
page: prepared.page,
|
|
508
|
-
limit: prepared.limit,
|
|
509
|
-
pages: Math.ceil(total / prepared.limit)
|
|
510
|
-
},
|
|
511
|
-
sql: {
|
|
512
|
-
count: countExecRes.sql,
|
|
513
|
-
data: dataExecRes.sql
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* 查询所有数据(不分页,有上限保护)
|
|
519
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
|
|
520
|
-
* @param options.fields - 字段列表(联查时需带表别名)
|
|
521
|
-
* @param options.joins - 多表联查选项
|
|
522
|
-
* ⚠️ 警告:此方法会查询大量数据,建议使用 getList 分页查询
|
|
523
|
-
* @example
|
|
524
|
-
* // 单表查询
|
|
525
|
-
* getAll({ table: 'userProfile', fields: ['userId', 'userName'] })
|
|
526
|
-
* // 联查
|
|
527
|
-
* getAll({
|
|
528
|
-
* table: 'order o',
|
|
529
|
-
* joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
|
|
530
|
-
* fields: ['o.id', 'u.username'],
|
|
531
|
-
* where: { 'o.state': 1 }
|
|
532
|
-
* })
|
|
533
|
-
*/
|
|
534
|
-
async getAll(options) {
|
|
535
|
-
// 添加硬性上限保护,防止内存溢出
|
|
536
|
-
const MAX_LIMIT = 10000;
|
|
537
|
-
const WARNING_LIMIT = 1000;
|
|
538
|
-
const prepareOptions = {
|
|
539
|
-
table: options.table,
|
|
540
|
-
page: 1,
|
|
541
|
-
limit: 10
|
|
542
|
-
};
|
|
543
|
-
if (options.fields !== undefined)
|
|
544
|
-
prepareOptions.fields = options.fields;
|
|
545
|
-
if (options.bigint !== undefined)
|
|
546
|
-
prepareOptions.bigint = options.bigint;
|
|
547
|
-
if (options.where !== undefined)
|
|
548
|
-
prepareOptions.where = options.where;
|
|
549
|
-
if (options.joins !== undefined)
|
|
550
|
-
prepareOptions.joins = options.joins;
|
|
551
|
-
if (options.orderBy !== undefined)
|
|
552
|
-
prepareOptions.orderBy = options.orderBy;
|
|
553
|
-
const prepared = await this.prepareQueryOptions(prepareOptions);
|
|
554
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, Array.isArray(prepared.joins) && prepared.joins.length > 0);
|
|
555
|
-
// 查询真实总数
|
|
556
|
-
const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
|
|
557
|
-
// 添加 JOIN(计数也需要)
|
|
558
|
-
this.applyJoins(countBuilder, prepared.joins);
|
|
559
|
-
const { sql: countSql, params: countParams } = countBuilder.toSelectSql();
|
|
560
|
-
const countExecRes = await this.executeSelect(countSql, countParams);
|
|
561
|
-
const total = countExecRes.data?.[0]?.total || 0;
|
|
562
|
-
// 如果总数为 0,直接返回
|
|
563
|
-
if (total === 0) {
|
|
564
|
-
return {
|
|
565
|
-
data: {
|
|
566
|
-
lists: [],
|
|
567
|
-
total: 0
|
|
568
|
-
},
|
|
569
|
-
sql: {
|
|
570
|
-
count: countExecRes.sql
|
|
571
|
-
}
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
// 查询数据(受上限保护)
|
|
575
|
-
const dataBuilder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(MAX_LIMIT);
|
|
576
|
-
// 添加 JOIN
|
|
577
|
-
this.applyJoins(dataBuilder, prepared.joins);
|
|
578
|
-
if (prepared.orderBy && prepared.orderBy.length > 0) {
|
|
579
|
-
dataBuilder.orderBy(prepared.orderBy);
|
|
580
|
-
}
|
|
581
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
582
|
-
const dataExecRes = await this.executeSelect(dataSql, dataParams);
|
|
583
|
-
const result = dataExecRes.data || [];
|
|
584
|
-
// 警告日志:返回数据超过警告阈值
|
|
585
|
-
if (result.length >= WARNING_LIMIT) {
|
|
586
|
-
Logger.warn({ table: options.table, count: result.length, total: total, msg: "getAll 返回数据过多,建议使用 getList 分页查询" });
|
|
587
|
-
}
|
|
588
|
-
// 如果达到上限,额外警告
|
|
589
|
-
if (result.length >= MAX_LIMIT) {
|
|
590
|
-
Logger.warn({ table: options.table, limit: MAX_LIMIT, total: total, msg: `getAll 达到最大限制 ${MAX_LIMIT},实际总数 ${total},只返回前 ${MAX_LIMIT} 条` });
|
|
591
|
-
}
|
|
592
|
-
// 字段名转换:下划线 → 小驼峰
|
|
593
|
-
const camelResult = arrayKeysToCamel(result);
|
|
594
|
-
// 反序列化数组字段
|
|
595
|
-
const deserializedList = camelResult.map((item) => DbUtils.deserializeArrayFields(item)).filter((item) => item !== null);
|
|
596
|
-
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
597
|
-
const lists = DbHelper.convertBigIntFields(deserializedList, options.bigint);
|
|
598
|
-
return {
|
|
599
|
-
data: {
|
|
600
|
-
lists: lists,
|
|
601
|
-
total: total
|
|
602
|
-
},
|
|
603
|
-
sql: {
|
|
604
|
-
count: countExecRes.sql,
|
|
605
|
-
data: dataExecRes.sql
|
|
606
|
-
}
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* 插入数据(自动生成 ID、时间戳、state)
|
|
611
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
612
|
-
*/
|
|
613
|
-
async insData(options) {
|
|
614
|
-
const { table, data } = options;
|
|
615
|
-
const snakeTable = snakeCase(table);
|
|
616
|
-
const now = Date.now();
|
|
617
|
-
let processed;
|
|
618
|
-
if (this.idMode === "autoId") {
|
|
619
|
-
processed = DbUtils.buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
let id;
|
|
623
|
-
try {
|
|
624
|
-
id = await this.redis.genTimeID();
|
|
625
|
-
}
|
|
626
|
-
catch (error) {
|
|
627
|
-
throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, { cause: error });
|
|
628
|
-
}
|
|
629
|
-
processed = DbUtils.buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
630
|
-
}
|
|
631
|
-
// 入口校验:保证进入 SqlBuilder 的数据无 undefined
|
|
632
|
-
SqlCheck.assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
|
|
633
|
-
// 构建 SQL
|
|
634
|
-
const builder = this.createSqlBuilder();
|
|
635
|
-
const { sql, params } = builder.toInsertSql(snakeTable, processed);
|
|
636
|
-
// 执行
|
|
637
|
-
const execRes = await this.executeRun(sql, params);
|
|
638
|
-
const processedId = processed["id"];
|
|
639
|
-
const processedIdNum = typeof processedId === "number" ? processedId : 0;
|
|
640
|
-
const lastInsertRowidNum = toNumberFromSql(execRes.data?.lastInsertRowid);
|
|
641
|
-
// timeId:优先返回显式写入的 id;autoId:依赖 lastInsertRowid
|
|
642
|
-
const insertedId = this.idMode === "autoId" ? lastInsertRowidNum || 0 : processedIdNum || lastInsertRowidNum || 0;
|
|
643
|
-
if (this.idMode === "autoId" && insertedId <= 0) {
|
|
644
|
-
throw new Error(`插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`);
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
data: insertedId,
|
|
648
|
-
sql: execRes.sql
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* 批量插入数据(真正的批量操作)
|
|
653
|
-
* 使用 INSERT INTO ... VALUES (...), (...), (...) 语法
|
|
654
|
-
* 自动生成系统字段并包装在事务中
|
|
655
|
-
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
656
|
-
*/
|
|
657
|
-
async insBatch(table, dataList) {
|
|
658
|
-
// 空数组直接返回
|
|
659
|
-
if (dataList.length === 0) {
|
|
660
|
-
const sql = { sql: "", params: [], duration: 0 };
|
|
661
|
-
return {
|
|
662
|
-
data: [],
|
|
663
|
-
sql: sql
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
// 限制批量大小
|
|
667
|
-
const MAX_BATCH_SIZE = 1000;
|
|
668
|
-
if (dataList.length > MAX_BATCH_SIZE) {
|
|
669
|
-
throw new Error(`批量插入数量 ${dataList.length} 超过最大限制 ${MAX_BATCH_SIZE}`);
|
|
670
|
-
}
|
|
671
|
-
// 转换表名:小驼峰 → 下划线
|
|
672
|
-
const snakeTable = snakeCase(table);
|
|
673
|
-
const now = Date.now();
|
|
674
|
-
// 处理所有数据(自动添加系统字段)
|
|
675
|
-
let ids = [];
|
|
676
|
-
let processedList;
|
|
677
|
-
if (this.idMode === "autoId") {
|
|
678
|
-
processedList = dataList.map((data) => {
|
|
679
|
-
return DbUtils.buildInsertRow({ idMode: "autoId", data: data, now: now });
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
// 批量生成 ID(逐个获取)
|
|
684
|
-
const nextIds = [];
|
|
685
|
-
for (let i = 0; i < dataList.length; i++) {
|
|
686
|
-
nextIds.push(await this.redis.genTimeID());
|
|
687
|
-
}
|
|
688
|
-
ids = nextIds;
|
|
689
|
-
processedList = dataList.map((data, index) => {
|
|
690
|
-
const id = nextIds[index];
|
|
691
|
-
if (typeof id !== "number") {
|
|
692
|
-
throw new Error(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${snakeTable})`);
|
|
693
|
-
}
|
|
694
|
-
return DbUtils.buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
// 入口校验:保证进入 SqlBuilder 的批量数据结构一致且无 undefined
|
|
698
|
-
const insertFields = SqlCheck.assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
699
|
-
// 构建批量插入 SQL
|
|
700
|
-
const builder = this.createSqlBuilder();
|
|
701
|
-
const { sql, params } = builder.toInsertSql(snakeTable, processedList);
|
|
702
|
-
// 在事务中执行批量插入
|
|
703
|
-
try {
|
|
704
|
-
const execRes = await this.executeRun(sql, params);
|
|
705
|
-
if (this.idMode === "autoId") {
|
|
706
|
-
const firstId = toNumberFromSql(execRes.data?.lastInsertRowid);
|
|
707
|
-
if (firstId <= 0) {
|
|
708
|
-
throw new Error(`批量插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`);
|
|
709
|
-
}
|
|
710
|
-
// 说明:这里假设 auto_increment_increment = 1(默认)。
|
|
711
|
-
// 如需支持非 1,请在此处增加查询 @@auto_increment_increment 并调整推导规则。
|
|
712
|
-
const outIds = [];
|
|
713
|
-
for (let i = 0; i < dataList.length; i++) {
|
|
714
|
-
outIds.push(firstId + i);
|
|
715
|
-
}
|
|
716
|
-
return {
|
|
717
|
-
data: outIds,
|
|
718
|
-
sql: execRes.sql
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
data: ids,
|
|
723
|
-
sql: execRes.sql
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
catch (error) {
|
|
727
|
-
Logger.error({
|
|
728
|
-
err: error,
|
|
729
|
-
table: table,
|
|
730
|
-
snakeTable: snakeTable,
|
|
731
|
-
count: dataList.length,
|
|
732
|
-
fields: insertFields,
|
|
733
|
-
msg: "批量插入失败"
|
|
734
|
-
});
|
|
735
|
-
throw new CoreError({
|
|
736
|
-
kind: "runtime",
|
|
737
|
-
message: "批量插入失败",
|
|
738
|
-
logged: true,
|
|
739
|
-
cause: error,
|
|
740
|
-
meta: {
|
|
741
|
-
subsystem: "sql",
|
|
742
|
-
operation: "insBatch",
|
|
743
|
-
table: table,
|
|
744
|
-
snakeTable: snakeTable,
|
|
745
|
-
count: dataList.length,
|
|
746
|
-
fields: insertFields
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
async delForceBatch(table, ids) {
|
|
752
|
-
if (ids.length === 0) {
|
|
753
|
-
const sql = { sql: "", params: [], duration: 0 };
|
|
754
|
-
return {
|
|
755
|
-
data: 0,
|
|
756
|
-
sql: sql
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
const snakeTable = snakeCase(table);
|
|
760
|
-
const query = SqlBuilder.toDeleteInSql({
|
|
761
|
-
table: snakeTable,
|
|
762
|
-
idField: "id",
|
|
763
|
-
ids: ids,
|
|
764
|
-
quoteIdent: quoteIdentMySql
|
|
765
|
-
});
|
|
766
|
-
const execRes = await this.executeRun(query.sql, query.params);
|
|
767
|
-
const changes = toNumberFromSql(execRes.data?.changes);
|
|
768
|
-
return {
|
|
769
|
-
data: changes,
|
|
770
|
-
sql: execRes.sql
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
async updBatch(table, dataList) {
|
|
774
|
-
if (dataList.length === 0) {
|
|
775
|
-
const sql = { sql: "", params: [], duration: 0 };
|
|
776
|
-
return {
|
|
777
|
-
data: 0,
|
|
778
|
-
sql: sql
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
const snakeTable = snakeCase(table);
|
|
782
|
-
const now = Date.now();
|
|
783
|
-
const processedList = [];
|
|
784
|
-
const fieldSet = new Set();
|
|
785
|
-
for (const item of dataList) {
|
|
786
|
-
const userData = DbUtils.buildPartialUpdateData({ data: item.data, allowState: true });
|
|
787
|
-
for (const key of Object.keys(userData)) {
|
|
788
|
-
fieldSet.add(key);
|
|
789
|
-
}
|
|
790
|
-
processedList.push({ id: item.id, data: userData });
|
|
791
|
-
}
|
|
792
|
-
const fields = Array.from(fieldSet).sort();
|
|
793
|
-
if (fields.length === 0) {
|
|
794
|
-
const sql = { sql: "", params: [], duration: 0 };
|
|
795
|
-
return {
|
|
796
|
-
data: 0,
|
|
797
|
-
sql: sql
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
const query = SqlBuilder.toUpdateCaseByIdSql({
|
|
801
|
-
table: snakeTable,
|
|
802
|
-
idField: "id",
|
|
803
|
-
rows: processedList,
|
|
804
|
-
fields: fields,
|
|
805
|
-
quoteIdent: quoteIdentMySql,
|
|
806
|
-
updatedAtField: "updated_at",
|
|
807
|
-
updatedAtValue: now,
|
|
808
|
-
stateField: "state",
|
|
809
|
-
stateGtZero: true
|
|
810
|
-
});
|
|
811
|
-
const execRes = await this.executeRun(query.sql, query.params);
|
|
812
|
-
const changes = toNumberFromSql(execRes.data?.changes);
|
|
813
|
-
return {
|
|
814
|
-
data: changes,
|
|
815
|
-
sql: execRes.sql
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* 更新数据(强制更新时间戳,系统字段不可修改)
|
|
820
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
821
|
-
*/
|
|
822
|
-
async updData(options) {
|
|
823
|
-
const { table, data, where } = options;
|
|
824
|
-
// 清理条件(排除 null 和 undefined,递归)
|
|
825
|
-
const cleanWhere = DbUtils.clearDeep(where);
|
|
826
|
-
// 转换表名:小驼峰 → 下划线
|
|
827
|
-
const snakeTable = snakeCase(table);
|
|
828
|
-
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
829
|
-
const processed = DbUtils.buildUpdateRow({ data: data, now: Date.now(), allowState: true });
|
|
830
|
-
// 构建 SQL
|
|
831
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
832
|
-
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
833
|
-
const { sql, params } = builder.toUpdateSql(snakeTable, processed);
|
|
834
|
-
// 执行
|
|
835
|
-
const execRes = await this.executeRun(sql, params);
|
|
836
|
-
const changes = toNumberFromSql(execRes.data?.changes);
|
|
837
|
-
return {
|
|
838
|
-
data: changes,
|
|
839
|
-
sql: execRes.sql
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* 软删除数据(deleted_at 设置为当前时间,state 设置为 0)
|
|
844
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
845
|
-
*/
|
|
846
|
-
async delData(options) {
|
|
847
|
-
const { table, where } = options;
|
|
848
|
-
return await this.updData({
|
|
849
|
-
table: table,
|
|
850
|
-
data: { state: 0, deleted_at: Date.now() },
|
|
851
|
-
where: where
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* 硬删除数据(物理删除,不可恢复)
|
|
856
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
857
|
-
*/
|
|
858
|
-
async delForce(options) {
|
|
859
|
-
const { table, where } = options;
|
|
860
|
-
// 转换表名:小驼峰 → 下划线
|
|
861
|
-
const snakeTable = snakeCase(table);
|
|
862
|
-
// 清理条件字段(排除 null 和 undefined,递归)
|
|
863
|
-
const cleanWhere = DbUtils.clearDeep(where);
|
|
864
|
-
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
865
|
-
// 物理删除
|
|
866
|
-
const builder = this.createSqlBuilder().where(snakeWhere);
|
|
867
|
-
const { sql, params } = builder.toDeleteSql(snakeTable);
|
|
868
|
-
const execRes = await this.executeRun(sql, params);
|
|
869
|
-
const changes = toNumberFromSql(execRes.data?.changes);
|
|
870
|
-
return {
|
|
871
|
-
data: changes,
|
|
872
|
-
sql: execRes.sql
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* 禁用数据(设置 state=2)
|
|
877
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
878
|
-
*/
|
|
879
|
-
async disableData(options) {
|
|
880
|
-
const { table, where } = options;
|
|
881
|
-
return await this.updData({
|
|
882
|
-
table: table,
|
|
883
|
-
data: {
|
|
884
|
-
state: 2
|
|
885
|
-
},
|
|
886
|
-
where: where
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* 启用数据(设置 state=1)
|
|
891
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
892
|
-
*/
|
|
893
|
-
async enableData(options) {
|
|
894
|
-
const { table, where } = options;
|
|
895
|
-
return await this.updData({
|
|
896
|
-
table: table,
|
|
897
|
-
data: {
|
|
898
|
-
state: 1
|
|
899
|
-
},
|
|
900
|
-
where: where
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* 执行事务
|
|
905
|
-
* 使用 Bun SQL 的 begin 方法开启事务
|
|
906
|
-
*/
|
|
907
|
-
async trans(callback) {
|
|
908
|
-
if (this.isTransaction) {
|
|
909
|
-
// 已经在事务中,直接执行回调
|
|
910
|
-
const innerResult = await callback(this);
|
|
911
|
-
if (isBeflyResponse(innerResult) && innerResult.code !== 0) {
|
|
912
|
-
throw new TransAbortError(innerResult);
|
|
913
|
-
}
|
|
914
|
-
return innerResult;
|
|
915
|
-
}
|
|
916
|
-
const sql = this.sql;
|
|
917
|
-
if (!sql) {
|
|
918
|
-
throw new Error("数据库连接未初始化");
|
|
919
|
-
}
|
|
920
|
-
if (!hasBegin(sql)) {
|
|
921
|
-
throw new Error("当前 SQL 客户端不支持事务 begin() 方法");
|
|
922
|
-
}
|
|
923
|
-
// 使用 Bun SQL 的 begin 方法开启事务
|
|
924
|
-
// begin 方法会自动处理 commit/rollback
|
|
925
|
-
try {
|
|
926
|
-
return await sql.begin(async (tx) => {
|
|
927
|
-
const trans = new DbHelper({ redis: this.redis, dbName: this.dbName, sql: tx, idMode: this.idMode });
|
|
928
|
-
const result = await callback(trans);
|
|
929
|
-
if (isBeflyResponse(result) && result.code !== 0) {
|
|
930
|
-
throw new TransAbortError(result);
|
|
931
|
-
}
|
|
932
|
-
return result;
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
catch (error) {
|
|
936
|
-
if (error instanceof TransAbortError) {
|
|
937
|
-
return error.payload;
|
|
938
|
-
}
|
|
939
|
-
throw error;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* 执行原始 SQL
|
|
944
|
-
*/
|
|
945
|
-
async query(sql, params) {
|
|
946
|
-
return await this.executeWithConn(sql, params);
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* 检查数据是否存在(优化性能)
|
|
950
|
-
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
951
|
-
*/
|
|
952
|
-
async exists(options) {
|
|
953
|
-
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
954
|
-
throw new Error("exists 不支持 joins(请使用显式 query 或拆分查询)");
|
|
955
|
-
}
|
|
956
|
-
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
957
|
-
if (!rawTable) {
|
|
958
|
-
throw new Error("exists.table 不能为空");
|
|
959
|
-
}
|
|
960
|
-
if (rawTable.includes(" ")) {
|
|
961
|
-
throw new Error(`exists 不支持别名表写法(table: ${rawTable})`);
|
|
962
|
-
}
|
|
963
|
-
if (rawTable.includes(".")) {
|
|
964
|
-
throw new Error(`exists 不支持 schema.table 写法(table: ${rawTable})`);
|
|
965
|
-
}
|
|
966
|
-
const snakeTable = snakeCase(rawTable);
|
|
967
|
-
const cleanWhere = DbUtils.clearDeep(options.where || {});
|
|
968
|
-
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
969
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
970
|
-
// 使用 COUNT(1) 实现:语义清晰、适配现有返回结构
|
|
971
|
-
const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
|
|
972
|
-
const { sql, params } = builder.toSelectSql();
|
|
973
|
-
const execRes = await this.executeSelect(sql, params);
|
|
974
|
-
const exists = (execRes.data?.[0]?.cnt || 0) > 0;
|
|
975
|
-
return { data: exists, sql: execRes.sql };
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* 查询单个字段值(带字段名验证)
|
|
979
|
-
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
980
|
-
*/
|
|
981
|
-
async getFieldValue(options) {
|
|
982
|
-
const field = options.field;
|
|
983
|
-
if (Array.isArray(options.joins) && options.joins.length > 0) {
|
|
984
|
-
throw new Error("getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
|
|
985
|
-
}
|
|
986
|
-
const rawTable = typeof options.table === "string" ? options.table.trim() : "";
|
|
987
|
-
if (!rawTable) {
|
|
988
|
-
throw new Error("getFieldValue.table 不能为空");
|
|
989
|
-
}
|
|
990
|
-
if (rawTable.includes(" ")) {
|
|
991
|
-
throw new Error(`getFieldValue 不支持别名表写法(table: ${rawTable})`);
|
|
992
|
-
}
|
|
993
|
-
if (rawTable.includes(".")) {
|
|
994
|
-
throw new Error(`getFieldValue 不支持 schema.table 写法(table: ${rawTable})`);
|
|
995
|
-
}
|
|
996
|
-
// (其余逻辑保持不变)
|
|
997
|
-
// 验证字段名格式(只允许字母、数字、下划线)
|
|
998
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
999
|
-
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
|
|
1000
|
-
}
|
|
1001
|
-
const oneOptions = {
|
|
1002
|
-
table: options.table
|
|
1003
|
-
};
|
|
1004
|
-
if (options.where !== undefined)
|
|
1005
|
-
oneOptions.where = options.where;
|
|
1006
|
-
if (options.joins !== undefined)
|
|
1007
|
-
oneOptions.joins = options.joins;
|
|
1008
|
-
if (options.orderBy !== undefined)
|
|
1009
|
-
oneOptions.orderBy = options.orderBy;
|
|
1010
|
-
if (options.page !== undefined)
|
|
1011
|
-
oneOptions.page = options.page;
|
|
1012
|
-
if (options.limit !== undefined)
|
|
1013
|
-
oneOptions.limit = options.limit;
|
|
1014
|
-
oneOptions.fields = [field];
|
|
1015
|
-
const oneRes = await this.getOne(oneOptions);
|
|
1016
|
-
const result = oneRes.data;
|
|
1017
|
-
if (!isPlainObject(result)) {
|
|
1018
|
-
return {
|
|
1019
|
-
data: null,
|
|
1020
|
-
sql: oneRes.sql
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
// 尝试直接访问字段(小驼峰)
|
|
1024
|
-
if (Object.hasOwn(result, field)) {
|
|
1025
|
-
return {
|
|
1026
|
-
data: result[field],
|
|
1027
|
-
sql: oneRes.sql
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
// 转换为小驼峰格式再尝试访问(支持用户传入下划线格式)
|
|
1031
|
-
const camelField = field.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1032
|
-
if (camelField !== field && Object.hasOwn(result, camelField)) {
|
|
1033
|
-
return {
|
|
1034
|
-
data: result[camelField],
|
|
1035
|
-
sql: oneRes.sql
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
// 转换为下划线格式再尝试访问(支持用户传入小驼峰格式)
|
|
1039
|
-
const snakeField = field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1040
|
-
if (snakeField !== field && Object.hasOwn(result, snakeField)) {
|
|
1041
|
-
return {
|
|
1042
|
-
data: result[snakeField],
|
|
1043
|
-
sql: oneRes.sql
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
return {
|
|
1047
|
-
data: null,
|
|
1048
|
-
sql: oneRes.sql
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* 自增字段(安全实现,防止 SQL 注入)
|
|
1053
|
-
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
1054
|
-
* @param field - 字段名(支持小驼峰或下划线格式,会自动转换)
|
|
1055
|
-
*/
|
|
1056
|
-
async increment(table, field, where, value = 1) {
|
|
1057
|
-
// 转换表名和字段名:小驼峰 → 下划线
|
|
1058
|
-
const snakeTable = snakeCase(table);
|
|
1059
|
-
const snakeField = snakeCase(field);
|
|
1060
|
-
// 验证表名格式(只允许字母、数字、下划线)
|
|
1061
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(snakeTable)) {
|
|
1062
|
-
throw new Error(`无效的表名: ${snakeTable}`);
|
|
1063
|
-
}
|
|
1064
|
-
// 验证字段名格式(只允许字母、数字、下划线)
|
|
1065
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(snakeField)) {
|
|
1066
|
-
throw new Error(`无效的字段名: ${field}`);
|
|
1067
|
-
}
|
|
1068
|
-
// 验证 value 必须是数字
|
|
1069
|
-
if (typeof value !== "number" || isNaN(value)) {
|
|
1070
|
-
throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
|
|
1071
|
-
}
|
|
1072
|
-
// 清理 where 条件(排除 null 和 undefined,递归)
|
|
1073
|
-
const cleanWhere = DbUtils.clearDeep(where);
|
|
1074
|
-
// 转换 where 条件字段名:小驼峰 → 下划线
|
|
1075
|
-
const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
|
|
1076
|
-
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
|
1077
|
-
const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
1078
|
-
const builder = this.createSqlBuilder().where(whereFiltered);
|
|
1079
|
-
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
1080
|
-
// 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
|
|
1081
|
-
const quotedTable = quoteIdentMySql(snakeTable);
|
|
1082
|
-
const quotedField = quoteIdentMySql(snakeField);
|
|
1083
|
-
const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
|
|
1084
|
-
const execRes = await this.executeRun(sql, [value, ...whereParams]);
|
|
1085
|
-
const changes = toNumberFromSql(execRes.data?.changes);
|
|
1086
|
-
return {
|
|
1087
|
-
data: changes,
|
|
1088
|
-
sql: execRes.sql
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* 自减字段
|
|
1093
|
-
* @param table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
1094
|
-
* @param field - 字段名(支持小驼峰或下划线格式,会自动转换)
|
|
1095
|
-
*/
|
|
1096
|
-
async decrement(table, field, where, value = 1) {
|
|
1097
|
-
return await this.increment(table, field, where, -value);
|
|
1098
|
-
}
|
|
1099
|
-
}
|