```
refactor(scheduler): 优化定时任务调度器的重复任务处理和启动注册机制

- 在scheduleMaintenanceTask和scheduleTimingTask方法中添加检查逻辑,先删除已存在的重复任务
- 重构rescheduleMaintenanceTask和rescheduleTimingTask方法,当触发器不存在或类型不符时不抛异常而重新注册
- 添加isRegistered方法判断任务是否已在调度器中注册
- 添加resolveStartTime方法处理空值或过期时间的情况,避免历史任务被补跑
- 新增MaintenanceTaskRegistrar和TimingTaskRegistrar组件,在系统启动时自动注册历史有效的定时任务
- 统一设置错过执行策略为withMisfireHandlingInstructionDoNothing
```
已添加2个文件
已修改2个文件
197 ■■■■■ 文件已修改
src/main/java/com/ruoyi/device/init/MaintenanceTaskRegistrar.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/init/TimingTaskRegistrar.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/init/MaintenanceTaskRegistrar.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.device.init;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.device.mapper.MaintenanceTaskMapper;
import com.ruoyi.device.pojo.MaintenanceTask;
import com.ruoyi.device.service.impl.MaintenanceTaskScheduler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * å¯åŠ¨æ—¶æŠŠå·²å¯ç”¨çš„ä¿å…»å®šæ—¶ä»»åŠ¡æ³¨å†Œåˆ°è°ƒåº¦å™¨ï¼Œä¿è¯åŽ†å²æ•°æ®ï¼ˆç›´æŽ¥å†™åº“ã€ä»Žæœªç»è¿‡ service)也能被执行
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MaintenanceTaskRegistrar implements ApplicationRunner {
    private final MaintenanceTaskMapper maintenanceTaskMapper;
    private final MaintenanceTaskScheduler maintenanceTaskScheduler;
    @Override
    public void run(ApplicationArguments args) {
        try {
            List<MaintenanceTask> tasks = maintenanceTaskMapper.selectList(
                    new QueryWrapper<MaintenanceTask>().eq("deleted", 0).eq("is_active", 1));
            int registered = 0;
            for (MaintenanceTask task : tasks) {
                try {
                    if (maintenanceTaskScheduler.isRegistered(task.getId())) {
                        continue;
                    }
                    maintenanceTaskScheduler.scheduleMaintenanceTask(task);
                    registered++;
                } catch (Exception e) {
                    log.warn("保养定时任务注册失败 id={} name={}", task.getId(), task.getTaskName(), e);
                }
            }
            log.info("保养定时任务注册完成:待注册 {} æ¡ï¼Œæœ¬æ¬¡æ–°æ³¨å†Œ {} æ¡", tasks.size(), registered);
        } catch (Exception e) {
            log.error("保养定时任务启动注册失败", e);
        }
    }
}
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
@@ -6,6 +6,7 @@
import org.quartz.*;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
@@ -31,6 +32,10 @@
        try {
            JobDetail jobDetail = buildJobDetail(task);
            Trigger trigger = buildJobTrigger(task, jobDetail);
            if (scheduler.checkExists(jobDetail.getKey())) {
                // æŒä¹…化的Job可能还在(例如历史数据直接写入、上次未清理),先移除旧的Job及其触发器
                scheduler.deleteJob(jobDetail.getKey());
            }
            scheduler.scheduleJob(jobDetail, trigger);
        }catch (SchedulerException e){
            log.error("SchedulerException scheduleMaintenanceTask ERROR",e);
@@ -45,30 +50,49 @@
       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");
               // è§¦å‘器不存在或类型不符(如任务由直接写库创建),重新注册而不是报错
               scheduleMaintenanceTask(task);
               return;
           }
           // 3. æž„建CronTrigger,确保持久化配置
           // æž„建CronTrigger,确保持久化配置
           CronTrigger newTrigger = TriggerBuilder.newTrigger()
                   .withIdentity(triggerKey)                // å”¯ä¸€æ ‡è¯†ï¼Œç”¨äºŽæŒä¹…化存储
                   .withDescription(task.getTaskName() + "_TRIGGER") // è§¦å‘器描述
                   .forJob(oldTrigger.getJobKey())                       // å…³è”对应的Job
                   .withSchedule(CronScheduleBuilder
                           .cronSchedule(convertToCronExpression(task)) // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                           .cronSchedule(convertToCronExpression(task))
                           .withMisfireHandlingInstructionDoNothing() // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                   )
                   // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull则立即生效)
                   .startAt(task.getNextExecutionTime() != null
                           ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                           : new Date())
                   // è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull或已过期则立即生效)
                   .startAt(resolveStartTime(task))
                   .build();
           scheduler.rescheduleJob(triggerKey, newTrigger);
       }catch (SchedulerException e){
           log.error("SchedulerException rescheduleMaintenanceTask ERROR",e);
           throw new RuntimeException(e);
       }
    }
    /**
     * åˆ¤æ–­ä»»åŠ¡æ˜¯å¦å·²æ³¨å†Œåˆ°è°ƒåº¦å™¨
     */
    public boolean isRegistered(Long taskId) throws SchedulerException {
        return scheduler.checkExists(new TriggerKey("triggerMaintenanceTask_" + taskId));
    }
    /**
     * è§¦å‘器开始时间:为空或已过期时从当前时间起算,避免补跑历史
     */
    private Date resolveStartTime(MaintenanceTask task) {
        LocalDateTime nextExecutionTime = task.getNextExecutionTime();
        if (nextExecutionTime == null || !nextExecutionTime.isAfter(LocalDateTime.now())) {
            return new Date();
        }
        return Date.from(nextExecutionTime.atZone(ZoneId.systemDefault()).toInstant());
    }
    /**
@@ -137,10 +161,8 @@
                        .cronSchedule(cronExpression)
                        .withMisfireHandlingInstructionDoNothing() // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                )
                // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull则立即生效)
                .startAt(task.getNextExecutionTime() != null
                        ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                        : new Date())
                // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull或已过期则立即生效)
                .startAt(resolveStartTime(task))
                .build();
    }
    private String convertToCronExpression(MaintenanceTask task) {
src/main/java/com/ruoyi/inspectiontask/init/TimingTaskRegistrar.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.inspectiontask.init;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.inspectiontask.mapper.TimingTaskMapper;
import com.ruoyi.inspectiontask.pojo.TimingTask;
import com.ruoyi.inspectiontask.service.impl.TimingTaskScheduler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * å¯åŠ¨æ—¶æŠŠå·²å¯ç”¨çš„å·¡æ£€å®šæ—¶ä»»åŠ¡æ³¨å†Œåˆ°è°ƒåº¦å™¨ï¼Œä¿è¯åŽ†å²æ•°æ®ï¼ˆç›´æŽ¥å†™åº“ã€ä»Žæœªç»è¿‡ service)也能被执行
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TimingTaskRegistrar implements ApplicationRunner {
    private final TimingTaskMapper timingTaskMapper;
    private final TimingTaskScheduler timingTaskScheduler;
    @Override
    public void run(ApplicationArguments args) {
        try {
            List<TimingTask> tasks = timingTaskMapper.selectList(
                    new QueryWrapper<TimingTask>().eq("deleted", 0).eq("is_enabled", 1));
            int registered = 0;
            for (TimingTask task : tasks) {
                try {
                    if (timingTaskScheduler.isRegistered(task.getId())) {
                        continue;
                    }
                    timingTaskScheduler.scheduleTimingTask(task);
                    registered++;
                } catch (Exception e) {
                    log.warn("巡检定时任务注册失败 id={} name={}", task.getId(), task.getTaskName(), e);
                }
            }
            log.info("巡检定时任务注册完成:待注册 {} æ¡ï¼Œæœ¬æ¬¡æ–°æ³¨å†Œ {} æ¡", tasks.size(), registered);
        } catch (Exception e) {
            log.error("巡检定时任务启动注册失败", e);
        }
    }
}
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
@@ -5,6 +5,7 @@
import org.quartz.*;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
@@ -24,6 +25,10 @@
    public void scheduleTimingTask(TimingTask task) throws SchedulerException {
        JobDetail jobDetail = buildJobDetail(task);
        Trigger trigger = buildJobTrigger(task, jobDetail);
        if (scheduler.checkExists(jobDetail.getKey())) {
            // æŒä¹…化的Job可能还在(例如历史数据直接写入、上次未清理),先移除旧的Job及其触发器
            scheduler.deleteJob(jobDetail.getKey());
        }
        scheduler.scheduleJob(jobDetail, trigger);
    }
@@ -33,36 +38,46 @@
    public void rescheduleTimingTask(TimingTask task) throws SchedulerException {
        TriggerKey triggerKey = new TriggerKey("trigger_" + task.getId());
        // èŽ·å–çŽ°æœ‰è§¦å‘å™¨å¹¶è½¬æ¢ä¸º CronTrigger
        // èŽ·å–çŽ°æœ‰è§¦å‘å™¨
        Trigger oldTrigger = scheduler.getTrigger(triggerKey);
        if (!(oldTrigger instanceof CronTrigger)) {
            throw new SchedulerException("Existing trigger is not a CronTrigger");
            // è§¦å‘器不存在或类型不符(如任务由直接写库创建),重新注册而不是报错
            scheduleTimingTask(task);
            return;
        }
        // 3. æž„建CronTrigger,确保持久化配置
        // æž„建CronTrigger,确保持久化配置
        CronTrigger newTrigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)                // å”¯ä¸€æ ‡è¯†ï¼Œç”¨äºŽæŒä¹…化存储
                .withDescription(task.getTaskName() + "_TRIGGER") // è§¦å‘器描述
                .forJob(oldTrigger.getJobKey())                       // å…³è”对应的Job
                .withSchedule(CronScheduleBuilder
                        .cronSchedule(convertToCronExpression(task)) // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                        .cronSchedule(convertToCronExpression(task))
                        .withMisfireHandlingInstructionDoNothing() // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                )
                // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull则立即生效)
                .startAt(task.getNextExecutionTime() != null
                        ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                        : new Date())
                // è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull或已过期则立即生效)
                .startAt(resolveStartTime(task))
                .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 boolean isRegistered(Long taskId) throws SchedulerException {
        return scheduler.checkExists(new TriggerKey("trigger_" + taskId));
    }
    /**
     * è§¦å‘器开始时间:为空或已过期时从当前时间起算,避免补跑历史
     */
    private Date resolveStartTime(TimingTask task) {
        LocalDateTime nextExecutionTime = task.getNextExecutionTime();
        if (nextExecutionTime == null || !nextExecutionTime.isAfter(LocalDateTime.now())) {
            return new Date();
        }
        return Date.from(nextExecutionTime.atZone(ZoneId.systemDefault()).toInstant());
    }
    /**
@@ -130,10 +145,8 @@
                        .cronSchedule(cronExpression)
                        .withMisfireHandlingInstructionDoNothing() // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                )
                // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull则立即生效)
                .startAt(task.getNextExecutionTime() != null
                        ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                        : new Date())
                // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull或已过期则立即生效)
                .startAt(resolveStartTime(task))
                .build();
    }
    private String convertToCronExpression(TimingTask task) {