chanjs 2.7.4 → 2.7.6
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/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/BaseComponent.js +27 -0
- package/core/Container.js +68 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +323 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +101 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +251 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +61 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +15 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +176 -197
- package/package.json +9 -2
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +84 -139
- package/security/keywords.js +33 -137
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +58 -198
- package/storage/index.js +3 -6
- package/storage/redis.js +124 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +20 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +95 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/storage/store.js
CHANGED
|
@@ -1,266 +1,241 @@
|
|
|
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
1
|
import { cache, DEFAULT_INCR_TTL } from "./cache.js";
|
|
57
2
|
import RedisBackend from "./redis.js";
|
|
3
|
+
import logger from "../utils/logger.js";
|
|
58
4
|
|
|
59
5
|
export { DEFAULT_INCR_TTL };
|
|
60
6
|
|
|
7
|
+
// Redis 故障日志防抖间隔:避免高频失败时刷屏
|
|
8
|
+
const FALLBACK_LOG_COOLDOWN = 1000;
|
|
9
|
+
// 熔断触发阈值:Redis 连续失败次数达到此值进入熔断
|
|
10
|
+
const CIRCUIT_THRESHOLD = 5;
|
|
11
|
+
// 熔断冷却时长:熔断开启后此段时间内所有请求直接走内存
|
|
12
|
+
const CIRCUIT_COOLDOWN = 30000;
|
|
13
|
+
|
|
61
14
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @private
|
|
15
|
+
* 内存缓存适配器
|
|
16
|
+
* 内部 LRU Cache 是同步方法,对外统一包装为 Promise/async 风格,
|
|
17
|
+
* 保持与 RedisAdapter 接口形状一致,让 Store 内部用同一套 _safeExec 调度。
|
|
66
18
|
*/
|
|
67
19
|
class MemoryAdapter {
|
|
68
|
-
|
|
69
|
-
* @param {Object} [options] - 配置
|
|
70
|
-
* @param {Object} [options.cache] - 自定义 Cache 实例(默认用全局单例)
|
|
71
|
-
*/
|
|
72
|
-
constructor(options = {}) {
|
|
73
|
-
this.cache = options.cache || cache;
|
|
74
|
-
}
|
|
20
|
+
constructor(instance = cache) { this._cache = instance; }
|
|
75
21
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
22
|
+
/** 读取缓存值,无值返回 null */
|
|
23
|
+
get(key) { return this._cache.get(key); }
|
|
79
24
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
25
|
+
/**
|
|
26
|
+
* 写入缓存
|
|
27
|
+
* @param {string} key 键
|
|
28
|
+
* @param {*} val 值
|
|
29
|
+
* @param {number} [ttlMs=60000] 过期时间(毫秒)
|
|
30
|
+
* @returns {boolean} 写入成功
|
|
31
|
+
*/
|
|
32
|
+
set(key, val, ttlMs = 60000) { this._cache.set(key, val, ttlMs); return true; }
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
34
|
+
/** 删除指定 key */
|
|
35
|
+
del(key) { return this._cache.del(key); }
|
|
88
36
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return this.cache.incr(key);
|
|
92
|
-
}
|
|
37
|
+
/** 原子自增;新 key 起始值 1 */
|
|
38
|
+
incr(key) { return this._cache.incr(key); }
|
|
93
39
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
40
|
+
/**
|
|
41
|
+
* 原子自增并自动设置 TTL(限流场景专用)
|
|
42
|
+
* @param {string} key 键
|
|
43
|
+
* @param {number} ttlMs 过期时间(毫秒)
|
|
44
|
+
*/
|
|
45
|
+
incrAndExpire(key, ttlMs) { return this._cache.incrAndExpire(key, ttlMs); }
|
|
97
46
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
47
|
+
/** 判断 key 是否存在 */
|
|
48
|
+
exists(key) { return this._cache.has(key); }
|
|
101
49
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
50
|
+
/**
|
|
51
|
+
* 给已存在的 key 设置过期时间
|
|
52
|
+
* @returns {boolean} key 不存在时返回 false
|
|
53
|
+
*/
|
|
54
|
+
expire(key, ttlMs) { return this._cache.expire(key, ttlMs); }
|
|
105
55
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
56
|
+
/** 获取当前缓存条目数 */
|
|
57
|
+
size() { return this._cache.size(); }
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
59
|
+
/** 清空所有缓存 */
|
|
60
|
+
clear() { this._cache.clear(); }
|
|
113
61
|
}
|
|
114
62
|
|
|
115
63
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
64
|
+
* 存储统一适配层(项目全局唯一入口)
|
|
65
|
+
*
|
|
66
|
+
* 设计目标:
|
|
67
|
+
* 1. 业务侧无需关心底层是 Redis 还是内存,统一调用 store.xxx()
|
|
68
|
+
* 2. Redis 故障时自动降级到内存,熔断保护避免雪崩
|
|
69
|
+
* 3. 接口形状与 Redis 客户端保持一致,零业务侵入
|
|
70
|
+
*
|
|
71
|
+
* 降级策略:
|
|
72
|
+
* - Redis 不可用 → 启动时直接走内存(useRedis=false)
|
|
73
|
+
* - Redis 运行时挂掉 → 连续失败 5 次触发熔断,30s 冷却期内全部走内存
|
|
74
|
+
* - 冷却期过后自动尝试 Redis,成功则关闭熔断,失败则续期
|
|
75
|
+
*
|
|
76
|
+
* 兜底语义:所有方法失败时返回安全默认值(null/false/0),不抛异常上抛
|
|
120
77
|
*/
|
|
121
78
|
class Store {
|
|
122
79
|
constructor() {
|
|
123
|
-
// 内存后端(始终存在,作为降级兜底)
|
|
124
80
|
this.memory = new MemoryAdapter();
|
|
125
|
-
// Redis 后端(按需创建)
|
|
126
81
|
this.redis = null;
|
|
127
|
-
// 当前是否使用 Redis
|
|
128
82
|
this.useRedis = false;
|
|
129
|
-
// 是否已初始化
|
|
130
83
|
this._initialized = false;
|
|
131
|
-
|
|
132
|
-
|
|
84
|
+
this._lastFallbackLog = 0;
|
|
85
|
+
// 熔断状态机:broken=是否熔断中 / failures=连续失败计数 / lastFailAt=最近失败时间戳
|
|
86
|
+
this._circuit = { broken: false, failures: 0, lastFailAt: 0 };
|
|
133
87
|
}
|
|
134
88
|
|
|
135
89
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* @param {
|
|
139
|
-
* @param {
|
|
140
|
-
* @param {Object} [config.REDIS] - Redis 连接配置
|
|
90
|
+
* 初始化存储后端
|
|
91
|
+
* @param {object} [cfg={}] 配置
|
|
92
|
+
* @param {boolean} [cfg.REDIS_ENABLED] 是否启用 Redis
|
|
93
|
+
* @param {object} [cfg.REDIS] Redis 连接配置(host/port/password/db 等)
|
|
141
94
|
*/
|
|
142
|
-
async init(
|
|
95
|
+
async init(cfg = {}) {
|
|
143
96
|
if (this._initialized) return;
|
|
97
|
+
this._initialized = true;
|
|
144
98
|
|
|
145
|
-
if (
|
|
146
|
-
|
|
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)');
|
|
99
|
+
if (!cfg.REDIS_ENABLED) {
|
|
100
|
+
logger.info("[Store] 运行内存存储模式(10w容量TTL-LRU)");
|
|
101
|
+
return;
|
|
160
102
|
}
|
|
161
103
|
|
|
162
|
-
this.
|
|
104
|
+
this.redis = new RedisBackend(cfg.REDIS ?? {});
|
|
105
|
+
try {
|
|
106
|
+
await this.redis._getClient();
|
|
107
|
+
this.useRedis = true;
|
|
108
|
+
logger.info("[Store] 已启用Redis分布式存储后端");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn(`[Store] Redis连接失败,自动降级内存:${err.message}`);
|
|
111
|
+
this.useRedis = false;
|
|
112
|
+
this.redis = null;
|
|
113
|
+
}
|
|
163
114
|
}
|
|
164
115
|
|
|
165
116
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @
|
|
169
|
-
* @param {
|
|
170
|
-
* @
|
|
171
|
-
* @param {*} fallbackValue - 内存 fallback 失败时的兜底返回值
|
|
172
|
-
* @returns {*} 操作结果
|
|
117
|
+
* 内部统一调度:优先 Redis,异常自动切换内存
|
|
118
|
+
* @param {string} method 适配器方法名
|
|
119
|
+
* @param {Array} args 参数数组
|
|
120
|
+
* @param {*} fallbackVal Redis + 内存全部失败时的兜底返回值
|
|
121
|
+
* @returns {Promise<*>}
|
|
173
122
|
*/
|
|
174
|
-
async
|
|
175
|
-
//
|
|
176
|
-
if (!this.useRedis)
|
|
123
|
+
async _safeExec(method, args, fallbackVal) {
|
|
124
|
+
// 1. 未启用 Redis → 直接走内存
|
|
125
|
+
if (!this.useRedis) return this.memory[method](...args);
|
|
126
|
+
|
|
127
|
+
// 2. 熔断中且冷却期未过 → 直接走内存,避免对死掉的 Redis 持续重试
|
|
128
|
+
if (this._circuit.broken && Date.now() - this._circuit.lastFailAt < CIRCUIT_COOLDOWN) {
|
|
177
129
|
return this.memory[method](...args);
|
|
178
130
|
}
|
|
179
131
|
|
|
132
|
+
// 3. 正常路径:尝试 Redis
|
|
180
133
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
134
|
+
const result = await this.redis[method](...args);
|
|
135
|
+
// 成功后重置熔断状态
|
|
136
|
+
this._circuit.failures = 0;
|
|
137
|
+
if (this._circuit.broken) {
|
|
138
|
+
this._circuit.broken = false;
|
|
139
|
+
logger.info("[Store] Redis 已恢复,熔断关闭");
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// 4. Redis 调用失败:累计失败次数,必要时触发熔断
|
|
144
|
+
this._printFallbackLog(method, err);
|
|
145
|
+
this._circuit.failures++;
|
|
146
|
+
this._circuit.lastFailAt = Date.now();
|
|
147
|
+
if (this._circuit.failures >= CIRCUIT_THRESHOLD && !this._circuit.broken) {
|
|
148
|
+
this._circuit.broken = true;
|
|
149
|
+
logger.warn(`[Store] Redis 连续失败${this._circuit.failures}次,触发熔断:${CIRCUIT_COOLDOWN}ms 后自动重试`);
|
|
150
|
+
}
|
|
151
|
+
// 5. 内存兜底:再失败则返回安全默认值,不抛异常
|
|
152
|
+
try { return await this.memory[method](...args); }
|
|
153
|
+
catch (memErr) {
|
|
154
|
+
logger.error(`[Store] 内存兜底调用${method}失败:${memErr.message}`);
|
|
155
|
+
return fallbackVal;
|
|
191
156
|
}
|
|
192
157
|
}
|
|
193
158
|
}
|
|
194
159
|
|
|
195
160
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
161
|
+
* 打印降级日志(防抖)
|
|
162
|
+
* 高频失败场景下避免日志被刷爆,1s 内只打一次
|
|
198
163
|
*/
|
|
199
|
-
|
|
164
|
+
_printFallbackLog(method, err) {
|
|
200
165
|
const now = Date.now();
|
|
201
|
-
if (now - this.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
166
|
+
if (now - this._lastFallbackLog < FALLBACK_LOG_COOLDOWN) return;
|
|
167
|
+
this._lastFallbackLog = now;
|
|
168
|
+
logger.warn(`[Store] Redis ${method} 请求异常,临时切换内存:${err.message}`);
|
|
205
169
|
}
|
|
206
170
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
171
|
+
// ===================== 统一对外 API =====================
|
|
172
|
+
// 业务侧统一通过 store.xxx() 调用,底层后端透明
|
|
210
173
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
174
|
+
/** 读取缓存;无值或异常返回 null */
|
|
175
|
+
get(key) { return this._safeExec("get", [key], null); }
|
|
214
176
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
177
|
+
/**
|
|
178
|
+
* 写入缓存
|
|
179
|
+
* @param {string} key
|
|
180
|
+
* @param {*} val
|
|
181
|
+
* @param {number} [ttlMs=60000] 过期毫秒数
|
|
182
|
+
* @returns {Promise<boolean>}
|
|
183
|
+
*/
|
|
184
|
+
set(key, val, ttlMs = 60000) { return this._safeExec("set", [key, val, ttlMs], false); }
|
|
218
185
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
186
|
+
/** 删除指定 key;不存在或异常返回 false */
|
|
187
|
+
del(key) { return this._safeExec("del", [key], false); }
|
|
222
188
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
189
|
+
/** 原子自增;首次返回 1,异常返回 0 */
|
|
190
|
+
incr(key) { return this._safeExec("incr", [key], 0); }
|
|
226
191
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
192
|
+
/**
|
|
193
|
+
* 原子自增并设置 TTL(限流/计数场景专用)
|
|
194
|
+
* @param {string} key
|
|
195
|
+
* @param {number} ttlMs
|
|
196
|
+
* @returns {Promise<number>}
|
|
197
|
+
*/
|
|
198
|
+
incrAndExpire(key, ttlMs) { return this._safeExec("incrAndExpire", [key, ttlMs], 0); }
|
|
230
199
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
200
|
+
/** 判断 key 是否存在;异常返回 false */
|
|
201
|
+
exists(key) { return this._safeExec("exists", [key], false); }
|
|
234
202
|
|
|
235
203
|
/**
|
|
236
|
-
*
|
|
237
|
-
* @
|
|
204
|
+
* 给已存在 key 设置过期时间
|
|
205
|
+
* @param {string} key
|
|
206
|
+
* @param {number} ttlMs
|
|
207
|
+
* @returns {Promise<boolean>}
|
|
208
|
+
*/
|
|
209
|
+
expire(key, ttlMs) { return this._safeExec("expire", [key, ttlMs], false); }
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取诊断信息(健康检查/监控上报用)
|
|
213
|
+
* @returns {{mode: string, memorySize: number, redisConnected: boolean, circuitOpen: boolean}}
|
|
238
214
|
*/
|
|
239
215
|
getInfo() {
|
|
216
|
+
let mode = "memory";
|
|
217
|
+
if (this.useRedis) mode = this._circuit.broken ? "memory(fallback:redis熔断)" : "redis";
|
|
240
218
|
return {
|
|
241
|
-
mode
|
|
219
|
+
mode,
|
|
242
220
|
memorySize: this.memory.size(),
|
|
243
|
-
redisConnected: this.useRedis && this.redis?.getStatus() ===
|
|
221
|
+
redisConnected: this.useRedis && this.redis?.getStatus() === "ready",
|
|
222
|
+
circuitOpen: this._circuit.broken,
|
|
244
223
|
};
|
|
245
224
|
}
|
|
246
225
|
|
|
247
226
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
227
|
+
* 关闭并释放资源(优雅停机时调用)
|
|
228
|
+
* Redis 异常不阻断,内存缓存直接清空
|
|
250
229
|
*/
|
|
251
230
|
async close() {
|
|
252
231
|
if (this.redis) {
|
|
253
|
-
try {
|
|
254
|
-
|
|
255
|
-
} catch (e) {
|
|
256
|
-
console.warn(`[Store] 关闭 Redis 连接失败: ${e.message}`);
|
|
257
|
-
}
|
|
232
|
+
try { await this.redis.close(); }
|
|
233
|
+
catch (e) { logger.warn(`[Store] Redis关闭异常:${e.message}`); }
|
|
258
234
|
}
|
|
259
235
|
this.memory.clear();
|
|
260
236
|
}
|
|
261
237
|
}
|
|
262
238
|
|
|
263
|
-
//
|
|
239
|
+
// 全局单例:业务侧统一通过 `import { store } from "chanjs"` 访问
|
|
264
240
|
export const store = new Store();
|
|
265
|
-
|
|
266
241
|
export default Store;
|
package/utils/data-parse.js
CHANGED
|
@@ -1,186 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
* @param {string}
|
|
32
|
-
* @
|
|
33
|
-
* @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* // { 'app.name': 'MyApp', 'app.port': '3000' }
|
|
44
|
-
*/
|
|
45
|
-
export const arrToObj = (
|
|
46
|
-
arr,
|
|
47
|
-
keyField = "config_key",
|
|
48
|
-
valueField = "config_value"
|
|
49
|
-
) => {
|
|
50
|
-
if (!Array.isArray(arr)) {
|
|
51
|
-
console.error("[arrToObj] 输入必须是数组");
|
|
52
|
-
return {};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return arr.reduce((result, item) => {
|
|
56
|
-
if (item && typeof item === "object") {
|
|
57
|
-
const key = item[keyField];
|
|
58
|
-
const value = item[valueField];
|
|
59
|
-
if (key !== undefined && value !== undefined) {
|
|
60
|
-
result[key] = value;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}, {});
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 解析对象中的 JSON 字符串字段
|
|
69
|
-
* @param {Object} obj - 要处理的对象
|
|
70
|
-
* @returns {Object} 处理后的对象
|
|
71
|
-
* @description
|
|
72
|
-
* 遍历对象的所有属性,将 JSON 字符串格式的值解析为对象
|
|
73
|
-
* 只解析以 '{' 或 '[' 开头的字符串值
|
|
74
|
-
* @example
|
|
75
|
-
* const obj = {
|
|
76
|
-
* name: '张三',
|
|
77
|
-
* settings: '{"theme":"dark","lang":"zh"}',
|
|
78
|
-
* tags: '["a","b","c"]'
|
|
79
|
-
* };
|
|
80
|
-
* const result = parseJsonFields(obj);
|
|
81
|
-
* console.log(result);
|
|
82
|
-
* // { name: '张三', settings: { theme: 'dark', lang: 'zh' }, tags: ['a', 'b', 'c'] }
|
|
83
|
-
*/
|
|
84
|
-
export function parseJsonFields(obj) {
|
|
85
|
-
const result = {};
|
|
86
|
-
for (const key in obj) {
|
|
87
|
-
if (!obj.hasOwnProperty(key)) continue;
|
|
88
|
-
const value = obj[key];
|
|
89
|
-
if (
|
|
90
|
-
typeof value === "string" &&
|
|
91
|
-
(value.startsWith("{") || value.startsWith("["))
|
|
92
|
-
) {
|
|
93
|
-
try {
|
|
94
|
-
result[key] = JSON.parse(value);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error(`JSON parse failed for field: ${key}`, e);
|
|
97
|
-
result[key] = value;
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
result[key] = value;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 构建树形结构
|
|
109
|
-
* @param {Array<Object>} arr - 扁平化的数据数组
|
|
110
|
-
* @param {number|string} [pid=0] - 根节点的父 ID
|
|
111
|
-
* @param {string} [idKey="id"] - ID 字段名
|
|
112
|
-
* @param {string} [pidKey="pid"] - 父 ID 字段名
|
|
113
|
-
* @param {string} [childrenKey="children"] - 子节点字段名
|
|
114
|
-
* @returns {Array<Object>} 树形结构数组
|
|
115
|
-
* @description
|
|
116
|
-
* 将扁平化的数组转换为树形结构
|
|
117
|
-
* 支持自定义 ID 和父 ID 字段名
|
|
118
|
-
* 自动检测并跳过循环引用
|
|
119
|
-
* 自动清理空的 children 数组
|
|
120
|
-
* @example
|
|
121
|
-
* const arr = [
|
|
122
|
-
* { id: 1, pid: 0, name: '根节点' },
|
|
123
|
-
* { id: 2, pid: 1, name: '子节点1' },
|
|
124
|
-
* { id: 3, pid: 1, name: '子节点2' },
|
|
125
|
-
* { id: 4, pid: 2, name: '孙节点' }
|
|
126
|
-
* ];
|
|
127
|
-
* const tree = buildTree(arr);
|
|
128
|
-
* console.log(tree);
|
|
129
|
-
* // [
|
|
130
|
-
* // { id: 1, pid: 0, name: '根节点', children: [
|
|
131
|
-
* // { id: 2, pid: 1, name: '子节点1', children: [
|
|
132
|
-
* // { id: 4, pid: 2, name: '孙节点' }
|
|
133
|
-
* // ]},
|
|
134
|
-
* // { id: 3, pid: 1, name: '子节点2' }
|
|
135
|
-
* // ]}
|
|
136
|
-
* // ]
|
|
137
|
-
*/
|
|
138
|
-
export function buildTree(
|
|
139
|
-
arr,
|
|
140
|
-
pid = 0,
|
|
141
|
-
idKey = "id",
|
|
142
|
-
pidKey = "pid",
|
|
143
|
-
childrenKey = "children"
|
|
144
|
-
) {
|
|
145
|
-
if (!Array.isArray(arr) || arr.length === 0) return [];
|
|
146
|
-
|
|
147
|
-
const nodeMap = new Map();
|
|
148
|
-
const tree = [];
|
|
149
|
-
const visited = new Set();
|
|
150
|
-
|
|
151
|
-
for (const item of arr) {
|
|
152
|
-
if (item && typeof item === 'object') {
|
|
153
|
-
nodeMap.set(item[idKey], { ...item, [childrenKey]: [] });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const node of nodeMap.values()) {
|
|
158
|
-
const parentId = node[pidKey];
|
|
159
|
-
if (parentId === pid || parentId == null) {
|
|
160
|
-
tree.push(node);
|
|
161
|
-
} else {
|
|
162
|
-
const parent = nodeMap.get(parentId);
|
|
163
|
-
if (parent) {
|
|
164
|
-
if (visited.has(node[idKey])) {
|
|
165
|
-
console.error(`[buildTree] 检测到循环引用: ${node[idKey]}`);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
visited.add(node[idKey]);
|
|
169
|
-
parent[childrenKey].push(node);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const cleanEmptyChildren = (nodes) => {
|
|
175
|
-
for (const node of nodes) {
|
|
176
|
-
if (node[childrenKey].length === 0) {
|
|
177
|
-
delete node[childrenKey];
|
|
178
|
-
} else {
|
|
179
|
-
cleanEmptyChildren(node[childrenKey]);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
cleanEmptyChildren(tree);
|
|
184
|
-
|
|
185
|
-
return tree;
|
|
186
|
-
}
|
|
1
|
+
import logger from "./logger.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 通用数据解析工具集
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 将对象数组转换成键值映射对象
|
|
9
|
+
* 适用于数据库配置列表、字典列表快速转map读取
|
|
10
|
+
* @param {Array<Object>} arr - 原始对象数组
|
|
11
|
+
* @param {string} [keyField="config_key"] - 映射key取自item的哪个字段
|
|
12
|
+
* @param {string} [valueField="config_value"] - 映射value取自item的哪个字段
|
|
13
|
+
* @returns {Record<string, any>} 键值映射对象
|
|
14
|
+
*/
|
|
15
|
+
export const arrToObj = (arr, keyField = "config_key", valueField = "config_value") => {
|
|
16
|
+
if (!Array.isArray(arr)) {
|
|
17
|
+
logger.error("[arrToObj] 参数类型错误:输入必须为数组", arr);
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
return arr.reduce((result, item) => {
|
|
21
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return result;
|
|
22
|
+
const mapKey = item[keyField];
|
|
23
|
+
const mapVal = item[valueField];
|
|
24
|
+
if (mapKey !== undefined && mapVal !== undefined) result[mapKey] = mapVal;
|
|
25
|
+
return result;
|
|
26
|
+
}, {});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 根据拼音/ID匹配分类,返回分类完整数据与分类ID
|
|
31
|
+
* @param {string | number} py - 检索条件:拼音字符串 / 数字ID
|
|
32
|
+
* @param {Array<Object>} source - 分类数据源数组
|
|
33
|
+
* @returns {{ cate: Object, id: string|number }}
|
|
34
|
+
*/
|
|
35
|
+
export function getChildrenId(py, source) {
|
|
36
|
+
if (!Array.isArray(source)) {
|
|
37
|
+
logger.warn("[getChildrenId] 数据源source不是数组", source);
|
|
38
|
+
return { cate: {}, id: "" };
|
|
39
|
+
}
|
|
40
|
+
const cate = source.find(item => item && typeof item === "object" && (item.pinyin === py || item.id === py)) || {};
|
|
41
|
+
return { cate, id: cate.id ?? "" };
|
|
42
|
+
}
|