befly 3.19.5 → 3.19.7
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/errorList.js +3 -0
- package/apis/tongJi/errorReport.js +2 -1
- package/apis/tongJi/errorStats.js +88 -5
- package/apis/tongJi/infoReport.js +104 -0
- package/apis/tongJi/infoStats.js +147 -0
- package/apis/tongJi/onlineReport.js +154 -0
- package/apis/tongJi/onlineStats.js +255 -0
- package/configs/beflyMenus.json +4 -9
- package/package.json +3 -3
- package/sql/befly.sql +43 -30
- package/apis/tongJi/visitReport.js +0 -241
- package/apis/tongJi/visitStats.js +0 -380
- package/utils/visitStats.js +0 -121
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { addDays, getDateYmdNumber } from "../../utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
const ONLINE_STATS_DAY_LIMIT = 30;
|
|
4
|
+
|
|
5
|
+
function toOnlineStatsNumber(value) {
|
|
6
|
+
const num = Number(value);
|
|
7
|
+
|
|
8
|
+
if (!Number.isFinite(num)) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return num;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getOnlineStatsWeekStartDate(timestamp = Date.now()) {
|
|
16
|
+
const date = Reflect.construct(Date, [timestamp]);
|
|
17
|
+
const day = date.getDay();
|
|
18
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
19
|
+
|
|
20
|
+
return getDateYmdNumber(addDays(timestamp, offset));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getOnlineStatsMonthStartDate(timestamp = Date.now()) {
|
|
24
|
+
const date = Reflect.construct(Date, [timestamp]);
|
|
25
|
+
|
|
26
|
+
return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getOnlineStatsRecentDateList(now = Date.now(), limit = ONLINE_STATS_DAY_LIMIT) {
|
|
30
|
+
const list = [];
|
|
31
|
+
|
|
32
|
+
for (let i = limit - 1; i >= 0; i--) {
|
|
33
|
+
list.push(getDateYmdNumber(addDays(now, -i)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return list;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPeriodKeys(periodType, periodValue) {
|
|
40
|
+
return {
|
|
41
|
+
pv: `online:${periodType}:${periodValue}:pv`,
|
|
42
|
+
members: `online:${periodType}:${periodValue}:members`,
|
|
43
|
+
reportTime: `online:${periodType}:${periodValue}:reportTime`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getOnlineStatsProductKey(productName) {
|
|
48
|
+
return encodeURIComponent(String(productName || ""));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
|
|
52
|
+
const productKey = getOnlineStatsProductKey(productName);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
|
|
56
|
+
members: `online:${periodType}:${periodValue}:product:${productKey}:members`,
|
|
57
|
+
reportTime: `online:${periodType}:${periodValue}:product:${productKey}:reportTime`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getOnlineStatsProductsKey(periodType, periodValue) {
|
|
62
|
+
return `online:${periodType}:${periodValue}:products`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getRedisNumber(befly, key) {
|
|
66
|
+
return toOnlineStatsNumber(await befly.redis.getString(key));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getOnlineStatsPeriodData(befly, periodType, periodValue) {
|
|
70
|
+
const keys = getPeriodKeys(periodType, periodValue);
|
|
71
|
+
return {
|
|
72
|
+
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
73
|
+
pv: await getRedisNumber(befly, keys.pv),
|
|
74
|
+
uv: toOnlineStatsNumber(await befly.redis.scard(keys.members))
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getOnlineStatsProductPeriodData(befly, periodType, periodValue, productName) {
|
|
79
|
+
const keys = getOnlineStatsProductPeriodKeys(periodType, periodValue, productName);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
reportTime: await getRedisNumber(befly, keys.reportTime),
|
|
83
|
+
pv: await getRedisNumber(befly, keys.pv),
|
|
84
|
+
uv: toOnlineStatsNumber(await befly.redis.scard(keys.members))
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getOnlineStatsProductNames(befly, recentDateList) {
|
|
89
|
+
const productSet = new Set();
|
|
90
|
+
|
|
91
|
+
for (const item of recentDateList) {
|
|
92
|
+
for (const productName of await befly.redis.smembers(getOnlineStatsProductsKey("day", item))) {
|
|
93
|
+
if (!productName) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
productSet.add(String(productName));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Array.from(productSet.values());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function getOnlineStatsOnlineCount(befly) {
|
|
105
|
+
const members = await befly.redis.smembers("online:visitors");
|
|
106
|
+
if (members.length === 0) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const existsList = await befly.redis.existsBatch(members.map((member) => `online:visitor:${member}`));
|
|
111
|
+
const expiredMembers = [];
|
|
112
|
+
let onlineCount = 0;
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < members.length; i++) {
|
|
115
|
+
if (existsList[i]) {
|
|
116
|
+
onlineCount += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
expiredMembers.push(members[i]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (expiredMembers.length > 0) {
|
|
124
|
+
await befly.redis.srem("online:visitors", expiredMembers);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return onlineCount;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sumProductTrendField(list, field) {
|
|
131
|
+
let total = 0;
|
|
132
|
+
|
|
133
|
+
for (const item of list) {
|
|
134
|
+
total += toOnlineStatsNumber(item?.[field]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return total;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
name: "获取在线统计",
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: "none",
|
|
144
|
+
auth: true,
|
|
145
|
+
fields: {},
|
|
146
|
+
required: [],
|
|
147
|
+
handler: async (befly) => {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const reportDate = getDateYmdNumber(now);
|
|
150
|
+
const weekStartDate = getOnlineStatsWeekStartDate(now);
|
|
151
|
+
const monthStartDate = getOnlineStatsMonthStartDate(now);
|
|
152
|
+
const recentDateList = getOnlineStatsRecentDateList(now, ONLINE_STATS_DAY_LIMIT);
|
|
153
|
+
const days = [];
|
|
154
|
+
const products = [];
|
|
155
|
+
|
|
156
|
+
for (const item of recentDateList) {
|
|
157
|
+
const dayData = await getOnlineStatsPeriodData(befly, "day", item);
|
|
158
|
+
|
|
159
|
+
if (dayData.reportTime <= 0) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
days.push({
|
|
164
|
+
reportDate: item,
|
|
165
|
+
reportTime: dayData.reportTime,
|
|
166
|
+
pv: dayData.pv,
|
|
167
|
+
uv: dayData.uv
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const currentDay = await getOnlineStatsPeriodData(befly, "day", reportDate);
|
|
172
|
+
const weekCurrent = await getOnlineStatsPeriodData(befly, "week", weekStartDate);
|
|
173
|
+
const monthCurrent = await getOnlineStatsPeriodData(befly, "month", monthStartDate);
|
|
174
|
+
const onlineCount = await getOnlineStatsOnlineCount(befly);
|
|
175
|
+
const productNames = await getOnlineStatsProductNames(befly, recentDateList);
|
|
176
|
+
|
|
177
|
+
for (const productName of productNames) {
|
|
178
|
+
const productDays = [];
|
|
179
|
+
|
|
180
|
+
for (const item of recentDateList) {
|
|
181
|
+
const dayData = await getOnlineStatsProductPeriodData(befly, "day", item, productName);
|
|
182
|
+
|
|
183
|
+
if (dayData.reportTime <= 0) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
productDays.push({
|
|
188
|
+
reportDate: item,
|
|
189
|
+
reportTime: dayData.reportTime,
|
|
190
|
+
pv: dayData.pv,
|
|
191
|
+
uv: dayData.uv
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const productToday = await getOnlineStatsProductPeriodData(befly, "day", reportDate, productName);
|
|
196
|
+
const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, productName);
|
|
197
|
+
const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, productName);
|
|
198
|
+
|
|
199
|
+
products.push({
|
|
200
|
+
key: productName,
|
|
201
|
+
productName: productName,
|
|
202
|
+
today: {
|
|
203
|
+
pv: productToday.pv,
|
|
204
|
+
uv: productToday.uv
|
|
205
|
+
},
|
|
206
|
+
week: {
|
|
207
|
+
pv: productWeek.pv,
|
|
208
|
+
uv: productWeek.uv
|
|
209
|
+
},
|
|
210
|
+
month: {
|
|
211
|
+
pv: productMonth.pv,
|
|
212
|
+
uv: productMonth.uv
|
|
213
|
+
},
|
|
214
|
+
days: productDays,
|
|
215
|
+
totalPv: sumProductTrendField(productDays, "pv"),
|
|
216
|
+
totalUv: sumProductTrendField(productDays, "uv")
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
products.sort((a, b) => {
|
|
221
|
+
if (b.totalPv !== a.totalPv) {
|
|
222
|
+
return b.totalPv - a.totalPv;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return String(a.productName).localeCompare(String(b.productName), "zh-CN");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return befly.tool.Yes("获取成功", {
|
|
229
|
+
queryTime: now,
|
|
230
|
+
onlineCount: onlineCount,
|
|
231
|
+
today: {
|
|
232
|
+
reportDate: reportDate,
|
|
233
|
+
reportTime: currentDay.reportTime,
|
|
234
|
+
pv: currentDay.pv,
|
|
235
|
+
uv: currentDay.uv
|
|
236
|
+
},
|
|
237
|
+
week: {
|
|
238
|
+
startDate: weekStartDate,
|
|
239
|
+
endDate: reportDate,
|
|
240
|
+
reportTime: weekCurrent.reportTime,
|
|
241
|
+
pv: weekCurrent.pv,
|
|
242
|
+
uv: weekCurrent.uv
|
|
243
|
+
},
|
|
244
|
+
month: {
|
|
245
|
+
startDate: monthStartDate,
|
|
246
|
+
endDate: reportDate,
|
|
247
|
+
reportTime: monthCurrent.reportTime,
|
|
248
|
+
pv: monthCurrent.pv,
|
|
249
|
+
uv: monthCurrent.uv
|
|
250
|
+
},
|
|
251
|
+
days: days,
|
|
252
|
+
products: products
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
package/configs/beflyMenus.json
CHANGED
|
@@ -65,30 +65,25 @@
|
|
|
65
65
|
"path": "/log",
|
|
66
66
|
"sort": 9003,
|
|
67
67
|
"children": [
|
|
68
|
-
{
|
|
69
|
-
"name": "访问统计",
|
|
70
|
-
"path": "/visit",
|
|
71
|
-
"sort": 1
|
|
72
|
-
},
|
|
73
68
|
{
|
|
74
69
|
"name": "登录日志",
|
|
75
70
|
"path": "/login",
|
|
76
|
-
"sort":
|
|
71
|
+
"sort": 1
|
|
77
72
|
},
|
|
78
73
|
{
|
|
79
74
|
"name": "邮件日志",
|
|
80
75
|
"path": "/email",
|
|
81
|
-
"sort":
|
|
76
|
+
"sort": 2
|
|
82
77
|
},
|
|
83
78
|
{
|
|
84
79
|
"name": "操作日志",
|
|
85
80
|
"path": "/operate",
|
|
86
|
-
"sort":
|
|
81
|
+
"sort": 3
|
|
87
82
|
},
|
|
88
83
|
{
|
|
89
84
|
"name": "错误报告",
|
|
90
85
|
"path": "/error",
|
|
91
|
-
"sort":
|
|
86
|
+
"sort": 4
|
|
92
87
|
}
|
|
93
88
|
]
|
|
94
89
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.19.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "3.19.7",
|
|
4
|
+
"gitHead": "c47d1e515dabe142531e0a8415979cccb2729259",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
|
|
7
7
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"test": "bun test"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"fast-xml-parser": "^5.5.
|
|
55
|
+
"fast-xml-parser": "^5.5.5",
|
|
56
56
|
"nodemailer": "^8.0.2",
|
|
57
57
|
"pathe": "^2.0.3",
|
|
58
58
|
"ua-parser-js": "^2.0.9",
|
package/sql/befly.sql
CHANGED
|
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS `befly_admin` (
|
|
|
15
15
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
16
16
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
17
17
|
PRIMARY KEY (`id`)
|
|
18
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
18
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
19
19
|
|
|
20
20
|
CREATE TABLE IF NOT EXISTS `befly_api` (
|
|
21
21
|
`id` BIGINT NOT NULL,
|
|
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS `befly_api` (
|
|
|
29
29
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
30
30
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
31
31
|
PRIMARY KEY (`id`)
|
|
32
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
32
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
33
33
|
|
|
34
34
|
CREATE TABLE IF NOT EXISTS `befly_dict` (
|
|
35
35
|
`id` BIGINT NOT NULL,
|
|
@@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS `befly_dict` (
|
|
|
43
43
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
44
44
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
45
45
|
PRIMARY KEY (`id`)
|
|
46
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
46
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
47
47
|
|
|
48
48
|
CREATE TABLE IF NOT EXISTS `befly_dict_type` (
|
|
49
49
|
`id` BIGINT NOT NULL,
|
|
@@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS `befly_dict_type` (
|
|
|
56
56
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
57
57
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
58
58
|
PRIMARY KEY (`id`)
|
|
59
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
59
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
60
60
|
|
|
61
61
|
CREATE TABLE IF NOT EXISTS `befly_email_log` (
|
|
62
62
|
`id` BIGINT NOT NULL,
|
|
@@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS `befly_email_log` (
|
|
|
77
77
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
78
78
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
79
79
|
PRIMARY KEY (`id`)
|
|
80
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
80
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
81
81
|
|
|
82
82
|
CREATE TABLE IF NOT EXISTS `befly_login_log` (
|
|
83
83
|
`id` BIGINT NOT NULL,
|
|
@@ -103,7 +103,7 @@ CREATE TABLE IF NOT EXISTS `befly_login_log` (
|
|
|
103
103
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
104
104
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
105
105
|
PRIMARY KEY (`id`)
|
|
106
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
106
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
107
107
|
|
|
108
108
|
CREATE TABLE IF NOT EXISTS `befly_menu` (
|
|
109
109
|
`id` BIGINT NOT NULL,
|
|
@@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS `befly_menu` (
|
|
|
116
116
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
117
117
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
118
118
|
PRIMARY KEY (`id`)
|
|
119
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
119
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
120
120
|
|
|
121
121
|
CREATE TABLE IF NOT EXISTS `befly_operate_log` (
|
|
122
122
|
`id` BIGINT NOT NULL,
|
|
@@ -139,7 +139,7 @@ CREATE TABLE IF NOT EXISTS `befly_operate_log` (
|
|
|
139
139
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
140
140
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
141
141
|
PRIMARY KEY (`id`)
|
|
142
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
142
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
143
143
|
|
|
144
144
|
CREATE TABLE IF NOT EXISTS `befly_role` (
|
|
145
145
|
`id` BIGINT NOT NULL,
|
|
@@ -154,7 +154,7 @@ CREATE TABLE IF NOT EXISTS `befly_role` (
|
|
|
154
154
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
155
155
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
156
156
|
PRIMARY KEY (`id`)
|
|
157
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
157
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
158
158
|
|
|
159
159
|
CREATE TABLE IF NOT EXISTS `befly_sys_config` (
|
|
160
160
|
`id` BIGINT NOT NULL,
|
|
@@ -171,26 +171,7 @@ CREATE TABLE IF NOT EXISTS `befly_sys_config` (
|
|
|
171
171
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
172
172
|
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
173
173
|
PRIMARY KEY (`id`)
|
|
174
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
175
|
-
|
|
176
|
-
CREATE TABLE IF NOT EXISTS `befly_visit_stats` (
|
|
177
|
-
`id` BIGINT NOT NULL,
|
|
178
|
-
`bucket_time` BIGINT NOT NULL DEFAULT 0,
|
|
179
|
-
`bucket_date` INT NOT NULL DEFAULT 0,
|
|
180
|
-
`pv` BIGINT NOT NULL DEFAULT 0,
|
|
181
|
-
`uv` BIGINT NOT NULL DEFAULT 0,
|
|
182
|
-
`error_count` BIGINT NOT NULL DEFAULT 0,
|
|
183
|
-
`duration_sum` BIGINT NOT NULL DEFAULT 0,
|
|
184
|
-
`duration_count` BIGINT NOT NULL DEFAULT 0,
|
|
185
|
-
`avg_duration` BIGINT NOT NULL DEFAULT 0,
|
|
186
|
-
`state` TINYINT NOT NULL DEFAULT 1,
|
|
187
|
-
`created_at` BIGINT NOT NULL DEFAULT 0,
|
|
188
|
-
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
189
|
-
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
190
|
-
PRIMARY KEY (`id`),
|
|
191
|
-
UNIQUE KEY `uk_befly_visit_stats_bucket_time` (`bucket_time`),
|
|
192
|
-
KEY `idx_befly_visit_stats_bucket_date` (`bucket_date`)
|
|
193
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
174
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
194
175
|
|
|
195
176
|
CREATE TABLE IF NOT EXISTS `befly_error_report` (
|
|
196
177
|
`id` BIGINT NOT NULL,
|
|
@@ -225,4 +206,36 @@ CREATE TABLE IF NOT EXISTS `befly_error_report` (
|
|
|
225
206
|
PRIMARY KEY (`id`),
|
|
226
207
|
KEY `idx_befly_error_report_time` (`report_time`),
|
|
227
208
|
KEY `idx_befly_error_report_bucket_date` (`bucket_date`)
|
|
228
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
209
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS `befly_info_report` (
|
|
212
|
+
`id` BIGINT NOT NULL,
|
|
213
|
+
`report_time` BIGINT NOT NULL DEFAULT 0,
|
|
214
|
+
`report_date` INT NOT NULL DEFAULT 0,
|
|
215
|
+
`member_key` VARCHAR(100) NOT NULL DEFAULT '',
|
|
216
|
+
`source` VARCHAR(50) NOT NULL DEFAULT '',
|
|
217
|
+
`product_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
218
|
+
`product_code` VARCHAR(100) NOT NULL DEFAULT '',
|
|
219
|
+
`product_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
220
|
+
`page_path` VARCHAR(200) NOT NULL DEFAULT '',
|
|
221
|
+
`page_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
222
|
+
`detail` TEXT NULL,
|
|
223
|
+
`user_agent` VARCHAR(500) NOT NULL DEFAULT '',
|
|
224
|
+
`browser_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
225
|
+
`browser_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
226
|
+
`os_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
227
|
+
`os_version` VARCHAR(100) NOT NULL DEFAULT '',
|
|
228
|
+
`device_type` VARCHAR(50) NOT NULL DEFAULT '',
|
|
229
|
+
`device_vendor` VARCHAR(100) NOT NULL DEFAULT '',
|
|
230
|
+
`device_model` VARCHAR(100) NOT NULL DEFAULT '',
|
|
231
|
+
`engine_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
232
|
+
`cpu_architecture` VARCHAR(100) NOT NULL DEFAULT '',
|
|
233
|
+
`state` TINYINT NOT NULL DEFAULT 1,
|
|
234
|
+
`created_at` BIGINT NOT NULL DEFAULT 0,
|
|
235
|
+
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
|
236
|
+
`deleted_at` BIGINT NULL DEFAULT NULL,
|
|
237
|
+
PRIMARY KEY (`id`),
|
|
238
|
+
UNIQUE KEY `uk_befly_info_report_date_member` (`report_date`, `member_key`),
|
|
239
|
+
KEY `idx_befly_info_report_time` (`report_time`),
|
|
240
|
+
KEY `idx_befly_info_report_date` (`report_date`)
|
|
241
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { UAParser } from "ua-parser-js";
|
|
2
|
-
|
|
3
|
-
import { isValidPositiveInt } from "../../utils/is.js";
|
|
4
|
-
import { getDateYmdNumber, getTimeBucketStart } from "../../utils/datetime.js";
|
|
5
|
-
import { VISIT_STATS_BUCKET_MS, VISIT_STATS_ONLINE_TTL_SECONDS, VISIT_STATS_PRODUCT_FIELDS, VISIT_STATS_REDIS_TTL_SECONDS, VISIT_STATS_UA_FIELDS, getVisitStatsBucketKey, getVisitStatsBucketProductPvKey, getVisitStatsBucketProductUvKey, getVisitStatsDayKey, getVisitStatsDayProductCountKey, getVisitStatsDayProductKeysKey, getVisitStatsDayProductPvKey, getVisitStatsDayProductUvKey, getVisitStatsDayProductValuesKey, getVisitStatsDayUaCountKey, getVisitStatsDayUaValuesKey, getVisitStatsProductKey, getVisitStatsProductMeta, getVisitStatsProductMetaKey, normalizeVisitStatsProductValue, normalizeVisitStatsUaValue, toVisitStatsNumber } from "../../utils/visitStats.js";
|
|
6
|
-
|
|
7
|
-
function getVisitStatsMember(ctx) {
|
|
8
|
-
if (isValidPositiveInt(ctx.userId)) {
|
|
9
|
-
return `user:${ctx.userId}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return `ip:${ctx.ip || "unknown"}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getVisitStatsUaData(ctx) {
|
|
16
|
-
let userAgent = "";
|
|
17
|
-
|
|
18
|
-
if (typeof ctx.req?.headers?.get === "function") {
|
|
19
|
-
userAgent = String(ctx.req.headers.get("user-agent") || "");
|
|
20
|
-
} else if (typeof ctx.headers?.get === "function") {
|
|
21
|
-
userAgent = String(ctx.headers.get("user-agent") || "");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const uaResult = UAParser(userAgent);
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
deviceType: normalizeVisitStatsUaValue(uaResult.device.type, "desktop"),
|
|
28
|
-
browserName: normalizeVisitStatsUaValue(uaResult.browser.name),
|
|
29
|
-
browserVersion: normalizeVisitStatsUaValue(uaResult.browser.version),
|
|
30
|
-
osName: normalizeVisitStatsUaValue(uaResult.os.name),
|
|
31
|
-
osVersion: normalizeVisitStatsUaValue(uaResult.os.version),
|
|
32
|
-
deviceVendor: normalizeVisitStatsUaValue(uaResult.device.vendor),
|
|
33
|
-
deviceModel: normalizeVisitStatsUaValue(uaResult.device.model),
|
|
34
|
-
engineName: normalizeVisitStatsUaValue(uaResult.engine.name),
|
|
35
|
-
cpuArchitecture: normalizeVisitStatsUaValue(uaResult.cpu.architecture)
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getVisitStatsProductData(ctx) {
|
|
40
|
-
return getVisitStatsProductMeta({
|
|
41
|
-
productName: ctx.body?.productName,
|
|
42
|
-
productCode: ctx.body?.productCode,
|
|
43
|
-
productVersion: ctx.body?.productVersion
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function saveVisitStatsUaData(befly, bucketDate, uaData) {
|
|
48
|
-
const saddItems = [];
|
|
49
|
-
const expireItems = [];
|
|
50
|
-
const countTasks = [];
|
|
51
|
-
|
|
52
|
-
for (const item of VISIT_STATS_UA_FIELDS) {
|
|
53
|
-
const field = item.field;
|
|
54
|
-
const defaultValue = item.defaultValue;
|
|
55
|
-
const value = normalizeVisitStatsUaValue(uaData[field], defaultValue);
|
|
56
|
-
const valuesKey = getVisitStatsDayUaValuesKey(bucketDate, field);
|
|
57
|
-
const countKey = getVisitStatsDayUaCountKey(bucketDate, field, value);
|
|
58
|
-
|
|
59
|
-
saddItems.push({ key: valuesKey, members: [value] });
|
|
60
|
-
expireItems.push({ key: valuesKey, seconds: VISIT_STATS_REDIS_TTL_SECONDS });
|
|
61
|
-
countTasks.push(befly.redis.incrWithExpire(countKey, VISIT_STATS_REDIS_TTL_SECONDS));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await befly.redis.saddBatch(saddItems);
|
|
65
|
-
await befly.redis.expireBatch(expireItems);
|
|
66
|
-
await Promise.all(countTasks);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function saveVisitStatsProductData(befly, bucketDate, productData) {
|
|
70
|
-
const saddItems = [];
|
|
71
|
-
const expireItems = [];
|
|
72
|
-
const countTasks = [];
|
|
73
|
-
|
|
74
|
-
for (const item of VISIT_STATS_PRODUCT_FIELDS) {
|
|
75
|
-
const field = item.field;
|
|
76
|
-
const defaultValue = item.defaultValue;
|
|
77
|
-
const value = normalizeVisitStatsProductValue(productData[field], defaultValue);
|
|
78
|
-
const valuesKey = getVisitStatsDayProductValuesKey(bucketDate, field);
|
|
79
|
-
const countKey = getVisitStatsDayProductCountKey(bucketDate, field, value);
|
|
80
|
-
|
|
81
|
-
saddItems.push({ key: valuesKey, members: [value] });
|
|
82
|
-
expireItems.push({ key: valuesKey, seconds: VISIT_STATS_REDIS_TTL_SECONDS });
|
|
83
|
-
countTasks.push(befly.redis.incrWithExpire(countKey, VISIT_STATS_REDIS_TTL_SECONDS));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
await befly.redis.saddBatch(saddItems);
|
|
87
|
-
await befly.redis.expireBatch(expireItems);
|
|
88
|
-
await Promise.all(countTasks);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function saveVisitStatsProductSummary(befly, bucketTime, bucketDate, member, productData) {
|
|
92
|
-
const productKey = getVisitStatsProductKey(productData);
|
|
93
|
-
const metaKey = getVisitStatsProductMetaKey(productKey);
|
|
94
|
-
const dayKeysKey = getVisitStatsDayProductKeysKey(bucketDate);
|
|
95
|
-
const bucketPvKey = getVisitStatsBucketProductPvKey(bucketTime, productKey);
|
|
96
|
-
const bucketUvKey = getVisitStatsBucketProductUvKey(bucketTime, productKey);
|
|
97
|
-
const dayPvKey = getVisitStatsDayProductPvKey(bucketDate, productKey);
|
|
98
|
-
const dayUvKey = getVisitStatsDayProductUvKey(bucketDate, productKey);
|
|
99
|
-
|
|
100
|
-
await befly.redis.setString(metaKey, JSON.stringify(productData), VISIT_STATS_REDIS_TTL_SECONDS);
|
|
101
|
-
await befly.redis.sadd(dayKeysKey, [productKey]);
|
|
102
|
-
await befly.redis.expire(dayKeysKey, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
103
|
-
await befly.redis.incrWithExpire(bucketPvKey, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
104
|
-
await befly.redis.sadd(bucketUvKey, [member]);
|
|
105
|
-
await befly.redis.expire(bucketUvKey, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
106
|
-
await befly.redis.incrWithExpire(dayPvKey, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
107
|
-
await befly.redis.sadd(dayUvKey, [member]);
|
|
108
|
-
await befly.redis.expire(dayUvKey, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function ensureVisitStatsTableReady(befly) {
|
|
112
|
-
if (befly.visitStatsTableReady !== undefined) {
|
|
113
|
-
return befly.visitStatsTableReady;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const tableExistsResult = await befly.mysql.tableExists("beflyVisitStats");
|
|
117
|
-
befly.visitStatsTableReady = tableExistsResult.data === true;
|
|
118
|
-
return befly.visitStatsTableReady;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function flushVisitStatsBucket(befly, bucketTime) {
|
|
122
|
-
if (bucketTime <= 0) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const currentBucketTime = getTimeBucketStart(Date.now(), VISIT_STATS_BUCKET_MS);
|
|
127
|
-
if (bucketTime >= currentBucketTime) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const tableReady = await ensureVisitStatsTableReady(befly);
|
|
132
|
-
if (!tableReady) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const flushedKey = `visitStats:flushed:${bucketTime}`;
|
|
137
|
-
const flushed = await befly.redis.exists(flushedKey);
|
|
138
|
-
if (flushed) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const bucketDate = getDateYmdNumber(bucketTime);
|
|
143
|
-
const bucketKey = getVisitStatsBucketKey(bucketTime);
|
|
144
|
-
const pv = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:pv`));
|
|
145
|
-
const uv = await befly.redis.scard(`${bucketKey}:uv`);
|
|
146
|
-
const errorCount = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:errorCount`));
|
|
147
|
-
const durationSum = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:durationSum`));
|
|
148
|
-
const durationCount = toVisitStatsNumber(await befly.redis.getString(`${bucketKey}:durationCount`));
|
|
149
|
-
const avgDuration = durationCount > 0 ? Math.round(durationSum / durationCount) : 0;
|
|
150
|
-
|
|
151
|
-
if (pv <= 0 && uv <= 0 && errorCount <= 0 && durationCount <= 0) {
|
|
152
|
-
await befly.redis.setString(flushedKey, "1", VISIT_STATS_REDIS_TTL_SECONDS);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const data = {
|
|
157
|
-
bucketTime: bucketTime,
|
|
158
|
-
bucketDate: bucketDate,
|
|
159
|
-
pv: pv,
|
|
160
|
-
uv: uv,
|
|
161
|
-
errorCount: errorCount,
|
|
162
|
-
durationSum: durationSum,
|
|
163
|
-
durationCount: durationCount,
|
|
164
|
-
avgDuration: avgDuration
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const detail = await befly.mysql.getOne({
|
|
168
|
-
table: "beflyVisitStats",
|
|
169
|
-
where: { bucketTime: bucketTime }
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (detail.data?.id) {
|
|
173
|
-
await befly.mysql.updData({
|
|
174
|
-
table: "beflyVisitStats",
|
|
175
|
-
data: data,
|
|
176
|
-
where: { bucketTime: bucketTime }
|
|
177
|
-
});
|
|
178
|
-
} else {
|
|
179
|
-
await befly.mysql.insData({
|
|
180
|
-
table: "beflyVisitStats",
|
|
181
|
-
data: data
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
await befly.redis.setString(flushedKey, "1", VISIT_STATS_REDIS_TTL_SECONDS);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function flushVisitStatsPreviousBucket(befly, now = Date.now()) {
|
|
189
|
-
const currentBucketTime = getTimeBucketStart(now, VISIT_STATS_BUCKET_MS);
|
|
190
|
-
await flushVisitStatsBucket(befly, currentBucketTime - 30 * 60 * 1000);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export default {
|
|
194
|
-
name: "上报访问统计",
|
|
195
|
-
method: "POST",
|
|
196
|
-
body: "none",
|
|
197
|
-
auth: false,
|
|
198
|
-
fields: {
|
|
199
|
-
pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
|
|
200
|
-
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
201
|
-
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
202
|
-
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
203
|
-
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
204
|
-
productVersion: { name: "产品版本", input: "string", min: 0, max: 100 }
|
|
205
|
-
},
|
|
206
|
-
required: [],
|
|
207
|
-
handler: async (befly, ctx) => {
|
|
208
|
-
const now = Date.now();
|
|
209
|
-
const bucketTime = getTimeBucketStart(now, VISIT_STATS_BUCKET_MS);
|
|
210
|
-
const bucketDate = getDateYmdNumber(now);
|
|
211
|
-
const bucketKey = getVisitStatsBucketKey(bucketTime);
|
|
212
|
-
const dayKey = getVisitStatsDayKey(bucketDate);
|
|
213
|
-
const member = getVisitStatsMember(ctx);
|
|
214
|
-
const uaData = getVisitStatsUaData(ctx);
|
|
215
|
-
const productData = getVisitStatsProductData(ctx);
|
|
216
|
-
|
|
217
|
-
await flushVisitStatsPreviousBucket(befly, now);
|
|
218
|
-
|
|
219
|
-
await befly.redis.incrWithExpire(`${bucketKey}:pv`, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
220
|
-
await befly.redis.incrWithExpire(`${dayKey}:pv`, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
221
|
-
|
|
222
|
-
await befly.redis.sadd(`${bucketKey}:uv`, [member]);
|
|
223
|
-
await befly.redis.expire(`${bucketKey}:uv`, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
224
|
-
await befly.redis.sadd(`${dayKey}:uv`, [member]);
|
|
225
|
-
await befly.redis.expire(`${dayKey}:uv`, VISIT_STATS_REDIS_TTL_SECONDS);
|
|
226
|
-
|
|
227
|
-
await befly.redis.setString(`visitStats:online:visitor:${member}`, "1", VISIT_STATS_ONLINE_TTL_SECONDS);
|
|
228
|
-
await befly.redis.sadd("visitStats:online:visitors", [member]);
|
|
229
|
-
await befly.redis.expire("visitStats:online:visitors", VISIT_STATS_REDIS_TTL_SECONDS);
|
|
230
|
-
await saveVisitStatsUaData(befly, bucketDate, uaData);
|
|
231
|
-
await saveVisitStatsProductData(befly, bucketDate, productData);
|
|
232
|
-
await saveVisitStatsProductSummary(befly, bucketTime, bucketDate, member, productData);
|
|
233
|
-
|
|
234
|
-
const result = {
|
|
235
|
-
bucketTime: bucketTime,
|
|
236
|
-
bucketDate: bucketDate
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
return befly.tool.Yes("上报成功", result);
|
|
240
|
-
}
|
|
241
|
-
};
|