package com.ruoyi.inspectiontask.service.impl;
|
|
import com.ruoyi.inspectiontask.pojo.TimingTask;
|
import org.quartz.*;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
|
import java.time.LocalTime;
|
import java.time.ZoneId;
|
import java.time.format.DateTimeParseException;
|
import java.util.Arrays;
|
import java.util.Date;
|
import java.util.stream.Collectors;
|
|
@Service
|
public class TimingTaskScheduler {
|
|
@Autowired
|
private Scheduler scheduler;
|
|
/**
|
* 添加新任务到调度器
|
*/
|
public void scheduleTimingTask(TimingTask task) throws SchedulerException {
|
JobDetail jobDetail = buildJobDetail(task);
|
Trigger trigger = buildJobTrigger(task, jobDetail);
|
scheduler.scheduleJob(jobDetail, trigger);
|
}
|
|
/**
|
* 更新已有任务
|
*/
|
public void rescheduleTimingTask(TimingTask task) throws SchedulerException {
|
TriggerKey triggerKey = new TriggerKey("trigger_" + task.getId(), "TIMING_TASK_TRIGGER_GROUP");
|
|
// 获取现有触发器并转换为 CronTrigger
|
Trigger oldTrigger = scheduler.getTrigger(triggerKey);
|
if (!(oldTrigger instanceof CronTrigger)) {
|
throw new SchedulerException("Existing trigger is not a CronTrigger");
|
}
|
|
// 3. 构建CronTrigger,确保持久化配置
|
CronTrigger newTrigger = TriggerBuilder.newTrigger()
|
.withIdentity(triggerKey) // 唯一标识,用于持久化存储
|
.withDescription(task.getTaskName() + "_TRIGGER") // 触发器描述
|
.forJob(oldTrigger.getJobKey()) // 关联对应的Job
|
.withSchedule(CronScheduleBuilder
|
.cronSchedule(convertToCronExpression(task)) // 错过执行时的策略(根据业务调整)
|
)
|
// 4. 设置开始时间(若为null则立即生效)
|
.startAt(task.getNextExecutionTime() != null
|
? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
|
: new Date())
|
.build();
|
|
// 构建新触发器
|
// Trigger newTrigger = TriggerBuilder.newTrigger()
|
// .withIdentity(triggerKey)
|
// .withDescription(task.getTaskName())
|
// .withSchedule(CronScheduleBuilder.cronSchedule(convertToCronExpression(task)))
|
// .startAt(Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant()))
|
// .forJob(oldTrigger.getJobKey())
|
// .build();
|
|
scheduler.rescheduleJob(triggerKey, newTrigger);
|
}
|
|
/**
|
* 暂停任务
|
*/
|
public void pauseTimingTask(Long taskId) throws SchedulerException {
|
JobKey jobKey = new JobKey("timingTask_" + taskId);
|
scheduler.pauseJob(jobKey);
|
}
|
|
/**
|
* 恢复任务
|
*/
|
public void resumeTimingTask(Long taskId) throws SchedulerException {
|
JobKey jobKey = new JobKey("timingTask_" + taskId);
|
scheduler.resumeJob(jobKey);
|
}
|
|
/**
|
* 删除任务
|
*/
|
public void unscheduleTimingTask(Long taskId) throws SchedulerException {
|
JobKey jobKey = new JobKey("timingTask_" + taskId);
|
scheduler.deleteJob(jobKey);
|
}
|
|
private JobDetail buildJobDetail(TimingTask task) {
|
// 1. 构建唯一JobKey(基于任务ID,确保重启后能识别)
|
JobKey jobKey = new JobKey("timingTask_" + task.getId(), "TIMING_TASK_GROUP");
|
|
// 2. 封装任务数据(仅使用基本类型,确保可序列化)
|
JobDataMap jobDataMap = new JobDataMap();
|
jobDataMap.put("taskId", task.getId()); // 任务ID(Long,可序列化)
|
jobDataMap.put("taskName", task.getTaskName()); // 任务名称(String,可序列化)
|
jobDataMap.put("taskType", task.getFrequencyType()); // 任务类型(String)
|
// 按需添加其他必要的基本类型参数
|
|
// 3. 构建JobDetail,设置持久化相关属性
|
return JobBuilder.newJob(TimingTaskJob.class)
|
.withIdentity(jobKey) // 唯一标识,用于持久化存储
|
.withDescription(task.getTaskName()) // 任务描述,存入数据库
|
.usingJobData(jobDataMap) // 绑定任务数据
|
.storeDurably() // 即使没有触发器关联也持久化保存
|
.requestRecovery(true) // 当调度器崩溃后恢复时,重新执行未完成的任务
|
.build();
|
}
|
|
private Trigger buildJobTrigger(TimingTask task, JobDetail jobDetail) {
|
// 1. 构建唯一TriggerKey(基于任务ID)
|
TriggerKey triggerKey = new TriggerKey("trigger_" + task.getId(), "TIMING_TASK_TRIGGER_GROUP");
|
|
// 2. 生成Cron表达式(原逻辑不变)
|
String cronExpression = convertToCronExpression(task);
|
|
// 3. 构建CronTrigger,确保持久化配置
|
CronTrigger trigger = TriggerBuilder.newTrigger()
|
.withIdentity(triggerKey) // 唯一标识,用于持久化存储
|
.withDescription(task.getTaskName() + "_TRIGGER") // 触发器描述
|
.forJob(jobDetail) // 关联对应的Job
|
.withSchedule(CronScheduleBuilder
|
.cronSchedule(cronExpression)
|
.withMisfireHandlingInstructionDoNothing() // 错过执行时的策略(根据业务调整)
|
)
|
// 4. 设置开始时间(若为null则立即生效)
|
.startAt(task.getNextExecutionTime() != null
|
? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
|
: new Date())
|
.build();
|
return trigger;
|
}
|
private String convertToCronExpression(TimingTask task) {
|
// 参数校验
|
if (task == null || task.getFrequencyType() == null || task.getFrequencyDetail() == null) {
|
throw new IllegalArgumentException("任务参数不能为空");
|
}
|
|
// 使用switch确保条件互斥
|
String frequencyType = task.getFrequencyType().toUpperCase(); // 统一转为大写比较
|
switch (frequencyType) {
|
case "DAILY":
|
return convertDailyToCron(task.getFrequencyDetail());
|
case "WEEKLY":
|
return convertWeeklyToCron(task.getFrequencyDetail());
|
case "MONTHLY":
|
return convertMonthlyToCron(task.getFrequencyDetail());
|
case "QUARTERLY":
|
return convertQuarterlyToCron(task.getFrequencyDetail());
|
default:
|
throw new IllegalArgumentException("不支持的频率类型: " + task.getFrequencyType());
|
}
|
}
|
|
// 每日任务转换
|
private String convertDailyToCron(String frequencyDetail) {
|
LocalTime time = parseTime(frequencyDetail);
|
return String.format("0 %d %d * * ?", time.getMinute(), time.getHour());
|
}
|
|
// 每周任务转换
|
private String convertWeeklyToCron(String frequencyDetail) {
|
String[] parts = validateAndSplit(frequencyDetail, ",", 2);
|
String daysOfWeek = convertDayNamesToCron(parts[0]);
|
LocalTime time = parseTime(parts[1]);
|
return String.format("0 %d %d ? * %s", time.getMinute(), time.getHour(), daysOfWeek);
|
}
|
|
// 每月任务转换
|
private String convertMonthlyToCron(String frequencyDetail) {
|
String[] parts = validateAndSplit(frequencyDetail, ",", 2);
|
int day = validateDayOfMonth(parts[0]);
|
LocalTime time = parseTime(parts[1]);
|
return String.format("0 %d %d %d * ?", time.getMinute(), time.getHour(), day);
|
}
|
|
// 每季度任务转换
|
private String convertQuarterlyToCron(String frequencyDetail) {
|
String[] parts = validateAndSplit(frequencyDetail, ",", 3);
|
int month = validateMonth(parts[0]); // 验证月份(1-12)
|
int day = validateDayOfMonth(parts[1]); // 验证日期
|
LocalTime time = parseTime(parts[2]); // 解析时间
|
|
// 计算季度起始月份(1月=1, 4月=4, 7月=7, 10月=10)
|
int quarterStartMonth = ((month - 1) / 3) * 3 + 1;
|
|
return String.format("0 %d %d %d %d/3 ?",
|
time.getMinute(),
|
time.getHour(),
|
day,
|
quarterStartMonth);
|
}
|
|
// 新增验证月份的方法(1-12)
|
private int validateMonth(String monthStr) {
|
try {
|
int month = Integer.parseInt(monthStr);
|
if (month < 1 || month > 12) {
|
throw new IllegalArgumentException("月份必须在1-12之间");
|
}
|
return month;
|
} catch (NumberFormatException e) {
|
throw new IllegalArgumentException("无效的月份格式");
|
}
|
}
|
|
// 辅助方法:解析时间
|
private LocalTime parseTime(String timeStr) {
|
try {
|
return LocalTime.parse(timeStr);
|
} catch (DateTimeParseException e) {
|
throw new IllegalArgumentException("时间格式必须为HH:mm", e);
|
}
|
}
|
|
// 辅助方法:验证并分割字符串
|
private String[] validateAndSplit(String input, String delimiter, int expectedParts) {
|
String[] parts = input.split(delimiter);
|
if (parts.length != expectedParts) {
|
throw new IllegalArgumentException(
|
String.format("格式错误,应为%d部分用'%s'分隔", expectedParts, delimiter));
|
}
|
return parts;
|
}
|
|
// 辅助方法:验证月份中的日
|
private int validateDayOfMonth(String dayStr) {
|
int day = Integer.parseInt(dayStr);
|
if (day < 1 || day > 31) {
|
throw new IllegalArgumentException("日期必须在1-31之间");
|
}
|
return day;
|
}
|
|
// 辅助方法:验证季度中的月
|
private int validateMonthInQuarter(String monthStr) {
|
int month = Integer.parseInt(monthStr);
|
if (month < 1 || month > 3) {
|
throw new IllegalArgumentException("季度月份必须是1、2或3");
|
}
|
return month;
|
}
|
|
// 转换星期几名称
|
private String convertDayNamesToCron(String dayNames) {
|
return Arrays.stream(dayNames.split("\\|"))
|
.map(this::convertSingleDayName)
|
.collect(Collectors.joining(","));
|
}
|
|
// 转换单个星期几名称
|
private String convertSingleDayName(String dayName) {
|
switch (dayName.toUpperCase()) {
|
case "MON": return "MON";
|
case "TUE": return "TUE";
|
case "WED": return "WED";
|
case "THU": return "THU";
|
case "FRI": return "FRI";
|
case "SAT": return "SAT";
|
case "SUN": return "SUN";
|
default: throw new IllegalArgumentException("无效的星期几: " + dayName);
|
}
|
}
|
}
|