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/index.js CHANGED
@@ -6,16 +6,17 @@
6
6
  // 检查
7
7
  import { checkApi } from "#befly/checks/api.js";
8
8
  import { checkConfig } from "#befly/checks/config.js";
9
- import { checkHook } from "#befly/checks/hook.js";
9
+ import { checkHook, checkPlugin } from "#befly/checks/items.js";
10
10
  import { checkMenu } from "#befly/checks/menu.js";
11
- import { checkPlugin } from "#befly/checks/plugin.js";
12
11
  import { checkTable } from "#befly/checks/table.js";
13
12
  // 配置
14
13
  import beflyConfig from "#befly/configs/beflyConfig.json";
15
14
  import beflyMenus from "#befly/configs/beflyMenus.json";
15
+ import { BEFLY_ADMIN_TABLE, BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "#befly/configs/constConfig.js";
16
16
  // ========== 相对导入(项目内部文件) ==========
17
17
  // 基础设施
18
18
  import { Connect } from "#befly/lib/connect.js";
19
+ // oxlint-disable-next-line unicorn/prefer-export-from -- Logger 同时被本文件大量使用,不能合并为 re-export
19
20
  import { Logger } from "#befly/lib/logger.js";
20
21
  // 路由处理
21
22
  import { apiHandler } from "#befly/router/api.js";
@@ -27,14 +28,20 @@ import { syncDev } from "#befly/sync/dev.js";
27
28
  import { syncMenu } from "#befly/sync/menu.js";
28
29
  // 工具
29
30
  import { calcPerfTime } from "#befly/utils/calcPerfTime.js";
31
+ import { formatYmdHms } from "#befly/utils/datetime.js";
30
32
  import { deepMerge } from "#befly/utils/deepMerge.js";
33
+ import { createError } from "#befly/utils/error.js";
31
34
  import { isPrimaryProcess } from "#befly/utils/is.js";
32
35
  import { scanSources } from "#befly/utils/scanSources.js";
36
+ import { getRunMode, waitFor } from "#befly/utils/util.js";
33
37
 
34
38
  export { syncDb } from "#befly/scripts/syncDb/index.js";
35
39
  export { xmlParse } from "#befly/lib/xmlParse.js";
36
40
  export { Logger };
37
41
 
42
+ // 主进程同步完成的 Redis 就绪标记(非主进程等待此标记后再对外服务)
43
+ const SYNC_READY_KEY = "befly:syncReady";
44
+
38
45
  function prefixMenuPaths(menus, prefix) {
39
46
  return menus.map((menu) => {
40
47
  const nextMenu = {
@@ -58,8 +65,7 @@ function prefixMenuPaths(menus, prefix) {
58
65
  async function syncReady(ctx) {
59
66
  const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
60
67
  if (missingCtxKeys.length > 0) {
61
- throw new Error(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
62
- cause: null,
68
+ throw createError(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
63
69
  code: "runtime",
64
70
  subsystem: "sync",
65
71
  operation: "ctxCheck",
@@ -67,7 +73,7 @@ async function syncReady(ctx) {
67
73
  });
68
74
  }
69
75
 
70
- const requiredTables = ["beflyApi", "beflyMenu", "beflyAdmin", "beflyRole"];
76
+ const requiredTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
71
77
  const missingTables = [];
72
78
 
73
79
  for (const table of requiredTables) {
@@ -77,8 +83,7 @@ async function syncReady(ctx) {
77
83
  }
78
84
 
79
85
  if (missingTables.length > 0) {
80
- throw new Error(`同步依赖表缺失: ${missingTables.join("、")}`, {
81
- cause: null,
86
+ throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, {
82
87
  code: "runtime",
83
88
  subsystem: "sync",
84
89
  operation: "tableExists",
@@ -93,7 +98,7 @@ export async function createBefly(config = {}, menus = []) {
93
98
  const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
94
99
 
95
100
  Logger.configure({
96
- runtimeEnv: mergedConfig.runMode,
101
+ runtimeEnv: getRunMode(),
97
102
  ...mergedConfig.logger
98
103
  });
99
104
  Logger.info(`启动 合并配置 耗时 ${calcPerfTime(mergeStartTime)}`);
@@ -127,14 +132,13 @@ export async function createBefly(config = {}, menus = []) {
127
132
  Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
128
133
 
129
134
  if (configHasError || apiHasError || tableHasError || pluginHasError || hookHasError || menuHasError) {
130
- throw new Error("检查失败:存在配置/结构问题", {
131
- cause: null,
132
- code: "policy",
133
- subsystem: "checks",
134
- operation: "checkAll"
135
- });
135
+ throw createError("检查失败:存在配置/结构问题", { code: "policy", subsystem: "checks", operation: "checkAll" });
136
136
  }
137
137
 
138
+ // 运行模式以环境为准(NODE_ENV,线上部署注入 production;本地开发默认 development),
139
+ // 覆盖配置值,保证 befly.config.runMode 与运行时判断一致
140
+ mergedConfig.runMode = getRunMode();
141
+
138
142
  return new Befly({
139
143
  config: mergedConfig,
140
144
  tables: tables,
@@ -168,12 +172,7 @@ export class Befly {
168
172
  return;
169
173
  }
170
174
 
171
- throw new Error("请使用 createBefly(...) 创建实例后再调用 start", {
172
- cause: null,
173
- code: "policy",
174
- subsystem: "start",
175
- operation: "factoryGuard"
176
- });
175
+ throw createError("请使用 createBefly(...) 创建实例后再调用 start", { code: "policy", subsystem: "start", operation: "factoryGuard" });
177
176
  }
178
177
 
179
178
  /**
@@ -207,7 +206,7 @@ export class Befly {
207
206
  await syncReady(this.context);
208
207
  Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
209
208
 
210
- // 自动同步(PM2 cluster:主进程执行,其它进程等待同步完成)
209
+ // 自动同步(bm2 多实例:主进程执行同步并写就绪标记,其它进程等待同步完成后再对外服务)
211
210
  if (isPrimaryProcess()) {
212
211
  stageStartTime = Bun.nanoseconds();
213
212
  await syncApi(this.context, this.apis);
@@ -224,6 +223,20 @@ export class Befly {
224
223
  stageStartTime = Bun.nanoseconds();
225
224
  await syncCache(this.context);
226
225
  Logger.info(`启动 同步缓存 耗时 ${calcPerfTime(stageStartTime)}`);
226
+
227
+ // 同步完成:写入就绪标记(TTL 防崩溃残留),非主进程据此放行
228
+ await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
229
+ } else {
230
+ // 非主进程:在监听端口前等待主进程本次同步完成(时间戳判断,同机时钟一致)
231
+ const startAt = Date.now();
232
+ await waitFor({
233
+ label: "主进程同步完成",
234
+ check: async () => {
235
+ const ready = await this.context.redis.getString(SYNC_READY_KEY);
236
+ return ready !== null && Number(ready) > startAt;
237
+ }
238
+ });
239
+ Logger.info("启动 等待主进程同步完成 完成");
227
240
  }
228
241
 
229
242
  // 加载钩子
@@ -241,7 +254,7 @@ export class Befly {
241
254
  stageStartTime = Bun.nanoseconds();
242
255
  const server = Bun.serve({
243
256
  port: this.context.config.appPort || 3000,
244
- hostname: this.context.config.appHost || "0.0.0.0",
257
+ hostname: this.context.config.appHost || "127.0.0.1",
245
258
  // 开发模式下启用详细错误信息
246
259
  development: this.context.config.runMode === "development",
247
260
  // 空闲连接超时时间(秒),防止恶意连接占用资源
@@ -256,6 +269,22 @@ export class Befly {
256
269
  return staticFetch(req);
257
270
  }
258
271
 
272
+ // 根路径返回服务基本信息
273
+ if (url.pathname === "/") {
274
+ return Response.json(
275
+ {
276
+ code: 0,
277
+ msg: "success",
278
+ data: {
279
+ name: this.context.config.appName,
280
+ mode: this.context.config.runMode,
281
+ time: formatYmdHms(Date.now())
282
+ }
283
+ },
284
+ { status: 200 }
285
+ );
286
+ }
287
+
259
288
  return Response.json({ code: 1, msg: "路径不存在" }, { status: 200 });
260
289
  },
261
290
  error: (error) => {
@@ -302,7 +331,7 @@ export class Befly {
302
331
  // - 作为库代码,也不应该强行终止宿主进程
303
332
 
304
333
  // 注意:测试要求 start() 失败时必须 Logger.error 一次,所以这里始终调用 error。
305
- Logger.error("项目启动失败", error, null, false);
334
+ Logger.error("项目启动失败", error);
306
335
 
307
336
  // 尽力把错误日志落盘/输出后再把异常抛给上层。
308
337
  try {
@@ -312,7 +341,7 @@ export class Befly {
312
341
  // ignore
313
342
  }
314
343
 
315
- throw new Error("运行时错误", {
344
+ throw createError("运行时错误", {
316
345
  cause: error,
317
346
  code: error?.code || "runtime",
318
347
  subsystem: "start",
@@ -1,8 +1,9 @@
1
1
  /**
2
- * 缓存助手 - JavaScript 版本
3
2
  * 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
4
3
  */
5
4
 
5
+ import { BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "../configs/constConfig.js";
6
+ import { createError } from "../utils/error.js";
6
7
  import { isNonEmptyString, isNullable, isString } from "../utils/is.js";
7
8
  import { Logger } from "./logger.js";
8
9
 
@@ -105,161 +106,105 @@ export class CacheHelper {
105
106
  }
106
107
 
107
108
  /**
108
- * 缓存所有接口到 Redis
109
+ * 缓存某类全部数据到 Redis(从数据库读取)
110
+ * @param kind - 数据类别(apis/menus),决定 Redis key 与日志文案
111
+ * @param table - 数据库表名
109
112
  */
110
- async cacheApis() {
113
+ async cacheAllItems(kind, table) {
111
114
  try {
112
- // 从数据库查询所有接口
113
- const apiList = await this.mysql.getAll({
114
- table: "beflyApi"
115
+ const items = await this.mysql.getAll({
116
+ table: table
115
117
  });
116
118
 
117
- // 缓存到 Redis
118
- const result = await this.redis.setObject("apis:all", apiList.data.lists);
119
+ const result = await this.redis.setObject(`${kind}:all`, items.data.lists);
119
120
 
120
121
  if (result === null) {
121
- Logger.warn("⚠️ 接口缓存失败", {
122
+ Logger.warn(`⚠️ ${kind} 缓存失败`, {
122
123
  subsystem: "cache",
123
- operation: "cacheApis"
124
+ operation: "cacheAllItems"
124
125
  });
125
126
  }
126
127
  } catch (error) {
127
- Logger.error("⚠️ 接口缓存异常", error);
128
- throw new Error("接口缓存异常", {
129
- cause: error,
130
- code: "runtime",
131
- subsystem: "cache",
132
- operation: "cacheApis"
133
- });
128
+ throw createError(`${kind} 缓存异常`, { cause: error, code: "runtime", subsystem: "cache", operation: "cacheAllItems" });
134
129
  }
135
130
  }
136
131
 
132
+ /**
133
+ * 缓存所有接口到 Redis
134
+ */
135
+ async cacheApis() {
136
+ return this.cacheAllItems("apis", BEFLY_API_TABLE);
137
+ }
138
+
137
139
  /**
138
140
  * 缓存所有菜单到 Redis(从数据库读取)
139
141
  */
140
142
  async cacheMenus() {
141
- try {
142
- // 从数据库查询所有菜单
143
- const menus = await this.mysql.getAll({
144
- table: "beflyMenu"
145
- });
146
-
147
- // 缓存到 Redis
148
- const result = await this.redis.setObject("menus:all", menus.data.lists);
149
-
150
- if (result === null) {
151
- Logger.warn("⚠️ 菜单缓存失败", {
152
- subsystem: "cache",
153
- operation: "cacheMenus"
154
- });
155
- }
156
- } catch (error) {
157
- Logger.error("⚠️ 菜单缓存异常", error);
158
- throw new Error("菜单缓存异常", {
159
- cause: error,
160
- code: "runtime",
161
- subsystem: "cache",
162
- operation: "cacheMenus"
163
- });
164
- }
143
+ return this.cacheAllItems("menus", BEFLY_MENU_TABLE);
165
144
  }
166
145
 
167
146
  /**
168
- * 缓存所有角色的接口权限到 Redis
169
- * 全量重建:清理所有角色权限缓存并重建
170
- * - 极简方案:每个角色一个 Set,直接覆盖更新(DEL + SADD)
147
+ * 缓存所有角色的某类权限到 Redis
148
+ * 全量重建:清理所有角色权限缓存并重建(DEL + SADD)
149
+ * @param kind - 权限类别(apis/menus),同时是 beflyRole 表字段名
171
150
  */
172
- async cacheRoleApis() {
151
+ async cacheRoleItems(kind) {
173
152
  try {
174
- // 查询所有角色(仅取必要字段)
175
153
  const roles = await this.mysql.getAll({
176
- table: "beflyRole",
177
- fields: ["code", "apis"]
154
+ table: BEFLY_ROLE_TABLE,
155
+ fields: ["code", kind]
178
156
  });
179
157
 
180
- const roleApiPathsMap = new Map();
158
+ const roleValuesMap = new Map();
181
159
 
182
160
  for (const role of roles.data.lists) {
183
161
  if (!role?.code) continue;
184
- roleApiPathsMap.set(role.code, role.apis);
162
+ roleValuesMap.set(role.code, role[kind]);
185
163
  }
186
164
 
187
- await this.rebuildRoleCache("apis", roleApiPathsMap);
165
+ await this.rebuildRoleCache(kind, roleValuesMap);
188
166
  } catch (error) {
189
- Logger.error("⚠️ 角色权限缓存异常(将阻断启动)", error);
190
- throw new Error("⚠️ 角色权限缓存异常(将阻断启动)", {
191
- cause: error,
192
- code: "runtime",
193
- subsystem: "cache",
194
- operation: "cacheRoleApis"
195
- });
167
+ throw createError(`⚠️ 角色${kind}权限缓存异常(将阻断启动)`, { cause: error, code: "runtime", subsystem: "cache", operation: "cacheRoleItems" });
196
168
  }
197
169
  }
198
170
 
171
+ /**
172
+ * 缓存所有角色的接口权限到 Redis
173
+ */
174
+ async cacheRoleApis() {
175
+ return this.cacheRoleItems("apis");
176
+ }
177
+
199
178
  /**
200
179
  * 缓存所有角色的菜单权限到 Redis
201
- * 全量重建:清理所有角色菜单权限缓存并重建
202
- * - 每个角色一个 Set,直接覆盖更新(DEL + SADD)
203
180
  */
204
181
  async cacheRoleMenus() {
205
- try {
206
- // 查询所有角色(仅取必要字段)
207
- const roles = await this.mysql.getAll({
208
- table: "beflyRole",
209
- fields: ["code", "menus"]
210
- });
211
-
212
- const roleMenuPathsMap = new Map();
213
-
214
- for (const role of roles.data.lists) {
215
- if (!role?.code) continue;
216
- roleMenuPathsMap.set(role.code, role.menus);
217
- }
218
-
219
- await this.rebuildRoleCache("menus", roleMenuPathsMap);
220
- } catch (error) {
221
- Logger.error("⚠️ 角色菜单权限缓存异常(将阻断启动)", error);
222
- throw new Error("⚠️ 角色菜单权限缓存异常(将阻断启动)", {
223
- cause: error,
224
- code: "runtime",
225
- subsystem: "cache",
226
- operation: "cacheRoleMenus"
227
- });
228
- }
182
+ return this.cacheRoleItems("menus");
229
183
  }
230
184
 
231
185
  /**
232
- * 增量刷新单个角色的接口权限缓存
233
- * - apiIds 为空数组:仅清理缓存(防止残留)
234
- * - apiIds 非空:使用 $in 最小查询,DEL 后 SADD
186
+ * 增量刷新单个角色的某类权限缓存
187
+ * - paths 为空数组:仅清理缓存(防止残留)
188
+ * - paths 非空:使用最小查询,DEL 后 SADD
189
+ * @param kind - 权限类别(apis/menus)
235
190
  */
236
- async refreshRoleApis(roleCode, apiPaths) {
191
+ async refreshRoleItems(kind, roleCode, paths) {
237
192
  if (!isNonEmptyString(roleCode)) {
238
- throw new Error("roleCode 参数不合法", {
239
- cause: null,
240
- code: "validation",
241
- subsystem: "cache",
242
- operation: "refreshRoleApis"
243
- });
193
+ throw createError("roleCode 参数不合法", { code: "validation", subsystem: "cache", operation: "refreshRoleItems" });
244
194
  }
245
- if (!Array.isArray(apiPaths)) {
246
- throw new Error("apiPaths 参数不合法", {
247
- cause: null,
248
- code: "validation",
249
- subsystem: "cache",
250
- operation: "refreshRoleApis"
251
- });
195
+ if (!Array.isArray(paths)) {
196
+ throw createError("paths 参数不合法", { code: "validation", subsystem: "cache", operation: "refreshRoleItems" });
252
197
  }
253
198
 
254
- const roleKey = await this.getRoleCacheReadKey("apis", roleCode);
199
+ const roleKey = await this.getRoleCacheReadKey(kind, roleCode);
255
200
 
256
201
  // 空数组短路:保证清理残留
257
- if (apiPaths.length === 0) {
202
+ if (paths.length === 0) {
258
203
  await this.redis.del(roleKey);
259
204
  return;
260
205
  }
261
206
 
262
- const members = Array.from(new Set(apiPaths));
207
+ const members = Array.from(new Set(paths));
263
208
 
264
209
  await this.redis.del(roleKey);
265
210
  if (members.length > 0) {
@@ -268,59 +213,21 @@ export class CacheHelper {
268
213
  }
269
214
 
270
215
  /**
271
- * 增量刷新单个角色的菜单权限缓存
272
- * - menuPaths 为空数组:仅清理缓存(防止残留)
273
- * - menuPaths 非空:DEL 后 SADD
216
+ * 增量刷新单个角色的接口权限缓存
217
+ * - apiPaths 为空数组:仅清理缓存(防止残留)
218
+ * - apiPaths 非空:使用 $in 最小查询,DEL 后 SADD
274
219
  */
275
- async refreshRoleMenus(roleCode, menuPaths) {
276
- if (!isNonEmptyString(roleCode)) {
277
- throw new Error("roleCode 参数不合法", {
278
- cause: null,
279
- code: "validation",
280
- subsystem: "cache",
281
- operation: "refreshRoleMenus"
282
- });
283
- }
284
- if (!Array.isArray(menuPaths)) {
285
- throw new Error("menuPaths 参数不合法", {
286
- cause: null,
287
- code: "validation",
288
- subsystem: "cache",
289
- operation: "refreshRoleMenus"
290
- });
291
- }
292
-
293
- const roleKey = await this.getRoleCacheReadKey("menus", roleCode);
294
-
295
- // 空数组短路:保证清理残留
296
- if (menuPaths.length === 0) {
297
- await this.redis.del(roleKey);
298
- return;
299
- }
300
-
301
- const members = Array.from(new Set(menuPaths));
302
-
303
- await this.redis.del(roleKey);
304
- if (members.length > 0) {
305
- await this.redis.sadd(roleKey, members);
306
- }
220
+ async refreshRoleApis(roleCode, apiPaths) {
221
+ return this.refreshRoleItems("apis", roleCode, apiPaths);
307
222
  }
308
223
 
309
224
  /**
310
- * 缓存所有数据
225
+ * 增量刷新单个角色的菜单权限缓存
226
+ * - menuPaths 为空数组:仅清理缓存(防止残留)
227
+ * - menuPaths 非空:DEL 后 SADD
311
228
  */
312
- async cacheAll() {
313
- // 1. 缓存接口
314
- await this.cacheApis();
315
-
316
- // 2. 缓存菜单
317
- await this.cacheMenus();
318
-
319
- // 3. 缓存角色权限
320
- await this.cacheRoleApis();
321
-
322
- // 4. 缓存角色菜单权限
323
- await this.cacheRoleMenus();
229
+ async refreshRoleMenus(roleCode, menuPaths) {
230
+ return this.refreshRoleItems("menus", roleCode, menuPaths);
324
231
  }
325
232
 
326
233
  /**
@@ -332,13 +239,7 @@ export class CacheHelper {
332
239
  const apis = await this.redis.getObject("apis:all");
333
240
  return apis || [];
334
241
  } catch (error) {
335
- Logger.error("获取接口缓存失败", error);
336
- throw new Error("获取接口缓存失败", {
337
- cause: error,
338
- code: "runtime",
339
- subsystem: "cache",
340
- operation: "getApis"
341
- });
242
+ throw createError("获取接口缓存失败", { cause: error, code: "runtime", subsystem: "cache", operation: "getApis" });
342
243
  }
343
244
  }
344
245
 
@@ -351,13 +252,7 @@ export class CacheHelper {
351
252
  const menus = await this.redis.getObject("menus:all");
352
253
  return menus || [];
353
254
  } catch (error) {
354
- Logger.error("获取菜单缓存失败", error);
355
- throw new Error("获取菜单缓存失败", {
356
- cause: error,
357
- code: "runtime",
358
- subsystem: "cache",
359
- operation: "getMenus"
360
- });
255
+ throw createError("获取菜单缓存失败", { cause: error, code: "runtime", subsystem: "cache", operation: "getMenus" });
361
256
  }
362
257
  }
363
258
 
@@ -372,14 +267,7 @@ export class CacheHelper {
372
267
  const permissions = await this.redis.smembers(roleKey);
373
268
  return permissions || [];
374
269
  } catch (error) {
375
- Logger.error("获取角色权限缓存失败", error, { roleCode: roleCode });
376
- throw new Error("获取角色权限缓存失败", {
377
- cause: error,
378
- code: "runtime",
379
- subsystem: "cache",
380
- operation: "getRoleApis",
381
- roleCode: roleCode
382
- });
270
+ throw createError("获取角色权限缓存失败", { cause: error, code: "runtime", subsystem: "cache", operation: "getRoleApis", roleCode: roleCode });
383
271
  }
384
272
  }
385
273
 
@@ -394,14 +282,7 @@ export class CacheHelper {
394
282
  const permissions = await this.redis.smembers(roleKey);
395
283
  return permissions || [];
396
284
  } catch (error) {
397
- Logger.error("获取角色菜单权限缓存失败", error, { roleCode: roleCode });
398
- throw new Error("获取角色菜单权限缓存失败", {
399
- cause: error,
400
- code: "runtime",
401
- subsystem: "cache",
402
- operation: "getRoleMenus",
403
- roleCode: roleCode
404
- });
285
+ throw createError("获取角色菜单权限缓存失败", { cause: error, code: "runtime", subsystem: "cache", operation: "getRoleMenus", roleCode: roleCode });
405
286
  }
406
287
  }
407
288
 
@@ -418,14 +299,7 @@ export class CacheHelper {
418
299
  const roleKey = await this.getRoleCacheReadKey("apis", roleCode);
419
300
  return await this.redis.sismember(roleKey, value);
420
301
  } catch (error) {
421
- Logger.error("检查角色权限失败", error, { roleCode: roleCode });
422
- throw new Error("检查角色权限失败", {
423
- cause: error,
424
- code: "runtime",
425
- subsystem: "cache",
426
- operation: "checkRoleApis",
427
- roleCode: roleCode
428
- });
302
+ throw createError("检查角色权限失败", { cause: error, code: "runtime", subsystem: "cache", operation: "checkRoleApis", roleCode: roleCode });
429
303
  }
430
304
  }
431
305
 
@@ -442,66 +316,46 @@ export class CacheHelper {
442
316
  const roleKey = await this.getRoleCacheReadKey("menus", roleCode);
443
317
  return await this.redis.sismember(roleKey, value);
444
318
  } catch (error) {
445
- Logger.error("检查角色菜单权限失败", error, { roleCode: roleCode });
446
- throw new Error("检查角色菜单权限失败", {
447
- cause: error,
448
- code: "runtime",
449
- subsystem: "cache",
450
- operation: "checkRoleMenus",
451
- roleCode: roleCode
452
- });
319
+ throw createError("检查角色菜单权限失败", { cause: error, code: "runtime", subsystem: "cache", operation: "checkRoleMenus", roleCode: roleCode });
453
320
  }
454
321
  }
455
322
 
456
323
  /**
457
- * 删除角色的接口权限缓存
324
+ * 删除角色的某类权限缓存
325
+ * @param kind - 权限类别(apis/menus)
458
326
  * @param roleCode - 角色代码
459
327
  * @returns 是否删除成功
460
328
  */
461
- async deleteRoleApis(roleCode) {
329
+ async deleteRoleItems(kind, roleCode) {
462
330
  try {
463
- const roleReadKey = await this.getRoleCacheReadKey("apis", roleCode);
464
- const result = await this.redis.delBatch([`role:apis:${roleCode}`, roleReadKey]);
331
+ const roleReadKey = await this.getRoleCacheReadKey(kind, roleCode);
332
+ // 同时清理无版本旧 key(permission hook 的兜底读取路径)
333
+ const result = await this.redis.delBatch([`role:${kind}:${roleCode}`, roleReadKey]);
465
334
  if (result > 0) {
466
- Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
335
+ Logger.info(`✅ 已删除角色 ${roleCode} 的${kind}权限缓存`);
467
336
  return true;
468
337
  }
469
338
  return false;
470
339
  } catch (error) {
471
- Logger.error("删除角色权限缓存失败", error, { roleCode: roleCode });
472
- throw new Error("删除角色权限缓存失败", {
473
- cause: error,
474
- code: "runtime",
475
- subsystem: "cache",
476
- operation: "deleteRoleApis",
477
- roleCode: roleCode
478
- });
340
+ throw createError("删除角色权限缓存失败", { cause: error, code: "runtime", subsystem: "cache", operation: "deleteRoleItems", roleCode: roleCode });
479
341
  }
480
342
  }
481
343
 
344
+ /**
345
+ * 删除角色的接口权限缓存
346
+ * @param roleCode - 角色代码
347
+ * @returns 是否删除成功
348
+ */
349
+ async deleteRoleApis(roleCode) {
350
+ return this.deleteRoleItems("apis", roleCode);
351
+ }
352
+
482
353
  /**
483
354
  * 删除角色的菜单权限缓存
484
355
  * @param roleCode - 角色代码
485
356
  * @returns 是否删除成功
486
357
  */
487
358
  async deleteRoleMenus(roleCode) {
488
- try {
489
- const roleReadKey = await this.getRoleCacheReadKey("menus", roleCode);
490
- const result = await this.redis.delBatch([`role:menus:${roleCode}`, roleReadKey]);
491
- if (result > 0) {
492
- Logger.info(`✅ 已删除角色 ${roleCode} 的菜单权限缓存`);
493
- return true;
494
- }
495
- return false;
496
- } catch (error) {
497
- Logger.error("删除角色菜单权限缓存失败", error, { roleCode: roleCode });
498
- throw new Error("删除角色菜单权限缓存失败", {
499
- cause: error,
500
- code: "runtime",
501
- subsystem: "cache",
502
- operation: "deleteRoleMenus",
503
- roleCode: roleCode
504
- });
505
- }
359
+ return this.deleteRoleItems("menus", roleCode);
506
360
  }
507
361
  }
package/lib/connect.js CHANGED
@@ -1,16 +1,13 @@
1
1
  /**
2
- * 连接管理器 - JavaScript 版本
3
2
  * 负责管理数据库和 Redis 连接
4
3
  */
5
4
 
6
5
  import { RedisClient, SQL } from "bun";
7
6
 
7
+ import { createError } from "../utils/error.js";
8
+ import { getRunMode } from "../utils/util.js";
8
9
  import { Logger } from "./logger.js";
9
10
 
10
- function getRunMode() {
11
- return Bun.env.RUN_MODE || "unknown";
12
- }
13
-
14
11
  function buildRedisUrl(config) {
15
12
  if (config.username) {
16
13
  const encodedUser = encodeURIComponent(config.username);
@@ -52,14 +49,7 @@ export class Connect {
52
49
  static async handleConnectError(clientKey, label, subsystem, hostname, error) {
53
50
  await this[clientKey]?.close();
54
51
  this[clientKey] = null;
55
- Logger.error(`${label} 连接失败 (${hostname} ${getRunMode()})`, error);
56
- throw new Error(`${label} 连接失败 (${hostname})`, {
57
- cause: error,
58
- code: "runtime",
59
- subsystem: subsystem,
60
- operation: "connect",
61
- runMode: getRunMode()
62
- });
52
+ throw createError(`${label} 连接失败 (${hostname})`, { cause: error, code: "runtime", subsystem: subsystem, operation: "connect", runMode: getRunMode() });
63
53
  }
64
54
 
65
55
  /**