From 1a89a2eca92540f2a6e9ed5c140b00610f2f4bfc Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 13 四月 2026 14:11:50 +0800
Subject: [PATCH] yys 1.设备巡检,保养加入年维度

---
 src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java      |  241 +++++++++++---------------
 src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java |   43 ++++
 src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java         |   33 ++
 src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java            |   19 ++
 src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java   |  184 +++++++------------
 5 files changed, 258 insertions(+), 262 deletions(-)

diff --git a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
index 0cabd36..69ba222 100644
--- a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
+++ b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
@@ -140,6 +140,8 @@
                     return calculateMonthlyNextTime(frequencyDetail, currentTime);
                 case "QUARTERLY":
                     return calculateQuarterlyNextTime(frequencyDetail, currentTime);
+                case "YEARLY":
+                    return calculateYearlyNextTime(frequencyDetail, currentTime);
                 default:
                     throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + frequencyType);
             }
@@ -205,6 +207,23 @@
         );
     }
 
+    private LocalDateTime calculateYearlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int month = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        YearMonth targetYearMonth = YearMonth.of(current.getYear(), month);
+        int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+        LocalDateTime target = LocalDateTime.of(current.getYear(), month, adjustedDay, time.getHour(), time.getMinute());
+        if (!target.isAfter(current)) {
+            targetYearMonth = YearMonth.of(current.getYear() + 1, month);
+            adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+            target = LocalDateTime.of(current.getYear() + 1, month, adjustedDay, time.getHour(), time.getMinute());
+        }
+        return target;
+    }
+
     private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
         Set<DayOfWeek> days = new HashSet<>();
         String[] dayStrs = dayOfWeekStr.split("\\|");
diff --git a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
index 384862b..da24a2e 100644
--- a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
+++ b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
@@ -2,7 +2,17 @@
 
 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;
 
@@ -13,10 +23,6 @@
 import java.util.Date;
 import java.util.stream.Collectors;
 
-/**
- * @author :yys
- * @date : 2025/12/22 15:16
- */
 @Service
 @Slf4j
 public class MaintenanceTaskScheduler {
@@ -24,133 +30,95 @@
     @Autowired
     private Scheduler scheduler;
 
-    /**
-     * 娣诲姞鏂颁换鍔″埌璋冨害鍣�
-     */
-    public void scheduleMaintenanceTask(MaintenanceTask task){
+    public void scheduleMaintenanceTask(MaintenanceTask task) {
         try {
             JobDetail jobDetail = buildJobDetail(task);
             Trigger trigger = buildJobTrigger(task, jobDetail);
             scheduler.scheduleJob(jobDetail, trigger);
-        }catch (SchedulerException e){
-            log.error("SchedulerException scheduleMaintenanceTask ERROR",e);
+        } catch (SchedulerException e) {
+            log.error("SchedulerException scheduleMaintenanceTask ERROR", e);
             throw new RuntimeException(e);
         }
     }
 
-    /**
-     * 鏇存柊宸叉湁浠诲姟
-     */
-    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())                       // 鍏宠仈瀵瑰簲鐨凧ob
-                   .withSchedule(CronScheduleBuilder
-                           .cronSchedule(convertToCronExpression(task)) // 閿欒繃鎵ц鏃剁殑绛栫暐锛堟牴鎹笟鍔¤皟鏁达級
-                   )
-                   // 4. 璁剧疆寮�濮嬫椂闂达紙鑻ヤ负null鍒欑珛鍗崇敓鏁堬級
-                   .startAt(task.getNextExecutionTime() != null
-                           ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
-                           : new Date())
-                   .build();
-           scheduler.rescheduleJob(triggerKey, newTrigger);
-       }catch (SchedulerException e){
-           log.error("SchedulerException rescheduleMaintenanceTask ERROR",e);
-           throw new RuntimeException(e);
-       }
-    }
-
-    /**
-     * 鏆傚仠浠诲姟
-     */
-    public void pauseMaintenanceTask(Long taskId) throws SchedulerException {
-        JobKey jobKey = new JobKey("MaintenanceTask_" + taskId);
-        scheduler.pauseJob(jobKey);
-    }
-
-    /**
-     * 鎭㈠浠诲姟
-     */
-    public void resumeMaintenanceTask(Long taskId) throws SchedulerException {
-        JobKey jobKey = new JobKey("MaintenanceTask_" + taskId);
-        scheduler.resumeJob(jobKey);
-    }
-
-    /**
-     * 鍒犻櫎浠诲姟
-     */
-    public void unscheduleMaintenanceTask(Long taskId){
+    public void rescheduleMaintenanceTask(MaintenanceTask task) {
         try {
-            JobKey jobKey = new JobKey("MaintenanceTask_" + taskId);
-            scheduler.deleteJob(jobKey);
-        }catch (SchedulerException e){
-            log.error("SchedulerException unscheduleMaintenanceTask ERROR",e);
+            TriggerKey triggerKey = new TriggerKey("triggerMaintenanceTask_" + task.getId());
+            Trigger oldTrigger = scheduler.getTrigger(triggerKey);
+            if (!(oldTrigger instanceof CronTrigger)) {
+                throw new SchedulerException("Existing trigger is not a CronTrigger");
+            }
+
+            CronTrigger newTrigger = TriggerBuilder.newTrigger()
+                    .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())
+                    .build();
+            scheduler.rescheduleJob(triggerKey, newTrigger);
+        } catch (SchedulerException e) {
+            log.error("SchedulerException rescheduleMaintenanceTask ERROR", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void pauseMaintenanceTask(Long taskId) throws SchedulerException {
+        scheduler.pauseJob(new JobKey("MaintenanceTask_" + taskId));
+    }
+
+    public void resumeMaintenanceTask(Long taskId) throws SchedulerException {
+        scheduler.resumeJob(new JobKey("MaintenanceTask_" + taskId));
+    }
+
+    public void unscheduleMaintenanceTask(Long taskId) {
+        try {
+            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锛堝熀浜庝换鍔D锛岀‘淇濋噸鍚悗鑳借瘑鍒級
         JobKey jobKey = new JobKey("MaintenanceTask_" + task.getId());
-
-        // 2. 灏佽浠诲姟鏁版嵁锛堜粎浣跨敤鍩烘湰绫诲瀷锛岀‘淇濆彲搴忓垪鍖栵級
         JobDataMap jobDataMap = new JobDataMap();
-        jobDataMap.put("maintenanceTaskId", task.getId());           // 浠诲姟ID锛圠ong锛屽彲搴忓垪鍖栵級
-        jobDataMap.put("taskName", task.getTaskName());   // 浠诲姟鍚嶇О锛圫tring锛屽彲搴忓垪鍖栵級
-        jobDataMap.put("taskType", task.getFrequencyType()); // 浠诲姟绫诲瀷锛圫tring锛�
-        // 鎸夐渶娣诲姞鍏朵粬蹇呰鐨勫熀鏈被鍨嬪弬鏁�
+        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锛堝熀浜庝换鍔D锛�
         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)                       // 鍏宠仈瀵瑰簲鐨凧ob
-                .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());
@@ -160,18 +128,18 @@
                 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]);
@@ -179,7 +147,6 @@
         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]);
@@ -187,29 +154,29 @@
         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]);  // 瑙f瀽鏃堕棿
+        int month = validateMonth(parts[0]);
+        int day = validateDayOfMonth(parts[1]);
+        LocalTime time = parseTime(parts[2]);
 
-        // 璁$畻瀛e害璧峰鏈堜唤(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);
             if (month < 1 || month > 12) {
-                throw new IllegalArgumentException("鏈堜唤蹇呴』鍦�1-12涔嬮棿");
+                throw new IllegalArgumentException("鏈堜唤蹇呴』鍦� 1-12 涔嬮棿");
             }
             return month;
         } catch (NumberFormatException e) {
@@ -217,62 +184,54 @@
         }
     }
 
-    // 杈呭姪鏂规硶锛氳В鏋愭椂闂�
     private LocalTime parseTime(String timeStr) {
         try {
             return LocalTime.parse(timeStr);
         } catch (DateTimeParseException e) {
-            throw new IllegalArgumentException("鏃堕棿鏍煎紡蹇呴』涓篐H:mm", 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));
+            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涔嬮棿");
+            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("瀛e害鏈堜唤蹇呴』鏄�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);
         }
     }
-    
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
index b32f82b..5d13ffa 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
@@ -66,12 +66,12 @@
             );
             TimingTask timingTask = tasks.isEmpty() ? null : tasks.get(0);
             if (timingTask == null) {
-                throw new JobExecutionException("鎵句笉鍒板畾鏃朵换鍔�: " + taskId);
+                throw new JobExecutionException("閹靛彞绗夐崚鏉跨暰閺冩湹鎹㈤崝? " + taskId);
             }
 
             List<Long> deviceIds = resolveTaskIds(timingTask);
             if (deviceIds.isEmpty()) {
-                throw new JobExecutionException("瀹氭椂浠诲姟鏈厤缃澶�: " + taskId);
+                throw new JobExecutionException("鐎规碍妞傛禒璇插閺堫亪鍘ょ純顔款啎婢�? " + taskId);
             }
 
             for (Long deviceId : deviceIds) {
@@ -130,7 +130,7 @@
         inspectionTask.setAreaId(deviceLedger.getAreaId());
         inspectionTask.setInspectorId(timingTask.getInspectorIds());
         inspectionTask.setInspectionLocation(timingTask.getInspectionLocation());
-        inspectionTask.setRemarks("鑷姩鐢熸垚鑷畾鏃朵换鍔D: " + timingTask.getId());
+        inspectionTask.setRemarks("閼奉亜濮╅悽鐔稿灇閼奉亜鐣鹃弮鏈垫崲閸旑搹D: " + timingTask.getId());
         inspectionTask.setRegistrantId(timingTask.getRegistrantId());
         inspectionTask.setRegistrant(timingTask.getRegistrant());
         inspectionTask.setFrequencyType(timingTask.getFrequencyType());
@@ -152,11 +152,13 @@
                     return calculateMonthlyNextTime(frequencyDetail, currentTime);
                 case "QUARTERLY":
                     return calculateQuarterlyNextTime(frequencyDetail, currentTime);
+                case "YEARLY":
+                    return calculateYearlyNextTime(frequencyDetail, currentTime);
                 default:
-                    throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + frequencyType);
+                    throw new IllegalArgumentException("娑撳秵鏁幐浣烘畱妫版垹宸肩猾璇茬��: " + frequencyType);
             }
         } catch (Exception e) {
-            throw new RuntimeException("璁$畻涓嬫鎵ц鏃堕棿澶辫触: " + e.getMessage(), e);
+            throw new RuntimeException("鐠侊紕鐣绘稉瀣偧閹笛嗩攽閺冨爼妫挎径杈Е: " + e.getMessage(), e);
         }
     }
 
@@ -179,7 +181,7 @@
                 return LocalDateTime.of(nextTime.toLocalDate(), time);
             }
             if (nextTime.isAfter(current.plusYears(1))) {
-                throw new RuntimeException("鏃犳硶鎵惧埌涓嬫鎵ц鏃堕棿");
+                throw new RuntimeException("閺冪姵纭堕幍鎯у煂娑撳顐奸幍褑顢戦弮鍫曟?");
             }
         }
     }
@@ -217,6 +219,23 @@
         );
     }
 
+    private LocalDateTime calculateYearlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int month = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        YearMonth targetYearMonth = YearMonth.of(current.getYear(), month);
+        int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+        LocalDateTime target = LocalDateTime.of(current.getYear(), month, adjustedDay, time.getHour(), time.getMinute());
+        if (!target.isAfter(current)) {
+            targetYearMonth = YearMonth.of(current.getYear() + 1, month);
+            adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+            target = LocalDateTime.of(current.getYear() + 1, month, adjustedDay, time.getHour(), time.getMinute());
+        }
+        return target;
+    }
+
     private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
         Set<DayOfWeek> days = new HashSet<>();
         String[] dayStrs = dayOfWeekStr.split("\\|");
@@ -245,7 +264,7 @@
                     days.add(DayOfWeek.SUNDAY);
                     break;
                 default:
-                    throw new IllegalArgumentException("鏃犳晥鐨勬槦鏈�: " + dayStr);
+                    throw new IllegalArgumentException("閺冪姵鏅ラ惃鍕Е閺�? " + dayStr);
             }
         }
 
diff --git a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
index 125f311..5671156 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
@@ -1,7 +1,17 @@
 package com.ruoyi.inspectiontask.service.impl;
 
 import com.ruoyi.inspectiontask.pojo.TimingTask;
-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;
 
@@ -18,132 +28,86 @@
     @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());
-
-        // 鑾峰彇鐜版湁瑙﹀彂鍣ㄥ苟杞崲涓� 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())                       // 鍏宠仈瀵瑰簲鐨凧ob
-                .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())
                 .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);
+        scheduler.pauseJob(new JobKey("timingTask_" + taskId));
     }
 
-    /**
-     * 鎭㈠浠诲姟
-     */
     public void resumeTimingTask(Long taskId) throws SchedulerException {
-        JobKey jobKey = new JobKey("timingTask_" + taskId);
-        scheduler.resumeJob(jobKey);
+        scheduler.resumeJob(new JobKey("timingTask_" + taskId));
     }
 
-    /**
-     * 鍒犻櫎浠诲姟
-     */
-    public void unscheduleTimingTask(Long taskId){
+    public void unscheduleTimingTask(Long taskId) {
         try {
-            JobKey jobKey = new JobKey("timingTask_" + taskId);
-            scheduler.deleteJob(jobKey);
-        }catch (SchedulerException e){
+            scheduler.deleteJob(new JobKey("timingTask_" + taskId));
+        } catch (SchedulerException e) {
             throw new RuntimeException(e);
         }
     }
 
     private JobDetail buildJobDetail(TimingTask task) {
-        // 1. 鏋勫缓鍞竴JobKey锛堝熀浜庝换鍔D锛岀‘淇濋噸鍚悗鑳借瘑鍒級
         JobKey jobKey = new JobKey("timingTask_" + task.getId());
-
-        // 2. 灏佽浠诲姟鏁版嵁锛堜粎浣跨敤鍩烘湰绫诲瀷锛岀‘淇濆彲搴忓垪鍖栵級
         JobDataMap jobDataMap = new JobDataMap();
-        jobDataMap.put("taskId", task.getId());           // 浠诲姟ID锛圠ong锛屽彲搴忓垪鍖栵級
-        jobDataMap.put("taskName", task.getTaskName());   // 浠诲姟鍚嶇О锛圫tring锛屽彲搴忓垪鍖栵級
-        jobDataMap.put("taskType", task.getFrequencyType()); // 浠诲姟绫诲瀷锛圫tring锛�
-        // 鎸夐渶娣诲姞鍏朵粬蹇呰鐨勫熀鏈被鍨嬪弬鏁�
+        jobDataMap.put("taskId", task.getId());
+        jobDataMap.put("taskName", task.getTaskName());
+        jobDataMap.put("taskType", task.getFrequencyType());
 
-        // 3. 鏋勫缓JobDetail锛岃缃寔涔呭寲鐩稿叧灞炴��
         return JobBuilder.newJob(TimingTaskJob.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(TimingTask task, JobDetail jobDetail) {
-        // 1. 鏋勫缓鍞竴TriggerKey锛堝熀浜庝换鍔D锛�
         TriggerKey triggerKey = new TriggerKey("trigger_" + task.getId());
-
-        // 2. 鐢熸垚Cron琛ㄨ揪寮忥紙鍘熼�昏緫涓嶅彉锛�
         String cronExpression = convertToCronExpression(task);
 
-        // 3. 鏋勫缓CronTrigger锛岀‘淇濇寔涔呭寲閰嶇疆
         return TriggerBuilder.newTrigger()
-                .withIdentity(triggerKey)                // 鍞竴鏍囪瘑锛岀敤浜庢寔涔呭寲瀛樺偍
-                .withDescription(task.getTaskName() + "_TRIGGER") // 瑙﹀彂鍣ㄦ弿杩�
-                .forJob(jobDetail)                       // 鍏宠仈瀵瑰簲鐨凧ob
-                .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(TimingTask 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());
@@ -153,18 +117,18 @@
                 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]);
@@ -172,7 +136,6 @@
         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]);
@@ -180,29 +143,29 @@
         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]);  // 瑙f瀽鏃堕棿
+        int month = validateMonth(parts[0]);
+        int day = validateDayOfMonth(parts[1]);
+        LocalTime time = parseTime(parts[2]);
 
-        // 璁$畻瀛e害璧峰鏈堜唤(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);
             if (month < 1 || month > 12) {
-                throw new IllegalArgumentException("鏈堜唤蹇呴』鍦�1-12涔嬮棿");
+                throw new IllegalArgumentException("鏈堜唤蹇呴』鍦� 1-12 涔嬮棿");
             }
             return month;
         } catch (NumberFormatException e) {
@@ -210,61 +173,54 @@
         }
     }
 
-    // 杈呭姪鏂规硶锛氳В鏋愭椂闂�
     private LocalTime parseTime(String timeStr) {
         try {
             return LocalTime.parse(timeStr);
         } catch (DateTimeParseException e) {
-            throw new IllegalArgumentException("鏃堕棿鏍煎紡蹇呴』涓篐H:mm", 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));
+            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涔嬮棿");
+            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("瀛e害鏈堜唤蹇呴』鏄�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);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
index f23a6a4..d551399 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -225,6 +225,8 @@
             return calculateWeeklyFirstExecution(task.getFrequencyDetail());
         } else if ("MONTHLY".equals(frequencyType)) {
             return calculateMonthlyFirstExecution(task.getFrequencyDetail());
+        } else if ("YEARLY".equals(frequencyType)) {
+            return calculateYearlyFirstExecution(task.getFrequencyDetail());
         } else if ("QUARTERLY".equals(frequencyType)) {
             return calculateCustomFirstExecution(task.getFrequencyDetail());
         } else {
@@ -315,6 +317,28 @@
         return targetDateTime;
     }
 
+    private LocalDateTime calculateYearlyFirstExecution(String frequencyDetail) {
+        String[] parts = frequencyDetail.split(",");
+        if (parts.length != 3) {
+            throw new IllegalArgumentException("鍙傛暟鏍煎紡閿欒锛屽簲涓� 03,15,17:00");
+        }
+
+        int month = Integer.parseInt(parts[0].trim());
+        int dayOfMonth = Integer.parseInt(parts[1].trim());
+        LocalTime targetTime = LocalTime.parse(parts[2].trim(), DateTimeFormatter.ofPattern("HH:mm"));
+
+        LocalDateTime now = LocalDateTime.now();
+        YearMonth currentYearMonth = YearMonth.of(now.getYear(), month);
+        int adjustedDay = Math.min(dayOfMonth, currentYearMonth.lengthOfMonth());
+        LocalDateTime targetDateTime = LocalDateTime.of(now.getYear(), month, adjustedDay, targetTime.getHour(), targetTime.getMinute());
+        if (!targetDateTime.isAfter(now)) {
+            YearMonth nextYearMonth = YearMonth.of(now.getYear() + 1, month);
+            adjustedDay = Math.min(dayOfMonth, nextYearMonth.lengthOfMonth());
+            targetDateTime = LocalDateTime.of(now.getYear() + 1, month, adjustedDay, targetTime.getHour(), targetTime.getMinute());
+        }
+        return targetDateTime;
+    }
+
     private LocalDateTime calculateCustomFirstExecution(String frequencyDetail) {
         return null;
     }
@@ -350,6 +374,8 @@
                     return calculateMonthlyNextTime(frequencyDetail, currentTime);
                 case "QUARTERLY":
                     return calculateQuarterlyNextTime(frequencyDetail, currentTime);
+                case "YEARLY":
+                    return calculateYearlyNextTime(frequencyDetail, currentTime);
                 default:
                     throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + frequencyType);
             }
@@ -415,6 +441,23 @@
         );
     }
 
+    private LocalDateTime calculateYearlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int month = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        YearMonth targetYearMonth = YearMonth.of(current.getYear(), month);
+        int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+        LocalDateTime target = LocalDateTime.of(current.getYear(), month, adjustedDay, time.getHour(), time.getMinute());
+        if (!target.isAfter(current)) {
+            targetYearMonth = YearMonth.of(current.getYear() + 1, month);
+            adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+            target = LocalDateTime.of(current.getYear() + 1, month, adjustedDay, time.getHour(), time.getMinute());
+        }
+        return target;
+    }
+
     private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
         Set<DayOfWeek> days = new HashSet<>();
         String[] dayStrs = dayOfWeekStr.split("\\|");

--
Gitblit v1.9.3