Skip to content

Commit 6926e77

Browse files
committed
feat(cron-task): implement cron task module with business logic and scheduling
- Added a new CronTaskModule to manage scheduled tasks, including cleaning access records, resetting IP access, and pushing to search engines. - Implemented CronBusinessService for task execution logic and CronTaskService for task management and scheduling. - Introduced CronTaskController for handling API requests related to cron tasks. - Created CronTaskScheduler to define and schedule cron jobs using NestJS's scheduling capabilities. - Defined cron task types and metadata for better organization and management of scheduled tasks. These changes enhance the application's ability to perform background tasks efficiently and improve overall system maintenance. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 772a082 commit 6926e77

File tree

11 files changed

+598
-134
lines changed

11 files changed

+598
-134
lines changed

apps/core/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { BackupModule } from './modules/backup/backup.module'
2929
import { CategoryModule } from './modules/category/category.module'
3030
import { CommentModule } from './modules/comment/comment.module'
3131
import { ConfigsModule } from './modules/configs/configs.module'
32+
import { CronTaskModule } from './modules/cron-task/cron-task.module'
3233
import { DebugModule } from './modules/debug/debug.module'
3334
import { DependencyModule } from './modules/dependency/dependency.module'
3435
import { DraftModule } from './modules/draft/draft.module'
@@ -87,6 +88,7 @@ import { TaskQueueModule } from './processors/task-queue/task-queue.module'
8788
CategoryModule,
8889
CommentModule,
8990
ConfigsModule,
91+
CronTaskModule,
9092

9193
DependencyModule,
9294
DraftModule,

apps/core/src/processors/helper/helper.cron.service.ts renamed to apps/core/src/modules/cron-task/cron-business.service.ts

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import { rm } from 'node:fs/promises'
22
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'
3-
import { CronExpression } from '@nestjs/schedule'
4-
import { CronDescription } from '~/common/decorators/cron-description.decorator'
5-
import { CronOnce } from '~/common/decorators/cron-once.decorator'
63
import { RedisKeys } from '~/constants/cache.constant'
74
import { STATIC_FILE_TRASH_DIR, TEMP_DIR } from '~/constants/path.constant'
85
import { AggregateService } from '~/modules/aggregate/aggregate.service'
96
import { AnalyzeModel } from '~/modules/analyze/analyze.model'
107
import { ConfigsService } from '~/modules/configs/configs.service'
118
import { FileReferenceService } from '~/modules/file/file-reference.service'
9+
import { HttpService } from '~/processors/helper/helper.http.service'
10+
import type { StoreJWTPayload } from '~/processors/helper/helper.jwt.service'
11+
import { JWTService } from '~/processors/helper/helper.jwt.service'
12+
import { RedisService } from '~/processors/redis/redis.service'
1213
import { InjectModel } from '~/transformers/model.transformer'
1314
import { getRedisKey } from '~/utils/redis.util'
1415
import dayjs from 'dayjs'
1516
import { mkdirp } from 'mkdirp'
16-
import { RedisService } from '../redis/redis.service'
17-
import { HttpService } from './helper.http.service'
18-
import type { StoreJWTPayload } from './helper.jwt.service'
19-
import { JWTService } from './helper.jwt.service'
2017

18+
/**
19+
* CronBusinessService - Cron 任务业务逻辑层
20+
*
21+
* 本服务仅保留业务方法的实现,供 CronTaskService 调用
22+
* 调度逻辑在 CronTaskScheduler 中
23+
*/
2124
@Injectable()
22-
export class CronService {
25+
export class CronBusinessService {
2326
private logger: Logger
2427
constructor(
2528
private readonly http: HttpService,
@@ -32,42 +35,38 @@ export class CronService {
3235
@Inject(forwardRef(() => AggregateService))
3336
private readonly aggregateService: AggregateService,
3437
) {
35-
this.logger = new Logger(CronService.name)
38+
this.logger = new Logger(CronBusinessService.name)
3639
}
3740

38-
@CronOnce(CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT, {
39-
name: 'cleanAccessRecord',
40-
})
41-
@CronDescription('清理访问记录')
41+
/**
42+
* 清理 7 天前的访问记录
43+
*/
4244
async cleanAccessRecord() {
4345
const cleanDate = dayjs().add(-7, 'd')
4446

45-
await this.analyzeModel.deleteMany({
47+
const result = await this.analyzeModel.deleteMany({
4648
timestamp: {
4749
$lte: cleanDate.toDate(),
4850
},
4951
})
5052

5153
this.logger.log('--> 清理访问记录成功')
54+
return { deletedCount: result.deletedCount }
5255
}
56+
5357
/**
54-
* @description 每天凌晨删除缓存
58+
* 每天凌晨删除 IP 访问缓存
5559
*/
56-
@CronOnce(CronExpression.EVERY_DAY_AT_MIDNIGHT, { name: 'resetIPAccess' })
57-
@CronDescription('清理 IP 访问记录')
5860
async resetIPAccess() {
5961
await this.redisService.getClient().del(getRedisKey(RedisKeys.AccessIp))
6062

6163
this.logger.log('--> 清理 IP 访问记录成功')
64+
return { success: true }
6265
}
6366

6467
/**
65-
* @description 每天凌晨删除缓存
68+
* 每天凌晨删除喜欢/阅读记录缓存
6669
*/
67-
@CronOnce(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
68-
name: 'resetLikedOrReadArticleRecord',
69-
})
70-
@CronDescription('清理喜欢数')
7170
async resetLikedOrReadArticleRecord() {
7271
const redis = this.redisService.getClient()
7372

@@ -81,18 +80,22 @@ export class CronService {
8180
)
8281

8382
this.logger.log('--> 清理喜欢数成功')
83+
return { success: true }
8484
}
8585

86-
@CronOnce(CronExpression.EVERY_DAY_AT_3AM, { name: 'cleanTempDirectory' })
87-
@CronDescription('清理临时文件')
86+
/**
87+
* 清理临时文件目录
88+
*/
8889
async cleanTempDirectory() {
8990
await rm(TEMP_DIR, { recursive: true })
9091
mkdirp.sync(STATIC_FILE_TRASH_DIR)
9192
this.logger.log('--> 清理临时文件成功')
93+
return { success: true }
9294
}
9395

94-
@CronOnce(CronExpression.EVERY_DAY_AT_1AM, { name: 'pushToBaiduSearch' })
95-
@CronDescription('推送到百度搜索')
96+
/**
97+
* 推送站点地图到百度搜索
98+
*/
9699
async pushToBaiduSearch() {
97100
const {
98101
url: { webUrl },
@@ -103,7 +106,7 @@ export class CronService {
103106
const token = configs.token
104107
if (!token) {
105108
this.logger.error('[BaiduSearchPushTask] token 为空')
106-
return
109+
return { skipped: true, reason: 'token is empty' }
107110
}
108111

109112
const pushUrls = await this.aggregateService.getSiteMapContent()
@@ -124,30 +127,31 @@ export class CronService {
124127
},
125128
)
126129
this.logger.log(`百度站长提交结果:${JSON.stringify(res.data)}`)
127-
return res.data
130+
return { response: res.data }
128131
} catch (error) {
129132
this.logger.error(`百度推送错误:${error.message}`)
130133
throw error
131134
}
132135
}
133-
return null
136+
return { skipped: true, reason: 'Baidu search push is disabled' }
134137
}
135138

136-
@CronOnce(CronExpression.EVERY_DAY_AT_1AM, { name: 'pushToBingSearch' })
137-
@CronDescription('推送到 Bing')
139+
/**
140+
* 推送站点地图到 Bing 搜索
141+
*/
138142
async pushToBingSearch() {
139143
const {
140144
url: { webUrl },
141145
bingSearchOptions: configs,
142146
} = await this.configs.waitForConfigReady()
143147

144148
if (!configs.enable) {
145-
return
149+
return { skipped: true, reason: 'Bing search push is disabled' }
146150
}
147151
const apiKey = configs.token
148152
if (!apiKey) {
149153
this.logger.error('[BingSearchPushTask] API key 为空')
150-
return
154+
return { skipped: true, reason: 'API key is empty' }
151155
}
152156

153157
const pushUrls = await this.aggregateService.getSiteMapContent()
@@ -172,17 +176,16 @@ export class CronService {
172176
} else {
173177
this.logger.log(`Bing 站长提交结果:${JSON.stringify(res.data)}`)
174178
}
175-
return res.data
179+
return { response: res.data }
176180
} catch (error) {
177181
this.logger.error(`Bing 推送错误:${error.message}`)
182+
throw error
178183
}
179-
return null
180184
}
181185

182-
@CronDescription('扫表:删除过期 JWT')
183-
@CronOnce(CronExpression.EVERY_DAY_AT_1AM, {
184-
name: 'deleteExpiredJWT',
185-
})
186+
/**
187+
* 扫表删除过期的 JWT
188+
*/
186189
async deleteExpiredJWT() {
187190
this.logger.log('--> 开始扫表,清除过期的 token')
188191
const redis = this.redisService.getClient()
@@ -218,18 +221,19 @@ export class CronService {
218221
)
219222

220223
this.logger.log(`--> 删除了 ${deleteCount} 个过期的 token`)
224+
return { deletedCount: deleteCount }
221225
}
222226

223-
@CronDescription('清理孤儿图片')
224-
@CronOnce(CronExpression.EVERY_HOUR, {
225-
name: 'cleanupOrphanImages',
226-
})
227+
/**
228+
* 清理孤儿图片
229+
*/
227230
async cleanupOrphanImages() {
228231
this.logger.log('--> 开始清理孤儿图片')
229232
const { deletedCount, totalOrphan } =
230233
await this.fileReferenceService.cleanupOrphanFiles(60)
231234
this.logger.log(
232235
`--> 清理孤儿图片完成:删除了 ${deletedCount}/${totalOrphan} 个文件`,
233236
)
237+
return { deletedCount, totalOrphan }
234238
}
235239
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Body, Delete, Get, Param, Post, Query } from '@nestjs/common'
2+
import { ApiController } from '~/common/decorators/api-controller.decorator'
3+
import { Auth } from '~/common/decorators/auth.decorator'
4+
import { HTTPDecorators } from '~/common/decorators/http.decorator'
5+
import { BizException } from '~/common/exceptions/biz.exception'
6+
import { ErrorCodeEnum } from '~/constants/error-code.constant'
7+
import { TaskStatus } from '~/processors/task-queue/task-queue.types'
8+
import { isString } from '~/utils/validator.util'
9+
import { CronTaskService } from './cron-task.service'
10+
import { CronTaskType, type CronTaskTypeValue } from './cron-task.types'
11+
12+
@ApiController('cron-task')
13+
@Auth()
14+
export class CronTaskController {
15+
constructor(private readonly cronTaskService: CronTaskService) {}
16+
17+
@Get('/')
18+
@HTTPDecorators.Bypass
19+
async getCronDefinitions() {
20+
return this.cronTaskService.getCronDefinitions()
21+
}
22+
23+
@Get('/tasks')
24+
@HTTPDecorators.Bypass
25+
async getTasks(
26+
@Query('status') status?: TaskStatus,
27+
@Query('type') type?: CronTaskTypeValue,
28+
@Query('page') page?: string,
29+
@Query('size') size?: string,
30+
) {
31+
return this.cronTaskService.getTasks({
32+
status,
33+
type,
34+
page: page ? Number.parseInt(page, 10) : 1,
35+
size: size ? Number.parseInt(size, 10) : 50,
36+
})
37+
}
38+
39+
@Get('/tasks/:taskId')
40+
@HTTPDecorators.Bypass
41+
async getTask(@Param('taskId') taskId: string) {
42+
if (!isString(taskId)) {
43+
throw new BizException(
44+
ErrorCodeEnum.InvalidParameter,
45+
'taskId must be string',
46+
)
47+
}
48+
const task = await this.cronTaskService.getTask(taskId)
49+
if (!task) {
50+
throw new BizException(ErrorCodeEnum.AITaskNotFound)
51+
}
52+
return task
53+
}
54+
55+
@Post('/run/:type')
56+
async runCronTask(@Param('type') type: string) {
57+
if (!isString(type)) {
58+
throw new BizException(
59+
ErrorCodeEnum.InvalidParameter,
60+
'type must be string',
61+
)
62+
}
63+
64+
const validTypes = Object.values(CronTaskType) as string[]
65+
if (!validTypes.includes(type)) {
66+
throw new BizException(ErrorCodeEnum.CronNotFound, type)
67+
}
68+
69+
return this.cronTaskService.createCronTask(type as CronTaskTypeValue)
70+
}
71+
72+
@Post('/tasks/:taskId/cancel')
73+
async cancelTask(@Param('taskId') taskId: string) {
74+
if (!isString(taskId)) {
75+
throw new BizException(
76+
ErrorCodeEnum.InvalidParameter,
77+
'taskId must be string',
78+
)
79+
}
80+
const success = await this.cronTaskService.cancelTask(taskId)
81+
return { success }
82+
}
83+
84+
@Post('/tasks/:taskId/retry')
85+
async retryTask(@Param('taskId') taskId: string) {
86+
if (!isString(taskId)) {
87+
throw new BizException(
88+
ErrorCodeEnum.InvalidParameter,
89+
'taskId must be string',
90+
)
91+
}
92+
return this.cronTaskService.retryTask(taskId)
93+
}
94+
95+
@Delete('/tasks/:taskId')
96+
async deleteTask(@Param('taskId') taskId: string) {
97+
if (!isString(taskId)) {
98+
throw new BizException(
99+
ErrorCodeEnum.InvalidParameter,
100+
'taskId must be string',
101+
)
102+
}
103+
await this.cronTaskService.deleteTask(taskId)
104+
return { success: true }
105+
}
106+
107+
@Delete('/tasks')
108+
async deleteTasks(
109+
@Body()
110+
body: {
111+
status?: TaskStatus
112+
type?: CronTaskTypeValue
113+
before: number
114+
},
115+
) {
116+
const deleted = await this.cronTaskService.deleteTasks(body)
117+
return { deleted }
118+
}
119+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { forwardRef, Module } from '@nestjs/common'
2+
import { AggregateModule } from '~/modules/aggregate/aggregate.module'
3+
import { CronBusinessService } from './cron-business.service'
4+
import { CronTaskController } from './cron-task.controller'
5+
import { CronTaskScheduler } from './cron-task.scheduler'
6+
import { CronTaskService } from './cron-task.service'
7+
8+
@Module({
9+
imports: [forwardRef(() => AggregateModule)],
10+
controllers: [CronTaskController],
11+
providers: [CronBusinessService, CronTaskService, CronTaskScheduler],
12+
exports: [CronTaskService, CronBusinessService],
13+
})
14+
export class CronTaskModule {}

0 commit comments

Comments
 (0)