befly 3.24.18 → 3.24.20

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 (71) hide show
  1. package/apis/_apis.js +20 -0
  2. package/apis/admin/delete.js +3 -1
  3. package/apis/admin/detail.js +1 -3
  4. package/apis/admin/select.js +9 -7
  5. package/apis/admin/update.js +2 -2
  6. package/apis/api/all.js +6 -11
  7. package/apis/api/select.js +18 -19
  8. package/apis/dashboard/environmentInfo.js +6 -1
  9. package/apis/dashboard/serviceStatus.js +78 -60
  10. package/apis/dict/_dict.js +24 -0
  11. package/apis/dict/all.js +6 -4
  12. package/apis/dict/detail.js +9 -5
  13. package/apis/dict/insert.js +4 -11
  14. package/apis/dict/items.js +5 -6
  15. package/apis/dict/select.js +13 -9
  16. package/apis/dict/update.js +9 -13
  17. package/apis/dictType/select.js +4 -4
  18. package/apis/email/config.js +9 -11
  19. package/apis/email/logList.js +14 -4
  20. package/apis/email/send.js +10 -3
  21. package/apis/email/verify.js +9 -7
  22. package/apis/loginLog/select.js +11 -4
  23. package/apis/menu/all.js +13 -19
  24. package/apis/menu/select.js +19 -14
  25. package/apis/operateLog/select.js +13 -4
  26. package/apis/role/_role.js +21 -0
  27. package/apis/role/all.js +1 -3
  28. package/apis/role/apiSave.js +8 -15
  29. package/apis/role/apis.js +4 -10
  30. package/apis/role/delete.js +28 -36
  31. package/apis/role/detail.js +4 -10
  32. package/apis/role/insert.js +12 -10
  33. package/apis/role/menuSave.js +9 -15
  34. package/apis/role/menus.js +4 -10
  35. package/apis/role/save.js +19 -23
  36. package/apis/role/select.js +12 -9
  37. package/apis/role/update.js +14 -15
  38. package/apis/source/imageList.js +3 -3
  39. package/apis/sysConfig/get.js +11 -23
  40. package/apis/sysConfig/insert.js +22 -27
  41. package/apis/sysConfig/select.js +11 -5
  42. package/apis/sysConfig/update.js +33 -38
  43. package/apis/tongJi/_tongJi.js +41 -0
  44. package/apis/tongJi/cacheHealth.js +192 -0
  45. package/apis/tongJi/errorList.js +26 -27
  46. package/apis/tongJi/errorReport.js +72 -18
  47. package/apis/tongJi/errorStats.js +154 -30
  48. package/apis/tongJi/fallbackReset.js +61 -0
  49. package/apis/tongJi/infoReport.js +112 -24
  50. package/apis/tongJi/infoStats.js +164 -84
  51. package/apis/tongJi/onlineReport.js +58 -73
  52. package/apis/tongJi/onlineStats.js +140 -151
  53. package/checks/config.js +44 -1
  54. package/configs/beflyConfig.json +10 -1
  55. package/hooks/permission.js +6 -2
  56. package/hooks/rateLimit.js +245 -0
  57. package/index.js +25 -0
  58. package/lib/cacheHelper.js +105 -60
  59. package/lib/dbHelper.js +1 -1
  60. package/lib/dbParse.js +61 -99
  61. package/lib/dbUtil.js +101 -21
  62. package/lib/redisHelper.js +93 -0
  63. package/lib/sqlBuilder.js +6 -0
  64. package/package.json +2 -2
  65. package/plugins/email.js +4 -3
  66. package/utils/email.js +3 -0
  67. package/apis/admin/cacheRefresh.js +0 -122
  68. package/apis/dashboard/configStatus.js +0 -39
  69. package/apis/dashboard/performanceMetrics.js +0 -20
  70. package/apis/dashboard/permissionStats.js +0 -27
  71. package/apis/dashboard/systemInfo.js +0 -19
@@ -1,8 +1,12 @@
1
1
  import { UAParser } from "ua-parser-js";
2
2
 
3
+ import errorReportTable from "#root/tables/errorReport.json";
3
4
  import { getDateYmdNumber, getTimeBucketStart } from "#root/utils/datetime.js";
4
5
 
6
+ import { expireTongJiRedisKeys, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
7
+
5
8
  const VISIT_STATS_BUCKET_MS = 30 * 60 * 1000;
9
+ const ERROR_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
6
10
 
7
11
  function getErrorReportUaData(ctx) {
8
12
  let userAgent = "";
@@ -29,21 +33,69 @@ function getErrorReportUaData(ctx) {
29
33
  };
30
34
  }
31
35
 
36
+ function getErrorStatsPeriodCountKey(periodType, periodValue) {
37
+ return `error:${periodType}:${periodValue}:count`;
38
+ }
39
+
40
+ function getErrorStatsDayBucketsKey(bucketDate) {
41
+ return `error:day:${bucketDate}:buckets`;
42
+ }
43
+
44
+ function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
45
+ return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
46
+ }
47
+
48
+ function getErrorStatsDayTypesKey(bucketDate) {
49
+ return `error:day:${bucketDate}:types`;
50
+ }
51
+
52
+ function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
53
+ return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
54
+ }
55
+
56
+ async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
57
+ if (!befly.redis) {
58
+ return;
59
+ }
60
+
61
+ const weekStartDate = getTongJiWeekStartDate(now);
62
+ const monthStartDate = getTongJiMonthStartDate(now);
63
+ const dayCountKey = getErrorStatsPeriodCountKey("day", bucketDate);
64
+ const weekCountKey = getErrorStatsPeriodCountKey("week", weekStartDate);
65
+ const monthCountKey = getErrorStatsPeriodCountKey("month", monthStartDate);
66
+ const dayBucketsKey = getErrorStatsDayBucketsKey(bucketDate);
67
+ const dayBucketCountKey = getErrorStatsDayBucketCountKey(bucketDate, bucketTime);
68
+ const dayTypesKey = getErrorStatsDayTypesKey(bucketDate);
69
+ const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate, errorType);
70
+
71
+ await befly.redis.incr(dayCountKey);
72
+ await befly.redis.incr(weekCountKey);
73
+ await befly.redis.incr(monthCountKey);
74
+
75
+ await befly.redis.sadd(dayBucketsKey, [String(bucketTime)]);
76
+ await befly.redis.incr(dayBucketCountKey);
77
+
78
+ await befly.redis.sadd(dayTypesKey, [String(errorType || "unknown")]);
79
+ await befly.redis.incr(dayTypeCountKey);
80
+
81
+ await expireTongJiRedisKeys(befly, [dayCountKey, weekCountKey, monthCountKey, dayBucketsKey, dayBucketCountKey, dayTypesKey, dayTypeCountKey], ERROR_STATS_REDIS_TTL_SECONDS);
82
+ }
83
+
32
84
  export default {
33
85
  name: "上报错误报告",
34
86
  method: "POST",
35
87
  body: "none",
36
88
  auth: false,
37
89
  fields: {
38
- pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
39
- pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
40
- source: { name: "来源", input: "string", min: 0, max: 50 },
41
- productName: { name: "产品名称", input: "string", min: 0, max: 100 },
42
- productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
43
- productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
44
- errorType: { name: "错误类型", input: "string", min: 0, max: 50 },
45
- message: { name: "错误信息", input: "string", min: 0, max: 500 },
46
- detail: { name: "错误详情", input: "string", min: 0, max: 5000 }
90
+ pagePath: errorReportTable.pagePath,
91
+ pageName: errorReportTable.pageName,
92
+ source: errorReportTable.source,
93
+ productName: errorReportTable.productName,
94
+ productCode: errorReportTable.productCode,
95
+ productVersion: errorReportTable.productVersion,
96
+ errorType: errorReportTable.errorType,
97
+ message: errorReportTable.message,
98
+ detail: errorReportTable.detail
47
99
  },
48
100
  required: [],
49
101
  handler: async (befly, ctx) => {
@@ -60,15 +112,15 @@ export default {
60
112
  bucketTime: bucketTime,
61
113
  bucketDate: bucketDate,
62
114
  hitCount: 1,
63
- source: ctx.body?.source,
64
- productName: ctx.body?.productName || "",
65
- productCode: ctx.body?.productCode || "",
66
- productVersion: ctx.body?.productVersion || "",
67
- pagePath: ctx.body?.pagePath || "",
68
- pageName: ctx.body?.pageName || "",
69
- errorType: ctx.body?.errorType || "",
70
- message: ctx.body?.message || "",
71
- detail: ctx.body?.detail || "",
115
+ source: ctx.body.source || "",
116
+ productName: ctx.body.productName || "",
117
+ productCode: ctx.body.productCode || "",
118
+ productVersion: ctx.body.productVersion || "",
119
+ pagePath: ctx.body.pagePath || "",
120
+ pageName: ctx.body.pageName || "",
121
+ errorType: ctx.body.errorType || "",
122
+ message: ctx.body.message || "",
123
+ detail: ctx.body.detail || "",
72
124
  userAgent: uaData.userAgent,
73
125
  browserName: uaData.browserName,
74
126
  browserVersion: uaData.browserVersion,
@@ -82,6 +134,8 @@ export default {
82
134
  }
83
135
  });
84
136
 
137
+ await updateErrorStatsRedis(befly, now, bucketDate, bucketTime, ctx.body.errorType || "unknown");
138
+
85
139
  return befly.tool.Yes("上报成功", {
86
140
  reportTime: now,
87
141
  bucketTime: bucketTime,
@@ -1,38 +1,152 @@
1
1
  import { getDateYmdNumber } from "#root/utils/datetime.js";
2
2
 
3
+ import { getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
4
+
3
5
  const ERROR_STATS_DAY_LIMIT = 30;
6
+ const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
7
+ const ERROR_STATS_FALLBACK_DAILY_COUNT_KEY_PREFIX = "stats:fallback:errorStats:count:day:";
8
+ const ERROR_STATS_FALLBACK_TTL_SECONDS = 7 * 24 * 60 * 60;
9
+
10
+ function getErrorStatsFallbackDailyCountKey(bucketDate) {
11
+ return `${ERROR_STATS_FALLBACK_DAILY_COUNT_KEY_PREFIX}${bucketDate}`;
12
+ }
13
+
14
+ function getErrorStatsPeriodCountKey(periodType, periodValue) {
15
+ return `error:${periodType}:${periodValue}:count`;
16
+ }
17
+
18
+ function getErrorStatsDayBucketsKey(bucketDate) {
19
+ return `error:day:${bucketDate}:buckets`;
20
+ }
21
+
22
+ function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
23
+ return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
24
+ }
25
+
26
+ function getErrorStatsDayTypesKey(bucketDate) {
27
+ return `error:day:${bucketDate}:types`;
28
+ }
29
+
30
+ function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
31
+ return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
32
+ }
33
+
34
+ async function getErrorStatsRedisSummary(befly, periodType, periodValue) {
35
+ return {
36
+ count: getTongJiNumber(await befly.redis.getString(getErrorStatsPeriodCountKey(periodType, periodValue)))
37
+ };
38
+ }
39
+
40
+ async function getErrorStatsRedisTrend(befly, bucketDate) {
41
+ const bucketSet = await befly.redis.smembers(getErrorStatsDayBucketsKey(bucketDate));
42
+ const trend = [];
43
+
44
+ for (const bucketTime of bucketSet) {
45
+ const itemBucketTime = getTongJiNumber(bucketTime);
46
+
47
+ trend.push({
48
+ bucketTime: itemBucketTime,
49
+ bucketDate: bucketDate,
50
+ count: getTongJiNumber(await befly.redis.getString(getErrorStatsDayBucketCountKey(bucketDate, itemBucketTime)))
51
+ });
52
+ }
53
+
54
+ trend.sort((a, b) => a.bucketTime - b.bucketTime);
55
+ return trend;
56
+ }
57
+
58
+ async function getErrorStatsRedisDays(befly, recentDateList) {
59
+ const days = [];
4
60
 
5
- function toNumber(value) {
6
- const num = Number(value);
7
- if (!Number.isFinite(num)) {
8
- return 0;
61
+ for (const item of recentDateList) {
62
+ days.push({
63
+ bucketDate: item,
64
+ count: getTongJiNumber(await befly.redis.getString(getErrorStatsPeriodCountKey("day", item)))
65
+ });
9
66
  }
10
67
 
11
- return num;
68
+ return days;
12
69
  }
13
70
 
14
- function getErrorStatsWeekStartDate(timestamp = Date.now()) {
15
- const date = Reflect.construct(Date, [timestamp]);
16
- const day = date.getDay();
17
- const offset = day === 0 ? -6 : 1 - day;
71
+ async function getErrorStatsRedisTopTypes(befly, bucketDate) {
72
+ const typeSet = await befly.redis.smembers(getErrorStatsDayTypesKey(bucketDate));
73
+ const topTypes = [];
74
+
75
+ for (const errorType of typeSet) {
76
+ const name = String(errorType || "unknown");
77
+ const count = getTongJiNumber(await befly.redis.getString(getErrorStatsDayTypeCountKey(bucketDate, name)));
78
+
79
+ if (count <= 0) {
80
+ continue;
81
+ }
82
+
83
+ topTypes.push({
84
+ errorType: name,
85
+ count: count
86
+ });
87
+ }
88
+
89
+ topTypes.sort((a, b) => {
90
+ if (b.count !== a.count) {
91
+ return b.count - a.count;
92
+ }
93
+
94
+ return String(a.errorType).localeCompare(String(b.errorType), "zh-CN");
95
+ });
18
96
 
19
- return getDateYmdNumber(timestamp + offset * 24 * 60 * 60 * 1000);
97
+ return topTypes.slice(0, 5);
20
98
  }
21
99
 
22
- function getErrorStatsMonthStartDate(timestamp = Date.now()) {
23
- const date = Reflect.construct(Date, [timestamp]);
100
+ async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList) {
101
+ const todaySummary = await getErrorStatsRedisSummary(befly, "day", bucketDate);
102
+ const weekSummary = await getErrorStatsRedisSummary(befly, "week", weekStartDate);
103
+ const monthSummary = await getErrorStatsRedisSummary(befly, "month", monthStartDate);
104
+ const trend = await getErrorStatsRedisTrend(befly, bucketDate);
105
+ const days = await getErrorStatsRedisDays(befly, recentDateList);
106
+ const topTypes = await getErrorStatsRedisTopTypes(befly, bucketDate);
24
107
 
25
- return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
108
+ if (todaySummary.count <= 0 && weekSummary.count <= 0 && monthSummary.count <= 0 && trend.length === 0 && topTypes.length === 0) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ today: {
114
+ bucketDate: bucketDate,
115
+ count: todaySummary.count
116
+ },
117
+ week: {
118
+ startDate: weekStartDate,
119
+ endDate: bucketDate,
120
+ count: weekSummary.count
121
+ },
122
+ month: {
123
+ startDate: monthStartDate,
124
+ endDate: bucketDate,
125
+ count: monthSummary.count
126
+ },
127
+ trend: trend,
128
+ days: days,
129
+ topTypes: topTypes
130
+ };
26
131
  }
27
132
 
28
- function getErrorStatsRecentDateList(now = Date.now(), limit = ERROR_STATS_DAY_LIMIT) {
29
- const list = [];
133
+ async function incrErrorStatsFallbackCount(befly, bucketDate) {
134
+ if (!befly.redis) {
135
+ return;
136
+ }
137
+
138
+ const dailyKey = getErrorStatsFallbackDailyCountKey(bucketDate);
30
139
 
31
- for (let i = limit - 1; i >= 0; i--) {
32
- list.push(getDateYmdNumber(now - i * 24 * 60 * 60 * 1000));
140
+ if (typeof befly.redis.incrWithExpire === "function") {
141
+ await befly.redis.incrWithExpire(ERROR_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_TTL_SECONDS);
142
+ await befly.redis.incrWithExpire(dailyKey, ERROR_STATS_FALLBACK_TTL_SECONDS);
143
+ return;
33
144
  }
34
145
 
35
- return list;
146
+ await befly.redis.incr(ERROR_STATS_FALLBACK_COUNT_KEY);
147
+ await befly.redis.expire(ERROR_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_TTL_SECONDS);
148
+ await befly.redis.incr(dailyKey);
149
+ await befly.redis.expire(dailyKey, ERROR_STATS_FALLBACK_TTL_SECONDS);
36
150
  }
37
151
 
38
152
  async function getErrorStatsSummary(befly, startDate, endDate) {
@@ -40,7 +154,7 @@ async function getErrorStatsSummary(befly, startDate, endDate) {
40
154
  const detail = result.data?.[0] || {};
41
155
 
42
156
  return {
43
- count: toNumber(detail.count)
157
+ count: getTongJiNumber(detail.count)
44
158
  };
45
159
  }
46
160
 
@@ -56,6 +170,9 @@ export default {
56
170
  const tableReady = tableExistsResult.data === true;
57
171
  const now = Date.now();
58
172
  const bucketDate = getDateYmdNumber(now);
173
+ const weekStartDate = getTongJiWeekStartDate(now);
174
+ const monthStartDate = getTongJiMonthStartDate(now);
175
+ const recentDateList = getTongJiRecentDateList(now, ERROR_STATS_DAY_LIMIT);
59
176
 
60
177
  if (!tableReady) {
61
178
  return befly.tool.Yes("获取成功", {
@@ -64,12 +181,12 @@ export default {
64
181
  count: 0
65
182
  },
66
183
  week: {
67
- startDate: getErrorStatsWeekStartDate(now),
184
+ startDate: weekStartDate,
68
185
  endDate: bucketDate,
69
186
  count: 0
70
187
  },
71
188
  month: {
72
- startDate: getErrorStatsMonthStartDate(now),
189
+ startDate: monthStartDate,
73
190
  endDate: bucketDate,
74
191
  count: 0
75
192
  },
@@ -79,9 +196,16 @@ export default {
79
196
  });
80
197
  }
81
198
 
82
- const weekStartDate = getErrorStatsWeekStartDate(now);
83
- const monthStartDate = getErrorStatsMonthStartDate(now);
84
- const recentDateList = getErrorStatsRecentDateList(now);
199
+ if (befly.redis) {
200
+ const redisResult = await getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList);
201
+
202
+ if (redisResult) {
203
+ return befly.tool.Yes("获取成功", redisResult);
204
+ }
205
+
206
+ await incrErrorStatsFallbackCount(befly, bucketDate);
207
+ }
208
+
85
209
  const todaySummary = await getErrorStatsSummary(befly, bucketDate, bucketDate);
86
210
  const weekSummary = await getErrorStatsSummary(befly, weekStartDate, bucketDate);
87
211
  const monthSummary = await getErrorStatsSummary(befly, monthStartDate, bucketDate);
@@ -92,9 +216,9 @@ export default {
92
216
  const trend = [];
93
217
  for (const item of trendRes.data || []) {
94
218
  trend.push({
95
- bucketTime: toNumber(item.bucketTime),
96
- bucketDate: toNumber(item.bucketDate),
97
- count: toNumber(item.count)
219
+ bucketTime: getTongJiNumber(item.bucketTime),
220
+ bucketDate: getTongJiNumber(item.bucketDate),
221
+ count: getTongJiNumber(item.count)
98
222
  });
99
223
  }
100
224
 
@@ -108,7 +232,7 @@ export default {
108
232
  }
109
233
 
110
234
  for (const item of daysRes.data || []) {
111
- const itemBucketDate = toNumber(item.bucketDate);
235
+ const itemBucketDate = getTongJiNumber(item.bucketDate);
112
236
 
113
237
  if (!daysMap.has(itemBucketDate)) {
114
238
  continue;
@@ -116,7 +240,7 @@ export default {
116
240
 
117
241
  daysMap.set(itemBucketDate, {
118
242
  bucketDate: itemBucketDate,
119
- count: toNumber(item.count)
243
+ count: getTongJiNumber(item.count)
120
244
  });
121
245
  }
122
246
 
@@ -126,7 +250,7 @@ export default {
126
250
  for (const item of topTypesRes.data || []) {
127
251
  topTypes.push({
128
252
  errorType: String(item.errorType || "unknown"),
129
- count: toNumber(item.count)
253
+ count: getTongJiNumber(item.count)
130
254
  });
131
255
  }
132
256
 
@@ -0,0 +1,61 @@
1
+ import { getDateYmdNumber } from "#root/utils/datetime.js";
2
+
3
+ import { getTongJiNumber } from "./_tongJi.js";
4
+
5
+ const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
6
+ const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
7
+
8
+ function getInfoStatsFallbackDailyCountKey(reportDate) {
9
+ return `stats:fallback:infoStats:count:day:${reportDate}`;
10
+ }
11
+
12
+ function getErrorStatsFallbackDailyCountKey(reportDate) {
13
+ return `stats:fallback:errorStats:count:day:${reportDate}`;
14
+ }
15
+
16
+ export default {
17
+ name: "重置统计回退计数",
18
+ method: "POST",
19
+ body: "none",
20
+ auth: true,
21
+ fields: {},
22
+ required: [],
23
+ handler: async (befly) => {
24
+ if (!befly.redis) {
25
+ return befly.tool.No("Redis 不可用,无法重置", {
26
+ reset: false
27
+ });
28
+ }
29
+
30
+ const reportDate = getDateYmdNumber(Date.now());
31
+ const infoDailyKey = getInfoStatsFallbackDailyCountKey(reportDate);
32
+ const errorDailyKey = getErrorStatsFallbackDailyCountKey(reportDate);
33
+ const resetKeys = [INFO_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_COUNT_KEY, infoDailyKey, errorDailyKey];
34
+ const before = {
35
+ infoStats: getTongJiNumber(await befly.redis.getString(INFO_STATS_FALLBACK_COUNT_KEY)),
36
+ errorStats: getTongJiNumber(await befly.redis.getString(ERROR_STATS_FALLBACK_COUNT_KEY)),
37
+ today: {
38
+ infoStats: getTongJiNumber(await befly.redis.getString(infoDailyKey)),
39
+ errorStats: getTongJiNumber(await befly.redis.getString(errorDailyKey))
40
+ }
41
+ };
42
+
43
+ const removedCount = await befly.redis.delBatch(resetKeys);
44
+
45
+ return befly.tool.Yes("重置成功", {
46
+ reset: true,
47
+ reportDate: reportDate,
48
+ removedCount: getTongJiNumber(removedCount),
49
+ keys: resetKeys,
50
+ before: before,
51
+ after: {
52
+ infoStats: 0,
53
+ errorStats: 0,
54
+ today: {
55
+ infoStats: 0,
56
+ errorStats: 0
57
+ }
58
+ }
59
+ });
60
+ }
61
+ };
@@ -1,8 +1,30 @@
1
1
  import { UAParser } from "ua-parser-js";
2
2
 
3
+ import infoReportTable from "#root/tables/infoReport.json";
3
4
  import { getDateYmdNumber } from "#root/utils/datetime.js";
4
5
  import { isValidPositiveInt } from "#root/utils/is.js";
5
6
 
7
+ import { getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
8
+
9
+ const INFO_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
10
+ const INFO_STATS_FIELDS = [
11
+ { key: "sources", valueKey: "source" },
12
+ { key: "productNames", valueKey: "productName" },
13
+ { key: "productCodes", valueKey: "productCode" },
14
+ { key: "productVersions", valueKey: "productVersion" },
15
+ { key: "pagePaths", valueKey: "pagePath" },
16
+ { key: "pageNames", valueKey: "pageName" },
17
+ { key: "deviceTypes", valueKey: "deviceType" },
18
+ { key: "browsers", valueKey: "browserName" },
19
+ { key: "browserVersions", valueKey: "browserVersion" },
20
+ { key: "osList", valueKey: "osName" },
21
+ { key: "osVersions", valueKey: "osVersion" },
22
+ { key: "deviceVendors", valueKey: "deviceVendor" },
23
+ { key: "deviceModels", valueKey: "deviceModel" },
24
+ { key: "engines", valueKey: "engineName" },
25
+ { key: "cpuArchitectures", valueKey: "cpuArchitecture" }
26
+ ];
27
+
6
28
  function getInfoStatsMember(ctx) {
7
29
  if (isValidPositiveInt(ctx.userId)) {
8
30
  return `user:${ctx.userId}`;
@@ -11,19 +33,66 @@ function getInfoStatsMember(ctx) {
11
33
  return `ip:${ctx.ip || "unknown"}`;
12
34
  }
13
35
 
36
+ function getInfoStatsNamesKey(periodType, periodValue, fieldKey) {
37
+ return `info:${periodType}:${periodValue}:${fieldKey}:names`;
38
+ }
39
+
40
+ function getInfoStatsCountKey(periodType, periodValue, fieldKey, value) {
41
+ return `info:${periodType}:${periodValue}:${fieldKey}:count:${encodeURIComponent(String(value || ""))}`;
42
+ }
43
+
44
+ function getInfoStatsReportTimeKey(periodType, periodValue) {
45
+ return `info:${periodType}:${periodValue}:reportTime`;
46
+ }
47
+
48
+ async function updateInfoStatsPeriodRedis(befly, periodType, periodValue, statData, now) {
49
+ const reportTimeKey = getInfoStatsReportTimeKey(periodType, periodValue);
50
+ await befly.redis.setString(reportTimeKey, String(now), INFO_STATS_REDIS_TTL_SECONDS);
51
+ await befly.redis.expire(reportTimeKey, INFO_STATS_REDIS_TTL_SECONDS);
52
+
53
+ for (const item of INFO_STATS_FIELDS) {
54
+ const rawValue = String(statData[item.valueKey] || "").trim();
55
+
56
+ if (!rawValue) {
57
+ continue;
58
+ }
59
+
60
+ const namesKey = getInfoStatsNamesKey(periodType, periodValue, item.key);
61
+ const countKey = getInfoStatsCountKey(periodType, periodValue, item.key, rawValue);
62
+
63
+ await befly.redis.sadd(namesKey, [rawValue]);
64
+ await befly.redis.incr(countKey);
65
+ await befly.redis.expire(namesKey, INFO_STATS_REDIS_TTL_SECONDS);
66
+ await befly.redis.expire(countKey, INFO_STATS_REDIS_TTL_SECONDS);
67
+ }
68
+ }
69
+
70
+ async function updateInfoStatsRedis(befly, now, reportDate, statData) {
71
+ if (!befly.redis) {
72
+ return;
73
+ }
74
+
75
+ const weekStartDate = getTongJiWeekStartDate(now);
76
+ const monthStartDate = getTongJiMonthStartDate(now);
77
+
78
+ await updateInfoStatsPeriodRedis(befly, "day", reportDate, statData, now);
79
+ await updateInfoStatsPeriodRedis(befly, "week", weekStartDate, statData, now);
80
+ await updateInfoStatsPeriodRedis(befly, "month", monthStartDate, statData, now);
81
+ }
82
+
14
83
  export default {
15
84
  name: "上报访问信息统计",
16
85
  method: "POST",
17
86
  body: "none",
18
87
  auth: false,
19
88
  fields: {
20
- pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
21
- pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
22
- source: { name: "来源", input: "string", min: 0, max: 50 },
23
- productName: { name: "产品名称", input: "string", min: 0, max: 100 },
24
- productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
25
- productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
26
- detail: { name: "扩展详情", input: "string", min: 0, max: 5000 }
89
+ pagePath: infoReportTable.pagePath,
90
+ pageName: infoReportTable.pageName,
91
+ source: infoReportTable.source,
92
+ productName: infoReportTable.productName,
93
+ productCode: infoReportTable.productCode,
94
+ productVersion: infoReportTable.productVersion,
95
+ detail: infoReportTable.detail
27
96
  },
28
97
  required: [],
29
98
  handler: async (befly, ctx) => {
@@ -31,7 +100,6 @@ export default {
31
100
  const reportDate = getDateYmdNumber(now);
32
101
  const member = getInfoStatsMember(ctx);
33
102
  const tableReady = (await befly.mysql.tableExists("beflyInfoReport")).data === true;
34
- const body = ctx.body || {};
35
103
  let userAgent = "";
36
104
 
37
105
  if (!tableReady) {
@@ -68,32 +136,52 @@ export default {
68
136
  });
69
137
  }
70
138
 
139
+ const statData = {
140
+ source: ctx.body.source || "",
141
+ productName: ctx.body.productName || "",
142
+ productCode: ctx.body.productCode || "",
143
+ productVersion: ctx.body.productVersion || "",
144
+ pagePath: ctx.body.pagePath || "",
145
+ pageName: ctx.body.pageName || "",
146
+ deviceType: uaResult.device.type || "desktop",
147
+ browserName: uaResult.browser.name || "",
148
+ browserVersion: uaResult.browser.version || "",
149
+ osName: uaResult.os.name || "",
150
+ osVersion: uaResult.os.version || "",
151
+ deviceVendor: uaResult.device.vendor || "",
152
+ deviceModel: uaResult.device.model || "",
153
+ engineName: uaResult.engine.name || "",
154
+ cpuArchitecture: uaResult.cpu.architecture || ""
155
+ };
156
+
71
157
  await befly.mysql.insData({
72
158
  table: "beflyInfoReport",
73
159
  data: {
74
160
  reportTime: now,
75
161
  reportDate: reportDate,
76
162
  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 || "",
83
- detail: body.detail || "",
163
+ source: statData.source,
164
+ productName: statData.productName,
165
+ productCode: statData.productCode,
166
+ productVersion: statData.productVersion,
167
+ pagePath: statData.pagePath,
168
+ pageName: statData.pageName,
169
+ detail: ctx.body.detail || "",
84
170
  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 || ""
171
+ deviceType: statData.deviceType,
172
+ browserName: statData.browserName,
173
+ browserVersion: statData.browserVersion,
174
+ osName: statData.osName,
175
+ osVersion: statData.osVersion,
176
+ deviceVendor: statData.deviceVendor,
177
+ deviceModel: statData.deviceModel,
178
+ engineName: statData.engineName,
179
+ cpuArchitecture: statData.cpuArchitecture
94
180
  }
95
181
  });
96
182
 
183
+ await updateInfoStatsRedis(befly, now, reportDate, statData);
184
+
97
185
  return befly.tool.Yes("上报成功", {
98
186
  reportTime: now,
99
187
  reportDate: reportDate,