111
gongchunyi
5 天以前 2ff409739ff1d4d14acb6784619679117df4da33
111
已添加5个文件
已修改15个文件
1507 ■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260523_新增入库记录表的预警数量.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApproveProcessMapper.xml 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/approve/service/impl/ApproveProcessIdModifyTest.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/device/service/impl/MaintenanceTaskJobTest.java 448 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJobTest.java 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -6,6 +6,7 @@
!gradle/wrapper/gradle-wrapper.jar
claude.md
target/
test/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
doc/20260523_ÐÂÔöÈë¿â¼Ç¼±íµÄÔ¤¾¯ÊýÁ¿.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
alter table stock_in_record
    add warn_num decimal(16, 4) null comment '预警数量';
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());
        };
    }
    // æ¯æ—¥ä»»åŠ¡è½¬æ¢
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;
}
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;
}
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;
}
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;
}
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不合格入库")
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
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>
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>
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>
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,
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=")">
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
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
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()));
    }
}
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]); // å­£åº¦ä¸­çš„第几个月
        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());
    }
}
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("自动生成自定时任务ID: " + 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);
    }
}
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;
        }
    }
}