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.
@@ -1,6 +1,13 @@
1
1
  import { getDateYmdNumber } from "#root/utils/datetime.js";
2
2
 
3
3
  const ERROR_STATS_DAY_LIMIT = 30;
4
+ const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
5
+ const ERROR_STATS_FALLBACK_DAILY_COUNT_KEY_PREFIX = "stats:fallback:errorStats:count:day:";
6
+ const ERROR_STATS_FALLBACK_TTL_SECONDS = 7 * 24 * 60 * 60;
7
+
8
+ function getErrorStatsFallbackDailyCountKey(bucketDate) {
9
+ return `${ERROR_STATS_FALLBACK_DAILY_COUNT_KEY_PREFIX}${bucketDate}`;
10
+ }
4
11
 
5
12
  function toNumber(value) {
6
13
  const num = Number(value);
@@ -35,6 +42,144 @@ function getErrorStatsRecentDateList(now = Date.now(), limit = ERROR_STATS_DAY_L
35
42
  return list;
36
43
  }
37
44
 
45
+ function getErrorStatsPeriodCountKey(periodType, periodValue) {
46
+ return `error:${periodType}:${periodValue}:count`;
47
+ }
48
+
49
+ function getErrorStatsDayBucketsKey(bucketDate) {
50
+ return `error:day:${bucketDate}:buckets`;
51
+ }
52
+
53
+ function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
54
+ return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
55
+ }
56
+
57
+ function getErrorStatsDayTypesKey(bucketDate) {
58
+ return `error:day:${bucketDate}:types`;
59
+ }
60
+
61
+ function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
62
+ return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
63
+ }
64
+
65
+ async function getErrorStatsRedisSummary(befly, periodType, periodValue) {
66
+ return {
67
+ count: toNumber(await befly.redis.getString(getErrorStatsPeriodCountKey(periodType, periodValue)))
68
+ };
69
+ }
70
+
71
+ async function getErrorStatsRedisTrend(befly, bucketDate) {
72
+ const bucketSet = await befly.redis.smembers(getErrorStatsDayBucketsKey(bucketDate));
73
+ const trend = [];
74
+
75
+ for (const bucketTime of bucketSet) {
76
+ const itemBucketTime = toNumber(bucketTime);
77
+
78
+ trend.push({
79
+ bucketTime: itemBucketTime,
80
+ bucketDate: bucketDate,
81
+ count: toNumber(await befly.redis.getString(getErrorStatsDayBucketCountKey(bucketDate, itemBucketTime)))
82
+ });
83
+ }
84
+
85
+ trend.sort((a, b) => a.bucketTime - b.bucketTime);
86
+ return trend;
87
+ }
88
+
89
+ async function getErrorStatsRedisDays(befly, recentDateList) {
90
+ const days = [];
91
+
92
+ for (const item of recentDateList) {
93
+ days.push({
94
+ bucketDate: item,
95
+ count: toNumber(await befly.redis.getString(getErrorStatsPeriodCountKey("day", item)))
96
+ });
97
+ }
98
+
99
+ return days;
100
+ }
101
+
102
+ async function getErrorStatsRedisTopTypes(befly, bucketDate) {
103
+ const typeSet = await befly.redis.smembers(getErrorStatsDayTypesKey(bucketDate));
104
+ const topTypes = [];
105
+
106
+ for (const errorType of typeSet) {
107
+ const name = String(errorType || "unknown");
108
+ const count = toNumber(await befly.redis.getString(getErrorStatsDayTypeCountKey(bucketDate, name)));
109
+
110
+ if (count <= 0) {
111
+ continue;
112
+ }
113
+
114
+ topTypes.push({
115
+ errorType: name,
116
+ count: count
117
+ });
118
+ }
119
+
120
+ topTypes.sort((a, b) => {
121
+ if (b.count !== a.count) {
122
+ return b.count - a.count;
123
+ }
124
+
125
+ return String(a.errorType).localeCompare(String(b.errorType), "zh-CN");
126
+ });
127
+
128
+ return topTypes.slice(0, 5);
129
+ }
130
+
131
+ async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList) {
132
+ const todaySummary = await getErrorStatsRedisSummary(befly, "day", bucketDate);
133
+ const weekSummary = await getErrorStatsRedisSummary(befly, "week", weekStartDate);
134
+ const monthSummary = await getErrorStatsRedisSummary(befly, "month", monthStartDate);
135
+ const trend = await getErrorStatsRedisTrend(befly, bucketDate);
136
+ const days = await getErrorStatsRedisDays(befly, recentDateList);
137
+ const topTypes = await getErrorStatsRedisTopTypes(befly, bucketDate);
138
+
139
+ if (todaySummary.count <= 0 && weekSummary.count <= 0 && monthSummary.count <= 0 && trend.length === 0 && topTypes.length === 0) {
140
+ return null;
141
+ }
142
+
143
+ return {
144
+ today: {
145
+ bucketDate: bucketDate,
146
+ count: todaySummary.count
147
+ },
148
+ week: {
149
+ startDate: weekStartDate,
150
+ endDate: bucketDate,
151
+ count: weekSummary.count
152
+ },
153
+ month: {
154
+ startDate: monthStartDate,
155
+ endDate: bucketDate,
156
+ count: monthSummary.count
157
+ },
158
+ trend: trend,
159
+ days: days,
160
+ topTypes: topTypes
161
+ };
162
+ }
163
+
164
+ async function incrErrorStatsFallbackCount(befly, bucketDate) {
165
+ if (!befly.redis) {
166
+ return;
167
+ }
168
+
169
+ const dailyKey = getErrorStatsFallbackDailyCountKey(bucketDate);
170
+
171
+ if (typeof befly.redis.incrWithExpire === "function") {
172
+ await befly.redis.incrWithExpire(ERROR_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_TTL_SECONDS);
173
+ await befly.redis.incrWithExpire(dailyKey, ERROR_STATS_FALLBACK_TTL_SECONDS);
174
+ return;
175
+ }
176
+
177
+ await befly.redis.incr(ERROR_STATS_FALLBACK_COUNT_KEY);
178
+ await befly.redis.expire(ERROR_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_TTL_SECONDS);
179
+ await befly.redis.incr(dailyKey);
180
+ await befly.redis.expire(dailyKey, ERROR_STATS_FALLBACK_TTL_SECONDS);
181
+ }
182
+
38
183
  async function getErrorStatsSummary(befly, startDate, endDate) {
39
184
  const result = await befly.mysql.execute("SELECT SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date BETWEEN ? AND ?", [startDate, endDate]);
40
185
  const detail = result.data?.[0] || {};
@@ -56,6 +201,9 @@ export default {
56
201
  const tableReady = tableExistsResult.data === true;
57
202
  const now = Date.now();
58
203
  const bucketDate = getDateYmdNumber(now);
204
+ const weekStartDate = getErrorStatsWeekStartDate(now);
205
+ const monthStartDate = getErrorStatsMonthStartDate(now);
206
+ const recentDateList = getErrorStatsRecentDateList(now);
59
207
 
60
208
  if (!tableReady) {
61
209
  return befly.tool.Yes("获取成功", {
@@ -64,12 +212,12 @@ export default {
64
212
  count: 0
65
213
  },
66
214
  week: {
67
- startDate: getErrorStatsWeekStartDate(now),
215
+ startDate: weekStartDate,
68
216
  endDate: bucketDate,
69
217
  count: 0
70
218
  },
71
219
  month: {
72
- startDate: getErrorStatsMonthStartDate(now),
220
+ startDate: monthStartDate,
73
221
  endDate: bucketDate,
74
222
  count: 0
75
223
  },
@@ -79,9 +227,16 @@ export default {
79
227
  });
80
228
  }
81
229
 
82
- const weekStartDate = getErrorStatsWeekStartDate(now);
83
- const monthStartDate = getErrorStatsMonthStartDate(now);
84
- const recentDateList = getErrorStatsRecentDateList(now);
230
+ if (befly.redis) {
231
+ const redisResult = await getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList);
232
+
233
+ if (redisResult) {
234
+ return befly.tool.Yes("获取成功", redisResult);
235
+ }
236
+
237
+ await incrErrorStatsFallbackCount(befly, bucketDate);
238
+ }
239
+
85
240
  const todaySummary = await getErrorStatsSummary(befly, bucketDate, bucketDate);
86
241
  const weekSummary = await getErrorStatsSummary(befly, weekStartDate, bucketDate);
87
242
  const monthSummary = await getErrorStatsSummary(befly, monthStartDate, bucketDate);
@@ -0,0 +1,69 @@
1
+ import { getDateYmdNumber } from "#root/utils/datetime.js";
2
+
3
+ const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
4
+ const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
5
+
6
+ function getInfoStatsFallbackDailyCountKey(reportDate) {
7
+ return `stats:fallback:infoStats:count:day:${reportDate}`;
8
+ }
9
+
10
+ function getErrorStatsFallbackDailyCountKey(reportDate) {
11
+ return `stats:fallback:errorStats:count:day:${reportDate}`;
12
+ }
13
+
14
+ function toNumber(value) {
15
+ const num = Number(value);
16
+
17
+ if (!Number.isFinite(num)) {
18
+ return 0;
19
+ }
20
+
21
+ return num;
22
+ }
23
+
24
+ export default {
25
+ name: "重置统计回退计数",
26
+ method: "POST",
27
+ body: "none",
28
+ auth: true,
29
+ fields: {},
30
+ required: [],
31
+ handler: async (befly) => {
32
+ if (!befly.redis) {
33
+ return befly.tool.No("Redis 不可用,无法重置", {
34
+ reset: false
35
+ });
36
+ }
37
+
38
+ const reportDate = getDateYmdNumber(Date.now());
39
+ const infoDailyKey = getInfoStatsFallbackDailyCountKey(reportDate);
40
+ const errorDailyKey = getErrorStatsFallbackDailyCountKey(reportDate);
41
+ const resetKeys = [INFO_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_COUNT_KEY, infoDailyKey, errorDailyKey];
42
+ const before = {
43
+ infoStats: toNumber(await befly.redis.getString(INFO_STATS_FALLBACK_COUNT_KEY)),
44
+ errorStats: toNumber(await befly.redis.getString(ERROR_STATS_FALLBACK_COUNT_KEY)),
45
+ today: {
46
+ infoStats: toNumber(await befly.redis.getString(infoDailyKey)),
47
+ errorStats: toNumber(await befly.redis.getString(errorDailyKey))
48
+ }
49
+ };
50
+
51
+ const removedCount = await befly.redis.delBatch(resetKeys);
52
+
53
+ return befly.tool.Yes("重置成功", {
54
+ reset: true,
55
+ reportDate: reportDate,
56
+ removedCount: toNumber(removedCount),
57
+ keys: resetKeys,
58
+ before: before,
59
+ after: {
60
+ infoStats: 0,
61
+ errorStats: 0,
62
+ today: {
63
+ infoStats: 0,
64
+ errorStats: 0
65
+ }
66
+ }
67
+ });
68
+ }
69
+ };
@@ -1,8 +1,27 @@
1
1
  import { UAParser } from "ua-parser-js";
2
2
 
3
- import { getDateYmdNumber } from "#root/utils/datetime.js";
3
+ import { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
4
4
  import { isValidPositiveInt } from "#root/utils/is.js";
5
5
 
6
+ const INFO_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
7
+ const INFO_STATS_FIELDS = [
8
+ { key: "sources", valueKey: "source" },
9
+ { key: "productNames", valueKey: "productName" },
10
+ { key: "productCodes", valueKey: "productCode" },
11
+ { key: "productVersions", valueKey: "productVersion" },
12
+ { key: "pagePaths", valueKey: "pagePath" },
13
+ { key: "pageNames", valueKey: "pageName" },
14
+ { key: "deviceTypes", valueKey: "deviceType" },
15
+ { key: "browsers", valueKey: "browserName" },
16
+ { key: "browserVersions", valueKey: "browserVersion" },
17
+ { key: "osList", valueKey: "osName" },
18
+ { key: "osVersions", valueKey: "osVersion" },
19
+ { key: "deviceVendors", valueKey: "deviceVendor" },
20
+ { key: "deviceModels", valueKey: "deviceModel" },
21
+ { key: "engines", valueKey: "engineName" },
22
+ { key: "cpuArchitectures", valueKey: "cpuArchitecture" }
23
+ ];
24
+
6
25
  function getInfoStatsMember(ctx) {
7
26
  if (isValidPositiveInt(ctx.userId)) {
8
27
  return `user:${ctx.userId}`;
@@ -11,6 +30,67 @@ function getInfoStatsMember(ctx) {
11
30
  return `ip:${ctx.ip || "unknown"}`;
12
31
  }
13
32
 
33
+ function getInfoStatsWeekStartDate(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 getInfoStatsMonthStartDate(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 getInfoStatsNamesKey(periodType, periodValue, fieldKey) {
48
+ return `info:${periodType}:${periodValue}:${fieldKey}:names`;
49
+ }
50
+
51
+ function getInfoStatsCountKey(periodType, periodValue, fieldKey, value) {
52
+ return `info:${periodType}:${periodValue}:${fieldKey}:count:${encodeURIComponent(String(value || ""))}`;
53
+ }
54
+
55
+ function getInfoStatsReportTimeKey(periodType, periodValue) {
56
+ return `info:${periodType}:${periodValue}:reportTime`;
57
+ }
58
+
59
+ async function updateInfoStatsPeriodRedis(befly, periodType, periodValue, statData, now) {
60
+ const reportTimeKey = getInfoStatsReportTimeKey(periodType, periodValue);
61
+ await befly.redis.setString(reportTimeKey, String(now), INFO_STATS_REDIS_TTL_SECONDS);
62
+ await befly.redis.expire(reportTimeKey, INFO_STATS_REDIS_TTL_SECONDS);
63
+
64
+ for (const item of INFO_STATS_FIELDS) {
65
+ const rawValue = String(statData[item.valueKey] || "").trim();
66
+
67
+ if (!rawValue) {
68
+ continue;
69
+ }
70
+
71
+ const namesKey = getInfoStatsNamesKey(periodType, periodValue, item.key);
72
+ const countKey = getInfoStatsCountKey(periodType, periodValue, item.key, rawValue);
73
+
74
+ await befly.redis.sadd(namesKey, [rawValue]);
75
+ await befly.redis.incr(countKey);
76
+ await befly.redis.expire(namesKey, INFO_STATS_REDIS_TTL_SECONDS);
77
+ await befly.redis.expire(countKey, INFO_STATS_REDIS_TTL_SECONDS);
78
+ }
79
+ }
80
+
81
+ async function updateInfoStatsRedis(befly, now, reportDate, statData) {
82
+ if (!befly.redis) {
83
+ return;
84
+ }
85
+
86
+ const weekStartDate = getInfoStatsWeekStartDate(now);
87
+ const monthStartDate = getInfoStatsMonthStartDate(now);
88
+
89
+ await updateInfoStatsPeriodRedis(befly, "day", reportDate, statData, now);
90
+ await updateInfoStatsPeriodRedis(befly, "week", weekStartDate, statData, now);
91
+ await updateInfoStatsPeriodRedis(befly, "month", monthStartDate, statData, now);
92
+ }
93
+
14
94
  export default {
15
95
  name: "上报访问信息统计",
16
96
  method: "POST",
@@ -68,32 +148,52 @@ export default {
68
148
  });
69
149
  }
70
150
 
151
+ const statData = {
152
+ source: body.source || "",
153
+ productName: body.productName || "",
154
+ productCode: body.productCode || "",
155
+ productVersion: body.productVersion || "",
156
+ pagePath: body.pagePath || "",
157
+ pageName: body.pageName || "",
158
+ deviceType: uaResult.device.type || "desktop",
159
+ browserName: uaResult.browser.name || "",
160
+ browserVersion: uaResult.browser.version || "",
161
+ osName: uaResult.os.name || "",
162
+ osVersion: uaResult.os.version || "",
163
+ deviceVendor: uaResult.device.vendor || "",
164
+ deviceModel: uaResult.device.model || "",
165
+ engineName: uaResult.engine.name || "",
166
+ cpuArchitecture: uaResult.cpu.architecture || ""
167
+ };
168
+
71
169
  await befly.mysql.insData({
72
170
  table: "beflyInfoReport",
73
171
  data: {
74
172
  reportTime: now,
75
173
  reportDate: reportDate,
76
174
  memberKey: member,
77
- source: body.source || "",
78
- productName: body.productName || "",
79
- productCode: body.productCode || "",
80
- productVersion: body.productVersion || "",
81
- pagePath: body.pagePath || "",
82
- pageName: body.pageName || "",
175
+ source: statData.source,
176
+ productName: statData.productName,
177
+ productCode: statData.productCode,
178
+ productVersion: statData.productVersion,
179
+ pagePath: statData.pagePath,
180
+ pageName: statData.pageName,
83
181
  detail: body.detail || "",
84
182
  userAgent: userAgent,
85
- deviceType: uaResult.device.type || "desktop",
86
- browserName: uaResult.browser.name || "",
87
- browserVersion: uaResult.browser.version || "",
88
- osName: uaResult.os.name || "",
89
- osVersion: uaResult.os.version || "",
90
- deviceVendor: uaResult.device.vendor || "",
91
- deviceModel: uaResult.device.model || "",
92
- engineName: uaResult.engine.name || "",
93
- cpuArchitecture: uaResult.cpu.architecture || ""
183
+ deviceType: statData.deviceType,
184
+ browserName: statData.browserName,
185
+ browserVersion: statData.browserVersion,
186
+ osName: statData.osName,
187
+ osVersion: statData.osVersion,
188
+ deviceVendor: statData.deviceVendor,
189
+ deviceModel: statData.deviceModel,
190
+ engineName: statData.engineName,
191
+ cpuArchitecture: statData.cpuArchitecture
94
192
  }
95
193
  });
96
194
 
195
+ await updateInfoStatsRedis(befly, now, reportDate, statData);
196
+
97
197
  return befly.tool.Yes("上报成功", {
98
198
  reportTime: now,
99
199
  reportDate: reportDate,