befly 3.62.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,136 +1,120 @@
1
- import { getDailyDateRangeList, getDailyDateYmdNumber, getDailyMonthStartDate, getDailyRecentDateList, getDailyStatsDayMembersKey, getDailyStatsDayProductMembersKey, getDailyWeekStartDate } from "./_daily.js";
1
+ import { getTongJiDateYmdNumber, getTongJiMonthStartDate, getTongJiRecentDateList } from "./_tongJi.js";
2
2
 
3
3
  const DAILY_STATS_DAY_LIMIT = 30;
4
+ const DAILY_STATS_HISTORY_CACHE_TTL_SECONDS = 2 * 24 * 60 * 60;
4
5
 
5
- async function getRedisSetCount(befly, key) {
6
- const count = await befly.redis.scard(key);
6
+ function getPreviousMonthDateRange(reportDate) {
7
+ const year = Math.floor(reportDate / 10000);
8
+ const month = Math.floor((reportDate % 10000) / 100);
9
+ const previousYear = month === 1 ? year - 1 : year;
10
+ const previousMonth = month === 1 ? 12 : month - 1;
11
+ const lastDay = new Date(Date.UTC(year, month - 1, 0)).getUTCDate();
7
12
 
8
- return Number.isFinite(Number(count)) ? Number(count) : 0;
13
+ return [previousYear * 10000 + previousMonth * 100 + 1, previousYear * 10000 + previousMonth * 100 + lastDay];
9
14
  }
10
15
 
11
- function getDayMembersKey(reportDate, productCode) {
12
- return productCode ? getDailyStatsDayProductMembersKey(reportDate, productCode) : getDailyStatsDayMembersKey(reportDate);
13
- }
14
-
15
- async function getDayCount(befly, reportDate, productCode) {
16
- return {
17
- reportDate: reportDate,
18
- count: await getRedisSetCount(befly, getDayMembersKey(reportDate, productCode))
19
- };
20
- }
16
+ async function loadHistory(befly, reportDate, dateStats) {
17
+ const cacheKey = `daily:stats:history:${reportDate}`;
21
18
 
22
- async function sumDayCounts(befly, dateList, productCode) {
23
- const counts = await Promise.all(dateList.map((date) => getRedisSetCount(befly, getDayMembersKey(date, productCode))));
19
+ if (befly.redis) {
20
+ const cached = await befly.redis.getObject(cacheKey);
24
21
 
25
- return counts.reduce((sum, count) => sum + count, 0);
26
- }
22
+ if (cached) {
23
+ return cached;
24
+ }
25
+ }
27
26
 
28
- async function getDayCounts(befly, dateList, productCode) {
29
- return Promise.all(dateList.map((date) => getDayCount(befly, date, productCode)));
30
- }
27
+ if (!befly.mysql) {
28
+ return {};
29
+ }
31
30
 
32
- function buildDateRangeSummary(startDate, endDate, count) {
33
- return {
34
- startDate: startDate,
35
- endDate: endDate,
36
- count: count
37
- };
38
- }
31
+ const res = await befly.mysql.execute("SELECT product_code as productCode, report_date as reportDate, COUNT(*) as count FROM befly_daily_report WHERE state > 0 AND report_date BETWEEN ? AND ? GROUP BY product_code, report_date", [dateStats.previousMonthStartDate, dateStats.yesterdayDate]);
32
+ const history = {};
33
+
34
+ for (const item of res.data || []) {
35
+ const code = String(item.productCode || "");
36
+ const date = Number(item.reportDate);
37
+ const count = Number(item.count) || 0;
38
+ const stats = history[code] || {
39
+ yesterday: 0,
40
+ dayBeforeYesterday: 0,
41
+ month: 0,
42
+ lastMonth: 0
43
+ };
39
44
 
40
- function buildDaySummary(reportDate, count) {
41
- return {
42
- reportDate: reportDate,
43
- count: count
44
- };
45
- }
45
+ if (date === dateStats.yesterdayDate) {
46
+ stats.yesterday = count;
47
+ }
48
+ if (date === dateStats.dayBeforeYesterdayDate) {
49
+ stats.dayBeforeYesterday = count;
50
+ }
51
+ if (date >= dateStats.monthStartDate && date <= dateStats.yesterdayDate) {
52
+ stats.month += count;
53
+ }
54
+ if (date >= dateStats.previousMonthStartDate && date <= dateStats.previousMonthEndDate) {
55
+ stats.lastMonth += count;
56
+ }
46
57
 
47
- async function getDailyStatsSummary(befly, params) {
48
- const { reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList, productCode } = params;
49
- const timeZone = befly.config?.tz;
50
- const weekDateList = getDailyDateRangeList(weekStartDate, reportDate, timeZone);
51
- const monthDateList = recentDateList.filter((date) => date >= monthStartDate);
52
-
53
- const [today, yesterday, dayBeforeYesterday, week, month, recent7, recent30, days] = await Promise.all([
54
- getDayCount(befly, reportDate, productCode),
55
- getDayCount(befly, recentDateList[recentDateList.length - 2] || reportDate, productCode),
56
- getDayCount(befly, recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || reportDate, productCode),
57
- sumDayCounts(befly, weekDateList, productCode).then((count) => buildDateRangeSummary(weekStartDate, reportDate, count)),
58
- sumDayCounts(befly, monthDateList, productCode).then((count) => buildDateRangeSummary(monthStartDate, reportDate, count)),
59
- sumDayCounts(befly, recent7DateList, productCode).then((count) => buildDateRangeSummary(recent7DateList[0], reportDate, count)),
60
- sumDayCounts(befly, recentDateList, productCode).then((count) => buildDateRangeSummary(recentDateList[0], reportDate, count)),
61
- getDayCounts(befly, recentDateList, productCode)
62
- ]);
58
+ history[code] = stats;
59
+ }
63
60
 
64
- return {
65
- today: today,
66
- yesterday: yesterday,
67
- dayBeforeYesterday: dayBeforeYesterday,
68
- week: week,
69
- month: month,
70
- recent7: recent7,
71
- recent30: recent30,
72
- days: days
73
- };
74
- }
61
+ if (befly.redis) {
62
+ await befly.redis.setObject(cacheKey, history, DAILY_STATS_HISTORY_CACHE_TTL_SECONDS);
63
+ }
75
64
 
76
- function buildEmptyDays(recentDateList) {
77
- return recentDateList.map((reportDate) => {
78
- return {
79
- reportDate: reportDate,
80
- count: 0
81
- };
82
- });
65
+ return history;
83
66
  }
84
67
 
85
- function buildEmptySummary(reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList) {
86
- const days = buildEmptyDays(recentDateList);
87
- const yesterdayDate = recentDateList[recentDateList.length - 2] || reportDate;
88
- const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
68
+ async function getTodayCount(befly, reportDate, productCode) {
69
+ if (!befly.redis) {
70
+ return 0;
71
+ }
89
72
 
90
- return {
91
- today: buildDaySummary(reportDate, 0),
92
- yesterday: buildDaySummary(yesterdayDate, 0),
93
- dayBeforeYesterday: buildDaySummary(dayBeforeYesterdayDate, 0),
94
- week: buildDateRangeSummary(weekStartDate, reportDate, 0),
95
- month: buildDateRangeSummary(monthStartDate, reportDate, 0),
96
- recent7: buildDateRangeSummary(recent7DateList[0], reportDate, 0),
97
- recent30: buildDateRangeSummary(recentDateList[0], reportDate, 0),
98
- days: days
99
- };
73
+ return Number(await befly.redis.scard(`daily:day:${reportDate}:members:${productCode}`)) || 0;
100
74
  }
101
75
 
102
- function buildProductSummary(item, summary) {
76
+ function buildProductSummary(item, stats, todayCount) {
103
77
  return {
104
78
  key: item.code,
105
79
  productName: item.name,
106
80
  productCode: item.code,
107
- today: summary.today.count,
108
- yesterday: summary.yesterday.count,
109
- dayBeforeYesterday: summary.dayBeforeYesterday.count,
110
- week: summary.week.count,
111
- month: summary.month.count,
112
- recent7: summary.recent7.count,
113
- recent30: summary.recent30.count,
114
- days: summary.days
81
+ today: todayCount,
82
+ yesterday: stats.yesterday,
83
+ dayBeforeYesterday: stats.dayBeforeYesterday,
84
+ month: stats.month,
85
+ lastMonth: stats.lastMonth
115
86
  };
116
87
  }
117
88
 
118
- async function buildDailyStatsProducts(befly, params, projectLists) {
119
- const summaries = await Promise.all(
120
- projectLists.map((item) => {
121
- return getDailyStatsSummary(befly, { ...params, productCode: item.code }).then((summary) => buildProductSummary(item, summary));
89
+ async function buildDailyStatsProducts(befly, projectLists, history, dateStats) {
90
+ const products = await Promise.all(
91
+ projectLists.map(async (item) => {
92
+ const stats = history[item.code] || { yesterday: 0, dayBeforeYesterday: 0, month: 0, lastMonth: 0 };
93
+ const todayCount = await getTodayCount(befly, dateStats.reportDate, item.code);
94
+
95
+ return buildProductSummary(item, stats, todayCount);
122
96
  })
123
97
  );
124
98
 
125
- return summaries.toSorted((a, b) => {
126
- if (b.recent30 !== a.recent30) {
127
- return b.recent30 - a.recent30;
99
+ return products.toSorted((a, b) => {
100
+ if (b.today !== a.today) {
101
+ return b.today - a.today;
128
102
  }
129
103
 
130
- return String(a.productName).localeCompare(String(b.productName), "zh-CN");
104
+ return a.productName.localeCompare(b.productName, "zh-CN");
131
105
  });
132
106
  }
133
107
 
108
+ function buildFullSummary(stats, dateStats, todayCount) {
109
+ return {
110
+ today: { reportDate: dateStats.reportDate, count: todayCount },
111
+ yesterday: { reportDate: dateStats.yesterdayDate, count: stats.yesterday },
112
+ dayBeforeYesterday: { reportDate: dateStats.dayBeforeYesterdayDate, count: stats.dayBeforeYesterday },
113
+ month: { startDate: dateStats.monthStartDate, endDate: dateStats.yesterdayDate, count: stats.month },
114
+ lastMonth: { startDate: dateStats.previousMonthStartDate, endDate: dateStats.previousMonthEndDate, count: stats.lastMonth }
115
+ };
116
+ }
117
+
134
118
  export default {
135
119
  name: "获取日在线统计",
136
120
  method: "POST",
@@ -142,36 +126,25 @@ export default {
142
126
  required: [],
143
127
  handler: async (befly, ctx) => {
144
128
  const now = Date.now();
145
- const timeZone = befly.config?.tz;
146
- const reportDate = getDailyDateYmdNumber(now, timeZone);
147
- const monthStartDate = getDailyMonthStartDate(now, timeZone);
148
- const weekStartDate = getDailyWeekStartDate(now, timeZone);
149
- const recentDateList = getDailyRecentDateList(now, DAILY_STATS_DAY_LIMIT, timeZone);
150
- const recent7DateList = getDailyRecentDateList(now, 7, timeZone);
151
- const productCode = ctx?.body?.productCode ?? "";
129
+ const reportDate = getTongJiDateYmdNumber(now, befly.config?.tz);
130
+ const monthStartDate = getTongJiMonthStartDate(now, befly.config?.tz);
131
+ const [previousMonthStartDate, previousMonthEndDate] = getPreviousMonthDateRange(reportDate);
132
+ const recentDateList = getTongJiRecentDateList(now, DAILY_STATS_DAY_LIMIT, befly.config?.tz);
133
+ const productCode = ctx.body?.productCode ?? "";
152
134
  const projectLists = productCode ? [] : befly.config?.projectLists || [];
153
- const params = {
135
+ const dateStats = {
154
136
  reportDate: reportDate,
137
+ yesterdayDate: recentDateList[recentDateList.length - 2],
138
+ dayBeforeYesterdayDate: recentDateList[recentDateList.length - 3],
155
139
  monthStartDate: monthStartDate,
156
- weekStartDate: weekStartDate,
157
- recentDateList: recentDateList,
158
- recent7DateList: recent7DateList,
159
- productCode: productCode
140
+ previousMonthStartDate: previousMonthStartDate,
141
+ previousMonthEndDate: previousMonthEndDate,
142
+ recentDateList: recentDateList
160
143
  };
161
-
162
- if (!befly.redis) {
163
- const summary = buildEmptySummary(reportDate, monthStartDate, weekStartDate, recentDateList, recent7DateList);
164
- const products = projectLists.map((item) => buildProductSummary(item, summary));
165
-
166
- return befly.tool.Yes("获取成功", {
167
- queryTime: now,
168
- ...summary,
169
- products: products
170
- });
171
- }
172
-
173
- const summary = await getDailyStatsSummary(befly, params);
174
- const products = await buildDailyStatsProducts(befly, params, projectLists);
144
+ const history = await loadHistory(befly, reportDate, dateStats);
145
+ const todayCount = productCode ? await getTodayCount(befly, reportDate, productCode) : 0;
146
+ const summary = productCode ? buildFullSummary(history[productCode] || { yesterday: 0, dayBeforeYesterday: 0, month: 0, lastMonth: 0 }, dateStats, todayCount) : null;
147
+ const products = await buildDailyStatsProducts(befly, projectLists, history, dateStats);
175
148
 
176
149
  return befly.tool.Yes("获取成功", {
177
150
  queryTime: now,
@@ -0,0 +1,86 @@
1
+ import { snakeCase } from "#befly/utils/util.js";
2
+
3
+ import { getTongJiDateYmdNumber, getTongJiMonthStartDate, getTongJiRecentDateList } from "./_tongJi.js";
4
+
5
+ const DISTRIBUTION_DIMENSIONS = ["clientType", "os", "brand", "model", "productVersion"];
6
+ const DAILY_STATS_DISTRIBUTION_DAY_LIMIT = 30;
7
+
8
+ function getDateRangeBounds(dateRange, now, timeZone) {
9
+ const reportDate = getTongJiDateYmdNumber(now, timeZone);
10
+ const recentDateList = getTongJiRecentDateList(now, DAILY_STATS_DISTRIBUTION_DAY_LIMIT, timeZone);
11
+
12
+ if (dateRange === "today") {
13
+ return [reportDate, reportDate];
14
+ }
15
+
16
+ if (dateRange === "yesterday") {
17
+ const yesterdayDate = recentDateList[recentDateList.length - 2];
18
+
19
+ return [yesterdayDate, yesterdayDate];
20
+ }
21
+
22
+ if (dateRange === "dayBeforeYesterday") {
23
+ const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3];
24
+
25
+ return [dayBeforeYesterdayDate, dayBeforeYesterdayDate];
26
+ }
27
+
28
+ if (dateRange === "recent30") {
29
+ return [recentDateList[0], recentDateList[recentDateList.length - 1]];
30
+ }
31
+
32
+ const monthStartDate = getTongJiMonthStartDate(now, timeZone);
33
+
34
+ return [monthStartDate, reportDate];
35
+ }
36
+
37
+ async function queryDimensionDistribution(befly, dimension, startDate, endDate, productCode) {
38
+ const snakeField = snakeCase(dimension);
39
+ const result = await befly.mysql.execute(`SELECT ${snakeField} as name, COUNT(*) as count FROM befly_daily_report WHERE state > 0 AND report_date BETWEEN ? AND ? AND product_code = ? GROUP BY ${snakeField} ORDER BY count DESC`, [startDate, endDate, productCode]);
40
+
41
+ return (result.data || []).map((item) => {
42
+ return {
43
+ name: String(item.name || "未知"),
44
+ count: Number(item.count) || 0
45
+ };
46
+ });
47
+ }
48
+
49
+ function buildEmptyDistribution() {
50
+ const result = {};
51
+
52
+ for (const dimension of DISTRIBUTION_DIMENSIONS) {
53
+ result[dimension] = [];
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ export default {
60
+ name: "获取日在线分布统计",
61
+ method: "POST",
62
+ body: "none",
63
+ auth: true,
64
+ fields: {
65
+ productCode: { name: "产品代号", input: "string", min: 1, max: 100 },
66
+ dateRange: { name: "日期范围", input: "enum", check: "today|yesterday|dayBeforeYesterday|recent30|month", min: 1, max: 20 }
67
+ },
68
+ required: ["productCode", "dateRange"],
69
+ handler: async (befly, ctx) => {
70
+ const productCode = ctx.body?.productCode ?? "";
71
+ const dateRange = ctx.body?.dateRange ?? "";
72
+
73
+ if (!befly.mysql) {
74
+ return befly.tool.Yes("获取成功", buildEmptyDistribution());
75
+ }
76
+
77
+ const [startDate, endDate] = getDateRangeBounds(dateRange, Date.now(), befly.config?.tz);
78
+ const result = {};
79
+
80
+ for (const dimension of DISTRIBUTION_DIMENSIONS) {
81
+ result[dimension] = await queryDimensionDistribution(befly, dimension, startDate, endDate, productCode);
82
+ }
83
+
84
+ return befly.tool.Yes("获取成功", result);
85
+ }
86
+ };
@@ -1,9 +1,9 @@
1
1
  import { queryFields } from "#befly/apis/_apis.js";
2
2
  import errorReportTable from "#befly/tables/errorReport.json";
3
3
 
4
- const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate", "hitCount"];
5
- const FILTER_FIELDS = ["errorType", "source", "productName", "productCode", "productVersion", "deviceType", "browserName", "osName"];
6
- const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "productName", "productCode", "productVersion", "browserName", "osName", "deviceType", "deviceVendor", "deviceModel"];
4
+ const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate"];
5
+ const FILTER_FIELDS = ["errorType", "productName", "productCode", "productVersion"];
6
+ const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "productName", "productCode", "productVersion"];
7
7
 
8
8
  export default {
9
9
  name: "获取错误报告列表",
@@ -13,13 +13,9 @@ export default {
13
13
  fields: {
14
14
  ...queryFields,
15
15
  errorType: errorReportTable.errorType,
16
- source: errorReportTable.source,
17
16
  productName: errorReportTable.productName,
18
17
  productCode: errorReportTable.productCode,
19
- productVersion: errorReportTable.productVersion,
20
- deviceType: errorReportTable.deviceType,
21
- browserName: errorReportTable.browserName,
22
- osName: errorReportTable.osName
18
+ productVersion: errorReportTable.productVersion
23
19
  },
24
20
  required: [],
25
21
  handler: async (befly, ctx) => {
@@ -65,7 +61,7 @@ export default {
65
61
  const row = Object.assign({}, item);
66
62
 
67
63
  for (const field of NUMBER_FIELDS) {
68
- row[field] = Number(item[field] || 0) || (field === "hitCount" ? 1 : 0);
64
+ row[field] = Number(item[field]) || 0;
69
65
  }
70
66
 
71
67
  return row;
@@ -1,43 +1,27 @@
1
+ import { Logger } from "#befly/lib/logger.js";
1
2
  import errorReportTable from "#befly/tables/errorReport.json";
2
3
  import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
3
- import { parseUserAgent } from "#befly/utils/userAgent.js";
4
-
5
- import { expireRedisKeys, getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
4
+ import { isValidPositiveInt } from "#befly/utils/is.js";
6
5
 
7
6
  const ERROR_STATS_BUCKET_MS = 30 * 60 * 1000;
8
- const ERROR_STATS_REDIS_TTL_SECONDS = 45 * 24 * 60 * 60;
9
-
10
- function getErrorReportUaData(ctx) {
11
- let userAgent = "";
7
+ const ERROR_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
12
8
 
13
- if (typeof ctx.req?.headers?.get === "function") {
14
- userAgent = ctx.req.headers.get("user-agent") || "";
15
- } else if (typeof ctx.headers?.get === "function") {
16
- userAgent = ctx.headers.get("user-agent") || "";
9
+ function extractErrorMessage(message) {
10
+ if (typeof message !== "string" || message.length === 0) {
11
+ return "";
17
12
  }
18
13
 
19
- return parseUserAgent(userAgent);
20
- }
21
-
22
- async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
23
- if (!befly.redis) {
24
- return;
25
- }
14
+ const lines = message.split(/\r?\n/);
26
15
 
27
- const weekStartDate = getTongJiWeekStartDate(now);
28
- const monthStartDate = getTongJiMonthStartDate(now);
29
- const safeErrorType = String(errorType || "unknown");
30
- const dayCountKey = getErrorStatsPeriodCountKey("day", bucketDate);
31
- const weekCountKey = getErrorStatsPeriodCountKey("week", weekStartDate);
32
- const monthCountKey = getErrorStatsPeriodCountKey("month", monthStartDate);
33
- const dayBucketsKey = getErrorStatsDayBucketsKey(bucketDate);
34
- const dayBucketCountKey = getErrorStatsDayBucketCountKey(bucketDate, bucketTime);
35
- const dayTypesKey = getErrorStatsDayTypesKey(bucketDate);
36
- const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate, safeErrorType);
16
+ for (const line of lines) {
17
+ const trimmed = line.trim();
37
18
 
38
- await Promise.all([befly.redis.incr(dayCountKey), befly.redis.incr(weekCountKey), befly.redis.incr(monthCountKey), befly.redis.sadd(dayBucketsKey, [String(bucketTime)]), befly.redis.incr(dayBucketCountKey), befly.redis.sadd(dayTypesKey, [safeErrorType]), befly.redis.incr(dayTypeCountKey)]);
19
+ if (trimmed.length > 0) {
20
+ return trimmed;
21
+ }
22
+ }
39
23
 
40
- await expireRedisKeys(befly, [dayCountKey, weekCountKey, monthCountKey, dayBucketsKey, dayBucketCountKey, dayTypesKey, dayTypeCountKey], ERROR_STATS_REDIS_TTL_SECONDS);
24
+ return "";
41
25
  }
42
26
 
43
27
  export default {
@@ -46,9 +30,10 @@ export default {
46
30
  body: "none",
47
31
  auth: false,
48
32
  fields: {
33
+ clientType: errorReportTable.clientType,
34
+ clientId: errorReportTable.clientId,
49
35
  pagePath: errorReportTable.pagePath,
50
36
  pageName: errorReportTable.pageName,
51
- source: errorReportTable.source,
52
37
  productName: errorReportTable.productName,
53
38
  productCode: errorReportTable.productCode,
54
39
  productVersion: errorReportTable.productVersion,
@@ -56,33 +41,85 @@ export default {
56
41
  message: errorReportTable.message,
57
42
  detail: errorReportTable.detail
58
43
  },
59
- required: [],
44
+ required: ["clientType"],
60
45
  handler: async (befly, ctx) => {
61
46
  const now = Date.now();
62
47
  const bucketTime = getTimeBucketStart(now, ERROR_STATS_BUCKET_MS);
63
48
  const bucketDate = getDateYmdNumber(now);
64
- const uaData = getErrorReportUaData(ctx);
65
49
 
66
- await befly.mysql.insData({
67
- table: "beflyErrorReport",
68
- data: {
69
- ...ctx.body,
70
- ...uaData,
50
+ const body = ctx.body || {};
51
+ const productCode = body.productCode || "";
52
+ const rawMessage = body.message || "";
53
+ const extractedMessage = extractErrorMessage(rawMessage);
54
+
55
+ let memberMeta;
56
+
57
+ if (isValidPositiveInt(ctx?.userId)) {
58
+ memberMeta = { type: "user", value: `user:${ctx.userId}:${productCode}`, userId: ctx.userId };
59
+ } else {
60
+ const clientId = body.clientId || ctx?.ip || "";
61
+
62
+ if (clientId) {
63
+ memberMeta = { type: "client", value: `client:${clientId}:${productCode}`, clientId: clientId };
64
+ } else {
65
+ memberMeta = { type: "ip", value: `ip:${ctx?.ip || "unknown"}:${productCode}`, ip: ctx?.ip || "unknown" };
66
+ }
67
+ }
68
+
69
+ const dedupMember = [body.errorType, extractedMessage, productCode, memberMeta.value].map((item) => encodeURIComponent(String(item || ""))).join(":");
70
+ let first = false;
71
+
72
+ if (befly.redis) {
73
+ const key = `error:day:${bucketDate}:dedup:${productCode}`;
74
+ const added = await befly.redis.sadd(key, [dedupMember]);
75
+
76
+ if (added > 0) {
77
+ await befly.redis.expire(key, ERROR_STATS_REDIS_TTL_SECONDS);
78
+ first = true;
79
+ }
80
+ }
81
+
82
+ let stored = false;
83
+
84
+ if (first && befly.mysql) {
85
+ const userId = isValidPositiveInt(ctx.userId) ? ctx.userId : null;
86
+
87
+ const data = {
88
+ clientType: body.clientType,
89
+ clientId: memberMeta.clientId || body.clientId || null,
90
+ pagePath: body.pagePath || null,
91
+ pageName: body.pageName || null,
92
+ productName: body.productName || null,
93
+ productCode: body.productCode || null,
94
+ productVersion: body.productVersion || null,
95
+ errorType: body.errorType || null,
96
+ message: rawMessage || null,
97
+ extractedMessage: extractedMessage || null,
98
+ detail: body.detail || null,
99
+ userId: userId,
100
+ memberType: memberMeta.type,
101
+ member: memberMeta.value,
102
+ ip: ctx?.ip || null,
71
103
  reportTime: now,
72
104
  firstReportTime: now,
105
+ lastReportTime: now,
73
106
  bucketTime: bucketTime,
74
- bucketDate: bucketDate,
75
- hitCount: 1
76
- }
77
- });
107
+ bucketDate: bucketDate
108
+ };
78
109
 
79
- await updateErrorStatsRedis(befly, now, bucketDate, bucketTime, ctx.body.errorType || "unknown");
110
+ try {
111
+ await befly.mysql.insData({ table: "errorReport", data: data });
112
+ stored = true;
113
+ } catch (error) {
114
+ Logger.error("errorReport 插入失败", error);
115
+ }
116
+ }
80
117
 
81
118
  return befly.tool.Yes("上报成功", {
82
119
  reportTime: now,
83
120
  bucketTime: bucketTime,
84
121
  bucketDate: bucketDate,
85
- stored: true
122
+ stored: stored
86
123
  });
87
124
  }
88
125
  };