befly 3.2.0 → 3.3.0
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/bin/index.ts +138 -0
- package/checks/conflict.ts +35 -25
- package/checks/table.ts +6 -6
- package/commands/addon.ts +57 -0
- package/commands/build.ts +74 -0
- package/commands/dev.ts +94 -0
- package/commands/index.ts +252 -0
- package/commands/script.ts +308 -0
- package/commands/start.ts +80 -0
- package/commands/syncApi.ts +328 -0
- package/{scripts → commands}/syncDb/apply.ts +2 -2
- package/{scripts → commands}/syncDb/constants.ts +13 -7
- package/{scripts → commands}/syncDb/ddl.ts +7 -5
- package/{scripts → commands}/syncDb/helpers.ts +18 -18
- package/{scripts → commands}/syncDb/index.ts +37 -23
- package/{scripts → commands}/syncDb/sqlite.ts +1 -1
- package/{scripts → commands}/syncDb/state.ts +10 -4
- package/{scripts → commands}/syncDb/table.ts +7 -7
- package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
- package/{scripts → commands}/syncDb/types.ts +5 -5
- package/{scripts → commands}/syncDb/version.ts +1 -1
- package/commands/syncDb.ts +35 -0
- package/commands/syncDev.ts +174 -0
- package/commands/syncMenu.ts +368 -0
- package/config/env.ts +4 -4
- package/config/menu.json +67 -0
- package/{utils/crypto.ts → lib/cipher.ts} +16 -67
- package/lib/database.ts +296 -0
- package/{utils → lib}/dbHelper.ts +102 -56
- package/{utils → lib}/jwt.ts +124 -151
- package/{utils → lib}/logger.ts +47 -24
- package/lib/middleware.ts +271 -0
- package/{utils → lib}/redisHelper.ts +4 -4
- package/{utils/validate.ts → lib/validator.ts} +101 -78
- package/lifecycle/bootstrap.ts +63 -0
- package/lifecycle/checker.ts +165 -0
- package/lifecycle/cluster.ts +241 -0
- package/lifecycle/lifecycle.ts +139 -0
- package/lifecycle/loader.ts +513 -0
- package/main.ts +14 -12
- package/package.json +21 -9
- package/paths.ts +34 -0
- package/plugins/cache.ts +187 -0
- package/plugins/db.ts +4 -4
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/router/api.ts +155 -0
- package/router/root.ts +53 -0
- package/router/static.ts +76 -0
- package/types/api.d.ts +0 -36
- package/types/befly.d.ts +8 -6
- package/types/common.d.ts +1 -1
- package/types/context.d.ts +3 -3
- package/types/util.d.ts +45 -0
- package/util.ts +301 -0
- package/config/fields.ts +0 -55
- package/config/regexAliases.ts +0 -51
- package/config/reserved.ts +0 -96
- package/scripts/syncDb/tests/constants.test.ts +0 -105
- package/scripts/syncDb/tests/ddl.test.ts +0 -134
- package/scripts/syncDb/tests/helpers.test.ts +0 -70
- package/scripts/syncDb.ts +0 -10
- package/types/index.d.ts +0 -450
- package/types/index.ts +0 -438
- package/types/validator.ts +0 -43
- package/utils/colors.ts +0 -221
- package/utils/database.ts +0 -348
- package/utils/helper.ts +0 -812
- package/utils/index.ts +0 -33
- package/utils/requestContext.ts +0 -167
- /package/{scripts → commands}/syncDb/schema.ts +0 -0
- /package/{utils → lib}/sqlBuilder.ts +0 -0
- /package/{utils → lib}/xml.ts +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中间件集合
|
|
3
|
+
* 整合所有请求处理中间件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isEmpty, isPlainObject } from 'es-toolkit/compat';
|
|
7
|
+
import { Env } from '../config/env.js';
|
|
8
|
+
import { Logger } from './logger.js';
|
|
9
|
+
import { Jwt } from './jwt.js';
|
|
10
|
+
import { Xml } from './xml.js';
|
|
11
|
+
import { Validator } from './validator.js';
|
|
12
|
+
import { pickFields } from '../util.js';
|
|
13
|
+
import type { ApiRoute } from '../types/api.js';
|
|
14
|
+
import type { RequestContext } from '../types/context.js';
|
|
15
|
+
import type { Plugin } from '../types/plugin.js';
|
|
16
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
17
|
+
|
|
18
|
+
// ========================================
|
|
19
|
+
// JWT 认证中间件
|
|
20
|
+
// ========================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 从请求头中提取并验证JWT token
|
|
24
|
+
*/
|
|
25
|
+
export async function authenticate(ctx: RequestContext): Promise<void> {
|
|
26
|
+
const authHeader = ctx.request.headers.get('authorization');
|
|
27
|
+
|
|
28
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
29
|
+
const token = authHeader.substring(7);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const payload = await Jwt.verify(token);
|
|
33
|
+
ctx.user = payload;
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
ctx.user = {};
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
ctx.user = {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ========================================
|
|
43
|
+
// CORS 中间件
|
|
44
|
+
// ========================================
|
|
45
|
+
|
|
46
|
+
export interface CorsResult {
|
|
47
|
+
headers: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 设置 CORS 选项
|
|
52
|
+
* 根据环境变量或请求头动态设置跨域配置
|
|
53
|
+
* @param req - 请求对象
|
|
54
|
+
* @returns CORS 配置对象
|
|
55
|
+
*/
|
|
56
|
+
export const setCorsOptions = (req: Request): CorsResult => {
|
|
57
|
+
return {
|
|
58
|
+
headers: {
|
|
59
|
+
'Access-Control-Allow-Origin': Env.ALLOWED_ORIGIN || req.headers.get('origin') || '*',
|
|
60
|
+
'Access-Control-Allow-Methods': Env.ALLOWED_METHODS || 'GET, POST, PUT, DELETE, OPTIONS',
|
|
61
|
+
'Access-Control-Allow-Headers': Env.ALLOWED_HEADERS || 'Content-Type, Authorization, authorization, token',
|
|
62
|
+
'Access-Control-Expose-Headers': Env.EXPOSE_HEADERS || 'Content-Range, X-Content-Range, Authorization, authorization, token',
|
|
63
|
+
'Access-Control-Max-Age': Env.MAX_AGE || 86400,
|
|
64
|
+
'Access-Control-Allow-Credentials': Env.ALLOW_CREDENTIALS || 'true'
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 处理OPTIONS预检请求
|
|
71
|
+
* @param corsOptions - CORS 配置对象
|
|
72
|
+
* @returns 204 响应
|
|
73
|
+
*/
|
|
74
|
+
export function handleOptionsRequest(corsOptions: CorsResult): Response {
|
|
75
|
+
return new Response(null, {
|
|
76
|
+
status: 204,
|
|
77
|
+
headers: corsOptions.headers
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ========================================
|
|
82
|
+
// 参数解析中间件
|
|
83
|
+
// ========================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 解析GET请求参数
|
|
87
|
+
*/
|
|
88
|
+
export function parseGetParams(api: ApiRoute, ctx: RequestContext): void {
|
|
89
|
+
const url = new URL(ctx.request.url);
|
|
90
|
+
|
|
91
|
+
if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
|
|
92
|
+
ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(api.fields));
|
|
93
|
+
} else {
|
|
94
|
+
ctx.body = Object.fromEntries(url.searchParams);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 解析POST请求参数
|
|
100
|
+
*/
|
|
101
|
+
export async function parsePostParams(api: ApiRoute, ctx: RequestContext): Promise<boolean> {
|
|
102
|
+
try {
|
|
103
|
+
const contentType = ctx.request.headers.get('content-type') || '';
|
|
104
|
+
|
|
105
|
+
if (contentType.indexOf('json') !== -1) {
|
|
106
|
+
try {
|
|
107
|
+
ctx.body = await ctx.request.json();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// 如果没有 body 或 JSON 解析失败,默认为空对象
|
|
110
|
+
ctx.body = {};
|
|
111
|
+
}
|
|
112
|
+
} else if (contentType.indexOf('xml') !== -1) {
|
|
113
|
+
const textData = await ctx.request.text();
|
|
114
|
+
const xmlData = Xml.parse(textData);
|
|
115
|
+
ctx.body = xmlData?.xml ? xmlData.xml : xmlData;
|
|
116
|
+
} else if (contentType.indexOf('form-data') !== -1) {
|
|
117
|
+
ctx.body = await ctx.request.formData();
|
|
118
|
+
} else if (contentType.indexOf('x-www-form-urlencoded') !== -1) {
|
|
119
|
+
const text = await ctx.request.text();
|
|
120
|
+
const formData = new URLSearchParams(text);
|
|
121
|
+
ctx.body = Object.fromEntries(formData);
|
|
122
|
+
} else {
|
|
123
|
+
ctx.body = {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isPlainObject(api.fields) && !isEmpty(api.fields)) {
|
|
127
|
+
ctx.body = pickFields(ctx.body, Object.keys(api.fields));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
Logger.error('处理请求参数时发生错误', err);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ========================================
|
|
138
|
+
// 权限验证中间件
|
|
139
|
+
// ========================================
|
|
140
|
+
|
|
141
|
+
export interface PermissionResult {
|
|
142
|
+
code: 0 | 1;
|
|
143
|
+
msg: string;
|
|
144
|
+
data: any;
|
|
145
|
+
[key: string]: any;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 检查权限
|
|
150
|
+
*
|
|
151
|
+
* 验证流程:
|
|
152
|
+
* 1. auth=false → 公开接口,直接通过
|
|
153
|
+
* 2. auth=true → 需要登录认证
|
|
154
|
+
* - 未登录 → 拒绝
|
|
155
|
+
* - roleCode='dev' → 超级管理员,拥有所有权限
|
|
156
|
+
* - 其他角色 → 检查接口是否在角色的可访问接口列表中(通过 Redis SISMEMBER 预判断)
|
|
157
|
+
*
|
|
158
|
+
* @param api - API 路由配置
|
|
159
|
+
* @param ctx - 请求上下文
|
|
160
|
+
* @param hasPermission - 是否有权限(通过 Redis SISMEMBER 预先判断)
|
|
161
|
+
* @returns 统一的响应格式 { code: 0/1, msg, data, ...extra }
|
|
162
|
+
*/
|
|
163
|
+
export function checkPermission(api: ApiRoute, ctx: RequestContext, hasPermission: boolean = false): PermissionResult {
|
|
164
|
+
// 1. 公开接口(auth=false),无需验证
|
|
165
|
+
if (api.auth === false) {
|
|
166
|
+
return { code: 0, msg: '', data: {} };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. 需要登录的接口(auth=true)
|
|
170
|
+
// 2.1 检查是否登录
|
|
171
|
+
if (!ctx.user?.id) {
|
|
172
|
+
return {
|
|
173
|
+
code: 1,
|
|
174
|
+
msg: '未登录',
|
|
175
|
+
data: {},
|
|
176
|
+
login: 'no'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2.2 dev 角色拥有所有权限(超级管理员)
|
|
181
|
+
if (ctx.user.roleCode === 'dev') {
|
|
182
|
+
return { code: 0, msg: '', data: {} };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2.3 检查接口权限(基于 Redis SISMEMBER 预判断结果)
|
|
186
|
+
if (!hasPermission) {
|
|
187
|
+
return {
|
|
188
|
+
code: 1,
|
|
189
|
+
msg: '没有权限访问此接口',
|
|
190
|
+
data: {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 验证通过
|
|
195
|
+
return { code: 0, msg: '', data: {} };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ========================================
|
|
199
|
+
// 插件钩子中间件
|
|
200
|
+
// ========================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 执行所有插件的onGet钩子
|
|
204
|
+
*/
|
|
205
|
+
export async function executePluginHooks(pluginLists: Plugin[], appContext: BeflyContext, ctx: RequestContext): Promise<void> {
|
|
206
|
+
for await (const plugin of pluginLists) {
|
|
207
|
+
try {
|
|
208
|
+
if (typeof plugin?.onGet === 'function') {
|
|
209
|
+
await plugin?.onGet(appContext, ctx, ctx.request);
|
|
210
|
+
}
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
Logger.error('插件处理请求时发生错误', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ========================================
|
|
218
|
+
// 请求日志中间件
|
|
219
|
+
// ========================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 过滤日志字段(内部函数)
|
|
223
|
+
* 用于从请求体中排除敏感字段(如密码、令牌等)
|
|
224
|
+
* @param body - 请求体对象
|
|
225
|
+
* @param excludeFields - 要排除的字段名(逗号分隔)
|
|
226
|
+
* @returns 过滤后的对象
|
|
227
|
+
*/
|
|
228
|
+
function filterLogFields(body: any, excludeFields: string = ''): any {
|
|
229
|
+
if (!body || (!isPlainObject(body) && !Array.isArray(body))) return body;
|
|
230
|
+
|
|
231
|
+
const fieldsArray = excludeFields
|
|
232
|
+
.split(',')
|
|
233
|
+
.map((field) => field.trim())
|
|
234
|
+
.filter((field) => field.length > 0);
|
|
235
|
+
|
|
236
|
+
const filtered: any = {};
|
|
237
|
+
for (const [key, value] of Object.entries(body)) {
|
|
238
|
+
if (!fieldsArray.includes(key)) {
|
|
239
|
+
filtered[key] = value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return filtered;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 记录请求日志
|
|
247
|
+
*/
|
|
248
|
+
export function logRequest(apiPath: string, ctx: RequestContext): void {
|
|
249
|
+
Logger.info({
|
|
250
|
+
msg: '通用接口日志',
|
|
251
|
+
请求路径: apiPath,
|
|
252
|
+
请求方法: ctx.request.method,
|
|
253
|
+
用户信息: ctx.user,
|
|
254
|
+
请求参数: filterLogFields(ctx.body, Env.LOG_EXCLUDE_FIELDS),
|
|
255
|
+
耗时: Date.now() - ctx.startTime + 'ms'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ========================================
|
|
260
|
+
// 参数验证中间件
|
|
261
|
+
// ========================================
|
|
262
|
+
|
|
263
|
+
// 创建全局validator实例
|
|
264
|
+
const validator = new Validator();
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 验证请求参数
|
|
268
|
+
*/
|
|
269
|
+
export function validateParams(api: ApiRoute, ctx: RequestContext) {
|
|
270
|
+
return validator.validate(ctx.body, api.fields, api.required);
|
|
271
|
+
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { RedisClient } from 'bun';
|
|
7
7
|
import { Env } from '../config/env.js';
|
|
8
|
-
import { Logger } from '
|
|
9
|
-
import {
|
|
8
|
+
import { Logger } from '../lib/logger.js';
|
|
9
|
+
import { Database } from './database.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Redis 键前缀
|
|
@@ -19,9 +19,9 @@ const prefix = Env.REDIS_KEY_PREFIX ? `${Env.REDIS_KEY_PREFIX}:` : '';
|
|
|
19
19
|
* @throws 如果客户端未初始化
|
|
20
20
|
*/
|
|
21
21
|
function getClient(): RedisClient {
|
|
22
|
-
const client = getRedis();
|
|
22
|
+
const client = Database.getRedis();
|
|
23
23
|
if (!client) {
|
|
24
|
-
throw new Error('Redis 客户端未初始化,请先调用
|
|
24
|
+
throw new Error('Redis 客户端未初始化,请先调用 Database.connectRedis()');
|
|
25
25
|
}
|
|
26
26
|
return client;
|
|
27
27
|
}
|
|
@@ -1,18 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 数据验证器 -
|
|
3
|
-
*
|
|
2
|
+
* 数据验证器 - Befly 项目专用
|
|
3
|
+
* 内置 RegexAliases,直接使用 util.ts 中的 parseRule
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { RegexAliases } from '../config/regexAliases.js';
|
|
9
|
-
import type { TableDefinition, FieldRule, ParsedFieldRule } from '../types/common.js';
|
|
6
|
+
import { parseRule } from '../util.js';
|
|
7
|
+
import type { TableDefinition, FieldRule } from '../types/common.js';
|
|
10
8
|
import type { ValidationResult, ValidationError } from '../types/validator';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
11
|
+
* 内置正则表达式别名(项目专用)
|
|
12
|
+
*/
|
|
13
|
+
const RegexAliases = {
|
|
14
|
+
// 数字类型
|
|
15
|
+
number: '^\\d+$',
|
|
16
|
+
integer: '^-?\\d+$',
|
|
17
|
+
float: '^-?\\d+(\\.\\d+)?$',
|
|
18
|
+
positive: '^[1-9]\\d*$',
|
|
19
|
+
negative: '^-\\d+$',
|
|
20
|
+
zero: '^0$',
|
|
21
|
+
|
|
22
|
+
// 字符串类型
|
|
23
|
+
word: '^[a-zA-Z]+$',
|
|
24
|
+
alphanumeric: '^[a-zA-Z0-9]+$',
|
|
25
|
+
alphanumeric_: '^[a-zA-Z0-9_]+$',
|
|
26
|
+
lowercase: '^[a-z]+$',
|
|
27
|
+
uppercase: '^[A-Z]+$',
|
|
28
|
+
|
|
29
|
+
// 中文
|
|
30
|
+
chinese: '^[\\u4e00-\\u9fa5]+$',
|
|
31
|
+
chinese_word: '^[\\u4e00-\\u9fa5a-zA-Z]+$',
|
|
32
|
+
|
|
33
|
+
// 常用格式
|
|
34
|
+
email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
35
|
+
phone: '^1[3-9]\\d{9}$',
|
|
36
|
+
url: '^https?://',
|
|
37
|
+
ip: '^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$',
|
|
38
|
+
domain: '^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$',
|
|
39
|
+
|
|
40
|
+
// 特殊格式
|
|
41
|
+
uuid: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
42
|
+
hex: '^[0-9a-fA-F]+$',
|
|
43
|
+
base64: '^[A-Za-z0-9+/=]+$',
|
|
44
|
+
md5: '^[a-f0-9]{32}$',
|
|
45
|
+
sha1: '^[a-f0-9]{40}$',
|
|
46
|
+
sha256: '^[a-f0-9]{64}$',
|
|
47
|
+
|
|
48
|
+
// 日期时间
|
|
49
|
+
date: '^\\d{4}-\\d{2}-\\d{2}$',
|
|
50
|
+
time: '^\\d{2}:\\d{2}:\\d{2}$',
|
|
51
|
+
datetime: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}',
|
|
52
|
+
year: '^\\d{4}$',
|
|
53
|
+
month: '^(0[1-9]|1[0-2])$',
|
|
54
|
+
day: '^(0[1-9]|[12]\\d|3[01])$',
|
|
55
|
+
|
|
56
|
+
// 代码相关
|
|
57
|
+
variable: '^[a-zA-Z_][a-zA-Z0-9_]*$',
|
|
58
|
+
constant: '^[A-Z][A-Z0-9_]*$',
|
|
59
|
+
package: '^[a-z][a-z0-9-]*$',
|
|
60
|
+
|
|
61
|
+
// 证件相关
|
|
62
|
+
id_card: '^\\d{17}[\\dXx]$',
|
|
63
|
+
passport: '^[a-zA-Z0-9]{5,17}$',
|
|
64
|
+
|
|
65
|
+
// 空值
|
|
66
|
+
empty: '^$',
|
|
67
|
+
notempty: '.+'
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 验证器类(Befly 项目专用)
|
|
72
|
+
* 内置 RegexAliases,直接使用 util.ts 中的 parseRule
|
|
14
73
|
*/
|
|
15
74
|
export class Validator {
|
|
75
|
+
/** 正则别名映射(内置) */
|
|
76
|
+
private readonly regexAliases: Record<string, string> = RegexAliases;
|
|
77
|
+
|
|
16
78
|
/**
|
|
17
79
|
* 验证数据
|
|
18
80
|
* @param data - 要验证的数据对象
|
|
@@ -107,25 +169,18 @@ export class Validator {
|
|
|
107
169
|
|
|
108
170
|
/**
|
|
109
171
|
* 解析 regex 别名
|
|
110
|
-
* 如果 regex 以 @ 开头,则从别名表中获取实际正则表达式
|
|
111
|
-
* @param regex - 原始 regex 或别名(如 @number)
|
|
112
|
-
* @returns 实际的正则表达式字符串
|
|
113
172
|
*/
|
|
114
173
|
private resolveRegexAlias(regex: string | null): string | null {
|
|
115
174
|
if (!regex || typeof regex !== 'string') {
|
|
116
175
|
return regex;
|
|
117
176
|
}
|
|
118
177
|
|
|
119
|
-
// 检查是否是别名(以 @ 开头)
|
|
120
178
|
if (regex.startsWith('@')) {
|
|
121
179
|
const aliasName = regex.substring(1);
|
|
122
|
-
const resolvedRegex =
|
|
123
|
-
|
|
180
|
+
const resolvedRegex = this.regexAliases[aliasName];
|
|
124
181
|
if (resolvedRegex) {
|
|
125
182
|
return resolvedRegex;
|
|
126
183
|
}
|
|
127
|
-
|
|
128
|
-
// 别名不存在,返回原值(让后续验证报错)
|
|
129
184
|
return regex;
|
|
130
185
|
}
|
|
131
186
|
|
|
@@ -139,14 +194,12 @@ export class Validator {
|
|
|
139
194
|
const parsed = parseRule(rule);
|
|
140
195
|
let { name, type, min, max, regex } = parsed;
|
|
141
196
|
|
|
142
|
-
// 解析 regex 别名
|
|
143
197
|
regex = this.resolveRegexAlias(regex);
|
|
144
198
|
|
|
145
199
|
switch (type.toLowerCase()) {
|
|
146
200
|
case 'number':
|
|
147
201
|
return this.validateNumber(value, name, min, max, regex, fieldName);
|
|
148
202
|
case 'string':
|
|
149
|
-
return this.validateString(value, name, min, max, regex, fieldName);
|
|
150
203
|
case 'text':
|
|
151
204
|
return this.validateString(value, name, min, max, regex, fieldName);
|
|
152
205
|
case 'array_string':
|
|
@@ -162,7 +215,7 @@ export class Validator {
|
|
|
162
215
|
*/
|
|
163
216
|
private validateNumber(value: any, name: string, min: number | null, max: number | null, spec: string | null, fieldName: string): ValidationError {
|
|
164
217
|
try {
|
|
165
|
-
if (
|
|
218
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
166
219
|
return `${name}(${fieldName})必须是数字`;
|
|
167
220
|
}
|
|
168
221
|
|
|
@@ -196,7 +249,7 @@ export class Validator {
|
|
|
196
249
|
*/
|
|
197
250
|
private validateString(value: any, name: string, min: number | null, max: number | null, spec: string | null, fieldName: string): ValidationError {
|
|
198
251
|
try {
|
|
199
|
-
if (
|
|
252
|
+
if (typeof value !== 'string') {
|
|
200
253
|
return `${name}(${fieldName})必须是字符串`;
|
|
201
254
|
}
|
|
202
255
|
|
|
@@ -262,64 +315,39 @@ export class Validator {
|
|
|
262
315
|
}
|
|
263
316
|
|
|
264
317
|
/**
|
|
265
|
-
*
|
|
266
|
-
* @param data - 要验证的数据
|
|
267
|
-
* @param rules - 验证规则
|
|
268
|
-
* @param required - 必填字段
|
|
269
|
-
* @returns 验证结果
|
|
318
|
+
* 验证单个值
|
|
270
319
|
*/
|
|
271
|
-
|
|
272
|
-
static validate(value: any, rule: string): { valid: boolean; value: any; errors: string[] };
|
|
273
|
-
static validate(dataOrValue: any, rulesOrRule: any, required: string[] = []): any {
|
|
274
|
-
// 如果第二个参数是字符串,则是单值验证
|
|
275
|
-
if (typeof rulesOrRule === 'string') {
|
|
276
|
-
return Validator.validateSingleValue(dataOrValue, rulesOrRule);
|
|
277
|
-
}
|
|
278
|
-
// 否则是对象验证
|
|
279
|
-
return validator.validate(dataOrValue, rulesOrRule, required);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* 验证单个值(静态方法)
|
|
284
|
-
* @param value - 要验证的值
|
|
285
|
-
* @param rule - 验证规则字符串
|
|
286
|
-
* @returns 验证结果 { valid: boolean, value: any, errors: string[] }
|
|
287
|
-
*/
|
|
288
|
-
static validateSingleValue(value: any, rule: string): { valid: boolean; value: any; errors: string[] } {
|
|
320
|
+
validateSingleValue(value: any, rule: string): { valid: boolean; value: any; errors: string[] } {
|
|
289
321
|
const parsed = parseRule(rule);
|
|
290
322
|
let { name, type, min, max, regex, default: defaultValue } = parsed;
|
|
291
323
|
|
|
292
|
-
|
|
293
|
-
if (regex && typeof regex === 'string' && regex.startsWith('@')) {
|
|
294
|
-
const aliasName = regex.substring(1);
|
|
295
|
-
const resolvedRegex = RegexAliases[aliasName as keyof typeof RegexAliases];
|
|
296
|
-
if (resolvedRegex) {
|
|
297
|
-
regex = resolvedRegex;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
324
|
+
regex = this.resolveRegexAlias(regex);
|
|
300
325
|
|
|
301
326
|
// 处理 undefined/null 值,使用默认值
|
|
302
327
|
if (value === undefined || value === null) {
|
|
303
328
|
if (defaultValue !== 'null' && defaultValue !== null) {
|
|
304
|
-
// 特殊处理数组类型的默认值字符串
|
|
305
329
|
if ((type === 'array_string' || type === 'array_text') && typeof defaultValue === 'string') {
|
|
306
330
|
if (defaultValue === '[]') {
|
|
307
331
|
return { valid: true, value: [], errors: [] };
|
|
308
332
|
}
|
|
309
|
-
// 尝试解析 JSON 格式的数组字符串
|
|
310
333
|
try {
|
|
311
334
|
const parsedArray = JSON.parse(defaultValue);
|
|
312
335
|
if (Array.isArray(parsedArray)) {
|
|
313
336
|
return { valid: true, value: parsedArray, errors: [] };
|
|
314
337
|
}
|
|
315
338
|
} catch {
|
|
316
|
-
// 解析失败,使用空数组
|
|
317
339
|
return { valid: true, value: [], errors: [] };
|
|
318
340
|
}
|
|
319
341
|
}
|
|
342
|
+
// 数字类型默认值转换
|
|
343
|
+
if (type === 'number' && typeof defaultValue === 'string') {
|
|
344
|
+
const numValue = Number(defaultValue);
|
|
345
|
+
if (!isNaN(numValue)) {
|
|
346
|
+
return { valid: true, value: numValue, errors: [] };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
320
349
|
return { valid: true, value: defaultValue, errors: [] };
|
|
321
350
|
}
|
|
322
|
-
// 如果没有默认值,根据类型返回默认值
|
|
323
351
|
if (type === 'number') {
|
|
324
352
|
return { valid: true, value: 0, errors: [] };
|
|
325
353
|
} else if (type === 'array_string' || type === 'array_text') {
|
|
@@ -344,7 +372,7 @@ export class Validator {
|
|
|
344
372
|
// 类型验证
|
|
345
373
|
switch (type.toLowerCase()) {
|
|
346
374
|
case 'number':
|
|
347
|
-
if (
|
|
375
|
+
if (typeof convertedValue !== 'number' || Number.isNaN(convertedValue)) {
|
|
348
376
|
errors.push(`${name || '值'}必须是数字`);
|
|
349
377
|
}
|
|
350
378
|
if (min !== null && convertedValue < min) {
|
|
@@ -359,15 +387,15 @@ export class Validator {
|
|
|
359
387
|
if (!regExp.test(String(convertedValue))) {
|
|
360
388
|
errors.push(`${name || '值'}格式不正确`);
|
|
361
389
|
}
|
|
362
|
-
} catch {
|
|
363
|
-
errors.push(`${name || '值'}
|
|
390
|
+
} catch (e: any) {
|
|
391
|
+
errors.push(`${name || '值'}的正则表达式格式错误: ${e.message}`);
|
|
364
392
|
}
|
|
365
393
|
}
|
|
366
394
|
break;
|
|
367
395
|
|
|
368
396
|
case 'string':
|
|
369
397
|
case 'text':
|
|
370
|
-
if (
|
|
398
|
+
if (typeof convertedValue !== 'string') {
|
|
371
399
|
errors.push(`${name || '值'}必须是字符串`);
|
|
372
400
|
}
|
|
373
401
|
if (min !== null && convertedValue.length < min) {
|
|
@@ -422,13 +450,25 @@ export class Validator {
|
|
|
422
450
|
};
|
|
423
451
|
}
|
|
424
452
|
|
|
453
|
+
/**
|
|
454
|
+
* 静态方法:快速验证
|
|
455
|
+
*/
|
|
456
|
+
static validate(data: Record<string, any>, rules: TableDefinition, required?: string[]): ValidationResult;
|
|
457
|
+
static validate(value: any, rule: string): { valid: boolean; value: any; errors: string[] };
|
|
458
|
+
static validate(dataOrValue: any, rulesOrRule: any, required?: string[]): any {
|
|
459
|
+
const validator = new Validator();
|
|
460
|
+
|
|
461
|
+
if (typeof rulesOrRule === 'string') {
|
|
462
|
+
return validator.validateSingleValue(dataOrValue, rulesOrRule);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return validator.validate(dataOrValue, rulesOrRule, required || []);
|
|
466
|
+
}
|
|
467
|
+
|
|
425
468
|
/**
|
|
426
469
|
* 检查验证是否通过
|
|
427
|
-
* @param result - 验证结果
|
|
428
|
-
* @returns 是否通过
|
|
429
470
|
*/
|
|
430
471
|
static isPassed(result: ValidationResult | { valid: boolean; value: any; errors: string[] }): boolean {
|
|
431
|
-
// 支持两种结果格式
|
|
432
472
|
if ('valid' in result) {
|
|
433
473
|
return result.valid === true;
|
|
434
474
|
}
|
|
@@ -437,8 +477,6 @@ export class Validator {
|
|
|
437
477
|
|
|
438
478
|
/**
|
|
439
479
|
* 检查验证是否失败
|
|
440
|
-
* @param result - 验证结果
|
|
441
|
-
* @returns 是否失败
|
|
442
480
|
*/
|
|
443
481
|
static isFailed(result: ValidationResult): boolean {
|
|
444
482
|
return result.code === 1;
|
|
@@ -446,15 +484,11 @@ export class Validator {
|
|
|
446
484
|
|
|
447
485
|
/**
|
|
448
486
|
* 获取第一个错误信息
|
|
449
|
-
* @param result - 验证结果
|
|
450
|
-
* @returns 错误信息
|
|
451
487
|
*/
|
|
452
488
|
static getFirstError(result: ValidationResult | { valid: boolean; value: any; errors: string[] }): string | null {
|
|
453
|
-
// 单值验证结果
|
|
454
489
|
if ('valid' in result) {
|
|
455
490
|
return result.errors.length > 0 ? result.errors[0] : null;
|
|
456
491
|
}
|
|
457
|
-
// 对象验证结果
|
|
458
492
|
if (result.code === 0) return null;
|
|
459
493
|
if (!result.fields) return null;
|
|
460
494
|
const errors = Object.values(result.fields);
|
|
@@ -463,15 +497,11 @@ export class Validator {
|
|
|
463
497
|
|
|
464
498
|
/**
|
|
465
499
|
* 获取所有错误信息
|
|
466
|
-
* @param result - 验证结果
|
|
467
|
-
* @returns 错误信息数组
|
|
468
500
|
*/
|
|
469
501
|
static getAllErrors(result: ValidationResult | { valid: boolean; value: any; errors: string[] }): string[] {
|
|
470
|
-
// 单值验证结果
|
|
471
502
|
if ('valid' in result) {
|
|
472
503
|
return result.errors;
|
|
473
504
|
}
|
|
474
|
-
// 对象验证结果
|
|
475
505
|
if (result.code === 0) return [];
|
|
476
506
|
if (!result.fields) return [];
|
|
477
507
|
return Object.values(result.fields);
|
|
@@ -479,15 +509,8 @@ export class Validator {
|
|
|
479
509
|
|
|
480
510
|
/**
|
|
481
511
|
* 获取错误字段列表
|
|
482
|
-
* @param result - 验证结果
|
|
483
|
-
* @returns 错误字段名数组
|
|
484
512
|
*/
|
|
485
513
|
static getErrorFields(result: ValidationResult): string[] {
|
|
486
514
|
return result.code === 0 ? [] : Object.keys(result.fields);
|
|
487
515
|
}
|
|
488
516
|
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* 导出验证器实例
|
|
492
|
-
*/
|
|
493
|
-
export const validator = new Validator();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务启动引导器
|
|
3
|
+
* 负责组装和启动Bun HTTP服务器
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Logger } from '../lib/logger.js';
|
|
7
|
+
import { calcPerfTime, No } from '../util.js';
|
|
8
|
+
import { Env } from '../config/env.js';
|
|
9
|
+
import { rootHandler } from '../router/root.js';
|
|
10
|
+
import { apiHandler } from '../router/api.js';
|
|
11
|
+
import { staticHandler } from '../router/static.js';
|
|
12
|
+
|
|
13
|
+
import type { Server } from 'bun';
|
|
14
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
15
|
+
import type { ApiRoute } from '../types/api.js';
|
|
16
|
+
import type { Plugin } from '../types/plugin.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 引导器类
|
|
20
|
+
*/
|
|
21
|
+
export class Bootstrap {
|
|
22
|
+
/**
|
|
23
|
+
* 启动HTTP服务器
|
|
24
|
+
* @param befly - Befly实例(需要访问 apiRoutes, pluginLists, appContext, appOptions)
|
|
25
|
+
* @param callback - 启动后的回调函数
|
|
26
|
+
*/
|
|
27
|
+
static async start(
|
|
28
|
+
befly: {
|
|
29
|
+
apiRoutes: Map<string, ApiRoute>;
|
|
30
|
+
pluginLists: Plugin[];
|
|
31
|
+
appContext: BeflyContext;
|
|
32
|
+
appOptions: any;
|
|
33
|
+
},
|
|
34
|
+
callback?: (server: Server) => void
|
|
35
|
+
): Promise<Server> {
|
|
36
|
+
const startTime = Bun.nanoseconds();
|
|
37
|
+
|
|
38
|
+
const server = Bun.serve({
|
|
39
|
+
port: Env.APP_PORT,
|
|
40
|
+
hostname: Env.APP_HOST,
|
|
41
|
+
routes: {
|
|
42
|
+
'/': rootHandler,
|
|
43
|
+
'/api/*': apiHandler(befly.apiRoutes, befly.pluginLists, befly.appContext),
|
|
44
|
+
'/*': staticHandler,
|
|
45
|
+
...(befly.appOptions.routes || {})
|
|
46
|
+
},
|
|
47
|
+
error: (error: Error) => {
|
|
48
|
+
Logger.error('服务启动时发生错误', error);
|
|
49
|
+
return Response.json(No('内部服务器错误'));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const finalStartupTime = calcPerfTime(startTime);
|
|
54
|
+
Logger.info(`Befly 服务器启动成功! 服务器启动耗时: ${finalStartupTime}`);
|
|
55
|
+
Logger.info(`服务器监听地址: http://${Env.APP_HOST}:${Env.APP_PORT}`);
|
|
56
|
+
|
|
57
|
+
if (callback && typeof callback === 'function') {
|
|
58
|
+
callback(server);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return server;
|
|
62
|
+
}
|
|
63
|
+
}
|