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.
@@ -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 userAgent = ctx.req.headers.get("user-agent") || "";
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
- userAgent: userAgent.substring(0, 500),
37
- browserName: uaData.browserName,
38
- browserVersion: uaData.browserVersion,
39
- osName: uaData.osName,
40
- osVersion: uaData.osVersion,
41
- deviceType: uaData.deviceType,
42
- deviceVendor: uaData.deviceVendor,
43
- deviceModel: uaData.deviceModel,
44
- engineName: uaData.engineName,
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: ""
@@ -1,69 +1,120 @@
1
- import { addDays, getDateYmdNumber } from "#befly/utils/datetime.js";
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
- export function getTongJiNumber(value) {
4
- const num = Number(value);
24
+ function buildYmdNumber(year, month, day) {
25
+ return year * 10000 + month * 100 + day;
26
+ }
5
27
 
6
- return Number.isFinite(num) ? num : 0;
28
+ function normalizeTimeZone(timeZone) {
29
+ return typeof timeZone === "string" && timeZone.trim().length > 0 ? timeZone.trim() : DEFAULT_TIME_ZONE;
7
30
  }
8
31
 
9
- export function getTongJiWeekStartDate(timestamp = Date.now()) {
10
- const date = Reflect.construct(Date, [timestamp]);
11
- const day = date.getDay();
12
- const offset = day === 0 ? -6 : 1 - day;
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
- return getDateYmdNumber(addDays(timestamp, offset));
46
+ do {
47
+ utc += step;
48
+ } while (getTongJiDateYmdNumber(utc, timeZone) !== targetYmd);
49
+
50
+ return utc;
15
51
  }
16
52
 
17
- export function getTongJiMonthStartDate(timestamp = Date.now()) {
18
- const date = Reflect.construct(Date, [timestamp]);
53
+ export function getTongJiDateYmdNumber(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
54
+ const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
19
55
 
20
- return getDateYmdNumber(Reflect.construct(Date, [date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0]).getTime());
56
+ return buildYmdNumber(parts.year, parts.month, parts.day);
21
57
  }
22
58
 
23
- export function getTongJiRecentDateList(now = Date.now(), limit = 30) {
24
- const list = [];
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
- for (let i = limit - 1; i >= 0; i -= 1) {
27
- list.push(getDateYmdNumber(addDays(now, -i)));
66
+ while (getTongJiDateYmdNumber(low, zone) >= targetYmd) {
67
+ low -= DAY_MS;
28
68
  }
29
69
 
30
- return list;
31
- }
70
+ while (getTongJiDateYmdNumber(high, zone) <= targetYmd) {
71
+ high += DAY_MS;
72
+ }
32
73
 
33
- export function getTongJiDateRangeList(startDate, endDate) {
34
- const list = [];
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
- while (current.getTime() <= end.getTime()) {
40
- list.push(getDateYmdNumber(current.getTime()));
41
- current.setDate(current.getDate() + 1);
77
+ if (getTongJiDateYmdNumber(mid, zone) < targetYmd) {
78
+ low = mid;
79
+ } else {
80
+ high = mid;
81
+ }
42
82
  }
43
83
 
44
- return list;
84
+ return high;
45
85
  }
46
86
 
47
- export async function expireRedisKeys(befly, keys, ttl) {
48
- await Promise.all(keys.map((key) => befly.redis.expire(key, ttl)));
49
- }
87
+ export function getTongJiMonthStartDate(timestamp = Date.now(), timeZone = DEFAULT_TIME_ZONE) {
88
+ const parts = getTimeZoneDateParts(timestamp, normalizeTimeZone(timeZone));
50
89
 
51
- export function getErrorStatsPeriodCountKey(periodType, periodValue) {
52
- return `error:${periodType}:${periodValue}:count`;
90
+ return buildYmdNumber(parts.year, parts.month, 1);
53
91
  }
54
92
 
55
- export function getErrorStatsDayBucketsKey(bucketDate) {
56
- return `error:day:${bucketDate}:buckets`;
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
- export function getErrorStatsDayBucketCountKey(bucketDate, bucketTime) {
60
- return `error:day:${bucketDate}:bucket:${bucketTime}:count`;
102
+ return list;
61
103
  }
62
104
 
63
- export function getErrorStatsDayTypesKey(bucketDate) {
64
- return `error:day:${bucketDate}:types`;
105
+ export function getTongJiDayStartTimeForYmd(reportDate, timeZone = DEFAULT_TIME_ZONE) {
106
+ return getTongJiDayStartTime(findTimestampInDay(reportDate, normalizeTimeZone(timeZone)), timeZone);
65
107
  }
66
108
 
67
- export function getErrorStatsDayTypeCountKey(bucketDate, errorType) {
68
- return `error:day:${bucketDate}:type:${encodeURIComponent(String(errorType || "unknown"))}:count`;
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 { getDailyDateYmdNumber, getDailyStatsDayMembersKey, getDailyStatsDayProductMembersKey } from "./_daily.js";
4
- import { expireRedisKeys } from "./_tongJi.js";
4
+ import { getTongJiDateYmdNumber } from "./_tongJi.js";
5
5
 
6
- const DAILY_STATS_REDIS_TTL_SECONDS = 90 * 24 * 60 * 60;
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
- pagePath: { name: "页面路径", input: "string", min: 0, max: 200 },
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 now = Date.now();
54
- const timeZone = befly.config?.tz;
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
- if (!befly.redis) {
59
- return befly.tool.Yes("上报成功", {
60
- reportTime: now,
61
- reportDate: reportDate,
62
- counted: false,
63
- stored: false
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
- const counted = await updateDailyStatsRedis(befly, reportDate, getDailyStatsMember(ctx), productCode);
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: now,
92
+ reportTime: reportTime,
71
93
  reportDate: reportDate,
72
94
  counted: counted,
73
- stored: true
95
+ stored: stored
74
96
  });
75
97
  }
76
98
  };