befly 3.9.39 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
package/lib/dbHelper.ts CHANGED
@@ -3,83 +3,56 @@
3
3
  * 提供数据库 CRUD 操作的封装
4
4
  */
5
5
 
6
- import type { BeflyContext } from "../types/befly.js";
7
6
  import type { WhereConditions, JoinOption } from "../types/common.js";
8
7
  import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, AllResult, TransactionCallback } from "../types/database.js";
8
+ import type { DbDialect } from "./dbDialect.js";
9
9
 
10
10
  import { snakeCase } from "es-toolkit/string";
11
11
 
12
12
  import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
13
+ import { convertBigIntFields } from "../utils/convertBigIntFields.js";
13
14
  import { fieldClear } from "../utils/fieldClear.js";
14
15
  import { keysToCamel } from "../utils/keysToCamel.js";
15
- import { keysToSnake } from "../utils/keysToSnake.js";
16
16
  import { CacheKeys } from "./cacheKeys.js";
17
+ import { MySqlDialect } from "./dbDialect.js";
18
+ import { DbUtils } from "./dbUtils.js";
17
19
  import { Logger } from "./logger.js";
18
20
  import { SqlBuilder } from "./sqlBuilder.js";
21
+ import { SqlCheck } from "./sqlCheck.js";
19
22
 
20
23
  const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
21
24
 
25
+ type RedisCacheLike = {
26
+ getObject<T = any>(key: string): Promise<T | null>;
27
+ setObject<T = any>(key: string, obj: T, ttl?: number | null): Promise<string | null>;
28
+ genTimeID(): Promise<number>;
29
+ };
30
+
22
31
  /**
23
32
  * 数据库助手类
24
33
  */
25
34
  export class DbHelper {
26
- private befly: BeflyContext;
35
+ private redis: RedisCacheLike;
36
+ private dialect: DbDialect;
27
37
  private sql: any = null;
28
38
  private isTransaction: boolean = false;
29
39
 
30
40
  /**
31
41
  * 构造函数
32
- * @param befly - Befly 上下文
42
+ * @param redis - Redis 实例
33
43
  * @param sql - Bun SQL 客户端(可选,用于事务)
34
44
  */
35
- constructor(befly: BeflyContext, sql: any = null) {
36
- this.befly = befly;
37
- this.sql = sql;
38
- this.isTransaction = !!sql;
39
- }
40
-
41
- /**
42
- * 验证 fields 格式并分类
43
- * @returns { type: 'all' | 'include' | 'exclude', fields: string[] }
44
- * @throws 如果 fields 格式非法
45
- */
46
- private validateAndClassifyFields(fields?: string[]): {
47
- type: "all" | "include" | "exclude";
48
- fields: string[];
49
- } {
50
- // 情况1:空数组或 undefined,表示查询所有
51
- if (!fields || fields.length === 0) {
52
- return { type: "all", fields: [] };
53
- }
54
-
55
- // 检测是否有星号(禁止)
56
- if (fields.some((f) => f === "*")) {
57
- throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段");
58
- }
59
-
60
- // 检测是否有空字符串或无效值
61
- if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
62
- throw new Error("fields 不能包含空字符串或无效值");
63
- }
64
-
65
- // 统计包含字段和排除字段
66
- const includeFields = fields.filter((f) => !f.startsWith("!"));
67
- const excludeFields = fields.filter((f) => f.startsWith("!"));
68
-
69
- // 情况2:全部是包含字段
70
- if (includeFields.length > 0 && excludeFields.length === 0) {
71
- return { type: "include", fields: includeFields };
72
- }
45
+ constructor(options: { redis: RedisCacheLike; sql?: any | null; dialect?: DbDialect }) {
46
+ this.redis = options.redis;
47
+ this.sql = options.sql || null;
48
+ this.isTransaction = !!options.sql;
73
49
 
74
- // 情况3:全部是排除字段
75
- if (excludeFields.length > 0 && includeFields.length === 0) {
76
- // 去掉感叹号前缀
77
- const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
78
- return { type: "exclude", fields: cleanExcludeFields };
79
- }
50
+ // 默认使用 MySQL 方言(当前 core 的表结构/语法也主要基于 MySQL)
51
+ this.dialect = options.dialect ? options.dialect : new MySqlDialect();
52
+ }
80
53
 
81
- // 混用情况:报错
82
- throw new Error('fields 不能同时包含普通字段和排除字段(! 开头)。只能使用以下3种方式之一:\n1. 空数组 [] 或不传(查询所有)\n2. 全部指定字段 ["id", "name"]\n3. 全部排除字段 ["!password", "!token"]');
54
+ private createSqlBuilder(): SqlBuilder {
55
+ return new SqlBuilder({ quoteIdent: this.dialect.quoteIdent.bind(this.dialect) });
83
56
  }
84
57
 
85
58
  /**
@@ -90,226 +63,68 @@ export class DbHelper {
90
63
  private async getTableColumns(table: string): Promise<string[]> {
91
64
  // 1. 先查 Redis 缓存
92
65
  const cacheKey = CacheKeys.tableColumns(table);
93
- const columns = await this.befly.redis.getObject<string[]>(cacheKey);
66
+ const columns = await this.redis.getObject<string[]>(cacheKey);
94
67
 
95
68
  if (columns && columns.length > 0) {
96
69
  return columns;
97
70
  }
98
71
 
99
72
  // 2. 缓存未命中,查询数据库
100
- const sql = `SHOW COLUMNS FROM \`${table}\``;
101
- const result = await this.executeWithConn(sql);
73
+ const query = this.dialect.getTableColumnsQuery(table);
74
+ const result = await this.executeWithConn(query.sql, query.params);
102
75
 
103
76
  if (!result || result.length === 0) {
104
77
  throw new Error(`表 ${table} 不存在或没有字段`);
105
78
  }
106
79
 
107
- const columnNames = result.map((row: any) => row.Field) as string[];
80
+ const columnNames = this.dialect.getTableColumnsFromResult(result);
108
81
 
109
82
  // 3. 写入 Redis 缓存
110
- await this.befly.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
111
-
112
- return columnNames;
113
- }
114
-
115
- /**
116
- * 字段数组转下划线格式(私有方法)
117
- * 支持排除字段语法
118
- */
119
- private async fieldsToSnake(table: string, fields: string[]): Promise<string[]> {
120
- if (!fields || !Array.isArray(fields)) return ["*"];
121
-
122
- // 验证并分类字段
123
- const { type, fields: classifiedFields } = this.validateAndClassifyFields(fields);
124
-
125
- // 情况1:查询所有字段
126
- if (type === "all") {
127
- return ["*"];
128
- }
129
-
130
- // 情况2:指定包含字段
131
- if (type === "include") {
132
- return classifiedFields.map((field) => {
133
- // 保留函数和特殊字段
134
- if (field.includes("(") || field.includes(" ")) {
135
- return field;
136
- }
137
- return snakeCase(field);
138
- });
139
- }
140
-
141
- // 情况3:排除字段
142
- if (type === "exclude") {
143
- // 获取表的所有字段
144
- const allColumns = await this.getTableColumns(table);
145
-
146
- // 转换排除字段为下划线格式
147
- const excludeSnakeFields = classifiedFields.map((f) => snakeCase(f));
148
-
149
- // 过滤掉排除字段
150
- const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
151
-
152
- if (resultFields.length === 0) {
153
- throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
154
- }
155
-
156
- return resultFields;
157
- }
158
-
159
- return ["*"];
160
- }
161
-
162
- /**
163
- * orderBy 数组转下划线格式(私有方法)
164
- */
165
- private orderByToSnake(orderBy: string[]): string[] {
166
- if (!orderBy || !Array.isArray(orderBy)) return orderBy;
167
- return orderBy.map((item) => {
168
- if (typeof item !== "string" || !item.includes("#")) return item;
169
- const [field, direction] = item.split("#");
170
- return `${snakeCase(field.trim())}#${direction.trim()}`;
171
- });
172
- }
173
-
174
- /**
175
- * 处理表名(转下划线格式)
176
- * 'userProfile' -> 'user_profile'
177
- */
178
- private processTableName(table: string): string {
179
- return snakeCase(table.trim());
180
- }
181
-
182
- /**
183
- * 处理联查字段(支持表名.字段名格式)
184
- * 'user.userId' -> 'user.user_id'
185
- * 'username' -> 'user_name'
186
- */
187
- private processJoinField(field: string): string {
188
- // 跳过函数、星号、已处理的字段
189
- if (field.includes("(") || field === "*" || field.startsWith("`")) {
190
- return field;
191
- }
192
-
193
- // 处理别名 AS
194
- if (field.toUpperCase().includes(" AS ")) {
195
- const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
196
- return `${this.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
83
+ const cacheRes = await this.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
84
+ if (cacheRes === null) {
85
+ Logger.warn({ table: table, cacheKey: cacheKey }, "表字段缓存写入 Redis 失败");
197
86
  }
198
87
 
199
- // 处理表名.字段名
200
- if (field.includes(".")) {
201
- const [tableName, fieldName] = field.split(".");
202
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
203
- }
204
-
205
- // 普通字段
206
- return snakeCase(field);
207
- }
208
-
209
- /**
210
- * 处理联查的 where 条件键名
211
- * 'user.userId': 1 -> 'user.user_id': 1
212
- * 'user.status$in': [...] -> 'user.status$in': [...]
213
- */
214
- private processJoinWhereKey(key: string): string {
215
- // 保留逻辑操作符
216
- if (key === "$or" || key === "$and") {
217
- return key;
218
- }
219
-
220
- // 处理带操作符的字段名(如 user.userId$gt)
221
- if (key.includes("$")) {
222
- const lastDollarIndex = key.lastIndexOf("$");
223
- const fieldPart = key.substring(0, lastDollarIndex);
224
- const operator = key.substring(lastDollarIndex);
225
-
226
- if (fieldPart.includes(".")) {
227
- const [tableName, fieldName] = fieldPart.split(".");
228
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
229
- }
230
- return `${snakeCase(fieldPart)}${operator}`;
231
- }
232
-
233
- // 处理表名.字段名
234
- if (key.includes(".")) {
235
- const [tableName, fieldName] = key.split(".");
236
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
237
- }
238
-
239
- // 普通字段
240
- return snakeCase(key);
241
- }
242
-
243
- /**
244
- * 递归处理联查的 where 条件
245
- */
246
- private processJoinWhere(where: any): any {
247
- if (!where || typeof where !== "object") return where;
248
-
249
- if (Array.isArray(where)) {
250
- return where.map((item) => this.processJoinWhere(item));
251
- }
252
-
253
- const result: any = {};
254
- for (const [key, value] of Object.entries(where)) {
255
- const newKey = this.processJoinWhereKey(key);
256
-
257
- if (key === "$or" || key === "$and") {
258
- result[newKey] = (value as any[]).map((item) => this.processJoinWhere(item));
259
- } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
260
- result[newKey] = this.processJoinWhere(value);
261
- } else {
262
- result[newKey] = value;
263
- }
264
- }
265
- return result;
266
- }
267
-
268
- /**
269
- * 处理联查的 orderBy
270
- * 'o.createdAt#DESC' -> 'o.created_at#DESC'
271
- */
272
- private processJoinOrderBy(orderBy: string[]): string[] {
273
- if (!orderBy || !Array.isArray(orderBy)) return orderBy;
274
- return orderBy.map((item) => {
275
- if (typeof item !== "string" || !item.includes("#")) return item;
276
- const [field, direction] = item.split("#");
277
- return `${this.processJoinField(field.trim())}#${direction.trim()}`;
278
- });
88
+ return columnNames;
279
89
  }
280
90
 
281
91
  /**
282
92
  * 统一的查询参数预处理方法
283
93
  */
284
94
  private async prepareQueryOptions(options: QueryOptions) {
285
- const cleanWhere = this.cleanFields(options.where || {});
95
+ const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
286
96
  const hasJoins = options.joins && options.joins.length > 0;
287
97
 
288
98
  // 联查时使用特殊处理逻辑
289
99
  if (hasJoins) {
290
100
  // 联查时字段直接处理(支持表名.字段名格式)
291
- const processedFields = (options.fields || []).map((f) => this.processJoinField(f));
101
+ const processedFields = (options.fields || []).map((f) => DbUtils.processJoinField(f));
102
+
103
+ const normalizedTableRef = DbUtils.normalizeTableRef(options.table);
104
+ const mainQualifier = DbUtils.getJoinMainQualifier(options.table);
292
105
 
293
106
  return {
294
- table: this.processTableName(options.table),
107
+ table: normalizedTableRef,
108
+ tableQualifier: mainQualifier,
295
109
  fields: processedFields.length > 0 ? processedFields : ["*"],
296
- where: this.processJoinWhere(cleanWhere),
110
+ where: DbUtils.processJoinWhere(cleanWhere),
297
111
  joins: options.joins,
298
- orderBy: this.processJoinOrderBy(options.orderBy || []),
112
+ orderBy: DbUtils.processJoinOrderBy(options.orderBy || []),
299
113
  page: options.page || 1,
300
114
  limit: options.limit || 10
301
115
  };
302
116
  }
303
117
 
304
118
  // 单表查询使用原有逻辑
305
- const processedFields = await this.fieldsToSnake(snakeCase(options.table), options.fields || []);
119
+ const processedFields = await DbUtils.fieldsToSnake(snakeCase(options.table), options.fields || [], this.getTableColumns.bind(this));
306
120
 
307
121
  return {
308
122
  table: snakeCase(options.table),
123
+ tableQualifier: snakeCase(options.table),
309
124
  fields: processedFields,
310
- where: this.whereKeysToSnake(cleanWhere),
125
+ where: DbUtils.whereKeysToSnake(cleanWhere),
311
126
  joins: undefined,
312
- orderBy: this.orderByToSnake(options.orderBy || []),
127
+ orderBy: DbUtils.orderByToSnake(options.orderBy || []),
313
128
  page: options.page || 1,
314
129
  limit: options.limit || 10
315
130
  };
@@ -322,7 +137,7 @@ export class DbHelper {
322
137
  if (!joins || joins.length === 0) return;
323
138
 
324
139
  for (const join of joins) {
325
- const processedTable = this.processTableName(join.table);
140
+ const processedTable = DbUtils.normalizeTableRef(join.table);
326
141
  const type = join.type || "left";
327
142
 
328
143
  switch (type) {
@@ -339,180 +154,6 @@ export class DbHelper {
339
154
  }
340
155
  }
341
156
  }
342
-
343
- /**
344
- * 添加默认的 state 过滤条件
345
- * 默认查询 state > 0 的数据(排除已删除和特殊状态)
346
- * @param where - where 条件
347
- * @param table - 主表名(JOIN 查询时需要,用于添加表名前缀避免歧义)
348
- * @param hasJoins - 是否有 JOIN 查询
349
- */
350
- private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
351
- // 如果用户已经指定了 state 条件,优先使用用户的条件
352
- const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
353
-
354
- if (hasStateCondition) {
355
- return where;
356
- }
357
-
358
- // JOIN 查询时需要指定主表名前缀避免歧义
359
- if (hasJoins && table) {
360
- return {
361
- ...where,
362
- [`${table}.state$gt`]: 0
363
- };
364
- }
365
-
366
- // 默认查询 state > 0 的数据
367
- return {
368
- ...where,
369
- state$gt: 0
370
- };
371
- }
372
-
373
- /**
374
- * 清理数据或 where 条件(默认排除 null 和 undefined)
375
- */
376
- public cleanFields<T extends Record<string, any>>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> {
377
- return fieldClear(data || ({} as T), { excludeValues, keepMap: keepValues });
378
- }
379
-
380
- /**
381
- * 转换数据库 BIGINT 字段为数字类型(私有方法)
382
- * 当 bigint: false 时,Bun SQL 会将大于 u32 的 BIGINT 返回为字符串,此方法将其转换为 number
383
- *
384
- * 转换规则:
385
- * 1. 白名单中的字段会被转换
386
- * 2. 所有以 'Id' 或 '_id' 结尾的字段会被自动转换
387
- * 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
388
- * 4. 其他字段保持不变
389
- */
390
- private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ["id", "pid", "sort"]): T[] {
391
- if (!arr || !Array.isArray(arr)) return arr as T[];
392
-
393
- return arr.map((item) => {
394
- const converted = { ...item };
395
-
396
- // 遍历对象的所有字段
397
- for (const [key, value] of Object.entries(converted)) {
398
- // 跳过 undefined 和 null
399
- if (value === undefined || value === null) {
400
- continue;
401
- }
402
-
403
- // 判断是否需要转换:
404
- // 1. 在白名单中
405
- // 2. 以 'Id' 结尾(如 userId, roleId, categoryId)
406
- // 3. 以 '_id' 结尾(如 user_id, role_id)
407
- // 4. 以 'At' 结尾(如 createdAt, updatedAt)
408
- // 5. 以 '_at' 结尾(如 created_at, updated_at)
409
- const shouldConvert = fields.includes(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
410
-
411
- if (shouldConvert && typeof value === "string") {
412
- const num = Number(value);
413
- if (!isNaN(num)) {
414
- converted[key] = num;
415
- }
416
- }
417
- // number 类型保持不变(小于 u32 的值)
418
- }
419
-
420
- return converted as T;
421
- }) as T[];
422
- }
423
-
424
- /**
425
- * 序列化数组字段(写入数据库前)
426
- * 将数组类型的字段转换为 JSON 字符串
427
- */
428
- private serializeArrayFields(data: Record<string, any>): Record<string, any> {
429
- const serialized = { ...data };
430
-
431
- for (const [key, value] of Object.entries(serialized)) {
432
- // 跳过 null 和 undefined
433
- if (value === null || value === undefined) continue;
434
-
435
- // 数组类型序列化为 JSON 字符串
436
- if (Array.isArray(value)) {
437
- serialized[key] = JSON.stringify(value);
438
- }
439
- }
440
-
441
- return serialized;
442
- }
443
-
444
- /**
445
- * 反序列化数组字段(从数据库读取后)
446
- * 将 JSON 字符串转换回数组
447
- */
448
- private deserializeArrayFields<T = any>(data: Record<string, any> | null): T | null {
449
- if (!data) return null;
450
-
451
- const deserialized = { ...data };
452
-
453
- for (const [key, value] of Object.entries(deserialized)) {
454
- // 跳过非字符串值
455
- if (typeof value !== "string") continue;
456
-
457
- // 尝试解析 JSON 数组字符串
458
- // 只解析符合 JSON 数组格式的字符串(以 [ 开头,以 ] 结尾)
459
- if (value.startsWith("[") && value.endsWith("]")) {
460
- try {
461
- const parsed = JSON.parse(value);
462
- if (Array.isArray(parsed)) {
463
- deserialized[key] = parsed;
464
- }
465
- } catch {
466
- // 解析失败则保持原值
467
- }
468
- }
469
- }
470
-
471
- return deserialized as T;
472
- }
473
-
474
- /**
475
- * Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
476
- * 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
477
- */
478
- private whereKeysToSnake(where: any): any {
479
- if (!where || typeof where !== "object") return where;
480
-
481
- // 处理数组($or, $and 等)
482
- if (Array.isArray(where)) {
483
- return where.map((item) => this.whereKeysToSnake(item));
484
- }
485
-
486
- const result: any = {};
487
- for (const [key, value] of Object.entries(where)) {
488
- // 保留 $or, $and 等逻辑操作符
489
- if (key === "$or" || key === "$and") {
490
- result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
491
- continue;
492
- }
493
-
494
- // 处理带操作符的字段名(如 userId$gt)
495
- if (key.includes("$")) {
496
- const lastDollarIndex = key.lastIndexOf("$");
497
- const fieldName = key.substring(0, lastDollarIndex);
498
- const operator = key.substring(lastDollarIndex);
499
- const snakeKey = snakeCase(fieldName) + operator;
500
- result[snakeKey] = value;
501
- continue;
502
- }
503
-
504
- // 普通字段:转换键名,递归处理值(支持嵌套对象)
505
- const snakeKey = snakeCase(key);
506
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
507
- result[snakeKey] = this.whereKeysToSnake(value);
508
- } else {
509
- result[snakeKey] = value;
510
- }
511
- }
512
-
513
- return result;
514
- }
515
-
516
157
  /**
517
158
  * 执行 SQL(使用 sql.unsafe,带慢查询日志和错误处理)
518
159
  */
@@ -580,6 +221,16 @@ export class DbHelper {
580
221
  }
581
222
  }
582
223
 
224
+ /**
225
+ * 执行原生 SQL(内部工具/同步脚本专用)
226
+ *
227
+ * - 复用当前 DbHelper 持有的连接/事务
228
+ * - 统一走 executeWithConn,保持参数校验与错误行为一致
229
+ */
230
+ public async unsafe(sqlStr: string, params?: any[]): Promise<any> {
231
+ return await this.executeWithConn(sqlStr, params);
232
+ }
233
+
583
234
  /**
584
235
  * 检查表是否存在
585
236
  * @param tableName - 表名(支持小驼峰,会自动转换为下划线)
@@ -589,7 +240,8 @@ export class DbHelper {
589
240
  // 将表名转换为下划线格式
590
241
  const snakeTableName = snakeCase(tableName);
591
242
 
592
- const result = await this.executeWithConn("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
243
+ const query = this.dialect.tableExistsQuery(snakeTableName);
244
+ const result = await this.executeWithConn(query.sql, query.params);
593
245
 
594
246
  return result?.[0]?.count > 0;
595
247
  }
@@ -612,12 +264,12 @@ export class DbHelper {
612
264
  * });
613
265
  */
614
266
  async getCount(options: Omit<QueryOptions, "fields" | "page" | "limit" | "orderBy">): Promise<number> {
615
- const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
267
+ const { table, where, joins, tableQualifier } = await this.prepareQueryOptions(options as QueryOptions);
616
268
 
617
- const builder = new SqlBuilder()
618
- .select(["COUNT(*) as count"])
269
+ const builder = this.createSqlBuilder()
270
+ .selectRaw("COUNT(*) as count")
619
271
  .from(table)
620
- .where(this.addDefaultStateFilter(where, table, !!joins));
272
+ .where(DbUtils.addDefaultStateFilter(where, tableQualifier, !!joins));
621
273
 
622
274
  // 添加 JOIN
623
275
  this.applyJoins(builder, joins);
@@ -645,12 +297,12 @@ export class DbHelper {
645
297
  * })
646
298
  */
647
299
  async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
648
- const { table, fields, where, joins } = await this.prepareQueryOptions(options);
300
+ const { table, fields, where, joins, tableQualifier } = await this.prepareQueryOptions(options);
649
301
 
650
- const builder = new SqlBuilder()
302
+ const builder = this.createSqlBuilder()
651
303
  .select(fields)
652
304
  .from(table)
653
- .where(this.addDefaultStateFilter(where, table, !!joins));
305
+ .where(DbUtils.addDefaultStateFilter(where, tableQualifier, !!joins));
654
306
 
655
307
  // 添加 JOIN
656
308
  this.applyJoins(builder, joins);
@@ -665,11 +317,11 @@ export class DbHelper {
665
317
  const camelRow = keysToCamel<T>(row);
666
318
 
667
319
  // 反序列化数组字段(JSON 字符串 → 数组)
668
- const deserialized = this.deserializeArrayFields<T>(camelRow);
320
+ const deserialized = DbUtils.deserializeArrayFields<T>(camelRow);
669
321
  if (!deserialized) return null;
670
322
 
671
323
  // 转换 BIGINT 字段(id, pid 等)为数字类型
672
- return this.convertBigIntFields<T>([deserialized])[0];
324
+ return convertBigIntFields<T>([deserialized])[0];
673
325
  }
674
326
 
675
327
  /**
@@ -706,10 +358,10 @@ export class DbHelper {
706
358
  }
707
359
 
708
360
  // 构建查询
709
- const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
361
+ const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, !!prepared.joins);
710
362
 
711
363
  // 查询总数
712
- const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
364
+ const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
713
365
 
714
366
  // 添加 JOIN(计数也需要)
715
367
  this.applyJoins(countBuilder, prepared.joins);
@@ -731,7 +383,7 @@ export class DbHelper {
731
383
 
732
384
  // 查询数据
733
385
  const offset = (prepared.page - 1) * prepared.limit;
734
- const dataBuilder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
386
+ const dataBuilder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
735
387
 
736
388
  // 添加 JOIN
737
389
  this.applyJoins(dataBuilder, prepared.joins);
@@ -748,11 +400,11 @@ export class DbHelper {
748
400
  const camelList = arrayKeysToCamel<T>(list);
749
401
 
750
402
  // 反序列化数组字段
751
- const deserializedList = camelList.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
403
+ const deserializedList = camelList.map((item) => DbUtils.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
752
404
 
753
405
  // 转换 BIGINT 字段(id, pid 等)为数字类型
754
406
  return {
755
- lists: this.convertBigIntFields<T>(deserializedList),
407
+ lists: convertBigIntFields<T>(deserializedList),
756
408
  total: total,
757
409
  page: prepared.page,
758
410
  limit: prepared.limit,
@@ -784,10 +436,10 @@ export class DbHelper {
784
436
 
785
437
  const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
786
438
 
787
- const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
439
+ const whereFiltered = DbUtils.addDefaultStateFilter(prepared.where, prepared.tableQualifier, !!prepared.joins);
788
440
 
789
441
  // 查询真实总数
790
- const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
442
+ const countBuilder = this.createSqlBuilder().selectRaw("COUNT(*) as total").from(prepared.table).where(whereFiltered);
791
443
 
792
444
  // 添加 JOIN(计数也需要)
793
445
  this.applyJoins(countBuilder, prepared.joins);
@@ -805,7 +457,7 @@ export class DbHelper {
805
457
  }
806
458
 
807
459
  // 查询数据(受上限保护)
808
- const dataBuilder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(MAX_LIMIT);
460
+ const dataBuilder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(MAX_LIMIT);
809
461
 
810
462
  // 添加 JOIN
811
463
  this.applyJoins(dataBuilder, prepared.joins);
@@ -831,10 +483,10 @@ export class DbHelper {
831
483
  const camelResult = arrayKeysToCamel<T>(result);
832
484
 
833
485
  // 反序列化数组字段
834
- const deserializedList = camelResult.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
486
+ const deserializedList = camelResult.map((item) => DbUtils.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
835
487
 
836
488
  // 转换 BIGINT 字段(id, pid 等)为数字类型
837
- const lists = this.convertBigIntFields<T>(deserializedList);
489
+ const lists = convertBigIntFields<T>(deserializedList);
838
490
 
839
491
  return {
840
492
  lists: lists,
@@ -849,43 +501,24 @@ export class DbHelper {
849
501
  async insData(options: InsertOptions): Promise<number> {
850
502
  const { table, data } = options;
851
503
 
852
- // 清理数据(排除 null 和 undefined)
853
- const cleanData = this.cleanFields(data);
854
-
855
- // 转换表名:小驼峰 → 下划线
856
504
  const snakeTable = snakeCase(table);
857
505
 
858
- // 处理数据(自动添加必要字段)
859
- // 字段名转换:小驼峰 → 下划线
860
- const snakeData = keysToSnake(cleanData);
861
-
862
- // 序列化数组字段(数组 → JSON 字符串)
863
- const serializedData = this.serializeArrayFields(snakeData);
864
-
865
- // 复制用户数据,但移除系统字段(防止用户尝试覆盖)
866
- const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
867
-
868
- const processed: Record<string, any> = { ...userData };
506
+ const now = Date.now();
869
507
 
870
- // 强制生成 ID(不可被用户覆盖)
508
+ let id: number;
871
509
  try {
872
- processed.id = await this.befly.redis.genTimeID();
510
+ id = await this.redis.genTimeID();
873
511
  } catch (error: any) {
874
- throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, error);
512
+ throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, { cause: error });
875
513
  }
876
514
 
877
- // 强制生成时间戳(不可被用户覆盖)
878
- const now = Date.now();
879
- processed.created_at = now;
880
- processed.updated_at = now;
881
-
882
- // 强制设置 state 为 1(激活状态,不可被用户覆盖)
883
- processed.state = 1;
515
+ const processed = DbUtils.buildInsertRow({ data: data, id: id, now: now });
884
516
 
885
- // 注意:deleted_at 字段不在插入时生成,只在软删除时设置
517
+ // 入口校验:保证进入 SqlBuilder 的数据无 undefined
518
+ SqlCheck.assertNoUndefinedInRecord(processed as any, `insData 插入数据 (table: ${snakeTable})`);
886
519
 
887
520
  // 构建 SQL
888
- const builder = new SqlBuilder();
521
+ const builder = this.createSqlBuilder();
889
522
  const { sql, params } = builder.toInsertSql(snakeTable, processed);
890
523
 
891
524
  // 执行
@@ -917,36 +550,20 @@ export class DbHelper {
917
550
  // 批量生成 ID(逐个获取)
918
551
  const ids: number[] = [];
919
552
  for (let i = 0; i < dataList.length; i++) {
920
- ids.push(await this.befly.redis.genTimeID());
553
+ ids.push(await this.redis.genTimeID());
921
554
  }
922
555
  const now = Date.now();
923
556
 
924
557
  // 处理所有数据(自动添加系统字段)
925
558
  const processedList = dataList.map((data, index) => {
926
- // 清理数据(排除 null undefined)
927
- const cleanData = this.cleanFields(data);
928
-
929
- // 字段名转换:小驼峰 → 下划线
930
- const snakeData = keysToSnake(cleanData);
931
-
932
- // 序列化数组字段(数组 → JSON 字符串)
933
- const serializedData = this.serializeArrayFields(snakeData);
934
-
935
- // 移除系统字段(防止用户尝试覆盖)
936
- const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
937
-
938
- // 强制生成系统字段(不可被用户覆盖)
939
- return {
940
- ...userData,
941
- id: ids[index],
942
- created_at: now,
943
- updated_at: now,
944
- state: 1
945
- };
559
+ return DbUtils.buildInsertRow({ data: data, id: ids[index], now: now });
946
560
  });
947
561
 
562
+ // 入口校验:保证进入 SqlBuilder 的批量数据结构一致且无 undefined
563
+ const insertFields = SqlCheck.assertBatchInsertRowsConsistent(processedList as any, { table: snakeTable });
564
+
948
565
  // 构建批量插入 SQL
949
- const builder = new SqlBuilder();
566
+ const builder = this.createSqlBuilder();
950
567
  const { sql, params } = builder.toInsertSql(snakeTable, processedList);
951
568
 
952
569
  // 在事务中执行批量插入
@@ -954,11 +571,79 @@ export class DbHelper {
954
571
  await this.executeWithConn(sql, params);
955
572
  return ids;
956
573
  } catch (error: any) {
957
- Logger.error({ err: error, table: table }, "批量插入失败");
574
+ Logger.error(
575
+ {
576
+ err: error,
577
+ table: table,
578
+ snakeTable: snakeTable,
579
+ count: dataList.length,
580
+ fields: insertFields
581
+ },
582
+ "批量插入失败"
583
+ );
958
584
  throw error;
959
585
  }
960
586
  }
961
587
 
588
+ async delForceBatch(table: string, ids: number[]): Promise<number> {
589
+ if (ids.length === 0) {
590
+ return 0;
591
+ }
592
+
593
+ const snakeTable = snakeCase(table);
594
+
595
+ const query = SqlBuilder.toDeleteInSql({
596
+ table: snakeTable,
597
+ idField: "id",
598
+ ids: ids,
599
+ quoteIdent: this.dialect.quoteIdent.bind(this.dialect)
600
+ });
601
+ const result: any = await this.executeWithConn(query.sql, query.params);
602
+ return result?.changes || 0;
603
+ }
604
+
605
+ async updBatch(table: string, dataList: Array<{ id: number; data: Record<string, any> }>): Promise<number> {
606
+ if (dataList.length === 0) {
607
+ return 0;
608
+ }
609
+
610
+ const snakeTable = snakeCase(table);
611
+ const now = Date.now();
612
+
613
+ const processedList: Array<{ id: number; data: Record<string, any> }> = [];
614
+ const fieldSet = new Set<string>();
615
+
616
+ for (const item of dataList) {
617
+ const userData = DbUtils.buildPartialUpdateData({ data: item.data, allowState: true });
618
+
619
+ for (const key of Object.keys(userData)) {
620
+ fieldSet.add(key);
621
+ }
622
+
623
+ processedList.push({ id: item.id, data: userData });
624
+ }
625
+
626
+ const fields = Array.from(fieldSet).sort();
627
+ if (fields.length === 0) {
628
+ return 0;
629
+ }
630
+
631
+ const query = SqlBuilder.toUpdateCaseByIdSql({
632
+ table: snakeTable,
633
+ idField: "id",
634
+ rows: processedList,
635
+ fields: fields,
636
+ quoteIdent: this.dialect.quoteIdent.bind(this.dialect),
637
+ updatedAtField: "updated_at",
638
+ updatedAtValue: now,
639
+ stateField: "state",
640
+ stateGtZero: true
641
+ });
642
+
643
+ const result: any = await this.executeWithConn(query.sql, query.params);
644
+ return result?.changes || 0;
645
+ }
646
+
962
647
  /**
963
648
  * 更新数据(强制更新时间戳,系统字段不可修改)
964
649
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
@@ -966,33 +651,18 @@ export class DbHelper {
966
651
  async updData(options: UpdateOptions): Promise<number> {
967
652
  const { table, data, where } = options;
968
653
 
969
- // 清理数据和条件(排除 null 和 undefined)
970
- const cleanData = this.cleanFields(data);
971
- const cleanWhere = this.cleanFields(where);
654
+ // 清理条件(排除 null 和 undefined)
655
+ const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
972
656
 
973
657
  // 转换表名:小驼峰 → 下划线
974
658
  const snakeTable = snakeCase(table);
659
+ const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
975
660
 
976
- // 字段名转换:小驼峰 下划线
977
- const snakeData = keysToSnake(cleanData);
978
- const snakeWhere = this.whereKeysToSnake(cleanWhere);
979
-
980
- // 序列化数组字段(数组 → JSON 字符串)
981
- const serializedData = this.serializeArrayFields(snakeData);
982
-
983
- // 移除系统字段(防止用户尝试修改)
984
- // 注意:state 允许用户修改(用于设置禁用状态 state=2)
985
- const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, ...userData } = serializedData;
986
-
987
- // 强制更新时间戳(不可被用户覆盖)
988
- const processed: Record<string, any> = {
989
- ...userData,
990
- updated_at: Date.now()
991
- };
661
+ const processed = DbUtils.buildUpdateRow({ data: data, now: Date.now(), allowState: true });
992
662
 
993
663
  // 构建 SQL
994
- const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
995
- const builder = new SqlBuilder().where(whereFiltered);
664
+ const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
665
+ const builder = this.createSqlBuilder().where(whereFiltered);
996
666
  const { sql, params } = builder.toUpdateSql(snakeTable, processed);
997
667
 
998
668
  // 执行
@@ -1025,11 +695,11 @@ export class DbHelper {
1025
695
  const snakeTable = snakeCase(table);
1026
696
 
1027
697
  // 清理条件字段
1028
- const cleanWhere = this.cleanFields(where);
1029
- const snakeWhere = this.whereKeysToSnake(cleanWhere);
698
+ const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
699
+ const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
1030
700
 
1031
701
  // 物理删除
1032
- const builder = new SqlBuilder().where(snakeWhere);
702
+ const builder = this.createSqlBuilder().where(snakeWhere);
1033
703
  const { sql, params } = builder.toDeleteSql(snakeTable);
1034
704
 
1035
705
  const result = await this.executeWithConn(sql, params);
@@ -1081,7 +751,7 @@ export class DbHelper {
1081
751
  // 使用 Bun SQL 的 begin 方法开启事务
1082
752
  // begin 方法会自动处理 commit/rollback
1083
753
  return await this.sql.begin(async (tx: any) => {
1084
- const trans = new DbHelper(this.befly, tx);
754
+ const trans = new DbHelper({ redis: this.redis, sql: tx, dialect: this.dialect });
1085
755
  return await callback(trans);
1086
756
  });
1087
757
  }
@@ -1098,13 +768,13 @@ export class DbHelper {
1098
768
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
1099
769
  */
1100
770
  async exists(options: Omit<QueryOptions, "fields" | "orderBy" | "page" | "limit">): Promise<boolean> {
1101
- const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
771
+ const { table, where, tableQualifier } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 } as any);
1102
772
 
1103
773
  // 使用 COUNT(1) 性能更好
1104
- const builder = new SqlBuilder()
774
+ const builder = this.createSqlBuilder()
1105
775
  .select(["COUNT(1) as cnt"])
1106
776
  .from(table)
1107
- .where(this.addDefaultStateFilter(where, table, false))
777
+ .where(DbUtils.addDefaultStateFilter(where, tableQualifier, false))
1108
778
  .limit(1);
1109
779
 
1110
780
  const { sql, params } = builder.toSelectSql();
@@ -1180,18 +850,20 @@ export class DbHelper {
1180
850
  }
1181
851
 
1182
852
  // 清理 where 条件(排除 null 和 undefined)
1183
- const cleanWhere = this.cleanFields(where);
853
+ const cleanWhere = fieldClear(where, { excludeValues: [null, undefined] });
1184
854
 
1185
855
  // 转换 where 条件字段名:小驼峰 → 下划线
1186
- const snakeWhere = this.whereKeysToSnake(cleanWhere);
856
+ const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
1187
857
 
1188
858
  // 使用 SqlBuilder 构建安全的 WHERE 条件
1189
- const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
1190
- const builder = new SqlBuilder().where(whereFiltered);
859
+ const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
860
+ const builder = this.createSqlBuilder().where(whereFiltered);
1191
861
  const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
1192
862
 
1193
863
  // 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
1194
- const sql = whereClause ? `UPDATE \`${snakeTable}\` SET \`${snakeField}\` = \`${snakeField}\` + ? WHERE ${whereClause}` : `UPDATE \`${snakeTable}\` SET \`${snakeField}\` = \`${snakeField}\` + ?`;
864
+ const quotedTable = this.dialect.quoteIdent(snakeTable);
865
+ const quotedField = this.dialect.quoteIdent(snakeField);
866
+ const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
1195
867
 
1196
868
  const result = await this.executeWithConn(sql, [value, ...whereParams]);
1197
869
  return result?.changes || 0;