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
|
@@ -72,7 +72,7 @@ Befly Hook 系统是请求处理的中间件机制,采用串联模式依次执
|
|
|
72
72
|
### 基础结构
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
|
-
import type { Hook } from
|
|
75
|
+
import type { Hook } from "befly/types/hook";
|
|
76
76
|
|
|
77
77
|
const hook: Hook = {
|
|
78
78
|
// 执行顺序(数字越小越先执行)
|
|
@@ -195,6 +195,13 @@ interface Hook {
|
|
|
195
195
|
|
|
196
196
|
## 内置钩子
|
|
197
197
|
|
|
198
|
+
内置钩子除本页总览外,也提供分文档说明(更聚焦配置与行为):
|
|
199
|
+
|
|
200
|
+
- [cors Hook](./cors.md)
|
|
201
|
+
- [auth Hook](./auth.md)
|
|
202
|
+
- [parser Hook](./parser.md)
|
|
203
|
+
- [rateLimit Hook](./rateLimit.md)
|
|
204
|
+
|
|
198
205
|
### cors - 跨域处理
|
|
199
206
|
|
|
200
207
|
处理 CORS 跨域请求,设置响应头。
|
|
@@ -208,12 +215,12 @@ const hook: Hook = {
|
|
|
208
215
|
|
|
209
216
|
// 合并默认配置和用户配置
|
|
210
217
|
const defaultConfig: CorsConfig = {
|
|
211
|
-
origin:
|
|
212
|
-
methods:
|
|
213
|
-
allowedHeaders:
|
|
214
|
-
exposedHeaders:
|
|
218
|
+
origin: "*",
|
|
219
|
+
methods: "GET, POST, PUT, DELETE, OPTIONS",
|
|
220
|
+
allowedHeaders: "Content-Type, Authorization, authorization, token",
|
|
221
|
+
exposedHeaders: "Content-Range, X-Content-Range, Authorization, authorization, token",
|
|
215
222
|
maxAge: 86400,
|
|
216
|
-
credentials:
|
|
223
|
+
credentials: "true"
|
|
217
224
|
};
|
|
218
225
|
|
|
219
226
|
const corsConfig = { ...defaultConfig, ...(beflyConfig.cors || {}) };
|
|
@@ -222,7 +229,7 @@ const hook: Hook = {
|
|
|
222
229
|
ctx.corsHeaders = setCorsOptions(req, corsConfig);
|
|
223
230
|
|
|
224
231
|
// 处理 OPTIONS 预检请求
|
|
225
|
-
if (req.method ===
|
|
232
|
+
if (req.method === "OPTIONS") {
|
|
226
233
|
ctx.response = new Response(null, {
|
|
227
234
|
status: 204,
|
|
228
235
|
headers: ctx.corsHeaders
|
|
@@ -236,7 +243,7 @@ const hook: Hook = {
|
|
|
236
243
|
**配置**:
|
|
237
244
|
|
|
238
245
|
```json
|
|
239
|
-
// befly.
|
|
246
|
+
// befly.development.json
|
|
240
247
|
{
|
|
241
248
|
"cors": {
|
|
242
249
|
"origin": "https://example.com",
|
|
@@ -259,9 +266,9 @@ const hook: Hook = {
|
|
|
259
266
|
const hook: Hook = {
|
|
260
267
|
order: 3,
|
|
261
268
|
handler: async (befly, ctx) => {
|
|
262
|
-
const authHeader = ctx.req.headers.get(
|
|
269
|
+
const authHeader = ctx.req.headers.get("authorization");
|
|
263
270
|
|
|
264
|
-
if (authHeader && authHeader.startsWith(
|
|
271
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
265
272
|
const token = authHeader.substring(7);
|
|
266
273
|
|
|
267
274
|
try {
|
|
@@ -297,7 +304,7 @@ const hook: Hook = {
|
|
|
297
304
|
if (!ctx.api) return;
|
|
298
305
|
|
|
299
306
|
// GET 请求:解析查询参数
|
|
300
|
-
if (ctx.req.method ===
|
|
307
|
+
if (ctx.req.method === "GET") {
|
|
301
308
|
const url = new URL(ctx.req.url);
|
|
302
309
|
const params = Object.fromEntries(url.searchParams);
|
|
303
310
|
|
|
@@ -310,17 +317,17 @@ const hook: Hook = {
|
|
|
310
317
|
}
|
|
311
318
|
}
|
|
312
319
|
// POST 请求:解析 JSON/XML
|
|
313
|
-
else if (ctx.req.method ===
|
|
314
|
-
const contentType = ctx.req.headers.get(
|
|
320
|
+
else if (ctx.req.method === "POST") {
|
|
321
|
+
const contentType = ctx.req.headers.get("content-type") || "";
|
|
315
322
|
|
|
316
|
-
if (contentType.includes(
|
|
323
|
+
if (contentType.includes("application/json")) {
|
|
317
324
|
const body = await ctx.req.json();
|
|
318
325
|
// 过滤字段...
|
|
319
326
|
ctx.body = pickFields(body, Object.keys(ctx.api.fields));
|
|
320
|
-
} else if (contentType.includes(
|
|
327
|
+
} else if (contentType.includes("application/xml")) {
|
|
321
328
|
// XML 解析...
|
|
322
329
|
} else {
|
|
323
|
-
ctx.response = ErrorResponse(ctx,
|
|
330
|
+
ctx.response = ErrorResponse(ctx, "无效的请求参数格式");
|
|
324
331
|
return;
|
|
325
332
|
}
|
|
326
333
|
}
|
|
@@ -350,9 +357,9 @@ const hook: Hook = {
|
|
|
350
357
|
requestId: ctx.requestId,
|
|
351
358
|
route: ctx.route,
|
|
352
359
|
ip: ctx.ip,
|
|
353
|
-
userId: ctx.user?.id ||
|
|
354
|
-
nickname: ctx.user?.nickname ||
|
|
355
|
-
roleCode: ctx.user?.roleCode ||
|
|
360
|
+
userId: ctx.user?.id || "",
|
|
361
|
+
nickname: ctx.user?.nickname || "",
|
|
362
|
+
roleCode: ctx.user?.roleCode || ""
|
|
356
363
|
};
|
|
357
364
|
|
|
358
365
|
// 截断大请求体
|
|
@@ -360,7 +367,7 @@ const hook: Hook = {
|
|
|
360
367
|
logData.body = truncateBody(ctx.body);
|
|
361
368
|
}
|
|
362
369
|
|
|
363
|
-
Logger.info(logData,
|
|
370
|
+
Logger.info(logData, "请求日志");
|
|
364
371
|
}
|
|
365
372
|
};
|
|
366
373
|
```
|
|
@@ -388,7 +395,7 @@ const hook: Hook = {
|
|
|
388
395
|
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
389
396
|
|
|
390
397
|
if (result.code !== 0) {
|
|
391
|
-
ctx.response = ErrorResponse(ctx, result.firstError ||
|
|
398
|
+
ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors);
|
|
392
399
|
return;
|
|
393
400
|
}
|
|
394
401
|
}
|
|
@@ -410,6 +417,8 @@ const hook: Hook = {
|
|
|
410
417
|
|
|
411
418
|
```typescript
|
|
412
419
|
// hooks/permission.ts
|
|
420
|
+
import { CacheKeys } from "befly/lib/cacheKeys";
|
|
421
|
+
|
|
413
422
|
const hook: Hook = {
|
|
414
423
|
order: 6,
|
|
415
424
|
handler: async (befly, ctx) => {
|
|
@@ -422,25 +431,28 @@ const hook: Hook = {
|
|
|
422
431
|
|
|
423
432
|
// 2. 用户未登录
|
|
424
433
|
if (!ctx.user || !ctx.user.id) {
|
|
425
|
-
ctx.response = ErrorResponse(ctx,
|
|
434
|
+
ctx.response = ErrorResponse(ctx, "未登录");
|
|
426
435
|
return;
|
|
427
436
|
}
|
|
428
437
|
|
|
429
438
|
// 3. 开发者权限(最高权限)
|
|
430
|
-
if (ctx.user.roleCode ===
|
|
439
|
+
if (ctx.user.roleCode === "dev") {
|
|
431
440
|
return;
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
// 4. 角色权限检查
|
|
435
444
|
let hasPermission = false;
|
|
436
445
|
if (ctx.user.roleCode && befly.redis) {
|
|
437
|
-
|
|
438
|
-
const
|
|
446
|
+
// 统一格式:METHOD/path(与写入缓存保持一致)
|
|
447
|
+
const apiPath = normalizeApiPath(ctx.req.method, new URL(ctx.req.url).pathname);
|
|
448
|
+
|
|
449
|
+
// 极简方案:每个角色一个 Set,直接判断成员是否存在
|
|
450
|
+
const roleApisKey = CacheKeys.roleApis(ctx.user.roleCode);
|
|
439
451
|
hasPermission = await befly.redis.sismember(roleApisKey, apiPath);
|
|
440
452
|
}
|
|
441
453
|
|
|
442
454
|
if (!hasPermission) {
|
|
443
|
-
ctx.response = ErrorResponse(ctx,
|
|
455
|
+
ctx.response = ErrorResponse(ctx, "无权访问");
|
|
444
456
|
return;
|
|
445
457
|
}
|
|
446
458
|
}
|
|
@@ -462,12 +474,12 @@ const hook: Hook = {
|
|
|
462
474
|
|
|
463
475
|
```typescript
|
|
464
476
|
// hooks/myHook.ts(项目钩子名:app_myHook)
|
|
465
|
-
import type { Hook } from
|
|
477
|
+
import type { Hook } from "befly/types/hook";
|
|
466
478
|
|
|
467
479
|
const hook: Hook = {
|
|
468
480
|
order: 10,
|
|
469
481
|
handler: async (befly, ctx) => {
|
|
470
|
-
befly.logger.info({ route: ctx.route },
|
|
482
|
+
befly.logger.info({ route: ctx.route }, "自定义钩子执行");
|
|
471
483
|
}
|
|
472
484
|
};
|
|
473
485
|
|
|
@@ -482,17 +494,17 @@ export default hook;
|
|
|
482
494
|
|
|
483
495
|
```typescript
|
|
484
496
|
// hooks/blacklist.ts
|
|
485
|
-
import type { Hook } from
|
|
486
|
-
import { ErrorResponse } from
|
|
497
|
+
import type { Hook } from "befly/types/hook";
|
|
498
|
+
import { ErrorResponse } from "befly/utils/response";
|
|
487
499
|
|
|
488
500
|
const hook: Hook = {
|
|
489
501
|
order: 1, // 最先执行
|
|
490
502
|
handler: async (befly, ctx) => {
|
|
491
503
|
// IP 黑名单检查
|
|
492
|
-
const blacklist = [
|
|
504
|
+
const blacklist = ["192.168.1.100", "10.0.0.1"];
|
|
493
505
|
|
|
494
506
|
if (blacklist.includes(ctx.ip)) {
|
|
495
|
-
ctx.response = ErrorResponse(ctx,
|
|
507
|
+
ctx.response = ErrorResponse(ctx, "您的 IP 已被禁止访问", 403);
|
|
496
508
|
return;
|
|
497
509
|
}
|
|
498
510
|
}
|
|
@@ -507,10 +519,51 @@ export default hook;
|
|
|
507
519
|
|
|
508
520
|
基于 Redis 的请求限流:
|
|
509
521
|
|
|
522
|
+
> 说明:Befly Core 已内置 `rateLimit` 钩子(默认启用)。
|
|
523
|
+
>
|
|
524
|
+
> 默认行为:按 IP 对所有 API 进行限流(默认阈值:$1000/60$,即 60 秒最多 1000 次)。
|
|
525
|
+
>
|
|
526
|
+
> - 关闭:`rateLimit.enable = 0`
|
|
527
|
+
> - 覆盖:配置 `rules`(更细粒度)或调整 `defaultLimit/defaultWindow`
|
|
528
|
+
> - 跳过:配置 `skipRoutes`(命中后直接跳过限流,优先级最高)
|
|
529
|
+
>
|
|
530
|
+
> 规则选择:当多条 `rules` 同时命中时,会优先选择更“具体”的规则(精确 > 前缀 > 通配);
|
|
531
|
+
> 同等具体度按 `rules` 的先后顺序。
|
|
532
|
+
>
|
|
533
|
+
> key 行为:当 `key = user` 且请求上下文中没有 `ctx.user.id` 时,会回退为按 IP 计数,避免所有匿名请求共享同一个计数桶。
|
|
534
|
+
|
|
535
|
+
配置示例(`configs/befly.common.json`):
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{
|
|
539
|
+
"rateLimit": {
|
|
540
|
+
"enable": 1,
|
|
541
|
+
"defaultLimit": 1000,
|
|
542
|
+
"defaultWindow": 60,
|
|
543
|
+
"key": "ip",
|
|
544
|
+
"skipRoutes": ["/api/health", "GET/api/metrics"],
|
|
545
|
+
"rules": [
|
|
546
|
+
{
|
|
547
|
+
"route": "/api/auth/*",
|
|
548
|
+
"limit": 20,
|
|
549
|
+
"window": 60,
|
|
550
|
+
"key": "ip"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"route": "POST/api/order/create",
|
|
554
|
+
"limit": 5,
|
|
555
|
+
"window": 60,
|
|
556
|
+
"key": "user"
|
|
557
|
+
}
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
510
563
|
```typescript
|
|
511
564
|
// hooks/rateLimit.ts
|
|
512
|
-
import type { Hook } from
|
|
513
|
-
import { ErrorResponse } from
|
|
565
|
+
import type { Hook } from "befly/types/hook";
|
|
566
|
+
import { ErrorResponse } from "befly/utils/response";
|
|
514
567
|
|
|
515
568
|
const hook: Hook = {
|
|
516
569
|
order: 7,
|
|
@@ -524,17 +577,12 @@ const hook: Hook = {
|
|
|
524
577
|
// 限流 key:IP + 路由
|
|
525
578
|
const key = `ratelimit:${ctx.ip}:${ctx.route}`;
|
|
526
579
|
|
|
527
|
-
//
|
|
528
|
-
const count = await befly.redis.
|
|
529
|
-
|
|
530
|
-
// 首次请求设置过期时间
|
|
531
|
-
if (count === 1) {
|
|
532
|
-
await befly.redis.expire(key, window);
|
|
533
|
-
}
|
|
580
|
+
// 原子计数 + 首次设置过期
|
|
581
|
+
const count = await befly.redis.incrWithExpire(key, window);
|
|
534
582
|
|
|
535
583
|
// 超过限制
|
|
536
584
|
if (count > limit) {
|
|
537
|
-
ctx.response = ErrorResponse(ctx,
|
|
585
|
+
ctx.response = ErrorResponse(ctx, "请求过于频繁,请稍后再试", 1);
|
|
538
586
|
return;
|
|
539
587
|
}
|
|
540
588
|
}
|
|
@@ -551,22 +599,22 @@ export default hook;
|
|
|
551
599
|
|
|
552
600
|
```typescript
|
|
553
601
|
// hooks/audit.ts
|
|
554
|
-
import type { Hook } from
|
|
602
|
+
import type { Hook } from "befly/types/hook";
|
|
555
603
|
|
|
556
604
|
const hook: Hook = {
|
|
557
605
|
order: 100, // 在 handler 执行后
|
|
558
606
|
handler: async (befly, ctx) => {
|
|
559
607
|
// 只记录写操作
|
|
560
|
-
if (!ctx.api || ctx.req.method ===
|
|
608
|
+
if (!ctx.api || ctx.req.method === "GET") return;
|
|
561
609
|
if (!ctx.user?.id) return;
|
|
562
610
|
|
|
563
611
|
// 记录审计日志
|
|
564
612
|
try {
|
|
565
613
|
await befly.db.insData({
|
|
566
|
-
table:
|
|
614
|
+
table: "audit_log",
|
|
567
615
|
data: {
|
|
568
616
|
userId: ctx.user.id,
|
|
569
|
-
username: ctx.user.username ||
|
|
617
|
+
username: ctx.user.username || "",
|
|
570
618
|
route: ctx.route,
|
|
571
619
|
method: ctx.req.method,
|
|
572
620
|
ip: ctx.ip,
|
|
@@ -575,7 +623,7 @@ const hook: Hook = {
|
|
|
575
623
|
}
|
|
576
624
|
});
|
|
577
625
|
} catch (error) {
|
|
578
|
-
befly.logger.error({ err: error },
|
|
626
|
+
befly.logger.error({ err: error }, "审计日志记录失败");
|
|
579
627
|
}
|
|
580
628
|
}
|
|
581
629
|
};
|
|
@@ -590,7 +638,7 @@ export default hook;
|
|
|
590
638
|
设置 `ctx.response` 可以中断后续 Hook 和 API Handler 的执行:
|
|
591
639
|
|
|
592
640
|
```typescript
|
|
593
|
-
import { ErrorResponse } from
|
|
641
|
+
import { ErrorResponse } from "befly/utils/response";
|
|
594
642
|
|
|
595
643
|
const hook: Hook = {
|
|
596
644
|
order: 5,
|
|
@@ -598,7 +646,7 @@ const hook: Hook = {
|
|
|
598
646
|
// 条件判断
|
|
599
647
|
if (someCondition) {
|
|
600
648
|
// 设置 response 中断请求
|
|
601
|
-
ctx.response = ErrorResponse(ctx,
|
|
649
|
+
ctx.response = ErrorResponse(ctx, "请求被拦截", 1);
|
|
602
650
|
return; // 必须 return
|
|
603
651
|
}
|
|
604
652
|
|
|
@@ -625,7 +673,7 @@ ErrorResponse(ctx, msg, code?, data?, detail?)
|
|
|
625
673
|
在配置文件中设置 `disableHooks` 数组:
|
|
626
674
|
|
|
627
675
|
```json
|
|
628
|
-
// befly.
|
|
676
|
+
// befly.development.json
|
|
629
677
|
{
|
|
630
678
|
"disableHooks": ["requestLogger", "permission"]
|
|
631
679
|
}
|
|
@@ -678,7 +726,7 @@ const hook: Hook = {
|
|
|
678
726
|
try {
|
|
679
727
|
await someOperation();
|
|
680
728
|
} catch (error) {
|
|
681
|
-
befly.logger.error({ err: error },
|
|
729
|
+
befly.logger.error({ err: error }, "钩子执行失败");
|
|
682
730
|
// 根据业务决定是否中断请求
|
|
683
731
|
}
|
|
684
732
|
}
|
|
@@ -699,7 +747,7 @@ const hook: Hook = {
|
|
|
699
747
|
|
|
700
748
|
### 5. 性能考虑
|
|
701
749
|
|
|
702
|
-
|
|
750
|
+
````typescript
|
|
703
751
|
// ✅ 推荐:异步操作使用 Promise
|
|
704
752
|
const hook: Hook = {
|
|
705
753
|
handler: async (befly, ctx) => {
|
|
@@ -707,7 +755,42 @@ const hook: Hook = {
|
|
|
707
755
|
const [result1, result2] = await Promise.all([operation1(), operation2()]);
|
|
708
756
|
}
|
|
709
757
|
};
|
|
710
|
-
|
|
758
|
+
|
|
759
|
+
### 6. 直接使用字段,避免多余变量
|
|
760
|
+
|
|
761
|
+
仅用于“转发参数 / 改名 / 只用一次”的中间变量不要定义;优先直接使用来源对象字段(例如 `ctx.body.xxx`、`payload.xxx`)。
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
// ❌ 避免:只为转发参数而改名/多一层
|
|
765
|
+
const hook: Hook = {
|
|
766
|
+
handler: async (befly, ctx) => {
|
|
767
|
+
const payload = await befly.jwt.verify(token); // token 解析略
|
|
768
|
+
const userId = payload.id;
|
|
769
|
+
setCtxUser(userId, payload.roleCode, payload.nickname, payload.roleType);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// ✅ 推荐:直接转发字段
|
|
774
|
+
const hook2: Hook = {
|
|
775
|
+
handler: async (befly, ctx) => {
|
|
776
|
+
const payload = await befly.jwt.verify(token); // token 解析略
|
|
777
|
+
setCtxUser(payload.id, payload.roleCode, payload.nickname, payload.roleType);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// ✅ 例外:需要类型收窄/非空校验/复用(>=2 次)时再定义变量
|
|
782
|
+
const hook3: Hook = {
|
|
783
|
+
handler: async (befly, ctx) => {
|
|
784
|
+
const payload = await befly.jwt.verify(token); // token 解析略
|
|
785
|
+
const userId = payload.id;
|
|
786
|
+
if (typeof userId !== "number") return;
|
|
787
|
+
|
|
788
|
+
setCtxUser(userId, payload.roleCode, payload.nickname, payload.roleType);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
````
|
|
792
|
+
|
|
793
|
+
````
|
|
711
794
|
|
|
712
795
|
---
|
|
713
796
|
|
|
@@ -727,11 +810,11 @@ const hook: Hook = {
|
|
|
727
810
|
await befly.db.getOne({
|
|
728
811
|
/* ... */
|
|
729
812
|
});
|
|
730
|
-
await befly.redis.get(
|
|
731
|
-
befly.logger.info(
|
|
813
|
+
await befly.redis.get("key");
|
|
814
|
+
befly.logger.info("日志");
|
|
732
815
|
}
|
|
733
816
|
};
|
|
734
|
-
|
|
817
|
+
````
|
|
735
818
|
|
|
736
819
|
### Q3: 如何在钩子间传递数据?
|
|
737
820
|
|
|
@@ -739,7 +822,7 @@ const hook: Hook = {
|
|
|
739
822
|
|
|
740
823
|
```typescript
|
|
741
824
|
// 钩子 A(order: 10)
|
|
742
|
-
ctx.customData = { key:
|
|
825
|
+
ctx.customData = { key: "value" };
|
|
743
826
|
|
|
744
827
|
// 钩子 B(order: 20)
|
|
745
828
|
const data = ctx.customData;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# parser Hook - 参数解析
|
|
2
|
+
|
|
3
|
+
> 解析请求体/查询参数,产出统一的 `ctx.body`。
|
|
4
|
+
|
|
5
|
+
## 作用
|
|
6
|
+
|
|
7
|
+
- GET:解析 URL 查询参数
|
|
8
|
+
- POST:解析 JSON(必要时也会支持 XML 等格式,取决于框架实现)
|
|
9
|
+
- 将解析结果写入 `ctx.body`
|
|
10
|
+
|
|
11
|
+
## 行为要点
|
|
12
|
+
|
|
13
|
+
- 解析失败(格式错误)时会提前中断请求,并返回安全的错误信息
|
|
14
|
+
- 该 hook 只负责“把数据变成 ctx.body”,字段校验由后续 `validator` 完成
|
|
15
|
+
|
|
16
|
+
## 常见问题
|
|
17
|
+
|
|
18
|
+
- Q: 为什么 `ctx.body` 里没有某些字段?
|
|
19
|
+
- A: 可能被 fields/required 体系过滤/校验拦截了;请检查 `validator` 与 API 的 `fields/required` 配置。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# rateLimit Hook - 全局限流
|
|
2
|
+
|
|
3
|
+
> 按路由与身份维度进行限流(默认启用)。
|
|
4
|
+
|
|
5
|
+
## 默认行为
|
|
6
|
+
|
|
7
|
+
- **默认启用**:`rateLimit.enable = 1`
|
|
8
|
+
- 无规则时:使用兜底配置(默认阈值由框架默认配置提供)
|
|
9
|
+
- `OPTIONS` 请求:不计数也不拦截
|
|
10
|
+
|
|
11
|
+
## 配置
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"rateLimit": {
|
|
16
|
+
"enable": 1,
|
|
17
|
+
"rules": [
|
|
18
|
+
{
|
|
19
|
+
"route": "/api/auth/*",
|
|
20
|
+
"limit": 10,
|
|
21
|
+
"window": 60,
|
|
22
|
+
"key": "ip"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"skipRoutes": ["GET/api/health"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### route 匹配
|
|
31
|
+
|
|
32
|
+
- 支持精确、前缀、通配(更具体的规则优先)
|
|
33
|
+
|
|
34
|
+
### key 维度
|
|
35
|
+
|
|
36
|
+
- `ip`:按 IP 限流
|
|
37
|
+
- `user`:按用户限流;当缺失 `ctx.user.id` 时会回退为按 IP 计数
|
|
38
|
+
- `ip_user`:IP + 用户组合
|
|
39
|
+
|
|
40
|
+
### skipRoutes
|
|
41
|
+
|
|
42
|
+
命中后直接跳过限流:**不计数也不拦截**。
|
|
43
|
+
|
|
44
|
+
## 存储
|
|
45
|
+
|
|
46
|
+
- 优先使用 Redis(分布式一致)
|
|
47
|
+
- 无 Redis 时降级为进程内计数
|