befly 3.8.29 → 3.8.31
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 +91 -6
- package/checks/checkApi.ts +2 -1
- package/checks/checkApp.ts +31 -1
- package/checks/checkTable.ts +3 -2
- package/hooks/cors.ts +3 -3
- package/hooks/parser.ts +8 -6
- package/hooks/permission.ts +12 -5
- package/hooks/validator.ts +1 -1
- package/lib/cacheHelper.ts +73 -65
- package/lib/cipher.ts +2 -1
- package/lib/connect.ts +23 -52
- package/lib/dbHelper.ts +14 -11
- package/lib/jwt.ts +58 -437
- package/lib/logger.ts +76 -197
- package/lib/redisHelper.ts +163 -1
- package/lib/sqlBuilder.ts +2 -1
- package/lib/validator.ts +150 -384
- package/loader/loadApis.ts +4 -7
- package/loader/loadHooks.ts +6 -5
- package/loader/loadPlugins.ts +11 -13
- package/main.ts +26 -53
- package/package.json +10 -8
- package/paths.ts +0 -6
- package/plugins/cipher.ts +1 -1
- package/plugins/config.ts +3 -4
- package/plugins/db.ts +6 -7
- package/plugins/jwt.ts +7 -6
- package/plugins/logger.ts +6 -6
- package/plugins/redis.ts +9 -13
- package/router/api.ts +2 -2
- package/router/static.ts +4 -8
- package/sync/syncAll.ts +8 -13
- package/sync/syncApi.ts +14 -10
- package/sync/syncDb/apply.ts +1 -2
- package/sync/syncDb.ts +12 -15
- package/sync/syncDev.ts +19 -56
- package/sync/syncMenu.ts +182 -137
- package/tests/cacheHelper.test.ts +327 -0
- package/tests/dbHelper-columns.test.ts +5 -20
- package/tests/dbHelper-execute.test.ts +14 -68
- package/tests/fields-redis-cache.test.ts +5 -3
- package/tests/integration.test.ts +17 -32
- package/tests/jwt.test.ts +36 -94
- package/tests/logger.test.ts +32 -34
- package/tests/redisHelper.test.ts +271 -2
- package/tests/redisKeys.test.ts +76 -0
- package/tests/sync-connection.test.ts +0 -6
- package/tests/syncDb-constants.test.ts +12 -12
- package/tests/util.test.ts +5 -1
- package/tests/validator.test.ts +611 -85
- package/types/befly.d.ts +9 -15
- package/types/cache.d.ts +73 -0
- package/types/common.d.ts +10 -128
- package/types/database.d.ts +221 -5
- package/types/index.ts +6 -5
- package/types/plugin.d.ts +1 -4
- package/types/redis.d.ts +37 -2
- package/types/table.d.ts +175 -0
- package/config.ts +0 -70
- package/hooks/_rateLimit.ts +0 -64
- package/lib/regexAliases.ts +0 -59
- package/lib/xml.ts +0 -383
- package/tests/validator-advanced.test.ts +0 -653
- package/tests/xml.test.ts +0 -101
- package/types/addon.d.ts +0 -50
- package/types/crypto.d.ts +0 -23
- package/types/jwt.d.ts +0 -99
- package/types/logger.d.ts +0 -43
- package/types/tool.d.ts +0 -67
- package/types/validator.d.ts +0 -43
package/lib/logger.ts
CHANGED
|
@@ -1,216 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 日志系统 -
|
|
3
|
-
* 直接集成环境变量,提供开箱即用的日志功能
|
|
2
|
+
* 日志系统 - 基于 pino 实现
|
|
4
3
|
*/
|
|
5
4
|
|
|
5
|
+
import pino from 'pino';
|
|
6
6
|
import { join } from 'pathe';
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
|
|
7
|
+
|
|
8
|
+
import type { LoggerConfig } from 'befly-shared/types';
|
|
9
|
+
|
|
10
|
+
let instance: pino.Logger | null = null;
|
|
11
|
+
let mockInstance: pino.Logger | null = null;
|
|
12
|
+
let config: LoggerConfig = {
|
|
13
|
+
debug: 0,
|
|
14
|
+
dir: './logs',
|
|
15
|
+
console: 1,
|
|
16
|
+
maxSize: 10
|
|
17
|
+
};
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
|
-
*
|
|
20
|
+
* 配置日志
|
|
13
21
|
*/
|
|
14
|
-
|
|
22
|
+
export function configure(cfg: LoggerConfig): void {
|
|
23
|
+
config = { ...config, ...cfg };
|
|
24
|
+
instance = null;
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
/**
|
|
17
|
-
*
|
|
28
|
+
* 设置 Mock Logger(用于测试)
|
|
29
|
+
* @param mock - Mock pino 实例,传 null 清除 mock
|
|
18
30
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
const year = now.getFullYear();
|
|
22
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
23
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
24
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
25
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
26
|
-
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
27
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
31
|
+
export function setMockLogger(mock: pino.Logger | null): void {
|
|
32
|
+
mockInstance = mock;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
36
|
+
* 获取 pino 日志实例
|
|
32
37
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 记录日志
|
|
56
|
-
* @param level - 日志级别
|
|
57
|
-
* @param message - 日志消息
|
|
58
|
-
*/
|
|
59
|
-
static async log(level: LogLevel, message: LogMessage): Promise<void> {
|
|
60
|
-
// debug 日志特殊处理:仅当 LOG_DEBUG=1 时才记录
|
|
61
|
-
if (level === 'debug' && this.config.debug !== 1) return;
|
|
62
|
-
|
|
63
|
-
// 格式化消息
|
|
64
|
-
const timestamp = formatDate();
|
|
65
|
-
|
|
66
|
-
// 处理消息内容
|
|
67
|
-
let content = '';
|
|
68
|
-
if (typeof message === 'object' && message !== null && Object.keys(message).length > 0) {
|
|
69
|
-
content = JSON.stringify(message, null, 0).replace(/\\"/g, '"');
|
|
70
|
-
} else {
|
|
71
|
-
content = String(message);
|
|
38
|
+
export function getLogger(): pino.Logger {
|
|
39
|
+
// 优先返回 mock 实例(用于测试)
|
|
40
|
+
if (mockInstance) return mockInstance;
|
|
41
|
+
|
|
42
|
+
if (instance) return instance;
|
|
43
|
+
|
|
44
|
+
const level = config.debug === 1 ? 'debug' : 'info';
|
|
45
|
+
const targets: pino.TransportTargetOptions[] = [];
|
|
46
|
+
|
|
47
|
+
// 文件输出
|
|
48
|
+
targets.push({
|
|
49
|
+
target: 'pino-roll',
|
|
50
|
+
level: level,
|
|
51
|
+
options: {
|
|
52
|
+
file: join(config.dir || './logs', 'app'),
|
|
53
|
+
frequency: 'daily',
|
|
54
|
+
size: `${config.maxSize || 10}m`,
|
|
55
|
+
mkdir: true,
|
|
56
|
+
dateFormat: 'yyyy-MM-dd'
|
|
72
57
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 文件输出
|
|
84
|
-
await this.writeToFile(logMessage, level);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// 控制台输出
|
|
61
|
+
if (config.console === 1) {
|
|
62
|
+
targets.push({
|
|
63
|
+
target: 'pino/file',
|
|
64
|
+
level: level,
|
|
65
|
+
options: { destination: 1 }
|
|
66
|
+
});
|
|
85
67
|
}
|
|
86
68
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
static async success(message: LogMessage): Promise<void> {
|
|
92
|
-
await this.log('info', message);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 写入日志文件
|
|
97
|
-
* @param message - 格式化后的消息
|
|
98
|
-
* @param level - 日志级别
|
|
99
|
-
*/
|
|
100
|
-
static async writeToFile(message: string, level: LogLevel = 'info'): Promise<void> {
|
|
101
|
-
try {
|
|
102
|
-
const logDir = this.config.dir || './logs';
|
|
103
|
-
// 确定文件前缀
|
|
104
|
-
const prefix = level === 'debug' ? 'debug' : new Date().toISOString().split('T')[0];
|
|
105
|
-
|
|
106
|
-
// 检查缓存的当前文件是否仍然可用
|
|
107
|
-
let currentLogFile = this.currentFiles.get(prefix);
|
|
108
|
-
|
|
109
|
-
if (currentLogFile) {
|
|
110
|
-
try {
|
|
111
|
-
const stats = await stat(currentLogFile);
|
|
112
|
-
if (stats.size >= (this.config.maxSize || 10 * 1024 * 1024)) {
|
|
113
|
-
this.currentFiles.delete(prefix);
|
|
114
|
-
currentLogFile = undefined;
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
this.currentFiles.delete(prefix);
|
|
118
|
-
currentLogFile = undefined;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 查找或创建新文件
|
|
123
|
-
if (!currentLogFile) {
|
|
124
|
-
const glob = new Bun.Glob(`${prefix}.*.log`);
|
|
125
|
-
const files = await Array.fromAsync(glob.scan(this.config.dir || 'logs'));
|
|
126
|
-
|
|
127
|
-
// 按索引排序并查找可用文件
|
|
128
|
-
const getIndex = (f: string) => parseInt(f.match(/\.(\d+)\.log$/)?.[1] || '0');
|
|
129
|
-
files.sort((a, b) => getIndex(a) - getIndex(b));
|
|
130
|
-
|
|
131
|
-
let foundFile = false;
|
|
132
|
-
for (let i = files.length - 1; i >= 0; i--) {
|
|
133
|
-
const filePath = join(this.config.dir || 'logs', files[i]);
|
|
134
|
-
try {
|
|
135
|
-
const stats = await stat(filePath);
|
|
136
|
-
// 检查文件大小
|
|
137
|
-
if (stats.size < (this.config.maxSize || 10 * 1024 * 1024)) {
|
|
138
|
-
currentLogFile = filePath;
|
|
139
|
-
foundFile = true;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 没有可用文件,创建新文件
|
|
148
|
-
if (!foundFile) {
|
|
149
|
-
const maxIndex = files.length > 0 ? Math.max(...files.map(getIndex)) : -1;
|
|
150
|
-
currentLogFile = join(this.config.dir || 'logs', `${prefix}.${maxIndex + 1}.log`);
|
|
151
|
-
}
|
|
69
|
+
instance = pino({
|
|
70
|
+
level: level,
|
|
71
|
+
transport: { targets: targets }
|
|
72
|
+
});
|
|
152
73
|
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
await appendFile(currentLogFile, message + '\n', 'utf8');
|
|
157
|
-
} catch (error: any) {
|
|
158
|
-
console.error('写入日志文件失败:', error?.message || error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 记录错误日志
|
|
164
|
-
* @param name - 错误名称/位置
|
|
165
|
-
* @param error - 错误对象或消息
|
|
166
|
-
*/
|
|
167
|
-
static async error(name: string, error?: any): Promise<void> {
|
|
168
|
-
if (!error) {
|
|
169
|
-
return this.log('error', name);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 构建错误消息
|
|
173
|
-
const parts = [name];
|
|
174
|
-
if (error?.message || error?.stack) {
|
|
175
|
-
if (error.message) parts.push(error.message);
|
|
176
|
-
if (error.stack) parts.push('\n' + error.stack);
|
|
177
|
-
} else {
|
|
178
|
-
const errorStr = typeof error === 'object' ? JSON.stringify(error) : String(error);
|
|
179
|
-
parts.push(errorStr);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
await this.log('error', parts.join(' - '));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 记录警告日志
|
|
187
|
-
* @param message - 日志消息
|
|
188
|
-
*/
|
|
189
|
-
static async warn(message: LogMessage): Promise<void> {
|
|
190
|
-
await this.log('warn', message);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* 记录信息日志
|
|
195
|
-
* @param message - 日志消息
|
|
196
|
-
*/
|
|
197
|
-
static async info(message: LogMessage): Promise<void> {
|
|
198
|
-
await this.log('info', message);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 记录调试日志
|
|
203
|
-
* 受 enableDebug 配置控制,仅当 enableDebug=true 时才记录
|
|
204
|
-
* @param message - 日志消息
|
|
205
|
-
*/
|
|
206
|
-
static async debug(message: LogMessage): Promise<void> {
|
|
207
|
-
await this.log('debug', message);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 清除文件缓存
|
|
212
|
-
*/
|
|
213
|
-
static clearCache(): void {
|
|
214
|
-
this.currentFiles.clear();
|
|
215
|
-
}
|
|
74
|
+
return instance;
|
|
216
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 日志实例(延迟初始化)
|
|
79
|
+
*/
|
|
80
|
+
export const Logger = {
|
|
81
|
+
get info() {
|
|
82
|
+
return getLogger().info.bind(getLogger());
|
|
83
|
+
},
|
|
84
|
+
get warn() {
|
|
85
|
+
return getLogger().warn.bind(getLogger());
|
|
86
|
+
},
|
|
87
|
+
get error() {
|
|
88
|
+
return getLogger().error.bind(getLogger());
|
|
89
|
+
},
|
|
90
|
+
get debug() {
|
|
91
|
+
return getLogger().debug.bind(getLogger());
|
|
92
|
+
},
|
|
93
|
+
configure: configure,
|
|
94
|
+
setMock: setMockLogger
|
|
95
|
+
};
|
package/lib/redisHelper.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { SQL, RedisClient } from 'bun';
|
|
7
7
|
import { Logger } from './logger.js';
|
|
8
8
|
import { Connect } from './connect.js';
|
|
9
|
-
import type { KeyValue } from '
|
|
9
|
+
import type { KeyValue } from 'befly-shared/types';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Redis 助手类
|
|
@@ -79,6 +79,11 @@ export class RedisHelper {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// ==================== ID 生成 ====================
|
|
83
|
+
// 注意:ID 生成功能强依赖 Redis 原子操作(INCR/INCRBY)保证分布式唯一性
|
|
84
|
+
// 主要被 DbHelper.insData/insBatch 使用
|
|
85
|
+
// 如未来有其他 ID 生成需求,可考虑提取到独立模块
|
|
86
|
+
|
|
82
87
|
/**
|
|
83
88
|
* 生成基于时间的唯一 ID
|
|
84
89
|
* 格式: 秒级时间戳(10位) + 4位自增 = 14位纯数字
|
|
@@ -209,6 +214,25 @@ export class RedisHelper {
|
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
/**
|
|
218
|
+
* 批量获取剩余过期时间(利用 Bun Redis 自动管道优化)
|
|
219
|
+
* @param keys - 键名数组
|
|
220
|
+
* @returns TTL 数组(-2 表示键不存在,-1 表示无过期时间)
|
|
221
|
+
*/
|
|
222
|
+
async ttlBatch(keys: string[]): Promise<number[]> {
|
|
223
|
+
if (keys.length === 0) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const results = await Promise.all(keys.map((key) => this.ttl(key)));
|
|
229
|
+
return results;
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
Logger.error('Redis ttlBatch 错误', error);
|
|
232
|
+
return keys.map(() => -1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
212
236
|
/**
|
|
213
237
|
* 向 Set 中添加一个或多个成员
|
|
214
238
|
* @param key - 键名
|
|
@@ -273,6 +297,44 @@ export class RedisHelper {
|
|
|
273
297
|
}
|
|
274
298
|
}
|
|
275
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 批量向多个 Set 添加成员(利用 Bun Redis 自动管道优化)
|
|
302
|
+
* @param items - [{ key, members }] 数组
|
|
303
|
+
* @returns 成功添加的总成员数量
|
|
304
|
+
*/
|
|
305
|
+
async saddBatch(items: Array<{ key: string; members: string[] }>): Promise<number> {
|
|
306
|
+
if (items.length === 0) {
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const results = await Promise.all(items.map((item) => this.sadd(item.key, item.members)));
|
|
312
|
+
return results.reduce((sum, count) => sum + count, 0);
|
|
313
|
+
} catch (error: any) {
|
|
314
|
+
Logger.error('Redis saddBatch 错误', error);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 批量检查成员是否在 Set 中(利用 Bun Redis 自动管道优化)
|
|
321
|
+
* @param items - [{ key, member }] 数组
|
|
322
|
+
* @returns 布尔数组(true 表示存在,false 表示不存在)
|
|
323
|
+
*/
|
|
324
|
+
async sismemberBatch(items: Array<{ key: string; member: string }>): Promise<boolean[]> {
|
|
325
|
+
if (items.length === 0) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const results = await Promise.all(items.map((item) => this.sismember(item.key, item.member)));
|
|
331
|
+
return results.map((r) => r > 0);
|
|
332
|
+
} catch (error: any) {
|
|
333
|
+
Logger.error('Redis sismemberBatch 错误', error);
|
|
334
|
+
return items.map(() => false);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
276
338
|
/**
|
|
277
339
|
* 删除键
|
|
278
340
|
* @param key - 键名
|
|
@@ -288,6 +350,106 @@ export class RedisHelper {
|
|
|
288
350
|
}
|
|
289
351
|
}
|
|
290
352
|
|
|
353
|
+
/**
|
|
354
|
+
* 批量删除键(利用 Bun Redis 自动管道优化)
|
|
355
|
+
* @param keys - 键名数组
|
|
356
|
+
* @returns 成功删除的键数量
|
|
357
|
+
*/
|
|
358
|
+
async delBatch(keys: string[]): Promise<number> {
|
|
359
|
+
if (keys.length === 0) {
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const results = await Promise.all(
|
|
365
|
+
keys.map((key) => {
|
|
366
|
+
const pkey = `${this.prefix}${key}`;
|
|
367
|
+
return this.client.del(pkey);
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
return results.reduce((sum, count) => sum + count, 0);
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
Logger.error('Redis delBatch 错误', error);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 批量设置对象(利用 Bun Redis 自动管道优化)
|
|
379
|
+
* @param items - 键值对数组 [{ key, value, ttl? }]
|
|
380
|
+
* @returns 成功设置的数量
|
|
381
|
+
*/
|
|
382
|
+
async setBatch<T = any>(items: Array<{ key: string; value: T; ttl?: number | null }>): Promise<number> {
|
|
383
|
+
if (items.length === 0) {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const results = await Promise.all(items.map((item) => this.setObject(item.key, item.value, item.ttl ?? null)));
|
|
389
|
+
return results.filter((r) => r !== null).length;
|
|
390
|
+
} catch (error: any) {
|
|
391
|
+
Logger.error('Redis setBatch 错误', error);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 批量获取对象(利用 Bun Redis 自动管道优化)
|
|
398
|
+
* @param keys - 键名数组
|
|
399
|
+
* @returns 对象数组(不存在的键返回 null)
|
|
400
|
+
*/
|
|
401
|
+
async getBatch<T = any>(keys: string[]): Promise<Array<T | null>> {
|
|
402
|
+
if (keys.length === 0) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const results = await Promise.all(keys.map((key) => this.getObject<T>(key)));
|
|
408
|
+
return results;
|
|
409
|
+
} catch (error: any) {
|
|
410
|
+
Logger.error('Redis getBatch 错误', error);
|
|
411
|
+
return keys.map(() => null);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 批量检查键是否存在(利用 Bun Redis 自动管道优化)
|
|
417
|
+
* @param keys - 键名数组
|
|
418
|
+
* @returns 布尔数组(true 表示存在,false 表示不存在)
|
|
419
|
+
*/
|
|
420
|
+
async existsBatch(keys: string[]): Promise<boolean[]> {
|
|
421
|
+
if (keys.length === 0) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const results = await Promise.all(keys.map((key) => this.exists(key)));
|
|
427
|
+
return results.map((r) => r > 0);
|
|
428
|
+
} catch (error: any) {
|
|
429
|
+
Logger.error('Redis existsBatch 错误', error);
|
|
430
|
+
return keys.map(() => false);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 批量设置过期时间(利用 Bun Redis 自动管道优化)
|
|
436
|
+
* @param items - 键名和过期时间数组 [{ key, seconds }]
|
|
437
|
+
* @returns 成功设置的数量
|
|
438
|
+
*/
|
|
439
|
+
async expireBatch(items: Array<{ key: string; seconds: number }>): Promise<number> {
|
|
440
|
+
if (items.length === 0) {
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const results = await Promise.all(items.map((item) => this.expire(item.key, item.seconds)));
|
|
446
|
+
return results.filter((r) => r > 0).length;
|
|
447
|
+
} catch (error: any) {
|
|
448
|
+
Logger.error('Redis expireBatch 错误', error);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
291
453
|
/**
|
|
292
454
|
* 测试 Redis 连接
|
|
293
455
|
* @returns ping 响应结果
|
package/lib/sqlBuilder.ts
CHANGED