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