befly 3.76.6 → 3.77.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 (55) hide show
  1. package/Befly.js +99 -31
  2. package/apis/admin/delete.js +1 -1
  3. package/apis/admin/detail.js +1 -0
  4. package/apis/admin/insert.js +1 -1
  5. package/apis/admin/update.js +1 -1
  6. package/apis/auth/login.js +3 -2
  7. package/apis/dashboard/systemResources.js +12 -1
  8. package/apis/dict/detail.js +1 -0
  9. package/apis/dictType/detail.js +1 -0
  10. package/apis/email/config.js +1 -1
  11. package/apis/role/apiSave.js +14 -1
  12. package/apis/role/detail.js +1 -0
  13. package/apis/role/menuSave.js +1 -1
  14. package/apis/tongJi/_tongJi.js +16 -0
  15. package/apis/tongJi/dailyReport.js +5 -1
  16. package/apis/tongJi/dailyStatsDistribution.js +5 -4
  17. package/apis/tongJi/errorReport.js +8 -2
  18. package/apis/tongJi/todayOnline.js +2 -5
  19. package/checks/api.js +2 -6
  20. package/checks/field.js +30 -6
  21. package/checks/menu.js +2 -1
  22. package/configs/beflyConfig.json +3 -2
  23. package/exports.js +1 -0
  24. package/hooks/auth.js +7 -1
  25. package/hooks/permission.js +22 -1
  26. package/hooks/rateLimit.js +36 -5
  27. package/hooks/validator.js +5 -16
  28. package/index.js +44 -38
  29. package/libs/cacheHelper.js +13 -18
  30. package/libs/logger/logger.js +30 -12
  31. package/libs/memCache.js +53 -0
  32. package/libs/mysql/dbHelper.js +39 -71
  33. package/libs/mysql/dbParse.js +5 -1
  34. package/libs/mysql/sql/sqlBuilder.js +9 -0
  35. package/libs/redis/redis.js +44 -57
  36. package/libs/smtpText.js +41 -22
  37. package/libs/validator/compiler.js +1 -0
  38. package/libs/validator/parser.js +10 -4
  39. package/libs/validator/util.js +1 -5
  40. package/package.json +1 -1
  41. package/paths.js +23 -10
  42. package/router/static.js +13 -6
  43. package/schemas/api.json +4 -0
  44. package/schemas/config.json +20 -0
  45. package/sync/api.js +15 -7
  46. package/sync/dev.js +27 -3
  47. package/sync/menu.js +3 -3
  48. package/sync/syncUtil.js +12 -6
  49. package/tables/api.json +12 -0
  50. package/tables/emailLog.json +6 -2
  51. package/tables/menu.json +3 -0
  52. package/utils/is.js +7 -0
  53. package/utils/scanFiles.js +49 -46
  54. package/utils/scanSources.js +17 -0
  55. package/utils/util.js +5 -1
package/Befly.js CHANGED
@@ -22,10 +22,9 @@ async function assertRuntimeReady(context) {
22
22
  if (!context[key]) throw createError(`启动失败:ctx.${key} 未初始化`, { code: "runtime", subsystem: "start", operation: "runtimeReady" });
23
23
  }
24
24
 
25
- const missingTables = [];
26
- for (const table of [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE]) {
27
- if (!(await context.mysql.tableExists(table)).data) missingTables.push(table);
28
- }
25
+ const coreTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
26
+ const tableResults = await Promise.all(coreTables.map((table) => context.mysql.tableExists(table)));
27
+ const missingTables = coreTables.filter((table, index) => !tableResults[index].data);
29
28
  if (missingTables.length > 0) throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, { code: "runtime", subsystem: "start", operation: "tableExists", missingTables: missingTables });
30
29
  }
31
30
 
@@ -41,9 +40,12 @@ export class Befly {
41
40
  this.apis = init.apis || [];
42
41
  this.hooks = init.hooks || [];
43
42
  this.plugins = init.plugins || [];
43
+ this.activePlugins = [];
44
44
  this.corns = init.corns || [];
45
45
  this.server = null;
46
46
  this.started = false;
47
+ this.stopping = null;
48
+ this.signalHandlers = null;
47
49
  }
48
50
 
49
51
  async start() {
@@ -55,6 +57,7 @@ export class Befly {
55
57
  for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
56
58
  const pluginStartTime = Bun.nanoseconds();
57
59
  this.context[item.fileName] = await item.handler(this.context);
60
+ this.activePlugins.push(item);
58
61
  Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
59
62
  }
60
63
  Logger.info(`启动 插件初始化完成,耗时 ${calcPerfTime(pluginsStartTime)}`);
@@ -64,21 +67,16 @@ export class Befly {
64
67
  Logger.info(`启动 运行时依赖检查完成,耗时 ${calcPerfTime(runtimeReadyStartTime)}`);
65
68
 
66
69
  if (isPrimaryProcess()) {
67
- const syncApiStartTime = Bun.nanoseconds();
68
- await syncApi(this.context, this.apis);
69
- Logger.info(`启动 API 同步完成,耗时 ${calcPerfTime(syncApiStartTime)}`);
70
-
71
- const syncMenuStartTime = Bun.nanoseconds();
72
- await syncMenu(this.context, this.menus);
73
- Logger.info(`启动 菜单同步完成,耗时 ${calcPerfTime(syncMenuStartTime)}`);
74
-
75
- const syncDevStartTime = Bun.nanoseconds();
70
+ const syncStartTime = Bun.nanoseconds();
71
+ const syncApiStart = Bun.nanoseconds();
72
+ const syncMenuStart = Bun.nanoseconds();
73
+ const syncDevStart = Bun.nanoseconds();
74
+ // api/menu 互不依赖可并行;syncDev 读取 api/menu 落库结果、syncCache 读取 syncDev 写入的角色表,需按序串行
75
+ await Promise.all([syncApi(this.context, this.apis).then(() => Logger.info(`启动 API 同步完成,耗时 ${calcPerfTime(syncApiStart)}`)), syncMenu(this.context, this.menus).then(() => Logger.info(`启动 菜单同步完成,耗时 ${calcPerfTime(syncMenuStart)}`))]);
76
76
  await syncDev(this.context);
77
- Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(syncDevStartTime)}`);
78
-
79
- const syncCacheStartTime = Bun.nanoseconds();
77
+ Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(syncDevStart)}`);
80
78
  await syncCache(this.context);
81
- Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(syncCacheStartTime)}`);
79
+ Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(syncStartTime)}`);
82
80
 
83
81
  const syncReadyStartTime = Bun.nanoseconds();
84
82
  await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
@@ -88,7 +86,11 @@ export class Befly {
88
86
  const startAt = Date.now();
89
87
  await waitFor({
90
88
  label: "主进程同步完成",
91
- check: async () => Number(await this.context.redis.getString(SYNC_READY_KEY)) > startAt
89
+ // 主进程先于本进程完成同步时标记时间早于 startAt,60 秒内的近期标记视为就绪
90
+ check: async () => {
91
+ const marker = Number(await this.context.redis.getString(SYNC_READY_KEY));
92
+ return marker > startAt || (marker > 0 && startAt - marker < 60_000);
93
+ }
92
94
  });
93
95
  Logger.info(`启动 等待主进程同步完成,耗时 ${calcPerfTime(syncWaitStartTime)}`);
94
96
  }
@@ -131,29 +133,95 @@ export class Befly {
131
133
  });
132
134
 
133
135
  this.started = true;
136
+ const startupTime = calcPerfTime(serverStartTime);
134
137
  Logger.info(`启动 Bun.serve 监听完成,耗时 ${calcPerfTime(listenStartTime)}`);
135
- Logger.info(`启动总耗时 ${calcPerfTime(serverStartTime)}`);
136
- Logger.info(`${this.context.config.appName} 启动成功`, { url: this.server.url, startupTime: calcPerfTime(serverStartTime) });
138
+ Logger.info(`启动总耗时 ${startupTime}`);
139
+ Logger.info(`${this.context.config.appName} 启动成功`, { url: this.server.url, startupTime: startupTime });
140
+
141
+ this.signalHandlers = {
142
+ SIGINT: async () => {
143
+ let exitCode = 130;
144
+ try {
145
+ await this.stop();
146
+ } catch {
147
+ exitCode = 1;
148
+ }
149
+ process.exit(exitCode);
150
+ },
151
+ SIGTERM: async () => {
152
+ let exitCode = 143;
153
+ try {
154
+ await this.stop();
155
+ } catch {
156
+ exitCode = 1;
157
+ }
158
+ process.exit(exitCode);
159
+ }
160
+ };
161
+ process.once("SIGINT", this.signalHandlers.SIGINT);
162
+ process.once("SIGTERM", this.signalHandlers.SIGTERM);
137
163
  return this.server;
138
164
  } catch (error) {
139
165
  Logger.error("项目启动失败", error);
140
- await this.stop();
166
+ try {
167
+ await this.stop();
168
+ } catch {}
141
169
  throw createError("运行时错误", { cause: error, code: error?.code || "runtime", subsystem: "start", operation: "start" });
142
170
  }
143
171
  }
144
172
 
145
173
  async stop() {
146
- this.server?.stop();
147
- this.server = null;
148
- this.context.cron?.stopAll();
174
+ if (this.stopping) return this.stopping;
149
175
 
150
- for (const item of this.plugins.toSorted((a, b) => b.order - a.order)) {
151
- const resource = this.context[item.fileName];
152
- if (resource?.close) await resource.close();
153
- else if (resource?.stop) await resource.stop();
154
- }
176
+ const stopping = (async () => {
177
+ let stopError;
155
178
 
156
- this.started = false;
157
- await Logger.flush();
179
+ if (this.signalHandlers) {
180
+ process.off("SIGINT", this.signalHandlers.SIGINT);
181
+ process.off("SIGTERM", this.signalHandlers.SIGTERM);
182
+ this.signalHandlers = null;
183
+ }
184
+
185
+ try {
186
+ this.server?.stop();
187
+ } catch (error) {
188
+ stopError = error;
189
+ Logger.error("停止 HTTP 服务失败", error);
190
+ }
191
+ this.server = null;
192
+
193
+ const cron = this.context.cron;
194
+ this.context.cron = null;
195
+ try {
196
+ cron?.stopAll();
197
+ } catch (error) {
198
+ stopError ||= error;
199
+ Logger.error("停止定时器失败", error);
200
+ }
201
+
202
+ for (const item of this.activePlugins.toReversed()) {
203
+ const resource = this.context[item.fileName];
204
+ try {
205
+ if (resource?.close) await resource.close();
206
+ else if (resource?.stop) await resource.stop();
207
+ } catch (error) {
208
+ stopError ||= error;
209
+ Logger.error(`关闭插件 ${item.fileName} 失败`, error);
210
+ }
211
+ }
212
+ this.activePlugins = [];
213
+
214
+ this.started = false;
215
+ await Logger.shutdown();
216
+
217
+ if (stopError) throw createError("项目停止失败", { cause: stopError, code: "runtime", subsystem: "stop", operation: "stop" });
218
+ })();
219
+ this.stopping = stopping;
220
+
221
+ try {
222
+ return await stopping;
223
+ } finally {
224
+ if (this.stopping === stopping) this.stopping = null;
225
+ }
158
226
  }
159
227
  }
@@ -2,7 +2,7 @@ export default {
2
2
  name: "删除管理员",
3
3
  method: "POST",
4
4
  body: "none",
5
- auth: true,
5
+ auth: "dev",
6
6
  fields: {
7
7
  id: { name: "ID", paramType: "integer", minValue: 1 }
8
8
  },
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  name: "获取管理员信息",
3
+ category: "select",
3
4
  method: "POST",
4
5
  body: "none",
5
6
  auth: true,
@@ -5,7 +5,7 @@ export default {
5
5
  name: "添加管理员",
6
6
  method: "POST",
7
7
  body: "none",
8
- auth: true,
8
+ auth: "dev",
9
9
  fields: {
10
10
  username: adminTable.username,
11
11
  password: adminTable.password,
@@ -5,7 +5,7 @@ export default {
5
5
  name: "更新管理员",
6
6
  method: "POST",
7
7
  body: "none",
8
- auth: true,
8
+ auth: "dev",
9
9
  fields: {
10
10
  id: { name: "ID", paramType: "integer", minValue: 1 },
11
11
  username: adminTable.username,
@@ -105,8 +105,9 @@ export default {
105
105
  loginAt: Date.now()
106
106
  };
107
107
 
108
- const sessionResult = await befly.redis.setObject(sessionKey, sessionData, ttlSeconds);
109
- if (!sessionResult) {
108
+ try {
109
+ await befly.redis.setObject(sessionKey, sessionData, ttlSeconds);
110
+ } catch {
110
111
  return befly.tool.No("登录失败,请稍后重试");
111
112
  }
112
113
 
@@ -2,6 +2,8 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import { promisify } from "node:util";
4
4
 
5
+ import { memRemember } from "#befly/libs/memCache.js";
6
+
5
7
  export default {
6
8
  name: "获取系统资源",
7
9
  method: "POST",
@@ -9,7 +11,16 @@ export default {
9
11
  auth: true,
10
12
  fields: {},
11
13
  required: [],
14
+ // CPU 采样含 100ms 等待,快照缓存 3 秒避免高并发下请求积压
12
15
  handler: async (befly) => {
16
+ return await memRemember("dashboard:systemResources", 3000, async () => {
17
+ return await collectSystemResources(befly);
18
+ });
19
+ }
20
+ };
21
+
22
+ async function collectSystemResources(befly) {
23
+ {
13
24
  const cpus = os.cpus();
14
25
  const cpuCount = cpus.length;
15
26
 
@@ -105,4 +116,4 @@ export default {
105
116
  }
106
117
  });
107
118
  }
108
- };
119
+ }
@@ -2,6 +2,7 @@ import { dictReadFields, dictReadLeftJoin, dictReadTable } from "./_dict.js";
2
2
 
3
3
  export default {
4
4
  name: "获取字典详情",
5
+ category: "select",
5
6
  method: "POST",
6
7
  body: "none",
7
8
  auth: false,
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  name: "字典类型详情",
3
+ category: "select",
3
4
  method: "POST",
4
5
  body: "none",
5
6
  auth: true,
@@ -2,7 +2,7 @@ export default {
2
2
  name: "获取邮件配置",
3
3
  method: "POST",
4
4
  body: "none",
5
- auth: true,
5
+ auth: "dev",
6
6
  fields: {},
7
7
  required: [],
8
8
  handler: async (befly) => {
@@ -6,7 +6,7 @@ export default {
6
6
  name: "保存角色接口权限",
7
7
  method: "POST",
8
8
  body: "none",
9
- auth: true,
9
+ auth: "dev",
10
10
  fields: {
11
11
  roleCode: roleTable.code,
12
12
  apiPaths: roleTable.apis
@@ -21,6 +21,19 @@ export default {
21
21
  return befly.tool.No("角色不存在");
22
22
  }
23
23
 
24
+ // 超级管理员专属接口是天生权限,不允许分配给任何角色(前端授权 UI 也应过滤 auth=dev)
25
+ if (apiPaths.length > 0) {
26
+ const checkResult = await befly.mysql.getAll({
27
+ table: "beflyApi",
28
+ fields: ["path", "auth"],
29
+ where: { path$in: apiPaths }
30
+ });
31
+ const devPaths = checkResult.data.lists.filter((row) => row.auth === "dev").map((row) => row.path);
32
+ if (devPaths.length > 0) {
33
+ return befly.tool.No(`超级管理员专属接口不允许分配:${devPaths.join("、")}`);
34
+ }
35
+ }
36
+
24
37
  await befly.mysql.updData({
25
38
  table: "beflyRole",
26
39
  where: { code: ctx.body.roleCode },
@@ -4,6 +4,7 @@ import { getRoleByCode } from "./_role.js";
4
4
 
5
5
  export default {
6
6
  name: "获取用户角色",
7
+ category: "select",
7
8
  method: "POST",
8
9
  body: "none",
9
10
  auth: true,
@@ -6,7 +6,7 @@ export default {
6
6
  name: "保存角色菜单权限",
7
7
  method: "POST",
8
8
  body: "none",
9
- auth: true,
9
+ auth: "dev",
10
10
  fields: {
11
11
  roleCode: roleTable.code,
12
12
  menuPaths: roleTable.menus
@@ -1,3 +1,4 @@
1
+ import { memRemember } from "#befly/libs/memCache.js";
1
2
  import { DAY_MS } from "#befly/utils/datetime.js";
2
3
 
3
4
  const DEFAULT_TIME_ZONE = "Asia/Shanghai";
@@ -118,3 +119,18 @@ export function getTongJiDateRangeList(startDateYmd, endDateYmd, timeZone = DEFA
118
119
 
119
120
  return list;
120
121
  }
122
+
123
+ // 上报接口的 productCode 必须是已注册项目(空值视为未分组上报,放行);结果进程内缓存 60 秒
124
+ export async function isRegisteredProductCode(befly, productCode) {
125
+ if (!productCode) {
126
+ return true;
127
+ }
128
+ return await memRemember(`tongJi:project:${productCode}`, 60000, async () => {
129
+ const result = await befly.mysql.getOne({
130
+ table: "beflyProject",
131
+ fields: ["id"],
132
+ where: { code: productCode }
133
+ });
134
+ return Boolean(result.data?.id);
135
+ });
136
+ }
@@ -1,6 +1,6 @@
1
1
  import { isValidPositiveInt } from "#befly/utils/is.js";
2
2
 
3
- import { getTongJiDateYmdNumber } from "./_tongJi.js";
3
+ import { getTongJiDateYmdNumber, isRegisteredProductCode } from "./_tongJi.js";
4
4
 
5
5
  const DAILY_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
6
6
 
@@ -29,6 +29,10 @@ export default {
29
29
 
30
30
  const productCode = ctx.body?.productCode || "";
31
31
 
32
+ if (!(await isRegisteredProductCode(befly, productCode))) {
33
+ return befly.tool.No("productCode 未注册");
34
+ }
35
+
32
36
  let memberMeta;
33
37
 
34
38
  if (isValidPositiveInt(ctx?.userId)) {
@@ -75,11 +75,12 @@ export default {
75
75
  }
76
76
 
77
77
  const [startDate, endDate] = getDateRangeBounds(dateRange, Date.now(), befly.config?.tz);
78
+ const dimensionList = Array.from(DISTRIBUTION_DIMENSIONS);
79
+ const rows = await Promise.all(dimensionList.map((dimension) => queryDimensionDistribution(befly, dimension, startDate, endDate, productCode)));
78
80
  const result = {};
79
-
80
- for (const dimension of DISTRIBUTION_DIMENSIONS) {
81
- result[dimension] = await queryDimensionDistribution(befly, dimension, startDate, endDate, productCode);
82
- }
81
+ dimensionList.forEach((dimension, index) => {
82
+ result[dimension] = rows[index];
83
+ });
83
84
 
84
85
  return befly.tool.Yes("获取成功", result);
85
86
  }
@@ -1,7 +1,9 @@
1
1
  import errorReportTable from "#befly/tables/errorReport.json";
2
- import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
2
+ import { getTimeBucketStart } from "#befly/utils/datetime.js";
3
3
  import { isValidPositiveInt } from "#befly/utils/is.js";
4
4
 
5
+ import { getTongJiDateYmdNumber, isRegisteredProductCode } from "./_tongJi.js";
6
+
5
7
  const ERROR_STATS_BUCKET_MS = 30 * 60 * 1000;
6
8
  const ERROR_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
7
9
 
@@ -44,10 +46,14 @@ export default {
44
46
  handler: async (befly, ctx) => {
45
47
  const now = Date.now();
46
48
  const bucketTime = getTimeBucketStart(now, ERROR_STATS_BUCKET_MS);
47
- const bucketDate = getDateYmdNumber(now);
49
+ const bucketDate = getTongJiDateYmdNumber(now, befly.config?.tz);
48
50
 
49
51
  const body = ctx.body || {};
50
52
  const productCode = body.productCode || "";
53
+
54
+ if (!(await isRegisteredProductCode(befly, productCode))) {
55
+ return befly.tool.No("productCode 未注册");
56
+ }
51
57
  const rawMessage = body.message || "";
52
58
  const extractedMessage = extractErrorMessage(rawMessage);
53
59
 
@@ -32,11 +32,8 @@ export default {
32
32
  handler: async (befly, _ctx) => {
33
33
  const reportDate = getTongJiDateYmdNumber(Date.now(), befly.config?.tz);
34
34
  const projects = await loadProjects(befly);
35
- let total = 0;
36
-
37
- for (const item of projects) {
38
- total += await getTodayCount(befly, reportDate, item.code);
39
- }
35
+ const counts = await Promise.all(projects.map((item) => getTodayCount(befly, reportDate, item.code)));
36
+ const total = counts.reduce((sum, count) => sum + count, 0);
40
37
 
41
38
  return befly.tool.Yes("获取成功", {
42
39
  today: {
package/checks/api.js CHANGED
@@ -2,7 +2,7 @@ import { Logger } from "#befly/libs/logger/index.js";
2
2
  import { compile } from "#befly/libs/validator/index.js";
3
3
 
4
4
  import apiSchema from "../schemas/api.json";
5
- import { getFieldSchema } from "./field.js";
5
+ import { buildApiSchema, getFieldSchema } from "./field.js";
6
6
  import { formatIssues } from "./validation.js";
7
7
 
8
8
  const validate = compile(apiSchema);
@@ -23,11 +23,7 @@ function checkApiRules(issues, apis) {
23
23
  }
24
24
  }
25
25
  try {
26
- compile({
27
- paramType: "object",
28
- fields: Object.fromEntries(Object.entries(fields).map(([field, definition]) => [field, { ...getFieldSchema(definition), optional: !(api.required || []).includes(field) }])),
29
- constraints: api.constraints
30
- });
26
+ compile(buildApiSchema(fields, api.required, api.constraints));
31
27
  } catch (error) {
32
28
  issues.push({ path: [index, "constraints"], code: "rule", message: error.message });
33
29
  }
package/checks/field.js CHANGED
@@ -9,10 +9,34 @@ export function getFieldSchema(field) {
9
9
  }
10
10
 
11
11
  export function getFieldErrors(issues, fields) {
12
- return Object.fromEntries(
13
- issues.map((issue) => {
14
- const field = issue.path[0];
15
- return [field, `${fields[field]?.name || field}${issue.message}`];
16
- })
17
- );
12
+ const errors = {};
13
+ for (const issue of issues) {
14
+ const field = issue.path[0];
15
+ // 同字段保留首条错误,避免后者覆盖更根本的原因
16
+ if (errors[field] === undefined) {
17
+ errors[field] = `${fields[field]?.name || field}${issue.message}`;
18
+ }
19
+ }
20
+ return errors;
21
+ }
22
+
23
+ /**
24
+ * 构造 API 请求体验证 schema:fields 剥离领域元数据、required 映射 optional、附加 constraints。
25
+ * 启动检查(checkApi)与运行时验证 hook 共用此构造,保证两处行为完全一致。
26
+ */
27
+ export function buildApiSchema(fields, required, constraints) {
28
+ const requiredSet = new Set(required || []);
29
+ return {
30
+ paramType: "object",
31
+ fields: Object.fromEntries(
32
+ Object.entries(fields || {}).map(([field, definition]) => [
33
+ field,
34
+ {
35
+ ...getFieldSchema(definition),
36
+ optional: !requiredSet.has(field)
37
+ }
38
+ ])
39
+ ),
40
+ constraints: constraints
41
+ };
18
42
  }
package/checks/menu.js CHANGED
@@ -6,7 +6,8 @@ import { formatIssues } from "./validation.js";
6
6
 
7
7
  const validate = compile(menuSchema);
8
8
 
9
- const menuPathRegex = /^\/(?:core(?:\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*|(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*)?)$/;
9
+ // 路径正则以 schemas/menu.json 的 children.path pattern 为唯一来源,避免双份维护漂移
10
+ const menuPathRegex = new RegExp(menuSchema.items.fields.path.pattern);
10
11
 
11
12
  function checkMenuPaths(issues, menus) {
12
13
  const paths = new Map();
@@ -3,7 +3,7 @@
3
3
  "appPort": 3000,
4
4
  "appHost": "127.0.0.1",
5
5
  "devEmail": "dev@qq.com",
6
- "devPassword": "123456",
6
+ "devPassword": "",
7
7
  "trustProxy": false,
8
8
  "bodyLimit": 1048576,
9
9
  "upload": {
@@ -62,6 +62,7 @@
62
62
  "defaultWindow": 60,
63
63
  "key": "ip",
64
64
  "skipRoutes": [],
65
- "failOpen": true
65
+ "failOpen": true,
66
+ "rules": [{ "path": "/api/core/tongJi/*Report", "limit": 120, "window": 60 }]
66
67
  }
67
68
  }
package/exports.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { Logger } from "./libs/logger/index.js";
2
+ export { genShortId, snakeCase, toSessionTtlSeconds } from "./utils/util.js";
2
3
  export { syncDb } from "./scripts/syncDb/index.js";
package/hooks/auth.js CHANGED
@@ -29,8 +29,14 @@ export default {
29
29
  ctx.roleCode = sessionData.roleCode;
30
30
  ctx.roleType = sessionData.roleType;
31
31
 
32
+ // 距上次续期不足半衰期时不续,避免每个请求两次 Redis 往返
33
+ const renewedAt = Number(sessionData.renewedAt || sessionData.loginAt || 0);
34
+ if (Date.now() - renewedAt < (ttlSeconds * 1000) / 2) {
35
+ return;
36
+ }
37
+
32
38
  try {
33
- await befly.redis.expire(sessionKey, ttlSeconds);
39
+ await Promise.all([befly.redis.expire(sessionKey, ttlSeconds), befly.redis.setObject(sessionKey, { ...sessionData, renewedAt: Date.now() }, ttlSeconds)]);
34
40
  } catch (error) {
35
41
  Logger.warn("刷新会话有效期失败", { error: error, sessionKey: sessionKey });
36
42
  }
@@ -1,11 +1,12 @@
1
1
  import { HOOK_ORDER } from "../configs/constConfig.js";
2
- import { isValidPositiveInt } from "../utils/is.js";
2
+ import { isSuperAdminContext, isValidPositiveInt } from "../utils/is.js";
3
3
  // 相对导入
4
4
  import { ErrorResponse } from "../utils/response.js";
5
5
 
6
6
  /**
7
7
  * 权限检查钩子
8
8
  * - 接口无需权限(auth=false):直接通过
9
+ * - 超级管理员专属(auth="dev"):仅管理员表 dev/admin 角色可用,不参与角色授权
9
10
  * - auth 为角色类型白名单(string[],如 ["admin"] / ["user"]):按 ctx.roleType 校验。
10
11
  * 类型与角色解耦:管理员表(beflyAdmin)roleType 固定 "admin",用户表固定 "user",
11
12
  * 任意角色 code 的管理员共享 "admin" 类型门槛,细粒度授权由下方集合检查(roleCode)负责
@@ -33,6 +34,26 @@ export default {
33
34
  return;
34
35
  }
35
36
 
37
+ // 3.4 超级管理员专属接口(auth="dev"):仅管理员表 dev/admin 角色可用,
38
+ // 为天生权限,不查角色权限集合,也不允许分配给任何角色
39
+ if (ctx.apiAuth === "dev") {
40
+ if (isSuperAdminContext(ctx)) {
41
+ return;
42
+ }
43
+ ctx.response = ErrorResponse(
44
+ ctx,
45
+ `无权访问 ${ctx.apiName} 接口`,
46
+ 1,
47
+ null,
48
+ {
49
+ apiName: ctx.apiName,
50
+ apiPath: ctx.apiPath
51
+ },
52
+ "permission"
53
+ );
54
+ return;
55
+ }
56
+
36
57
  // 3.5 apiAuth 为角色类型白名单时,仅做 ctx.roleType 校验
37
58
  if (Array.isArray(ctx.apiAuth) && ctx.apiAuth.includes(ctx.roleType) === false) {
38
59
  ctx.response = ErrorResponse(