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.
Files changed (68) 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/dict/_dict.js +24 -0
  9. package/apis/dict/all.js +6 -4
  10. package/apis/dict/detail.js +9 -5
  11. package/apis/dict/insert.js +4 -11
  12. package/apis/dict/items.js +5 -6
  13. package/apis/dict/select.js +13 -9
  14. package/apis/dict/update.js +9 -13
  15. package/apis/dictType/select.js +4 -4
  16. package/apis/email/config.js +9 -11
  17. package/apis/email/logList.js +14 -4
  18. package/apis/email/send.js +10 -3
  19. package/apis/email/verify.js +9 -7
  20. package/apis/loginLog/select.js +11 -4
  21. package/apis/menu/all.js +13 -19
  22. package/apis/menu/select.js +19 -14
  23. package/apis/operateLog/select.js +13 -4
  24. package/apis/role/_role.js +21 -0
  25. package/apis/role/all.js +1 -3
  26. package/apis/role/apiSave.js +8 -15
  27. package/apis/role/apis.js +4 -10
  28. package/apis/role/delete.js +28 -36
  29. package/apis/role/detail.js +4 -10
  30. package/apis/role/insert.js +12 -10
  31. package/apis/role/menuSave.js +9 -15
  32. package/apis/role/menus.js +4 -10
  33. package/apis/role/save.js +19 -23
  34. package/apis/role/select.js +12 -9
  35. package/apis/role/update.js +14 -15
  36. package/apis/source/imageList.js +3 -3
  37. package/apis/sysConfig/get.js +11 -23
  38. package/apis/sysConfig/insert.js +22 -27
  39. package/apis/sysConfig/select.js +11 -5
  40. package/apis/sysConfig/update.js +33 -38
  41. package/apis/tongJi/_tongJi.js +41 -0
  42. package/apis/tongJi/cacheHealth.js +8 -30
  43. package/apis/tongJi/errorList.js +26 -27
  44. package/apis/tongJi/errorReport.js +26 -43
  45. package/apis/tongJi/errorStats.js +17 -48
  46. package/apis/tongJi/fallbackReset.js +7 -15
  47. package/apis/tongJi/infoReport.js +20 -32
  48. package/apis/tongJi/infoStats.js +5 -17
  49. package/apis/tongJi/onlineReport.js +50 -56
  50. package/apis/tongJi/onlineStats.js +97 -111
  51. package/checks/config.js +44 -1
  52. package/configs/beflyConfig.json +10 -1
  53. package/index.js +25 -0
  54. package/lib/dbHelper.js +1 -1
  55. package/lib/dbParse.js +61 -99
  56. package/lib/dbUtil.js +101 -21
  57. package/lib/redisHelper.js +25 -0
  58. package/lib/sqlBuilder.js +6 -0
  59. package/package.json +1 -1
  60. package/plugins/email.js +3 -6
  61. package/router/api.js +0 -7
  62. package/utils/email.js +3 -0
  63. package/apis/admin/cacheRefresh.js +0 -122
  64. package/apis/dashboard/configStatus.js +0 -39
  65. package/apis/dashboard/performanceMetrics.js +0 -23
  66. package/apis/dashboard/permissionStats.js +0 -27
  67. package/apis/dashboard/systemInfo.js +0 -19
  68. 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: toNumber(await befly.redis.getString(INFO_STATS_FALLBACK_COUNT_KEY)),
44
- errorStats: toNumber(await befly.redis.getString(ERROR_STATS_FALLBACK_COUNT_KEY)),
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: toNumber(await befly.redis.getString(infoDailyKey)),
47
- errorStats: toNumber(await befly.redis.getString(errorDailyKey))
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: toNumber(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 { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
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 = getInfoStatsWeekStartDate(now);
87
- const monthStartDate = getInfoStatsMonthStartDate(now);
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: { name: "页面路径", input: "string", min: 0, max: 200 },
101
- pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
102
- source: { name: "来源", input: "string", min: 0, max: 50 },
103
- productName: { name: "产品名称", input: "string", min: 0, max: 100 },
104
- productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
105
- productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
106
- 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
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,
@@ -1,4 +1,6 @@
1
- import { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
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 = getInfoStatsWeekStartDate(now);
215
- const monthStartDate = getInfoStatsMonthStartDate(now);
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 { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
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
- return `ip:${ctx.ip || "unknown"}`;
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
- return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
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
- function getOnlineStatsProductKey(productName) {
40
- return encodeURIComponent(String(productName || ""));
22
+ return `ip:${ctx.ip || "unknown"}`;
41
23
  }
42
24
 
43
- function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
44
- const productKey = getOnlineStatsProductKey(productName);
25
+ async function updateOnlineStatsActiveMemberProduct(befly, member, productName, expireAt) {
26
+ const normalizedProductName = String(productName || "").trim();
45
27
 
46
- return {
47
- pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
48
- members: `online:${periodType}:${periodValue}:product:${productKey}:members`,
49
- reportTime: `online:${periodType}:${periodValue}:product:${productKey}:reportTime`
50
- };
51
- }
28
+ if (!normalizedProductName) {
29
+ return;
30
+ }
52
31
 
53
- function getOnlineStatsProductsKey(periodType, periodValue) {
54
- return `online:${periodType}:${periodValue}:products`;
55
- }
32
+ const memberProductKey = `online:active:member:${encodeURIComponent(member)}:product`;
33
+ const previousProductName = String((await befly.redis.getString(memberProductKey)) || "").trim();
56
34
 
57
- async function touchRedisKeys(befly, keys, ttl) {
58
- for (const key of keys) {
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
- async function getOnlineStatsOnlineCount(befly) {
64
- const now = Date.now();
65
- await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
66
- return await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
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 = getPeriodKeys(periodType, periodValue);
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 touchRedisKeys(befly, Object.values(keys), ONLINE_STATS_TEMP_TTL_SECONDS);
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 keys = getOnlineStatsProductPeriodKeys(periodType, periodValue, productName);
96
- const productsKey = getOnlineStatsProductsKey(periodType, periodValue);
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 touchRedisKeys(befly, [...Object.values(keys), productsKey], ONLINE_STATS_TEMP_TTL_SECONDS);
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 = getOnlineStatsWeekStartDate(now);
123
- const monthStartDate = getOnlineStatsMonthStartDate(now);
115
+ const weekStartDate = getTongJiWeekStartDate(now);
116
+ const monthStartDate = getTongJiMonthStartDate(now);
124
117
  const member = getOnlineStatsMember(ctx);
125
- const productName = ctx.body?.productName || "";
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
- const onlineCount = await getOnlineStatsOnlineCount(befly);
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 { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
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
- function toOnlineStatsNumber(value) {
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
- function getOnlineStatsProductKey(productName) {
49
- return encodeURIComponent(String(productName || ""));
50
- }
51
-
17
+ // 构造单产品统计的 Redis 键。
52
18
  function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
53
- const productKey = getOnlineStatsProductKey(productName);
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
- function getOnlineStatsProductsKey(periodType, periodValue) {
63
- return `online:${periodType}:${periodValue}:products`;
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 toOnlineStatsNumber(await befly.redis.getString(key));
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: toOnlineStatsNumber(await befly.redis.scard(keys.members))
46
+ uv: getTongJiNumber(await befly.redis.scard(keys.members))
76
47
  };
77
48
  }
78
49
 
79
- async function getOnlineStatsPeriodDataByProduct(befly, periodType, periodValue, productName) {
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: toOnlineStatsNumber(await befly.redis.scard(keys.members))
57
+ uv: getTongJiNumber(await befly.redis.scard(keys.members))
94
58
  };
95
59
  }
96
60
 
97
- async function getOnlineStatsProductNames(befly, recentDateList) {
98
- const productSet = new Set();
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
- return Array.from(productSet.values());
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
- async function getOnlineStatsOnlineCount(befly) {
69
+ // 清理过期成员后返回单产品实时在线人数。
70
+ async function getOnlineStatsProductOnlineCount(befly, productName) {
114
71
  const now = Date.now();
115
- await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
116
- return await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
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
- function sumProductTrendField(list, field) {
120
- let total = 0;
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
- for (const item of list) {
123
- total += toOnlineStatsNumber(item?.[field]);
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 total;
97
+ return days;
127
98
  }
128
99
 
129
- async function buildOnlineStatsDays(befly, recentDateList, productName) {
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 getOnlineStatsPeriodDataByProduct(befly, "day", item, productName);
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
- async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, productNames) {
122
+ // 按配置项目列表构造项目统计,并按近 30 PV 排序。
123
+ async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists) {
151
124
  const products = [];
152
125
 
153
- for (const productItemName of productNames) {
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 item of recentDateList) {
157
- const dayData = await getOnlineStatsProductPeriodData(befly, "day", item, productItemName);
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, productItemName);
172
- const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, productItemName);
173
- const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, productItemName);
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: productItemName,
177
- productName: productItemName,
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: sumProductTrendField(productDays, "pv"),
192
- totalUv: sumProductTrendField(productDays, "uv")
158
+ totalPv: totalPv
193
159
  });
194
160
  }
195
161
 
196
- return products.toSorted((a, b) => {
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 = getOnlineStatsWeekStartDate(now);
218
- const monthStartDate = getOnlineStatsMonthStartDate(now);
219
- const recentDateList = getOnlineStatsRecentDateList(now, ONLINE_STATS_DAY_LIMIT);
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 buildOnlineStatsDays(befly, recentDateList, productName);
223
-
224
- const currentDay = await getOnlineStatsPeriodDataByProduct(befly, "day", reportDate, productName);
225
- const weekCurrent = await getOnlineStatsPeriodDataByProduct(befly, "week", weekStartDate, productName);
226
- const monthCurrent = await getOnlineStatsPeriodDataByProduct(befly, "month", monthStartDate, productName);
227
- const onlineCount = await getOnlineStatsOnlineCount(befly);
228
- const productNames = hasProductFilter ? [] : await getOnlineStatsProductNames(befly, recentDateList);
229
- const products = await buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, productNames);
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,