befly 3.76.7 → 3.77.1
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/Befly.js +97 -34
- package/apis/admin/_meta.js +3 -0
- package/apis/admin/delete.js +1 -1
- package/apis/admin/detail.js +1 -0
- package/apis/admin/insert.js +1 -1
- package/apis/admin/update.js +1 -1
- package/apis/api/_meta.js +3 -0
- package/apis/auth/_meta.js +3 -0
- package/apis/auth/login.js +3 -2
- package/apis/dashboard/_meta.js +3 -0
- package/apis/dashboard/systemResources.js +12 -1
- package/apis/dict/_meta.js +3 -0
- package/apis/dict/detail.js +1 -0
- package/apis/dictType/_meta.js +3 -0
- package/apis/dictType/detail.js +1 -0
- package/apis/email/_meta.js +3 -0
- package/apis/email/config.js +1 -1
- package/apis/loginLog/_meta.js +3 -0
- package/apis/menu/_meta.js +3 -0
- package/apis/operateLog/_meta.js +3 -0
- package/apis/role/_meta.js +3 -0
- package/apis/role/apiSave.js +14 -1
- package/apis/role/detail.js +1 -0
- package/apis/role/menuSave.js +1 -1
- package/apis/source/_meta.js +3 -0
- package/apis/tongJi/_meta.js +3 -0
- package/apis/tongJi/_tongJi.js +16 -0
- package/apis/tongJi/dailyReport.js +5 -1
- package/apis/tongJi/dailyStatsDistribution.js +5 -4
- package/apis/tongJi/errorReport.js +8 -2
- package/apis/tongJi/todayOnline.js +2 -5
- package/apis/upload/_meta.js +3 -0
- package/checks/api.js +2 -6
- package/checks/field.js +30 -6
- package/checks/menu.js +2 -1
- package/checks/table.js +1 -1
- package/configs/beflyConfig.json +7 -3
- package/exports.js +1 -0
- package/hooks/auth.js +7 -1
- package/hooks/permission.js +22 -1
- package/hooks/rateLimit.js +36 -5
- package/hooks/validator.js +5 -16
- package/index.js +46 -38
- package/libs/cacheHelper.js +13 -18
- package/libs/logger/logger.js +103 -24
- package/libs/logger/sanitize.js +40 -15
- package/libs/memCache.js +53 -0
- package/libs/mysql/dbHelper.js +39 -71
- package/libs/mysql/dbParse.js +5 -1
- package/libs/mysql/sql/sqlBuilder.js +9 -0
- package/libs/redis/redis.js +44 -57
- package/libs/smtpText.js +41 -22
- package/libs/validator/compiler.js +3 -2
- package/libs/validator/parser.js +11 -5
- package/libs/validator/util.js +1 -5
- package/package.json +1 -1
- package/paths.js +23 -10
- package/router/static.js +13 -6
- package/schemas/api.json +10 -0
- package/schemas/config.json +38 -0
- package/sync/api.js +18 -7
- package/sync/dev.js +27 -3
- package/sync/menu.js +3 -3
- package/sync/syncUtil.js +12 -6
- package/tables/api.json +21 -0
- package/tables/emailLog.json +6 -2
- package/tables/menu.json +3 -0
- package/utils/is.js +7 -0
- package/utils/prettyError.js +102 -0
- package/utils/scanFiles.js +85 -46
- package/utils/scanSources.js +56 -4
- package/utils/util.js +5 -1
package/utils/scanFiles.js
CHANGED
|
@@ -12,6 +12,42 @@ const selectFields = {
|
|
|
12
12
|
state: { name: "状态", paramType: "integer" }
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 扫描接口目录下的 _meta.js 目录元数据(约定:_ 前缀文件不注册路由)。
|
|
17
|
+
* 返回 Map<目录 apiPath, 中文标题>,键与接口 apiPath 的目录部分对齐,供 scanSources 挂载 parentTitle。
|
|
18
|
+
*/
|
|
19
|
+
export async function scanApiMetaTitles(dir, source) {
|
|
20
|
+
const titles = new Map();
|
|
21
|
+
if (!existsSync(dir)) return titles;
|
|
22
|
+
|
|
23
|
+
const glob = new Bun.Glob("**/_meta.js");
|
|
24
|
+
const files = await Array.fromAsync(
|
|
25
|
+
glob.scan({
|
|
26
|
+
cwd: dir,
|
|
27
|
+
onlyFiles: true,
|
|
28
|
+
absolute: true,
|
|
29
|
+
followSymlinks: true
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const filePath = normalize(file);
|
|
35
|
+
const meta = await importDefault(filePath, {});
|
|
36
|
+
const record = isPlainObject(meta) ? meta : {};
|
|
37
|
+
const title = record.title;
|
|
38
|
+
if (typeof title !== "string" || title.trim().length === 0 || title.trim().length > 50) {
|
|
39
|
+
throw createError(`目录元数据无效:${filePath} 的 title 必须是 1-50 个字符`, { code: "policy", subsystem: "scan", operation: "scanApiMetaTitles" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parsedRelativePath = parse(relative(dir, filePath));
|
|
43
|
+
const relativeDir = parsedRelativePath.dir.replaceAll("\\", "/");
|
|
44
|
+
const dirApiPath = source === "core" ? `/api/core/${relativeDir}` : `/api/${relativeDir}`;
|
|
45
|
+
titles.set(dirApiPath, title.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return titles;
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
/**
|
|
16
52
|
* 扫描指定目录下的文件
|
|
17
53
|
* @param dir 目录路径
|
|
@@ -24,58 +60,61 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
24
60
|
const glob = new Bun.Glob(pattern);
|
|
25
61
|
if (!existsSync(dir)) return [];
|
|
26
62
|
|
|
27
|
-
const
|
|
63
|
+
const files = await Array.fromAsync(
|
|
64
|
+
glob.scan({
|
|
65
|
+
cwd: dir,
|
|
66
|
+
onlyFiles: true,
|
|
67
|
+
absolute: true,
|
|
68
|
+
followSymlinks: true
|
|
69
|
+
})
|
|
70
|
+
);
|
|
28
71
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
72
|
+
// 并行导入源文件,Promise.all 保持与扫描顺序一致的结果顺序
|
|
73
|
+
const results = await Promise.all(
|
|
74
|
+
files.map(async (file) => {
|
|
75
|
+
const filePath = normalize(file);
|
|
76
|
+
const parsedFile = parse(filePath);
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const parsedFile = parse(filePath);
|
|
78
|
+
// 获取文件名(去除扩展名)
|
|
79
|
+
const fileName = parsedFile.name;
|
|
39
80
|
|
|
40
|
-
|
|
41
|
-
|
|
81
|
+
// 计算相对路径(去除扩展名)
|
|
82
|
+
const parsedRelativePath = parse(relative(dir, filePath));
|
|
83
|
+
const relativePath = (parsedRelativePath.dir ? join(parsedRelativePath.dir, parsedRelativePath.name) : parsedRelativePath.name).replaceAll("\\", "/");
|
|
42
84
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
// 固定默认过滤(不可关闭):忽略下划线开头的文件/目录
|
|
86
|
+
if (relativePath.split("/").some((part) => part.startsWith("_"))) return null;
|
|
87
|
+
const content = await importDefault(filePath, {});
|
|
88
|
+
const contentObj = isPlainObject(content) ? content : {};
|
|
46
89
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fileName: fileName,
|
|
58
|
-
apiPath: source === "core" ? `/api/${source}/${relativePath}` : `/api/${relativePath}`
|
|
59
|
-
};
|
|
60
|
-
if (type === "table") {
|
|
61
|
-
base["fieldsDef"] = contentObj;
|
|
62
|
-
}
|
|
63
|
-
if (type === "api") {
|
|
64
|
-
base["name"] = "";
|
|
65
|
-
}
|
|
66
|
-
if (type !== "table") {
|
|
67
|
-
for (const [key, value] of Object.entries(contentObj)) {
|
|
68
|
-
if (base[key]) continue;
|
|
69
|
-
base[key] = value;
|
|
90
|
+
const base = {
|
|
91
|
+
source: source,
|
|
92
|
+
type: type,
|
|
93
|
+
filePath: filePath,
|
|
94
|
+
relativePath: relativePath,
|
|
95
|
+
fileName: fileName,
|
|
96
|
+
apiPath: source === "core" ? `/api/${source}/${relativePath}` : `/api/${relativePath}`
|
|
97
|
+
};
|
|
98
|
+
if (type === "table") {
|
|
99
|
+
base["fieldsDef"] = contentObj;
|
|
70
100
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
if (type === "api") {
|
|
102
|
+
base["name"] = "";
|
|
103
|
+
}
|
|
104
|
+
if (type !== "table") {
|
|
105
|
+
for (const [key, value] of Object.entries(contentObj)) {
|
|
106
|
+
if (base[key]) continue;
|
|
107
|
+
base[key] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (type === "api") {
|
|
111
|
+
base.category ||= "other";
|
|
112
|
+
if (base.category === "select") base.fields = { ...selectFields, ...base.fields };
|
|
113
|
+
}
|
|
114
|
+
return base;
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
return results.filter((item) => item !== null);
|
|
79
118
|
} catch (error) {
|
|
80
119
|
throw createError(`扫描失败: source=${source} type=${type} dir=${dir} pattern=${pattern}`, { cause: error, code: "runtime" });
|
|
81
120
|
}
|
package/utils/scanSources.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../libs/logger/index.js";
|
|
1
4
|
import { buildTableModel } from "../libs/tableModel.js";
|
|
2
5
|
import {
|
|
3
6
|
//
|
|
@@ -13,7 +16,42 @@ import {
|
|
|
13
16
|
appCornDir
|
|
14
17
|
} from "../paths.js";
|
|
15
18
|
import { createError } from "./error.js";
|
|
16
|
-
import { scanFiles } from "./scanFiles.js";
|
|
19
|
+
import { scanApiMetaTitles, scanFiles } from "./scanFiles.js";
|
|
20
|
+
|
|
21
|
+
// 逐级拼接目录显示标题:每级目录有 _meta.js 标题用标题,没有用目录名,以 / 连接
|
|
22
|
+
// 例:/api/admin/product/insert + 标题映射 -> "后台管理/product"
|
|
23
|
+
export function composeTitlePath(apiPath, metaTitles, missingCollector) {
|
|
24
|
+
const segments = apiPath.split("/").filter(Boolean);
|
|
25
|
+
const start = segments[1] === "core" ? 2 : 1;
|
|
26
|
+
const dirs = segments.slice(start, -1);
|
|
27
|
+
if (dirs.length === 0) return "";
|
|
28
|
+
|
|
29
|
+
const parts = [];
|
|
30
|
+
let current = `/${segments.slice(0, start).join("/")}`;
|
|
31
|
+
for (const dir of dirs) {
|
|
32
|
+
current = `${current}/${dir}`;
|
|
33
|
+
const title = metaTitles.get(current);
|
|
34
|
+
if (title) {
|
|
35
|
+
parts.push(title);
|
|
36
|
+
} else {
|
|
37
|
+
parts.push(dir);
|
|
38
|
+
if (missingCollector) missingCollector.add(current);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return parts.join("/");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function warnWhenAppSourcesMissing() {
|
|
45
|
+
// 全部 app 源码目录都不存在时显式告警,避免"项目源码静默消失、以纯 core 形态运行"的配置事故
|
|
46
|
+
const appDirs = [appApiDir, appTableDir, appPluginDir, appHookDir, appCornDir];
|
|
47
|
+
if (appDirs.every((dir) => !existsSync(dir))) {
|
|
48
|
+
Logger.warn("项目源码目录(apis/tables/plugins/hooks/corns)均不存在,请检查启动目录是否为项目根目录", {
|
|
49
|
+
subsystem: "scan",
|
|
50
|
+
operation: "scanSources",
|
|
51
|
+
appDir: appDirs[0]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
17
55
|
|
|
18
56
|
export function assertUnique(items, key, label) {
|
|
19
57
|
const values = new Map();
|
|
@@ -72,6 +110,8 @@ export const scanSources = async ({ beflyMode = "auto" } = {}) => {
|
|
|
72
110
|
const tables = await scanTables({ beflyMode: beflyMode });
|
|
73
111
|
const corns = [];
|
|
74
112
|
|
|
113
|
+
warnWhenAppSourcesMissing();
|
|
114
|
+
|
|
75
115
|
// 处理插件
|
|
76
116
|
const allCorePlugins = await scanFiles(corePluginDir, "core", "plugin", "*.js");
|
|
77
117
|
for (const item of allCorePlugins) {
|
|
@@ -102,17 +142,29 @@ export const scanSources = async ({ beflyMode = "auto" } = {}) => {
|
|
|
102
142
|
corns.push(item);
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
//
|
|
106
|
-
|
|
145
|
+
// 处理接口(读取目录 _meta.js 标题挂载 parentTitle);
|
|
146
|
+
// 每一级接口目录都必须有中文标题,缺失即启动失败并列出清单
|
|
147
|
+
const [allCoreApis, coreMetaTitles, allAppApis, appMetaTitles] = await Promise.all([scanFiles(coreApiDir, "core", "api", "**/*.js"), scanApiMetaTitles(coreApiDir, "core"), scanFiles(appApiDir, "app", "api", "**/*.js"), scanApiMetaTitles(appApiDir, "app")]);
|
|
148
|
+
|
|
149
|
+
const missingTitleDirs = new Set();
|
|
107
150
|
for (const item of allCoreApis) {
|
|
151
|
+
item.parentTitle = composeTitlePath(item.apiPath, coreMetaTitles, missingTitleDirs);
|
|
108
152
|
apis.push(item);
|
|
109
153
|
}
|
|
110
|
-
const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
|
|
111
154
|
for (const item of allAppApis) {
|
|
112
155
|
assertAppApiNamespace(item);
|
|
156
|
+
item.parentTitle = composeTitlePath(item.apiPath, appMetaTitles, missingTitleDirs);
|
|
113
157
|
apis.push(item);
|
|
114
158
|
}
|
|
115
159
|
|
|
160
|
+
if (missingTitleDirs.size > 0) {
|
|
161
|
+
throw createError(`以下接口目录缺少 _meta.js 中文标题:${Array.from(missingTitleDirs).toSorted().join("、")}`, {
|
|
162
|
+
code: "policy",
|
|
163
|
+
subsystem: "scan",
|
|
164
|
+
operation: "scanSources"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
116
168
|
assertUnique(plugins, "fileName", "Plugin 文件名");
|
|
117
169
|
assertUnique(hooks, "fileName", "Hook 文件名");
|
|
118
170
|
assertUnique(corns, "fileName", "Cron 文件名");
|
package/utils/util.js
CHANGED
|
@@ -37,7 +37,11 @@ export function getRunMode() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function toSessionTtlSeconds(ttlDays) {
|
|
40
|
-
|
|
40
|
+
const parsed = typeof ttlDays === "number" ? ttlDays : Number(String(ttlDays ?? "").trim());
|
|
41
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
42
|
+
return Math.floor(parsed * 24 * 60 * 60);
|
|
43
|
+
}
|
|
44
|
+
return 7 * 24 * 60 * 60;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
export function genShortId() {
|