befly 3.24.18 → 3.24.20

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.
Files changed (71) hide show
  1. package/apis/_apis.js +20 -0
  2. package/apis/admin/delete.js +3 -1
  3. package/apis/admin/detail.js +1 -3
  4. package/apis/admin/select.js +9 -7
  5. package/apis/admin/update.js +2 -2
  6. package/apis/api/all.js +6 -11
  7. package/apis/api/select.js +18 -19
  8. package/apis/dashboard/environmentInfo.js +6 -1
  9. package/apis/dashboard/serviceStatus.js +78 -60
  10. package/apis/dict/_dict.js +24 -0
  11. package/apis/dict/all.js +6 -4
  12. package/apis/dict/detail.js +9 -5
  13. package/apis/dict/insert.js +4 -11
  14. package/apis/dict/items.js +5 -6
  15. package/apis/dict/select.js +13 -9
  16. package/apis/dict/update.js +9 -13
  17. package/apis/dictType/select.js +4 -4
  18. package/apis/email/config.js +9 -11
  19. package/apis/email/logList.js +14 -4
  20. package/apis/email/send.js +10 -3
  21. package/apis/email/verify.js +9 -7
  22. package/apis/loginLog/select.js +11 -4
  23. package/apis/menu/all.js +13 -19
  24. package/apis/menu/select.js +19 -14
  25. package/apis/operateLog/select.js +13 -4
  26. package/apis/role/_role.js +21 -0
  27. package/apis/role/all.js +1 -3
  28. package/apis/role/apiSave.js +8 -15
  29. package/apis/role/apis.js +4 -10
  30. package/apis/role/delete.js +28 -36
  31. package/apis/role/detail.js +4 -10
  32. package/apis/role/insert.js +12 -10
  33. package/apis/role/menuSave.js +9 -15
  34. package/apis/role/menus.js +4 -10
  35. package/apis/role/save.js +19 -23
  36. package/apis/role/select.js +12 -9
  37. package/apis/role/update.js +14 -15
  38. package/apis/source/imageList.js +3 -3
  39. package/apis/sysConfig/get.js +11 -23
  40. package/apis/sysConfig/insert.js +22 -27
  41. package/apis/sysConfig/select.js +11 -5
  42. package/apis/sysConfig/update.js +33 -38
  43. package/apis/tongJi/_tongJi.js +41 -0
  44. package/apis/tongJi/cacheHealth.js +192 -0
  45. package/apis/tongJi/errorList.js +26 -27
  46. package/apis/tongJi/errorReport.js +72 -18
  47. package/apis/tongJi/errorStats.js +154 -30
  48. package/apis/tongJi/fallbackReset.js +61 -0
  49. package/apis/tongJi/infoReport.js +112 -24
  50. package/apis/tongJi/infoStats.js +164 -84
  51. package/apis/tongJi/onlineReport.js +58 -73
  52. package/apis/tongJi/onlineStats.js +140 -151
  53. package/checks/config.js +44 -1
  54. package/configs/beflyConfig.json +10 -1
  55. package/hooks/permission.js +6 -2
  56. package/hooks/rateLimit.js +245 -0
  57. package/index.js +25 -0
  58. package/lib/cacheHelper.js +105 -60
  59. package/lib/dbHelper.js +1 -1
  60. package/lib/dbParse.js +61 -99
  61. package/lib/dbUtil.js +101 -21
  62. package/lib/redisHelper.js +93 -0
  63. package/lib/sqlBuilder.js +6 -0
  64. package/package.json +2 -2
  65. package/plugins/email.js +4 -3
  66. package/utils/email.js +3 -0
  67. package/apis/admin/cacheRefresh.js +0 -122
  68. package/apis/dashboard/configStatus.js +0 -39
  69. package/apis/dashboard/performanceMetrics.js +0 -20
  70. package/apis/dashboard/permissionStats.js +0 -27
  71. package/apis/dashboard/systemInfo.js +0 -19
@@ -1,41 +1,11 @@
1
- import { addDays, getDateYmdNumber } from "#root/utils/datetime.js";
1
+ import { getDateYmdNumber } from "#root/utils/datetime.js";
2
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 = [];
3
+ import { getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
31
4
 
32
- for (let i = limit - 1; i >= 0; i--) {
33
- list.push(getDateYmdNumber(addDays(now, -i)));
34
- }
35
-
36
- return list;
37
- }
5
+ const ONLINE_STATS_DAY_LIMIT = 30;
6
+ const ONLINE_STATS_ACTIVE_KEY = "online:active";
38
7
 
8
+ // 构造全量统计的 Redis 键。
39
9
  function getPeriodKeys(periodType, periodValue) {
40
10
  return {
41
11
  pv: `online:${periodType}:${periodValue}:pv`,
@@ -44,12 +14,9 @@ function getPeriodKeys(periodType, periodValue) {
44
14
  };
45
15
  }
46
16
 
47
- function getOnlineStatsProductKey(productName) {
48
- return encodeURIComponent(String(productName || ""));
49
- }
50
-
17
+ // 构造单产品统计的 Redis 键。
51
18
  function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
52
- const productKey = getOnlineStatsProductKey(productName);
19
+ const productKey = encodeURIComponent(productName);
53
20
 
54
21
  return {
55
22
  pv: `online:${periodType}:${periodValue}:product:${productKey}:pv`,
@@ -58,83 +25,153 @@ function getOnlineStatsProductPeriodKeys(periodType, periodValue, productName) {
58
25
  };
59
26
  }
60
27
 
61
- function getOnlineStatsProductsKey(periodType, periodValue) {
62
- return `online:${periodType}:${periodValue}:products`;
28
+ // 构造单产品实时在线集合的 Redis 键。
29
+ function getOnlineStatsActiveProductKey(productName) {
30
+ const productKey = encodeURIComponent(productName);
31
+
32
+ return `online:active:product:${productKey}`;
63
33
  }
64
34
 
35
+ // 读取 Redis 字符串并转成统计数字。
65
36
  async function getRedisNumber(befly, key) {
66
- return toOnlineStatsNumber(await befly.redis.getString(key));
37
+ return getTongJiNumber(await befly.redis.getString(key));
67
38
  }
68
39
 
40
+ // 读取某个周期的全量统计数据。
69
41
  async function getOnlineStatsPeriodData(befly, periodType, periodValue) {
70
42
  const keys = getPeriodKeys(periodType, periodValue);
71
43
  return {
72
44
  reportTime: await getRedisNumber(befly, keys.reportTime),
73
45
  pv: await getRedisNumber(befly, keys.pv),
74
- uv: toOnlineStatsNumber(await befly.redis.scard(keys.members))
46
+ uv: getTongJiNumber(await befly.redis.scard(keys.members))
75
47
  };
76
48
  }
77
49
 
50
+ // 读取某个周期的单产品统计数据。
78
51
  async function getOnlineStatsProductPeriodData(befly, periodType, periodValue, productName) {
79
52
  const keys = getOnlineStatsProductPeriodKeys(periodType, periodValue, productName);
80
53
 
81
54
  return {
82
55
  reportTime: await getRedisNumber(befly, keys.reportTime),
83
56
  pv: await getRedisNumber(befly, keys.pv),
84
- uv: toOnlineStatsNumber(await befly.redis.scard(keys.members))
57
+ uv: getTongJiNumber(await befly.redis.scard(keys.members))
85
58
  };
86
59
  }
87
60
 
88
- async function getOnlineStatsProductNames(befly, recentDateList) {
89
- const productSet = new Set();
61
+ // 清理过期成员后返回全站实时在线人数。
62
+ async function getOnlineStatsTotalOnlineCount(befly) {
63
+ const now = Date.now();
64
+
65
+ await befly.redis.zremrangebyscore(ONLINE_STATS_ACTIVE_KEY, "-inf", now);
66
+ return await befly.redis.zcard(ONLINE_STATS_ACTIVE_KEY);
67
+ }
68
+
69
+ // 清理过期成员后返回单产品实时在线人数。
70
+ async function getOnlineStatsProductOnlineCount(befly, productName) {
71
+ const now = Date.now();
72
+ const productKey = getOnlineStatsActiveProductKey(productName);
73
+
74
+ await befly.redis.zremrangebyscore(productKey, "-inf", now);
75
+ return await befly.redis.zcard(productKey);
76
+ }
77
+
78
+ // 构造全量统计的按天趋势列表。
79
+ async function buildOnlineStatsDays(befly, recentDateList) {
80
+ const days = [];
90
81
 
91
82
  for (const item of recentDateList) {
92
- for (const productName of await befly.redis.smembers(getOnlineStatsProductsKey("day", item))) {
93
- if (!productName) {
94
- continue;
95
- }
83
+ const dayData = await getOnlineStatsPeriodData(befly, "day", item);
96
84
 
97
- productSet.add(String(productName));
85
+ if (dayData.reportTime <= 0) {
86
+ continue;
98
87
  }
88
+
89
+ days.push({
90
+ reportDate: item,
91
+ reportTime: dayData.reportTime,
92
+ pv: dayData.pv,
93
+ uv: dayData.uv
94
+ });
99
95
  }
100
96
 
101
- return Array.from(productSet.values());
97
+ return days;
102
98
  }
103
99
 
104
- async function getOnlineStatsOnlineCount(befly) {
105
- const members = await befly.redis.smembers("online:visitors");
106
- if (members.length === 0) {
107
- return 0;
108
- }
100
+ // 构造单产品统计的按天趋势列表。
101
+ async function buildOnlineStatsProductDays(befly, recentDateList, productName) {
102
+ const days = [];
109
103
 
110
- const existsList = await befly.redis.existsBatch(members.map((member) => `online:visitor:${member}`));
111
- const expiredMembers = [];
112
- let onlineCount = 0;
104
+ for (const item of recentDateList) {
105
+ const dayData = await getOnlineStatsProductPeriodData(befly, "day", item, productName);
113
106
 
114
- for (let i = 0; i < members.length; i++) {
115
- if (existsList[i]) {
116
- onlineCount += 1;
107
+ if (dayData.reportTime <= 0) {
117
108
  continue;
118
109
  }
119
110
 
120
- expiredMembers.push(members[i]);
111
+ days.push({
112
+ reportDate: item,
113
+ reportTime: dayData.reportTime,
114
+ pv: dayData.pv,
115
+ uv: dayData.uv
116
+ });
121
117
  }
122
118
 
123
- if (expiredMembers.length > 0) {
124
- await befly.redis.srem("online:visitors", expiredMembers);
119
+ return days;
120
+ }
121
+
122
+ // 按配置项目列表构造项目统计,并按近 30 天 PV 排序。
123
+ async function buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists) {
124
+ const products = [];
125
+
126
+ for (const item of projectLists) {
127
+ const productDays = await buildOnlineStatsProductDays(befly, recentDateList, item.productName);
128
+ let totalPv = 0;
129
+
130
+ for (const productDay of productDays) {
131
+ totalPv += productDay.pv;
132
+ }
133
+
134
+ const productToday = await getOnlineStatsProductPeriodData(befly, "day", reportDate, item.productName);
135
+ const productWeek = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, item.productName);
136
+ const productMonth = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, item.productName);
137
+ const productOnlineCount = await getOnlineStatsProductOnlineCount(befly, item.productName);
138
+
139
+ products.push({
140
+ key: item.productCode,
141
+ productName: item.productName,
142
+ productCode: item.productCode,
143
+ productVersion: item.productVersion || "",
144
+ onlineCount: productOnlineCount,
145
+ today: {
146
+ pv: productToday.pv,
147
+ uv: productToday.uv
148
+ },
149
+ week: {
150
+ pv: productWeek.pv,
151
+ uv: productWeek.uv
152
+ },
153
+ month: {
154
+ pv: productMonth.pv,
155
+ uv: productMonth.uv
156
+ },
157
+ days: productDays,
158
+ totalPv: totalPv
159
+ });
125
160
  }
126
161
 
127
- return onlineCount;
128
- }
162
+ const sortedProducts = products.toSorted((a, b) => {
163
+ if (b.totalPv !== a.totalPv) {
164
+ return b.totalPv - a.totalPv;
165
+ }
129
166
 
130
- function sumProductTrendField(list, field) {
131
- let total = 0;
167
+ return String(a.productName).localeCompare(String(b.productName), "zh-CN");
168
+ });
132
169
 
133
- for (const item of list) {
134
- total += toOnlineStatsNumber(item?.[field]);
170
+ for (const item of sortedProducts) {
171
+ delete item.totalPv;
135
172
  }
136
173
 
137
- return total;
174
+ return sortedProducts;
138
175
  }
139
176
 
140
177
  export default {
@@ -142,88 +179,40 @@ export default {
142
179
  method: "POST",
143
180
  body: "none",
144
181
  auth: true,
145
- fields: {},
182
+ fields: {
183
+ productName: { name: "产品名称", input: "string", min: 0, max: 100 }
184
+ },
146
185
  required: [],
147
- handler: async (befly) => {
186
+ // 汇总当前查询范围的在线统计,并在未筛选产品时附带配置项目统计。
187
+ handler: async (befly, ctx) => {
148
188
  const now = Date.now();
149
189
  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
- });
190
+ const weekStartDate = getTongJiWeekStartDate(now);
191
+ const monthStartDate = getTongJiMonthStartDate(now);
192
+ const recentDateList = getTongJiRecentDateList(now, ONLINE_STATS_DAY_LIMIT);
193
+ const productName = String(ctx?.body?.productName || "").trim();
194
+ const hasProductFilter = productName.length > 0;
195
+ const days = hasProductFilter ? await buildOnlineStatsProductDays(befly, recentDateList, productName) : await buildOnlineStatsDays(befly, recentDateList);
196
+
197
+ let currentDay;
198
+ let weekCurrent;
199
+ let monthCurrent;
200
+ let onlineCount;
201
+
202
+ if (hasProductFilter) {
203
+ currentDay = await getOnlineStatsProductPeriodData(befly, "day", reportDate, productName);
204
+ weekCurrent = await getOnlineStatsProductPeriodData(befly, "week", weekStartDate, productName);
205
+ monthCurrent = await getOnlineStatsProductPeriodData(befly, "month", monthStartDate, productName);
206
+ onlineCount = await getOnlineStatsProductOnlineCount(befly, productName);
207
+ } else {
208
+ currentDay = await getOnlineStatsPeriodData(befly, "day", reportDate);
209
+ weekCurrent = await getOnlineStatsPeriodData(befly, "week", weekStartDate);
210
+ monthCurrent = await getOnlineStatsPeriodData(befly, "month", monthStartDate);
211
+ onlineCount = await getOnlineStatsTotalOnlineCount(befly);
218
212
  }
219
213
 
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
- });
214
+ const projectLists = hasProductFilter ? [] : befly.config.projectLists;
215
+ const products = await buildOnlineStatsProducts(befly, recentDateList, reportDate, weekStartDate, monthStartDate, projectLists);
227
216
 
228
217
  return befly.tool.Yes("获取成功", {
229
218
  queryTime: now,
package/checks/config.js CHANGED
@@ -10,6 +10,47 @@ z.config(z.locales.zhCN());
10
10
  const boolIntSchema = z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]);
11
11
  const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
12
12
  const beflyModeSchema = z.union([z.literal("manual"), z.literal("auto")]);
13
+ const projectListCodeSchema = z.string().regex(/^[a-z][a-zA-Z0-9]*$/, "必须是小驼峰命名");
14
+ const projectListItemSchema = z
15
+ .object({
16
+ code: projectListCodeSchema,
17
+ name: noTrimString.min(1),
18
+ productName: noTrimString.min(1),
19
+ productCode: noTrimString.min(1),
20
+ productVersion: noTrimString.optional()
21
+ })
22
+ .strict();
23
+ const projectListsSchema = z.array(projectListItemSchema).superRefine((projectLists, ctx) => {
24
+ const codeSet = new Set();
25
+ const productCodeSet = new Set();
26
+
27
+ for (let index = 0; index < projectLists.length; index += 1) {
28
+ const code = projectLists[index].code;
29
+ const productCode = projectLists[index].productCode;
30
+
31
+ if (codeSet.has(code)) {
32
+ ctx.addIssue({
33
+ code: z.ZodIssueCode.custom,
34
+ message: `projectLists[${index}].code 重复`,
35
+ path: [index, "code"]
36
+ });
37
+ continue;
38
+ }
39
+
40
+ codeSet.add(code);
41
+
42
+ if (productCodeSet.has(productCode)) {
43
+ ctx.addIssue({
44
+ code: z.ZodIssueCode.custom,
45
+ message: `projectLists[${index}].productCode 重复`,
46
+ path: [index, "productCode"]
47
+ });
48
+ continue;
49
+ }
50
+
51
+ productCodeSet.add(productCode);
52
+ }
53
+ });
13
54
 
14
55
  const configSchema = z
15
56
  .object({
@@ -98,7 +139,9 @@ const configSchema = z
98
139
  skipRoutes: z.array(noTrimString),
99
140
  rules: z.array(z.object({}).passthrough())
100
141
  })
101
- .strict()
142
+ .strict(),
143
+
144
+ projectLists: projectListsSchema
102
145
  })
103
146
  .strict();
104
147
 
@@ -68,5 +68,14 @@
68
68
  "key": "ip",
69
69
  "skipRoutes": [],
70
70
  "rules": []
71
- }
71
+ },
72
+ "projectLists": [
73
+ {
74
+ "code": "beflyCore",
75
+ "name": "后台管理",
76
+ "productName": "后台管理",
77
+ "productCode": "beflyCore",
78
+ "productVersion": "1.0.0"
79
+ }
80
+ ]
72
81
  }
@@ -47,9 +47,13 @@ export default {
47
47
 
48
48
  // 4. 角色权限检查
49
49
  // apiPath 在 apiHandler 中已统一生成并写入 ctx.apiPath
50
+ let hasPermission = false;
50
51
 
51
- // 极简方案:每个角色一个 Set,直接判断成员是否存在
52
- const hasPermission = await befly.redis.sismember(`role:apis:${ctx.roleCode}`, ctx.apiPath);
52
+ if (befly.cache && typeof befly.cache.checkRoleApis === "function") {
53
+ hasPermission = await befly.cache.checkRoleApis(ctx.roleCode, ctx.apiPath);
54
+ } else {
55
+ hasPermission = await befly.redis.sismember(`role:apis:${ctx.roleCode}`, ctx.apiPath);
56
+ }
53
57
 
54
58
  if (!hasPermission) {
55
59
  ctx.response = ErrorResponse(