befly 3.9.37 → 3.9.39
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/README.md +38 -39
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +225 -235
- package/docs/cipher.md +71 -69
- package/docs/database.md +155 -153
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +7 -7
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +15 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -81
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +211 -109
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +53 -47
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -54
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -66
- package/sync/syncMenu.ts +190 -57
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/sync/syncMenu.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* SyncMenu 命令 - 同步菜单数据到数据库
|
|
3
3
|
* 说明:扫描 addon 的 views 目录和项目的 menus.json,同步菜单数据
|
|
4
4
|
*
|
|
5
5
|
* 流程:
|
|
6
|
-
* 1. 扫描所有 addon 的 views 目录下的
|
|
6
|
+
* 1. 扫描所有 addon 的 views 目录下的 index.vue,并从 definePage({ meta }) 解析菜单元信息
|
|
7
7
|
* 2. 根据目录层级构建菜单树(无层级限制)
|
|
8
8
|
* 3. 读取项目的 menus.json 文件(手动配置的菜单)
|
|
9
9
|
* 4. 根据菜单的 path 字段检查是否存在
|
|
@@ -13,27 +13,137 @@
|
|
|
13
13
|
* 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
import {
|
|
16
|
+
import type { SyncMenuOptions, MenuConfig } from "../types/sync.js";
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
-
import { DbHelper } from '../lib/dbHelper.js';
|
|
22
|
-
import { RedisHelper } from '../lib/redisHelper.js';
|
|
23
|
-
import { RedisKeys } from 'befly-shared/redisKeys';
|
|
24
|
-
import { scanAddons, getAddonDir } from 'befly-shared/addonHelper';
|
|
25
|
-
import { Logger } from '../lib/logger.js';
|
|
26
|
-
import { projectDir } from '../paths.js';
|
|
27
|
-
import { beflyConfig } from '../befly.config.js';
|
|
21
|
+
import { join } from "pathe";
|
|
28
22
|
|
|
29
|
-
import
|
|
23
|
+
import { beflyConfig } from "../befly.config.js";
|
|
24
|
+
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
25
|
+
import { Connect } from "../lib/connect.js";
|
|
26
|
+
import { DbHelper } from "../lib/dbHelper.js";
|
|
27
|
+
import { Logger } from "../lib/logger.js";
|
|
28
|
+
import { RedisHelper } from "../lib/redisHelper.js";
|
|
29
|
+
import { projectDir } from "../paths.js";
|
|
30
|
+
import { scanAddons, getAddonDir } from "../utils/addonHelper.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* 清理目录名中的数字后缀
|
|
33
34
|
* 如:login_1 → login, index_2 → index
|
|
34
35
|
*/
|
|
35
36
|
function cleanDirName(name: string): string {
|
|
36
|
-
return name.replace(/_\d+$/,
|
|
37
|
+
return name.replace(/_\d+$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ViewDirMeta = {
|
|
41
|
+
title: string;
|
|
42
|
+
order?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function normalizeMenuPath(path: string): string {
|
|
46
|
+
// 约束:统一 path 形态,避免隐藏菜单匹配、DB 同步出现重复
|
|
47
|
+
// - 必须以 / 开头
|
|
48
|
+
// - 折叠多个 /
|
|
49
|
+
// - 去掉尾随 /(根 / 除外)
|
|
50
|
+
let result = path;
|
|
51
|
+
|
|
52
|
+
if (!result) {
|
|
53
|
+
return "/";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!result.startsWith("/")) {
|
|
57
|
+
result = `/${result}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
result = result.replace(/\/+/g, "/");
|
|
61
|
+
|
|
62
|
+
if (result.length > 1) {
|
|
63
|
+
result = result.replace(/\/+$/, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeMenuTree(menus: MenuConfig[]): MenuConfig[] {
|
|
70
|
+
// 递归规范化并按 path 去重(同 path 的 children 合并)
|
|
71
|
+
const map = new Map<string, MenuConfig>();
|
|
72
|
+
|
|
73
|
+
for (const menu of menus) {
|
|
74
|
+
const menuPath = menu.path ? normalizeMenuPath(menu.path) : "";
|
|
75
|
+
const cloned: MenuConfig = {
|
|
76
|
+
name: menu.name,
|
|
77
|
+
path: menuPath,
|
|
78
|
+
sort: menu.sort
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (menu.children && menu.children.length > 0) {
|
|
82
|
+
cloned.children = normalizeMenuTree(menu.children);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!menuPath) {
|
|
86
|
+
// path 缺失的菜单无法参与同步/去重,直接丢弃
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const existing = map.get(menuPath);
|
|
91
|
+
if (existing) {
|
|
92
|
+
if (cloned.children && cloned.children.length > 0) {
|
|
93
|
+
existing.children = existing.children || [];
|
|
94
|
+
existing.children.push(...cloned.children);
|
|
95
|
+
existing.children = normalizeMenuTree(existing.children);
|
|
96
|
+
}
|
|
97
|
+
if (typeof cloned.sort === "number") {
|
|
98
|
+
existing.sort = cloned.sort;
|
|
99
|
+
}
|
|
100
|
+
if (cloned.name) {
|
|
101
|
+
existing.name = cloned.name;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
map.set(menuPath, cloned);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = Array.from(map.values());
|
|
109
|
+
result.sort((a, b) => (a.sort || 1) - (b.sort || 1));
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractScriptSetupBlock(vueContent: string): string | null {
|
|
114
|
+
// 只取第一个 <script ... setup ...> 块
|
|
115
|
+
const openTag = /<script\b[^>]*\bsetup\b[^>]*>/i.exec(vueContent);
|
|
116
|
+
if (!openTag) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const start = openTag.index + openTag[0].length;
|
|
121
|
+
const closeIndex = vueContent.indexOf("</script>", start);
|
|
122
|
+
if (closeIndex < 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return vueContent.slice(start, closeIndex);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractDefinePageMetaFromScriptSetup(scriptSetup: string): ViewDirMeta | null {
|
|
130
|
+
// 简化约束:
|
|
131
|
+
// - 每个页面只有一个 definePage
|
|
132
|
+
// - title 是纯字符串字面量
|
|
133
|
+
// - order 是数字字面量(可选)
|
|
134
|
+
// - 不考虑变量/表达式/多段 meta 组合
|
|
135
|
+
|
|
136
|
+
const titleMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?title\s*:\s*(["'`])([^"'`]+)\1/);
|
|
137
|
+
if (!titleMatch) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const orderMatch = scriptSetup.match(/definePage\s*\([\s\S]*?meta\s*:\s*\{[\s\S]*?order\s*:\s*(\d+)/);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
title: titleMatch[2],
|
|
145
|
+
order: orderMatch ? Number(orderMatch[1]) : undefined
|
|
146
|
+
};
|
|
37
147
|
}
|
|
38
148
|
|
|
39
149
|
/**
|
|
@@ -43,7 +153,7 @@ function cleanDirName(name: string): string {
|
|
|
43
153
|
* @param parentPath 父级路径
|
|
44
154
|
* @returns 菜单数组
|
|
45
155
|
*/
|
|
46
|
-
async function scanViewsDir(viewsDir: string, prefix: string, parentPath: string =
|
|
156
|
+
async function scanViewsDir(viewsDir: string, prefix: string, parentPath: string = ""): Promise<MenuConfig[]> {
|
|
47
157
|
if (!existsSync(viewsDir)) {
|
|
48
158
|
return [];
|
|
49
159
|
}
|
|
@@ -53,43 +163,59 @@ async function scanViewsDir(viewsDir: string, prefix: string, parentPath: string
|
|
|
53
163
|
|
|
54
164
|
for (const entry of entries) {
|
|
55
165
|
// 只处理目录,忽略 components 目录
|
|
56
|
-
if (!entry.isDirectory() || entry.name ===
|
|
166
|
+
if (!entry.isDirectory() || entry.name === "components") {
|
|
57
167
|
continue;
|
|
58
168
|
}
|
|
59
169
|
|
|
60
170
|
const dirPath = join(viewsDir, entry.name);
|
|
61
|
-
const
|
|
171
|
+
const indexVuePath = join(dirPath, "index.vue");
|
|
62
172
|
|
|
63
|
-
// 没有
|
|
64
|
-
if (!existsSync(
|
|
173
|
+
// 没有 index.vue 的目录不处理
|
|
174
|
+
if (!existsSync(indexVuePath)) {
|
|
65
175
|
continue;
|
|
66
176
|
}
|
|
67
177
|
|
|
68
|
-
//
|
|
69
|
-
let meta:
|
|
178
|
+
// 从 index.vue 中解析 definePage({ meta })
|
|
179
|
+
let meta: ViewDirMeta | null = null;
|
|
70
180
|
try {
|
|
71
|
-
const content = await readFile(
|
|
72
|
-
|
|
181
|
+
const content = await readFile(indexVuePath, "utf-8");
|
|
182
|
+
|
|
183
|
+
const scriptSetup = extractScriptSetupBlock(content);
|
|
184
|
+
if (!scriptSetup) {
|
|
185
|
+
Logger.warn({ path: indexVuePath }, "index.vue 缺少 <script setup>,已跳过该目录菜单同步");
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
meta = extractDefinePageMetaFromScriptSetup(scriptSetup);
|
|
190
|
+
if (!meta?.title) {
|
|
191
|
+
Logger.warn({ path: indexVuePath }, "index.vue 未声明 definePage({ meta: { title, order? } }),已跳过该目录菜单同步");
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
73
194
|
} catch (error: any) {
|
|
74
|
-
Logger.warn({ err: error, path:
|
|
195
|
+
Logger.warn({ err: error, path: indexVuePath }, "读取 index.vue 失败");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 没有 definePage meta 的目录不处理
|
|
200
|
+
if (!meta?.title) {
|
|
75
201
|
continue;
|
|
76
202
|
}
|
|
77
203
|
|
|
78
204
|
// 计算路径:清理数字后缀,index 目录特殊处理
|
|
79
205
|
const cleanName = cleanDirName(entry.name);
|
|
80
206
|
let menuPath: string;
|
|
81
|
-
if (cleanName ===
|
|
82
|
-
// index
|
|
83
|
-
menuPath = parentPath
|
|
207
|
+
if (cleanName === "index") {
|
|
208
|
+
// index 目录路径为父级路径;根级别用空字符串(避免 addon prefix 拼出尾随 /)
|
|
209
|
+
menuPath = parentPath;
|
|
84
210
|
} else {
|
|
85
211
|
menuPath = parentPath ? `${parentPath}/${cleanName}` : `/${cleanName}`;
|
|
86
212
|
}
|
|
87
213
|
|
|
88
214
|
// 添加 addon 前缀
|
|
89
|
-
const fullPath = prefix ? `${prefix}${menuPath}` : menuPath;
|
|
215
|
+
const fullPath = prefix ? (menuPath ? `${prefix}${menuPath}` : prefix) : menuPath || "/";
|
|
90
216
|
|
|
91
217
|
const menu: MenuConfig = {
|
|
92
|
-
name: meta.
|
|
218
|
+
name: meta.title,
|
|
93
219
|
path: fullPath,
|
|
94
220
|
sort: meta.order || 1
|
|
95
221
|
};
|
|
@@ -186,7 +312,7 @@ function collectPaths(menus: MenuConfig[]): Set<string> {
|
|
|
186
312
|
* 递归同步单个菜单(无层级限制)
|
|
187
313
|
*/
|
|
188
314
|
async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, existingMenuMap: Map<string, any>): Promise<number> {
|
|
189
|
-
const existing = existingMenuMap.get(menu.path ||
|
|
315
|
+
const existing = existingMenuMap.get(menu.path || "");
|
|
190
316
|
let menuId: number;
|
|
191
317
|
|
|
192
318
|
if (existing) {
|
|
@@ -196,7 +322,7 @@ async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, exi
|
|
|
196
322
|
|
|
197
323
|
if (needUpdate) {
|
|
198
324
|
await helper.updData({
|
|
199
|
-
table:
|
|
325
|
+
table: "addon_admin_menu",
|
|
200
326
|
where: { id: existing.id },
|
|
201
327
|
data: {
|
|
202
328
|
pid: pid,
|
|
@@ -207,11 +333,11 @@ async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, exi
|
|
|
207
333
|
}
|
|
208
334
|
} else {
|
|
209
335
|
menuId = await helper.insData({
|
|
210
|
-
table:
|
|
336
|
+
table: "addon_admin_menu",
|
|
211
337
|
data: {
|
|
212
338
|
pid: pid,
|
|
213
339
|
name: menu.name,
|
|
214
|
-
path: menu.path ||
|
|
340
|
+
path: menu.path || "",
|
|
215
341
|
sort: menu.sort || 1
|
|
216
342
|
}
|
|
217
343
|
});
|
|
@@ -231,8 +357,7 @@ async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, exi
|
|
|
231
357
|
*/
|
|
232
358
|
async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
233
359
|
const allExistingMenus = await helper.getAll({
|
|
234
|
-
table:
|
|
235
|
-
fields: ['id', 'pid', 'name', 'path', 'sort']
|
|
360
|
+
table: "addon_admin_menu"
|
|
236
361
|
});
|
|
237
362
|
const existingMenuMap = new Map<string, any>();
|
|
238
363
|
for (const menu of allExistingMenus.lists) {
|
|
@@ -245,7 +370,7 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
|
245
370
|
try {
|
|
246
371
|
await syncMenuRecursive(helper, menu, 0, existingMenuMap);
|
|
247
372
|
} catch (error: any) {
|
|
248
|
-
Logger.error({ err: error, menu: menu.name },
|
|
373
|
+
Logger.error({ err: error, menu: menu.name }, "同步菜单失败");
|
|
249
374
|
throw error;
|
|
250
375
|
}
|
|
251
376
|
}
|
|
@@ -256,15 +381,15 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
|
256
381
|
*/
|
|
257
382
|
async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Promise<void> {
|
|
258
383
|
const allRecords = await helper.getAll({
|
|
259
|
-
table:
|
|
260
|
-
fields: [
|
|
384
|
+
table: "addon_admin_menu",
|
|
385
|
+
fields: ["id", "path"],
|
|
261
386
|
where: { state$gte: 0 }
|
|
262
387
|
});
|
|
263
388
|
|
|
264
389
|
for (const record of allRecords.lists) {
|
|
265
390
|
if (record.path && !configPaths.has(record.path)) {
|
|
266
391
|
await helper.delForce({
|
|
267
|
-
table:
|
|
392
|
+
table: "addon_admin_menu",
|
|
268
393
|
where: { id: record.id }
|
|
269
394
|
});
|
|
270
395
|
}
|
|
@@ -282,8 +407,8 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
282
407
|
|
|
283
408
|
for (const addonName of addonNames) {
|
|
284
409
|
try {
|
|
285
|
-
const addonDir = getAddonDir(addonName,
|
|
286
|
-
const viewsDir = join(addonDir,
|
|
410
|
+
const addonDir = getAddonDir(addonName, "");
|
|
411
|
+
const viewsDir = join(addonDir, "views");
|
|
287
412
|
|
|
288
413
|
if (existsSync(viewsDir)) {
|
|
289
414
|
const prefix = `/addon/${addonName}`;
|
|
@@ -296,24 +421,24 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
296
421
|
}
|
|
297
422
|
}
|
|
298
423
|
} catch (error: any) {
|
|
299
|
-
Logger.warn({ err: error, addon: addonName },
|
|
424
|
+
Logger.warn({ err: error, addon: addonName }, "扫描 addon views 目录失败");
|
|
300
425
|
}
|
|
301
426
|
}
|
|
302
427
|
|
|
303
428
|
// 2. 读取项目的 menus.json
|
|
304
|
-
const menusJsonPath = join(projectDir,
|
|
429
|
+
const menusJsonPath = join(projectDir, "menus.json");
|
|
305
430
|
if (existsSync(menusJsonPath)) {
|
|
306
431
|
try {
|
|
307
|
-
const content = await readFile(menusJsonPath,
|
|
432
|
+
const content = await readFile(menusJsonPath, "utf-8");
|
|
308
433
|
const projectMenus = JSON.parse(content);
|
|
309
434
|
if (Array.isArray(projectMenus) && projectMenus.length > 0) {
|
|
310
435
|
allMenus.push({
|
|
311
436
|
menus: projectMenus,
|
|
312
|
-
source:
|
|
437
|
+
source: "project"
|
|
313
438
|
});
|
|
314
439
|
}
|
|
315
440
|
} catch (error: any) {
|
|
316
|
-
Logger.warn({ err: error },
|
|
441
|
+
Logger.warn({ err: error }, "读取项目 menus.json 失败");
|
|
317
442
|
}
|
|
318
443
|
}
|
|
319
444
|
|
|
@@ -326,7 +451,7 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
326
451
|
export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<void> {
|
|
327
452
|
try {
|
|
328
453
|
if (options.plan) {
|
|
329
|
-
Logger.debug(
|
|
454
|
+
Logger.debug("[计划] 同步菜单配置到数据库(plan 模式不执行)");
|
|
330
455
|
return;
|
|
331
456
|
}
|
|
332
457
|
|
|
@@ -336,11 +461,15 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
336
461
|
// 2. 合并菜单配置
|
|
337
462
|
let mergedMenus = mergeMenuConfigs(allMenus);
|
|
338
463
|
|
|
464
|
+
// 2.1 规范化并去重(防止尾随 / 或多 / 导致隐藏菜单与 DB 同步异常)
|
|
465
|
+
mergedMenus = normalizeMenuTree(mergedMenus);
|
|
466
|
+
|
|
339
467
|
// 3. 过滤隐藏菜单(根据 hiddenMenus 配置)
|
|
340
468
|
const hiddenMenus = (beflyConfig as any).hiddenMenus || [];
|
|
341
469
|
if (Array.isArray(hiddenMenus) && hiddenMenus.length > 0) {
|
|
342
|
-
const hiddenSet = new Set(hiddenMenus);
|
|
470
|
+
const hiddenSet = new Set(hiddenMenus.map((item: string) => normalizeMenuPath(item)));
|
|
343
471
|
mergedMenus = filterHiddenMenus(mergedMenus, hiddenSet);
|
|
472
|
+
mergedMenus = normalizeMenuTree(mergedMenus);
|
|
344
473
|
}
|
|
345
474
|
|
|
346
475
|
// 连接数据库
|
|
@@ -349,10 +478,10 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
349
478
|
const helper = new DbHelper({ redis: new RedisHelper() } as any, Connect.getSql());
|
|
350
479
|
|
|
351
480
|
// 3. 检查表是否存在
|
|
352
|
-
const exists = await helper.tableExists(
|
|
481
|
+
const exists = await helper.tableExists("addon_admin_menu");
|
|
353
482
|
|
|
354
483
|
if (!exists) {
|
|
355
|
-
Logger.debug(
|
|
484
|
+
Logger.debug("表 addon_admin_menu 不存在,跳过菜单同步");
|
|
356
485
|
return;
|
|
357
486
|
}
|
|
358
487
|
|
|
@@ -367,22 +496,26 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
367
496
|
|
|
368
497
|
// 7. 获取最终菜单数据(用于缓存)
|
|
369
498
|
const allMenusData = await helper.getAll({
|
|
370
|
-
table:
|
|
371
|
-
fields: ['id', 'pid', 'name', 'path', 'sort'],
|
|
372
|
-
orderBy: ['sort#ASC', 'id#ASC']
|
|
499
|
+
table: "addon_admin_menu"
|
|
373
500
|
});
|
|
374
501
|
|
|
375
502
|
// 8. 缓存菜单数据到 Redis
|
|
376
503
|
try {
|
|
377
504
|
const redisHelper = new RedisHelper();
|
|
378
|
-
await redisHelper.setObject(
|
|
505
|
+
await redisHelper.setObject(CacheKeys.menusAll(), allMenusData.lists);
|
|
379
506
|
} catch (error: any) {
|
|
380
|
-
Logger.warn({ err: error },
|
|
507
|
+
Logger.warn({ err: error }, "Redis 缓存菜单数据失败");
|
|
381
508
|
}
|
|
382
509
|
} catch (error: any) {
|
|
383
|
-
Logger.error({ err: error },
|
|
510
|
+
Logger.error({ err: error }, "菜单同步失败");
|
|
384
511
|
throw error;
|
|
385
512
|
} finally {
|
|
386
513
|
await Connect.disconnect();
|
|
387
514
|
}
|
|
388
515
|
}
|
|
516
|
+
|
|
517
|
+
// 仅测试用(避免将内部扫描逻辑变成稳定 API)
|
|
518
|
+
export const __test__ = {
|
|
519
|
+
scanViewsDir: scanViewsDir,
|
|
520
|
+
normalizeMenuPath: normalizeMenuPath
|
|
521
|
+
};
|