befly 3.76.7 → 3.77.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/Befly.js +97 -34
  2. package/apis/admin/_meta.js +3 -0
  3. package/apis/admin/delete.js +1 -1
  4. package/apis/admin/detail.js +1 -0
  5. package/apis/admin/insert.js +1 -1
  6. package/apis/admin/update.js +1 -1
  7. package/apis/api/_meta.js +3 -0
  8. package/apis/auth/_meta.js +3 -0
  9. package/apis/auth/login.js +3 -2
  10. package/apis/dashboard/_meta.js +3 -0
  11. package/apis/dashboard/systemResources.js +12 -1
  12. package/apis/dict/_meta.js +3 -0
  13. package/apis/dict/detail.js +1 -0
  14. package/apis/dictType/_meta.js +3 -0
  15. package/apis/dictType/detail.js +1 -0
  16. package/apis/email/_meta.js +3 -0
  17. package/apis/email/config.js +1 -1
  18. package/apis/loginLog/_meta.js +3 -0
  19. package/apis/menu/_meta.js +3 -0
  20. package/apis/operateLog/_meta.js +3 -0
  21. package/apis/role/_meta.js +3 -0
  22. package/apis/role/apiSave.js +14 -1
  23. package/apis/role/detail.js +1 -0
  24. package/apis/role/menuSave.js +1 -1
  25. package/apis/source/_meta.js +3 -0
  26. package/apis/tongJi/_meta.js +3 -0
  27. package/apis/tongJi/_tongJi.js +16 -0
  28. package/apis/tongJi/dailyReport.js +5 -1
  29. package/apis/tongJi/dailyStatsDistribution.js +5 -4
  30. package/apis/tongJi/errorReport.js +8 -2
  31. package/apis/tongJi/todayOnline.js +2 -5
  32. package/apis/upload/_meta.js +3 -0
  33. package/checks/api.js +2 -6
  34. package/checks/field.js +30 -6
  35. package/checks/menu.js +2 -1
  36. package/checks/table.js +1 -1
  37. package/configs/beflyConfig.json +7 -3
  38. package/exports.js +1 -0
  39. package/hooks/auth.js +7 -1
  40. package/hooks/permission.js +22 -1
  41. package/hooks/rateLimit.js +36 -5
  42. package/hooks/validator.js +5 -16
  43. package/index.js +46 -38
  44. package/libs/cacheHelper.js +13 -18
  45. package/libs/logger/logger.js +103 -24
  46. package/libs/logger/sanitize.js +40 -15
  47. package/libs/memCache.js +53 -0
  48. package/libs/mysql/dbHelper.js +39 -71
  49. package/libs/mysql/dbParse.js +5 -1
  50. package/libs/mysql/sql/sqlBuilder.js +9 -0
  51. package/libs/redis/redis.js +44 -57
  52. package/libs/smtpText.js +41 -22
  53. package/libs/validator/compiler.js +3 -2
  54. package/libs/validator/parser.js +11 -5
  55. package/libs/validator/util.js +1 -5
  56. package/package.json +1 -1
  57. package/paths.js +23 -10
  58. package/router/static.js +13 -6
  59. package/schemas/api.json +10 -0
  60. package/schemas/config.json +38 -0
  61. package/sync/api.js +18 -7
  62. package/sync/dev.js +27 -3
  63. package/sync/menu.js +3 -3
  64. package/sync/syncUtil.js +12 -6
  65. package/tables/api.json +21 -0
  66. package/tables/emailLog.json +6 -2
  67. package/tables/menu.json +3 -0
  68. package/utils/is.js +7 -0
  69. package/utils/prettyError.js +102 -0
  70. package/utils/scanFiles.js +85 -46
  71. package/utils/scanSources.js +56 -4
  72. package/utils/util.js +5 -1
package/Befly.js CHANGED
@@ -13,6 +13,7 @@ import { syncMenu } from "./sync/menu.js";
13
13
  import { calcPerfTime } from "./utils/calcPerfTime.js";
14
14
  import { createError } from "./utils/error.js";
15
15
  import { isPrimaryProcess } from "./utils/is.js";
16
+ import { printErrorSummary } from "./utils/prettyError.js";
16
17
  import { waitFor } from "./utils/util.js";
17
18
 
18
19
  const SYNC_READY_KEY = "befly:syncReady";
@@ -22,10 +23,9 @@ async function assertRuntimeReady(context) {
22
23
  if (!context[key]) throw createError(`启动失败:ctx.${key} 未初始化`, { code: "runtime", subsystem: "start", operation: "runtimeReady" });
23
24
  }
24
25
 
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
- }
26
+ const coreTables = [BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ADMIN_TABLE, BEFLY_ROLE_TABLE];
27
+ const tableResults = await Promise.all(coreTables.map((table) => context.mysql.tableExists(table)));
28
+ const missingTables = coreTables.filter((table, index) => !tableResults[index].data);
29
29
  if (missingTables.length > 0) throw createError(`同步依赖表缺失: ${missingTables.join("、")}`, { code: "runtime", subsystem: "start", operation: "tableExists", missingTables: missingTables });
30
30
  }
31
31
 
@@ -41,9 +41,12 @@ export class Befly {
41
41
  this.apis = init.apis || [];
42
42
  this.hooks = init.hooks || [];
43
43
  this.plugins = init.plugins || [];
44
+ this.activePlugins = [];
44
45
  this.corns = init.corns || [];
45
46
  this.server = null;
46
47
  this.started = false;
48
+ this.stopping = null;
49
+ this.signalHandlers = null;
47
50
  }
48
51
 
49
52
  async start() {
@@ -55,6 +58,7 @@ export class Befly {
55
58
  for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
56
59
  const pluginStartTime = Bun.nanoseconds();
57
60
  this.context[item.fileName] = await item.handler(this.context);
61
+ this.activePlugins.push(item);
58
62
  Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
59
63
  }
60
64
  Logger.info(`启动 插件初始化完成,耗时 ${calcPerfTime(pluginsStartTime)}`);
@@ -64,21 +68,16 @@ export class Befly {
64
68
  Logger.info(`启动 运行时依赖检查完成,耗时 ${calcPerfTime(runtimeReadyStartTime)}`);
65
69
 
66
70
  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();
71
+ const syncStartTime = Bun.nanoseconds();
72
+ const syncApiStart = Bun.nanoseconds();
73
+ const syncMenuStart = Bun.nanoseconds();
74
+ const syncDevStart = Bun.nanoseconds();
75
+ // api/menu 互不依赖可并行;syncDev 读取 api/menu 落库结果、syncCache 读取 syncDev 写入的角色表,需按序串行
76
+ 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
77
  await syncDev(this.context);
77
- Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(syncDevStartTime)}`);
78
-
79
- const syncCacheStartTime = Bun.nanoseconds();
78
+ Logger.info(`启动 开发者数据同步完成,耗时 ${calcPerfTime(syncDevStart)}`);
80
79
  await syncCache(this.context);
81
- Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(syncCacheStartTime)}`);
80
+ Logger.info(`启动 缓存同步完成,耗时 ${calcPerfTime(syncStartTime)}`);
82
81
 
83
82
  const syncReadyStartTime = Bun.nanoseconds();
84
83
  await this.context.redis.setString(SYNC_READY_KEY, String(Date.now()), 300);
@@ -88,7 +87,11 @@ export class Befly {
88
87
  const startAt = Date.now();
89
88
  await waitFor({
90
89
  label: "主进程同步完成",
91
- check: async () => Number(await this.context.redis.getString(SYNC_READY_KEY)) > startAt
90
+ // 主进程先于本进程完成同步时标记时间早于 startAt,60 秒内的近期标记视为就绪
91
+ check: async () => {
92
+ const marker = Number(await this.context.redis.getString(SYNC_READY_KEY));
93
+ return marker > startAt || (marker > 0 && startAt - marker < 60_000);
94
+ }
92
95
  });
93
96
  Logger.info(`启动 等待主进程同步完成,耗时 ${calcPerfTime(syncWaitStartTime)}`);
94
97
  }
@@ -136,31 +139,91 @@ export class Befly {
136
139
  Logger.info(`启动总耗时 ${startupTime}`);
137
140
  Logger.info(`${this.context.config.appName} 启动成功`, { url: this.server.url, startupTime: startupTime });
138
141
 
139
- process.stdout.write(`${this.context.config.appName} 启动成功\n`);
140
- process.stdout.write(`启动耗时: ${startupTime}\n`);
141
- process.stdout.write(`监听地址: ${this.server.url}\n`);
142
- process.stdout.write(`Mysql 数据库地址: ${this.context.config.mysql.hostname}:${this.context.config.mysql.port}\n`);
143
- process.stdout.write(`Redis 缓存地址: ${this.context.config.redis.hostname}:${this.context.config.redis.port}\n`);
142
+ this.signalHandlers = {
143
+ SIGINT: async () => {
144
+ let exitCode = 130;
145
+ try {
146
+ await this.stop();
147
+ } catch {
148
+ exitCode = 1;
149
+ }
150
+ process.exit(exitCode);
151
+ },
152
+ SIGTERM: async () => {
153
+ let exitCode = 143;
154
+ try {
155
+ await this.stop();
156
+ } catch {
157
+ exitCode = 1;
158
+ }
159
+ process.exit(exitCode);
160
+ }
161
+ };
162
+ process.once("SIGINT", this.signalHandlers.SIGINT);
163
+ process.once("SIGTERM", this.signalHandlers.SIGTERM);
144
164
  return this.server;
145
165
  } catch (error) {
166
+ printErrorSummary(error, { title: "项目启动失败" });
146
167
  Logger.error("项目启动失败", error);
147
- await this.stop();
168
+ try {
169
+ await this.stop();
170
+ } catch {}
148
171
  throw createError("运行时错误", { cause: error, code: error?.code || "runtime", subsystem: "start", operation: "start" });
149
172
  }
150
173
  }
151
174
 
152
175
  async stop() {
153
- this.server?.stop();
154
- this.server = null;
155
- this.context.cron?.stopAll();
176
+ if (this.stopping) return this.stopping;
156
177
 
157
- for (const item of this.plugins.toSorted((a, b) => b.order - a.order)) {
158
- const resource = this.context[item.fileName];
159
- if (resource?.close) await resource.close();
160
- else if (resource?.stop) await resource.stop();
161
- }
178
+ const stopping = (async () => {
179
+ let stopError;
162
180
 
163
- this.started = false;
164
- await Logger.flush();
181
+ if (this.signalHandlers) {
182
+ process.off("SIGINT", this.signalHandlers.SIGINT);
183
+ process.off("SIGTERM", this.signalHandlers.SIGTERM);
184
+ this.signalHandlers = null;
185
+ }
186
+
187
+ try {
188
+ this.server?.stop();
189
+ } catch (error) {
190
+ stopError = error;
191
+ Logger.error("停止 HTTP 服务失败", error);
192
+ }
193
+ this.server = null;
194
+
195
+ const cron = this.context.cron;
196
+ this.context.cron = null;
197
+ try {
198
+ cron?.stopAll();
199
+ } catch (error) {
200
+ stopError ||= error;
201
+ Logger.error("停止定时器失败", error);
202
+ }
203
+
204
+ for (const item of this.activePlugins.toReversed()) {
205
+ const resource = this.context[item.fileName];
206
+ try {
207
+ if (resource?.close) await resource.close();
208
+ else if (resource?.stop) await resource.stop();
209
+ } catch (error) {
210
+ stopError ||= error;
211
+ Logger.error(`关闭插件 ${item.fileName} 失败`, error);
212
+ }
213
+ }
214
+ this.activePlugins = [];
215
+
216
+ this.started = false;
217
+ await Logger.shutdown();
218
+
219
+ if (stopError) throw createError("项目停止失败", { cause: stopError, code: "runtime", subsystem: "stop", operation: "stop" });
220
+ })();
221
+ this.stopping = stopping;
222
+
223
+ try {
224
+ return await stopping;
225
+ } finally {
226
+ if (this.stopping === stopping) this.stopping = null;
227
+ }
165
228
  }
166
229
  }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "系统管理"
3
+ };
@@ -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,
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "接口管理"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "认证登录"
3
+ };
@@ -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
 
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "仪表盘"
3
+ };
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "字典项"
3
+ };
@@ -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,
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "字典类型"
3
+ };
@@ -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,
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "邮件"
3
+ };
@@ -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) => {
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "登录日志"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "菜单"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "操作日志"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "角色"
3
+ };
@@ -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
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "资源"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "数据统计"
3
+ };
@@ -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("产品代号未注册");
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("产品代号未注册");
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: {
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: "上传"
3
+ };
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();
package/checks/table.js CHANGED
@@ -26,7 +26,7 @@ function checkFieldDefinition(issues, tableName, fieldName, field) {
26
26
  if (typeof field.name !== "string" || !field.name || field.name !== field.name.trim() || field.name.includes("\n") || field.name.length > 20) addIssue(issues, [...path, "name"], "必须是 1..20 字符的简短名称,不允许首尾空白或换行");
27
27
  if (field.detail !== undefined && (typeof field.detail !== "string" || !field.detail || field.detail !== field.detail.trim())) addIssue(issues, [...path, "detail"], "必须是非空无首尾空白字符串");
28
28
  if (!fieldParamTypes[field.fieldType]) addIssue(issues, [...path, "fieldType"], "必须是 integer、number、varchar、text");
29
- if (typeof field.fieldNullable !== "boolean") addIssue(issues, [...path, "fieldNullable"], "必须是 boolean");
29
+ if (typeof field.fieldNullable !== "boolean") addIssue(issues, [...path, "fieldNullable"], "必须是布尔值");
30
30
 
31
31
  let parseField;
32
32
  try {
@@ -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": {
@@ -19,7 +19,10 @@
19
19
  "excludeApisLog": ["/api/core/tongJi/*Report"],
20
20
  "logger": {
21
21
  "debug": true,
22
- "excludeFields": ["password", "token", "secret"]
22
+ "excludeFields": ["password", "token", "secret"],
23
+ "truncateStringLength": 512,
24
+ "truncateArrayLength": 20,
25
+ "truncatePreviewLength": 200
23
26
  },
24
27
  "mysql": {
25
28
  "hostname": "127.0.0.1",
@@ -62,6 +65,7 @@
62
65
  "defaultWindow": 60,
63
66
  "key": "ip",
64
67
  "skipRoutes": [],
65
- "failOpen": true
68
+ "failOpen": true,
69
+ "rules": [{ "path": "/api/core/tongJi/*Report", "limit": 120, "window": 60 }]
66
70
  }
67
71
  }
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";