chanjs 2.7.2 → 2.7.3
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/App.js +232 -16
- package/base/Aop.js +20 -3
- package/base/Container.js +80 -3
- package/base/Controller.js +38 -9
- package/base/Database.js +50 -0
- package/base/Event.js +12 -0
- package/base/{Service.js → Repository.js} +644 -539
- package/common/api.js +18 -8
- package/common/code.js +25 -15
- package/common/email.js +98 -17
- package/common/index.js +1 -1
- package/config/code.js +138 -82
- package/global/index.js +1 -1
- package/helper/index.js +43 -41
- package/index.js +19 -6
- package/loader/index.js +6 -0
- package/{helper → loader}/loader.js +41 -27
- package/middleware/compress.js +185 -0
- package/middleware/cors.js +36 -24
- package/middleware/header.js +5 -10
- package/middleware/index.js +1 -0
- package/middleware/log.js +27 -3
- package/middleware/setBody.js +9 -1
- package/middleware/static.js +2 -1
- package/middleware/template.js +139 -4
- package/middleware/waf.js +136 -76
- package/package.json +2 -3
- package/realtime/index.js +7 -0
- package/realtime/sse.js +424 -0
- package/realtime/websocket.js +540 -0
- package/response/index.js +12 -0
- package/response/response.js +258 -0
- package/schedule/index.js +6 -0
- package/schedule/schedule.js +491 -0
- package/{helper → security}/checker.js +23 -8
- package/security/index.js +14 -0
- package/{helper → security}/jwt.js +175 -107
- package/security/keywords.js +179 -0
- package/security/rate-limit.js +105 -0
- package/security/sign.js +210 -0
- package/security/xss-filter.js +63 -0
- package/storage/cache.js +258 -0
- package/storage/index.js +9 -0
- package/storage/redis.js +258 -0
- package/storage/store.js +266 -0
- package/{helper → utils}/file.js +106 -15
- package/{helper → utils}/filter.js +2 -1
- package/{helper → utils}/html.js +19 -1
- package/utils/index.js +34 -0
- package/{helper → utils}/ip.js +25 -16
- package/utils/request.js +172 -0
- package/{helper → utils}/time.js +1 -1
- package/utils/tree.js +121 -0
- package/common/category.js +0 -22
- package/common/sms.js +0 -104
- package/extend/art-template.js +0 -129
- package/extend/index.js +0 -6
- package/global/global.js +0 -63
- package/helper/cache.js +0 -187
- package/helper/keywords.js +0 -132
- package/helper/rate-limit.js +0 -116
- package/helper/request.js +0 -47
- package/helper/response.js +0 -180
- package/helper/sign.js +0 -96
- package/helper/tree.js +0 -77
- package/helper/xss-filter.js +0 -42
- /package/{helper → utils}/data-parse.js +0 -0
package/storage/redis.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 Redis 后端(ioredis 封装)
|
|
3
|
+
*
|
|
4
|
+
* ============================================================
|
|
5
|
+
* 使用方法
|
|
6
|
+
* ============================================================
|
|
7
|
+
*
|
|
8
|
+
* 本模块为纯 Redis 后端,仅负责与 Redis 通信,不包含内存降级逻辑。
|
|
9
|
+
* 适合明确知道自己需要 Redis 的场景独立使用。
|
|
10
|
+
*
|
|
11
|
+
* 1. 独立使用
|
|
12
|
+
* import RedisBackend from 'chanjs/helper/redis.js';
|
|
13
|
+
* const redis = new RedisBackend({ host: '127.0.0.1', port: 6379 });
|
|
14
|
+
* await redis.set('key', 'value', 60000);
|
|
15
|
+
* const val = await redis.get('key');
|
|
16
|
+
*
|
|
17
|
+
* 2. 需要自动适配(Redis + 内存降级)
|
|
18
|
+
* 请使用 helper/store.js(适配层)。
|
|
19
|
+
*
|
|
20
|
+
* 3. 只需要内存缓存(同步 API)
|
|
21
|
+
* 请使用 helper/cache.js。
|
|
22
|
+
*
|
|
23
|
+
* 4. API(全部返回 Promise)
|
|
24
|
+
* - get(key) 读取
|
|
25
|
+
* - set(key, value, ttlMs=60000) 写入(毫秒级 TTL)
|
|
26
|
+
* - del(key) 删除
|
|
27
|
+
* - incr(key) 自增(新建 key 自动附加默认 TTL)
|
|
28
|
+
* - incrAndExpire(key, ttlMs) 自增 + 首次设置过期
|
|
29
|
+
* - exists(key) 存在性检查
|
|
30
|
+
* - expire(key, ttlMs) 刷新过期时间
|
|
31
|
+
* - close() 关闭连接
|
|
32
|
+
*
|
|
33
|
+
* 5. 常量
|
|
34
|
+
* - DEFAULT_INCR_TTL = 60_000ms incr 新建 key 的默认 TTL
|
|
35
|
+
*
|
|
36
|
+
* ============================================================
|
|
37
|
+
*
|
|
38
|
+
* 设计要点:
|
|
39
|
+
* 1. 仅负责 Redis 通信,不处理降级(降级逻辑在 store.js 适配层)
|
|
40
|
+
* 2. 客户端懒加载,第一次调用时建立连接
|
|
41
|
+
* 3. incr/incrAndExpire 用 SET NX PX 保证原子性(兼容 Redis 2.6.12+)
|
|
42
|
+
* 4. 连接事件监听,便于排查网络问题
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* incr 默认 TTL(毫秒)
|
|
47
|
+
* 用于 incr 新建 key 时自动附加过期,避免永久占用内存
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_INCR_TTL = 60 * 1000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 纯 Redis 后端
|
|
53
|
+
* @class RedisBackend
|
|
54
|
+
*/
|
|
55
|
+
class RedisBackend {
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} config - Redis 连接配置
|
|
58
|
+
* @param {string} [config.host='127.0.0.1'] - Redis 主机
|
|
59
|
+
* @param {number} [config.port=6379] - Redis 端口
|
|
60
|
+
* @param {string} [config.password] - Redis 密码
|
|
61
|
+
* @param {number} [config.db=0] - Redis 数据库
|
|
62
|
+
* @param {number} [config.connectTimeout=5000] - 连接超时(毫秒)
|
|
63
|
+
* @param {number} [config.commandTimeout=1000] - 命令超时(毫秒)
|
|
64
|
+
*/
|
|
65
|
+
constructor(config = {}) {
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.client = null;
|
|
68
|
+
this._initPromise = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 懒加载 Redis 客户端
|
|
73
|
+
* @private
|
|
74
|
+
* @returns {Promise<Object>} ioredis 客户端实例
|
|
75
|
+
* @throws {Error} 连接失败时抛出
|
|
76
|
+
*/
|
|
77
|
+
async _ensureClient() {
|
|
78
|
+
if (this.client) return this.client;
|
|
79
|
+
if (this._initPromise) return this._initPromise;
|
|
80
|
+
|
|
81
|
+
this._initPromise = (async () => {
|
|
82
|
+
const { default: Redis } = await import('ioredis');
|
|
83
|
+
this.client = new Redis({
|
|
84
|
+
host: this.config.host || '127.0.0.1',
|
|
85
|
+
port: this.config.port || 6379,
|
|
86
|
+
password: this.config.password || undefined,
|
|
87
|
+
db: this.config.db || 0,
|
|
88
|
+
connectTimeout: this.config.connectTimeout || 5000,
|
|
89
|
+
commandTimeout: this.config.commandTimeout || 1000,
|
|
90
|
+
retryStrategy: this.config.retryStrategy || ((times) => {
|
|
91
|
+
if (times > 3) return null;
|
|
92
|
+
return Math.min(times * 200, 1000);
|
|
93
|
+
}),
|
|
94
|
+
lazyConnect: true,
|
|
95
|
+
maxRetriesPerRequest: 1,
|
|
96
|
+
enableOfflineQueue: false,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.client.on('error', (err) => {
|
|
100
|
+
console.error('[Redis] 连接错误:', err.message);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.client.on('connect', () => {
|
|
104
|
+
console.log(`[Redis] 已连接 ${this.config.host}:${this.config.port}`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.client.on('reconnecting', () => {
|
|
108
|
+
console.log('[Redis] 正在重连...');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await this.client.connect();
|
|
112
|
+
return this.client;
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return await this._initPromise;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.client = null;
|
|
119
|
+
this._initPromise = null;
|
|
120
|
+
throw new Error(`Redis 连接失败: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 读取
|
|
126
|
+
* @param {string} key - 键
|
|
127
|
+
* @returns {Promise<*|null>} 值(自动 JSON 解析),不存在返回 null
|
|
128
|
+
*/
|
|
129
|
+
async get(key) {
|
|
130
|
+
const client = await this._ensureClient();
|
|
131
|
+
const value = await client.get(key);
|
|
132
|
+
if (value === null) return null;
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(value);
|
|
135
|
+
} catch {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 写入
|
|
142
|
+
* @param {string} key - 键
|
|
143
|
+
* @param {*} value - 值(自动 JSON 序列化)
|
|
144
|
+
* @param {number} [ttlMs=60000] - TTL 毫秒,<=0 表示永久
|
|
145
|
+
* @returns {Promise<boolean>} 始终返回 true
|
|
146
|
+
*/
|
|
147
|
+
async set(key, value, ttlMs = 60000) {
|
|
148
|
+
const client = await this._ensureClient();
|
|
149
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
150
|
+
if (ttlMs > 0) {
|
|
151
|
+
// PX 毫秒级过期
|
|
152
|
+
await client.set(key, serialized, 'PX', ttlMs);
|
|
153
|
+
} else {
|
|
154
|
+
await client.set(key, serialized);
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 删除
|
|
161
|
+
* @param {string} key - 键
|
|
162
|
+
* @returns {Promise<boolean>} 是否删除了至少 1 条
|
|
163
|
+
*/
|
|
164
|
+
async del(key) {
|
|
165
|
+
const client = await this._ensureClient();
|
|
166
|
+
const result = await client.del(key);
|
|
167
|
+
return result > 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 自增,新建 key 自动附加默认 TTL
|
|
172
|
+
*
|
|
173
|
+
* 实现方式:SET key 1 PX ttl NX + 分支判断
|
|
174
|
+
* - NX 选项:只有 key 不存在时才设置(原子操作)
|
|
175
|
+
* - 设置成功返回 "OK" → 新建 key,返回 1
|
|
176
|
+
* - 设置失败返回 null → key 已存在,走 INCR 递增(不刷新 TTL)
|
|
177
|
+
*
|
|
178
|
+
* 兼容性:SET ... NX PX 是 Redis 2.6.12+ 标准用法,覆盖所有生产环境
|
|
179
|
+
*
|
|
180
|
+
* @param {string} key - 键
|
|
181
|
+
* @returns {Promise<number>} 自增后的值
|
|
182
|
+
*/
|
|
183
|
+
async incr(key) {
|
|
184
|
+
const client = await this._ensureClient();
|
|
185
|
+
// 尝试新建 key 并设置默认 TTL(原子操作)
|
|
186
|
+
const setResult = await client.set(key, 1, 'PX', DEFAULT_INCR_TTL, 'NX');
|
|
187
|
+
if (setResult === 'OK') {
|
|
188
|
+
return 1; // 新建成功
|
|
189
|
+
}
|
|
190
|
+
// key 已存在,INCR 递增(不刷新 TTL)
|
|
191
|
+
return client.incr(key);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 自增并首次设置过期时间
|
|
196
|
+
* 用于 Rate Limit 场景:第一次访问设置 TTL,后续访问只递增不续期
|
|
197
|
+
*
|
|
198
|
+
* @param {string} key - 键
|
|
199
|
+
* @param {number} ttlMs - TTL 毫秒
|
|
200
|
+
* @returns {Promise<number>} 自增后的值
|
|
201
|
+
*/
|
|
202
|
+
async incrAndExpire(key, ttlMs) {
|
|
203
|
+
const client = await this._ensureClient();
|
|
204
|
+
// 尝试新建 key 并设置指定 TTL(原子操作)
|
|
205
|
+
const setResult = await client.set(key, 1, 'PX', ttlMs, 'NX');
|
|
206
|
+
if (setResult === 'OK') {
|
|
207
|
+
return 1; // 新建成功,TTL 已设置
|
|
208
|
+
}
|
|
209
|
+
// key 已存在,INCR 递增(不刷新 TTL)
|
|
210
|
+
return client.incr(key);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 存在性检查
|
|
215
|
+
* @param {string} key - 键
|
|
216
|
+
* @returns {Promise<boolean>}
|
|
217
|
+
*/
|
|
218
|
+
async exists(key) {
|
|
219
|
+
const client = await this._ensureClient();
|
|
220
|
+
const result = await client.exists(key);
|
|
221
|
+
return result === 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 刷新过期时间
|
|
226
|
+
* @param {string} key - 键
|
|
227
|
+
* @param {number} ttlMs - TTL 毫秒
|
|
228
|
+
* @returns {Promise<boolean>} 是否设置成功
|
|
229
|
+
*/
|
|
230
|
+
async expire(key, ttlMs) {
|
|
231
|
+
const client = await this._ensureClient();
|
|
232
|
+
return client.pexpire(key, ttlMs);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 关闭连接
|
|
237
|
+
* @returns {Promise<void>}
|
|
238
|
+
*/
|
|
239
|
+
async close() {
|
|
240
|
+
if (this.client) {
|
|
241
|
+
await this.client.quit();
|
|
242
|
+
this.client = null;
|
|
243
|
+
this._initPromise = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 获取客户端状态(诊断用)
|
|
249
|
+
* @returns {string} 'idle' | 'ready' | 'connecting' | 'closed'
|
|
250
|
+
*/
|
|
251
|
+
getStatus() {
|
|
252
|
+
if (!this.client) return 'idle';
|
|
253
|
+
return this.client.status || 'unknown';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { DEFAULT_INCR_TTL };
|
|
258
|
+
export default RedisBackend;
|
package/storage/store.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存储适配层
|
|
3
|
+
* 自动选择后端:内存(cache.js)或 Redis(redis.js),并支持运行时降级
|
|
4
|
+
*
|
|
5
|
+
* ============================================================
|
|
6
|
+
* 使用方法
|
|
7
|
+
* ============================================================
|
|
8
|
+
*
|
|
9
|
+
* 1. 内存模式(默认,零配置开箱即用)
|
|
10
|
+
* 适用:单机部署、中小流量
|
|
11
|
+
* 上限:10 万条约 20MB,超出 LRU 淘汰
|
|
12
|
+
* 底层:基于 helper/cache.js(同步 API 被包装为异步)
|
|
13
|
+
*
|
|
14
|
+
* 2. Redis 模式(生产推荐,多机共享)
|
|
15
|
+
* 启用方式:设置环境变量 REDIS_ENABLED=true
|
|
16
|
+
* 默认连接:chancms-redis:6379
|
|
17
|
+
* 可通过环境变量覆盖:
|
|
18
|
+
* REDIS_ENABLED=true
|
|
19
|
+
* REDIS_HOST=127.0.0.1
|
|
20
|
+
* REDIS_PORT=6379
|
|
21
|
+
* REDIS_PASSWORD=xxx
|
|
22
|
+
* REDIS_DB=0
|
|
23
|
+
* 底层:基于 helper/redis.js(纯 Redis 后端)
|
|
24
|
+
*
|
|
25
|
+
* 3. 自动降级(容灾)
|
|
26
|
+
* - 初始化时 Redis 连接失败 → 自动降级到内存模式
|
|
27
|
+
* - 运行中 Redis 断连 → 单次操作自动 fallback 到内存执行,不中断业务
|
|
28
|
+
* - 后续操作仍会尝试 Redis(自动恢复)
|
|
29
|
+
*
|
|
30
|
+
* 4. API(全部返回 Promise)
|
|
31
|
+
* - init(config) 初始化(应用启动时调用一次)
|
|
32
|
+
* - get(key) 读取
|
|
33
|
+
* - set(key, value, ttlMs=60000) 写入(毫秒级 TTL)
|
|
34
|
+
* - del(key) 删除
|
|
35
|
+
* - incr(key) 自增(新建 key 自动附加默认 TTL)
|
|
36
|
+
* - incrAndExpire(key, ttlMs) 自增 + 首次设置过期
|
|
37
|
+
* - exists(key) 存在性检查
|
|
38
|
+
* - expire(key, ttlMs) 刷新过期时间
|
|
39
|
+
* - getInfo() 诊断信息(当前后端/连接状态/内存大小)
|
|
40
|
+
* - close() 关闭连接
|
|
41
|
+
*
|
|
42
|
+
* ============================================================
|
|
43
|
+
*
|
|
44
|
+
* 架构关系:
|
|
45
|
+
*
|
|
46
|
+
* cache.js (同步内存) ◄──── 独立可用,含 incr/incrAndExpire/expire
|
|
47
|
+
* redis.js (异步 Redis) ◄──── 独立可用,纯 Redis 后端
|
|
48
|
+
* store.js (适配层) ◄──── 组合 cache + redis,自动选后端 + 降级
|
|
49
|
+
*
|
|
50
|
+
* 适用场景:
|
|
51
|
+
* - 只想要内存缓存(同步):用 cache.js
|
|
52
|
+
* - 只想要 Redis:用 redis.js
|
|
53
|
+
* - 想要自动适配(Redis + 降级):用 store.js(本模块)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { cache, DEFAULT_INCR_TTL } from "./cache.js";
|
|
57
|
+
import RedisBackend from "./redis.js";
|
|
58
|
+
|
|
59
|
+
export { DEFAULT_INCR_TTL };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 内存后端包装器
|
|
63
|
+
* 把同步的 cache.js API 包装成异步,对齐 RedisBackend 的接口
|
|
64
|
+
* 直接复用 cache.js 的 incr/incrAndExpire/expire,保证 TTL 行为与 Redis 一致
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
class MemoryAdapter {
|
|
68
|
+
/**
|
|
69
|
+
* @param {Object} [options] - 配置
|
|
70
|
+
* @param {Object} [options.cache] - 自定义 Cache 实例(默认用全局单例)
|
|
71
|
+
*/
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.cache = options.cache || cache;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async get(key) {
|
|
77
|
+
return this.cache.get(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async set(key, value, ttlMs = 60000) {
|
|
81
|
+
this.cache.set(key, value, ttlMs);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async del(key) {
|
|
86
|
+
return this.cache.del(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async incr(key) {
|
|
90
|
+
// 直接调用 cache.incr,保证 TTL 不被刷新(与 Redis INCR 行为对齐)
|
|
91
|
+
return this.cache.incr(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async incrAndExpire(key, ttlMs) {
|
|
95
|
+
return this.cache.incrAndExpire(key, ttlMs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async exists(key) {
|
|
99
|
+
return this.cache.has(key);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async expire(key, ttlMs) {
|
|
103
|
+
return this.cache.expire(key, ttlMs);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
size() {
|
|
107
|
+
return this.cache.size();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
clear() {
|
|
111
|
+
this.cache.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 存储适配器
|
|
117
|
+
* @class Store
|
|
118
|
+
* 自动选择后端:Redis(启用且可用时)或 内存(默认/降级时)
|
|
119
|
+
* 运行时 Redis 异常自动 fallback 到内存,不中断业务
|
|
120
|
+
*/
|
|
121
|
+
class Store {
|
|
122
|
+
constructor() {
|
|
123
|
+
// 内存后端(始终存在,作为降级兜底)
|
|
124
|
+
this.memory = new MemoryAdapter();
|
|
125
|
+
// Redis 后端(按需创建)
|
|
126
|
+
this.redis = null;
|
|
127
|
+
// 当前是否使用 Redis
|
|
128
|
+
this.useRedis = false;
|
|
129
|
+
// 是否已初始化
|
|
130
|
+
this._initialized = false;
|
|
131
|
+
// 降级日志冷却时间,避免高频刷屏
|
|
132
|
+
this._fallbackCooldown = 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 初始化适配器
|
|
137
|
+
* 必须在应用启动时调用
|
|
138
|
+
* @param {Object} config - 应用配置
|
|
139
|
+
* @param {boolean} [config.REDIS_ENABLED=false] - 是否启用 Redis
|
|
140
|
+
* @param {Object} [config.REDIS] - Redis 连接配置
|
|
141
|
+
*/
|
|
142
|
+
async init(config = {}) {
|
|
143
|
+
if (this._initialized) return;
|
|
144
|
+
|
|
145
|
+
if (config.REDIS_ENABLED) {
|
|
146
|
+
this.redis = new RedisBackend(config.REDIS || {});
|
|
147
|
+
// 预热连接,失败则降级到内存模式
|
|
148
|
+
try {
|
|
149
|
+
// 触发连接
|
|
150
|
+
await this.redis._ensureClient();
|
|
151
|
+
this.useRedis = true;
|
|
152
|
+
console.log('[Store] 已启用 Redis 后端');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn(`[Store] Redis 不可用,降级到内存模式: ${err.message}`);
|
|
155
|
+
this.useRedis = false;
|
|
156
|
+
this.redis = null;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.log('[Store] 使用内存模式(Map+TTL,上限 10万条/约20MB)');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this._initialized = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 运行时异常兜底:Redis 操作抛错时临时切内存执行
|
|
167
|
+
* 不中断业务,打印降级日志(带冷却避免刷屏)
|
|
168
|
+
* @private
|
|
169
|
+
* @param {string} method - 方法名
|
|
170
|
+
* @param {Array} args - 参数
|
|
171
|
+
* @param {*} fallbackValue - 内存 fallback 失败时的兜底返回值
|
|
172
|
+
* @returns {*} 操作结果
|
|
173
|
+
*/
|
|
174
|
+
async _safeCall(method, args, fallbackValue) {
|
|
175
|
+
// 内存模式直接执行
|
|
176
|
+
if (!this.useRedis) {
|
|
177
|
+
return this.memory[method](...args);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return await this.redis[method](...args);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Redis 异常,降级到内存执行
|
|
184
|
+
this._logFallback(method, e);
|
|
185
|
+
try {
|
|
186
|
+
return await this.memory[method](...args);
|
|
187
|
+
} catch (memErr) {
|
|
188
|
+
// 内存也失败(理论上极少发生),返回兜底值
|
|
189
|
+
console.error(`[Store] 内存 fallback 也失败: ${memErr.message}`);
|
|
190
|
+
return fallbackValue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 打印降级日志(带冷却,1 秒内最多一次)
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
_logFallback(method, err) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
if (now - this._fallbackCooldown > 1000) {
|
|
202
|
+
this._fallbackCooldown = now;
|
|
203
|
+
console.warn(`[Store] Redis ${method} 失败,临时降级内存执行: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async get(key) {
|
|
208
|
+
return this._safeCall('get', [key], null);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async set(key, value, ttlMs = 60000) {
|
|
212
|
+
return this._safeCall('set', [key, value, ttlMs], false);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async del(key) {
|
|
216
|
+
return this._safeCall('del', [key], false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async incr(key) {
|
|
220
|
+
return this._safeCall('incr', [key], 0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async incrAndExpire(key, ttlMs) {
|
|
224
|
+
return this._safeCall('incrAndExpire', [key, ttlMs], 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async exists(key) {
|
|
228
|
+
return this._safeCall('exists', [key], false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async expire(key, ttlMs) {
|
|
232
|
+
return this._safeCall('expire', [key, ttlMs], false);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 获取当前后端信息(用于诊断)
|
|
237
|
+
* @returns {Object} { mode, memorySize, redisConnected }
|
|
238
|
+
*/
|
|
239
|
+
getInfo() {
|
|
240
|
+
return {
|
|
241
|
+
mode: this.useRedis ? 'redis' : 'memory',
|
|
242
|
+
memorySize: this.memory.size(),
|
|
243
|
+
redisConnected: this.useRedis && this.redis?.getStatus() === 'ready',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 关闭连接
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async close() {
|
|
252
|
+
if (this.redis) {
|
|
253
|
+
try {
|
|
254
|
+
await this.redis.close();
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.warn(`[Store] 关闭 Redis 连接失败: ${e.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.memory.clear();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 全局单例
|
|
264
|
+
export const store = new Store();
|
|
265
|
+
|
|
266
|
+
export default Store;
|