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.
Files changed (178) hide show
  1. package/README.md +0 -129
  2. package/befly.js +12769 -0
  3. package/befly.min.js +47 -0
  4. package/package.json +18 -29
  5. package/dist/befly.config.d.ts +0 -7
  6. package/dist/befly.config.js +0 -128
  7. package/dist/befly.js +0 -17348
  8. package/dist/befly.min.js +0 -23
  9. package/dist/checks/checkApi.d.ts +0 -1
  10. package/dist/checks/checkApi.js +0 -139
  11. package/dist/checks/checkConfig.d.ts +0 -9
  12. package/dist/checks/checkConfig.js +0 -255
  13. package/dist/checks/checkHook.d.ts +0 -1
  14. package/dist/checks/checkHook.js +0 -132
  15. package/dist/checks/checkMenu.d.ts +0 -3
  16. package/dist/checks/checkMenu.js +0 -106
  17. package/dist/checks/checkPlugin.d.ts +0 -1
  18. package/dist/checks/checkPlugin.js +0 -132
  19. package/dist/checks/checkTable.d.ts +0 -7
  20. package/dist/checks/checkTable.js +0 -431
  21. package/dist/configs/presetRegexp.d.ts +0 -145
  22. package/dist/configs/presetRegexp.js +0 -218
  23. package/dist/hooks/auth.d.ts +0 -3
  24. package/dist/hooks/auth.js +0 -24
  25. package/dist/hooks/cors.d.ts +0 -7
  26. package/dist/hooks/cors.js +0 -36
  27. package/dist/hooks/parser.d.ts +0 -10
  28. package/dist/hooks/parser.js +0 -76
  29. package/dist/hooks/permission.d.ts +0 -11
  30. package/dist/hooks/permission.js +0 -78
  31. package/dist/hooks/validator.d.ts +0 -7
  32. package/dist/hooks/validator.js +0 -52
  33. package/dist/index.d.ts +0 -28
  34. package/dist/index.js +0 -316
  35. package/dist/lib/asyncContext.d.ts +0 -21
  36. package/dist/lib/asyncContext.js +0 -27
  37. package/dist/lib/cacheHelper.d.ts +0 -128
  38. package/dist/lib/cacheHelper.js +0 -477
  39. package/dist/lib/cacheKeys.d.ts +0 -27
  40. package/dist/lib/cacheKeys.js +0 -37
  41. package/dist/lib/cipher.d.ts +0 -153
  42. package/dist/lib/cipher.js +0 -237
  43. package/dist/lib/connect.d.ts +0 -95
  44. package/dist/lib/connect.js +0 -313
  45. package/dist/lib/dbHelper.d.ts +0 -229
  46. package/dist/lib/dbHelper.js +0 -1099
  47. package/dist/lib/dbUtils.d.ts +0 -91
  48. package/dist/lib/dbUtils.js +0 -544
  49. package/dist/lib/jwt.d.ts +0 -13
  50. package/dist/lib/jwt.js +0 -77
  51. package/dist/lib/logger.d.ts +0 -46
  52. package/dist/lib/logger.js +0 -731
  53. package/dist/lib/redisHelper.d.ts +0 -193
  54. package/dist/lib/redisHelper.js +0 -598
  55. package/dist/lib/sqlBuilder.d.ts +0 -160
  56. package/dist/lib/sqlBuilder.js +0 -837
  57. package/dist/lib/sqlCheck.d.ts +0 -23
  58. package/dist/lib/sqlCheck.js +0 -119
  59. package/dist/lib/validator.d.ts +0 -45
  60. package/dist/lib/validator.js +0 -424
  61. package/dist/loader/loadApis.d.ts +0 -12
  62. package/dist/loader/loadApis.js +0 -71
  63. package/dist/loader/loadHooks.d.ts +0 -7
  64. package/dist/loader/loadHooks.js +0 -50
  65. package/dist/loader/loadPlugins.d.ts +0 -8
  66. package/dist/loader/loadPlugins.js +0 -69
  67. package/dist/paths.d.ts +0 -93
  68. package/dist/paths.js +0 -100
  69. package/dist/plugins/cache.d.ts +0 -10
  70. package/dist/plugins/cache.js +0 -24
  71. package/dist/plugins/cipher.d.ts +0 -7
  72. package/dist/plugins/cipher.js +0 -14
  73. package/dist/plugins/config.d.ts +0 -3
  74. package/dist/plugins/config.js +0 -9
  75. package/dist/plugins/db.d.ts +0 -10
  76. package/dist/plugins/db.js +0 -48
  77. package/dist/plugins/jwt.d.ts +0 -6
  78. package/dist/plugins/jwt.js +0 -13
  79. package/dist/plugins/logger.d.ts +0 -10
  80. package/dist/plugins/logger.js +0 -21
  81. package/dist/plugins/redis.d.ts +0 -10
  82. package/dist/plugins/redis.js +0 -40
  83. package/dist/plugins/tool.d.ts +0 -75
  84. package/dist/plugins/tool.js +0 -105
  85. package/dist/router/api.d.ts +0 -14
  86. package/dist/router/api.js +0 -109
  87. package/dist/router/static.d.ts +0 -9
  88. package/dist/router/static.js +0 -56
  89. package/dist/scripts/ensureDist.d.ts +0 -1
  90. package/dist/scripts/ensureDist.js +0 -296
  91. package/dist/sync/syncApi.d.ts +0 -3
  92. package/dist/sync/syncApi.js +0 -163
  93. package/dist/sync/syncCache.d.ts +0 -2
  94. package/dist/sync/syncCache.js +0 -14
  95. package/dist/sync/syncDev.d.ts +0 -6
  96. package/dist/sync/syncDev.js +0 -166
  97. package/dist/sync/syncMenu.d.ts +0 -14
  98. package/dist/sync/syncMenu.js +0 -308
  99. package/dist/sync/syncTable.d.ts +0 -126
  100. package/dist/sync/syncTable.js +0 -1129
  101. package/dist/types/api.d.ts +0 -177
  102. package/dist/types/api.js +0 -4
  103. package/dist/types/befly.d.ts +0 -231
  104. package/dist/types/befly.js +0 -4
  105. package/dist/types/cache.d.ts +0 -96
  106. package/dist/types/cache.js +0 -4
  107. package/dist/types/cipher.d.ts +0 -27
  108. package/dist/types/cipher.js +0 -7
  109. package/dist/types/common.d.ts +0 -127
  110. package/dist/types/common.js +0 -5
  111. package/dist/types/context.d.ts +0 -39
  112. package/dist/types/context.js +0 -4
  113. package/dist/types/coreError.d.ts +0 -31
  114. package/dist/types/coreError.js +0 -38
  115. package/dist/types/crypto.d.ts +0 -20
  116. package/dist/types/crypto.js +0 -4
  117. package/dist/types/database.d.ts +0 -182
  118. package/dist/types/database.js +0 -4
  119. package/dist/types/hook.d.ts +0 -30
  120. package/dist/types/hook.js +0 -19
  121. package/dist/types/jwt.d.ts +0 -76
  122. package/dist/types/jwt.js +0 -4
  123. package/dist/types/logger.d.ts +0 -95
  124. package/dist/types/logger.js +0 -6
  125. package/dist/types/plugin.d.ts +0 -27
  126. package/dist/types/plugin.js +0 -17
  127. package/dist/types/redis.d.ts +0 -80
  128. package/dist/types/redis.js +0 -4
  129. package/dist/types/roleApisCache.d.ts +0 -21
  130. package/dist/types/roleApisCache.js +0 -8
  131. package/dist/types/sync.d.ts +0 -93
  132. package/dist/types/sync.js +0 -4
  133. package/dist/types/table.d.ts +0 -34
  134. package/dist/types/table.js +0 -4
  135. package/dist/types/validate.d.ts +0 -113
  136. package/dist/types/validate.js +0 -4
  137. package/dist/utils/calcPerfTime.d.ts +0 -4
  138. package/dist/utils/calcPerfTime.js +0 -13
  139. package/dist/utils/cors.d.ts +0 -8
  140. package/dist/utils/cors.js +0 -17
  141. package/dist/utils/dbFieldRules.d.ts +0 -31
  142. package/dist/utils/dbFieldRules.js +0 -94
  143. package/dist/utils/fieldClear.d.ts +0 -11
  144. package/dist/utils/fieldClear.js +0 -57
  145. package/dist/utils/formatYmdHms.d.ts +0 -1
  146. package/dist/utils/formatYmdHms.js +0 -20
  147. package/dist/utils/getClientIp.d.ts +0 -6
  148. package/dist/utils/getClientIp.js +0 -39
  149. package/dist/utils/importDefault.d.ts +0 -1
  150. package/dist/utils/importDefault.js +0 -53
  151. package/dist/utils/isDirentDirectory.d.ts +0 -3
  152. package/dist/utils/isDirentDirectory.js +0 -18
  153. package/dist/utils/loadMenuConfigs.d.ts +0 -11
  154. package/dist/utils/loadMenuConfigs.js +0 -130
  155. package/dist/utils/loggerUtils.d.ts +0 -18
  156. package/dist/utils/loggerUtils.js +0 -171
  157. package/dist/utils/mergeAndConcat.d.ts +0 -7
  158. package/dist/utils/mergeAndConcat.js +0 -77
  159. package/dist/utils/normalizeFieldDefinition.d.ts +0 -18
  160. package/dist/utils/normalizeFieldDefinition.js +0 -27
  161. package/dist/utils/processInfo.d.ts +0 -26
  162. package/dist/utils/processInfo.js +0 -41
  163. package/dist/utils/response.d.ts +0 -20
  164. package/dist/utils/response.js +0 -96
  165. package/dist/utils/scanAddons.d.ts +0 -15
  166. package/dist/utils/scanAddons.js +0 -35
  167. package/dist/utils/scanCoreBuiltins.d.ts +0 -3
  168. package/dist/utils/scanCoreBuiltins.js +0 -72
  169. package/dist/utils/scanFiles.d.ts +0 -32
  170. package/dist/utils/scanFiles.js +0 -124
  171. package/dist/utils/scanSources.d.ts +0 -10
  172. package/dist/utils/scanSources.js +0 -46
  173. package/dist/utils/sortModules.d.ts +0 -28
  174. package/dist/utils/sortModules.js +0 -105
  175. package/dist/utils/sqlUtil.d.ts +0 -33
  176. package/dist/utils/sqlUtil.js +0 -146
  177. package/dist/utils/util.d.ts +0 -172
  178. package/dist/utils/util.js +0 -517
@@ -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
- }