befly 3.62.0 → 3.64.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,74 +1,14 @@
1
- import { getDateYmdNumber } from "#befly/utils/datetime.js";
2
-
3
- import { getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiDateRangeList, getTongJiMonthStartDate, getTongJiNumber, getTongJiRecentDateList, getTongJiWeekStartDate } from "./_tongJi.js";
1
+ import { getTongJiDateRangeList, getTongJiDateYmdNumber, getTongJiMonthStartDate, getTongJiRecentDateList } from "./_tongJi.js";
4
2
 
5
3
  const ERROR_STATS_DAY_LIMIT = 30;
6
4
 
7
- function sumCounts(items) {
8
- return items.reduce((sum, item) => sum + item.count, 0);
9
- }
10
-
11
- async function getErrorStatsRedisSummary(befly, periodType, periodValue) {
12
- return {
13
- count: getTongJiNumber(await befly.redis.getString(getErrorStatsPeriodCountKey(periodType, periodValue)))
14
- };
15
- }
16
-
17
- async function getErrorStatsRedisTrend(befly, bucketDate) {
18
- const bucketSet = await befly.redis.smembers(getErrorStatsDayBucketsKey(bucketDate));
19
- const trend = await Promise.all(
20
- bucketSet.map(async (bucketTime) => {
21
- const itemBucketTime = getTongJiNumber(bucketTime);
22
-
23
- return {
24
- bucketTime: itemBucketTime,
25
- bucketDate: bucketDate,
26
- count: getTongJiNumber(await befly.redis.getString(getErrorStatsDayBucketCountKey(bucketDate, itemBucketTime)))
27
- };
28
- })
29
- );
5
+ function toCount(value) {
6
+ const num = Number(value);
30
7
 
31
- trend.sort((a, b) => a.bucketTime - b.bucketTime);
32
- return trend;
8
+ return Number.isFinite(num) ? num : 0;
33
9
  }
34
10
 
35
- async function getErrorStatsRedisDays(befly, recentDateList) {
36
- return Promise.all(
37
- recentDateList.map(async (item) => {
38
- return {
39
- bucketDate: item,
40
- count: getTongJiNumber(await befly.redis.getString(getErrorStatsPeriodCountKey("day", item)))
41
- };
42
- })
43
- );
44
- }
45
-
46
- async function getErrorStatsRedisTopTypes(befly, bucketDate) {
47
- const typeSet = await befly.redis.smembers(getErrorStatsDayTypesKey(bucketDate));
48
- const topTypes = await Promise.all(
49
- typeSet.map(async (errorType) => {
50
- const name = String(errorType || "unknown");
51
-
52
- return {
53
- errorType: name,
54
- count: getTongJiNumber(await befly.redis.getString(getErrorStatsDayTypeCountKey(bucketDate, name)))
55
- };
56
- })
57
- );
58
-
59
- return topTypes
60
- .filter((item) => item.count > 0)
61
- .toSorted((a, b) => {
62
- if (b.count !== a.count) {
63
- return b.count - a.count;
64
- }
65
-
66
- return String(a.errorType).localeCompare(String(b.errorType), "zh-CN");
67
- })
68
- .slice(0, 5);
69
- }
70
-
71
- function buildErrorStatsResponse(bucketDate, weekStartDate, monthStartDate, recentDateList, todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, recent30Count, trend, days, topTypes) {
11
+ function buildErrorStatsResponse(bucketDate, monthStartDate, recentDateList, todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, monthSummary, recent30Count, trend, days, topTypes) {
72
12
  return {
73
13
  today: {
74
14
  bucketDate: bucketDate,
@@ -82,11 +22,6 @@ function buildErrorStatsResponse(bucketDate, weekStartDate, monthStartDate, rece
82
22
  bucketDate: recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || bucketDate,
83
23
  count: dayBeforeYesterdaySummary.count
84
24
  },
85
- week: {
86
- startDate: weekStartDate,
87
- endDate: bucketDate,
88
- count: weekSummary.count
89
- },
90
25
  month: {
91
26
  startDate: monthStartDate,
92
27
  endDate: bucketDate,
@@ -103,37 +38,12 @@ function buildErrorStatsResponse(bucketDate, weekStartDate, monthStartDate, rece
103
38
  };
104
39
  }
105
40
 
106
- function isRedisResultEmpty(todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, recent30Count, trend, topTypes) {
107
- return todaySummary.count <= 0 && yesterdaySummary.count <= 0 && dayBeforeYesterdaySummary.count <= 0 && weekSummary.count <= 0 && monthSummary.count <= 0 && recent30Count <= 0 && trend.length === 0 && topTypes.length === 0;
108
- }
109
-
110
- async function getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList) {
111
- const [todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, trend, days, topTypes] = await Promise.all([
112
- getErrorStatsRedisSummary(befly, "day", bucketDate),
113
- getErrorStatsRedisSummary(befly, "day", recentDateList[recentDateList.length - 2] || bucketDate),
114
- getErrorStatsRedisSummary(befly, "day", recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || bucketDate),
115
- getErrorStatsRedisSummary(befly, "week", weekStartDate),
116
- getErrorStatsRedisSummary(befly, "month", monthStartDate),
117
- getErrorStatsRedisTrend(befly, bucketDate),
118
- getErrorStatsRedisDays(befly, recentDateList),
119
- getErrorStatsRedisTopTypes(befly, bucketDate)
120
- ]);
121
-
122
- const recent30Count = sumCounts(days);
123
-
124
- if (isRedisResultEmpty(todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, recent30Count, trend, topTypes)) {
125
- return null;
126
- }
127
-
128
- return buildErrorStatsResponse(bucketDate, weekStartDate, monthStartDate, recentDateList, todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, recent30Count, trend, days, topTypes);
129
- }
130
-
131
41
  async function getErrorStatsDayCountMap(befly, startDate, endDate) {
132
- const result = await befly.mysql.execute("SELECT bucket_date as bucketDate, SUM(hit_count) as count FROM befly_error_report WHERE state = 1 AND bucket_date BETWEEN ? AND ? GROUP BY bucket_date", [startDate, endDate]);
42
+ const result = await befly.mysql.execute("SELECT bucket_date as bucketDate, COUNT(*) as count FROM befly_error_report WHERE state = 1 AND bucket_date BETWEEN ? AND ? GROUP BY bucket_date", [startDate, endDate]);
133
43
  const map = new Map();
134
44
 
135
45
  for (const item of result.data || []) {
136
- map.set(Number(item.bucketDate), getTongJiNumber(item.count));
46
+ map.set(Number(item.bucketDate), toCount(item.count));
137
47
  }
138
48
 
139
49
  return map;
@@ -143,37 +53,35 @@ function sumDateCounts(map, dateList) {
143
53
  return dateList.reduce((sum, date) => sum + (map.get(date) || 0), 0);
144
54
  }
145
55
 
146
- async function getErrorStatsFromMysql(befly, bucketDate, weekStartDate, monthStartDate, recentDateList) {
56
+ async function getErrorStatsFromMysql(befly, bucketDate, monthStartDate, recentDateList) {
147
57
  const yesterdayDate = recentDateList[recentDateList.length - 2] || bucketDate;
148
58
  const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3] || yesterdayDate;
149
- const weekDateList = getTongJiDateRangeList(weekStartDate, bucketDate);
150
59
  const monthDateList = getTongJiDateRangeList(monthStartDate, bucketDate);
151
60
 
152
61
  const [dayCountMap, trendRes, topTypesRes] = await Promise.all([
153
62
  getErrorStatsDayCountMap(befly, recentDateList[0], bucketDate),
154
- 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]),
155
- 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])
63
+ befly.mysql.execute("SELECT bucket_time as bucketTime, bucket_date as bucketDate, 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]),
64
+ befly.mysql.execute("SELECT error_type as errorType, COUNT(*) as count FROM befly_error_report WHERE state = 1 AND bucket_date = ? GROUP BY error_type ORDER BY count DESC LIMIT 5", [bucketDate])
156
65
  ]);
157
66
 
158
67
  const todaySummary = { count: dayCountMap.get(bucketDate) || 0 };
159
68
  const yesterdaySummary = { count: dayCountMap.get(yesterdayDate) || 0 };
160
69
  const dayBeforeYesterdaySummary = { count: dayCountMap.get(dayBeforeYesterdayDate) || 0 };
161
- const weekSummary = { count: sumDateCounts(dayCountMap, weekDateList) };
162
70
  const monthSummary = { count: sumDateCounts(dayCountMap, monthDateList) };
163
71
  const recent30Count = sumDateCounts(dayCountMap, recentDateList);
164
72
 
165
73
  const trend = (trendRes.data || []).map((item) => {
166
74
  return {
167
- bucketTime: getTongJiNumber(item.bucketTime),
168
- bucketDate: getTongJiNumber(item.bucketDate),
169
- count: getTongJiNumber(item.count)
75
+ bucketTime: toCount(item.bucketTime),
76
+ bucketDate: toCount(item.bucketDate),
77
+ count: toCount(item.count)
170
78
  };
171
79
  });
172
80
 
173
81
  const topTypes = (topTypesRes.data || []).map((item) => {
174
82
  return {
175
83
  errorType: String(item.errorType || "unknown"),
176
- count: getTongJiNumber(item.count)
84
+ count: toCount(item.count)
177
85
  };
178
86
  });
179
87
 
@@ -184,7 +92,7 @@ async function getErrorStatsFromMysql(befly, bucketDate, weekStartDate, monthSta
184
92
  };
185
93
  });
186
94
 
187
- return buildErrorStatsResponse(bucketDate, weekStartDate, monthStartDate, recentDateList, todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, weekSummary, monthSummary, recent30Count, trend, days, topTypes);
95
+ return buildErrorStatsResponse(bucketDate, monthStartDate, recentDateList, todaySummary, yesterdaySummary, dayBeforeYesterdaySummary, monthSummary, recent30Count, trend, days, topTypes);
188
96
  }
189
97
 
190
98
  function buildEmptyDays(recentDateList) {
@@ -196,7 +104,7 @@ function buildEmptyDays(recentDateList) {
196
104
  });
197
105
  }
198
106
 
199
- function buildEmptyErrorStats(bucketDate, weekStartDate, monthStartDate, recentDateList) {
107
+ function buildEmptyErrorStats(bucketDate, monthStartDate, recentDateList) {
200
108
  return {
201
109
  today: {
202
110
  bucketDate: bucketDate,
@@ -210,11 +118,6 @@ function buildEmptyErrorStats(bucketDate, weekStartDate, monthStartDate, recentD
210
118
  bucketDate: recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || bucketDate,
211
119
  count: 0
212
120
  },
213
- week: {
214
- startDate: weekStartDate,
215
- endDate: bucketDate,
216
- count: 0
217
- },
218
121
  month: {
219
122
  startDate: monthStartDate,
220
123
  endDate: bucketDate,
@@ -242,23 +145,14 @@ export default {
242
145
  const tableExistsResult = await befly.mysql.tableExists("beflyErrorReport");
243
146
  const tableReady = tableExistsResult.data === true;
244
147
  const now = Date.now();
245
- const bucketDate = getDateYmdNumber(now);
246
- const weekStartDate = getTongJiWeekStartDate(now);
148
+ const bucketDate = getTongJiDateYmdNumber(now);
247
149
  const monthStartDate = getTongJiMonthStartDate(now);
248
150
  const recentDateList = getTongJiRecentDateList(now, ERROR_STATS_DAY_LIMIT);
249
151
 
250
152
  if (!tableReady) {
251
- return befly.tool.Yes("获取成功", buildEmptyErrorStats(bucketDate, weekStartDate, monthStartDate, recentDateList));
252
- }
253
-
254
- if (befly.redis) {
255
- const redisResult = await getErrorStatsFromRedis(befly, bucketDate, weekStartDate, monthStartDate, recentDateList);
256
-
257
- if (redisResult) {
258
- return befly.tool.Yes("获取成功", redisResult);
259
- }
153
+ return befly.tool.Yes("获取成功", buildEmptyErrorStats(bucketDate, monthStartDate, recentDateList));
260
154
  }
261
155
 
262
- return befly.tool.Yes("获取成功", await getErrorStatsFromMysql(befly, bucketDate, weekStartDate, monthStartDate, recentDateList));
156
+ return befly.tool.Yes("获取成功", await getErrorStatsFromMysql(befly, bucketDate, monthStartDate, recentDateList));
263
157
  }
264
158
  };
@@ -0,0 +1,20 @@
1
+ import projectTable from "#befly/tables/project.json";
2
+
3
+ export default {
4
+ name: "删除产品",
5
+ method: "POST",
6
+ body: "none",
7
+ auth: true,
8
+ fields: {
9
+ id: projectTable.id
10
+ },
11
+ required: ["id"],
12
+ handler: async (befly, ctx) => {
13
+ await befly.mysql.delData({
14
+ table: "beflyProject",
15
+ where: { id: ctx.body.id }
16
+ });
17
+
18
+ return befly.tool.Yes("删除成功");
19
+ }
20
+ };
@@ -0,0 +1,35 @@
1
+ import projectTable from "#befly/tables/project.json";
2
+
3
+ export default {
4
+ name: "添加产品",
5
+ method: "POST",
6
+ body: "none",
7
+ auth: true,
8
+ fields: {
9
+ code: projectTable.code,
10
+ name: projectTable.name,
11
+ sort: projectTable.sort
12
+ },
13
+ required: ["code", "name"],
14
+ handler: async (befly, ctx) => {
15
+ const existing = await befly.mysql.getOne({
16
+ table: "beflyProject",
17
+ where: { code: ctx.body.code }
18
+ });
19
+
20
+ if (existing.data?.id) {
21
+ return befly.tool.No("产品代号已存在");
22
+ }
23
+
24
+ const projectId = await befly.mysql.insData({
25
+ table: "beflyProject",
26
+ data: {
27
+ code: ctx.body.code,
28
+ name: ctx.body.name,
29
+ sort: ctx.body.sort
30
+ }
31
+ });
32
+
33
+ return befly.tool.Yes("添加成功", { id: projectId.data });
34
+ }
35
+ };
@@ -0,0 +1,31 @@
1
+ import { queryFields } from "#befly/apis/_apis.js";
2
+ import projectTable from "#befly/tables/project.json";
3
+
4
+ export default {
5
+ name: "获取产品列表",
6
+ method: "POST",
7
+ body: "none",
8
+ auth: true,
9
+ fields: {
10
+ ...queryFields,
11
+ code: projectTable.code,
12
+ state: projectTable.state
13
+ },
14
+ required: [],
15
+ handler: async (befly, ctx) => {
16
+ const result = await befly.mysql.getList({
17
+ table: "beflyProject",
18
+ fields: ["id", "code", "name", "sort", "state", "createdAt", "updatedAt"],
19
+ where: {
20
+ code$like$or: ctx.body.code,
21
+ name$like$or: ctx.body.keyword,
22
+ state: ctx.body.state
23
+ },
24
+ page: ctx.body.page,
25
+ limit: ctx.body.limit,
26
+ orderBy: ["sort#asc"]
27
+ });
28
+
29
+ return befly.tool.Yes("获取成功", result.data);
30
+ }
31
+ };
@@ -0,0 +1,52 @@
1
+ import projectTable from "#befly/tables/project.json";
2
+
3
+ export default {
4
+ name: "更新产品",
5
+ method: "POST",
6
+ body: "none",
7
+ auth: true,
8
+ fields: {
9
+ id: projectTable.id,
10
+ code: projectTable.code,
11
+ name: projectTable.name,
12
+ sort: projectTable.sort,
13
+ state: projectTable.state
14
+ },
15
+ required: ["id"],
16
+ handler: async (befly, ctx) => {
17
+ const current = await befly.mysql.getOne({
18
+ table: "beflyProject",
19
+ where: { id: ctx.body.id }
20
+ });
21
+
22
+ if (!current.data?.id) {
23
+ return befly.tool.No("产品不存在");
24
+ }
25
+
26
+ if (ctx.body.code !== undefined && ctx.body.code !== current.data.code) {
27
+ const existing = await befly.mysql.getOne({
28
+ table: "beflyProject",
29
+ where: { code: ctx.body.code }
30
+ });
31
+
32
+ if (existing.data?.id) {
33
+ return befly.tool.No("产品代号已存在");
34
+ }
35
+ }
36
+
37
+ const updateData = {};
38
+
39
+ if (ctx.body.code !== undefined) updateData.code = ctx.body.code;
40
+ if (ctx.body.name !== undefined) updateData.name = ctx.body.name;
41
+ if (ctx.body.sort !== undefined) updateData.sort = ctx.body.sort;
42
+ if (ctx.body.state !== undefined) updateData.state = ctx.body.state;
43
+
44
+ await befly.mysql.updData({
45
+ table: "beflyProject",
46
+ data: updateData,
47
+ where: { id: ctx.body.id }
48
+ });
49
+
50
+ return befly.tool.Yes("更新成功");
51
+ }
52
+ };
@@ -5,9 +5,26 @@
5
5
  "sort": 0
6
6
  },
7
7
  {
8
- "name": "项目统计",
9
- "path": "/project-stats",
10
- "sort": 1
8
+ "name": "统计管理",
9
+ "path": "/tong-ji",
10
+ "sort": 2,
11
+ "children": [
12
+ {
13
+ "name": "统计数据",
14
+ "path": "/statistics-data",
15
+ "sort": 1
16
+ },
17
+ {
18
+ "name": "项目管理",
19
+ "path": "/project-manage",
20
+ "sort": 2
21
+ },
22
+ {
23
+ "name": "统计记录",
24
+ "path": "/statistics-record",
25
+ "sort": 3
26
+ }
27
+ ]
11
28
  },
12
29
  {
13
30
  "name": "人员管理",
package/index.js CHANGED
@@ -120,6 +120,11 @@ export async function createBefly(env = {}, config = {}, menus = []) {
120
120
  const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
121
121
  mergedConfig.projectLists = mergeProjectListsByCode(mergedConfig.projectLists);
122
122
 
123
+ Logger.configure({
124
+ runtimeEnv: mergedConfig.runMode,
125
+ ...mergedConfig.logger
126
+ });
127
+
123
128
  const configHasError = await checkConfig(mergedConfig);
124
129
  const { apis, tables, plugins, hooks } = await scanSources();
125
130
 
@@ -138,12 +143,6 @@ export async function createBefly(env = {}, config = {}, menus = []) {
138
143
  });
139
144
  }
140
145
 
141
- // 在插件加载前用项目 runMode 初始化 Logger,确保后续所有日志目录正确
142
- Logger.configure({
143
- runtimeEnv: mergedConfig.runMode,
144
- ...mergedConfig.logger
145
- });
146
-
147
146
  return new Befly({
148
147
  env: env,
149
148
  config: mergedConfig,
package/lib/logger.js CHANGED
@@ -69,6 +69,10 @@ class LogFileSink {
69
69
  }
70
70
 
71
71
  getFilePath(date, index) {
72
+ if (this.prefix === "debug") {
73
+ return nodePathJoin(config.dir, "dev.log");
74
+ }
75
+
72
76
  const suffix = index > 0 ? `.${index}` : "";
73
77
  const filename = this.prefix === "app" ? `${date}${suffix}.log` : `${this.prefix}.${date}${suffix}.log`;
74
78
  return nodePathJoin(config.dir, filename);
@@ -89,6 +93,10 @@ class LogFileSink {
89
93
  this.openStream(this.getFilePath(date, 0));
90
94
  }
91
95
 
96
+ if (this.prefix === "debug") {
97
+ return;
98
+ }
99
+
92
100
  if (this.stream && this.streamSizeBytes + nextChunkBytes > MAX_FILE_BYTES) {
93
101
  this.closeStreamSync();
94
102
  this.streamIndex += 1;
@@ -177,28 +185,20 @@ export async function shutdown() {
177
185
  }
178
186
  }
179
187
 
180
- function getRuntimeLogDir(runtimeEnv) {
181
- const cwd = process.cwd();
182
- if (runtimeEnv === "development") {
183
- return nodePathResolve(cwd, "logsDev");
184
- }
185
- return nodePathResolve(cwd, "logs");
188
+ function getRuntimeLogDir() {
189
+ return nodePathResolve(process.cwd(), "logs");
186
190
  }
187
191
 
188
192
  function prepareLogDir(runtimeEnv, shouldClearDev = false) {
189
193
  const logDir = getRuntimeLogDir(runtimeEnv);
190
- const devLogDir = nodePathResolve(process.cwd(), "logsDev");
194
+ const devLogPath = nodePathResolve(logDir, "dev.log");
191
195
 
192
196
  try {
193
197
  if (!existsSync(logDir)) {
194
198
  mkdirSync(logDir, { recursive: true });
195
199
  }
196
- if (!existsSync(devLogDir)) {
197
- mkdirSync(devLogDir, { recursive: true });
198
- }
199
- if (shouldClearDev && runtimeEnv === "development" && existsSync(devLogDir)) {
200
- rmSync(devLogDir, { recursive: true, force: true });
201
- mkdirSync(devLogDir, { recursive: true });
200
+ if (shouldClearDev && runtimeEnv === "development" && existsSync(devLogPath)) {
201
+ rmSync(devLogPath, { force: true });
202
202
  }
203
203
  } catch (error) {
204
204
  throw new Error(`创建日志目录失败: ${logDir}. ${error?.message || error}`, {
@@ -214,8 +214,6 @@ function ensureLogDirExists() {
214
214
  config.dir = prepareLogDir(config.runtimeEnv);
215
215
  }
216
216
 
217
- // 方案B:删除“启动时清理旧日志”功能(减少 I/O 与复杂度)。
218
-
219
217
  /**
220
218
  * 配置日志
221
219
  */
@@ -285,6 +283,13 @@ function buildJsonLine(level, timeMs, record) {
285
283
  function ensureSinksReady(kind) {
286
284
  ensureLogDirExists();
287
285
 
286
+ if (config.runtimeEnv === "development") {
287
+ if (!appFileSink) {
288
+ appFileSink = new LogFileSink("debug");
289
+ }
290
+ return appFileSink;
291
+ }
292
+
288
293
  if (kind === "app") {
289
294
  if (!appFileSink) {
290
295
  appFileSink = new LogFileSink("app");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.62.0",
3
+ "version": "3.64.0",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 定时任务插件
3
+ * 基于 Bun.cron 提供进程内定时任务能力。
4
+ */
5
+
6
+ import { Logger } from "../lib/logger.js";
7
+ import { isPrimaryProcess } from "../utils/is.js";
8
+
9
+ const jobs = new Map();
10
+
11
+ /**
12
+ * 注册一个定时任务
13
+ * @param {string} name - 任务名称,用于日志标识
14
+ * @param {string} schedule - cron 表达式或预定义昵称
15
+ * @param {function} task - 任务函数,接收 befly 上下文
16
+ * @returns {CronJob|null}
17
+ */
18
+ function register(name, schedule, task) {
19
+ if (!isPrimaryProcess(this.env)) {
20
+ return null;
21
+ }
22
+
23
+ if (jobs.has(name)) {
24
+ return jobs.get(name);
25
+ }
26
+
27
+ const context = this;
28
+ const job = Bun.cron(schedule, async function () {
29
+ Logger.info(`[定时任务] ${name} 开始执行`);
30
+ try {
31
+ await task(context);
32
+ Logger.info(`[定时任务] ${name} 执行完成`);
33
+ } catch (error) {
34
+ Logger.error(`[定时任务] ${name} 执行失败`, error);
35
+ }
36
+ });
37
+
38
+ jobs.set(name, job);
39
+ return job;
40
+ }
41
+
42
+ /**
43
+ * 注册一个按秒执行的定时任务
44
+ */
45
+ function second(name, seconds, task) {
46
+ if (!isPrimaryProcess(this.env)) {
47
+ return null;
48
+ }
49
+
50
+ if (jobs.has(name)) {
51
+ return jobs.get(name);
52
+ }
53
+
54
+ const context = this;
55
+ let running = false;
56
+ const timer = setInterval(async () => {
57
+ if (running) {
58
+ return;
59
+ }
60
+
61
+ running = true;
62
+ try {
63
+ await task(context);
64
+ } catch (error) {
65
+ Logger.error(`[定时任务] ${name} 执行失败`, error);
66
+ } finally {
67
+ running = false;
68
+ }
69
+ }, seconds * 1000);
70
+ const job = {
71
+ stop: () => clearInterval(timer)
72
+ };
73
+
74
+ jobs.set(name, job);
75
+ return job;
76
+ }
77
+
78
+ /**
79
+ * 停止所有已注册的定时任务
80
+ */
81
+ export function stopAll() {
82
+ for (const job of jobs.values()) {
83
+ job.stop();
84
+ }
85
+ jobs.clear();
86
+ }
87
+
88
+ export default {
89
+ order: 8,
90
+ handler: async function (befly) {
91
+ return {
92
+ register: register.bind(befly),
93
+ second: second.bind(befly)
94
+ };
95
+ }
96
+ };