befly 3.9.37 → 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.
Files changed (155) hide show
  1. package/README.md +38 -39
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +225 -235
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +155 -153
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +7 -7
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +15 -7
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -81
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +211 -109
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +53 -47
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -54
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -66
  75. package/sync/syncMenu.ts +190 -57
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
@@ -72,7 +72,7 @@ Befly Hook 系统是请求处理的中间件机制,采用串联模式依次执
72
72
  ### 基础结构
73
73
 
74
74
  ```typescript
75
- import type { Hook } from 'befly/types/hook';
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: 'GET, POST, PUT, DELETE, OPTIONS',
213
- allowedHeaders: 'Content-Type, Authorization, authorization, token',
214
- exposedHeaders: 'Content-Range, X-Content-Range, Authorization, authorization, token',
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: 'true'
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 === 'OPTIONS') {
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.local.json
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('authorization');
269
+ const authHeader = ctx.req.headers.get("authorization");
263
270
 
264
- if (authHeader && authHeader.startsWith('Bearer ')) {
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 === 'GET') {
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 === 'POST') {
314
- const contentType = ctx.req.headers.get('content-type') || '';
320
+ else if (ctx.req.method === "POST") {
321
+ const contentType = ctx.req.headers.get("content-type") || "";
315
322
 
316
- if (contentType.includes('application/json')) {
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('application/xml')) {
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 || '参数验证失败', 1, null, result.fieldErrors);
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 === 'dev') {
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
- const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
438
- const roleApisKey = RedisKeys.roleApis(ctx.user.roleCode);
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 'befly/types/hook';
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 'befly/types/hook';
486
- import { ErrorResponse } from 'befly/util';
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 = ['192.168.1.100', '10.0.0.1'];
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, '您的 IP 已被禁止访问', 403);
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 'befly/types/hook';
513
- import { ErrorResponse } from 'befly/util';
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.incr(key);
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, '请求过于频繁,请稍后再试', 429);
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 'befly/types/hook';
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 === 'GET') return;
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: 'audit_log',
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 'befly/util';
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, '请求被拦截', 1);
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.local.json
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
- ```typescript
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('key');
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: 'value' };
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 时降级为进程内计数