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.
Files changed (67) hide show
  1. package/App.js +232 -16
  2. package/base/Aop.js +20 -3
  3. package/base/Container.js +80 -3
  4. package/base/Controller.js +38 -9
  5. package/base/Database.js +50 -0
  6. package/base/Event.js +12 -0
  7. package/base/{Service.js → Repository.js} +644 -539
  8. package/common/api.js +18 -8
  9. package/common/code.js +25 -15
  10. package/common/email.js +98 -17
  11. package/common/index.js +1 -1
  12. package/config/code.js +138 -82
  13. package/global/index.js +1 -1
  14. package/helper/index.js +43 -41
  15. package/index.js +19 -6
  16. package/loader/index.js +6 -0
  17. package/{helper → loader}/loader.js +41 -27
  18. package/middleware/compress.js +185 -0
  19. package/middleware/cors.js +36 -24
  20. package/middleware/header.js +5 -10
  21. package/middleware/index.js +1 -0
  22. package/middleware/log.js +27 -3
  23. package/middleware/setBody.js +9 -1
  24. package/middleware/static.js +2 -1
  25. package/middleware/template.js +139 -4
  26. package/middleware/waf.js +136 -76
  27. package/package.json +2 -3
  28. package/realtime/index.js +7 -0
  29. package/realtime/sse.js +424 -0
  30. package/realtime/websocket.js +540 -0
  31. package/response/index.js +12 -0
  32. package/response/response.js +258 -0
  33. package/schedule/index.js +6 -0
  34. package/schedule/schedule.js +491 -0
  35. package/{helper → security}/checker.js +23 -8
  36. package/security/index.js +14 -0
  37. package/{helper → security}/jwt.js +175 -107
  38. package/security/keywords.js +179 -0
  39. package/security/rate-limit.js +105 -0
  40. package/security/sign.js +210 -0
  41. package/security/xss-filter.js +63 -0
  42. package/storage/cache.js +258 -0
  43. package/storage/index.js +9 -0
  44. package/storage/redis.js +258 -0
  45. package/storage/store.js +266 -0
  46. package/{helper → utils}/file.js +106 -15
  47. package/{helper → utils}/filter.js +2 -1
  48. package/{helper → utils}/html.js +19 -1
  49. package/utils/index.js +34 -0
  50. package/{helper → utils}/ip.js +25 -16
  51. package/utils/request.js +172 -0
  52. package/{helper → utils}/time.js +1 -1
  53. package/utils/tree.js +121 -0
  54. package/common/category.js +0 -22
  55. package/common/sms.js +0 -104
  56. package/extend/art-template.js +0 -129
  57. package/extend/index.js +0 -6
  58. package/global/global.js +0 -63
  59. package/helper/cache.js +0 -187
  60. package/helper/keywords.js +0 -132
  61. package/helper/rate-limit.js +0 -116
  62. package/helper/request.js +0 -47
  63. package/helper/response.js +0 -180
  64. package/helper/sign.js +0 -96
  65. package/helper/tree.js +0 -77
  66. package/helper/xss-filter.js +0 -42
  67. /package/{helper → utils}/data-parse.js +0 -0
@@ -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;
@@ -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;