befly 3.9.40 → 3.10.1

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 (144) hide show
  1. package/README.md +47 -19
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +17 -11
  9. package/docs/api/api.md +16 -2
  10. package/docs/guide/quickstart.md +31 -10
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +26 -14
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +5 -328
  16. package/docs/reference/addon.md +0 -4
  17. package/docs/reference/config.md +14 -31
  18. package/docs/reference/logger.md +3 -3
  19. package/docs/reference/sync.md +132 -237
  20. package/docs/reference/table.md +28 -30
  21. package/hooks/auth.ts +3 -4
  22. package/hooks/cors.ts +4 -6
  23. package/hooks/parser.ts +3 -4
  24. package/hooks/permission.ts +3 -4
  25. package/hooks/validator.ts +3 -4
  26. package/lib/cacheHelper.ts +89 -153
  27. package/lib/cacheKeys.ts +1 -1
  28. package/lib/connect.ts +9 -13
  29. package/lib/dbDialect.ts +285 -0
  30. package/lib/dbHelper.ts +179 -507
  31. package/lib/dbUtils.ts +450 -0
  32. package/lib/logger.ts +41 -5
  33. package/lib/redisHelper.ts +1 -0
  34. package/lib/sqlBuilder.ts +358 -58
  35. package/lib/sqlCheck.ts +136 -0
  36. package/lib/validator.ts +1 -1
  37. package/loader/loadApis.ts +23 -126
  38. package/loader/loadHooks.ts +31 -46
  39. package/loader/loadPlugins.ts +37 -52
  40. package/main.ts +58 -19
  41. package/package.json +24 -25
  42. package/paths.ts +14 -14
  43. package/plugins/cache.ts +12 -6
  44. package/plugins/cipher.ts +2 -2
  45. package/plugins/config.ts +6 -8
  46. package/plugins/db.ts +14 -19
  47. package/plugins/jwt.ts +6 -7
  48. package/plugins/logger.ts +7 -9
  49. package/plugins/redis.ts +8 -10
  50. package/plugins/tool.ts +3 -4
  51. package/router/api.ts +3 -2
  52. package/router/static.ts +7 -5
  53. package/sync/syncApi.ts +80 -235
  54. package/sync/syncCache.ts +16 -0
  55. package/sync/syncDev.ts +167 -202
  56. package/sync/syncMenu.ts +230 -444
  57. package/sync/syncTable.ts +1247 -0
  58. package/tests/_mocks/mockSqliteDb.ts +204 -0
  59. package/tests/addonHelper-cache.test.ts +32 -0
  60. package/tests/apiHandler-routePath-only.test.ts +32 -0
  61. package/tests/cacheHelper.test.ts +16 -51
  62. package/tests/checkApi-routePath-strict.test.ts +166 -0
  63. package/tests/checkMenu.test.ts +346 -0
  64. package/tests/checkTable-smoke.test.ts +157 -0
  65. package/tests/dbDialect-cache.test.ts +23 -0
  66. package/tests/dbDialect.test.ts +46 -0
  67. package/tests/dbHelper-advanced.test.ts +1 -1
  68. package/tests/dbHelper-all-array-types.test.ts +15 -15
  69. package/tests/dbHelper-batch-write.test.ts +90 -0
  70. package/tests/dbHelper-columns.test.ts +36 -54
  71. package/tests/dbHelper-execute.test.ts +26 -26
  72. package/tests/dbHelper-joins.test.ts +85 -176
  73. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  75. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  76. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  77. package/tests/logger.test.ts +6 -6
  78. package/tests/redisHelper.test.ts +6 -1
  79. package/tests/scanFiles-routePath.test.ts +46 -0
  80. package/tests/smoke-sql.test.ts +24 -0
  81. package/tests/sqlBuilder-advanced.test.ts +18 -5
  82. package/tests/sqlBuilder.test.ts +24 -0
  83. package/tests/sync-init-guard.test.ts +105 -0
  84. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  85. package/tests/syncApi-obsolete-records.test.ts +69 -0
  86. package/tests/syncApi-type-compat.test.ts +72 -0
  87. package/tests/syncDev-permissions.test.ts +81 -0
  88. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  89. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  90. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  91. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  92. package/tests/syncMenu-paths.test.ts +0 -9
  93. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  94. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  95. package/tests/syncTable-constants.test.ts +101 -0
  96. package/tests/syncTable-db-integration.test.ts +237 -0
  97. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  98. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  99. package/tests/syncTable-schema.test.ts +99 -0
  100. package/tests/syncTable-testkit.test.ts +25 -0
  101. package/tests/syncTable-types.test.ts +122 -0
  102. package/tests/tableRef-and-deserialize.test.ts +67 -0
  103. package/tsconfig.json +1 -1
  104. package/types/api.d.ts +1 -1
  105. package/types/befly.d.ts +13 -12
  106. package/types/cache.d.ts +2 -2
  107. package/types/context.d.ts +1 -1
  108. package/types/database.d.ts +0 -5
  109. package/types/hook.d.ts +1 -10
  110. package/types/plugin.d.ts +2 -96
  111. package/types/sync.d.ts +19 -25
  112. package/utils/convertBigIntFields.ts +38 -0
  113. package/utils/disableMenusGlob.ts +85 -0
  114. package/utils/importDefault.ts +21 -0
  115. package/utils/isDirentDirectory.ts +23 -0
  116. package/utils/loadMenuConfigs.ts +145 -0
  117. package/utils/processFields.ts +25 -0
  118. package/utils/scanAddons.ts +72 -0
  119. package/utils/scanFiles.ts +129 -21
  120. package/utils/scanSources.ts +64 -0
  121. package/utils/sortModules.ts +137 -0
  122. package/checks/checkApp.ts +0 -55
  123. package/docs/cipher.md +0 -582
  124. package/docs/database.md +0 -1176
  125. package/hooks/rateLimit.ts +0 -276
  126. package/sync/syncAll.ts +0 -35
  127. package/sync/syncDb/apply.ts +0 -192
  128. package/sync/syncDb/constants.ts +0 -119
  129. package/sync/syncDb/ddl.ts +0 -251
  130. package/sync/syncDb/helpers.ts +0 -84
  131. package/sync/syncDb/schema.ts +0 -202
  132. package/sync/syncDb/sqlite.ts +0 -48
  133. package/sync/syncDb/table.ts +0 -207
  134. package/sync/syncDb/tableCreate.ts +0 -163
  135. package/sync/syncDb/types.ts +0 -132
  136. package/sync/syncDb/version.ts +0 -69
  137. package/sync/syncDb.ts +0 -168
  138. package/tests/rateLimit-hook.test.ts +0 -477
  139. package/tests/syncDb-constants.test.ts +0 -130
  140. package/tests/syncDb-schema.test.ts +0 -179
  141. package/tests/syncDb-types.test.ts +0 -139
  142. package/utils/addonHelper.ts +0 -90
  143. package/utils/modules.ts +0 -98
  144. package/utils/route.ts +0 -23
package/hooks/cors.ts CHANGED
@@ -2,7 +2,6 @@ import type { CorsConfig } from "../types/befly.js";
2
2
  // 类型导入
3
3
  import type { Hook } from "../types/hook.js";
4
4
 
5
- import { beflyConfig } from "../befly.config.js";
6
5
  // 相对导入
7
6
  import { setCorsOptions } from "../utils/cors.js";
8
7
 
@@ -10,8 +9,8 @@ import { setCorsOptions } from "../utils/cors.js";
10
9
  * CORS 跨域处理钩子
11
10
  * 设置跨域响应头并处理 OPTIONS 预检请求
12
11
  */
13
- const hook: Hook = {
14
- order: 2,
12
+ export default {
13
+ deps: [],
15
14
  handler: async (befly, ctx) => {
16
15
  const req = ctx.req;
17
16
 
@@ -25,7 +24,7 @@ const hook: Hook = {
25
24
  credentials: "true"
26
25
  };
27
26
 
28
- const corsConfig = { ...defaultConfig, ...beflyConfig.cors };
27
+ const corsConfig = Object.assign({}, defaultConfig, befly.config && befly.config.cors ? befly.config.cors : {});
29
28
 
30
29
  // 设置 CORS 响应头
31
30
  const headers = setCorsOptions(req, corsConfig);
@@ -41,5 +40,4 @@ const hook: Hook = {
41
40
  return;
42
41
  }
43
42
  }
44
- };
45
- export default hook;
43
+ } satisfies Hook;
package/hooks/parser.ts CHANGED
@@ -18,8 +18,8 @@ const xmlParser = new XMLParser();
18
18
  * - 根据 API 定义的 fields 过滤字段
19
19
  * - rawBody: true 时跳过解析,由 handler 自行处理原始请求
20
20
  */
21
- const hook: Hook = {
22
- order: 4,
21
+ export default {
22
+ deps: ["auth"],
23
23
  handler: async (befly, ctx) => {
24
24
  if (!ctx.api) return;
25
25
 
@@ -103,5 +103,4 @@ const hook: Hook = {
103
103
  }
104
104
  }
105
105
  }
106
- };
107
- export default hook;
106
+ } satisfies Hook;
@@ -13,8 +13,8 @@ import { ErrorResponse } from "../utils/response.js";
13
13
  * - 开发者角色(dev):最高权限,直接通过
14
14
  * - 其他角色:检查 Redis 中的角色权限集合
15
15
  */
16
- const hook: Hook = {
17
- order: 6,
16
+ export default {
17
+ deps: ["validator"],
18
18
  handler: async (befly, ctx) => {
19
19
  if (!ctx.api) return;
20
20
 
@@ -65,5 +65,4 @@ const hook: Hook = {
65
65
  return;
66
66
  }
67
67
  }
68
- };
69
- export default hook;
68
+ } satisfies Hook;
@@ -9,8 +9,8 @@ import { ErrorResponse } from "../utils/response.js";
9
9
  * 参数验证钩子
10
10
  * 根据 API 定义的 fields 和 required 验证请求参数
11
11
  */
12
- const hook: Hook = {
13
- order: 8,
12
+ export default {
13
+ deps: ["parser"],
14
14
  handler: async (befly, ctx) => {
15
15
  if (!ctx.api) return;
16
16
 
@@ -35,5 +35,4 @@ const hook: Hook = {
35
35
  return;
36
36
  }
37
37
  }
38
- };
39
- export default hook;
38
+ } satisfies Hook;
@@ -3,122 +3,79 @@
3
3
  * 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
4
4
  */
5
5
 
6
- import type { BeflyContext } from "../types/befly.js";
7
-
8
- import { makeRouteKey } from "../utils/route.js";
9
6
  import { CacheKeys } from "./cacheKeys.js";
10
7
  import { Logger } from "./logger.js";
11
8
 
9
+ type CacheHelperDb = {
10
+ tableExists(table: string): Promise<boolean>;
11
+ getAll(options: any): Promise<{ lists: any[] }>;
12
+ };
13
+
14
+ type CacheHelperRedis = {
15
+ setObject<T = any>(key: string, obj: T, ttl?: number | null): Promise<string | null>;
16
+ getObject<T = any>(key: string): Promise<T | null>;
17
+ del(key: string): Promise<number>;
18
+ delBatch(keys: string[]): Promise<number>;
19
+ sadd(key: string, members: string[]): Promise<number>;
20
+ saddBatch(items: Array<{ key: string; members: string[] }>): Promise<number>;
21
+ smembers(key: string): Promise<string[]>;
22
+ sismember(key: string, member: string): Promise<boolean>;
23
+ };
24
+
25
+ type CacheHelperDeps = {
26
+ db: CacheHelperDb;
27
+ redis: CacheHelperRedis;
28
+ };
29
+
12
30
  /**
13
31
  * 缓存助手类
14
32
  */
15
33
  export class CacheHelper {
16
- private befly: BeflyContext;
17
-
18
- private static readonly API_ID_IN_CHUNK_SIZE = 1000;
34
+ private db: CacheHelperDb;
35
+ private redis: CacheHelperRedis;
19
36
 
20
- private normalizeNumberIdList(value: unknown): number[] {
21
- if (value === null || value === undefined) return [];
37
+ constructor(deps: CacheHelperDeps) {
38
+ this.db = deps.db;
39
+ this.redis = deps.redis;
40
+ }
22
41
 
23
- const normalizeSingle = (item: unknown): number | null => {
24
- if (typeof item === "number") {
25
- if (!Number.isFinite(item)) return null;
26
- const intValue = Math.trunc(item);
27
- return Number.isSafeInteger(intValue) ? intValue : null;
28
- }
29
- if (typeof item === "bigint") {
30
- const intValue = Number(item);
31
- return Number.isSafeInteger(intValue) ? intValue : null;
32
- }
33
- if (typeof item === "string") {
34
- const trimmed = item.trim();
35
- if (!trimmed) return null;
36
- const intValue = Number.parseInt(trimmed, 10);
37
- return Number.isSafeInteger(intValue) ? intValue : null;
38
- }
39
- return null;
40
- };
41
-
42
- if (Array.isArray(value)) {
43
- const ids: number[] = [];
44
- for (const item of value) {
45
- const id = normalizeSingle(item);
46
- if (id !== null) ids.push(id);
47
- }
48
- return ids;
42
+ private assertApiPathname(value: unknown, errorPrefix: string): string {
43
+ if (typeof value !== "string") {
44
+ throw new Error(`${errorPrefix} 必须是字符串`);
49
45
  }
50
46
 
51
- if (typeof value === "string") {
52
- const str = value.trim();
53
- if (!str) return [];
54
-
55
- if (str.startsWith("[")) {
56
- try {
57
- const parsed = JSON.parse(str);
58
- return this.normalizeNumberIdList(parsed);
59
- } catch {
60
- // ignore
61
- }
62
- }
63
-
64
- const ids: number[] = [];
65
- const parts = str.split(",");
66
- for (const part of parts) {
67
- const id = normalizeSingle(part);
68
- if (id !== null) ids.push(id);
69
- }
70
- return ids;
47
+ const trimmed = value.trim();
48
+ if (!trimmed) {
49
+ throw new Error(`${errorPrefix} 不允许为空字符串`);
71
50
  }
72
51
 
73
- const single = normalizeSingle(value);
74
- return single === null ? [] : [single];
75
- }
76
-
77
- private chunkNumberArray(arr: number[], chunkSize: number): number[][] {
78
- if (chunkSize <= 0) {
79
- throw new Error(`chunkSize 必须为正整数 (chunkSize: ${chunkSize})`);
52
+ if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(trimmed)) {
53
+ throw new Error(`${errorPrefix} 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)`);
80
54
  }
81
- if (arr.length === 0) return [];
82
55
 
83
- const chunks: number[][] = [];
84
- for (let i = 0; i < arr.length; i += chunkSize) {
85
- chunks.push(arr.slice(i, i + chunkSize));
56
+ if (!trimmed.startsWith("/")) {
57
+ throw new Error(`${errorPrefix} 必须是 pathname(以 / 开头)`);
86
58
  }
87
- return chunks;
88
- }
89
59
 
90
- private async buildApiPathMapByIds(apiIds: number[]): Promise<Map<number, string>> {
91
- const uniqueIds = Array.from(new Set(apiIds));
92
- if (uniqueIds.length === 0) {
93
- return new Map();
60
+ if (trimmed.includes(" ")) {
61
+ throw new Error(`${errorPrefix} 不允许包含空格`);
94
62
  }
95
63
 
96
- const apiMap = new Map<number, string>();
97
- const chunks = this.chunkNumberArray(uniqueIds, CacheHelper.API_ID_IN_CHUNK_SIZE);
64
+ return trimmed;
65
+ }
98
66
 
99
- for (const chunk of chunks) {
100
- const apis = await this.befly.db.getAll({
101
- table: "addon_admin_api",
102
- fields: ["id", "method", "path"],
103
- where: {
104
- id$in: chunk
105
- }
106
- });
67
+ private assertApiPathList(value: unknown, roleCode: string): string[] {
68
+ if (value === null || value === undefined) return [];
107
69
 
108
- for (const api of apis.lists) {
109
- apiMap.set(api.id, makeRouteKey(api.method, api.path));
110
- }
70
+ if (!Array.isArray(value)) {
71
+ throw new Error(`角色权限数据不合法:addon_admin_role.apis 必须是字符串数组,roleCode=${roleCode}`);
111
72
  }
112
73
 
113
- return apiMap;
114
- }
115
-
116
- /**
117
- * 构造函数
118
- * @param befly - Befly 上下文
119
- */
120
- constructor(befly: BeflyContext) {
121
- this.befly = befly;
74
+ const out: string[] = [];
75
+ for (const item of value) {
76
+ out.push(this.assertApiPathname(item, `角色权限数据不合法:addon_admin_role.apis 元素,roleCode=${roleCode}`));
77
+ }
78
+ return out;
122
79
  }
123
80
 
124
81
  /**
@@ -127,19 +84,19 @@ export class CacheHelper {
127
84
  async cacheApis(): Promise<void> {
128
85
  try {
129
86
  // 检查表是否存在
130
- const tableExists = await this.befly.db.tableExists("addon_admin_api");
87
+ const tableExists = await this.db.tableExists("addon_admin_api");
131
88
  if (!tableExists) {
132
89
  Logger.warn("⚠️ 接口表不存在,跳过接口缓存");
133
90
  return;
134
91
  }
135
92
 
136
93
  // 从数据库查询所有接口
137
- const apiList = await this.befly.db.getAll({
94
+ const apiList = await this.db.getAll({
138
95
  table: "addon_admin_api"
139
96
  });
140
97
 
141
98
  // 缓存到 Redis
142
- const result = await this.befly.redis.setObject(CacheKeys.apisAll(), apiList.lists);
99
+ const result = await this.redis.setObject(CacheKeys.apisAll(), apiList.lists);
143
100
 
144
101
  if (result === null) {
145
102
  Logger.warn("⚠️ 接口缓存失败");
@@ -155,19 +112,19 @@ export class CacheHelper {
155
112
  async cacheMenus(): Promise<void> {
156
113
  try {
157
114
  // 检查表是否存在
158
- const tableExists = await this.befly.db.tableExists("addon_admin_menu");
115
+ const tableExists = await this.db.tableExists("addon_admin_menu");
159
116
  if (!tableExists) {
160
117
  Logger.warn("⚠️ 菜单表不存在,跳过菜单缓存");
161
118
  return;
162
119
  }
163
120
 
164
121
  // 从数据库查询所有菜单
165
- const menus = await this.befly.db.getAll({
122
+ const menus = await this.db.getAll({
166
123
  table: "addon_admin_menu"
167
124
  });
168
125
 
169
126
  // 缓存到 Redis
170
- const result = await this.befly.redis.setObject(CacheKeys.menusAll(), menus.lists);
127
+ const result = await this.redis.setObject(CacheKeys.menusAll(), menus.lists);
171
128
 
172
129
  if (result === null) {
173
130
  Logger.warn("⚠️ 菜单缓存失败");
@@ -185,61 +142,43 @@ export class CacheHelper {
185
142
  async rebuildRoleApiPermissions(): Promise<void> {
186
143
  try {
187
144
  // 检查表是否存在
188
- const apiTableExists = await this.befly.db.tableExists("addon_admin_api");
189
- const roleTableExists = await this.befly.db.tableExists("addon_admin_role");
145
+ const roleTableExists = await this.db.tableExists("addon_admin_role");
190
146
 
191
- if (!apiTableExists || !roleTableExists) {
192
- Logger.warn("⚠️ 接口或角色表不存在,跳过角色权限缓存");
147
+ if (!roleTableExists) {
148
+ Logger.warn("⚠️ 角色表不存在,跳过角色权限缓存");
193
149
  return;
194
150
  }
195
151
 
196
152
  // 查询所有角色(仅取必要字段)
197
- const roles = await this.befly.db.getAll({
153
+ const roles = await this.db.getAll({
198
154
  table: "addon_admin_role",
199
155
  fields: ["code", "apis"]
200
156
  });
201
157
 
202
- const roleApiIdsMap = new Map<string, number[]>();
203
- const allApiIdsSet = new Set<number>();
158
+ const roleApiPathsMap = new Map<string, string[]>();
204
159
 
205
160
  for (const role of roles.lists) {
206
161
  if (!role?.code) continue;
207
- const apiIds = this.normalizeNumberIdList(role.apis);
208
- roleApiIdsMap.set(role.code, apiIds);
209
- for (const id of apiIds) {
210
- allApiIdsSet.add(id);
211
- }
162
+ const apiPaths = this.assertApiPathList(role.apis, role.code);
163
+ roleApiPathsMap.set(role.code, apiPaths);
212
164
  }
213
165
 
214
- const roleCodes = Array.from(roleApiIdsMap.keys());
166
+ const roleCodes = Array.from(roleApiPathsMap.keys());
215
167
  if (roleCodes.length === 0) {
216
168
  Logger.info("✅ 没有需要缓存的角色权限");
217
169
  return;
218
170
  }
219
171
 
220
- // 构建所有需要的 API 映射(按 ID 分块使用 $in,避免全表扫描/避免超长 IN)
221
- const allApiIds = Array.from(allApiIdsSet);
222
- const apiMap = await this.buildApiPathMapByIds(allApiIds);
223
-
224
172
  // 清理所有角色的缓存 key(保证幂等)
225
173
  const roleKeys = roleCodes.map((code) => CacheKeys.roleApis(code));
226
- await this.befly.redis.delBatch(roleKeys);
174
+ await this.redis.delBatch(roleKeys);
227
175
 
228
176
  // 批量写入新缓存(只写入非空权限)
229
177
  const items: Array<{ key: string; members: string[] }> = [];
230
178
 
231
179
  for (const roleCode of roleCodes) {
232
- const apiIds = roleApiIdsMap.get(roleCode) || [];
233
- const membersSet = new Set<string>();
234
-
235
- for (const id of apiIds) {
236
- const apiPath = apiMap.get(id);
237
- if (apiPath) {
238
- membersSet.add(apiPath);
239
- }
240
- }
241
-
242
- const members = Array.from(membersSet).sort();
180
+ const apiPaths = roleApiPathsMap.get(roleCode) || [];
181
+ const members = Array.from(new Set(apiPaths)).sort();
243
182
 
244
183
  if (members.length > 0) {
245
184
  items.push({ key: CacheKeys.roleApis(roleCode), members: members });
@@ -247,12 +186,13 @@ export class CacheHelper {
247
186
  }
248
187
 
249
188
  if (items.length > 0) {
250
- await this.befly.redis.saddBatch(items);
189
+ await this.redis.saddBatch(items);
251
190
  }
252
191
 
253
192
  // 极简方案不做版本/ready/meta:重建完成即生效
254
193
  } catch (error: any) {
255
- Logger.warn({ err: error }, "⚠️ 角色权限缓存异常");
194
+ Logger.error({ err: error }, "⚠️ 角色权限缓存异常(将阻断启动)");
195
+ throw error;
256
196
  }
257
197
  }
258
198
 
@@ -261,33 +201,28 @@ export class CacheHelper {
261
201
  * - apiIds 为空数组:仅清理缓存(防止残留)
262
202
  * - apiIds 非空:使用 $in 最小查询,DEL 后 SADD
263
203
  */
264
- async refreshRoleApiPermissions(roleCode: string, apiIds: number[]): Promise<void> {
204
+ async refreshRoleApiPermissions(roleCode: string, apiPaths: string[]): Promise<void> {
265
205
  if (!roleCode || typeof roleCode !== "string") {
266
206
  throw new Error("roleCode 必须是非空字符串");
267
207
  }
268
- const normalizedIds = this.normalizeNumberIdList(apiIds);
208
+ if (!Array.isArray(apiPaths)) {
209
+ throw new Error("apiPaths 必须是数组");
210
+ }
211
+
212
+ const normalizedPaths = apiPaths.map((p) => this.assertApiPathname(p, `refreshRoleApiPermissions: apiPaths 元素,roleCode=${roleCode}`));
269
213
  const roleKey = CacheKeys.roleApis(roleCode);
270
214
 
271
- // 空数组短路:避免触发 $in 空数组异常,同时保证清理残留
272
- if (normalizedIds.length === 0) {
273
- await this.befly.redis.del(roleKey);
215
+ // 空数组短路:保证清理残留
216
+ if (normalizedPaths.length === 0) {
217
+ await this.redis.del(roleKey);
274
218
  return;
275
219
  }
276
220
 
277
- const apiMap = await this.buildApiPathMapByIds(normalizedIds);
278
- const membersSet = new Set<string>();
279
- for (const id of normalizedIds) {
280
- const apiPath = apiMap.get(id);
281
- if (apiPath) {
282
- membersSet.add(apiPath);
283
- }
284
- }
285
-
286
- const members = Array.from(membersSet);
221
+ const members = Array.from(new Set(normalizedPaths));
287
222
 
288
- await this.befly.redis.del(roleKey);
223
+ await this.redis.del(roleKey);
289
224
  if (members.length > 0) {
290
- await this.befly.redis.sadd(roleKey, members);
225
+ await this.redis.sadd(roleKey, members);
291
226
  }
292
227
  }
293
228
 
@@ -311,7 +246,7 @@ export class CacheHelper {
311
246
  */
312
247
  async getApis(): Promise<any[]> {
313
248
  try {
314
- const apis = await this.befly.redis.getObject<any[]>(CacheKeys.apisAll());
249
+ const apis = await this.redis.getObject<any[]>(CacheKeys.apisAll());
315
250
  return apis || [];
316
251
  } catch (error: any) {
317
252
  Logger.error({ err: error }, "获取接口缓存失败");
@@ -325,7 +260,7 @@ export class CacheHelper {
325
260
  */
326
261
  async getMenus(): Promise<any[]> {
327
262
  try {
328
- const menus = await this.befly.redis.getObject<any[]>(CacheKeys.menusAll());
263
+ const menus = await this.redis.getObject<any[]>(CacheKeys.menusAll());
329
264
  return menus || [];
330
265
  } catch (error: any) {
331
266
  Logger.error({ err: error }, "获取菜单缓存失败");
@@ -340,7 +275,7 @@ export class CacheHelper {
340
275
  */
341
276
  async getRolePermissions(roleCode: string): Promise<string[]> {
342
277
  try {
343
- const permissions = await this.befly.redis.smembers(CacheKeys.roleApis(roleCode));
278
+ const permissions = await this.redis.smembers(CacheKeys.roleApis(roleCode));
344
279
  return permissions || [];
345
280
  } catch (error: any) {
346
281
  Logger.error({ err: error, roleCode: roleCode }, "获取角色权限缓存失败");
@@ -351,12 +286,13 @@ export class CacheHelper {
351
286
  /**
352
287
  * 检查角色是否有指定接口权限
353
288
  * @param roleCode - 角色代码
354
- * @param apiPath - 接口路径(格式:METHOD/path)
289
+ * @param apiPath - 接口路径(url.pathname,例如 /api/user/login;与 method 无关)
355
290
  * @returns 是否有权限
356
291
  */
357
292
  async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
358
293
  try {
359
- return await this.befly.redis.sismember(CacheKeys.roleApis(roleCode), apiPath);
294
+ const pathname = this.assertApiPathname(apiPath, "checkRolePermission: apiPath");
295
+ return await this.redis.sismember(CacheKeys.roleApis(roleCode), pathname);
360
296
  } catch (error: any) {
361
297
  Logger.error({ err: error, roleCode: roleCode }, "检查角色权限失败");
362
298
  return false;
@@ -370,7 +306,7 @@ export class CacheHelper {
370
306
  */
371
307
  async deleteRolePermissions(roleCode: string): Promise<boolean> {
372
308
  try {
373
- const result = await this.befly.redis.del(CacheKeys.roleApis(roleCode));
309
+ const result = await this.redis.del(CacheKeys.roleApis(roleCode));
374
310
  if (result > 0) {
375
311
  Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
376
312
  return true;
package/lib/cacheKeys.ts CHANGED
@@ -25,7 +25,7 @@ export class CacheKeys {
25
25
  /**
26
26
  * 角色接口权限缓存(Set 集合)
27
27
  * - key: befly:role:apis:${roleCode}
28
- * - member: METHOD/path
28
+ * - member: url.pathname(例如 /api/user/login;与 method 无关)
29
29
  */
30
30
  static roleApis(roleCode: string): string {
31
31
  return `befly:role:apis:${roleCode}`;
package/lib/connect.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  /**
2
2
  * 数据库连接管理器
3
3
  * 统一管理 SQL 和 Redis 连接
4
- * 配置从 beflyConfig 全局对象获取
5
4
  */
6
5
 
6
+ import type { DatabaseConfig, RedisConfig } from "../types/befly.js";
7
+
7
8
  import { SQL, RedisClient } from "bun";
8
9
 
9
- import { beflyConfig } from "../befly.config.js";
10
10
  import { Logger } from "./logger.js";
11
11
 
12
12
  /**
13
13
  * 数据库连接管理器
14
14
  * 使用静态方法管理全局单例连接
15
- * 所有配置从 beflyConfig 自动获取
16
15
  */
17
16
  export class Connect {
18
17
  private static sqlClient: SQL | null = null;
@@ -29,11 +28,10 @@ export class Connect {
29
28
 
30
29
  /**
31
30
  * 连接 SQL 数据库
32
- * 配置从 beflyConfig.db 获取
33
31
  * @returns SQL 客户端实例
34
32
  */
35
- static async connectSql(): Promise<SQL> {
36
- const config = beflyConfig.db || {};
33
+ static async connectSql(dbConfig: DatabaseConfig): Promise<SQL> {
34
+ const config = dbConfig || {};
37
35
 
38
36
  // 构建数据库连接字符串
39
37
  const type = config.type || "mysql";
@@ -137,11 +135,10 @@ export class Connect {
137
135
 
138
136
  /**
139
137
  * 连接 Redis
140
- * 配置从 beflyConfig.redis 获取
141
138
  * @returns Redis 客户端实例
142
139
  */
143
- static async connectRedis(): Promise<RedisClient> {
144
- const config = beflyConfig.redis || {};
140
+ static async connectRedis(redisConfig: RedisConfig): Promise<RedisClient> {
141
+ const config = redisConfig || {};
145
142
 
146
143
  try {
147
144
  // 构建 Redis URL
@@ -212,15 +209,14 @@ export class Connect {
212
209
 
213
210
  /**
214
211
  * 连接所有数据库(SQL + Redis)
215
- * 配置从 beflyConfig 自动获取
216
212
  */
217
- static async connect(): Promise<void> {
213
+ static async connect(config: { db: DatabaseConfig; redis: RedisConfig }): Promise<void> {
218
214
  try {
219
215
  // 连接 SQL
220
- await this.connectSql();
216
+ await this.connectSql(config.db || {});
221
217
 
222
218
  // 连接 Redis
223
- await this.connectRedis();
219
+ await this.connectRedis(config.redis || {});
224
220
  } catch (error: any) {
225
221
  Logger.error({ err: error }, "数据库初始化失败");
226
222
  await this.disconnect();