befly 3.57.0 → 3.58.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/tongJi/_daily.js +159 -0
- package/apis/tongJi/_tongJi.js +13 -33
- package/apis/tongJi/dailyReport.js +76 -0
- package/apis/tongJi/dailyStats.js +182 -0
- package/apis/tongJi/errorList.js +20 -58
- package/apis/tongJi/errorReport.js +10 -34
- package/apis/tongJi/errorStats.js +171 -198
- package/package.json +1 -1
- package/apis/tongJi/infoReport.js +0 -91
- package/apis/tongJi/infoStats.js +0 -190
- package/apis/tongJi/onlineReport.js +0 -102
- package/apis/tongJi/onlineStats.js +0 -85
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { DAY_MS } from "#befly/utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
4
|
+
|
|
5
|
+
function getTimeZoneDateParts(timestamp, timeZone) {
|
|
6
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
7
|
+
timeZone: timeZone,
|
|
8
|
+
year: "numeric",
|
|
9
|
+
month: "2-digit",
|
|
10
|
+
day: "2-digit"
|
|
11
|
+
});
|
|
12
|
+
const parts = formatter.formatToParts(timestamp);
|
|
13
|
+
const result = {};
|
|
14
|
+
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (part.type !== "literal") {
|
|
17
|
+
result[part.type] = Number(part.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildYmdNumber(year, month, day) {
|
|
25
|
+
return year * 10000 + month * 100 + day;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTimeZone(timeZone) {
|
|
29
|
+
return typeof timeZone === "string" && timeZone.trim().length > 0 ? timeZone.trim() : DEFAULT_TIME_ZONE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findTimestampInDay(targetYmd, timeZone) {
|
|
33
|
+
const year = Math.floor(targetYmd / 10000);
|
|
34
|
+
const month = Math.floor((targetYmd % 10000) / 100);
|
|
35
|
+
const day = targetYmd % 100;
|
|
36
|
+
// 取当天正午作为锚点,再向前/后逐小时修正,确保落在目标日期内
|
|
37
|
+
let utc = Date.UTC(year, month - 1, day, 12, 0, 0, 0);
|
|
38
|
+
const ymdAtUtc = getDailyDateYmdNumber(utc, timeZone);
|
|
39
|
+
|
|
40
|
+
if (ymdAtUtc === targetYmd) {
|
|
41
|
+
return utc;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const step = ymdAtUtc < targetYmd ? 3600000 : -3600000;
|
|
45
|
+
|
|
46
|
+
do {
|
|
47
|
+
utc += step;
|
|
48
|
+
} while (getDailyDateYmdNumber(utc, timeZone) !== targetYmd);
|
|
49
|
+
|
|
50
|
+
return utc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getWeekdayIndex(timestamp, timeZone) {
|
|
54
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
55
|
+
timeZone: timeZone,
|
|
56
|
+
weekday: "short"
|
|
57
|
+
});
|
|
58
|
+
const parts = formatter.formatToParts(timestamp);
|
|
59
|
+
const shortWeekday = parts.find((item) => item.type === "weekday").value;
|
|
60
|
+
const map = {
|
|
61
|
+
Sun: 0,
|
|
62
|
+
Mon: 1,
|
|
63
|
+
Tue: 2,
|
|
64
|
+
Wed: 3,
|
|
65
|
+
Thu: 4,
|
|
66
|
+
Fri: 5,
|
|
67
|
+
Sat: 6
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return map[shortWeekday] ?? 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getDailyDateYmdNumber(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
74
|
+
const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
|
|
75
|
+
|
|
76
|
+
return buildYmdNumber(parts.year, parts.month, parts.day);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getDailyDayStartTime(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
80
|
+
const zone = normalizeTimeZone(timeZone);
|
|
81
|
+
const targetYmd = getDailyDateYmdNumber(timestamp, zone);
|
|
82
|
+
const timestampInDay = findTimestampInDay(targetYmd, zone);
|
|
83
|
+
let low = timestampInDay - DAY_MS;
|
|
84
|
+
let high = timestampInDay + DAY_MS;
|
|
85
|
+
|
|
86
|
+
while (getDailyDateYmdNumber(low, zone) >= targetYmd) {
|
|
87
|
+
low -= DAY_MS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
while (getDailyDateYmdNumber(high, zone) <= targetYmd) {
|
|
91
|
+
high += DAY_MS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
while (low + 1 < high) {
|
|
95
|
+
const mid = Math.floor((low + high) / 2);
|
|
96
|
+
|
|
97
|
+
if (getDailyDateYmdNumber(mid, zone) < targetYmd) {
|
|
98
|
+
low = mid;
|
|
99
|
+
} else {
|
|
100
|
+
high = mid;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return high;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getDailyMonthStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
108
|
+
const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
|
|
109
|
+
|
|
110
|
+
return buildYmdNumber(parts.year, parts.month, 1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getDailyWeekStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
114
|
+
const zone = normalizeTimeZone(timeZone);
|
|
115
|
+
let dayStart = getDailyDayStartTime(timestamp, zone);
|
|
116
|
+
|
|
117
|
+
while (getWeekdayIndex(dayStart, zone) !== 1) {
|
|
118
|
+
dayStart = getDailyDayStartTime(dayStart - DAY_MS, zone);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return getDailyDateYmdNumber(dayStart, zone);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getDailyRecentDateList(timestamp = Date.now(), limit = 30, timeZone = DEFAULT_TIME_ZONE) {
|
|
125
|
+
const zone = normalizeTimeZone(timeZone);
|
|
126
|
+
const dayStart = getDailyDayStartTime(timestamp, zone);
|
|
127
|
+
const list = [];
|
|
128
|
+
|
|
129
|
+
for (let i = limit - 1; i >= 0; i -= 1) {
|
|
130
|
+
list.push(getDailyDateYmdNumber(dayStart - i * DAY_MS, zone));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return list;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getDailyDayStartTimeForYmd(reportDate, timeZone = DEFAULT_TIME_ZONE) {
|
|
137
|
+
return getDailyDayStartTime(findTimestampInDay(reportDate, normalizeTimeZone(timeZone)), timeZone);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getDailyDateRangeList(startDateYmd, endDateYmd, timeZone = DEFAULT_TIME_ZONE) {
|
|
141
|
+
const zone = normalizeTimeZone(timeZone);
|
|
142
|
+
const startDayStart = getDailyDayStartTimeForYmd(startDateYmd, zone);
|
|
143
|
+
const endDayStart = getDailyDayStartTimeForYmd(endDateYmd, zone);
|
|
144
|
+
const list = [];
|
|
145
|
+
|
|
146
|
+
for (let t = startDayStart; t <= endDayStart; t += DAY_MS) {
|
|
147
|
+
list.push(getDailyDateYmdNumber(t, zone));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return list;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getDailyStatsDayMembersKey(reportDate) {
|
|
154
|
+
return `daily:day:${reportDate}:members`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getDailyStatsDayProductMembersKey(reportDate, productCode) {
|
|
158
|
+
return `daily:day:${reportDate}:product:${encodeURIComponent(productCode)}:members`;
|
|
159
|
+
}
|
package/apis/tongJi/_tongJi.js
CHANGED
|
@@ -3,11 +3,7 @@ import { addDays, getDateYmdNumber } from "#befly/utils/datetime.js";
|
|
|
3
3
|
export function getTongJiNumber(value) {
|
|
4
4
|
const num = Number(value);
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
return 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return num;
|
|
6
|
+
return Number.isFinite(num) ? num : 0;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
export function getTongJiWeekStartDate(timestamp = Date.now()) {
|
|
@@ -34,38 +30,22 @@ export function getTongJiRecentDateList(now = Date.now(), limit = 30) {
|
|
|
34
30
|
return list;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
33
|
+
export function getTongJiDateRangeList(startDate, endDate) {
|
|
34
|
+
const list = [];
|
|
35
|
+
const start = new Date(Math.floor(startDate / 10000), Math.floor((startDate % 10000) / 100) - 1, startDate % 100);
|
|
36
|
+
const end = new Date(Math.floor(endDate / 10000), Math.floor((endDate % 10000) / 100) - 1, endDate % 100);
|
|
37
|
+
const current = new Date(start);
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
while (current.getTime() <= end.getTime()) {
|
|
40
|
+
list.push(getDateYmdNumber(current.getTime()));
|
|
41
|
+
current.setDate(current.getDate() + 1);
|
|
42
|
+
}
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
return `visit:recent30:product:${encodeURIComponent(productCode)}:members`;
|
|
44
|
+
return list;
|
|
65
45
|
}
|
|
66
46
|
|
|
67
|
-
export function
|
|
68
|
-
|
|
47
|
+
export async function expireRedisKeys(befly, keys, ttl) {
|
|
48
|
+
await Promise.all(keys.map((key) => befly.redis.expire(key, ttl)));
|
|
69
49
|
}
|
|
70
50
|
|
|
71
51
|
export function getErrorStatsPeriodCountKey(periodType, periodValue) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
2
|
+
|
|
3
|
+
import { getDailyDateYmdNumber, getDailyStatsDayMembersKey, getDailyStatsDayProductMembersKey } from "./_daily.js";
|
|
4
|
+
import { expireRedisKeys } from "./_tongJi.js";
|
|
5
|
+
|
|
6
|
+
const DAILY_STATS_REDIS_TTL_SECONDS = 90 * 24 * 60 * 60;
|
|
7
|
+
|
|
8
|
+
function getDailyStatsMember(ctx) {
|
|
9
|
+
if (isValidPositiveInt(ctx?.userId)) {
|
|
10
|
+
return `user:${ctx.userId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clientId = ctx?.body?.clientId ?? "";
|
|
14
|
+
|
|
15
|
+
if (clientId) {
|
|
16
|
+
return `client:${clientId}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `ip:${ctx?.ip || "unknown"}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function updateDailyStatsRedis(befly, reportDate, member, productCode) {
|
|
23
|
+
const dayKey = getDailyStatsDayMembersKey(reportDate);
|
|
24
|
+
const keys = [dayKey];
|
|
25
|
+
|
|
26
|
+
if (productCode) {
|
|
27
|
+
keys.push(getDailyStatsDayProductMembersKey(reportDate, productCode));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const results = await Promise.all(keys.map((key) => befly.redis.sadd(key, [member])));
|
|
31
|
+
|
|
32
|
+
await expireRedisKeys(befly, keys, DAILY_STATS_REDIS_TTL_SECONDS);
|
|
33
|
+
|
|
34
|
+
return results[0] > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
name: "上报日在线统计",
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: "none",
|
|
41
|
+
auth: false,
|
|
42
|
+
fields: {
|
|
43
|
+
pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
|
|
44
|
+
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
45
|
+
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
46
|
+
clientId: { name: "客户端标识", input: "string", min: 0, max: 120 },
|
|
47
|
+
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
48
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
49
|
+
productVersion: { name: "产品版本", input: "string", min: 0, max: 50 }
|
|
50
|
+
},
|
|
51
|
+
required: [],
|
|
52
|
+
handler: async (befly, ctx) => {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const timeZone = befly.config?.tz;
|
|
55
|
+
const reportDate = getDailyDateYmdNumber(now, timeZone);
|
|
56
|
+
const productCode = ctx?.body?.productCode ?? "";
|
|
57
|
+
|
|
58
|
+
if (!befly.redis) {
|
|
59
|
+
return befly.tool.Yes("上报成功", {
|
|
60
|
+
reportTime: now,
|
|
61
|
+
reportDate: reportDate,
|
|
62
|
+
counted: false,
|
|
63
|
+
stored: false
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const counted = await updateDailyStatsRedis(befly, reportDate, getDailyStatsMember(ctx), productCode);
|
|
68
|
+
|
|
69
|
+
return befly.tool.Yes("上报成功", {
|
|
70
|
+
reportTime: now,
|
|
71
|
+
reportDate: reportDate,
|
|
72
|
+
counted: counted,
|
|
73
|
+
stored: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { getDailyDateRangeList, getDailyDateYmdNumber, getDailyMonthStartDate, getDailyRecentDateList, getDailyStatsDayMembersKey, getDailyStatsDayProductMembersKey, getDailyWeekStartDate } from "./_daily.js";
|
|
2
|
+
|
|
3
|
+
const DAILY_STATS_DAY_LIMIT = 30;
|
|
4
|
+
|
|
5
|
+
async function getRedisSetCount(befly, key) {
|
|
6
|
+
const count = await befly.redis.scard(key);
|
|
7
|
+
|
|
8
|
+
return Number.isFinite(Number(count)) ? Number(count) : 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getDayMembersKey(reportDate, productCode) {
|
|
12
|
+
return productCode ? getDailyStatsDayProductMembersKey(reportDate, productCode) : getDailyStatsDayMembersKey(reportDate);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getDayCount(befly, reportDate, productCode) {
|
|
16
|
+
return {
|
|
17
|
+
reportDate: reportDate,
|
|
18
|
+
count: await getRedisSetCount(befly, getDayMembersKey(reportDate, productCode))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function sumDayCounts(befly, dateList, productCode) {
|
|
23
|
+
const counts = await Promise.all(dateList.map((date) => getRedisSetCount(befly, getDayMembersKey(date, productCode))));
|
|
24
|
+
|
|
25
|
+
return counts.reduce((sum, count) => sum + count, 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getDayCounts(befly, dateList, productCode) {
|
|
29
|
+
return Promise.all(dateList.map((date) => getDayCount(befly, date, productCode)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildDateRangeSummary(startDate, endDate, count) {
|
|
33
|
+
return {
|
|
34
|
+
startDate: startDate,
|
|
35
|
+
endDate: endDate,
|
|
36
|
+
count: count
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildDaySummary(reportDate, count) {
|
|
41
|
+
return {
|
|
42
|
+
reportDate: reportDate,
|
|
43
|
+
count: count
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getDailyStatsSummary(befly, params) {
|
|
48
|
+
const { reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList, productCode } = params;
|
|
49
|
+
const timeZone = befly.config?.tz;
|
|
50
|
+
const weekDateList = getDailyDateRangeList(weekStartDate, reportDate, timeZone);
|
|
51
|
+
const monthDateList = recentDateList.filter((date) => date >= monthStartDate);
|
|
52
|
+
|
|
53
|
+
const [today, yesterday, dayBeforeYesterday, week, month, recent7, recent30, days] = await Promise.all([
|
|
54
|
+
getDayCount(befly, reportDate, productCode),
|
|
55
|
+
getDayCount(befly, recentDateList[recentDateList.length - 2] || reportDate, productCode),
|
|
56
|
+
getDayCount(befly, recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || reportDate, productCode),
|
|
57
|
+
sumDayCounts(befly, weekDateList, productCode).then((count) => buildDateRangeSummary(weekStartDate, reportDate, count)),
|
|
58
|
+
sumDayCounts(befly, monthDateList, productCode).then((count) => buildDateRangeSummary(monthStartDate, reportDate, count)),
|
|
59
|
+
sumDayCounts(befly, recent7DateList, productCode).then((count) => buildDateRangeSummary(recent7DateList[0], reportDate, count)),
|
|
60
|
+
sumDayCounts(befly, recentDateList, productCode).then((count) => buildDateRangeSummary(recentDateList[0], reportDate, count)),
|
|
61
|
+
getDayCounts(befly, recentDateList, productCode)
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
today: today,
|
|
66
|
+
yesterday: yesterday,
|
|
67
|
+
dayBeforeYesterday: dayBeforeYesterday,
|
|
68
|
+
week: week,
|
|
69
|
+
month: month,
|
|
70
|
+
recent7: recent7,
|
|
71
|
+
recent30: recent30,
|
|
72
|
+
days: days
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildEmptyDays(recentDateList) {
|
|
77
|
+
return recentDateList.map((reportDate) => {
|
|
78
|
+
return {
|
|
79
|
+
reportDate: reportDate,
|
|
80
|
+
count: 0
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildEmptySummary(reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList) {
|
|
86
|
+
const days = buildEmptyDays(recentDateList);
|
|
87
|
+
const yesterdayDate = recentDateList[recentDateList.length - 2] || reportDate;
|
|
88
|
+
const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
today: buildDaySummary(reportDate, 0),
|
|
92
|
+
yesterday: buildDaySummary(yesterdayDate, 0),
|
|
93
|
+
dayBeforeYesterday: buildDaySummary(dayBeforeYesterdayDate, 0),
|
|
94
|
+
week: buildDateRangeSummary(weekStartDate, reportDate, 0),
|
|
95
|
+
month: buildDateRangeSummary(monthStartDate, reportDate, 0),
|
|
96
|
+
recent7: buildDateRangeSummary(recent7DateList[0], reportDate, 0),
|
|
97
|
+
recent30: buildDateRangeSummary(recentDateList[0], reportDate, 0),
|
|
98
|
+
days: days
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildProductSummary(item, summary) {
|
|
103
|
+
return {
|
|
104
|
+
key: item.code,
|
|
105
|
+
productName: item.name,
|
|
106
|
+
productCode: item.code,
|
|
107
|
+
today: summary.today.count,
|
|
108
|
+
yesterday: summary.yesterday.count,
|
|
109
|
+
dayBeforeYesterday: summary.dayBeforeYesterday.count,
|
|
110
|
+
week: summary.week.count,
|
|
111
|
+
month: summary.month.count,
|
|
112
|
+
recent7: summary.recent7.count,
|
|
113
|
+
recent30: summary.recent30.count,
|
|
114
|
+
days: summary.days
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function buildDailyStatsProducts(befly, params, projectLists) {
|
|
119
|
+
const summaries = await Promise.all(
|
|
120
|
+
projectLists.map((item) => {
|
|
121
|
+
return getDailyStatsSummary(befly, { ...params, productCode: item.code }).then((summary) => buildProductSummary(item, summary));
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return summaries.toSorted((a, b) => {
|
|
126
|
+
if (b.recent30 !== a.recent30) {
|
|
127
|
+
return b.recent30 - a.recent30;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return String(a.productName).localeCompare(String(b.productName), "zh-CN");
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default {
|
|
135
|
+
name: "获取日在线统计",
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: "none",
|
|
138
|
+
auth: true,
|
|
139
|
+
fields: {
|
|
140
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 }
|
|
141
|
+
},
|
|
142
|
+
required: [],
|
|
143
|
+
handler: async (befly, ctx) => {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const timeZone = befly.config?.tz;
|
|
146
|
+
const reportDate = getDailyDateYmdNumber(now, timeZone);
|
|
147
|
+
const monthStartDate = getDailyMonthStartDate(now, timeZone);
|
|
148
|
+
const weekStartDate = getDailyWeekStartDate(now, timeZone);
|
|
149
|
+
const recentDateList = getDailyRecentDateList(now, DAILY_STATS_DAY_LIMIT, timeZone);
|
|
150
|
+
const recent7DateList = getDailyRecentDateList(now, 7, timeZone);
|
|
151
|
+
const productCode = ctx?.body?.productCode ?? "";
|
|
152
|
+
const projectLists = productCode ? [] : befly.config?.projectLists || [];
|
|
153
|
+
const params = {
|
|
154
|
+
reportDate: reportDate,
|
|
155
|
+
monthStartDate: monthStartDate,
|
|
156
|
+
weekStartDate: weekStartDate,
|
|
157
|
+
recentDateList: recentDateList,
|
|
158
|
+
recent7DateList: recent7DateList,
|
|
159
|
+
productCode: productCode
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (!befly.redis) {
|
|
163
|
+
const summary = buildEmptySummary(reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList);
|
|
164
|
+
const products = projectLists.map((item) => buildProductSummary(item, summary));
|
|
165
|
+
|
|
166
|
+
return befly.tool.Yes("获取成功", {
|
|
167
|
+
queryTime: now,
|
|
168
|
+
...summary,
|
|
169
|
+
products: products
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const summary = await getDailyStatsSummary(befly, params);
|
|
174
|
+
const products = await buildDailyStatsProducts(befly, params, projectLists);
|
|
175
|
+
|
|
176
|
+
return befly.tool.Yes("获取成功", {
|
|
177
|
+
queryTime: now,
|
|
178
|
+
...summary,
|
|
179
|
+
products: products
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
package/apis/tongJi/errorList.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
2
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
3
3
|
|
|
4
|
+
const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate", "hitCount"];
|
|
5
|
+
const FILTER_FIELDS = ["errorType", "source", "productName", "productCode", "productVersion", "deviceType", "browserName", "osName"];
|
|
6
|
+
const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "productName", "productCode", "productVersion", "browserName", "osName", "deviceType", "deviceVendor", "deviceModel"];
|
|
7
|
+
|
|
4
8
|
export default {
|
|
5
9
|
name: "获取错误报告列表",
|
|
6
10
|
method: "POST",
|
|
@@ -33,61 +37,20 @@ export default {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
const keyword = ctx.body.keyword ?? "";
|
|
36
|
-
const errorType = ctx.body.errorType ?? "";
|
|
37
|
-
const source = ctx.body.source ?? "";
|
|
38
|
-
const productName = ctx.body.productName ?? "";
|
|
39
|
-
const productCode = ctx.body.productCode ?? "";
|
|
40
|
-
const productVersion = ctx.body.productVersion ?? "";
|
|
41
|
-
const deviceType = ctx.body.deviceType ?? "";
|
|
42
|
-
const browserName = ctx.body.browserName ?? "";
|
|
43
|
-
const osName = ctx.body.osName ?? "";
|
|
44
40
|
const where = {};
|
|
45
41
|
|
|
46
42
|
if (keyword) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
where.errorType$like$or = keyword;
|
|
51
|
-
where.productName$like$or = keyword;
|
|
52
|
-
where.productCode$like$or = keyword;
|
|
53
|
-
where.productVersion$like$or = keyword;
|
|
54
|
-
where.browserName$like$or = keyword;
|
|
55
|
-
where.osName$like$or = keyword;
|
|
56
|
-
where.deviceType$like$or = keyword;
|
|
57
|
-
where.deviceVendor$like$or = keyword;
|
|
58
|
-
where.deviceModel$like$or = keyword;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (errorType) {
|
|
62
|
-
where.errorType = errorType;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (source) {
|
|
66
|
-
where.source = source;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (productName) {
|
|
70
|
-
where.productName = productName;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (productCode) {
|
|
74
|
-
where.productCode = productCode;
|
|
43
|
+
for (const field of KEYWORD_FIELDS) {
|
|
44
|
+
where[`${field}$like$or`] = keyword;
|
|
45
|
+
}
|
|
75
46
|
}
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (deviceType) {
|
|
82
|
-
where.deviceType = deviceType;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (browserName) {
|
|
86
|
-
where.browserName = browserName;
|
|
87
|
-
}
|
|
48
|
+
for (const field of FILTER_FIELDS) {
|
|
49
|
+
const value = ctx.body[field] ?? "";
|
|
88
50
|
|
|
89
|
-
|
|
90
|
-
|
|
51
|
+
if (value) {
|
|
52
|
+
where[field] = value;
|
|
53
|
+
}
|
|
91
54
|
}
|
|
92
55
|
|
|
93
56
|
const result = await befly.mysql.getList({
|
|
@@ -98,16 +61,15 @@ export default {
|
|
|
98
61
|
orderBy: ["reportTime#DESC"]
|
|
99
62
|
});
|
|
100
63
|
|
|
101
|
-
const lists = []
|
|
102
|
-
for (const item of result.data?.lists || []) {
|
|
64
|
+
const lists = (result.data?.lists || []).map((item) => {
|
|
103
65
|
const row = Object.assign({}, item);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
66
|
+
|
|
67
|
+
for (const field of NUMBER_FIELDS) {
|
|
68
|
+
row[field] = Number(item[field] || 0) || (field === "hitCount" ? 1 : 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return row;
|
|
72
|
+
});
|
|
111
73
|
|
|
112
74
|
const data = Object.assign({}, result.data);
|
|
113
75
|
data.lists = lists;
|
|
@@ -2,9 +2,9 @@ import errorReportTable from "#befly/tables/errorReport.json";
|
|
|
2
2
|
import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
|
|
3
3
|
import { parseUserAgent } from "#befly/utils/userAgent.js";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expireRedisKeys, getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const ERROR_STATS_BUCKET_MS = 30 * 60 * 1000;
|
|
8
8
|
const ERROR_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
|
|
9
9
|
|
|
10
10
|
function getErrorReportUaData(ctx) {
|
|
@@ -26,25 +26,18 @@ async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorTy
|
|
|
26
26
|
|
|
27
27
|
const weekStartDate = getTongJiWeekStartDate(now);
|
|
28
28
|
const monthStartDate = getTongJiMonthStartDate(now);
|
|
29
|
+
const safeErrorType = String(errorType || "unknown");
|
|
29
30
|
const dayCountKey = getErrorStatsPeriodCountKey("day", bucketDate);
|
|
30
31
|
const weekCountKey = getErrorStatsPeriodCountKey("week", weekStartDate);
|
|
31
32
|
const monthCountKey = getErrorStatsPeriodCountKey("month", monthStartDate);
|
|
32
33
|
const dayBucketsKey = getErrorStatsDayBucketsKey(bucketDate);
|
|
33
34
|
const dayBucketCountKey = getErrorStatsDayBucketCountKey(bucketDate, bucketTime);
|
|
34
35
|
const dayTypesKey = getErrorStatsDayTypesKey(bucketDate);
|
|
35
|
-
const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate,
|
|
36
|
+
const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate, safeErrorType);
|
|
36
37
|
|
|
37
|
-
await befly.redis.incr(dayCountKey);
|
|
38
|
-
await befly.redis.incr(weekCountKey);
|
|
39
|
-
await befly.redis.incr(monthCountKey);
|
|
38
|
+
await Promise.all([befly.redis.incr(dayCountKey), befly.redis.incr(weekCountKey), befly.redis.incr(monthCountKey), befly.redis.sadd(dayBucketsKey, [String(bucketTime)]), befly.redis.incr(dayBucketCountKey), befly.redis.sadd(dayTypesKey, [safeErrorType]), befly.redis.incr(dayTypeCountKey)]);
|
|
40
39
|
|
|
41
|
-
await befly
|
|
42
|
-
await befly.redis.incr(dayBucketCountKey);
|
|
43
|
-
|
|
44
|
-
await befly.redis.sadd(dayTypesKey, [String(errorType || "unknown")]);
|
|
45
|
-
await befly.redis.incr(dayTypeCountKey);
|
|
46
|
-
|
|
47
|
-
await expireTongJiRedisKeys(befly, [dayCountKey, weekCountKey, monthCountKey, dayBucketsKey, dayBucketCountKey, dayTypesKey, dayTypeCountKey], ERROR_STATS_REDIS_TTL_SECONDS);
|
|
40
|
+
await expireRedisKeys(befly, [dayCountKey, weekCountKey, monthCountKey, dayBucketsKey, dayBucketCountKey, dayTypesKey, dayTypeCountKey], ERROR_STATS_REDIS_TTL_SECONDS);
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
export default {
|
|
@@ -66,37 +59,20 @@ export default {
|
|
|
66
59
|
required: [],
|
|
67
60
|
handler: async (befly, ctx) => {
|
|
68
61
|
const now = Date.now();
|
|
69
|
-
const bucketTime = getTimeBucketStart(now,
|
|
62
|
+
const bucketTime = getTimeBucketStart(now, ERROR_STATS_BUCKET_MS);
|
|
70
63
|
const bucketDate = getDateYmdNumber(now);
|
|
71
64
|
const uaData = getErrorReportUaData(ctx);
|
|
72
65
|
|
|
73
66
|
await befly.mysql.insData({
|
|
74
67
|
table: "beflyErrorReport",
|
|
75
68
|
data: {
|
|
69
|
+
...ctx.body,
|
|
70
|
+
...uaData,
|
|
76
71
|
reportTime: now,
|
|
77
72
|
firstReportTime: now,
|
|
78
73
|
bucketTime: bucketTime,
|
|
79
74
|
bucketDate: bucketDate,
|
|
80
|
-
hitCount: 1
|
|
81
|
-
source: ctx.body.source || "",
|
|
82
|
-
productName: ctx.body.productName || "",
|
|
83
|
-
productCode: ctx.body.productCode || "",
|
|
84
|
-
productVersion: ctx.body.productVersion || "",
|
|
85
|
-
pagePath: ctx.body.pagePath || "",
|
|
86
|
-
pageName: ctx.body.pageName || "",
|
|
87
|
-
errorType: ctx.body.errorType || "",
|
|
88
|
-
message: ctx.body.message || "",
|
|
89
|
-
detail: ctx.body.detail || "",
|
|
90
|
-
userAgent: uaData.userAgent,
|
|
91
|
-
browserName: uaData.browserName,
|
|
92
|
-
browserVersion: uaData.browserVersion,
|
|
93
|
-
osName: uaData.osName,
|
|
94
|
-
osVersion: uaData.osVersion,
|
|
95
|
-
deviceType: uaData.deviceType,
|
|
96
|
-
deviceVendor: uaData.deviceVendor,
|
|
97
|
-
deviceModel: uaData.deviceModel,
|
|
98
|
-
engineName: uaData.engineName,
|
|
99
|
-
cpuArchitecture: uaData.cpuArchitecture
|
|
75
|
+
hitCount: 1
|
|
100
76
|
}
|
|
101
77
|
});
|
|
102
78
|
|