befly 3.9.39 → 3.10.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 (141) hide show
  1. package/README.md +39 -8
  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 +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
@@ -37,7 +37,7 @@
37
37
  - [字段名规范](#字段名规范)
38
38
  - [类型联动校验](#类型联动校验)
39
39
  - [数据库同步](#数据库同步)
40
- - [同步命令](#同步命令)
40
+ - [触发方式](#触发方式)
41
41
  - [同步流程](#同步流程)
42
42
  - [变更检测](#变更检测)
43
43
  - [安全机制](#安全机制)
@@ -429,13 +429,13 @@ interface FieldDefinition {
429
429
 
430
430
  以下字段由系统自动创建和管理,**不能**在表定义中使用:
431
431
 
432
- | 字段名 | 类型 | 说明 |
433
- | ------------ | --------- | ------------------------ |
434
- | `id` | `BIGINT` | 主键 ID,自增 |
435
- | `created_at` | `BIGINT` | 创建时间戳(毫秒) |
436
- | `updated_at` | `BIGINT` | 更新时间戳(毫秒) |
437
- | `deleted_at` | `BIGINT` | 删除时间戳(软删除标记) |
438
- | `state` | `TINYINT` | 状态字段 |
432
+ | 字段名 | 类型 | 说明 |
433
+ | ------------ | -------- | ------------------------ |
434
+ | `id` | `BIGINT` | 主键 ID,自增 |
435
+ | `created_at` | `BIGINT` | 创建时间戳(毫秒) |
436
+ | `updated_at` | `BIGINT` | 更新时间戳(毫秒) |
437
+ | `deleted_at` | `BIGINT` | 删除时间戳(软删除标记) |
438
+ | `state` | `BIGINT` | 状态字段 |
439
439
 
440
440
  **系统自动创建的索引:**
441
441
 
@@ -510,20 +510,19 @@ interface FieldDefinition {
510
510
 
511
511
  ## 数据库同步
512
512
 
513
- ### 同步命令
513
+ ### 触发方式
514
514
 
515
- ```bash
516
- # 同步所有表
517
- bun run sync:db
515
+ 服务启动时会在**主进程**自动执行 `syncTable()`。
518
516
 
519
- # 同步指定表
520
- bun run sync:db --table=user
517
+ 如需在代码中手动触发:
521
518
 
522
- # 计划模式(仅输出 SQL,不执行)
523
- bun run sync:db --plan
519
+ ```typescript
520
+ import { syncTable } from "../../sync/syncTable.js";
521
+ import { scanSources } from "../../utils/scanSources.js";
524
522
 
525
- # 强制模式(执行危险变更,如字段长度收缩)
526
- bun run sync:db --force
523
+ // ctx:BeflyContext(需已具备 ctx.db / ctx.redis / ctx.config)
524
+ const sources = await scanSources();
525
+ await syncTable(ctx, sources.tables);
527
526
  ```
528
527
 
529
528
  ### 同步流程
@@ -556,22 +555,21 @@ bun run sync:db --force
556
555
 
557
556
  同步时检测以下变更:
558
557
 
559
- | 变更类型 | 说明 | 自动执行 |
560
- | -------- | ---------------------------- | ---------- |
561
- | 长度扩展 | VARCHAR 长度增加 | ✓ |
562
- | 长度收缩 | VARCHAR 长度减少 | --force |
563
- | 类型宽化 | INT → BIGINT, VARCHAR → TEXT | ✓ |
564
- | 类型变更 | 其他类型变更 | ✗ 禁止 |
565
- | 默认值 | 默认值变更 | ✓ |
566
- | 注释 | 字段注释变更 | ✓ |
567
- | 索引 | 添加/删除索引 | ✓ |
558
+ | 变更类型 | 说明 | 自动执行 |
559
+ | -------- | ---------------------------- | -------------------- |
560
+ | 长度扩展 | VARCHAR 长度增加 | ✓ |
561
+ | 长度收缩 | VARCHAR 长度减少 | 跳过(需手动处理) |
562
+ | 类型宽化 | INT → BIGINT, VARCHAR → TEXT | ✓ |
563
+ | 类型变更 | 其他类型变更 | ✗ 禁止 |
564
+ | 默认值 | 默认值变更 | ✓ |
565
+ | 注释 | 字段注释变更 | ✓ |
566
+ | 索引 | 添加/删除索引 | ✓ |
568
567
 
569
568
  ### 安全机制
570
569
 
571
570
  1. **禁止危险类型变更**:不允许从 `BIGINT` 改为 `VARCHAR` 等不兼容变更
572
571
  2. **长度收缩保护**:默认跳过长度收缩,避免数据截断
573
- 3. **计划模式**:`--plan` 仅输出 SQL 不执行,用于审查
574
- 4. **保留字段保护**:不允许定义系统保留字段
572
+ 3. **保留字段保护**:不允许定义系统保留字段
575
573
 
576
574
  ---
577
575
 
package/hooks/auth.ts CHANGED
@@ -2,8 +2,8 @@ import type { Hook } from "../types/hook.js";
2
2
 
3
3
  import { setCtxUser } from "../lib/asyncContext.js";
4
4
 
5
- const hook: Hook = {
6
- order: 3,
5
+ export default {
6
+ deps: ["cors"],
7
7
  handler: async (befly, ctx) => {
8
8
  const authHeader = ctx.req.headers.get("authorization");
9
9
 
@@ -22,5 +22,4 @@ const hook: Hook = {
22
22
  ctx.user = {};
23
23
  }
24
24
  }
25
- };
26
- export default hook;
25
+ } satisfies Hook;
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
 
@@ -22,7 +22,7 @@ const hook: Hook = {
22
22
  // 应用字段默认值
23
23
  for (const [field, fieldDef] of Object.entries(ctx.api.fields)) {
24
24
  // 字段未传值且定义了默认值时,应用默认值
25
- if (ctx.body[field] === undefined && (fieldDef as any)?.default !== undefined) {
25
+ if (ctx.body[field] === undefined && (fieldDef as any)?.default !== undefined && (fieldDef as any)?.default !== null) {
26
26
  ctx.body[field] = (fieldDef as any).default;
27
27
  }
28
28
  }
@@ -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}`;