befly 3.17.0 → 3.17.2
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 +6 -0
- package/apis/admin/cacheRefresh.js +122 -0
- package/apis/admin/del.js +34 -0
- package/apis/admin/detail.js +23 -0
- package/apis/admin/ins.js +69 -0
- package/apis/admin/list.js +28 -0
- package/apis/admin/upd.js +95 -0
- package/apis/api/all.js +24 -0
- package/apis/api/list.js +31 -0
- package/apis/auth/login.js +123 -0
- package/apis/auth/sendSmsCode.js +24 -0
- package/apis/dashboard/configStatus.js +39 -0
- package/apis/dashboard/environmentInfo.js +43 -0
- package/apis/dashboard/performanceMetrics.js +20 -0
- package/apis/dashboard/permissionStats.js +27 -0
- package/apis/dashboard/serviceStatus.js +75 -0
- package/apis/dashboard/systemInfo.js +19 -0
- package/apis/dashboard/systemOverview.js +30 -0
- package/apis/dashboard/systemResources.js +106 -0
- package/apis/dict/all.js +23 -0
- package/apis/dict/del.js +16 -0
- package/apis/dict/detail.js +27 -0
- package/apis/dict/ins.js +51 -0
- package/apis/dict/items.js +30 -0
- package/apis/dict/list.js +36 -0
- package/apis/dict/upd.js +74 -0
- package/apis/dictType/all.js +16 -0
- package/apis/dictType/del.js +38 -0
- package/apis/dictType/detail.js +20 -0
- package/apis/dictType/ins.js +37 -0
- package/apis/dictType/list.js +26 -0
- package/apis/dictType/upd.js +51 -0
- package/apis/email/config.js +25 -0
- package/apis/email/logList.js +23 -0
- package/apis/email/send.js +66 -0
- package/apis/email/verify.js +21 -0
- package/apis/loginLog/list.js +23 -0
- package/apis/menu/all.js +41 -0
- package/apis/menu/list.js +25 -0
- package/apis/operateLog/list.js +23 -0
- package/apis/role/all.js +21 -0
- package/apis/role/apiSave.js +43 -0
- package/apis/role/apis.js +22 -0
- package/apis/role/del.js +49 -0
- package/apis/role/detail.js +32 -0
- package/apis/role/ins.js +46 -0
- package/apis/role/list.js +27 -0
- package/apis/role/menuSave.js +42 -0
- package/apis/role/menus.js +22 -0
- package/apis/role/save.js +40 -0
- package/apis/role/upd.js +50 -0
- package/apis/sysConfig/all.js +16 -0
- package/apis/sysConfig/del.js +36 -0
- package/apis/sysConfig/get.js +49 -0
- package/apis/sysConfig/ins.js +50 -0
- package/apis/sysConfig/list.js +24 -0
- package/apis/sysConfig/upd.js +62 -0
- package/checks/api.js +55 -0
- package/checks/config.js +107 -0
- package/checks/hook.js +38 -0
- package/checks/menu.js +58 -0
- package/checks/plugin.js +38 -0
- package/checks/table.js +78 -0
- package/configs/beflyConfig.json +61 -0
- package/configs/beflyMenus.json +85 -0
- package/configs/constConfig.js +34 -0
- package/configs/regexpAlias.json +55 -0
- package/hooks/auth.js +34 -0
- package/hooks/cors.js +39 -0
- package/hooks/parser.js +90 -0
- package/hooks/permission.js +71 -0
- package/hooks/validator.js +43 -0
- package/index.js +326 -0
- package/lib/cacheHelper.js +483 -0
- package/lib/cacheKeys.js +42 -0
- package/lib/connect.js +120 -0
- package/lib/dbHelper/builders.js +698 -0
- package/lib/dbHelper/context.js +131 -0
- package/lib/dbHelper/dataOps.js +505 -0
- package/lib/dbHelper/execute.js +65 -0
- package/lib/dbHelper/index.js +27 -0
- package/lib/dbHelper/transaction.js +43 -0
- package/lib/dbHelper/util.js +58 -0
- package/lib/dbHelper/validate.js +549 -0
- package/lib/emailHelper.js +110 -0
- package/lib/logger.js +604 -0
- package/lib/redisHelper.js +684 -0
- package/lib/sqlBuilder/batch.js +113 -0
- package/lib/sqlBuilder/check.js +150 -0
- package/lib/sqlBuilder/compiler.js +347 -0
- package/lib/sqlBuilder/errors.js +60 -0
- package/lib/sqlBuilder/index.js +218 -0
- package/lib/sqlBuilder/parser.js +296 -0
- package/lib/sqlBuilder/util.js +260 -0
- package/lib/validator.js +303 -0
- package/package.json +19 -12
- package/paths.js +112 -0
- package/plugins/cache.js +16 -0
- package/plugins/config.js +11 -0
- package/plugins/email.js +27 -0
- package/plugins/logger.js +20 -0
- package/plugins/mysql.js +36 -0
- package/plugins/redis.js +34 -0
- package/plugins/tool.js +155 -0
- package/router/api.js +140 -0
- package/router/static.js +71 -0
- package/sql/admin.sql +18 -0
- package/sql/api.sql +12 -0
- package/sql/dict.sql +13 -0
- package/sql/dictType.sql +12 -0
- package/sql/emailLog.sql +20 -0
- package/sql/loginLog.sql +25 -0
- package/sql/menu.sql +12 -0
- package/sql/operateLog.sql +22 -0
- package/sql/role.sql +14 -0
- package/sql/sysConfig.sql +16 -0
- package/sync/api.js +93 -0
- package/sync/cache.js +13 -0
- package/sync/dev.js +171 -0
- package/sync/menu.js +99 -0
- package/tables/admin.json +56 -0
- package/tables/api.json +26 -0
- package/tables/dict.json +30 -0
- package/tables/dictType.json +24 -0
- package/tables/emailLog.json +61 -0
- package/tables/loginLog.json +86 -0
- package/tables/menu.json +24 -0
- package/tables/operateLog.json +68 -0
- package/tables/role.json +32 -0
- package/tables/sysConfig.json +43 -0
- package/utils/calcPerfTime.js +13 -0
- package/utils/cors.js +17 -0
- package/utils/deepMerge.js +78 -0
- package/utils/fieldClear.js +65 -0
- package/utils/formatYmdHms.js +23 -0
- package/utils/formatZodIssues.js +109 -0
- package/utils/getClientIp.js +47 -0
- package/utils/importDefault.js +51 -0
- package/utils/is.js +462 -0
- package/utils/loggerUtils.js +185 -0
- package/utils/processInfo.js +39 -0
- package/utils/regexpUtil.js +52 -0
- package/utils/response.js +114 -0
- package/utils/scanFiles.js +124 -0
- package/utils/scanSources.js +68 -0
- package/utils/sortModules.js +75 -0
- package/utils/toSessionTtlSeconds.js +14 -0
- package/utils/util.js +374 -0
- package/befly.js +0 -16413
- package/befly.min.js +0 -72
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis 助手 - JavaScript 版本
|
|
3
|
+
* 提供 Redis 操作的便捷方法
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Connect } from "./connect.js";
|
|
7
|
+
import { Logger } from "./logger.js";
|
|
8
|
+
import { isFiniteNumber, isNonEmptyString, isString } from "../utils/is.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Redis 助手类
|
|
12
|
+
* 约定:除构造函数外,方法默认不抛异常;失败时返回 null/false/0/[] 并记录日志。
|
|
13
|
+
*/
|
|
14
|
+
export class RedisHelper {
|
|
15
|
+
slowThresholdMs = 500;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 构造函数
|
|
19
|
+
* @param prefix - Key 前缀
|
|
20
|
+
*/
|
|
21
|
+
constructor(prefix = "") {
|
|
22
|
+
const client = Connect.getRedis();
|
|
23
|
+
if (!client) {
|
|
24
|
+
throw new Error("Redis 客户端未初始化,请先调用 Connect.connectRedis()", {
|
|
25
|
+
cause: null,
|
|
26
|
+
code: "runtime"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.prefix = prefix ? `${prefix}:` : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
logSlow(cmd, key, duration, extra = {}) {
|
|
34
|
+
if (duration <= this.slowThresholdMs) return;
|
|
35
|
+
Logger.warn("🐌 Redis 慢操作", {
|
|
36
|
+
subsystem: "redis",
|
|
37
|
+
event: "slow",
|
|
38
|
+
duration: duration,
|
|
39
|
+
cmd: cmd,
|
|
40
|
+
key: key,
|
|
41
|
+
extra: extra
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 设置对象到 Redis
|
|
47
|
+
* @param key - 键名
|
|
48
|
+
* @param obj - 对象
|
|
49
|
+
* @param ttl - 过期时间(秒)
|
|
50
|
+
* @returns 操作结果
|
|
51
|
+
*/
|
|
52
|
+
async setObject(key, obj, ttl = null) {
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.stringify(obj);
|
|
55
|
+
const pkey = `${this.prefix}${key}`;
|
|
56
|
+
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
|
|
59
|
+
if (ttl) {
|
|
60
|
+
const res = await this.client.setex(pkey, ttl, data);
|
|
61
|
+
const duration = Date.now() - startTime;
|
|
62
|
+
this.logSlow("SETEX", pkey, duration, { ttl: ttl });
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = await this.client.set(pkey, data);
|
|
67
|
+
const duration = Date.now() - startTime;
|
|
68
|
+
this.logSlow("SET", pkey, duration);
|
|
69
|
+
return res;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
Logger.error("Redis setObject 错误", error);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 从 Redis 获取对象
|
|
78
|
+
* @param key - 键名
|
|
79
|
+
* @returns 对象或 null
|
|
80
|
+
*/
|
|
81
|
+
async getObject(key) {
|
|
82
|
+
try {
|
|
83
|
+
const pkey = `${this.prefix}${key}`;
|
|
84
|
+
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
const data = await this.client.get(pkey);
|
|
87
|
+
const duration = Date.now() - startTime;
|
|
88
|
+
this.logSlow("GET", pkey, duration);
|
|
89
|
+
return data ? JSON.parse(data) : null;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
Logger.error("Redis getObject 错误", error);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 从 Redis 删除对象
|
|
98
|
+
* @param key - 键名
|
|
99
|
+
*/
|
|
100
|
+
async delObject(key) {
|
|
101
|
+
try {
|
|
102
|
+
const pkey = `${this.prefix}${key}`;
|
|
103
|
+
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
await this.client.del(pkey);
|
|
106
|
+
const duration = Date.now() - startTime;
|
|
107
|
+
this.logSlow("DEL", pkey, duration);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
Logger.error("Redis delObject 错误", error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ==================== ID 生成 ====================
|
|
114
|
+
// 注意:ID 生成功能强依赖 Redis 原子操作(INCR)保证分布式唯一性
|
|
115
|
+
// 主要被 DbHelper.insData 使用
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 生成基于时间的唯一 ID
|
|
119
|
+
* 格式: 毫秒时间戳(13位) + 3位后缀(100-999) = 16位纯数字
|
|
120
|
+
* 每毫秒起点基于时间戳偏移,后缀分布更均匀
|
|
121
|
+
* @returns 唯一 ID (16位纯数字)
|
|
122
|
+
*/
|
|
123
|
+
async genTimeID() {
|
|
124
|
+
const timestamp = Date.now();
|
|
125
|
+
const key = `${this.prefix}time_id:${timestamp}`;
|
|
126
|
+
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
|
|
129
|
+
const counter = await this.client.incr(key);
|
|
130
|
+
if (counter === 1) {
|
|
131
|
+
await this.client.expire(key, 1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const duration = Date.now() - startTime;
|
|
135
|
+
this.logSlow("INCR", key, duration, { expireSeconds: 1 });
|
|
136
|
+
|
|
137
|
+
// 基于时间戳偏移起点,后缀 100-999 循环
|
|
138
|
+
const suffix = 100 + (((timestamp % 900) + counter - 1) % 900);
|
|
139
|
+
|
|
140
|
+
const timeId = Number(`${timestamp}${suffix}`);
|
|
141
|
+
return timeId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 设置字符串值
|
|
146
|
+
* @param key - 键名
|
|
147
|
+
* @param value - 值
|
|
148
|
+
* @param ttl - 过期时间(秒)
|
|
149
|
+
*/
|
|
150
|
+
async setString(key, value, ttl = null) {
|
|
151
|
+
try {
|
|
152
|
+
const pkey = `${this.prefix}${key}`;
|
|
153
|
+
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
if (ttl) {
|
|
156
|
+
const res = await this.client.setex(pkey, ttl, value);
|
|
157
|
+
const duration = Date.now() - startTime;
|
|
158
|
+
this.logSlow("SETEX", pkey, duration, { ttl: ttl });
|
|
159
|
+
return res;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const res = await this.client.set(pkey, value);
|
|
163
|
+
const duration = Date.now() - startTime;
|
|
164
|
+
this.logSlow("SET", pkey, duration);
|
|
165
|
+
return res;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
Logger.error("Redis setString 错误", error);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 尝试获取分布式锁(SET key token NX PX ttlMs)
|
|
174
|
+
*/
|
|
175
|
+
async tryAcquireLock(key, token, ttlMs) {
|
|
176
|
+
try {
|
|
177
|
+
if (!isNonEmptyString(key)) {
|
|
178
|
+
throw new Error("tryAcquireLock: key 必须是非空字符串", {
|
|
179
|
+
cause: null,
|
|
180
|
+
code: "validation"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (!isNonEmptyString(token)) {
|
|
184
|
+
throw new Error("tryAcquireLock: token 必须是非空字符串", {
|
|
185
|
+
cause: null,
|
|
186
|
+
code: "validation"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (!(isFiniteNumber(ttlMs) && ttlMs > 0)) {
|
|
190
|
+
throw new Error("tryAcquireLock: ttlMs 必须是正数", {
|
|
191
|
+
cause: null,
|
|
192
|
+
code: "validation"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pkey = `${this.prefix}${key}`;
|
|
197
|
+
const startTime = Date.now();
|
|
198
|
+
const ttlMsString = String(Math.floor(ttlMs));
|
|
199
|
+
const res = await this.client.set(pkey, token, "NX", "PX", ttlMsString);
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
201
|
+
this.logSlow("SET NX PX", pkey, duration, { ttlMs: ttlMs });
|
|
202
|
+
|
|
203
|
+
return res === "OK";
|
|
204
|
+
} catch (error) {
|
|
205
|
+
Logger.error("Redis tryAcquireLock 错误", error);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 释放分布式锁(原子校验 token 后删除)
|
|
212
|
+
*/
|
|
213
|
+
async releaseLock(key, token) {
|
|
214
|
+
try {
|
|
215
|
+
if (!isNonEmptyString(key)) {
|
|
216
|
+
throw new Error("releaseLock: key 必须是非空字符串", {
|
|
217
|
+
cause: null,
|
|
218
|
+
code: "validation"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!isNonEmptyString(token)) {
|
|
222
|
+
throw new Error("releaseLock: token 必须是非空字符串", {
|
|
223
|
+
cause: null,
|
|
224
|
+
code: "validation"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pkey = `${this.prefix}${key}`;
|
|
229
|
+
|
|
230
|
+
// 注意:Bun.RedisClient 当前未暴露 Lua/EVAL 等原子 compare-and-del API。
|
|
231
|
+
// 这里使用 get + del 的最小窗口实现;理论上存在极端竞态(TTL 恰好过期并被他人抢占)。
|
|
232
|
+
// 对启动期同步场景来说风险可接受;同时锁本身带 TTL,最坏情况下依赖 TTL 自动释放。
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
|
|
235
|
+
const current = await this.client.get(pkey);
|
|
236
|
+
if (current !== token) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const deleted = await this.client.del(pkey);
|
|
241
|
+
const duration = Date.now() - startTime;
|
|
242
|
+
this.logSlow("GET+DEL", pkey, duration);
|
|
243
|
+
|
|
244
|
+
return deleted > 0;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
Logger.error("Redis releaseLock 错误", error);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 获取字符串值
|
|
253
|
+
* @param key - 键名
|
|
254
|
+
*/
|
|
255
|
+
async getString(key) {
|
|
256
|
+
try {
|
|
257
|
+
const pkey = `${this.prefix}${key}`;
|
|
258
|
+
|
|
259
|
+
const startTime = Date.now();
|
|
260
|
+
const res = await this.client.get(pkey);
|
|
261
|
+
const duration = Date.now() - startTime;
|
|
262
|
+
this.logSlow("GET", pkey, duration);
|
|
263
|
+
return res;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
Logger.error("Redis getString 错误", error);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 检查键是否存在
|
|
272
|
+
* @param key - 键名
|
|
273
|
+
* @returns 是否存在(true/false)
|
|
274
|
+
*/
|
|
275
|
+
async exists(key) {
|
|
276
|
+
try {
|
|
277
|
+
const pkey = `${this.prefix}${key}`;
|
|
278
|
+
|
|
279
|
+
const startTime = Date.now();
|
|
280
|
+
const res = await this.client.exists(pkey);
|
|
281
|
+
const duration = Date.now() - startTime;
|
|
282
|
+
this.logSlow("EXISTS", pkey, duration);
|
|
283
|
+
return res;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
Logger.error("Redis exists 错误", error);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 原子自增
|
|
292
|
+
* @param key - 键名
|
|
293
|
+
* @returns 自增后的值
|
|
294
|
+
*/
|
|
295
|
+
async incr(key) {
|
|
296
|
+
try {
|
|
297
|
+
const pkey = `${this.prefix}${key}`;
|
|
298
|
+
|
|
299
|
+
const startTime = Date.now();
|
|
300
|
+
const res = await this.client.incr(pkey);
|
|
301
|
+
const duration = Date.now() - startTime;
|
|
302
|
+
this.logSlow("INCR", pkey, duration);
|
|
303
|
+
return res;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
Logger.error("Redis incr 错误", error);
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 原子自增并在首次自增时设置过期时间(常用于限流/计数)
|
|
312
|
+
* @param key - 键名
|
|
313
|
+
* @param seconds - 过期秒数
|
|
314
|
+
* @returns 自增后的值
|
|
315
|
+
*/
|
|
316
|
+
async incrWithExpire(key, seconds) {
|
|
317
|
+
try {
|
|
318
|
+
const pkey = `${this.prefix}${key}`;
|
|
319
|
+
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
const res = await this.client.incr(pkey);
|
|
322
|
+
if (res === 1) {
|
|
323
|
+
await this.client.expire(pkey, seconds);
|
|
324
|
+
}
|
|
325
|
+
const duration = Date.now() - startTime;
|
|
326
|
+
this.logSlow("INCR", pkey, duration, { expireSeconds: seconds });
|
|
327
|
+
return res;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
Logger.error("Redis incrWithExpire 错误", error);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 设置过期时间
|
|
336
|
+
* @param key - 键名
|
|
337
|
+
* @param seconds - 秒数
|
|
338
|
+
*/
|
|
339
|
+
async expire(key, seconds) {
|
|
340
|
+
try {
|
|
341
|
+
const pkey = `${this.prefix}${key}`;
|
|
342
|
+
|
|
343
|
+
const startTime = Date.now();
|
|
344
|
+
const res = await this.client.expire(pkey, seconds);
|
|
345
|
+
const duration = Date.now() - startTime;
|
|
346
|
+
this.logSlow("EXPIRE", pkey, duration, { seconds: seconds });
|
|
347
|
+
return res;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
Logger.error("Redis expire 错误", error);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 获取剩余过期时间
|
|
356
|
+
* @param key - 键名
|
|
357
|
+
*/
|
|
358
|
+
async ttl(key) {
|
|
359
|
+
try {
|
|
360
|
+
const pkey = `${this.prefix}${key}`;
|
|
361
|
+
|
|
362
|
+
const startTime = Date.now();
|
|
363
|
+
const res = await this.client.ttl(pkey);
|
|
364
|
+
const duration = Date.now() - startTime;
|
|
365
|
+
this.logSlow("TTL", pkey, duration);
|
|
366
|
+
return res;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
Logger.error("Redis ttl 错误", error);
|
|
369
|
+
return -1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 批量获取剩余过期时间(利用 Bun Redis 自动管道优化)
|
|
375
|
+
* @param keys - 键名数组
|
|
376
|
+
* @returns TTL 数组(-2 表示键不存在,-1 表示无过期时间)
|
|
377
|
+
*/
|
|
378
|
+
async ttlBatch(keys) {
|
|
379
|
+
if (keys.length === 0) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const results = await Promise.all(keys.map((key) => this.ttl(key)));
|
|
385
|
+
return results;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
Logger.error("Redis ttlBatch 错误", error);
|
|
388
|
+
return keys.map(() => -1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 向 Set 中添加一个或多个成员
|
|
394
|
+
* @param key - 键名
|
|
395
|
+
* @param members - 成员数组
|
|
396
|
+
* @returns 成功添加的成员数量
|
|
397
|
+
*/
|
|
398
|
+
async sadd(key, members) {
|
|
399
|
+
try {
|
|
400
|
+
if (members.length === 0) return 0;
|
|
401
|
+
|
|
402
|
+
const pkey = `${this.prefix}${key}`;
|
|
403
|
+
|
|
404
|
+
const startTime = Date.now();
|
|
405
|
+
const args = [pkey];
|
|
406
|
+
for (const member of members) {
|
|
407
|
+
args.push(member);
|
|
408
|
+
}
|
|
409
|
+
const res = await this.client.sadd.apply(this.client, args);
|
|
410
|
+
const duration = Date.now() - startTime;
|
|
411
|
+
this.logSlow("SADD", pkey, duration, {
|
|
412
|
+
membersCount: members.length
|
|
413
|
+
});
|
|
414
|
+
return res;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
Logger.error("Redis sadd 错误", error);
|
|
417
|
+
return 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 判断成员是否在 Set 中
|
|
423
|
+
* @param key - 键名
|
|
424
|
+
* @param member - 成员
|
|
425
|
+
* @returns 是否存在(true/false)
|
|
426
|
+
*/
|
|
427
|
+
async sismember(key, member) {
|
|
428
|
+
try {
|
|
429
|
+
const pkey = `${this.prefix}${key}`;
|
|
430
|
+
|
|
431
|
+
const startTime = Date.now();
|
|
432
|
+
const res = await this.client.sismember(pkey, member);
|
|
433
|
+
const duration = Date.now() - startTime;
|
|
434
|
+
this.logSlow("SISMEMBER", pkey, duration);
|
|
435
|
+
return res;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
Logger.error("Redis sismember 错误", error);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 获取 Set 的成员数量
|
|
444
|
+
* @param key - 键名
|
|
445
|
+
* @returns 成员数量
|
|
446
|
+
*/
|
|
447
|
+
async scard(key) {
|
|
448
|
+
try {
|
|
449
|
+
const pkey = `${this.prefix}${key}`;
|
|
450
|
+
|
|
451
|
+
const startTime = Date.now();
|
|
452
|
+
const res = await this.client.scard(pkey);
|
|
453
|
+
const duration = Date.now() - startTime;
|
|
454
|
+
this.logSlow("SCARD", pkey, duration);
|
|
455
|
+
return res;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
Logger.error("Redis scard 错误", error);
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 获取 Set 的所有成员
|
|
464
|
+
* @param key - 键名
|
|
465
|
+
* @returns 成员数组
|
|
466
|
+
*/
|
|
467
|
+
async smembers(key) {
|
|
468
|
+
try {
|
|
469
|
+
const pkey = `${this.prefix}${key}`;
|
|
470
|
+
|
|
471
|
+
const startTime = Date.now();
|
|
472
|
+
const res = await this.client.smembers(pkey);
|
|
473
|
+
const duration = Date.now() - startTime;
|
|
474
|
+
this.logSlow("SMEMBERS", pkey, duration);
|
|
475
|
+
return res;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
Logger.error("Redis smembers 错误", error);
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 批量向多个 Set 添加成员(利用 Bun Redis 自动管道优化)
|
|
484
|
+
* @param items - [{ key, members }] 数组
|
|
485
|
+
* @returns 成功添加的总成员数量
|
|
486
|
+
*/
|
|
487
|
+
async saddBatch(items) {
|
|
488
|
+
if (items.length === 0) {
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const results = await Promise.all(items.map((item) => this.sadd(item.key, item.members)));
|
|
494
|
+
return results.reduce((sum, count) => sum + count, 0);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
Logger.error("Redis saddBatch 错误", error);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 批量检查成员是否在 Set 中(利用 Bun Redis 自动管道优化)
|
|
503
|
+
* @param items - [{ key, member }] 数组
|
|
504
|
+
* @returns 布尔数组(true 表示存在,false 表示不存在)
|
|
505
|
+
*/
|
|
506
|
+
async sismemberBatch(items) {
|
|
507
|
+
if (items.length === 0) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
return await Promise.all(items.map((item) => this.sismember(item.key, item.member)));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
Logger.error("Redis sismemberBatch 错误", error);
|
|
515
|
+
return items.map(() => false);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 删除键
|
|
521
|
+
* @param key - 键名
|
|
522
|
+
* @returns 删除的键数量
|
|
523
|
+
*/
|
|
524
|
+
async del(key) {
|
|
525
|
+
try {
|
|
526
|
+
const pkey = `${this.prefix}${key}`;
|
|
527
|
+
|
|
528
|
+
const startTime = Date.now();
|
|
529
|
+
const res = await this.client.del(pkey);
|
|
530
|
+
const duration = Date.now() - startTime;
|
|
531
|
+
this.logSlow("DEL", pkey, duration);
|
|
532
|
+
return res;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
Logger.error("Redis del 错误", error);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* 批量删除键(利用 Bun Redis 自动管道优化)
|
|
541
|
+
* @param keys - 键名数组
|
|
542
|
+
* @returns 成功删除的键数量
|
|
543
|
+
*/
|
|
544
|
+
async delBatch(keys) {
|
|
545
|
+
if (keys.length === 0) {
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const results = await Promise.all(
|
|
551
|
+
keys.map((key) => {
|
|
552
|
+
const pkey = `${this.prefix}${key}`;
|
|
553
|
+
return this.client.del(pkey);
|
|
554
|
+
})
|
|
555
|
+
);
|
|
556
|
+
return results.reduce((sum, count) => sum + count, 0);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
Logger.error("Redis delBatch 错误", error);
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 批量设置对象(利用 Bun Redis 自动管道优化)
|
|
565
|
+
* @param items - 键值对数组 [{ key, value, ttl? }]
|
|
566
|
+
* @returns 成功设置的数量
|
|
567
|
+
*/
|
|
568
|
+
async setBatch(items) {
|
|
569
|
+
if (items.length === 0) {
|
|
570
|
+
return 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const results = await Promise.all(items.map((item) => this.setObject(item.key, item.value, item.ttl ?? null)));
|
|
575
|
+
return results.filter((r) => r !== null).length;
|
|
576
|
+
} catch (error) {
|
|
577
|
+
Logger.error("Redis setBatch 错误", error);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 批量获取对象(利用 Bun Redis 自动管道优化)
|
|
584
|
+
* @param keys - 键名数组
|
|
585
|
+
* @returns 对象数组(不存在的键返回 null)
|
|
586
|
+
*/
|
|
587
|
+
async getBatch(keys) {
|
|
588
|
+
if (keys.length === 0) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const results = await Promise.all(keys.map((key) => this.getObject(key)));
|
|
594
|
+
return results;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
Logger.error("Redis getBatch 错误", error);
|
|
597
|
+
return keys.map(() => null);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 批量检查键是否存在(利用 Bun Redis 自动管道优化)
|
|
603
|
+
* @param keys - 键名数组
|
|
604
|
+
* @returns 布尔数组(true 表示存在,false 表示不存在)
|
|
605
|
+
*/
|
|
606
|
+
async existsBatch(keys) {
|
|
607
|
+
if (keys.length === 0) {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
return await Promise.all(keys.map((key) => this.exists(key)));
|
|
613
|
+
} catch (error) {
|
|
614
|
+
Logger.error("Redis existsBatch 错误", error);
|
|
615
|
+
return keys.map(() => false);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 批量设置过期时间(利用 Bun Redis 自动管道优化)
|
|
621
|
+
* @param items - 键名和过期时间数组 [{ key, seconds }]
|
|
622
|
+
* @returns 成功设置的数量
|
|
623
|
+
*/
|
|
624
|
+
async expireBatch(items) {
|
|
625
|
+
if (items.length === 0) {
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const results = await Promise.all(items.map((item) => this.expire(item.key, item.seconds)));
|
|
631
|
+
return results.filter((r) => r > 0).length;
|
|
632
|
+
} catch (error) {
|
|
633
|
+
Logger.error("Redis expireBatch 错误", error);
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 获取 Redis INFO 文本
|
|
640
|
+
* @param section - 可选 section(如 "server")
|
|
641
|
+
*/
|
|
642
|
+
async info(section) {
|
|
643
|
+
try {
|
|
644
|
+
const client = this.client;
|
|
645
|
+
if (typeof client.info !== "function") {
|
|
646
|
+
throw new Error("Redis info 不支持", {
|
|
647
|
+
cause: null,
|
|
648
|
+
code: "runtime"
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const startTime = Date.now();
|
|
653
|
+
const res = await client.info(section);
|
|
654
|
+
const duration = Date.now() - startTime;
|
|
655
|
+
this.logSlow("INFO", "(no-key)", duration, { section: section ?? "" });
|
|
656
|
+
return res;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
Logger.error("Redis info 错误", error);
|
|
659
|
+
return "";
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* 测试 Redis 连接
|
|
665
|
+
* @returns ping 响应结果
|
|
666
|
+
*/
|
|
667
|
+
async ping() {
|
|
668
|
+
try {
|
|
669
|
+
const startTime = Date.now();
|
|
670
|
+
const res = await this.client.ping();
|
|
671
|
+
const duration = Date.now() - startTime;
|
|
672
|
+
this.logSlow("PING", "(no-key)", duration);
|
|
673
|
+
return res;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
Logger.error("Redis ping 错误", error);
|
|
676
|
+
throw new Error("Redis ping 错误", {
|
|
677
|
+
cause: error,
|
|
678
|
+
code: "runtime",
|
|
679
|
+
subsystem: "redis",
|
|
680
|
+
operation: "ping"
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|