chanjs 2.0.16 → 2.0.18

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/config/code.js CHANGED
@@ -1,4 +1,6 @@
1
- // 简化的状态码定义(数字作为键)
1
+ /**
2
+ * @description 状态码定义
3
+ */
2
4
  export const CODE = {
3
5
  200: "操作成功", // 成功
4
6
  201: "操作失败", // 通用失败
@@ -36,7 +38,9 @@ export const CODE = {
36
38
  6009: "数据库连接已关闭,请重试", // 连接已关闭
37
39
  };
38
40
 
39
- // 数据库错误码映射(仅映射状态码)
41
+ /**
42
+ * @description 数据库错误码定义
43
+ */
40
44
  export const DB_ERROR = {
41
45
  ECONNREFUSED: 6001,
42
46
  ER_ACCESS_DENIED_ERROR: 6002,
package/core/service.js CHANGED
@@ -1,7 +1,25 @@
1
1
  import { success, fail, error } from "../utils/response.js";
2
2
  import { CODE } from "../config/code.js";
3
- // ====================== 共享数据库方法 ======================
4
- const databaseMethods = {
3
+
4
+ /**
5
+ * 数据库服务类
6
+ */
7
+ class Service {
8
+ /**
9
+ * 构造函数
10
+ * @param {Object} knex - Knex实例
11
+ * @param {string} model - 表名/模型名
12
+ */
13
+ constructor(knex, model) {
14
+ if (!knex || !model) {
15
+ throw new Error("Service: knex instance and model name are required");
16
+ }
17
+
18
+ this.knex = knex;
19
+ this.model = model;
20
+ this.pageSize = 100;
21
+ }
22
+
5
23
  /**
6
24
  * 查询表所有记录(慎用)
7
25
  * @param {Object} query - 查询条件
@@ -18,7 +36,7 @@ const databaseMethods = {
18
36
  } catch (err) {
19
37
  return error(err);
20
38
  }
21
- },
39
+ }
22
40
 
23
41
  /**
24
42
  * 获取单个记录
@@ -36,7 +54,7 @@ const databaseMethods = {
36
54
  } catch (err) {
37
55
  return error(err);
38
56
  }
39
- },
57
+ }
40
58
 
41
59
  /**
42
60
  * 根据ID查询记录
@@ -51,13 +69,13 @@ const databaseMethods = {
51
69
  if (field.length > 0) dataQuery = dataQuery.select(field);
52
70
  if (len === 1) dataQuery = dataQuery.first();
53
71
  else if (len > 1) dataQuery = dataQuery.limit(len);
54
-
72
+
55
73
  const res = await dataQuery;
56
74
  return success(res || (len === 1 ? {} : []));
57
75
  } catch (err) {
58
76
  return error(err);
59
77
  }
60
- },
78
+ }
61
79
 
62
80
  /**
63
81
  * 创建新记录
@@ -66,18 +84,15 @@ const databaseMethods = {
66
84
  */
67
85
  async insert(data = {}) {
68
86
  try {
69
- console.log('data--->',data)
70
87
  if (Object.keys(data).length === 0) {
71
88
  return fail(CODE[2002], { code: 2002 });
72
89
  }
73
-
74
- console.log('this.model--->',this.model)
75
90
  const result = await this.knex(this.model).insert(data);
76
91
  return success(result?.length > 0 || !!result);
77
92
  } catch (err) {
78
93
  return error(err);
79
94
  }
80
- },
95
+ }
81
96
 
82
97
  /**
83
98
  * 插入多条记录
@@ -94,7 +109,7 @@ const databaseMethods = {
94
109
  } catch (err) {
95
110
  return error(err);
96
111
  }
97
- },
112
+ }
98
113
 
99
114
  /**
100
115
  * 根据指定条件删除记录
@@ -111,7 +126,7 @@ const databaseMethods = {
111
126
  } catch (err) {
112
127
  return error(err);
113
128
  }
114
- },
129
+ }
115
130
 
116
131
  /**
117
132
  * 根据指定条件更新记录
@@ -125,17 +140,12 @@ const databaseMethods = {
125
140
  if (!query || !params || Object.keys(query).length === 0) {
126
141
  return fail(CODE[2001], { code: 2001 });
127
142
  }
128
- console.log('query--->',query)
129
- console.log('this.model--->',this.model)
130
-
131
- console.log('params---->',params)
132
143
  const result = await this.knex(this.model).where(query).update(params);
133
- console.log('result--->',result)
134
144
  return success(!!result);
135
145
  } catch (err) {
136
146
  return error(err);
137
147
  }
138
- },
148
+ }
139
149
 
140
150
  /**
141
151
  * 批量更新多条记录
@@ -146,7 +156,7 @@ const databaseMethods = {
146
156
  if (!Array.isArray(updates) || updates.length === 0) {
147
157
  return fail(CODE[2002], { code: 2002 });
148
158
  }
149
-
159
+
150
160
  const trx = await this.knex.transaction();
151
161
  try {
152
162
  for (const { query, params } of updates) {
@@ -162,7 +172,7 @@ const databaseMethods = {
162
172
  await trx.rollback();
163
173
  return error(err);
164
174
  }
165
- },
175
+ }
166
176
 
167
177
  /**
168
178
  * 分页查询
@@ -178,7 +188,7 @@ const databaseMethods = {
178
188
  const offset = (current - 1) * pageSize;
179
189
  let countQuery = this.knex(this.model).count("* as total");
180
190
  let dataQuery = this.knex(this.model);
181
-
191
+
182
192
  if (Object.keys(query).length > 0) {
183
193
  Object.entries(query).forEach(([key, value]) => {
184
194
  countQuery = countQuery.where(key, value);
@@ -190,7 +200,7 @@ const databaseMethods = {
190
200
 
191
201
  const [totalResult, list] = await Promise.all([
192
202
  countQuery.first(),
193
- dataQuery.offset(offset).limit(pageSize)
203
+ dataQuery.offset(offset).limit(pageSize),
194
204
  ]);
195
205
 
196
206
  const total = totalResult?.total || 0;
@@ -198,7 +208,7 @@ const databaseMethods = {
198
208
  } catch (err) {
199
209
  return error(err);
200
210
  }
201
- },
211
+ }
202
212
 
203
213
  /**
204
214
  * 计数查询
@@ -209,7 +219,7 @@ const databaseMethods = {
209
219
  try {
210
220
  let dataQuery = this.knex(this.model);
211
221
  if (query.length > 0) {
212
- query.forEach(condition => dataQuery = dataQuery.where(condition));
222
+ query.forEach((condition) => (dataQuery = dataQuery.where(condition)));
213
223
  }
214
224
  const result = await dataQuery.count("* as total").first();
215
225
  return success(Number(result?.total) || 0);
@@ -217,36 +227,6 @@ const databaseMethods = {
217
227
  return error(err);
218
228
  }
219
229
  }
220
- };
221
-
222
- // ====================== 工厂函数 ======================
223
- /**
224
- * 创建数据库服务实例
225
- * @param {Object} knex - Knex实例
226
- * @param {string} model - 表名/模型名
227
- * @returns {Object} 数据库服务实例
228
- */
229
- export default function Service(knex, model) {
230
- if (!knex || !model) {
231
- throw new Error('Service: knex instance and model name are required');
232
- }
233
-
234
- // 创建继承数据库方法的轻量对象
235
- const service = Object.create(databaseMethods);
236
-
237
- // 设置实例专属状态(不可写)
238
- Object.defineProperties(service, {
239
- knex: {
240
- value: knex,
241
- writable: false,
242
- enumerable: true
243
- },
244
- model: {
245
- value: model,
246
- writable: false,
247
- enumerable: true
248
- }
249
- });
250
-
251
- return service;
252
230
  }
231
+
232
+ export default Service;
@@ -1,5 +1,5 @@
1
1
  const template = requirejs("art-template");
2
- import dayjs from 'dayjs';
2
+ import dayjs from "dayjs";
3
3
  // 注册 dateFormat 函数
4
4
  template.defaults.imports.dateFormat = function (date, format) {
5
5
  if (!date) {
package/global/import.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  import fs from "fs/promises";
3
2
  import { pathToFileURL } from "url";
4
3
  import { createRequire } from "module";
@@ -9,8 +8,8 @@ import { createRequire } from "module";
9
8
  * @returns {Promise<object|null>} 模块对象或null
10
9
  */
11
10
  const importFile = async (filepath) => {
12
- if (!filepath || typeof filepath !== 'string') {
13
- console.error('错误: 文件路径必须是有效的字符串');
11
+ if (!filepath || typeof filepath !== "string") {
12
+ console.error("错误: 文件路径必须是有效的字符串");
14
13
  return null;
15
14
  }
16
15
 
@@ -20,9 +19,9 @@ const importFile = async (filepath) => {
20
19
  const module = await import(fileUrl);
21
20
  return module.default || module;
22
21
  } catch (error) {
23
- if (error.code === 'ENOENT') {
22
+ if (error.code === "ENOENT") {
24
23
  console.error(`文件不存在: ${filepath}`);
25
- } else if (error.code === 'EACCES') {
24
+ } else if (error.code === "EACCES") {
26
25
  console.error(`没有权限访问文件: ${filepath}`);
27
26
  } else {
28
27
  console.error(`导入文件时出错 [${filepath}]:`, error.message);
@@ -37,4 +36,4 @@ const importFile = async (filepath) => {
37
36
  export const importjs = createRequire(import.meta.url);
38
37
 
39
38
  global.requirejs = importjs;
40
- global.importFile = importFile;
39
+ global.importFile = importFile;
@@ -5,53 +5,58 @@
5
5
  * @param {string} valueField - 作为值的字段名
6
6
  * @returns {Object} 转换后的对象
7
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
-
8
+ export const arrToObj = (
9
+ arr,
10
+ keyField = "config_key",
11
+ valueField = "config_value"
12
+ ) => {
13
+ // 输入验证
14
+ if (!Array.isArray(arr)) {
15
+ throw new Error("arrToObj 期望接收数组作为第一个参数");
16
+ }
26
17
 
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 {
18
+ return arr.reduce((result, item) => {
19
+ if (item && typeof item === "object") {
20
+ const key = item[keyField];
21
+ const value = item[valueField];
22
+ if (key !== undefined && value !== undefined) {
46
23
  result[key] = value;
47
24
  }
48
25
  }
49
-
50
26
  return result;
27
+ }, {});
28
+ };
29
+
30
+ /**
31
+ * @description 将字符串中的 JSON 字符串转换为对象
32
+ * @param {*} obj - 包含 JSON 字符串的对象
33
+ * @returns
34
+ */
35
+ export function parseJsonFields(obj) {
36
+ const result = {};
37
+ for (const key in obj) {
38
+ if (!obj.hasOwnProperty(key)) continue;
39
+ const value = obj[key];
40
+ // 如果是字符串,并且看起来像 JSON(以 { 或 [ 开头)
41
+ if (
42
+ typeof value === "string" &&
43
+ (value.startsWith("{") || value.startsWith("["))
44
+ ) {
45
+ try {
46
+ result[key] = JSON.parse(value);
47
+ } catch (e) {
48
+ console.warn(`JSON parse failed for field: ${key}`, e);
49
+ result[key] = value; // 保留原始值
50
+ }
51
+ } else {
52
+ result[key] = value;
53
+ }
51
54
  }
52
55
 
56
+ return result;
57
+ }
53
58
 
54
- /**
59
+ /**
55
60
  * @param {Array} arr - 原始数据数组
56
61
  * @param {number|string} [pid=0] - 根节点父ID
57
62
  * @param {string} [idKey='id'] - ID字段名
@@ -59,27 +64,39 @@ export const arrToObj = (arr, keyField = 'config_key', valueField = 'config_valu
59
64
  * @param {string} [childrenKey='children'] - 子节点字段名
60
65
  * @returns {Array} 树形结构数组
61
66
  */
62
- export function buildTree(arr, pid = 0, idKey = 'id', pidKey = 'pid', childrenKey = 'children') {
67
+ export function buildTree(
68
+ arr,
69
+ pid = 0,
70
+ idKey = "id",
71
+ pidKey = "pid",
72
+ childrenKey = "children"
73
+ ) {
63
74
  // 基础参数校验
64
75
  if (!Array.isArray(arr)) return [];
65
-
76
+
66
77
  const tree = [];
67
-
78
+
68
79
  for (let i = 0; i < arr.length; i++) {
69
80
  const item = arr[i];
70
81
  // 找到当前层级的节点
71
82
  if (item[pidKey] === pid) {
72
83
  // 递归查找子节点(通过slice创建子数组,避免重复遍历已处理项)
73
- const children = buildTree(arr.slice(i + 1), item[idKey], idKey, pidKey, childrenKey);
74
-
84
+ const children = buildTree(
85
+ arr.slice(i + 1),
86
+ item[idKey],
87
+ idKey,
88
+ pidKey,
89
+ childrenKey
90
+ );
91
+
75
92
  // 有子节点则添加,避免空数组
76
93
  if (children.length) {
77
94
  item[childrenKey] = children;
78
95
  }
79
-
96
+
80
97
  tree.push(item);
81
98
  }
82
99
  }
83
-
100
+
84
101
  return tree;
85
102
  }
package/helper/db.js CHANGED
@@ -19,7 +19,7 @@ const getDefaultErrorMessage = (error) => {
19
19
  }
20
20
  return "数据库发生未知错误,请稍后重试。";
21
21
  };
22
- export const db =({
22
+ export const db = ({
23
23
  client = "mysql2",
24
24
  host = "127.0.0.1",
25
25
  user = "root",
@@ -30,8 +30,8 @@ export const db =({
30
30
  charset = "utf8mb4",
31
31
  min = 0,
32
32
  max = 2,
33
- filename=''
34
- }) =>{
33
+ filename = "",
34
+ }) => {
35
35
  let config = {
36
36
  client,
37
37
  connection: {
@@ -40,20 +40,23 @@ export const db =({
40
40
  user,
41
41
  password,
42
42
  database,
43
- charset
43
+ charset,
44
44
  },
45
45
  debug,
46
46
  pool: {
47
47
  //默认为{min: 2, max: 10},连接池配置
48
48
  min,
49
49
  max,
50
- // 添加连接池错误处理
51
- afterCreate: (conn, done) => {
52
- conn.on('error', (error) => {
53
- console.error(`[连接池错误] 连接出错: ${getDefaultErrorMessage(error)}`, error);
50
+ // 添加连接池错误处理
51
+ afterCreate: (conn, done) => {
52
+ conn.on("error", (error) => {
53
+ console.error(
54
+ `[连接池错误] 连接出错: ${getDefaultErrorMessage(error)}`,
55
+ error
56
+ );
54
57
  });
55
58
  done(null, conn);
56
- }
59
+ },
57
60
  },
58
61
  log: {
59
62
  warn(message) {
@@ -79,10 +82,10 @@ export const db =({
79
82
  },
80
83
  },
81
84
  };
82
-
83
- if(client==='sqlite3' || client === 'better-sqlite3'){
84
- config.connection.filename = filename
85
- }
85
+
86
+ if (client === "sqlite3" || client === "better-sqlite3") {
87
+ config.connection.filename = filename;
88
+ }
86
89
 
87
90
  return knex(config);
88
91
  };
package/helper/file.js CHANGED
@@ -1,17 +1,16 @@
1
- import fs from 'fs/promises';
2
- import { accessSync, unlinkSync, existsSync, mkdirSync } from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
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
5
 
6
- export const dirname = (url) =>{
6
+ export const dirname = (url) => {
7
7
  const __filename = fileURLToPath(url);
8
8
  return path.dirname(__filename);
9
- }
10
-
9
+ };
11
10
 
12
11
  /**
13
12
  * 获取指定路径的文件树结构
14
- *
13
+ *
15
14
  * @param {string} basePath - 要扫描的基础路径
16
15
  * @param {boolean} [deep=true] - 是否深度遍历子目录
17
16
  * @returns {Promise<Array<Object>>} 文件树数组,每个元素包含文件/目录信息
@@ -43,13 +42,15 @@ export const getFileTree = async (basePath, deep = true) => {
43
42
  name: item,
44
43
  path: itemPath,
45
44
  relativePath: path.relative(ROOT_PATH, itemPath),
46
- type: itemStats.isDirectory() ? 'directory' : 'file',
45
+ type: itemStats.isDirectory() ? "directory" : "file",
47
46
  size: itemStats.size,
48
47
  modified: itemStats.mtime,
49
- depth: itemPath.split(path.sep).length - path.resolve(ROOT_PATH).split(path.sep).length
48
+ depth:
49
+ itemPath.split(path.sep).length -
50
+ path.resolve(ROOT_PATH).split(path.sep).length,
50
51
  };
51
52
 
52
- if (treeItem.type === 'directory' && deep) {
53
+ if (treeItem.type === "directory" && deep) {
53
54
  treeItem.children = await getFileTree(itemPath, deep);
54
55
  }
55
56
 
@@ -59,9 +60,9 @@ export const getFileTree = async (basePath, deep = true) => {
59
60
  // 排序:文件夹在前,文件在后,名称按字母顺序排列
60
61
  return tree.sort((a, b) => {
61
62
  if (a.type === b.type) {
62
- return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
63
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
63
64
  }
64
- return a.type === 'directory' ? -1 : 1;
65
+ return a.type === "directory" ? -1 : 1;
65
66
  });
66
67
  } catch (error) {
67
68
  console.error(`获取文件树失败: ${basePath}`, error);
@@ -71,7 +72,7 @@ export const getFileTree = async (basePath, deep = true) => {
71
72
 
72
73
  /**
73
74
  * 读取文件内容(UTF-8编码)
74
- *
75
+ *
75
76
  * @param {string} filePath - 要读取的文件路径
76
77
  * @returns {Promise<string>} 文件内容字符串
77
78
  * @throws {Error} 当文件不存在、无法读取或不是文件时抛出错误
@@ -79,17 +80,17 @@ export const getFileTree = async (basePath, deep = true) => {
79
80
  export const readFileContent = async (filePath) => {
80
81
  try {
81
82
  // 先检查路径是否安全
82
- if (!isPathSafe(filePath,APP_PATH) && !isPathSafe(filePath,ROOT_PATH)) {
83
+ if (!isPathSafe(filePath, APP_PATH) && !isPathSafe(filePath, ROOT_PATH)) {
83
84
  throw new Error(`路径不安全: ${filePath}`);
84
85
  }
85
-
86
+
86
87
  // 检查是否为文件
87
88
  const stats = await fs.stat(filePath);
88
89
  if (!stats.isFile()) {
89
90
  throw new Error(`不是文件: ${filePath}`);
90
91
  }
91
-
92
- return await fs.readFile(filePath, 'utf8');
92
+
93
+ return await fs.readFile(filePath, "utf8");
93
94
  } catch (error) {
94
95
  console.error(`读取文件失败: ${filePath}`, error);
95
96
  throw error;
@@ -98,7 +99,7 @@ export const readFileContent = async (filePath) => {
98
99
 
99
100
  /**
100
101
  * 保存内容到文件(UTF-8编码)
101
- *
102
+ *
102
103
  * @param {string} filePath - 要保存的文件路径
103
104
  * @param {string} content - 要写入的内容
104
105
  * @returns {Promise<void>} 无返回值
@@ -107,16 +108,16 @@ export const readFileContent = async (filePath) => {
107
108
  export const saveFileContent = async (filePath, content) => {
108
109
  try {
109
110
  // 先检查路径是否安全
110
- if (!isPathSafe(filePath,APP_PATH) && !isPathSafe(filePath,ROOT_PATH)) {
111
+ if (!isPathSafe(filePath, APP_PATH) && !isPathSafe(filePath, ROOT_PATH)) {
111
112
  throw new Error(`路径不安全: ${filePath}`);
112
113
  }
113
-
114
+
114
115
  // 确保目录存在
115
116
  const dirname = path.dirname(filePath);
116
117
  await fs.mkdir(dirname, { recursive: true });
117
-
118
+
118
119
  // 写入文件
119
- await fs.writeFile(filePath, content, 'utf8');
120
+ await fs.writeFile(filePath, content, "utf8");
120
121
  } catch (error) {
121
122
  console.error(`保存文件失败: ${filePath}`, error);
122
123
  throw error;
@@ -125,7 +126,7 @@ export const saveFileContent = async (filePath, content) => {
125
126
 
126
127
  /**
127
128
  * 验证路径是否在基础路径范围内(防止路径遍历攻击)
128
- *
129
+ *
129
130
  * @param {string} requestedPath - 要验证的路径
130
131
  * @param {string} basePath - 基础路径,requestedPath必须在此路径下
131
132
  * @returns {boolean} 如果路径安全则返回true,否则返回false
@@ -139,7 +140,7 @@ export const isPathSafe = (requestedPath, basePath) => {
139
140
 
140
141
  /**
141
142
  * 删除指定路径的图片文件
142
- *
143
+ *
143
144
  * @param {string} link - 图片文件的路径
144
145
  * @returns {boolean} 成功删除返回true,否则返回false
145
146
  * @description 同步操作,会检查文件是否存在后再删除
@@ -151,10 +152,10 @@ export function delImg(link) {
151
152
  console.error(`路径不安全: ${link}`);
152
153
  return false;
153
154
  }
154
-
155
+
155
156
  // 检查文件是否存在
156
157
  accessSync(link);
157
-
158
+
158
159
  // 删除文件
159
160
  unlinkSync(link);
160
161
  return true;
@@ -166,7 +167,7 @@ export function delImg(link) {
166
167
 
167
168
  /**
168
169
  * 递归创建目录(同步操作)
169
- *
170
+ *
170
171
  * @param {string} dirname - 要创建的目录路径
171
172
  * @returns {boolean} 成功创建或目录已存在返回true,否则返回false
172
173
  * @description 类似于mkdir -p命令,会创建所有不存在的父目录
@@ -178,18 +179,18 @@ export function mkdirsSync(dirname) {
178
179
  console.error(`路径不安全: ${dirname}`);
179
180
  return false;
180
181
  }
181
-
182
+
182
183
  if (existsSync(dirname)) {
183
184
  return true;
184
185
  }
185
-
186
+
186
187
  // 递归创建父目录
187
188
  const parentDir = path.dirname(dirname);
188
189
  if (mkdirsSync(parentDir)) {
189
190
  mkdirSync(dirname);
190
191
  return true;
191
192
  }
192
-
193
+
193
194
  return false;
194
195
  } catch (err) {
195
196
  console.error(`创建目录失败: ${dirname}`, err);
@@ -197,7 +198,6 @@ export function mkdirsSync(dirname) {
197
198
  }
198
199
  }
199
200
 
200
-
201
201
  export default {
202
202
  mkdirsSync,
203
203
  delImg,
@@ -205,4 +205,4 @@ export default {
205
205
  saveFileContent,
206
206
  readFileContent,
207
207
  getFileTree,
208
- }
208
+ };
package/helper/html.js CHANGED
@@ -5,17 +5,21 @@
5
5
  */
6
6
  export const htmlEncode = (str) => {
7
7
  // 非字符串直接返回,避免报错
8
- if (typeof str !== 'string') return str;
9
-
8
+ if (typeof str !== "string") return str;
9
+
10
10
  // 一次性替换所有特殊字符,减少函数调用
11
- return str.replace(/[&<>"' ]/g, match => ({
12
- '&': '&amp;',
13
- '<': '&lt;',
14
- '>': '&gt;',
15
- '"': '&quot;',
16
- "'": '&#39;',
17
- ' ': '&nbsp;'
18
- }[match]));
11
+ return str.replace(
12
+ /[&<>"' ]/g,
13
+ (match) =>
14
+ ({
15
+ "&": "&amp;",
16
+ "<": "&lt;",
17
+ ">": "&gt;",
18
+ '"': "&quot;",
19
+ "'": "&#39;",
20
+ " ": "&nbsp;",
21
+ }[match])
22
+ );
19
23
  };
20
24
 
21
25
  /**
@@ -25,15 +29,19 @@ export const htmlEncode = (str) => {
25
29
  */
26
30
  export const htmlDecode = (str) => {
27
31
  // 非字符串直接返回,避免报错
28
- if (typeof str !== 'string') return str;
29
-
32
+ if (typeof str !== "string") return str;
33
+
30
34
  // 一次性替换所有常见实体,减少函数调用
31
- return str.replace(/&amp;|&lt;|&gt;|&nbsp;|&#39;|&quot;/g, match => ({
32
- '&amp;': '&',
33
- '&lt;': '<',
34
- '&gt;': '>',
35
- '&nbsp;': ' ',
36
- '&#39;': "'",
37
- '&quot;': '"'
38
- }[match]));
39
- };
35
+ return str.replace(
36
+ /&amp;|&lt;|&gt;|&nbsp;|&#39;|&quot;/g,
37
+ (match) =>
38
+ ({
39
+ "&amp;": "&",
40
+ "&lt;": "<",
41
+ "&gt;": ">",
42
+ "&nbsp;": " ",
43
+ "&#39;": "'",
44
+ "&quot;": '"',
45
+ }[match])
46
+ );
47
+ };
package/helper/index.js CHANGED
@@ -1,9 +1,4 @@
1
+ import { db } from "./db.js";
2
+ import { loaderSort, loadConfig } from "./loader.js";
1
3
 
2
- import { db } from './db.js'
3
- import { loaderSort, loadConfig } from './loader.js'
4
-
5
- export {
6
- db,
7
- loaderSort,
8
- loadConfig,
9
- }
4
+ export { db, loaderSort, loadConfig };
package/helper/ip.js CHANGED
@@ -3,34 +3,35 @@
3
3
  * @param {Object} req - Express请求对象
4
4
  * @returns {string} 格式化后的IP地址,默认返回空字符串
5
5
  */
6
- export const getIp = (req) =>{
7
-
6
+ export const getIp = (req) => {
8
7
  // 优先级从高到低获取可能的IP来源
9
8
  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 // 连接套接字的远程地址
9
+ req.headers["x-forwarded-for"], // 代理场景下的真实IP(可能多个,逗号分隔)
10
+ req.headers["x-real-ip"], // 部分代理服务器使用
11
+ req.ip, // Express内置的IP获取(已处理代理)
12
+ req.connection?.remoteAddress, // 底层连接的远程地址
13
+ req.socket?.remoteAddress, // 套接字的远程地址
14
+ req.connection?.socket?.remoteAddress, // 连接套接字的远程地址
16
15
  ];
17
16
 
18
17
  // 从来源中找到第一个有效IP字符串
19
- let ip = ipSources.find(source => typeof source === 'string' && source.trim() !== '');
20
-
18
+ let ip = ipSources.find(
19
+ (source) => typeof source === "string" && source.trim() !== ""
20
+ );
21
+
21
22
  // 若未找到有效IP,直接返回空
22
- if (!ip) return '';
23
+ if (!ip) return "";
23
24
 
24
25
  // 处理多IP情况(取第一个)
25
- ip = ip.split(',').shift().trim();
26
+ ip = ip.split(",").shift().trim();
26
27
 
27
28
  // IPv6转IPv4处理
28
- if (ip === '::1') {
29
- return '127.0.0.1'; // 本地环回地址
29
+ if (ip === "::1") {
30
+ return "127.0.0.1"; // 本地环回地址
30
31
  }
31
- if (ip.startsWith('::ffff:')) {
32
- return ip.slice(7); // 去除IPv6映射的IPv4前缀
32
+ if (ip.startsWith("::ffff:")) {
33
+ return ip.slice(7); // 去除IPv6映射的IPv4前缀
33
34
  }
34
35
 
35
36
  return ip;
36
- }
37
+ };
package/helper/jwt.js CHANGED
@@ -1,41 +1,40 @@
1
1
  import jwt from "jsonwebtoken";
2
2
 
3
-
4
3
  /**
5
- *
4
+ *
6
5
  * @param {Object} data - 要存储在令牌中的数据(不建议存放敏感信息)
7
6
  * @param {string} secretKey - 用于签名的密钥
8
7
  * @param {string} time - 令牌过期时间,如 '1h'、'7d' 或秒数
9
8
  * @returns {string} 生成的JWT令牌
10
9
  */
11
- export function setToken(data={}, secretKey='chancms', time='7d') {
10
+ export function setToken(data = {}, secretKey = "chancms", time = "7d") {
12
11
  try {
13
12
  return jwt.sign(data, secretKey, {
14
13
  expiresIn: time,
15
- algorithm: 'HS256',
14
+ algorithm: "HS256",
16
15
  });
17
16
  } catch (error) {
18
- console.error('令牌生成失败:', error.message);
17
+ console.error("令牌生成失败:", error.message);
19
18
  throw new Error(`生成令牌失败: ${error.message}`);
20
19
  }
21
20
  }
22
21
 
23
22
  // 获取token
24
- export async function getToken(token, secretKey='chancms') {
23
+ export async function getToken(token, secretKey = "chancms") {
25
24
  return new Promise((resolve, reject) => {
26
- jwt.verify(token, secretKey,(err, decode) => {
25
+ jwt.verify(token, secretKey, (err, decode) => {
27
26
  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));
27
+ let errorMessage = "令牌验证失败";
28
+ if (err.name === "TokenExpiredError") {
29
+ errorMessage = "令牌已过期";
30
+ } else if (err.name === "JsonWebTokenError") {
31
+ errorMessage = "无效的令牌";
32
+ }
33
+ console.error(errorMessage, "令牌异常信息:", err.message);
34
+ return reject(new Error(errorMessage));
36
35
  } else {
37
36
  resolve(decode);
38
37
  }
39
38
  });
40
39
  });
41
- }
40
+ }
package/helper/loader.js CHANGED
@@ -1,5 +1,27 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { pathToFileURL } from "url";
4
+
5
+ /**
6
+ * @description 实例化一个类,并将该类的所有方法绑定到一个新的对象上。
7
+ * @param {Function} className - 需要实例化的类。
8
+ *@returns {Object} 包含绑定方法的对象。
9
+ */
10
+ export const bindClass = function (className) {
11
+ let obj = {};
12
+ const cls = new className();
13
+ Object.getOwnPropertyNames(cls.constructor.prototype).forEach(
14
+ (methodName) => {
15
+ if (
16
+ methodName !== "constructor" &&
17
+ typeof cls[methodName] === "function"
18
+ ) {
19
+ obj[methodName] = cls[methodName].bind(cls);
20
+ }
21
+ }
22
+ );
23
+ return obj;
24
+ };
3
25
 
4
26
  /**
5
27
  *
package/helper/time.js CHANGED
@@ -21,8 +21,8 @@ export const formatDay = (data, time = true, format = "YYYY-MM-DD") => {
21
21
  }
22
22
  });
23
23
  return data;
24
- }
24
+ };
25
25
 
26
26
  export const formatTime = (data, format = "YYYY-MM-DD HH:mm:ss") => {
27
27
  return dayjs(data).format("YYYY-MM-DD HH:mm:ss");
28
- }
28
+ };
package/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  waf,
18
18
  } from "./middleware/index.js";
19
19
  import { db, loaderSort, loadConfig } from "./helper/index.js";
20
- import {dirname} from "./helper/file.js";
20
+ import { dirname } from "./helper/file.js";
21
21
 
22
22
  class Chan {
23
23
  //版本号
@@ -30,6 +30,7 @@ class Chan {
30
30
  static extend = {}; //组件扩展
31
31
  static middleware = {}; //中间件
32
32
 
33
+
33
34
  constructor() {
34
35
  this.app = express();
35
36
  this.router = express.Router();
@@ -65,8 +66,8 @@ class Chan {
65
66
  logger,
66
67
  cors,
67
68
  } = Chan.config;
68
-
69
- this.app.set('trust proxy', true);
69
+
70
+ this.app.set("trust proxy", true);
70
71
  log(this.app, logger);
71
72
  setFavicon(this.app);
72
73
  setCookie(this.app, cookieKey);
@@ -134,7 +135,9 @@ class Chan {
134
135
 
135
136
  async loadFn(_path, key) {
136
137
  if (fs.existsSync(_path)) {
137
- const files = fs.readdirSync(_path).filter((file) => file.endsWith(".js"));
138
+ const files = fs
139
+ .readdirSync(_path)
140
+ .filter((file) => file.endsWith(".js"));
138
141
  for (const file of files) {
139
142
  const filePath = path.join(_path, file);
140
143
  let helperModule = await importFile(filePath);
@@ -1,4 +1,4 @@
1
1
  import cookieParser from "cookie-parser";
2
- export const setCookie = (app,cookieKey) => {
3
- app.use(cookieParser(cookieKey));
2
+ export const setCookie = (app, cookieKey) => {
3
+ app.use(cookieParser(cookieKey));
4
4
  };
@@ -1,4 +1,4 @@
1
1
  import cors from "cors";
2
- export const Cors = (app,_cors) => {
3
- app.use(cors(_cors));
4
- };
2
+ export const Cors = (app, _cors) => {
3
+ app.use(cors(_cors));
4
+ };
@@ -1,4 +1,4 @@
1
- export let setHeader = (app, { APP_NAME, APP_VERSION })=>{
1
+ export let setHeader = (app, { APP_NAME, APP_VERSION }) => {
2
2
  app.use((req, res, next) => {
3
3
  res.setHeader("Create-By", "Chanjs");
4
4
  res.setHeader("X-Powered-By", "ChanCMS");
@@ -1,23 +1,23 @@
1
- import {log} from "./log.js";
2
- import {setCookie} from "./cookie.js";
3
- import {setFavicon} from "./favicon.js";
4
- import {setBody} from "./setBody.js";
5
- import {setStatic} from "./static.js";
6
- import {setHeader} from "./header.js";
7
- import {setTemplate} from "./template.js";
8
- import {validator} from "./validator.js";
9
- import {Cors} from "./cors.js";
10
- import {waf} from "./waf.js";
1
+ import { log } from "./log.js";
2
+ import { setCookie } from "./cookie.js";
3
+ import { setFavicon } from "./favicon.js";
4
+ import { setBody } from "./setBody.js";
5
+ import { setStatic } from "./static.js";
6
+ import { setHeader } from "./header.js";
7
+ import { setTemplate } from "./template.js";
8
+ import { validator } from "./validator.js";
9
+ import { Cors } from "./cors.js";
10
+ import { waf } from "./waf.js";
11
11
 
12
12
  export {
13
- log,
14
- setCookie,
15
- setFavicon,
16
- setBody,
17
- setStatic,
18
- setHeader,
19
- setTemplate,
20
- Cors,
21
- validator,
22
- waf
23
- }
13
+ log,
14
+ setCookie,
15
+ setFavicon,
16
+ setBody,
17
+ setStatic,
18
+ setHeader,
19
+ setTemplate,
20
+ Cors,
21
+ validator,
22
+ waf,
23
+ };
@@ -1,11 +1,10 @@
1
1
  import express from "express";
2
-
3
2
  let setBody = function (app, JSON_LIMIT) {
3
+ // 1. 优先解析 XML 数据(必须在 json/urlencoded 之前)
4
+ app.use(express.raw({ type: "application/xml", limit: JSON_LIMIT }));
5
+ // 2. 再解析 JSON 和表单数据
4
6
  app.use(express.json({ limit: JSON_LIMIT }));
5
7
  app.use(express.urlencoded({ extended: false }));
6
- app.use(express.raw({ type: 'application/xml', limit: JSON_LIMIT }));
7
8
  };
8
9
 
9
- export {
10
- setBody
11
- }
10
+ export { setBody };
@@ -1,8 +1,8 @@
1
- import '../extend/art-template.js'
1
+ import "../extend/art-template.js";
2
2
  export let setTemplate = (app, config) => {
3
- const { views, env } = config;
4
- //合并插件中的view
5
- const all = [...views, 'app/modules/web/view'];
3
+ const { views, env } = config;
4
+ //合并插件中的view
5
+ const all = [...views, "app/modules/web/view"];
6
6
  app.set("view options", {
7
7
  debug: env === "dev",
8
8
  cache: env === "prd",
@@ -4,12 +4,12 @@ export const validator = (schemas) => (req, res, next) => {
4
4
  headers: schemas.headers,
5
5
  params: schemas.params,
6
6
  query: schemas.query,
7
- body: schemas.body
7
+ body: schemas.body,
8
8
  };
9
9
 
10
10
  for (const [key, schema] of Object.entries(sections)) {
11
11
  if (!schema) continue;
12
-
12
+
13
13
  const result = schema.safeParse(req[key]);
14
14
  if (!result.success) {
15
15
  // 返回首个错误信息
package/middleware/waf.js CHANGED
@@ -6,47 +6,154 @@ const keywords = {
6
6
  // 需要全词匹配的关键词(避免短词误报)
7
7
  wholeWord: [
8
8
  // 系统命令/工具(容易产生短词误报)
9
- "opt", "cmd", "rm", "mdc", "netcat", "nc", "mdb", "bin", "mk", "sys","sh","chomd",
10
- "php-cgi","process","require","child_process","execSync","mainModule"
9
+ "opt",
10
+ "cmd",
11
+ "rm",
12
+ "mdc",
13
+ "netcat",
14
+ "nc",
15
+ "mdb",
16
+ "bin",
17
+ "mk",
18
+ "sys",
19
+ "sh",
20
+ "chomd",
21
+ "php-cgi",
22
+ "process",
23
+ "require",
24
+ "child_process",
25
+ "execSync",
26
+ "mainModule",
11
27
  ],
12
-
28
+
13
29
  // 普通关键词(包含特殊字符或较长关键词)
14
30
  normal: [
15
31
  // 文件扩展名 & 敏感文件
16
- ".php", ".asp", ".aspx", ".jsp", ".jspx", ".do", ".action", ".cgi",
17
- ".py", ".pl", ".md", ".log", ".conf", ".config", ".env", ".jsa",
18
- ".go", ".jhtml", ".shtml", ".cfm", ".svn", ".keys", ".hidden",
19
- ".bod", ".ll", ".backup", ".json", ".xml", ".bak", ".aws",
20
- ".database", ".cookie", ".rsp", ".old", ".tf", ".sql", ".vscode",
21
- ".docker", ".map", ".save", ".gz", ".yml", ".tar", ".sh", ".idea", ".s3",
32
+ ".php",
33
+ ".asp",
34
+ ".aspx",
35
+ ".jsp",
36
+ ".jspx",
37
+ ".do",
38
+ ".action",
39
+ ".cgi",
40
+ ".py",
41
+ ".pl",
42
+ ".md",
43
+ ".log",
44
+ ".conf",
45
+ ".config",
46
+ ".env",
47
+ ".jsa",
48
+ ".go",
49
+ ".jhtml",
50
+ ".shtml",
51
+ ".cfm",
52
+ ".svn",
53
+ ".keys",
54
+ ".hidden",
55
+ ".bod",
56
+ ".ll",
57
+ ".backup",
58
+ ".json",
59
+ ".xml",
60
+ ".bak",
61
+ ".aws",
62
+ ".database",
63
+ ".cookie",
64
+ ".rsp",
65
+ ".old",
66
+ ".tf",
67
+ ".sql",
68
+ ".vscode",
69
+ ".docker",
70
+ ".map",
71
+ ".save",
72
+ ".gz",
73
+ ".yml",
74
+ ".tar",
75
+ ".sh",
76
+ ".idea",
77
+ ".s3",
22
78
 
23
79
  // 敏感目录
24
- "/administrator", "/wp-admin",
80
+ "/administrator",
81
+ "/wp-admin",
25
82
 
26
83
  // 高危路径/应用
27
- "phpMyAdmin", "setup", "wp-", "cgi-bin", "xampp", "staging", "internal",
28
- "debug", "metadata", "secret", "smtp", "redirect", "configs",
84
+ "phpMyAdmin",
85
+ "setup",
86
+ "wp-",
87
+ "cgi-bin",
88
+ "xampp",
89
+ "staging",
90
+ "internal",
91
+ "debug",
92
+ "metadata",
93
+ "secret",
94
+ "smtp",
95
+ "redirect",
96
+ "configs",
29
97
 
30
98
  // SQL 注入
31
- "sleep(", "benchmark(", "concat(", "extractvalue(", "updatexml(",
32
- "version(", "union select", "union all", "select @@", "drop",
33
- "alter", "truncate", "exec", "(select", "information_schema",
34
- "load_file(", "into outfile", "into dumpfile",
99
+ "sleep(",
100
+ "benchmark(",
101
+ "concat(",
102
+ "extractvalue(",
103
+ "updatexml(",
104
+ "version(",
105
+ "union select",
106
+ "union all",
107
+ "select @@",
108
+ "drop",
109
+ "alter",
110
+ "truncate",
111
+ "exec",
112
+ "(select",
113
+ "information_schema",
114
+ "load_file(",
115
+ "into outfile",
116
+ "into dumpfile",
35
117
 
36
118
  // 命令注入
37
- "cmd=", "system(", "exec(", "shell_exec(", "passthru(", "base64_decode",
38
- "eval(", "assert(", "preg_replace", "bash -i", "rm -rf", "wget ", "curl ",
39
- "chmod ", "phpinfo()",
119
+ "cmd=",
120
+ "system(",
121
+ "exec(",
122
+ "shell_exec(",
123
+ "passthru(",
124
+ "base64_decode",
125
+ "eval(",
126
+ "assert(",
127
+ "preg_replace",
128
+ "bash -i",
129
+ "rm -rf",
130
+ "wget ",
131
+ "curl ",
132
+ "chmod ",
133
+ "phpinfo()",
40
134
 
41
135
  // 路径遍历
42
- "../", "..\\", "/etc/passwd", "/etc/shadow",
136
+ "../",
137
+ "..\\",
138
+ "/etc/passwd",
139
+ "/etc/shadow",
43
140
 
44
141
  // XSS
45
- "<script", "javascript:", "onerror=", "onload=", "alert(", "document.cookie",
142
+ "<script",
143
+ "javascript:",
144
+ "onerror=",
145
+ "onload=",
146
+ "alert(",
147
+ "document.cookie",
46
148
 
47
149
  // 特殊编码
48
- "0x7e", "UNION%20SELECT", "%27OR%27", "{{", "}}", "1+1"
49
- ]
150
+ "0x7e",
151
+ "UNION%20SELECT",
152
+ "%27OR%27",
153
+ "{{",
154
+ "}}",
155
+ "1+1",
156
+ ],
50
157
  };
51
158
 
52
159
  // === 预处理:构建正则缓存 ===
@@ -116,7 +223,7 @@ const safe = (req, res, next) => {
116
223
  }
117
224
 
118
225
  const fullText = checkText + bodyText;
119
-
226
+
120
227
  // 空文本直接跳过检测
121
228
  if (!fullText.trim()) return next();
122
229
 
@@ -151,7 +258,8 @@ const safe = (req, res, next) => {
151
258
  userAgent: userAgent.substring(0, 100),
152
259
  method: req.method,
153
260
  matchedKeyword,
154
- sample: fullText.substring(0, 200) + (fullText.length > 200 ? "..." : ""),
261
+ sample:
262
+ fullText.substring(0, 200) + (fullText.length > 200 ? "..." : ""),
155
263
  });
156
264
 
157
265
  // 根据WAF级别决定拦截方式
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.0.16",
4
+ "version": "2.0.18",
5
5
  "description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
package/utils/response.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { CODE, DB_ERROR } from "../config/code.js";
2
2
 
3
- // ====================== 模块级别工具函数 ======================
4
- export const getDefaultErrorCode = (error) => {
3
+ /**
4
+ * @description 获取数据库错误码
5
+ * @param {*} error
6
+ * @returns {Number} 默认错误码
7
+ */
8
+ const getDefaultErrorCode = (error) => {
5
9
  if (error.message.includes("syntax") || error.message.includes("SQL")) {
6
10
  return 6008;
7
11
  } else if (error.message.includes("Connection closed")) {
@@ -12,11 +16,14 @@ export const getDefaultErrorCode = (error) => {
12
16
  return 5001;
13
17
  };
14
18
 
19
+ /**
20
+ * @description 数据库错误响应处理函数
21
+ * @param {*} err
22
+ * @returns {Object} 错误响应对象
23
+ */
15
24
  export const error = (err) => {
16
25
  console.error("DB Error:", err);
17
- // 从映射表获取状态码或使用默认错误码
18
26
  const errorCode = DB_ERROR[err.code] || getDefaultErrorCode(err);
19
-
20
27
  return {
21
28
  success: false,
22
29
  msg: CODE[errorCode], // 从CODE对象获取错误消息
@@ -28,8 +35,13 @@ export const error = (err) => {
28
35
  };
29
36
  };
30
37
 
38
+ /**
39
+ * @description 错误响应处理函数
40
+ * @param {*} msg
41
+ * @param {*} data
42
+ * @returns {Object} 失败响应对象
43
+ */
31
44
  export const fail = (msg = CODE[201], data = {}) => {
32
- console.warn("Operation failed:", msg);
33
45
  return {
34
46
  success: false,
35
47
  msg,
@@ -38,6 +50,12 @@ export const fail = (msg = CODE[201], data = {}) => {
38
50
  };
39
51
  };
40
52
 
53
+ /**
54
+ * @description 成功响应处理函数
55
+ * @param {*} data
56
+ * @param {*} msg
57
+ * @returns {Object} 成功响应对象
58
+ */
41
59
  export const success = (data = {}, msg = CODE[200]) => ({
42
60
  success: true,
43
61
  msg,