befly 3.9.37 → 3.9.39

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 (155) hide show
  1. package/README.md +38 -39
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +225 -235
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +155 -153
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +7 -7
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +15 -7
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -81
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +211 -109
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +53 -47
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -54
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -66
  75. package/sync/syncMenu.ts +190 -57
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
package/lib/dbHelper.ts CHANGED
@@ -1,19 +1,23 @@
1
- /**
1
+ /**
2
2
  * 数据库助手 - TypeScript 版本
3
3
  * 提供数据库 CRUD 操作的封装
4
4
  */
5
5
 
6
- import { snakeCase } from 'es-toolkit/string';
7
- import { SqlBuilder } from './sqlBuilder.js';
8
- import { keysToCamel } from 'befly-shared/keysToCamel';
9
- import { arrayKeysToCamel } from 'befly-shared/arrayKeysToCamel';
10
- import { keysToSnake } from 'befly-shared/keysToSnake';
11
- import { fieldClear } from 'befly-shared/fieldClear';
12
- import { RedisTTL, RedisKeys } from 'befly-shared/redisKeys';
13
- import { Logger } from './logger.js';
14
- import type { WhereConditions, JoinOption } from '../types/common.js';
15
- import type { BeflyContext } from '../types/befly.js';
16
- import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, AllResult, TransactionCallback } from '../types/database.js';
6
+ import type { BeflyContext } from "../types/befly.js";
7
+ import type { WhereConditions, JoinOption } from "../types/common.js";
8
+ import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, AllResult, TransactionCallback } from "../types/database.js";
9
+
10
+ import { snakeCase } from "es-toolkit/string";
11
+
12
+ import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
13
+ import { fieldClear } from "../utils/fieldClear.js";
14
+ import { keysToCamel } from "../utils/keysToCamel.js";
15
+ import { keysToSnake } from "../utils/keysToSnake.js";
16
+ import { CacheKeys } from "./cacheKeys.js";
17
+ import { Logger } from "./logger.js";
18
+ import { SqlBuilder } from "./sqlBuilder.js";
19
+
20
+ const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
17
21
 
18
22
  /**
19
23
  * 数据库助手类
@@ -40,38 +44,38 @@ export class DbHelper {
40
44
  * @throws 如果 fields 格式非法
41
45
  */
42
46
  private validateAndClassifyFields(fields?: string[]): {
43
- type: 'all' | 'include' | 'exclude';
47
+ type: "all" | "include" | "exclude";
44
48
  fields: string[];
45
49
  } {
46
50
  // 情况1:空数组或 undefined,表示查询所有
47
51
  if (!fields || fields.length === 0) {
48
- return { type: 'all', fields: [] };
52
+ return { type: "all", fields: [] };
49
53
  }
50
54
 
51
55
  // 检测是否有星号(禁止)
52
- if (fields.some((f) => f === '*')) {
53
- throw new Error('fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段');
56
+ if (fields.some((f) => f === "*")) {
57
+ throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段");
54
58
  }
55
59
 
56
60
  // 检测是否有空字符串或无效值
57
- if (fields.some((f) => !f || typeof f !== 'string' || f.trim() === '')) {
58
- throw new Error('fields 不能包含空字符串或无效值');
61
+ if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
62
+ throw new Error("fields 不能包含空字符串或无效值");
59
63
  }
60
64
 
61
65
  // 统计包含字段和排除字段
62
- const includeFields = fields.filter((f) => !f.startsWith('!'));
63
- const excludeFields = fields.filter((f) => f.startsWith('!'));
66
+ const includeFields = fields.filter((f) => !f.startsWith("!"));
67
+ const excludeFields = fields.filter((f) => f.startsWith("!"));
64
68
 
65
69
  // 情况2:全部是包含字段
66
70
  if (includeFields.length > 0 && excludeFields.length === 0) {
67
- return { type: 'include', fields: includeFields };
71
+ return { type: "include", fields: includeFields };
68
72
  }
69
73
 
70
74
  // 情况3:全部是排除字段
71
75
  if (excludeFields.length > 0 && includeFields.length === 0) {
72
76
  // 去掉感叹号前缀
73
77
  const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
74
- return { type: 'exclude', fields: cleanExcludeFields };
78
+ return { type: "exclude", fields: cleanExcludeFields };
75
79
  }
76
80
 
77
81
  // 混用情况:报错
@@ -85,7 +89,7 @@ export class DbHelper {
85
89
  */
86
90
  private async getTableColumns(table: string): Promise<string[]> {
87
91
  // 1. 先查 Redis 缓存
88
- const cacheKey = RedisKeys.tableColumns(table);
92
+ const cacheKey = CacheKeys.tableColumns(table);
89
93
  const columns = await this.befly.redis.getObject<string[]>(cacheKey);
90
94
 
91
95
  if (columns && columns.length > 0) {
@@ -103,7 +107,7 @@ export class DbHelper {
103
107
  const columnNames = result.map((row: any) => row.Field) as string[];
104
108
 
105
109
  // 3. 写入 Redis 缓存
106
- await this.befly.redis.setObject(cacheKey, columnNames, RedisTTL.tableColumns);
110
+ await this.befly.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
107
111
 
108
112
  return columnNames;
109
113
  }
@@ -113,21 +117,21 @@ export class DbHelper {
113
117
  * 支持排除字段语法
114
118
  */
115
119
  private async fieldsToSnake(table: string, fields: string[]): Promise<string[]> {
116
- if (!fields || !Array.isArray(fields)) return ['*'];
120
+ if (!fields || !Array.isArray(fields)) return ["*"];
117
121
 
118
122
  // 验证并分类字段
119
123
  const { type, fields: classifiedFields } = this.validateAndClassifyFields(fields);
120
124
 
121
125
  // 情况1:查询所有字段
122
- if (type === 'all') {
123
- return ['*'];
126
+ if (type === "all") {
127
+ return ["*"];
124
128
  }
125
129
 
126
130
  // 情况2:指定包含字段
127
- if (type === 'include') {
131
+ if (type === "include") {
128
132
  return classifiedFields.map((field) => {
129
133
  // 保留函数和特殊字段
130
- if (field.includes('(') || field.includes(' ')) {
134
+ if (field.includes("(") || field.includes(" ")) {
131
135
  return field;
132
136
  }
133
137
  return snakeCase(field);
@@ -135,7 +139,7 @@ export class DbHelper {
135
139
  }
136
140
 
137
141
  // 情况3:排除字段
138
- if (type === 'exclude') {
142
+ if (type === "exclude") {
139
143
  // 获取表的所有字段
140
144
  const allColumns = await this.getTableColumns(table);
141
145
 
@@ -146,13 +150,13 @@ export class DbHelper {
146
150
  const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
147
151
 
148
152
  if (resultFields.length === 0) {
149
- throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(', ')}`);
153
+ throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
150
154
  }
151
155
 
152
156
  return resultFields;
153
157
  }
154
158
 
155
- return ['*'];
159
+ return ["*"];
156
160
  }
157
161
 
158
162
  /**
@@ -161,8 +165,8 @@ export class DbHelper {
161
165
  private orderByToSnake(orderBy: string[]): string[] {
162
166
  if (!orderBy || !Array.isArray(orderBy)) return orderBy;
163
167
  return orderBy.map((item) => {
164
- if (typeof item !== 'string' || !item.includes('#')) return item;
165
- const [field, direction] = item.split('#');
168
+ if (typeof item !== "string" || !item.includes("#")) return item;
169
+ const [field, direction] = item.split("#");
166
170
  return `${snakeCase(field.trim())}#${direction.trim()}`;
167
171
  });
168
172
  }
@@ -182,19 +186,19 @@ export class DbHelper {
182
186
  */
183
187
  private processJoinField(field: string): string {
184
188
  // 跳过函数、星号、已处理的字段
185
- if (field.includes('(') || field === '*' || field.startsWith('`')) {
189
+ if (field.includes("(") || field === "*" || field.startsWith("`")) {
186
190
  return field;
187
191
  }
188
192
 
189
193
  // 处理别名 AS
190
- if (field.toUpperCase().includes(' AS ')) {
194
+ if (field.toUpperCase().includes(" AS ")) {
191
195
  const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
192
196
  return `${this.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
193
197
  }
194
198
 
195
199
  // 处理表名.字段名
196
- if (field.includes('.')) {
197
- const [tableName, fieldName] = field.split('.');
200
+ if (field.includes(".")) {
201
+ const [tableName, fieldName] = field.split(".");
198
202
  return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
199
203
  }
200
204
 
@@ -209,26 +213,26 @@ export class DbHelper {
209
213
  */
210
214
  private processJoinWhereKey(key: string): string {
211
215
  // 保留逻辑操作符
212
- if (key === '$or' || key === '$and') {
216
+ if (key === "$or" || key === "$and") {
213
217
  return key;
214
218
  }
215
219
 
216
220
  // 处理带操作符的字段名(如 user.userId$gt)
217
- if (key.includes('$')) {
218
- const lastDollarIndex = key.lastIndexOf('$');
221
+ if (key.includes("$")) {
222
+ const lastDollarIndex = key.lastIndexOf("$");
219
223
  const fieldPart = key.substring(0, lastDollarIndex);
220
224
  const operator = key.substring(lastDollarIndex);
221
225
 
222
- if (fieldPart.includes('.')) {
223
- const [tableName, fieldName] = fieldPart.split('.');
226
+ if (fieldPart.includes(".")) {
227
+ const [tableName, fieldName] = fieldPart.split(".");
224
228
  return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
225
229
  }
226
230
  return `${snakeCase(fieldPart)}${operator}`;
227
231
  }
228
232
 
229
233
  // 处理表名.字段名
230
- if (key.includes('.')) {
231
- const [tableName, fieldName] = key.split('.');
234
+ if (key.includes(".")) {
235
+ const [tableName, fieldName] = key.split(".");
232
236
  return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
233
237
  }
234
238
 
@@ -240,7 +244,7 @@ export class DbHelper {
240
244
  * 递归处理联查的 where 条件
241
245
  */
242
246
  private processJoinWhere(where: any): any {
243
- if (!where || typeof where !== 'object') return where;
247
+ if (!where || typeof where !== "object") return where;
244
248
 
245
249
  if (Array.isArray(where)) {
246
250
  return where.map((item) => this.processJoinWhere(item));
@@ -250,9 +254,9 @@ export class DbHelper {
250
254
  for (const [key, value] of Object.entries(where)) {
251
255
  const newKey = this.processJoinWhereKey(key);
252
256
 
253
- if (key === '$or' || key === '$and') {
257
+ if (key === "$or" || key === "$and") {
254
258
  result[newKey] = (value as any[]).map((item) => this.processJoinWhere(item));
255
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
259
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
256
260
  result[newKey] = this.processJoinWhere(value);
257
261
  } else {
258
262
  result[newKey] = value;
@@ -268,8 +272,8 @@ export class DbHelper {
268
272
  private processJoinOrderBy(orderBy: string[]): string[] {
269
273
  if (!orderBy || !Array.isArray(orderBy)) return orderBy;
270
274
  return orderBy.map((item) => {
271
- if (typeof item !== 'string' || !item.includes('#')) return item;
272
- const [field, direction] = item.split('#');
275
+ if (typeof item !== "string" || !item.includes("#")) return item;
276
+ const [field, direction] = item.split("#");
273
277
  return `${this.processJoinField(field.trim())}#${direction.trim()}`;
274
278
  });
275
279
  }
@@ -283,12 +287,12 @@ export class DbHelper {
283
287
 
284
288
  // 联查时使用特殊处理逻辑
285
289
  if (hasJoins) {
286
- // 联查时字段直接处理(支持表别名)
290
+ // 联查时字段直接处理(支持表名.字段名格式)
287
291
  const processedFields = (options.fields || []).map((f) => this.processJoinField(f));
288
292
 
289
293
  return {
290
294
  table: this.processTableName(options.table),
291
- fields: processedFields.length > 0 ? processedFields : ['*'],
295
+ fields: processedFields.length > 0 ? processedFields : ["*"],
292
296
  where: this.processJoinWhere(cleanWhere),
293
297
  joins: options.joins,
294
298
  orderBy: this.processJoinOrderBy(options.orderBy || []),
@@ -319,16 +323,16 @@ export class DbHelper {
319
323
 
320
324
  for (const join of joins) {
321
325
  const processedTable = this.processTableName(join.table);
322
- const type = join.type || 'left';
326
+ const type = join.type || "left";
323
327
 
324
328
  switch (type) {
325
- case 'inner':
329
+ case "inner":
326
330
  builder.innerJoin(processedTable, join.on);
327
331
  break;
328
- case 'right':
332
+ case "right":
329
333
  builder.rightJoin(processedTable, join.on);
330
334
  break;
331
- case 'left':
335
+ case "left":
332
336
  default:
333
337
  builder.leftJoin(processedTable, join.on);
334
338
  break;
@@ -339,15 +343,26 @@ export class DbHelper {
339
343
  /**
340
344
  * 添加默认的 state 过滤条件
341
345
  * 默认查询 state > 0 的数据(排除已删除和特殊状态)
346
+ * @param where - where 条件
347
+ * @param table - 主表名(JOIN 查询时需要,用于添加表名前缀避免歧义)
348
+ * @param hasJoins - 是否有 JOIN 查询
342
349
  */
343
- private addDefaultStateFilter(where: WhereConditions = {}): WhereConditions {
350
+ private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
344
351
  // 如果用户已经指定了 state 条件,优先使用用户的条件
345
- const hasStateCondition = Object.keys(where).some((key) => key.startsWith('state'));
352
+ const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
346
353
 
347
354
  if (hasStateCondition) {
348
355
  return where;
349
356
  }
350
357
 
358
+ // JOIN 查询时需要指定主表名前缀避免歧义
359
+ if (hasJoins && table) {
360
+ return {
361
+ ...where,
362
+ [`${table}.state$gt`]: 0
363
+ };
364
+ }
365
+
351
366
  // 默认查询 state > 0 的数据
352
367
  return {
353
368
  ...where,
@@ -372,7 +387,7 @@ export class DbHelper {
372
387
  * 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
373
388
  * 4. 其他字段保持不变
374
389
  */
375
- private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ['id', 'pid', 'sort']): T[] {
390
+ private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ["id", "pid", "sort"]): T[] {
376
391
  if (!arr || !Array.isArray(arr)) return arr as T[];
377
392
 
378
393
  return arr.map((item) => {
@@ -391,9 +406,9 @@ export class DbHelper {
391
406
  // 3. 以 '_id' 结尾(如 user_id, role_id)
392
407
  // 4. 以 'At' 结尾(如 createdAt, updatedAt)
393
408
  // 5. 以 '_at' 结尾(如 created_at, updated_at)
394
- const shouldConvert = fields.includes(key) || key.endsWith('Id') || key.endsWith('_id') || key.endsWith('At') || key.endsWith('_at');
409
+ const shouldConvert = fields.includes(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
395
410
 
396
- if (shouldConvert && typeof value === 'string') {
411
+ if (shouldConvert && typeof value === "string") {
397
412
  const num = Number(value);
398
413
  if (!isNaN(num)) {
399
414
  converted[key] = num;
@@ -406,12 +421,62 @@ export class DbHelper {
406
421
  }) as T[];
407
422
  }
408
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
+
409
474
  /**
410
475
  * Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
411
476
  * 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
412
477
  */
413
478
  private whereKeysToSnake(where: any): any {
414
- if (!where || typeof where !== 'object') return where;
479
+ if (!where || typeof where !== "object") return where;
415
480
 
416
481
  // 处理数组($or, $and 等)
417
482
  if (Array.isArray(where)) {
@@ -421,14 +486,14 @@ export class DbHelper {
421
486
  const result: any = {};
422
487
  for (const [key, value] of Object.entries(where)) {
423
488
  // 保留 $or, $and 等逻辑操作符
424
- if (key === '$or' || key === '$and') {
489
+ if (key === "$or" || key === "$and") {
425
490
  result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
426
491
  continue;
427
492
  }
428
493
 
429
494
  // 处理带操作符的字段名(如 userId$gt)
430
- if (key.includes('$')) {
431
- const lastDollarIndex = key.lastIndexOf('$');
495
+ if (key.includes("$")) {
496
+ const lastDollarIndex = key.lastIndexOf("$");
432
497
  const fieldName = key.substring(0, lastDollarIndex);
433
498
  const operator = key.substring(lastDollarIndex);
434
499
  const snakeKey = snakeCase(fieldName) + operator;
@@ -438,7 +503,7 @@ export class DbHelper {
438
503
 
439
504
  // 普通字段:转换键名,递归处理值(支持嵌套对象)
440
505
  const snakeKey = snakeCase(key);
441
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
506
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
442
507
  result[snakeKey] = this.whereKeysToSnake(value);
443
508
  } else {
444
509
  result[snakeKey] = value;
@@ -453,11 +518,11 @@ export class DbHelper {
453
518
  */
454
519
  private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
455
520
  if (!this.sql) {
456
- throw new Error('数据库连接未初始化');
521
+ throw new Error("数据库连接未初始化");
457
522
  }
458
523
 
459
524
  // 强制类型检查:只接受字符串类型的 SQL
460
- if (typeof sqlStr !== 'string') {
525
+ if (typeof sqlStr !== "string") {
461
526
  throw new Error(`executeWithConn 只接受字符串类型的 SQL,收到类型: ${typeof sqlStr},值: ${JSON.stringify(sqlStr)}`);
462
527
  }
463
528
 
@@ -476,27 +541,35 @@ export class DbHelper {
476
541
  // 计算执行时间
477
542
  const duration = Date.now() - startTime;
478
543
 
479
- // 慢查询警告(超过 1000ms
480
- if (duration > 1000) {
481
- const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
482
- Logger.warn(`🐌 检测到慢查询 (${duration}ms): ${sqlPreview}`);
544
+ // 慢查询警告(超过 5000ms
545
+ if (duration > 5000) {
546
+ Logger.warn(
547
+ {
548
+ subsystem: "db",
549
+ event: "slow",
550
+ duration: duration,
551
+ sqlPreview: sqlStr,
552
+ params: params || [],
553
+ paramsCount: (params || []).length
554
+ },
555
+ "🐌 检测到慢查询"
556
+ );
483
557
  }
484
558
 
485
559
  return result;
486
560
  } catch (error: any) {
487
561
  const duration = Date.now() - startTime;
488
562
 
489
- Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
490
- Logger.error('SQL 执行错误');
491
- Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
492
- Logger.error(`SQL 语句: ${sqlStr.length > 200 ? sqlStr.substring(0, 200) + '...' : sqlStr}`);
493
- Logger.error(`参数列表: ${JSON.stringify(params || [])}`);
494
- Logger.error(`执行耗时: ${duration}ms`);
495
- Logger.error(`错误信息: ${error.message}`);
496
- if (error.stack) {
497
- Logger.error(`错误堆栈:\n${error.stack}`);
498
- }
499
- Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
563
+ const sqlPreview = sqlStr.length > 200 ? sqlStr.substring(0, 200) + "..." : sqlStr;
564
+ Logger.error(
565
+ {
566
+ err: error,
567
+ sqlPreview: sqlPreview,
568
+ params: params || [],
569
+ duration: duration
570
+ },
571
+ "SQL 执行错误"
572
+ );
500
573
 
501
574
  const enhancedError: any = new Error(`SQL执行失败: ${error.message}`);
502
575
  enhancedError.originalError = error;
@@ -516,7 +589,7 @@ export class DbHelper {
516
589
  // 将表名转换为下划线格式
517
590
  const snakeTableName = snakeCase(tableName);
518
591
 
519
- const result = await this.executeWithConn('SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', [snakeTableName]);
592
+ const result = await this.executeWithConn("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
520
593
 
521
594
  return result?.[0]?.count > 0;
522
595
  }
@@ -538,10 +611,13 @@ export class DbHelper {
538
611
  * where: { 'o.state': 1 }
539
612
  * });
540
613
  */
541
- async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
614
+ async getCount(options: Omit<QueryOptions, "fields" | "page" | "limit" | "orderBy">): Promise<number> {
542
615
  const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
543
616
 
544
- const builder = new SqlBuilder().select(['COUNT(*) as count']).from(table).where(this.addDefaultStateFilter(where));
617
+ const builder = new SqlBuilder()
618
+ .select(["COUNT(*) as count"])
619
+ .from(table)
620
+ .where(this.addDefaultStateFilter(where, table, !!joins));
545
621
 
546
622
  // 添加 JOIN
547
623
  this.applyJoins(builder, joins);
@@ -571,7 +647,10 @@ export class DbHelper {
571
647
  async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
572
648
  const { table, fields, where, joins } = await this.prepareQueryOptions(options);
573
649
 
574
- const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
650
+ const builder = new SqlBuilder()
651
+ .select(fields)
652
+ .from(table)
653
+ .where(this.addDefaultStateFilter(where, table, !!joins));
575
654
 
576
655
  // 添加 JOIN
577
656
  this.applyJoins(builder, joins);
@@ -585,8 +664,12 @@ export class DbHelper {
585
664
 
586
665
  const camelRow = keysToCamel<T>(row);
587
666
 
667
+ // 反序列化数组字段(JSON 字符串 → 数组)
668
+ const deserialized = this.deserializeArrayFields<T>(camelRow);
669
+ if (!deserialized) return null;
670
+
588
671
  // 转换 BIGINT 字段(id, pid 等)为数字类型
589
- return this.convertBigIntFields<T>([camelRow])[0];
672
+ return this.convertBigIntFields<T>([deserialized])[0];
590
673
  }
591
674
 
592
675
  /**
@@ -623,10 +706,10 @@ export class DbHelper {
623
706
  }
624
707
 
625
708
  // 构建查询
626
- const whereFiltered = this.addDefaultStateFilter(prepared.where);
709
+ const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
627
710
 
628
711
  // 查询总数
629
- const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
712
+ const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
630
713
 
631
714
  // 添加 JOIN(计数也需要)
632
715
  this.applyJoins(countBuilder, prepared.joins);
@@ -664,9 +747,12 @@ export class DbHelper {
664
747
  // 字段名转换:下划线 → 小驼峰
665
748
  const camelList = arrayKeysToCamel<T>(list);
666
749
 
750
+ // 反序列化数组字段
751
+ const deserializedList = camelList.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
752
+
667
753
  // 转换 BIGINT 字段(id, pid 等)为数字类型
668
754
  return {
669
- lists: this.convertBigIntFields<T>(camelList),
755
+ lists: this.convertBigIntFields<T>(deserializedList),
670
756
  total: total,
671
757
  page: prepared.page,
672
758
  limit: prepared.limit,
@@ -691,17 +777,17 @@ export class DbHelper {
691
777
  * where: { 'o.state': 1 }
692
778
  * })
693
779
  */
694
- async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<AllResult<T>> {
780
+ async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, "page" | "limit">): Promise<AllResult<T>> {
695
781
  // 添加硬性上限保护,防止内存溢出
696
782
  const MAX_LIMIT = 10000;
697
783
  const WARNING_LIMIT = 1000;
698
784
 
699
785
  const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
700
786
 
701
- const whereFiltered = this.addDefaultStateFilter(prepared.where);
787
+ const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
702
788
 
703
789
  // 查询真实总数
704
- const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
790
+ const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
705
791
 
706
792
  // 添加 JOIN(计数也需要)
707
793
  this.applyJoins(countBuilder, prepared.joins);
@@ -733,7 +819,7 @@ export class DbHelper {
733
819
 
734
820
  // 警告日志:返回数据超过警告阈值
735
821
  if (result.length >= WARNING_LIMIT) {
736
- Logger.warn({ table: options.table, count: result.length, total: total }, 'getAll 返回数据过多,建议使用 getList 分页查询');
822
+ Logger.warn({ table: options.table, count: result.length, total: total }, "getAll 返回数据过多,建议使用 getList 分页查询");
737
823
  }
738
824
 
739
825
  // 如果达到上限,额外警告
@@ -744,8 +830,11 @@ export class DbHelper {
744
830
  // 字段名转换:下划线 → 小驼峰
745
831
  const camelResult = arrayKeysToCamel<T>(result);
746
832
 
833
+ // 反序列化数组字段
834
+ const deserializedList = camelResult.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
835
+
747
836
  // 转换 BIGINT 字段(id, pid 等)为数字类型
748
- const lists = this.convertBigIntFields<T>(camelResult);
837
+ const lists = this.convertBigIntFields<T>(deserializedList);
749
838
 
750
839
  return {
751
840
  lists: lists,
@@ -770,8 +859,11 @@ export class DbHelper {
770
859
  // 字段名转换:小驼峰 → 下划线
771
860
  const snakeData = keysToSnake(cleanData);
772
861
 
862
+ // 序列化数组字段(数组 → JSON 字符串)
863
+ const serializedData = this.serializeArrayFields(snakeData);
864
+
773
865
  // 复制用户数据,但移除系统字段(防止用户尝试覆盖)
774
- const { id, created_at, updated_at, deleted_at, state, ...userData } = snakeData;
866
+ const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
775
867
 
776
868
  const processed: Record<string, any> = { ...userData };
777
869
 
@@ -837,8 +929,11 @@ export class DbHelper {
837
929
  // 字段名转换:小驼峰 → 下划线
838
930
  const snakeData = keysToSnake(cleanData);
839
931
 
932
+ // 序列化数组字段(数组 → JSON 字符串)
933
+ const serializedData = this.serializeArrayFields(snakeData);
934
+
840
935
  // 移除系统字段(防止用户尝试覆盖)
841
- const { id, created_at, updated_at, deleted_at, state, ...userData } = snakeData;
936
+ const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
842
937
 
843
938
  // 强制生成系统字段(不可被用户覆盖)
844
939
  return {
@@ -859,7 +954,7 @@ export class DbHelper {
859
954
  await this.executeWithConn(sql, params);
860
955
  return ids;
861
956
  } catch (error: any) {
862
- Logger.error({ err: error, table: table }, '批量插入失败');
957
+ Logger.error({ err: error, table: table }, "批量插入失败");
863
958
  throw error;
864
959
  }
865
960
  }
@@ -882,9 +977,12 @@ export class DbHelper {
882
977
  const snakeData = keysToSnake(cleanData);
883
978
  const snakeWhere = this.whereKeysToSnake(cleanWhere);
884
979
 
980
+ // 序列化数组字段(数组 → JSON 字符串)
981
+ const serializedData = this.serializeArrayFields(snakeData);
982
+
885
983
  // 移除系统字段(防止用户尝试修改)
886
984
  // 注意:state 允许用户修改(用于设置禁用状态 state=2)
887
- const { id, created_at, updated_at, deleted_at, ...userData } = snakeData;
985
+ const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, ...userData } = serializedData;
888
986
 
889
987
  // 强制更新时间戳(不可被用户覆盖)
890
988
  const processed: Record<string, any> = {
@@ -893,7 +991,7 @@ export class DbHelper {
893
991
  };
894
992
 
895
993
  // 构建 SQL
896
- const whereFiltered = this.addDefaultStateFilter(snakeWhere);
994
+ const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
897
995
  const builder = new SqlBuilder().where(whereFiltered);
898
996
  const { sql, params } = builder.toUpdateSql(snakeTable, processed);
899
997
 
@@ -920,7 +1018,7 @@ export class DbHelper {
920
1018
  * 硬删除数据(物理删除,不可恢复)
921
1019
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
922
1020
  */
923
- async delForce(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1021
+ async delForce(options: Omit<DeleteOptions, "hard">): Promise<number> {
924
1022
  const { table, where } = options;
925
1023
 
926
1024
  // 转换表名:小驼峰 → 下划线
@@ -942,7 +1040,7 @@ export class DbHelper {
942
1040
  * 禁用数据(设置 state=2)
943
1041
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
944
1042
  */
945
- async disableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1043
+ async disableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
946
1044
  const { table, where } = options;
947
1045
 
948
1046
  return await this.updData({
@@ -958,7 +1056,7 @@ export class DbHelper {
958
1056
  * 启用数据(设置 state=1)
959
1057
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
960
1058
  */
961
- async enableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1059
+ async enableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
962
1060
  const { table, where } = options;
963
1061
 
964
1062
  return await this.updData({
@@ -999,11 +1097,15 @@ export class DbHelper {
999
1097
  * 检查数据是否存在(优化性能)
1000
1098
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
1001
1099
  */
1002
- async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
1100
+ async exists(options: Omit<QueryOptions, "fields" | "orderBy" | "page" | "limit">): Promise<boolean> {
1003
1101
  const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
1004
1102
 
1005
1103
  // 使用 COUNT(1) 性能更好
1006
- const builder = new SqlBuilder().select(['COUNT(1) as cnt']).from(table).where(this.addDefaultStateFilter(where)).limit(1);
1104
+ const builder = new SqlBuilder()
1105
+ .select(["COUNT(1) as cnt"])
1106
+ .from(table)
1107
+ .where(this.addDefaultStateFilter(where, table, false))
1108
+ .limit(1);
1007
1109
 
1008
1110
  const { sql, params } = builder.toSelectSql();
1009
1111
  const result = await this.executeWithConn(sql, params);
@@ -1015,7 +1117,7 @@ export class DbHelper {
1015
1117
  * 查询单个字段值(带字段名验证)
1016
1118
  * @param field - 字段名(支持小驼峰或下划线格式)
1017
1119
  */
1018
- async getFieldValue<T = any>(options: Omit<QueryOptions, 'fields'> & { field: string }): Promise<T | null> {
1120
+ async getFieldValue<T = any>(options: Omit<QueryOptions, "fields"> & { field: string }): Promise<T | null> {
1019
1121
  const { field, ...queryOptions } = options;
1020
1122
 
1021
1123
  // 验证字段名格式(只允许字母、数字、下划线)
@@ -1073,7 +1175,7 @@ export class DbHelper {
1073
1175
  }
1074
1176
 
1075
1177
  // 验证 value 必须是数字
1076
- if (typeof value !== 'number' || isNaN(value)) {
1178
+ if (typeof value !== "number" || isNaN(value)) {
1077
1179
  throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
1078
1180
  }
1079
1181
 
@@ -1084,7 +1186,7 @@ export class DbHelper {
1084
1186
  const snakeWhere = this.whereKeysToSnake(cleanWhere);
1085
1187
 
1086
1188
  // 使用 SqlBuilder 构建安全的 WHERE 条件
1087
- const whereFiltered = this.addDefaultStateFilter(snakeWhere);
1189
+ const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
1088
1190
  const builder = new SqlBuilder().where(whereFiltered);
1089
1191
  const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
1090
1192