chanjs 2.0.4 → 2.0.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.
- package/global/env.js +5 -0
- package/global/global.js +23 -0
- package/global/import.js +40 -0
- package/global/index.js +3 -0
- package/helper/data-parse.js +85 -0
- package/{utils → helper}/db.js +22 -22
- package/helper/file.js +208 -0
- package/helper/html.js +19 -0
- package/helper/index.js +9 -0
- package/helper/ip.js +36 -0
- package/helper/jwt.js +41 -0
- package/{utils → helper}/loader.js +21 -24
- package/helper/time.js +28 -0
- package/helper/validate.js +2 -0
- package/index.js +40 -26
- package/middleware/index.js +3 -1
- package/middleware/waf.js +195 -0
- package/package.json +1 -1
- package/common/global.js +0 -61
- package/extend/import.js +0 -6
- package/utils/bind.js +0 -21
- package/utils/index.js +0 -12
- package/utils/response.js +0 -20
package/global/env.js
ADDED
package/global/global.js
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import { pathToFileURL, fileURLToPath } from "url";
|
2
|
+
import path from "path";
|
3
|
+
const ROOT_PATH = process.cwd();
|
4
|
+
const APP_PATH = path.join(ROOT_PATH, "app");
|
5
|
+
const globals = {
|
6
|
+
ROOT_PATH,
|
7
|
+
APP_PATH,
|
8
|
+
CONFIG_PATH: path.join(ROOT_PATH, "config"),
|
9
|
+
EXTEND_PATH: path.join(APP_PATH, "extend"),
|
10
|
+
PUBLIC_PATH: path.join(ROOT_PATH, "public"),
|
11
|
+
MODULES_PATH: path.join(APP_PATH, "modules"),
|
12
|
+
COMMON_PATH: path.join(APP_PATH, "common"),
|
13
|
+
HELPER_PATH: path.join(APP_PATH, "helper"),
|
14
|
+
};
|
15
|
+
|
16
|
+
for (const [key, value] of Object.entries(globals)) {
|
17
|
+
Object.defineProperty(global, key, {
|
18
|
+
value,
|
19
|
+
writable: false, // 禁止修改
|
20
|
+
configurable: false, // 禁止删除或重新定义
|
21
|
+
enumerable: true, // 可枚举(调试时可见)
|
22
|
+
});
|
23
|
+
}
|
package/global/import.js
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
import fs from "fs/promises";
|
3
|
+
import { pathToFileURL } from "url";
|
4
|
+
import { createRequire } from "module";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* @description 安全导入ES模块文件
|
8
|
+
* @param {string} filepath - 文件路径
|
9
|
+
* @returns {Promise<object|null>} 模块对象或null
|
10
|
+
*/
|
11
|
+
const importFile = async (filepath) => {
|
12
|
+
if (!filepath || typeof filepath !== 'string') {
|
13
|
+
console.error('错误: 文件路径必须是有效的字符串');
|
14
|
+
return null;
|
15
|
+
}
|
16
|
+
|
17
|
+
try {
|
18
|
+
await fs.access(filepath);
|
19
|
+
const fileUrl = pathToFileURL(filepath).href;
|
20
|
+
const module = await import(fileUrl);
|
21
|
+
return module.default || module;
|
22
|
+
} catch (error) {
|
23
|
+
if (error.code === 'ENOENT') {
|
24
|
+
console.error(`文件不存在: ${filepath}`);
|
25
|
+
} else if (error.code === 'EACCES') {
|
26
|
+
console.error(`没有权限访问文件: ${filepath}`);
|
27
|
+
} else {
|
28
|
+
console.error(`导入文件时出错 [${filepath}]:`, error.message);
|
29
|
+
}
|
30
|
+
return null;
|
31
|
+
}
|
32
|
+
};
|
33
|
+
|
34
|
+
/**
|
35
|
+
* @description: 兼容低版本 Node 的 CommonJS 模块加载
|
36
|
+
*/
|
37
|
+
export const importjs = createRequire(import.meta.url);
|
38
|
+
|
39
|
+
global.requirejs = importjs;
|
40
|
+
global.importFile = importFile;
|
package/global/index.js
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
/**
|
2
|
+
* 将数组转换为键值对对象
|
3
|
+
* @param {Array} arr - 包含键值对的数组
|
4
|
+
* @param {string} keyField - 作为键的字段名
|
5
|
+
* @param {string} valueField - 作为值的字段名
|
6
|
+
* @returns {Object} 转换后的对象
|
7
|
+
*/
|
8
|
+
export const arrToObj = (arr, keyField = 'config_key', valueField = 'config_value') => {
|
9
|
+
// 输入验证
|
10
|
+
if (!Array.isArray(arr)) {
|
11
|
+
throw new Error('arrToObj 期望接收数组作为第一个参数');
|
12
|
+
}
|
13
|
+
|
14
|
+
return arr.reduce((result, item) => {
|
15
|
+
if (item && typeof item === 'object') {
|
16
|
+
const key = item[keyField];
|
17
|
+
const value = item[valueField];
|
18
|
+
if (key !== undefined && value !== undefined) {
|
19
|
+
result[key] = value;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
return result;
|
23
|
+
}, {});
|
24
|
+
};
|
25
|
+
|
26
|
+
|
27
|
+
/**
|
28
|
+
* @description 将字符串中的 JSON 字符串转换为对象
|
29
|
+
* @param {*} obj - 包含 JSON 字符串的对象
|
30
|
+
* @returns
|
31
|
+
*/
|
32
|
+
export function parseJsonFields(obj) {
|
33
|
+
const result = {};
|
34
|
+
for (const key in obj) {
|
35
|
+
if (!obj.hasOwnProperty(key)) continue;
|
36
|
+
const value = obj[key];
|
37
|
+
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
38
|
+
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
|
39
|
+
try {
|
40
|
+
result[key] = JSON.parse(value);
|
41
|
+
} catch (e) {
|
42
|
+
console.warn(`JSON parse failed for field: ${key}`, e);
|
43
|
+
result[key] = value; // 保留原始值
|
44
|
+
}
|
45
|
+
} else {
|
46
|
+
result[key] = value;
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
return result;
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
/**
|
55
|
+
* @param {Array} arr - 原始数据数组
|
56
|
+
* @param {number|string} [pid=0] - 根节点父ID
|
57
|
+
* @param {string} [idKey='id'] - ID字段名
|
58
|
+
* @param {string} [pidKey='pid'] - 父ID字段名
|
59
|
+
* @param {string} [childrenKey='children'] - 子节点字段名
|
60
|
+
* @returns {Array} 树形结构数组
|
61
|
+
*/
|
62
|
+
export function buildTree(arr, pid = 0, idKey = 'id', pidKey = 'pid', childrenKey = 'children') {
|
63
|
+
// 基础参数校验
|
64
|
+
if (!Array.isArray(arr)) return [];
|
65
|
+
|
66
|
+
const tree = [];
|
67
|
+
|
68
|
+
for (let i = 0; i < arr.length; i++) {
|
69
|
+
const item = arr[i];
|
70
|
+
// 找到当前层级的节点
|
71
|
+
if (item[pidKey] === pid) {
|
72
|
+
// 递归查找子节点(通过slice创建子数组,避免重复遍历已处理项)
|
73
|
+
const children = buildTree(arr.slice(i + 1), item[idKey], idKey, pidKey, childrenKey);
|
74
|
+
|
75
|
+
// 有子节点则添加,避免空数组
|
76
|
+
if (children.length) {
|
77
|
+
item[childrenKey] = children;
|
78
|
+
}
|
79
|
+
|
80
|
+
tree.push(item);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
return tree;
|
85
|
+
}
|
package/{utils → helper}/db.js
RENAMED
@@ -1,5 +1,25 @@
|
|
1
1
|
import knex from "knex";
|
2
|
-
|
2
|
+
|
3
|
+
const errCode = {
|
4
|
+
ECONNREFUSED: "数据库连接被拒绝,请检查数据库服务是否正常运行。",
|
5
|
+
ER_ACCESS_DENIED_ERROR: "无权限访问,账号或密码错误。",
|
6
|
+
ER_ROW_IS_REFERENCED_2: "无法删除或更新记录,存在关联数据。",
|
7
|
+
ER_BAD_FIELD_ERROR: "SQL语句中包含无效字段,请检查查询条件或列名。",
|
8
|
+
ER_DUP_ENTRY: "插入失败:数据重复,违反唯一性约束。",
|
9
|
+
ER_NO_SUCH_TABLE: "操作失败:目标表不存在。",
|
10
|
+
ETIMEOUT: "数据库操作超时,请稍后再试。",
|
11
|
+
};
|
12
|
+
const getDefaultErrorMessage = (error) => {
|
13
|
+
if (error.message.includes("syntax") || error.message.includes("SQL")) {
|
14
|
+
return "数据库语法错误,请检查您的查询语句。";
|
15
|
+
} else if (error.message.includes("Connection closed")) {
|
16
|
+
return "数据库连接已关闭,请重试。";
|
17
|
+
} else if (error.message.includes("permission")) {
|
18
|
+
return "数据库权限不足,请检查配置。";
|
19
|
+
}
|
20
|
+
return "数据库发生未知错误,请稍后重试。";
|
21
|
+
};
|
22
|
+
export const db =({
|
3
23
|
client = "mysql2",
|
4
24
|
host = "127.0.0.1",
|
5
25
|
user = "root",
|
@@ -10,7 +30,7 @@ export const db = function ({
|
|
10
30
|
charset = "utf8mb4",
|
11
31
|
min = 0,
|
12
32
|
max = 2,
|
13
|
-
}) {
|
33
|
+
}) =>{
|
14
34
|
let config = {
|
15
35
|
client,
|
16
36
|
connection: {
|
@@ -53,23 +73,3 @@ export const db = function ({
|
|
53
73
|
};
|
54
74
|
return knex(config);
|
55
75
|
};
|
56
|
-
|
57
|
-
const errCode = {
|
58
|
-
ECONNREFUSED: "数据库连接被拒绝,请检查数据库服务是否正常运行。",
|
59
|
-
ER_ACCESS_DENIED_ERROR: "无权限访问,账号或密码错误。",
|
60
|
-
ER_ROW_IS_REFERENCED_2: "无法删除或更新记录,存在关联数据。",
|
61
|
-
ER_BAD_FIELD_ERROR: "SQL语句中包含无效字段,请检查查询条件或列名。",
|
62
|
-
ER_DUP_ENTRY: "插入失败:数据重复,违反唯一性约束。",
|
63
|
-
ER_NO_SUCH_TABLE: "操作失败:目标表不存在。",
|
64
|
-
ETIMEOUT: "数据库操作超时,请稍后再试。",
|
65
|
-
};
|
66
|
-
const getDefaultErrorMessage = (error) => {
|
67
|
-
if (error.message.includes("syntax") || error.message.includes("SQL")) {
|
68
|
-
return "数据库语法错误,请检查您的查询语句。";
|
69
|
-
} else if (error.message.includes("Connection closed")) {
|
70
|
-
return "数据库连接已关闭,请重试。";
|
71
|
-
} else if (error.message.includes("permission")) {
|
72
|
-
return "数据库权限不足,请检查配置。";
|
73
|
-
}
|
74
|
-
return "数据库发生未知错误,请稍后重试。";
|
75
|
-
};
|
package/helper/file.js
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
import fs from 'fs/promises';
|
2
|
+
import { accessSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
3
|
+
import path from 'path';
|
4
|
+
import { fileURLToPath } from 'url';
|
5
|
+
|
6
|
+
export const dirname = (url) =>{
|
7
|
+
const __filename = fileURLToPath(url);
|
8
|
+
return path.dirname(__filename);
|
9
|
+
}
|
10
|
+
|
11
|
+
|
12
|
+
/**
|
13
|
+
* 获取指定路径的文件树结构
|
14
|
+
*
|
15
|
+
* @param {string} basePath - 要扫描的基础路径
|
16
|
+
* @param {boolean} [deep=true] - 是否深度遍历子目录
|
17
|
+
* @returns {Promise<Array<Object>>} 文件树数组,每个元素包含文件/目录信息
|
18
|
+
* @returns {string} return[].name - 名称
|
19
|
+
* @returns {string} return[].path - 完整路径
|
20
|
+
* @returns {string} return[].relativePath - 相对于APP_PATH的路径
|
21
|
+
* @returns {'directory'|'file'} return[].type - 类型(目录或文件)
|
22
|
+
* @returns {number} return[].size - 大小(字节)
|
23
|
+
* @returns {Date} return[].modified - 最后修改时间
|
24
|
+
* @returns {number} return[].depth - 深度
|
25
|
+
* @returns {Array<Object>} [return[].children] - 子目录内容(仅目录有)
|
26
|
+
* @throws {Error} 当路径不存在或没有访问权限时抛出错误
|
27
|
+
*/
|
28
|
+
export const getFileTree = async (basePath, deep = true) => {
|
29
|
+
try {
|
30
|
+
const stats = await fs.stat(basePath);
|
31
|
+
if (!stats.isDirectory()) {
|
32
|
+
return [];
|
33
|
+
}
|
34
|
+
|
35
|
+
const items = await fs.readdir(basePath);
|
36
|
+
const tree = [];
|
37
|
+
|
38
|
+
for (const item of items) {
|
39
|
+
const itemPath = path.join(basePath, item);
|
40
|
+
const itemStats = await fs.stat(itemPath);
|
41
|
+
|
42
|
+
const treeItem = {
|
43
|
+
name: item,
|
44
|
+
path: itemPath,
|
45
|
+
relativePath: path.relative(ROOT_PATH, itemPath),
|
46
|
+
type: itemStats.isDirectory() ? 'directory' : 'file',
|
47
|
+
size: itemStats.size,
|
48
|
+
modified: itemStats.mtime,
|
49
|
+
depth: itemPath.split(path.sep).length - path.resolve(ROOT_PATH).split(path.sep).length
|
50
|
+
};
|
51
|
+
|
52
|
+
if (treeItem.type === 'directory' && deep) {
|
53
|
+
treeItem.children = await getFileTree(itemPath, deep);
|
54
|
+
}
|
55
|
+
|
56
|
+
tree.push(treeItem);
|
57
|
+
}
|
58
|
+
|
59
|
+
// 排序:文件夹在前,文件在后,名称按字母顺序排列
|
60
|
+
return tree.sort((a, b) => {
|
61
|
+
if (a.type === b.type) {
|
62
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
63
|
+
}
|
64
|
+
return a.type === 'directory' ? -1 : 1;
|
65
|
+
});
|
66
|
+
} catch (error) {
|
67
|
+
console.error(`获取文件树失败: ${basePath}`, error);
|
68
|
+
throw error;
|
69
|
+
}
|
70
|
+
};
|
71
|
+
|
72
|
+
/**
|
73
|
+
* 读取文件内容(UTF-8编码)
|
74
|
+
*
|
75
|
+
* @param {string} filePath - 要读取的文件路径
|
76
|
+
* @returns {Promise<string>} 文件内容字符串
|
77
|
+
* @throws {Error} 当文件不存在、无法读取或不是文件时抛出错误
|
78
|
+
*/
|
79
|
+
export const readFileContent = async (filePath) => {
|
80
|
+
try {
|
81
|
+
// 先检查路径是否安全
|
82
|
+
if (!isPathSafe(filePath,APP_PATH) && !isPathSafe(filePath,ROOT_PATH)) {
|
83
|
+
throw new Error(`路径不安全: ${filePath}`);
|
84
|
+
}
|
85
|
+
|
86
|
+
// 检查是否为文件
|
87
|
+
const stats = await fs.stat(filePath);
|
88
|
+
if (!stats.isFile()) {
|
89
|
+
throw new Error(`不是文件: ${filePath}`);
|
90
|
+
}
|
91
|
+
|
92
|
+
return await fs.readFile(filePath, 'utf8');
|
93
|
+
} catch (error) {
|
94
|
+
console.error(`读取文件失败: ${filePath}`, error);
|
95
|
+
throw error;
|
96
|
+
}
|
97
|
+
};
|
98
|
+
|
99
|
+
/**
|
100
|
+
* 保存内容到文件(UTF-8编码)
|
101
|
+
*
|
102
|
+
* @param {string} filePath - 要保存的文件路径
|
103
|
+
* @param {string} content - 要写入的内容
|
104
|
+
* @returns {Promise<void>} 无返回值
|
105
|
+
* @throws {Error} 当路径不安全、无法写入或发生其他错误时抛出错误
|
106
|
+
*/
|
107
|
+
export const saveFileContent = async (filePath, content) => {
|
108
|
+
try {
|
109
|
+
// 先检查路径是否安全
|
110
|
+
if (!isPathSafe(filePath,APP_PATH) && !isPathSafe(filePath,ROOT_PATH)) {
|
111
|
+
throw new Error(`路径不安全: ${filePath}`);
|
112
|
+
}
|
113
|
+
|
114
|
+
// 确保目录存在
|
115
|
+
const dirname = path.dirname(filePath);
|
116
|
+
await fs.mkdir(dirname, { recursive: true });
|
117
|
+
|
118
|
+
// 写入文件
|
119
|
+
await fs.writeFile(filePath, content, 'utf8');
|
120
|
+
} catch (error) {
|
121
|
+
console.error(`保存文件失败: ${filePath}`, error);
|
122
|
+
throw error;
|
123
|
+
}
|
124
|
+
};
|
125
|
+
|
126
|
+
/**
|
127
|
+
* 验证路径是否在基础路径范围内(防止路径遍历攻击)
|
128
|
+
*
|
129
|
+
* @param {string} requestedPath - 要验证的路径
|
130
|
+
* @param {string} basePath - 基础路径,requestedPath必须在此路径下
|
131
|
+
* @returns {boolean} 如果路径安全则返回true,否则返回false
|
132
|
+
*/
|
133
|
+
export const isPathSafe = (requestedPath, basePath) => {
|
134
|
+
const resolvedRequestedPath = path.resolve(requestedPath);
|
135
|
+
const resolvedBasePath = path.resolve(basePath);
|
136
|
+
// 检查请求的路径是否以基础路径为前缀
|
137
|
+
return resolvedRequestedPath.startsWith(resolvedBasePath);
|
138
|
+
};
|
139
|
+
|
140
|
+
/**
|
141
|
+
* 删除指定路径的图片文件
|
142
|
+
*
|
143
|
+
* @param {string} link - 图片文件的路径
|
144
|
+
* @returns {boolean} 成功删除返回true,否则返回false
|
145
|
+
* @description 同步操作,会检查文件是否存在后再删除
|
146
|
+
*/
|
147
|
+
export function delImg(link) {
|
148
|
+
try {
|
149
|
+
// 先检查路径是否安全
|
150
|
+
if (!isPathSafe(link, APP_PATH)) {
|
151
|
+
console.error(`路径不安全: ${link}`);
|
152
|
+
return false;
|
153
|
+
}
|
154
|
+
|
155
|
+
// 检查文件是否存在
|
156
|
+
accessSync(link);
|
157
|
+
|
158
|
+
// 删除文件
|
159
|
+
unlinkSync(link);
|
160
|
+
return true;
|
161
|
+
} catch (err) {
|
162
|
+
console.error(`删除图片失败: ${link}`, err);
|
163
|
+
return false;
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
/**
|
168
|
+
* 递归创建目录(同步操作)
|
169
|
+
*
|
170
|
+
* @param {string} dirname - 要创建的目录路径
|
171
|
+
* @returns {boolean} 成功创建或目录已存在返回true,否则返回false
|
172
|
+
* @description 类似于mkdir -p命令,会创建所有不存在的父目录
|
173
|
+
*/
|
174
|
+
export function mkdirsSync(dirname) {
|
175
|
+
try {
|
176
|
+
// 先检查路径是否安全
|
177
|
+
if (!isPathSafe(dirname, APP_PATH)) {
|
178
|
+
console.error(`路径不安全: ${dirname}`);
|
179
|
+
return false;
|
180
|
+
}
|
181
|
+
|
182
|
+
if (existsSync(dirname)) {
|
183
|
+
return true;
|
184
|
+
}
|
185
|
+
|
186
|
+
// 递归创建父目录
|
187
|
+
const parentDir = path.dirname(dirname);
|
188
|
+
if (mkdirsSync(parentDir)) {
|
189
|
+
mkdirSync(dirname);
|
190
|
+
return true;
|
191
|
+
}
|
192
|
+
|
193
|
+
return false;
|
194
|
+
} catch (err) {
|
195
|
+
console.error(`创建目录失败: ${dirname}`, err);
|
196
|
+
return false;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
|
201
|
+
export default {
|
202
|
+
mkdirsSync,
|
203
|
+
delImg,
|
204
|
+
isPathSafe,
|
205
|
+
saveFileContent,
|
206
|
+
readFileContent,
|
207
|
+
getFileTree,
|
208
|
+
}
|
package/helper/html.js
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
/**
|
2
|
+
* 处理最常用的HTML特殊字符实体
|
3
|
+
* @param {string} str - 需要解码的字符串
|
4
|
+
* @returns {string} 解码后的字符串
|
5
|
+
*/
|
6
|
+
export const htmlDecode = (str) => {
|
7
|
+
// 非字符串直接返回,避免报错
|
8
|
+
if (typeof str !== 'string') return str;
|
9
|
+
|
10
|
+
// 一次性替换所有常见实体,减少函数调用
|
11
|
+
return str.replace(/&|<|>| |'|"/g, match => ({
|
12
|
+
'&': '&',
|
13
|
+
'<': '<',
|
14
|
+
'>': '>',
|
15
|
+
' ': ' ',
|
16
|
+
''': "'",
|
17
|
+
'"': '"'
|
18
|
+
}[match]));
|
19
|
+
}
|
package/helper/index.js
ADDED
package/helper/ip.js
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
/**
|
2
|
+
* 获取用户登录IP地址
|
3
|
+
* @param {Object} req - Express请求对象
|
4
|
+
* @returns {string} 格式化后的IP地址,默认返回空字符串
|
5
|
+
*/
|
6
|
+
export const getIp = (req) =>{
|
7
|
+
|
8
|
+
// 优先级从高到低获取可能的IP来源
|
9
|
+
const ipSources = [
|
10
|
+
req.headers['x-forwarded-for'], // 代理场景下的真实IP(可能多个,逗号分隔)
|
11
|
+
req.headers['x-real-ip'], // 部分代理服务器使用
|
12
|
+
req.ip, // Express内置的IP获取(已处理代理)
|
13
|
+
req.connection?.remoteAddress, // 底层连接的远程地址
|
14
|
+
req.socket?.remoteAddress, // 套接字的远程地址
|
15
|
+
req.connection?.socket?.remoteAddress // 连接套接字的远程地址
|
16
|
+
];
|
17
|
+
|
18
|
+
// 从来源中找到第一个有效IP字符串
|
19
|
+
let ip = ipSources.find(source => typeof source === 'string' && source.trim() !== '');
|
20
|
+
|
21
|
+
// 若未找到有效IP,直接返回空
|
22
|
+
if (!ip) return '';
|
23
|
+
|
24
|
+
// 处理多IP情况(取第一个)
|
25
|
+
ip = ip.split(',').shift().trim();
|
26
|
+
|
27
|
+
// IPv6转IPv4处理
|
28
|
+
if (ip === '::1') {
|
29
|
+
return '127.0.0.1'; // 本地环回地址
|
30
|
+
}
|
31
|
+
if (ip.startsWith('::ffff:')) {
|
32
|
+
return ip.slice(7); // 去除IPv6映射的IPv4前缀
|
33
|
+
}
|
34
|
+
|
35
|
+
return ip;
|
36
|
+
}
|
package/helper/jwt.js
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import jwt from "jsonwebtoken";
|
2
|
+
|
3
|
+
|
4
|
+
/**
|
5
|
+
*
|
6
|
+
* @param {Object} data - 要存储在令牌中的数据(不建议存放敏感信息)
|
7
|
+
* @param {string} secretKey - 用于签名的密钥
|
8
|
+
* @param {string} time - 令牌过期时间,如 '1h'、'7d' 或秒数
|
9
|
+
* @returns {string} 生成的JWT令牌
|
10
|
+
*/
|
11
|
+
export function setToken(data={}, secretKey='chancms', time='7d') {
|
12
|
+
try {
|
13
|
+
return jwt.sign(data, secretKey, {
|
14
|
+
expiresIn: time,
|
15
|
+
algorithm: 'HS256',
|
16
|
+
});
|
17
|
+
} catch (error) {
|
18
|
+
console.error('令牌生成失败:', error.message);
|
19
|
+
throw new Error(`生成令牌失败: ${error.message}`);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
// 获取token
|
24
|
+
export async function getToken(token, secretKey='chancms') {
|
25
|
+
return new Promise((resolve, reject) => {
|
26
|
+
jwt.verify(token, secretKey,(err, decode) => {
|
27
|
+
if (err) {
|
28
|
+
let errorMessage = '令牌验证失败';
|
29
|
+
if (err.name === 'TokenExpiredError') {
|
30
|
+
errorMessage = '令牌已过期';
|
31
|
+
} else if (err.name === 'JsonWebTokenError') {
|
32
|
+
errorMessage = '无效的令牌';
|
33
|
+
}
|
34
|
+
console.error(errorMessage, '令牌异常信息:', err.message);
|
35
|
+
return reject(new Error(errorMessage));
|
36
|
+
} else {
|
37
|
+
resolve(decode);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
});
|
41
|
+
}
|
@@ -1,32 +1,30 @@
|
|
1
|
-
import fs from
|
2
|
-
import path from
|
3
|
-
import { bindClass } from './bind.js';
|
1
|
+
import fs from "fs";
|
2
|
+
import path from "path";
|
4
3
|
|
5
4
|
/**
|
6
|
-
*
|
5
|
+
*
|
7
6
|
* @param {*} module 模块目录
|
8
|
-
* @returns Array
|
7
|
+
* @returns Array
|
9
8
|
* @description 将web模块放到最后加载
|
10
9
|
*/
|
11
|
-
export const loaderSort = (modules=[])=>{
|
12
|
-
const index = modules.indexOf(
|
10
|
+
export const loaderSort = (modules = []) => {
|
11
|
+
const index = modules.indexOf("web");
|
13
12
|
if (index !== -1) {
|
14
|
-
|
15
|
-
|
13
|
+
const web = modules.splice(index, 1);
|
14
|
+
modules.push(web[0]);
|
16
15
|
}
|
17
16
|
return modules;
|
18
|
-
}
|
17
|
+
};
|
19
18
|
|
20
|
-
export const getPackage = async function(){
|
21
|
-
let pkg = await importFile(
|
19
|
+
export const getPackage = async function () {
|
20
|
+
let pkg = await importFile("package.json");
|
22
21
|
return pkg;
|
23
|
-
}
|
22
|
+
};
|
24
23
|
|
25
|
-
export const loadConfig = async function(){
|
26
|
-
let config = await importFile(
|
24
|
+
export const loadConfig = async function () {
|
25
|
+
let config = await importFile("config/index.js");
|
27
26
|
return config;
|
28
|
-
}
|
29
|
-
|
27
|
+
};
|
30
28
|
|
31
29
|
/**
|
32
30
|
* 加载指定模块名下的所有控制器文件
|
@@ -36,22 +34,24 @@ export const loadConfig = async function(){
|
|
36
34
|
export const loadController = async function (moduleName) {
|
37
35
|
const controller = {};
|
38
36
|
|
39
|
-
const dir = path.join(MODULES_PATH, moduleName,
|
37
|
+
const dir = path.join(MODULES_PATH, moduleName, "controller");
|
40
38
|
|
41
39
|
if (!fs.existsSync(dir)) {
|
42
40
|
console.warn(`模块路径不存在,跳过加载控制器: ${dir}`);
|
43
41
|
return controller;
|
44
42
|
}
|
45
43
|
|
46
|
-
const files = fs.readdirSync(dir).filter(file => file.endsWith(
|
44
|
+
const files = fs.readdirSync(dir).filter((file) => file.endsWith(".js"));
|
47
45
|
|
48
46
|
for (const file of files) {
|
49
47
|
const filePath = path.join(dir, file);
|
50
|
-
const name = file.replace(/\.js$/i,
|
48
|
+
const name = file.replace(/\.js$/i, ""); // 安全处理 .js 后缀
|
51
49
|
|
52
50
|
try {
|
53
51
|
const module = await importFile(filePath);
|
54
|
-
|
52
|
+
let obj = module.default || module;
|
53
|
+
|
54
|
+
controller[name] = obj;
|
55
55
|
} catch (e) {
|
56
56
|
console.error(`加载控制器失败: ${filePath}`, e);
|
57
57
|
// 可选:抛出错误或继续加载其他文件
|
@@ -61,6 +61,3 @@ export const loadController = async function (moduleName) {
|
|
61
61
|
|
62
62
|
return controller;
|
63
63
|
};
|
64
|
-
|
65
|
-
|
66
|
-
|
package/helper/time.js
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import dayjs from "dayjs";
|
2
|
+
import "dayjs/locale/zh-cn.js";
|
3
|
+
import relativeTime from "dayjs/plugin/relativeTime.js";
|
4
|
+
|
5
|
+
dayjs.extend(relativeTime);
|
6
|
+
dayjs.locale("zh-cn");
|
7
|
+
|
8
|
+
/**
|
9
|
+
* @description 格式化时间
|
10
|
+
* @param {Array} data 数组
|
11
|
+
* @param {Boolean} time 是否开启具体时间
|
12
|
+
* @param {String} format YYYY-MM-DD HH:mm:ss
|
13
|
+
* @returns 返回处理过的数组
|
14
|
+
*/
|
15
|
+
export const formatDay = (data, time = true, format = "YYYY-MM-DD") => {
|
16
|
+
data.forEach((item) => {
|
17
|
+
if (item.createdAt) {
|
18
|
+
item.createdAt = time
|
19
|
+
? dayjs(item.createdAt).format(format)
|
20
|
+
: dayjs(item.createdAt).fromNow().replace(" ", "");
|
21
|
+
}
|
22
|
+
});
|
23
|
+
return data;
|
24
|
+
}
|
25
|
+
|
26
|
+
export const formatTime = (data, format = "YYYY-MM-DD HH:mm:ss") => {
|
27
|
+
return dayjs(data).format("YYYY-MM-DD HH:mm:ss");
|
28
|
+
}
|
package/index.js
CHANGED
@@ -1,15 +1,8 @@
|
|
1
|
-
import "./
|
1
|
+
import "./global/index.js";
|
2
2
|
import path from "path";
|
3
3
|
import fs from "fs";
|
4
|
-
import
|
5
|
-
|
6
|
-
db,
|
7
|
-
loaderSort,
|
8
|
-
getPackage,
|
9
|
-
loadConfig,
|
10
|
-
loadController,
|
11
|
-
} from "./utils/index.js";
|
12
|
-
import { express, z } from "./extend/import.js";
|
4
|
+
import express from "express";
|
5
|
+
|
13
6
|
import { Controller, Service } from "./core/index.js";
|
14
7
|
import {
|
15
8
|
log,
|
@@ -21,17 +14,16 @@ import {
|
|
21
14
|
setTemplate,
|
22
15
|
Cors,
|
23
16
|
validator,
|
17
|
+
waf,
|
24
18
|
} from "./middleware/index.js";
|
19
|
+
import { db, loaderSort, loadConfig } from "./helper/index.js";
|
20
|
+
import {dirname} from "./helper/file.js";
|
25
21
|
|
26
22
|
class Chan {
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
db,
|
32
|
-
z,
|
33
|
-
validator,
|
34
|
-
};
|
23
|
+
//版本号
|
24
|
+
#version = "0.0.0";
|
25
|
+
static helper = {};
|
26
|
+
static common = {}; //公共方法
|
35
27
|
static config = {}; //配置
|
36
28
|
static Service = Service; //服务
|
37
29
|
static Controller = Controller; //控制器
|
@@ -78,10 +70,12 @@ class Chan {
|
|
78
70
|
setFavicon(this.app);
|
79
71
|
setCookie(this.app, cookieKey);
|
80
72
|
setBody(this.app, BODY_LIMIT);
|
73
|
+
waf(this.app);
|
81
74
|
setTemplate(this.app, { views, env });
|
82
75
|
setStatic(this.app, statics);
|
83
76
|
Cors(this.app, cors);
|
84
77
|
setHeader(this.app, { APP_NAME, APP_VERSION });
|
78
|
+
|
85
79
|
}
|
86
80
|
|
87
81
|
//数据库操作
|
@@ -102,7 +96,7 @@ class Chan {
|
|
102
96
|
if (fs.existsSync(configPath)) {
|
103
97
|
const dirs = loaderSort(Chan.config.modules);
|
104
98
|
for (const item of dirs) {
|
105
|
-
let router = await
|
99
|
+
let router = await importFile(`app/modules/${item}/router.js`);
|
106
100
|
router(this.app, this.router, Chan.config);
|
107
101
|
}
|
108
102
|
}
|
@@ -111,7 +105,7 @@ class Chan {
|
|
111
105
|
//通用路由,加载错误处理和500路由和爬虫处理
|
112
106
|
async loadCommonRouter() {
|
113
107
|
try {
|
114
|
-
let router = await
|
108
|
+
let router = await importFile("app/router.js");
|
115
109
|
router(this.app, this.router, Chan.config);
|
116
110
|
} catch (error) {
|
117
111
|
console.log(error);
|
@@ -119,14 +113,34 @@ class Chan {
|
|
119
113
|
}
|
120
114
|
|
121
115
|
async loadExtend() {
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
116
|
+
let arr = [
|
117
|
+
{
|
118
|
+
_path: COMMON_PATH,
|
119
|
+
key: "common",
|
120
|
+
},
|
121
|
+
{
|
122
|
+
_path: HELPER_PATH,
|
123
|
+
key: "helper",
|
124
|
+
},
|
125
|
+
{
|
126
|
+
_path: path.join(dirname(import.meta.url), "helper"),
|
127
|
+
key: "helper",
|
128
|
+
},
|
129
|
+
];
|
130
|
+
for (let item of arr) {
|
131
|
+
await this.loadFn(item._path, item.key);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
async loadFn(_path, key) {
|
136
|
+
if (fs.existsSync(_path)) {
|
137
|
+
const files = fs.readdirSync(_path).filter((file) => file.endsWith(".js"));
|
126
138
|
for (const file of files) {
|
127
|
-
const filePath = path.join(
|
139
|
+
const filePath = path.join(_path, file);
|
128
140
|
let helperModule = await importFile(filePath);
|
129
|
-
Chan.
|
141
|
+
// Chan.common[file.replace(".js", "")] = helperModule;
|
142
|
+
// 将模块导出的所有方法/属性,直接混入到 Chan.common 上
|
143
|
+
Object.assign(Chan[key], helperModule);
|
130
144
|
}
|
131
145
|
}
|
132
146
|
}
|
package/middleware/index.js
CHANGED
@@ -7,6 +7,7 @@ import {setHeader} from "./header.js";
|
|
7
7
|
import {setTemplate} from "./template.js";
|
8
8
|
import {validator} from "./validator.js";
|
9
9
|
import {Cors} from "./cors.js";
|
10
|
+
import {waf} from "./waf.js";
|
10
11
|
|
11
12
|
export {
|
12
13
|
log,
|
@@ -17,5 +18,6 @@ export {
|
|
17
18
|
setHeader,
|
18
19
|
setTemplate,
|
19
20
|
Cors,
|
20
|
-
validator
|
21
|
+
validator,
|
22
|
+
waf
|
21
23
|
}
|
@@ -0,0 +1,195 @@
|
|
1
|
+
import url from "url";
|
2
|
+
import {getIp} from "../helper/ip.js";
|
3
|
+
|
4
|
+
// 原始关键词列表(保持不变)
|
5
|
+
const keywords = [
|
6
|
+
".aspx",
|
7
|
+
".php",
|
8
|
+
".pl",
|
9
|
+
".jsa",
|
10
|
+
".jsp",
|
11
|
+
".asp",
|
12
|
+
".go",
|
13
|
+
".jhtml",
|
14
|
+
".shtml",
|
15
|
+
".cfm",
|
16
|
+
".cgi",
|
17
|
+
".svn",
|
18
|
+
".env",
|
19
|
+
".keys",
|
20
|
+
".cache",
|
21
|
+
".hidden",
|
22
|
+
".bod",
|
23
|
+
".ll",
|
24
|
+
".backup",
|
25
|
+
".json",
|
26
|
+
".xml",
|
27
|
+
".bak",
|
28
|
+
".aws",
|
29
|
+
".database",
|
30
|
+
// ".cookie",
|
31
|
+
".location",
|
32
|
+
".dump",
|
33
|
+
".ftp",
|
34
|
+
".idea",
|
35
|
+
".s3",
|
36
|
+
".sh",
|
37
|
+
".old",
|
38
|
+
".tf",
|
39
|
+
".sql",
|
40
|
+
".vscode",
|
41
|
+
".docker",
|
42
|
+
".map",
|
43
|
+
"1+1",
|
44
|
+
".save",
|
45
|
+
".gz",
|
46
|
+
".yml",
|
47
|
+
".tar",
|
48
|
+
".rar",
|
49
|
+
".7z",
|
50
|
+
".zip",
|
51
|
+
".git",
|
52
|
+
".log",
|
53
|
+
".local",
|
54
|
+
"../",
|
55
|
+
"db_",
|
56
|
+
"smtp",
|
57
|
+
"meta",
|
58
|
+
"debug",
|
59
|
+
"secret",
|
60
|
+
"/xampp/",
|
61
|
+
"/metadata/",
|
62
|
+
"/internal/",
|
63
|
+
"/aws/",
|
64
|
+
"/debug/",
|
65
|
+
"/configs/",
|
66
|
+
"/cgi-bin/",
|
67
|
+
"/tmp/",
|
68
|
+
"/staging/",
|
69
|
+
"/mail/",
|
70
|
+
"/docker/",
|
71
|
+
"/.secure/",
|
72
|
+
"/php-cgi/",
|
73
|
+
"/wp-",
|
74
|
+
"/backup/",
|
75
|
+
"redirect",
|
76
|
+
"/phpMyAdmin/",
|
77
|
+
"/setup/",
|
78
|
+
"concat(",
|
79
|
+
"version(",
|
80
|
+
"sleep(",
|
81
|
+
"benchmark(",
|
82
|
+
"0x7e",
|
83
|
+
"extractvalue(",
|
84
|
+
"(select",
|
85
|
+
"a%",
|
86
|
+
"union",
|
87
|
+
"drop",
|
88
|
+
"alter",
|
89
|
+
"truncate",
|
90
|
+
"exec",
|
91
|
+
];
|
92
|
+
|
93
|
+
// 预处理:合并字符串关键词和正则关键词为两个单一正则(核心优化)
|
94
|
+
const { combinedStrRegex, combinedRegRegex } = (() => {
|
95
|
+
const regexSpecialChars = /[.*+?^${}()|[\]\\]/g;
|
96
|
+
const strKwParts = []; // 存储无特殊字符的关键词(用于合并正则)
|
97
|
+
const regKwParts = []; // 存储转义后的正则关键词(用于合并正则)
|
98
|
+
|
99
|
+
keywords.forEach((keyword) => {
|
100
|
+
if (regexSpecialChars.test(keyword)) {
|
101
|
+
// 含正则特殊字符:转义后加入正则关键词部分
|
102
|
+
const escaped = keyword.replace(regexSpecialChars, "\\$&");
|
103
|
+
regKwParts.push(escaped);
|
104
|
+
} else {
|
105
|
+
// 无特殊字符:直接加入字符串关键词部分
|
106
|
+
strKwParts.push(keyword);
|
107
|
+
}
|
108
|
+
});
|
109
|
+
|
110
|
+
// 构建合并后的正则(空数组时返回匹配失败的正则,避免报错)
|
111
|
+
const buildCombinedRegex = (parts) => {
|
112
|
+
return parts.length
|
113
|
+
? new RegExp(`(?:${parts.join("|")})`, "i") // 非捕获组+不区分大小写
|
114
|
+
: new RegExp("^$"); // 匹配空字符串(永远不命中)
|
115
|
+
};
|
116
|
+
|
117
|
+
return {
|
118
|
+
combinedStrRegex: buildCombinedRegex(strKwParts),
|
119
|
+
combinedRegRegex: buildCombinedRegex(regKwParts),
|
120
|
+
};
|
121
|
+
})();
|
122
|
+
|
123
|
+
const safe = (req, res, next) => {
|
124
|
+
try {
|
125
|
+
// 1. 设置安全头(保持不变)
|
126
|
+
// res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
127
|
+
// res.setHeader("X-Content-Type-Options", "nosniff");
|
128
|
+
// res.setHeader("Referrer-Policy", "no-referrer-when-downgrade");
|
129
|
+
// res.removeHeader("Server");
|
130
|
+
|
131
|
+
// 2. 构建检查文本:req.path(仅路径)+ query(非空才加)(优化冗余)
|
132
|
+
let checkText = req.path || "";
|
133
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
134
|
+
const queryStr = Object.entries(req.query)
|
135
|
+
.map(([k, v]) => `${k}=${v}`)
|
136
|
+
.join(' ');
|
137
|
+
checkText += ` ${queryStr}`;
|
138
|
+
}
|
139
|
+
|
140
|
+
// 3. 处理请求体(优化序列化逻辑)
|
141
|
+
let bodyText = "";
|
142
|
+
const contentType = req.headers["content-type"] || "";
|
143
|
+
const isMultipart = contentType.includes("multipart/form-data");
|
144
|
+
|
145
|
+
if (!isMultipart && req.body) {
|
146
|
+
try {
|
147
|
+
// 若已是字符串直接用,否则序列化(避免重复序列化)
|
148
|
+
const bodyStr =
|
149
|
+
typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
150
|
+
|
151
|
+
// 限制大小(保持原逻辑,避免大文本开销)
|
152
|
+
if (bodyStr.length < 10000) {
|
153
|
+
bodyText = ` ${bodyStr}`;
|
154
|
+
}
|
155
|
+
} catch (e) {
|
156
|
+
// 忽略序列化错误
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
// 合并完整文本(无需 toLowerCase)
|
161
|
+
const fullText = checkText + bodyText;
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
// 4. 高效匹配:两次正则 test 替代 N 次循环(核心优化)
|
166
|
+
let foundMatch = false;
|
167
|
+
// 先检查字符串关键词合并正则(更快,因为无复杂正则逻辑)
|
168
|
+
if (combinedStrRegex.test(fullText)) {
|
169
|
+
foundMatch = true;
|
170
|
+
}
|
171
|
+
// 再检查正则关键词合并正则(仅当字符串匹配未命中时)
|
172
|
+
else if (combinedRegRegex.test(fullText)) {
|
173
|
+
foundMatch = true;
|
174
|
+
}
|
175
|
+
|
176
|
+
if (foundMatch) {
|
177
|
+
console.error("[安全拦截] 疑似恶意请求:", {
|
178
|
+
url: req.url,
|
179
|
+
ip: getIp(req),
|
180
|
+
userAgent: req.get("User-Agent") || "",
|
181
|
+
});
|
182
|
+
|
183
|
+
return res.status(403).send("非法风险请求,已拦截");
|
184
|
+
}
|
185
|
+
|
186
|
+
next();
|
187
|
+
} catch (error) {
|
188
|
+
console.error("[安全中间件异常]", error);
|
189
|
+
res.status(500).send("服务器内部错误");
|
190
|
+
}
|
191
|
+
};
|
192
|
+
|
193
|
+
export const waf = (app) => {
|
194
|
+
app.use(safe);
|
195
|
+
};
|
package/package.json
CHANGED
package/common/global.js
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
|
2
|
-
import path from "path";
|
3
|
-
import fs from "fs";
|
4
|
-
import { pathToFileURL } from 'url'; // 新增顶部导入
|
5
|
-
import dotenv from "dotenv";
|
6
|
-
|
7
|
-
const ROOT_PATH = process.cwd();
|
8
|
-
const APP_PATH = path.join(ROOT_PATH, "app");
|
9
|
-
const CONFIG_PATH = path.join(APP_PATH, "config");
|
10
|
-
const EXTEND_PATH = path.join(APP_PATH, "extend");
|
11
|
-
const PUBLIC_PATH = path.join(APP_PATH, "public");
|
12
|
-
const MODULES_PATH = path.join(APP_PATH, "modules");
|
13
|
-
// 兼容低版本node common包
|
14
|
-
import { createRequire } from "module";
|
15
|
-
const requirejs = createRequire(import.meta.url);
|
16
|
-
|
17
|
-
//let user = getFilePath('app/controller/user.js')
|
18
|
-
|
19
|
-
//实现dirname
|
20
|
-
global.__dirname = path.dirname(new URL(import.meta.url).pathname);
|
21
|
-
//实现__filename
|
22
|
-
global.__filename = new URL(import.meta.url).pathname;
|
23
|
-
|
24
|
-
//加载环境变量
|
25
|
-
const envFile = process.env.ENV_FILE || '.env.prd'
|
26
|
-
dotenv.config({ path: path.join(ROOT_PATH, envFile) })
|
27
|
-
|
28
|
-
// app
|
29
|
-
global.APP_PATH = APP_PATH;
|
30
|
-
// config
|
31
|
-
global.CONFIG_PATH = CONFIG_PATH;
|
32
|
-
// run root path
|
33
|
-
global.ROOT_PATH = ROOT_PATH;
|
34
|
-
// extend
|
35
|
-
global.EXTEND_PATH = EXTEND_PATH;
|
36
|
-
// public
|
37
|
-
global.PUBLIC_PATH = PUBLIC_PATH;
|
38
|
-
// modules
|
39
|
-
global.MODULES_PATH = MODULES_PATH;
|
40
|
-
// require 兼容低版本node common包
|
41
|
-
global.requirejs = requirejs;
|
42
|
-
//解决多重...问题
|
43
|
-
const importRootFile = async (str) => {
|
44
|
-
let filepath = path.join(global.ROOT_PATH, str);
|
45
|
-
if (fs.existsSync(filepath)) {
|
46
|
-
const fileUrl = pathToFileURL(filepath).href; // 新增转换
|
47
|
-
const module = await import(fileUrl);
|
48
|
-
return module.default || module;
|
49
|
-
}
|
50
|
-
};
|
51
|
-
|
52
|
-
const importFile = async (filepath) => {
|
53
|
-
if (fs.existsSync(filepath)) {
|
54
|
-
const fileUrl = pathToFileURL(filepath).href; // 新增转换
|
55
|
-
const module = await import(fileUrl);
|
56
|
-
return module.default || module;
|
57
|
-
}
|
58
|
-
};
|
59
|
-
|
60
|
-
global.importFile = importFile;
|
61
|
-
global.importRootFile = importRootFile;
|
package/extend/import.js
DELETED
package/utils/bind.js
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
|
2
|
-
/**
|
3
|
-
* @description 实例化一个类,并将该类的所有方法绑定到一个新的对象上。
|
4
|
-
* @param {Function} className - 需要实例化的类。
|
5
|
-
*@returns {Object} 包含绑定方法的对象。
|
6
|
-
*/
|
7
|
-
export const bindClass = function(className) {
|
8
|
-
let obj = {};
|
9
|
-
const cls = new className();
|
10
|
-
Object.getOwnPropertyNames(cls.constructor.prototype).forEach(
|
11
|
-
(methodName) => {
|
12
|
-
if (
|
13
|
-
methodName !== "constructor" &&
|
14
|
-
typeof cls[methodName] === "function"
|
15
|
-
) {
|
16
|
-
obj[methodName] = cls[methodName].bind(cls);
|
17
|
-
}
|
18
|
-
}
|
19
|
-
);
|
20
|
-
return obj;
|
21
|
-
}
|
package/utils/index.js
DELETED
package/utils/response.js
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
export function parseJsonFields(obj) {
|
2
|
-
const result = {};
|
3
|
-
for (const key in obj) {
|
4
|
-
if (!obj.hasOwnProperty(key)) continue;
|
5
|
-
const value = obj[key];
|
6
|
-
// 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
|
7
|
-
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
|
8
|
-
try {
|
9
|
-
result[key] = JSON.parse(value);
|
10
|
-
} catch (e) {
|
11
|
-
console.warn(`JSON parse failed for field: ${key}`, e);
|
12
|
-
result[key] = value; // 保留原始值
|
13
|
-
}
|
14
|
-
} else {
|
15
|
-
result[key] = value;
|
16
|
-
}
|
17
|
-
}
|
18
|
-
|
19
|
-
return result;
|
20
|
-
}
|