befly 3.17.0 → 3.17.3

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 (150) hide show
  1. package/README.md +6 -0
  2. package/apis/admin/cacheRefresh.js +122 -0
  3. package/apis/admin/del.js +34 -0
  4. package/apis/admin/detail.js +23 -0
  5. package/apis/admin/ins.js +69 -0
  6. package/apis/admin/list.js +28 -0
  7. package/apis/admin/upd.js +95 -0
  8. package/apis/api/all.js +24 -0
  9. package/apis/api/list.js +31 -0
  10. package/apis/auth/login.js +123 -0
  11. package/apis/auth/sendSmsCode.js +24 -0
  12. package/apis/dashboard/configStatus.js +39 -0
  13. package/apis/dashboard/environmentInfo.js +43 -0
  14. package/apis/dashboard/performanceMetrics.js +20 -0
  15. package/apis/dashboard/permissionStats.js +27 -0
  16. package/apis/dashboard/serviceStatus.js +75 -0
  17. package/apis/dashboard/systemInfo.js +19 -0
  18. package/apis/dashboard/systemOverview.js +30 -0
  19. package/apis/dashboard/systemResources.js +106 -0
  20. package/apis/dict/all.js +23 -0
  21. package/apis/dict/del.js +16 -0
  22. package/apis/dict/detail.js +27 -0
  23. package/apis/dict/ins.js +51 -0
  24. package/apis/dict/items.js +30 -0
  25. package/apis/dict/list.js +36 -0
  26. package/apis/dict/upd.js +74 -0
  27. package/apis/dictType/all.js +16 -0
  28. package/apis/dictType/del.js +38 -0
  29. package/apis/dictType/detail.js +20 -0
  30. package/apis/dictType/ins.js +37 -0
  31. package/apis/dictType/list.js +26 -0
  32. package/apis/dictType/upd.js +51 -0
  33. package/apis/email/config.js +25 -0
  34. package/apis/email/logList.js +23 -0
  35. package/apis/email/send.js +66 -0
  36. package/apis/email/verify.js +21 -0
  37. package/apis/loginLog/list.js +23 -0
  38. package/apis/menu/all.js +41 -0
  39. package/apis/menu/list.js +25 -0
  40. package/apis/operateLog/list.js +23 -0
  41. package/apis/role/all.js +21 -0
  42. package/apis/role/apiSave.js +43 -0
  43. package/apis/role/apis.js +22 -0
  44. package/apis/role/del.js +49 -0
  45. package/apis/role/detail.js +32 -0
  46. package/apis/role/ins.js +46 -0
  47. package/apis/role/list.js +27 -0
  48. package/apis/role/menuSave.js +42 -0
  49. package/apis/role/menus.js +22 -0
  50. package/apis/role/save.js +40 -0
  51. package/apis/role/upd.js +50 -0
  52. package/apis/sysConfig/all.js +16 -0
  53. package/apis/sysConfig/del.js +36 -0
  54. package/apis/sysConfig/get.js +49 -0
  55. package/apis/sysConfig/ins.js +50 -0
  56. package/apis/sysConfig/list.js +24 -0
  57. package/apis/sysConfig/upd.js +62 -0
  58. package/checks/api.js +55 -0
  59. package/checks/config.js +107 -0
  60. package/checks/hook.js +38 -0
  61. package/checks/menu.js +58 -0
  62. package/checks/plugin.js +38 -0
  63. package/checks/table.js +78 -0
  64. package/configs/beflyConfig.json +61 -0
  65. package/configs/beflyMenus.json +85 -0
  66. package/configs/constConfig.js +34 -0
  67. package/configs/regexpAlias.json +55 -0
  68. package/hooks/auth.js +34 -0
  69. package/hooks/cors.js +39 -0
  70. package/hooks/parser.js +90 -0
  71. package/hooks/permission.js +71 -0
  72. package/hooks/validator.js +43 -0
  73. package/index.js +334 -0
  74. package/lib/cacheHelper.js +483 -0
  75. package/lib/cacheKeys.js +42 -0
  76. package/lib/connect.js +120 -0
  77. package/lib/dbHelper/builders.js +698 -0
  78. package/lib/dbHelper/context.js +131 -0
  79. package/lib/dbHelper/dataOps.js +505 -0
  80. package/lib/dbHelper/execute.js +65 -0
  81. package/lib/dbHelper/index.js +27 -0
  82. package/lib/dbHelper/transaction.js +43 -0
  83. package/lib/dbHelper/util.js +58 -0
  84. package/lib/dbHelper/validate.js +549 -0
  85. package/lib/emailHelper.js +110 -0
  86. package/lib/logger.js +604 -0
  87. package/lib/redisHelper.js +684 -0
  88. package/lib/sqlBuilder/batch.js +113 -0
  89. package/lib/sqlBuilder/check.js +150 -0
  90. package/lib/sqlBuilder/compiler.js +347 -0
  91. package/lib/sqlBuilder/errors.js +60 -0
  92. package/lib/sqlBuilder/index.js +218 -0
  93. package/lib/sqlBuilder/parser.js +296 -0
  94. package/lib/sqlBuilder/util.js +260 -0
  95. package/lib/validator.js +303 -0
  96. package/package.json +19 -12
  97. package/paths.js +112 -0
  98. package/plugins/cache.js +16 -0
  99. package/plugins/config.js +11 -0
  100. package/plugins/email.js +27 -0
  101. package/plugins/logger.js +20 -0
  102. package/plugins/mysql.js +36 -0
  103. package/plugins/redis.js +34 -0
  104. package/plugins/tool.js +155 -0
  105. package/router/api.js +140 -0
  106. package/router/static.js +71 -0
  107. package/sql/admin.sql +18 -0
  108. package/sql/api.sql +12 -0
  109. package/sql/dict.sql +13 -0
  110. package/sql/dictType.sql +12 -0
  111. package/sql/emailLog.sql +20 -0
  112. package/sql/loginLog.sql +25 -0
  113. package/sql/menu.sql +12 -0
  114. package/sql/operateLog.sql +22 -0
  115. package/sql/role.sql +14 -0
  116. package/sql/sysConfig.sql +16 -0
  117. package/sync/api.js +93 -0
  118. package/sync/cache.js +13 -0
  119. package/sync/dev.js +171 -0
  120. package/sync/menu.js +99 -0
  121. package/tables/admin.json +56 -0
  122. package/tables/api.json +26 -0
  123. package/tables/dict.json +30 -0
  124. package/tables/dictType.json +24 -0
  125. package/tables/emailLog.json +61 -0
  126. package/tables/loginLog.json +86 -0
  127. package/tables/menu.json +24 -0
  128. package/tables/operateLog.json +68 -0
  129. package/tables/role.json +32 -0
  130. package/tables/sysConfig.json +43 -0
  131. package/utils/calcPerfTime.js +13 -0
  132. package/utils/cors.js +17 -0
  133. package/utils/deepMerge.js +78 -0
  134. package/utils/fieldClear.js +65 -0
  135. package/utils/formatYmdHms.js +23 -0
  136. package/utils/formatZodIssues.js +109 -0
  137. package/utils/getClientIp.js +47 -0
  138. package/utils/importDefault.js +51 -0
  139. package/utils/is.js +462 -0
  140. package/utils/loggerUtils.js +185 -0
  141. package/utils/processInfo.js +39 -0
  142. package/utils/regexpUtil.js +52 -0
  143. package/utils/response.js +114 -0
  144. package/utils/scanFiles.js +124 -0
  145. package/utils/scanSources.js +68 -0
  146. package/utils/sortModules.js +75 -0
  147. package/utils/toSessionTtlSeconds.js +14 -0
  148. package/utils/util.js +374 -0
  149. package/befly.js +0 -16413
  150. package/befly.min.js +0 -72
@@ -0,0 +1,61 @@
1
+ {
2
+ "runMode": "development",
3
+ "appName": "野蜂飞舞",
4
+ "appPort": 3000,
5
+ "appHost": "127.0.0.1",
6
+ "devEmail": "dev@qq.com",
7
+ "devPassword": "111111",
8
+ "bodyLimit": 1048576,
9
+ "tz": "Asia/Shanghai",
10
+
11
+ "logger": {
12
+ "debug": 1,
13
+ "excludeFields": ["password", "token", "secret"],
14
+ "dir": "./logs",
15
+ "console": 1,
16
+ "maxSize": 20,
17
+ "maxStringLen": 100,
18
+ "maxArrayItems": 100
19
+ },
20
+
21
+ "mysql": {
22
+ "idMode": "timeId",
23
+ "hostname": "127.0.0.1",
24
+ "port": 3306,
25
+ "username": "root",
26
+ "password": "root",
27
+ "database": "befly_dev",
28
+ "max": 10
29
+ },
30
+
31
+ "redis": {
32
+ "hostname": "127.0.0.1",
33
+ "port": 6379,
34
+ "username": "",
35
+ "password": "",
36
+ "db": 0,
37
+ "prefix": "befly_demo"
38
+ },
39
+
40
+ "session": {
41
+ "expireDays": 7
42
+ },
43
+
44
+ "cors": {
45
+ "origin": "*",
46
+ "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
47
+ "allowedHeaders": "Content-Type,Authorization",
48
+ "exposedHeaders": "",
49
+ "maxAge": 86400,
50
+ "credentials": "true"
51
+ },
52
+
53
+ "rateLimit": {
54
+ "enable": 1,
55
+ "defaultLimit": 1000,
56
+ "defaultWindow": 60,
57
+ "key": "ip",
58
+ "skipRoutes": [],
59
+ "rules": []
60
+ }
61
+ }
@@ -0,0 +1,85 @@
1
+ [
2
+ {
3
+ "name": "首页",
4
+ "path": "/",
5
+ "sort": 1
6
+ },
7
+ {
8
+ "name": "人员管理",
9
+ "path": "/people",
10
+ "sort": 2,
11
+ "children": [
12
+ {
13
+ "name": "管理员",
14
+ "path": "/admin",
15
+ "sort": 1
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "name": "权限设置",
21
+ "path": "/permission",
22
+ "sort": 3,
23
+ "children": [
24
+ {
25
+ "name": "角色管理",
26
+ "path": "/role",
27
+ "sort": 1
28
+ },
29
+ {
30
+ "name": "菜单列表",
31
+ "path": "/menu",
32
+ "sort": 2
33
+ },
34
+ {
35
+ "name": "接口列表",
36
+ "path": "/api",
37
+ "sort": 3
38
+ }
39
+ ]
40
+ },
41
+ {
42
+ "name": "配置管理",
43
+ "path": "/config",
44
+ "sort": 4,
45
+ "children": [
46
+ {
47
+ "name": "字典类型",
48
+ "path": "/dictType",
49
+ "sort": 1
50
+ },
51
+ {
52
+ "name": "字典列表",
53
+ "path": "/dict",
54
+ "sort": 2
55
+ },
56
+ {
57
+ "name": "系统配置",
58
+ "path": "/system",
59
+ "sort": 3
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "name": "日志管理",
65
+ "path": "/log",
66
+ "sort": 5,
67
+ "children": [
68
+ {
69
+ "name": "登录日志",
70
+ "path": "/login",
71
+ "sort": 1
72
+ },
73
+ {
74
+ "name": "邮件日志",
75
+ "path": "/email",
76
+ "sort": 2
77
+ },
78
+ {
79
+ "name": "操作日志",
80
+ "path": "/operate",
81
+ "sort": 3
82
+ }
83
+ ]
84
+ }
85
+ ]
@@ -0,0 +1,34 @@
1
+ // ==========================
2
+ // 数据库类型分类
3
+ // ==========================
4
+
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
+ export const RUN_MODE_VALUES = ["development", "production"];
32
+
33
+ // 数据库 ID 生成模式可选值
34
+ export const DB_ID_MODE_VALUES = ["timeId", "autoId"];
@@ -0,0 +1,55 @@
1
+ {
2
+ "number": "^\\d+$",
3
+ "integer": "^-?\\d+$",
4
+ "float": "^-?\\d+(\\.\\d+)?$",
5
+ "positive": "^[1-9]\\d*$",
6
+ "negative": "^-\\d+$",
7
+ "zero": "^0$",
8
+ "word": "^[a-zA-Z]+$",
9
+ "alphanumeric": "^[a-zA-Z0-9]+$",
10
+ "alphanumeric_": "^[a-zA-Z0-9_]+$",
11
+ "alphanumericDash_": "^[a-zA-Z0-9_-]+$",
12
+ "lowercase": "^[a-z]+$",
13
+ "uppercase": "^[A-Z]+$",
14
+ "chinese": "^[\\u4e00-\\u9fa5]+$",
15
+ "chineseWord": "^[\\u4e00-\\u9fa5a-zA-Z]+$",
16
+ "email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
17
+ "phone": "^1[3-9]\\d{9}$",
18
+ "telephone": "^0\\d{2,3}-?\\d{7,8}$",
19
+ "url": "^https?://",
20
+ "ip": "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
21
+ "ipv6": "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$",
22
+ "domain": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$",
23
+ "uuid": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
24
+ "hex": "^[0-9a-fA-F]+$",
25
+ "base64": "^[A-Za-z0-9+/=]+$",
26
+ "md5": "^[a-f0-9]{32}$",
27
+ "sha1": "^[a-f0-9]{40}$",
28
+ "sha256": "^[a-f0-9]{64}$",
29
+ "date": "^\\d{4}-\\d{2}-\\d{2}$",
30
+ "time": "^\\d{2}:\\d{2}:\\d{2}$",
31
+ "datetime": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
32
+ "year": "^\\d{4}$",
33
+ "month": "^(0[1-9]|1[0-2])$",
34
+ "day": "^(0[1-9]|[12]\\d|3[01])$",
35
+ "variable": "^[a-zA-Z_][a-zA-Z0-9_]*$",
36
+ "constant": "^[A-Z][A-Z0-9_]*$",
37
+ "package": "^[a-z][a-z0-9-]*$",
38
+ "idCard": "^\\d{17}[\\dXx]$",
39
+ "passport": "^[a-zA-Z0-9]{5,17}$",
40
+ "bankCard": "^\\d{16,19}$",
41
+ "wechat": "^[a-zA-Z][a-zA-Z0-9_-]{5,19}$",
42
+ "qq": "^[1-9]\\d{4,10}$",
43
+ "alipay": "^(1[3-9]\\d{9}|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})$",
44
+ "username": "^[a-zA-Z][a-zA-Z0-9_]{3,19}$",
45
+ "nickname": "^[\\u4e00-\\u9fa5a-zA-Z0-9]{2,20}$",
46
+ "passwordWeak": "^.{6,}$",
47
+ "passwordMedium": "^(?=.*[a-zA-Z])(?=.*\\d).{8,}$",
48
+ "passwordStrong": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}$",
49
+ "licensePlate": "^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$",
50
+ "postalCode": "^\\d{6}$",
51
+ "semver": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?(\\+[a-zA-Z0-9.]+)?$",
52
+ "colorHex": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
53
+ "empty": "^$",
54
+ "notempty": ".+"
55
+ }
package/hooks/auth.js ADDED
@@ -0,0 +1,34 @@
1
+ import { toSessionTtlSeconds } from "../utils/toSessionTtlSeconds.js";
2
+
3
+ export default {
4
+ deps: ["cors"],
5
+ handler: async (befly, ctx) => {
6
+ const authHeader = ctx.req.headers.get("authorization");
7
+ const ttlSeconds = toSessionTtlSeconds(befly.config?.session?.expireDays);
8
+
9
+ if (authHeader && authHeader.startsWith("Bearer ")) {
10
+ const sid = authHeader.substring(7).trim();
11
+
12
+ if (!sid) {
13
+ return;
14
+ }
15
+
16
+ const sessionKey = `session:${sid}`;
17
+ const sessionData = await befly.redis.getObject(sessionKey);
18
+
19
+ if (!sessionData?.id) {
20
+ return;
21
+ }
22
+
23
+ try {
24
+ await befly.redis.expire(sessionKey, ttlSeconds);
25
+ ctx.userId = sessionData.id;
26
+ ctx.nickname = sessionData.nickname;
27
+ ctx.roleCode = sessionData.roleCode;
28
+ ctx.roleType = sessionData.roleType;
29
+ } catch {
30
+ return;
31
+ }
32
+ }
33
+ }
34
+ };
package/hooks/cors.js ADDED
@@ -0,0 +1,39 @@
1
+ // 相对导入
2
+ import { setCorsOptions } from "../utils/cors.js";
3
+
4
+ /**
5
+ * CORS 跨域处理钩子
6
+ * 设置跨域响应头并处理 OPTIONS 预检请求
7
+ */
8
+ export default {
9
+ deps: [],
10
+ handler: async (befly, ctx) => {
11
+ const req = ctx.req;
12
+
13
+ // 合并默认配置和用户配置
14
+ const defaultConfig = {
15
+ origin: "*",
16
+ methods: "GET, POST, OPTIONS",
17
+ allowedHeaders: "Content-Type, Authorization, authorization, token",
18
+ exposedHeaders: "Content-Range, X-Content-Range, Authorization, authorization, token",
19
+ maxAge: 86400,
20
+ credentials: "true"
21
+ };
22
+
23
+ const corsConfig = Object.assign({}, defaultConfig, befly.config && befly.config.cors ? befly.config.cors : {});
24
+
25
+ // 设置 CORS 响应头
26
+ const headers = setCorsOptions(req, corsConfig);
27
+
28
+ ctx.corsHeaders = headers;
29
+
30
+ // 处理 OPTIONS 预检请求
31
+ if (req.method === "OPTIONS") {
32
+ ctx.response = new Response(null, {
33
+ status: 204,
34
+ headers: headers
35
+ });
36
+ return;
37
+ }
38
+ }
39
+ };
@@ -0,0 +1,90 @@
1
+ // 外部依赖
2
+ import { XMLParser } from "fast-xml-parser";
3
+
4
+ import { ErrorResponse } from "../utils/response.js";
5
+
6
+ const xmlParser = new XMLParser();
7
+
8
+ /**
9
+ * 请求参数解析钩子
10
+ * - GET 请求:解析 URL 查询参数
11
+ * - POST 请求:解析 JSON 或 XML 请求体
12
+ * - 仅负责解析与合并参数;字段过滤/映射/校验由 validator hook 处理
13
+ * - body: "raw" 时跳过解析,由 handler 自行处理原始请求
14
+ */
15
+ export default {
16
+ deps: ["auth"],
17
+ handler: async (befly, ctx) => {
18
+ if (!ctx.apiPath) {
19
+ return;
20
+ }
21
+
22
+ // body=raw 模式:跳过解析,保留原始请求供 handler 自行处理
23
+ // 适用于:微信回调、支付回调、webhook 等需要手动解密/验签的场景
24
+ if (ctx.body === "raw") {
25
+ ctx.body = {};
26
+ return;
27
+ }
28
+
29
+ // GET 请求:解析查询参数
30
+ if (ctx.req.method === "GET") {
31
+ const url = new URL(ctx.req.url);
32
+ const params = Object.fromEntries(url.searchParams);
33
+ ctx.body = params;
34
+ } else if (ctx.req.method === "POST") {
35
+ // POST 请求:解析请求体
36
+ const contentType = ctx.req.headers.get("content-type") || "";
37
+ // 获取 URL 查询参数(POST 请求也可能带参数)
38
+ const url = new URL(ctx.req.url);
39
+ const queryParams = Object.fromEntries(url.searchParams);
40
+
41
+ try {
42
+ // JSON 格式
43
+ if (contentType.includes("application/json")) {
44
+ const body = await ctx.req.json();
45
+ // 合并 URL 参数和请求体(请求体优先)
46
+ const merged = Object.assign({}, queryParams, body);
47
+ ctx.body = merged;
48
+ } else if (contentType.includes("application/xml") || contentType.includes("text/xml")) {
49
+ // XML 格式
50
+ const text = await ctx.req.text();
51
+ const parsed = xmlParser.parse(text);
52
+ // 提取根节点内容(如 xml),使 body 扁平化
53
+ const rootKey = Object.keys(parsed)[0];
54
+ const body = rootKey && typeof parsed[rootKey] === "object" ? parsed[rootKey] : parsed;
55
+ // 合并 URL 参数和请求体(请求体优先)
56
+ const merged = Object.assign({}, queryParams, body);
57
+ ctx.body = merged;
58
+ } else {
59
+ // 不支持的 Content-Type
60
+ ctx.response = ErrorResponse(
61
+ ctx,
62
+ "无效的请求参数格式",
63
+ 1,
64
+ null,
65
+ {
66
+ location: "content-type",
67
+ value: contentType
68
+ },
69
+ "parser"
70
+ );
71
+ return;
72
+ }
73
+ } catch {
74
+ // 解析失败:属于客户端输入错误,返回安全 detail(不回传异常栈/原始 body)
75
+ ctx.response = ErrorResponse(
76
+ ctx,
77
+ "无效的请求参数格式",
78
+ 1,
79
+ null,
80
+ {
81
+ location: "body",
82
+ reason: contentType.includes("application/json") ? "invalid_json" : contentType.includes("xml") ? "invalid_xml" : "invalid_body"
83
+ },
84
+ "parser"
85
+ );
86
+ return;
87
+ }
88
+ }
89
+ }
90
+ };
@@ -0,0 +1,71 @@
1
+ import { CacheKeys } from "../lib/cacheKeys.js";
2
+ import { Logger } from "../lib/logger.js";
3
+ // 相对导入
4
+ import { ErrorResponse } from "../utils/response.js";
5
+ import { isNonEmptyString, isString, isValidPositiveInt } from "../utils/is.js";
6
+
7
+ /**
8
+ * 权限检查钩子
9
+ * - 接口无需权限(auth=false):直接通过
10
+ * - auth 为 ctx.roleType 白名单(string[]):登录后按 ctx.roleType 放行
11
+ * - 用户未登录:返回 401
12
+ * - 开发者角色(dev):最高权限,直接通过
13
+ * - 其他角色:检查 Redis 中的角色权限集合
14
+ */
15
+ export default {
16
+ deps: ["validator"],
17
+ handler: async (befly, ctx) => {
18
+ // 1. 接口无需权限
19
+ if (ctx.auth === false) {
20
+ return;
21
+ }
22
+
23
+ // 2. 用户未登录
24
+ if (!isValidPositiveInt(ctx.userId)) {
25
+ ctx.response = ErrorResponse(ctx, "未登录", 1, null, null, "auth");
26
+ return;
27
+ }
28
+
29
+ // 3. 开发者权限(最高权限)
30
+ if (ctx.roleCode === "dev") {
31
+ return;
32
+ }
33
+
34
+ // 3.5 auth 为角色类型白名单时,仅做 ctx.roleType 校验
35
+ if (Array.isArray(ctx.auth) && ctx.auth.includes(ctx.roleType) === false) {
36
+ ctx.response = ErrorResponse(
37
+ ctx,
38
+ `无权访问 ${ctx.apiName} 接口`,
39
+ 1,
40
+ null,
41
+ {
42
+ apiName: ctx.apiName,
43
+ apiPath: ctx.apiPath
44
+ },
45
+ "permission"
46
+ );
47
+ return;
48
+ }
49
+
50
+ // 4. 角色权限检查
51
+ // apiPath 在 apiHandler 中已统一生成并写入 ctx.apiPath
52
+
53
+ // 极简方案:每个角色一个 Set,直接判断成员是否存在
54
+ const hasPermission = await befly.redis.sismember(CacheKeys.roleApis(ctx.roleCode), ctx.apiPath);
55
+
56
+ if (!hasPermission) {
57
+ ctx.response = ErrorResponse(
58
+ ctx,
59
+ `无权访问 ${ctx.apiName} 接口`,
60
+ 1,
61
+ null,
62
+ {
63
+ apiName: ctx.apiName,
64
+ apiPath: ctx.apiPath
65
+ },
66
+ "permission"
67
+ );
68
+ return;
69
+ }
70
+ }
71
+ };
@@ -0,0 +1,43 @@
1
+ // 相对导入
2
+ import { Validator } from "../lib/validator.js";
3
+ import { ErrorResponse } from "../utils/response.js";
4
+ import { isPlainObject } from "../utils/is.js";
5
+ import { snakeCase } from "../utils/util.js";
6
+
7
+ /**
8
+ * 参数验证钩子
9
+ * 根据 API 定义的 fields 和 required 验证请求参数
10
+ */
11
+ export default {
12
+ deps: ["parser"],
13
+ handler: async (befly, ctx) => {
14
+ // 仅保留 fields 中声明的字段,并支持 snake_case 入参回退(例如 agent_id -> agentId)
15
+ const rawBody = isPlainObject(ctx.body) ? ctx.body : {};
16
+ const nextBody = {};
17
+
18
+ for (const [field] of Object.entries(ctx.fields)) {
19
+ let value = rawBody[field];
20
+
21
+ if (value === undefined) {
22
+ const snakeField = snakeCase(field);
23
+ if (rawBody[snakeField] !== undefined) {
24
+ value = rawBody[snakeField];
25
+ }
26
+ }
27
+
28
+ if (value !== undefined) {
29
+ nextBody[field] = value;
30
+ }
31
+ }
32
+
33
+ ctx.body = nextBody;
34
+
35
+ // 验证参数
36
+ const result = Validator.validate(ctx.body, ctx.fields, ctx.required || []);
37
+
38
+ if (result.code !== 0) {
39
+ ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors, "validator");
40
+ return;
41
+ }
42
+ }
43
+ };