befly 3.24.19 → 3.25.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/_apis.js +20 -0
- package/apis/admin/delete.js +3 -1
- package/apis/admin/detail.js +1 -3
- package/apis/admin/select.js +9 -7
- package/apis/admin/update.js +2 -2
- package/apis/api/all.js +6 -11
- package/apis/api/select.js +18 -19
- package/apis/dict/_dict.js +24 -0
- package/apis/dict/all.js +6 -4
- package/apis/dict/detail.js +9 -5
- package/apis/dict/insert.js +4 -11
- package/apis/dict/items.js +5 -6
- package/apis/dict/select.js +13 -9
- package/apis/dict/update.js +9 -13
- package/apis/dictType/select.js +4 -4
- package/apis/email/config.js +9 -11
- package/apis/email/logList.js +14 -4
- package/apis/email/send.js +10 -3
- package/apis/email/verify.js +9 -7
- package/apis/loginLog/select.js +11 -4
- package/apis/menu/all.js +13 -19
- package/apis/menu/select.js +19 -14
- package/apis/operateLog/select.js +13 -4
- package/apis/role/_role.js +21 -0
- package/apis/role/all.js +1 -3
- package/apis/role/apiSave.js +8 -15
- package/apis/role/apis.js +4 -10
- package/apis/role/delete.js +28 -36
- package/apis/role/detail.js +4 -10
- package/apis/role/insert.js +12 -10
- package/apis/role/menuSave.js +9 -15
- package/apis/role/menus.js +4 -10
- package/apis/role/save.js +19 -23
- package/apis/role/select.js +12 -9
- package/apis/role/update.js +14 -15
- package/apis/source/imageList.js +3 -3
- package/apis/sysConfig/get.js +11 -23
- package/apis/sysConfig/insert.js +22 -27
- package/apis/sysConfig/select.js +11 -5
- package/apis/sysConfig/update.js +33 -38
- package/apis/tongJi/_tongJi.js +41 -0
- package/apis/tongJi/cacheHealth.js +8 -30
- package/apis/tongJi/errorList.js +26 -27
- package/apis/tongJi/errorReport.js +26 -43
- package/apis/tongJi/errorStats.js +17 -48
- package/apis/tongJi/fallbackReset.js +7 -15
- package/apis/tongJi/infoReport.js +20 -32
- package/apis/tongJi/infoStats.js +5 -17
- package/apis/tongJi/onlineReport.js +50 -56
- package/apis/tongJi/onlineStats.js +97 -111
- package/checks/config.js +44 -1
- package/configs/beflyConfig.json +10 -1
- package/index.js +25 -0
- package/lib/dbHelper.js +1 -1
- package/lib/dbParse.js +61 -99
- package/lib/dbUtil.js +101 -21
- package/lib/redisHelper.js +25 -0
- package/lib/sqlBuilder.js +6 -0
- package/package.json +1 -1
- package/plugins/email.js +3 -6
- package/router/api.js +0 -7
- package/utils/email.js +3 -0
- package/apis/admin/cacheRefresh.js +0 -122
- package/apis/dashboard/configStatus.js +0 -39
- package/apis/dashboard/performanceMetrics.js +0 -23
- package/apis/dashboard/permissionStats.js +0 -27
- package/apis/dashboard/systemInfo.js +0 -19
- package/lib/requestMetrics.js +0 -203
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getDateYmdNumber } from "#root/utils/datetime.js";
|
|
2
2
|
|
|
3
|
+
import { getTongJiNumber } from "./_tongJi.js";
|
|
4
|
+
|
|
3
5
|
const INFO_STATS_FALLBACK_COUNT_KEY = "stats:fallback:infoStats:count";
|
|
4
6
|
const ERROR_STATS_FALLBACK_COUNT_KEY = "stats:fallback:errorStats:count";
|
|
5
7
|
|
|
@@ -11,16 +13,6 @@ function getErrorStatsFallbackDailyCountKey(reportDate) {
|
|
|
11
13
|
return `stats:fallback:errorStats:count:day:${reportDate}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
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
16
|
export default {
|
|
25
17
|
name: "重置统计回退计数",
|
|
26
18
|
method: "POST",
|
|
@@ -40,11 +32,11 @@ export default {
|
|
|
40
32
|
const errorDailyKey = getErrorStatsFallbackDailyCountKey(reportDate);
|
|
41
33
|
const resetKeys = [INFO_STATS_FALLBACK_COUNT_KEY, ERROR_STATS_FALLBACK_COUNT_KEY, infoDailyKey, errorDailyKey];
|
|
42
34
|
const before = {
|
|
43
|
-
infoStats:
|
|
44
|
-
errorStats:
|
|
35
|
+
infoStats: getTongJiNumber(await befly.redis.getString(INFO_STATS_FALLBACK_COUNT_KEY)),
|
|
36
|
+
errorStats: getTongJiNumber(await befly.redis.getString(ERROR_STATS_FALLBACK_COUNT_KEY)),
|
|
45
37
|
today: {
|
|
46
|
-
infoStats:
|
|
47
|
-
errorStats:
|
|
38
|
+
infoStats: getTongJiNumber(await befly.redis.getString(infoDailyKey)),
|
|
39
|
+
errorStats: getTongJiNumber(await befly.redis.getString(errorDailyKey))
|
|
48
40
|
}
|
|
49
41
|
};
|
|
50
42
|
|
|
@@ -53,7 +45,7 @@ export default {
|
|
|
53
45
|
return befly.tool.Yes("重置成功", {
|
|
54
46
|
reset: true,
|
|
55
47
|
reportDate: reportDate,
|
|
56
|
-
removedCount:
|
|
48
|
+
removedCount: getTongJiNumber(removedCount),
|
|
57
49
|
keys: resetKeys,
|
|
58
50
|
before: before,
|
|
59
51
|
after: {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { UAParser } from "ua-parser-js";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import infoReportTable from "#root/tables/infoReport.json";
|
|
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
|
+
|
|
6
9
|
const INFO_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
|
|
7
10
|
const INFO_STATS_FIELDS = [
|
|
8
11
|
{ key: "sources", valueKey: "source" },
|
|
@@ -30,20 +33,6 @@ function getInfoStatsMember(ctx) {
|
|
|
30
33
|
return `ip:${ctx.ip || "unknown"}`;
|
|
31
34
|
}
|
|
32
35
|
|
|
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
36
|
function getInfoStatsNamesKey(periodType, periodValue, fieldKey) {
|
|
48
37
|
return `info:${periodType}:${periodValue}:${fieldKey}:names`;
|
|
49
38
|
}
|
|
@@ -83,8 +72,8 @@ async function updateInfoStatsRedis(befly, now, reportDate, statData) {
|
|
|
83
72
|
return;
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
const weekStartDate =
|
|
87
|
-
const monthStartDate =
|
|
75
|
+
const weekStartDate = getTongJiWeekStartDate(now);
|
|
76
|
+
const monthStartDate = getTongJiMonthStartDate(now);
|
|
88
77
|
|
|
89
78
|
await updateInfoStatsPeriodRedis(befly, "day", reportDate, statData, now);
|
|
90
79
|
await updateInfoStatsPeriodRedis(befly, "week", weekStartDate, statData, now);
|
|
@@ -97,13 +86,13 @@ export default {
|
|
|
97
86
|
body: "none",
|
|
98
87
|
auth: false,
|
|
99
88
|
fields: {
|
|
100
|
-
pagePath:
|
|
101
|
-
pageName:
|
|
102
|
-
source:
|
|
103
|
-
productName:
|
|
104
|
-
productCode:
|
|
105
|
-
productVersion:
|
|
106
|
-
detail:
|
|
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
|
|
107
96
|
},
|
|
108
97
|
required: [],
|
|
109
98
|
handler: async (befly, ctx) => {
|
|
@@ -111,7 +100,6 @@ export default {
|
|
|
111
100
|
const reportDate = getDateYmdNumber(now);
|
|
112
101
|
const member = getInfoStatsMember(ctx);
|
|
113
102
|
const tableReady = (await befly.mysql.tableExists("beflyInfoReport")).data === true;
|
|
114
|
-
const body = ctx.body || {};
|
|
115
103
|
let userAgent = "";
|
|
116
104
|
|
|
117
105
|
if (!tableReady) {
|
|
@@ -149,12 +137,12 @@ export default {
|
|
|
149
137
|
}
|
|
150
138
|
|
|
151
139
|
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 || "",
|
|
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 || "",
|
|
158
146
|
deviceType: uaResult.device.type || "desktop",
|
|
159
147
|
browserName: uaResult.browser.name || "",
|
|
160
148
|
browserVersion: uaResult.browser.version || "",
|
|
@@ -178,7 +166,7 @@ export default {
|
|
|
178
166
|
productVersion: statData.productVersion,
|
|
179
167
|
pagePath: statData.pagePath,
|
|
180
168
|
pageName: statData.pageName,
|
|
181
|
-
detail: body.detail || "",
|
|
169
|
+
detail: ctx.body.detail || "",
|
|
182
170
|
userAgent: userAgent,
|
|
183
171
|
deviceType: statData.deviceType,
|
|
184
172
|
browserName: statData.browserName,
|
package/apis/tongJi/infoStats.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDateYmdNumber } from "#root/utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
import { getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
2
4
|
|
|
3
5
|
const INFO_STATS_FIELDS = [
|
|
4
6
|
{ dbField: "source", key: "sources" },
|
|
@@ -25,20 +27,6 @@ function getInfoStatsFallbackDailyCountKey(reportDate) {
|
|
|
25
27
|
return `${INFO_STATS_FALLBACK_DAILY_COUNT_KEY_PREFIX}${reportDate}`;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function getInfoStatsWeekStartDate(timestamp = Date.now()) {
|
|
29
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
30
|
-
const day = date.getDay();
|
|
31
|
-
const offset = day === 0 ? -6 : 1 - day;
|
|
32
|
-
|
|
33
|
-
return getDateYmdNumber(addDays(timestamp, offset));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getInfoStatsMonthStartDate(timestamp = Date.now()) {
|
|
37
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
38
|
-
|
|
39
|
-
return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
function formatInfoStatsList(rows = []) {
|
|
43
31
|
const list = [];
|
|
44
32
|
|
|
@@ -211,8 +199,8 @@ export default {
|
|
|
211
199
|
handler: async (befly, ctx) => {
|
|
212
200
|
const now = Date.now();
|
|
213
201
|
const reportDate = getDateYmdNumber(now);
|
|
214
|
-
const weekStartDate =
|
|
215
|
-
const monthStartDate =
|
|
202
|
+
const weekStartDate = getTongJiWeekStartDate(now);
|
|
203
|
+
const monthStartDate = getTongJiMonthStartDate(now);
|
|
216
204
|
const productName = String(ctx?.body?.productName || "").trim();
|
|
217
205
|
const hasProductFilter = productName.length > 0;
|
|
218
206
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDateYmdNumber } from "#root/utils/datetime.js";
|
|
2
2
|
import { isValidPositiveInt } from "#root/utils/is.js";
|
|
3
3
|
|
|
4
|
+
import { expireTongJiRedisKeys, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
5
|
+
|
|
4
6
|
const ONLINE_STATS_ONLINE_TTL_SECONDS = 10 * 60;
|
|
5
7
|
const ONLINE_STATS_REDIS_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
6
8
|
const ONLINE_STATS_TEMP_TTL_SECONDS = 45 * 24 * 60 * 60;
|
|
@@ -11,62 +13,42 @@ function getOnlineStatsMember(ctx) {
|
|
|
11
13
|
return `user:${ctx.userId}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getOnlineStatsWeekStartDate(timestamp = Date.now()) {
|
|
18
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
19
|
-
const day = date.getDay();
|
|
20
|
-
const offset = day === 0 ? -6 : 1 - day;
|
|
21
|
-
|
|
22
|
-
return getDateYmdNumber(addDays(timestamp, offset));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getOnlineStatsMonthStartDate(timestamp = Date.now()) {
|
|
26
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
16
|
+
const clientId = String(ctx.body.clientId || "").trim();
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getPeriodKeys(periodType, periodValue) {
|
|
32
|
-
return {
|
|
33
|
-
pv: `online:${periodType}:${periodValue}:pv`,
|
|
34
|
-
members: `online:${periodType}:${periodValue}:members`,
|
|
35
|
-
reportTime: `online:${periodType}:${periodValue}:reportTime`
|
|
36
|
-
};
|
|
37
|
-
}
|
|
18
|
+
if (clientId) {
|
|
19
|
+
return `client:${clientId}`;
|
|
20
|
+
}
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
return encodeURIComponent(String(productName || ""));
|
|
22
|
+
return `ip:${ctx.ip || "unknown"}`;
|
|
41
23
|
}
|
|
42
24
|
|
|
43
|
-
function
|
|
44
|
-
const
|
|
25
|
+
async function updateOnlineStatsActiveMemberProduct(befly, member, productName, expireAt) {
|
|
26
|
+
const normalizedProductName = String(productName || "").trim();
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
reportTime: `online:${periodType}:${periodValue}:product:${productKey}:reportTime`
|
|
50
|
-
};
|
|
51
|
-
}
|
|
28
|
+
if (!normalizedProductName) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
52
31
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
32
|
+
const memberProductKey = `online:active:member:${encodeURIComponent(member)}:product`;
|
|
33
|
+
const previousProductName = String((await befly.redis.getString(memberProductKey)) || "").trim();
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await befly.redis.expire(key, ttl);
|
|
35
|
+
if (previousProductName && previousProductName !== normalizedProductName) {
|
|
36
|
+
await befly.redis.zrem(`online:active:product:${encodeURIComponent(previousProductName)}`, [member]);
|
|
60
37
|
}
|
|
61
|
-
}
|
|
62
38
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await befly.redis.
|
|
66
|
-
|
|
39
|
+
const activeProductKey = `online:active:product:${encodeURIComponent(normalizedProductName)}`;
|
|
40
|
+
|
|
41
|
+
await befly.redis.setString(memberProductKey, normalizedProductName, ONLINE_STATS_ONLINE_TTL_SECONDS);
|
|
42
|
+
await befly.redis.zadd(activeProductKey, [
|
|
43
|
+
{
|
|
44
|
+
score: expireAt,
|
|
45
|
+
member: member
|
|
46
|
+
}
|
|
47
|
+
]);
|
|
48
|
+
await befly.redis.expire(activeProductKey, ONLINE_STATS_REDIS_TTL_SECONDS);
|
|
67
49
|
}
|
|
68
50
|
|
|
69
|
-
async function updateOnlineStatsActiveMember(befly, member, now) {
|
|
51
|
+
async function updateOnlineStatsActiveMember(befly, member, now, productName) {
|
|
70
52
|
const expireAt = now + ONLINE_STATS_ONLINE_TTL_SECONDS * 1000;
|
|
71
53
|
|
|
72
54
|
await befly.redis.zadd(ONLINE_STATS_ACTIVE_KEY, [
|
|
@@ -75,16 +57,21 @@ async function updateOnlineStatsActiveMember(befly, member, now) {
|
|
|
75
57
|
member: member
|
|
76
58
|
}
|
|
77
59
|
]);
|
|
60
|
+
await updateOnlineStatsActiveMemberProduct(befly, member, productName, expireAt);
|
|
78
61
|
await befly.redis.expire(ONLINE_STATS_ACTIVE_KEY, ONLINE_STATS_REDIS_TTL_SECONDS);
|
|
79
62
|
}
|
|
80
63
|
|
|
81
64
|
async function updateOnlineStatsPeriod(befly, periodType, periodValue, member) {
|
|
82
|
-
const keys =
|
|
65
|
+
const keys = {
|
|
66
|
+
pv: `online:${periodType}:${periodValue}:pv`,
|
|
67
|
+
members: `online:${periodType}:${periodValue}:members`,
|
|
68
|
+
reportTime: `online:${periodType}:${periodValue}:reportTime`
|
|
69
|
+
};
|
|
83
70
|
|
|
84
71
|
await befly.redis.incr(keys.pv);
|
|
85
72
|
await befly.redis.sadd(keys.members, [member]);
|
|
86
73
|
await befly.redis.setString(keys.reportTime, String(Date.now()), ONLINE_STATS_TEMP_TTL_SECONDS);
|
|
87
|
-
await
|
|
74
|
+
await expireTongJiRedisKeys(befly, Object.values(keys), ONLINE_STATS_TEMP_TTL_SECONDS);
|
|
88
75
|
}
|
|
89
76
|
|
|
90
77
|
async function updateOnlineStatsProductPeriod(befly, periodType, periodValue, member, productName) {
|
|
@@ -92,14 +79,19 @@ async function updateOnlineStatsProductPeriod(befly, periodType, periodValue, me
|
|
|
92
79
|
return;
|
|
93
80
|
}
|
|
94
81
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
82
|
+
const productKey = encodeURIComponent(productName);
|
|
83
|
+
const keys = {
|
|
84
|
+
pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
|
|
85
|
+
members: `online:${periodType}:${periodValue}:product:${productKey}:members`,
|
|
86
|
+
reportTime: `online:${periodType}:${periodValue}:product:${productKey}:reportTime`
|
|
87
|
+
};
|
|
88
|
+
const productsKey = `online:${periodType}:${periodValue}:products`;
|
|
97
89
|
|
|
98
90
|
await befly.redis.incr(keys.pv);
|
|
99
91
|
await befly.redis.sadd(keys.members, [member]);
|
|
100
92
|
await befly.redis.setString(keys.reportTime, String(Date.now()), ONLINE_STATS_TEMP_TTL_SECONDS);
|
|
101
93
|
await befly.redis.sadd(productsKey, [productName]);
|
|
102
|
-
await
|
|
94
|
+
await expireTongJiRedisKeys(befly, [...Object.values(keys), productsKey], ONLINE_STATS_TEMP_TTL_SECONDS);
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
export default {
|
|
@@ -111,6 +103,7 @@ export default {
|
|
|
111
103
|
pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
|
|
112
104
|
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
113
105
|
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
106
|
+
clientId: { name: "客户端标识", input: "string", min: 0, max: 120 },
|
|
114
107
|
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
115
108
|
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
116
109
|
productVersion: { name: "产品版本", input: "string", min: 0, max: 50 }
|
|
@@ -119,14 +112,15 @@ export default {
|
|
|
119
112
|
handler: async (befly, ctx) => {
|
|
120
113
|
const now = Date.now();
|
|
121
114
|
const reportDate = getDateYmdNumber(now);
|
|
122
|
-
const weekStartDate =
|
|
123
|
-
const monthStartDate =
|
|
115
|
+
const weekStartDate = getTongJiWeekStartDate(now);
|
|
116
|
+
const monthStartDate = getTongJiMonthStartDate(now);
|
|
124
117
|
const member = getOnlineStatsMember(ctx);
|
|
125
|
-
const productName = ctx.body
|
|
118
|
+
const productName = String(ctx.body.productName || "").trim();
|
|
126
119
|
|
|
127
|
-
await updateOnlineStatsActiveMember(befly, member, now);
|
|
120
|
+
await updateOnlineStatsActiveMember(befly, member, now, productName);
|
|
128
121
|
|
|
129
|
-
|
|
122
|
+
await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
|
|
123
|
+
const onlineCount = await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
|
|
130
124
|
|
|
131
125
|
await updateOnlineStatsPeriod(befly, "day", reportDate, member);
|
|
132
126
|
await updateOnlineStatsPeriod(befly, "week", weekStartDate, member);
|
|
@@ -1,42 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDateYmdNumber } from "#root/utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
import { getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
2
4
|
|
|
3
5
|
const ONLINE_STATS_DAY_LIMIT = 30;
|
|
4
6
|
const ONLINE_STATS_ACTIVE_KEY = "online:active";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const num = Number(value);
|
|
8
|
-
|
|
9
|
-
if (!Number.isFinite(num)) {
|
|
10
|
-
return 0;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return num;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getOnlineStatsWeekStartDate(timestamp = Date.now()) {
|
|
17
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
18
|
-
const day = date.getDay();
|
|
19
|
-
const offset = day === 0 ? -6 : 1 - day;
|
|
20
|
-
|
|
21
|
-
return getDateYmdNumber(addDays(timestamp, offset));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getOnlineStatsMonthStartDate(timestamp = Date.now()) {
|
|
25
|
-
const date = Reflect.construct(Date, [timestamp]);
|
|
26
|
-
|
|
27
|
-
return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function getOnlineStatsRecentDateList(now = Date.now(), limit = ONLINE_STATS_DAY_LIMIT) {
|
|
31
|
-
const list = [];
|
|
32
|
-
|
|
33
|
-
for (let i = limit - 1; i >= 0; i--) {
|
|
34
|
-
list.push(getDateYmdNumber(addDays(now, -i)));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return list;
|
|
38
|
-
}
|
|
39
|
-
|
|
8
|
+
// 构造全量统计的 Redis 键。
|
|
40
9
|
function getPeriodKeys(periodType, periodValue) {
|
|
41
10
|
return {
|
|
42
11
|
pv: `online:${periodType}:${periodValue}:pv`,
|
|
@@ -45,12 +14,9 @@ function getPeriodKeys(periodType, periodValue) {
|
|
|
45
14
|
};
|
|
46
15
|
}
|
|
47
16
|
|
|
48
|
-
|
|
49
|
-
return encodeURIComponent(String(productName || ""));
|
|
50
|
-
}
|
|
51
|
-
|
|
17
|
+
// 构造单产品统计的 Redis 键。
|
|
52
18
|
function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
|
|
53
|
-
const productKey =
|
|
19
|
+
const productKey = encodeURIComponent(productName);
|
|
54
20
|
|
|
55
21
|
return {
|
|
56
22
|
pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
|
|
@@ -59,78 +25,84 @@ function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
|
|
|
59
25
|
};
|
|
60
26
|
}
|
|
61
27
|
|
|
62
|
-
|
|
63
|
-
|
|
28
|
+
// 构造单产品实时在线集合的 Redis 键。
|
|
29
|
+
function getOnlineStatsActiveProductKey(productName) {
|
|
30
|
+
const productKey = encodeURIComponent(productName);
|
|
31
|
+
|
|
32
|
+
return `online:active:product:${productKey}`;
|
|
64
33
|
}
|
|
65
34
|
|
|
35
|
+
// 读取 Redis 字符串并转成统计数字。
|
|
66
36
|
async function getRedisNumber(befly, key) {
|
|
67
|
-
return
|
|
37
|
+
return getTongJiNumber(await befly.redis.getString(key));
|
|
68
38
|
}
|
|
69
39
|
|
|
40
|
+
// 读取某个周期的全量统计数据。
|
|
70
41
|
async function getOnlineStatsPeriodData(befly, periodType, periodValue) {
|
|
71
42
|
const keys = getPeriodKeys(periodType, periodValue);
|
|
72
43
|
return {
|
|
73
44
|
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
74
45
|
pv: await getRedisNumber(befly, keys.pv),
|
|
75
|
-
uv:
|
|
46
|
+
uv: getTongJiNumber(await befly.redis.scard(keys.members))
|
|
76
47
|
};
|
|
77
48
|
}
|
|
78
49
|
|
|
79
|
-
|
|
80
|
-
if (!productName) {
|
|
81
|
-
return await getOnlineStatsPeriodData(befly, periodType, periodValue);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return await getOnlineStatsProductPeriodData(befly, periodType, periodValue, productName);
|
|
85
|
-
}
|
|
86
|
-
|
|
50
|
+
// 读取某个周期的单产品统计数据。
|
|
87
51
|
async function getOnlineStatsProductPeriodData(befly, periodType, periodValue, productName) {
|
|
88
52
|
const keys = getOnlineStatsProductPeriodKeys(periodType, periodValue, productName);
|
|
89
53
|
|
|
90
54
|
return {
|
|
91
55
|
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
92
56
|
pv: await getRedisNumber(befly, keys.pv),
|
|
93
|
-
uv:
|
|
57
|
+
uv: getTongJiNumber(await befly.redis.scard(keys.members))
|
|
94
58
|
};
|
|
95
59
|
}
|
|
96
60
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const item of recentDateList) {
|
|
101
|
-
for (const productName of await befly.redis.smembers(getOnlineStatsProductsKey("day", item))) {
|
|
102
|
-
if (!productName) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
productSet.add(String(productName));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
61
|
+
// 清理过期成员后返回全站实时在线人数。
|
|
62
|
+
async function getOnlineStatsTotalOnlineCount(befly) {
|
|
63
|
+
const now = Date.now();
|
|
109
64
|
|
|
110
|
-
|
|
65
|
+
await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
|
|
66
|
+
return await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
|
|
111
67
|
}
|
|
112
68
|
|
|
113
|
-
|
|
69
|
+
// 清理过期成员后返回单产品实时在线人数。
|
|
70
|
+
async function getOnlineStatsProductOnlineCount(befly, productName) {
|
|
114
71
|
const now = Date.now();
|
|
115
|
-
|
|
116
|
-
|
|
72
|
+
const productKey = getOnlineStatsActiveProductKey(productName);
|
|
73
|
+
|
|
74
|
+
await befly.redis.zremrangebyscore(productKey, "-inf", now);
|
|
75
|
+
return await befly.redis.zcard(productKey);
|
|
117
76
|
}
|
|
118
77
|
|
|
119
|
-
|
|
120
|
-
|
|
78
|
+
// 构造全量统计的按天趋势列表。
|
|
79
|
+
async function buildOnlineStatsDays(befly, recentDateList) {
|
|
80
|
+
const days = [];
|
|
81
|
+
|
|
82
|
+
for (const item of recentDateList) {
|
|
83
|
+
const dayData = await getOnlineStatsPeriodData(befly, "day", item);
|
|
84
|
+
|
|
85
|
+
if (dayData.reportTime <= 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
121
88
|
|
|
122
|
-
|
|
123
|
-
|
|
89
|
+
days.push({
|
|
90
|
+
reportDate: item,
|
|
91
|
+
reportTime: dayData.reportTime,
|
|
92
|
+
pv: dayData.pv,
|
|
93
|
+
uv: dayData.uv
|
|
94
|
+
});
|
|
124
95
|
}
|
|
125
96
|
|
|
126
|
-
return
|
|
97
|
+
return days;
|
|
127
98
|
}
|
|
128
99
|
|
|
129
|
-
|
|
100
|
+
// 构造单产品统计的按天趋势列表。
|
|
101
|
+
async function buildOnlineStatsProductDays(befly, recentDateList, productName) {
|
|
130
102
|
const days = [];
|
|
131
103
|
|
|
132
104
|
for (const item of recentDateList) {
|
|
133
|
-
const dayData = await
|
|
105
|
+
const dayData = await getOnlineStatsProductPeriodData(befly, "day", item, productName);
|
|
134
106
|
|
|
135
107
|
if (dayData.reportTime <= 0) {
|
|
136
108
|
continue;
|
|
@@ -147,34 +119,29 @@ async function buildOnlineStatsDays(befly, recentDateList, productName) {
|
|
|
147
119
|
return days;
|
|
148
120
|
}
|
|
149
121
|
|
|
150
|
-
|
|
122
|
+
// 按配置项目列表构造项目统计,并按近 30 天 PV 排序。
|
|
123
|
+
async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists) {
|
|
151
124
|
const products = [];
|
|
152
125
|
|
|
153
|
-
for (const
|
|
154
|
-
const productDays =
|
|
126
|
+
for (const item of projectLists) {
|
|
127
|
+
const productDays = await buildOnlineStatsProductDays(befly, recentDateList, item.productName);
|
|
128
|
+
let totalPv = 0;
|
|
155
129
|
|
|
156
|
-
for (const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (dayData.reportTime <= 0) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
productDays.push({
|
|
164
|
-
reportDate: item,
|
|
165
|
-
reportTime: dayData.reportTime,
|
|
166
|
-
pv: dayData.pv,
|
|
167
|
-
uv: dayData.uv
|
|
168
|
-
});
|
|
130
|
+
for (const productDay of productDays) {
|
|
131
|
+
totalPv += productDay.pv;
|
|
169
132
|
}
|
|
170
133
|
|
|
171
|
-
const productToday = await getOnlineStatsProductPeriodData(befly, "day", reportDate,
|
|
172
|
-
const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate,
|
|
173
|
-
const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate,
|
|
134
|
+
const productToday = await getOnlineStatsProductPeriodData(befly, "day", reportDate, item.productName);
|
|
135
|
+
const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, item.productName);
|
|
136
|
+
const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, item.productName);
|
|
137
|
+
const productOnlineCount = await getOnlineStatsProductOnlineCount(befly, item.productName);
|
|
174
138
|
|
|
175
139
|
products.push({
|
|
176
|
-
key:
|
|
177
|
-
productName:
|
|
140
|
+
key: item.productCode,
|
|
141
|
+
productName: item.productName,
|
|
142
|
+
productCode: item.productCode,
|
|
143
|
+
productVersion: item.productVersion || "",
|
|
144
|
+
onlineCount: productOnlineCount,
|
|
178
145
|
today: {
|
|
179
146
|
pv: productToday.pv,
|
|
180
147
|
uv: productToday.uv
|
|
@@ -188,18 +155,23 @@ async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekS
|
|
|
188
155
|
uv: productMonth.uv
|
|
189
156
|
},
|
|
190
157
|
days: productDays,
|
|
191
|
-
totalPv:
|
|
192
|
-
totalUv: sumProductTrendField(productDays, "uv")
|
|
158
|
+
totalPv: totalPv
|
|
193
159
|
});
|
|
194
160
|
}
|
|
195
161
|
|
|
196
|
-
|
|
162
|
+
const sortedProducts = products.toSorted((a, b) => {
|
|
197
163
|
if (b.totalPv !== a.totalPv) {
|
|
198
164
|
return b.totalPv - a.totalPv;
|
|
199
165
|
}
|
|
200
166
|
|
|
201
167
|
return String(a.productName).localeCompare(String(b.productName), "zh-CN");
|
|
202
168
|
});
|
|
169
|
+
|
|
170
|
+
for (const item of sortedProducts) {
|
|
171
|
+
delete item.totalPv;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return sortedProducts;
|
|
203
175
|
}
|
|
204
176
|
|
|
205
177
|
export default {
|
|
@@ -211,22 +183,36 @@ export default {
|
|
|
211
183
|
productName: { name: "产品名称", input: "string", min: 0, max: 100 }
|
|
212
184
|
},
|
|
213
185
|
required: [],
|
|
186
|
+
// 汇总当前查询范围的在线统计,并在未筛选产品时附带配置项目统计。
|
|
214
187
|
handler: async (befly, ctx) => {
|
|
215
188
|
const now = Date.now();
|
|
216
189
|
const reportDate = getDateYmdNumber(now);
|
|
217
|
-
const weekStartDate =
|
|
218
|
-
const monthStartDate =
|
|
219
|
-
const recentDateList =
|
|
190
|
+
const weekStartDate = getTongJiWeekStartDate(now);
|
|
191
|
+
const monthStartDate = getTongJiMonthStartDate(now);
|
|
192
|
+
const recentDateList = getTongJiRecentDateList(now, ONLINE_STATS_DAY_LIMIT);
|
|
220
193
|
const productName = String(ctx?.body?.productName || "").trim();
|
|
221
194
|
const hasProductFilter = productName.length > 0;
|
|
222
|
-
const days = await
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
195
|
+
const days = hasProductFilter ? await buildOnlineStatsProductDays(befly, recentDateList, productName) : await buildOnlineStatsDays(befly, recentDateList);
|
|
196
|
+
|
|
197
|
+
let currentDay;
|
|
198
|
+
let weekCurrent;
|
|
199
|
+
let monthCurrent;
|
|
200
|
+
let onlineCount;
|
|
201
|
+
|
|
202
|
+
if (hasProductFilter) {
|
|
203
|
+
currentDay = await getOnlineStatsProductPeriodData(befly, "day", reportDate, productName);
|
|
204
|
+
weekCurrent = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, productName);
|
|
205
|
+
monthCurrent = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, productName);
|
|
206
|
+
onlineCount = await getOnlineStatsProductOnlineCount(befly, productName);
|
|
207
|
+
} else {
|
|
208
|
+
currentDay = await getOnlineStatsPeriodData(befly, "day", reportDate);
|
|
209
|
+
weekCurrent = await getOnlineStatsPeriodData(befly, "week", weekStartDate);
|
|
210
|
+
monthCurrent = await getOnlineStatsPeriodData(befly, "month", monthStartDate);
|
|
211
|
+
onlineCount = await getOnlineStatsTotalOnlineCount(befly);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const projectLists = hasProductFilter ? [] : befly.config.projectLists;
|
|
215
|
+
const products = await buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists);
|
|
230
216
|
|
|
231
217
|
return befly.tool.Yes("获取成功", {
|
|
232
218
|
queryTime: now,
|