befly 3.8.2 → 3.8.3

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/check.ts CHANGED
@@ -3,10 +3,13 @@
3
3
  * 验证表定义文件的格式和规则
4
4
  */
5
5
 
6
- import { basename } from 'pathe';
6
+ import { basename, relative } from 'pathe';
7
+ import { join } from 'node:path';
8
+ import { existsSync } from 'node:fs';
9
+ import { isPlainObject } from 'es-toolkit/compat';
7
10
  import { Logger } from './lib/logger.js';
8
- import { projectTableDir } from './paths.js';
9
- import { Addon } from './lib/addon.js';
11
+ import { projectTableDir, projectApiDir } from './paths.js';
12
+ import { scanAddons, getAddonDir, addonDirExists } from './util.js';
10
13
  import type { FieldDefinition } from './types/common.d.ts';
11
14
 
12
15
  /**
@@ -54,16 +57,10 @@ const MAX_VARCHAR_LENGTH = 65535;
54
57
  * 检查表定义文件
55
58
  * @throws 当检查失败时抛出异常
56
59
  */
57
- export const checkDefault = async function (): Promise<void> {
60
+ export const checkTable = async function (): Promise<boolean> {
58
61
  try {
59
62
  const tablesGlob = new Bun.Glob('*.json');
60
63
 
61
- // 统计信息
62
- let totalFiles = 0;
63
- let totalRules = 0;
64
- let validFiles = 0;
65
- let invalidFiles = 0;
66
-
67
64
  // 收集所有表文件
68
65
  const allTableFiles: TableFileInfo[] = [];
69
66
 
@@ -73,60 +70,58 @@ export const checkDefault = async function (): Promise<void> {
73
70
  absolute: true,
74
71
  onlyFiles: true
75
72
  })) {
76
- allTableFiles.push({ file: file, type: 'project' });
73
+ allTableFiles.push({
74
+ file: file,
75
+ typeCode: 'project',
76
+ typeName: '项目'
77
+ });
77
78
  }
78
79
 
79
80
  // 收集 addon 表字段定义文件
80
- const addons = Addon.scan();
81
+ const addons = scanAddons();
81
82
  for (const addonName of addons) {
82
- const addonTablesDir = Addon.getDir(addonName, 'tables');
83
+ const addonTablesDir = getAddonDir(addonName, 'tables');
83
84
 
84
85
  for await (const file of tablesGlob.scan({
85
86
  cwd: addonTablesDir,
86
87
  absolute: true,
87
88
  onlyFiles: true
88
89
  })) {
89
- allTableFiles.push({ file: file, type: 'addon', addonName: addonName });
90
+ allTableFiles.push({
91
+ file: file,
92
+ typeCode: 'addon',
93
+ typeName: `组件${addonName}`,
94
+ addonName: addonName
95
+ });
90
96
  }
91
97
  }
92
98
 
93
99
  // 合并进行验证逻辑
94
- for (const { file, type, addonName } of allTableFiles) {
95
- totalFiles++;
96
- const fileName = basename(file);
97
- const fileBaseName = basename(file, '.json');
98
- const fileType = type === 'project' ? '项目' : `组件${addonName}`;
100
+ for (const item of allTableFiles) {
101
+ const fileName = basename(item.file);
102
+ const fileBaseName = basename(item.file, '.json');
99
103
 
100
104
  try {
101
105
  // 1) 文件名小驼峰校验
102
106
  if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
103
- Logger.warn(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
104
- // 命名不合规,记录错误并计为无效文件,继续下一个文件
105
- invalidFiles++;
107
+ Logger.warn(`${item.typeName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
106
108
  continue;
107
109
  }
108
110
 
109
- // 读取并解析 JSON 文件
110
- const table = await Bun.file(file).json();
111
- let fileValid = true;
112
- let fileRules = 0;
111
+ // 动态导入 JSON 文件
112
+ const tableModule = await import(item.file, { with: { type: 'json' } });
113
+ const table = tableModule.default;
113
114
 
114
115
  // 检查 table 中的每个验证规则
115
116
  for (const [colKey, fieldDef] of Object.entries(table)) {
116
117
  if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
117
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
118
- fileValid = false;
118
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
119
119
  continue;
120
120
  }
121
121
 
122
- // 验证规则格式
123
- fileRules++;
124
- totalRules++;
125
-
126
122
  // 检查是否使用了保留字段
127
123
  if (RESERVED_FIELDS.includes(colKey as any)) {
128
- Logger.warn(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
129
- fileValid = false;
124
+ Logger.warn(`${item.typeName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
130
125
  }
131
126
 
132
127
  // 直接使用字段对象
@@ -134,81 +129,66 @@ export const checkDefault = async function (): Promise<void> {
134
129
 
135
130
  // 检查必填字段:name, type, min, max
136
131
  if (!field.name || typeof field.name !== 'string') {
137
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
138
- fileValid = false;
132
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
139
133
  continue;
140
134
  }
141
135
  if (!field.type || typeof field.type !== 'string') {
142
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
143
- fileValid = false;
136
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
144
137
  continue;
145
138
  }
146
139
  if (field.min === undefined) {
147
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 缺少必填字段 min`);
148
- fileValid = false;
140
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 min`);
149
141
  continue;
150
142
  }
151
143
  if (field.max === undefined) {
152
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 缺少必填字段 max`);
153
- fileValid = false;
144
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 max`);
154
145
  continue;
155
146
  }
156
147
 
157
148
  // 检查可选字段的类型
158
149
  if (field.detail !== undefined && typeof field.detail !== 'string') {
159
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
160
- fileValid = false;
150
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
161
151
  }
162
152
  if (field.index !== undefined && typeof field.index !== 'boolean') {
163
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
164
- fileValid = false;
153
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
165
154
  }
166
155
  if (field.unique !== undefined && typeof field.unique !== 'boolean') {
167
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
168
- fileValid = false;
156
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
169
157
  }
170
158
  if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
171
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
172
- fileValid = false;
159
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
173
160
  }
174
161
  if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
175
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
176
- fileValid = false;
162
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
177
163
  }
178
164
  if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
179
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
180
- fileValid = false;
165
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
181
166
  }
182
167
 
183
168
  const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regexp: fieldRegexp } = field;
184
169
 
185
170
  // 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
186
171
  if (!FIELD_NAME_REGEX.test(fieldName)) {
187
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
188
- fileValid = false;
172
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
189
173
  }
190
174
 
191
175
  // 第2个值:字段类型必须为string,number,text,array_string,array_text之一
192
176
  if (!FIELD_TYPES.includes(fieldType as any)) {
193
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
194
- fileValid = false;
177
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
195
178
  }
196
179
 
197
180
  // 第3/4个值:需要是 null 或 数字
198
181
  if (!(fieldMin === null || typeof fieldMin === 'number')) {
199
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
200
- fileValid = false;
182
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
201
183
  }
202
184
  if (!(fieldMax === null || typeof fieldMax === 'number')) {
203
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
204
- fileValid = false;
185
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
205
186
  }
206
187
 
207
188
  // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
208
189
  if (fieldMin !== null && fieldMax !== null) {
209
190
  if (fieldMin > fieldMax) {
210
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
211
- fileValid = false;
191
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
212
192
  }
213
193
  }
214
194
 
@@ -216,57 +196,150 @@ export const checkDefault = async function (): Promise<void> {
216
196
  if (fieldType === 'text') {
217
197
  // text:min/max 必须为 null,默认值必须为 null
218
198
  if (fieldMin !== null) {
219
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
220
- fileValid = false;
199
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
221
200
  }
222
201
  if (fieldMax !== null) {
223
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
224
- fileValid = false;
202
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
225
203
  }
226
204
  if (fieldDefault !== null) {
227
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
228
- fileValid = false;
205
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
229
206
  }
230
207
  } else if (fieldType === 'string' || fieldType === 'array') {
231
208
  if (fieldMax === null || typeof fieldMax !== 'number') {
232
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
233
- fileValid = false;
209
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
234
210
  } else if (fieldMax > MAX_VARCHAR_LENGTH) {
235
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
236
- fileValid = false;
211
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
237
212
  }
238
213
  } else if (fieldType === 'number') {
239
214
  // number 类型:default 如果存在,必须为 null 或 number
240
215
  if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
241
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
242
- fileValid = false;
216
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
243
217
  }
244
218
  }
245
219
  }
220
+ } catch (error: any) {
221
+ Logger.error(`${item.typeName}表 ${fileName} 解析失败`, error);
222
+ }
223
+ }
224
+
225
+ return true;
226
+ } catch (error: any) {
227
+ Logger.error('数据表定义检查过程中出错', error);
228
+ return false;
229
+ }
230
+ };
231
+
232
+ /**
233
+ * 检查所有 API 定义
234
+ */
235
+ export const checkApi = async function (): Promise<boolean> {
236
+ try {
237
+ const apiGlob = new Bun.Glob('**/*.ts');
238
+
239
+ // 收集所有 API 文件
240
+ const allApiFiles: Array<{ file: string; displayName: string }> = [];
241
+
242
+ // 收集项目 API 文件
243
+ if (existsSync(projectApiDir)) {
244
+ for await (const file of apiGlob.scan({
245
+ cwd: projectApiDir,
246
+ onlyFiles: true,
247
+ absolute: true
248
+ })) {
249
+ allApiFiles.push({
250
+ file: file,
251
+ displayName: '用户'
252
+ });
253
+ }
254
+ }
255
+
256
+ // 收集组件 API 文件
257
+ const addons = scanAddons();
258
+ for (const addon of addons) {
259
+ if (!addonDirExists(addon, 'apis')) continue;
260
+ const addonApiDir = getAddonDir(addon, 'apis');
261
+
262
+ for await (const file of apiGlob.scan({
263
+ cwd: addonApiDir,
264
+ onlyFiles: true,
265
+ absolute: true
266
+ })) {
267
+ allApiFiles.push({
268
+ file: file,
269
+ displayName: `组件${addon}`
270
+ });
271
+ }
272
+ }
246
273
 
247
- if (fileValid) {
248
- validFiles++;
249
- // Logger.info(`${fileType}表 ${fileName} 验证通过(${fileRules} 个字段)`);
250
- } else {
251
- invalidFiles++;
274
+ // 合并进行验证逻辑
275
+ for (const item of allApiFiles) {
276
+ const fileName = basename(item.file).replace(/\.ts$/, '');
277
+ const apiPath = relative(item.displayName === '用户' ? projectApiDir : getAddonDir(item.displayName.replace('组件', ''), 'apis'), item.file).replace(/\.ts$/, '');
278
+
279
+ // 跳过以下划线开头的文件
280
+ if (apiPath.indexOf('_') !== -1) continue;
281
+
282
+ try {
283
+ // Windows 下路径需要转换为正斜杠格式
284
+ const filePath = item.file.replace(/\\/g, '/');
285
+ const apiImport = await import(filePath);
286
+ const api = apiImport.default;
287
+
288
+ // 验证必填属性:name 和 handler
289
+ if (typeof api.name !== 'string' || api.name.trim() === '') {
290
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
291
+ continue;
292
+ }
293
+ if (typeof api.handler !== 'function') {
294
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
295
+ continue;
296
+ }
297
+
298
+ // 验证可选属性的类型(如果提供了)
299
+ if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
300
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
301
+ }
302
+ if (api.auth !== undefined && typeof api.auth !== 'boolean') {
303
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
304
+ }
305
+ if (api.fields && !isPlainObject(api.fields)) {
306
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
307
+ }
308
+ if (api.required && !Array.isArray(api.required)) {
309
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
310
+ }
311
+ if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
312
+ Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
252
313
  }
253
314
  } catch (error: any) {
254
- Logger.error(`${fileType} ${fileName} 解析失败`, error);
255
- invalidFiles++;
315
+ Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
256
316
  }
257
317
  }
258
318
 
259
- // 输出统计信息
260
- // Logger.info(` 总文件数: ${totalFiles}`);
261
- // Logger.info(` 总规则数: ${totalRules}`);
262
- // Logger.info(` 通过文件: ${validFiles}`);
263
- // Logger.info(` 失败文件: ${invalidFiles}`);
319
+ return true;
320
+ } catch (error: any) {
321
+ Logger.error('API 定义检查过程中出错', error);
322
+ return false;
323
+ }
324
+ };
264
325
 
265
- if (invalidFiles > 0) {
266
- throw new Error('表定义检查失败,请修复上述错误后重试');
326
+ /**
327
+ * 检查项目结构
328
+ */
329
+ export const checkApp = async function (): Promise<boolean> {
330
+ try {
331
+ // 检查项目 apis 目录下是否存在名为 addon 的目录
332
+ if (existsSync(projectApiDir)) {
333
+ const addonDir = join(projectApiDir, 'addon');
334
+ if (existsSync(addonDir)) {
335
+ Logger.error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
336
+ return false;
337
+ }
267
338
  }
339
+
340
+ return true;
268
341
  } catch (error: any) {
269
- Logger.error('数据表定义检查过程中出错', error);
270
- throw error;
342
+ Logger.error('项目结构检查过程中出错', error);
343
+ return false;
271
344
  }
272
345
  };
package/env.ts CHANGED
@@ -18,14 +18,14 @@ const coreEnv: EnvConfig = {
18
18
  NODE_ENV: process.env.NODE_ENV || 'development',
19
19
  APP_NAME: isProd ? '野蜂飞舞正式环境' : '野蜂飞舞开发环境',
20
20
  APP_PORT: 3000,
21
- APP_HOST: isProd ? '127.0.0.1' : '0.0.0.0',
21
+ APP_HOST: '127.0.0.1',
22
22
  DEV_EMAIL: 'dev@qq.com',
23
23
  DEV_PASSWORD: '123456',
24
24
  BODY_LIMIT: 10485760, // 10MB
25
25
  PARAMS_CHECK: false,
26
26
 
27
27
  // ========== 日志配置 ==========
28
- LOG_DEBUG: isProd ? 0 : 1,
28
+ LOG_DEBUG: 0,
29
29
  LOG_EXCLUDE_FIELDS: 'password,token,secret',
30
30
  LOG_DIR: './logs',
31
31
  LOG_TO_CONSOLE: 1,
@@ -55,7 +55,7 @@ const coreEnv: EnvConfig = {
55
55
  REDIS_KEY_PREFIX: 'befly',
56
56
 
57
57
  // ========== JWT 配置 ==========
58
- JWT_SECRET: isProd ? 'befly-dev-secret' : 'befly-prod-secret',
58
+ JWT_SECRET: 'befly-secret',
59
59
  JWT_EXPIRES_IN: '7d',
60
60
  JWT_ALGORITHM: 'HS256',
61
61
 
@@ -166,4 +166,85 @@ export class CacheHelper {
166
166
 
167
167
  Logger.info('========== 数据缓存完成 ==========\n');
168
168
  }
169
+
170
+ /**
171
+ * 获取缓存的所有接口
172
+ * @returns 接口列表
173
+ */
174
+ async getApis(): Promise<any[]> {
175
+ try {
176
+ const apis = await this.befly.redis.getObject<any[]>('apis:all');
177
+ return apis || [];
178
+ } catch (error: any) {
179
+ Logger.error('获取接口缓存失败:', error);
180
+ return [];
181
+ }
182
+ }
183
+
184
+ /**
185
+ * 获取缓存的所有菜单
186
+ * @returns 菜单列表
187
+ */
188
+ async getMenus(): Promise<any[]> {
189
+ try {
190
+ const menus = await this.befly.redis.getObject<any[]>('menus:all');
191
+ return menus || [];
192
+ } catch (error: any) {
193
+ Logger.error('获取菜单缓存失败:', error);
194
+ return [];
195
+ }
196
+ }
197
+
198
+ /**
199
+ * 获取角色的接口权限
200
+ * @param roleCode - 角色代码
201
+ * @returns 接口路径列表
202
+ */
203
+ async getRolePermissions(roleCode: string): Promise<string[]> {
204
+ try {
205
+ const redisKey = `role:apis:${roleCode}`;
206
+ const permissions = await this.befly.redis.smembers(redisKey);
207
+ return permissions || [];
208
+ } catch (error: any) {
209
+ Logger.error(`获取角色 ${roleCode} 权限缓存失败:`, error);
210
+ return [];
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 检查角色是否有指定接口权限
216
+ * @param roleCode - 角色代码
217
+ * @param apiPath - 接口路径(格式:METHOD/path)
218
+ * @returns 是否有权限
219
+ */
220
+ async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
221
+ try {
222
+ const redisKey = `role:apis:${roleCode}`;
223
+ const result = await this.befly.redis.sismember(redisKey, apiPath);
224
+ return result === 1;
225
+ } catch (error: any) {
226
+ Logger.error(`检查角色 ${roleCode} 权限失败:`, error);
227
+ return false;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 删除角色的接口权限缓存
233
+ * @param roleCode - 角色代码
234
+ * @returns 是否删除成功
235
+ */
236
+ async deleteRolePermissions(roleCode: string): Promise<boolean> {
237
+ try {
238
+ const redisKey = `role:apis:${roleCode}`;
239
+ const result = await this.befly.redis.del(redisKey);
240
+ if (result > 0) {
241
+ Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
242
+ return true;
243
+ }
244
+ return false;
245
+ } catch (error: any) {
246
+ Logger.error(`删除角色 ${roleCode} 权限缓存失败:`, error);
247
+ return false;
248
+ }
249
+ }
169
250
  }