befly 3.24.17 → 3.24.19
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/admin/cacheRefresh.js +2 -2
- package/apis/dashboard/environmentInfo.js +6 -1
- package/apis/dashboard/performanceMetrics.js +11 -8
- package/apis/dashboard/serviceStatus.js +78 -60
- package/apis/tongJi/cacheHealth.js +214 -0
- package/apis/tongJi/errorReport.js +72 -1
- package/apis/tongJi/errorStats.js +160 -5
- package/apis/tongJi/fallbackReset.js +69 -0
- package/apis/tongJi/infoReport.js +116 -16
- package/apis/tongJi/infoStats.js +160 -68
- package/apis/tongJi/onlineReport.js +14 -23
- package/apis/tongJi/onlineStats.js +94 -91
- package/hooks/permission.js +6 -2
- package/hooks/rateLimit.js +245 -0
- package/lib/cacheHelper.js +105 -60
- package/lib/redisHelper.js +68 -0
- package/lib/requestMetrics.js +203 -0
- package/package.json +2 -2
- package/plugins/email.js +6 -2
- package/router/api.js +7 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
|
|
3
|
+
import { isValidPositiveInt } from "../utils/is.js";
|
|
4
|
+
import { ErrorResponse } from "../utils/response.js";
|
|
5
|
+
|
|
6
|
+
const RATE_LIMIT_ENABLED_VALUES = new Set([1, true, "1", "true"]);
|
|
7
|
+
|
|
8
|
+
function isRateLimitEnabled(value) {
|
|
9
|
+
return RATE_LIMIT_ENABLED_VALUES.has(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toPositiveInt(value, fallbackValue) {
|
|
13
|
+
const num = Number(value);
|
|
14
|
+
|
|
15
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
16
|
+
return fallbackValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Math.floor(num);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toSafeString(value, fallbackValue) {
|
|
23
|
+
if (typeof value !== "string") {
|
|
24
|
+
return fallbackValue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return fallbackValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toRuleMethods(rule) {
|
|
37
|
+
if (Array.isArray(rule?.methods)) {
|
|
38
|
+
const values = [];
|
|
39
|
+
|
|
40
|
+
for (const method of rule.methods) {
|
|
41
|
+
const text = toSafeString(method, "");
|
|
42
|
+
if (!text) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
values.push(text.toUpperCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof rule?.method === "string") {
|
|
53
|
+
const value = toSafeString(rule.method, "");
|
|
54
|
+
if (value) {
|
|
55
|
+
return [value.toUpperCase()];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeRule(rawRule, defaultLimit, defaultWindow, defaultKeyType) {
|
|
63
|
+
const rule = rawRule && typeof rawRule === "object" ? rawRule : {};
|
|
64
|
+
const pathPattern = toSafeString(rule.path || rule.apiPath || rule.route, "");
|
|
65
|
+
const methods = toRuleMethods(rule);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
pathPattern: pathPattern,
|
|
69
|
+
methods: methods,
|
|
70
|
+
limit: toPositiveInt(rule.limit, defaultLimit),
|
|
71
|
+
window: toPositiveInt(rule.window, defaultWindow),
|
|
72
|
+
keyType: toSafeString(rule.key, defaultKeyType)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createMatcher(pattern) {
|
|
77
|
+
try {
|
|
78
|
+
return picomatch(pattern);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeConfig(config) {
|
|
85
|
+
const defaultLimit = toPositiveInt(config?.defaultLimit, 1000);
|
|
86
|
+
const defaultWindow = toPositiveInt(config?.defaultWindow, 60);
|
|
87
|
+
const defaultKeyType = toSafeString(config?.key, "ip");
|
|
88
|
+
|
|
89
|
+
const skipMatchers = [];
|
|
90
|
+
const skipRoutes = Array.isArray(config?.skipRoutes) ? config.skipRoutes : [];
|
|
91
|
+
|
|
92
|
+
for (const route of skipRoutes) {
|
|
93
|
+
const pattern = toSafeString(route, "");
|
|
94
|
+
if (!pattern) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const matcher = createMatcher(pattern);
|
|
99
|
+
if (!matcher) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
skipMatchers.push(matcher);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rules = [];
|
|
107
|
+
const rawRules = Array.isArray(config?.rules) ? config.rules : [];
|
|
108
|
+
|
|
109
|
+
for (const rawRule of rawRules) {
|
|
110
|
+
const normalizedRule = normalizeRule(rawRule, defaultLimit, defaultWindow, defaultKeyType);
|
|
111
|
+
|
|
112
|
+
if (!normalizedRule.pathPattern) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const matcher = createMatcher(normalizedRule.pathPattern);
|
|
117
|
+
if (!matcher) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
rules.push({
|
|
122
|
+
matcher: matcher,
|
|
123
|
+
methods: normalizedRule.methods,
|
|
124
|
+
limit: normalizedRule.limit,
|
|
125
|
+
window: normalizedRule.window,
|
|
126
|
+
keyType: normalizedRule.keyType
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
enabled: isRateLimitEnabled(config?.enable),
|
|
132
|
+
defaultLimit: defaultLimit,
|
|
133
|
+
defaultWindow: defaultWindow,
|
|
134
|
+
defaultKeyType: defaultKeyType,
|
|
135
|
+
skipMatchers: skipMatchers,
|
|
136
|
+
rules: rules
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveScopeByKeyType(keyType, ctx) {
|
|
141
|
+
if (keyType === "userId") {
|
|
142
|
+
if (isValidPositiveInt(ctx.userId)) {
|
|
143
|
+
return `user:${ctx.userId}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `ip:${ctx.ip || "unknown"}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (keyType === "roleCode") {
|
|
150
|
+
return `role:${ctx.roleCode || "guest"}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (keyType === "apiPath") {
|
|
154
|
+
return `path:${ctx.apiPath || "unknown"}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (keyType === "global") {
|
|
158
|
+
return "global";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `ip:${ctx.ip || "unknown"}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveRateRule(normalized, ctx) {
|
|
165
|
+
for (const rule of normalized.rules) {
|
|
166
|
+
if (!rule.matcher(ctx.apiPath)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (rule.methods.length > 0 && !rule.methods.includes(ctx.method)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
limit: rule.limit,
|
|
176
|
+
window: rule.window,
|
|
177
|
+
keyType: rule.keyType
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
limit: normalized.defaultLimit,
|
|
183
|
+
window: normalized.defaultWindow,
|
|
184
|
+
keyType: normalized.defaultKeyType
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function shouldSkipRateLimit(normalized, ctx) {
|
|
189
|
+
for (const matcher of normalized.skipMatchers) {
|
|
190
|
+
if (matcher(ctx.apiPath)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildRateLimitRedisKey(ctx, keyType, scope) {
|
|
199
|
+
return `rate_limit:${ctx.apiPath}:${ctx.method}:${keyType}:${scope}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default {
|
|
203
|
+
order: 1.5,
|
|
204
|
+
handler: async (befly, ctx) => {
|
|
205
|
+
const normalized = normalizeConfig(befly?.config?.rateLimit);
|
|
206
|
+
|
|
207
|
+
if (!normalized.enabled) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!befly?.redis || typeof befly.redis.incrWithExpire !== "function") {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (shouldSkipRateLimit(normalized, ctx)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rateRule = resolveRateRule(normalized, ctx);
|
|
220
|
+
const scope = resolveScopeByKeyType(rateRule.keyType, ctx);
|
|
221
|
+
const redisKey = buildRateLimitRedisKey(ctx, rateRule.keyType, scope);
|
|
222
|
+
const current = await befly.redis.incrWithExpire(redisKey, rateRule.window);
|
|
223
|
+
|
|
224
|
+
if (current <= rateRule.limit) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ctx.corsHeaders = ctx.corsHeaders || {};
|
|
229
|
+
ctx.corsHeaders["Retry-After"] = String(rateRule.window);
|
|
230
|
+
ctx.response = ErrorResponse(
|
|
231
|
+
ctx,
|
|
232
|
+
"请求过于频繁,请稍后再试",
|
|
233
|
+
1,
|
|
234
|
+
null,
|
|
235
|
+
{
|
|
236
|
+
limit: rateRule.limit,
|
|
237
|
+
window: rateRule.window,
|
|
238
|
+
current: current,
|
|
239
|
+
apiPath: ctx.apiPath,
|
|
240
|
+
key: rateRule.keyType
|
|
241
|
+
},
|
|
242
|
+
"rateLimit"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
};
|
package/lib/cacheHelper.js
CHANGED
|
@@ -15,6 +15,95 @@ export class CacheHelper {
|
|
|
15
15
|
this.redis = deps.redis;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
getRoleCacheVersionKey(kind) {
|
|
19
|
+
return `role:${kind}:activeVersion`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getRoleCacheCodesKey(kind, version) {
|
|
23
|
+
return `role:${kind}:v:${version}:codes`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getRoleCacheItemKey(kind, roleCode, version) {
|
|
27
|
+
if (!version) {
|
|
28
|
+
return `role:${kind}:${roleCode}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `role:${kind}:v:${version}:${roleCode}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getRoleCacheActiveVersion(kind) {
|
|
35
|
+
const versionKey = this.getRoleCacheVersionKey(kind);
|
|
36
|
+
const version = await this.redis.getString(versionKey);
|
|
37
|
+
|
|
38
|
+
if (!isNonEmptyString(version)) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return version;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getRoleCacheReadKey(kind, roleCode) {
|
|
46
|
+
const version = await this.getRoleCacheActiveVersion(kind);
|
|
47
|
+
return this.getRoleCacheItemKey(kind, roleCode, version);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async rebuildRoleCache(kind, roleValuesMap) {
|
|
51
|
+
const roleCodes = Array.from(roleValuesMap.keys());
|
|
52
|
+
|
|
53
|
+
if (roleCodes.length === 0) {
|
|
54
|
+
Logger.info(`✅ 没有需要缓存的角色 ${kind}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const oldVersion = await this.getRoleCacheActiveVersion(kind);
|
|
59
|
+
let nextVersion = String(Date.now());
|
|
60
|
+
|
|
61
|
+
if (nextVersion === oldVersion) {
|
|
62
|
+
nextVersion = `${nextVersion}_1`;
|
|
63
|
+
}
|
|
64
|
+
const nextCodesKey = this.getRoleCacheCodesKey(kind, nextVersion);
|
|
65
|
+
const nextItems = [];
|
|
66
|
+
|
|
67
|
+
for (const roleCode of roleCodes) {
|
|
68
|
+
const values = roleValuesMap.get(roleCode) || [];
|
|
69
|
+
const members = Array.from(new Set(values)).toSorted();
|
|
70
|
+
|
|
71
|
+
if (members.length === 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
nextItems.push({
|
|
76
|
+
key: this.getRoleCacheItemKey(kind, roleCode, nextVersion),
|
|
77
|
+
members: members
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await this.redis.del(nextCodesKey);
|
|
82
|
+
await this.redis.sadd(nextCodesKey, roleCodes);
|
|
83
|
+
|
|
84
|
+
if (nextItems.length > 0) {
|
|
85
|
+
await this.redis.saddBatch(nextItems);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await this.redis.setString(this.getRoleCacheVersionKey(kind), nextVersion);
|
|
89
|
+
|
|
90
|
+
if (!oldVersion || oldVersion === nextVersion) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const oldCodesKey = this.getRoleCacheCodesKey(kind, oldVersion);
|
|
95
|
+
const oldRoleCodes = await this.redis.smembers(oldCodesKey);
|
|
96
|
+
const cleanupKeys = [oldCodesKey];
|
|
97
|
+
|
|
98
|
+
for (const roleCode of oldRoleCodes) {
|
|
99
|
+
cleanupKeys.push(this.getRoleCacheItemKey(kind, roleCode, oldVersion));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cleanupKeys.length > 0) {
|
|
103
|
+
await this.redis.delBatch(cleanupKeys);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
18
107
|
/**
|
|
19
108
|
* 缓存所有接口到 Redis
|
|
20
109
|
*/
|
|
@@ -95,33 +184,7 @@ export class CacheHelper {
|
|
|
95
184
|
roleApiPathsMap.set(role.code, role.apis);
|
|
96
185
|
}
|
|
97
186
|
|
|
98
|
-
|
|
99
|
-
if (roleCodes.length === 0) {
|
|
100
|
-
Logger.info("✅ 没有需要缓存的角色权限");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 清理所有角色的缓存 key(保证幂等)
|
|
105
|
-
const roleKeys = roleCodes.map((code) => `role:apis:${code}`);
|
|
106
|
-
await this.redis.delBatch(roleKeys);
|
|
107
|
-
|
|
108
|
-
// 批量写入新缓存(只写入非空权限)
|
|
109
|
-
const items = [];
|
|
110
|
-
|
|
111
|
-
for (const roleCode of roleCodes) {
|
|
112
|
-
const apiPaths = roleApiPathsMap.get(roleCode) || [];
|
|
113
|
-
const members = Array.from(new Set(apiPaths)).toSorted();
|
|
114
|
-
|
|
115
|
-
if (members.length > 0) {
|
|
116
|
-
items.push({ key: `role:apis:${roleCode}`, members: members });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (items.length > 0) {
|
|
121
|
-
await this.redis.saddBatch(items);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 极简方案不做版本/ready/meta:重建完成即生效
|
|
187
|
+
await this.rebuildRoleCache("apis", roleApiPathsMap);
|
|
125
188
|
} catch (error) {
|
|
126
189
|
Logger.error("⚠️ 角色权限缓存异常(将阻断启动)", error);
|
|
127
190
|
throw new Error("⚠️ 角色权限缓存异常(将阻断启动)", {
|
|
@@ -153,31 +216,7 @@ export class CacheHelper {
|
|
|
153
216
|
roleMenuPathsMap.set(role.code, role.menus);
|
|
154
217
|
}
|
|
155
218
|
|
|
156
|
-
|
|
157
|
-
if (roleCodes.length === 0) {
|
|
158
|
-
Logger.info("✅ 没有需要缓存的角色菜单权限");
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 清理所有角色的缓存 key(保证幂等)
|
|
163
|
-
const roleKeys = roleCodes.map((code) => `role:menus:${code}`);
|
|
164
|
-
await this.redis.delBatch(roleKeys);
|
|
165
|
-
|
|
166
|
-
// 批量写入新缓存(只写入非空权限)
|
|
167
|
-
const items = [];
|
|
168
|
-
|
|
169
|
-
for (const roleCode of roleCodes) {
|
|
170
|
-
const menuPaths = roleMenuPathsMap.get(roleCode) || [];
|
|
171
|
-
const members = Array.from(new Set(menuPaths)).toSorted();
|
|
172
|
-
|
|
173
|
-
if (members.length > 0) {
|
|
174
|
-
items.push({ key: `role:menus:${roleCode}`, members: members });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (items.length > 0) {
|
|
179
|
-
await this.redis.saddBatch(items);
|
|
180
|
-
}
|
|
219
|
+
await this.rebuildRoleCache("menus", roleMenuPathsMap);
|
|
181
220
|
} catch (error) {
|
|
182
221
|
Logger.error("⚠️ 角色菜单权限缓存异常(将阻断启动)", error);
|
|
183
222
|
throw new Error("⚠️ 角色菜单权限缓存异常(将阻断启动)", {
|
|
@@ -212,7 +251,7 @@ export class CacheHelper {
|
|
|
212
251
|
});
|
|
213
252
|
}
|
|
214
253
|
|
|
215
|
-
const roleKey =
|
|
254
|
+
const roleKey = await this.getRoleCacheReadKey("apis", roleCode);
|
|
216
255
|
|
|
217
256
|
// 空数组短路:保证清理残留
|
|
218
257
|
if (apiPaths.length === 0) {
|
|
@@ -251,7 +290,7 @@ export class CacheHelper {
|
|
|
251
290
|
});
|
|
252
291
|
}
|
|
253
292
|
|
|
254
|
-
const roleKey =
|
|
293
|
+
const roleKey = await this.getRoleCacheReadKey("menus", roleCode);
|
|
255
294
|
|
|
256
295
|
// 空数组短路:保证清理残留
|
|
257
296
|
if (menuPaths.length === 0) {
|
|
@@ -329,7 +368,8 @@ export class CacheHelper {
|
|
|
329
368
|
*/
|
|
330
369
|
async getRoleApis(roleCode) {
|
|
331
370
|
try {
|
|
332
|
-
const
|
|
371
|
+
const roleKey = await this.getRoleCacheReadKey("apis", roleCode);
|
|
372
|
+
const permissions = await this.redis.smembers(roleKey);
|
|
333
373
|
return permissions || [];
|
|
334
374
|
} catch (error) {
|
|
335
375
|
Logger.error("获取角色权限缓存失败", error, { roleCode: roleCode });
|
|
@@ -350,7 +390,8 @@ export class CacheHelper {
|
|
|
350
390
|
*/
|
|
351
391
|
async getRoleMenus(roleCode) {
|
|
352
392
|
try {
|
|
353
|
-
const
|
|
393
|
+
const roleKey = await this.getRoleCacheReadKey("menus", roleCode);
|
|
394
|
+
const permissions = await this.redis.smembers(roleKey);
|
|
354
395
|
return permissions || [];
|
|
355
396
|
} catch (error) {
|
|
356
397
|
Logger.error("获取角色菜单权限缓存失败", error, { roleCode: roleCode });
|
|
@@ -374,7 +415,8 @@ export class CacheHelper {
|
|
|
374
415
|
try {
|
|
375
416
|
if (isNullable(apiPath)) return false;
|
|
376
417
|
const value = isString(apiPath) ? apiPath : String(apiPath);
|
|
377
|
-
|
|
418
|
+
const roleKey = await this.getRoleCacheReadKey("apis", roleCode);
|
|
419
|
+
return await this.redis.sismember(roleKey, value);
|
|
378
420
|
} catch (error) {
|
|
379
421
|
Logger.error("检查角色权限失败", error, { roleCode: roleCode });
|
|
380
422
|
throw new Error("检查角色权限失败", {
|
|
@@ -397,7 +439,8 @@ export class CacheHelper {
|
|
|
397
439
|
try {
|
|
398
440
|
if (isNullable(menuPath)) return false;
|
|
399
441
|
const value = isString(menuPath) ? menuPath : String(menuPath);
|
|
400
|
-
|
|
442
|
+
const roleKey = await this.getRoleCacheReadKey("menus", roleCode);
|
|
443
|
+
return await this.redis.sismember(roleKey, value);
|
|
401
444
|
} catch (error) {
|
|
402
445
|
Logger.error("检查角色菜单权限失败", error, { roleCode: roleCode });
|
|
403
446
|
throw new Error("检查角色菜单权限失败", {
|
|
@@ -417,7 +460,8 @@ export class CacheHelper {
|
|
|
417
460
|
*/
|
|
418
461
|
async deleteRoleApis(roleCode) {
|
|
419
462
|
try {
|
|
420
|
-
const
|
|
463
|
+
const roleReadKey = await this.getRoleCacheReadKey("apis", roleCode);
|
|
464
|
+
const result = await this.redis.delBatch([`role:apis:${roleCode}`, roleReadKey]);
|
|
421
465
|
if (result > 0) {
|
|
422
466
|
Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
|
|
423
467
|
return true;
|
|
@@ -442,7 +486,8 @@ export class CacheHelper {
|
|
|
442
486
|
*/
|
|
443
487
|
async deleteRoleMenus(roleCode) {
|
|
444
488
|
try {
|
|
445
|
-
const
|
|
489
|
+
const roleReadKey = await this.getRoleCacheReadKey("menus", roleCode);
|
|
490
|
+
const result = await this.redis.delBatch([`role:menus:${roleCode}`, roleReadKey]);
|
|
446
491
|
if (result > 0) {
|
|
447
492
|
Logger.info(`✅ 已删除角色 ${roleCode} 的菜单权限缓存`);
|
|
448
493
|
return true;
|
package/lib/redisHelper.js
CHANGED
|
@@ -380,6 +380,74 @@ export class RedisHelper {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* 向有序集合添加成员
|
|
385
|
+
* @param key - 键名
|
|
386
|
+
* @param items - [{ score, member }] 数组
|
|
387
|
+
* @returns 成功添加的成员数量
|
|
388
|
+
*/
|
|
389
|
+
async zadd(key, items) {
|
|
390
|
+
try {
|
|
391
|
+
if (items.length === 0) {
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const pkey = `${this.prefix}${key}`;
|
|
396
|
+
const args = [pkey];
|
|
397
|
+
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
const score = Number(item.score);
|
|
400
|
+
const member = String(item.member);
|
|
401
|
+
|
|
402
|
+
if (!Number.isFinite(score) || member.length === 0) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
args.push(score);
|
|
407
|
+
args.push(member);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (args.length <= 1) {
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return await this.client.zadd.apply(this.client, args);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
Logger.error("Redis zadd 错误", error);
|
|
417
|
+
return 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 获取有序集合成员数量
|
|
423
|
+
* @param key - 键名
|
|
424
|
+
*/
|
|
425
|
+
async zcard(key) {
|
|
426
|
+
try {
|
|
427
|
+
const pkey = `${this.prefix}${key}`;
|
|
428
|
+
return await this.client.zcard(pkey);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
Logger.error("Redis zcard 错误", error);
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 按 score 区间删除有序集合成员
|
|
437
|
+
* @param key - 键名
|
|
438
|
+
* @param min - 最小分值
|
|
439
|
+
* @param max - 最大分值
|
|
440
|
+
*/
|
|
441
|
+
async zremrangebyscore(key, min, max) {
|
|
442
|
+
try {
|
|
443
|
+
const pkey = `${this.prefix}${key}`;
|
|
444
|
+
return await this.client.zremrangebyscore(pkey, min, max);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
Logger.error("Redis zremrangebyscore 错误", error);
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
383
451
|
/**
|
|
384
452
|
* 删除键
|
|
385
453
|
* @param key - 键名
|