localhost_oa_based/src/modules/system/task/task.service.ts

370 lines
9.7 KiB
TypeScript
Raw Normal View History

2024-02-28 08:32:35 +08:00
import { InjectRedis } from '@liaoliaots/nestjs-redis'
import { InjectQueue } from '@nestjs/bull'
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
OnModuleInit,
} from '@nestjs/common'
import { ModuleRef, Reflector } from '@nestjs/core'
import { UnknownElementException } from '@nestjs/core/errors/exceptions/unknown-element.exception'
import { InjectRepository } from '@nestjs/typeorm'
import { Queue } from 'bull'
import Redis from 'ioredis'
import { isEmpty, isNumber } from 'lodash'
2024-02-28 08:32:35 +08:00
import { Like, Repository } from 'typeorm'
import { BusinessException } from '~/common/exceptions/biz.exception'
import { ErrorEnum } from '~/constants/error-code.constant'
import { paginate } from '~/helper/paginate'
import { Pagination } from '~/helper/paginate/pagination'
import { TaskEntity } from '~/modules/system/task/task.entity'
import { MISSION_DECORATOR_KEY } from '~/modules/tasks/mission.decorator'
import {
SYS_TASK_QUEUE_NAME,
SYS_TASK_QUEUE_PREFIX,
TaskStatus,
} from './constant'
import { TaskDto, TaskQueryDto, TaskUpdateDto } from './task.dto'
@Injectable()
export class TaskService implements OnModuleInit {
private logger = new Logger(TaskService.name)
constructor(
@InjectRepository(TaskEntity)
private taskRepository: Repository<TaskEntity>,
@InjectQueue(SYS_TASK_QUEUE_NAME) private taskQueue: Queue,
private moduleRef: ModuleRef,
private reflector: Reflector,
@InjectRedis() private redis: Redis,
) {}
/**
* module init
*/
async onModuleInit() {
await this.initTask()
}
/**
*
*/
async initTask(): Promise<void> {
const initKey = `${SYS_TASK_QUEUE_PREFIX}:init`
// 防止重复初始化
const result = await this.redis
.multi()
.setnx(initKey, new Date().getTime())
.expire(initKey, 60 * 30)
.exec()
if (result[0][1] === 0) {
// 存在锁则直接跳过防止重复初始化
this.logger.log('Init task is lock', TaskService.name)
return
}
const jobs = await this.taskQueue.getJobs([
'active',
'delayed',
'failed',
'paused',
'waiting',
'completed',
])
jobs.forEach((j) => {
j.remove()
})
// 查找所有需要运行的任务
const tasks = await this.taskRepository.findBy({ status: 1 })
if (tasks && tasks.length > 0) {
for (const t of tasks)
await this.start(t)
}
// 启动后释放锁
await this.redis.del(initKey)
}
async list({
page,
pageSize,
name,
service,
type,
status,
}: TaskQueryDto): Promise<Pagination<TaskEntity>> {
const queryBuilder = this.taskRepository
.createQueryBuilder('task')
.where({
...(name ? { name: Like(`%${name}%`) } : null),
...(service ? { service: Like(`%${service}%`) } : null),
...(type ? { type } : null),
...(isNumber(status) ? { status } : null),
2024-02-28 08:32:35 +08:00
})
.orderBy('task.id', 'ASC')
return paginate(queryBuilder, { page, pageSize })
}
/**
* task info
*/
async info(id: number): Promise<TaskEntity> {
const task = this.taskRepository
.createQueryBuilder('task')
.where({ id })
.getOne()
if (!task)
throw new NotFoundException('Task Not Found')
return task
}
/**
* delete task
*/
async delete(task: TaskEntity): Promise<void> {
if (!task)
throw new BadRequestException('Task is Empty')
await this.stop(task)
await this.taskRepository.delete(task.id)
}
/**
*
*/
async once(task: TaskEntity): Promise<void | never> {
if (task) {
await this.taskQueue.add(
{ id: task.id, service: task.service, args: task.data },
{ jobId: task.id, removeOnComplete: true, removeOnFail: true },
)
}
else {
throw new BadRequestException('Task is Empty')
}
}
async create(dto: TaskDto): Promise<void> {
const result = await this.taskRepository.save(dto)
const task = await this.info(result.id)
if (result.status === 0)
await this.stop(task)
else if (result.status === TaskStatus.Activited)
await this.start(task)
}
async update(id: number, dto: TaskUpdateDto): Promise<void> {
await this.taskRepository.update(id, dto)
const task = await this.info(id)
if (task.status === 0)
await this.stop(task)
else if (task.status === TaskStatus.Activited)
await this.start(task)
}
/**
*
*/
async start(task: TaskEntity): Promise<void> {
if (!task)
throw new BadRequestException('Task is Empty')
// 先停掉之前存在的任务
await this.stop(task)
let repeat: any
if (task.type === 1) {
// 间隔 Repeat every millis (cron setting cannot be used together with this setting.)
repeat = {
every: task.every,
}
}
else {
// cron
repeat = {
cron: task.cron,
}
// Start date when the repeat job should start repeating (only with cron).
if (task.startTime)
repeat.startDate = task.startTime
if (task.endTime)
repeat.endDate = task.endTime
}
if (task.limit > 0)
repeat.limit = task.limit
const job = await this.taskQueue.add(
{ id: task.id, service: task.service, args: task.data },
{ jobId: task.id, removeOnComplete: true, removeOnFail: true, repeat },
)
if (job && job.opts) {
await this.taskRepository.update(task.id, {
jobOpts: JSON.stringify(job.opts.repeat),
status: 1,
})
}
else {
// update status to 0标识暂停任务因为启动失败
await job?.remove()
await this.taskRepository.update(task.id, {
status: TaskStatus.Disabled,
})
throw new BadRequestException('Task Start failed')
}
}
/**
*
*/
async stop(task: TaskEntity): Promise<void> {
if (!task)
throw new BadRequestException('Task is Empty')
const exist = await this.existJob(task.id.toString())
if (!exist) {
await this.taskRepository.update(task.id, {
status: TaskStatus.Disabled,
})
return
}
const jobs = await this.taskQueue.getJobs([
'active',
'delayed',
'failed',
'paused',
'waiting',
'completed',
])
jobs
.filter(j => j.data.id === task.id)
.forEach(async (j) => {
await j.remove()
})
await this.taskRepository.update(task.id, { status: TaskStatus.Disabled })
// if (task.jobOpts) {
// await this.app.queue.sys.removeRepeatable(JSON.parse(task.jobOpts));
// // update status
// await this.getRepo().admin.sys.Task.update(task.id, { status: TaskStatus.Disabled, });
// }
}
/**
*
*/
async existJob(jobId: string): Promise<boolean> {
// https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueremoverepeatablebykey
const jobs = await this.taskQueue.getRepeatableJobs()
const ids = jobs.map((e) => {
return e.id
})
return ids.includes(jobId)
}
/**
*
*/
async updateTaskCompleteStatus(tid: number): Promise<void> {
const jobs = await this.taskQueue.getRepeatableJobs()
const task = await this.taskRepository.findOneBy({ id: tid })
// 如果下次执行时间小于当前时间,则表示已经执行完成。
for (const job of jobs) {
const currentTime = new Date().getTime()
if (job.id === tid.toString() && job.next < currentTime) {
// 如果下次执行时间小于当前时间,则表示已经执行完成。
await this.stop(task)
break
}
}
}
/**
* service是否有注解定义
* @param serviceName service
*/
async checkHasMissionMeta(
nameOrInstance: string | unknown,
exec: string,
): Promise<void | never> {
try {
let service: any
if (typeof nameOrInstance === 'string')
service = await this.moduleRef.get(nameOrInstance, { strict: false })
else
service = nameOrInstance
// 所执行的任务不存在
if (!service || !(exec in service))
throw new NotFoundException('任务不存在')
// 检测是否有Mission注解
const hasMission = this.reflector.get<boolean>(
MISSION_DECORATOR_KEY,
service.constructor,
)
// 如果没有,则抛出错误
if (!hasMission)
throw new BusinessException(ErrorEnum.INSECURE_MISSION)
}
catch (e) {
if (e instanceof UnknownElementException) {
// 任务不存在
throw new NotFoundException('任务不存在')
}
else {
// 其余错误则不处理,继续抛出
throw e
}
}
}
/**
* serviceName调用service LogService.clearReqLog
*/
async callService(name: string, args: string): Promise<void> {
if (name) {
const [serviceName, methodName] = name.split('.')
if (!methodName)
throw new BadRequestException('serviceName define BadRequestException')
const service = await this.moduleRef.get(serviceName, {
strict: false,
})
// 安全注解检查
await this.checkHasMissionMeta(service, methodName)
if (isEmpty(args)) {
await service[methodName]()
}
else {
// 参数安全判断
const parseArgs = this.safeParse(args)
if (Array.isArray(parseArgs)) {
// 数组形式则自动扩展成方法参数回掉
await service[methodName](...parseArgs)
}
else {
await service[methodName](parseArgs)
}
}
}
}
safeParse(args: string): unknown | string {
try {
return JSON.parse(args)
}
catch (e) {
return args
}
}
}