befly 3.24.17 → 3.24.19

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.
@@ -5,8 +5,8 @@
5
5
  * 1. 刷新接口缓存(apis:all)
6
6
  * 2. 刷新菜单缓存(menus:all)
7
7
  * 3. 刷新角色缓存(role:info:{code})
8
- * 4. 重建角色接口权限缓存(role:apis:{code},Set
9
- * 5. 重建角色菜单权限缓存(role:menus:{code},Set
8
+ * 4. 重建角色接口权限缓存(版本化 Set + activeVersion 原子切换)
9
+ * 5. 重建角色菜单权限缓存(版本化 Set + activeVersion 原子切换)
10
10
  *
11
11
  * 使用场景:
12
12
  * - 执行数据库同步后
@@ -10,6 +10,10 @@ export default {
10
10
  fields: {},
11
11
  required: [],
12
12
  handler: async (befly) => {
13
+ const isBunRuntime = typeof Bun !== "undefined";
14
+ const runtimeVersion = isBunRuntime ? `Bun ${Bun.version}` : `Node.js ${process.version}`;
15
+ const nodeCompatVersion = process.version;
16
+
13
17
  let databaseVersion = "Unknown";
14
18
  try {
15
19
  const versionResult = await befly.mysql.execute("SELECT VERSION() as version");
@@ -35,7 +39,8 @@ export default {
35
39
  return befly.tool.Yes("获取成功", {
36
40
  os: `${os.type()} ${os.arch()}`,
37
41
  server: `${os.platform()} ${os.release()}`,
38
- nodeVersion: process.version,
42
+ runtimeVersion: runtimeVersion,
43
+ nodeVersion: nodeCompatVersion,
39
44
  database: `MySQL ${databaseVersion}`,
40
45
  cache: `Redis ${redisVersion}`,
41
46
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -1,3 +1,5 @@
1
+ import { requestMetrics } from "#root/lib/requestMetrics.js";
2
+
1
3
  export default {
2
4
  name: "获取性能指标",
3
5
  method: "POST",
@@ -6,15 +8,16 @@ export default {
6
8
  fields: {},
7
9
  required: [],
8
10
  handler: async (befly) => {
11
+ const metrics = requestMetrics.getSnapshot();
12
+
9
13
  return befly.tool.Yes("获取成功", {
10
- avgResponseTime: 125,
11
- qps: 856,
12
- errorRate: 0.8,
13
- activeConnections: 45,
14
- slowestApi: {
15
- path: "/api/core/menu/select",
16
- time: 450
17
- }
14
+ avgResponseTime: Number(metrics.avgResponseTime || 0),
15
+ qps: Number(metrics.qps || 0),
16
+ errorRate: Number(metrics.errorRate || 0),
17
+ activeRequests: Number(metrics.activeRequests || 0),
18
+ activeConnections: Number(metrics.activeRequests || 0),
19
+ slowestApi: metrics.slowestApi || null,
20
+ slowestApis: Array.isArray(metrics.slowestApis) ? metrics.slowestApis : []
18
21
  });
19
22
  }
20
23
  };
@@ -1,3 +1,72 @@
1
+ import cacheHealthApi from "#root/apis/tongJi/cacheHealth.js";
2
+
3
+ function isEmailConfigured(emailConfig) {
4
+ if (!emailConfig || typeof emailConfig !== "object") {
5
+ return false;
6
+ }
7
+
8
+ return Boolean(String(emailConfig.host || "").trim() && String(emailConfig.user || "").trim() && String(emailConfig.pass || "").trim());
9
+ }
10
+
11
+ function buildServiceItem(name, status, responseTime) {
12
+ return {
13
+ name: name,
14
+ status: status,
15
+ responseTime: responseTime
16
+ };
17
+ }
18
+
19
+ async function probeMysqlService(befly) {
20
+ try {
21
+ const startTime = Date.now();
22
+ await befly.mysql.execute("SELECT 1");
23
+ const responseTime = Date.now() - startTime;
24
+ return buildServiceItem("数据库", "running", `${responseTime}ms`);
25
+ } catch (error) {
26
+ befly.logger.error("数据库状态检测失败", error);
27
+ return buildServiceItem("数据库", "stopped", "-");
28
+ }
29
+ }
30
+
31
+ async function probeRedisService(befly) {
32
+ if (!befly.redis) {
33
+ return buildServiceItem("Redis", "stopped", "-");
34
+ }
35
+
36
+ try {
37
+ const startTime = Date.now();
38
+ await befly.redis.ping();
39
+ const responseTime = Date.now() - startTime;
40
+ return buildServiceItem("Redis", "running", `${responseTime}ms`);
41
+ } catch (error) {
42
+ befly.logger.error("Redis状态检测失败", error);
43
+ return buildServiceItem("Redis", "stopped", "-");
44
+ }
45
+ }
46
+
47
+ function buildStaticServices(befly) {
48
+ return [buildServiceItem("文件系统", "running", "-"), buildServiceItem("邮件服务", isEmailConfigured(befly.config?.email) ? "running" : "unconfigured", "-"), buildServiceItem("OSS存储", "unconfigured", "-")];
49
+ }
50
+
51
+ function buildCacheHealthSummary(cacheHealthData) {
52
+ if (!cacheHealthData) {
53
+ return null;
54
+ }
55
+
56
+ return {
57
+ redisStatus: cacheHealthData.redis?.status || "unknown",
58
+ onlineReady: Boolean(cacheHealthData.online?.ready),
59
+ infoReady: Boolean(cacheHealthData.info?.ready),
60
+ errorReady: Boolean(cacheHealthData.error?.ready),
61
+ roleApisReady: Boolean(cacheHealthData.roleCache?.apis?.ready),
62
+ roleMenusReady: Boolean(cacheHealthData.roleCache?.menus?.ready),
63
+ infoFallbackCount: Number(cacheHealthData.fallback?.infoStats || 0),
64
+ errorFallbackCount: Number(cacheHealthData.fallback?.errorStats || 0),
65
+ infoFallbackTodayCount: Number(cacheHealthData.fallback?.today?.infoStats || 0),
66
+ errorFallbackTodayCount: Number(cacheHealthData.fallback?.today?.errorStats || 0)
67
+ };
68
+ }
69
+
1
70
  export default {
2
71
  name: "获取服务状态",
3
72
  method: "POST",
@@ -6,70 +75,19 @@ export default {
6
75
  fields: {},
7
76
  required: [],
8
77
  handler: async (befly) => {
9
- const services = [];
78
+ const services = [await probeMysqlService(befly), await probeRedisService(befly), ...buildStaticServices(befly)];
79
+
80
+ let cacheHealthSummary = null;
10
81
 
11
82
  try {
12
- const startTime = Date.now();
13
- await befly.mysql.execute("SELECT 1");
14
- const responseTime = Date.now() - startTime;
15
- services.push({
16
- name: "数据库",
17
- status: "running",
18
- responseTime: `${responseTime}ms`
19
- });
20
- } catch (error) {
21
- befly.logger.error("数据库状态检测失败", error);
22
- services.push({
23
- name: "数据库",
24
- status: "stopped",
25
- responseTime: "-"
26
- });
27
- }
83
+ const cacheHealthResult = await cacheHealthApi.handler(befly);
84
+ const cacheHealthData = cacheHealthResult?.data || null;
28
85
 
29
- if (befly.redis) {
30
- try {
31
- const startTime = Date.now();
32
- await befly.redis.ping();
33
- const responseTime = Date.now() - startTime;
34
- services.push({
35
- name: "Redis",
36
- status: "running",
37
- responseTime: `${responseTime}ms`
38
- });
39
- } catch (error) {
40
- befly.logger.error("Redis状态检测失败", error);
41
- services.push({
42
- name: "Redis",
43
- status: "stopped",
44
- responseTime: "-"
45
- });
46
- }
47
- } else {
48
- services.push({
49
- name: "Redis",
50
- status: "stopped",
51
- responseTime: "-"
52
- });
86
+ cacheHealthSummary = buildCacheHealthSummary(cacheHealthData);
87
+ } catch (error) {
88
+ befly.logger.error("缓存健康状态聚合失败", error);
53
89
  }
54
90
 
55
- services.push({
56
- name: "文件系统",
57
- status: "running",
58
- responseTime: "-"
59
- });
60
-
61
- services.push({
62
- name: "邮件服务",
63
- status: "unconfigured",
64
- responseTime: "-"
65
- });
66
-
67
- services.push({
68
- name: "OSS存储",
69
- status: "unconfigured",
70
- responseTime: "-"
71
- });
72
-
73
- return befly.tool.Yes("获取成功", { services: services });
91
+ return befly.tool.Yes("获取成功", { services: services, cacheHealth: cacheHealthSummary });
74
92
  }
75
93
  };
@@ -0,0 +1,214 @@
1
+ import { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
2
+
3
+ const ONLINE_ACTIVE_KEY = "online:active";
4
+ const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
5
+ const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
6
+
7
+ function getInfoStatsFallbackDailyCountKey(reportDate) {
8
+ return `stats:fallback:infoStats:count:day:${reportDate}`;
9
+ }
10
+
11
+ function getErrorStatsFallbackDailyCountKey(reportDate) {
12
+ return `stats:fallback:errorStats:count:day:${reportDate}`;
13
+ }
14
+
15
+ function toNumber(value) {
16
+ const num = Number(value);
17
+
18
+ if (!Number.isFinite(num)) {
19
+ return 0;
20
+ }
21
+
22
+ return num;
23
+ }
24
+
25
+ function getWeekStartDate(timestamp = Date.now()) {
26
+ const date = Reflect.construct(Date, [timestamp]);
27
+ const day = date.getDay();
28
+ const offset = day === 0 ? -6 : 1 - day;
29
+
30
+ return getDateYmdNumber(addDays(timestamp, offset));
31
+ }
32
+
33
+ function getMonthStartDate(timestamp = Date.now()) {
34
+ const date = Reflect.construct(Date, [timestamp]);
35
+
36
+ return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
37
+ }
38
+
39
+ function getOnlineReportTimeKey(periodType, periodValue) {
40
+ return `online:${periodType}:${periodValue}:reportTime`;
41
+ }
42
+
43
+ function getInfoReportTimeKey(periodType, periodValue) {
44
+ return `info:${periodType}:${periodValue}:reportTime`;
45
+ }
46
+
47
+ function getErrorCountKey(periodType, periodValue) {
48
+ return `error:${periodType}:${periodValue}:count`;
49
+ }
50
+
51
+ async function getRedisStringNumber(redis, key) {
52
+ return toNumber(await redis.getString(key));
53
+ }
54
+
55
+ async function getRedisSetCount(redis, key) {
56
+ if (typeof redis.scard !== "function") {
57
+ return 0;
58
+ }
59
+
60
+ return toNumber(await redis.scard(key));
61
+ }
62
+
63
+ async function getRedisSortedSetCount(redis, key) {
64
+ if (typeof redis.zcard !== "function") {
65
+ return 0;
66
+ }
67
+
68
+ return toNumber(await redis.zcard(key));
69
+ }
70
+
71
+ async function getRedisConnectivity(redis) {
72
+ if (!redis || typeof redis.ping !== "function") {
73
+ return {
74
+ status: "unavailable",
75
+ responseTime: -1
76
+ };
77
+ }
78
+
79
+ try {
80
+ const start = Date.now();
81
+ await redis.ping();
82
+
83
+ return {
84
+ status: "running",
85
+ responseTime: Date.now() - start
86
+ };
87
+ } catch {
88
+ return {
89
+ status: "stopped",
90
+ responseTime: -1
91
+ };
92
+ }
93
+ }
94
+
95
+ export default {
96
+ name: "获取统计缓存健康状态",
97
+ method: "POST",
98
+ body: "none",
99
+ auth: true,
100
+ fields: {},
101
+ required: [],
102
+ handler: async (befly) => {
103
+ const now = Date.now();
104
+ const reportDate = getDateYmdNumber(now);
105
+ const weekStartDate = getWeekStartDate(now);
106
+ const monthStartDate = getMonthStartDate(now);
107
+ const redisConnectivity = await getRedisConnectivity(befly.redis);
108
+
109
+ if (!befly.redis) {
110
+ return befly.tool.Yes("获取成功", {
111
+ queryTime: now,
112
+ redis: redisConnectivity,
113
+ online: null,
114
+ info: null,
115
+ error: null,
116
+ roleCache: null,
117
+ hints: ["Redis 不可用,统计接口将回退到 MySQL 聚合"]
118
+ });
119
+ }
120
+
121
+ const onlineTodayReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("day", reportDate));
122
+ const onlineWeekReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("week", weekStartDate));
123
+ const onlineMonthReportTime = await getRedisStringNumber(befly.redis, getOnlineReportTimeKey("month", monthStartDate));
124
+
125
+ const infoTodayReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("day", reportDate));
126
+ const infoWeekReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("week", weekStartDate));
127
+ const infoMonthReportTime = await getRedisStringNumber(befly.redis, getInfoReportTimeKey("month", monthStartDate));
128
+
129
+ const errorDayCount = await getRedisStringNumber(befly.redis, getErrorCountKey("day", reportDate));
130
+ const errorWeekCount = await getRedisStringNumber(befly.redis, getErrorCountKey("week", weekStartDate));
131
+ const errorMonthCount = await getRedisStringNumber(befly.redis, getErrorCountKey("month", monthStartDate));
132
+
133
+ const roleApisVersion = String((await befly.redis.getString("role:apis:activeVersion")) || "");
134
+ const roleMenusVersion = String((await befly.redis.getString("role:menus:activeVersion")) || "");
135
+
136
+ const online = {
137
+ activeMembers: await getRedisSortedSetCount(befly.redis, ONLINE_ACTIVE_KEY),
138
+ reportTime: {
139
+ today: onlineTodayReportTime,
140
+ week: onlineWeekReportTime,
141
+ month: onlineMonthReportTime
142
+ },
143
+ ready: onlineTodayReportTime > 0 || onlineWeekReportTime > 0 || onlineMonthReportTime > 0
144
+ };
145
+
146
+ const info = {
147
+ reportTime: {
148
+ today: infoTodayReportTime,
149
+ week: infoWeekReportTime,
150
+ month: infoMonthReportTime
151
+ },
152
+ daySourcesCount: await getRedisSetCount(befly.redis, `info:day:${reportDate}:sources:names`),
153
+ ready: infoTodayReportTime > 0 || infoWeekReportTime > 0 || infoMonthReportTime > 0
154
+ };
155
+
156
+ const error = {
157
+ count: {
158
+ today: errorDayCount,
159
+ week: errorWeekCount,
160
+ month: errorMonthCount
161
+ },
162
+ ready: errorDayCount > 0 || errorWeekCount > 0 || errorMonthCount > 0
163
+ };
164
+
165
+ const roleCache = {
166
+ apis: {
167
+ activeVersion: roleApisVersion,
168
+ ready: roleApisVersion.length > 0
169
+ },
170
+ menus: {
171
+ activeVersion: roleMenusVersion,
172
+ ready: roleMenusVersion.length > 0
173
+ }
174
+ };
175
+
176
+ const fallback = {
177
+ infoStats: await getRedisStringNumber(befly.redis, INFO_STATS_FALLBACK_COUNT_KEY),
178
+ errorStats: await getRedisStringNumber(befly.redis, ERROR_STATS_FALLBACK_COUNT_KEY),
179
+ today: {
180
+ infoStats: await getRedisStringNumber(befly.redis, getInfoStatsFallbackDailyCountKey(reportDate)),
181
+ errorStats: await getRedisStringNumber(befly.redis, getErrorStatsFallbackDailyCountKey(reportDate))
182
+ }
183
+ };
184
+
185
+ const hints = [];
186
+
187
+ if (!online.ready) {
188
+ hints.push("online 统计预聚合尚未建立,onlineStats 将返回空或低命中结果");
189
+ }
190
+ if (!info.ready) {
191
+ hints.push("info 统计预聚合尚未建立,infoStats 将回退到 MySQL 聚合");
192
+ }
193
+ if (!error.ready) {
194
+ hints.push("error 统计预聚合尚未建立,errorStats 将回退到 MySQL 聚合");
195
+ }
196
+ if (!roleCache.apis.ready || !roleCache.menus.ready) {
197
+ hints.push("角色权限版本缓存未就绪,建议执行一次 cacheRefresh");
198
+ }
199
+ if (fallback.infoStats > 0 || fallback.errorStats > 0) {
200
+ hints.push("检测到统计查询回退到 MySQL,可关注预聚合写入是否稳定,必要时可执行 tongJi/fallbackReset 重置观测计数");
201
+ }
202
+
203
+ return befly.tool.Yes("获取成功", {
204
+ queryTime: now,
205
+ redis: redisConnectivity,
206
+ online: online,
207
+ info: info,
208
+ error: error,
209
+ roleCache: roleCache,
210
+ fallback: fallback,
211
+ hints: hints
212
+ });
213
+ }
214
+ };
@@ -1,8 +1,9 @@
1
1
  import { UAParser } from "ua-parser-js";
2
2
 
3
- import { getDateYmdNumber, getTimeBucketStart } from "#root/utils/datetime.js";
3
+ import { addDays, getDateYmdNumber, getTimeBucketStart } from "#root/utils/datetime.js";
4
4
 
5
5
  const VISIT_STATS_BUCKET_MS = 30 * 60 * 1000;
6
+ const ERROR_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
6
7
 
7
8
  function getErrorReportUaData(ctx) {
8
9
  let userAgent = "";
@@ -29,6 +30,74 @@ function getErrorReportUaData(ctx) {
29
30
  };
30
31
  }
31
32
 
33
+ function getErrorStatsWeekStartDate(timestamp = Date.now()) {
34
+ const date = Reflect.construct(Date, [timestamp]);
35
+ const day = date.getDay();
36
+ const offset = day === 0 ? -6 : 1 - day;
37
+
38
+ return getDateYmdNumber(addDays(timestamp, offset));
39
+ }
40
+
41
+ function getErrorStatsMonthStartDate(timestamp = Date.now()) {
42
+ const date = Reflect.construct(Date, [timestamp]);
43
+
44
+ return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
45
+ }
46
+
47
+ function getErrorStatsPeriodCountKey(periodType, periodValue) {
48
+ return `error:${periodType}:${periodValue}:count`;
49
+ }
50
+
51
+ function getErrorStatsDayBucketsKey(bucketDate) {
52
+ return `error:day:${bucketDate}:buckets`;
53
+ }
54
+
55
+ function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
56
+ return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
57
+ }
58
+
59
+ function getErrorStatsDayTypesKey(bucketDate) {
60
+ return `error:day:${bucketDate}:types`;
61
+ }
62
+
63
+ function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
64
+ return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
65
+ }
66
+
67
+ async function setErrorStatsRedisTtl(befly, keys) {
68
+ for (const key of keys) {
69
+ await befly.redis.expire(key, ERROR_STATS_REDIS_TTL_SECONDS);
70
+ }
71
+ }
72
+
73
+ async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
74
+ if (!befly.redis) {
75
+ return;
76
+ }
77
+
78
+ const weekStartDate = getErrorStatsWeekStartDate(now);
79
+ const monthStartDate = getErrorStatsMonthStartDate(now);
80
+ const dayCountKey = getErrorStatsPeriodCountKey("day", bucketDate);
81
+ const weekCountKey = getErrorStatsPeriodCountKey("week", weekStartDate);
82
+ const monthCountKey = getErrorStatsPeriodCountKey("month", monthStartDate);
83
+ const dayBucketsKey = getErrorStatsDayBucketsKey(bucketDate);
84
+ const dayBucketCountKey = getErrorStatsDayBucketCountKey(bucketDate, bucketTime);
85
+ const dayTypesKey = getErrorStatsDayTypesKey(bucketDate);
86
+ const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate, errorType);
87
+
88
+ await befly.redis.incr(dayCountKey);
89
+ await befly.redis.incr(weekCountKey);
90
+ await befly.redis.incr(monthCountKey);
91
+
92
+ await befly.redis.sadd(dayBucketsKey, [String(bucketTime)]);
93
+ await befly.redis.incr(dayBucketCountKey);
94
+
95
+ await befly.redis.sadd(dayTypesKey, [String(errorType || "unknown")]);
96
+ await befly.redis.incr(dayTypeCountKey);
97
+
98
+ await setErrorStatsRedisTtl(befly, [dayCountKey, weekCountKey, monthCountKey, dayBucketsKey, dayBucketCountKey, dayTypesKey, dayTypeCountKey]);
99
+ }
100
+
32
101
  export default {
33
102
  name: "上报错误报告",
34
103
  method: "POST",
@@ -82,6 +151,8 @@ export default {
82
151
  }
83
152
  });
84
153
 
154
+ await updateErrorStatsRedis(befly, now, bucketDate, bucketTime, ctx.body?.errorType || "unknown");
155
+
85
156
  return befly.tool.Yes("上报成功", {
86
157
  reportTime: now,
87
158
  bucketTime: bucketTime,