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/utils/time.js CHANGED
@@ -1,89 +1,47 @@
1
1
  /**
2
- * 格式化时间戳为指定格式的字符串
3
- * @param {number|string|Date} timestamp - 时间戳、日期字符串或 Date 对象
4
- * @param {string} [format='YYYY-MM-DD HH:mm:ss'] - 输出格式
5
- * @returns {string} 格式化后的时间字符串
6
- * @description
7
- * 支持的格式占位符:
8
- * - YYYY: 四位年份
9
- * - MM: 两位月份(01-12)
10
- * - DD: 两位日期(01-31)
11
- * - HH: 两位小时(00-23)
12
- * - mm: 两位分钟(00-59)
13
- * - ss: 两位秒数(00-59)
14
- * @example
15
- * formatTime(1704067200000); // '2024-01-01 00:00:00'
16
- * formatTime(1704067200000, 'YYYY/MM/DD'); // '2024/01/01'
17
- * formatTime('2024-01-01', 'YYYY-MM-DD'); // '2024-01-01'
2
+ * 时间格式化工具
3
+ */
4
+ const TIME_TOKENS = [
5
+ [/Y{2,4}/, 'getFullYear'],
6
+ [/M{2}/, 'getMonth', 1],
7
+ [/D{2}/, 'getDate'],
8
+ [/H{2}/, 'getHours'],
9
+ [/m{2}/, 'getMinutes'],
10
+ [/s{2}/, 'getSeconds'],
11
+ ];
12
+
13
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
14
+
15
+ /**
16
+ * 格式化时间戳为指定格式
17
+ * @param {number|string|Date} timestamp 时间戳/日期字符串/Date 对象
18
+ * @param {string} [format='YYYY-MM-DD HH:mm:ss'] 输出格式
19
+ * @returns {string}
18
20
  */
19
21
  export function formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
20
- try {
21
- const date = new Date(timestamp);
22
- if (isNaN(date.getTime())) {
23
- return timestamp;
22
+ const date = new Date(timestamp);
23
+ if (isNaN(date.getTime())) return String(timestamp ?? '');
24
+ return format.replace(/Y{2,4}|M{2}|D{2}|H{2}|m{2}|s{2}/g, m => {
25
+ for (const [re, fn, off = 0] of TIME_TOKENS) {
26
+ if (re.test(m)) return pad(date[fn]() + off, m.length);
24
27
  }
25
- const year = date.getFullYear();
26
- const month = String(date.getMonth() + 1).padStart(2, '0');
27
- const day = String(date.getDate()).padStart(2, '0');
28
- const hours = String(date.getHours()).padStart(2, '0');
29
- const minutes = String(date.getMinutes()).padStart(2, '0');
30
- const seconds = String(date.getSeconds()).padStart(2, '0');
31
-
32
- return format
33
- .replace('YYYY', year)
34
- .replace('MM', month)
35
- .replace('DD', day)
36
- .replace('HH', hours)
37
- .replace('mm', minutes)
38
- .replace('ss', seconds);
39
- } catch (error) {
40
- console.error('[formatTime] 格式化时间失败:', error, 'timestamp:', timestamp);
41
- return timestamp;
42
- }
28
+ return m;
29
+ });
43
30
  }
44
31
 
45
32
  /**
46
- * 格式化对象或数组中的日期字段
47
- * @param {Object|Array<Object>} data - 要处理的数据对象或数组
48
- * @param {Array<string>} [fields] - 需要格式化的字段名数组(可选,默认处理常用日期字段)
49
- * @returns {Object|Array<Object>} 处理后的数据
50
- * @description
51
- * 将对象或数组中指定的日期字段格式化为 'YYYY-MM-DD HH:mm:ss' 格式
52
- * 支持递归处理数组中的每个对象
53
- * 自动匹配下划线和驼峰格式的字段名(如 createdAt/created_at)
54
- * @example
55
- * const data = { id: 1, createdAt: 1704067200000, updatedAt: 1704067200000 };
56
- * const result = formatDateFields(data);
57
- * console.log(result);
58
- * // { id: 1, createdAt: '2024-01-01 00:00:00', updatedAt: '2024-01-01 00:00:00' }
59
- *
60
- * const list = [
61
- * { id: 1, createdAt: 1704067200000 },
62
- * { id: 2, createdAt: 1704153600000 }
63
- * ];
64
- * const formatted = formatDateFields(list);
65
- * console.log(formatted);
66
- * // [
67
- * // { id: 1, createdAt: '2024-01-01 00:00:00' },
68
- * // { id: 2, createdAt: '2024-01-02 00:00:00' }
69
- * // ]
33
+ * 格式化对象/数组中的日期字段
34
+ * @param {Object|Array<Object>} data
35
+ * @param {string[]} fields 需要格式化的字段名数组
36
+ * @returns {Object|Array<Object>}
70
37
  */
71
38
  export function formatDateFields(data, fields) {
72
39
  if (!data || typeof data !== 'object') return data;
73
-
74
- if (Array.isArray(data)) {
75
- return data.map(item => formatDateFields(item, fields));
76
- }
77
-
40
+ if (Array.isArray(data)) return data.map(item => formatDateFields(item, fields));
41
+ if (!Array.isArray(fields) || !fields.length) return data;
78
42
  const result = { ...data };
79
-
80
- const defaultFields = ['createdAt', 'updatedAt', 'created_at', 'updated_at'];
81
- const targetFields = fields || defaultFields;
82
-
83
- for (const field of targetFields) {
84
- if (result[field]) {
85
- result[field] = formatTime(result[field]);
86
- }
43
+ for (const field of fields) {
44
+ if (result[field]) result[field] = formatTime(result[field]);
87
45
  }
88
46
  return result;
89
47
  }
package/utils/tree.js CHANGED
@@ -1,121 +1,129 @@
1
- /**
2
- * 树形结构工具函数
3
- * 提供数组转树形结构和路径查找功能
4
- */
1
+ import logger from "./logger.js";
5
2
 
6
- /**
7
- * 默认最大递归深度,防止循环引用导致栈溢出
8
- */
9
- const DEFAULT_MAX_DEPTH = 20;
3
+ /** 默认最大递归深度,防止循环引用栈溢出 */
4
+ const MAX_DEPTH = 10;
10
5
 
11
6
  /**
12
- * 将扁平数组转换为树形结构
13
- * @param {Array<Object>} arr - 扁平化的数据数组
14
- * @param {number} [pid=0] - 父节点 ID,默认为 0
15
- * @param {Object} [opts] - 选项
16
- * @param {number} [opts.maxDepth=20] - 最大递归深度
17
- * @param {Set} [opts._visited] - 已访问节点 id 集合(防循环引用,内部用)
18
- * @param {number} [opts._depth=0] - 当前递归深度(内部用)
19
- * @returns {Array<Object>} 树形结构数组
20
- * @description
21
- * 递归构建树形结构,为每个节点添加 level 属性
22
- * 如果有子节点,则添加 children 属性
23
- *
24
- * 安全改进:
25
- * - maxDepth 限制递归深度,防止恶意数据导致栈溢出
26
- * - _visited 跟踪已访问的 id,遇到循环引用时立即终止
27
- * @example
28
- * const arr = [
29
- * { id: 1, pid: 0, name: '根节点' },
30
- * { id: 2, pid: 1, name: '子节点' },
31
- * { id: 3, pid: 2, name: '孙节点' }
32
- * ];
33
- * const tree = tree(arr);
34
- * // 返回带有 children 和 level 的树形结构
7
+ * 扁平数组转树形结构(预建Map优化性能,支持自定义id/pid字段、防循环引用)
8
+ * @param {Array<Object>} list 扁平源数组
9
+ * @param {number|string} [rootPid=0] 根节点父ID
10
+ * @param {Object} [options={}] 配置项
11
+ * @param {number} [options.maxDepth=20] 最大递归深度
12
+ * @param {string} [options.idKey='id'] 节点唯一标识字段
13
+ * @param {string} [options.pidKey='pid'] 父ID字段
14
+ * @returns {Array<Object>} 树形节点数组,节点自带 children、level
35
15
  */
36
- export function tree(arr, pid = 0, opts = {}) {
37
- const maxDepth = opts.maxDepth || DEFAULT_MAX_DEPTH;
38
- const visited = opts._visited || new Set();
39
- let depth = opts._depth || 0;
40
-
41
- // 空数组 / 超过最大深度 / 循环引用检测
42
- if (!Array.isArray(arr) || arr.length === 0) return [];
43
- if (depth >= maxDepth) {
44
- console.warn(`[tree] 已达到最大递归深度 ${maxDepth},可能存在循环引用`);
45
- return [];
16
+ export function tree(list, rootPid = 0, options = {}) {
17
+ const {
18
+ maxDepth = MAX_DEPTH,
19
+ idKey = "id",
20
+ pidKey = "pid",
21
+ } = options;
22
+
23
+ // 基础校验
24
+ if (!Array.isArray(list) || list.length === 0) return [];
25
+
26
+ // 1. 一次性构建id映射,O(n),避免递归重复遍历数组(核心性能优化)
27
+ const nodeMap = new Map();
28
+ const rootNodes = [];
29
+ const allNodes = [];
30
+
31
+ for (const item of list) {
32
+ const node = { ...item, children: [], level: 0 };
33
+ nodeMap.set(node[idKey], node);
34
+ allNodes.push(node);
46
35
  }
47
- if (visited.has(pid)) {
48
- console.warn(`[tree] 检测到循环引用,pid=${pid} 已访问过`);
49
- return [];
36
+
37
+ // 2. 父子关联
38
+ for (const node of allNodes) {
39
+ const parentId = node[pidKey];
40
+ if (parentId === rootPid) {
41
+ rootNodes.push(node);
42
+ node.level = 1;
43
+ continue;
44
+ }
45
+ const parent = nodeMap.get(parentId);
46
+ if (parent) {
47
+ parent.children.push(node);
48
+ }
50
49
  }
51
- visited.add(pid);
52
-
53
- let result = [];
54
- arr.forEach((item) => {
55
- if (item.pid === pid) {
56
- let children = tree(arr, item.id, { maxDepth, _visited: visited, _depth: depth + 1 });
57
- if (children.length) {
58
- item.children = children;
59
- }
60
- item.level = 1;
61
- result.push(item);
50
+
51
+ // 3. 递归填充层级 + 循环引用深度拦截
52
+ const visitedIds = new Set();
53
+ function setLevel(node, depth = 1) {
54
+ if (depth > maxDepth) {
55
+ logger.warn(`[tree] 递归深度超限${maxDepth},疑似循环引用,id:${node[idKey]}`);
56
+ return;
57
+ }
58
+ const nodeId = node[idKey];
59
+ if (visitedIds.has(nodeId)) {
60
+ logger.warn(`[tree] 检测循环引用 id:${nodeId}`);
61
+ return;
62
+ }
63
+ visitedIds.add(nodeId);
64
+ node.level = depth;
65
+ for (const child of node.children) {
66
+ setLevel(child, depth + 1);
62
67
  }
63
- });
64
- return result;
68
+ }
69
+
70
+ for (const root of rootNodes) {
71
+ setLevel(root, 1);
72
+ // 无孩子则删除空children字段,精简结构
73
+ if (root.children.length === 0) delete root.children;
74
+ }
75
+
76
+ return rootNodes;
65
77
  }
66
78
 
67
79
  /**
68
- * 根据 ID 查找节点路径
69
- * @param {number} id - 要查找的节点 ID
70
- * @param {Array<Object>} source - 数据源数组
71
- * @param {Object} [opts] - 选项
72
- * @param {number} [opts.maxDepth=20] - 最大递归深度
73
- * @returns {Array<Object>} 从根节点到目标节点的路径数组
74
- * @description
75
- * 递归查找指定 ID 的节点及其所有父节点
76
- * 为每个节点添加 path 属性,包含拼音路径
77
- *
78
- * 安全改进:
79
- * - maxDepth 限制递归深度,防止恶意数据导致栈溢出
80
- * - 用 visited 集合检测循环引用
81
- * @example
82
- * const arr = [
83
- * { id: 1, pid: 0, pinyin: 'root' },
84
- * { id: 2, pid: 1, pinyin: 'child' }
85
- * ];
86
- * const path = treeById(2, arr);
87
- * // 返回包含 root 和 child 节点的数组
80
+ * 根据节点ID向上追溯完整父级路径(通用版,剥离硬编码pinyin业务逻辑)
81
+ * @param {number|string} targetId 目标节点ID
82
+ * @param {Array<Object>} list 扁平源数组
83
+ * @param {Object} [options={}] 配置
84
+ * @param {number} [options.maxDepth=20] 最大追溯深度
85
+ * @param {string} [options.idKey='id'] id字段
86
+ * @param {string} [options.pidKey='pid'] 父ID字段
87
+ * @returns {Array<Object>} 路径数组 [根节点...父节点...目标节点]
88
88
  */
89
- export function treeById(id, source, opts = {}) {
90
- const maxDepth = opts.maxDepth || DEFAULT_MAX_DEPTH;
91
- const arr = [];
89
+ export function treeById(targetId, list, options = {}) {
90
+ const {
91
+ maxDepth = MAX_DEPTH,
92
+ idKey = "id",
93
+ pidKey = "pid",
94
+ } = options;
95
+
96
+ if (!Array.isArray(list) || targetId == null) return [];
97
+
98
+ // 预构建Map加速查找
99
+ const nodeMap = new Map();
100
+ for (const item of list) {
101
+ nodeMap.set(item[idKey], item);
102
+ }
103
+
104
+ const path = [];
92
105
  const visited = new Set();
106
+ let currentId = targetId;
107
+ let depth = 0;
93
108
 
94
- const findId = (id, source, depth = 0) => {
109
+ while (currentId != null && currentId !== 0) {
95
110
  if (depth >= maxDepth) {
96
- console.warn(`[treeById] 已达到最大递归深度 ${maxDepth},可能存在循环引用`);
97
- return;
98
- }
99
- if (visited.has(id)) {
100
- console.warn(`[treeById] 检测到循环引用,id=${id} 已访问过`);
101
- return;
111
+ logger.warn(`[treeById] 追溯深度超限${maxDepth},疑似循环引用 id:${currentId}`);
112
+ break;
102
113
  }
103
- visited.add(id);
104
- for (let i = 0, item; i < source.length; i++) {
105
- item = source[i];
106
- if (item.id == id) {
107
- arr.unshift(item);
108
- if (item.pid != 0) {
109
- findId(item.pid, source, depth + 1);
110
- }
111
- }
114
+ if (visited.has(currentId)) {
115
+ logger.warn(`[treeById] 检测循环引用 id:${currentId}`);
116
+ break;
112
117
  }
113
- };
114
- findId(id, source);
115
- const _path = [];
116
- arr.forEach((item) => {
117
- _path.push("/" + item.pinyin);
118
- item.path = _path.join("");
119
- });
120
- return arr;
118
+ visited.add(currentId);
119
+
120
+ const node = nodeMap.get(currentId);
121
+ if (!node) break;
122
+
123
+ path.unshift({ ...node });
124
+ currentId = node[pidKey];
125
+ depth++;
126
+ }
127
+
128
+ return path;
121
129
  }