befly 3.9.38 → 3.9.40
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 +37 -38
- 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} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- 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} +1 -1
- 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 +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- 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 +48 -44
- 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 -52
- 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 -65
- package/sync/syncMenu.ts +190 -55
- 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,7 +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:
|
|
360
|
+
table: "addon_admin_menu"
|
|
235
361
|
});
|
|
236
362
|
const existingMenuMap = new Map<string, any>();
|
|
237
363
|
for (const menu of allExistingMenus.lists) {
|
|
@@ -244,7 +370,7 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
|
244
370
|
try {
|
|
245
371
|
await syncMenuRecursive(helper, menu, 0, existingMenuMap);
|
|
246
372
|
} catch (error: any) {
|
|
247
|
-
Logger.error({ err: error, menu: menu.name },
|
|
373
|
+
Logger.error({ err: error, menu: menu.name }, "同步菜单失败");
|
|
248
374
|
throw error;
|
|
249
375
|
}
|
|
250
376
|
}
|
|
@@ -255,15 +381,15 @@ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
|
|
|
255
381
|
*/
|
|
256
382
|
async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Promise<void> {
|
|
257
383
|
const allRecords = await helper.getAll({
|
|
258
|
-
table:
|
|
259
|
-
fields: [
|
|
384
|
+
table: "addon_admin_menu",
|
|
385
|
+
fields: ["id", "path"],
|
|
260
386
|
where: { state$gte: 0 }
|
|
261
387
|
});
|
|
262
388
|
|
|
263
389
|
for (const record of allRecords.lists) {
|
|
264
390
|
if (record.path && !configPaths.has(record.path)) {
|
|
265
391
|
await helper.delForce({
|
|
266
|
-
table:
|
|
392
|
+
table: "addon_admin_menu",
|
|
267
393
|
where: { id: record.id }
|
|
268
394
|
});
|
|
269
395
|
}
|
|
@@ -281,8 +407,8 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
281
407
|
|
|
282
408
|
for (const addonName of addonNames) {
|
|
283
409
|
try {
|
|
284
|
-
const addonDir = getAddonDir(addonName,
|
|
285
|
-
const viewsDir = join(addonDir,
|
|
410
|
+
const addonDir = getAddonDir(addonName, "");
|
|
411
|
+
const viewsDir = join(addonDir, "views");
|
|
286
412
|
|
|
287
413
|
if (existsSync(viewsDir)) {
|
|
288
414
|
const prefix = `/addon/${addonName}`;
|
|
@@ -295,24 +421,24 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
295
421
|
}
|
|
296
422
|
}
|
|
297
423
|
} catch (error: any) {
|
|
298
|
-
Logger.warn({ err: error, addon: addonName },
|
|
424
|
+
Logger.warn({ err: error, addon: addonName }, "扫描 addon views 目录失败");
|
|
299
425
|
}
|
|
300
426
|
}
|
|
301
427
|
|
|
302
428
|
// 2. 读取项目的 menus.json
|
|
303
|
-
const menusJsonPath = join(projectDir,
|
|
429
|
+
const menusJsonPath = join(projectDir, "menus.json");
|
|
304
430
|
if (existsSync(menusJsonPath)) {
|
|
305
431
|
try {
|
|
306
|
-
const content = await readFile(menusJsonPath,
|
|
432
|
+
const content = await readFile(menusJsonPath, "utf-8");
|
|
307
433
|
const projectMenus = JSON.parse(content);
|
|
308
434
|
if (Array.isArray(projectMenus) && projectMenus.length > 0) {
|
|
309
435
|
allMenus.push({
|
|
310
436
|
menus: projectMenus,
|
|
311
|
-
source:
|
|
437
|
+
source: "project"
|
|
312
438
|
});
|
|
313
439
|
}
|
|
314
440
|
} catch (error: any) {
|
|
315
|
-
Logger.warn({ err: error },
|
|
441
|
+
Logger.warn({ err: error }, "读取项目 menus.json 失败");
|
|
316
442
|
}
|
|
317
443
|
}
|
|
318
444
|
|
|
@@ -325,7 +451,7 @@ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; source: s
|
|
|
325
451
|
export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<void> {
|
|
326
452
|
try {
|
|
327
453
|
if (options.plan) {
|
|
328
|
-
Logger.debug(
|
|
454
|
+
Logger.debug("[计划] 同步菜单配置到数据库(plan 模式不执行)");
|
|
329
455
|
return;
|
|
330
456
|
}
|
|
331
457
|
|
|
@@ -335,11 +461,15 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
335
461
|
// 2. 合并菜单配置
|
|
336
462
|
let mergedMenus = mergeMenuConfigs(allMenus);
|
|
337
463
|
|
|
464
|
+
// 2.1 规范化并去重(防止尾随 / 或多 / 导致隐藏菜单与 DB 同步异常)
|
|
465
|
+
mergedMenus = normalizeMenuTree(mergedMenus);
|
|
466
|
+
|
|
338
467
|
// 3. 过滤隐藏菜单(根据 hiddenMenus 配置)
|
|
339
468
|
const hiddenMenus = (beflyConfig as any).hiddenMenus || [];
|
|
340
469
|
if (Array.isArray(hiddenMenus) && hiddenMenus.length > 0) {
|
|
341
|
-
const hiddenSet = new Set(hiddenMenus);
|
|
470
|
+
const hiddenSet = new Set(hiddenMenus.map((item: string) => normalizeMenuPath(item)));
|
|
342
471
|
mergedMenus = filterHiddenMenus(mergedMenus, hiddenSet);
|
|
472
|
+
mergedMenus = normalizeMenuTree(mergedMenus);
|
|
343
473
|
}
|
|
344
474
|
|
|
345
475
|
// 连接数据库
|
|
@@ -348,10 +478,10 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
348
478
|
const helper = new DbHelper({ redis: new RedisHelper() } as any, Connect.getSql());
|
|
349
479
|
|
|
350
480
|
// 3. 检查表是否存在
|
|
351
|
-
const exists = await helper.tableExists(
|
|
481
|
+
const exists = await helper.tableExists("addon_admin_menu");
|
|
352
482
|
|
|
353
483
|
if (!exists) {
|
|
354
|
-
Logger.debug(
|
|
484
|
+
Logger.debug("表 addon_admin_menu 不存在,跳过菜单同步");
|
|
355
485
|
return;
|
|
356
486
|
}
|
|
357
487
|
|
|
@@ -366,21 +496,26 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}): Promise<vo
|
|
|
366
496
|
|
|
367
497
|
// 7. 获取最终菜单数据(用于缓存)
|
|
368
498
|
const allMenusData = await helper.getAll({
|
|
369
|
-
table:
|
|
370
|
-
orderBy: ['sort#ASC', 'id#ASC']
|
|
499
|
+
table: "addon_admin_menu"
|
|
371
500
|
});
|
|
372
501
|
|
|
373
502
|
// 8. 缓存菜单数据到 Redis
|
|
374
503
|
try {
|
|
375
504
|
const redisHelper = new RedisHelper();
|
|
376
|
-
await redisHelper.setObject(
|
|
505
|
+
await redisHelper.setObject(CacheKeys.menusAll(), allMenusData.lists);
|
|
377
506
|
} catch (error: any) {
|
|
378
|
-
Logger.warn({ err: error },
|
|
507
|
+
Logger.warn({ err: error }, "Redis 缓存菜单数据失败");
|
|
379
508
|
}
|
|
380
509
|
} catch (error: any) {
|
|
381
|
-
Logger.error({ err: error },
|
|
510
|
+
Logger.error({ err: error }, "菜单同步失败");
|
|
382
511
|
throw error;
|
|
383
512
|
} finally {
|
|
384
513
|
await Connect.disconnect();
|
|
385
514
|
}
|
|
386
515
|
}
|
|
516
|
+
|
|
517
|
+
// 仅测试用(避免将内部扫描逻辑变成稳定 API)
|
|
518
|
+
export const __test__ = {
|
|
519
|
+
scanViewsDir: scanViewsDir,
|
|
520
|
+
normalizeMenuPath: normalizeMenuPath
|
|
521
|
+
};
|