befly 3.9.38 → 3.9.40

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 +37 -38
  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} +223 -231
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +143 -141
  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} +1 -1
  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 +8 -8
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -77
  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 +183 -102
  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 +48 -44
  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 -52
  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 -65
  75. package/sync/syncMenu.ts +190 -55
  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
  }
@@ -288,7 +292,7 @@ export class DbHelper {
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;
@@ -345,7 +349,7 @@ export class DbHelper {
345
349
  */
346
350
  private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
347
351
  // 如果用户已经指定了 state 条件,优先使用用户的条件
348
- const hasStateCondition = Object.keys(where).some((key) => key.startsWith('state') || key.includes('.state'));
352
+ const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
349
353
 
350
354
  if (hasStateCondition) {
351
355
  return where;
@@ -383,7 +387,7 @@ export class DbHelper {
383
387
  * 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
384
388
  * 4. 其他字段保持不变
385
389
  */
386
- 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[] {
387
391
  if (!arr || !Array.isArray(arr)) return arr as T[];
388
392
 
389
393
  return arr.map((item) => {
@@ -402,9 +406,9 @@ export class DbHelper {
402
406
  // 3. 以 '_id' 结尾(如 user_id, role_id)
403
407
  // 4. 以 'At' 结尾(如 createdAt, updatedAt)
404
408
  // 5. 以 '_at' 结尾(如 created_at, updated_at)
405
- 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");
406
410
 
407
- if (shouldConvert && typeof value === 'string') {
411
+ if (shouldConvert && typeof value === "string") {
408
412
  const num = Number(value);
409
413
  if (!isNaN(num)) {
410
414
  converted[key] = num;
@@ -417,12 +421,62 @@ export class DbHelper {
417
421
  }) as T[];
418
422
  }
419
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
+
420
474
  /**
421
475
  * Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
422
476
  * 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
423
477
  */
424
478
  private whereKeysToSnake(where: any): any {
425
- if (!where || typeof where !== 'object') return where;
479
+ if (!where || typeof where !== "object") return where;
426
480
 
427
481
  // 处理数组($or, $and 等)
428
482
  if (Array.isArray(where)) {
@@ -432,14 +486,14 @@ export class DbHelper {
432
486
  const result: any = {};
433
487
  for (const [key, value] of Object.entries(where)) {
434
488
  // 保留 $or, $and 等逻辑操作符
435
- if (key === '$or' || key === '$and') {
489
+ if (key === "$or" || key === "$and") {
436
490
  result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
437
491
  continue;
438
492
  }
439
493
 
440
494
  // 处理带操作符的字段名(如 userId$gt)
441
- if (key.includes('$')) {
442
- const lastDollarIndex = key.lastIndexOf('$');
495
+ if (key.includes("$")) {
496
+ const lastDollarIndex = key.lastIndexOf("$");
443
497
  const fieldName = key.substring(0, lastDollarIndex);
444
498
  const operator = key.substring(lastDollarIndex);
445
499
  const snakeKey = snakeCase(fieldName) + operator;
@@ -449,7 +503,7 @@ export class DbHelper {
449
503
 
450
504
  // 普通字段:转换键名,递归处理值(支持嵌套对象)
451
505
  const snakeKey = snakeCase(key);
452
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
506
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
453
507
  result[snakeKey] = this.whereKeysToSnake(value);
454
508
  } else {
455
509
  result[snakeKey] = value;
@@ -464,11 +518,11 @@ export class DbHelper {
464
518
  */
465
519
  private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
466
520
  if (!this.sql) {
467
- throw new Error('数据库连接未初始化');
521
+ throw new Error("数据库连接未初始化");
468
522
  }
469
523
 
470
524
  // 强制类型检查:只接受字符串类型的 SQL
471
- if (typeof sqlStr !== 'string') {
525
+ if (typeof sqlStr !== "string") {
472
526
  throw new Error(`executeWithConn 只接受字符串类型的 SQL,收到类型: ${typeof sqlStr},值: ${JSON.stringify(sqlStr)}`);
473
527
  }
474
528
 
@@ -487,27 +541,35 @@ export class DbHelper {
487
541
  // 计算执行时间
488
542
  const duration = Date.now() - startTime;
489
543
 
490
- // 慢查询警告(超过 1000ms
491
- if (duration > 1000) {
492
- const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
493
- 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
+ );
494
557
  }
495
558
 
496
559
  return result;
497
560
  } catch (error: any) {
498
561
  const duration = Date.now() - startTime;
499
562
 
500
- Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
501
- Logger.error('SQL 执行错误');
502
- Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
503
- Logger.error(`SQL 语句: ${sqlStr.length > 200 ? sqlStr.substring(0, 200) + '...' : sqlStr}`);
504
- Logger.error(`参数列表: ${JSON.stringify(params || [])}`);
505
- Logger.error(`执行耗时: ${duration}ms`);
506
- Logger.error(`错误信息: ${error.message}`);
507
- if (error.stack) {
508
- Logger.error(`错误堆栈:\n${error.stack}`);
509
- }
510
- 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
+ );
511
573
 
512
574
  const enhancedError: any = new Error(`SQL执行失败: ${error.message}`);
513
575
  enhancedError.originalError = error;
@@ -527,7 +589,7 @@ export class DbHelper {
527
589
  // 将表名转换为下划线格式
528
590
  const snakeTableName = snakeCase(tableName);
529
591
 
530
- 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]);
531
593
 
532
594
  return result?.[0]?.count > 0;
533
595
  }
@@ -549,11 +611,11 @@ export class DbHelper {
549
611
  * where: { 'o.state': 1 }
550
612
  * });
551
613
  */
552
- async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
614
+ async getCount(options: Omit<QueryOptions, "fields" | "page" | "limit" | "orderBy">): Promise<number> {
553
615
  const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
554
616
 
555
617
  const builder = new SqlBuilder()
556
- .select(['COUNT(*) as count'])
618
+ .select(["COUNT(*) as count"])
557
619
  .from(table)
558
620
  .where(this.addDefaultStateFilter(where, table, !!joins));
559
621
 
@@ -602,8 +664,12 @@ export class DbHelper {
602
664
 
603
665
  const camelRow = keysToCamel<T>(row);
604
666
 
667
+ // 反序列化数组字段(JSON 字符串 → 数组)
668
+ const deserialized = this.deserializeArrayFields<T>(camelRow);
669
+ if (!deserialized) return null;
670
+
605
671
  // 转换 BIGINT 字段(id, pid 等)为数字类型
606
- return this.convertBigIntFields<T>([camelRow])[0];
672
+ return this.convertBigIntFields<T>([deserialized])[0];
607
673
  }
608
674
 
609
675
  /**
@@ -643,7 +709,7 @@ export class DbHelper {
643
709
  const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
644
710
 
645
711
  // 查询总数
646
- 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);
647
713
 
648
714
  // 添加 JOIN(计数也需要)
649
715
  this.applyJoins(countBuilder, prepared.joins);
@@ -681,9 +747,12 @@ export class DbHelper {
681
747
  // 字段名转换:下划线 → 小驼峰
682
748
  const camelList = arrayKeysToCamel<T>(list);
683
749
 
750
+ // 反序列化数组字段
751
+ const deserializedList = camelList.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
752
+
684
753
  // 转换 BIGINT 字段(id, pid 等)为数字类型
685
754
  return {
686
- lists: this.convertBigIntFields<T>(camelList),
755
+ lists: this.convertBigIntFields<T>(deserializedList),
687
756
  total: total,
688
757
  page: prepared.page,
689
758
  limit: prepared.limit,
@@ -708,7 +777,7 @@ export class DbHelper {
708
777
  * where: { 'o.state': 1 }
709
778
  * })
710
779
  */
711
- 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>> {
712
781
  // 添加硬性上限保护,防止内存溢出
713
782
  const MAX_LIMIT = 10000;
714
783
  const WARNING_LIMIT = 1000;
@@ -718,7 +787,7 @@ export class DbHelper {
718
787
  const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
719
788
 
720
789
  // 查询真实总数
721
- 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);
722
791
 
723
792
  // 添加 JOIN(计数也需要)
724
793
  this.applyJoins(countBuilder, prepared.joins);
@@ -750,7 +819,7 @@ export class DbHelper {
750
819
 
751
820
  // 警告日志:返回数据超过警告阈值
752
821
  if (result.length >= WARNING_LIMIT) {
753
- 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 分页查询");
754
823
  }
755
824
 
756
825
  // 如果达到上限,额外警告
@@ -761,8 +830,11 @@ export class DbHelper {
761
830
  // 字段名转换:下划线 → 小驼峰
762
831
  const camelResult = arrayKeysToCamel<T>(result);
763
832
 
833
+ // 反序列化数组字段
834
+ const deserializedList = camelResult.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
835
+
764
836
  // 转换 BIGINT 字段(id, pid 等)为数字类型
765
- const lists = this.convertBigIntFields<T>(camelResult);
837
+ const lists = this.convertBigIntFields<T>(deserializedList);
766
838
 
767
839
  return {
768
840
  lists: lists,
@@ -787,8 +859,11 @@ export class DbHelper {
787
859
  // 字段名转换:小驼峰 → 下划线
788
860
  const snakeData = keysToSnake(cleanData);
789
861
 
862
+ // 序列化数组字段(数组 → JSON 字符串)
863
+ const serializedData = this.serializeArrayFields(snakeData);
864
+
790
865
  // 复制用户数据,但移除系统字段(防止用户尝试覆盖)
791
- 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;
792
867
 
793
868
  const processed: Record<string, any> = { ...userData };
794
869
 
@@ -854,8 +929,11 @@ export class DbHelper {
854
929
  // 字段名转换:小驼峰 → 下划线
855
930
  const snakeData = keysToSnake(cleanData);
856
931
 
932
+ // 序列化数组字段(数组 → JSON 字符串)
933
+ const serializedData = this.serializeArrayFields(snakeData);
934
+
857
935
  // 移除系统字段(防止用户尝试覆盖)
858
- 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;
859
937
 
860
938
  // 强制生成系统字段(不可被用户覆盖)
861
939
  return {
@@ -876,7 +954,7 @@ export class DbHelper {
876
954
  await this.executeWithConn(sql, params);
877
955
  return ids;
878
956
  } catch (error: any) {
879
- Logger.error({ err: error, table: table }, '批量插入失败');
957
+ Logger.error({ err: error, table: table }, "批量插入失败");
880
958
  throw error;
881
959
  }
882
960
  }
@@ -899,9 +977,12 @@ export class DbHelper {
899
977
  const snakeData = keysToSnake(cleanData);
900
978
  const snakeWhere = this.whereKeysToSnake(cleanWhere);
901
979
 
980
+ // 序列化数组字段(数组 → JSON 字符串)
981
+ const serializedData = this.serializeArrayFields(snakeData);
982
+
902
983
  // 移除系统字段(防止用户尝试修改)
903
984
  // 注意:state 允许用户修改(用于设置禁用状态 state=2)
904
- 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;
905
986
 
906
987
  // 强制更新时间戳(不可被用户覆盖)
907
988
  const processed: Record<string, any> = {
@@ -937,7 +1018,7 @@ export class DbHelper {
937
1018
  * 硬删除数据(物理删除,不可恢复)
938
1019
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
939
1020
  */
940
- async delForce(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1021
+ async delForce(options: Omit<DeleteOptions, "hard">): Promise<number> {
941
1022
  const { table, where } = options;
942
1023
 
943
1024
  // 转换表名:小驼峰 → 下划线
@@ -959,7 +1040,7 @@ export class DbHelper {
959
1040
  * 禁用数据(设置 state=2)
960
1041
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
961
1042
  */
962
- async disableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1043
+ async disableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
963
1044
  const { table, where } = options;
964
1045
 
965
1046
  return await this.updData({
@@ -975,7 +1056,7 @@ export class DbHelper {
975
1056
  * 启用数据(设置 state=1)
976
1057
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
977
1058
  */
978
- async enableData(options: Omit<DeleteOptions, 'hard'>): Promise<number> {
1059
+ async enableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
979
1060
  const { table, where } = options;
980
1061
 
981
1062
  return await this.updData({
@@ -1016,12 +1097,12 @@ export class DbHelper {
1016
1097
  * 检查数据是否存在(优化性能)
1017
1098
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
1018
1099
  */
1019
- async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
1100
+ async exists(options: Omit<QueryOptions, "fields" | "orderBy" | "page" | "limit">): Promise<boolean> {
1020
1101
  const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
1021
1102
 
1022
1103
  // 使用 COUNT(1) 性能更好
1023
1104
  const builder = new SqlBuilder()
1024
- .select(['COUNT(1) as cnt'])
1105
+ .select(["COUNT(1) as cnt"])
1025
1106
  .from(table)
1026
1107
  .where(this.addDefaultStateFilter(where, table, false))
1027
1108
  .limit(1);
@@ -1036,7 +1117,7 @@ export class DbHelper {
1036
1117
  * 查询单个字段值(带字段名验证)
1037
1118
  * @param field - 字段名(支持小驼峰或下划线格式)
1038
1119
  */
1039
- 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> {
1040
1121
  const { field, ...queryOptions } = options;
1041
1122
 
1042
1123
  // 验证字段名格式(只允许字母、数字、下划线)
@@ -1094,7 +1175,7 @@ export class DbHelper {
1094
1175
  }
1095
1176
 
1096
1177
  // 验证 value 必须是数字
1097
- if (typeof value !== 'number' || isNaN(value)) {
1178
+ if (typeof value !== "number" || isNaN(value)) {
1098
1179
  throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
1099
1180
  }
1100
1181