befly 3.74.2 → 3.75.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 (65) hide show
  1. package/apis/dashboard/systemResources.js +3 -1
  2. package/apis/dict/_dict.js +1 -1
  3. package/apis/role/_role.js +2 -2
  4. package/apis/tongJi/dailyStats.js +2 -2
  5. package/apis/tongJi/dailyStatsDistribution.js +3 -3
  6. package/apis/tongJi/errorList.js +2 -2
  7. package/apis/tongJi/errorStats.js +6 -6
  8. package/checks/api.js +11 -6
  9. package/checks/items.js +56 -0
  10. package/checks/menu.js +1 -1
  11. package/checks/table.js +4 -5
  12. package/configs/beflyConfig.json +5 -10
  13. package/configs/constConfig.js +44 -3
  14. package/hooks/auth.js +3 -2
  15. package/hooks/parser.js +15 -4
  16. package/hooks/permission.js +3 -2
  17. package/hooks/rateLimit.js +16 -3
  18. package/hooks/validator.js +2 -1
  19. package/index.js +53 -24
  20. package/lib/cacheHelper.js +83 -229
  21. package/lib/connect.js +3 -13
  22. package/lib/dbHelper.js +57 -142
  23. package/lib/dbParse.js +65 -248
  24. package/lib/dbUtil.js +19 -63
  25. package/lib/logger.js +45 -22
  26. package/lib/redisHelper.js +186 -420
  27. package/lib/smtpText.js +7 -18
  28. package/lib/sqlBuilder.js +22 -51
  29. package/lib/xmlParse.js +15 -59
  30. package/package.json +1 -1
  31. package/plugins/cache.js +2 -3
  32. package/plugins/config.js +3 -1
  33. package/plugins/cron.js +2 -2
  34. package/plugins/email.js +3 -1
  35. package/plugins/logger.js +2 -2
  36. package/plugins/mysql.js +5 -10
  37. package/plugins/redis.js +4 -11
  38. package/plugins/tool.js +3 -3
  39. package/plugins/xmlParse.js +3 -1
  40. package/router/api.js +2 -6
  41. package/router/static.js +9 -10
  42. package/schemas/config.js +4 -4
  43. package/scripts/syncDb/context.js +4 -7
  44. package/scripts/syncDb/diff.js +2 -1
  45. package/scripts/syncDb/index.js +2 -6
  46. package/sync/api.js +7 -8
  47. package/sync/cache.js +3 -1
  48. package/sync/dev.js +28 -65
  49. package/sync/menu.js +5 -6
  50. package/sync/syncUtil.js +25 -0
  51. package/utils/calcPerfTime.js +2 -3
  52. package/utils/crypto.js +0 -40
  53. package/utils/deepMerge.js +2 -6
  54. package/utils/error.js +64 -0
  55. package/utils/fieldClear.js +13 -56
  56. package/utils/importDefault.js +11 -12
  57. package/utils/is.js +5 -182
  58. package/utils/loggerUtils.js +3 -1
  59. package/utils/response.js +12 -8
  60. package/utils/scanFiles.js +2 -53
  61. package/utils/scanSources.js +9 -0
  62. package/utils/util.js +25 -222
  63. package/checks/hook.js +0 -39
  64. package/checks/plugin.js +0 -39
  65. package/utils/processInfo.js +0 -39
package/lib/dbUtil.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { validationError } from "../utils/error.js";
1
2
  import { isFiniteNumber, isNonEmptyString, isNullable, isString } from "../utils/is.js";
2
3
  import { snakeCase } from "../utils/util.js";
3
4
 
@@ -11,57 +12,36 @@ export function assertNoExprField(field) {
11
12
 
12
13
  const trimmed = field.trim();
13
14
  if (trimmed.includes("(") || trimmed.includes(")")) {
14
- throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`, {
15
- cause: null,
16
- code: "validation"
17
- });
15
+ throw validationError(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`);
18
16
  }
19
17
  }
20
18
 
21
19
  export function parseTableRef(tableRef) {
22
20
  if (!isString(tableRef)) {
23
- throw new Error(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`, {
24
- cause: null,
25
- code: "validation"
26
- });
21
+ throw validationError(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`);
27
22
  }
28
23
 
29
24
  const trimmed = tableRef.trim();
30
25
  if (!trimmed) {
31
- throw new Error("tableRef 不能为空", {
32
- cause: null,
33
- code: "validation"
34
- });
26
+ throw validationError("tableRef 不能为空");
35
27
  }
36
28
 
37
29
  const parts = trimmed.split(/\s+/);
38
30
  if (parts.length > 2) {
39
- throw new Error(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`, {
40
- cause: null,
41
- code: "validation"
42
- });
31
+ throw validationError(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`);
43
32
  }
44
33
 
45
34
  const nameSegments = parts[0].split(".");
46
35
  if (nameSegments.length > 2) {
47
- throw new Error(`不支持的表引用格式(schema 层级过深) (tableRef: ${trimmed})`, {
48
- cause: null,
49
- code: "validation"
50
- });
36
+ throw validationError(`不支持的表引用格式(schema 层级过深) (tableRef: ${trimmed})`);
51
37
  }
52
38
 
53
39
  if (nameSegments.length === 2) {
54
40
  if (!nameSegments[0]) {
55
- throw new Error(`tableRef 解析失败:schema 为空 (tableRef: ${trimmed})`, {
56
- cause: null,
57
- code: "validation"
58
- });
41
+ throw validationError(`tableRef 解析失败:schema 为空 (tableRef: ${trimmed})`);
59
42
  }
60
43
  if (!nameSegments[1]) {
61
- throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`, {
62
- cause: null,
63
- code: "validation"
64
- });
44
+ throw validationError(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
65
45
  }
66
46
 
67
47
  return {
@@ -72,10 +52,7 @@ export function parseTableRef(tableRef) {
72
52
  }
73
53
 
74
54
  if (!nameSegments[0]) {
75
- throw new Error(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`, {
76
- cause: null,
77
- code: "validation"
78
- });
55
+ throw validationError(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
79
56
  }
80
57
 
81
58
  return {
@@ -92,7 +69,7 @@ function isQuotedIdentPaired(value) {
92
69
  }
93
70
 
94
71
  const first = trimmed[0];
95
- const last = trimmed[trimmed.length - 1];
72
+ const last = trimmed.at(-1);
96
73
  return (first === "`" && last === "`") || (first === '"' && last === '"');
97
74
  }
98
75
 
@@ -124,10 +101,7 @@ export function parseFieldAliasParts(field) {
124
101
  }
125
102
 
126
103
  if (/\s+AS\s+/i.test(trimmed)) {
127
- throw new Error(`字段别名不支持 AS,请使用单个空格分隔字段与别名 (field: ${trimmed})`, {
128
- cause: null,
129
- code: "validation"
130
- });
104
+ throw validationError(`字段别名不支持 AS,请使用单个空格分隔字段与别名 (field: ${trimmed})`);
131
105
  }
132
106
 
133
107
  if (!/\s/.test(trimmed)) {
@@ -135,10 +109,7 @@ export function parseFieldAliasParts(field) {
135
109
  }
136
110
 
137
111
  if (!/^[^\s]+ [^\s]+$/.test(trimmed)) {
138
- throw new Error(`字段别名必须使用单个空格分隔字段与别名 (field: ${trimmed})`, {
139
- cause: null,
140
- code: "validation"
141
- });
112
+ throw validationError(`字段别名必须使用单个空格分隔字段与别名 (field: ${trimmed})`);
142
113
  }
143
114
 
144
115
  const parts = trimmed.split(" ");
@@ -399,10 +370,7 @@ export async function fieldsToSnake(table, classified, getTableColumns) {
399
370
  const excludeSnakeFields = classified.fields.map((field) => snakeCase(field));
400
371
  const resultFields = allColumns.filter((column) => !excludeSnakeFields.includes(column));
401
372
  if (resultFields.length === 0) {
402
- throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`, {
403
- cause: null,
404
- code: "validation"
405
- });
373
+ throw validationError(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
406
374
  }
407
375
  return resultFields;
408
376
  }
@@ -415,7 +383,7 @@ export function normalizeQualifierField(field) {
415
383
  return snakeCase(field);
416
384
  }
417
385
 
418
- const fieldName = parts[parts.length - 1].trim();
386
+ const fieldName = parts.at(-1).trim();
419
387
  const qualifier = parts
420
388
  .slice(0, parts.length - 1)
421
389
  .map((item) => snakeCase(item.trim()))
@@ -469,10 +437,7 @@ function buildFlatWhereKey(field, operator, orGroup) {
469
437
 
470
438
  export function parseFlatWhereKey(key) {
471
439
  if (!isNonEmptyString(key)) {
472
- throw new Error("where 字段名不能为空", {
473
- cause: null,
474
- code: "validation"
475
- });
440
+ throw validationError("where 字段名不能为空");
476
441
  }
477
442
 
478
443
  assertNoExprField(key);
@@ -481,24 +446,18 @@ export function parseFlatWhereKey(key) {
481
446
  const field = rawParts[0];
482
447
 
483
448
  if (!isNonEmptyString(field)) {
484
- throw new Error(`where 字段名不能为空 (field: ${key})`, {
485
- cause: null,
486
- code: "validation"
487
- });
449
+ throw validationError(`where 字段名不能为空 (field: ${key})`);
488
450
  }
489
451
 
490
452
  const suffixParts = rawParts.slice(1);
491
453
  let orGroup = null;
492
454
 
493
- if (suffixParts.length > 0 && isFlatWhereOrGroupToken(suffixParts[suffixParts.length - 1])) {
455
+ if (suffixParts.length > 0 && isFlatWhereOrGroupToken(suffixParts.at(-1))) {
494
456
  orGroup = suffixParts.pop();
495
457
  }
496
458
 
497
459
  if (suffixParts.length > 1) {
498
- throw new Error(`where 字段后缀格式无效,请使用 field、field$op、field$or、field$op$orN (field: ${key})`, {
499
- cause: null,
500
- code: "validation"
501
- });
460
+ throw validationError(`where 字段后缀格式无效,请使用 field、field$op、field$or、field$op$orN (field: ${key})`);
502
461
  }
503
462
 
504
463
  if (suffixParts.length === 0) {
@@ -512,10 +471,7 @@ export function parseFlatWhereKey(key) {
512
471
  const operatorToken = suffixParts[0];
513
472
 
514
473
  if (!flatWhereOperatorTokens.has(operatorToken)) {
515
- throw new Error(`where 操作符无效,请使用受支持的扁平操作符 (field: ${key}, operator: ${operatorToken})`, {
516
- cause: null,
517
- code: "validation"
518
- });
474
+ throw validationError(`where 操作符无效,请使用受支持的扁平操作符 (field: ${key}, operator: ${operatorToken})`);
519
475
  }
520
476
 
521
477
  return {
package/lib/logger.js CHANGED
@@ -6,6 +6,7 @@ import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from "node
6
6
  import { join as nodePathJoin, resolve as nodePathResolve } from "node:path";
7
7
 
8
8
  import { formatYmdHms } from "../utils/datetime.js";
9
+ import { createError } from "../utils/error.js";
9
10
  import { isPlainObject } from "../utils/is.js";
10
11
  import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils.js";
11
12
  import { normalizePositiveInt } from "../utils/util.js";
@@ -32,7 +33,9 @@ let errorFileSink = null;
32
33
  let config = {
33
34
  debug: 0,
34
35
  dir: "",
35
- runtimeEnv: "production"
36
+ runtimeEnv: "production",
37
+ // bm2 多实例标识(BM2_INSTANCE_ID 优先,BM2_APP_INSTANCE 兜底);无 bm2 环境为空串,文件名不带后缀
38
+ instanceId: ""
36
39
  };
37
40
 
38
41
  function safeWriteStderr(msg) {
@@ -48,6 +51,8 @@ class LogFileSink {
48
51
  this.streamIndex = 0;
49
52
  this.streamSizeBytes = 0;
50
53
  this.disabled = false;
54
+ // 最近一次 write 的完成回调(write 回调按提交顺序触发,等待它即等待全部已提交数据排空)
55
+ this.lastWriteDone = null;
51
56
  }
52
57
 
53
58
  write(line) {
@@ -55,13 +60,20 @@ class LogFileSink {
55
60
 
56
61
  this.ensureStreamReady(line.length);
57
62
  if (this.stream) {
58
- this.stream.write(line);
59
63
  this.streamSizeBytes += line.length;
64
+ this.lastWriteDone = new Promise((resolve) => {
65
+ this.stream.write(line, resolve);
66
+ });
60
67
  }
61
68
  }
62
69
 
70
+ /**
71
+ * 等待已提交数据排空(不关闭流)
72
+ */
63
73
  async flushNow() {
64
- return this.closeStream();
74
+ if (this.lastWriteDone) {
75
+ await this.lastWriteDone;
76
+ }
65
77
  }
66
78
 
67
79
  async shutdown() {
@@ -73,8 +85,10 @@ class LogFileSink {
73
85
  return nodePathJoin(config.dir, "dev.log");
74
86
  }
75
87
 
88
+ // bm2 多实例时按实例分文件,避免多进程写同一文件(例:app.2026-08-01.0.1.log)
76
89
  const suffix = index > 0 ? `.${index}` : "";
77
- const filename = this.prefix === "app" ? `${date}${suffix}.log` : `${this.prefix}.${date}${suffix}.log`;
90
+ const instanceSuffix = config.instanceId ? `.${config.instanceId}` : "";
91
+ const filename = this.prefix === "app" ? `${date}${instanceSuffix}${suffix}.log` : `${this.prefix}.${date}${instanceSuffix}${suffix}.log`;
78
92
  return nodePathJoin(config.dir, filename);
79
93
  }
80
94
 
@@ -155,7 +169,7 @@ class LogFileSink {
155
169
  }
156
170
  }
157
171
 
158
- export async function flush() {
172
+ async function flush() {
159
173
  if (mockInstance) return;
160
174
 
161
175
  for (const sink of [appFileSink, errorFileSink]) {
@@ -165,7 +179,7 @@ export async function flush() {
165
179
  }
166
180
  }
167
181
 
168
- export async function shutdown() {
182
+ async function shutdown() {
169
183
  if (mockInstance) return;
170
184
 
171
185
  const currentAppFileSink = appFileSink;
@@ -190,7 +204,7 @@ function getRuntimeLogDir() {
190
204
  }
191
205
 
192
206
  function prepareLogDir(runtimeEnv, shouldClearDev = false) {
193
- const logDir = getRuntimeLogDir(runtimeEnv);
207
+ const logDir = getRuntimeLogDir();
194
208
  const devLogPath = nodePathResolve(logDir, "dev.log");
195
209
 
196
210
  try {
@@ -201,10 +215,7 @@ function prepareLogDir(runtimeEnv, shouldClearDev = false) {
201
215
  rmSync(devLogPath, { force: true });
202
216
  }
203
217
  } catch (error) {
204
- throw new Error(`创建日志目录失败: ${logDir}. ${error?.message || error}`, {
205
- cause: error,
206
- code: "runtime"
207
- });
218
+ throw createError(`创建日志目录失败: ${logDir}. ${error?.message || error}`, { cause: error, code: "runtime" });
208
219
  }
209
220
 
210
221
  return logDir;
@@ -217,19 +228,21 @@ function ensureLogDirExists() {
217
228
  /**
218
229
  * 配置日志
219
230
  */
220
- export function configure(cfg) {
231
+ function configure(cfg) {
221
232
  shutdown();
222
233
 
223
- config = Object.assign(
224
- {
225
- debug: 0,
226
- dir: "",
227
- runtimeEnv: "production"
228
- },
229
- cfg
230
- );
234
+ config = {
235
+ debug: 0,
236
+ dir: "",
237
+ runtimeEnv: "production",
238
+ instanceId: "",
239
+ ...cfg
240
+ };
241
+
242
+ // 实例 ID 来自运行环境(bm2 注入),不允许配置覆盖
243
+ config.instanceId = Bun.env.BM2_INSTANCE_ID || Bun.env.BM2_APP_INSTANCE || "";
231
244
 
232
- config.dir = getRuntimeLogDir(config.runtimeEnv);
245
+ config.dir = getRuntimeLogDir();
233
246
  prepareLogDir(config.runtimeEnv, true);
234
247
 
235
248
  appFileSink = null;
@@ -247,7 +260,7 @@ export function configure(cfg) {
247
260
  * 设置 Mock Logger(用于测试)
248
261
  * @param mock - Mock Logger 实例(形如 {info/warn/error/debug}),传 null 清除 mock
249
262
  */
250
- export function setMockLogger(mock) {
263
+ function setMockLogger(mock) {
251
264
  mockInstance = mock;
252
265
  }
253
266
 
@@ -379,10 +392,20 @@ function buildLogMethod(level) {
379
392
 
380
393
  /**
381
394
  * 日志实例(延迟初始化)
395
+ * 方法签名约定:
396
+ * - info/warn/debug: (msg, data) —— msg 为字符串或直接传对象(对象时作为完整 record)
397
+ * - error: (msg, err, data) —— err 为 Error 或任意附加信息,data 为可选附加数据
398
+ * - 输出统一为 JSONL 单行:{ level, time, pid, msg, ...record }
382
399
  */
383
400
  export const Logger = {
384
401
  info: buildLogMethod("info"),
385
402
  warn: buildLogMethod("warn"),
403
+ /**
404
+ * 错误日志
405
+ * @param msg - 错误说明(字符串;传对象时作为完整 record)
406
+ * @param err - 错误对象(Error 实例会被序列化并附带 chain 摘要)或任意附加信息
407
+ * @param data - 可选附加数据
408
+ */
386
409
  error: function (msg, err, data) {
387
410
  if (isPlainObject(msg)) {
388
411
  logWrite("error", msg);