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/Befly.js
CHANGED
|
@@ -13,6 +13,7 @@ import { syncMenu } from "./sync/menu.js";
|
|
|
13
13
|
import { calcPerfTime } from "./utils/calcPerfTime.js";
|
|
14
14
|
import { createError } from "./utils/error.js";
|
|
15
15
|
import { isPrimaryProcess } from "./utils/is.js";
|
|
16
|
+
import { printErrorSummary } from "./utils/prettyError.js";
|
|
16
17
|
import { waitFor } from "./utils/util.js";
|
|
17
18
|
|
|
18
19
|
const SYNC_READY_KEY = "befly:syncReady";
|
|
@@ -22,10 +23,9 @@ async function assertRuntimeReady(context) {
|
|
|
22
23
|
if (!context[key]) throw createError(`启动失败:ctx.${key} 未初始化`, { code: "runtime", subsystem: "start", operation: "runtimeReady" });
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
26
|
+
const coreTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
|
|
27
|
+
const tableResults = await Promise.all(coreTables.map((table) => context.mysql.tableExists(table)));
|
|
28
|
+
const missingTables = coreTables.filter((table, index) => !tableResults[index].data);
|
|
29
29
|
if (missingTables.length > 0) throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, { code: "runtime", subsystem: "start", operation: "tableExists", missingTables: missingTables });
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -41,9 +41,12 @@ export class Befly {
|
|
|
41
41
|
this.apis = init.apis || [];
|
|
42
42
|
this.hooks = init.hooks || [];
|
|
43
43
|
this.plugins = init.plugins || [];
|
|
44
|
+
this.activePlugins = [];
|
|
44
45
|
this.corns = init.corns || [];
|
|
45
46
|
this.server = null;
|
|
46
47
|
this.started = false;
|
|
48
|
+
this.stopping = null;
|
|
49
|
+
this.signalHandlers = null;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
async start() {
|
|
@@ -55,6 +58,7 @@ export class Befly {
|
|
|
55
58
|
for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
|
|
56
59
|
const pluginStartTime = Bun.nanoseconds();
|
|
57
60
|
this.context[item.fileName] = await item.handler(this.context);
|
|
61
|
+
this.activePlugins.push(item);
|
|
58
62
|
Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
|
|
59
63
|
}
|
|
60
64
|
Logger.info(`启动 插件初始化完成,耗时 ${calcPerfTime(pluginsStartTime)}`);
|
|
@@ -64,21 +68,16 @@ export class Befly {
|
|
|
64
68
|
Logger.info(`启动 运行时依赖检查完成,耗时 ${calcPerfTime(runtimeReadyStartTime)}`);
|
|
65
69
|
|
|
66
70
|
if (isPrimaryProcess()) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await syncMenu(this.context, this.menus);
|
|
73
|
-
Logger.info(`启动 菜单同步完成,耗时 ${calcPerfTime(syncMenuStartTime)}`);
|
|
74
|
-
|
|
75
|
-
const syncDevStartTime = Bun.nanoseconds();
|
|
71
|
+
const syncStartTime = Bun.nanoseconds();
|
|
72
|
+
const syncApiStart = Bun.nanoseconds();
|
|
73
|
+
const syncMenuStart = Bun.nanoseconds();
|
|
74
|
+
const syncDevStart = Bun.nanoseconds();
|
|
75
|
+
// api/menu 互不依赖可并行;syncDev 读取 api/menu 落库结果、syncCache 读取 syncDev 写入的角色表,需按序串行
|
|
76
|
+
await Promise.all([syncApi(this.context, this.apis).then(() => Logger.info(`启动 API 同步完成,耗时 ${calcPerfTime(syncApiStart)}`)), syncMenu(this.context, this.menus).then(() => Logger.info(`启动 菜单同步完成,耗时 ${calcPerfTime(syncMenuStart)}`))]);
|
|
76
77
|
await syncDev(this.context);
|
|
77
|
-
Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(
|
|
78
|
-
|
|
79
|
-
const syncCacheStartTime = Bun.nanoseconds();
|
|
78
|
+
Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(syncDevStart)}`);
|
|
80
79
|
await syncCache(this.context);
|
|
81
|
-
Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(
|
|
80
|
+
Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(syncStartTime)}`);
|
|
82
81
|
|
|
83
82
|
const syncReadyStartTime = Bun.nanoseconds();
|
|
84
83
|
await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
|
|
@@ -88,7 +87,11 @@ export class Befly {
|
|
|
88
87
|
const startAt = Date.now();
|
|
89
88
|
await waitFor({
|
|
90
89
|
label: "主进程同步完成",
|
|
91
|
-
|
|
90
|
+
// 主进程先于本进程完成同步时标记时间早于 startAt,60 秒内的近期标记视为就绪
|
|
91
|
+
check: async () => {
|
|
92
|
+
const marker = Number(await this.context.redis.getString(SYNC_READY_KEY));
|
|
93
|
+
return marker > startAt || (marker > 0 && startAt - marker < 60_000);
|
|
94
|
+
}
|
|
92
95
|
});
|
|
93
96
|
Logger.info(`启动 等待主进程同步完成,耗时 ${calcPerfTime(syncWaitStartTime)}`);
|
|
94
97
|
}
|
|
@@ -136,31 +139,91 @@ export class Befly {
|
|
|
136
139
|
Logger.info(`启动总耗时 ${startupTime}`);
|
|
137
140
|
Logger.info(`${this.context.config.appName} 启动成功`, { url: this.server.url, startupTime: startupTime });
|
|
138
141
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
this.signalHandlers = {
|
|
143
|
+
SIGINT: async () => {
|
|
144
|
+
let exitCode = 130;
|
|
145
|
+
try {
|
|
146
|
+
await this.stop();
|
|
147
|
+
} catch {
|
|
148
|
+
exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
process.exit(exitCode);
|
|
151
|
+
},
|
|
152
|
+
SIGTERM: async () => {
|
|
153
|
+
let exitCode = 143;
|
|
154
|
+
try {
|
|
155
|
+
await this.stop();
|
|
156
|
+
} catch {
|
|
157
|
+
exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
process.exit(exitCode);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
process.once("SIGINT", this.signalHandlers.SIGINT);
|
|
163
|
+
process.once("SIGTERM", this.signalHandlers.SIGTERM);
|
|
144
164
|
return this.server;
|
|
145
165
|
} catch (error) {
|
|
166
|
+
printErrorSummary(error, { title: "项目启动失败" });
|
|
146
167
|
Logger.error("项目启动失败", error);
|
|
147
|
-
|
|
168
|
+
try {
|
|
169
|
+
await this.stop();
|
|
170
|
+
} catch {}
|
|
148
171
|
throw createError("运行时错误", { cause: error, code: error?.code || "runtime", subsystem: "start", operation: "start" });
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
async stop() {
|
|
153
|
-
this.
|
|
154
|
-
this.server = null;
|
|
155
|
-
this.context.cron?.stopAll();
|
|
176
|
+
if (this.stopping) return this.stopping;
|
|
156
177
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (resource?.close) await resource.close();
|
|
160
|
-
else if (resource?.stop) await resource.stop();
|
|
161
|
-
}
|
|
178
|
+
const stopping = (async () => {
|
|
179
|
+
let stopError;
|
|
162
180
|
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
if (this.signalHandlers) {
|
|
182
|
+
process.off("SIGINT", this.signalHandlers.SIGINT);
|
|
183
|
+
process.off("SIGTERM", this.signalHandlers.SIGTERM);
|
|
184
|
+
this.signalHandlers = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
this.server?.stop();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
stopError = error;
|
|
191
|
+
Logger.error("停止 HTTP 服务失败", error);
|
|
192
|
+
}
|
|
193
|
+
this.server = null;
|
|
194
|
+
|
|
195
|
+
const cron = this.context.cron;
|
|
196
|
+
this.context.cron = null;
|
|
197
|
+
try {
|
|
198
|
+
cron?.stopAll();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
stopError ||= error;
|
|
201
|
+
Logger.error("停止定时器失败", error);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const item of this.activePlugins.toReversed()) {
|
|
205
|
+
const resource = this.context[item.fileName];
|
|
206
|
+
try {
|
|
207
|
+
if (resource?.close) await resource.close();
|
|
208
|
+
else if (resource?.stop) await resource.stop();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
stopError ||= error;
|
|
211
|
+
Logger.error(`关闭插件 ${item.fileName} 失败`, error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.activePlugins = [];
|
|
215
|
+
|
|
216
|
+
this.started = false;
|
|
217
|
+
await Logger.shutdown();
|
|
218
|
+
|
|
219
|
+
if (stopError) throw createError("项目停止失败", { cause: stopError, code: "runtime", subsystem: "stop", operation: "stop" });
|
|
220
|
+
})();
|
|
221
|
+
this.stopping = stopping;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return await stopping;
|
|
225
|
+
} finally {
|
|
226
|
+
if (this.stopping === stopping) this.stopping = null;
|
|
227
|
+
}
|
|
165
228
|
}
|
|
166
229
|
}
|
package/apis/admin/delete.js
CHANGED
package/apis/admin/detail.js
CHANGED
package/apis/admin/insert.js
CHANGED
package/apis/admin/update.js
CHANGED
package/apis/auth/login.js
CHANGED
|
@@ -105,8 +105,9 @@ export default {
|
|
|
105
105
|
loginAt: Date.now()
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
try {
|
|
109
|
+
await befly.redis.setObject(sessionKey, sessionData, ttlSeconds);
|
|
110
|
+
} catch {
|
|
110
111
|
return befly.tool.No("登录失败,请稍后重试");
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
|
|
5
|
+
import { memRemember } from "#befly/libs/memCache.js";
|
|
6
|
+
|
|
5
7
|
export default {
|
|
6
8
|
name: "获取系统资源",
|
|
7
9
|
method: "POST",
|
|
@@ -9,7 +11,16 @@ export default {
|
|
|
9
11
|
auth: true,
|
|
10
12
|
fields: {},
|
|
11
13
|
required: [],
|
|
14
|
+
// CPU 采样含 100ms 等待,快照缓存 3 秒避免高并发下请求积压
|
|
12
15
|
handler: async (befly) => {
|
|
16
|
+
return await memRemember("dashboard:systemResources", 3000, async () => {
|
|
17
|
+
return await collectSystemResources(befly);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function collectSystemResources(befly) {
|
|
23
|
+
{
|
|
13
24
|
const cpus = os.cpus();
|
|
14
25
|
const cpuCount = cpus.length;
|
|
15
26
|
|
|
@@ -105,4 +116,4 @@ export default {
|
|
|
105
116
|
}
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
|
-
}
|
|
119
|
+
}
|
package/apis/dict/detail.js
CHANGED
package/apis/dictType/detail.js
CHANGED
package/apis/email/config.js
CHANGED
package/apis/role/apiSave.js
CHANGED
|
@@ -6,7 +6,7 @@ export default {
|
|
|
6
6
|
name: "保存角色接口权限",
|
|
7
7
|
method: "POST",
|
|
8
8
|
body: "none",
|
|
9
|
-
auth:
|
|
9
|
+
auth: "dev",
|
|
10
10
|
fields: {
|
|
11
11
|
roleCode: roleTable.code,
|
|
12
12
|
apiPaths: roleTable.apis
|
|
@@ -21,6 +21,19 @@ export default {
|
|
|
21
21
|
return befly.tool.No("角色不存在");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// 超级管理员专属接口是天生权限,不允许分配给任何角色(前端授权 UI 也应过滤 auth=dev)
|
|
25
|
+
if (apiPaths.length > 0) {
|
|
26
|
+
const checkResult = await befly.mysql.getAll({
|
|
27
|
+
table: "beflyApi",
|
|
28
|
+
fields: ["path", "auth"],
|
|
29
|
+
where: { path$in: apiPaths }
|
|
30
|
+
});
|
|
31
|
+
const devPaths = checkResult.data.lists.filter((row) => row.auth === "dev").map((row) => row.path);
|
|
32
|
+
if (devPaths.length > 0) {
|
|
33
|
+
return befly.tool.No(`超级管理员专属接口不允许分配:${devPaths.join("、")}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
await befly.mysql.updData({
|
|
25
38
|
table: "beflyRole",
|
|
26
39
|
where: { code: ctx.body.roleCode },
|
package/apis/role/detail.js
CHANGED
package/apis/role/menuSave.js
CHANGED
package/apis/tongJi/_tongJi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memRemember } from "#befly/libs/memCache.js";
|
|
1
2
|
import { DAY_MS } from "#befly/utils/datetime.js";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
@@ -118,3 +119,18 @@ export function getTongJiDateRangeList(startDateYmd, endDateYmd, timeZone = DEFA
|
|
|
118
119
|
|
|
119
120
|
return list;
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
// 上报接口的 productCode 必须是已注册项目(空值视为未分组上报,放行);结果进程内缓存 60 秒
|
|
124
|
+
export async function isRegisteredProductCode(befly, productCode) {
|
|
125
|
+
if (!productCode) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return await memRemember(`tongJi:project:${productCode}`, 60000, async () => {
|
|
129
|
+
const result = await befly.mysql.getOne({
|
|
130
|
+
table: "beflyProject",
|
|
131
|
+
fields: ["id"],
|
|
132
|
+
where: { code: productCode }
|
|
133
|
+
});
|
|
134
|
+
return Boolean(result.data?.id);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
2
2
|
|
|
3
|
-
import { getTongJiDateYmdNumber } from "./_tongJi.js";
|
|
3
|
+
import { getTongJiDateYmdNumber, isRegisteredProductCode } from "./_tongJi.js";
|
|
4
4
|
|
|
5
5
|
const DAILY_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
|
|
6
6
|
|
|
@@ -29,6 +29,10 @@ export default {
|
|
|
29
29
|
|
|
30
30
|
const productCode = ctx.body?.productCode || "";
|
|
31
31
|
|
|
32
|
+
if (!(await isRegisteredProductCode(befly, productCode))) {
|
|
33
|
+
return befly.tool.No("产品代号未注册");
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
let memberMeta;
|
|
33
37
|
|
|
34
38
|
if (isValidPositiveInt(ctx?.userId)) {
|
|
@@ -75,11 +75,12 @@ export default {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const [startDate, endDate] = getDateRangeBounds(dateRange, Date.now(), befly.config?.tz);
|
|
78
|
+
const dimensionList = Array.from(DISTRIBUTION_DIMENSIONS);
|
|
79
|
+
const rows = await Promise.all(dimensionList.map((dimension) => queryDimensionDistribution(befly, dimension, startDate, endDate, productCode)));
|
|
78
80
|
const result = {};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
81
|
+
dimensionList.forEach((dimension, index) => {
|
|
82
|
+
result[dimension] = rows[index];
|
|
83
|
+
});
|
|
83
84
|
|
|
84
85
|
return befly.tool.Yes("获取成功", result);
|
|
85
86
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
2
|
-
import {
|
|
2
|
+
import { getTimeBucketStart } from "#befly/utils/datetime.js";
|
|
3
3
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
4
4
|
|
|
5
|
+
import { getTongJiDateYmdNumber, isRegisteredProductCode } from "./_tongJi.js";
|
|
6
|
+
|
|
5
7
|
const ERROR_STATS_BUCKET_MS = 30 * 60 * 1000;
|
|
6
8
|
const ERROR_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
|
|
7
9
|
|
|
@@ -44,10 +46,14 @@ export default {
|
|
|
44
46
|
handler: async (befly, ctx) => {
|
|
45
47
|
const now = Date.now();
|
|
46
48
|
const bucketTime = getTimeBucketStart(now, ERROR_STATS_BUCKET_MS);
|
|
47
|
-
const bucketDate =
|
|
49
|
+
const bucketDate = getTongJiDateYmdNumber(now, befly.config?.tz);
|
|
48
50
|
|
|
49
51
|
const body = ctx.body || {};
|
|
50
52
|
const productCode = body.productCode || "";
|
|
53
|
+
|
|
54
|
+
if (!(await isRegisteredProductCode(befly, productCode))) {
|
|
55
|
+
return befly.tool.No("产品代号未注册");
|
|
56
|
+
}
|
|
51
57
|
const rawMessage = body.message || "";
|
|
52
58
|
const extractedMessage = extractErrorMessage(rawMessage);
|
|
53
59
|
|
|
@@ -32,11 +32,8 @@ export default {
|
|
|
32
32
|
handler: async (befly, _ctx) => {
|
|
33
33
|
const reportDate = getTongJiDateYmdNumber(Date.now(), befly.config?.tz);
|
|
34
34
|
const projects = await loadProjects(befly);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const item of projects) {
|
|
38
|
-
total += await getTodayCount(befly, reportDate, item.code);
|
|
39
|
-
}
|
|
35
|
+
const counts = await Promise.all(projects.map((item) => getTodayCount(befly, reportDate, item.code)));
|
|
36
|
+
const total = counts.reduce((sum, count) => sum + count, 0);
|
|
40
37
|
|
|
41
38
|
return befly.tool.Yes("获取成功", {
|
|
42
39
|
today: {
|
package/checks/api.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Logger } from "#befly/libs/logger/index.js";
|
|
|
2
2
|
import { compile } from "#befly/libs/validator/index.js";
|
|
3
3
|
|
|
4
4
|
import apiSchema from "../schemas/api.json";
|
|
5
|
-
import { getFieldSchema } from "./field.js";
|
|
5
|
+
import { buildApiSchema, getFieldSchema } from "./field.js";
|
|
6
6
|
import { formatIssues } from "./validation.js";
|
|
7
7
|
|
|
8
8
|
const validate = compile(apiSchema);
|
|
@@ -23,11 +23,7 @@ function checkApiRules(issues, apis) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
compile(
|
|
27
|
-
paramType: "object",
|
|
28
|
-
fields: Object.fromEntries(Object.entries(fields).map(([field, definition]) => [field, { ...getFieldSchema(definition), optional: !(api.required || []).includes(field) }])),
|
|
29
|
-
constraints: api.constraints
|
|
30
|
-
});
|
|
26
|
+
compile(buildApiSchema(fields, api.required, api.constraints));
|
|
31
27
|
} catch (error) {
|
|
32
28
|
issues.push({ path: [index, "constraints"], code: "rule", message: error.message });
|
|
33
29
|
}
|
package/checks/field.js
CHANGED
|
@@ -9,10 +9,34 @@ export function getFieldSchema(field) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function getFieldErrors(issues, fields) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const errors = {};
|
|
13
|
+
for (const issue of issues) {
|
|
14
|
+
const field = issue.path[0];
|
|
15
|
+
// 同字段保留首条错误,避免后者覆盖更根本的原因
|
|
16
|
+
if (errors[field] === undefined) {
|
|
17
|
+
errors[field] = `${fields[field]?.name || field}${issue.message}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return errors;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 构造 API 请求体验证 schema:fields 剥离领域元数据、required 映射 optional、附加 constraints。
|
|
25
|
+
* 启动检查(checkApi)与运行时验证 hook 共用此构造,保证两处行为完全一致。
|
|
26
|
+
*/
|
|
27
|
+
export function buildApiSchema(fields, required, constraints) {
|
|
28
|
+
const requiredSet = new Set(required || []);
|
|
29
|
+
return {
|
|
30
|
+
paramType: "object",
|
|
31
|
+
fields: Object.fromEntries(
|
|
32
|
+
Object.entries(fields || {}).map(([field, definition]) => [
|
|
33
|
+
field,
|
|
34
|
+
{
|
|
35
|
+
...getFieldSchema(definition),
|
|
36
|
+
optional: !requiredSet.has(field)
|
|
37
|
+
}
|
|
38
|
+
])
|
|
39
|
+
),
|
|
40
|
+
constraints: constraints
|
|
41
|
+
};
|
|
18
42
|
}
|
package/checks/menu.js
CHANGED
|
@@ -6,7 +6,8 @@ import { formatIssues } from "./validation.js";
|
|
|
6
6
|
|
|
7
7
|
const validate = compile(menuSchema);
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// 路径正则以 schemas/menu.json 的 children.path pattern 为唯一来源,避免双份维护漂移
|
|
10
|
+
const menuPathRegex = new RegExp(menuSchema.items.fields.path.pattern);
|
|
10
11
|
|
|
11
12
|
function checkMenuPaths(issues, menus) {
|
|
12
13
|
const paths = new Map();
|
package/checks/table.js
CHANGED
|
@@ -26,7 +26,7 @@ function checkFieldDefinition(issues, tableName, fieldName, field) {
|
|
|
26
26
|
if (typeof field.name !== "string" || !field.name || field.name !== field.name.trim() || field.name.includes("\n") || field.name.length > 20) addIssue(issues, [...path, "name"], "必须是 1..20 字符的简短名称,不允许首尾空白或换行");
|
|
27
27
|
if (field.detail !== undefined && (typeof field.detail !== "string" || !field.detail || field.detail !== field.detail.trim())) addIssue(issues, [...path, "detail"], "必须是非空无首尾空白字符串");
|
|
28
28
|
if (!fieldParamTypes[field.fieldType]) addIssue(issues, [...path, "fieldType"], "必须是 integer、number、varchar、text");
|
|
29
|
-
if (typeof field.fieldNullable !== "boolean") addIssue(issues, [...path, "fieldNullable"], "
|
|
29
|
+
if (typeof field.fieldNullable !== "boolean") addIssue(issues, [...path, "fieldNullable"], "必须是布尔值");
|
|
30
30
|
|
|
31
31
|
let parseField;
|
|
32
32
|
try {
|
package/configs/beflyConfig.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"appPort": 3000,
|
|
4
4
|
"appHost": "127.0.0.1",
|
|
5
5
|
"devEmail": "dev@qq.com",
|
|
6
|
-
"devPassword": "
|
|
6
|
+
"devPassword": "",
|
|
7
7
|
"trustProxy": false,
|
|
8
8
|
"bodyLimit": 1048576,
|
|
9
9
|
"upload": {
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"excludeApisLog": ["/api/core/tongJi/*Report"],
|
|
20
20
|
"logger": {
|
|
21
21
|
"debug": true,
|
|
22
|
-
"excludeFields": ["password", "token", "secret"]
|
|
22
|
+
"excludeFields": ["password", "token", "secret"],
|
|
23
|
+
"truncateStringLength": 512,
|
|
24
|
+
"truncateArrayLength": 20,
|
|
25
|
+
"truncatePreviewLength": 200
|
|
23
26
|
},
|
|
24
27
|
"mysql": {
|
|
25
28
|
"hostname": "127.0.0.1",
|
|
@@ -62,6 +65,7 @@
|
|
|
62
65
|
"defaultWindow": 60,
|
|
63
66
|
"key": "ip",
|
|
64
67
|
"skipRoutes": [],
|
|
65
|
-
"failOpen": true
|
|
68
|
+
"failOpen": true,
|
|
69
|
+
"rules": [{ "path": "/api/core/tongJi/*Report", "limit": 120, "window": 60 }]
|
|
66
70
|
}
|
|
67
71
|
}
|
package/exports.js
CHANGED