befly 3.9.7 → 3.9.8

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/docs/redis.md ADDED
@@ -0,0 +1,624 @@
1
+ # Redis 使用指南
2
+
3
+ ## 概述
4
+
5
+ Befly 框架使用 Redis 作为缓存层,提供高性能的数据缓存、会话管理和分布式 ID 生成等功能。Redis 插件基于 Bun 内置的 `RedisClient` 实现,封装了常用操作并自动利用 Bun 的 pipeline 特性优化批量操作。
6
+
7
+ ## 快速开始
8
+
9
+ ### 配置
10
+
11
+ 在 `befly.*.json` 配置文件中配置 Redis:
12
+
13
+ ```json
14
+ {
15
+ "redis": {
16
+ "host": "127.0.0.1",
17
+ "port": 6379,
18
+ "password": "",
19
+ "db": 0,
20
+ "prefix": "befly"
21
+ }
22
+ }
23
+ ```
24
+
25
+ ### 访问 Redis
26
+
27
+ 通过 `befly.redis` 访问 Redis 助手实例:
28
+
29
+ ```typescript
30
+ // 在 API handler 中
31
+ export default {
32
+ name: '示例接口',
33
+ handler: async (befly, ctx) => {
34
+ // 设置缓存
35
+ await befly.redis.setObject('user:1', { name: '张三', age: 25 });
36
+
37
+ // 获取缓存
38
+ const user = await befly.redis.getObject('user:1');
39
+
40
+ return befly.tool.Yes('成功', user);
41
+ }
42
+ };
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 核心方法
48
+
49
+ ### 字符串操作
50
+
51
+ #### setString - 设置字符串
52
+
53
+ ```typescript
54
+ // 基本设置
55
+ await befly.redis.setString('key', 'value');
56
+
57
+ // 带过期时间(秒)
58
+ await befly.redis.setString('key', 'value', 3600); // 1小时后过期
59
+ ```
60
+
61
+ #### getString - 获取字符串
62
+
63
+ ```typescript
64
+ const value = await befly.redis.getString('key');
65
+ // 返回: 'value' 或 null(不存在时)
66
+ ```
67
+
68
+ ### 对象操作
69
+
70
+ #### setObject - 设置对象
71
+
72
+ 自动序列化为 JSON 存储。
73
+
74
+ ```typescript
75
+ // 基本设置
76
+ await befly.redis.setObject('user:1', {
77
+ id: 1,
78
+ name: '张三',
79
+ roles: ['admin', 'user']
80
+ });
81
+
82
+ // 带过期时间(秒)
83
+ await befly.redis.setObject('session:abc123', { userId: 1 }, 7200); // 2小时
84
+ ```
85
+
86
+ #### getObject - 获取对象
87
+
88
+ 自动反序列化 JSON。
89
+
90
+ ```typescript
91
+ const user = await befly.redis.getObject<UserInfo>('user:1');
92
+ // 返回: { id: 1, name: '张三', roles: ['admin', 'user'] } 或 null
93
+ ```
94
+
95
+ #### delObject - 删除对象
96
+
97
+ ```typescript
98
+ await befly.redis.delObject('user:1');
99
+ ```
100
+
101
+ ### 键操作
102
+
103
+ #### exists - 检查键是否存在
104
+
105
+ ```typescript
106
+ const exists = await befly.redis.exists('user:1');
107
+ // 返回: true 或 false
108
+ ```
109
+
110
+ #### del - 删除键
111
+
112
+ ```typescript
113
+ const count = await befly.redis.del('user:1');
114
+ // 返回: 删除的键数量(0 或 1)
115
+ ```
116
+
117
+ #### expire - 设置过期时间
118
+
119
+ ```typescript
120
+ await befly.redis.expire('user:1', 3600); // 1小时后过期
121
+ ```
122
+
123
+ #### ttl - 获取剩余过期时间
124
+
125
+ ```typescript
126
+ const seconds = await befly.redis.ttl('user:1');
127
+ // 返回: 剩余秒数,-1 表示永不过期,-2 表示键不存在
128
+ ```
129
+
130
+ ### Set 集合操作
131
+
132
+ 适用于存储不重复的元素集合,如权限列表、标签等。
133
+
134
+ #### sadd - 添加成员
135
+
136
+ ```typescript
137
+ // 添加单个成员
138
+ await befly.redis.sadd('tags:article:1', ['技术']);
139
+
140
+ // 添加多个成员
141
+ await befly.redis.sadd('user:1:roles', ['admin', 'editor', 'viewer']);
142
+ ```
143
+
144
+ #### sismember - 检查成员是否存在
145
+
146
+ ```typescript
147
+ const isMember = await befly.redis.sismember('user:1:roles', 'admin');
148
+ // 返回: true 或 false
149
+ ```
150
+
151
+ #### smembers - 获取所有成员
152
+
153
+ ```typescript
154
+ const roles = await befly.redis.smembers('user:1:roles');
155
+ // 返回: ['admin', 'editor', 'viewer']
156
+ ```
157
+
158
+ #### scard - 获取成员数量
159
+
160
+ ```typescript
161
+ const count = await befly.redis.scard('user:1:roles');
162
+ // 返回: 3
163
+ ```
164
+
165
+ ---
166
+
167
+ ## 批量操作
168
+
169
+ 批量操作利用 Bun Redis 的自动 pipeline 特性,显著提升性能。
170
+
171
+ ### setBatch - 批量设置对象
172
+
173
+ ```typescript
174
+ const count = await befly.redis.setBatch([
175
+ { key: 'user:1', value: { name: '张三' }, ttl: 3600 },
176
+ { key: 'user:2', value: { name: '李四' }, ttl: 3600 },
177
+ { key: 'user:3', value: { name: '王五' } } // 无 TTL,永不过期
178
+ ]);
179
+ // 返回: 成功设置的数量
180
+ ```
181
+
182
+ ### getBatch - 批量获取对象
183
+
184
+ ```typescript
185
+ const users = await befly.redis.getBatch<UserInfo>(['user:1', 'user:2', 'user:3']);
186
+ // 返回: [{ name: '张三' }, { name: '李四' }, null](不存在的返回 null)
187
+ ```
188
+
189
+ ### delBatch - 批量删除键
190
+
191
+ ```typescript
192
+ const count = await befly.redis.delBatch(['user:1', 'user:2', 'user:3']);
193
+ // 返回: 成功删除的数量
194
+ ```
195
+
196
+ ### existsBatch - 批量检查存在
197
+
198
+ ```typescript
199
+ const results = await befly.redis.existsBatch(['user:1', 'user:2', 'user:3']);
200
+ // 返回: [true, true, false]
201
+ ```
202
+
203
+ ### ttlBatch - 批量获取过期时间
204
+
205
+ ```typescript
206
+ const ttls = await befly.redis.ttlBatch(['user:1', 'user:2', 'user:3']);
207
+ // 返回: [3600, 7200, -1]
208
+ ```
209
+
210
+ ### expireBatch - 批量设置过期时间
211
+
212
+ ```typescript
213
+ const count = await befly.redis.expireBatch([
214
+ { key: 'user:1', seconds: 3600 },
215
+ { key: 'user:2', seconds: 7200 }
216
+ ]);
217
+ // 返回: 成功设置的数量
218
+ ```
219
+
220
+ ### saddBatch - 批量添加 Set 成员
221
+
222
+ ```typescript
223
+ const count = await befly.redis.saddBatch([
224
+ { key: 'role:admin:apis', members: ['GET/api/user', 'POST/api/user'] },
225
+ { key: 'role:editor:apis', members: ['GET/api/article', 'POST/api/article'] }
226
+ ]);
227
+ // 返回: 成功添加的总成员数量
228
+ ```
229
+
230
+ ### sismemberBatch - 批量检查 Set 成员
231
+
232
+ ```typescript
233
+ const results = await befly.redis.sismemberBatch([
234
+ { key: 'role:admin:apis', member: 'GET/api/user' },
235
+ { key: 'role:admin:apis', member: 'DELETE/api/user' }
236
+ ]);
237
+ // 返回: [true, false]
238
+ ```
239
+
240
+ ---
241
+
242
+ ## 唯一 ID 生成
243
+
244
+ ### genTimeID - 生成基于时间的唯一 ID
245
+
246
+ 生成 16 位纯数字 ID,格式:`毫秒时间戳(13位) + 后缀(3位)`。
247
+
248
+ 利用 Redis `INCR` 原子操作保证分布式环境下的唯一性。
249
+
250
+ ```typescript
251
+ const id = await befly.redis.genTimeID();
252
+ // 返回: 1733395200000123(示例)
253
+ ```
254
+
255
+ **使用场景:**
256
+
257
+ ```typescript
258
+ // 在 DbHelper.insData 中自动调用
259
+ const id = await befly.db.insData({
260
+ table: 'article',
261
+ data: {
262
+ title: '文章标题',
263
+ content: '文章内容'
264
+ }
265
+ });
266
+ // id 由 genTimeID 自动生成
267
+ ```
268
+
269
+ **特点:**
270
+
271
+ - 16 位纯数字,可直接存储为 BIGINT
272
+ - 毫秒级时间戳 + 3 位后缀(100-999)
273
+ - 每毫秒支持 900 个并发 ID
274
+ - 分布式环境安全(基于 Redis INCR)
275
+
276
+ ---
277
+
278
+ ## 缓存键管理
279
+
280
+ ### RedisKeys - 统一键名管理
281
+
282
+ 避免硬编码,统一管理所有缓存键。
283
+
284
+ ```typescript
285
+ import { RedisKeys, RedisTTL } from 'befly-shared/redisKeys';
286
+
287
+ // 获取键名
288
+ const key = RedisKeys.apisAll(); // 'befly:apis:all'
289
+ const key = RedisKeys.menusAll(); // 'befly:menus:all'
290
+ const key = RedisKeys.roleInfo('admin'); // 'befly:role:info:admin'
291
+ const key = RedisKeys.roleApis('admin'); // 'befly:role:apis:admin'
292
+ const key = RedisKeys.tableColumns('user'); // 'befly:table:columns:user'
293
+
294
+ // 获取 TTL
295
+ const ttl = RedisTTL.tableColumns; // 3600(1小时)
296
+ const ttl = RedisTTL.roleApis; // 86400(24小时)
297
+ const ttl = RedisTTL.apisAll; // null(永不过期)
298
+ ```
299
+
300
+ ### 键名前缀
301
+
302
+ Redis 插件支持配置全局前缀,避免键名冲突:
303
+
304
+ ```json
305
+ {
306
+ "redis": {
307
+ "prefix": "myapp"
308
+ }
309
+ }
310
+ ```
311
+
312
+ 所有键会自动添加前缀:`myapp:user:1`
313
+
314
+ ---
315
+
316
+ ## 实际应用场景
317
+
318
+ ### 场景1:表结构缓存
319
+
320
+ DbHelper 自动缓存表字段信息,避免重复查询数据库。
321
+
322
+ ```typescript
323
+ // 首次查询 - 缓存未命中,查询数据库
324
+ const columns = await befly.db.getTableColumns('user');
325
+ // ❌ Redis 缓存未命中
326
+ // 🔍 查询数据库表结构
327
+ // 📝 写入 Redis 缓存 (TTL: 3600s)
328
+
329
+ // 后续查询 - 直接从缓存获取
330
+ const columns = await befly.db.getTableColumns('user');
331
+ // ✅ Redis 缓存命中
332
+ ```
333
+
334
+ **PM2 Cluster 模式:** 多个 Worker 进程共享同一份 Redis 缓存,只有第一个进程需要查询数据库。
335
+
336
+ ### 场景2:接口权限缓存
337
+
338
+ 使用 Set 集合存储角色的接口权限,实现 O(1) 时间复杂度的权限检查。
339
+
340
+ ```typescript
341
+ // 缓存角色权限(启动时自动执行)
342
+ await befly.redis.sadd('befly:role:apis:admin', ['GET/api/user/list', 'POST/api/user/add', 'DELETE/api/user/del']);
343
+
344
+ // 权限检查(请求时)
345
+ const hasPermission = await befly.redis.sismember('befly:role:apis:admin', 'POST/api/user/add');
346
+ // 返回: true
347
+ ```
348
+
349
+ ### 场景3:会话管理
350
+
351
+ ```typescript
352
+ // 登录时创建会话
353
+ const sessionId = crypto.randomUUID();
354
+ await befly.redis.setObject(
355
+ `session:${sessionId}`,
356
+ {
357
+ userId: user.id,
358
+ username: user.username,
359
+ roleCode: user.roleCode,
360
+ loginTime: Date.now()
361
+ },
362
+ 7200
363
+ ); // 2小时过期
364
+
365
+ // 验证会话
366
+ const session = await befly.redis.getObject(`session:${sessionId}`);
367
+ if (!session) {
368
+ return befly.tool.No('会话已过期');
369
+ }
370
+
371
+ // 登出时删除会话
372
+ await befly.redis.delObject(`session:${sessionId}`);
373
+ ```
374
+
375
+ ### 场景4:Token 黑名单
376
+
377
+ ```typescript
378
+ // 用户登出时,将 token 加入黑名单
379
+ const token = ctx.req.headers.get('Authorization')?.replace('Bearer ', '');
380
+ if (token) {
381
+ const key = `token:blacklist:${token}`;
382
+ await befly.redis.setString(key, '1', 7 * 24 * 60 * 60); // 7天
383
+ }
384
+
385
+ // 验证时检查黑名单
386
+ const isBlacklisted = await befly.redis.exists(`token:blacklist:${token}`);
387
+ if (isBlacklisted) {
388
+ return befly.tool.No('Token 已失效');
389
+ }
390
+ ```
391
+
392
+ ### 场景5:接口限流
393
+
394
+ ```typescript
395
+ // 简单的滑动窗口限流
396
+ const key = `ratelimit:${ctx.ip}:${ctx.route}`;
397
+ const current = await befly.redis.getString(key);
398
+ const count = current ? parseInt(current) : 0;
399
+
400
+ if (count >= 100) {
401
+ // 每分钟最多 100 次
402
+ return befly.tool.No('请求过于频繁');
403
+ }
404
+
405
+ if (count === 0) {
406
+ await befly.redis.setString(key, '1', 60); // 60秒窗口
407
+ } else {
408
+ await befly.redis.setString(key, String(count + 1), await befly.redis.ttl(key));
409
+ }
410
+ ```
411
+
412
+ ### 场景6:分布式锁
413
+
414
+ ```typescript
415
+ // 获取锁
416
+ const lockKey = `lock:order:${orderId}`;
417
+ const acquired = await befly.redis.setString(lockKey, '1', 30); // 30秒自动释放
418
+
419
+ if (!acquired) {
420
+ return befly.tool.No('操作正在进行中,请稍后');
421
+ }
422
+
423
+ try {
424
+ // 执行业务逻辑
425
+ await processOrder(orderId);
426
+ } finally {
427
+ // 释放锁
428
+ await befly.redis.del(lockKey);
429
+ }
430
+ ```
431
+
432
+ ### 场景7:数据缓存
433
+
434
+ ```typescript
435
+ // 获取热门文章(带缓存)
436
+ const cacheKey = 'articles:hot:10';
437
+ let articles = await befly.redis.getObject(cacheKey);
438
+
439
+ if (!articles) {
440
+ // 缓存未命中,查询数据库
441
+ articles = await befly.db.getAll({
442
+ table: 'article',
443
+ fields: ['id', 'title', 'viewCount'],
444
+ orderBy: ['viewCount#DESC'],
445
+ limit: 10
446
+ });
447
+
448
+ // 写入缓存,5分钟过期
449
+ await befly.redis.setObject(cacheKey, articles, 300);
450
+ }
451
+
452
+ return befly.tool.Yes('成功', articles);
453
+ ```
454
+
455
+ ---
456
+
457
+ ## CacheHelper 缓存助手
458
+
459
+ 框架内置的缓存助手,管理接口、菜单和角色权限的缓存。
460
+
461
+ ### 服务启动时自动缓存
462
+
463
+ ```typescript
464
+ // 框架启动时自动调用
465
+ await befly.cache.cacheAll();
466
+
467
+ // 等同于依次执行:
468
+ await befly.cache.cacheApis(); // 缓存接口列表
469
+ await befly.cache.cacheMenus(); // 缓存菜单列表
470
+ await befly.cache.cacheRolePermissions(); // 缓存角色权限
471
+ ```
472
+
473
+ ### 获取缓存数据
474
+
475
+ ```typescript
476
+ // 获取所有接口
477
+ const apis = await befly.cache.getApis();
478
+
479
+ // 获取所有菜单
480
+ const menus = await befly.cache.getMenus();
481
+
482
+ // 获取角色权限
483
+ const permissions = await befly.cache.getRolePermissions('admin');
484
+ // 返回: ['GET/api/user/list', 'POST/api/user/add', ...]
485
+ ```
486
+
487
+ ### 权限检查
488
+
489
+ ```typescript
490
+ // 检查角色是否有指定接口权限
491
+ const hasPermission = await befly.cache.checkRolePermission('admin', 'POST/api/user/add');
492
+ // 返回: true 或 false
493
+ ```
494
+
495
+ ### 更新缓存
496
+
497
+ 角色权限变更后,需要刷新缓存:
498
+
499
+ ```typescript
500
+ // 删除指定角色的权限缓存
501
+ await befly.cache.deleteRolePermissions('admin');
502
+
503
+ // 重新缓存所有角色权限
504
+ await befly.cache.cacheRolePermissions();
505
+ ```
506
+
507
+ ---
508
+
509
+ ## 性能优化
510
+
511
+ ### 1. 利用 Bun 自动 Pipeline
512
+
513
+ Bun Redis 客户端自动将多个并发请求合并为 pipeline,无需手动处理。
514
+
515
+ ```typescript
516
+ // 这些请求会自动合并为一个 pipeline
517
+ const [user1, user2, user3] = await Promise.all([befly.redis.getObject('user:1'), befly.redis.getObject('user:2'), befly.redis.getObject('user:3')]);
518
+ ```
519
+
520
+ ### 2. 使用批量方法
521
+
522
+ 对于明确的批量操作,使用专用的批量方法:
523
+
524
+ ```typescript
525
+ // ✅ 推荐:使用批量方法
526
+ const users = await befly.redis.getBatch(['user:1', 'user:2', 'user:3']);
527
+
528
+ // ❌ 不推荐:循环调用
529
+ const users = [];
530
+ for (const id of [1, 2, 3]) {
531
+ users.push(await befly.redis.getObject(`user:${id}`));
532
+ }
533
+ ```
534
+
535
+ ### 3. 合理设置 TTL
536
+
537
+ ```typescript
538
+ // 高频访问、变化少的数据 - 较长 TTL
539
+ await befly.redis.setObject('config:system', config, 86400); // 24小时
540
+
541
+ // 实时性要求高的数据 - 较短 TTL
542
+ await befly.redis.setObject('stats:online', count, 60); // 1分钟
543
+
544
+ // 永久缓存(慎用)
545
+ await befly.redis.setObject('constants:provinces', provinces); // 无 TTL
546
+ ```
547
+
548
+ ### 4. 避免大 Key
549
+
550
+ ```typescript
551
+ // ❌ 避免:存储大量数据在单个 key
552
+ await befly.redis.setObject('all:users', hugeUserList); // 可能有 10MB+
553
+
554
+ // ✅ 推荐:分散存储
555
+ for (const user of users) {
556
+ await befly.redis.setObject(`user:${user.id}`, user);
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## 连接测试
563
+
564
+ ```typescript
565
+ // 测试 Redis 连接
566
+ const pong = await befly.redis.ping();
567
+ // 返回: 'PONG'
568
+ ```
569
+
570
+ ---
571
+
572
+ ## 错误处理
573
+
574
+ 所有 Redis 方法都内置了错误处理,不会抛出异常:
575
+
576
+ ```typescript
577
+ // 操作失败时返回默认值,不会中断程序
578
+ const value = await befly.redis.getObject('key'); // 返回 null
579
+ const exists = await befly.redis.exists('key'); // 返回 false
580
+ const count = await befly.redis.del('key'); // 返回 0
581
+
582
+ // 错误会记录到日志
583
+ // Logger.error('Redis getObject 错误', error);
584
+ ```
585
+
586
+ 如需捕获错误,可以检查返回值:
587
+
588
+ ```typescript
589
+ const result = await befly.redis.setObject('key', data);
590
+ if (result === null) {
591
+ Logger.warn('缓存写入失败');
592
+ // 降级处理...
593
+ }
594
+ ```
595
+
596
+ ---
597
+
598
+ ## 方法速查表
599
+
600
+ | 方法 | 说明 | 返回值 |
601
+ | ---------------- | ----------------- | ------------------ |
602
+ | `setString` | 设置字符串 | `'OK'` / `null` |
603
+ | `getString` | 获取字符串 | `string` / `null` |
604
+ | `setObject` | 设置对象(JSON) | `'OK'` / `null` |
605
+ | `getObject` | 获取对象(JSON) | `T` / `null` |
606
+ | `delObject` | 删除对象 | `void` |
607
+ | `exists` | 检查键是否存在 | `boolean` |
608
+ | `del` | 删除键 | `number` |
609
+ | `expire` | 设置过期时间 | `number` |
610
+ | `ttl` | 获取剩余过期时间 | `number` |
611
+ | `sadd` | 添加 Set 成员 | `number` |
612
+ | `sismember` | 检查 Set 成员 | `boolean` |
613
+ | `smembers` | 获取所有 Set 成员 | `string[]` |
614
+ | `scard` | 获取 Set 成员数量 | `number` |
615
+ | `genTimeID` | 生成唯一 ID | `number` |
616
+ | `ping` | 测试连接 | `'PONG'` |
617
+ | `setBatch` | 批量设置对象 | `number` |
618
+ | `getBatch` | 批量获取对象 | `Array<T \| null>` |
619
+ | `delBatch` | 批量删除键 | `number` |
620
+ | `existsBatch` | 批量检查存在 | `boolean[]` |
621
+ | `ttlBatch` | 批量获取 TTL | `number[]` |
622
+ | `expireBatch` | 批量设置过期时间 | `number` |
623
+ | `saddBatch` | 批量添加 Set 成员 | `number` |
624
+ | `sismemberBatch` | 批量检查 Set 成员 | `boolean[]` |
@@ -0,0 +1,84 @@
1
+ // 相对导入
2
+ import { Logger } from '../lib/logger.js';
3
+
4
+ // 类型导入
5
+ import type { Hook } from '../types/hook.js';
6
+
7
+ /** 单个字段最大记录长度(字符数) */
8
+ const MAX_FIELD_LENGTH = 500;
9
+
10
+ /**
11
+ * 截断单个值
12
+ */
13
+ function truncateValue(value: any): any {
14
+ if (value === null || value === undefined) {
15
+ return value;
16
+ }
17
+
18
+ if (typeof value === 'string') {
19
+ if (value.length > MAX_FIELD_LENGTH) {
20
+ return value.substring(0, MAX_FIELD_LENGTH) + `... [truncated, total ${value.length} chars]`;
21
+ }
22
+ return value;
23
+ }
24
+
25
+ if (Array.isArray(value)) {
26
+ const str = JSON.stringify(value);
27
+ if (str.length > MAX_FIELD_LENGTH) {
28
+ return `[Array, ${value.length} items, ${str.length} chars]`;
29
+ }
30
+ return value;
31
+ }
32
+
33
+ if (typeof value === 'object') {
34
+ const str = JSON.stringify(value);
35
+ if (str.length > MAX_FIELD_LENGTH) {
36
+ return `[Object, ${Object.keys(value).length} keys, ${str.length} chars]`;
37
+ }
38
+ return value;
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ /**
45
+ * 截断 body 对象的每个字段
46
+ */
47
+ function truncateBody(body: Record<string, any>): Record<string, any> {
48
+ const result: Record<string, any> = {};
49
+ for (const key in body) {
50
+ result[key] = truncateValue(body[key]);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * 请求日志钩子
57
+ * 在认证和解析之后记录完整的请求日志
58
+ * order: 5 (在 parser 之后、validator 之前)
59
+ */
60
+ const hook: Hook = {
61
+ order: 5,
62
+ handler: async (befly, ctx) => {
63
+ // 只记录有效的 API 请求
64
+ if (!ctx.api) return;
65
+
66
+ const logData: Record<string, any> = {
67
+ requestId: ctx.requestId,
68
+ route: ctx.route,
69
+ ip: ctx.ip,
70
+ userId: ctx.user?.id || '',
71
+ nickname: ctx.user?.nickname || '',
72
+ roleCode: ctx.user?.roleCode || '',
73
+ roleType: ctx.user?.roleType || ''
74
+ };
75
+
76
+ // 截断大请求体
77
+ if (ctx.body && Object.keys(ctx.body).length > 0) {
78
+ logData.body = truncateBody(ctx.body);
79
+ }
80
+
81
+ Logger.info(logData, '请求日志');
82
+ }
83
+ };
84
+ export default hook;
@@ -10,7 +10,7 @@ import type { Hook } from '../types/hook.js';
10
10
  * 根据 API 定义的 fields 和 required 验证请求参数
11
11
  */
12
12
  const hook: Hook = {
13
- order: 5,
13
+ order: 6,
14
14
  handler: async (befly, ctx) => {
15
15
  if (!ctx.api) return;
16
16
 
package/main.ts CHANGED
@@ -82,14 +82,34 @@ export class Befly {
82
82
  const server = Bun.serve({
83
83
  port: this.config!.appPort,
84
84
  hostname: this.config!.appHost,
85
+ // 开发模式下启用详细错误信息
86
+ development: this.config!.nodeEnv === 'development',
87
+ // 空闲连接超时时间(秒),防止恶意连接占用资源
88
+ idleTimeout: 30,
85
89
  routes: {
86
90
  '/': () => Response.json({ code: 0, msg: `${this.config!.appName} 接口服务已启动` }),
87
91
  '/api/*': apiHandler(this.apis, this.hooks, this.context as BeflyContext),
88
92
  '/*': staticHandler()
89
93
  },
94
+ // 未匹配路由的兜底处理
95
+ fetch: () => {
96
+ return Response.json({ code: 1, msg: '路由未找到' }, { status: 404 });
97
+ },
90
98
  error: (error: Error) => {
91
99
  Logger.error({ err: error }, '服务启动时发生错误');
92
- return Response.json({ code: 1, msg: '内部服务器错误' });
100
+ // 开发模式下返回详细错误信息
101
+ if (this.config!.nodeEnv === 'development') {
102
+ return Response.json(
103
+ {
104
+ code: 1,
105
+ msg: '内部服务器错误',
106
+ error: error.message,
107
+ stack: error.stack
108
+ },
109
+ { status: 200 }
110
+ );
111
+ }
112
+ return Response.json({ code: 1, msg: '内部服务器错误' }, { status: 200 });
93
113
  }
94
114
  });
95
115
 
@@ -100,13 +120,19 @@ export class Befly {
100
120
 
101
121
  Logger.info(`${this.config!.appName} 启动成功! (${roleLabel}${envLabel})`);
102
122
  Logger.info(`服务器启动耗时: ${finalStartupTime}`);
103
- Logger.info(`服务器监听地址: http://${this.config!.appHost}:${this.config!.appPort}`);
123
+ Logger.info(`服务器监听地址: ${server.url}`);
104
124
 
105
125
  // 7. 注册优雅关闭处理
106
126
  const gracefulShutdown = async (signal: string) => {
107
- // 停止接收新请求
108
- server.stop(true);
109
- Logger.info('HTTP 服务器已停止');
127
+ Logger.info(`收到 ${signal} 信号,开始优雅关闭...`);
128
+
129
+ // 优雅停止(等待进行中的请求完成)
130
+ try {
131
+ await server.stop();
132
+ Logger.info('HTTP 服务器已停止');
133
+ } catch (error: any) {
134
+ Logger.error({ err: error }, '停止 HTTP 服务器时出错');
135
+ }
110
136
 
111
137
  // 关闭数据库连接
112
138
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.9.7",
3
+ "version": "3.9.8",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -74,7 +74,7 @@
74
74
  "pino": "^10.1.0",
75
75
  "pino-roll": "^4.0.0"
76
76
  },
77
- "gitHead": "2a0ccabca4ae72fbf490d41a6c3e5aeb63fb08c6",
77
+ "gitHead": "6ee1c2a6513518c9864a8652ed9c50115ada5c7b",
78
78
  "devDependencies": {
79
79
  "typescript": "^5.9.3"
80
80
  }