befly 3.76.0 → 3.76.2

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/Befly.js +159 -0
  2. package/README.md +50 -11
  3. package/apis/admin/delete.js +1 -3
  4. package/apis/admin/insert.js +2 -1
  5. package/apis/admin/select.js +2 -7
  6. package/apis/admin/update.js +3 -8
  7. package/apis/api/select.js +2 -7
  8. package/apis/auth/login.js +16 -20
  9. package/apis/dict/delete.js +1 -1
  10. package/apis/dict/detail.js +1 -3
  11. package/apis/dict/select.js +1 -2
  12. package/apis/dict/update.js +1 -1
  13. package/apis/dictType/delete.js +1 -1
  14. package/apis/dictType/detail.js +1 -1
  15. package/apis/dictType/insert.js +1 -1
  16. package/apis/dictType/select.js +2 -7
  17. package/apis/dictType/update.js +2 -2
  18. package/apis/email/config.js +4 -7
  19. package/apis/email/logList.js +2 -7
  20. package/apis/email/send.js +1 -2
  21. package/apis/email/verify.js +1 -3
  22. package/apis/loginLog/select.js +2 -7
  23. package/apis/menu/select.js +2 -7
  24. package/apis/operateLog/select.js +2 -7
  25. package/apis/role/apiSave.js +2 -2
  26. package/apis/role/menuSave.js +2 -2
  27. package/apis/role/select.js +2 -7
  28. package/apis/source/imageList.js +2 -5
  29. package/apis/tongJi/dailyReport.js +12 -13
  30. package/apis/tongJi/dailyReportSelect.js +1 -2
  31. package/apis/tongJi/dailyStats.js +1 -1
  32. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  33. package/apis/tongJi/errorList.js +1 -2
  34. package/apis/tongJi/errorReport.js +1 -2
  35. package/apis/tongJi/projectDelete.js +1 -3
  36. package/apis/tongJi/projectSelect.js +2 -4
  37. package/apis/tongJi/projectUpdate.js +2 -2
  38. package/apis/upload/file.js +7 -5
  39. package/checks/api.js +43 -66
  40. package/checks/config.js +17 -12
  41. package/checks/corns.js +28 -46
  42. package/checks/field.js +18 -0
  43. package/checks/items.js +16 -60
  44. package/checks/menu.js +28 -39
  45. package/checks/table.js +106 -26
  46. package/checks/validation.js +13 -0
  47. package/configs/beflyConfig.json +6 -6
  48. package/configs/constConfig.js +2 -63
  49. package/exports.js +2 -0
  50. package/hooks/auth.js +11 -7
  51. package/hooks/parser.js +55 -59
  52. package/hooks/permission.js +5 -2
  53. package/hooks/rateLimit.js +25 -62
  54. package/hooks/validator.js +43 -31
  55. package/index.js +43 -344
  56. package/{lib → libs}/cacheHelper.js +3 -8
  57. package/libs/cron.js +60 -0
  58. package/libs/logger/index.js +1 -0
  59. package/libs/logger/logger.js +245 -0
  60. package/libs/logger/sanitize.js +115 -0
  61. package/{lib → libs/mysql}/dbHelper.js +100 -73
  62. package/{lib → libs/mysql}/dbParse.js +16 -9
  63. package/{lib → libs/mysql}/dbUtil.js +15 -32
  64. package/libs/mysql/error.js +29 -0
  65. package/libs/mysql/index.js +6 -0
  66. package/libs/mysql/is.js +29 -0
  67. package/libs/mysql/mysql.js +43 -0
  68. package/libs/mysql/sql/identifier.js +85 -0
  69. package/libs/mysql/sql/index.js +2 -0
  70. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  71. package/libs/mysql/util.js +44 -0
  72. package/libs/redis/index.js +2 -0
  73. package/libs/redis/redis.js +204 -0
  74. package/libs/redis/redisError.js +8 -0
  75. package/libs/tableModel.js +72 -0
  76. package/libs/tool.js +33 -0
  77. package/libs/validator/compiler.js +248 -0
  78. package/libs/validator/index.js +10 -0
  79. package/libs/validator/parser.js +173 -0
  80. package/libs/validator/util.js +5 -0
  81. package/package.json +10 -8
  82. package/paths.js +2 -3
  83. package/plugins/cache.js +2 -1
  84. package/plugins/email.js +4 -4
  85. package/plugins/mysql.js +5 -15
  86. package/plugins/redis.js +4 -12
  87. package/router/api.js +14 -10
  88. package/router/root.js +14 -0
  89. package/router/static.js +3 -3
  90. package/schemas/api.json +99 -0
  91. package/schemas/config.json +255 -0
  92. package/schemas/corn.json +39 -0
  93. package/schemas/item.json +43 -0
  94. package/schemas/menu.json +44 -0
  95. package/schemas/table.json +80 -0
  96. package/scripts/syncDb/apply.js +87 -0
  97. package/scripts/syncDb/ddl.js +43 -0
  98. package/scripts/syncDb/index.js +175 -30
  99. package/scripts/syncDb/model.js +60 -0
  100. package/scripts/syncDb/plan.js +160 -0
  101. package/scripts/syncDb/preflight.js +98 -0
  102. package/scripts/syncDb/report.js +177 -162
  103. package/scripts/syncDb/snapshot.js +118 -0
  104. package/sync/syncUtil.js +7 -13
  105. package/tables/admin.json +51 -53
  106. package/tables/api.json +29 -40
  107. package/tables/dailyReport.json +85 -73
  108. package/tables/dict.json +27 -39
  109. package/tables/dictType.json +20 -34
  110. package/tables/emailLog.json +49 -47
  111. package/tables/errorReport.json +86 -60
  112. package/tables/file.json +47 -50
  113. package/tables/loginLog.json +68 -55
  114. package/tables/menu.json +20 -34
  115. package/tables/operateLog.json +53 -47
  116. package/tables/project.json +18 -30
  117. package/tables/role.json +33 -37
  118. package/utils/cors.js +37 -41
  119. package/utils/deepMerge.js +3 -1
  120. package/utils/error.js +1 -56
  121. package/utils/id.js +44 -0
  122. package/utils/is.js +6 -243
  123. package/utils/response.js +12 -1
  124. package/utils/scanFiles.js +13 -4
  125. package/utils/scanSources.js +58 -20
  126. package/utils/util.js +11 -163
  127. package/apis/_apis.js +0 -20
  128. package/apis/auth/sendSmsCode.js +0 -24
  129. package/apis/role/delete.js +0 -41
  130. package/apis/role/insert.js +0 -45
  131. package/apis/role/save.js +0 -36
  132. package/apis/role/update.js +0 -66
  133. package/configs/regexpAlias.json +0 -55
  134. package/lib/connect.js +0 -119
  135. package/lib/logger.js +0 -430
  136. package/lib/redisHelper.js +0 -399
  137. package/lib/validator.js +0 -274
  138. package/lib/xmlParse.js +0 -149
  139. package/plugins/config.js +0 -13
  140. package/plugins/cron.js +0 -115
  141. package/plugins/logger.js +0 -17
  142. package/plugins/tool.js +0 -131
  143. package/plugins/xmlParse.js +0 -10
  144. package/schemas/config.js +0 -67
  145. package/scripts/syncDb/context.js +0 -120
  146. package/scripts/syncDb/diff.js +0 -279
  147. package/scripts/syncDb/transform.js +0 -113
  148. package/sql/befly.sql +0 -271
  149. package/utils/email.js +0 -3
  150. package/utils/fieldClear.js +0 -22
  151. package/utils/formatValidationIssues.js +0 -28
  152. package/utils/getClientIp.js +0 -47
  153. package/utils/loggerUtils.js +0 -196
  154. package/utils/regexpUtil.js +0 -52
  155. /package/{lib → libs}/smtpText.js +0 -0
@@ -1,95 +1,58 @@
1
- import picomatch from "picomatch";
2
-
3
1
  import { HOOK_ORDER } from "../configs/constConfig.js";
4
2
  import { isValidPositiveInt } from "../utils/is.js";
5
3
  import { ErrorResponse } from "../utils/response.js";
6
4
 
7
- const RATE_LIMIT_ENABLED_VALUES = new Set([1, true, "1", "true"]);
8
-
9
- function toPositiveInt(value, fallbackValue) {
10
- const number = Number(value);
11
- return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallbackValue;
12
- }
5
+ const matcherCache = new WeakMap();
13
6
 
14
- function toSafeString(value, fallbackValue) {
15
- return typeof value === "string" && value.trim() ? value.trim() : fallbackValue;
16
- }
17
-
18
- function createMatcher(pattern) {
19
- try {
20
- return picomatch(pattern);
21
- } catch {
22
- return null;
7
+ function getSkipMatchers(config) {
8
+ let matchers = matcherCache.get(config);
9
+ if (!matchers) {
10
+ matchers = config.skipRoutes.map((pattern) => new Bun.Glob(pattern));
11
+ matcherCache.set(config, matchers);
23
12
  }
13
+ return matchers;
24
14
  }
25
15
 
26
- function normalizeConfig(config) {
27
- const skipMatchers = [];
28
- for (const route of Array.isArray(config?.skipRoutes) ? config.skipRoutes : []) {
29
- const matcher = createMatcher(toSafeString(route, ""));
30
- if (matcher) skipMatchers.push(matcher);
31
- }
32
-
33
- return {
34
- enabled: RATE_LIMIT_ENABLED_VALUES.has(config?.enable),
35
- // failOpen:限流服务(Redis)不可用时是否放行;默认放行(保持兼容),可配置为 failClosed
36
- failOpen: config?.failOpen === undefined ? true : RATE_LIMIT_ENABLED_VALUES.has(config?.failOpen),
37
- limit: toPositiveInt(config?.defaultLimit, 1000),
38
- window: toPositiveInt(config?.defaultWindow, 60),
39
- keyType: toSafeString(config?.key, "ip"),
40
- skipMatchers: skipMatchers
41
- };
42
- }
43
-
44
- function resolveScopeByKeyType(keyType, ctx) {
45
- if (keyType === "userId") return isValidPositiveInt(ctx.userId) ? `user:${ctx.userId}` : `ip:${ctx.ip || "unknown"}`;
16
+ function resolveScope(keyType, ctx) {
17
+ if (keyType === "userId") return isValidPositiveInt(ctx.userId) ? `user:${ctx.userId}` : `ip:${ctx.ip}`;
46
18
  if (keyType === "roleCode") return `role:${ctx.roleCode || "guest"}`;
47
- if (keyType === "apiPath") return `path:${ctx.apiPath || "unknown"}`;
19
+ if (keyType === "apiPath") return `path:${ctx.apiPath}`;
48
20
  if (keyType === "global") return "global";
49
- return `ip:${ctx.ip || "unknown"}`;
50
- }
51
-
52
- function shouldSkipRateLimit(normalized, ctx) {
53
- return normalized.skipMatchers.some((matcher) => matcher(ctx.apiPath));
54
- }
55
-
56
- function buildRateLimitRedisKey(ctx, keyType, scope) {
57
- return `rate_limit:${ctx.apiPath}:${ctx.method}:${keyType}:${scope}`;
21
+ return `ip:${ctx.ip}`;
58
22
  }
59
23
 
60
24
  export default {
25
+ name: "限流",
61
26
  order: HOOK_ORDER.RATE_LIMIT,
62
27
  handler: async (befly, ctx) => {
63
- const rateLimit = normalizeConfig(befly?.config?.rateLimit);
64
- if (!rateLimit.enabled || shouldSkipRateLimit(rateLimit, ctx)) return;
28
+ const config = befly.config.rateLimit;
29
+ if (!config.enable || getSkipMatchers(config).some((matcher) => matcher.match(ctx.apiPath))) return;
65
30
 
66
- // 限流服务(Redis)不可用:failOpen 放行(默认),failClosed 直接拒绝
67
- if (!befly?.redis || typeof befly.redis.incrWithExpire !== "function") {
68
- if (!rateLimit.failOpen) {
69
- ctx.corsHeaders ||= {};
70
- ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
31
+ if (!befly.redis?.incrWithExpire) {
32
+ if (!config.failOpen) {
33
+ ctx.corsHeaders["Retry-After"] = String(config.defaultWindow);
71
34
  ctx.response = ErrorResponse(ctx, "服务繁忙,请稍后再试", 1, null, { reason: "rate_limit_unavailable" }, "rateLimit");
72
35
  }
73
36
  return;
74
37
  }
75
38
 
76
- const scope = resolveScopeByKeyType(rateLimit.keyType, ctx);
77
- const current = await befly.redis.incrWithExpire(buildRateLimitRedisKey(ctx, rateLimit.keyType, scope), rateLimit.window);
78
- if (current <= rateLimit.limit) return;
39
+ const scope = resolveScope(config.key, ctx);
40
+ const key = `rate_limit:${ctx.apiPath}:${ctx.method}:${config.key}:${scope}`;
41
+ const current = await befly.redis.incrWithExpire(key, config.defaultWindow);
42
+ if (current <= config.defaultLimit) return;
79
43
 
80
- ctx.corsHeaders ||= {};
81
- ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
44
+ ctx.corsHeaders["Retry-After"] = String(config.defaultWindow);
82
45
  ctx.response = ErrorResponse(
83
46
  ctx,
84
47
  "请求过于频繁,请稍后再试",
85
48
  1,
86
49
  null,
87
50
  {
88
- limit: rateLimit.limit,
89
- window: rateLimit.window,
51
+ limit: config.defaultLimit,
52
+ window: config.defaultWindow,
90
53
  current: current,
91
54
  apiPath: ctx.apiPath,
92
- key: rateLimit.keyType
55
+ key: config.key
93
56
  },
94
57
  "rateLimit"
95
58
  );
@@ -1,48 +1,60 @@
1
+ import { compile } from "#befly/libs/validator/index.js";
2
+
3
+ import { getFieldSchema } from "../checks/field.js";
1
4
  import { HOOK_ORDER } from "../configs/constConfig.js";
2
- // 相对导入
3
- import { validateFields } from "../lib/validator.js";
4
- import { isPlainObject, isString } from "../utils/is.js";
5
+ import { isPlainObject } from "../utils/is.js";
5
6
  import { ErrorResponse } from "../utils/response.js";
6
7
  import { snakeCase } from "../utils/util.js";
7
8
 
8
- /**
9
- * 参数验证钩子
10
- * 根据 API 定义的 fields required 验证请求参数
11
- */
9
+ const validatorCache = new WeakMap();
10
+
11
+ function getValidator(fields, required, constraints) {
12
+ let validators = validatorCache.get(fields);
13
+ if (!validators) {
14
+ validators = new Map();
15
+ validatorCache.set(fields, validators);
16
+ }
17
+ const cacheKey = JSON.stringify([required, constraints]);
18
+ let validate = validators.get(cacheKey);
19
+ if (validate) return validate;
20
+ const requiredSet = new Set(required);
21
+ validate = compile({
22
+ paramType: "object",
23
+ fields: Object.fromEntries(
24
+ Object.entries(fields).map(([field, definition]) => [
25
+ field,
26
+ {
27
+ ...getFieldSchema(definition),
28
+ optional: !requiredSet.has(field)
29
+ }
30
+ ])
31
+ ),
32
+ constraints: constraints
33
+ });
34
+ validators.set(cacheKey, validate);
35
+ return validate;
36
+ }
37
+
12
38
  export default {
39
+ name: "参数验证",
13
40
  order: HOOK_ORDER.VALIDATOR,
14
41
  handler: async (befly, ctx) => {
15
- // 仅保留 fields 中声明的字段,并支持 snake_case 入参回退(例如 agent_id -> agentId)
16
42
  const rawBody = isPlainObject(ctx.body) ? ctx.body : {};
17
43
  const nextBody = {};
18
44
 
19
- for (const [field] of Object.entries(ctx.apiFields)) {
20
- let value = rawBody[field];
21
-
22
- if (value === undefined) {
23
- const snakeField = snakeCase(field);
24
- if (rawBody[snakeField] !== undefined) {
25
- value = rawBody[snakeField];
26
- }
27
- }
28
-
29
- if (isString(value)) {
30
- value = value.trim();
31
- }
32
-
33
- if (value !== undefined) {
34
- nextBody[field] = value;
35
- }
45
+ for (const field of Object.keys(ctx.apiFields)) {
46
+ const value = rawBody[field] !== undefined ? rawBody[field] : rawBody[snakeCase(field)];
47
+ if (value !== undefined) nextBody[field] = value;
36
48
  }
37
49
 
38
50
  ctx.body = nextBody;
39
-
40
- // 验证参数
41
- const result = validateFields(ctx.body, ctx.apiFields, ctx.apiRequired);
42
-
43
- if (result.code !== 0) {
44
- ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors, "validator");
51
+ const result = getValidator(ctx.apiFields, ctx.apiRequired, ctx.apiConstraints)(nextBody);
52
+ if (result.ok) {
53
+ ctx.body = result.data;
45
54
  return;
46
55
  }
56
+
57
+ const fieldErrors = Object.fromEntries(result.issues.map((issue) => [issue.path[0], `${ctx.apiFields[issue.path[0]]?.name || issue.path[0]}${issue.message}`]));
58
+ ctx.response = ErrorResponse(ctx, Object.values(fieldErrors)[0] || "参数验证失败", 1, null, fieldErrors, "validator");
47
59
  }
48
60
  };
package/index.js CHANGED
@@ -1,98 +1,35 @@
1
- /**
2
- * Befly 框架主入口文件
3
- * 提供简洁的框架接口,核心逻辑已提取到 loader
4
- */
5
-
6
- // 检查
7
- import { checkApi } from "#befly/checks/api.js";
8
- import { checkConfig } from "#befly/checks/config.js";
9
- import { checkCorn } from "#befly/checks/corns.js";
10
- import { checkHook, checkPlugin } from "#befly/checks/items.js";
11
- import { checkMenu } from "#befly/checks/menu.js";
12
- import { checkTable } from "#befly/checks/table.js";
13
- // 配置
14
- import beflyConfig from "#befly/configs/beflyConfig.json";
15
- import beflyMenus from "#befly/configs/beflyMenus.json";
16
- import { BEFLY_ADMIN_TABLE, BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "#befly/configs/constConfig.js";
17
- // ========== 相对导入(项目内部文件) ==========
18
- // 基础设施
19
- import { Connect } from "#befly/lib/connect.js";
20
- // oxlint-disable-next-line unicorn/prefer-export-from -- Logger 同时被本文件大量使用,不能合并为 re-export
21
- import { Logger } from "#befly/lib/logger.js";
22
- // 定时任务
23
- import { registerCorns } from "#befly/plugins/cron.js";
24
- // 路由处理
25
- import { apiHandler } from "#befly/router/api.js";
26
- import { staticHandler } from "#befly/router/static.js";
27
- // 同步
28
- import { syncApi } from "#befly/sync/api.js";
29
- import { syncCache } from "#befly/sync/cache.js";
30
- import { syncDev } from "#befly/sync/dev.js";
31
- import { syncMenu } from "#befly/sync/menu.js";
32
- // 工具
33
- import { calcPerfTime } from "#befly/utils/calcPerfTime.js";
34
- import { formatYmdHms } from "#befly/utils/datetime.js";
35
- import { deepMerge } from "#befly/utils/deepMerge.js";
36
- import { createError } from "#befly/utils/error.js";
37
- import { isPrimaryProcess } from "#befly/utils/is.js";
38
- import { scanSources } from "#befly/utils/scanSources.js";
39
- import { getRunMode, waitFor } from "#befly/utils/util.js";
40
-
41
- export { syncDb } from "#befly/scripts/syncDb/index.js";
42
- export { xmlParse } from "#befly/lib/xmlParse.js";
43
- export { Logger };
44
-
45
- // 主进程同步完成的 Redis 就绪标记(非主进程等待此标记后再对外服务)
46
- const SYNC_READY_KEY = "befly:syncReady";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+
3
+ import { Befly } from "./Befly.js";
4
+ import { checkApi } from "./checks/api.js";
5
+ import { checkConfig } from "./checks/config.js";
6
+ import { checkCorn } from "./checks/corns.js";
7
+ import { checkHook, checkPlugin } from "./checks/items.js";
8
+ import { checkMenu } from "./checks/menu.js";
9
+ import { checkTable } from "./checks/table.js";
10
+ import beflyConfig from "./configs/beflyConfig.json";
11
+ import beflyMenus from "./configs/beflyMenus.json";
12
+ import { calcPerfTime } from "./utils/calcPerfTime.js";
13
+ import { deepMerge } from "./utils/deepMerge.js";
14
+ import { createError } from "./utils/error.js";
15
+ import { scanSources } from "./utils/scanSources.js";
16
+ import { getRunMode } from "./utils/util.js";
47
17
 
48
18
  function prefixMenuPaths(menus, prefix) {
49
- return menus.map((menu) => {
50
- const nextMenu = {
51
- name: menu.name,
52
- path: menu.path === "/" ? `/${prefix}` : `/${prefix}${menu.path}`,
53
- sort: menu.sort
54
- };
55
-
56
- if (Array.isArray(menu.children)) {
57
- nextMenu.children = menu.children.map((child) => ({
58
- name: child.name,
59
- path: child.path,
60
- sort: child.sort
61
- }));
62
- }
63
-
64
- return nextMenu;
65
- });
66
- }
67
-
68
- async function syncReady(ctx) {
69
- const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
70
- if (missingCtxKeys.length > 0) {
71
- throw createError(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
72
- code: "runtime",
73
- subsystem: "sync",
74
- operation: "ctxCheck",
75
- missingCtxKeys: missingCtxKeys
76
- });
77
- }
78
-
79
- const requiredTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
80
- const missingTables = [];
81
-
82
- for (const table of requiredTables) {
83
- if (!(await ctx.mysql.tableExists(table)).data) {
84
- missingTables.push(table);
85
- }
86
- }
87
-
88
- if (missingTables.length > 0) {
89
- throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, {
90
- code: "runtime",
91
- subsystem: "sync",
92
- operation: "tableExists",
93
- missingTables: missingTables
94
- });
95
- }
19
+ return menus.map((menu) => ({
20
+ name: menu.name,
21
+ path: menu.path === "/" ? `/${prefix}` : `/${prefix}${menu.path}`,
22
+ sort: menu.sort,
23
+ ...(Array.isArray(menu.children)
24
+ ? {
25
+ children: menu.children.map((child) => ({
26
+ name: child.name,
27
+ path: child.path,
28
+ sort: child.sort
29
+ }))
30
+ }
31
+ : {})
32
+ }));
96
33
  }
97
34
 
98
35
  export async function createBefly(config = {}, menus = []) {
@@ -100,10 +37,7 @@ export async function createBefly(config = {}, menus = []) {
100
37
  const mergedConfig = deepMerge(beflyConfig, config);
101
38
  const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
102
39
 
103
- Logger.configure({
104
- runtimeEnv: getRunMode(),
105
- ...mergedConfig.logger
106
- });
40
+ Logger.configure({ runtimeEnv: getRunMode(), ...mergedConfig.logger });
107
41
  Logger.info(`启动 合并配置 耗时 ${calcPerfTime(mergeStartTime)}`);
108
42
 
109
43
  let stageStartTime = Bun.nanoseconds();
@@ -111,43 +45,19 @@ export async function createBefly(config = {}, menus = []) {
111
45
  Logger.info(`启动 检查配置 耗时 ${calcPerfTime(stageStartTime)}`);
112
46
 
113
47
  stageStartTime = Bun.nanoseconds();
114
- const { apis, tables, plugins, hooks, corns } = await scanSources();
48
+ const { apis, tables, plugins, hooks, corns } = await scanSources({ beflyMode: mergedConfig.mysql.beflyMode });
115
49
  Logger.info(`启动 扫描源码 耗时 ${calcPerfTime(stageStartTime)}`);
116
50
 
117
- stageStartTime = Bun.nanoseconds();
118
- const apiErrors = await checkApi(apis);
119
- Logger.info(`启动 检查接口 耗时 ${calcPerfTime(stageStartTime)}`);
120
-
121
- stageStartTime = Bun.nanoseconds();
122
- const tableErrors = await checkTable(tables);
123
- Logger.info(`启动 检查表 耗时 ${calcPerfTime(stageStartTime)}`);
124
-
125
- stageStartTime = Bun.nanoseconds();
126
- const pluginErrors = await checkPlugin(plugins);
127
- Logger.info(`启动 检查插件 耗时 ${calcPerfTime(stageStartTime)}`);
128
-
129
- stageStartTime = Bun.nanoseconds();
130
- const hookErrors = await checkHook(hooks);
131
- Logger.info(`启动 检查钩子 耗时 ${calcPerfTime(stageStartTime)}`);
132
-
133
- stageStartTime = Bun.nanoseconds();
134
- const cornErrors = await checkCorn(corns);
135
- Logger.info(`启动 检查定时器 耗时 ${calcPerfTime(stageStartTime)}`);
136
-
137
- stageStartTime = Bun.nanoseconds();
138
- const menuErrors = await checkMenu(mergedMenus);
139
- Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
140
-
141
- // 汇总各检查明细,报错可直接定位到具体文件/路径/配置/字段
142
- const failedChecks = [
143
- { check: "配置", errors: configErrors },
144
- { check: "接口", errors: apiErrors },
145
- { check: "表结构", errors: tableErrors },
146
- { check: "插件", errors: pluginErrors },
147
- { check: "钩子", errors: hookErrors },
148
- { check: "定时器", errors: cornErrors },
149
- { check: "菜单", errors: menuErrors }
150
- ].filter((entry) => entry.errors.length > 0);
51
+ const checks = [
52
+ ["配置", configErrors],
53
+ ["接口", await checkApi(apis)],
54
+ ["表结构", await checkTable(tables)],
55
+ ["插件", await checkPlugin(plugins)],
56
+ ["钩子", await checkHook(hooks)],
57
+ ["定时器", checkCorn(corns)],
58
+ ["菜单", await checkMenu(mergedMenus)]
59
+ ];
60
+ const failedChecks = checks.filter(([, errors]) => errors.length > 0).map(([check, errors]) => ({ check: check, errors: errors }));
151
61
 
152
62
  if (failedChecks.length > 0) {
153
63
  throw createError("检查失败:存在配置/结构问题", {
@@ -158,10 +68,7 @@ export async function createBefly(config = {}, menus = []) {
158
68
  });
159
69
  }
160
70
 
161
- // 运行模式以环境为准(NODE_ENV,线上部署注入 production;本地开发默认 development),
162
- // 覆盖配置值,保证 befly.config.runMode 与运行时判断一致
163
71
  mergedConfig.runMode = getRunMode();
164
-
165
72
  return new Befly({
166
73
  config: mergedConfig,
167
74
  tables: tables,
@@ -169,214 +76,6 @@ export async function createBefly(config = {}, menus = []) {
169
76
  apis: apis,
170
77
  hooks: hooks,
171
78
  plugins: plugins,
172
- corns: corns,
173
- createdByFactory: true
79
+ corns: corns
174
80
  });
175
81
  }
176
-
177
- /**
178
- * Befly 框架核心类
179
- * 职责:管理应用上下文和生命周期
180
- */
181
- export class Befly {
182
- constructor(init = {}) {
183
- this.context = {
184
- config: init.config || {},
185
- tables: init.tables || {}
186
- };
187
- this.menus = Array.isArray(init.menus) ? init.menus : [];
188
- this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
189
- this.apis = Array.isArray(init.apis) ? init.apis : [];
190
- this.plugins = Array.isArray(init.plugins) ? init.plugins : [];
191
- this.corns = Array.isArray(init.corns) ? init.corns : [];
192
- this.createdByFactory = init.createdByFactory === true;
193
- }
194
-
195
- ensureCreatedByFactory() {
196
- if (this.createdByFactory) {
197
- return;
198
- }
199
-
200
- throw createError("请使用 createBefly(...) 创建实例后再调用 start", { code: "policy", subsystem: "start", operation: "factoryGuard" });
201
- }
202
-
203
- /**
204
- * 启动完整的生命周期流程
205
- * @returns HTTP 服务器实例
206
- */
207
- async start() {
208
- try {
209
- this.ensureCreatedByFactory();
210
-
211
- const serverStartTime = Bun.nanoseconds();
212
-
213
- // 启动期建立基础连接(SQL + Redis)
214
- // 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
215
- let stageStartTime = Bun.nanoseconds();
216
- await Connect.connectRedis(this.context.config.redis);
217
- Logger.info(`启动 连接 Redis 耗时 ${calcPerfTime(stageStartTime)}`);
218
-
219
- stageStartTime = Bun.nanoseconds();
220
- await Connect.connectMysql(this.context.config.mysql);
221
- Logger.info(`启动 连接 Mysql 耗时 ${calcPerfTime(stageStartTime)}`);
222
-
223
- // 加载插件
224
- for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
225
- const pluginStartTime = Bun.nanoseconds();
226
- this.context[item.fileName] = await item.handler(this.context);
227
- Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
228
- }
229
-
230
- // 注册定时器(依赖插件加载完成的 ctx.cron,非主进程自动跳过)
231
- stageStartTime = Bun.nanoseconds();
232
- registerCorns(this.context, this.corns);
233
- Logger.info(`启动 注册定时器 耗时 ${calcPerfTime(stageStartTime)}`);
234
-
235
- stageStartTime = Bun.nanoseconds();
236
- await syncReady(this.context);
237
- Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
238
-
239
- // 自动同步(bm2 多实例:主进程执行同步并写就绪标记,其它进程等待同步完成后再对外服务)
240
- if (isPrimaryProcess()) {
241
- stageStartTime = Bun.nanoseconds();
242
- await syncApi(this.context, this.apis);
243
- Logger.info(`启动 同步接口 耗时 ${calcPerfTime(stageStartTime)}`);
244
-
245
- stageStartTime = Bun.nanoseconds();
246
- await syncMenu(this.context, this.menus);
247
- Logger.info(`启动 同步菜单 耗时 ${calcPerfTime(stageStartTime)}`);
248
-
249
- stageStartTime = Bun.nanoseconds();
250
- await syncDev(this.context);
251
- Logger.info(`启动 同步开发账号 耗时 ${calcPerfTime(stageStartTime)}`);
252
-
253
- stageStartTime = Bun.nanoseconds();
254
- await syncCache(this.context);
255
- Logger.info(`启动 同步缓存 耗时 ${calcPerfTime(stageStartTime)}`);
256
-
257
- // 同步完成:写入就绪标记(TTL 防崩溃残留),非主进程据此放行
258
- await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
259
- } else {
260
- // 非主进程:在监听端口前等待主进程本次同步完成(时间戳判断,同机时钟一致)
261
- const startAt = Date.now();
262
- await waitFor({
263
- label: "主进程同步完成",
264
- check: async () => {
265
- const ready = await this.context.redis.getString(SYNC_READY_KEY);
266
- return ready !== null && Number(ready) > startAt;
267
- }
268
- });
269
- Logger.info("启动 等待主进程同步完成 完成");
270
- }
271
-
272
- // 加载钩子
273
- stageStartTime = Bun.nanoseconds();
274
- this.hooks = this.hooks.toSorted((a, b) => a.order - b.order);
275
-
276
- // 加载所有 API
277
- this.apis = Object.fromEntries(this.apis.map((api) => [api.apiPath, api]));
278
-
279
- // 启动 HTTP服务器
280
- const apiFetch = apiHandler(this.apis, this.hooks, this.context);
281
- const staticFetch = staticHandler(this.context.config.cors, this.context.config.upload);
282
- Logger.info(`启动 装配接口 耗时 ${calcPerfTime(stageStartTime)}`);
283
-
284
- stageStartTime = Bun.nanoseconds();
285
- const server = Bun.serve({
286
- port: this.context.config.appPort || 3000,
287
- hostname: this.context.config.appHost || "127.0.0.1",
288
- // 开发模式下启用详细错误信息
289
- development: this.context.config.runMode === "development",
290
- // 空闲连接超时时间(秒),防止恶意连接占用资源
291
- fetch: async (req, httpServer) => {
292
- const url = new URL(req.url);
293
-
294
- if (url.pathname.startsWith("/api/")) {
295
- return apiFetch(req, httpServer);
296
- }
297
-
298
- if (url.pathname.startsWith("/public/")) {
299
- return staticFetch(req);
300
- }
301
-
302
- // 根路径返回服务基本信息
303
- if (url.pathname === "/") {
304
- return Response.json(
305
- {
306
- code: 0,
307
- msg: "success",
308
- data: {
309
- name: this.context.config.appName,
310
- mode: this.context.config.runMode,
311
- time: formatYmdHms(Date.now())
312
- }
313
- },
314
- { status: 200 }
315
- );
316
- }
317
-
318
- return Response.json({ code: 1, msg: "路径不存在" }, { status: 200 });
319
- },
320
- error: (error) => {
321
- Logger.error("服务启动时发生错误", error);
322
- // 开发模式下返回详细错误信息
323
- if (this.context.config.runMode === "development") {
324
- return Response.json(
325
- {
326
- code: 1,
327
- msg: "内部服务器错误",
328
- error: error.message,
329
- stack: error.stack
330
- },
331
- { status: 200 }
332
- );
333
- }
334
- return Response.json({ code: 1, msg: "内部服务器错误" }, { status: 200 });
335
- }
336
- });
337
- Logger.info(`启动 监听服务 耗时 ${calcPerfTime(stageStartTime)}`);
338
-
339
- const finalStartupTime = calcPerfTime(serverStartTime);
340
-
341
- Logger.info(`${this.context.config.appName} 启动成功!`);
342
- Logger.info(`服务器启动耗时: ${finalStartupTime}`);
343
- Logger.info(`服务器监听地址: ${server.url}`);
344
- Logger.info(`Mysql 数据库地址:${this.context.config.mysql.hostname}`);
345
- Logger.info(`Redis 缓存地址:${this.context.config.redis.hostname}`);
346
- try {
347
- process.stdout.write(`${this.context.config.appName} 启动成功!\n`);
348
- process.stdout.write(`服务器启动耗时: ${finalStartupTime}\n`);
349
- process.stdout.write(`服务器监听地址: ${server.url}\n`);
350
- process.stdout.write(`Mysql 数据库地址:${this.context.config.mysql.hostname}\n`);
351
- process.stdout.write(`Redis 缓存地址:${this.context.config.redis.hostname}\n`);
352
- } catch {
353
- // ignore
354
- }
355
- // 注意:作为库代码,这里不注册 SIGINT/SIGTERM 处理器,也不调用 process.exit。
356
- // 宿主应用应自行处理信号并决定退出策略(包括是否调用 server.stop / Connect.disconnect / Logger.flush)。
357
- return server;
358
- } catch (error) {
359
- // 注意:这里不能直接 process.exit(1)
360
- // - Logger 是异步缓冲写入,exit 会导致日志来不及 flush(实际项目里表现为“完全没打印”)
361
- // - 作为库代码,也不应该强行终止宿主进程
362
-
363
- // 注意:测试要求 start() 失败时必须 Logger.error 一次,所以这里始终调用 error。
364
- Logger.error("项目启动失败", error);
365
-
366
- // 尽力把错误日志落盘/输出后再把异常抛给上层。
367
- try {
368
- await Logger.flush();
369
- await Logger.shutdown();
370
- } catch {
371
- // ignore
372
- }
373
-
374
- throw createError("运行时错误", {
375
- cause: error,
376
- code: error?.code || "runtime",
377
- subsystem: "start",
378
- operation: "start"
379
- });
380
- }
381
- }
382
- }
@@ -2,10 +2,11 @@
2
2
  * 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
3
3
  */
4
4
 
5
+ import { Logger } from "#befly/libs/logger/index.js";
6
+
5
7
  import { BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "../configs/constConfig.js";
6
8
  import { createError } from "../utils/error.js";
7
9
  import { isNonEmptyString, isNullable, isString } from "../utils/is.js";
8
- import { Logger } from "./logger.js";
9
10
 
10
11
  /**
11
12
  * 缓存助手类
@@ -50,12 +51,6 @@ export class CacheHelper {
50
51
 
51
52
  async rebuildRoleCache(kind, roleValuesMap) {
52
53
  const roleCodes = Array.from(roleValuesMap.keys());
53
-
54
- if (roleCodes.length === 0) {
55
- Logger.info(`✅ 没有需要缓存的角色 ${kind}`);
56
- return;
57
- }
58
-
59
54
  const oldVersion = await this.getRoleCacheActiveVersion(kind);
60
55
  let nextVersion = String(Date.now());
61
56
 
@@ -80,7 +75,7 @@ export class CacheHelper {
80
75
  }
81
76
 
82
77
  await this.redis.del(nextCodesKey);
83
- await this.redis.sadd(nextCodesKey, roleCodes);
78
+ if (roleCodes.length > 0) await this.redis.sadd(nextCodesKey, roleCodes);
84
79
 
85
80
  if (nextItems.length > 0) {
86
81
  await this.redis.saddBatch(nextItems);