befly 3.9.38 → 3.9.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +37 -38
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +223 -231
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +143 -141
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +1 -1
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +8 -8
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -77
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +183 -102
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +48 -44
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -52
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -65
  75. package/sync/syncMenu.ts +190 -55
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
package/router/api.ts CHANGED
@@ -1,20 +1,21 @@
1
- /**
1
+ /**
2
2
  * API路由处理器
3
3
  * 处理 /api/* 路径的请求
4
4
  */
5
5
 
6
- // 外部依赖
7
- import { genShortId } from 'befly-shared/genShortId';
6
+ import type { ApiRoute } from "../types/api.js";
7
+ import type { BeflyContext } from "../types/befly.js";
8
+ // 类型导入
9
+ import type { RequestContext } from "../types/context.js";
10
+ import type { Hook } from "../types/hook.js";
8
11
 
12
+ import { withCtx } from "../lib/asyncContext.js";
13
+ import { Logger } from "../lib/logger.js";
9
14
  // 相对导入
10
- import { FinalResponse } from '../util.js';
11
- import { Logger } from '../lib/logger.js';
12
-
13
- // 类型导入
14
- import type { RequestContext } from '../types/context.js';
15
- import type { ApiRoute } from '../types/api.js';
16
- import type { Hook } from '../types/hook.js';
17
- import type { BeflyContext } from '../types/befly.js';
15
+ import { genShortId } from "../utils/genShortId.js";
16
+ import { getClientIp } from "../utils/getClientIp.js";
17
+ import { FinalResponse } from "../utils/response.js";
18
+ import { makeRouteKey } from "../utils/route.js";
18
19
 
19
20
  /**
20
21
  * API处理器工厂函数
@@ -23,78 +24,106 @@ import type { BeflyContext } from '../types/befly.js';
23
24
  * @param context - 应用上下文
24
25
  */
25
26
  export function apiHandler(apis: Map<string, ApiRoute>, hooks: Hook[], context: BeflyContext) {
26
- return async (req: Request): Promise<Response> => {
27
+ return async (req: Request, server?: any): Promise<Response> => {
27
28
  // 1. 生成请求 ID
28
29
  const requestId = genShortId();
29
30
 
30
31
  // 2. 创建请求上下文
31
32
  const url = new URL(req.url);
32
- const apiPath = `${req.method}${url.pathname}`;
33
- const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || 'unknown';
33
+ const apiPath = makeRouteKey(req.method, url.pathname);
34
+
35
+ const clientIp = getClientIp(req, server);
36
+
37
+ const now = Date.now();
34
38
 
35
39
  const ctx: RequestContext = {
36
40
  method: req.method,
37
41
  body: {},
38
42
  user: {},
39
43
  req: req,
40
- now: Date.now(),
44
+ now: now,
41
45
  ip: clientIp,
42
46
  headers: req.headers,
43
47
  route: apiPath,
44
48
  requestId: requestId,
45
49
  corsHeaders: {
46
- 'X-Request-ID': requestId
50
+ "X-Request-ID": requestId
47
51
  },
48
52
  api: apis.get(apiPath),
49
53
  response: undefined,
50
54
  result: undefined
51
55
  };
52
56
 
53
- try {
54
- // 4. 串联执行所有钩子
55
- for (const hook of hooks) {
56
- await hook.handler(context, ctx);
57
+ return withCtx(
58
+ {
59
+ requestId: requestId,
60
+ method: req.method,
61
+ route: apiPath,
62
+ ip: clientIp,
63
+ now: now
64
+ },
65
+ async () => {
66
+ try {
67
+ // 4. 串联执行所有钩子
68
+ for (const hook of hooks) {
69
+ await hook.handler(context, ctx);
57
70
 
58
- // 如果钩子已经设置了 response,停止执行
59
- if (ctx.response) {
60
- return ctx.response;
61
- }
62
- }
71
+ // 如果钩子已经设置了 response,停止执行
72
+ if (ctx.response) {
73
+ return ctx.response;
74
+ }
75
+ }
76
+
77
+ // hooks 全部通过后记录请求日志(拦截请求仅由 ErrorResponse 记录)
78
+ if (ctx.api && req.method !== "OPTIONS") {
79
+ const logData: Record<string, any> = {
80
+ event: "request",
81
+ apiName: ctx.api.name
82
+ };
63
83
 
64
- // 5. 执行 API handler
65
- if (!ctx.api) {
66
- if (req.method !== 'OPTIONS') {
67
- ctx.response = Response.json(
68
- {
69
- code: 1,
70
- msg: '接口不存在'
71
- },
72
- {
73
- headers: ctx.corsHeaders
84
+ if (ctx.body && Object.keys(ctx.body).length > 0) {
85
+ logData.body = ctx.body;
74
86
  }
75
- );
76
- }
77
- } else if (ctx.api.handler) {
78
- const result = await ctx.api.handler(context, ctx);
79
87
 
80
- if (result instanceof Response) {
81
- ctx.response = result;
82
- } else {
83
- ctx.result = result;
88
+ Logger.info(logData, "request");
89
+ }
90
+
91
+ // 5. 执行 API handler
92
+ if (!ctx.api) {
93
+ if (req.method !== "OPTIONS") {
94
+ ctx.response = Response.json(
95
+ {
96
+ code: 1,
97
+ msg: "接口不存在"
98
+ },
99
+ {
100
+ headers: ctx.corsHeaders
101
+ }
102
+ );
103
+ }
104
+ } else if (ctx.api.handler) {
105
+ const result = await ctx.api.handler(context, ctx);
106
+
107
+ if (result instanceof Response) {
108
+ ctx.response = result;
109
+ } else {
110
+ ctx.result = result;
111
+ }
112
+ }
113
+
114
+ // 7. 返回响应(自动处理 response/result/日志)
115
+ return FinalResponse(ctx);
116
+ } catch (err: any) {
117
+ // 全局错误处理
118
+ const errorPath = ctx.api ? apiPath : req.url;
119
+ Logger.error({ err: err, path: errorPath }, "请求错误");
120
+ ctx.result = {
121
+ code: 1,
122
+ msg: "内部服务错误"
123
+ };
124
+ return FinalResponse(ctx);
84
125
  }
85
126
  }
86
-
87
- // 7. 返回响应(自动处理 response/result/日志)
88
- return FinalResponse(ctx);
89
- } catch (err: any) {
90
- // 全局错误处理
91
- const errorPath = ctx.api ? apiPath : req.url;
92
- Logger.error({ err: err, path: errorPath }, '请求错误');
93
- ctx.result = {
94
- code: 1,
95
- msg: '内部服务错误'
96
- };
97
- return FinalResponse(ctx);
98
- }
127
+ );
99
128
  };
100
129
  }
package/router/static.ts CHANGED
@@ -1,16 +1,16 @@
1
- /**
1
+ /**
2
2
  * 静态文件路由处理器
3
3
  * 处理 /* 路径的静态文件请求
4
4
  */
5
5
 
6
6
  // 外部依赖
7
- import { join } from 'pathe';
7
+ import { join } from "pathe";
8
8
 
9
+ import { beflyConfig } from "../befly.config.js";
10
+ import { Logger } from "../lib/logger.js";
9
11
  // 相对导入
10
- import { projectDir } from '../paths.js';
11
- import { Logger } from '../lib/logger.js';
12
- import { setCorsOptions } from '../util.js';
13
- import { beflyConfig } from '../befly.config.js';
12
+ import { projectDir } from "../paths.js";
13
+ import { setCorsOptions } from "../utils/cors.js";
14
14
 
15
15
  /**
16
16
  * 静态文件处理器工厂
@@ -21,11 +21,11 @@ export function staticHandler() {
21
21
  const corsHeaders = setCorsOptions(req, beflyConfig.cors);
22
22
 
23
23
  const url = new URL(req.url);
24
- const filePath = join(projectDir, 'public', url.pathname);
24
+ const filePath = join(projectDir, "public", url.pathname);
25
25
 
26
26
  try {
27
27
  // OPTIONS预检请求
28
- if (req.method === 'OPTIONS') {
28
+ if (req.method === "OPTIONS") {
29
29
  return new Response(null, {
30
30
  status: 204,
31
31
  headers: corsHeaders
@@ -36,7 +36,7 @@ export function staticHandler() {
36
36
  if (await file.exists()) {
37
37
  return new Response(file, {
38
38
  headers: {
39
- 'Content-Type': file.type || 'application/octet-stream',
39
+ "Content-Type": file.type || "application/octet-stream",
40
40
  ...corsHeaders
41
41
  }
42
42
  });
@@ -44,7 +44,7 @@ export function staticHandler() {
44
44
  return Response.json(
45
45
  {
46
46
  code: 1,
47
- msg: '文件未找到'
47
+ msg: "文件未找到"
48
48
  },
49
49
  {
50
50
  headers: corsHeaders
@@ -52,12 +52,12 @@ export function staticHandler() {
52
52
  );
53
53
  }
54
54
  } catch (error: any) {
55
- Logger.error({ err: error }, '静态文件处理失败');
55
+ Logger.error({ err: error }, "静态文件处理失败");
56
56
 
57
57
  return Response.json(
58
58
  {
59
59
  code: 1,
60
- msg: '文件读取失败'
60
+ msg: "文件读取失败"
61
61
  },
62
62
  {
63
63
  headers: corsHeaders
package/sync/syncAll.ts CHANGED
@@ -1,16 +1,16 @@
1
- /**
1
+ /**
2
2
  * Sync 命令 - 一次性执行所有同步操作
3
- * 按顺序执行:syncDb → syncApi → syncMenu → syncDev
3
+ * 按顺序执行:syncDb → syncApi → syncMenu → syncDev(syncDev 内会重建角色接口权限缓存)
4
4
  */
5
5
 
6
- import { checkApp } from '../checks/checkApp.js';
7
- import { Logger } from '../lib/logger.js';
8
- import { syncDbCommand } from './syncDb.js';
9
- import { syncApiCommand } from './syncApi.js';
10
- import { syncMenuCommand } from './syncMenu.js';
11
- import { syncDevCommand } from './syncDev.js';
12
- import { beflyConfig } from '../befly.config.js';
13
- import type { SyncOptions } from '../types/index.js';
6
+ import type { SyncOptions } from "../types/sync.js";
7
+
8
+ import { checkApp } from "../checks/checkApp.js";
9
+ import { Logger } from "../lib/logger.js";
10
+ import { syncApiCommand } from "./syncApi.js";
11
+ import { syncDbCommand } from "./syncDb.js";
12
+ import { syncDevCommand } from "./syncDev.js";
13
+ import { syncMenuCommand } from "./syncMenu.js";
14
14
 
15
15
  export async function syncAllCommand(options: SyncOptions = {}) {
16
16
  try {
@@ -26,10 +26,10 @@ export async function syncAllCommand(options: SyncOptions = {}) {
26
26
  // 3. 同步菜单(并缓存)
27
27
  await syncMenuCommand();
28
28
 
29
- // 4. 同步开发管理员(并缓存角色权限)
29
+ // 4. 同步开发管理员(syncDev 内会重建角色接口权限缓存)
30
30
  await syncDevCommand();
31
31
  } catch (error: any) {
32
- Logger.error({ err: error }, '同步过程中发生错误');
32
+ Logger.error({ err: error }, "同步过程中发生错误");
33
33
  throw error;
34
34
  }
35
35
  }
package/sync/syncApi.ts CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * SyncApi 命令 - 同步 API 接口数据到数据库
3
3
  * 说明:遍历所有 API 文件,收集接口路由信息并同步到 addon_admin_api 表
4
4
  *
@@ -10,27 +10,25 @@
10
10
  * 5. 存在则更新,不存在则新增
11
11
  * 6. 删除配置中不存在的接口记录
12
12
  */
13
- import { readdirSync, statSync } from 'node:fs';
14
- import { join, dirname, relative, basename } from 'pathe';
15
- import { Connect } from '../lib/connect.js';
16
- import { DbHelper } from '../lib/dbHelper.js';
17
- import { RedisHelper } from '../lib/redisHelper.js';
18
- import { RedisKeys } from 'befly-shared/redisKeys';
19
- import { scanFiles } from 'befly-shared/scanFiles';
20
- import { scanAddons, addonDirExists, getAddonDir } from 'befly-shared/addonHelper';
13
+ import type { SyncApiOptions, ApiInfo } from "../types/sync.js";
21
14
 
22
- import { Logger } from '../lib/logger.js';
23
- import { projectDir } from '../paths.js';
24
- import { beflyConfig } from '../befly.config.js';
15
+ import { join, relative } from "pathe";
25
16
 
26
- import type { SyncApiOptions, ApiInfo } from '../types/index.js';
17
+ import { CacheHelper } from "../lib/cacheHelper.js";
18
+ import { Connect } from "../lib/connect.js";
19
+ import { DbHelper } from "../lib/dbHelper.js";
20
+ import { Logger } from "../lib/logger.js";
21
+ import { RedisHelper } from "../lib/redisHelper.js";
22
+ import { projectDir } from "../paths.js";
23
+ import { scanAddons, addonDirExists, getAddonDir } from "../utils/addonHelper.js";
24
+ import { scanFiles } from "../utils/scanFiles.js";
27
25
 
28
26
  /**
29
27
  * 从 API 文件中提取接口信息
30
28
  */
31
- async function extractApiInfo(filePath: string, apiRoot: string, type: 'app' | 'addon', addonName: string = '', addonTitle: string = ''): Promise<ApiInfo | null> {
29
+ async function extractApiInfo(filePath: string, apiRoot: string, type: "app" | "addon", addonName: string = "", addonTitle: string = ""): Promise<ApiInfo | null> {
32
30
  try {
33
- const normalizedFilePath = filePath.replace(/\\/g, '/');
31
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
34
32
  const apiModule = await import(normalizedFilePath);
35
33
  const apiConfig = apiModule.default;
36
34
 
@@ -38,32 +36,32 @@ async function extractApiInfo(filePath: string, apiRoot: string, type: 'app' | '
38
36
  return null;
39
37
  }
40
38
 
41
- let apiPath = '';
39
+ let apiPath = "";
42
40
 
43
- if (type === 'addon') {
41
+ if (type === "addon") {
44
42
  // Addon 接口:保留完整目录层级
45
43
  // 例: apis/menu/list.ts → /api/addon/admin/menu/list
46
44
  const relativePath = relative(apiRoot, filePath);
47
- const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, '');
45
+ const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
48
46
  apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
49
47
  } else {
50
48
  // 项目接口:保留完整目录层级
51
49
  // 例: apis/user/list.ts → /api/user/list
52
50
  const relativePath = relative(apiRoot, filePath);
53
- const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, '');
51
+ const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
54
52
  apiPath = `/api/${pathWithoutExt}`;
55
53
  }
56
54
 
57
55
  return {
58
- name: apiConfig.name || '',
56
+ name: apiConfig.name || "",
59
57
  path: apiPath,
60
- method: apiConfig.method || 'POST',
61
- description: apiConfig.description || '',
58
+ method: apiConfig.method || "POST",
59
+ description: apiConfig.description || "",
62
60
  addonName: addonName,
63
61
  addonTitle: addonTitle || addonName
64
62
  };
65
63
  } catch (error: any) {
66
- Logger.error({ err: error }, '同步 API 失败');
64
+ Logger.error({ err: error }, "同步 API 失败");
67
65
  throw error;
68
66
  }
69
67
  }
@@ -71,12 +69,12 @@ async function extractApiInfo(filePath: string, apiRoot: string, type: 'app' | '
71
69
  /**
72
70
  * 扫描所有 API 文件
73
71
  */
74
- async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
72
+ async function scanAllApis(): Promise<ApiInfo[]> {
75
73
  const apis: ApiInfo[] = [];
76
74
 
77
75
  // 1. 扫描项目 API(只扫描 apis 目录)
78
76
  try {
79
- const projectApisDir = join(projectDir, 'apis');
77
+ const projectApisDir = join(projectDir, "apis");
80
78
 
81
79
  // 扫描项目 API 文件
82
80
  const projectApiFiles: string[] = [];
@@ -90,7 +88,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
90
88
  }
91
89
 
92
90
  for (const filePath of projectApiFiles) {
93
- const apiInfo = await extractApiInfo(filePath, projectApisDir, 'app', '', '项目接口');
91
+ const apiInfo = await extractApiInfo(filePath, projectApisDir, "app", "", "项目接口");
94
92
  if (apiInfo) {
95
93
  apis.push(apiInfo);
96
94
  }
@@ -103,19 +101,19 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
103
101
  // addonName 格式: admin, demo 等
104
102
 
105
103
  // 检查 apis 子目录是否存在
106
- if (!addonDirExists(addonName, 'apis')) {
104
+ if (!addonDirExists(addonName, "apis")) {
107
105
  continue;
108
106
  }
109
107
 
110
- const addonApisDir = getAddonDir(addonName, 'apis');
108
+ const addonApisDir = getAddonDir(addonName, "apis");
111
109
 
112
110
  // 读取 addon 配置
113
- const addonPackageJsonPath = getAddonDir(addonName, 'package.json');
111
+ const addonPackageJsonPath = getAddonDir(addonName, "package.json");
114
112
  let addonTitle = addonName;
115
113
  try {
116
- const packageJson = await import(addonPackageJsonPath, { with: { type: 'json' } });
114
+ const packageJson = await import(addonPackageJsonPath, { with: { type: "json" } });
117
115
  addonTitle = packageJson.default?.title || addonName;
118
- } catch (error) {
116
+ } catch {
119
117
  // 忽略配置读取错误
120
118
  }
121
119
 
@@ -131,7 +129,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
131
129
  }
132
130
 
133
131
  for (const filePath of addonApiFiles) {
134
- const apiInfo = await extractApiInfo(filePath, addonApisDir, 'addon', addonName, addonTitle);
132
+ const apiInfo = await extractApiInfo(filePath, addonApisDir, "addon", addonName, addonTitle);
135
133
  if (apiInfo) {
136
134
  apis.push(apiInfo);
137
135
  }
@@ -140,7 +138,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
140
138
 
141
139
  return apis;
142
140
  } catch (error: any) {
143
- Logger.error({ err: error }, '接口扫描失败');
141
+ Logger.error({ err: error }, "接口扫描失败");
144
142
  return apis;
145
143
  }
146
144
  }
@@ -153,7 +151,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
153
151
  try {
154
152
  // 根据 path 查询是否存在
155
153
  const existing = await helper.getOne({
156
- table: 'addon_admin_api',
154
+ table: "addon_admin_api",
157
155
  where: { path: api.path }
158
156
  });
159
157
 
@@ -163,7 +161,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
163
161
 
164
162
  if (needUpdate) {
165
163
  await helper.updData({
166
- table: 'addon_admin_api',
164
+ table: "addon_admin_api",
167
165
  where: { id: existing.id },
168
166
  data: {
169
167
  name: api.name,
@@ -176,7 +174,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
176
174
  }
177
175
  } else {
178
176
  await helper.insData({
179
- table: 'addon_admin_api',
177
+ table: "addon_admin_api",
180
178
  data: {
181
179
  name: api.name,
182
180
  path: api.path,
@@ -188,7 +186,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
188
186
  });
189
187
  }
190
188
  } catch (error: any) {
191
- Logger.error({ err: error, api: api.name }, '同步接口失败');
189
+ Logger.error({ err: error, api: api.name }, "同步接口失败");
192
190
  }
193
191
  }
194
192
  }
@@ -198,14 +196,14 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
198
196
  */
199
197
  async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<void> {
200
198
  const allRecords = await helper.getAll({
201
- table: 'addon_admin_api',
199
+ table: "addon_admin_api",
202
200
  where: { state$gte: 0 }
203
201
  });
204
202
 
205
203
  for (const record of allRecords.lists) {
206
204
  if (record.path && !apiPaths.has(record.path)) {
207
205
  await helper.delForce({
208
- table: 'addon_admin_api',
206
+ table: "addon_admin_api",
209
207
  where: { id: record.id }
210
208
  });
211
209
  }
@@ -218,25 +216,27 @@ async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promis
218
216
  export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void> {
219
217
  try {
220
218
  if (options.plan) {
221
- Logger.debug('[计划] 同步 API 接口到数据库(plan 模式不执行)');
219
+ Logger.debug("[计划] 同步 API 接口到数据库(plan 模式不执行)");
222
220
  return;
223
221
  }
224
222
 
225
223
  // 连接数据库(SQL + Redis)
226
224
  await Connect.connect();
227
225
 
228
- const helper = new DbHelper({ redis: new RedisHelper() } as any, Connect.getSql());
226
+ const redisHelper = new RedisHelper();
227
+ const helper = new DbHelper({ redis: redisHelper } as any, Connect.getSql());
228
+ const cacheHelper = new CacheHelper({ db: helper, redis: redisHelper } as any);
229
229
 
230
230
  // 1. 检查表是否存在(addon_admin_api 来自 addon-admin 组件)
231
- const exists = await helper.tableExists('addon_admin_api');
231
+ const exists = await helper.tableExists("addon_admin_api");
232
232
 
233
233
  if (!exists) {
234
- Logger.debug('表 addon_admin_api 不存在,跳过 API 同步(需要安装 addon-admin 组件)');
234
+ Logger.debug("表 addon_admin_api 不存在,跳过 API 同步(需要安装 addon-admin 组件)");
235
235
  return;
236
236
  }
237
237
 
238
238
  // 2. 扫描所有 API 文件
239
- const apis = await scanAllApis(projectDir);
239
+ const apis = await scanAllApis();
240
240
  const apiPaths = new Set(apis.map((api) => api.path));
241
241
 
242
242
  // 3. 同步 API 数据
@@ -247,18 +247,21 @@ export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void
247
247
 
248
248
  // 5. 缓存接口数据到 Redis
249
249
  try {
250
- const apiList = await helper.getAll({
251
- table: 'addon_admin_api',
252
- orderBy: ['id#ASC']
253
- });
250
+ await cacheHelper.cacheApis();
251
+ } catch {
252
+ // 忽略缓存错误
253
+ }
254
254
 
255
- const redisHelper = new RedisHelper();
256
- await redisHelper.setObject(RedisKeys.apisAll(), apiList.lists);
255
+ // 6. API 表发生变更后,重建角色接口权限缓存(不使用定时器)
256
+ // 说明:role permission set 的成员是 METHOD/path,API 的 method/path 变更会影响所有角色权限。
257
+ try {
258
+ await cacheHelper.rebuildRoleApiPermissions();
257
259
  } catch (error: any) {
258
- // 忽略缓存错误
260
+ // 不阻塞 syncApi 主流程,但记录日志
261
+ Logger.warn({ err: error }, "API 同步完成,但重建角色权限缓存失败");
259
262
  }
260
263
  } catch (error: any) {
261
- Logger.error({ err: error }, 'API 同步失败');
264
+ Logger.error({ err: error }, "API 同步失败");
262
265
  throw error;
263
266
  } finally {
264
267
  await Connect.disconnect();