befly 3.35.0 → 3.37.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.
- package/apis/dashboard/serviceStatus.js +1 -33
- package/apis/tongJi/_tongJi.js +48 -0
- package/apis/tongJi/errorReport.js +1 -21
- package/apis/tongJi/errorStats.js +65 -51
- package/apis/tongJi/infoReport.js +47 -148
- package/apis/tongJi/infoStats.js +151 -186
- package/apis/tongJi/onlineReport.js +24 -59
- package/apis/tongJi/onlineStats.js +34 -191
- package/apis/upload/file.js +24 -6
- package/checks/api.js +12 -1
- package/checks/config.js +26 -6
- package/checks/menu.js +1 -1
- package/configs/beflyConfig.json +11 -6
- package/index.js +1 -1
- package/lib/dbHelper.js +52 -0
- package/package.json +1 -1
- package/paths.js +1 -1
- package/router/api.js +17 -2
- package/router/static.js +36 -4
- package/sql/befly.sql +0 -32
- package/tables/errorReport.json +2 -1
- package/utils/cors.js +10 -19
- package/apis/tongJi/cacheHealth.js +0 -192
- package/apis/tongJi/fallbackReset.js +0 -61
- package/tables/infoReport.json +0 -123
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import cacheHealthApi from "#befly/apis/tongJi/cacheHealth.js";
|
|
2
|
-
|
|
3
1
|
function isEmailConfigured(emailConfig) {
|
|
4
2
|
if (!emailConfig || typeof emailConfig !== "object") {
|
|
5
3
|
return false;
|
|
@@ -48,25 +46,6 @@ function buildStaticServices(befly) {
|
|
|
48
46
|
return [buildServiceItem("文件系统", "running", "-"), buildServiceItem("邮件服务", isEmailConfigured(befly.config?.email) ? "running" : "unconfigured", "-"), buildServiceItem("OSS存储", "unconfigured", "-")];
|
|
49
47
|
}
|
|
50
48
|
|
|
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
|
-
|
|
70
49
|
export default {
|
|
71
50
|
name: "获取服务状态",
|
|
72
51
|
method: "POST",
|
|
@@ -77,17 +56,6 @@ export default {
|
|
|
77
56
|
handler: async (befly) => {
|
|
78
57
|
const services = [await probeMysqlService(befly), await probeRedisService(befly), ...buildStaticServices(befly)];
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const cacheHealthResult = await cacheHealthApi.handler(befly);
|
|
84
|
-
const cacheHealthData = cacheHealthResult?.data || null;
|
|
85
|
-
|
|
86
|
-
cacheHealthSummary = buildCacheHealthSummary(cacheHealthData);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
befly.logger.error("缓存健康状态聚合失败", error);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return befly.tool.Yes("获取成功", { services: services, cacheHealth: cacheHealthSummary });
|
|
59
|
+
return befly.tool.Yes("获取成功", { services: services });
|
|
92
60
|
}
|
|
93
61
|
};
|
package/apis/tongJi/_tongJi.js
CHANGED
|
@@ -39,3 +39,51 @@ export async function expireTongJiRedisKeys(befly, keys, ttl) {
|
|
|
39
39
|
await befly.redis.expire(key, ttl);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
export function getVisitStatsDayMembersKey(reportDate) {
|
|
44
|
+
return `visit:day:${reportDate}:members`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getVisitStatsDayProductMembersKey(reportDate, productCode) {
|
|
48
|
+
return `visit:day:${reportDate}:product:${encodeURIComponent(productCode)}:members`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getVisitStatsMonthMembersKey(monthStartDate) {
|
|
52
|
+
return `visit:month:${monthStartDate}:members`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getVisitStatsMonthProductMembersKey(monthStartDate, productCode) {
|
|
56
|
+
return `visit:month:${monthStartDate}:product:${encodeURIComponent(productCode)}:members`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getVisitStatsRecentMembersKey() {
|
|
60
|
+
return "visit:recent30:members";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getVisitStatsRecentProductMembersKey(productCode) {
|
|
64
|
+
return `visit:recent30:product:${encodeURIComponent(productCode)}:members`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getOnlineStatsActiveProductKey(productCode) {
|
|
68
|
+
return `online:active:product:${encodeURIComponent(productCode)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getErrorStatsPeriodCountKey(periodType, periodValue) {
|
|
72
|
+
return `error:${periodType}:${periodValue}:count`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getErrorStatsDayBucketsKey(bucketDate) {
|
|
76
|
+
return `error:day:${bucketDate}:buckets`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
|
|
80
|
+
return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getErrorStatsDayTypesKey(bucketDate) {
|
|
84
|
+
return `error:day:${bucketDate}:types`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
|
|
88
|
+
return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
|
|
89
|
+
}
|
|
@@ -3,7 +3,7 @@ import { UAParser } from "ua-parser-js";
|
|
|
3
3
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
4
4
|
import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
|
|
5
5
|
|
|
6
|
-
import { expireTongJiRedisKeys, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
6
|
+
import { expireTongJiRedisKeys, getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
7
7
|
|
|
8
8
|
const VISIT_STATS_BUCKET_MS = 30 * 60 * 1000;
|
|
9
9
|
const ERROR_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
|
|
@@ -33,26 +33,6 @@ function getErrorReportUaData(ctx) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
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
36
|
async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
|
|
57
37
|
if (!befly.redis) {
|
|
58
38
|
return;
|
|
@@ -1,35 +1,8 @@
|
|
|
1
1
|
import { getDateYmdNumber } from "#befly/utils/datetime.js";
|
|
2
2
|
|
|
3
|
-
import { getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
3
|
+
import { getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
4
4
|
|
|
5
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
6
|
|
|
34
7
|
async function getErrorStatsRedisSummary(befly, periodType, periodValue) {
|
|
35
8
|
return {
|
|
@@ -99,13 +72,22 @@ async function getErrorStatsRedisTopTypes(befly, bucketDate) {
|
|
|
99
72
|
|
|
100
73
|
async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList) {
|
|
101
74
|
const todaySummary = await getErrorStatsRedisSummary(befly, "day", bucketDate);
|
|
75
|
+
const yesterdayDate = recentDateList[recentDateList.length - 2] || bucketDate;
|
|
76
|
+
const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
|
|
77
|
+
const yesterdaySummary = await getErrorStatsRedisSummary(befly, "day", yesterdayDate);
|
|
78
|
+
const dayBeforeYesterdaySummary = await getErrorStatsRedisSummary(befly, "day", dayBeforeYesterdayDate);
|
|
102
79
|
const weekSummary = await getErrorStatsRedisSummary(befly, "week", weekStartDate);
|
|
103
80
|
const monthSummary = await getErrorStatsRedisSummary(befly, "month", monthStartDate);
|
|
104
81
|
const trend = await getErrorStatsRedisTrend(befly, bucketDate);
|
|
105
82
|
const days = await getErrorStatsRedisDays(befly, recentDateList);
|
|
106
83
|
const topTypes = await getErrorStatsRedisTopTypes(befly, bucketDate);
|
|
84
|
+
let recent30Count = 0;
|
|
85
|
+
|
|
86
|
+
for (const item of days) {
|
|
87
|
+
recent30Count += item.count;
|
|
88
|
+
}
|
|
107
89
|
|
|
108
|
-
if (todaySummary.count <= 0 && weekSummary.count <= 0 && monthSummary.count <= 0 && trend.length === 0 && topTypes.length === 0) {
|
|
90
|
+
if (todaySummary.count <= 0 && yesterdaySummary.count <= 0 && dayBeforeYesterdaySummary.count <= 0 && weekSummary.count <= 0 && monthSummary.count <= 0 && recent30Count <= 0 && trend.length === 0 && topTypes.length === 0) {
|
|
109
91
|
return null;
|
|
110
92
|
}
|
|
111
93
|
|
|
@@ -114,6 +96,14 @@ async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthSta
|
|
|
114
96
|
bucketDate: bucketDate,
|
|
115
97
|
count: todaySummary.count
|
|
116
98
|
},
|
|
99
|
+
yesterday: {
|
|
100
|
+
bucketDate: yesterdayDate,
|
|
101
|
+
count: yesterdaySummary.count
|
|
102
|
+
},
|
|
103
|
+
dayBeforeYesterday: {
|
|
104
|
+
bucketDate: dayBeforeYesterdayDate,
|
|
105
|
+
count: dayBeforeYesterdaySummary.count
|
|
106
|
+
},
|
|
117
107
|
week: {
|
|
118
108
|
startDate: weekStartDate,
|
|
119
109
|
endDate: bucketDate,
|
|
@@ -124,31 +114,17 @@ async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthSta
|
|
|
124
114
|
endDate: bucketDate,
|
|
125
115
|
count: monthSummary.count
|
|
126
116
|
},
|
|
117
|
+
recent30: {
|
|
118
|
+
startDate: recentDateList[0],
|
|
119
|
+
endDate: bucketDate,
|
|
120
|
+
count: recent30Count
|
|
121
|
+
},
|
|
127
122
|
trend: trend,
|
|
128
123
|
days: days,
|
|
129
124
|
topTypes: topTypes
|
|
130
125
|
};
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
async function incrErrorStatsFallbackCount(befly, bucketDate) {
|
|
134
|
-
if (!befly.redis) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const dailyKey = getErrorStatsFallbackDailyCountKey(bucketDate);
|
|
139
|
-
|
|
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;
|
|
144
|
-
}
|
|
145
|
-
|
|
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);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
128
|
async function getErrorStatsSummary(befly, startDate, endDate) {
|
|
153
129
|
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]);
|
|
154
130
|
const detail = result.data?.[0] || {};
|
|
@@ -173,13 +149,32 @@ export default {
|
|
|
173
149
|
const weekStartDate = getTongJiWeekStartDate(now);
|
|
174
150
|
const monthStartDate = getTongJiMonthStartDate(now);
|
|
175
151
|
const recentDateList = getTongJiRecentDateList(now, ERROR_STATS_DAY_LIMIT);
|
|
152
|
+
const yesterdayDate = recentDateList[recentDateList.length - 2] || bucketDate;
|
|
153
|
+
const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
|
|
176
154
|
|
|
177
155
|
if (!tableReady) {
|
|
156
|
+
const days = [];
|
|
157
|
+
|
|
158
|
+
for (const item of recentDateList) {
|
|
159
|
+
days.push({
|
|
160
|
+
bucketDate: item,
|
|
161
|
+
count: 0
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
178
165
|
return befly.tool.Yes("获取成功", {
|
|
179
166
|
today: {
|
|
180
167
|
bucketDate: bucketDate,
|
|
181
168
|
count: 0
|
|
182
169
|
},
|
|
170
|
+
yesterday: {
|
|
171
|
+
bucketDate: yesterdayDate,
|
|
172
|
+
count: 0
|
|
173
|
+
},
|
|
174
|
+
dayBeforeYesterday: {
|
|
175
|
+
bucketDate: dayBeforeYesterdayDate,
|
|
176
|
+
count: 0
|
|
177
|
+
},
|
|
183
178
|
week: {
|
|
184
179
|
startDate: weekStartDate,
|
|
185
180
|
endDate: bucketDate,
|
|
@@ -190,8 +185,13 @@ export default {
|
|
|
190
185
|
endDate: bucketDate,
|
|
191
186
|
count: 0
|
|
192
187
|
},
|
|
188
|
+
recent30: {
|
|
189
|
+
startDate: recentDateList[0],
|
|
190
|
+
endDate: bucketDate,
|
|
191
|
+
count: 0
|
|
192
|
+
},
|
|
193
193
|
trend: [],
|
|
194
|
-
days:
|
|
194
|
+
days: days,
|
|
195
195
|
topTypes: []
|
|
196
196
|
});
|
|
197
197
|
}
|
|
@@ -202,13 +202,14 @@ export default {
|
|
|
202
202
|
if (redisResult) {
|
|
203
203
|
return befly.tool.Yes("获取成功", redisResult);
|
|
204
204
|
}
|
|
205
|
-
|
|
206
|
-
await incrErrorStatsFallbackCount(befly, bucketDate);
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
const todaySummary = await getErrorStatsSummary(befly, bucketDate, bucketDate);
|
|
208
|
+
const yesterdaySummary = await getErrorStatsSummary(befly, yesterdayDate, yesterdayDate);
|
|
209
|
+
const dayBeforeYesterdaySummary = await getErrorStatsSummary(befly, dayBeforeYesterdayDate, dayBeforeYesterdayDate);
|
|
210
210
|
const weekSummary = await getErrorStatsSummary(befly, weekStartDate, bucketDate);
|
|
211
211
|
const monthSummary = await getErrorStatsSummary(befly, monthStartDate, bucketDate);
|
|
212
|
+
const recent30Summary = await getErrorStatsSummary(befly, recentDateList[0], bucketDate);
|
|
212
213
|
const trendRes = await befly.mysql.execute("SELECT bucket_time as bucketTime, bucket_date as bucketDate, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date = ? GROUP BY bucket_time, bucket_date ORDER BY bucket_time ASC", [bucketDate]);
|
|
213
214
|
const daysRes = await befly.mysql.execute("SELECT bucket_date as bucketDate, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date >= ? GROUP BY bucket_date ORDER BY bucket_date ASC", [recentDateList[0]]);
|
|
214
215
|
const topTypesRes = await befly.mysql.execute("SELECT error_type as errorType, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date = ? GROUP BY error_type ORDER BY count DESC LIMIT 5", [bucketDate]);
|
|
@@ -259,6 +260,14 @@ export default {
|
|
|
259
260
|
bucketDate: bucketDate,
|
|
260
261
|
count: todaySummary.count
|
|
261
262
|
},
|
|
263
|
+
yesterday: {
|
|
264
|
+
bucketDate: yesterdayDate,
|
|
265
|
+
count: yesterdaySummary.count
|
|
266
|
+
},
|
|
267
|
+
dayBeforeYesterday: {
|
|
268
|
+
bucketDate: dayBeforeYesterdayDate,
|
|
269
|
+
count: dayBeforeYesterdaySummary.count
|
|
270
|
+
},
|
|
262
271
|
week: {
|
|
263
272
|
startDate: weekStartDate,
|
|
264
273
|
endDate: bucketDate,
|
|
@@ -269,6 +278,11 @@ export default {
|
|
|
269
278
|
endDate: bucketDate,
|
|
270
279
|
count: monthSummary.count
|
|
271
280
|
},
|
|
281
|
+
recent30: {
|
|
282
|
+
startDate: recentDateList[0],
|
|
283
|
+
endDate: bucketDate,
|
|
284
|
+
count: recent30Summary.count
|
|
285
|
+
},
|
|
272
286
|
trend: trend,
|
|
273
287
|
days: days,
|
|
274
288
|
topTypes: topTypes
|
|
@@ -1,108 +1,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import infoReportTable from "#befly/tables/infoReport.json";
|
|
4
|
-
import { getDateYmdNumber } from "#befly/utils/datetime.js";
|
|
1
|
+
import { addDays, getDateYmdNumber, getDayStartTime } from "#befly/utils/datetime.js";
|
|
5
2
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
6
3
|
|
|
7
|
-
import { getTongJiMonthStartDate,
|
|
4
|
+
import { expireTongJiRedisKeys, getTongJiMonthStartDate, getVisitStatsDayMembersKey, getVisitStatsDayProductMembersKey, getVisitStatsMonthMembersKey, getVisitStatsMonthProductMembersKey, getVisitStatsRecentMembersKey, getVisitStatsRecentProductMembersKey } from "./_tongJi.js";
|
|
8
5
|
|
|
9
|
-
const
|
|
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
|
-
];
|
|
6
|
+
const VISIT_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
|
|
27
7
|
|
|
28
|
-
function
|
|
29
|
-
if (isValidPositiveInt(ctx
|
|
8
|
+
function getVisitStatsMember(ctx) {
|
|
9
|
+
if (isValidPositiveInt(ctx?.userId)) {
|
|
30
10
|
return `user:${ctx.userId}`;
|
|
31
11
|
}
|
|
32
12
|
|
|
33
|
-
return `ip:${ctx
|
|
13
|
+
return `ip:${ctx?.ip || "unknown"}`;
|
|
34
14
|
}
|
|
35
15
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
}
|
|
16
|
+
async function addRecentVisitMember(befly, key, member, now) {
|
|
17
|
+
const cutoffTime = getDayStartTime(addDays(now, -29));
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
|
19
|
+
await befly.redis.zadd(key, [
|
|
20
|
+
{
|
|
21
|
+
score: now,
|
|
22
|
+
member: member
|
|
58
23
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
}
|
|
24
|
+
]);
|
|
25
|
+
await befly.redis.zremrangebyscore(key, "-inf", cutoffTime - 1);
|
|
26
|
+
await befly.redis.expire(key, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
68
27
|
}
|
|
69
28
|
|
|
70
|
-
async function
|
|
71
|
-
if (!befly.redis) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const weekStartDate = getTongJiWeekStartDate(now);
|
|
29
|
+
async function updateVisitStatsRedis(befly, now, reportDate, member, productCode) {
|
|
76
30
|
const monthStartDate = getTongJiMonthStartDate(now);
|
|
31
|
+
const dayKey = getVisitStatsDayMembersKey(reportDate);
|
|
32
|
+
const monthKey = getVisitStatsMonthMembersKey(monthStartDate);
|
|
33
|
+
const recentKey = getVisitStatsRecentMembersKey();
|
|
34
|
+
const dayAdded = await befly.redis.sadd(dayKey, [member]);
|
|
35
|
+
|
|
36
|
+
await befly.redis.sadd(monthKey, [member]);
|
|
37
|
+
await addRecentVisitMember(befly, recentKey, member, now);
|
|
38
|
+
await expireTongJiRedisKeys(befly, [dayKey, monthKey], VISIT_STATS_REDIS_TTL_SECONDS);
|
|
39
|
+
|
|
40
|
+
if (productCode) {
|
|
41
|
+
const dayProductKey = getVisitStatsDayProductMembersKey(reportDate, productCode);
|
|
42
|
+
const monthProductKey = getVisitStatsMonthProductMembersKey(monthStartDate, productCode);
|
|
43
|
+
const recentProductKey = getVisitStatsRecentProductMembersKey(productCode);
|
|
44
|
+
|
|
45
|
+
await befly.redis.sadd(dayProductKey, [member]);
|
|
46
|
+
await befly.redis.sadd(monthProductKey, [member]);
|
|
47
|
+
await addRecentVisitMember(befly, recentProductKey, member, now);
|
|
48
|
+
await expireTongJiRedisKeys(befly, [dayProductKey, monthProductKey], VISIT_STATS_REDIS_TTL_SECONDS);
|
|
49
|
+
}
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
await updateInfoStatsPeriodRedis(befly, "week", weekStartDate, statData, now);
|
|
80
|
-
await updateInfoStatsPeriodRedis(befly, "month", monthStartDate, statData, now);
|
|
51
|
+
return dayAdded > 0;
|
|
81
52
|
}
|
|
82
53
|
|
|
83
54
|
export default {
|
|
84
|
-
name: "
|
|
55
|
+
name: "上报独立访客统计",
|
|
85
56
|
method: "POST",
|
|
86
57
|
body: "none",
|
|
87
58
|
auth: false,
|
|
88
59
|
fields: {
|
|
89
|
-
pagePath:
|
|
90
|
-
pageName:
|
|
91
|
-
source:
|
|
92
|
-
productName:
|
|
93
|
-
productCode:
|
|
94
|
-
productVersion:
|
|
95
|
-
detail:
|
|
60
|
+
pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
|
|
61
|
+
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
62
|
+
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
63
|
+
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
64
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
65
|
+
productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
|
|
66
|
+
detail: { name: "扩展详情", input: "string", min: 0, max: 1000 }
|
|
96
67
|
},
|
|
97
68
|
required: [],
|
|
98
69
|
handler: async (befly, ctx) => {
|
|
99
70
|
const now = Date.now();
|
|
100
71
|
const reportDate = getDateYmdNumber(now);
|
|
101
|
-
const member = getInfoStatsMember(ctx);
|
|
102
|
-
const tableReady = (await befly.mysql.tableExists("beflyInfoReport")).data === true;
|
|
103
|
-
let userAgent = "";
|
|
104
72
|
|
|
105
|
-
if (!
|
|
73
|
+
if (!befly.redis) {
|
|
106
74
|
return befly.tool.Yes("上报成功", {
|
|
107
75
|
reportTime: now,
|
|
108
76
|
reportDate: reportDate,
|
|
@@ -111,81 +79,12 @@ export default {
|
|
|
111
79
|
});
|
|
112
80
|
}
|
|
113
81
|
|
|
114
|
-
|
|
115
|
-
userAgent = ctx.req.headers.get("user-agent") || "";
|
|
116
|
-
} else if (typeof ctx.headers?.get === "function") {
|
|
117
|
-
userAgent = ctx.headers.get("user-agent") || "";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const uaResult = UAParser(userAgent);
|
|
121
|
-
|
|
122
|
-
const detail = await befly.mysql.getOne({
|
|
123
|
-
table: "beflyInfoReport",
|
|
124
|
-
where: {
|
|
125
|
-
reportDate: reportDate,
|
|
126
|
-
memberKey: member
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (detail.data?.id) {
|
|
131
|
-
return befly.tool.Yes("上报成功", {
|
|
132
|
-
reportTime: now,
|
|
133
|
-
reportDate: reportDate,
|
|
134
|
-
counted: false,
|
|
135
|
-
stored: false
|
|
136
|
-
});
|
|
137
|
-
}
|
|
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
|
-
|
|
157
|
-
await befly.mysql.insData({
|
|
158
|
-
table: "beflyInfoReport",
|
|
159
|
-
data: {
|
|
160
|
-
reportTime: now,
|
|
161
|
-
reportDate: reportDate,
|
|
162
|
-
memberKey: member,
|
|
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 || "",
|
|
170
|
-
userAgent: userAgent,
|
|
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
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await updateInfoStatsRedis(befly, now, reportDate, statData);
|
|
82
|
+
const counted = await updateVisitStatsRedis(befly, now, reportDate, getVisitStatsMember(ctx), ctx.body?.productCode || "");
|
|
184
83
|
|
|
185
84
|
return befly.tool.Yes("上报成功", {
|
|
186
85
|
reportTime: now,
|
|
187
86
|
reportDate: reportDate,
|
|
188
|
-
counted:
|
|
87
|
+
counted: counted,
|
|
189
88
|
stored: true
|
|
190
89
|
});
|
|
191
90
|
}
|