From 2ff409739ff1d4d14acb6784619679117df4da33 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期二, 26 五月 2026 09:59:19 +0800
Subject: [PATCH] 111

---
 .gitignore                                                                    |    1 
 src/main/java/com/ruoyi/stock/pojo/StockInRecord.java                         |    6 
 src/main/resources/mapper/basic/CustomerMapper.xml                            |   46 -
 src/main/resources/mapper/stock/StockOutRecordMapper.xml                      |    2 
 src/main/resources/mapper/device/DeviceMaintenanceMapper.xml                  |   36 
 src/main/resources/mapper/basic/SupplierManageMapper.xml                      |  124 +--
 src/main/resources/mapper/stock/StockInRecordMapper.xml                       |    4 
 src/test/java/com/ruoyi/device/service/impl/MaintenanceTaskJobTest.java       |  448 +++++++++++++++++
 src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java      |   49 +
 src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java  |   30 
 src/main/resources/mapper/approve/ApproveProcessMapper.xml                    |    5 
 src/test/java/com/ruoyi/approve/service/impl/ApproveProcessIdModifyTest.java  |  117 ++++
 doc/20260523_新增入库记录表的预警数量.sql                                                 |    2 
 src/main/resources/mapper/production/ProductionOperationTaskMapper.xml        |    6 
 src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java               |   11 
 src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java           |    8 
 src/test/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJobTest.java    |  339 +++++++++++++
 src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java                  |   11 
 src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java |  254 +++++++++
 src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java        |    8 
 20 files changed, 1,338 insertions(+), 169 deletions(-)

diff --git a/.gitignore b/.gitignore
index d3f800c..797d1c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 !gradle/wrapper/gradle-wrapper.jar
 claude.md
 target/
+test/
 !.mvn/wrapper/maven-wrapper.jar
 
 ######################################################################
diff --git "a/doc/20260523_\346\226\260\345\242\236\345\205\245\345\272\223\350\256\260\345\275\225\350\241\250\347\232\204\351\242\204\350\255\246\346\225\260\351\207\217.sql" "b/doc/20260523_\346\226\260\345\242\236\345\205\245\345\272\223\350\256\260\345\275\225\350\241\250\347\232\204\351\242\204\350\255\246\346\225\260\351\207\217.sql"
new file mode 100644
index 0000000..90392d2
--- /dev/null
+++ "b/doc/20260523_\346\226\260\345\242\236\345\205\245\345\272\223\350\256\260\345\275\225\350\241\250\347\232\204\351\242\204\350\255\246\346\225\260\351\207\217.sql"
@@ -0,0 +1,2 @@
+alter table stock_in_record
+    add warn_num decimal(16, 4) null comment '棰勮鏁伴噺';
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 a6179f6..63289a7 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
@@ -35,6 +35,17 @@
 
         // 鑾峰彇鐜版湁瑙﹀彂鍣ㄥ苟杞崲涓� CronTrigger
         Trigger oldTrigger = scheduler.getTrigger(triggerKey);
+        if (oldTrigger == null) {
+            JobKey jobKey = new JobKey("timingTask_" + task.getId());
+            JobDetail jobDetail = scheduler.getJobDetail(jobKey);
+            if (jobDetail != null) {
+                Trigger trigger = buildJobTrigger(task, jobDetail);
+                scheduler.scheduleJob(trigger);
+            } else {
+                scheduleTimingTask(task);
+            }
+            return;
+        }
         if (!(oldTrigger instanceof CronTrigger)) {
             throw new SchedulerException("Existing trigger is not a CronTrigger");
         }
@@ -144,18 +155,13 @@
 
         // 浣跨敤switch纭繚鏉′欢浜掓枼
         String frequencyType = task.getFrequencyType().toUpperCase(); // 缁熶竴杞负澶у啓姣旇緝
-        switch (frequencyType) {
-            case "DAILY":
-                return convertDailyToCron(task.getFrequencyDetail());
-            case "WEEKLY":
-                return convertWeeklyToCron(task.getFrequencyDetail());
-            case "MONTHLY":
-                return convertMonthlyToCron(task.getFrequencyDetail());
-            case "QUARTERLY":
-                return convertQuarterlyToCron(task.getFrequencyDetail());
-            default:
-                throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + task.getFrequencyType());
-        }
+        return switch (frequencyType) {
+            case "DAILY" -> convertDailyToCron(task.getFrequencyDetail());
+            case "WEEKLY" -> convertWeeklyToCron(task.getFrequencyDetail());
+            case "MONTHLY" -> convertMonthlyToCron(task.getFrequencyDetail());
+            case "QUARTERLY" -> convertQuarterlyToCron(task.getFrequencyDetail());
+            default -> throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + task.getFrequencyType());
+        };
     }
 
     // 姣忔棩浠诲姟杞崲
diff --git a/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java b/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java
index 3533e6e..845825e 100644
--- a/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java
+++ b/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java
@@ -24,10 +24,10 @@
     @Schema(description = "鍚堝悓閲戦")
     private BigDecimal contractAmount;
 
-    @Schema(description = "浠樻閲戦")
-    private BigDecimal paymentAmount;
+    @Schema(description = "宸插叆搴撻噾棰�")
+    private BigDecimal shippedAmount;
 
-    @Schema(description = "搴斾粯閲戦")
-    private BigDecimal payableAmount;
+    @Schema(description = "鏈叆搴撻噾棰�")
+    private BigDecimal unshippedAmount;
 
 }
diff --git a/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java b/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java
index 35cefc0..df811bb 100644
--- a/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java
+++ b/src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java
@@ -16,15 +16,12 @@
     private String supplierName;
 
     @Schema(description = "鍚堝悓鎬婚噾棰�")
-    //璇ヤ緵搴斿晢閲囪喘鍚堝悓绱閲戦
     private BigDecimal contractAmounts;
 
-    @Schema(description = "浠樻閲戦")
-    //璇ヤ緵搴斿晢閲囪喘浠樻绱閲戦
-    private BigDecimal paymentAmount;
+    @Schema(description = "宸插叆搴撻噾棰�")
+    private BigDecimal shippedAmount;
 
-    @Schema(description = "搴斾粯閲戦")
-    //璇ヤ緵搴斿晢閲囪喘搴斾粯绱閲戦=璐㈠姟(鍏ュ簱-閫�璐�)
-    private BigDecimal payableAmount;
+    @Schema(description = "鏈叆搴撻噾棰�")
+    private BigDecimal unshippedAmount;
 
 }
diff --git a/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java b/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java
index 992860e..96ed799 100644
--- a/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java
+++ b/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java
@@ -24,10 +24,10 @@
     @Schema(description = "鍚堝悓閲戦")
     private BigDecimal contractAmount;
 
-    @Schema(description = "鏀舵閲戦")
-    private BigDecimal receiptPaymentAmount;
+    @Schema(description = "宸插嚭搴撻噾棰�")
+    private BigDecimal shippedAmount;
 
-    @Schema(description = "搴旀敹閲戦")
-    private BigDecimal receiptableAmount;
+    @Schema(description = "鏈嚭搴撻噾棰�")
+    private BigDecimal unshippedAmount;
 
 }
diff --git a/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java b/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java
index 7b8fc1a..a88b6ea 100644
--- a/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java
+++ b/src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java
@@ -16,15 +16,12 @@
     private String customerName;
 
     @Schema(description = "鍚堝悓鎬婚噾棰�")
-    //璇ュ鎴烽攢鍞悎鍚岀疮璁¢噾棰�
     private BigDecimal contractAmounts;
 
-    @Schema(description = "鏀舵閲戦")
-    //璇ュ鎴烽攢鍞敹娆剧疮璁¢噾棰�
-    private BigDecimal receiptPaymentAmount;
+    @Schema(description = "宸插嚭搴撻噾棰�")
+    private BigDecimal shippedAmount;
 
-    @Schema(description = "搴旀敹閲戦")
-    //璇ュ鎴烽攢鍞簲鏀剁疮璁¢噾棰�=璐㈠姟(鍑哄簱-閫�璐�)
-    private BigDecimal receiptableAmount;
+    @Schema(description = "鏈嚭搴撻噾棰�")
+    private BigDecimal unshippedAmount;
 
 }
diff --git a/src/main/java/com/ruoyi/stock/pojo/StockInRecord.java b/src/main/java/com/ruoyi/stock/pojo/StockInRecord.java
index 2c1fecb..f9525ee 100644
--- a/src/main/java/com/ruoyi/stock/pojo/StockInRecord.java
+++ b/src/main/java/com/ruoyi/stock/pojo/StockInRecord.java
@@ -7,14 +7,17 @@
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
+import java.io.Serial;
+import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 @Data
 @TableName("stock_in_record")
 @Schema(name = "鍏ュ簱绠$悊")
-public class StockInRecord {
+public class StockInRecord implements Serializable {
 
+    @Serial
     private static final long serialVersionUID = 1L;
     /**
      * 搴忓彿
@@ -44,6 +47,7 @@
     private String remark;
 
     @Schema(description = "棰勮鏁伴噺")
+    @TableField(exist = false)
     private BigDecimal warnNum;
 
     @Schema(description = "绫诲瀷  0鍚堟牸鍏ュ簱 1涓嶅悎鏍煎叆搴�")
diff --git a/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java b/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
index a84374c..5207cc9 100644
--- a/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
+++ b/src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -69,9 +69,56 @@
             throw new BaseException("璇ュ叆搴撹褰曚笉瀛樺湪,鏃犳硶鏇存柊!!!");
         }
 
+        // 璁板綍淇敼鍓嶇殑 batch_no
+        String oldBatchNo = stockInRecord.getBatchNo();
+        String newBatchNo = stockInRecordDto.getBatchNo();
+
         String[] ignoreProperties = {"id", "inbound_batches"};//鎺掗櫎id灞炴��
         BeanUtils.copyProperties(stockInRecordDto, stockInRecord, ignoreProperties);
-        return stockInRecordMapper.updateById(stockInRecord);
+        int result = stockInRecordMapper.updateById(stockInRecord);
+
+        // 濡傛灉 batch_no 鍙戠敓鍙樺寲锛岄渶瑕佸悓姝ユ洿鏂板叧鑱旇〃
+        if (newBatchNo != null && !newBatchNo.equals(oldBatchNo)) {
+            updateRelatedBatchNo(stockInRecord, oldBatchNo, newBatchNo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 鍚屾鏇存柊鍏宠仈琛ㄧ殑 batch_no
+     * @param stockInRecord 鍏ュ簱璁板綍
+     * @param oldBatchNo 淇敼鍓嶇殑鎵瑰彿
+     * @param newBatchNo 淇敼鍚庣殑鎵瑰彿
+     */
+    private void updateRelatedBatchNo(StockInRecord stockInRecord, String oldBatchNo, String newBatchNo) {
+        // 1. 鏇存柊 stock_inventory 琛紙鍚堟牸搴撳瓨锛�
+        LambdaQueryWrapper<StockInventory> inventoryEq = new LambdaQueryWrapper<StockInventory>()
+                .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId());
+        if (StringUtils.isEmpty(oldBatchNo)) {
+            inventoryEq.isNull(StockInventory::getBatchNo);
+        } else {
+            inventoryEq.eq(StockInventory::getBatchNo, oldBatchNo);
+        }
+        StockInventory stockInventory = stockInventoryMapper.selectOne(inventoryEq);
+        if (stockInventory != null) {
+            stockInventory.setBatchNo(newBatchNo);
+            stockInventoryMapper.updateById(stockInventory);
+        }
+
+        // 2. 鏇存柊 stock_uninventory 琛紙涓嶅悎鏍煎簱瀛橈級
+        LambdaQueryWrapper<StockUninventory> uninventoryEq = new LambdaQueryWrapper<StockUninventory>()
+                .eq(StockUninventory::getProductModelId, stockInRecord.getProductModelId());
+        if (StringUtils.isEmpty(oldBatchNo)) {
+            uninventoryEq.isNull(StockUninventory::getBatchNo);
+        } else {
+            uninventoryEq.eq(StockUninventory::getBatchNo, oldBatchNo);
+        }
+        StockUninventory stockUninventory = stockUninventoryMapper.selectOne(uninventoryEq);
+        if (stockUninventory != null) {
+            stockUninventory.setBatchNo(newBatchNo);
+            stockUninventoryMapper.updateById(stockUninventory);
+        }
     }
 
     @Override
diff --git a/src/main/resources/mapper/approve/ApproveProcessMapper.xml b/src/main/resources/mapper/approve/ApproveProcessMapper.xml
index dd651c0..70d132b 100644
--- a/src/main/resources/mapper/approve/ApproveProcessMapper.xml
+++ b/src/main/resources/mapper/approve/ApproveProcessMapper.xml
@@ -29,16 +29,19 @@
         approve_user_names,approve_reason,approve_time,approve_over_time,approve_status,
         approve_delete,tenant_id,approve_type,approve_remark,start_date_time,end_date_time
     </sql>
+
     <select id="listPage" resultType="com.ruoyi.approve.vo.ApproveProcessVo">
         select * from approve_process where approve_delete = 0
         <if test="req.approveId != null and req.approveId != ''">
             and approve_id like concat('%',#{req.approveId},'%')
         </if>
-        <if test="req.approveStatus != null or req.approveStatus == 0">
+        <if test="req.approveStatus != null">
             and approve_status = #{req.approveStatus}
         </if>
         <if test="req.approveType != null ">
             and approve_type = #{req.approveType}
         </if>
+        order by id desc
     </select>
+
 </mapper>
diff --git a/src/main/resources/mapper/basic/CustomerMapper.xml b/src/main/resources/mapper/basic/CustomerMapper.xml
index 8117c66..99a5a28 100644
--- a/src/main/resources/mapper/basic/CustomerMapper.xml
+++ b/src/main/resources/mapper/basic/CustomerMapper.xml
@@ -9,9 +9,9 @@
     <select id="listPage" resultType="com.ruoyi.basic.vo.CustomerVo">
         select
         c.*,
-        u.user_name usage_user_name,
+        u.nick_name as usage_user_name,
         (
-        select group_concat(u2.user_name separator ', ')
+        select group_concat(u2.nick_name separator ', ')
         from customer_user cu
         left join sys_user u2 on cu.user_id = u2.user_id
         where cu.customer_id = c.id
@@ -107,14 +107,14 @@
             </if>
         </where>
     </select>
+
     <select id="customewTransactions" resultType="com.ruoyi.sales.vo.CustomerTransactionsVo">
         select T1.customer_id,
                c.customer_name,
                T1.contractAmounts,
-               IFNULL(T2.receiptPaymentAmount, 0) AS receiptPaymentAmount,
-               IFNULL(T3.outboundAmount, 0) - IFNULL(T4.returnAmount, 0) AS receiptableAmount
+               IFNULL(T3.outboundAmount, 0) AS shippedAmount,
+               GREATEST(T1.contractAmounts - IFNULL(T3.outboundAmount, 0), 0) AS unshippedAmount
         from (select customer_id, sum(contract_amount) as contractAmounts from sales_ledger group by customer_id) T1
-        left join (select customer_id, sum(collection_amount) as receiptPaymentAmount from account_sales_collection group by customer_id) T2 on T1.customer_id = T2.customer_id
         left join (
             SELECT
                 sl.customer_id,
@@ -128,16 +128,6 @@
                 and slp.type = 1
             group by sl.customer_id
         ) T3 on T3.customer_id=T1.customer_id
-        left join (
-            select
-                sl.customer_id,
-                sum(rm.refund_amount) as returnAmount
-            from return_management rm
-            left join shipping_info si on rm.shipping_id = si.id
-            left join sales_ledger sl on si.sales_ledger_id = sl.id
-            where rm.status=1
-            group by sl.customer_id
-        ) T4 on T4.customer_id=T1.customer_id
         left join customer c on T1.customer_id = c.id
         <where>
             <if test="customerName!=null and customerName!=''">
@@ -145,27 +135,16 @@
             </if>
         </where>
     </select>
+
     <select id="customewTransactionsDetails"
             resultType="com.ruoyi.sales.vo.CustomerTransactionsDetailsVo">
         select sl.id salesLedgerId,
                sl.sales_contract_no,
                sl.execution_date,
                sl.contract_amount,
-               IFNULL(T1.receiptPaymentAmount, 0) AS receiptPaymentAmount,
-               IFNULL(T2.outboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS receiptableAmount
+               IFNULL(T2.outboundAmount, 0) AS shippedAmount,
+               GREATEST(sl.contract_amount - IFNULL(T2.outboundAmount, 0), 0) AS unshippedAmount
         from sales_ledger sl
-        left join (
-            select
-                sl.id,
-                sum(ascc.collection_amount) as receiptPaymentAmount
-            from account_sales_collection ascc
-            left join stock_out_record sor on FIND_IN_SET(sor.id, ascc.stock_out_record_ids) > 0
-            left join shipping_info s on sor.record_id = s.id
-            LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
-            WHERE sor.record_type='13'
-              and sor.approval_status=1
-            group by  sl.id
-        )T1 on T1.id = sl.id
         left join (
             SELECT
                 sl.id,
@@ -179,15 +158,6 @@
               and slp.type = 1
             group by  sl.id
         )T2 on T2.id = sl.id
-        left join (
-            select sl.id,
-                   sum(rm.refund_amount) as returnAmount
-            from return_management rm
-                     left join shipping_info si on rm.shipping_id = si.id
-                     left join sales_ledger sl on si.sales_ledger_id = sl.id
-            where rm.status=1
-            group by sl.id
-        )T3 on T3.id = sl.id
         where sl.customer_id = #{customerId}
     </select>
 </mapper>
diff --git a/src/main/resources/mapper/basic/SupplierManageMapper.xml b/src/main/resources/mapper/basic/SupplierManageMapper.xml
index 5c5af54..6f3ec54 100644
--- a/src/main/resources/mapper/basic/SupplierManageMapper.xml
+++ b/src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -68,95 +68,69 @@
             </if>
         </where>
     </select>
+    
     <select id="supplierTransactions" resultType="com.ruoyi.purchase.vo.SupplierTransactionsVo">
-        select T1.supplier_id,
+        SELECT T1.supplier_id,
                sm.supplier_name,
                T1.contractAmounts,
-               IFNULL(T2.paymentAmount, 0) AS paymentAmount,
-               IFNULL(T3.InboundAmount, 0) - IFNULL(T4.returnAmount, 0) AS payableAmount
-        from (select supplier_id, sum(contract_amount) as contractAmounts from purchase_ledger group by supplier_id) T1
-        left join (select supplier_id, sum(payment_amount) as paymentAmount from account_purchase_payment group by supplier_id) T2 on T1.supplier_id = T2.supplier_id
-        left join (
-            SELECT
-                pl.supplier_id,
-                sum(sir.stock_in_num * slp.tax_inclusive_unit_price) AS InboundAmount
-            FROM stock_in_record sir
-                     -- 10 绫诲瀷鎵嶅叧鑱旇川妫�琛�
-                     LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
-                -- 鍔ㄦ�佸叧鑱旈噰璐紙鑷姩閫傞厤 7 鍜� 10锛�
-                     LEFT JOIN purchase_ledger pl
-                               ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
-                -- 浜у搧鍏宠仈涓嶅姩
-                     LEFT JOIN sales_ledger_product slp ON pl.id = slp.sales_ledger_id
-            -- 鏉′欢
-            WHERE sir.approval_status = 1 AND slp.type = 2
-              AND sir.record_type IN ('7','10')
-            group by pl.supplier_id
-        ) T3 on T3.supplier_id=T1.supplier_id
-        left join (
-            select
-                supplier_id,
-                sum(total_amount) as returnAmount
-            from purchase_return_orders pro
-            group by supplier_id
-        ) T4 on T4.supplier_id=T1.supplier_id
-        left join supplier_manage sm on T1.supplier_id = sm.id
+               IFNULL(T3.InboundAmount, 0) AS shippedAmount,
+               T1.contractAmounts - IFNULL(T3.InboundAmount, 0) AS unshippedAmount
+        FROM (SELECT supplier_id, SUM(contract_amount) AS contractAmounts FROM purchase_ledger GROUP BY supplier_id) T1
+        LEFT JOIN (
+            SELECT t.supplier_id,
+                   SUM(t.inbound_amount) AS InboundAmount
+            FROM (
+                SELECT sir.stock_in_num * slp.tax_inclusive_unit_price AS inbound_amount, pl.supplier_id
+                FROM stock_in_record sir
+                INNER JOIN sales_ledger_product slp ON slp.id = sir.record_id
+                INNER JOIN purchase_ledger pl ON pl.id = slp.sales_ledger_id
+                WHERE sir.approval_status = 1 AND sir.record_type = 7 AND slp.type = 2
+                UNION ALL
+                SELECT sir.stock_in_num * slp.tax_inclusive_unit_price AS inbound_amount, pl.supplier_id
+                FROM stock_in_record sir
+                INNER JOIN quality_inspect qi ON qi.id = sir.record_id
+                INNER JOIN purchase_ledger pl ON pl.id = qi.purchase_ledger_id
+                INNER JOIN sales_ledger_product slp ON slp.sales_ledger_id = pl.id AND slp.product_model_id = sir.product_model_id
+                WHERE sir.approval_status = 1 AND sir.record_type = 10 AND slp.type = 2
+            ) t
+            GROUP BY t.supplier_id
+        ) T3 ON T3.supplier_id = T1.supplier_id
+        LEFT JOIN supplier_manage sm ON T1.supplier_id = sm.id
         <where>
             <if test="supplierName!=null and supplierName!=''">
                 AND sm.supplier_name LIKE CONCAT('%',#{supplierName},'%')
             </if>
         </where>
     </select>
+
     <select id="supplierTransactionsDetails"
             resultType="com.ruoyi.purchase.vo.SupplierTransactionsDetailsVo">
-       select pl.id  purchaseLedgerId,
+       SELECT pl.id purchaseLedgerId,
               pl.purchase_contract_number,
               pl.execution_date,
               pl.contract_amount,
-              IFNULL(T1.paymentAmount, 0) AS paymentAmount,
-              IFNULL(T2.InboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS payableAmount
-       from purchase_ledger pl
-       left join (
-           select
-               pl.id,
-               sum(app.payment_amount) as paymentAmount
-           from account_purchase_payment app
-           left join account_payment_application apa on app.account_payment_application_id = apa.id
-           left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
-               -- 10 绫诲瀷鎵嶅叧鑱旇川妫�琛�
-           LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
-               -- 鍔ㄦ�佸叧鑱旈噰璐紙鑷姩閫傞厤 7 鍜� 10锛�
-           LEFT JOIN purchase_ledger pl
-                     ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
-           WHERE sir.approval_status = 1
-             AND sir.record_type IN ('7','10')
-           group by pl.id
-       )T1 on T1.id = pl.id
-       left join (
-           SELECT
-               pl.id,
-               sum(sir.stock_in_num * slp.tax_inclusive_unit_price) AS InboundAmount
-           FROM stock_in_record sir
-                    -- 10 绫诲瀷鎵嶅叧鑱旇川妫�琛�
-                    LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
-               -- 鍔ㄦ�佸叧鑱旈噰璐紙鑷姩閫傞厤 7 鍜� 10锛�
-                    LEFT JOIN purchase_ledger pl
-                              ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
-               -- 浜у搧鍏宠仈涓嶅姩
-                    LEFT JOIN sales_ledger_product slp ON pl.id = slp.sales_ledger_id
-           -- 鏉′欢
-           WHERE sir.approval_status = 1 AND slp.type = 2
-             AND sir.record_type IN ('7','10')
-           group by pl.id
-       )T2 on T2.id = pl.id
-       left join (
-           select pl.id,
-                  sum(pro.total_amount) as returnAmount
-           from purchase_return_orders pro
-                    left join purchase_ledger pl on pro.purchase_ledger_id = pl.id
-           group by pl.id
-       )T3 on T3.id = pl.id
-       where pl.supplier_id = #{supplierId}
+              IFNULL(T2.InboundAmount, 0) AS shippedAmount,
+              pl.contract_amount - IFNULL(T2.InboundAmount, 0) AS unshippedAmount
+       FROM purchase_ledger pl
+       LEFT JOIN (
+           SELECT t.sales_ledger_id,
+                  SUM(t.inbound_amount) AS InboundAmount
+           FROM (
+               SELECT sir.stock_in_num * slp.tax_inclusive_unit_price AS inbound_amount, slp.sales_ledger_id
+               FROM stock_in_record sir
+               INNER JOIN sales_ledger_product slp ON slp.id = sir.record_id
+               WHERE sir.approval_status = 1 AND sir.record_type = 7 AND slp.type = 2
+               UNION ALL
+               SELECT sir.stock_in_num * slp.tax_inclusive_unit_price AS inbound_amount, slp.sales_ledger_id
+               FROM stock_in_record sir
+               INNER JOIN quality_inspect qi ON qi.id = sir.record_id
+               INNER JOIN purchase_ledger pl2 ON pl2.id = qi.purchase_ledger_id
+               INNER JOIN sales_ledger_product slp ON slp.sales_ledger_id = pl2.id AND slp.product_model_id = sir.product_model_id
+               WHERE sir.approval_status = 1 AND sir.record_type = 10 AND slp.type = 2
+           ) t
+           GROUP BY t.sales_ledger_id
+       ) T2 ON T2.sales_ledger_id = pl.id
+       WHERE pl.supplier_id = #{supplierId}
     </select>
 
 </mapper>
diff --git a/src/main/resources/mapper/device/DeviceMaintenanceMapper.xml b/src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
index e21669f..99b9bbc 100644
--- a/src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
+++ b/src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
@@ -26,31 +26,41 @@
         left join device_ledger dl on dm.device_ledger_id = dl.id
         left join sys_user su on dm.create_user = su.user_id
         <where>
-            1 = 1
-            <if test="deviceMaintenanceDto.deviceName != null">
-                and dl.device_name like concat('%',#{deviceMaintenanceDto.deviceName},'%')
+            <if test="deviceMaintenanceDto.deviceName != null and deviceMaintenanceDto.deviceName != ''">
+                and dl.device_name like concat('%', #{deviceMaintenanceDto.deviceName}, '%')
             </if>
-            <if test="deviceMaintenanceDto.deviceModel != null">
-                and dl.device_model like concat('%',#{deviceMaintenanceDto.deviceModel},'%')
+            <if test="deviceMaintenanceDto.deviceModel != null and deviceMaintenanceDto.deviceModel != ''">
+                and dl.device_model like concat('%', #{deviceMaintenanceDto.deviceModel}, '%')
             </if>
             <if test="deviceMaintenanceDto.status != null">
                 and dm.status = #{deviceMaintenanceDto.status}
             </if>
-            <if test="deviceMaintenanceDto.maintenanceActuallyName != null">
-                and dm.maintenance_actually_name like concat('%',#{deviceMaintenanceDto.maintenanceActuallyName},'%')
+            <if test="deviceMaintenanceDto.maintenanceActuallyName != null and deviceMaintenanceDto.maintenanceActuallyName != ''">
+                and dm.maintenance_actually_name like concat('%', #{deviceMaintenanceDto.maintenanceActuallyName}, '%')
             </if>
             <if test="deviceMaintenanceDto.maintenancePlanTime != null">
-                and dm.maintenance_plan_time like concat('%',#{deviceMaintenanceDto.maintenancePlanTime},'%')
+                and dm.maintenance_plan_time = #{deviceMaintenanceDto.maintenancePlanTime}
             </if>
             <if test="deviceMaintenanceDto.maintenanceActuallyTime != null">
-                and dm.maintenance_actually_time like concat('%',#{deviceMaintenanceDto.maintenanceActuallyTime},'%')
-            </if>
-            <if test="deviceMaintenanceDto.maintenanceActuallyTime != null">
-                and dm.maintenance_actually_time >= str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d')
-                and dm.maintenance_actually_time &lt; date_add(str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d'), interval 1 day)
+                and dm.maintenance_actually_time &gt;= str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime},
+                '%Y-%m-%d')
+                and dm.maintenance_actually_time &lt;
+                date_add(str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d'), interval 1 day)
             </if>
         </where>
+        order by
+        <!--    寰呬繚鍏�(0)浼樺厛鎺掑湪涓婇潰锛屽凡瀹岀粨(1)鍦ㄤ笅闈� -->
+        dm.status asc,
+        case
+        <!-- 褰撶姸鎬佹槸 0锛堝緟淇濆吇锛夋椂锛屾寜璁″垝鏃堕棿鍗囧簭锛屽嵆鏃堕棿鏈�杩滅殑鍗曞瓙鍦ㄦ渶涓婇潰 -->
+        when dm.status = 0 then dm.maintenance_plan_time
+        end asc,
+        case
+        <!-- 褰撶姸鎬佹槸 1锛堝凡瀹岀粨锛夋椂锛屾寜瀹為檯淇濆吇鏃堕棿闄嶅簭锛屾渶杩戝垰淇濆吇瀹岀殑鍗曞瓙鍦ㄥ凡瀹岀粨閲屾帓鏈�鍓� -->
+        when dm.status = 1 then dm.maintenance_actually_time
+        end desc
     </select>
+
     <select id="detailById" resultType="com.ruoyi.device.vo.DeviceMaintenanceVo">
         select dm.id,
                dm.device_ledger_id,
diff --git a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
index 5ecf0e9..973e44e 100644
--- a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -123,9 +123,9 @@
             <if test="endDateTime != null">
                 and ppo.create_time &lt;= #{endDateTime}
             </if>
-            <if test="userId != null">
-                and ppm.create_user = #{userId}
-            </if>
+<!--            <if test="userId != null">-->
+<!--                and ppm.create_user = #{userId}-->
+<!--            </if>-->
             <if test="processIds != null and processIds.size() > 0">
                 and poro.technology_operation_id in
                 <foreach collection="processIds" item="id" open="(" separator="," close=")">
diff --git a/src/main/resources/mapper/stock/StockInRecordMapper.xml b/src/main/resources/mapper/stock/StockInRecordMapper.xml
index a3bf3f9..89ba5aa 100644
--- a/src/main/resources/mapper/stock/StockInRecordMapper.xml
+++ b/src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -132,7 +132,7 @@
                 and p.id in (select id from product_tree)
             </if>
         </where>
-        order by sir.id desc
+        order by sir.create_time desc
     </select>
     <select id="listStockInRecordExportData" resultType="com.ruoyi.stock.execl.StockInRecordExportData">
         SELECT
@@ -159,7 +159,7 @@
                 and sir.record_type = #{params.recordType}
             </if>
         </where>
-        order by sir.id desc
+        order by sir.create_time desc
     </select>
     <select id="listPageAccountPurchase" resultType="com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo">
         SELECT
diff --git a/src/main/resources/mapper/stock/StockOutRecordMapper.xml b/src/main/resources/mapper/stock/StockOutRecordMapper.xml
index ad5976f..33e42ec 100644
--- a/src/main/resources/mapper/stock/StockOutRecordMapper.xml
+++ b/src/main/resources/mapper/stock/StockOutRecordMapper.xml
@@ -62,7 +62,7 @@
                 and p.id in (select id from product_tree)
             </if>
         </where>
-        order by sor.id desc
+        order by sor.create_time desc
     </select>
     <select id="listStockOutRecordExportData" resultType="com.ruoyi.stock.execl.StockOutRecordExportData">
         SELECT
diff --git a/src/test/java/com/ruoyi/approve/service/impl/ApproveProcessIdModifyTest.java b/src/test/java/com/ruoyi/approve/service/impl/ApproveProcessIdModifyTest.java
new file mode 100644
index 0000000..b8c4f3b
--- /dev/null
+++ b/src/test/java/com/ruoyi/approve/service/impl/ApproveProcessIdModifyTest.java
@@ -0,0 +1,117 @@
+package com.ruoyi.approve.service.impl;
+
+import com.ruoyi.approve.mapper.ApproveNodeMapper;
+import com.ruoyi.approve.mapper.ApproveProcessMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@SpringBootTest
+public class ApproveProcessIdModifyTest {
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    private ApproveProcessMapper approveProcessMapper;
+
+    @Autowired
+    private ApproveNodeMapper approveNodeMapper;
+
+    /**
+     * 淇敼瀹℃壒娴佺▼缂栧彿
+     */
+    @Test
+    void testModifyApproveId() {
+        Map<String, String> modifyMap = new LinkedHashMap<>();
+        modifyMap.put("20260523020", "2026-05-09");
+        modifyMap.put("20260523019", "2026-04-29");
+        modifyMap.put("20260523018", "2026-04-16");
+        modifyMap.put("20260523009", "2026-05-07");
+        modifyMap.put("20260523008", "2026-04-11");
+        modifyMap.put("20260523007", "2026-04-10");
+        modifyMap.put("20260523006", "2026-04-07");
+        modifyMap.put("20260523003", "2026-04-07");
+
+        for (Map.Entry<String, String> entry : modifyMap.entrySet()) {
+            processOne(entry.getKey(), entry.getValue());
+        }
+        System.out.println("鍏ㄩ儴瀹屾垚锛�");
+    }
+
+    private void processOne(String oldApproveId, String targetDateStr) {
+        LocalDate targetDate = LocalDate.parse(targetDateStr);
+        String datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+
+        String maxId = jdbcTemplate.queryForObject(
+                "SELECT MAX(approve_id) FROM approve_process WHERE approve_id LIKE ?",
+                String.class, datePrefix + "%");
+
+        long nextSeq = 1;
+        if (maxId != null && maxId.length() >= 11) {
+            nextSeq = Long.parseLong(maxId.substring(8)) + 1;
+        }
+        String newApproveId = datePrefix + String.format("%03d", nextSeq);
+
+        System.out.println("鏃ф壒鍙�: " + oldApproveId + " -> 鏂版壒鍙�: " + newApproveId);
+
+        List<Map<String, Object>> processList = jdbcTemplate.queryForList(
+                "SELECT * FROM approve_process WHERE approve_id = ?", oldApproveId);
+
+        if (processList.isEmpty()) {
+            System.out.println("  鈿� 鏈壘鍒拌褰曪紝璺宠繃");
+            return;
+        }
+
+        Map<String, Object> process = processList.get(0);
+
+        Timestamp newApproveTime = replaceDate(process.get("approve_time"), targetDate);
+        Timestamp newApproveOverTime = replaceDate(process.get("approve_over_time"), targetDate);
+        Timestamp newCreateTime = replaceDate(process.get("create_time"), targetDate);
+
+        jdbcTemplate.update(
+                "UPDATE approve_process SET approve_id = ?, approve_time = ?, approve_over_time = ?, create_time = ? WHERE approve_id = ?",
+                newApproveId, newApproveTime, newApproveOverTime, newCreateTime, oldApproveId);
+        System.out.println("  鉁� approve_process 宸叉洿鏂�");
+
+        List<Map<String, Object>> nodeList = jdbcTemplate.queryForList(
+                "SELECT * FROM approve_node WHERE approve_process_id = ?", oldApproveId);
+
+        if (!nodeList.isEmpty()) {
+            Map<String, Object> node = nodeList.get(0);
+
+            Timestamp newNodeTime = replaceDate(node.get("approve_node_time"), targetDate);
+            Timestamp newNodeCreateTime = replaceDate(node.get("create_time"), targetDate);
+            Timestamp newNodeUpdateTime = replaceDate(node.get("update_time"), targetDate);
+
+            jdbcTemplate.update(
+                    "UPDATE approve_node SET approve_process_id = ?, approve_node_time = ?, create_time = ?, update_time = ? WHERE approve_process_id = ?",
+                    newApproveId, newNodeTime, newNodeCreateTime, newNodeUpdateTime, oldApproveId);
+            System.out.println("  鉁� approve_node 宸叉洿鏂� (" + nodeList.size() + " 鏉�)");
+        } else {
+            System.out.println("  - approve_node 鏃犺褰�");
+        }
+    }
+
+    private static Timestamp replaceDate(Object dateObj, LocalDate targetDate) {
+        if (dateObj == null) return null;
+        LocalDateTime ldt;
+        if (dateObj instanceof Timestamp ts) {
+            ldt = ts.toLocalDateTime();
+        } else if (dateObj instanceof LocalDateTime dt) {
+            ldt = dt;
+        } else {
+            return null;
+        }
+        return Timestamp.valueOf(LocalDateTime.of(targetDate, ldt.toLocalTime()));
+    }
+}
diff --git a/src/test/java/com/ruoyi/device/service/impl/MaintenanceTaskJobTest.java b/src/test/java/com/ruoyi/device/service/impl/MaintenanceTaskJobTest.java
new file mode 100644
index 0000000..0c41f05
--- /dev/null
+++ b/src/test/java/com/ruoyi/device/service/impl/MaintenanceTaskJobTest.java
@@ -0,0 +1,448 @@
+package com.ruoyi.device.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.device.mapper.MaintenanceTaskMapper;
+import com.ruoyi.device.pojo.DeviceMaintenance;
+import com.ruoyi.device.pojo.MaintenanceTask;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.YearMonth;
+import java.time.LocalDate;
+import java.time.DayOfWeek;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 璁惧淇濆吇浠诲姟娴嬭瘯绫�
+ *
+ * 鏌ヨ鏁版嵁搴撲腑鐨勪繚鍏讳换鍔★紝鏍规嵁鐧昏鏃ユ湡銆侀娆°�佸紑濮嬫棩鏈熶笌鏃堕棿鏉ョ敓鎴愪繚鍏昏褰�
+ * 浠庣櫥璁版棩鏈熷埌浠婂ぉ锛屾寜棰戞鐢熸垚鎵�鏈夌鍚堢殑璁板綍
+ */
+@SpringBootTest
+public class MaintenanceTaskJobTest {
+
+    @Autowired
+    private MaintenanceTaskMapper maintenanceTaskMapper;
+
+    @Autowired
+    private DeviceMaintenanceServiceImpl deviceMaintenanceService;
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    /**
+     * 娴嬭瘯锛氭寜鏃ユ湡鍒嗘暎鐢熸垚淇濆吇璁板綍
+     * 浠庣櫥璁版棩鏈熷埌浠婂ぉ锛屾寜鏃ユ湡椤哄簭锛屾瘡澶╃殑鎵�鏈夎澶囦繚鍏昏褰曚竴璧风敓鎴�
+     */
+    @Test
+    void testGenerateOneByOne() {
+        // 鏌ヨ鎵�鏈夊惎鐢ㄧ殑淇濆吇浠诲姟
+        List<MaintenanceTask> tasks = maintenanceTaskMapper.selectList(
+                new LambdaQueryWrapper<MaintenanceTask>()
+                        .eq(MaintenanceTask::getIsActive, 1)
+                        .eq(MaintenanceTask::getDeleted, 0)
+        );
+
+        if (tasks.isEmpty()) {
+            System.out.println("=== 娌℃湁鎵惧埌淇濆吇浠诲姟 ===");
+            return;
+        }
+
+        // 鎵惧嚭鏈�鏃╁拰鏈�鏅氱殑鐧昏鏃ユ湡
+        LocalDate earliestDate = tasks.stream()
+                .map(MaintenanceTask::getRegistrationDate)
+                .filter(Objects::nonNull)
+                .min(LocalDate::compareTo)
+                .orElse(LocalDate.now());
+        LocalDate latestDate = LocalDate.now();
+
+        System.out.println("=== 鐧昏鏃ユ湡鑼冨洿: " + earliestDate + " 鑷� " + latestDate + " ===");
+        System.out.println("=== 鍏辨壘鍒� " + tasks.size() + " 涓繚鍏讳换鍔� ===\n");
+
+        // 鎸夋棩鏈熼『搴忕敓鎴愶紝姣忓ぉ鐢熸垚涓�娆�
+        LocalDate currentDate = earliestDate;
+        int totalRecords = 0;
+
+        while (!currentDate.isAfter(latestDate)) {
+            final LocalDate date = currentDate;
+            final List<DeviceMaintenance> recordsToSave = new ArrayList<>();
+
+            // 鎵惧嚭褰撳ぉ闇�瑕佷繚鍏荤殑鎵�鏈変换鍔�
+            for (MaintenanceTask task : tasks) {
+                LocalDate startDate = task.getRegistrationDate() != null ? task.getRegistrationDate() : LocalDate.now();
+                if (date.isBefore(startDate)) {
+                    continue; // 璺宠繃鐧昏鏃ユ湡涔嬪墠鐨勬棩鏈�
+                }
+
+                // 妫�鏌ヨ浠诲姟鐨勯娆℃槸鍚﹀尮閰嶅綋澶�
+                if (isExecutionDate(task.getFrequencyType(), task.getFrequencyDetail(), startDate, date)) {
+                    LocalTime time = getExecutionTime(task.getFrequencyDetail());
+                    LocalDateTime executionDateTime = LocalDateTime.of(date, time);
+
+                    DeviceMaintenance record = createMaintenanceRecord(task, executionDateTime);
+                    recordsToSave.add(record);
+                }
+            }
+
+            // 濡傛灉褰撳ぉ鏈夎褰曪紝鎵归噺淇濆瓨
+            if (!recordsToSave.isEmpty()) {
+                for (DeviceMaintenance record : recordsToSave) {
+                    try {
+                        deviceMaintenanceService.save(record);
+                        System.out.println("  鈫� 璁″垝鏃ユ湡: " + date + " | 璁惧: " + record.getDeviceName()
+                                + " | 璁板綍ID: " + record.getId());
+                    } catch (Exception e) {
+                        System.out.println("  鉁� 璁″垝鏃ユ湡: " + date + " | 璁惧: " + record.getDeviceName()
+                                + " | 澶辫触: " + e.getMessage());
+                    }
+                }
+                totalRecords += recordsToSave.size();
+                System.out.println("  === 鏃ユ湡 " + date + " 鍏辩敓鎴� " + recordsToSave.size() + " 鏉¤褰� ===\n");
+            }
+
+            currentDate = currentDate.plusDays(1);
+        }
+
+        System.out.println("=== 鎵ц瀹屾垚: 鍏辩敓鎴� " + totalRecords + " 鏉¤褰� ===");
+    }
+
+    /**
+     * 妫�鏌ユ寚瀹氭棩鏈熸槸鍚︾鍚堜换鍔$殑鎵ц棰戞
+     */
+    private boolean isExecutionDate(String frequencyType, String frequencyDetail, LocalDate startDate, LocalDate checkDate) {
+        if (checkDate.isBefore(startDate)) {
+            return false;
+        }
+
+        switch (frequencyType) {
+            case "DAILY":
+                return true; // 姣忓ぉ閮介渶瑕佹墽琛�
+            case "WEEKLY":
+                return isWeeklyMatch(frequencyDetail, checkDate);
+            case "MONTHLY":
+                return isMonthlyMatch(frequencyDetail, checkDate);
+            case "QUARTERLY":
+                return isQuarterlyMatch(frequencyDetail, checkDate);
+            default:
+                return false;
+        }
+    }
+
+    private boolean isWeeklyMatch(String detail, LocalDate date) {
+        String[] parts = detail.split(",");
+        String dayOfWeekStr = parts[0];
+        DayOfWeek targetDay = parseDayOfWeek(dayOfWeekStr);
+        return date.getDayOfWeek() == targetDay;
+    }
+
+    private boolean isMonthlyMatch(String detail, LocalDate date) {
+        String[] parts = detail.split(",");
+        int targetDay = Integer.parseInt(parts[0]);
+        return date.getDayOfMonth() == targetDay;
+    }
+
+    private boolean isQuarterlyMatch(String detail, LocalDate date) {
+        String[] parts = detail.split(",");
+        int quarterMonth = Integer.parseInt(parts[0]); // 瀛e害涓殑绗嚑涓湀
+        int targetDay = Integer.parseInt(parts[1]);
+
+        int currentMonth = date.getMonthValue();
+        int monthInQuarter = (currentMonth - 1) % 3 + 1;
+
+        return monthInQuarter == quarterMonth && date.getDayOfMonth() == targetDay;
+    }
+
+    private LocalTime getExecutionTime(String frequencyDetail) {
+        String[] parts = frequencyDetail.split(",");
+        if (parts.length >= 2 && (parts.length == 2 || parts.length == 3)) {
+            return LocalTime.parse(parts[parts.length - 1]);
+        }
+        return LocalTime.of(9, 0); // 榛樿鏃堕棿
+    }
+
+    /**
+     * 娴嬭瘯锛氭牴鎹墍鏈変繚鍏讳换鍔$敓鎴愯澶囦繚鍏昏褰�
+     * 鎸夐『搴忓鐞嗘瘡涓换鍔★紝浠庣櫥璁版棩鏈熷埌浠婂ぉ锛屾寜棰戞渚濇鐢熸垚鎵�鏈夌鍚堢殑璁板綍
+     */
+    @Test
+    void testGenerateMaintenanceRecordForAllTasks() {
+        // 鏌ヨ鎵�鏈夊惎鐢ㄧ殑淇濆吇浠诲姟锛屾寜ID鎺掑簭
+        List<MaintenanceTask> tasks = maintenanceTaskMapper.selectList(
+                new LambdaQueryWrapper<MaintenanceTask>()
+                        .eq(MaintenanceTask::getIsActive, 1)
+                        .eq(MaintenanceTask::getDeleted, 0)
+                        .orderByAsc(MaintenanceTask::getId)
+        );
+
+        System.out.println("=== 鍏辨壘鍒� " + tasks.size() + " 涓繚鍏讳换鍔� ===\n");
+
+        int totalRecords = 0;
+        for (MaintenanceTask task : tasks) {
+            try {
+                // 鎸夐『搴忕敓鎴愯浠诲姟鎵�鏈夌鍚堥娆$殑璁板綍
+                int recordCount = generateRecordsForTaskSequentially(task);
+                totalRecords += recordCount;
+                System.out.println("鉁� 浠诲姟ID: " + task.getId() + " | " + task.getTaskName()
+                        + " | 鐧昏鏃ユ湡: " + task.getRegistrationDate()
+                        + " | 鐢熸垚璁板綍鏁�: " + recordCount);
+            } catch (Exception e) {
+                System.out.println("鉁� 浠诲姟ID: " + task.getId() + " | " + task.getTaskName() + " | 澶辫触: " + e.getMessage());
+            }
+        }
+
+        System.out.println("\n=== 鎵ц瀹屾垚: 鍏辩敓鎴� " + totalRecords + " 鏉¤褰� ===");
+    }
+
+    /**
+     * 涓哄崟涓换鍔℃寜椤哄簭鐢熸垚鎵�鏈夌鍚堥娆$殑璁板綍
+     */
+    private int generateRecordsForTaskSequentially(MaintenanceTask task) {
+        LocalDate startDate = task.getRegistrationDate();
+        if (startDate == null) {
+            startDate = LocalDate.now();
+        }
+        LocalDate endDate = LocalDate.now();
+
+        // 鏍规嵁棰戞鑾峰彇鎵�鏈夐渶瑕佹墽琛岀殑鏃ユ湡
+        List<LocalDateTime> executionDates = getExecutionDates(task.getFrequencyType(), task.getFrequencyDetail(), startDate, endDate);
+
+        if (executionDates.isEmpty()) {
+            return 0;
+        }
+
+        int count = 0;
+        for (LocalDateTime executionDate : executionDates) {
+            try {
+                // 鎸夐『搴忕敓鎴愪繚鍏昏褰�
+                DeviceMaintenance record = createMaintenanceRecord(task, executionDate);
+                deviceMaintenanceService.save(record);
+                count++;
+            } catch (Exception e) {
+                System.out.println("  鉁� 鏃ユ湡: " + executionDate + " | 澶辫触: " + e.getMessage());
+            }
+        }
+
+        // 鏇存柊浠诲姟鐨勪笅娆℃墽琛屾椂闂�
+        if (count > 0) {
+            // first_execution = 绗竴鏉¤褰曠殑鏃ユ湡 = last_execution_time
+            LocalDateTime firstExecution = executionDates.get(0);
+            // next_execution = 鏈�鍚庝竴鏉¤褰曠殑涓嬩竴娆℃墽琛屾棩鏈�
+            LocalDateTime lastExecution = executionDates.get(executionDates.size() - 1);
+            LocalDateTime nextExecution = calculateNextExecutionTime(task.getFrequencyType(), task.getFrequencyDetail(), lastExecution);
+            updateTaskExecutionTime(task.getId(), firstExecution, nextExecution);
+        }
+
+        return count;
+    }
+
+    /**
+     * 鑾峰彇鎸囧畾鏃ユ湡鑼冨洿鍐呮墍鏈夌鍚堥娆$殑鎵ц鏃堕棿
+     */
+    private List<LocalDateTime> getExecutionDates(String frequencyType, String frequencyDetail, LocalDate startDate, LocalDate endDate) {
+        List<LocalDateTime> dates = new ArrayList<>();
+
+        switch (frequencyType) {
+            case "DAILY":
+                dates.addAll(getDailyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "WEEKLY":
+                dates.addAll(getWeeklyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "MONTHLY":
+                dates.addAll(getMonthlyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "QUARTERLY":
+                dates.addAll(getQuarterlyDates(startDate, endDate, frequencyDetail));
+                break;
+        }
+
+        return dates;
+    }
+
+    private List<LocalDateTime> getDailyDates(LocalDate startDate, LocalDate endDate, String timeStr) {
+        List<LocalDateTime> dates = new ArrayList<>();
+        LocalTime time = LocalTime.parse(timeStr);
+
+        LocalDate current = startDate;
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusDays(1);
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getMonthlyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new ArrayList<>();
+        String[] parts = detail.split(",");
+        int dayOfMonth = Integer.parseInt(parts[0]);
+        LocalTime time = LocalTime.parse(parts[1]);
+
+        // 浠庣櫥璁版棩鏈熸墍鍦ㄦ湀鎵剧涓�涓鍚堟潯浠剁殑鏃ユ湡
+        int actualDay = Math.min(dayOfMonth, startDate.lengthOfMonth());
+        LocalDate current = startDate.withDayOfMonth(actualDay);
+
+        // 濡傛灉杩欎釜鏃ユ湡鍦ㄧ櫥璁版棩鏈熶箣鍓嶏紝寰�鍚庢帹涓�涓湀
+        if (current.isBefore(startDate)) {
+            current = current.plusMonths(1);
+            actualDay = Math.min(dayOfMonth, current.lengthOfMonth());
+            current = current.withDayOfMonth(actualDay);
+        }
+
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusMonths(1);
+            actualDay = Math.min(dayOfMonth, current.lengthOfMonth());
+            current = current.withDayOfMonth(actualDay);
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getWeeklyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new ArrayList<>();
+        String[] parts = detail.split(",");
+        String dayOfWeekStr = parts[0];
+        LocalTime time = LocalTime.parse(parts[1]);
+
+        java.time.DayOfWeek targetDay = parseDayOfWeek(dayOfWeekStr);
+
+        LocalDate current = startDate;
+        while (!current.isAfter(endDate)) {
+            if (current.getDayOfWeek() == targetDay) {
+                dates.add(LocalDateTime.of(current, time));
+            }
+            current = current.plusDays(1);
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getQuarterlyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new ArrayList<>();
+        String[] parts = detail.split(",");
+        int quarterMonth = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        int currentMonth = startDate.getMonthValue();
+        int targetMonth = ((currentMonth - 1) / 3) * 3 + quarterMonth;
+        int yearAdjust = 0;
+
+        if (targetMonth > 12) {
+            targetMonth -= 12;
+            yearAdjust = 1;
+        }
+
+        int actualDay = Math.min(dayOfMonth, YearMonth.of(startDate.getYear() + yearAdjust, targetMonth).lengthOfMonth());
+        LocalDate current = startDate.withYear(startDate.getYear() + yearAdjust)
+                .withMonth(targetMonth)
+                .withDayOfMonth(actualDay);
+
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusMonths(3);
+            actualDay = Math.min(dayOfMonth, current.lengthOfMonth());
+            current = current.withDayOfMonth(actualDay);
+        }
+        return dates;
+    }
+
+    private java.time.DayOfWeek parseDayOfWeek(String dayOfWeekStr) {
+        switch (dayOfWeekStr.toUpperCase()) {
+            case "MON": return java.time.DayOfWeek.MONDAY;
+            case "TUE": return java.time.DayOfWeek.TUESDAY;
+            case "WED": return java.time.DayOfWeek.WEDNESDAY;
+            case "THU": return java.time.DayOfWeek.THURSDAY;
+            case "FRI": return java.time.DayOfWeek.FRIDAY;
+            case "SAT": return java.time.DayOfWeek.SATURDAY;
+            case "SUN": return java.time.DayOfWeek.SUNDAY;
+            default: throw new IllegalArgumentException("鏃犳晥鐨勬槦鏈熷嚑: " + dayOfWeekStr);
+        }
+    }
+
+    /**
+     * 鍒涘缓淇濆吇璁板綍
+     */
+    private DeviceMaintenance createMaintenanceRecord(MaintenanceTask task, LocalDateTime executionDate) {
+        DeviceMaintenance record = new DeviceMaintenance();
+        record.setDeviceName(task.getTaskName());
+        record.setMaintenanceTaskId(task.getId());
+        record.setDeviceLedgerId(task.getTaskId());
+        record.setMaintenancePlanTime(executionDate);
+        record.setMaintenanceActuallyName(task.getMaintenancePerson());
+        record.setFrequencyType(task.getFrequencyType());
+        record.setFrequencyDetail(task.getFrequencyDetail());
+        record.setTenantId(task.getTenantId());
+        record.setStatus(0); // 寰呬繚鍏�
+        record.setDeviceModel(task.getDeviceModel());
+        record.setMachineryCategory(task.getMachineryCategory());
+        record.setCreateUser(Integer.parseInt(task.getRegistrantId().toString()));
+        record.setUpdateTime(executionDate);
+        record.setCreateTime(executionDate);
+        record.setUpdateUser(Integer.parseInt(task.getRegistrantId().toString()));
+        return record;
+    }
+
+    /**
+     * 鏇存柊浠诲姟鐨勬墽琛屾椂闂�
+     */
+    private void updateTaskExecutionTime(Long taskId, LocalDateTime lastExecutionTime, LocalDateTime nextExecutionTime) {
+        String sql = "UPDATE maintenance_task SET last_execution_time = ?, next_execution_time = ? WHERE id = ?";
+        jdbcTemplate.update(sql, lastExecutionTime, nextExecutionTime, taskId);
+    }
+
+    /**
+     * 璁$畻涓嬫鎵ц鏃堕棿
+     */
+    private LocalDateTime calculateNextExecutionTime(String frequencyType, String frequencyDetail, LocalDateTime currentTime) {
+        return switch (frequencyType) {
+            case "DAILY" -> calculateDailyNextTime(frequencyDetail, currentTime);
+            case "WEEKLY" -> calculateWeeklyNextTime(frequencyDetail, currentTime);
+            case "MONTHLY" -> calculateMonthlyNextTime(frequencyDetail, currentTime);
+            case "QUARTERLY" -> calculateQuarterlyNextTime(frequencyDetail, currentTime);
+            default -> throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戞绫诲瀷: " + frequencyType);
+        };
+    }
+
+    private LocalDateTime calculateDailyNextTime(String timeStr, LocalDateTime current) {
+        LocalTime executionTime = LocalTime.parse(timeStr);
+        LocalDateTime nextTime = LocalDateTime.of(current.toLocalDate(), executionTime);
+        return current.isBefore(nextTime) ? nextTime : nextTime.plusDays(1);
+    }
+
+    private LocalDateTime calculateMonthlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int dayOfMonth = Integer.parseInt(parts[0]);
+        LocalTime time = LocalTime.parse(parts[1]);
+        return current.plusMonths(1)
+                .withDayOfMonth(Math.min(dayOfMonth, current.plusMonths(1).toLocalDate().lengthOfMonth()))
+                .with(time);
+    }
+
+    private LocalDateTime calculateWeeklyNextTime(String detail, LocalDateTime current) {
+        return current.plusWeeks(1);
+    }
+
+    private LocalDateTime calculateQuarterlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int quarterMonth = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        int currentMonthInQuarter = (current.getMonthValue() - 1) % 3 + 1;
+
+        YearMonth targetYearMonth;
+        if (currentMonthInQuarter < quarterMonth) {
+            targetYearMonth = YearMonth.from(current).plusMonths(quarterMonth - currentMonthInQuarter);
+        } else {
+            targetYearMonth = YearMonth.from(current).plusMonths(3 - currentMonthInQuarter + quarterMonth);
+        }
+
+        int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+        return LocalDateTime.of(targetYearMonth.getYear(), targetYearMonth.getMonthValue(), adjustedDay, time.getHour(), time.getMinute());
+    }
+}
diff --git a/src/test/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJobTest.java b/src/test/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJobTest.java
new file mode 100644
index 0000000..426f332
--- /dev/null
+++ b/src/test/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJobTest.java
@@ -0,0 +1,339 @@
+package com.ruoyi.inspectiontask.service.impl;
+
+import com.ruoyi.inspectiontask.mapper.InspectionTaskMapper;
+import com.ruoyi.inspectiontask.pojo.InspectionTask;
+import com.ruoyi.inspectiontask.pojo.TimingTask;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.time.DayOfWeek;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.YearMonth;
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 璁惧宸℃瀹氭椂浠诲姟娴嬭瘯绫�
+ *
+ * 鏌ヨ鏁版嵁搴撲腑鐨勫贰妫�浠诲姟锛屾牴鎹櫥璁版棩鏈熴�侀娆°�佸紑濮嬫棩鏈熶笌鏃堕棿鏉ョ敓鎴愬贰妫�璁板綍
+ * 骞舵洿鏂� timing_task 琛ㄧ殑鏈�鍚庢墽琛屾椂闂村拰涓嬫鎵ц鏃堕棿
+ */
+@SpringBootTest
+public class TimingTaskJobTest {
+
+    @Autowired
+    private InspectionTaskMapper inspectionTaskMapper;
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    /**
+     * 娴嬭瘯锛氭牴鎹墍鏈夊贰妫�浠诲姟鐢熸垚宸℃璁板綍
+     * 浠庣櫥璁版棩鏈熷埌浠婂ぉ锛屾寜棰戞姣忓懆鐢熸垚涓�鏉¤褰�
+     */
+    @Test
+    void testGenerateInspectionRecordForAllTasks() {
+        // 鏌ヨ鎵�鏈夊惎鐢ㄧ殑宸℃浠诲姟
+        String sql = "SELECT * FROM timing_task WHERE is_enabled = 1 AND deleted = 0";
+        List<TimingTask> tasks = jdbcTemplate.query(sql,
+                (rs, rowNum) -> {
+                    TimingTask task = new TimingTask();
+                    task.setId(rs.getLong("id"));
+                    task.setTaskName(rs.getString("task_name"));
+                    task.setInspectionProject(rs.getString("inspection_project"));
+                    task.setTaskId(rs.getInt("task_id"));
+                    task.setInspectorIds(rs.getString("inspector_ids"));
+                    task.setInspectionLocation(rs.getString("inspection_location"));
+                    task.setFrequencyType(rs.getString("frequency_type"));
+                    task.setFrequencyDetail(rs.getString("frequency_detail"));
+                    task.setRemarks(rs.getString("remarks"));
+                    task.setRegistrantId(rs.getLong("registrant_id"));
+                    task.setRegistrant(rs.getString("registrant"));
+                    task.setTenantId(rs.getLong("tenant_id"));
+                    // 鑾峰彇鐧昏鏃ユ湡
+                    java.sql.Date regDate = rs.getDate("registration_date");
+                    if (regDate != null) {
+                        task.setRegistrationDate(regDate.toLocalDate());
+                    }
+                    return task;
+                });
+
+        System.out.println("=== 鍏辨壘鍒� " + tasks.size() + " 涓贰妫�浠诲姟 ===\n");
+
+        int totalRecords = 0;
+        for (TimingTask task : tasks) {
+            try {
+                // 鐢熸垚鎵�鏈夌鍚堥娆$殑璁板綍
+                int recordCount = generateRecordsForTask(task);
+                totalRecords += recordCount;
+                System.out.println("鉁� 浠诲姟ID: " + task.getId() + " | " + task.getTaskName()
+                        + " | 鐧昏鏃ユ湡: " + task.getRegistrationDate()
+                        + " | 鐢熸垚璁板綍鏁�: " + recordCount);
+            } catch (Exception e) {
+                System.out.println("鉁� 浠诲姟ID: " + task.getId() + " | " + task.getTaskName() + " | 澶辫触: " + e.getMessage());
+            }
+        }
+
+        System.out.println("\n=== 鎵ц瀹屾垚: 鍏辩敓鎴� " + totalRecords + " 鏉¤褰� ===");
+    }
+
+    /**
+     * 涓哄崟涓换鍔$敓鎴愭墍鏈夌鍚堥娆$殑璁板綍
+     */
+    private int generateRecordsForTask(TimingTask task) {
+        LocalDate startDate = task.getRegistrationDate();
+        if (startDate == null) {
+            startDate = LocalDate.now();
+        }
+        LocalDate endDate = LocalDate.now();
+
+        // 鏍规嵁棰戞鑾峰彇鎵�鏈夐渶瑕佹墽琛岀殑鏃ユ湡
+        List<LocalDateTime> executionDates = getExecutionDates(task.getFrequencyType(), task.getFrequencyDetail(), startDate, endDate);
+
+        int count = 0;
+        for (LocalDateTime executionDate : executionDates) {
+            try {
+                // 鐢熸垚宸℃璁板綍
+                InspectionTask record = createInspectionRecord(task, executionDate);
+                inspectionTaskMapper.insert(record);
+
+                // 鏇存柊浠诲姟鐨勪笂娆℃墽琛屾椂闂�
+                updateTaskLastExecutionTime(task.getId(), executionDate);
+
+                count++;
+            } catch (Exception e) {
+                System.out.println("  鉁� 鏃ユ湡: " + executionDate + " | 澶辫触: " + e.getMessage());
+            }
+        }
+
+        // 鏇存柊浠诲姟鐨勪笅娆℃墽琛屾椂闂�
+        if (count > 0) {
+            LocalDateTime lastExecution = executionDates.get(executionDates.size() - 1);
+            LocalDateTime nextExecution = calculateNextExecutionTime(task.getFrequencyType(), task.getFrequencyDetail(), lastExecution);
+            updateTaskNextExecutionTime(task.getId(), nextExecution);
+        }
+
+        return count;
+    }
+
+    /**
+     * 鑾峰彇鎸囧畾鏃ユ湡鑼冨洿鍐呮墍鏈夌鍚堥娆$殑鎵ц鏃堕棿
+     */
+    private List<LocalDateTime> getExecutionDates(String frequencyType, String frequencyDetail, LocalDate startDate, LocalDate endDate) {
+        List<LocalDateTime> dates = new java.util.ArrayList<>();
+
+        switch (frequencyType) {
+            case "DAILY":
+                dates.addAll(getDailyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "WEEKLY":
+                dates.addAll(getWeeklyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "MONTHLY":
+                dates.addAll(getMonthlyDates(startDate, endDate, frequencyDetail));
+                break;
+            case "QUARTERLY":
+                dates.addAll(getQuarterlyDates(startDate, endDate, frequencyDetail));
+                break;
+        }
+
+        return dates;
+    }
+
+    private List<LocalDateTime> getDailyDates(LocalDate startDate, LocalDate endDate, String timeStr) {
+        List<LocalDateTime> dates = new java.util.ArrayList<>();
+        LocalTime time = LocalTime.parse(timeStr);
+
+        LocalDate current = startDate;
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusDays(1);
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getWeeklyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new java.util.ArrayList<>();
+        String[] parts = detail.split(",");
+        String dayOfWeekStr = parts[0];
+        LocalTime time = LocalTime.parse(parts[1]);
+
+        Set<DayOfWeek> targetDays = parseDayOfWeeks(dayOfWeekStr);
+
+        LocalDate current = startDate;
+        while (!current.isAfter(endDate)) {
+            if (targetDays.contains(current.getDayOfWeek())) {
+                dates.add(LocalDateTime.of(current, time));
+            }
+            current = current.plusDays(1);
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getMonthlyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new java.util.ArrayList<>();
+        String[] parts = detail.split(",");
+        int dayOfMonth = Integer.parseInt(parts[0]);
+        LocalTime time = LocalTime.parse(parts[1]);
+
+        LocalDate current = startDate.plusMonths(0).withDayOfMonth(Math.min(dayOfMonth, startDate.lengthOfMonth()));
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusMonths(1).withDayOfMonth(Math.min(dayOfMonth, current.plusMonths(1).lengthOfMonth()));
+        }
+        return dates;
+    }
+
+    private List<LocalDateTime> getQuarterlyDates(LocalDate startDate, LocalDate endDate, String detail) {
+        List<LocalDateTime> dates = new java.util.ArrayList<>();
+        String[] parts = detail.split(",");
+        int quarterMonth = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        int currentMonth = startDate.getMonthValue();
+        int targetMonth = ((currentMonth - 1) / 3) * 3 + quarterMonth;
+        int yearAdjust = 0;
+
+        if (targetMonth > 12) {
+            targetMonth -= 12;
+            yearAdjust = 1;
+        }
+
+        LocalDate current = startDate.withYear(startDate.getYear() + yearAdjust)
+                .withMonth(targetMonth)
+                .withDayOfMonth(Math.min(dayOfMonth, YearMonth.of(startDate.getYear() + yearAdjust, targetMonth).lengthOfMonth()));
+
+        while (!current.isAfter(endDate)) {
+            dates.add(LocalDateTime.of(current, time));
+            current = current.plusMonths(3).withDayOfMonth(Math.min(dayOfMonth, current.plusMonths(3).lengthOfMonth()));
+        }
+        return dates;
+    }
+
+    private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
+        Set<DayOfWeek> days = new HashSet<>();
+        String[] dayStrs = dayOfWeekStr.split("\\|");
+
+        for (String dayStr : dayStrs) {
+            switch (dayStr.toUpperCase()) {
+                case "MON": days.add(DayOfWeek.MONDAY); break;
+                case "TUE": days.add(DayOfWeek.TUESDAY); break;
+                case "WED": days.add(DayOfWeek.WEDNESDAY); break;
+                case "THU": days.add(DayOfWeek.THURSDAY); break;
+                case "FRI": days.add(DayOfWeek.FRIDAY); break;
+                case "SAT": days.add(DayOfWeek.SATURDAY); break;
+                case "SUN": days.add(DayOfWeek.SUNDAY); break;
+            }
+        }
+        return days;
+    }
+
+    /**
+     * 璁$畻涓嬫鎵ц鏃堕棿
+     */
+    private LocalDateTime calculateNextExecutionTime(String frequencyType, String frequencyDetail, LocalDateTime currentTime) {
+        return switch (frequencyType) {
+            case "DAILY" -> calculateDailyNextTime(frequencyDetail, currentTime);
+            case "WEEKLY" -> calculateWeeklyNextTime(frequencyDetail, currentTime);
+            case "MONTHLY" -> calculateMonthlyNextTime(frequencyDetail, currentTime);
+            case "QUARTERLY" -> calculateQuarterlyNextTime(frequencyDetail, currentTime);
+            default -> throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戞绫诲瀷: " + frequencyType);
+        };
+    }
+
+    private LocalDateTime calculateDailyNextTime(String timeStr, LocalDateTime current) {
+        LocalTime executionTime = LocalTime.parse(timeStr);
+        LocalDateTime nextTime = LocalDateTime.of(current.toLocalDate(), executionTime);
+        return current.isBefore(nextTime) ? nextTime : nextTime.plusDays(1);
+    }
+
+    private LocalDateTime calculateMonthlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int dayOfMonth = Integer.parseInt(parts[0]);
+        LocalTime time = LocalTime.parse(parts[1]);
+        return current.plusMonths(1)
+                .withDayOfMonth(Math.min(dayOfMonth, current.plusMonths(1).toLocalDate().lengthOfMonth()))
+                .with(time);
+    }
+
+    private LocalDateTime calculateWeeklyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        String dayOfWeekStr = parts[0];
+        LocalTime time = LocalTime.parse(parts[1]);
+
+        Set<DayOfWeek> targetDays = parseDayOfWeeks(dayOfWeekStr);
+
+        LocalDateTime nextTime = current;
+        while (true) {
+            nextTime = nextTime.plusDays(1);
+            if (targetDays.contains(nextTime.getDayOfWeek())) {
+                return LocalDateTime.of(nextTime.toLocalDate(), time);
+            }
+            if (nextTime.isAfter(current.plusYears(1))) {
+                throw new RuntimeException("鏃犳硶鎵惧埌涓嬫鎵ц鏃堕棿");
+            }
+        }
+    }
+
+    private LocalDateTime calculateQuarterlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int quarterMonth = Integer.parseInt(parts[0]);
+        int dayOfMonth = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        int currentMonthInQuarter = (current.getMonthValue() - 1) % 3 + 1;
+
+        YearMonth targetYearMonth;
+        if (currentMonthInQuarter < quarterMonth) {
+            targetYearMonth = YearMonth.from(current).plusMonths(quarterMonth - currentMonthInQuarter);
+        } else {
+            targetYearMonth = YearMonth.from(current).plusMonths(3 - currentMonthInQuarter + quarterMonth);
+        }
+
+        int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
+        return LocalDateTime.of(targetYearMonth.getYear(), targetYearMonth.getMonthValue(), adjustedDay, time.getHour(), time.getMinute());
+    }
+
+    /**
+     * 鍒涘缓宸℃璁板綍
+     */
+    private InspectionTask createInspectionRecord(TimingTask timingTask, LocalDateTime executionDate) {
+        InspectionTask inspectionTask = new InspectionTask();
+        inspectionTask.setTaskName(timingTask.getTaskName());
+        inspectionTask.setInspectionProject(timingTask.getInspectionProject());
+        inspectionTask.setTaskId(timingTask.getTaskId());
+        inspectionTask.setInspectorId(timingTask.getInspectorIds());
+        inspectionTask.setInspectionLocation(timingTask.getInspectionLocation());
+        inspectionTask.setRemarks("鑷姩鐢熸垚鑷畾鏃朵换鍔D: " + timingTask.getId() + "锛�" + timingTask.getRemarks());
+        inspectionTask.setRegistrantId(timingTask.getRegistrantId());
+        inspectionTask.setFrequencyType(timingTask.getFrequencyType());
+        inspectionTask.setFrequencyDetail(timingTask.getFrequencyDetail());
+        inspectionTask.setTenantId(timingTask.getTenantId());
+        // 璁剧疆鐧昏鏃ユ湡涓烘墽琛屾棩鏈�
+        inspectionTask.setCreateTime(executionDate);
+        inspectionTask.setUpdateTime(executionDate);
+        return inspectionTask;
+    }
+
+    /**
+     * 鏇存柊浠诲姟鐨勪笂娆℃墽琛屾椂闂�
+     */
+    private void updateTaskLastExecutionTime(Long taskId, LocalDateTime lastExecutionTime) {
+        String updateSql = "UPDATE timing_task SET last_execution_time = ? WHERE id = ?";
+        jdbcTemplate.update(updateSql, lastExecutionTime, taskId);
+    }
+
+    /**
+     * 鏇存柊浠诲姟鐨勪笅娆℃墽琛屾椂闂�
+     */
+    private void updateTaskNextExecutionTime(Long taskId, LocalDateTime nextExecutionTime) {
+        String updateSql = "UPDATE timing_task SET next_execution_time = ? WHERE id = ?";
+        jdbcTemplate.update(updateSql, nextExecutionTime, taskId);
+    }
+}
diff --git a/src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java b/src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java
new file mode 100644
index 0000000..352818d
--- /dev/null
+++ b/src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java
@@ -0,0 +1,254 @@
+package com.ruoyi.stock.service.impl;
+
+import com.ruoyi.production.mapper.ProductionOrderMapper;
+import com.ruoyi.production.mapper.ProductionOrderPickMapper;
+import com.ruoyi.production.pojo.ProductionOrder;
+import com.ruoyi.production.pojo.ProductionOrderPick;
+import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
+import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
+import com.ruoyi.sales.mapper.SalesLedgerMapper;
+import com.ruoyi.sales.mapper.ShippingInfoMapper;
+import com.ruoyi.sales.pojo.SalesLedger;
+import com.ruoyi.sales.pojo.ShippingInfo;
+import com.ruoyi.stock.mapper.StockOutRecordMapper;
+import com.ruoyi.stock.pojo.StockOutRecord;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@SpringBootTest
+public class StockOutRecordBatchUpdateTest {
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    private StockOutRecordMapper stockOutRecordMapper;
+
+    @Autowired
+    private PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
+
+    @Autowired
+    private ShippingInfoMapper shippingInfoMapper;
+
+    @Autowired
+    private SalesLedgerMapper salesLedgerMapper;
+
+    @Autowired
+    private ProductionOrderPickMapper productionOrderPickMapper;
+
+    @Autowired
+    private ProductionOrderMapper productionOrderMapper;
+
+    /**
+     * 鏇存柊鍑哄簱鍗曟椂闂�
+     */
+    @Test
+    void testUpdateStockOutRecords() {
+        List<StockOutRecord> allRecords = stockOutRecordMapper.selectList(null);
+        System.out.println("鎬昏褰曟暟: " + allRecords.size());
+
+        List<RecordWithDate> recordWithDates = new ArrayList<>();
+
+        for (StockOutRecord record : allRecords) {
+            String type = record.getRecordType();
+            if (type == null) continue;
+
+            switch (type) {
+                case "1":
+                case "10":
+                    recordWithDates.add(resolveType1or10(record, type));
+                    break;
+                case "3":
+                    System.out.println("绫诲瀷3锛堢敓浜ф姤宸�-鍑哄簱锛夐鐣欏垎鏀紝褰撳墠鏃犳暟鎹紝璺宠繃");
+                    break;
+                case "8":
+                    System.out.println("绫诲瀷8锛堥攢鍞�-鍑哄簱锛夐鐣欏垎鏀紝褰撳墠鏃犳暟鎹紝璺宠繃");
+                    break;
+                case "9":
+                    recordWithDates.add(resolveType9(record));
+                    break;
+                case "13":
+                    recordWithDates.add(resolveType13(record));
+                    break;
+                case "14":
+                    recordWithDates.add(resolveType14or15(record));
+                    break;
+                case "15":
+                    recordWithDates.add(resolveType14or15(record));
+                    break;
+                default:
+                    System.out.println("鏈煡绫诲瀷 " + type + "锛岃烦杩� ID=" + record.getId());
+            }
+        }
+
+        Map<LocalDate, List<RecordWithDate>> byDate = recordWithDates.stream()
+                .collect(Collectors.groupingBy(
+                        rwd -> rwd.dateTime.toLocalDate(),
+                        LinkedHashMap::new,
+                        Collectors.toList()
+                ));
+
+        int totalUpdated = 0;
+        for (Map.Entry<LocalDate, List<RecordWithDate>> dateEntry : byDate.entrySet()) {
+            LocalDate date = dateEntry.getKey();
+            List<RecordWithDate> records = dateEntry.getValue();
+            records.sort(Comparator.comparing(rwd -> rwd.dateTime));
+
+            assignRandomTimes(records, date);
+
+            records.sort(Comparator.comparing(rwd -> rwd.dateTime));
+
+            int seq = 1;
+            for (RecordWithDate rwd : records) {
+                StockOutRecord record = rwd.record;
+                String type = record.getRecordType();
+                String batchNo = "CK" + date.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%03d", seq++);
+                LocalDateTime createTime = rwd.dateTime;
+
+                jdbcTemplate.update(
+                        "UPDATE stock_out_record SET outbound_batches = ?, create_time = ?, update_time = ? WHERE id = ?",
+                        batchNo, createTime, createTime, record.getId());
+
+                System.out.println("ID=" + record.getId()
+                        + " type=" + type
+                        + " batch=" + batchNo
+                        + " time=" + createTime);
+                totalUpdated++;
+            }
+        }
+
+        System.out.println("鍏ㄩ儴瀹屾垚锛佸叡鏇存柊 " + totalUpdated + " 鏉¤褰�");
+    }
+
+    private void assignRandomTimes(List<RecordWithDate> records, LocalDate date) {
+        List<RecordWithDate> individual = new ArrayList<>();
+        Map<Long, List<RecordWithDate>> grouped = new LinkedHashMap<>();
+
+        for (RecordWithDate rwd : records) {
+            String type = rwd.record.getRecordType();
+            if (!"1".equals(type) && !"9".equals(type) && !"10".equals(type)
+                    && !"13".equals(type) && !"14".equals(type) && !"15".equals(type)) {
+                continue;
+            }
+            if (("14".equals(type) || "15".equals(type)) && rwd.record.getRecordId() != null) {
+                grouped.computeIfAbsent(rwd.record.getRecordId(), k -> new ArrayList<>()).add(rwd);
+            } else {
+                individual.add(rwd);
+            }
+        }
+
+        List<TimeSlot> slots = new ArrayList<>();
+        for (RecordWithDate rwd : individual) {
+            slots.add(new TimeSlot(rwd.record.getId(), List.of(rwd)));
+        }
+        for (Map.Entry<Long, List<RecordWithDate>> entry : grouped.entrySet()) {
+            long minId = entry.getValue().stream()
+                    .mapToLong(rwd -> rwd.record.getId())
+                    .min().orElse(0);
+            slots.add(new TimeSlot(minId, entry.getValue()));
+        }
+
+        if (slots.isEmpty()) return;
+
+        slots.sort(Comparator.comparingLong(s -> s.sortKey));
+
+        int totalMinutes = 480;
+        for (int i = 0; i < slots.size(); i++) {
+            int offsetMinutes = (int) ((long) i * totalMinutes / slots.size());
+            int jitter = (int) (Math.random() * 6 - 3);
+            offsetMinutes = Math.max(0, Math.min(479, offsetMinutes + jitter));
+
+            LocalTime time;
+            if (offsetMinutes < 240) {
+                time = LocalTime.of(8, 0).plusMinutes(offsetMinutes);
+            } else {
+                time = LocalTime.of(14, 0).plusMinutes(offsetMinutes - 240);
+            }
+            LocalDateTime dt = LocalDateTime.of(date, time);
+            for (RecordWithDate rwd : slots.get(i).members) {
+                rwd.dateTime = dt;
+            }
+        }
+    }
+
+    private static class TimeSlot {
+        final long sortKey;
+        final List<RecordWithDate> members;
+
+        TimeSlot(long sortKey, List<RecordWithDate> members) {
+            this.sortKey = sortKey;
+            this.members = members;
+        }
+    }
+
+    private RecordWithDate resolveType1or10(StockOutRecord record, String type) {
+        LocalDate date = record.getCreateTime() != null
+                ? record.getCreateTime().toLocalDate()
+                : LocalDate.now();
+        return new RecordWithDate(record, LocalDateTime.of(date, LocalTime.of(8, 0)));
+    }
+
+    private RecordWithDate resolveType9(StockOutRecord record) {
+        Long recordId = record.getRecordId();
+        if (recordId != null) {
+            PurchaseReturnOrders pro = purchaseReturnOrdersMapper.selectById(recordId);
+            if (pro != null && pro.getPreparedAt() != null) {
+                return new RecordWithDate(record, LocalDateTime.of(pro.getPreparedAt(), LocalTime.of(8, 0)));
+            }
+        }
+        return fallbackDateTime(record);
+    }
+
+    private RecordWithDate resolveType13(StockOutRecord record) {
+        Long recordId = record.getRecordId();
+        if (recordId != null) {
+            ShippingInfo shippingInfo = shippingInfoMapper.selectById(recordId);
+            if (shippingInfo != null && shippingInfo.getSalesLedgerId() != null) {
+                SalesLedger salesLedger = salesLedgerMapper.selectById(shippingInfo.getSalesLedgerId());
+                if (salesLedger != null && salesLedger.getDeliveryDate() != null) {
+                    return new RecordWithDate(record, LocalDateTime.of(salesLedger.getDeliveryDate(), LocalTime.of(8, 0)));
+                }
+            }
+        }
+        return fallbackDateTime(record);
+    }
+
+    private RecordWithDate resolveType14or15(StockOutRecord record) {
+        Long recordId = record.getRecordId();
+        if (recordId != null) {
+            ProductionOrderPick pick = productionOrderPickMapper.selectById(recordId);
+            if (pick != null && pick.getProductionOrderId() != null) {
+                ProductionOrder order = productionOrderMapper.selectById(pick.getProductionOrderId());
+                if (order != null && order.getStartTime() != null) {
+                    return new RecordWithDate(record, LocalDateTime.of(order.getStartTime().toLocalDate(), LocalTime.of(8, 0)));
+                }
+            }
+        }
+        return fallbackDateTime(record);
+    }
+
+    private RecordWithDate fallbackDateTime(StockOutRecord record) {
+        return record.getCreateTime() != null
+                ? new RecordWithDate(record, record.getCreateTime())
+                : new RecordWithDate(record, LocalDateTime.now());
+    }
+
+    private static class RecordWithDate {
+        final StockOutRecord record;
+        LocalDateTime dateTime;
+
+        RecordWithDate(StockOutRecord record, LocalDateTime dateTime) {
+            this.record = record;
+            this.dateTime = dateTime;
+        }
+    }
+}

--
Gitblit v1.9.3