befly 3.8.1 → 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,11 +3,14 @@
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 { parseRule } from './util.js';
9
- import { projectTableDir } from './paths.js';
10
- import { Addon } from './lib/addon.js';
11
+ import { projectTableDir, projectApiDir } from './paths.js';
12
+ import { scanAddons, getAddonDir, addonDirExists } from './util.js';
13
+ import type { FieldDefinition } from './types/common.d.ts';
11
14
 
12
15
  /**
13
16
  * 表文件信息接口
@@ -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,167 +70,276 @@ 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
- for (const [colKey, rule] of Object.entries(table)) {
116
- if (typeof rule !== 'string') {
117
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
118
- fileValid = false;
116
+ for (const [colKey, fieldDef] of Object.entries(table)) {
117
+ if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
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
- // 使用 parseRule 解析字段规则
133
- let parsed;
134
- try {
135
- parsed = parseRule(rule);
136
- } catch (error: any) {
137
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
138
- fileValid = false;
127
+ // 直接使用字段对象
128
+ const field = fieldDef as FieldDefinition;
129
+
130
+ // 检查必填字段:name, type, min, max
131
+ if (!field.name || typeof field.name !== 'string') {
132
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
133
+ continue;
134
+ }
135
+ if (!field.type || typeof field.type !== 'string') {
136
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
137
+ continue;
138
+ }
139
+ if (field.min === undefined) {
140
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 min`);
141
+ continue;
142
+ }
143
+ if (field.max === undefined) {
144
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 max`);
139
145
  continue;
140
146
  }
141
147
 
142
- const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regex: fieldRegx } = parsed;
148
+ // 检查可选字段的类型
149
+ if (field.detail !== undefined && typeof field.detail !== 'string') {
150
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
151
+ }
152
+ if (field.index !== undefined && typeof field.index !== 'boolean') {
153
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
154
+ }
155
+ if (field.unique !== undefined && typeof field.unique !== 'boolean') {
156
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
157
+ }
158
+ if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
159
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
160
+ }
161
+ if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
162
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
163
+ }
164
+ if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
165
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
166
+ }
167
+
168
+ const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regexp: fieldRegexp } = field;
143
169
 
144
170
  // 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
145
171
  if (!FIELD_NAME_REGEX.test(fieldName)) {
146
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
147
- fileValid = false;
172
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
148
173
  }
149
174
 
150
175
  // 第2个值:字段类型必须为string,number,text,array_string,array_text之一
151
176
  if (!FIELD_TYPES.includes(fieldType as any)) {
152
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
153
- fileValid = false;
177
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
154
178
  }
155
179
 
156
180
  // 第3/4个值:需要是 null 或 数字
157
181
  if (!(fieldMin === null || typeof fieldMin === 'number')) {
158
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
159
- fileValid = false;
182
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
160
183
  }
161
184
  if (!(fieldMax === null || typeof fieldMax === 'number')) {
162
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
163
- fileValid = false;
185
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
164
186
  }
165
187
 
166
188
  // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
167
189
  if (fieldMin !== null && fieldMax !== null) {
168
190
  if (fieldMin > fieldMax) {
169
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
170
- fileValid = false;
191
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
171
192
  }
172
193
  }
173
194
 
174
- // 第6个值:是否创建索引必须为0或1
175
- if (fieldIndex !== 0 && fieldIndex !== 1) {
176
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
177
- fileValid = false;
178
- }
179
-
180
- // 第7个值:必须为null或正则表达式(parseRule已经验证过了)
181
- // parseRule 已经将正则字符串转换为 RegExp 或 null,这里不需要再验证
182
-
183
195
  // 第4个值与类型联动校验 + 默认值规则
184
196
  if (fieldType === 'text') {
185
- // text:min/max 必须为 null,默认值必须为 'null'
197
+ // text:min/max 必须为 null,默认值必须为 null
186
198
  if (fieldMin !== null) {
187
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
188
- fileValid = false;
199
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
189
200
  }
190
201
  if (fieldMax !== null) {
191
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
192
- fileValid = false;
202
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
193
203
  }
194
- if (fieldDefault !== 'null') {
195
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
196
- fileValid = false;
204
+ if (fieldDefault !== null) {
205
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
197
206
  }
198
207
  } else if (fieldType === 'string' || fieldType === 'array') {
199
208
  if (fieldMax === null || typeof fieldMax !== 'number') {
200
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
201
- fileValid = false;
209
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
202
210
  } else if (fieldMax > MAX_VARCHAR_LENGTH) {
203
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
204
- fileValid = false;
211
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
205
212
  }
206
213
  } else if (fieldType === 'number') {
207
- if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
208
- Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
209
- fileValid = false;
214
+ // number 类型:default 如果存在,必须为 null number
215
+ if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
216
+ Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
210
217
  }
211
218
  }
212
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
+ }
273
+
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
+ }
213
297
 
214
- if (fileValid) {
215
- validFiles++;
216
- // Logger.info(`${fileType} ${fileName} 验证通过(${fileRules} 个字段)`);
217
- } else {
218
- invalidFiles++;
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 属性必须是字符串数组`);
219
313
  }
220
314
  } catch (error: any) {
221
- Logger.error(`${fileType} ${fileName} 解析失败`, error);
222
- invalidFiles++;
315
+ Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
223
316
  }
224
317
  }
225
318
 
226
- // 输出统计信息
227
- // Logger.info(` 总文件数: ${totalFiles}`);
228
- // Logger.info(` 总规则数: ${totalRules}`);
229
- // Logger.info(` 通过文件: ${validFiles}`);
230
- // Logger.info(` 失败文件: ${invalidFiles}`);
319
+ return true;
320
+ } catch (error: any) {
321
+ Logger.error('API 定义检查过程中出错', error);
322
+ return false;
323
+ }
324
+ };
231
325
 
232
- if (invalidFiles > 0) {
233
- 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
+ }
234
338
  }
339
+
340
+ return true;
235
341
  } catch (error: any) {
236
- Logger.error('数据表定义检查过程中出错', error);
237
- throw error;
342
+ Logger.error('项目结构检查过程中出错', error);
343
+ return false;
238
344
  }
239
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
 
@@ -1,21 +1,23 @@
1
- /**
2
- * 缓存插件 - TypeScript 版本
1
+ /**
2
+ * 缓存助手 - TypeScript 版本
3
3
  * 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
4
4
  */
5
5
 
6
- import { Logger } from '../lib/logger.js';
7
- import type { Plugin } from '../types/plugin.js';
6
+ import { Logger } from './logger.js';
8
7
  import type { BeflyContext } from '../types/befly.js';
9
- import type { ApiRoute } from '../types/api.js';
10
8
 
11
9
  /**
12
- * 缓存管理器类
10
+ * 缓存助手类
13
11
  */
14
- class CacheManager {
15
- private appContext: BeflyContext;
12
+ export class CacheHelper {
13
+ private befly: BeflyContext;
16
14
 
17
- constructor(appContext: BeflyContext) {
18
- this.appContext = appContext;
15
+ /**
16
+ * 构造函数
17
+ * @param befly - Befly 上下文
18
+ */
19
+ constructor(befly: BeflyContext) {
20
+ this.befly = befly;
19
21
  }
20
22
 
21
23
  /**
@@ -24,21 +26,21 @@ class CacheManager {
24
26
  async cacheApis(): Promise<void> {
25
27
  try {
26
28
  // 检查表是否存在
27
- const tableExists = await this.appContext.db.tableExists('core_api');
29
+ const tableExists = await this.befly.db.tableExists('core_api');
28
30
  if (!tableExists) {
29
31
  Logger.warn('⚠️ 接口表不存在,跳过接口缓存');
30
32
  return;
31
33
  }
32
34
 
33
35
  // 从数据库查询所有接口(与 apiAll.ts 保持一致)
34
- const apiList = await this.appContext.db.getAll({
36
+ const apiList = await this.befly.db.getAll({
35
37
  table: 'core_api',
36
38
  fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
37
39
  orderBy: ['addonName#ASC', 'path#ASC']
38
40
  });
39
41
 
40
42
  // 缓存到 Redis
41
- const result = await this.appContext.redis.setObject('apis:all', apiList);
43
+ const result = await this.befly.redis.setObject('apis:all', apiList);
42
44
 
43
45
  if (result === null) {
44
46
  Logger.warn('⚠️ 接口缓存失败');
@@ -56,21 +58,21 @@ class CacheManager {
56
58
  async cacheMenus(): Promise<void> {
57
59
  try {
58
60
  // 检查表是否存在
59
- const tableExists = await this.appContext.db.tableExists('core_menu');
61
+ const tableExists = await this.befly.db.tableExists('core_menu');
60
62
  if (!tableExists) {
61
63
  Logger.warn('⚠️ 菜单表不存在,跳过菜单缓存');
62
64
  return;
63
65
  }
64
66
 
65
67
  // 从数据库查询所有菜单
66
- const menus = await this.appContext.db.getAll({
68
+ const menus = await this.befly.db.getAll({
67
69
  table: 'core_menu',
68
70
  fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
69
71
  orderBy: ['sort#ASC', 'id#ASC']
70
72
  });
71
73
 
72
74
  // 缓存到 Redis
73
- const result = await this.appContext.redis.setObject('menus:all', menus);
75
+ const result = await this.befly.redis.setObject('menus:all', menus);
74
76
 
75
77
  if (result === null) {
76
78
  Logger.warn('⚠️ 菜单缓存失败');
@@ -89,8 +91,8 @@ class CacheManager {
89
91
  async cacheRolePermissions(): Promise<void> {
90
92
  try {
91
93
  // 检查表是否存在
92
- const apiTableExists = await this.appContext.db.tableExists('core_api');
93
- const roleTableExists = await this.appContext.db.tableExists('core_role');
94
+ const apiTableExists = await this.befly.db.tableExists('core_api');
95
+ const roleTableExists = await this.befly.db.tableExists('core_role');
94
96
 
95
97
  if (!apiTableExists || !roleTableExists) {
96
98
  Logger.warn('⚠️ 接口或角色表不存在,跳过角色权限缓存');
@@ -98,13 +100,13 @@ class CacheManager {
98
100
  }
99
101
 
100
102
  // 查询所有角色
101
- const roles = await this.appContext.db.getAll({
103
+ const roles = await this.befly.db.getAll({
102
104
  table: 'core_role',
103
105
  fields: ['id', 'code', 'apis']
104
106
  });
105
107
 
106
108
  // 查询所有接口(用于权限映射)
107
- const allApis = await this.appContext.db.getAll({
109
+ const allApis = await this.befly.db.getAll({
108
110
  table: 'core_api',
109
111
  fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
110
112
  });
@@ -129,10 +131,10 @@ class CacheManager {
129
131
  const redisKey = `role:apis:${role.code}`;
130
132
 
131
133
  // 先删除旧数据
132
- await this.appContext.redis.del(redisKey);
134
+ await this.befly.redis.del(redisKey);
133
135
 
134
136
  // 批量添加到 Set
135
- const result = await this.appContext.redis.sadd(redisKey, roleApiPaths);
137
+ const result = await this.befly.redis.sadd(redisKey, roleApiPaths);
136
138
 
137
139
  if (result > 0) {
138
140
  cachedRoles++;
@@ -164,23 +166,85 @@ class CacheManager {
164
166
 
165
167
  Logger.info('========== 数据缓存完成 ==========\n');
166
168
  }
167
- }
168
169
 
169
- /**
170
- * 缓存插件
171
- */
172
- const cachePlugin: Plugin = {
173
- name: '_cache',
174
- after: ['_db', '_redis'],
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
+ }
175
197
 
176
- async onInit(befly: BeflyContext): Promise<CacheManager> {
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> {
177
221
  try {
178
- const cacheManager = new CacheManager(befly);
179
- return cacheManager;
222
+ const redisKey = `role:apis:${roleCode}`;
223
+ const result = await this.befly.redis.sismember(redisKey, apiPath);
224
+ return result === 1;
180
225
  } catch (error: any) {
181
- throw error;
226
+ Logger.error(`检查角色 ${roleCode} 权限失败:`, error);
227
+ return false;
182
228
  }
183
229
  }
184
- };
185
230
 
186
- export default cachePlugin;
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
+ }
250
+ }