befly 3.9.40 → 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.
- package/README.md +39 -8
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +1 -1
- package/docs/api/api.md +1 -1
- package/docs/guide/quickstart.md +16 -9
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +7 -7
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +16 -9
- package/docs/reference/addon.md +12 -1
- package/docs/reference/config.md +13 -30
- package/docs/reference/sync.md +62 -193
- package/docs/reference/table.md +27 -29
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +3 -4
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- package/utils/route.ts +0 -23
package/docs/reference/table.md
CHANGED
|
@@ -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`
|
|
435
|
-
| `created_at` | `BIGINT`
|
|
436
|
-
| `updated_at` | `BIGINT`
|
|
437
|
-
| `deleted_at` | `BIGINT`
|
|
438
|
-
| `state` | `
|
|
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
|
-
|
|
516
|
-
# 同步所有表
|
|
517
|
-
bun run sync:db
|
|
515
|
+
服务启动时会在**主进程**自动执行 `syncTable()`。
|
|
518
516
|
|
|
519
|
-
|
|
520
|
-
bun run sync:db --table=user
|
|
517
|
+
如需在代码中手动触发:
|
|
521
518
|
|
|
522
|
-
|
|
523
|
-
|
|
519
|
+
```typescript
|
|
520
|
+
import { syncTable } from "../../sync/syncTable.js";
|
|
521
|
+
import { scanSources } from "../../utils/scanSources.js";
|
|
524
522
|
|
|
525
|
-
|
|
526
|
-
|
|
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 长度减少 |
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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 = {
|
|
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
|
-
|
|
22
|
-
|
|
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;
|
package/hooks/permission.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { ErrorResponse } from "../utils/response.js";
|
|
|
13
13
|
* - 开发者角色(dev):最高权限,直接通过
|
|
14
14
|
* - 其他角色:检查 Redis 中的角色权限集合
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
package/hooks/validator.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { ErrorResponse } from "../utils/response.js";
|
|
|
9
9
|
* 参数验证钩子
|
|
10
10
|
* 根据 API 定义的 fields 和 required 验证请求参数
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
package/lib/cacheHelper.ts
CHANGED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
private static readonly API_ID_IN_CHUNK_SIZE = 1000;
|
|
34
|
+
private db: CacheHelperDb;
|
|
35
|
+
private redis: CacheHelperRedis;
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
constructor(deps: CacheHelperDeps) {
|
|
38
|
+
this.db = deps.db;
|
|
39
|
+
this.redis = deps.redis;
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
if (uniqueIds.length === 0) {
|
|
93
|
-
return new Map();
|
|
60
|
+
if (trimmed.includes(" ")) {
|
|
61
|
+
throw new Error(`${errorPrefix} 不允许包含空格`);
|
|
94
62
|
}
|
|
95
63
|
|
|
96
|
-
|
|
97
|
-
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
98
66
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
throw new Error(`角色权限数据不合法:addon_admin_role.apis 必须是字符串数组,roleCode=${roleCode}`);
|
|
111
72
|
}
|
|
112
73
|
|
|
113
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 (!
|
|
192
|
-
Logger.warn("⚠️
|
|
147
|
+
if (!roleTableExists) {
|
|
148
|
+
Logger.warn("⚠️ 角色表不存在,跳过角色权限缓存");
|
|
193
149
|
return;
|
|
194
150
|
}
|
|
195
151
|
|
|
196
152
|
// 查询所有角色(仅取必要字段)
|
|
197
|
-
const roles = await this.
|
|
153
|
+
const roles = await this.db.getAll({
|
|
198
154
|
table: "addon_admin_role",
|
|
199
155
|
fields: ["code", "apis"]
|
|
200
156
|
});
|
|
201
157
|
|
|
202
|
-
const
|
|
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
|
|
208
|
-
|
|
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(
|
|
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.
|
|
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
|
|
233
|
-
const
|
|
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.
|
|
189
|
+
await this.redis.saddBatch(items);
|
|
251
190
|
}
|
|
252
191
|
|
|
253
192
|
// 极简方案不做版本/ready/meta:重建完成即生效
|
|
254
193
|
} catch (error: any) {
|
|
255
|
-
Logger.
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
272
|
-
if (
|
|
273
|
-
await this.
|
|
215
|
+
// 空数组短路:保证清理残留
|
|
216
|
+
if (normalizedPaths.length === 0) {
|
|
217
|
+
await this.redis.del(roleKey);
|
|
274
218
|
return;
|
|
275
219
|
}
|
|
276
220
|
|
|
277
|
-
const
|
|
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.
|
|
223
|
+
await this.redis.del(roleKey);
|
|
289
224
|
if (members.length > 0) {
|
|
290
|
-
await this.
|
|
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.
|
|
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.
|
|
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.
|
|
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 -
|
|
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
|
-
|
|
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.
|
|
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:
|
|
28
|
+
* - member: url.pathname(例如 /api/user/login;与 method 无关)
|
|
29
29
|
*/
|
|
30
30
|
static roleApis(roleCode: string): string {
|
|
31
31
|
return `befly:role:apis:${roleCode}`;
|