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 +624 -0
- package/hooks/requestLogger.ts +84 -0
- package/hooks/validator.ts +1 -1
- package/main.ts +31 -5
- package/package.json +2 -2
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;
|
package/hooks/validator.ts
CHANGED
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
|
-
|
|
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(`服务器监听地址:
|
|
123
|
+
Logger.info(`服务器监听地址: ${server.url}`);
|
|
104
124
|
|
|
105
125
|
// 7. 注册优雅关闭处理
|
|
106
126
|
const gracefulShutdown = async (signal: string) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
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": "
|
|
77
|
+
"gitHead": "6ee1c2a6513518c9864a8652ed9c50115ada5c7b",
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"typescript": "^5.9.3"
|
|
80
80
|
}
|