befly 3.76.0 → 3.76.2

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 (155) hide show
  1. package/Befly.js +159 -0
  2. package/README.md +50 -11
  3. package/apis/admin/delete.js +1 -3
  4. package/apis/admin/insert.js +2 -1
  5. package/apis/admin/select.js +2 -7
  6. package/apis/admin/update.js +3 -8
  7. package/apis/api/select.js +2 -7
  8. package/apis/auth/login.js +16 -20
  9. package/apis/dict/delete.js +1 -1
  10. package/apis/dict/detail.js +1 -3
  11. package/apis/dict/select.js +1 -2
  12. package/apis/dict/update.js +1 -1
  13. package/apis/dictType/delete.js +1 -1
  14. package/apis/dictType/detail.js +1 -1
  15. package/apis/dictType/insert.js +1 -1
  16. package/apis/dictType/select.js +2 -7
  17. package/apis/dictType/update.js +2 -2
  18. package/apis/email/config.js +4 -7
  19. package/apis/email/logList.js +2 -7
  20. package/apis/email/send.js +1 -2
  21. package/apis/email/verify.js +1 -3
  22. package/apis/loginLog/select.js +2 -7
  23. package/apis/menu/select.js +2 -7
  24. package/apis/operateLog/select.js +2 -7
  25. package/apis/role/apiSave.js +2 -2
  26. package/apis/role/menuSave.js +2 -2
  27. package/apis/role/select.js +2 -7
  28. package/apis/source/imageList.js +2 -5
  29. package/apis/tongJi/dailyReport.js +12 -13
  30. package/apis/tongJi/dailyReportSelect.js +1 -2
  31. package/apis/tongJi/dailyStats.js +1 -1
  32. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  33. package/apis/tongJi/errorList.js +1 -2
  34. package/apis/tongJi/errorReport.js +1 -2
  35. package/apis/tongJi/projectDelete.js +1 -3
  36. package/apis/tongJi/projectSelect.js +2 -4
  37. package/apis/tongJi/projectUpdate.js +2 -2
  38. package/apis/upload/file.js +7 -5
  39. package/checks/api.js +43 -66
  40. package/checks/config.js +17 -12
  41. package/checks/corns.js +28 -46
  42. package/checks/field.js +18 -0
  43. package/checks/items.js +16 -60
  44. package/checks/menu.js +28 -39
  45. package/checks/table.js +106 -26
  46. package/checks/validation.js +13 -0
  47. package/configs/beflyConfig.json +6 -6
  48. package/configs/constConfig.js +2 -63
  49. package/exports.js +2 -0
  50. package/hooks/auth.js +11 -7
  51. package/hooks/parser.js +55 -59
  52. package/hooks/permission.js +5 -2
  53. package/hooks/rateLimit.js +25 -62
  54. package/hooks/validator.js +43 -31
  55. package/index.js +43 -344
  56. package/{lib → libs}/cacheHelper.js +3 -8
  57. package/libs/cron.js +60 -0
  58. package/libs/logger/index.js +1 -0
  59. package/libs/logger/logger.js +245 -0
  60. package/libs/logger/sanitize.js +115 -0
  61. package/{lib → libs/mysql}/dbHelper.js +100 -73
  62. package/{lib → libs/mysql}/dbParse.js +16 -9
  63. package/{lib → libs/mysql}/dbUtil.js +15 -32
  64. package/libs/mysql/error.js +29 -0
  65. package/libs/mysql/index.js +6 -0
  66. package/libs/mysql/is.js +29 -0
  67. package/libs/mysql/mysql.js +43 -0
  68. package/libs/mysql/sql/identifier.js +85 -0
  69. package/libs/mysql/sql/index.js +2 -0
  70. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  71. package/libs/mysql/util.js +44 -0
  72. package/libs/redis/index.js +2 -0
  73. package/libs/redis/redis.js +204 -0
  74. package/libs/redis/redisError.js +8 -0
  75. package/libs/tableModel.js +72 -0
  76. package/libs/tool.js +33 -0
  77. package/libs/validator/compiler.js +248 -0
  78. package/libs/validator/index.js +10 -0
  79. package/libs/validator/parser.js +173 -0
  80. package/libs/validator/util.js +5 -0
  81. package/package.json +10 -8
  82. package/paths.js +2 -3
  83. package/plugins/cache.js +2 -1
  84. package/plugins/email.js +4 -4
  85. package/plugins/mysql.js +5 -15
  86. package/plugins/redis.js +4 -12
  87. package/router/api.js +14 -10
  88. package/router/root.js +14 -0
  89. package/router/static.js +3 -3
  90. package/schemas/api.json +99 -0
  91. package/schemas/config.json +255 -0
  92. package/schemas/corn.json +39 -0
  93. package/schemas/item.json +43 -0
  94. package/schemas/menu.json +44 -0
  95. package/schemas/table.json +80 -0
  96. package/scripts/syncDb/apply.js +87 -0
  97. package/scripts/syncDb/ddl.js +43 -0
  98. package/scripts/syncDb/index.js +175 -30
  99. package/scripts/syncDb/model.js +60 -0
  100. package/scripts/syncDb/plan.js +160 -0
  101. package/scripts/syncDb/preflight.js +98 -0
  102. package/scripts/syncDb/report.js +177 -162
  103. package/scripts/syncDb/snapshot.js +118 -0
  104. package/sync/syncUtil.js +7 -13
  105. package/tables/admin.json +51 -53
  106. package/tables/api.json +29 -40
  107. package/tables/dailyReport.json +85 -73
  108. package/tables/dict.json +27 -39
  109. package/tables/dictType.json +20 -34
  110. package/tables/emailLog.json +49 -47
  111. package/tables/errorReport.json +86 -60
  112. package/tables/file.json +47 -50
  113. package/tables/loginLog.json +68 -55
  114. package/tables/menu.json +20 -34
  115. package/tables/operateLog.json +53 -47
  116. package/tables/project.json +18 -30
  117. package/tables/role.json +33 -37
  118. package/utils/cors.js +37 -41
  119. package/utils/deepMerge.js +3 -1
  120. package/utils/error.js +1 -56
  121. package/utils/id.js +44 -0
  122. package/utils/is.js +6 -243
  123. package/utils/response.js +12 -1
  124. package/utils/scanFiles.js +13 -4
  125. package/utils/scanSources.js +58 -20
  126. package/utils/util.js +11 -163
  127. package/apis/_apis.js +0 -20
  128. package/apis/auth/sendSmsCode.js +0 -24
  129. package/apis/role/delete.js +0 -41
  130. package/apis/role/insert.js +0 -45
  131. package/apis/role/save.js +0 -36
  132. package/apis/role/update.js +0 -66
  133. package/configs/regexpAlias.json +0 -55
  134. package/lib/connect.js +0 -119
  135. package/lib/logger.js +0 -430
  136. package/lib/redisHelper.js +0 -399
  137. package/lib/validator.js +0 -274
  138. package/lib/xmlParse.js +0 -149
  139. package/plugins/config.js +0 -13
  140. package/plugins/cron.js +0 -115
  141. package/plugins/logger.js +0 -17
  142. package/plugins/tool.js +0 -131
  143. package/plugins/xmlParse.js +0 -10
  144. package/schemas/config.js +0 -67
  145. package/scripts/syncDb/context.js +0 -120
  146. package/scripts/syncDb/diff.js +0 -279
  147. package/scripts/syncDb/transform.js +0 -113
  148. package/sql/befly.sql +0 -271
  149. package/utils/email.js +0 -3
  150. package/utils/fieldClear.js +0 -22
  151. package/utils/formatValidationIssues.js +0 -28
  152. package/utils/getClientIp.js +0 -47
  153. package/utils/loggerUtils.js +0 -196
  154. package/utils/regexpUtil.js +0 -52
  155. /package/{lib → libs}/smtpText.js +0 -0
package/checks/menu.js CHANGED
@@ -1,49 +1,38 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
3
- import { isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
4
-
5
- const menuPathSegmentRegexSource = "[a-z][a-z0-9]*(?:-[a-z0-9]+)*";
6
- const fullMenuPathRegex = new RegExp(`^/(?:core(?:/${menuPathSegmentRegexSource})*|(?:${menuPathSegmentRegexSource}(?:/${menuPathSegmentRegexSource})*)?)$`);
7
- const childMenuPathRegex = new RegExp(`^/${menuPathSegmentRegexSource}$`);
8
- const menuKeys = new Set(["path", "name", "sort", "children"]);
9
-
10
- function checkMenuItem(issues, menu, path, child = false) {
11
- if (!isPlainObject(menu)) {
12
- issues.push({ path: path, message: "必须是对象格式" });
13
- return;
14
- }
15
- for (const key of Object.keys(menu)) {
16
- if (!menuKeys.has(key)) issues.push({ path: [...path, key], message: "不允许出现" });
17
- }
18
- const pathRegex = child ? childMenuPathRegex : fullMenuPathRegex;
19
- if (!isString(menu.path) || !pathRegex.test(menu.path)) issues.push({ path: [...path, "path"], message: "路径格式无效" });
20
- if (!isString(menu.name) || menu.name.trim() !== menu.name || menu.name.length === 0) issues.push({ path: [...path, "name"], message: "必须是非空且无首尾空格的字符串" });
21
- if (!isFiniteNumber(menu.sort)) issues.push({ path: [...path, "sort"], message: "必须是有限数字" });
22
-
23
- if (child) {
24
- if (menu.children !== undefined && (!Array.isArray(menu.children) || menu.children.length > 0)) issues.push({ path: [...path, "children"], message: "只能为空数组" });
25
- return;
26
- }
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
27
3
 
28
- if (menu.children !== undefined && !Array.isArray(menu.children)) {
29
- issues.push({ path: [...path, "children"], message: "必须是数组" });
30
- return;
31
- }
32
- for (const [index, item] of (menu.children || []).entries()) {
33
- checkMenuItem(issues, item, [...path, "children", index], true);
34
- if (isPlainObject(item) && isString(menu.path) && isString(item.path) && !fullMenuPathRegex.test(`${menu.path}${item.path}`)) {
35
- issues.push({ path: [...path, "children", index, "path"], message: "菜单完整路径必须是 /core/xxx 或无前缀路径" });
4
+ import menuSchema from "../schemas/menu.json";
5
+ import { formatIssues } from "./validation.js";
6
+
7
+ const validate = compile(menuSchema);
8
+
9
+ const menuPathRegex = /^\/(?:core(?:\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*|(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*)?)$/;
10
+
11
+ function checkMenuPaths(issues, menus) {
12
+ const paths = new Map();
13
+ for (const [index, menu] of menus.entries()) {
14
+ if (paths.has(menu.path)) issues.push({ path: [index, "path"], message: `菜单路径与第 ${paths.get(menu.path) + 1} 项重复` });
15
+ else paths.set(menu.path, index);
16
+
17
+ if (!Array.isArray(menu?.children)) continue;
18
+ for (const [childIndex, child] of menu.children.entries()) {
19
+ const fullPath = `${menu.path}${child.path}`;
20
+ if (paths.has(fullPath)) issues.push({ path: [index, "children", childIndex, "path"], message: `菜单完整路径与第 ${paths.get(fullPath) + 1} 项重复` });
21
+ else paths.set(fullPath, index);
22
+
23
+ if (typeof menu.path === "string" && typeof child?.path === "string" && !menuPathRegex.test(fullPath)) {
24
+ issues.push({ path: [index, "children", childIndex, "path"], message: "菜单完整路径必须是 /core/xxx 或无前缀路径" });
25
+ }
36
26
  }
37
27
  }
38
28
  }
39
29
 
40
30
  export async function checkMenu(menus) {
41
- const issues = [];
42
- if (!Array.isArray(menus)) issues.push({ path: [], message: "必须是数组" });
43
- else menus.forEach((menu, index) => checkMenuItem(issues, menu, [index]));
44
-
31
+ const issues = validate(menus).issues;
32
+ if (Array.isArray(menus)) checkMenuPaths(issues, menus);
45
33
  if (issues.length === 0) return [];
46
- const errors = formatValidationIssues(issues, { items: menus, itemLabel: "menus" });
34
+
35
+ const errors = formatIssues(issues, { items: menus, itemLabel: "menus" });
47
36
  Logger.warn("菜单校验失败", { errors: errors });
48
37
  return errors;
49
38
  }
package/checks/table.js CHANGED
@@ -1,42 +1,122 @@
1
- import { Logger } from "../lib/logger.js";
2
- import { validateFieldRule } from "../lib/validator.js";
3
- import { formatValidationIssues } from "../utils/formatValidationIssues.js";
4
- import { isLowerCamelCaseName, isPlainObject } from "../utils/is.js";
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+ import { compile } from "#befly/libs/validator/index.js";
3
+
4
+ import { isTableAutoFieldName } from "../libs/tableModel.js";
5
+ import tableSchema from "../schemas/table.json";
5
6
  import { snakeCase } from "../utils/util.js";
7
+ import { getFieldSchema } from "./field.js";
8
+ import { formatIssues } from "./validation.js";
6
9
 
7
- const lowerSnakeRegex = /^_?[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
10
+ const validate = compile(tableSchema);
11
+ const fieldParamTypes = {
12
+ integer: new Set(["integer"]),
13
+ number: new Set(["number"]),
14
+ varchar: new Set(["string"]),
15
+ text: new Set(["string", "array", "object", "record"])
16
+ };
8
17
 
9
- function isValidFieldName(fieldName) {
10
- return fieldName.includes("_") ? lowerSnakeRegex.test(fieldName) : isLowerCamelCaseName(fieldName);
18
+ function addIssue(issues, path, message) {
19
+ issues.push({ path: path, code: "rule", message: message });
11
20
  }
12
21
 
13
- export async function checkTable(tables) {
14
- const issues = [];
15
- if (!isPlainObject(tables)) {
16
- issues.push({ path: [], message: "必须是对象格式" });
22
+ function checkFieldDefinition(issues, tableName, fieldName, field) {
23
+ const path = [tableName, "fields", fieldName];
24
+ if (!field || typeof field !== "object" || Array.isArray(field)) return;
25
+
26
+ if (typeof field.name !== "string" || !field.name || field.name !== field.name.trim() || field.name.includes("\n") || field.name.length > 20) addIssue(issues, [...path, "name"], "必须是 1..20 字符的简短名称,不允许首尾空白或换行");
27
+ if (field.detail !== undefined && (typeof field.detail !== "string" || !field.detail || field.detail !== field.detail.trim())) addIssue(issues, [...path, "detail"], "必须是非空无首尾空白字符串");
28
+ if (!fieldParamTypes[field.fieldType]) addIssue(issues, [...path, "fieldType"], "必须是 integer、number、varchar、text");
29
+ if (typeof field.fieldNullable !== "boolean") addIssue(issues, [...path, "fieldNullable"], "必须是 boolean");
30
+
31
+ let parseField;
32
+ try {
33
+ parseField = compile(getFieldSchema(field));
34
+ } catch (error) {
35
+ addIssue(issues, path, error.message);
36
+ }
37
+
38
+ if (!fieldParamTypes[field.fieldType]?.has(field.paramType)) addIssue(issues, [...path, "paramType"], `不允许与 fieldType=${field.fieldType} 搭配`);
39
+
40
+ if (Object.hasOwn(field, "fieldDefault")) {
41
+ if (field.fieldDefault === null) addIssue(issues, [...path, "fieldDefault"], "不允许为 null");
42
+ else if (parseField && !(field.paramType === "string" && field.fieldDefault === "")) {
43
+ const result = parseField(field.fieldDefault);
44
+ for (const issue of result.issues) addIssue(issues, [...path, "fieldDefault", ...issue.path], issue.message);
45
+ }
46
+ }
47
+
48
+ if (field.fieldType === "varchar" && (!Number.isSafeInteger(field.maxValue) || field.maxValue < 1 || field.maxValue > 10000)) addIssue(issues, [...path, "maxValue"], "varchar.maxValue 必须是 1..10000 的整数");
49
+ if (field.fieldType === "number") {
50
+ if (!Number.isSafeInteger(field.precision) || field.precision < 1 || field.precision > 15) addIssue(issues, [...path, "precision"], "必须是 1..15 的整数");
51
+ if (!Number.isSafeInteger(field.scale) || field.scale < 0 || field.scale > field.precision) addIssue(issues, [...path, "scale"], "必须是 0..precision 的整数");
17
52
  } else {
18
- for (const [tableName, fields] of Object.entries(tables)) {
19
- if (!isLowerCamelCaseName(tableName)) issues.push({ path: [tableName], message: "表名必须为 lowerCamelCase" });
20
- if (!isPlainObject(fields)) {
21
- issues.push({ path: [tableName], message: "必须是对象格式" });
22
- continue;
23
- }
53
+ if (field.precision !== undefined) addIssue(issues, [...path, "precision"], "仅 fieldType=number 允许使用");
54
+ if (field.scale !== undefined) addIssue(issues, [...path, "scale"], " fieldType=number 允许使用");
55
+ }
56
+ if (field.fieldType === "text" && Object.hasOwn(field, "fieldDefault")) addIssue(issues, [...path, "fieldDefault"], "text 不允许默认值");
57
+ }
58
+
59
+ function checkIndex(issues, tableName, table, indexName, index) {
60
+ if (!Array.isArray(index?.fields)) return;
61
+ let varcharBytes = 0;
62
+ for (const fieldName of index.fields) {
63
+ const field = table.fields?.[fieldName];
64
+ if (!field) {
65
+ addIssue(issues, [tableName, "indexes", indexName, "fields"], `索引字段不存在: ${fieldName}`);
66
+ continue;
67
+ }
68
+ if (field.fieldType === "text") addIssue(issues, [tableName, "indexes", indexName, "fields"], `text 字段不能直接建立索引: ${fieldName}`);
69
+ if (field.fieldType === "varchar") varcharBytes += field.maxValue * 4;
70
+ }
71
+ if (varcharBytes > 3072) addIssue(issues, [tableName, "indexes", indexName, "fields"], `utf8mb4 varchar 索引估算长度 ${varcharBytes} 超过 3072 字节`);
72
+ }
73
+
74
+ function checkTableRelations(issues, tables) {
75
+ for (const [tableName, table] of Object.entries(tables)) {
76
+ if (!table || typeof table !== "object" || Array.isArray(table)) continue;
77
+ if (table.codeName !== tableName) addIssue(issues, [tableName, "codeName"], "必须与注册键一致");
78
+ if (typeof table.codeName === "string" && table.dbName !== snakeCase(table.codeName)) addIssue(issues, [tableName, "dbName"], "必须由 codeName 转为 snake_case");
24
79
 
25
- const fieldNameMap = new Map();
26
- for (const [fieldName, rule] of Object.entries(fields)) {
27
- if (!isValidFieldName(fieldName)) issues.push({ path: [tableName, fieldName], message: "字段名必须为 lowerCamelCase 或 snake_case" });
28
- for (const issue of validateFieldRule(rule)) issues.push({ path: [tableName, fieldName, ...issue.path], message: issue.message });
80
+ const auto = table.fields?.id?.automatic === true;
81
+ const dbFieldNames = new Map();
82
+ for (const [fieldName, field] of Object.entries(table.fields || {})) {
83
+ const path = [tableName, "fields", fieldName];
84
+ const shouldBeAutomatic = auto && isTableAutoFieldName(fieldName);
85
+ if (field?.automatic !== shouldBeAutomatic) addIssue(issues, [...path, "automatic"], "自动字段标记与表模式不一致");
86
+ const dbFieldName = snakeCase(fieldName);
87
+ if (dbFieldNames.has(dbFieldName)) addIssue(issues, path, `字段名 ${fieldName} 与 ${dbFieldNames.get(dbFieldName)} 指向同一数据库字段 ${dbFieldName}`);
88
+ else dbFieldNames.set(dbFieldName, fieldName);
89
+ checkFieldDefinition(issues, tableName, fieldName, field);
90
+ }
29
91
 
30
- const dbFieldName = snakeCase(fieldName);
31
- const previousFieldName = fieldNameMap.get(dbFieldName);
32
- if (previousFieldName) issues.push({ path: [tableName, fieldName], message: `字段名 ${fieldName} 与 ${previousFieldName} 指向同一数据库字段 ${dbFieldName}` });
33
- else fieldNameMap.set(dbFieldName, fieldName);
92
+ const primaryField = table.fields?.[table.primary];
93
+ if (auto) {
94
+ if (table.primary !== "id") addIssue(issues, [tableName, "primary"], "auto 模式主键必须是 id");
95
+ if (!primaryField?.primary || primaryField?.autoIncrement) addIssue(issues, [tableName, "fields", "id"], "auto 模式 id 必须是非自增主键");
96
+ } else {
97
+ if (!table.primary) addIssue(issues, [tableName, "primary"], "manual 模式必须配置 $tablePrimary");
98
+ if (!primaryField) addIssue(issues, [tableName, "primary"], "manual 模式必须通过 $tablePrimary 指定已定义字段");
99
+ else {
100
+ if (primaryField.fieldType !== "integer" || primaryField.paramType !== "integer") addIssue(issues, [tableName, "fields", table.primary], "manual 模式主键必须是 integer 字段");
101
+ if (primaryField.fieldNullable !== false) addIssue(issues, [tableName, "fields", table.primary, "fieldNullable"], "manual 模式主键不允许 NULL");
102
+ if (Object.hasOwn(primaryField, "fieldDefault")) addIssue(issues, [tableName, "fields", table.primary, "fieldDefault"], "manual 模式主键不允许默认值");
103
+ if (!primaryField.primary || !primaryField.autoIncrement) addIssue(issues, [tableName, "fields", table.primary], "manual 模式主键必须自动自增");
34
104
  }
35
105
  }
106
+
107
+ if (Object.values(table.fields || {}).filter((field) => field?.primary).length !== 1) addIssue(issues, [tableName, "fields"], "每张表必须且只能有一个主键");
108
+ if (Object.values(table.fields || {}).filter((field) => field?.autoIncrement).length > 1) addIssue(issues, [tableName, "fields"], "每张表最多只能有一个自增字段");
109
+
110
+ for (const [indexName, index] of Object.entries(table.indexes || {})) checkIndex(issues, tableName, table, indexName, index);
36
111
  }
112
+ }
37
113
 
114
+ export async function checkTable(tables) {
115
+ const issues = validate(tables).issues;
116
+ if (tables && typeof tables === "object" && !Array.isArray(tables)) checkTableRelations(issues, tables);
38
117
  if (issues.length === 0) return [];
39
- const errors = formatValidationIssues(issues, { item: tables, itemLabel: "table" });
118
+
119
+ const errors = formatIssues(issues, { item: tables, itemLabel: "table" });
40
120
  Logger.warn("表结构校验失败", { errors: errors });
41
121
  return errors;
42
122
  }
@@ -0,0 +1,13 @@
1
+ export function formatIssues(issues, options = {}) {
2
+ const items = Array.isArray(options.items) ? options.items : null;
3
+ return issues.map((issue) => {
4
+ const hasIndex = items && typeof issue.path[0] === "number";
5
+ const index = hasIndex ? issue.path[0] : null;
6
+ const item = hasIndex ? items[index] : options.item;
7
+ const fieldPath = (hasIndex ? issue.path.slice(1) : issue.path).join(".") || "(root)";
8
+ return {
9
+ file: item?.filePath || item?.relativePath || item?.fileName || item?.name || (index === null ? options.itemLabel : `${options.itemLabel}[${index}]`),
10
+ expected: `字段 ${fieldPath} ${issue.message}`
11
+ };
12
+ });
13
+ }
@@ -3,7 +3,8 @@
3
3
  "appPort": 3000,
4
4
  "appHost": "127.0.0.1",
5
5
  "devEmail": "dev@qq.com",
6
- "devPassword": "",
6
+ "devPassword": "123456",
7
+ "trustProxy": false,
7
8
  "bodyLimit": 1048576,
8
9
  "upload": {
9
10
  "maxSize": 20,
@@ -17,7 +18,7 @@
17
18
  "apiHost": "http://127.0.0.1",
18
19
  "excludeApisLog": ["/api/core/tongJi/*Report"],
19
20
  "logger": {
20
- "debug": 1,
21
+ "debug": true,
21
22
  "excludeFields": ["password", "token", "secret"]
22
23
  },
23
24
  "mysql": {
@@ -50,18 +51,17 @@
50
51
  "label": "befly"
51
52
  },
52
53
  "cors": {
53
- "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
54
54
  "allowedHeaders": "Content-Type,Authorization",
55
55
  "exposedHeaders": "",
56
56
  "maxAge": 86400,
57
- "credentials": "false"
57
+ "credentials": false
58
58
  },
59
59
  "rateLimit": {
60
- "enable": 1,
60
+ "enable": true,
61
61
  "defaultLimit": 1000,
62
62
  "defaultWindow": 60,
63
63
  "key": "ip",
64
64
  "skipRoutes": [],
65
- "failOpen": 1
65
+ "failOpen": true
66
66
  }
67
67
  }
@@ -1,66 +1,11 @@
1
- // ==========================
2
- // 数据库类型分类
3
- // ==========================
1
+ export const TABLE_AUTO_FIELD_NAMES = ["id", "state", "createdAt", "updatedAt", "deletedAt"];
4
2
 
5
- // 字符串类数据库类型
6
- export const STRING_KIND_TYPES = ["char", "varchar", "enum", "tinytext", "text", "mediumtext", "longtext"];
7
-
8
- // 文本类数据库类型
9
- export const TEXT_KIND_TYPES = ["tinytext", "text", "mediumtext", "longtext"];
10
-
11
- // 整数类数据库类型
12
- export const INT_KIND_TYPES = ["tinyint", "smallint", "mediumint", "int", "bigint"];
13
-
14
- // 小数类数据库类型
15
- export const DECIMAL_KIND_TYPES = ["decimal"];
16
-
17
- // 浮点类数据库类型
18
- export const FLOAT_KIND_TYPES = ["float", "double"];
19
-
20
- // JSON 类数据库类型
21
- export const JSON_KIND_TYPES = ["json"];
22
-
23
- // 枚举类数据库类型
24
- export const ENUM_KIND_TYPES = ["enum"];
25
-
26
- // ==========================
27
- // 运行配置可选值
28
- // ==========================
29
-
30
- // ==========================
31
- // 字段验证规则配置
32
- // ==========================
33
-
34
- // 字段 input 可选值(统一用于 validator / checkTable / checkApi)
35
- export const FIELD_RULE_INPUT_TYPES = ["number", "integer", "string", "char", "array", "arrayNumber", "arrayInteger", "arrayObject", "json", "jsonNumber", "jsonInteger", "regexp", "enum", "enumNumber", "enumInteger"];
36
-
37
- // 字段 min/max 缺省值
38
- export const FIELD_RULE_DEFAULT_MIN = 0;
39
- export const FIELD_RULE_DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
40
-
41
- // ==========================
42
- // 框架系统表名
43
- // ==========================
44
-
45
- // 系统表前缀(syncDb 对比时跳过)
46
3
  export const BEFLY_TABLE_PREFIX = "befly_";
47
-
48
- // 接口表(sync 层与启动检查共用)
49
4
  export const BEFLY_API_TABLE = "beflyApi";
50
- // 菜单表
51
5
  export const BEFLY_MENU_TABLE = "beflyMenu";
52
-
53
- // 角色表
54
6
  export const BEFLY_ROLE_TABLE = "beflyRole";
55
-
56
- // 管理员表
57
7
  export const BEFLY_ADMIN_TABLE = "beflyAdmin";
58
8
 
59
- // ==========================
60
- // 钩子与插件执行顺序(大间隔编号,插入用中间值如 15/25/35)
61
- // ==========================
62
-
63
- // 钩子执行顺序(按请求生命周期:鉴权 → 限流 → 解析 → 校验 → 权限)
64
9
  export const HOOK_ORDER = {
65
10
  AUTH: 10,
66
11
  RATE_LIMIT: 20,
@@ -69,15 +14,9 @@ export const HOOK_ORDER = {
69
14
  PERMISSION: 50
70
15
  };
71
16
 
72
- // 插件初始化顺序(按依赖:配置 → 日志 → 工具 → Redis → XML → MySQL → 缓存 → 邮件 → 定时任务)
73
17
  export const PLUGIN_ORDER = {
74
- CONFIG: 10,
75
- LOGGER: 20,
76
- TOOL: 30,
77
18
  REDIS: 40,
78
- XML_PARSE: 50,
79
19
  MYSQL: 60,
80
20
  CACHE: 70,
81
- EMAIL: 80,
82
- CRON: 90
21
+ EMAIL: 80
83
22
  };
package/exports.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Logger } from "./libs/logger/index.js";
2
+ export { syncDb } from "./scripts/syncDb/index.js";
package/hooks/auth.js CHANGED
@@ -1,11 +1,14 @@
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+
1
3
  import { HOOK_ORDER } from "../configs/constConfig.js";
2
4
  import { toSessionTtlSeconds } from "../utils/util.js";
3
5
 
4
6
  export default {
7
+ name: "身份识别",
5
8
  order: HOOK_ORDER.AUTH,
6
9
  handler: async (befly, ctx) => {
7
10
  const authHeader = ctx.headers.get("authorization");
8
- const ttlSeconds = toSessionTtlSeconds(befly.config?.session?.expireDays);
11
+ const ttlSeconds = toSessionTtlSeconds(befly.config.session.expireDays);
9
12
 
10
13
  if (authHeader && authHeader.startsWith("Bearer ")) {
11
14
  const sid = authHeader.slice(7).trim();
@@ -21,14 +24,15 @@ export default {
21
24
  return;
22
25
  }
23
26
 
27
+ ctx.userId = sessionData.id;
28
+ ctx.nickname = sessionData.nickname;
29
+ ctx.roleCode = sessionData.roleCode;
30
+ ctx.roleType = sessionData.roleType;
31
+
24
32
  try {
25
33
  await befly.redis.expire(sessionKey, ttlSeconds);
26
- ctx.userId = sessionData.id;
27
- ctx.nickname = sessionData.nickname;
28
- ctx.roleCode = sessionData.roleCode;
29
- ctx.roleType = sessionData.roleType;
30
- } catch {
31
- return;
34
+ } catch (error) {
35
+ Logger.warn("刷新会话有效期失败", { error: error, sessionKey: sessionKey });
32
36
  }
33
37
  }
34
38
  }
package/hooks/parser.js CHANGED
@@ -1,81 +1,77 @@
1
- import { xmlParse } from "#befly/lib/xmlParse.js";
2
1
  import { ErrorResponse } from "#befly/utils/response.js";
3
2
 
4
3
  import { HOOK_ORDER } from "../configs/constConfig.js";
4
+ import { snakeCase } from "../utils/util.js";
5
+
6
+ function parseScalar(value, schema) {
7
+ if (schema.paramType === "integer" && /^-?\d+$/.test(value)) return Number(value);
8
+ if (schema.paramType === "number" && value.trim() && Number.isFinite(Number(value))) return Number(value);
9
+ if (schema.paramType === "boolean" && ["true", "1"].includes(value)) return true;
10
+ if (schema.paramType === "boolean" && ["false", "0"].includes(value)) return false;
11
+ return value;
12
+ }
13
+
14
+ function parseQuery(url, fields) {
15
+ const params = new URL(url).searchParams;
16
+ const body = {};
17
+
18
+ for (const [field, schema] of Object.entries(fields)) {
19
+ const key = params.has(field) ? field : snakeCase(field);
20
+ if (!params.has(key)) continue;
21
+ const values = params.getAll(key);
22
+
23
+ if (schema.paramType === "array") {
24
+ body[field] = values.map((value) => parseScalar(value, schema.items || {}));
25
+ } else if (["object", "record"].includes(schema.paramType)) {
26
+ try {
27
+ body[field] = JSON.parse(values.at(-1));
28
+ } catch {
29
+ body[field] = values.at(-1);
30
+ }
31
+ } else {
32
+ body[field] = parseScalar(values.at(-1), schema);
33
+ }
34
+ }
35
+
36
+ return body;
37
+ }
5
38
 
6
- /**
7
- * 请求参数解析钩子
8
- * - GET 请求:解析 URL 查询参数
9
- * - POST 请求:解析 JSON 或 XML 请求体
10
- * - 仅负责解析与合并参数;字段过滤/映射/校验由 validator hook 处理
11
- * - body: "raw" 时跳过解析,由 handler 自行处理原始请求
12
- */
13
39
  export default {
40
+ name: "参数解析",
14
41
  order: HOOK_ORDER.PARSER,
15
42
  handler: async (befly, ctx) => {
16
- // apiBody=raw 模式:跳过解析,保留原始请求供 handler 自行处理
17
- // 适用于:微信回调、支付回调、webhook 等需要手动解密/验签的场景(大小由 handler/upload.maxSize 控制)
18
43
  if (ctx.apiBody === "raw") {
19
44
  ctx.body = {};
20
45
  return;
21
46
  }
22
47
 
23
- // 请求体大小限制(bodyLimit):按 Content-Length 前置拦截,避免 JSON/XML 整体读入内存
24
48
  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");
49
+ if (contentLength > befly.config.bodyLimit) {
50
+ ctx.response = ErrorResponse(ctx, "请求体过大", 1, null, { limit: befly.config.bodyLimit, actual: contentLength }, "parser");
28
51
  return;
29
52
  }
30
53
 
31
- const queryParams = Object.fromEntries(new URL(ctx.url).searchParams);
32
- const contentType = ctx.headers.get("content-type") || "";
33
- // GET 请求:解析查询参数
54
+ const query = parseQuery(ctx.url, ctx.apiFields);
34
55
  if (ctx.method === "GET") {
35
- ctx.body = queryParams;
36
- } else if (ctx.method === "POST") {
37
- // POST 请求:解析请求体
38
- try {
39
- // JSON 格式
40
- if (contentType.includes("application/json")) {
41
- const body = await ctx.req.json();
42
- // 合并 URL 参数和请求体(请求体优先)
43
- ctx.body = { ...queryParams, ...body };
44
- } else if (contentType.includes("application/xml") || contentType.includes("text/xml")) {
45
- // XML 格式
46
- const text = await ctx.req.text();
47
- // 合并 URL 参数和请求体(请求体优先)
48
- ctx.body = { ...queryParams, ...xmlParse(text) };
49
- } else {
50
- // 不支持的 Content-Type
51
- ctx.response = ErrorResponse(
52
- ctx,
53
- "无效的请求参数格式",
54
- 1,
55
- null,
56
- {
57
- location: "content-type",
58
- value: contentType
59
- },
60
- "parser"
61
- );
62
- return;
63
- }
64
- } catch {
65
- // 解析失败:属于客户端输入错误,返回安全 detail(不回传异常栈/原始 body)
66
- ctx.response = ErrorResponse(
67
- ctx,
68
- "无效的请求参数格式",
69
- 1,
70
- null,
71
- {
72
- location: "body",
73
- reason: contentType.includes("application/json") ? "invalid_json" : contentType.includes("xml") ? "invalid_xml" : "invalid_body"
74
- },
75
- "parser"
76
- );
56
+ ctx.body = query;
57
+ return;
58
+ }
59
+
60
+ const contentType = ctx.headers.get("content-type") || "";
61
+ try {
62
+ if (contentType.includes("application/json")) {
63
+ ctx.body = { ...query, ...(await ctx.req.json()) };
64
+ return;
65
+ }
66
+ if (contentType.includes("application/xml") || contentType.includes("text/xml")) {
67
+ ctx.body = { ...query, ...Bun.XML.parse(await ctx.req.text()) };
77
68
  return;
78
69
  }
70
+ } catch {
71
+ ctx.response = ErrorResponse(ctx, "无效的请求参数格式", 1, null, { location: "body", reason: contentType.includes("json") ? "invalid_json" : "invalid_xml" }, "parser");
72
+ return;
79
73
  }
74
+
75
+ ctx.response = ErrorResponse(ctx, "无效的请求参数格式", 1, null, { location: "content-type", value: contentType }, "parser");
80
76
  }
81
77
  };
@@ -6,12 +6,15 @@ import { ErrorResponse } from "../utils/response.js";
6
6
  /**
7
7
  * 权限检查钩子
8
8
  * - 接口无需权限(auth=false):直接通过
9
- * - auth ctx.roleType 白名单(string[]):登录后按 ctx.roleType 放行
9
+ * - auth 为角色类型白名单(string[],如 ["admin"] / ["user"]):按 ctx.roleType 校验。
10
+ * 类型与角色解耦:管理员表(beflyAdmin)roleType 固定 "admin",用户表固定 "user",
11
+ * 任意角色 code 的管理员共享 "admin" 类型门槛,细粒度授权由下方集合检查(roleCode)负责
10
12
  * - 用户未登录:返回 401
11
- * - 开发者角色(dev):最高权限,直接通过
13
+ * - 开发者角色(dev):最高权限,直接通过(唯一内置角色,不可创建)
12
14
  * - 其他角色:检查 Redis 中的角色权限集合
13
15
  */
14
16
  export default {
17
+ name: "权限检查",
15
18
  order: HOOK_ORDER.PERMISSION,
16
19
  handler: async (befly, ctx) => {
17
20
  // 1. 接口无需权限