befly 3.9.38 → 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 +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 +7 -7
- 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/utils/modules.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Hook } from "../types/hook.js";
|
|
2
|
+
import type { Plugin } from "../types/plugin.js";
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
import { camelCase } from "es-toolkit/string";
|
|
7
|
+
|
|
8
|
+
import { Logger } from "../lib/logger.js";
|
|
9
|
+
import { scanFiles } from "./scanFiles.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 扫描模块(插件或钩子)
|
|
13
|
+
* @param dir - 目录路径
|
|
14
|
+
* @param type - 模块类型(core/addon/app)
|
|
15
|
+
* @param moduleLabel - 模块标签(如"插件"、"钩子")
|
|
16
|
+
* @param addonName - 组件名称(仅 type='addon' 时需要)
|
|
17
|
+
* @returns 模块列表
|
|
18
|
+
*/
|
|
19
|
+
export async function scanModules<T extends Plugin | Hook>(dir: string, type: "core" | "addon" | "app", moduleLabel: string, addonName?: string): Promise<T[]> {
|
|
20
|
+
if (!existsSync(dir)) return [];
|
|
21
|
+
|
|
22
|
+
const items: T[] = [];
|
|
23
|
+
const files = await scanFiles(dir, "*.ts");
|
|
24
|
+
|
|
25
|
+
for (const { filePath, fileName } of files) {
|
|
26
|
+
// 生成模块名称
|
|
27
|
+
const name = camelCase(fileName);
|
|
28
|
+
const moduleName = type === "core" ? name : type === "addon" ? `addon_${camelCase(addonName!)}_${name}` : `app_${name}`;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
32
|
+
const moduleImport = await import(normalizedFilePath);
|
|
33
|
+
const item = moduleImport.default;
|
|
34
|
+
|
|
35
|
+
item.name = moduleName;
|
|
36
|
+
// 为 addon 模块记录 addon 名称
|
|
37
|
+
if (type === "addon" && addonName) {
|
|
38
|
+
item.addonName = addonName;
|
|
39
|
+
}
|
|
40
|
+
items.push(item);
|
|
41
|
+
} catch (err: any) {
|
|
42
|
+
const typeLabel = type === "core" ? "核心" : type === "addon" ? `组件${addonName}` : "项目";
|
|
43
|
+
Logger.error({ err: err, module: fileName }, `${typeLabel}${moduleLabel} 导入失败`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return items;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 排序模块(根据依赖关系)
|
|
53
|
+
* @param modules - 待排序的模块列表
|
|
54
|
+
* @returns 排序后的模块列表,如果存在循环依赖或依赖不存在则返回 false
|
|
55
|
+
*/
|
|
56
|
+
export function sortModules<T extends { name?: string; after?: string[] }>(modules: T[]): T[] | false {
|
|
57
|
+
const result: T[] = [];
|
|
58
|
+
const visited = new Set<string>();
|
|
59
|
+
const visiting = new Set<string>();
|
|
60
|
+
const moduleMap: Record<string, T> = Object.fromEntries(modules.map((m) => [m.name!, m]));
|
|
61
|
+
let isPass = true;
|
|
62
|
+
|
|
63
|
+
// 检查依赖是否存在
|
|
64
|
+
for (const module of modules) {
|
|
65
|
+
if (module.after) {
|
|
66
|
+
for (const dep of module.after) {
|
|
67
|
+
if (!moduleMap[dep]) {
|
|
68
|
+
Logger.error({ module: module.name, dependency: dep }, "依赖的模块未找到");
|
|
69
|
+
isPass = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isPass) return false;
|
|
76
|
+
|
|
77
|
+
const visit = (name: string): void => {
|
|
78
|
+
if (visited.has(name)) return;
|
|
79
|
+
if (visiting.has(name)) {
|
|
80
|
+
Logger.error({ module: name }, "模块循环依赖");
|
|
81
|
+
isPass = false;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const module = moduleMap[name];
|
|
86
|
+
if (!module) return;
|
|
87
|
+
|
|
88
|
+
visiting.add(name);
|
|
89
|
+
(module.after || []).forEach(visit);
|
|
90
|
+
visiting.delete(name);
|
|
91
|
+
visited.add(name);
|
|
92
|
+
result.push(module);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
modules.forEach((m) => visit(m.name!));
|
|
96
|
+
|
|
97
|
+
return isPass ? result : false;
|
|
98
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isPlainObject } from "es-toolkit/compat";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 挑选指定字段
|
|
5
|
+
*/
|
|
6
|
+
export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
|
|
7
|
+
if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result: any = {};
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (key in obj) {
|
|
14
|
+
result[key] = obj[key];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result;
|
|
19
|
+
};
|
package/utils/process.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程角色信息
|
|
3
|
+
*/
|
|
4
|
+
export interface ProcessRole {
|
|
5
|
+
/** 进程角色:primary(主进程)或 worker(工作进程) */
|
|
6
|
+
role: "primary" | "worker";
|
|
7
|
+
/** 实例 ID(PM2 或 Bun Worker) */
|
|
8
|
+
instanceId: string | null;
|
|
9
|
+
/** 运行环境:bun-cluster、pm2-cluster 或 standalone */
|
|
10
|
+
env: "bun-cluster" | "pm2-cluster" | "standalone";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取当前进程角色信息
|
|
15
|
+
* @returns 进程角色、实例 ID 和运行环境
|
|
16
|
+
*/
|
|
17
|
+
export function getProcessRole(): ProcessRole {
|
|
18
|
+
const bunWorkerId = process.env.BUN_WORKER_ID;
|
|
19
|
+
const pm2InstanceId = process.env.PM2_INSTANCE_ID;
|
|
20
|
+
|
|
21
|
+
// Bun 集群模式
|
|
22
|
+
if (bunWorkerId !== undefined) {
|
|
23
|
+
return {
|
|
24
|
+
role: bunWorkerId === "" ? "primary" : "worker",
|
|
25
|
+
instanceId: bunWorkerId || "0",
|
|
26
|
+
env: "bun-cluster"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// PM2 集群模式
|
|
31
|
+
if (pm2InstanceId !== undefined) {
|
|
32
|
+
return {
|
|
33
|
+
role: pm2InstanceId === "0" ? "primary" : "worker",
|
|
34
|
+
instanceId: pm2InstanceId,
|
|
35
|
+
env: "pm2-cluster"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 单进程模式
|
|
40
|
+
return {
|
|
41
|
+
role: "primary",
|
|
42
|
+
instanceId: null,
|
|
43
|
+
env: "standalone"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 检测当前进程是否为主进程
|
|
49
|
+
* 用于集群模式下避免重复执行同步任务
|
|
50
|
+
* - Bun 集群:BUN_WORKER_ID 为空时是主进程
|
|
51
|
+
* - PM2 集群:PM2_INSTANCE_ID 为 '0' 或不存在时是主进程
|
|
52
|
+
* @returns 是否为主进程
|
|
53
|
+
*/
|
|
54
|
+
export function isPrimaryProcess(): boolean {
|
|
55
|
+
return getProcessRole().role === "primary";
|
|
56
|
+
}
|
package/utils/regex.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置正则表达式别名
|
|
3
|
+
* 用于表单验证和数据校验
|
|
4
|
+
* 命名规范:小驼峰格式
|
|
5
|
+
*/
|
|
6
|
+
export const RegexAliases = {
|
|
7
|
+
// ============================================
|
|
8
|
+
// 数字类型
|
|
9
|
+
// ============================================
|
|
10
|
+
/** 正整数(不含0) */
|
|
11
|
+
number: "^\\d+$",
|
|
12
|
+
/** 整数(含负数) */
|
|
13
|
+
integer: "^-?\\d+$",
|
|
14
|
+
/** 浮点数 */
|
|
15
|
+
float: "^-?\\d+(\\.\\d+)?$",
|
|
16
|
+
/** 正整数(不含0) */
|
|
17
|
+
positive: "^[1-9]\\d*$",
|
|
18
|
+
/** 负整数 */
|
|
19
|
+
negative: "^-\\d+$",
|
|
20
|
+
/** 零 */
|
|
21
|
+
zero: "^0$",
|
|
22
|
+
|
|
23
|
+
// ============================================
|
|
24
|
+
// 字符串类型
|
|
25
|
+
// ============================================
|
|
26
|
+
/** 纯字母 */
|
|
27
|
+
word: "^[a-zA-Z]+$",
|
|
28
|
+
/** 字母和数字 */
|
|
29
|
+
alphanumeric: "^[a-zA-Z0-9]+$",
|
|
30
|
+
/** 字母、数字和下划线 */
|
|
31
|
+
alphanumeric_: "^[a-zA-Z0-9_]+$",
|
|
32
|
+
/** 字母、数字、下划线和短横线 */
|
|
33
|
+
alphanumericDash_: "^[a-zA-Z0-9_-]+$",
|
|
34
|
+
/** 小写字母 */
|
|
35
|
+
lowercase: "^[a-z]+$",
|
|
36
|
+
/** 大写字母 */
|
|
37
|
+
uppercase: "^[A-Z]+$",
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// 中文
|
|
41
|
+
// ============================================
|
|
42
|
+
/** 纯中文 */
|
|
43
|
+
chinese: "^[\\u4e00-\\u9fa5]+$",
|
|
44
|
+
/** 中文和字母 */
|
|
45
|
+
chineseWord: "^[\\u4e00-\\u9fa5a-zA-Z]+$",
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// 常用格式
|
|
49
|
+
// ============================================
|
|
50
|
+
/** 邮箱地址 */
|
|
51
|
+
email: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
|
52
|
+
/** 中国大陆手机号 */
|
|
53
|
+
phone: "^1[3-9]\\d{9}$",
|
|
54
|
+
/** 固定电话(区号-号码) */
|
|
55
|
+
telephone: "^0\\d{2,3}-?\\d{7,8}$",
|
|
56
|
+
/** URL 地址 */
|
|
57
|
+
url: "^https?://",
|
|
58
|
+
/** IPv4 地址 */
|
|
59
|
+
ip: "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
|
|
60
|
+
/** IPv6 地址 */
|
|
61
|
+
ipv6: "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$",
|
|
62
|
+
/** 域名 */
|
|
63
|
+
domain: "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$",
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// 特殊格式
|
|
67
|
+
// ============================================
|
|
68
|
+
/** UUID */
|
|
69
|
+
uuid: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
70
|
+
/** 十六进制字符串 */
|
|
71
|
+
hex: "^[0-9a-fA-F]+$",
|
|
72
|
+
/** Base64 编码 */
|
|
73
|
+
base64: "^[A-Za-z0-9+/=]+$",
|
|
74
|
+
/** MD5 哈希 */
|
|
75
|
+
md5: "^[a-f0-9]{32}$",
|
|
76
|
+
/** SHA1 哈希 */
|
|
77
|
+
sha1: "^[a-f0-9]{40}$",
|
|
78
|
+
/** SHA256 哈希 */
|
|
79
|
+
sha256: "^[a-f0-9]{64}$",
|
|
80
|
+
|
|
81
|
+
// ============================================
|
|
82
|
+
// 日期时间
|
|
83
|
+
// ============================================
|
|
84
|
+
/** 日期 YYYY-MM-DD */
|
|
85
|
+
date: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
86
|
+
/** 时间 HH:MM:SS */
|
|
87
|
+
time: "^\\d{2}:\\d{2}:\\d{2}$",
|
|
88
|
+
/** ISO 日期时间 */
|
|
89
|
+
datetime: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
|
|
90
|
+
/** 年份 */
|
|
91
|
+
year: "^\\d{4}$",
|
|
92
|
+
/** 月份 01-12 */
|
|
93
|
+
month: "^(0[1-9]|1[0-2])$",
|
|
94
|
+
/** 日期 01-31 */
|
|
95
|
+
day: "^(0[1-9]|[12]\\d|3[01])$",
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// 代码相关
|
|
99
|
+
// ============================================
|
|
100
|
+
/** 变量名 */
|
|
101
|
+
variable: "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
|
102
|
+
/** 常量名(全大写) */
|
|
103
|
+
constant: "^[A-Z][A-Z0-9_]*$",
|
|
104
|
+
/** 包名(小写+连字符) */
|
|
105
|
+
package: "^[a-z][a-z0-9-]*$",
|
|
106
|
+
|
|
107
|
+
// ============================================
|
|
108
|
+
// 证件相关
|
|
109
|
+
// ============================================
|
|
110
|
+
/** 中国身份证号(18位) */
|
|
111
|
+
idCard: "^\\d{17}[\\dXx]$",
|
|
112
|
+
/** 护照号 */
|
|
113
|
+
passport: "^[a-zA-Z0-9]{5,17}$",
|
|
114
|
+
|
|
115
|
+
// ============================================
|
|
116
|
+
// 账号相关(国内常用)
|
|
117
|
+
// ============================================
|
|
118
|
+
/** 银行卡号(16-19位数字) */
|
|
119
|
+
bankCard: "^\\d{16,19}$",
|
|
120
|
+
/** 微信号(6-20位,字母开头,可包含字母、数字、下划线、减号) */
|
|
121
|
+
wechat: "^[a-zA-Z][a-zA-Z0-9_-]{5,19}$",
|
|
122
|
+
/** QQ号(5-11位数字,首位非0) */
|
|
123
|
+
qq: "^[1-9]\\d{4,10}$",
|
|
124
|
+
/** 支付宝账号(手机号或邮箱) */
|
|
125
|
+
alipay: "^(1[3-9]\\d{9}|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})$",
|
|
126
|
+
/** 用户名(4-20位,字母开头,可包含字母、数字、下划线) */
|
|
127
|
+
username: "^[a-zA-Z][a-zA-Z0-9_]{3,19}$",
|
|
128
|
+
/** 昵称(2-20位,支持中文、字母、数字) */
|
|
129
|
+
nickname: "^[\\u4e00-\\u9fa5a-zA-Z0-9]{2,20}$",
|
|
130
|
+
|
|
131
|
+
// ============================================
|
|
132
|
+
// 密码强度
|
|
133
|
+
// ============================================
|
|
134
|
+
/** 弱密码(至少6位) */
|
|
135
|
+
passwordWeak: "^.{6,}$",
|
|
136
|
+
/** 中等密码(至少8位,包含字母和数字) */
|
|
137
|
+
passwordMedium: "^(?=.*[a-zA-Z])(?=.*\\d).{8,}$",
|
|
138
|
+
/** 强密码(至少8位,包含大小写字母、数字和特殊字符) */
|
|
139
|
+
passwordStrong: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}$",
|
|
140
|
+
|
|
141
|
+
// ============================================
|
|
142
|
+
// 其他常用
|
|
143
|
+
// ============================================
|
|
144
|
+
/** 车牌号(新能源+普通) */
|
|
145
|
+
licensePlate: "^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$",
|
|
146
|
+
/** 邮政编码 */
|
|
147
|
+
postalCode: "^\\d{6}$",
|
|
148
|
+
/** 版本号(语义化版本) */
|
|
149
|
+
semver: "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?(\\+[a-zA-Z0-9.]+)?$",
|
|
150
|
+
/** 颜色值(十六进制) */
|
|
151
|
+
colorHex: "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
|
|
152
|
+
|
|
153
|
+
// ============================================
|
|
154
|
+
// 空值
|
|
155
|
+
// ============================================
|
|
156
|
+
/** 空字符串 */
|
|
157
|
+
empty: "^$",
|
|
158
|
+
/** 非空 */
|
|
159
|
+
notempty: ".+"
|
|
160
|
+
} as const;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 正则别名类型
|
|
164
|
+
*/
|
|
165
|
+
export type RegexAliasName = keyof typeof RegexAliases;
|
|
166
|
+
|
|
167
|
+
// ============================================
|
|
168
|
+
// 正则表达式缓存(性能优化)
|
|
169
|
+
// ============================================
|
|
170
|
+
const regexCache = new Map<string, RegExp>();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 获取正则表达式字符串
|
|
174
|
+
* @param name 正则别名(以 @ 开头)或自定义正则字符串
|
|
175
|
+
* @returns 正则表达式字符串
|
|
176
|
+
*/
|
|
177
|
+
export function getRegex(name: string): string {
|
|
178
|
+
if (name.startsWith("@")) {
|
|
179
|
+
const alias = name.slice(1) as RegexAliasName;
|
|
180
|
+
return RegexAliases[alias] || name;
|
|
181
|
+
}
|
|
182
|
+
return name;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 获取编译后的正则表达式对象(带缓存)
|
|
187
|
+
* @param pattern 正则别名或正则字符串
|
|
188
|
+
* @param flags 正则标志(如 'i', 'g')
|
|
189
|
+
* @returns 编译后的 RegExp 对象
|
|
190
|
+
*/
|
|
191
|
+
export function getCompiledRegex(pattern: string, flags?: string): RegExp {
|
|
192
|
+
const regexStr = getRegex(pattern);
|
|
193
|
+
const cacheKey = `${regexStr}:${flags || ""}`;
|
|
194
|
+
|
|
195
|
+
let cached = regexCache.get(cacheKey);
|
|
196
|
+
if (!cached) {
|
|
197
|
+
cached = new RegExp(regexStr, flags);
|
|
198
|
+
regexCache.set(cacheKey, cached);
|
|
199
|
+
}
|
|
200
|
+
return cached;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 验证值是否匹配正则(使用缓存)
|
|
205
|
+
* @param value 要验证的值
|
|
206
|
+
* @param pattern 正则别名或正则字符串
|
|
207
|
+
* @returns 是否匹配
|
|
208
|
+
*/
|
|
209
|
+
export function matchRegex(value: string, pattern: string): boolean {
|
|
210
|
+
return getCompiledRegex(pattern).test(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 清除正则缓存
|
|
215
|
+
*/
|
|
216
|
+
export function clearRegexCache(): void {
|
|
217
|
+
regexCache.clear();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 获取缓存大小
|
|
222
|
+
*/
|
|
223
|
+
export function getRegexCacheSize(): number {
|
|
224
|
+
return regexCache.size;
|
|
225
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { RequestContext } from "../types/context.js";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 创建错误响应(专用于 Hook 中间件)
|
|
7
|
+
* 在钩子中提前拦截请求时使用
|
|
8
|
+
* @param ctx - 请求上下文
|
|
9
|
+
* @param msg - 错误消息
|
|
10
|
+
* @param code - 错误码,默认 1
|
|
11
|
+
* @param data - 附加数据,默认 null
|
|
12
|
+
* @param detail - 详细信息,用于标记具体提示位置,默认 null
|
|
13
|
+
* @param reasonCode - 拦截原因标识(用于统计/聚合),默认 null
|
|
14
|
+
* @returns Response 对象
|
|
15
|
+
*/
|
|
16
|
+
export function ErrorResponse(ctx: RequestContext, msg: string, code: number = 1, data: any = null, detail: any = null, reasonCode: string | null = null): Response {
|
|
17
|
+
// 记录拦截日志
|
|
18
|
+
if (ctx.requestId) {
|
|
19
|
+
// requestId/route/user/duration 等字段由 ALS 统一注入,避免在 msg 中重复拼接
|
|
20
|
+
Logger.info(
|
|
21
|
+
{
|
|
22
|
+
event: "request_blocked",
|
|
23
|
+
reason: msg,
|
|
24
|
+
reasonCode: reasonCode,
|
|
25
|
+
code: code,
|
|
26
|
+
detail: detail
|
|
27
|
+
},
|
|
28
|
+
"request blocked"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Response.json(
|
|
33
|
+
{
|
|
34
|
+
code: code,
|
|
35
|
+
msg: msg,
|
|
36
|
+
data: data,
|
|
37
|
+
detail: detail
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
headers: ctx.corsHeaders
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建最终响应(专用于 API 路由结尾)
|
|
47
|
+
* 自动处理 ctx.response/ctx.result,并记录请求日志
|
|
48
|
+
* @param ctx - 请求上下文
|
|
49
|
+
* @returns Response 对象
|
|
50
|
+
*/
|
|
51
|
+
export function FinalResponse(ctx: RequestContext): Response {
|
|
52
|
+
// 记录请求日志
|
|
53
|
+
if (ctx.api && ctx.requestId) {
|
|
54
|
+
// requestId/route/user/duration 等字段由 ALS 统一注入,避免在 msg 中重复拼接
|
|
55
|
+
Logger.info(
|
|
56
|
+
{
|
|
57
|
+
event: "request_done"
|
|
58
|
+
},
|
|
59
|
+
"request done"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1. 如果已经有 response,直接返回
|
|
64
|
+
if (ctx.response) {
|
|
65
|
+
return ctx.response;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. 如果有 result,格式化为响应
|
|
69
|
+
if (ctx.result !== undefined) {
|
|
70
|
+
let result = ctx.result;
|
|
71
|
+
|
|
72
|
+
// 如果是字符串,自动包裹为成功响应
|
|
73
|
+
if (typeof result === "string") {
|
|
74
|
+
result = {
|
|
75
|
+
code: 0,
|
|
76
|
+
msg: result
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// 如果是对象,自动补充 code: 0
|
|
80
|
+
else if (result && typeof result === "object") {
|
|
81
|
+
if (!("code" in result)) {
|
|
82
|
+
result = {
|
|
83
|
+
code: 0,
|
|
84
|
+
...result
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 处理 BigInt 序列化问题
|
|
90
|
+
if (result && typeof result === "object") {
|
|
91
|
+
const jsonString = JSON.stringify(result, (key, value) => (typeof value === "bigint" ? value.toString() : value));
|
|
92
|
+
return new Response(jsonString, {
|
|
93
|
+
headers: {
|
|
94
|
+
...ctx.corsHeaders,
|
|
95
|
+
"Content-Type": "application/json"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
return Response.json(result, {
|
|
100
|
+
headers: ctx.corsHeaders
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. 默认响应:没有生成响应
|
|
106
|
+
return Response.json(
|
|
107
|
+
{
|
|
108
|
+
code: 1,
|
|
109
|
+
msg: "未生成响应"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
headers: ctx.corsHeaders
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
package/utils/route.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标准化接口权限路径(用于写入/读取权限缓存时保持一致)
|
|
3
|
+
* 规则(简化版):method 大写;path 为空则为 '/';确保以 '/' 开头
|
|
4
|
+
* 说明:当前框架内 api.path 来源于目录结构生成、请求侧使用 URL.pathname,默认不包含 query/hash,且不期望出现尾部斜杠等异常输入。
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeApiPath(method: string, path: string): string {
|
|
7
|
+
const normalizedMethod = (method || "").toUpperCase();
|
|
8
|
+
let normalizedPath = path || "/";
|
|
9
|
+
|
|
10
|
+
if (!normalizedPath.startsWith("/")) {
|
|
11
|
+
normalizedPath = "/" + normalizedPath;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `${normalizedMethod}${normalizedPath}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 生成路由 Key(用于路由匹配 / 权限缓存 / 日志等场景统一使用)
|
|
19
|
+
* 格式:METHOD/path
|
|
20
|
+
*/
|
|
21
|
+
export function makeRouteKey(method: string, pathname: string): string {
|
|
22
|
+
return normalizeApiPath(method, pathname);
|
|
23
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { isPlainObject } from "es-toolkit";
|
|
6
|
+
import { get, set } from "es-toolkit/compat";
|
|
7
|
+
import { mergeAndConcat } from "merge-anything";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 加载配置选项
|
|
11
|
+
* - 该类型为 scanConfig 专用:按“就近放置”原则直接与实现放在同文件,避免拆到单独的 *Types.ts 造成目录噪音。
|
|
12
|
+
*/
|
|
13
|
+
export interface LoadConfigOptions {
|
|
14
|
+
/** 当前工作目录,默认 process.cwd() */
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** 目录数组:要搜索的目录路径(相对于 cwd) */
|
|
17
|
+
dirs: string[];
|
|
18
|
+
/** 文件数组:要匹配的文件名 */
|
|
19
|
+
files: string[];
|
|
20
|
+
/** 文件扩展名,默认 ['.js', '.ts', '.json'] */
|
|
21
|
+
extensions?: string[];
|
|
22
|
+
/** 加载模式:'first' = 返回第一个找到的配置(默认),'merge' = 合并所有配置 */
|
|
23
|
+
mode?: "merge" | "first";
|
|
24
|
+
/** 指定要提取的字段路径数组,如 ['menus', 'database.host'],为空则返回完整对象 */
|
|
25
|
+
paths?: string[];
|
|
26
|
+
/** 默认配置,会与找到的配置合并(优先级最低) */
|
|
27
|
+
defaults?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 扫描并合并配置文件(矩阵搜索:dirs × files)
|
|
32
|
+
* @param options - 加载选项
|
|
33
|
+
* @returns 合并后的配置对象(或第一个找到的配置)
|
|
34
|
+
*/
|
|
35
|
+
export async function scanConfig(options: LoadConfigOptions): Promise<Record<string, any>> {
|
|
36
|
+
const {
|
|
37
|
+
//
|
|
38
|
+
cwd = process.cwd(),
|
|
39
|
+
dirs,
|
|
40
|
+
files,
|
|
41
|
+
extensions = [".js", ".ts", ".json"],
|
|
42
|
+
mode = "first",
|
|
43
|
+
paths,
|
|
44
|
+
defaults = {}
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
// 参数验证
|
|
48
|
+
if (!Array.isArray(dirs) || dirs.length === 0) {
|
|
49
|
+
throw new Error("dirs 必须是非空数组");
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
52
|
+
throw new Error("files 必须是非空数组");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const configs: Record<string, any>[] = [];
|
|
56
|
+
|
|
57
|
+
// 矩阵搜索:dirs × files × extensions
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
// 如果是绝对路径则直接使用,否则拼接 cwd
|
|
60
|
+
const fullDir = isAbsolute(dir) ? dir : join(cwd, dir);
|
|
61
|
+
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
for (const ext of extensions) {
|
|
64
|
+
const fileName = file.endsWith(ext) ? file : file + ext;
|
|
65
|
+
const filePath = join(fullDir, fileName);
|
|
66
|
+
|
|
67
|
+
if (existsSync(filePath)) {
|
|
68
|
+
try {
|
|
69
|
+
const importUrl = pathToFileURL(filePath).href + `?t=${Date.now()}`;
|
|
70
|
+
|
|
71
|
+
// 动态导入配置文件(使用 import 断言处理 JSON)
|
|
72
|
+
let data: any;
|
|
73
|
+
|
|
74
|
+
if (ext === ".json") {
|
|
75
|
+
// JSON 文件使用 import 断言
|
|
76
|
+
const module = await import(importUrl, { with: { type: "json" } });
|
|
77
|
+
data = module.default;
|
|
78
|
+
} else {
|
|
79
|
+
// JS/TS 文件使用动态导入
|
|
80
|
+
const module = await import(importUrl);
|
|
81
|
+
data = module.default || module;
|
|
82
|
+
|
|
83
|
+
// 处理 async 函数导出(如 defineAddonConfig)
|
|
84
|
+
if (data instanceof Promise) {
|
|
85
|
+
data = await data;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 验证配置数据
|
|
90
|
+
if (!isPlainObject(data)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
configs.push(data);
|
|
95
|
+
|
|
96
|
+
// 如果模式为 'first',找到第一个配置后立即返回
|
|
97
|
+
if (mode === "first") {
|
|
98
|
+
const firstConfig = mergeAndConcat(defaults, data);
|
|
99
|
+
|
|
100
|
+
// 如果指定了 paths,则只返回指定路径的字段
|
|
101
|
+
if (paths && paths.length > 0) {
|
|
102
|
+
const result: Record<string, any> = {};
|
|
103
|
+
for (const path of paths) {
|
|
104
|
+
const value = get(firstConfig, path);
|
|
105
|
+
if (value !== undefined) {
|
|
106
|
+
set(result, path, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return firstConfig;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 找到后跳过同名文件的其他扩展名
|
|
116
|
+
break;
|
|
117
|
+
} catch {
|
|
118
|
+
// 保持静默:继续尝试下一个候选配置文件
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 合并配置(使用 mergeAndConcat 深度合并)
|
|
126
|
+
// 合并顺序:defaults ← configs[0] ← configs[1] ← ...
|
|
127
|
+
const finalConfig = mergeAndConcat(defaults, ...configs);
|
|
128
|
+
|
|
129
|
+
// 如果指定了 paths,则只返回指定路径的字段
|
|
130
|
+
if (paths && paths.length > 0) {
|
|
131
|
+
const result: Record<string, any> = {};
|
|
132
|
+
for (const path of paths) {
|
|
133
|
+
const value = get(finalConfig, path);
|
|
134
|
+
if (value !== undefined) {
|
|
135
|
+
set(result, path, value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return finalConfig;
|
|
142
|
+
}
|