befly 3.76.0 → 3.76.2
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 +159 -0
- package/README.md +50 -11
- package/apis/admin/delete.js +1 -3
- package/apis/admin/insert.js +2 -1
- package/apis/admin/select.js +2 -7
- package/apis/admin/update.js +3 -8
- package/apis/api/select.js +2 -7
- package/apis/auth/login.js +16 -20
- package/apis/dict/delete.js +1 -1
- package/apis/dict/detail.js +1 -3
- package/apis/dict/select.js +1 -2
- package/apis/dict/update.js +1 -1
- package/apis/dictType/delete.js +1 -1
- package/apis/dictType/detail.js +1 -1
- package/apis/dictType/insert.js +1 -1
- package/apis/dictType/select.js +2 -7
- package/apis/dictType/update.js +2 -2
- package/apis/email/config.js +4 -7
- package/apis/email/logList.js +2 -7
- package/apis/email/send.js +1 -2
- package/apis/email/verify.js +1 -3
- package/apis/loginLog/select.js +2 -7
- package/apis/menu/select.js +2 -7
- package/apis/operateLog/select.js +2 -7
- package/apis/role/apiSave.js +2 -2
- package/apis/role/menuSave.js +2 -2
- package/apis/role/select.js +2 -7
- package/apis/source/imageList.js +2 -5
- package/apis/tongJi/dailyReport.js +12 -13
- package/apis/tongJi/dailyReportSelect.js +1 -2
- package/apis/tongJi/dailyStats.js +1 -1
- package/apis/tongJi/dailyStatsDistribution.js +2 -2
- package/apis/tongJi/errorList.js +1 -2
- package/apis/tongJi/errorReport.js +1 -2
- package/apis/tongJi/projectDelete.js +1 -3
- package/apis/tongJi/projectSelect.js +2 -4
- package/apis/tongJi/projectUpdate.js +2 -2
- package/apis/upload/file.js +7 -5
- package/checks/api.js +43 -66
- package/checks/config.js +17 -12
- package/checks/corns.js +28 -46
- package/checks/field.js +18 -0
- package/checks/items.js +16 -60
- package/checks/menu.js +28 -39
- package/checks/table.js +106 -26
- package/checks/validation.js +13 -0
- package/configs/beflyConfig.json +6 -6
- package/configs/constConfig.js +2 -63
- package/exports.js +2 -0
- package/hooks/auth.js +11 -7
- package/hooks/parser.js +55 -59
- package/hooks/permission.js +5 -2
- package/hooks/rateLimit.js +25 -62
- package/hooks/validator.js +43 -31
- package/index.js +43 -344
- package/{lib → libs}/cacheHelper.js +3 -8
- package/libs/cron.js +60 -0
- package/libs/logger/index.js +1 -0
- package/libs/logger/logger.js +245 -0
- package/libs/logger/sanitize.js +115 -0
- package/{lib → libs/mysql}/dbHelper.js +100 -73
- package/{lib → libs/mysql}/dbParse.js +16 -9
- package/{lib → libs/mysql}/dbUtil.js +15 -32
- package/libs/mysql/error.js +29 -0
- package/libs/mysql/index.js +6 -0
- package/libs/mysql/is.js +29 -0
- package/libs/mysql/mysql.js +43 -0
- package/libs/mysql/sql/identifier.js +85 -0
- package/libs/mysql/sql/index.js +2 -0
- package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
- package/libs/mysql/util.js +44 -0
- package/libs/redis/index.js +2 -0
- package/libs/redis/redis.js +204 -0
- package/libs/redis/redisError.js +8 -0
- package/libs/tableModel.js +72 -0
- package/libs/tool.js +33 -0
- package/libs/validator/compiler.js +248 -0
- package/libs/validator/index.js +10 -0
- package/libs/validator/parser.js +173 -0
- package/libs/validator/util.js +5 -0
- package/package.json +10 -8
- package/paths.js +2 -3
- package/plugins/cache.js +2 -1
- package/plugins/email.js +4 -4
- package/plugins/mysql.js +5 -15
- package/plugins/redis.js +4 -12
- package/router/api.js +14 -10
- package/router/root.js +14 -0
- package/router/static.js +3 -3
- package/schemas/api.json +99 -0
- package/schemas/config.json +255 -0
- package/schemas/corn.json +39 -0
- package/schemas/item.json +43 -0
- package/schemas/menu.json +44 -0
- package/schemas/table.json +80 -0
- package/scripts/syncDb/apply.js +87 -0
- package/scripts/syncDb/ddl.js +43 -0
- package/scripts/syncDb/index.js +175 -30
- package/scripts/syncDb/model.js +60 -0
- package/scripts/syncDb/plan.js +160 -0
- package/scripts/syncDb/preflight.js +98 -0
- package/scripts/syncDb/report.js +177 -162
- package/scripts/syncDb/snapshot.js +118 -0
- package/sync/syncUtil.js +7 -13
- package/tables/admin.json +51 -53
- package/tables/api.json +29 -40
- package/tables/dailyReport.json +85 -73
- package/tables/dict.json +27 -39
- package/tables/dictType.json +20 -34
- package/tables/emailLog.json +49 -47
- package/tables/errorReport.json +86 -60
- package/tables/file.json +47 -50
- package/tables/loginLog.json +68 -55
- package/tables/menu.json +20 -34
- package/tables/operateLog.json +53 -47
- package/tables/project.json +18 -30
- package/tables/role.json +33 -37
- package/utils/cors.js +37 -41
- package/utils/deepMerge.js +3 -1
- package/utils/error.js +1 -56
- package/utils/id.js +44 -0
- package/utils/is.js +6 -243
- package/utils/response.js +12 -1
- package/utils/scanFiles.js +13 -4
- package/utils/scanSources.js +58 -20
- package/utils/util.js +11 -163
- package/apis/_apis.js +0 -20
- package/apis/auth/sendSmsCode.js +0 -24
- package/apis/role/delete.js +0 -41
- package/apis/role/insert.js +0 -45
- package/apis/role/save.js +0 -36
- package/apis/role/update.js +0 -66
- package/configs/regexpAlias.json +0 -55
- package/lib/connect.js +0 -119
- package/lib/logger.js +0 -430
- package/lib/redisHelper.js +0 -399
- package/lib/validator.js +0 -274
- package/lib/xmlParse.js +0 -149
- package/plugins/config.js +0 -13
- package/plugins/cron.js +0 -115
- package/plugins/logger.js +0 -17
- package/plugins/tool.js +0 -131
- package/plugins/xmlParse.js +0 -10
- package/schemas/config.js +0 -67
- package/scripts/syncDb/context.js +0 -120
- package/scripts/syncDb/diff.js +0 -279
- package/scripts/syncDb/transform.js +0 -113
- package/sql/befly.sql +0 -271
- package/utils/email.js +0 -3
- package/utils/fieldClear.js +0 -22
- package/utils/formatValidationIssues.js +0 -28
- package/utils/getClientIp.js +0 -47
- package/utils/loggerUtils.js +0 -196
- package/utils/regexpUtil.js +0 -52
- /package/{lib → libs}/smtpText.js +0 -0
package/hooks/rateLimit.js
CHANGED
|
@@ -1,95 +1,58 @@
|
|
|
1
|
-
import picomatch from "picomatch";
|
|
2
|
-
|
|
3
1
|
import { HOOK_ORDER } from "../configs/constConfig.js";
|
|
4
2
|
import { isValidPositiveInt } from "../utils/is.js";
|
|
5
3
|
import { ErrorResponse } from "../utils/response.js";
|
|
6
4
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
function toPositiveInt(value, fallbackValue) {
|
|
10
|
-
const number = Number(value);
|
|
11
|
-
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallbackValue;
|
|
12
|
-
}
|
|
5
|
+
const matcherCache = new WeakMap();
|
|
13
6
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
return picomatch(pattern);
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
7
|
+
function getSkipMatchers(config) {
|
|
8
|
+
let matchers = matcherCache.get(config);
|
|
9
|
+
if (!matchers) {
|
|
10
|
+
matchers = config.skipRoutes.map((pattern) => new Bun.Glob(pattern));
|
|
11
|
+
matcherCache.set(config, matchers);
|
|
23
12
|
}
|
|
13
|
+
return matchers;
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
for (const route of Array.isArray(config?.skipRoutes) ? config.skipRoutes : []) {
|
|
29
|
-
const matcher = createMatcher(toSafeString(route, ""));
|
|
30
|
-
if (matcher) skipMatchers.push(matcher);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
enabled: RATE_LIMIT_ENABLED_VALUES.has(config?.enable),
|
|
35
|
-
// failOpen:限流服务(Redis)不可用时是否放行;默认放行(保持兼容),可配置为 failClosed
|
|
36
|
-
failOpen: config?.failOpen === undefined ? true : RATE_LIMIT_ENABLED_VALUES.has(config?.failOpen),
|
|
37
|
-
limit: toPositiveInt(config?.defaultLimit, 1000),
|
|
38
|
-
window: toPositiveInt(config?.defaultWindow, 60),
|
|
39
|
-
keyType: toSafeString(config?.key, "ip"),
|
|
40
|
-
skipMatchers: skipMatchers
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveScopeByKeyType(keyType, ctx) {
|
|
45
|
-
if (keyType === "userId") return isValidPositiveInt(ctx.userId) ? `user:${ctx.userId}` : `ip:${ctx.ip || "unknown"}`;
|
|
16
|
+
function resolveScope(keyType, ctx) {
|
|
17
|
+
if (keyType === "userId") return isValidPositiveInt(ctx.userId) ? `user:${ctx.userId}` : `ip:${ctx.ip}`;
|
|
46
18
|
if (keyType === "roleCode") return `role:${ctx.roleCode || "guest"}`;
|
|
47
|
-
if (keyType === "apiPath") return `path:${ctx.apiPath
|
|
19
|
+
if (keyType === "apiPath") return `path:${ctx.apiPath}`;
|
|
48
20
|
if (keyType === "global") return "global";
|
|
49
|
-
return `ip:${ctx.ip
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function shouldSkipRateLimit(normalized, ctx) {
|
|
53
|
-
return normalized.skipMatchers.some((matcher) => matcher(ctx.apiPath));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildRateLimitRedisKey(ctx, keyType, scope) {
|
|
57
|
-
return `rate_limit:${ctx.apiPath}:${ctx.method}:${keyType}:${scope}`;
|
|
21
|
+
return `ip:${ctx.ip}`;
|
|
58
22
|
}
|
|
59
23
|
|
|
60
24
|
export default {
|
|
25
|
+
name: "限流",
|
|
61
26
|
order: HOOK_ORDER.RATE_LIMIT,
|
|
62
27
|
handler: async (befly, ctx) => {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
28
|
+
const config = befly.config.rateLimit;
|
|
29
|
+
if (!config.enable || getSkipMatchers(config).some((matcher) => matcher.match(ctx.apiPath))) return;
|
|
65
30
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ctx.corsHeaders ||= {};
|
|
70
|
-
ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
|
|
31
|
+
if (!befly.redis?.incrWithExpire) {
|
|
32
|
+
if (!config.failOpen) {
|
|
33
|
+
ctx.corsHeaders["Retry-After"] = String(config.defaultWindow);
|
|
71
34
|
ctx.response = ErrorResponse(ctx, "服务繁忙,请稍后再试", 1, null, { reason: "rate_limit_unavailable" }, "rateLimit");
|
|
72
35
|
}
|
|
73
36
|
return;
|
|
74
37
|
}
|
|
75
38
|
|
|
76
|
-
const scope =
|
|
77
|
-
const
|
|
78
|
-
|
|
39
|
+
const scope = resolveScope(config.key, ctx);
|
|
40
|
+
const key = `rate_limit:${ctx.apiPath}:${ctx.method}:${config.key}:${scope}`;
|
|
41
|
+
const current = await befly.redis.incrWithExpire(key, config.defaultWindow);
|
|
42
|
+
if (current <= config.defaultLimit) return;
|
|
79
43
|
|
|
80
|
-
ctx.corsHeaders
|
|
81
|
-
ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
|
|
44
|
+
ctx.corsHeaders["Retry-After"] = String(config.defaultWindow);
|
|
82
45
|
ctx.response = ErrorResponse(
|
|
83
46
|
ctx,
|
|
84
47
|
"请求过于频繁,请稍后再试",
|
|
85
48
|
1,
|
|
86
49
|
null,
|
|
87
50
|
{
|
|
88
|
-
limit:
|
|
89
|
-
window:
|
|
51
|
+
limit: config.defaultLimit,
|
|
52
|
+
window: config.defaultWindow,
|
|
90
53
|
current: current,
|
|
91
54
|
apiPath: ctx.apiPath,
|
|
92
|
-
key:
|
|
55
|
+
key: config.key
|
|
93
56
|
},
|
|
94
57
|
"rateLimit"
|
|
95
58
|
);
|
package/hooks/validator.js
CHANGED
|
@@ -1,48 +1,60 @@
|
|
|
1
|
+
import { compile } from "#befly/libs/validator/index.js";
|
|
2
|
+
|
|
3
|
+
import { getFieldSchema } from "../checks/field.js";
|
|
1
4
|
import { HOOK_ORDER } from "../configs/constConfig.js";
|
|
2
|
-
|
|
3
|
-
import { validateFields } from "../lib/validator.js";
|
|
4
|
-
import { isPlainObject, isString } from "../utils/is.js";
|
|
5
|
+
import { isPlainObject } from "../utils/is.js";
|
|
5
6
|
import { ErrorResponse } from "../utils/response.js";
|
|
6
7
|
import { snakeCase } from "../utils/util.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const validatorCache = new WeakMap();
|
|
10
|
+
|
|
11
|
+
function getValidator(fields, required, constraints) {
|
|
12
|
+
let validators = validatorCache.get(fields);
|
|
13
|
+
if (!validators) {
|
|
14
|
+
validators = new Map();
|
|
15
|
+
validatorCache.set(fields, validators);
|
|
16
|
+
}
|
|
17
|
+
const cacheKey = JSON.stringify([required, constraints]);
|
|
18
|
+
let validate = validators.get(cacheKey);
|
|
19
|
+
if (validate) return validate;
|
|
20
|
+
const requiredSet = new Set(required);
|
|
21
|
+
validate = compile({
|
|
22
|
+
paramType: "object",
|
|
23
|
+
fields: Object.fromEntries(
|
|
24
|
+
Object.entries(fields).map(([field, definition]) => [
|
|
25
|
+
field,
|
|
26
|
+
{
|
|
27
|
+
...getFieldSchema(definition),
|
|
28
|
+
optional: !requiredSet.has(field)
|
|
29
|
+
}
|
|
30
|
+
])
|
|
31
|
+
),
|
|
32
|
+
constraints: constraints
|
|
33
|
+
});
|
|
34
|
+
validators.set(cacheKey, validate);
|
|
35
|
+
return validate;
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
export default {
|
|
39
|
+
name: "参数验证",
|
|
13
40
|
order: HOOK_ORDER.VALIDATOR,
|
|
14
41
|
handler: async (befly, ctx) => {
|
|
15
|
-
// 仅保留 fields 中声明的字段,并支持 snake_case 入参回退(例如 agent_id -> agentId)
|
|
16
42
|
const rawBody = isPlainObject(ctx.body) ? ctx.body : {};
|
|
17
43
|
const nextBody = {};
|
|
18
44
|
|
|
19
|
-
for (const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (value === undefined) {
|
|
23
|
-
const snakeField = snakeCase(field);
|
|
24
|
-
if (rawBody[snakeField] !== undefined) {
|
|
25
|
-
value = rawBody[snakeField];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (isString(value)) {
|
|
30
|
-
value = value.trim();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (value !== undefined) {
|
|
34
|
-
nextBody[field] = value;
|
|
35
|
-
}
|
|
45
|
+
for (const field of Object.keys(ctx.apiFields)) {
|
|
46
|
+
const value = rawBody[field] !== undefined ? rawBody[field] : rawBody[snakeCase(field)];
|
|
47
|
+
if (value !== undefined) nextBody[field] = value;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
ctx.body = nextBody;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (result.code !== 0) {
|
|
44
|
-
ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors, "validator");
|
|
51
|
+
const result = getValidator(ctx.apiFields, ctx.apiRequired, ctx.apiConstraints)(nextBody);
|
|
52
|
+
if (result.ok) {
|
|
53
|
+
ctx.body = result.data;
|
|
45
54
|
return;
|
|
46
55
|
}
|
|
56
|
+
|
|
57
|
+
const fieldErrors = Object.fromEntries(result.issues.map((issue) => [issue.path[0], `${ctx.apiFields[issue.path[0]]?.name || issue.path[0]}${issue.message}`]));
|
|
58
|
+
ctx.response = ErrorResponse(ctx, Object.values(fieldErrors)[0] || "参数验证失败", 1, null, fieldErrors, "validator");
|
|
47
59
|
}
|
|
48
60
|
};
|
package/index.js
CHANGED
|
@@ -1,98 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
// ========== 相对导入(项目内部文件) ==========
|
|
18
|
-
// 基础设施
|
|
19
|
-
import { Connect } from "#befly/lib/connect.js";
|
|
20
|
-
// oxlint-disable-next-line unicorn/prefer-export-from -- Logger 同时被本文件大量使用,不能合并为 re-export
|
|
21
|
-
import { Logger } from "#befly/lib/logger.js";
|
|
22
|
-
// 定时任务
|
|
23
|
-
import { registerCorns } from "#befly/plugins/cron.js";
|
|
24
|
-
// 路由处理
|
|
25
|
-
import { apiHandler } from "#befly/router/api.js";
|
|
26
|
-
import { staticHandler } from "#befly/router/static.js";
|
|
27
|
-
// 同步
|
|
28
|
-
import { syncApi } from "#befly/sync/api.js";
|
|
29
|
-
import { syncCache } from "#befly/sync/cache.js";
|
|
30
|
-
import { syncDev } from "#befly/sync/dev.js";
|
|
31
|
-
import { syncMenu } from "#befly/sync/menu.js";
|
|
32
|
-
// 工具
|
|
33
|
-
import { calcPerfTime } from "#befly/utils/calcPerfTime.js";
|
|
34
|
-
import { formatYmdHms } from "#befly/utils/datetime.js";
|
|
35
|
-
import { deepMerge } from "#befly/utils/deepMerge.js";
|
|
36
|
-
import { createError } from "#befly/utils/error.js";
|
|
37
|
-
import { isPrimaryProcess } from "#befly/utils/is.js";
|
|
38
|
-
import { scanSources } from "#befly/utils/scanSources.js";
|
|
39
|
-
import { getRunMode, waitFor } from "#befly/utils/util.js";
|
|
40
|
-
|
|
41
|
-
export { syncDb } from "#befly/scripts/syncDb/index.js";
|
|
42
|
-
export { xmlParse } from "#befly/lib/xmlParse.js";
|
|
43
|
-
export { Logger };
|
|
44
|
-
|
|
45
|
-
// 主进程同步完成的 Redis 就绪标记(非主进程等待此标记后再对外服务)
|
|
46
|
-
const SYNC_READY_KEY = "befly:syncReady";
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
|
|
3
|
+
import { Befly } from "./Befly.js";
|
|
4
|
+
import { checkApi } from "./checks/api.js";
|
|
5
|
+
import { checkConfig } from "./checks/config.js";
|
|
6
|
+
import { checkCorn } from "./checks/corns.js";
|
|
7
|
+
import { checkHook, checkPlugin } from "./checks/items.js";
|
|
8
|
+
import { checkMenu } from "./checks/menu.js";
|
|
9
|
+
import { checkTable } from "./checks/table.js";
|
|
10
|
+
import beflyConfig from "./configs/beflyConfig.json";
|
|
11
|
+
import beflyMenus from "./configs/beflyMenus.json";
|
|
12
|
+
import { calcPerfTime } from "./utils/calcPerfTime.js";
|
|
13
|
+
import { deepMerge } from "./utils/deepMerge.js";
|
|
14
|
+
import { createError } from "./utils/error.js";
|
|
15
|
+
import { scanSources } from "./utils/scanSources.js";
|
|
16
|
+
import { getRunMode } from "./utils/util.js";
|
|
47
17
|
|
|
48
18
|
function prefixMenuPaths(menus, prefix) {
|
|
49
|
-
return menus.map((menu) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return nextMenu;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function syncReady(ctx) {
|
|
69
|
-
const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
|
|
70
|
-
if (missingCtxKeys.length > 0) {
|
|
71
|
-
throw createError(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
|
|
72
|
-
code: "runtime",
|
|
73
|
-
subsystem: "sync",
|
|
74
|
-
operation: "ctxCheck",
|
|
75
|
-
missingCtxKeys: missingCtxKeys
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const requiredTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
|
|
80
|
-
const missingTables = [];
|
|
81
|
-
|
|
82
|
-
for (const table of requiredTables) {
|
|
83
|
-
if (!(await ctx.mysql.tableExists(table)).data) {
|
|
84
|
-
missingTables.push(table);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (missingTables.length > 0) {
|
|
89
|
-
throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, {
|
|
90
|
-
code: "runtime",
|
|
91
|
-
subsystem: "sync",
|
|
92
|
-
operation: "tableExists",
|
|
93
|
-
missingTables: missingTables
|
|
94
|
-
});
|
|
95
|
-
}
|
|
19
|
+
return menus.map((menu) => ({
|
|
20
|
+
name: menu.name,
|
|
21
|
+
path: menu.path === "/" ? `/${prefix}` : `/${prefix}${menu.path}`,
|
|
22
|
+
sort: menu.sort,
|
|
23
|
+
...(Array.isArray(menu.children)
|
|
24
|
+
? {
|
|
25
|
+
children: menu.children.map((child) => ({
|
|
26
|
+
name: child.name,
|
|
27
|
+
path: child.path,
|
|
28
|
+
sort: child.sort
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
: {})
|
|
32
|
+
}));
|
|
96
33
|
}
|
|
97
34
|
|
|
98
35
|
export async function createBefly(config = {}, menus = []) {
|
|
@@ -100,10 +37,7 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
100
37
|
const mergedConfig = deepMerge(beflyConfig, config);
|
|
101
38
|
const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
|
|
102
39
|
|
|
103
|
-
Logger.configure({
|
|
104
|
-
runtimeEnv: getRunMode(),
|
|
105
|
-
...mergedConfig.logger
|
|
106
|
-
});
|
|
40
|
+
Logger.configure({ runtimeEnv: getRunMode(), ...mergedConfig.logger });
|
|
107
41
|
Logger.info(`启动 合并配置 耗时 ${calcPerfTime(mergeStartTime)}`);
|
|
108
42
|
|
|
109
43
|
let stageStartTime = Bun.nanoseconds();
|
|
@@ -111,43 +45,19 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
111
45
|
Logger.info(`启动 检查配置 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
112
46
|
|
|
113
47
|
stageStartTime = Bun.nanoseconds();
|
|
114
|
-
const { apis, tables, plugins, hooks, corns } = await scanSources();
|
|
48
|
+
const { apis, tables, plugins, hooks, corns } = await scanSources({ beflyMode: mergedConfig.mysql.beflyMode });
|
|
115
49
|
Logger.info(`启动 扫描源码 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
116
50
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
Logger.info(`启动 检查插件 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
128
|
-
|
|
129
|
-
stageStartTime = Bun.nanoseconds();
|
|
130
|
-
const hookErrors = await checkHook(hooks);
|
|
131
|
-
Logger.info(`启动 检查钩子 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
132
|
-
|
|
133
|
-
stageStartTime = Bun.nanoseconds();
|
|
134
|
-
const cornErrors = await checkCorn(corns);
|
|
135
|
-
Logger.info(`启动 检查定时器 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
136
|
-
|
|
137
|
-
stageStartTime = Bun.nanoseconds();
|
|
138
|
-
const menuErrors = await checkMenu(mergedMenus);
|
|
139
|
-
Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
140
|
-
|
|
141
|
-
// 汇总各检查明细,报错可直接定位到具体文件/路径/配置/字段
|
|
142
|
-
const failedChecks = [
|
|
143
|
-
{ check: "配置", errors: configErrors },
|
|
144
|
-
{ check: "接口", errors: apiErrors },
|
|
145
|
-
{ check: "表结构", errors: tableErrors },
|
|
146
|
-
{ check: "插件", errors: pluginErrors },
|
|
147
|
-
{ check: "钩子", errors: hookErrors },
|
|
148
|
-
{ check: "定时器", errors: cornErrors },
|
|
149
|
-
{ check: "菜单", errors: menuErrors }
|
|
150
|
-
].filter((entry) => entry.errors.length > 0);
|
|
51
|
+
const checks = [
|
|
52
|
+
["配置", configErrors],
|
|
53
|
+
["接口", await checkApi(apis)],
|
|
54
|
+
["表结构", await checkTable(tables)],
|
|
55
|
+
["插件", await checkPlugin(plugins)],
|
|
56
|
+
["钩子", await checkHook(hooks)],
|
|
57
|
+
["定时器", checkCorn(corns)],
|
|
58
|
+
["菜单", await checkMenu(mergedMenus)]
|
|
59
|
+
];
|
|
60
|
+
const failedChecks = checks.filter(([, errors]) => errors.length > 0).map(([check, errors]) => ({ check: check, errors: errors }));
|
|
151
61
|
|
|
152
62
|
if (failedChecks.length > 0) {
|
|
153
63
|
throw createError("检查失败:存在配置/结构问题", {
|
|
@@ -158,10 +68,7 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
158
68
|
});
|
|
159
69
|
}
|
|
160
70
|
|
|
161
|
-
// 运行模式以环境为准(NODE_ENV,线上部署注入 production;本地开发默认 development),
|
|
162
|
-
// 覆盖配置值,保证 befly.config.runMode 与运行时判断一致
|
|
163
71
|
mergedConfig.runMode = getRunMode();
|
|
164
|
-
|
|
165
72
|
return new Befly({
|
|
166
73
|
config: mergedConfig,
|
|
167
74
|
tables: tables,
|
|
@@ -169,214 +76,6 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
169
76
|
apis: apis,
|
|
170
77
|
hooks: hooks,
|
|
171
78
|
plugins: plugins,
|
|
172
|
-
corns: corns
|
|
173
|
-
createdByFactory: true
|
|
79
|
+
corns: corns
|
|
174
80
|
});
|
|
175
81
|
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Befly 框架核心类
|
|
179
|
-
* 职责:管理应用上下文和生命周期
|
|
180
|
-
*/
|
|
181
|
-
export class Befly {
|
|
182
|
-
constructor(init = {}) {
|
|
183
|
-
this.context = {
|
|
184
|
-
config: init.config || {},
|
|
185
|
-
tables: init.tables || {}
|
|
186
|
-
};
|
|
187
|
-
this.menus = Array.isArray(init.menus) ? init.menus : [];
|
|
188
|
-
this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
|
|
189
|
-
this.apis = Array.isArray(init.apis) ? init.apis : [];
|
|
190
|
-
this.plugins = Array.isArray(init.plugins) ? init.plugins : [];
|
|
191
|
-
this.corns = Array.isArray(init.corns) ? init.corns : [];
|
|
192
|
-
this.createdByFactory = init.createdByFactory === true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
ensureCreatedByFactory() {
|
|
196
|
-
if (this.createdByFactory) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
throw createError("请使用 createBefly(...) 创建实例后再调用 start", { code: "policy", subsystem: "start", operation: "factoryGuard" });
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* 启动完整的生命周期流程
|
|
205
|
-
* @returns HTTP 服务器实例
|
|
206
|
-
*/
|
|
207
|
-
async start() {
|
|
208
|
-
try {
|
|
209
|
-
this.ensureCreatedByFactory();
|
|
210
|
-
|
|
211
|
-
const serverStartTime = Bun.nanoseconds();
|
|
212
|
-
|
|
213
|
-
// 启动期建立基础连接(SQL + Redis)
|
|
214
|
-
// 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
|
|
215
|
-
let stageStartTime = Bun.nanoseconds();
|
|
216
|
-
await Connect.connectRedis(this.context.config.redis);
|
|
217
|
-
Logger.info(`启动 连接 Redis 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
218
|
-
|
|
219
|
-
stageStartTime = Bun.nanoseconds();
|
|
220
|
-
await Connect.connectMysql(this.context.config.mysql);
|
|
221
|
-
Logger.info(`启动 连接 Mysql 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
222
|
-
|
|
223
|
-
// 加载插件
|
|
224
|
-
for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
|
|
225
|
-
const pluginStartTime = Bun.nanoseconds();
|
|
226
|
-
this.context[item.fileName] = await item.handler(this.context);
|
|
227
|
-
Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 注册定时器(依赖插件加载完成的 ctx.cron,非主进程自动跳过)
|
|
231
|
-
stageStartTime = Bun.nanoseconds();
|
|
232
|
-
registerCorns(this.context, this.corns);
|
|
233
|
-
Logger.info(`启动 注册定时器 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
234
|
-
|
|
235
|
-
stageStartTime = Bun.nanoseconds();
|
|
236
|
-
await syncReady(this.context);
|
|
237
|
-
Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
238
|
-
|
|
239
|
-
// 自动同步(bm2 多实例:主进程执行同步并写就绪标记,其它进程等待同步完成后再对外服务)
|
|
240
|
-
if (isPrimaryProcess()) {
|
|
241
|
-
stageStartTime = Bun.nanoseconds();
|
|
242
|
-
await syncApi(this.context, this.apis);
|
|
243
|
-
Logger.info(`启动 同步接口 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
244
|
-
|
|
245
|
-
stageStartTime = Bun.nanoseconds();
|
|
246
|
-
await syncMenu(this.context, this.menus);
|
|
247
|
-
Logger.info(`启动 同步菜单 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
248
|
-
|
|
249
|
-
stageStartTime = Bun.nanoseconds();
|
|
250
|
-
await syncDev(this.context);
|
|
251
|
-
Logger.info(`启动 同步开发账号 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
252
|
-
|
|
253
|
-
stageStartTime = Bun.nanoseconds();
|
|
254
|
-
await syncCache(this.context);
|
|
255
|
-
Logger.info(`启动 同步缓存 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
256
|
-
|
|
257
|
-
// 同步完成:写入就绪标记(TTL 防崩溃残留),非主进程据此放行
|
|
258
|
-
await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
|
|
259
|
-
} else {
|
|
260
|
-
// 非主进程:在监听端口前等待主进程本次同步完成(时间戳判断,同机时钟一致)
|
|
261
|
-
const startAt = Date.now();
|
|
262
|
-
await waitFor({
|
|
263
|
-
label: "主进程同步完成",
|
|
264
|
-
check: async () => {
|
|
265
|
-
const ready = await this.context.redis.getString(SYNC_READY_KEY);
|
|
266
|
-
return ready !== null && Number(ready) > startAt;
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
Logger.info("启动 等待主进程同步完成 完成");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 加载钩子
|
|
273
|
-
stageStartTime = Bun.nanoseconds();
|
|
274
|
-
this.hooks = this.hooks.toSorted((a, b) => a.order - b.order);
|
|
275
|
-
|
|
276
|
-
// 加载所有 API
|
|
277
|
-
this.apis = Object.fromEntries(this.apis.map((api) => [api.apiPath, api]));
|
|
278
|
-
|
|
279
|
-
// 启动 HTTP服务器
|
|
280
|
-
const apiFetch = apiHandler(this.apis, this.hooks, this.context);
|
|
281
|
-
const staticFetch = staticHandler(this.context.config.cors, this.context.config.upload);
|
|
282
|
-
Logger.info(`启动 装配接口 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
283
|
-
|
|
284
|
-
stageStartTime = Bun.nanoseconds();
|
|
285
|
-
const server = Bun.serve({
|
|
286
|
-
port: this.context.config.appPort || 3000,
|
|
287
|
-
hostname: this.context.config.appHost || "127.0.0.1",
|
|
288
|
-
// 开发模式下启用详细错误信息
|
|
289
|
-
development: this.context.config.runMode === "development",
|
|
290
|
-
// 空闲连接超时时间(秒),防止恶意连接占用资源
|
|
291
|
-
fetch: async (req, httpServer) => {
|
|
292
|
-
const url = new URL(req.url);
|
|
293
|
-
|
|
294
|
-
if (url.pathname.startsWith("/api/")) {
|
|
295
|
-
return apiFetch(req, httpServer);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (url.pathname.startsWith("/public/")) {
|
|
299
|
-
return staticFetch(req);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 根路径返回服务基本信息
|
|
303
|
-
if (url.pathname === "/") {
|
|
304
|
-
return Response.json(
|
|
305
|
-
{
|
|
306
|
-
code: 0,
|
|
307
|
-
msg: "success",
|
|
308
|
-
data: {
|
|
309
|
-
name: this.context.config.appName,
|
|
310
|
-
mode: this.context.config.runMode,
|
|
311
|
-
time: formatYmdHms(Date.now())
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
{ status: 200 }
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return Response.json({ code: 1, msg: "路径不存在" }, { status: 200 });
|
|
319
|
-
},
|
|
320
|
-
error: (error) => {
|
|
321
|
-
Logger.error("服务启动时发生错误", error);
|
|
322
|
-
// 开发模式下返回详细错误信息
|
|
323
|
-
if (this.context.config.runMode === "development") {
|
|
324
|
-
return Response.json(
|
|
325
|
-
{
|
|
326
|
-
code: 1,
|
|
327
|
-
msg: "内部服务器错误",
|
|
328
|
-
error: error.message,
|
|
329
|
-
stack: error.stack
|
|
330
|
-
},
|
|
331
|
-
{ status: 200 }
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
return Response.json({ code: 1, msg: "内部服务器错误" }, { status: 200 });
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
Logger.info(`启动 监听服务 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
338
|
-
|
|
339
|
-
const finalStartupTime = calcPerfTime(serverStartTime);
|
|
340
|
-
|
|
341
|
-
Logger.info(`${this.context.config.appName} 启动成功!`);
|
|
342
|
-
Logger.info(`服务器启动耗时: ${finalStartupTime}`);
|
|
343
|
-
Logger.info(`服务器监听地址: ${server.url}`);
|
|
344
|
-
Logger.info(`Mysql 数据库地址:${this.context.config.mysql.hostname}`);
|
|
345
|
-
Logger.info(`Redis 缓存地址:${this.context.config.redis.hostname}`);
|
|
346
|
-
try {
|
|
347
|
-
process.stdout.write(`${this.context.config.appName} 启动成功!\n`);
|
|
348
|
-
process.stdout.write(`服务器启动耗时: ${finalStartupTime}\n`);
|
|
349
|
-
process.stdout.write(`服务器监听地址: ${server.url}\n`);
|
|
350
|
-
process.stdout.write(`Mysql 数据库地址:${this.context.config.mysql.hostname}\n`);
|
|
351
|
-
process.stdout.write(`Redis 缓存地址:${this.context.config.redis.hostname}\n`);
|
|
352
|
-
} catch {
|
|
353
|
-
// ignore
|
|
354
|
-
}
|
|
355
|
-
// 注意:作为库代码,这里不注册 SIGINT/SIGTERM 处理器,也不调用 process.exit。
|
|
356
|
-
// 宿主应用应自行处理信号并决定退出策略(包括是否调用 server.stop / Connect.disconnect / Logger.flush)。
|
|
357
|
-
return server;
|
|
358
|
-
} catch (error) {
|
|
359
|
-
// 注意:这里不能直接 process.exit(1)
|
|
360
|
-
// - Logger 是异步缓冲写入,exit 会导致日志来不及 flush(实际项目里表现为“完全没打印”)
|
|
361
|
-
// - 作为库代码,也不应该强行终止宿主进程
|
|
362
|
-
|
|
363
|
-
// 注意:测试要求 start() 失败时必须 Logger.error 一次,所以这里始终调用 error。
|
|
364
|
-
Logger.error("项目启动失败", error);
|
|
365
|
-
|
|
366
|
-
// 尽力把错误日志落盘/输出后再把异常抛给上层。
|
|
367
|
-
try {
|
|
368
|
-
await Logger.flush();
|
|
369
|
-
await Logger.shutdown();
|
|
370
|
-
} catch {
|
|
371
|
-
// ignore
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
throw createError("运行时错误", {
|
|
375
|
-
cause: error,
|
|
376
|
-
code: error?.code || "runtime",
|
|
377
|
-
subsystem: "start",
|
|
378
|
-
operation: "start"
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
6
|
+
|
|
5
7
|
import { BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "../configs/constConfig.js";
|
|
6
8
|
import { createError } from "../utils/error.js";
|
|
7
9
|
import { isNonEmptyString, isNullable, isString } from "../utils/is.js";
|
|
8
|
-
import { Logger } from "./logger.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 缓存助手类
|
|
@@ -50,12 +51,6 @@ export class CacheHelper {
|
|
|
50
51
|
|
|
51
52
|
async rebuildRoleCache(kind, roleValuesMap) {
|
|
52
53
|
const roleCodes = Array.from(roleValuesMap.keys());
|
|
53
|
-
|
|
54
|
-
if (roleCodes.length === 0) {
|
|
55
|
-
Logger.info(`✅ 没有需要缓存的角色 ${kind}`);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
54
|
const oldVersion = await this.getRoleCacheActiveVersion(kind);
|
|
60
55
|
let nextVersion = String(Date.now());
|
|
61
56
|
|
|
@@ -80,7 +75,7 @@ export class CacheHelper {
|
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
await this.redis.del(nextCodesKey);
|
|
83
|
-
await this.redis.sadd(nextCodesKey, roleCodes);
|
|
78
|
+
if (roleCodes.length > 0) await this.redis.sadd(nextCodesKey, roleCodes);
|
|
84
79
|
|
|
85
80
|
if (nextItems.length > 0) {
|
|
86
81
|
await this.redis.saddBatch(nextItems);
|