befly 3.74.2 → 3.75.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.
Files changed (65) hide show
  1. package/apis/dashboard/systemResources.js +3 -1
  2. package/apis/dict/_dict.js +1 -1
  3. package/apis/role/_role.js +2 -2
  4. package/apis/tongJi/dailyStats.js +2 -2
  5. package/apis/tongJi/dailyStatsDistribution.js +3 -3
  6. package/apis/tongJi/errorList.js +2 -2
  7. package/apis/tongJi/errorStats.js +6 -6
  8. package/checks/api.js +11 -6
  9. package/checks/items.js +56 -0
  10. package/checks/menu.js +1 -1
  11. package/checks/table.js +4 -5
  12. package/configs/beflyConfig.json +5 -10
  13. package/configs/constConfig.js +44 -3
  14. package/hooks/auth.js +3 -2
  15. package/hooks/parser.js +15 -4
  16. package/hooks/permission.js +3 -2
  17. package/hooks/rateLimit.js +16 -3
  18. package/hooks/validator.js +2 -1
  19. package/index.js +53 -24
  20. package/lib/cacheHelper.js +83 -229
  21. package/lib/connect.js +3 -13
  22. package/lib/dbHelper.js +57 -142
  23. package/lib/dbParse.js +65 -248
  24. package/lib/dbUtil.js +19 -63
  25. package/lib/logger.js +45 -22
  26. package/lib/redisHelper.js +186 -420
  27. package/lib/smtpText.js +7 -18
  28. package/lib/sqlBuilder.js +22 -51
  29. package/lib/xmlParse.js +15 -59
  30. package/package.json +1 -1
  31. package/plugins/cache.js +2 -3
  32. package/plugins/config.js +3 -1
  33. package/plugins/cron.js +2 -2
  34. package/plugins/email.js +3 -1
  35. package/plugins/logger.js +2 -2
  36. package/plugins/mysql.js +5 -10
  37. package/plugins/redis.js +4 -11
  38. package/plugins/tool.js +3 -3
  39. package/plugins/xmlParse.js +3 -1
  40. package/router/api.js +2 -6
  41. package/router/static.js +9 -10
  42. package/schemas/config.js +4 -4
  43. package/scripts/syncDb/context.js +4 -7
  44. package/scripts/syncDb/diff.js +2 -1
  45. package/scripts/syncDb/index.js +2 -6
  46. package/sync/api.js +7 -8
  47. package/sync/cache.js +3 -1
  48. package/sync/dev.js +28 -65
  49. package/sync/menu.js +5 -6
  50. package/sync/syncUtil.js +25 -0
  51. package/utils/calcPerfTime.js +2 -3
  52. package/utils/crypto.js +0 -40
  53. package/utils/deepMerge.js +2 -6
  54. package/utils/error.js +64 -0
  55. package/utils/fieldClear.js +13 -56
  56. package/utils/importDefault.js +11 -12
  57. package/utils/is.js +5 -182
  58. package/utils/loggerUtils.js +3 -1
  59. package/utils/response.js +12 -8
  60. package/utils/scanFiles.js +2 -53
  61. package/utils/scanSources.js +9 -0
  62. package/utils/util.js +25 -222
  63. package/checks/hook.js +0 -39
  64. package/checks/plugin.js +0 -39
  65. package/utils/processInfo.js +0 -39
@@ -24,7 +24,9 @@ export default {
24
24
  };
25
25
  });
26
26
 
27
- await new Promise((resolve) => setTimeout(resolve, 100));
27
+ await new Promise((resolve) => {
28
+ setTimeout(resolve, 100);
29
+ });
28
30
 
29
31
  const endCpus = os.cpus();
30
32
  const endMeasure = endCpus.map((cpu) => {
@@ -12,7 +12,7 @@ export async function getDictTypeByCode(befly, typeCode) {
12
12
  });
13
13
  }
14
14
 
15
- export async function getDictByTypeCodeAndKey(befly, typeCode, key, dictId = undefined) {
15
+ export async function getDictByTypeCodeAndKey(befly, typeCode, key, dictId) {
16
16
  return await befly.mysql.getOne({
17
17
  table: "beflyDict",
18
18
  where: {
@@ -1,6 +1,6 @@
1
1
  export const systemRoleCodes = ["dev", "user", "admin", "guest"];
2
2
 
3
- export async function getRoleByCode(befly, roleCode, fields = undefined) {
3
+ export async function getRoleByCode(befly, roleCode, fields) {
4
4
  return await befly.mysql.getOne({
5
5
  table: "beflyRole",
6
6
  fields: fields,
@@ -10,7 +10,7 @@ export async function getRoleByCode(befly, roleCode, fields = undefined) {
10
10
  });
11
11
  }
12
12
 
13
- export async function getRoleById(befly, id, fields = undefined) {
13
+ export async function getRoleById(befly, id, fields) {
14
14
  return await befly.mysql.getOne({
15
15
  table: "beflyRole",
16
16
  fields: fields,
@@ -149,8 +149,8 @@ export default {
149
149
  const productCode = ctx.body?.productCode ?? "";
150
150
  const dateStats = {
151
151
  reportDate: reportDate,
152
- yesterdayDate: recentDateList[recentDateList.length - 2],
153
- dayBeforeYesterdayDate: recentDateList[recentDateList.length - 3],
152
+ yesterdayDate: recentDateList.at(-2),
153
+ dayBeforeYesterdayDate: recentDateList.at(-3),
154
154
  monthStartDate: monthStartDate,
155
155
  previousMonthStartDate: previousMonthStartDate,
156
156
  previousMonthEndDate: previousMonthEndDate,
@@ -14,19 +14,19 @@ function getDateRangeBounds(dateRange, now, timeZone) {
14
14
  }
15
15
 
16
16
  if (dateRange === "yesterday") {
17
- const yesterdayDate = recentDateList[recentDateList.length - 2];
17
+ const yesterdayDate = recentDateList.at(-2);
18
18
 
19
19
  return [yesterdayDate, yesterdayDate];
20
20
  }
21
21
 
22
22
  if (dateRange === "dayBeforeYesterday") {
23
- const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3];
23
+ const dayBeforeYesterdayDate = recentDateList.at(-3);
24
24
 
25
25
  return [dayBeforeYesterdayDate, dayBeforeYesterdayDate];
26
26
  }
27
27
 
28
28
  if (dateRange === "recent30") {
29
- return [recentDateList[0], recentDateList[recentDateList.length - 1]];
29
+ return [recentDateList[0], recentDateList.at(-1)];
30
30
  }
31
31
 
32
32
  const monthStartDate = getTongJiMonthStartDate(now, timeZone);
@@ -58,7 +58,7 @@ export default {
58
58
  });
59
59
 
60
60
  const lists = (result.data?.lists || []).map((item) => {
61
- const row = Object.assign({}, item);
61
+ const row = { ...item };
62
62
 
63
63
  for (const field of NUMBER_FIELDS) {
64
64
  row[field] = Number(item[field]) || 0;
@@ -67,7 +67,7 @@ export default {
67
67
  return row;
68
68
  });
69
69
 
70
- const data = Object.assign({}, result.data);
70
+ const data = { ...result.data };
71
71
  data.lists = lists;
72
72
 
73
73
  return befly.tool.Yes("获取成功", data);
@@ -15,11 +15,11 @@ function buildErrorStatsResponse(bucketDate, monthStartDate, recentDateList, tod
15
15
  count: todaySummary.count
16
16
  },
17
17
  yesterday: {
18
- bucketDate: recentDateList[recentDateList.length - 2] || bucketDate,
18
+ bucketDate: recentDateList.at(-2) || bucketDate,
19
19
  count: yesterdaySummary.count
20
20
  },
21
21
  dayBeforeYesterday: {
22
- bucketDate: recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || bucketDate,
22
+ bucketDate: recentDateList.at(-3) || recentDateList.at(-2) || bucketDate,
23
23
  count: dayBeforeYesterdaySummary.count
24
24
  },
25
25
  month: {
@@ -54,8 +54,8 @@ function sumDateCounts(map, dateList) {
54
54
  }
55
55
 
56
56
  async function getErrorStatsFromMysql(befly, bucketDate, monthStartDate, recentDateList) {
57
- const yesterdayDate = recentDateList[recentDateList.length - 2] || bucketDate;
58
- const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
57
+ const yesterdayDate = recentDateList.at(-2) || bucketDate;
58
+ const dayBeforeYesterdayDate = recentDateList.at(-3) || yesterdayDate;
59
59
  const monthDateList = getTongJiDateRangeList(monthStartDate, bucketDate);
60
60
 
61
61
  const [dayCountMap, trendRes, topTypesRes] = await Promise.all([
@@ -111,11 +111,11 @@ function buildEmptyErrorStats(bucketDate, monthStartDate, recentDateList) {
111
111
  count: 0
112
112
  },
113
113
  yesterday: {
114
- bucketDate: recentDateList[recentDateList.length - 2] || bucketDate,
114
+ bucketDate: recentDateList.at(-2) || bucketDate,
115
115
  count: 0
116
116
  },
117
117
  dayBeforeYesterday: {
118
- bucketDate: recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || bucketDate,
118
+ bucketDate: recentDateList.at(-3) || recentDateList.at(-2) || bucketDate,
119
119
  count: 0
120
120
  },
121
121
  month: {
package/checks/api.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { Logger } from "../lib/logger.js";
2
2
  import { validateFieldRule } from "../lib/validator.js";
3
3
  import { formatValidationIssues } from "../utils/formatValidationIssues.js";
4
- import { isBoolean, isFunction, isPlainObject, isString } from "../utils/is.js";
4
+ import { isBoolean, isFunction, isLowerCamelCaseName, isPlainObject, isString } from "../utils/is.js";
5
5
 
6
- const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
7
6
  const apiPathRegex = /^\/api\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*$/;
8
7
  const apiKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "name", "handler", "apiPath", "method", "body", "auth", "fields", "required"]);
9
8
 
@@ -29,11 +28,18 @@ function checkApiItem(api, index) {
29
28
  }
30
29
  for (const key of ["source", "type", "filePath", "relativePath"]) checkNoTrimString(issues, api[key], [index, key], 1);
31
30
  checkNoTrimString(issues, api.name, [index, "name"], 1, 100);
32
- if (!isString(api.fileName) || !lowerCamelRegex.test(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
31
+ if (!isString(api.fileName) || !isLowerCamelCaseName(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
33
32
  if (!isFunction(api.handler)) issues.push({ path: [index, "handler"], message: "必须是函数" });
34
33
  if (!isString(api.apiPath) || !apiPathRegex.test(api.apiPath)) issues.push({ path: [index, "apiPath"], message: "路径格式无效" });
35
34
  if (!["GET", "POST", "ALL"].includes(api.method)) issues.push({ path: [index, "method"], message: "仅允许 GET|POST|ALL" });
36
35
  if (!["none", "raw"].includes(api.body)) issues.push({ path: [index, "body"], message: "仅允许 none|raw" });
36
+ // body 为 raw 时不允许配置 fields/required:parser 跳过解析,参数校验由 handler 自行承担
37
+ if (api.body === "raw" && isPlainObject(api.fields) && Object.keys(api.fields).length > 0) {
38
+ issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 fields,由 handler 自行解析校验' });
39
+ }
40
+ if (api.body === "raw" && Array.isArray(api.required) && api.required.length > 0) {
41
+ issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 required,由 handler 自行解析校验' });
42
+ }
37
43
 
38
44
  if (!isBoolean(api.auth) && (!Array.isArray(api.auth) || api.auth.length === 0 || api.auth.some((item) => !isString(item) || (item !== "" && item.trim() !== item)))) {
39
45
  issues.push({ path: [index, "auth"], message: "必须是 boolean 或非空字符串数组" });
@@ -43,13 +49,12 @@ function checkApiItem(api, index) {
43
49
  issues.push({ path: [index, "fields"], message: "必须是对象格式" });
44
50
  } else {
45
51
  for (const [fieldName, rule] of Object.entries(api.fields)) {
46
- if (!lowerCamelRegex.test(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
52
+ if (!isLowerCamelCaseName(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
47
53
  for (const issue of validateFieldRule(rule)) issues.push({ path: [index, "fields", fieldName, ...issue.path], message: issue.message });
48
54
  }
49
55
  }
50
56
 
51
57
  if (!Array.isArray(api.required) || api.required.some((item) => !isString(item) || (item !== "" && item.trim() !== item))) issues.push({ path: [index, "required"], message: "必须是字符串数组" });
52
- if (api.source === "app" && isString(api.relativePath) && api.relativePath.split("/")[0] === "core") issues.push({ path: [index, "relativePath"], message: "app API 不能使用 core 作为一级目录,避免占用 /api/core 命名空间" });
53
58
 
54
59
  return issues;
55
60
  }
@@ -63,6 +68,6 @@ export async function checkApi(apis) {
63
68
  }
64
69
 
65
70
  if (issues.length === 0) return false;
66
- Logger.warn("接口校验失败", { errors: formatValidationIssues(issues, { items: apis, itemLabel: "api" }) }, false);
71
+ Logger.warn("接口校验失败", { errors: formatValidationIssues(issues, { items: apis, itemLabel: "api" }) });
67
72
  return true;
68
73
  }
@@ -0,0 +1,56 @@
1
+ import { Logger } from "../lib/logger.js";
2
+ import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
+ import { isFunction, isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
4
+
5
+ const itemKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
6
+
7
+ function checkItem(item) {
8
+ const issues = [];
9
+ if (!isPlainObject(item)) {
10
+ issues.push({ path: [], message: "必须是对象格式" });
11
+ return issues;
12
+ }
13
+
14
+ for (const key of Object.keys(item)) {
15
+ if (!itemKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
16
+ }
17
+ for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
18
+ if (!isString(item[key]) || item[key].length === 0 || item[key].trim() !== item[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
19
+ }
20
+ if (!isFunction(item.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
21
+ if (!isFiniteNumber(item.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
22
+ return issues;
23
+ }
24
+
25
+ function checkItemList(items, label, labelEn) {
26
+ if (!Array.isArray(items)) {
27
+ Logger.warn(`${label}校验失败`, { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: items, itemLabel: labelEn }) });
28
+ return true;
29
+ }
30
+
31
+ let hasError = false;
32
+
33
+ // order 冲突校验:同 order 的执行顺序依赖扫描顺序,要求显式区分
34
+ const orderMap = new Map();
35
+ for (const item of items) {
36
+ if (!isFiniteNumber(item.order)) continue;
37
+ orderMap.set(item.order, (orderMap.get(item.order) || 0) + 1);
38
+ }
39
+ const duplicateOrders = [...orderMap].filter(([, count]) => count > 1).map(([order]) => order);
40
+ if (duplicateOrders.length > 0) {
41
+ Logger.warn(`${label}校验失败`, { errors: [{ path: [], message: `order 重复: ${duplicateOrders.join("、")}` }] });
42
+ hasError = true;
43
+ }
44
+
45
+ for (const item of items) {
46
+ const issues = checkItem(item);
47
+ if (issues.length === 0) continue;
48
+ Logger.warn(`${label}校验失败`, { errors: formatValidationIssues(issues, { item: item, itemLabel: labelEn }) });
49
+ hasError = true;
50
+ }
51
+ return hasError;
52
+ }
53
+
54
+ export const checkHook = (hooks) => checkItemList(hooks, "钩子", "hook");
55
+
56
+ export const checkPlugin = (plugins) => checkItemList(plugins, "插件", "plugin");
package/checks/menu.js CHANGED
@@ -43,6 +43,6 @@ export async function checkMenu(menus) {
43
43
  else menus.forEach((menu, index) => checkMenuItem(issues, menu, [index]));
44
44
 
45
45
  if (issues.length === 0) return false;
46
- Logger.warn("菜单校验失败", { errors: formatValidationIssues(issues, { items: menus, itemLabel: "menus" }) }, false);
46
+ Logger.warn("菜单校验失败", { errors: formatValidationIssues(issues, { items: menus, itemLabel: "menus" }) });
47
47
  return true;
48
48
  }
package/checks/table.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { Logger } from "../lib/logger.js";
2
2
  import { validateFieldRule } from "../lib/validator.js";
3
3
  import { formatValidationIssues } from "../utils/formatValidationIssues.js";
4
- import { isPlainObject } from "../utils/is.js";
4
+ import { isLowerCamelCaseName, isPlainObject } from "../utils/is.js";
5
5
  import { snakeCase } from "../utils/util.js";
6
6
 
7
- const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
8
7
  const lowerSnakeRegex = /^_?[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
9
8
 
10
9
  function isValidFieldName(fieldName) {
11
- return fieldName.includes("_") ? lowerSnakeRegex.test(fieldName) : lowerCamelRegex.test(fieldName);
10
+ return fieldName.includes("_") ? lowerSnakeRegex.test(fieldName) : isLowerCamelCaseName(fieldName);
12
11
  }
13
12
 
14
13
  export async function checkTable(tables) {
@@ -17,7 +16,7 @@ export async function checkTable(tables) {
17
16
  issues.push({ path: [], message: "必须是对象格式" });
18
17
  } else {
19
18
  for (const [tableName, fields] of Object.entries(tables)) {
20
- if (!lowerCamelRegex.test(tableName)) issues.push({ path: [tableName], message: "表名必须为 lowerCamelCase" });
19
+ if (!isLowerCamelCaseName(tableName)) issues.push({ path: [tableName], message: "表名必须为 lowerCamelCase" });
21
20
  if (!isPlainObject(fields)) {
22
21
  issues.push({ path: [tableName], message: "必须是对象格式" });
23
22
  continue;
@@ -37,6 +36,6 @@ export async function checkTable(tables) {
37
36
  }
38
37
 
39
38
  if (issues.length === 0) return false;
40
- Logger.warn("表结构校验失败", { errors: formatValidationIssues(issues, { item: tables, itemLabel: "table" }) }, false);
39
+ Logger.warn("表结构校验失败", { errors: formatValidationIssues(issues, { item: tables, itemLabel: "table" }) });
41
40
  return true;
42
41
  }
@@ -1,10 +1,9 @@
1
1
  {
2
- "runMode": "development",
3
2
  "appName": "野蜂飞舞",
4
3
  "appPort": 3000,
5
4
  "appHost": "127.0.0.1",
6
5
  "devEmail": "dev@qq.com",
7
- "devPassword": "111111",
6
+ "devPassword": "",
8
7
  "bodyLimit": 1048576,
9
8
  "upload": {
10
9
  "maxSize": 20,
@@ -21,7 +20,6 @@
21
20
  "debug": 1,
22
21
  "excludeFields": ["password", "token", "secret"]
23
22
  },
24
-
25
23
  "mysql": {
26
24
  "hostname": "127.0.0.1",
27
25
  "port": 3306,
@@ -29,9 +27,9 @@
29
27
  "password": "root",
30
28
  "database": "befly",
31
29
  "max": 10,
32
- "beflyMode": "auto"
30
+ "beflyMode": "auto",
31
+ "queryTimeout": 30
33
32
  },
34
-
35
33
  "redis": {
36
34
  "hostname": "127.0.0.1",
37
35
  "port": 6379,
@@ -40,11 +38,9 @@
40
38
  "db": 0,
41
39
  "prefix": "befly"
42
40
  },
43
-
44
41
  "session": {
45
42
  "expireDays": 7
46
43
  },
47
-
48
44
  "email": {
49
45
  "host": "smtp.qq.com",
50
46
  "port": 465,
@@ -53,7 +49,6 @@
53
49
  "pass": "",
54
50
  "label": "befly"
55
51
  },
56
-
57
52
  "cors": {
58
53
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
59
54
  "allowedHeaders": "Content-Type,Authorization",
@@ -61,12 +56,12 @@
61
56
  "maxAge": 86400,
62
57
  "credentials": "false"
63
58
  },
64
-
65
59
  "rateLimit": {
66
60
  "enable": 1,
67
61
  "defaultLimit": 1000,
68
62
  "defaultWindow": 60,
69
63
  "key": "ip",
70
- "skipRoutes": []
64
+ "skipRoutes": [],
65
+ "failOpen": 1
71
66
  }
72
67
  }
@@ -27,9 +27,6 @@ export const ENUM_KIND_TYPES = ["enum"];
27
27
  // 运行配置可选值
28
28
  // ==========================
29
29
 
30
- // 运行模式可选值
31
- export const RUN_MODE_VALUES = ["development", "production"];
32
-
33
30
  // ==========================
34
31
  // 字段验证规则配置
35
32
  // ==========================
@@ -40,3 +37,47 @@ export const FIELD_RULE_INPUT_TYPES = ["number", "integer", "string", "char", "a
40
37
  // 字段 min/max 缺省值
41
38
  export const FIELD_RULE_DEFAULT_MIN = 0;
42
39
  export const FIELD_RULE_DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
40
+
41
+ // ==========================
42
+ // 框架系统表名
43
+ // ==========================
44
+
45
+ // 系统表前缀(syncDb 对比时跳过)
46
+ export const BEFLY_TABLE_PREFIX = "befly_";
47
+
48
+ // 接口表(sync 层与启动检查共用)
49
+ export const BEFLY_API_TABLE = "beflyApi";
50
+ // 菜单表
51
+ export const BEFLY_MENU_TABLE = "beflyMenu";
52
+
53
+ // 角色表
54
+ export const BEFLY_ROLE_TABLE = "beflyRole";
55
+
56
+ // 管理员表
57
+ export const BEFLY_ADMIN_TABLE = "beflyAdmin";
58
+
59
+ // ==========================
60
+ // 钩子与插件执行顺序(大间隔编号,插入用中间值如 15/25/35)
61
+ // ==========================
62
+
63
+ // 钩子执行顺序(按请求生命周期:鉴权 → 限流 → 解析 → 校验 → 权限)
64
+ export const HOOK_ORDER = {
65
+ AUTH: 10,
66
+ RATE_LIMIT: 20,
67
+ PARSER: 30,
68
+ VALIDATOR: 40,
69
+ PERMISSION: 50
70
+ };
71
+
72
+ // 插件初始化顺序(按依赖:配置 → 日志 → 工具 → Redis → XML → MySQL → 缓存 → 邮件 → 定时任务)
73
+ export const PLUGIN_ORDER = {
74
+ CONFIG: 10,
75
+ LOGGER: 20,
76
+ TOOL: 30,
77
+ REDIS: 40,
78
+ XML_PARSE: 50,
79
+ MYSQL: 60,
80
+ CACHE: 70,
81
+ EMAIL: 80,
82
+ CRON: 90
83
+ };
package/hooks/auth.js CHANGED
@@ -1,13 +1,14 @@
1
+ import { HOOK_ORDER } from "../configs/constConfig.js";
1
2
  import { toSessionTtlSeconds } from "../utils/util.js";
2
3
 
3
4
  export default {
4
- order: 1,
5
+ order: HOOK_ORDER.AUTH,
5
6
  handler: async (befly, ctx) => {
6
7
  const authHeader = ctx.headers.get("authorization");
7
8
  const ttlSeconds = toSessionTtlSeconds(befly.config?.session?.expireDays);
8
9
 
9
10
  if (authHeader && authHeader.startsWith("Bearer ")) {
10
- const sid = authHeader.substring(7).trim();
11
+ const sid = authHeader.slice(7).trim();
11
12
 
12
13
  if (!sid) {
13
14
  return;
package/hooks/parser.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { xmlParse } from "#befly/lib/xmlParse.js";
2
2
  import { ErrorResponse } from "#befly/utils/response.js";
3
3
 
4
+ import { HOOK_ORDER } from "../configs/constConfig.js";
5
+
4
6
  /**
5
7
  * 请求参数解析钩子
6
8
  * - GET 请求:解析 URL 查询参数
@@ -9,14 +11,23 @@ import { ErrorResponse } from "#befly/utils/response.js";
9
11
  * - body: "raw" 时跳过解析,由 handler 自行处理原始请求
10
12
  */
11
13
  export default {
12
- order: 2,
14
+ order: HOOK_ORDER.PARSER,
13
15
  handler: async (befly, ctx) => {
14
16
  // apiBody=raw 模式:跳过解析,保留原始请求供 handler 自行处理
15
- // 适用于:微信回调、支付回调、webhook 等需要手动解密/验签的场景
17
+ // 适用于:微信回调、支付回调、webhook 等需要手动解密/验签的场景(大小由 handler/upload.maxSize 控制)
16
18
  if (ctx.apiBody === "raw") {
17
19
  ctx.body = {};
18
20
  return;
19
21
  }
22
+
23
+ // 请求体大小限制(bodyLimit):按 Content-Length 前置拦截,避免 JSON/XML 整体读入内存
24
+ const contentLength = Number(ctx.headers.get("content-length") || 0);
25
+ const bodyLimit = befly.config?.bodyLimit;
26
+ if (bodyLimit !== undefined && contentLength > bodyLimit) {
27
+ ctx.response = ErrorResponse(ctx, "请求体过大", 1, null, { limit: bodyLimit, actual: contentLength }, "parser");
28
+ return;
29
+ }
30
+
20
31
  const queryParams = Object.fromEntries(new URL(ctx.url).searchParams);
21
32
  const contentType = ctx.headers.get("content-type") || "";
22
33
  // GET 请求:解析查询参数
@@ -29,12 +40,12 @@ export default {
29
40
  if (contentType.includes("application/json")) {
30
41
  const body = await ctx.req.json();
31
42
  // 合并 URL 参数和请求体(请求体优先)
32
- ctx.body = Object.assign({}, queryParams, body);
43
+ ctx.body = { ...queryParams, ...body };
33
44
  } else if (contentType.includes("application/xml") || contentType.includes("text/xml")) {
34
45
  // XML 格式
35
46
  const text = await ctx.req.text();
36
47
  // 合并 URL 参数和请求体(请求体优先)
37
- ctx.body = Object.assign({}, queryParams, xmlParse(text));
48
+ ctx.body = { ...queryParams, ...xmlParse(text) };
38
49
  } else {
39
50
  // 不支持的 Content-Type
40
51
  ctx.response = ErrorResponse(
@@ -1,3 +1,4 @@
1
+ import { HOOK_ORDER } from "../configs/constConfig.js";
1
2
  import { isValidPositiveInt } from "../utils/is.js";
2
3
  // 相对导入
3
4
  import { ErrorResponse } from "../utils/response.js";
@@ -11,7 +12,7 @@ import { ErrorResponse } from "../utils/response.js";
11
12
  * - 其他角色:检查 Redis 中的角色权限集合
12
13
  */
13
14
  export default {
14
- order: 4,
15
+ order: HOOK_ORDER.PERMISSION,
15
16
  handler: async (befly, ctx) => {
16
17
  // 1. 接口无需权限
17
18
  if (ctx.apiAuth === false) {
@@ -47,7 +48,7 @@ export default {
47
48
 
48
49
  // 4. 角色权限检查
49
50
  // apiPath 在 apiHandler 中已统一生成并写入 ctx.apiPath
50
- let hasPermission = false;
51
+ let hasPermission;
51
52
 
52
53
  if (befly.cache && typeof befly.cache.checkRoleApis === "function") {
53
54
  hasPermission = await befly.cache.checkRoleApis(ctx.roleCode, ctx.apiPath);
@@ -1,5 +1,6 @@
1
1
  import picomatch from "picomatch";
2
2
 
3
+ import { HOOK_ORDER } from "../configs/constConfig.js";
3
4
  import { isValidPositiveInt } from "../utils/is.js";
4
5
  import { ErrorResponse } from "../utils/response.js";
5
6
 
@@ -31,6 +32,8 @@ function normalizeConfig(config) {
31
32
 
32
33
  return {
33
34
  enabled: RATE_LIMIT_ENABLED_VALUES.has(config?.enable),
35
+ // failOpen:限流服务(Redis)不可用时是否放行;默认放行(保持兼容),可配置为 failClosed
36
+ failOpen: config?.failOpen === undefined ? true : RATE_LIMIT_ENABLED_VALUES.has(config?.failOpen),
34
37
  limit: toPositiveInt(config?.defaultLimit, 1000),
35
38
  window: toPositiveInt(config?.defaultWindow, 60),
36
39
  keyType: toSafeString(config?.key, "ip"),
@@ -55,16 +58,26 @@ function buildRateLimitRedisKey(ctx, keyType, scope) {
55
58
  }
56
59
 
57
60
  export default {
58
- order: 1.5,
61
+ order: HOOK_ORDER.RATE_LIMIT,
59
62
  handler: async (befly, ctx) => {
60
63
  const rateLimit = normalizeConfig(befly?.config?.rateLimit);
61
- if (!rateLimit.enabled || !befly?.redis || typeof befly.redis.incrWithExpire !== "function" || shouldSkipRateLimit(rateLimit, ctx)) return;
64
+ if (!rateLimit.enabled || shouldSkipRateLimit(rateLimit, ctx)) return;
65
+
66
+ // 限流服务(Redis)不可用:failOpen 放行(默认),failClosed 直接拒绝
67
+ if (!befly?.redis || typeof befly.redis.incrWithExpire !== "function") {
68
+ if (!rateLimit.failOpen) {
69
+ ctx.corsHeaders ||= {};
70
+ ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
71
+ ctx.response = ErrorResponse(ctx, "服务繁忙,请稍后再试", 1, null, { reason: "rate_limit_unavailable" }, "rateLimit");
72
+ }
73
+ return;
74
+ }
62
75
 
63
76
  const scope = resolveScopeByKeyType(rateLimit.keyType, ctx);
64
77
  const current = await befly.redis.incrWithExpire(buildRateLimitRedisKey(ctx, rateLimit.keyType, scope), rateLimit.window);
65
78
  if (current <= rateLimit.limit) return;
66
79
 
67
- ctx.corsHeaders = ctx.corsHeaders || {};
80
+ ctx.corsHeaders ||= {};
68
81
  ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
69
82
  ctx.response = ErrorResponse(
70
83
  ctx,
@@ -1,3 +1,4 @@
1
+ import { HOOK_ORDER } from "../configs/constConfig.js";
1
2
  // 相对导入
2
3
  import { validateFields } from "../lib/validator.js";
3
4
  import { isPlainObject, isString } from "../utils/is.js";
@@ -9,7 +10,7 @@ import { snakeCase } from "../utils/util.js";
9
10
  * 根据 API 定义的 fields 和 required 验证请求参数
10
11
  */
11
12
  export default {
12
- order: 3,
13
+ order: HOOK_ORDER.VALIDATOR,
13
14
  handler: async (befly, ctx) => {
14
15
  // 仅保留 fields 中声明的字段,并支持 snake_case 入参回退(例如 agent_id -> agentId)
15
16
  const rawBody = isPlainObject(ctx.body) ? ctx.body : {};