云
2026-10-27 0a4d548b6bf9cfef252400fb6d6612e45cf5b602
fix(scheduler): 修复定时任务调度问题并增强系统稳定性

- 在MaintenanceTaskJob中添加任务状态检查,停用任务不再生成保养记录
- 修复MaintenanceTaskScheduler和TimingTaskScheduler中的触发器不存在处理逻辑
- 添加错过执行策略配置withMisfireHandlingInstructionDoNothing
- 实现应用启动时自动恢复未注册的定时任务功能
- 添加频率变更时重新计算下次执行时间的逻辑
- 修复季度任务执行时间计算方法并增强参数验证
- 在任务更新时根据激活状态决定是否重新调度或取消任务
已修改5个文件
177 ■■■■■ 文件已修改
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
@@ -48,6 +48,10 @@
            if (timingTask == null) {
                throw new JobExecutionException("MaintenanceTaskJob找不到定时任务: " + taskId);
            }
            // 任务已停用时不生成保养记录
            if (!timingTask.isActive()) {
                return;
            }
            // 2. 创建并保存巡检任务记录 - 这就是您提供的代码应该放的位置
            DeviceMaintenance deviceMaintenance = createInspectionTask(timingTask);
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
@@ -48,7 +48,13 @@
           // 获取现有触发器并转换为 CronTrigger
           Trigger oldTrigger = scheduler.getTrigger(triggerKey);
           if (!(oldTrigger instanceof CronTrigger)) {
               throw new SchedulerException("Existing trigger is not a CronTrigger");
               // 触发器不存在(首次部署后从未注册,或调度记录被清理),改为重新创建
               JobKey jobKey = new JobKey("MaintenanceTask_" + task.getId());
               if (scheduler.checkExists(jobKey)) {
                   scheduler.deleteJob(jobKey);
               }
               scheduleMaintenanceTask(task);
               return;
           }
           // 3. 构建CronTrigger,确保持久化配置
@@ -57,7 +63,8 @@
                   .withDescription(task.getTaskName() + "_TRIGGER") // 触发器描述
                   .forJob(oldTrigger.getJobKey())                       // 关联对应的Job
                   .withSchedule(CronScheduleBuilder
                           .cronSchedule(convertToCronExpression(task)) // 错过执行时的策略(根据业务调整)
                           .cronSchedule(convertToCronExpression(task))
                           .withMisfireHandlingInstructionDoNothing()
                   )
                   // 4. 设置开始时间(若为null则立即生效)
                   .startAt(task.getNextExecutionTime() != null
@@ -72,6 +79,22 @@
    }
    /**
     * 确保任务已在调度器中,未注册时补注册(应用启动时恢复使用)
     */
    public void ensureScheduled(MaintenanceTask task){
        try {
            JobKey jobKey = new JobKey("MaintenanceTask_" + task.getId());
            if (scheduler.checkExists(jobKey)) {
                return;
            }
            scheduleMaintenanceTask(task);
            log.info("补注册保养定时任务: id={}, name={}", task.getId(), task.getTaskName());
        } catch (SchedulerException e) {
            log.error("SchedulerException ensureScheduled ERROR", e);
        }
    }
    /**
     * 暂停任务
     */
    public void pauseMaintenanceTask(Long taskId) throws SchedulerException {
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java
@@ -12,6 +12,7 @@
import com.ruoyi.inspectiontask.service.impl.TimingTaskServiceImpl;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -32,6 +33,36 @@
    private final SysUserMapper sysUserMapper;
    private final TimingTaskServiceImpl timingTaskService;
    private final MaintenanceTaskScheduler maintenanceTaskScheduler;
    /**
     * 启动时把已启用但未注册到 Quartz 的保养任务补注册,
     * 避免调度记录丢失后定时任务静默不执行
     */
    @PostConstruct
    public void restoreScheduledTasks() {
        try {
            List<MaintenanceTask> tasks = maintenanceTaskMapper.selectList(new QueryWrapper<MaintenanceTask>()
                    .eq("is_active", 1)
                    .eq("deleted", 0));
            for (MaintenanceTask task : tasks) {
                try {
                    TimingTask timingTask = new TimingTask();
                    timingTask.setFrequencyType(task.getFrequencyType());
                    timingTask.setFrequencyDetail(task.getFrequencyDetail());
                    LocalDateTime nextTime = timingTaskService.calculateFirstExecutionTime(timingTask);
                    if (!Objects.equals(task.getNextExecutionTime(), nextTime)) {
                        task.setNextExecutionTime(nextTime);
                        maintenanceTaskMapper.updateById(task);
                    }
                    maintenanceTaskScheduler.ensureScheduled(task);
                } catch (Exception e) {
                    log.error("恢复保养定时任务失败: id={}, name={}", task.getId(), task.getTaskName(), e);
                }
            }
        } catch (Exception e) {
            log.error("恢复保养定时任务异常", e);
        }
    }
    @Override
    public AjaxResult listPage(Page page, MaintenanceTask maintenanceTask) {
@@ -88,10 +119,23 @@
        if (maintenanceTask1 == null) {
            return AjaxResult.warn("没有此数据");
        }
        // 频率变化时重算下次执行时间,避免页面显示与实际调度不一致
        boolean frequencyChanged = !Objects.equals(maintenanceTask.getFrequencyType(), maintenanceTask1.getFrequencyType())
                || !Objects.equals(maintenanceTask.getFrequencyDetail(), maintenanceTask1.getFrequencyDetail());
        BeanUtils.copyProperties(maintenanceTask, maintenanceTask1);
        if (frequencyChanged) {
            TimingTask task = new TimingTask();
            task.setFrequencyType(maintenanceTask1.getFrequencyType());
            task.setFrequencyDetail(maintenanceTask1.getFrequencyDetail());
            maintenanceTask1.setNextExecutionTime(timingTaskService.calculateFirstExecutionTime(task));
        }
        int update = maintenanceTaskMapper.updateById(maintenanceTask1);
        if (update > 0) {
            maintenanceTaskScheduler.rescheduleMaintenanceTask(maintenanceTask1);
            if (maintenanceTask1.isActive()) {
                maintenanceTaskScheduler.rescheduleMaintenanceTask(maintenanceTask1);
            } else {
                maintenanceTaskScheduler.unscheduleMaintenanceTask(maintenanceTask1.getId());
            }
        }
        return AjaxResult.success("更新成功");
    }
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
@@ -2,6 +2,7 @@
import com.ruoyi.inspectiontask.pojo.TimingTask;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.stereotype.Service;
@@ -13,6 +14,7 @@
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class TimingTaskScheduler {
@@ -36,7 +38,13 @@
        // 获取现有触发器并转换为 CronTrigger
        Trigger oldTrigger = scheduler.getTrigger(triggerKey);
        if (!(oldTrigger instanceof CronTrigger)) {
            throw new SchedulerException("Existing trigger is not a CronTrigger");
            // 触发器不存在(首次部署后从未注册,或调度记录被清理),改为重新创建
            JobKey jobKey = new JobKey("timingTask_" + task.getId());
            if (scheduler.checkExists(jobKey)) {
                scheduler.deleteJob(jobKey);
            }
            scheduleTimingTask(task);
            return;
        }
        // 3. 构建CronTrigger,确保持久化配置
@@ -45,7 +53,8 @@
                .withDescription(task.getTaskName() + "_TRIGGER") // 触发器描述
                .forJob(oldTrigger.getJobKey())                       // 关联对应的Job
                .withSchedule(CronScheduleBuilder
                        .cronSchedule(convertToCronExpression(task)) // 错过执行时的策略(根据业务调整)
                        .cronSchedule(convertToCronExpression(task))
                        .withMisfireHandlingInstructionDoNothing()
                )
                // 4. 设置开始时间(若为null则立即生效)
                .startAt(task.getNextExecutionTime() != null
@@ -66,6 +75,18 @@
    }
    /**
     * 确保任务已在调度器中,未注册时补注册(应用启动时恢复使用)
     */
    public void ensureScheduled(TimingTask task) throws SchedulerException {
        JobKey jobKey = new JobKey("timingTask_" + task.getId());
        if (scheduler.checkExists(jobKey)) {
            return;
        }
        scheduleTimingTask(task);
        log.info("补注册巡检定时任务: id={}, name={}", task.getId(), task.getTaskName());
    }
    /**
     * 暂停任务
     */
    public void pauseTimingTask(Long taskId) throws SchedulerException {
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -12,6 +12,7 @@
import com.ruoyi.inspectiontask.service.TimingTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
@@ -37,6 +38,33 @@
    private final SysUserMapper sysUserMapper;
    private static final int ENABLED = 1;
    private static final int DISABLED = 0;
    /**
     * 启动时把已启用但未注册到 Quartz 的巡检任务补注册,
     * 避免调度记录丢失后定时任务静默不执行
     */
    @PostConstruct
    public void restoreScheduledTasks() {
        try {
            List<TimingTask> tasks = timingTaskMapper.selectList(new LambdaQueryWrapper<TimingTask>()
                    .eq(TimingTask::getIsEnabled, ENABLED)
                    .eq(TimingTask::getDeleted, 0));
            for (TimingTask task : tasks) {
                try {
                    LocalDateTime nextTime = calculateFirstExecutionTime(task);
                    if (!Objects.equals(task.getNextExecutionTime(), nextTime)) {
                        task.setNextExecutionTime(nextTime);
                        timingTaskMapper.updateById(task);
                    }
                    timingTaskScheduler.ensureScheduled(task);
                } catch (Exception e) {
                    log.error("恢复巡检定时任务失败: id={}, name={}", task.getId(), task.getTaskName(), e);
                }
            }
        } catch (Exception e) {
            log.error("恢复巡检定时任务异常", e);
        }
    }
    @Override
@@ -161,8 +189,11 @@
            }
            return result;
        } else {
            // 频率变化时重算下次执行时间,避免页面显示与实际调度不一致
            if (!Objects.equals(oldTimingTask.getFrequencyType(), timingTask.getFrequencyType())
                    || !Objects.equals(oldTimingTask.getFrequencyDetail(), timingTask.getFrequencyDetail())) {
                timingTask.setNextExecutionTime(calculateFirstExecutionTime(timingTask));
            }
            int result = timingTaskMapper.updateById(timingTask);
            if (result > 0) {
                boolean oldEnabled = isEnabled(oldTimingTask == null ? null : oldTimingTask.getIsEnabled(), oldTimingTask != null && oldTimingTask.isActive());
@@ -194,8 +225,8 @@
            // 如果是每月执行,计算下个月的具体日期
            return calculateMonthlyFirstExecution(task.getFrequencyDetail());
        } else if ("QUARTERLY".equals(frequencyType)) {
            // 自定义频率,如每小时、每30分钟等
            return calculateCustomFirstExecution(task.getFrequencyDetail());
            // 季度执行,格式为 月,日,时间
            return calculateQuarterlyFirstExecution(task.getFrequencyDetail());
        } else {
            throw new IllegalArgumentException("不支持的频率类型: " + task.getFrequencyType());
        }
@@ -314,8 +345,40 @@
        return targetDateTime;
    }
    private LocalDateTime calculateCustomFirstExecution(String frequencyDetail) {
        return null;
    /**
     * 计算季度任务的首次执行时间,格式为 月,日,时间(如 01,15,14:00)
     * 执行月份按季度起点对齐,与 convertQuarterlyToCron 生成的 cron 保持一致
     */
    private LocalDateTime calculateQuarterlyFirstExecution(String frequencyDetail) {
        String[] parts = frequencyDetail.split(",");
        if (parts.length != 3) {
            throw new IllegalArgumentException("参数格式错误,应为'01,15,14:00'格式");
        }
        int month = Integer.parseInt(parts[0].trim());
        int dayOfMonth = Integer.parseInt(parts[1].trim());
        LocalTime time = LocalTime.parse(parts[2].trim(), DateTimeFormatter.ofPattern("HH:mm"));
        if (month < 1 || month > 12) {
            throw new IllegalArgumentException("月份必须在1-12之间: " + month);
        }
        if (dayOfMonth < 1 || dayOfMonth > 31) {
            throw new IllegalArgumentException("日期必须在1-31之间: " + dayOfMonth);
        }
        // 按季度起点对齐执行月份(1、4、7、10 月)
        int quarterStartMonth = ((month - 1) / 3) * 3 + 1;
        LocalDateTime now = LocalDateTime.now();
        int currentYear = now.getYear();
        for (int i = 0; i < 8; i++) {
            int totalMonth = quarterStartMonth + i * 3;
            int year = currentYear + (totalMonth - 1) / 12;
            int targetMonth = ((totalMonth - 1) % 12) + 1;
            int adjustedDay = Math.min(dayOfMonth, YearMonth.of(year, targetMonth).lengthOfMonth());
            LocalDateTime candidate = LocalDateTime.of(year, targetMonth, adjustedDay, time.getHour(), time.getMinute());
            if (candidate.isAfter(now)) {
                return candidate;
            }
        }
        throw new IllegalStateException("无法计算季度任务的首次执行时间: " + frequencyDetail);
    }
    @Override