chanjs 2.1.1 → 2.3.0
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/App.js +387 -0
- package/base/Context.js +78 -0
- package/base/Controller.js +137 -0
- package/base/Database.js +314 -0
- package/base/Service.js +539 -0
- package/common/api.js +25 -0
- package/common/category.js +22 -0
- package/common/code.js +42 -0
- package/common/email.js +110 -0
- package/common/index.js +7 -0
- package/common/pages.js +86 -0
- package/common/sms.js +104 -0
- package/common/utils.js +73 -0
- package/config/code.js +110 -52
- package/config/index.js +10 -0
- package/config/paths.js +60 -0
- package/extend/art-template.js +46 -28
- package/extend/index.js +6 -0
- package/global/env.js +11 -5
- package/global/global.js +63 -39
- package/global/import.js +43 -39
- package/global/index.js +8 -3
- package/helper/cache.js +182 -0
- package/helper/data-parse.js +121 -37
- package/helper/db.js +71 -83
- package/helper/file.js +158 -208
- package/helper/filter.js +34 -0
- package/helper/html.js +30 -47
- package/helper/index.js +29 -5
- package/helper/ip.js +48 -31
- package/helper/jwt.js +78 -11
- package/helper/loader.js +93 -50
- package/helper/request.js +41 -144
- package/helper/sign.js +96 -33
- package/helper/time.js +89 -74
- package/helper/tree.js +77 -0
- package/index.js +15 -181
- package/middleware/cookie.js +20 -4
- package/middleware/cors.js +20 -0
- package/middleware/favicon.js +21 -5
- package/middleware/header.js +26 -9
- package/middleware/index.js +14 -23
- package/middleware/preventRetry.js +30 -0
- package/middleware/setBody.js +24 -10
- package/middleware/static.js +31 -10
- package/middleware/template.js +34 -14
- package/middleware/validator.js +43 -23
- package/middleware/waf.js +147 -287
- package/package.json +1 -1
- package/utils/checker.js +68 -0
- package/utils/error-handler.js +115 -0
- package/utils/error.js +81 -0
- package/utils/index.js +6 -0
- package/utils/keywords.js +126 -0
- package/utils/rate-limit.js +116 -0
- package/utils/response.js +103 -64
- package/utils/xss-filter.js +42 -0
- package/core/controller.js +0 -33
- package/core/index.js +0 -3
- package/core/service.js +0 -307
- package/middleware/log.js +0 -21
package/helper/data-parse.js
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param {
|
|
4
|
-
* @
|
|
5
|
-
* @
|
|
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"] - 作为对象值的字段名
|
|
6
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' }
|
|
7
44
|
*/
|
|
8
45
|
export const arrToObj = (
|
|
9
46
|
arr,
|
|
10
47
|
keyField = "config_key",
|
|
11
48
|
valueField = "config_value"
|
|
12
49
|
) => {
|
|
13
|
-
// 输入验证
|
|
14
50
|
if (!Array.isArray(arr)) {
|
|
15
|
-
|
|
51
|
+
console.error("[arrToObj] 输入必须是数组");
|
|
52
|
+
return {};
|
|
16
53
|
}
|
|
17
54
|
|
|
18
55
|
return arr.reduce((result, item) => {
|
|
@@ -28,16 +65,27 @@ export const arrToObj = (
|
|
|
28
65
|
};
|
|
29
66
|
|
|
30
67
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param {
|
|
33
|
-
* @returns
|
|
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'] }
|
|
34
83
|
*/
|
|
35
84
|
export function parseJsonFields(obj) {
|
|
36
85
|
const result = {};
|
|
37
86
|
for (const key in obj) {
|
|
38
87
|
if (!obj.hasOwnProperty(key)) continue;
|
|
39
88
|
const value = obj[key];
|
|
40
|
-
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
|
41
89
|
if (
|
|
42
90
|
typeof value === "string" &&
|
|
43
91
|
(value.startsWith("{") || value.startsWith("["))
|
|
@@ -45,8 +93,8 @@ export function parseJsonFields(obj) {
|
|
|
45
93
|
try {
|
|
46
94
|
result[key] = JSON.parse(value);
|
|
47
95
|
} catch (e) {
|
|
48
|
-
console.
|
|
49
|
-
result[key] = value;
|
|
96
|
+
console.error(`JSON parse failed for field: ${key}`, e);
|
|
97
|
+
result[key] = value;
|
|
50
98
|
}
|
|
51
99
|
} else {
|
|
52
100
|
result[key] = value;
|
|
@@ -57,12 +105,35 @@ export function parseJsonFields(obj) {
|
|
|
57
105
|
}
|
|
58
106
|
|
|
59
107
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {string} [
|
|
63
|
-
* @param {string} [
|
|
64
|
-
* @param {string} [
|
|
65
|
-
* @
|
|
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
|
+
* // ]
|
|
66
137
|
*/
|
|
67
138
|
export function buildTree(
|
|
68
139
|
arr,
|
|
@@ -71,32 +142,45 @@ export function buildTree(
|
|
|
71
142
|
pidKey = "pid",
|
|
72
143
|
childrenKey = "children"
|
|
73
144
|
) {
|
|
74
|
-
|
|
75
|
-
if (!Array.isArray(arr)) return [];
|
|
145
|
+
if (!Array.isArray(arr) || arr.length === 0) return [];
|
|
76
146
|
|
|
147
|
+
const nodeMap = new Map();
|
|
77
148
|
const tree = [];
|
|
149
|
+
const visited = new Set();
|
|
78
150
|
|
|
79
|
-
for (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const children = buildTree(
|
|
85
|
-
arr.slice(i + 1),
|
|
86
|
-
item[idKey],
|
|
87
|
-
idKey,
|
|
88
|
-
pidKey,
|
|
89
|
-
childrenKey
|
|
90
|
-
);
|
|
151
|
+
for (const item of arr) {
|
|
152
|
+
if (item && typeof item === 'object') {
|
|
153
|
+
nodeMap.set(item[idKey], { ...item, [childrenKey]: [] });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
91
156
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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);
|
|
95
170
|
}
|
|
96
|
-
|
|
97
|
-
tree.push(item);
|
|
98
171
|
}
|
|
99
172
|
}
|
|
100
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
|
+
|
|
101
185
|
return tree;
|
|
102
186
|
}
|
package/helper/db.js
CHANGED
|
@@ -1,91 +1,79 @@
|
|
|
1
1
|
import knex from "knex";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
/**
|
|
4
|
+
* 创建 Knex 数据库连接实例
|
|
5
|
+
* @param {Object} config - 数据库配置对象
|
|
6
|
+
* @param {string} config.client - 数据库客户端类型(如 mysql2, pg, sqlite3)
|
|
7
|
+
* @param {Object} config.connection - 数据库连接配置
|
|
8
|
+
* @param {Object} [config.pool] - 连接池配置
|
|
9
|
+
* @returns {Knex} Knex 实例
|
|
10
|
+
* @example
|
|
11
|
+
* const db = db({
|
|
12
|
+
* client: 'mysql2',
|
|
13
|
+
* connection: {
|
|
14
|
+
* host: 'localhost',
|
|
15
|
+
* user: 'root',
|
|
16
|
+
* password: 'password',
|
|
17
|
+
* database: 'mydb'
|
|
18
|
+
* }
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
export function db(config) {
|
|
22
|
+
return knex(config);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 根据前缀从环境变量中获取数据库配置
|
|
27
|
+
* @param {string} prefix - 配置前缀(如 primary, secondary, redis)
|
|
28
|
+
* @returns {Object|null} 解析后的配置对象,如果配置不存在或解析失败则返回 null
|
|
29
|
+
* @description
|
|
30
|
+
* 支持的前缀映射:
|
|
31
|
+
* - primary -> DB_PRIMARY
|
|
32
|
+
* - secondary -> DB_SECONDARY
|
|
33
|
+
* - redis -> REDIS
|
|
34
|
+
* - 其他 -> DB_{PREFIX.toUpperCase()}
|
|
35
|
+
*
|
|
36
|
+
* 配置格式为 JSON 字符串,存储在环境变量中
|
|
37
|
+
* @example
|
|
38
|
+
* // 环境变量: DB_PRIMARY='{"client":"mysql2","connection":{"host":"localhost"}}'
|
|
39
|
+
* const config = prefixDbConfig('primary');
|
|
40
|
+
* // 返回: { client: 'mysql2', connection: { host: 'localhost' } }
|
|
41
|
+
*/
|
|
42
|
+
export function prefixDbConfig(prefix) {
|
|
43
|
+
const prefixMap = {
|
|
44
|
+
primary: "DB_PRIMARY",
|
|
45
|
+
secondary: "DB_SECONDARY",
|
|
46
|
+
redis: "REDIS",
|
|
47
|
+
};
|
|
48
|
+
const envKey = prefixMap[prefix] || `DB_${prefix.toUpperCase()}`;
|
|
49
|
+
const configStr = process.env[envKey];
|
|
50
|
+
|
|
51
|
+
if (configStr) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(configStr);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`[DB] 配置解析失败: ${envKey}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
19
58
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
database = "test",
|
|
28
|
-
port = 3306,
|
|
29
|
-
debug = true,
|
|
30
|
-
charset = "utf8mb4",
|
|
31
|
-
min = 0,
|
|
32
|
-
max = 2,
|
|
33
|
-
filename = "",
|
|
34
|
-
}) => {
|
|
35
|
-
let config = {
|
|
59
|
+
|
|
60
|
+
const client = process.env.DB_CLIENT || "mysql2";
|
|
61
|
+
if (client !== "mysql2") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
36
66
|
client,
|
|
37
67
|
connection: {
|
|
38
|
-
host,
|
|
39
|
-
port,
|
|
40
|
-
user,
|
|
41
|
-
password,
|
|
42
|
-
database
|
|
43
|
-
charset,
|
|
68
|
+
host: process.env.DB_HOST || "localhost",
|
|
69
|
+
port: parseInt(process.env.DB_PORT || "3306"),
|
|
70
|
+
user: process.env.DB_USER || "root",
|
|
71
|
+
password: process.env.DB_PASS || "123456",
|
|
72
|
+
database: process.env.DB_DATABASE || "chancms"
|
|
44
73
|
},
|
|
45
|
-
debug,
|
|
46
74
|
pool: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// 添加连接池错误处理
|
|
51
|
-
afterCreate: (conn, done) => {
|
|
52
|
-
conn.on("error", (error) => {
|
|
53
|
-
console.error(
|
|
54
|
-
`[连接池错误] 连接出错: ${getDefaultErrorMessage(error)}`,
|
|
55
|
-
error
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
done(null, conn);
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
log: {
|
|
62
|
-
warn(message) {
|
|
63
|
-
console.error("[knex warn]", message);
|
|
64
|
-
},
|
|
65
|
-
error(message) {
|
|
66
|
-
console.error("[knex error]", message);
|
|
67
|
-
},
|
|
68
|
-
debug(message) {
|
|
69
|
-
console.log("[knex debug]", message);
|
|
70
|
-
},
|
|
71
|
-
deprecate(message) {
|
|
72
|
-
console.warn("[knex deprecate]", message);
|
|
73
|
-
},
|
|
74
|
-
trace(message) {
|
|
75
|
-
console.log("[knex trace]", message);
|
|
76
|
-
},
|
|
77
|
-
log(message) {
|
|
78
|
-
console.log("[knex log]", message);
|
|
79
|
-
},
|
|
80
|
-
info(message) {
|
|
81
|
-
console.log("[knex info]", message);
|
|
82
|
-
},
|
|
83
|
-
},
|
|
75
|
+
min: parseInt(process.env.DB_POOL_MIN || "2"),
|
|
76
|
+
max: parseInt(process.env.DB_POOL_MAX || "10")
|
|
77
|
+
}
|
|
84
78
|
};
|
|
85
|
-
|
|
86
|
-
if (client === "sqlite3" || client === "better-sqlite3") {
|
|
87
|
-
config.connection.filename = filename;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return knex(config);
|
|
91
|
-
};
|
|
79
|
+
}
|