昨天 684e4306a08feeae188070f264148e6765da8dcf
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -105,6 +105,11 @@
     */
    private static final Integer LEDGER_TYPE_PURCHASE = 2;
    /**
     * 备件流水来源:采购入库。见 spare_parts_record.source_type
     */
    private static final Integer SPARE_PARTS_SOURCE_PURCHASE = 1;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -719,10 +724,13 @@
                salesLedgerProduct.setRegisterDate(localDateTime);
                salesLedgerProductMapper.insert(salesLedgerProduct);
            }
            // 仅新增明细回写备件,编辑走 update 分支天然不重复计数
            if (LEDGER_TYPE_PURCHASE.equals(ledgerType)) {
                recordSparePartsInbound(insertList, purchaseLedger);
            }
        }
        // 采购明细回写备件:先按台账回滚上一次的流水,再按当前明细整体重建。
        // 编辑时明细行可能被移除,只认新增行会漏掉移除的那些;整体重建天然与当前明细对齐。
        // 注意这段必须在 insertList 之外,否则「只删明细、不新增」的编辑不会触发回滚
        if (LEDGER_TYPE_PURCHASE.equals(ledgerType)) {
            rollbackSparePartsInbound(salesLedgerId);
            recordSparePartsInbound(products, purchaseLedger);
        }
        // 计算总含税金额
@@ -739,11 +747,54 @@
    }
    /**
     * 采购台账新增明细按规格匹配备件:累加备件库存并记一条入库流水。
     * 未维护规格的备件不参与,静默跳过。
     * 按采购台账回滚备件:把该台账产生的入库流水逐备件减回库存,然后删除这些流水。
     *
     * <p>备件先由采购入账、后续被维修/保养领用出账,若入账量已经被领用掉,减回会出现负库存,
     * 此时直接拦住不让删,避免把备件账做花。删除台账与编辑重建都走这里,口径统一。</p>
     */
    private void recordSparePartsInbound(List<SalesLedgerProduct> insertList, PurchaseLedger purchaseLedger) {
        List<Long> modelIds = insertList.stream()
    private void rollbackSparePartsInbound(Long ledgerId) {
        List<SparePartsRecord> records = sparePartsRecordService.list(new LambdaQueryWrapper<SparePartsRecord>()
                .eq(SparePartsRecord::getSourceType, SPARE_PARTS_SOURCE_PURCHASE)
                .eq(SparePartsRecord::getSourceId, ledgerId));
        if (CollectionUtils.isEmpty(records)) {
            return;
        }
        Map<Long, BigDecimal> rollbackBySparePartsId = new HashMap<>();
        for (SparePartsRecord record : records) {
            BigDecimal quantity = record.getQuantity() == null ? BigDecimal.ZERO : record.getQuantity();
            rollbackBySparePartsId.merge(record.getSparePartsId(), quantity, BigDecimal::add);
        }
        for (Map.Entry<Long, BigDecimal> entry : rollbackBySparePartsId.entrySet()) {
            if (entry.getKey() == null) {
                continue;
            }
            SpareParts spareParts = sparePartsMapper.selectById(entry.getKey());
            if (spareParts == null) {
                continue;
            }
            BigDecimal current = spareParts.getQuantity() == null ? BigDecimal.ZERO : spareParts.getQuantity();
            if (current.compareTo(entry.getValue()) < 0) {
                throw new BaseException("备件「" + spareParts.getName() + "」当前库存 " + current
                        + ",不足以回滚本次采购入库的 " + entry.getValue() + ",请先处理领用记录");
            }
            spareParts.setQuantity(current.subtract(entry.getValue()));
            sparePartsMapper.updateById(spareParts);
        }
        sparePartsRecordService.removeByIds(records.stream()
                .map(SparePartsRecord::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
    }
    /**
     * 采购台账明细按规格匹配备件:累加备件库存并记一条入库流水。
     * 未维护规格的备件不参与,静默跳过。
     *
     * <p>调用前必须先 {@link #rollbackSparePartsInbound(Long)},两者成对出现:
     * 回滚旧流水 + 按当前明细重建,才能保证编辑台账后备件数量与明细一致。</p>
     */
    private void recordSparePartsInbound(List<SalesLedgerProduct> products, PurchaseLedger purchaseLedger) {
        List<Long> modelIds = products.stream()
                .map(SalesLedgerProduct::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
@@ -758,7 +809,7 @@
        }
        Map<Long, SpareParts> sparePartsByModelId = sparePartsList.stream()
                .collect(Collectors.toMap(SpareParts::getProductModelId, s -> s, (a, b) -> a));
        for (SalesLedgerProduct product : insertList) {
        for (SalesLedgerProduct product : products) {
            SpareParts spareParts = sparePartsByModelId.get(product.getProductModelId());
            if (spareParts == null || product.getQuantity() == null
                    || product.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -790,6 +841,9 @@
            if (purchaseLedger.getApprovalStatus().equals(3)) {
                throw new BaseException(purchaseLedger.getPurchaseContractNumber()+"已经审批通过,不允许删除");
            }
            // 台账删除要把该台账采购入库时累加到备件上的数量减回去,并删掉对应流水,
            // 否则备件库存虚高、流水残留。备件已领用导致减回为负时会在这里拦住
            rollbackSparePartsInbound(id);
        }
        // 批量删除关联的采购入库记录
        LambdaQueryWrapper<SalesLedgerProduct> salesLedgerProductQueryWrapper = new LambdaQueryWrapper<>();