chanjs 2.7.4 → 2.7.5
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/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -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 +224 -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 +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -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 +174 -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 +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -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 +21 -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 +94 -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/utils/time.js
CHANGED
|
@@ -1,89 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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>}
|
|
14
|
-
* @param {number} [
|
|
15
|
-
* @param {Object} [
|
|
16
|
-
* @param {number} [
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
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(
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
69
|
-
* @param {number}
|
|
70
|
-
* @param {Array<Object>}
|
|
71
|
-
* @param {Object} [
|
|
72
|
-
* @param {number} [
|
|
73
|
-
* @
|
|
74
|
-
* @
|
|
75
|
-
*
|
|
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(
|
|
90
|
-
const
|
|
91
|
-
|
|
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
|
-
|
|
109
|
+
while (currentId != null && currentId !== 0) {
|
|
95
110
|
if (depth >= maxDepth) {
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
}
|