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.
Files changed (94) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/BaseComponent.js +27 -0
  5. package/core/Container.js +68 -0
  6. package/core/Controller.js +29 -0
  7. package/core/Database.js +93 -0
  8. package/core/Repository.js +323 -0
  9. package/core/Service.js +11 -0
  10. package/core/bootstrap/error-handler.js +101 -0
  11. package/core/bootstrap/hook-runner.js +64 -0
  12. package/core/bootstrap/middleware.js +35 -0
  13. package/core/bootstrap/router-loader.js +53 -0
  14. package/core/errors.js +251 -0
  15. package/core/loader.js +89 -0
  16. package/core/registry.js +17 -0
  17. package/doc/Cache.md +279 -106
  18. package/doc/Common.md +590 -134
  19. package/doc/Controller.md +166 -95
  20. package/doc/Help.md +299 -698
  21. package/doc/QuickStart.md +116 -0
  22. package/doc/Repository.md +560 -0
  23. package/doc/Service.md +201 -527
  24. package/index.js +61 -37
  25. package/middleware/body.js +17 -0
  26. package/middleware/cookie.js +7 -15
  27. package/middleware/cors.js +9 -27
  28. package/middleware/favicon.js +15 -17
  29. package/middleware/header.js +15 -16
  30. package/middleware/index.js +11 -11
  31. package/middleware/log.js +26 -56
  32. package/middleware/static.js +15 -28
  33. package/middleware/template.js +75 -115
  34. package/middleware/validate.js +79 -0
  35. package/middleware/waf.js +176 -197
  36. package/package.json +9 -2
  37. package/response/code.js +73 -0
  38. package/response/index.js +9 -6
  39. package/response/response.js +82 -236
  40. package/security/checker.js +26 -74
  41. package/security/index.js +4 -9
  42. package/security/jwt.js +84 -139
  43. package/security/keywords.js +33 -137
  44. package/security/rate-limit.js +38 -80
  45. package/security/sign.js +83 -176
  46. package/security/xss-filter.js +21 -53
  47. package/storage/cache.js +58 -198
  48. package/storage/index.js +3 -6
  49. package/storage/redis.js +124 -181
  50. package/storage/store.js +163 -188
  51. package/utils/data-parse.js +42 -186
  52. package/utils/file.js +73 -244
  53. package/utils/filter.js +22 -25
  54. package/utils/html.js +49 -33
  55. package/utils/index.js +20 -7
  56. package/utils/ip.js +31 -71
  57. package/utils/logger.js +117 -0
  58. package/utils/pages.js +55 -0
  59. package/utils/paths.js +18 -0
  60. package/utils/request.js +95 -136
  61. package/utils/signal.js +87 -0
  62. package/utils/time.js +33 -75
  63. package/utils/tree.js +112 -104
  64. package/App.js +0 -533
  65. package/base/Aop.js +0 -195
  66. package/base/Container.js +0 -161
  67. package/base/Controller.js +0 -65
  68. package/base/Database.js +0 -133
  69. package/base/Event.js +0 -61
  70. package/base/Repository.js +0 -644
  71. package/common/api.js +0 -35
  72. package/common/code.js +0 -52
  73. package/common/email.js +0 -191
  74. package/common/index.js +0 -5
  75. package/common/pages.js +0 -120
  76. package/common/utils.js +0 -73
  77. package/config/code.js +0 -166
  78. package/config/paths.js +0 -60
  79. package/doc/Aop.md +0 -269
  80. package/doc/Email.md +0 -114
  81. package/doc/Event.md +0 -232
  82. package/global/env.js +0 -11
  83. package/global/import.js +0 -39
  84. package/global/index.js +0 -8
  85. package/helper/index.js +0 -79
  86. package/loader/index.js +0 -6
  87. package/loader/loader.js +0 -138
  88. package/middleware/compress.js +0 -185
  89. package/middleware/setBody.js +0 -32
  90. package/realtime/index.js +0 -7
  91. package/realtime/sse.js +0 -424
  92. package/realtime/websocket.js +0 -540
  93. package/schedule/index.js +0 -6
  94. 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
- * 把同步的 cache.js API 包装成异步,对齐 RedisBackend 的接口
64
- * 直接复用 cache.js incr/incrAndExpire/expire,保证 TTL 行为与 Redis 一致
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
- async get(key) {
77
- return this.cache.get(key);
78
- }
22
+ /** 读取缓存值,无值返回 null */
23
+ get(key) { return this._cache.get(key); }
79
24
 
80
- async set(key, value, ttlMs = 60000) {
81
- this.cache.set(key, value, ttlMs);
82
- return true;
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
- async del(key) {
86
- return this.cache.del(key);
87
- }
34
+ /** 删除指定 key */
35
+ del(key) { return this._cache.del(key); }
88
36
 
89
- async incr(key) {
90
- // 直接调用 cache.incr,保证 TTL 不被刷新(与 Redis INCR 行为对齐)
91
- return this.cache.incr(key);
92
- }
37
+ /** 原子自增;新 key 起始值 1 */
38
+ incr(key) { return this._cache.incr(key); }
93
39
 
94
- async incrAndExpire(key, ttlMs) {
95
- return this.cache.incrAndExpire(key, ttlMs);
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
- async exists(key) {
99
- return this.cache.has(key);
100
- }
47
+ /** 判断 key 是否存在 */
48
+ exists(key) { return this._cache.has(key); }
101
49
 
102
- async expire(key, ttlMs) {
103
- return this.cache.expire(key, ttlMs);
104
- }
50
+ /**
51
+ * 给已存在的 key 设置过期时间
52
+ * @returns {boolean} key 不存在时返回 false
53
+ */
54
+ expire(key, ttlMs) { return this._cache.expire(key, ttlMs); }
105
55
 
106
- size() {
107
- return this.cache.size();
108
- }
56
+ /** 获取当前缓存条目数 */
57
+ size() { return this._cache.size(); }
109
58
 
110
- clear() {
111
- this.cache.clear();
112
- }
59
+ /** 清空所有缓存 */
60
+ clear() { this._cache.clear(); }
113
61
  }
114
62
 
115
63
  /**
116
- * 存储适配器
117
- * @class Store
118
- * 自动选择后端:Redis(启用且可用时)或 内存(默认/降级时)
119
- * 运行时 Redis 异常自动 fallback 到内存,不中断业务
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
- this._fallbackCooldown = 0;
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 {Object} config - 应用配置
139
- * @param {boolean} [config.REDIS_ENABLED=false] - 是否启用 Redis
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(config = {}) {
95
+ async init(cfg = {}) {
143
96
  if (this._initialized) return;
97
+ this._initialized = true;
144
98
 
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)');
99
+ if (!cfg.REDIS_ENABLED) {
100
+ logger.info("[Store] 运行内存存储模式(10w容量TTL-LRU)");
101
+ return;
160
102
  }
161
103
 
162
- this._initialized = true;
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
- * 运行时异常兜底:Redis 操作抛错时临时切内存执行
167
- * 不中断业务,打印降级日志(带冷却避免刷屏)
168
- * @private
169
- * @param {string} method - 方法名
170
- * @param {Array} args - 参数
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 _safeCall(method, args, fallbackValue) {
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
- 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;
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
- * 打印降级日志(带冷却,1 秒内最多一次)
197
- * @private
161
+ * 打印降级日志(防抖)
162
+ * 高频失败场景下避免日志被刷爆,1s 内只打一次
198
163
  */
199
- _logFallback(method, err) {
164
+ _printFallbackLog(method, err) {
200
165
  const now = Date.now();
201
- if (now - this._fallbackCooldown > 1000) {
202
- this._fallbackCooldown = now;
203
- console.warn(`[Store] Redis ${method} 失败,临时降级内存执行: ${err.message}`);
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
- async get(key) {
208
- return this._safeCall('get', [key], null);
209
- }
171
+ // ===================== 统一对外 API =====================
172
+ // 业务侧统一通过 store.xxx() 调用,底层后端透明
210
173
 
211
- async set(key, value, ttlMs = 60000) {
212
- return this._safeCall('set', [key, value, ttlMs], false);
213
- }
174
+ /** 读取缓存;无值或异常返回 null */
175
+ get(key) { return this._safeExec("get", [key], null); }
214
176
 
215
- async del(key) {
216
- return this._safeCall('del', [key], false);
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
- async incr(key) {
220
- return this._safeCall('incr', [key], 0);
221
- }
186
+ /** 删除指定 key;不存在或异常返回 false */
187
+ del(key) { return this._safeExec("del", [key], false); }
222
188
 
223
- async incrAndExpire(key, ttlMs) {
224
- return this._safeCall('incrAndExpire', [key, ttlMs], 0);
225
- }
189
+ /** 原子自增;首次返回 1,异常返回 0 */
190
+ incr(key) { return this._safeExec("incr", [key], 0); }
226
191
 
227
- async exists(key) {
228
- return this._safeCall('exists', [key], false);
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
- async expire(key, ttlMs) {
232
- return this._safeCall('expire', [key, ttlMs], false);
233
- }
200
+ /** 判断 key 是否存在;异常返回 false */
201
+ exists(key) { return this._safeExec("exists", [key], false); }
234
202
 
235
203
  /**
236
- * 获取当前后端信息(用于诊断)
237
- * @returns {Object} { mode, memorySize, redisConnected }
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: this.useRedis ? 'redis' : 'memory',
219
+ mode,
242
220
  memorySize: this.memory.size(),
243
- redisConnected: this.useRedis && this.redis?.getStatus() === 'ready',
221
+ redisConnected: this.useRedis && this.redis?.getStatus() === "ready",
222
+ circuitOpen: this._circuit.broken,
244
223
  };
245
224
  }
246
225
 
247
226
  /**
248
- * 关闭连接
249
- * @returns {Promise<void>}
227
+ * 关闭并释放资源(优雅停机时调用)
228
+ * Redis 异常不阻断,内存缓存直接清空
250
229
  */
251
230
  async close() {
252
231
  if (this.redis) {
253
- try {
254
- await this.redis.close();
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;
@@ -1,186 +1,42 @@
1
- /**
2
- * 解析 JSON 字符串或返回原始值
3
- * @param {string} str - 要解析的字符串
4
- * @returns {Object|string} 解析后的对象或原始字符串
5
- * @description
6
- * 尝试将字符串解析为 JSON 对象
7
- * 如果解析失败或结果不是对象,则返回原始字符串
8
- * @example
9
- * const obj = dataParse('{"name":"张三","age":25}');
10
- * console.log(obj); // { name: '张三', age: 25 }
11
- *
12
- * const str = dataParse('hello');
13
- * console.log(str); // 'hello'
14
- */
15
- export function dataParse(str) {
16
- try {
17
- const data = JSON.parse(str);
18
- if (typeof data === 'object' && data !== null) {
19
- return data;
20
- }
21
- } catch (e) {
22
- return str;
23
- }
24
- return str;
25
- }
26
-
27
- /**
28
- * 将数组转换为对象
29
- * @param {Array<Object>} arr - 要转换的数组
30
- * @param {string} [keyField="config_key"] - 作为对象键的字段名
31
- * @param {string} [valueField="config_value"] - 作为对象值的字段名
32
- * @returns {Object} 转换后的对象
33
- * @description
34
- * 将数组中的每个元素转换为一个键值对
35
- * 键由 keyField 指定,值由 valueField 指定
36
- * @example
37
- * const arr = [
38
- * { config_key: 'app.name', config_value: 'MyApp' },
39
- * { config_key: 'app.port', config_value: '3000' }
40
- * ];
41
- * const obj = arrToObj(arr);
42
- * console.log(obj);
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
+ }