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.
- package/apis/dashboard/systemResources.js +3 -1
- package/apis/dict/_dict.js +1 -1
- package/apis/role/_role.js +2 -2
- package/apis/tongJi/dailyStats.js +2 -2
- package/apis/tongJi/dailyStatsDistribution.js +3 -3
- package/apis/tongJi/errorList.js +2 -2
- package/apis/tongJi/errorStats.js +6 -6
- package/checks/api.js +11 -6
- package/checks/items.js +56 -0
- package/checks/menu.js +1 -1
- package/checks/table.js +4 -5
- package/configs/beflyConfig.json +5 -10
- package/configs/constConfig.js +44 -3
- package/hooks/auth.js +3 -2
- package/hooks/parser.js +15 -4
- package/hooks/permission.js +3 -2
- package/hooks/rateLimit.js +16 -3
- package/hooks/validator.js +2 -1
- package/index.js +53 -24
- package/lib/cacheHelper.js +83 -229
- package/lib/connect.js +3 -13
- package/lib/dbHelper.js +57 -142
- package/lib/dbParse.js +65 -248
- package/lib/dbUtil.js +19 -63
- package/lib/logger.js +45 -22
- package/lib/redisHelper.js +186 -420
- package/lib/smtpText.js +7 -18
- package/lib/sqlBuilder.js +22 -51
- package/lib/xmlParse.js +15 -59
- package/package.json +1 -1
- package/plugins/cache.js +2 -3
- package/plugins/config.js +3 -1
- package/plugins/cron.js +2 -2
- package/plugins/email.js +3 -1
- package/plugins/logger.js +2 -2
- package/plugins/mysql.js +5 -10
- package/plugins/redis.js +4 -11
- package/plugins/tool.js +3 -3
- package/plugins/xmlParse.js +3 -1
- package/router/api.js +2 -6
- package/router/static.js +9 -10
- package/schemas/config.js +4 -4
- package/scripts/syncDb/context.js +4 -7
- package/scripts/syncDb/diff.js +2 -1
- package/scripts/syncDb/index.js +2 -6
- package/sync/api.js +7 -8
- package/sync/cache.js +3 -1
- package/sync/dev.js +28 -65
- package/sync/menu.js +5 -6
- package/sync/syncUtil.js +25 -0
- package/utils/calcPerfTime.js +2 -3
- package/utils/crypto.js +0 -40
- package/utils/deepMerge.js +2 -6
- package/utils/error.js +64 -0
- package/utils/fieldClear.js +13 -56
- package/utils/importDefault.js +11 -12
- package/utils/is.js +5 -182
- package/utils/loggerUtils.js +3 -1
- package/utils/response.js +12 -8
- package/utils/scanFiles.js +2 -53
- package/utils/scanSources.js +9 -0
- package/utils/util.js +25 -222
- package/checks/hook.js +0 -39
- package/checks/plugin.js +0 -39
- 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/
|
|
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
|
|
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 = [
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
// 自动同步(
|
|
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 || "
|
|
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
|
|
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
|
|
344
|
+
throw createError("运行时错误", {
|
|
316
345
|
cause: error,
|
|
317
346
|
code: error?.code || "runtime",
|
|
318
347
|
subsystem: "start",
|
package/lib/cacheHelper.js
CHANGED
|
@@ -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
|
-
*
|
|
109
|
+
* 缓存某类全部数据到 Redis(从数据库读取)
|
|
110
|
+
* @param kind - 数据类别(apis/menus),决定 Redis key 与日志文案
|
|
111
|
+
* @param table - 数据库表名
|
|
109
112
|
*/
|
|
110
|
-
async
|
|
113
|
+
async cacheAllItems(kind, table) {
|
|
111
114
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
table: "beflyApi"
|
|
115
|
+
const items = await this.mysql.getAll({
|
|
116
|
+
table: table
|
|
115
117
|
});
|
|
116
118
|
|
|
117
|
-
|
|
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: "
|
|
124
|
+
operation: "cacheAllItems"
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
} catch (error) {
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* -
|
|
147
|
+
* 缓存所有角色的某类权限到 Redis
|
|
148
|
+
* 全量重建:清理所有角色权限缓存并重建(DEL + SADD)
|
|
149
|
+
* @param kind - 权限类别(apis/menus),同时是 beflyRole 表字段名
|
|
171
150
|
*/
|
|
172
|
-
async
|
|
151
|
+
async cacheRoleItems(kind) {
|
|
173
152
|
try {
|
|
174
|
-
// 查询所有角色(仅取必要字段)
|
|
175
153
|
const roles = await this.mysql.getAll({
|
|
176
|
-
table:
|
|
177
|
-
fields: ["code",
|
|
154
|
+
table: BEFLY_ROLE_TABLE,
|
|
155
|
+
fields: ["code", kind]
|
|
178
156
|
});
|
|
179
157
|
|
|
180
|
-
const
|
|
158
|
+
const roleValuesMap = new Map();
|
|
181
159
|
|
|
182
160
|
for (const role of roles.data.lists) {
|
|
183
161
|
if (!role?.code) continue;
|
|
184
|
-
|
|
162
|
+
roleValuesMap.set(role.code, role[kind]);
|
|
185
163
|
}
|
|
186
164
|
|
|
187
|
-
await this.rebuildRoleCache(
|
|
165
|
+
await this.rebuildRoleCache(kind, roleValuesMap);
|
|
188
166
|
} catch (error) {
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
234
|
-
* -
|
|
186
|
+
* 增量刷新单个角色的某类权限缓存
|
|
187
|
+
* - paths 为空数组:仅清理缓存(防止残留)
|
|
188
|
+
* - paths 非空:使用最小查询,DEL 后 SADD
|
|
189
|
+
* @param kind - 权限类别(apis/menus)
|
|
235
190
|
*/
|
|
236
|
-
async
|
|
191
|
+
async refreshRoleItems(kind, roleCode, paths) {
|
|
237
192
|
if (!isNonEmptyString(roleCode)) {
|
|
238
|
-
throw
|
|
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(
|
|
246
|
-
throw
|
|
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(
|
|
199
|
+
const roleKey = await this.getRoleCacheReadKey(kind, roleCode);
|
|
255
200
|
|
|
256
201
|
// 空数组短路:保证清理残留
|
|
257
|
-
if (
|
|
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(
|
|
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
|
-
* -
|
|
273
|
-
* -
|
|
216
|
+
* 增量刷新单个角色的接口权限缓存
|
|
217
|
+
* - apiPaths 为空数组:仅清理缓存(防止残留)
|
|
218
|
+
* - apiPaths 非空:使用 $in 最小查询,DEL 后 SADD
|
|
274
219
|
*/
|
|
275
|
-
async
|
|
276
|
-
|
|
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
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
329
|
+
async deleteRoleItems(kind, roleCode) {
|
|
462
330
|
try {
|
|
463
|
-
const roleReadKey = await this.getRoleCacheReadKey(
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|