befly 3.8.25 → 3.8.27

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 (61) hide show
  1. package/config.ts +8 -9
  2. package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
  3. package/hooks/auth.ts +3 -11
  4. package/hooks/cors.ts +1 -4
  5. package/hooks/parser.ts +6 -8
  6. package/hooks/permission.ts +9 -12
  7. package/hooks/validator.ts +6 -9
  8. package/lib/cacheHelper.ts +0 -4
  9. package/lib/{database.ts → connect.ts} +65 -18
  10. package/lib/logger.ts +1 -17
  11. package/lib/redisHelper.ts +6 -5
  12. package/loader/loadApis.ts +3 -3
  13. package/loader/loadHooks.ts +15 -41
  14. package/loader/loadPlugins.ts +10 -16
  15. package/main.ts +25 -28
  16. package/package.json +3 -3
  17. package/plugins/cache.ts +2 -2
  18. package/plugins/cipher.ts +15 -0
  19. package/plugins/config.ts +16 -0
  20. package/plugins/db.ts +7 -17
  21. package/plugins/jwt.ts +15 -0
  22. package/plugins/logger.ts +1 -1
  23. package/plugins/redis.ts +4 -4
  24. package/plugins/tool.ts +50 -0
  25. package/router/api.ts +56 -42
  26. package/router/static.ts +12 -12
  27. package/sync/syncAll.ts +2 -20
  28. package/sync/syncApi.ts +7 -7
  29. package/sync/syncDb/apply.ts +1 -4
  30. package/sync/syncDb/constants.ts +3 -0
  31. package/sync/syncDb/ddl.ts +2 -1
  32. package/sync/syncDb/helpers.ts +5 -117
  33. package/sync/syncDb/sqlite.ts +1 -3
  34. package/sync/syncDb/table.ts +8 -142
  35. package/sync/syncDb/tableCreate.ts +25 -9
  36. package/sync/syncDb/types.ts +125 -0
  37. package/sync/syncDb/version.ts +0 -3
  38. package/sync/syncDb.ts +146 -6
  39. package/sync/syncDev.ts +19 -15
  40. package/sync/syncMenu.ts +87 -75
  41. package/tests/redisHelper.test.ts +15 -16
  42. package/tests/sync-connection.test.ts +189 -0
  43. package/tests/syncDb-apply.test.ts +287 -0
  44. package/tests/syncDb-constants.test.ts +150 -0
  45. package/tests/syncDb-ddl.test.ts +205 -0
  46. package/tests/syncDb-helpers.test.ts +112 -0
  47. package/tests/syncDb-schema.test.ts +178 -0
  48. package/tests/syncDb-types.test.ts +129 -0
  49. package/tsconfig.json +2 -2
  50. package/types/api.d.ts +1 -1
  51. package/types/befly.d.ts +23 -21
  52. package/types/common.d.ts +0 -29
  53. package/types/context.d.ts +8 -6
  54. package/types/hook.d.ts +3 -4
  55. package/types/plugin.d.ts +3 -0
  56. package/hooks/errorHandler.ts +0 -23
  57. package/hooks/requestId.ts +0 -24
  58. package/hooks/requestLogger.ts +0 -25
  59. package/hooks/responseFormatter.ts +0 -64
  60. package/router/root.ts +0 -56
  61. package/sync/syncDb/index.ts +0 -164
package/sync/syncDb.ts CHANGED
@@ -1,19 +1,159 @@
1
1
  /**
2
2
  * SyncDb 命令 - 同步数据库表结构
3
+ *
4
+ * 功能:
5
+ * - 协调所有模块,执行数据库表结构同步
6
+ * - 处理核心表、项目表、addon 表
7
+ * - 提供统计信息和错误处理
3
8
  */
4
9
 
5
- import { join } from 'pathe';
10
+ import { resolve } from 'pathe';
6
11
  import { existsSync } from 'node:fs';
12
+ import { snakeCase } from 'es-toolkit/string';
13
+ import { Connect } from '../lib/connect.js';
14
+ import { RedisHelper } from '../lib/redisHelper.js';
15
+ import { checkTable } from '../checks/checkTable.js';
16
+ import { scanFiles, scanAddons, addonDirExists, getAddonDir } from 'befly-util';
7
17
  import { Logger } from '../lib/logger.js';
8
- import { SyncDb } from './syncDb/index.js';
9
- import type { SyncDbOptions, BeflyOptions } from '../types/index.js';
18
+ import { projectDir } from '../paths.js';
10
19
 
11
- export async function syncDbCommand(config: BeflyOptions, options: SyncDbOptions): Promise<void> {
20
+ // 导入模块化的功能
21
+ import { ensureDbVersion } from './syncDb/version.js';
22
+ import { tableExists } from './syncDb/schema.js';
23
+ import { modifyTable } from './syncDb/table.js';
24
+ import { createTable } from './syncDb/tableCreate.js';
25
+ import { applyFieldDefaults } from './syncDb/helpers.js';
26
+ import type { SQL } from 'bun';
27
+ import type { BeflyOptions, SyncDbOptions } from '../types/index.js';
28
+
29
+ // 全局 SQL 客户端实例
30
+ let sql: SQL | null = null;
31
+
32
+ // 记录处理过的表名(用于清理缓存)
33
+ const processedTables: string[] = [];
34
+
35
+ /**
36
+ * syncDbCommand - 数据库同步命令入口
37
+ *
38
+ * 流程:
39
+ * 1. 验证表定义文件
40
+ * 2. 建立数据库连接并检查版本
41
+ * 3. 扫描表定义文件(核心表、项目表、addon表)
42
+ * 4. 对比并应用表结构变更
43
+ */
44
+ export async function syncDbCommand(config: BeflyOptions, options: SyncDbOptions = {}): Promise<void> {
12
45
  try {
13
- // 执行同步
14
- await SyncDb(config, options);
46
+ // 清空处理记录
47
+ processedTables.length = 0;
48
+
49
+ // 验证表定义文件
50
+ await checkTable();
51
+
52
+ // 建立数据库连接并检查版本
53
+ sql = await Connect.connectSql({ max: 1 });
54
+ await ensureDbVersion(sql);
55
+
56
+ // 初始化 Redis 连接(用于清理缓存)
57
+ await Connect.connectRedis();
58
+
59
+ // 扫描表定义文件
60
+ const directories: Array<{ path: string; type: 'app' | 'addon'; addonName?: string; addonNameSnake?: string }> = [];
61
+
62
+ // 1. 项目表(无前缀)- 如果 tables 目录存在
63
+ const projectTablesDir = resolve(projectDir, 'tables');
64
+ if (existsSync(projectTablesDir)) {
65
+ directories.push({ path: projectTablesDir, type: 'app' });
66
+ }
67
+
68
+ // 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
69
+ const addons = scanAddons();
70
+ for (const addon of addons) {
71
+ if (addonDirExists(addon, 'tables')) {
72
+ directories.push({
73
+ path: getAddonDir(addon, 'tables'),
74
+ type: 'addon',
75
+ addonName: addon,
76
+ addonNameSnake: snakeCase(addon) // 提前转换,避免每个文件都转换
77
+ });
78
+ }
79
+ }
80
+
81
+ // 处理表文件
82
+ for (const dirConfig of directories) {
83
+ const { path: dir, type } = dirConfig;
84
+
85
+ const files = await scanFiles(dir, '*.json');
86
+
87
+ for (const { filePath: file, fileName } of files) {
88
+ // 确定表名:
89
+ // - addon 表:{addonName}_{表名}
90
+ // 例如:admin addon 的 user.json → admin_user
91
+ // - 项目表:{表名}
92
+ // 例如:user.json → user
93
+ let tableName = snakeCase(fileName);
94
+ if (type === 'addon' && dirConfig.addonNameSnake) {
95
+ // addon 表,使用提前转换好的名称
96
+ tableName = `addon_${dirConfig.addonNameSnake}_${tableName}`;
97
+ }
98
+
99
+ // 如果指定了表名,则只同步该表
100
+ if (options.table && options.table !== tableName) {
101
+ continue;
102
+ }
103
+
104
+ const tableDefinitionModule = await import(file, { with: { type: 'json' } });
105
+ const tableDefinition = tableDefinitionModule.default;
106
+
107
+ // 为字段属性设置默认值
108
+ for (const fieldDef of Object.values(tableDefinition)) {
109
+ applyFieldDefaults(fieldDef);
110
+ }
111
+
112
+ const dbName = config.db?.database;
113
+ const existsTable = await tableExists(sql!, tableName, dbName);
114
+
115
+ // 读取 force 参数
116
+ const force = options.force || false;
117
+
118
+ if (existsTable) {
119
+ await modifyTable(sql!, tableName, tableDefinition, force, dbName);
120
+ } else {
121
+ await createTable(sql!, tableName, tableDefinition, ['created_at', 'updated_at', 'state'], dbName);
122
+ }
123
+
124
+ // 记录处理过的表名(用于清理缓存)
125
+ processedTables.push(tableName);
126
+ }
127
+ }
128
+
129
+ // 清理 Redis 缓存(如果有表被处理)
130
+ if (processedTables.length > 0) {
131
+ const redisHelper = new RedisHelper();
132
+ for (const tableName of processedTables) {
133
+ const cacheKey = `table:columns:${tableName}`;
134
+ try {
135
+ await redisHelper.del(cacheKey);
136
+ } catch (error: any) {
137
+ Logger.warn(`清理表 ${tableName} 的缓存失败: ${error.message}`);
138
+ }
139
+ }
140
+ }
15
141
  } catch (error: any) {
16
142
  Logger.error('数据库同步失败', error);
17
143
  throw error;
144
+ } finally {
145
+ if (sql) {
146
+ try {
147
+ await Connect.disconnectSql();
148
+ } catch (error: any) {
149
+ Logger.warn(`关闭数据库连接时出错: ${error.message}`);
150
+ }
151
+ }
152
+
153
+ try {
154
+ await Connect.disconnectRedis();
155
+ } catch (error: any) {
156
+ Logger.warn(`关闭 Redis 连接时出错: ${error.message}`);
157
+ }
18
158
  }
19
159
  }
package/sync/syncDev.ts CHANGED
@@ -7,9 +7,12 @@
7
7
  * - 表名: addon_admin_admin
8
8
  */
9
9
 
10
- import { Database } from '../lib/database.js';
11
- import { Cipher } from '../lib/cipher.js';
10
+ import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-util';
11
+
12
12
  import { Logger } from '../lib/logger.js';
13
+ import { Cipher } from '../lib/cipher.js';
14
+ import { Connect } from '../lib/connect.js';
15
+ import { DbHelper } from '../lib/dbHelper.js';
13
16
  import type { SyncDevOptions, SyncDevStats, BeflyOptions } from '../types/index.js';
14
17
 
15
18
  /**
@@ -22,33 +25,34 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
22
25
  return;
23
26
  }
24
27
 
25
- const devPassword = config.devPassword;
26
- const devEmail = config.devEmail || 'dev@qq.com';
27
-
28
- if (!devPassword) {
28
+ if (!config.devPassword) {
29
+ // 未配置开发者密码,跳过同步
29
30
  return;
30
31
  }
31
32
 
32
33
  // 连接数据库(SQL + Redis)
33
- await Database.connect();
34
+ await Connect.connect(config);
34
35
 
35
- const helper = Database.getDbHelper();
36
+ const helper = Connect.getDbHelper();
36
37
 
37
38
  // 检查 addon_admin_admin 表是否存在
38
39
  const existAdmin = await helper.tableExists('addon_admin_admin');
39
40
  if (!existAdmin) {
41
+ Logger.debug('[SyncDev] 表 addon_admin_admin 不存在,跳过开发者账号同步');
40
42
  return;
41
43
  }
42
44
 
43
45
  // 检查 addon_admin_role 表是否存在
44
46
  const existRole = await helper.tableExists('addon_admin_role');
45
47
  if (!existRole) {
48
+ Logger.debug('[SyncDev] 表 addon_admin_role 不存在,跳过开发者账号同步');
46
49
  return;
47
50
  }
48
51
 
49
52
  // 检查 addon_admin_menu 表是否存在
50
53
  const existMenu = await helper.tableExists('addon_admin_menu');
51
54
  if (!existMenu) {
55
+ Logger.debug('[SyncDev] 表 addon_admin_menu 不存在,跳过开发者账号同步');
52
56
  return;
53
57
  }
54
58
 
@@ -59,6 +63,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
59
63
  });
60
64
 
61
65
  if (!allMenus || !Array.isArray(allMenus)) {
66
+ Logger.debug('[SyncDev] 菜单数据为空,跳过开发者账号同步');
62
67
  return;
63
68
  }
64
69
 
@@ -113,13 +118,12 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
113
118
  }
114
119
 
115
120
  // 使用 bcrypt 加密密码
116
- const hashed = await Cipher.hashPassword(devPassword);
121
+ const hashed = await Cipher.hashPassword(config.devPassword);
117
122
 
118
123
  // 准备开发管理员数据
119
124
  const devData = {
120
- name: '开发者',
121
125
  nickname: '开发者',
122
- email: devEmail,
126
+ email: config.devEmail,
123
127
  username: 'dev',
124
128
  password: hashed,
125
129
  roleId: devRole.id,
@@ -130,14 +134,14 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
130
134
  // 查询现有账号
131
135
  const existing = await helper.getOne({
132
136
  table: 'addon_admin_admin',
133
- where: { email: devEmail }
137
+ where: { email: config.devEmail }
134
138
  });
135
139
 
136
140
  if (existing) {
137
141
  // 更新现有账号
138
142
  await helper.updData({
139
143
  table: 'addon_admin_admin',
140
- where: { email: devEmail },
144
+ where: { email: config.devEmail },
141
145
  data: devData
142
146
  });
143
147
  } else {
@@ -167,7 +171,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
167
171
  fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
168
172
  });
169
173
 
170
- const redis = Database.getRedis();
174
+ const redis = Connect.getRedis();
171
175
 
172
176
  // 为每个角色缓存接口权限
173
177
  for (const role of roles) {
@@ -201,6 +205,6 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
201
205
  Logger.error('同步开发者管理员失败', error);
202
206
  throw error;
203
207
  } finally {
204
- await Database?.disconnect();
208
+ await Connect.disconnect();
205
209
  }
206
210
  }
package/sync/syncMenu.ts CHANGED
@@ -1,50 +1,39 @@
1
1
  /**
2
2
  * SyncMenu 命令 - 同步菜单数据到数据库
3
- * 说明:根据 menu.json 配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
3
+ * 说明:根据配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
4
4
  *
5
5
  * 流程:
6
- * 1. 扫描项目根目录和所有 addon 的 menu.json 配置文件
7
- * 2. 项目的 menu.json 优先级最高,可以覆盖 addon 的菜单配置
8
- * 3. 文件不存在或格式错误时默认为空数组
9
- * 4. 根据菜单的 path 字段检查是否存在
10
- * 5. 存在则更新其他字段(name、sort、type、pid)
11
- * 6. 不存在则新增菜单记录
12
- * 7. 强制删除配置中不存在的菜单记录
6
+ * 1. 扫描所有 addon 的 addon.config.js/ts 配置文件
7
+ * 2. 扫描项目根目录的 app.config.js/ts 配置文件
8
+ * 3. 项目的 app.config 优先级最高,可以覆盖 addon 的菜单配置
9
+ * 4. 文件不存在或格式错误时默认为空数组
10
+ * 5. 根据菜单的 path 字段检查是否存在
11
+ * 6. 存在则更新其他字段(name、sort、type、pid)
12
+ * 7. 不存在则新增菜单记录
13
+ * 8. 强制删除配置中不存在的菜单记录
13
14
  * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
14
15
  */
15
16
 
16
17
  import { join } from 'pathe';
17
- import { existsSync } from 'node:fs';
18
- import { Database } from '../lib/database.js';
18
+ import { cloneDeep } from 'es-toolkit';
19
+ import { Connect } from '../lib/connect.js';
19
20
  import { RedisHelper } from '../lib/redisHelper.js';
20
- import { scanAddons, getAddonDir } from 'befly-util';
21
+ import { scanAddons, getAddonDir, scanConfig } from 'befly-util';
21
22
  import { Logger } from '../lib/logger.js';
22
23
  import { projectDir } from '../paths.js';
23
24
 
24
25
  import type { SyncMenuOptions, MenuConfig, BeflyOptions } from '../types/index.js';
25
26
 
26
27
  /**
27
- * 读取菜单配置文件
28
- * 如果文件不存在或不是数组格式,返回空数组
28
+ * 递归转换菜单路径
29
+ * @param menu 菜单对象(会被修改)
30
+ * @param transform 路径转换函数
29
31
  */
30
- async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
31
- try {
32
- if (!existsSync(filePath)) {
33
- return [];
34
- }
35
-
36
- const content = await import(filePath, { with: { type: 'json' } });
37
-
38
- // 验证是否为数组
39
- if (!Array.isArray(content.default)) {
40
- return [];
41
- }
42
-
43
- return content.default;
44
- } catch (error: any) {
45
- Logger.warn(`读取菜单配置失败 ${filePath}: ${error.message}`);
46
- return [];
32
+ function transformMenuPaths(menu: MenuConfig, transform: (path: string) => string): void {
33
+ if (menu.path && menu.path.startsWith('/')) {
34
+ menu.path = transform(menu.path);
47
35
  }
36
+ menu.children?.forEach((child) => transformMenuPaths(child, transform));
48
37
  }
49
38
 
50
39
  /**
@@ -57,25 +46,15 @@ async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
57
46
  */
58
47
  function addAddonPrefix(menus: MenuConfig[], addonName: string): MenuConfig[] {
59
48
  return menus.map((menu) => {
60
- const newMenu = { ...menu };
61
-
62
- // 处理当前菜单的 path(包括根路径 /)
63
- if (newMenu.path && newMenu.path.startsWith('/')) {
64
- newMenu.path = `/addon/${addonName}${newMenu.path}`;
65
- }
66
-
67
- // 递归处理子菜单
68
- if (newMenu.children && newMenu.children.length > 0) {
69
- newMenu.children = addAddonPrefix(newMenu.children, addonName);
70
- }
71
-
72
- return newMenu;
49
+ const cloned = cloneDeep(menu);
50
+ transformMenuPaths(cloned, (path) => `/addon/${addonName}${path}`);
51
+ return cloned;
73
52
  });
74
53
  }
75
54
 
76
55
  /**
77
56
  * 合并菜单配置
78
- * 优先级:项目 menu.json > addon menu.json
57
+ * 优先级:项目 package.json > addon package.json
79
58
  * 支持三级菜单结构:父级、子级、孙级
80
59
  */
81
60
  function mergeMenuConfigs(allMenus: Array<{ menus: MenuConfig[]; addonName: string }>): MenuConfig[] {
@@ -251,6 +230,58 @@ async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Pro
251
230
  }
252
231
  }
253
232
 
233
+ /**
234
+ * 加载所有菜单配置(addon + 项目)
235
+ * @returns 菜单配置数组
236
+ */
237
+ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; addonName: string }>> {
238
+ const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
239
+
240
+ // 1. 加载所有 addon 配置
241
+ const addonNames = scanAddons();
242
+
243
+ for (const addonName of addonNames) {
244
+ try {
245
+ const addonDir = getAddonDir(addonName, '');
246
+ const addonConfigData = await scanConfig({
247
+ dirs: [addonDir],
248
+ files: ['addon.config'],
249
+ mode: 'first',
250
+ paths: ['menus']
251
+ });
252
+
253
+ const addonMenus = addonConfigData?.menus || [];
254
+ if (Array.isArray(addonMenus) && addonMenus.length > 0) {
255
+ // 为 addon 菜单添加路径前缀
256
+ const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
257
+ allMenus.push({ menus: menusWithPrefix, addonName: addonName });
258
+ }
259
+ } catch (error: any) {
260
+ Logger.warn(`读取 addon 配置失败 ${addonName}: ${error.message}`);
261
+ }
262
+ }
263
+
264
+ // 2. 加载项目配置
265
+ try {
266
+ const appConfigData = await scanConfig({
267
+ dirs: [projectDir],
268
+ files: ['app.config'],
269
+ mode: 'first',
270
+ paths: ['menus']
271
+ });
272
+
273
+ const appMenus = appConfigData?.menus || [];
274
+ if (Array.isArray(appMenus) && appMenus.length > 0) {
275
+ // 项目菜单不添加前缀
276
+ allMenus.push({ menus: appMenus, addonName: 'app' });
277
+ }
278
+ } catch (error: any) {
279
+ Logger.warn(`读取项目配置失败: ${error.message}`);
280
+ }
281
+
282
+ return allMenus;
283
+ }
284
+
254
285
  /**
255
286
  * SyncMenu 命令主函数
256
287
  */
@@ -261,37 +292,18 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
261
292
  return;
262
293
  }
263
294
 
264
- // 1. 扫描所有 addon menu.json 配置文件
265
- const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
266
-
267
- const addonNames = scanAddons();
268
-
269
- for (const addonName of addonNames) {
270
- const addonMenuPath = getAddonDir(addonName, 'menu.json');
271
- if (existsSync(addonMenuPath)) {
272
- const addonMenus = await readMenuConfig(addonMenuPath);
273
- if (addonMenus.length > 0) {
274
- // 为 addon 菜单添加路径前缀
275
- const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
276
- allMenus.push({ menus: menusWithPrefix, addonName: addonName });
277
- }
278
- }
279
- }
295
+ // 1. 加载所有菜单配置(addon + 项目)
296
+ const allMenus = await loadMenuConfigs();
280
297
 
281
- // 2. 读取项目根目录的 menu.json(优先级最高,不添加前缀)
282
- const projectMenuPath = join(projectDir, 'menu.json');
283
- const projectMenus = await readMenuConfig(projectMenuPath);
284
- if (projectMenus.length > 0) {
285
- allMenus.push({ menus: projectMenus, addonName: 'project' });
286
- } // 3. 合并菜单配置(项目配置优先)
298
+ // 2. 合并菜单配置
287
299
  const mergedMenus = mergeMenuConfigs(allMenus);
288
300
 
289
301
  // 连接数据库(SQL + Redis)
290
- await Database.connect();
302
+ await Connect.connect(config);
291
303
 
292
- const helper = Database.getDbHelper();
304
+ const helper = Connect.getDbHelper();
293
305
 
294
- // 4. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
306
+ // 3. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
295
307
  const exists = await helper.tableExists('addon_admin_menu');
296
308
 
297
309
  if (!exists) {
@@ -299,23 +311,23 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
299
311
  return;
300
312
  }
301
313
 
302
- // 5. 收集配置文件中所有菜单的 path
314
+ // 4. 收集配置文件中所有菜单的 path
303
315
  const configPaths = collectPaths(mergedMenus);
304
316
 
305
- // 6. 同步菜单
317
+ // 5. 同步菜单
306
318
  await syncMenus(helper, mergedMenus);
307
319
 
308
- // 7. 删除文件中不存在的菜单(强制删除)
320
+ // 6. 删除文件中不存在的菜单(强制删除)
309
321
  await deleteObsoleteRecords(helper, configPaths);
310
322
 
311
- // 8. 获取最终菜单数据(用于缓存)
323
+ // 7. 获取最终菜单数据(用于缓存)
312
324
  const allMenusData = await helper.getAll({
313
325
  table: 'addon_admin_menu',
314
326
  fields: ['id', 'pid', 'name', 'path', 'sort'],
315
327
  orderBy: ['sort#ASC', 'id#ASC']
316
328
  });
317
329
 
318
- // 9. 缓存菜单数据到 Redis
330
+ // 8. 缓存菜单数据到 Redis
319
331
  try {
320
332
  const redisHelper = new RedisHelper();
321
333
  await redisHelper.setObject('menus:all', allMenusData);
@@ -326,6 +338,6 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
326
338
  Logger.error('菜单同步失败', error);
327
339
  throw error;
328
340
  } finally {
329
- await Database?.disconnect();
341
+ await Connect.disconnect();
330
342
  }
331
343
  }
@@ -3,29 +3,24 @@
3
3
  * 测试 Redis 操作功能
4
4
  */
5
5
 
6
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
7
- import { Database } from '../lib/database.js';
8
- import { RedisHelper } from '../lib/redisHelper.js';
6
+ import { describe, expect, it, test, beforeAll, afterAll } from 'bun:test';
7
+ import { RedisClient } from 'bun';
8
+
9
9
  import { defaultOptions } from '../config.js';
10
+ import { Connect } from '../lib/connect.js';
11
+ import { RedisHelper } from '../lib/redisHelper.js';
10
12
 
11
13
  let redis: RedisHelper;
12
14
 
13
15
  beforeAll(async () => {
14
- // 使用项目默认配置连接 Redis
15
- await Database.connectRedis(defaultOptions.redis);
16
- // 使用项目配置的 prefix
17
- redis = new RedisHelper(defaultOptions.redis.prefix);
16
+ // 连接 Redis
17
+ await Connect.connectRedis(defaultOptions.redis);
18
+ redis = new RedisHelper();
18
19
  });
19
20
 
20
21
  afterAll(async () => {
21
- // 清理测试数据
22
- await redis.del('test:string');
23
- await redis.del('test:object');
24
- await redis.del('test:set');
25
- await redis.del('test:ttl');
26
- await redis.del('test:expire');
27
-
28
- await Database.disconnectRedis();
22
+ // 断开 Redis 连接
23
+ await Connect.disconnectRedis();
29
24
  });
30
25
 
31
26
  describe('RedisHelper - 字符串操作', () => {
@@ -102,11 +97,15 @@ describe('RedisHelper - 对象操作', () => {
102
97
 
103
98
  describe('RedisHelper - Set 操作', () => {
104
99
  test('sadd - 添加成员到 Set', async () => {
105
- const count = await redis.sadd('test:set', ['member1', 'member2', 'member3']);
100
+ // 先清除,确保测试隔离
101
+ await redis.del('test:set:sadd');
102
+ const count = await redis.sadd('test:set:sadd', ['member1', 'member2', 'member3']);
106
103
  expect(count).toBeGreaterThan(0);
104
+ await redis.del('test:set:sadd');
107
105
  });
108
106
 
109
107
  test('sismember - 检查成员是否存在', async () => {
108
+ await redis.del('test:set');
110
109
  await redis.sadd('test:set', ['member1']);
111
110
 
112
111
  const exists = await redis.sismember('test:set', 'member1');