| | |
| | | |
| | | import com.ruoyi.device.pojo.MaintenanceTask; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.quartz.*; |
| | | import org.quartz.CronScheduleBuilder; |
| | | import org.quartz.CronTrigger; |
| | | import org.quartz.JobBuilder; |
| | | import org.quartz.JobDataMap; |
| | | import org.quartz.JobDetail; |
| | | import org.quartz.JobKey; |
| | | import org.quartz.Scheduler; |
| | | import org.quartz.SchedulerException; |
| | | import org.quartz.Trigger; |
| | | import org.quartz.TriggerBuilder; |
| | | import org.quartz.TriggerKey; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | |
| | | import java.util.Date; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * @author :yys |
| | | * @date : 2025/12/22 15:16 |
| | | */ |
| | | @Service |
| | | @Slf4j |
| | | public class MaintenanceTaskScheduler { |
| | |
| | | @Autowired |
| | | private Scheduler scheduler; |
| | | |
| | | /** |
| | | * 添加新任务到调度器 |
| | | */ |
| | | public void scheduleMaintenanceTask(MaintenanceTask task){ |
| | | try { |
| | | JobDetail jobDetail = buildJobDetail(task); |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 更新已有任务 |
| | | */ |
| | | public void rescheduleMaintenanceTask(MaintenanceTask task){ |
| | | try{ |
| | | TriggerKey triggerKey = new TriggerKey("triggerMaintenanceTask_" + task.getId()); |
| | | |
| | | // 获取现有触发器并转换为 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则立即生效) |
| | | .withIdentity(triggerKey) |
| | | .withDescription(task.getTaskName() + "_TRIGGER") |
| | | .forJob(oldTrigger.getJobKey()) |
| | | .withSchedule(CronScheduleBuilder.cronSchedule(convertToCronExpression(task))) |
| | | .startAt(task.getNextExecutionTime() != null |
| | | ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant()) |
| | | : new Date()) |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 暂停任务 |
| | | */ |
| | | public void pauseMaintenanceTask(Long taskId) throws SchedulerException { |
| | | JobKey jobKey = new JobKey("MaintenanceTask_" + taskId); |
| | | scheduler.pauseJob(jobKey); |
| | | scheduler.pauseJob(new JobKey("MaintenanceTask_" + taskId)); |
| | | } |
| | | |
| | | /** |
| | | * 恢复任务 |
| | | */ |
| | | public void resumeMaintenanceTask(Long taskId) throws SchedulerException { |
| | | JobKey jobKey = new JobKey("MaintenanceTask_" + taskId); |
| | | scheduler.resumeJob(jobKey); |
| | | scheduler.resumeJob(new JobKey("MaintenanceTask_" + taskId)); |
| | | } |
| | | |
| | | /** |
| | | * 删除任务 |
| | | */ |
| | | public void unscheduleMaintenanceTask(Long taskId){ |
| | | try { |
| | | JobKey jobKey = new JobKey("MaintenanceTask_" + taskId); |
| | | scheduler.deleteJob(jobKey); |
| | | scheduler.deleteJob(new JobKey("MaintenanceTask_" + taskId)); |
| | | }catch (SchedulerException e){ |
| | | log.error("SchedulerException unscheduleMaintenanceTask ERROR",e); |
| | | throw new RuntimeException(e); |
| | |
| | | } |
| | | |
| | | private JobDetail buildJobDetail(MaintenanceTask task) { |
| | | // 1. 构建唯一JobKey(基于任务ID,确保重启后能识别) |
| | | JobKey jobKey = new JobKey("MaintenanceTask_" + task.getId()); |
| | | |
| | | // 2. 封装任务数据(仅使用基本类型,确保可序列化) |
| | | JobDataMap jobDataMap = new JobDataMap(); |
| | | jobDataMap.put("maintenanceTaskId", task.getId()); // 任务ID(Long,可序列化) |
| | | jobDataMap.put("taskName", task.getTaskName()); // 任务名称(String,可序列化) |
| | | jobDataMap.put("taskType", task.getFrequencyType()); // 任务类型(String) |
| | | // 按需添加其他必要的基本类型参数 |
| | | jobDataMap.put("maintenanceTaskId", task.getId()); |
| | | jobDataMap.put("taskName", task.getTaskName()); |
| | | jobDataMap.put("taskType", task.getFrequencyType()); |
| | | |
| | | // 3. 构建JobDetail,设置持久化相关属性 |
| | | return JobBuilder.newJob(MaintenanceTaskJob.class) |
| | | .withIdentity(jobKey) // 唯一标识,用于持久化存储 |
| | | .withDescription(task.getTaskName()) // 任务描述,存入数据库 |
| | | .usingJobData(jobDataMap) // 绑定任务数据 |
| | | .storeDurably(true) // 即使没有触发器关联也持久化保存 |
| | | .requestRecovery(true) // 当调度器崩溃后恢复时,重新执行未完成的任务 |
| | | .withIdentity(jobKey) |
| | | .withDescription(task.getTaskName()) |
| | | .usingJobData(jobDataMap) |
| | | .storeDurably(true) |
| | | .requestRecovery(true) |
| | | .build(); |
| | | } |
| | | |
| | | private Trigger buildJobTrigger(MaintenanceTask task, JobDetail jobDetail) { |
| | | // 1. 构建唯一TriggerKey(基于任务ID) |
| | | TriggerKey triggerKey = new TriggerKey("triggerMaintenanceTask_" + task.getId()); |
| | | |
| | | // 2. 生成Cron表达式(原逻辑不变) |
| | | String cronExpression = convertToCronExpression(task); |
| | | |
| | | // 3. 构建CronTrigger,确保持久化配置 |
| | | return TriggerBuilder.newTrigger() |
| | | .withIdentity(triggerKey) // 唯一标识,用于持久化存储 |
| | | .withDescription(task.getTaskName() + "_TRIGGER") // 触发器描述 |
| | | .forJob(jobDetail) // 关联对应的Job |
| | | .withSchedule(CronScheduleBuilder |
| | | .cronSchedule(cronExpression) |
| | | .withMisfireHandlingInstructionDoNothing() // 错过执行时的策略(根据业务调整) |
| | | ) |
| | | // 4. 设置开始时间(若为null则立即生效) |
| | | .withIdentity(triggerKey) |
| | | .withDescription(task.getTaskName() + "_TRIGGER") |
| | | .forJob(jobDetail) |
| | | .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression) |
| | | .withMisfireHandlingInstructionDoNothing()) |
| | | .startAt(task.getNextExecutionTime() != null |
| | | ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant()) |
| | | : new Date()) |
| | | .build(); |
| | | } |
| | | |
| | | private String convertToCronExpression(MaintenanceTask task) { |
| | | // 参数校验 |
| | | if (task == null || task.getFrequencyType() == null || task.getFrequencyDetail() == null) { |
| | | throw new IllegalArgumentException("任务参数不能为空"); |
| | | } |
| | | |
| | | // 使用switch确保条件互斥 |
| | | String frequencyType = task.getFrequencyType().toUpperCase(); // 统一转为大写比较 |
| | | String frequencyType = task.getFrequencyType().toUpperCase(); |
| | | switch (frequencyType) { |
| | | case "DAILY": |
| | | return convertDailyToCron(task.getFrequencyDetail()); |
| | |
| | | return convertMonthlyToCron(task.getFrequencyDetail()); |
| | | case "QUARTERLY": |
| | | return convertQuarterlyToCron(task.getFrequencyDetail()); |
| | | case "YEARLY": |
| | | return convertYearlyToCron(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]); |
| | |
| | | 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]); |
| | |
| | | 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]); // 解析时间 |
| | | int month = validateMonth(parts[0]); |
| | | 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); |
| | | return String.format("0 %d %d %d %d/3 ?", time.getMinute(), time.getHour(), day, quarterStartMonth); |
| | | } |
| | | |
| | | // 新增验证月份的方法(1-12) |
| | | private String convertYearlyToCron(String frequencyDetail) { |
| | | String[] parts = validateAndSplit(frequencyDetail, ",", 3); |
| | | int month = validateMonth(parts[0]); |
| | | int day = validateDayOfMonth(parts[1]); |
| | | LocalTime time = parseTime(parts[2]); |
| | | return String.format("0 %d %d %d %d ?", time.getMinute(), time.getHour(), day, month); |
| | | } |
| | | |
| | | private int validateMonth(String monthStr) { |
| | | try { |
| | | int month = Integer.parseInt(monthStr); |
| | |
| | | } |
| | | } |
| | | |
| | | // 辅助方法:解析时间 |
| | | private LocalTime parseTime(String timeStr) { |
| | | try { |
| | | return LocalTime.parse(timeStr); |
| | |
| | | } |
| | | } |
| | | |
| | | // 辅助方法:验证并分割字符串 |
| | | 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)); |
| | | 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) { |
| | |
| | | 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); |
| | | 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); |
| | | } |
| | | } |
| | | |
| | | } |