befly 3.61.0 → 3.63.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/auth/login.js +65 -13
- package/apis/tongJi/_tongJi.js +92 -41
- package/apis/tongJi/dailyReport.js +73 -51
- package/apis/tongJi/dailyStats.js +98 -125
- package/apis/tongJi/dailyStatsDistribution.js +86 -0
- package/apis/tongJi/errorList.js +5 -9
- package/apis/tongJi/errorReport.js +81 -44
- package/apis/tongJi/errorStats.js +19 -125
- package/checks/config.js +1 -4
- package/configs/beflyConfig.json +1 -4
- package/index.js +5 -0
- package/lib/logger.js +125 -296
- package/package.json +1 -1
- package/plugins/cron.js +96 -0
- package/plugins/logger.js +2 -5
- package/sql/befly.sql +53 -25
- package/tables/dailyReport.json +124 -0
- package/tables/errorReport.json +31 -48
- package/tables/loginLog.json +29 -32
- package/utils/is.js +3 -36
- package/utils/loggerUtils.js +0 -3
- package/utils/userAgent.js +229 -104
- package/apis/tongJi/_daily.js +0 -159
package/apis/auth/login.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import adminTable from "#befly/tables/admin.json";
|
|
2
|
-
import { parseUserAgent } from "#befly/utils/userAgent.js";
|
|
3
2
|
import { toSessionTtlSeconds } from "#befly/utils/util.js";
|
|
4
3
|
|
|
5
4
|
export default {
|
|
@@ -21,28 +20,81 @@ export default {
|
|
|
21
20
|
check: "username|email|phone",
|
|
22
21
|
min: null,
|
|
23
22
|
max: null
|
|
23
|
+
},
|
|
24
|
+
clientType: {
|
|
25
|
+
name: "客户端类型",
|
|
26
|
+
input: "enum",
|
|
27
|
+
check: "browser|vscode|desktop|miniapp|app",
|
|
28
|
+
min: 3,
|
|
29
|
+
max: 20
|
|
30
|
+
},
|
|
31
|
+
productName: {
|
|
32
|
+
name: "产品名称",
|
|
33
|
+
input: "string",
|
|
34
|
+
min: 0,
|
|
35
|
+
max: 100
|
|
36
|
+
},
|
|
37
|
+
productCode: {
|
|
38
|
+
name: "产品代号",
|
|
39
|
+
input: "string",
|
|
40
|
+
min: 0,
|
|
41
|
+
max: 100
|
|
42
|
+
},
|
|
43
|
+
productVersion: {
|
|
44
|
+
name: "产品版本",
|
|
45
|
+
input: "string",
|
|
46
|
+
min: 0,
|
|
47
|
+
max: 50
|
|
48
|
+
},
|
|
49
|
+
clientVersion: {
|
|
50
|
+
name: "客户端版本",
|
|
51
|
+
input: "string",
|
|
52
|
+
min: 0,
|
|
53
|
+
max: 100
|
|
54
|
+
},
|
|
55
|
+
os: {
|
|
56
|
+
name: "操作系统",
|
|
57
|
+
input: "string",
|
|
58
|
+
min: 0,
|
|
59
|
+
max: 100
|
|
60
|
+
},
|
|
61
|
+
system: {
|
|
62
|
+
name: "系统",
|
|
63
|
+
input: "string",
|
|
64
|
+
min: 0,
|
|
65
|
+
max: 100
|
|
66
|
+
},
|
|
67
|
+
brand: {
|
|
68
|
+
name: "设备品牌",
|
|
69
|
+
input: "string",
|
|
70
|
+
min: 0,
|
|
71
|
+
max: 100
|
|
72
|
+
},
|
|
73
|
+
model: {
|
|
74
|
+
name: "设备型号",
|
|
75
|
+
input: "string",
|
|
76
|
+
min: 0,
|
|
77
|
+
max: 100
|
|
24
78
|
}
|
|
25
79
|
},
|
|
26
80
|
required: ["account", "password", "loginType"],
|
|
27
81
|
handler: async (befly, ctx) => {
|
|
28
|
-
const
|
|
29
|
-
const uaData = parseUserAgent(userAgent);
|
|
82
|
+
const body = ctx.body || {};
|
|
30
83
|
|
|
31
84
|
const logData = {
|
|
32
85
|
adminId: 0,
|
|
33
86
|
username: ctx.body.account,
|
|
34
87
|
nickname: "",
|
|
35
88
|
ip: ctx.ip || "",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
cpuArchitecture: uaData.cpuArchitecture,
|
|
89
|
+
clientType: body.clientType,
|
|
90
|
+
productName: body.productName || null,
|
|
91
|
+
productCode: body.productCode || null,
|
|
92
|
+
productVersion: body.productVersion || null,
|
|
93
|
+
clientVersion: body.clientVersion || null,
|
|
94
|
+
os: body.os || null,
|
|
95
|
+
system: body.system || null,
|
|
96
|
+
brand: body.brand || null,
|
|
97
|
+
model: body.model || null,
|
|
46
98
|
loginTime: Date.now(),
|
|
47
99
|
loginResult: 0,
|
|
48
100
|
failReason: ""
|
package/apis/tongJi/_tongJi.js
CHANGED
|
@@ -1,69 +1,120 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DAY_MS } from "#befly/utils/datetime.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
4
|
+
|
|
5
|
+
function getTimeZoneDateParts(timestamp, timeZone) {
|
|
6
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
7
|
+
timeZone: timeZone,
|
|
8
|
+
year: "numeric",
|
|
9
|
+
month: "2-digit",
|
|
10
|
+
day: "2-digit"
|
|
11
|
+
});
|
|
12
|
+
const parts = formatter.formatToParts(timestamp);
|
|
13
|
+
const result = {};
|
|
14
|
+
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (part.type !== "literal") {
|
|
17
|
+
result[part.type] = Number(part.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
2
23
|
|
|
3
|
-
|
|
4
|
-
|
|
24
|
+
function buildYmdNumber(year, month, day) {
|
|
25
|
+
return year * 10000 + month * 100 + day;
|
|
26
|
+
}
|
|
5
27
|
|
|
6
|
-
|
|
28
|
+
function normalizeTimeZone(timeZone) {
|
|
29
|
+
return typeof timeZone === "string" && timeZone.trim().length > 0 ? timeZone.trim() : DEFAULT_TIME_ZONE;
|
|
7
30
|
}
|
|
8
31
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
32
|
+
function findTimestampInDay(targetYmd, timeZone) {
|
|
33
|
+
const year = Math.floor(targetYmd / 10000);
|
|
34
|
+
const month = Math.floor((targetYmd % 10000) / 100);
|
|
35
|
+
const day = targetYmd % 100;
|
|
36
|
+
// 取当天正午作为锚点,再向前/后逐小时修正,确保落在目标日期内
|
|
37
|
+
let utc = Date.UTC(year, month - 1, day, 12, 0, 0, 0);
|
|
38
|
+
const ymdAtUtc = getTongJiDateYmdNumber(utc, timeZone);
|
|
39
|
+
|
|
40
|
+
if (ymdAtUtc === targetYmd) {
|
|
41
|
+
return utc;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const step = ymdAtUtc < targetYmd ? 3600000 : -3600000;
|
|
13
45
|
|
|
14
|
-
|
|
46
|
+
do {
|
|
47
|
+
utc += step;
|
|
48
|
+
} while (getTongJiDateYmdNumber(utc, timeZone) !== targetYmd);
|
|
49
|
+
|
|
50
|
+
return utc;
|
|
15
51
|
}
|
|
16
52
|
|
|
17
|
-
export function
|
|
18
|
-
const
|
|
53
|
+
export function getTongJiDateYmdNumber(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
54
|
+
const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
|
|
19
55
|
|
|
20
|
-
return
|
|
56
|
+
return buildYmdNumber(parts.year, parts.month, parts.day);
|
|
21
57
|
}
|
|
22
58
|
|
|
23
|
-
export function
|
|
24
|
-
const
|
|
59
|
+
export function getTongJiDayStartTime(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
60
|
+
const zone = normalizeTimeZone(timeZone);
|
|
61
|
+
const targetYmd = getTongJiDateYmdNumber(timestamp, zone);
|
|
62
|
+
const timestampInDay = findTimestampInDay(targetYmd, zone);
|
|
63
|
+
let low = timestampInDay - DAY_MS;
|
|
64
|
+
let high = timestampInDay + DAY_MS;
|
|
25
65
|
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
while (getTongJiDateYmdNumber(low, zone) >= targetYmd) {
|
|
67
|
+
low -= DAY_MS;
|
|
28
68
|
}
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
while (getTongJiDateYmdNumber(high, zone) <= targetYmd) {
|
|
71
|
+
high += DAY_MS;
|
|
72
|
+
}
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const start = new Date(Math.floor(startDate / 10000), Math.floor((startDate % 10000) / 100) - 1, startDate % 100);
|
|
36
|
-
const end = new Date(Math.floor(endDate / 10000), Math.floor((endDate % 10000) / 100) - 1, endDate % 100);
|
|
37
|
-
const current = new Date(start);
|
|
74
|
+
while (low + 1 < high) {
|
|
75
|
+
const mid = Math.floor((low + high) / 2);
|
|
38
76
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
77
|
+
if (getTongJiDateYmdNumber(mid, zone) < targetYmd) {
|
|
78
|
+
low = mid;
|
|
79
|
+
} else {
|
|
80
|
+
high = mid;
|
|
81
|
+
}
|
|
42
82
|
}
|
|
43
83
|
|
|
44
|
-
return
|
|
84
|
+
return high;
|
|
45
85
|
}
|
|
46
86
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
}
|
|
87
|
+
export function getTongJiMonthStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
|
|
88
|
+
const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
|
|
50
89
|
|
|
51
|
-
|
|
52
|
-
return `error:${periodType}:${periodValue}:count`;
|
|
90
|
+
return buildYmdNumber(parts.year, parts.month, 1);
|
|
53
91
|
}
|
|
54
92
|
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
export function getTongJiRecentDateList(timestamp = Date.now(), limit = 30, timeZone = DEFAULT_TIME_ZONE) {
|
|
94
|
+
const zone = normalizeTimeZone(timeZone);
|
|
95
|
+
const dayStart = getTongJiDayStartTime(timestamp, zone);
|
|
96
|
+
const list = [];
|
|
97
|
+
|
|
98
|
+
for (let i = limit - 1; i >= 0; i -= 1) {
|
|
99
|
+
list.push(getTongJiDateYmdNumber(dayStart - i * DAY_MS, zone));
|
|
100
|
+
}
|
|
58
101
|
|
|
59
|
-
|
|
60
|
-
return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
|
|
102
|
+
return list;
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
export function
|
|
64
|
-
return
|
|
105
|
+
export function getTongJiDayStartTimeForYmd(reportDate, timeZone = DEFAULT_TIME_ZONE) {
|
|
106
|
+
return getTongJiDayStartTime(findTimestampInDay(reportDate, normalizeTimeZone(timeZone)), timeZone);
|
|
65
107
|
}
|
|
66
108
|
|
|
67
|
-
export function
|
|
68
|
-
|
|
109
|
+
export function getTongJiDateRangeList(startDateYmd, endDateYmd, timeZone = DEFAULT_TIME_ZONE) {
|
|
110
|
+
const zone = normalizeTimeZone(timeZone);
|
|
111
|
+
const startDayStart = getTongJiDayStartTimeForYmd(startDateYmd, zone);
|
|
112
|
+
const endDayStart = getTongJiDayStartTimeForYmd(endDateYmd, zone);
|
|
113
|
+
const list = [];
|
|
114
|
+
|
|
115
|
+
for (let t = startDayStart; t <= endDayStart; t += DAY_MS) {
|
|
116
|
+
list.push(getTongJiDateYmdNumber(t, zone));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return list;
|
|
69
120
|
}
|
|
@@ -1,38 +1,9 @@
|
|
|
1
|
+
import { Logger } from "#befly/lib/logger.js";
|
|
1
2
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import { expireRedisKeys } from "./_tongJi.js";
|
|
4
|
+
import { getTongJiDateYmdNumber } from "./_tongJi.js";
|
|
5
5
|
|
|
6
|
-
const DAILY_STATS_REDIS_TTL_SECONDS =
|
|
7
|
-
|
|
8
|
-
function getDailyStatsMember(ctx) {
|
|
9
|
-
if (isValidPositiveInt(ctx?.userId)) {
|
|
10
|
-
return `user:${ctx.userId}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const clientId = ctx?.body?.clientId ?? "";
|
|
14
|
-
|
|
15
|
-
if (clientId) {
|
|
16
|
-
return `client:${clientId}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return `ip:${ctx?.ip || "unknown"}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function updateDailyStatsRedis(befly, reportDate, member, productCode) {
|
|
23
|
-
const dayKey = getDailyStatsDayMembersKey(reportDate);
|
|
24
|
-
const keys = [dayKey];
|
|
25
|
-
|
|
26
|
-
if (productCode) {
|
|
27
|
-
keys.push(getDailyStatsDayProductMembersKey(reportDate, productCode));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const results = await Promise.all(keys.map((key) => befly.redis.sadd(key, [member])));
|
|
31
|
-
|
|
32
|
-
await expireRedisKeys(befly, keys, DAILY_STATS_REDIS_TTL_SECONDS);
|
|
33
|
-
|
|
34
|
-
return results[0] > 0;
|
|
35
|
-
}
|
|
6
|
+
const DAILY_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
|
|
36
7
|
|
|
37
8
|
export default {
|
|
38
9
|
name: "上报日在线统计",
|
|
@@ -40,37 +11,88 @@ export default {
|
|
|
40
11
|
body: "none",
|
|
41
12
|
auth: false,
|
|
42
13
|
fields: {
|
|
43
|
-
|
|
44
|
-
pageName: { name: "页面名称", input: "string", min: 0, max: 100 },
|
|
45
|
-
source: { name: "来源", input: "string", min: 0, max: 50 },
|
|
14
|
+
clientType: { name: "客户端类型", input: "enum", check: "browser|vscode|desktop|miniapp|app", min: 3, max: 20 },
|
|
46
15
|
clientId: { name: "客户端标识", input: "string", min: 0, max: 120 },
|
|
47
16
|
productName: { name: "产品名称", input: "string", min: 0, max: 100 },
|
|
48
17
|
productCode: { name: "产品代号", input: "string", min: 0, max: 100 },
|
|
49
|
-
productVersion: { name: "产品版本", input: "string", min: 0, max: 50 }
|
|
18
|
+
productVersion: { name: "产品版本", input: "string", min: 0, max: 50 },
|
|
19
|
+
clientVersion: { name: "客户端版本", input: "string", min: 0, max: 100 },
|
|
20
|
+
os: { name: "操作系统", input: "string", min: 0, max: 100 },
|
|
21
|
+
deviceId: { name: "设备标识", input: "string", min: 0, max: 120 },
|
|
22
|
+
brand: { name: "设备品牌", input: "string", min: 0, max: 100 },
|
|
23
|
+
model: { name: "设备型号", input: "string", min: 0, max: 100 },
|
|
24
|
+
system: { name: "系统", input: "string", min: 0, max: 100 }
|
|
50
25
|
},
|
|
51
|
-
required: [],
|
|
26
|
+
required: ["clientType"],
|
|
52
27
|
handler: async (befly, ctx) => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const reportDate = getDailyDateYmdNumber(now, timeZone);
|
|
56
|
-
const productCode = ctx?.body?.productCode ?? "";
|
|
28
|
+
const reportTime = Date.now();
|
|
29
|
+
const reportDate = getTongJiDateYmdNumber(reportTime, befly.config?.tz);
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
31
|
+
const productCode = ctx.body?.productCode || "";
|
|
32
|
+
|
|
33
|
+
let memberMeta;
|
|
34
|
+
|
|
35
|
+
if (isValidPositiveInt(ctx?.userId)) {
|
|
36
|
+
memberMeta = { type: "user", value: `user:${ctx.userId}:${productCode}`, userId: ctx.userId };
|
|
37
|
+
} else {
|
|
38
|
+
const clientId = ctx.body?.clientId ?? "";
|
|
39
|
+
|
|
40
|
+
if (clientId) {
|
|
41
|
+
memberMeta = { type: "client", value: `client:${clientId}:${productCode}`, clientId: clientId };
|
|
42
|
+
} else {
|
|
43
|
+
memberMeta = { type: "ip", value: `ip:${ctx?.ip || "unknown"}:${productCode}`, ip: ctx?.ip || "unknown" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let counted = false;
|
|
48
|
+
|
|
49
|
+
if (befly.redis) {
|
|
50
|
+
const key = `daily:day:${reportDate}:members:${productCode}`;
|
|
51
|
+
const added = await befly.redis.sadd(key, [memberMeta.value]);
|
|
52
|
+
|
|
53
|
+
if (added > 0) {
|
|
54
|
+
await befly.redis.expire(key, DAILY_STATS_REDIS_TTL_SECONDS);
|
|
55
|
+
counted = true;
|
|
56
|
+
}
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
let stored = false;
|
|
60
|
+
|
|
61
|
+
if (counted && befly.mysql) {
|
|
62
|
+
const body = ctx.body || {};
|
|
63
|
+
|
|
64
|
+
const row = {
|
|
65
|
+
reportTime: reportTime,
|
|
66
|
+
reportDate: reportDate,
|
|
67
|
+
memberType: memberMeta.type,
|
|
68
|
+
member: memberMeta.value,
|
|
69
|
+
userId: memberMeta.userId || 0,
|
|
70
|
+
clientId: memberMeta.clientId || body.clientId || body.deviceId || "",
|
|
71
|
+
ip: ctx?.ip || "",
|
|
72
|
+
clientType: body.clientType,
|
|
73
|
+
productName: body.productName || "",
|
|
74
|
+
productCode: body.productCode || "",
|
|
75
|
+
productVersion: body.productVersion || "",
|
|
76
|
+
clientVersion: body.clientVersion || "",
|
|
77
|
+
os: body.os || "",
|
|
78
|
+
system: body.system || "",
|
|
79
|
+
brand: body.brand || "",
|
|
80
|
+
model: body.model || ""
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await befly.mysql.insData({ table: "beflyDailyReport", data: row });
|
|
85
|
+
stored = true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
Logger.error("daily_report 落盘失败", error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
68
90
|
|
|
69
91
|
return befly.tool.Yes("上报成功", {
|
|
70
|
-
reportTime:
|
|
92
|
+
reportTime: reportTime,
|
|
71
93
|
reportDate: reportDate,
|
|
72
94
|
counted: counted,
|
|
73
|
-
stored:
|
|
95
|
+
stored: stored
|
|
74
96
|
});
|
|
75
97
|
}
|
|
76
98
|
};
|