befly 3.8.1 → 3.8.3
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/check.ts +201 -95
- package/env.ts +3 -3
- package/{plugins/cache.ts → lib/cacheHelper.ts} +99 -35
- package/lib/redisHelper.ts +49 -65
- package/lib/validator.ts +29 -32
- package/loader/loadApis.ts +172 -0
- package/loader/loadPlugins.ts +244 -0
- package/main.ts +116 -23
- package/menu.json +14 -14
- package/package.json +4 -4
- package/paths.ts +7 -0
- package/types/common.d.ts +28 -7
- package/util.ts +76 -36
- package/lib/addon.ts +0 -77
- package/lifecycle/bootstrap.ts +0 -63
- package/lifecycle/checker.ts +0 -122
- package/lifecycle/lifecycle.ts +0 -104
- package/lifecycle/loader.ts +0 -427
- package/plugins/db.ts +0 -59
- package/plugins/logger.ts +0 -27
- package/plugins/redis.ts +0 -41
package/lib/redisHelper.ts
CHANGED
|
@@ -14,22 +14,22 @@ import { Database } from './database.js';
|
|
|
14
14
|
const prefix = Env.REDIS_KEY_PREFIX ? `${Env.REDIS_KEY_PREFIX}:` : '';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @returns Redis 客户端实例
|
|
19
|
-
* @throws 如果客户端未初始化
|
|
17
|
+
* Redis 助手类
|
|
20
18
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
export class RedisHelper {
|
|
20
|
+
private client: RedisClient;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 构造函数
|
|
24
|
+
*/
|
|
25
|
+
constructor() {
|
|
26
|
+
const client = Database.getRedis();
|
|
27
|
+
if (!client) {
|
|
28
|
+
throw new Error('Redis 客户端未初始化,请先调用 Database.connectRedis()');
|
|
29
|
+
}
|
|
30
|
+
this.client = client;
|
|
25
31
|
}
|
|
26
|
-
return client;
|
|
27
|
-
}
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* Redis 助手对象
|
|
31
|
-
*/
|
|
32
|
-
export const RedisHelper = {
|
|
33
33
|
/**
|
|
34
34
|
* 设置对象到 Redis
|
|
35
35
|
* @param key - 键名
|
|
@@ -39,19 +39,18 @@ export const RedisHelper = {
|
|
|
39
39
|
*/
|
|
40
40
|
async setObject<T = any>(key: string, obj: T, ttl: number | null = null): Promise<string | null> {
|
|
41
41
|
try {
|
|
42
|
-
const client = getClient();
|
|
43
42
|
const data = JSON.stringify(obj);
|
|
44
43
|
const pkey = `${prefix}${key}`;
|
|
45
44
|
|
|
46
45
|
if (ttl) {
|
|
47
|
-
return await client.setex(pkey, ttl, data);
|
|
46
|
+
return await this.client.setex(pkey, ttl, data);
|
|
48
47
|
}
|
|
49
|
-
return await client.set(pkey, data);
|
|
48
|
+
return await this.client.set(pkey, data);
|
|
50
49
|
} catch (error: any) {
|
|
51
50
|
Logger.error('Redis setObject 错误', error);
|
|
52
51
|
return null;
|
|
53
52
|
}
|
|
54
|
-
}
|
|
53
|
+
}
|
|
55
54
|
|
|
56
55
|
/**
|
|
57
56
|
* 从 Redis 获取对象
|
|
@@ -60,15 +59,14 @@ export const RedisHelper = {
|
|
|
60
59
|
*/
|
|
61
60
|
async getObject<T = any>(key: string): Promise<T | null> {
|
|
62
61
|
try {
|
|
63
|
-
const client = getClient();
|
|
64
62
|
const pkey = `${prefix}${key}`;
|
|
65
|
-
const data = await client.get(pkey);
|
|
63
|
+
const data = await this.client.get(pkey);
|
|
66
64
|
return data ? JSON.parse(data) : null;
|
|
67
65
|
} catch (error: any) {
|
|
68
66
|
Logger.error('Redis getObject 错误', error);
|
|
69
67
|
return null;
|
|
70
68
|
}
|
|
71
|
-
}
|
|
69
|
+
}
|
|
72
70
|
|
|
73
71
|
/**
|
|
74
72
|
* 从 Redis 删除对象
|
|
@@ -76,13 +74,12 @@ export const RedisHelper = {
|
|
|
76
74
|
*/
|
|
77
75
|
async delObject(key: string): Promise<void> {
|
|
78
76
|
try {
|
|
79
|
-
const client = getClient();
|
|
80
77
|
const pkey = `${prefix}${key}`;
|
|
81
|
-
await client.del(pkey);
|
|
78
|
+
await this.client.del(pkey);
|
|
82
79
|
} catch (error: any) {
|
|
83
80
|
Logger.error('Redis delObject 错误', error);
|
|
84
81
|
}
|
|
85
|
-
}
|
|
82
|
+
}
|
|
86
83
|
|
|
87
84
|
/**
|
|
88
85
|
* 生成基于时间的唯一 ID
|
|
@@ -92,17 +89,16 @@ export const RedisHelper = {
|
|
|
92
89
|
* @returns 唯一 ID (14位纯数字)
|
|
93
90
|
*/
|
|
94
91
|
async genTimeID(): Promise<number> {
|
|
95
|
-
const client = getClient();
|
|
96
92
|
const timestamp = Math.floor(Date.now() / 1000); // 秒级时间戳
|
|
97
93
|
const key = `${prefix}time_id_counter:${timestamp}`;
|
|
98
94
|
|
|
99
|
-
const counter = await client.incr(key);
|
|
100
|
-
await client.expire(key, 1);
|
|
95
|
+
const counter = await this.client.incr(key);
|
|
96
|
+
await this.client.expire(key, 1);
|
|
101
97
|
|
|
102
98
|
const counterSuffix = (counter % 10000).toString().padStart(4, '0');
|
|
103
99
|
|
|
104
100
|
return Number(`${timestamp}${counterSuffix}`);
|
|
105
|
-
}
|
|
101
|
+
}
|
|
106
102
|
|
|
107
103
|
/**
|
|
108
104
|
* 批量生成基于时间的唯一 ID
|
|
@@ -121,13 +117,12 @@ export const RedisHelper = {
|
|
|
121
117
|
throw new Error(`批量大小 ${count} 超过最大限制 ${MAX_BATCH_SIZE}`);
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
const client = getClient();
|
|
125
120
|
const timestamp = Math.floor(Date.now() / 1000); // 秒级时间戳
|
|
126
121
|
const key = `${prefix}time_id_counter:${timestamp}`;
|
|
127
122
|
|
|
128
123
|
// 使用 INCRBY 一次性获取 N 个连续计数
|
|
129
|
-
const startCounter = await client.incrby(key, count);
|
|
130
|
-
await client.expire(key, 1);
|
|
124
|
+
const startCounter = await this.client.incrby(key, count);
|
|
125
|
+
await this.client.expire(key, 1);
|
|
131
126
|
|
|
132
127
|
// 生成 ID 数组
|
|
133
128
|
const ids: number[] = [];
|
|
@@ -138,7 +133,7 @@ export const RedisHelper = {
|
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
return ids;
|
|
141
|
-
}
|
|
136
|
+
}
|
|
142
137
|
|
|
143
138
|
/**
|
|
144
139
|
* 设置字符串值
|
|
@@ -148,17 +143,16 @@ export const RedisHelper = {
|
|
|
148
143
|
*/
|
|
149
144
|
async setString(key: string, value: string, ttl: number | null = null): Promise<string | null> {
|
|
150
145
|
try {
|
|
151
|
-
const client = getClient();
|
|
152
146
|
const pkey = `${prefix}${key}`;
|
|
153
147
|
if (ttl) {
|
|
154
|
-
return await client.setex(pkey, ttl, value);
|
|
148
|
+
return await this.client.setex(pkey, ttl, value);
|
|
155
149
|
}
|
|
156
|
-
return await client.set(pkey, value);
|
|
150
|
+
return await this.client.set(pkey, value);
|
|
157
151
|
} catch (error: any) {
|
|
158
152
|
Logger.error('Redis setString 错误', error);
|
|
159
153
|
return null;
|
|
160
154
|
}
|
|
161
|
-
}
|
|
155
|
+
}
|
|
162
156
|
|
|
163
157
|
/**
|
|
164
158
|
* 获取字符串值
|
|
@@ -166,14 +160,13 @@ export const RedisHelper = {
|
|
|
166
160
|
*/
|
|
167
161
|
async getString(key: string): Promise<string | null> {
|
|
168
162
|
try {
|
|
169
|
-
const client = getClient();
|
|
170
163
|
const pkey = `${prefix}${key}`;
|
|
171
|
-
return await client.get(pkey);
|
|
164
|
+
return await this.client.get(pkey);
|
|
172
165
|
} catch (error: any) {
|
|
173
166
|
Logger.error('Redis getString 错误', error);
|
|
174
167
|
return null;
|
|
175
168
|
}
|
|
176
|
-
}
|
|
169
|
+
}
|
|
177
170
|
|
|
178
171
|
/**
|
|
179
172
|
* 检查键是否存在
|
|
@@ -181,14 +174,13 @@ export const RedisHelper = {
|
|
|
181
174
|
*/
|
|
182
175
|
async exists(key: string): Promise<number> {
|
|
183
176
|
try {
|
|
184
|
-
const client = getClient();
|
|
185
177
|
const pkey = `${prefix}${key}`;
|
|
186
|
-
return await client.exists(pkey);
|
|
178
|
+
return await this.client.exists(pkey);
|
|
187
179
|
} catch (error: any) {
|
|
188
180
|
Logger.error('Redis exists 错误', error);
|
|
189
181
|
return 0;
|
|
190
182
|
}
|
|
191
|
-
}
|
|
183
|
+
}
|
|
192
184
|
|
|
193
185
|
/**
|
|
194
186
|
* 设置过期时间
|
|
@@ -197,14 +189,13 @@ export const RedisHelper = {
|
|
|
197
189
|
*/
|
|
198
190
|
async expire(key: string, seconds: number): Promise<number> {
|
|
199
191
|
try {
|
|
200
|
-
const client = getClient();
|
|
201
192
|
const pkey = `${prefix}${key}`;
|
|
202
|
-
return await client.expire(pkey, seconds);
|
|
193
|
+
return await this.client.expire(pkey, seconds);
|
|
203
194
|
} catch (error: any) {
|
|
204
195
|
Logger.error('Redis expire 错误', error);
|
|
205
196
|
return 0;
|
|
206
197
|
}
|
|
207
|
-
}
|
|
198
|
+
}
|
|
208
199
|
|
|
209
200
|
/**
|
|
210
201
|
* 获取剩余过期时间
|
|
@@ -212,14 +203,13 @@ export const RedisHelper = {
|
|
|
212
203
|
*/
|
|
213
204
|
async ttl(key: string): Promise<number> {
|
|
214
205
|
try {
|
|
215
|
-
const client = getClient();
|
|
216
206
|
const pkey = `${prefix}${key}`;
|
|
217
|
-
return await client.ttl(pkey);
|
|
207
|
+
return await this.client.ttl(pkey);
|
|
218
208
|
} catch (error: any) {
|
|
219
209
|
Logger.error('Redis ttl 错误', error);
|
|
220
210
|
return -1;
|
|
221
211
|
}
|
|
222
|
-
}
|
|
212
|
+
}
|
|
223
213
|
|
|
224
214
|
/**
|
|
225
215
|
* 向 Set 中添加一个或多个成员
|
|
@@ -231,14 +221,13 @@ export const RedisHelper = {
|
|
|
231
221
|
try {
|
|
232
222
|
if (members.length === 0) return 0;
|
|
233
223
|
|
|
234
|
-
const client = getClient();
|
|
235
224
|
const pkey = `${prefix}${key}`;
|
|
236
|
-
return await client.sadd(pkey, ...members);
|
|
225
|
+
return await this.client.sadd(pkey, ...members);
|
|
237
226
|
} catch (error: any) {
|
|
238
227
|
Logger.error('Redis sadd 错误', error);
|
|
239
228
|
return 0;
|
|
240
229
|
}
|
|
241
|
-
}
|
|
230
|
+
}
|
|
242
231
|
|
|
243
232
|
/**
|
|
244
233
|
* 判断成员是否在 Set 中
|
|
@@ -248,14 +237,13 @@ export const RedisHelper = {
|
|
|
248
237
|
*/
|
|
249
238
|
async sismember(key: string, member: string): Promise<number> {
|
|
250
239
|
try {
|
|
251
|
-
const client = getClient();
|
|
252
240
|
const pkey = `${prefix}${key}`;
|
|
253
|
-
return await client.sismember(pkey, member);
|
|
241
|
+
return await this.client.sismember(pkey, member);
|
|
254
242
|
} catch (error: any) {
|
|
255
243
|
Logger.error('Redis sismember 错误', error);
|
|
256
244
|
return 0;
|
|
257
245
|
}
|
|
258
|
-
}
|
|
246
|
+
}
|
|
259
247
|
|
|
260
248
|
/**
|
|
261
249
|
* 获取 Set 的成员数量
|
|
@@ -264,14 +252,13 @@ export const RedisHelper = {
|
|
|
264
252
|
*/
|
|
265
253
|
async scard(key: string): Promise<number> {
|
|
266
254
|
try {
|
|
267
|
-
const client = getClient();
|
|
268
255
|
const pkey = `${prefix}${key}`;
|
|
269
|
-
return await client.scard(pkey);
|
|
256
|
+
return await this.client.scard(pkey);
|
|
270
257
|
} catch (error: any) {
|
|
271
258
|
Logger.error('Redis scard 错误', error);
|
|
272
259
|
return 0;
|
|
273
260
|
}
|
|
274
|
-
}
|
|
261
|
+
}
|
|
275
262
|
|
|
276
263
|
/**
|
|
277
264
|
* 获取 Set 的所有成员
|
|
@@ -280,14 +267,13 @@ export const RedisHelper = {
|
|
|
280
267
|
*/
|
|
281
268
|
async smembers(key: string): Promise<string[]> {
|
|
282
269
|
try {
|
|
283
|
-
const client = getClient();
|
|
284
270
|
const pkey = `${prefix}${key}`;
|
|
285
|
-
return await client.smembers(pkey);
|
|
271
|
+
return await this.client.smembers(pkey);
|
|
286
272
|
} catch (error: any) {
|
|
287
273
|
Logger.error('Redis smembers 错误', error);
|
|
288
274
|
return [];
|
|
289
275
|
}
|
|
290
|
-
}
|
|
276
|
+
}
|
|
291
277
|
|
|
292
278
|
/**
|
|
293
279
|
* 删除键
|
|
@@ -296,14 +282,13 @@ export const RedisHelper = {
|
|
|
296
282
|
*/
|
|
297
283
|
async del(key: string): Promise<number> {
|
|
298
284
|
try {
|
|
299
|
-
const client = getClient();
|
|
300
285
|
const pkey = `${prefix}${key}`;
|
|
301
|
-
return await client.del(pkey);
|
|
286
|
+
return await this.client.del(pkey);
|
|
302
287
|
} catch (error: any) {
|
|
303
288
|
Logger.error('Redis del 错误', error);
|
|
304
289
|
return 0;
|
|
305
290
|
}
|
|
306
|
-
}
|
|
291
|
+
}
|
|
307
292
|
|
|
308
293
|
/**
|
|
309
294
|
* 测试 Redis 连接
|
|
@@ -311,11 +296,10 @@ export const RedisHelper = {
|
|
|
311
296
|
*/
|
|
312
297
|
async ping(): Promise<string> {
|
|
313
298
|
try {
|
|
314
|
-
|
|
315
|
-
return await client.ping();
|
|
299
|
+
return await this.client.ping();
|
|
316
300
|
} catch (error: any) {
|
|
317
301
|
Logger.error('Redis ping 错误', error);
|
|
318
302
|
throw error;
|
|
319
303
|
}
|
|
320
304
|
}
|
|
321
|
-
}
|
|
305
|
+
}
|
package/lib/validator.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 数据验证器 - Befly 项目专用
|
|
3
|
-
* 内置 RegexAliases
|
|
3
|
+
* 内置 RegexAliases,支持对象格式的字段定义
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import type { TableDefinition, FieldRule } from '../types/common.js';
|
|
6
|
+
import type { TableDefinition, FieldDefinition } from '../types/common.js';
|
|
8
7
|
import type { ValidationResult, ValidationError } from '../types/validator';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -106,7 +105,7 @@ export class Validator {
|
|
|
106
105
|
* 检查参数有效性
|
|
107
106
|
*/
|
|
108
107
|
private checkParams(data: any, rules: any, required: any, result: ValidationResult): boolean {
|
|
109
|
-
if (!data || typeof data !== 'object') {
|
|
108
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
110
109
|
result.code = 1;
|
|
111
110
|
result.fields.error = '数据必须是对象格式';
|
|
112
111
|
return false;
|
|
@@ -135,8 +134,8 @@ export class Validator {
|
|
|
135
134
|
const value = data[fieldName];
|
|
136
135
|
if (!(fieldName in data) || value === undefined || value === null || value === '') {
|
|
137
136
|
result.code = 1;
|
|
138
|
-
const
|
|
139
|
-
const fieldLabel =
|
|
137
|
+
const fieldDef = rules[fieldName];
|
|
138
|
+
const fieldLabel = fieldDef?.name || fieldName;
|
|
140
139
|
result.fields[fieldName] = `${fieldLabel}(${fieldName})为必填项`;
|
|
141
140
|
}
|
|
142
141
|
}
|
|
@@ -158,7 +157,7 @@ export class Validator {
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
const value = data[fieldName];
|
|
161
|
-
const error = this.validateFieldValue(value,
|
|
160
|
+
const error = this.validateFieldValue(value, rules[fieldName], fieldName);
|
|
162
161
|
|
|
163
162
|
if (error) {
|
|
164
163
|
result.code = 1;
|
|
@@ -190,21 +189,20 @@ export class Validator {
|
|
|
190
189
|
/**
|
|
191
190
|
* 验证单个字段的值
|
|
192
191
|
*/
|
|
193
|
-
private validateFieldValue(value: any,
|
|
194
|
-
|
|
195
|
-
let { name, type, min, max, regex } = parsed;
|
|
192
|
+
private validateFieldValue(value: any, fieldDef: FieldDefinition, fieldName: string): ValidationError {
|
|
193
|
+
let { name, type, min, max, regexp } = fieldDef;
|
|
196
194
|
|
|
197
|
-
|
|
195
|
+
regexp = this.resolveRegexAlias(regexp);
|
|
198
196
|
|
|
199
197
|
switch (type.toLowerCase()) {
|
|
200
198
|
case 'number':
|
|
201
|
-
return this.validateNumber(value, name, min, max,
|
|
199
|
+
return this.validateNumber(value, name, min, max, regexp, fieldName);
|
|
202
200
|
case 'string':
|
|
203
201
|
case 'text':
|
|
204
|
-
return this.validateString(value, name, min, max,
|
|
202
|
+
return this.validateString(value, name, min, max, regexp, fieldName);
|
|
205
203
|
case 'array_string':
|
|
206
204
|
case 'array_text':
|
|
207
|
-
return this.validateArray(value, name, min, max,
|
|
205
|
+
return this.validateArray(value, name, min, max, regexp, fieldName);
|
|
208
206
|
default:
|
|
209
207
|
return `字段 ${fieldName} 的类型 ${type} 不支持`;
|
|
210
208
|
}
|
|
@@ -215,7 +213,7 @@ export class Validator {
|
|
|
215
213
|
*/
|
|
216
214
|
private validateNumber(value: any, name: string, min: number | null, max: number | null, spec: string | null, fieldName: string): ValidationError {
|
|
217
215
|
try {
|
|
218
|
-
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
216
|
+
if (typeof value !== 'number' || Number.isNaN(value) || !isFinite(value)) {
|
|
219
217
|
return `${name}(${fieldName})必须是数字`;
|
|
220
218
|
}
|
|
221
219
|
|
|
@@ -315,17 +313,16 @@ export class Validator {
|
|
|
315
313
|
}
|
|
316
314
|
|
|
317
315
|
/**
|
|
318
|
-
*
|
|
316
|
+
* 验证单个值(支持对象格式字段定义)
|
|
319
317
|
*/
|
|
320
|
-
validateSingleValue(value: any,
|
|
321
|
-
|
|
322
|
-
let { name, type, min, max, regex, default: defaultValue } = parsed;
|
|
318
|
+
validateSingleValue(value: any, fieldDef: FieldDefinition): { valid: boolean; value: any; errors: string[] } {
|
|
319
|
+
let { name, type, min, max, regexp, default: defaultValue } = fieldDef;
|
|
323
320
|
|
|
324
|
-
|
|
321
|
+
regexp = this.resolveRegexAlias(regexp);
|
|
325
322
|
|
|
326
323
|
// 处理 undefined/null 值,使用默认值
|
|
327
324
|
if (value === undefined || value === null) {
|
|
328
|
-
if (defaultValue !==
|
|
325
|
+
if (defaultValue !== null) {
|
|
329
326
|
if ((type === 'array_string' || type === 'array_text') && typeof defaultValue === 'string') {
|
|
330
327
|
if (defaultValue === '[]') {
|
|
331
328
|
return { valid: true, value: [], errors: [] };
|
|
@@ -381,9 +378,9 @@ export class Validator {
|
|
|
381
378
|
if (max !== null && max > 0 && convertedValue > max) {
|
|
382
379
|
errors.push(`${name || '值'}不能大于${max}`);
|
|
383
380
|
}
|
|
384
|
-
if (
|
|
381
|
+
if (regexp && regexp.trim() !== '') {
|
|
385
382
|
try {
|
|
386
|
-
const regExp = new RegExp(
|
|
383
|
+
const regExp = new RegExp(regexp);
|
|
387
384
|
if (!regExp.test(String(convertedValue))) {
|
|
388
385
|
errors.push(`${name || '值'}格式不正确`);
|
|
389
386
|
}
|
|
@@ -404,9 +401,9 @@ export class Validator {
|
|
|
404
401
|
if (max !== null && max > 0 && convertedValue.length > max) {
|
|
405
402
|
errors.push(`${name || '值'}长度不能超过${max}个字符`);
|
|
406
403
|
}
|
|
407
|
-
if (
|
|
404
|
+
if (regexp && regexp.trim() !== '') {
|
|
408
405
|
try {
|
|
409
|
-
const regExp = new RegExp(
|
|
406
|
+
const regExp = new RegExp(regexp);
|
|
410
407
|
if (!regExp.test(convertedValue)) {
|
|
411
408
|
errors.push(`${name || '值'}格式不正确`);
|
|
412
409
|
}
|
|
@@ -427,9 +424,9 @@ export class Validator {
|
|
|
427
424
|
if (max !== null && max > 0 && convertedValue.length > max) {
|
|
428
425
|
errors.push(`${name || '值'}元素数量不能超过${max}个`);
|
|
429
426
|
}
|
|
430
|
-
if (
|
|
427
|
+
if (regexp && regexp.trim() !== '') {
|
|
431
428
|
try {
|
|
432
|
-
const regExp = new RegExp(
|
|
429
|
+
const regExp = new RegExp(regexp);
|
|
433
430
|
for (const item of convertedValue) {
|
|
434
431
|
if (!regExp.test(String(item))) {
|
|
435
432
|
errors.push(`${name || '值'}的元素格式不正确`);
|
|
@@ -454,15 +451,15 @@ export class Validator {
|
|
|
454
451
|
* 静态方法:快速验证
|
|
455
452
|
*/
|
|
456
453
|
static validate(data: Record<string, any>, rules: TableDefinition, required?: string[]): ValidationResult;
|
|
457
|
-
static validate(value: any,
|
|
458
|
-
static validate(dataOrValue: any,
|
|
454
|
+
static validate(value: any, fieldDef: FieldDefinition): { valid: boolean; value: any; errors: string[] };
|
|
455
|
+
static validate(dataOrValue: any, rulesOrFieldDef: any, required?: string[]): any {
|
|
459
456
|
const validator = new Validator();
|
|
460
457
|
|
|
461
|
-
if (
|
|
462
|
-
return validator.validateSingleValue(dataOrValue,
|
|
458
|
+
if (rulesOrFieldDef && 'type' in rulesOrFieldDef) {
|
|
459
|
+
return validator.validateSingleValue(dataOrValue, rulesOrFieldDef);
|
|
463
460
|
}
|
|
464
461
|
|
|
465
|
-
return validator.validate(dataOrValue,
|
|
462
|
+
return validator.validate(dataOrValue, rulesOrFieldDef, required || []);
|
|
466
463
|
}
|
|
467
464
|
|
|
468
465
|
/**
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 加载器
|
|
3
|
+
* 负责扫描和加载所有 API 路由(组件、用户)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { relative, basename, join } from 'pathe';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
10
|
+
import { calcPerfTime } from '../util.js';
|
|
11
|
+
import { projectApiDir } from '../paths.js';
|
|
12
|
+
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
13
|
+
import type { ApiRoute } from '../types/api.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* API 默认字段定义
|
|
17
|
+
* 这些字段会自动合并到所有 API 的 fields 中
|
|
18
|
+
* API 自定义的同名字段可以覆盖这些默认值
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_API_FIELDS = {
|
|
21
|
+
id: {
|
|
22
|
+
name: 'ID',
|
|
23
|
+
type: 'number',
|
|
24
|
+
min: 1,
|
|
25
|
+
max: null
|
|
26
|
+
},
|
|
27
|
+
page: {
|
|
28
|
+
name: '页码',
|
|
29
|
+
type: 'number',
|
|
30
|
+
min: 1,
|
|
31
|
+
max: 9999
|
|
32
|
+
},
|
|
33
|
+
limit: {
|
|
34
|
+
name: '每页数量',
|
|
35
|
+
type: 'number',
|
|
36
|
+
min: 1,
|
|
37
|
+
max: 100
|
|
38
|
+
},
|
|
39
|
+
keyword: {
|
|
40
|
+
name: '关键词',
|
|
41
|
+
type: 'string',
|
|
42
|
+
min: 1,
|
|
43
|
+
max: 50
|
|
44
|
+
},
|
|
45
|
+
state: {
|
|
46
|
+
name: '状态',
|
|
47
|
+
type: 'number',
|
|
48
|
+
min: 0,
|
|
49
|
+
max: 2
|
|
50
|
+
}
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 扫描用户 API 文件
|
|
55
|
+
*/
|
|
56
|
+
async function scanUserApis(): Promise<Array<{ file: string; routePrefix: string; displayName: string }>> {
|
|
57
|
+
const apis: Array<{ file: string; routePrefix: string; displayName: string }> = [];
|
|
58
|
+
|
|
59
|
+
if (!existsSync(projectApiDir)) {
|
|
60
|
+
return apis;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const glob = new Bun.Glob('**/*.ts');
|
|
64
|
+
|
|
65
|
+
for await (const file of glob.scan({
|
|
66
|
+
cwd: projectApiDir,
|
|
67
|
+
onlyFiles: true,
|
|
68
|
+
absolute: true
|
|
69
|
+
})) {
|
|
70
|
+
const apiPath = relative(projectApiDir, file).replace(/\.ts$/, '');
|
|
71
|
+
if (apiPath.indexOf('_') !== -1) continue;
|
|
72
|
+
|
|
73
|
+
apis.push({
|
|
74
|
+
file: file,
|
|
75
|
+
routePrefix: '',
|
|
76
|
+
displayName: '用户'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return apis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 扫描组件 API 文件
|
|
85
|
+
*/
|
|
86
|
+
async function scanAddonApis(): Promise<Array<{ file: string; routePrefix: string; displayName: string }>> {
|
|
87
|
+
const apis: Array<{ file: string; routePrefix: string; displayName: string }> = [];
|
|
88
|
+
const glob = new Bun.Glob('**/*.ts');
|
|
89
|
+
const addons = scanAddons();
|
|
90
|
+
|
|
91
|
+
for (const addon of addons) {
|
|
92
|
+
if (!addonDirExists(addon, 'apis')) continue;
|
|
93
|
+
|
|
94
|
+
const addonApiDir = getAddonDir(addon, 'apis');
|
|
95
|
+
for await (const file of glob.scan({
|
|
96
|
+
cwd: addonApiDir,
|
|
97
|
+
onlyFiles: true,
|
|
98
|
+
absolute: true
|
|
99
|
+
})) {
|
|
100
|
+
const apiPath = relative(addonApiDir, file).replace(/\.ts$/, '');
|
|
101
|
+
if (apiPath.indexOf('_') !== -1) continue;
|
|
102
|
+
|
|
103
|
+
apis.push({
|
|
104
|
+
file: file,
|
|
105
|
+
routePrefix: `addon/${addon}`,
|
|
106
|
+
displayName: `组件${addon}`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return apis;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 初始化单个 API
|
|
116
|
+
*/
|
|
117
|
+
async function initApi(apiRoutes: Map<string, ApiRoute>, apiInfo: { file: string; routePrefix: string; displayName: string }): Promise<void> {
|
|
118
|
+
const { file, routePrefix, displayName } = apiInfo;
|
|
119
|
+
const apiDir = routePrefix === '' ? projectApiDir : getAddonDir(routePrefix.replace('addon/', ''), 'apis');
|
|
120
|
+
const apiPath = relative(apiDir, file).replace(/\.ts$/, '');
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Windows 下路径需要转换为正斜杠格式
|
|
124
|
+
const filePath = file.replace(/\\/g, '/');
|
|
125
|
+
const apiImport = await import(filePath);
|
|
126
|
+
const api = apiImport.default;
|
|
127
|
+
|
|
128
|
+
// 设置默认值
|
|
129
|
+
api.method = api.method || 'POST';
|
|
130
|
+
api.auth = api.auth !== undefined ? api.auth : true;
|
|
131
|
+
// 合并默认字段:先设置自定义字段,再用默认字段覆盖(默认字段优先级更高)
|
|
132
|
+
api.fields = { ...(api.fields || {}), ...DEFAULT_API_FIELDS };
|
|
133
|
+
api.required = api.required || [];
|
|
134
|
+
|
|
135
|
+
// 构建路由
|
|
136
|
+
api.route = `${api.method.toUpperCase()}/api/${routePrefix ? routePrefix + '/' : ''}${apiPath}`;
|
|
137
|
+
apiRoutes.set(api.route, api);
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
Logger.error(`[${displayName}] 接口 ${apiPath} 加载失败`, error);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 加载所有 API 路由
|
|
146
|
+
* @param apiRoutes - API 路由映射表
|
|
147
|
+
*/
|
|
148
|
+
export async function loadApis(apiRoutes: Map<string, ApiRoute>): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
const loadStartTime = Bun.nanoseconds();
|
|
151
|
+
|
|
152
|
+
// 阶段1:扫描所有 API
|
|
153
|
+
const userApis = await scanUserApis();
|
|
154
|
+
const addonApis = await scanAddonApis();
|
|
155
|
+
|
|
156
|
+
// 阶段2:初始化所有 API(用户 → 组件)
|
|
157
|
+
// 2.1 初始化用户 APIs
|
|
158
|
+
for (const apiInfo of userApis) {
|
|
159
|
+
await initApi(apiRoutes, apiInfo);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2.2 初始化组件 APIs
|
|
163
|
+
for (const apiInfo of addonApis) {
|
|
164
|
+
await initApi(apiRoutes, apiInfo);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const totalLoadTime = calcPerfTime(loadStartTime);
|
|
168
|
+
} catch (error: any) {
|
|
169
|
+
Logger.error('加载 API 时发生错误', error);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|