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.
- package/config.ts +8 -9
- package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
- package/hooks/auth.ts +3 -11
- package/hooks/cors.ts +1 -4
- package/hooks/parser.ts +6 -8
- package/hooks/permission.ts +9 -12
- package/hooks/validator.ts +6 -9
- package/lib/cacheHelper.ts +0 -4
- package/lib/{database.ts → connect.ts} +65 -18
- package/lib/logger.ts +1 -17
- package/lib/redisHelper.ts +6 -5
- package/loader/loadApis.ts +3 -3
- package/loader/loadHooks.ts +15 -41
- package/loader/loadPlugins.ts +10 -16
- package/main.ts +25 -28
- package/package.json +3 -3
- package/plugins/cache.ts +2 -2
- package/plugins/cipher.ts +15 -0
- package/plugins/config.ts +16 -0
- package/plugins/db.ts +7 -17
- package/plugins/jwt.ts +15 -0
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/plugins/tool.ts +50 -0
- package/router/api.ts +56 -42
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +2 -20
- package/sync/syncApi.ts +7 -7
- package/sync/syncDb/apply.ts +1 -4
- package/sync/syncDb/constants.ts +3 -0
- package/sync/syncDb/ddl.ts +2 -1
- package/sync/syncDb/helpers.ts +5 -117
- package/sync/syncDb/sqlite.ts +1 -3
- package/sync/syncDb/table.ts +8 -142
- package/sync/syncDb/tableCreate.ts +25 -9
- package/sync/syncDb/types.ts +125 -0
- package/sync/syncDb/version.ts +0 -3
- package/sync/syncDb.ts +146 -6
- package/sync/syncDev.ts +19 -15
- package/sync/syncMenu.ts +87 -75
- package/tests/redisHelper.test.ts +15 -16
- package/tests/sync-connection.test.ts +189 -0
- package/tests/syncDb-apply.test.ts +287 -0
- package/tests/syncDb-constants.test.ts +150 -0
- package/tests/syncDb-ddl.test.ts +205 -0
- package/tests/syncDb-helpers.test.ts +112 -0
- package/tests/syncDb-schema.test.ts +178 -0
- package/tests/syncDb-types.test.ts +129 -0
- package/tsconfig.json +2 -2
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +23 -21
- package/types/common.d.ts +0 -29
- package/types/context.d.ts +8 -6
- package/types/hook.d.ts +3 -4
- package/types/plugin.d.ts +3 -0
- package/hooks/errorHandler.ts +0 -23
- package/hooks/requestId.ts +0 -24
- package/hooks/requestLogger.ts +0 -25
- package/hooks/responseFormatter.ts +0 -64
- package/router/root.ts +0 -56
- 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 {
|
|
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 {
|
|
9
|
-
import type { SyncDbOptions, BeflyOptions } from '../types/index.js';
|
|
18
|
+
import { projectDir } from '../paths.js';
|
|
10
19
|
|
|
11
|
-
|
|
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
|
-
|
|
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 {
|
|
11
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!devPassword) {
|
|
28
|
+
if (!config.devPassword) {
|
|
29
|
+
// 未配置开发者密码,跳过同步
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// 连接数据库(SQL + Redis)
|
|
33
|
-
await
|
|
34
|
+
await Connect.connect(config);
|
|
34
35
|
|
|
35
|
-
const helper =
|
|
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 =
|
|
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
|
|
208
|
+
await Connect.disconnect();
|
|
205
209
|
}
|
|
206
210
|
}
|
package/sync/syncMenu.ts
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SyncMenu 命令 - 同步菜单数据到数据库
|
|
3
|
-
*
|
|
3
|
+
* 说明:根据配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
|
|
4
4
|
*
|
|
5
5
|
* 流程:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
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 {
|
|
18
|
-
import {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
* 优先级:项目
|
|
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.
|
|
265
|
-
const allMenus
|
|
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.
|
|
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
|
|
302
|
+
await Connect.connect(config);
|
|
291
303
|
|
|
292
|
-
const helper =
|
|
304
|
+
const helper = Connect.getDbHelper();
|
|
293
305
|
|
|
294
|
-
//
|
|
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
|
-
//
|
|
314
|
+
// 4. 收集配置文件中所有菜单的 path
|
|
303
315
|
const configPaths = collectPaths(mergedMenus);
|
|
304
316
|
|
|
305
|
-
//
|
|
317
|
+
// 5. 同步菜单
|
|
306
318
|
await syncMenus(helper, mergedMenus);
|
|
307
319
|
|
|
308
|
-
//
|
|
320
|
+
// 6. 删除文件中不存在的菜单(强制删除)
|
|
309
321
|
await deleteObsoleteRecords(helper, configPaths);
|
|
310
322
|
|
|
311
|
-
//
|
|
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
|
-
//
|
|
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
|
|
341
|
+
await Connect.disconnect();
|
|
330
342
|
}
|
|
331
343
|
}
|
|
@@ -3,29 +3,24 @@
|
|
|
3
3
|
* 测试 Redis 操作功能
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe,
|
|
7
|
-
import {
|
|
8
|
-
|
|
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
|
-
//
|
|
15
|
-
await
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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');
|