befly 3.18.23 → 3.19.5
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/auth/login.js +2 -3
- package/apis/tongJi/errorList.js +102 -0
- package/apis/tongJi/errorReport.js +90 -0
- package/apis/tongJi/errorStats.js +70 -0
- package/apis/tongJi/visitReport.js +241 -0
- package/apis/tongJi/visitStats.js +380 -0
- package/checks/api.js +60 -4
- package/checks/config.js +1 -0
- package/checks/table.js +47 -13
- package/configs/beflyConfig.json +1 -0
- package/configs/beflyMenus.json +13 -3
- package/configs/constConfig.js +11 -0
- package/index.js +1 -1
- package/lib/logger.js +3 -3
- package/lib/redisHelper.js +38 -0
- package/lib/sqlBuilder/compiler.js +0 -9
- package/lib/sqlBuilder/index.js +1 -19
- package/lib/sqlBuilder/parser.js +0 -21
- package/lib/validator.js +108 -49
- package/package.json +4 -4
- package/paths.js +14 -1
- package/router/static.js +3 -3
- package/sql/befly.sql +54 -0
- package/sync/dev.js +16 -11
- package/sync/menu.js +6 -4
- package/tables/admin.json +10 -5
- package/tables/dict.json +4 -2
- package/tables/dictType.json +2 -1
- package/tables/emailLog.json +2 -1
- package/tables/role.json +2 -1
- package/tables/sysConfig.json +4 -2
- package/utils/cors.js +34 -1
- package/utils/datetime.js +76 -0
- package/utils/visitStats.js +121 -0
- package/utils/formatYmdHms.js +0 -23
package/apis/auth/login.js
CHANGED
|
@@ -18,7 +18,8 @@ export default {
|
|
|
18
18
|
password: adminTable.password,
|
|
19
19
|
loginType: {
|
|
20
20
|
name: "登录类型",
|
|
21
|
-
input: "
|
|
21
|
+
input: "enum",
|
|
22
|
+
check: "username|email|phone",
|
|
22
23
|
min: null,
|
|
23
24
|
max: null
|
|
24
25
|
}
|
|
@@ -96,7 +97,6 @@ export default {
|
|
|
96
97
|
id: admin.data.id,
|
|
97
98
|
username: admin.data.username,
|
|
98
99
|
nickname: admin.data.nickname,
|
|
99
|
-
state: admin.data.state,
|
|
100
100
|
roleCode: admin.data.roleCode,
|
|
101
101
|
roleType: admin.data.roleType,
|
|
102
102
|
loginAt: Date.now()
|
|
@@ -113,7 +113,6 @@ export default {
|
|
|
113
113
|
id: sessionData.id,
|
|
114
114
|
username: sessionData.username,
|
|
115
115
|
nickname: sessionData.nickname,
|
|
116
|
-
state: sessionData.state,
|
|
117
116
|
roleCode: sessionData.roleCode,
|
|
118
117
|
roleType: sessionData.roleType
|
|
119
118
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
name: "获取错误报告列表",
|
|
3
|
+
method: "POST",
|
|
4
|
+
body: "none",
|
|
5
|
+
auth: true,
|
|
6
|
+
fields: {
|
|
7
|
+
page: { name: "页码", input: "integer", min: 1, max: 9999 },
|
|
8
|
+
limit: { name: "每页数量", input: "integer", min: 1, max: 100 },
|
|
9
|
+
keyword: { name: "关键词", input: "string", min: 0, max: 100 },
|
|
10
|
+
errorType: { name: "错误类型", input: "string", min: 0, max: 50 },
|
|
11
|
+
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
12
|
+
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
13
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
14
|
+
productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
|
|
15
|
+
deviceType: { name: "设备类型", input: "string", min: 0, max: 50 },
|
|
16
|
+
browserName: { name: "浏览器", input: "string", min: 0, max: 100 },
|
|
17
|
+
osName: { name: "操作系统", input: "string", min: 0, max: 100 }
|
|
18
|
+
},
|
|
19
|
+
required: [],
|
|
20
|
+
handler: async (befly, ctx) => {
|
|
21
|
+
const tableExistsResult = await befly.mysql.tableExists("beflyErrorReport");
|
|
22
|
+
const page = Number(ctx.body.page || 1);
|
|
23
|
+
const limit = Number(ctx.body.limit || 20);
|
|
24
|
+
|
|
25
|
+
if (tableExistsResult.data !== true) {
|
|
26
|
+
return befly.tool.Yes("获取成功", {
|
|
27
|
+
lists: [],
|
|
28
|
+
total: 0,
|
|
29
|
+
page: page,
|
|
30
|
+
limit: limit
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const keyword = String(ctx.body.keyword || "").trim();
|
|
35
|
+
const errorType = String(ctx.body.errorType || "").trim();
|
|
36
|
+
const source = String(ctx.body.source || "").trim();
|
|
37
|
+
const productName = String(ctx.body.productName || "").trim();
|
|
38
|
+
const productCode = String(ctx.body.productCode || "").trim();
|
|
39
|
+
const productVersion = String(ctx.body.productVersion || "").trim();
|
|
40
|
+
const deviceType = String(ctx.body.deviceType || "").trim();
|
|
41
|
+
const browserName = String(ctx.body.browserName || "").trim();
|
|
42
|
+
const osName = String(ctx.body.osName || "").trim();
|
|
43
|
+
const where = {};
|
|
44
|
+
|
|
45
|
+
if (keyword) {
|
|
46
|
+
where["$or"] = [{ pagePath$like: keyword }, { pageName$like: keyword }, { message$like: keyword }, { errorType$like: keyword }, { productName$like: keyword }, { productCode$like: keyword }, { productVersion$like: keyword }, { browserName$like: keyword }, { osName$like: keyword }, { deviceType$like: keyword }, { deviceVendor$like: keyword }, { deviceModel$like: keyword }];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (errorType) {
|
|
50
|
+
where.errorType = errorType;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (source) {
|
|
54
|
+
where.source = source;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (productName) {
|
|
58
|
+
where.productName = productName;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (productCode) {
|
|
62
|
+
where.productCode = productCode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (productVersion) {
|
|
66
|
+
where.productVersion = productVersion;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (deviceType) {
|
|
70
|
+
where.deviceType = deviceType;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (browserName) {
|
|
74
|
+
where.browserName = browserName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (osName) {
|
|
78
|
+
where.osName = osName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await befly.mysql.getList({
|
|
82
|
+
table: "beflyErrorReport",
|
|
83
|
+
where: where,
|
|
84
|
+
page: page,
|
|
85
|
+
limit: limit,
|
|
86
|
+
orderBy: ["reportTime#DESC"]
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const lists = [];
|
|
90
|
+
for (const item of result.data?.lists || []) {
|
|
91
|
+
const row = Object.assign({}, item);
|
|
92
|
+
row.firstReportTime = Number(item.firstReportTime || 0);
|
|
93
|
+
row.hitCount = Number(item.hitCount || 0) || 1;
|
|
94
|
+
lists.push(row);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = Object.assign({}, result.data);
|
|
98
|
+
data.lists = lists;
|
|
99
|
+
|
|
100
|
+
return befly.tool.Yes("获取成功", data);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { UAParser } from "ua-parser-js";
|
|
2
|
+
import { getDateYmdNumber, getTimeBucketStart } from "../../utils/datetime.js";
|
|
3
|
+
import { VISIT_STATS_BUCKET_MS } from "../../utils/visitStats.js";
|
|
4
|
+
|
|
5
|
+
function getErrorReportUaData(ctx) {
|
|
6
|
+
let userAgent = "";
|
|
7
|
+
|
|
8
|
+
if (typeof ctx.req?.headers?.get === "function") {
|
|
9
|
+
userAgent = ctx.req.headers.get("user-agent") || "";
|
|
10
|
+
} else if (typeof ctx.headers?.get === "function") {
|
|
11
|
+
userAgent = ctx.headers.get("user-agent") || "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const uaResult = UAParser(userAgent);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
userAgent: userAgent,
|
|
18
|
+
browserName: uaResult.browser.name || "",
|
|
19
|
+
browserVersion: uaResult.browser.version || "",
|
|
20
|
+
osName: uaResult.os.name || "",
|
|
21
|
+
osVersion: uaResult.os.version || "",
|
|
22
|
+
deviceType: uaResult.device.type || "desktop",
|
|
23
|
+
deviceVendor: uaResult.device.vendor || "",
|
|
24
|
+
deviceModel: uaResult.device.model || "",
|
|
25
|
+
engineName: uaResult.engine.name || "",
|
|
26
|
+
cpuArchitecture: uaResult.cpu.architecture || ""
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
name: "上报错误报告",
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: "none",
|
|
34
|
+
auth: false,
|
|
35
|
+
fields: {
|
|
36
|
+
pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
|
|
37
|
+
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
38
|
+
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
39
|
+
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
40
|
+
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
41
|
+
productVersion: { name: "产品版本", input: "string", min: 0, max: 100 },
|
|
42
|
+
errorType: { name: "错误类型", input: "string", min: 0, max: 50 },
|
|
43
|
+
message: { name: "错误信息", input: "string", min: 0, max: 500 },
|
|
44
|
+
detail: { name: "错误详情", input: "string", min: 0, max: 5000 }
|
|
45
|
+
},
|
|
46
|
+
required: [],
|
|
47
|
+
handler: async (befly, ctx) => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const bucketTime = getTimeBucketStart(now, VISIT_STATS_BUCKET_MS);
|
|
50
|
+
const bucketDate = getDateYmdNumber(now);
|
|
51
|
+
const uaData = getErrorReportUaData(ctx);
|
|
52
|
+
|
|
53
|
+
await befly.mysql.insData({
|
|
54
|
+
table: "beflyErrorReport",
|
|
55
|
+
data: {
|
|
56
|
+
reportTime: now,
|
|
57
|
+
firstReportTime: now,
|
|
58
|
+
bucketTime: bucketTime,
|
|
59
|
+
bucketDate: bucketDate,
|
|
60
|
+
hitCount: 1,
|
|
61
|
+
source: ctx.body?.source,
|
|
62
|
+
productName: ctx.body?.productName || "",
|
|
63
|
+
productCode: ctx.body?.productCode || "",
|
|
64
|
+
productVersion: ctx.body?.productVersion || "",
|
|
65
|
+
pagePath: ctx.body?.pagePath || "",
|
|
66
|
+
pageName: ctx.body?.pageName || "",
|
|
67
|
+
errorType: ctx.body?.errorType || "",
|
|
68
|
+
message: ctx.body?.message || "",
|
|
69
|
+
detail: ctx.body?.detail || "",
|
|
70
|
+
userAgent: uaData.userAgent,
|
|
71
|
+
browserName: uaData.browserName,
|
|
72
|
+
browserVersion: uaData.browserVersion,
|
|
73
|
+
osName: uaData.osName,
|
|
74
|
+
osVersion: uaData.osVersion,
|
|
75
|
+
deviceType: uaData.deviceType,
|
|
76
|
+
deviceVendor: uaData.deviceVendor,
|
|
77
|
+
deviceModel: uaData.deviceModel,
|
|
78
|
+
engineName: uaData.engineName,
|
|
79
|
+
cpuArchitecture: uaData.cpuArchitecture
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return befly.tool.Yes("上报成功", {
|
|
84
|
+
reportTime: now,
|
|
85
|
+
bucketTime: bucketTime,
|
|
86
|
+
bucketDate: bucketDate,
|
|
87
|
+
stored: true
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getDateYmdNumber } from "../../utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
function toNumber(value) {
|
|
4
|
+
const num = Number(value);
|
|
5
|
+
if (!Number.isFinite(num)) {
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return num;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
name: "获取错误统计",
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: "none",
|
|
16
|
+
auth: true,
|
|
17
|
+
fields: {},
|
|
18
|
+
required: [],
|
|
19
|
+
handler: async (befly) => {
|
|
20
|
+
const tableExistsResult = await befly.mysql.tableExists("beflyErrorReport");
|
|
21
|
+
const tableReady = tableExistsResult.data === true;
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const bucketDate = getDateYmdNumber(now);
|
|
24
|
+
|
|
25
|
+
if (!tableReady) {
|
|
26
|
+
return befly.tool.Yes("获取成功", {
|
|
27
|
+
trend: [],
|
|
28
|
+
days: [],
|
|
29
|
+
topTypes: []
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const trendRes = await befly.mysql.execute("SELECT bucket_time as bucketTime, bucket_date as bucketDate, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date = ? GROUP BY bucket_time, bucket_date ORDER BY bucket_time ASC", [bucketDate]);
|
|
34
|
+
const daysRes = await befly.mysql.execute("SELECT bucket_date as bucketDate, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 GROUP BY bucket_date ORDER BY bucket_date DESC LIMIT 7", []);
|
|
35
|
+
const topTypesRes = await befly.mysql.execute("SELECT error_type as errorType, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date = ? GROUP BY error_type ORDER BY count DESC LIMIT 5", [bucketDate]);
|
|
36
|
+
|
|
37
|
+
const trend = [];
|
|
38
|
+
for (const item of trendRes.data || []) {
|
|
39
|
+
trend.push({
|
|
40
|
+
bucketTime: toNumber(item.bucketTime),
|
|
41
|
+
bucketDate: toNumber(item.bucketDate),
|
|
42
|
+
count: toNumber(item.count)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const days = [];
|
|
47
|
+
for (const item of daysRes.data || []) {
|
|
48
|
+
days.push({
|
|
49
|
+
bucketDate: toNumber(item.bucketDate),
|
|
50
|
+
count: toNumber(item.count)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
days.sort((a, b) => a.bucketDate - b.bucketDate);
|
|
55
|
+
|
|
56
|
+
const topTypes = [];
|
|
57
|
+
for (const item of topTypesRes.data || []) {
|
|
58
|
+
topTypes.push({
|
|
59
|
+
errorType: String(item.errorType || "unknown"),
|
|
60
|
+
count: toNumber(item.count)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return befly.tool.Yes("获取成功", {
|
|
65
|
+
trend: trend,
|
|
66
|
+
days: days,
|
|
67
|
+
topTypes: topTypes
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
};
|