From a3c54410de03f0fd242e1a1118d6471300cf1eda Mon Sep 17 00:00:00 2001
From: zss <zss@example.com>
Date: 星期四, 30 四月 2026 17:36:25 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro

---
 src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java |  903 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 901 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
index b4e3826..72a237e 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -1,20 +1,919 @@
 package com.ruoyi.production.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
+import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
+import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
+import com.ruoyi.production.mapper.ProductionOrderMapper;
 import com.ruoyi.production.mapper.ProductionOrderPickMapper;
+import com.ruoyi.production.mapper.ProductionOrderPickRecordMapper;
+import com.ruoyi.production.pojo.ProductionOrder;
 import com.ruoyi.production.pojo.ProductionOrderPick;
+import com.ruoyi.production.pojo.ProductionOrderPickRecord;
 import com.ruoyi.production.service.ProductionOrderPickService;
+import com.ruoyi.stock.dto.StockInventoryDto;
+import com.ruoyi.stock.mapper.StockInventoryMapper;
+import com.ruoyi.stock.pojo.StockInventory;
+import com.ruoyi.stock.service.StockInventoryService;
+import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * <p>
- * 璁㈠崟棰嗘枡绾胯竟浠� 鏈嶅姟瀹炵幇绫�
+ * 鐠併垹宕熸0鍡樻灐缁捐儻绔熸禒?閺堝秴濮熺�圭偟骞囩猾?
  * </p>
  *
- * @author 鑺杞欢锛堟睙鑻忥級鏈夐檺鍏徃
+ * @author 閼侯垰顕辨潪顖欐閿涘牊鐫欓懟蹇ョ礆閺堝妾洪崗顒�寰�
  * @since 2026-04-21 03:55:52
  */
 @Service
+@RequiredArgsConstructor
 public class ProductionOrderPickServiceImpl extends ServiceImpl<ProductionOrderPickMapper, ProductionOrderPick> implements ProductionOrderPickService {
 
+    private static final byte PICK_TYPE_NORMAL = 1;
+    private static final byte PICK_TYPE_FEEDING = 2;
+
+    private final ProductionOrderMapper productionOrderMapper;
+    private final ProductionOperationTaskMapper productionOperationTaskMapper;
+    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
+    private final StockInventoryMapper stockInventoryMapper;
+    private final StockInventoryService stockInventoryService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean savePick(ProductionOrderPickDto dto) {
+        List<ProductionOrderPickDto> pickItems = resolvePickItems(dto);
+        for (int i = 0; i < pickItems.size(); i++) {
+            int rowNo = i + 1;
+            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
+            validatePickParam(resolvedDto, rowNo);
+
+            List<String> batchNoList = resolveBatchNoList(resolvedDto);
+            String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
+            String storedBatchNo = formatBatchNoStorage(batchNoList);
+            subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
+
+            ProductionOrderPick orderPick = new ProductionOrderPick();
+            orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
+            orderPick.setProductModelId(resolvedDto.getProductModelId());
+            orderPick.setBatchNo(storedBatchNo);
+            orderPick.setQuantity(resolvedDto.getPickQuantity());
+            orderPick.setRemark(resolvedDto.getRemark());
+            orderPick.setOperationName(resolvedDto.getOperationName());
+            orderPick.setTechnologyOperationId(resolvedDto.getTechnologyOperationId());
+            orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity());
+            orderPick.setBom(resolvedDto.getBom());
+            orderPick.setReturned(false);
+            baseMapper.insert(orderPick);
+
+            insertPickRecord(orderPick.getId(),
+                    resolvedDto.getProductionOrderId(),
+                    resolvedDto.getProductionOperationTaskId(),
+                    resolvedDto.getProductModelId(),
+                    inventoryBatchNo,
+                    resolvedDto.getPickQuantity(),
+                    BigDecimal.ZERO,
+                    resolvedDto.getPickQuantity(),
+                    resolvedDto.getPickType(),
+                    resolvedDto.getRemark(),
+                    resolvedDto.getFeedingReason());
+        }
+        return true;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updatePick(ProductionOrderPickDto dto) {
+        if (dto == null) {
+            throw new ServiceException("鍙樻洿鍙傛暟涓嶈兘涓虹┖");
+        }
+        Long productionOrderId = resolveProductionOrderId(dto);
+        if (productionOrderId == null) {
+            throw new ServiceException("鐢熶骇璁㈠崟ID涓嶈兘涓虹┖");
+        }
+        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
+        if (productionOrder == null) {
+            throw new ServiceException("鐢熶骇璁㈠崟涓嶅瓨鍦�");
+        }
+
+        List<ProductionOrderPick> existingPickList = baseMapper.selectList(
+                Wrappers.<ProductionOrderPick>lambdaQuery()
+                        .eq(ProductionOrderPick::getProductionOrderId, productionOrderId));
+        Map<Long, ProductionOrderPick> existingPickMap = existingPickList.stream()
+                .filter(item -> item.getId() != null)
+                .collect(Collectors.toMap(ProductionOrderPick::getId, Function.identity(), (a, b) -> a));
+
+        if (isFeedingRequest(dto)) {
+            processFeedingPickItems(dto, existingPickMap, productionOrderId);
+            return true;
+        }
+        if (isReturnRequest(dto)) {
+            processReturnPickItems(dto, existingPickMap, productionOrderId);
+            return true;
+        }
+
+        processDeletePickIds(dto, existingPickMap, productionOrderId);
+
+        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(dto);
+        Set<Long> keepPickIdSet = new HashSet<>();
+        for (int i = 0; i < pickItems.size(); i++) {
+            int rowNo = i + 1;
+            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
+            if (isEmptyUpdateItem(resolvedDto)) {
+                continue;
+            }
+            if (resolvedDto.getProductionOrderId() == null) {
+                resolvedDto.setProductionOrderId(productionOrderId);
+            }
+            validatePickParam(resolvedDto, rowNo);
+
+            if (resolvedDto.getId() == null) {
+                addNewPickInUpdate(resolvedDto, rowNo);
+                continue;
+            }
+            keepPickIdSet.add(resolvedDto.getId());
+            updateExistingPick(resolvedDto, rowNo, existingPickMap);
+        }
+        processMissingPickItems(dto, existingPickMap, productionOrderId, keepPickIdSet);
+        return true;
+    }
+
+    @Override
+    public List<ProductionOrderPickVo> listPickedDetail(Long productionOrderId) {
+        if (productionOrderId == null) {
+            return Collections.emptyList();
+        }
+        List<ProductionOrderPickVo> detailList = baseMapper.listPickedDetailByOrderId(productionOrderId);
+        fillBatchNoList(detailList);
+        fillSelectableBatchNoList(detailList);
+        return detailList;
+    }
+
+    private void processDeletePickIds(ProductionOrderPickDto rootDto,
+                                      Map<Long, ProductionOrderPick> existingPickMap,
+                                      Long productionOrderId) {
+        if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) {
+            return;
+        }
+        Set<Long> deleteIdSet = new LinkedHashSet<>(rootDto.getDeletePickIds());
+        for (Long deleteId : deleteIdSet) {
+            if (deleteId == null) {
+                continue;
+            }
+            ProductionOrderPick existingPick = existingPickMap.get(deleteId);
+            if (existingPick == null || !Objects.equals(existingPick.getProductionOrderId(), productionOrderId)) {
+                throw new ServiceException("瑕佸垹闄ょ殑棰嗘枡璁板綍涓嶅瓨鍦ㄦ垨涓嶅睘浜庡綋鍓嶈鍗曪紝ID=" + deleteId);
+            }
+            String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo());
+            BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity());
+            addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity);
+            int affected = baseMapper.deleteById(deleteId);
+            if (affected <= 0) {
+                throw new ServiceException("鍒犻櫎棰嗘枡澶辫触锛孖D=" + deleteId);
+            }
+            insertPickRecord(existingPick.getId(),
+                    existingPick.getProductionOrderId(),
+                    rootDto.getProductionOperationTaskId(),
+                    existingPick.getProductModelId(),
+                    oldBatchNo,
+                    oldQuantity,
+                    oldQuantity,
+                    BigDecimal.ZERO,
+                    rootDto.getPickType(),
+                    rootDto.getRemark(),
+                    rootDto.getFeedingReason());
+            existingPickMap.remove(deleteId);
+        }
+    }
+
+    private void processMissingPickItems(ProductionOrderPickDto rootDto,
+                                         Map<Long, ProductionOrderPick> existingPickMap,
+                                         Long productionOrderId,
+                                         Set<Long> keepPickIdSet) {
+        if (rootDto.getPickList() == null) {
+            return;
+        }
+        List<ProductionOrderPick> missingPickList = existingPickMap.values().stream()
+                .filter(Objects::nonNull)
+                .filter(item -> item.getId() != null)
+                .filter(item -> Objects.equals(item.getProductionOrderId(), productionOrderId))
+                .filter(item -> !keepPickIdSet.contains(item.getId()))
+                .toList();
+        for (ProductionOrderPick missingPick : missingPickList) {
+            String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo());
+            BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity());
+            addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity);
+            int affected = baseMapper.deleteById(missingPick.getId());
+            if (affected <= 0) {
+                throw new ServiceException("鍒犻櫎棰嗘枡澶辫触锛孖D=" + missingPick.getId());
+            }
+            insertPickRecord(missingPick.getId(),
+                    missingPick.getProductionOrderId(),
+                    rootDto.getProductionOperationTaskId(),
+                    missingPick.getProductModelId(),
+                    oldBatchNo,
+                    oldQuantity,
+                    oldQuantity,
+                    BigDecimal.ZERO,
+                    rootDto.getPickType(),
+                    rootDto.getRemark(),
+                    rootDto.getFeedingReason());
+            existingPickMap.remove(missingPick.getId());
+        }
+    }
+
+    private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) {
+        List<String> batchNoList = resolveBatchNoList(dto);
+        String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
+        String storedBatchNo = formatBatchNoStorage(batchNoList);
+        subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo);
+
+        ProductionOrderPick orderPick = new ProductionOrderPick();
+        orderPick.setProductionOrderId(dto.getProductionOrderId());
+        orderPick.setProductModelId(dto.getProductModelId());
+        orderPick.setBatchNo(storedBatchNo);
+        orderPick.setQuantity(dto.getPickQuantity());
+        orderPick.setRemark(dto.getRemark());
+        orderPick.setOperationName(dto.getOperationName());
+        orderPick.setTechnologyOperationId(dto.getTechnologyOperationId());
+        orderPick.setDemandedQuantity(dto.getDemandedQuantity());
+        orderPick.setBom(dto.getBom());
+        orderPick.setReturned(false);
+        baseMapper.insert(orderPick);
+
+        insertPickRecord(orderPick.getId(),
+                dto.getProductionOrderId(),
+                dto.getProductionOperationTaskId(),
+                dto.getProductModelId(),
+                inventoryBatchNo,
+                dto.getPickQuantity(),
+                BigDecimal.ZERO,
+                dto.getPickQuantity(),
+                dto.getPickType(),
+                dto.getRemark(),
+                dto.getFeedingReason());
+    }
+
+    private void processFeedingPickItems(ProductionOrderPickDto rootDto,
+                                         Map<Long, ProductionOrderPick> existingPickMap,
+                                         Long productionOrderId) {
+        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
+        for (int i = 0; i < pickItems.size(); i++) {
+            int rowNo = i + 1;
+            ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
+            if (isEmptyUpdateItem(resolvedDto)) {
+                continue;
+            }
+            if (!isFeedingPick(resolvedDto)) {
+                throw new ServiceException("琛ユ枡璇锋眰涓殑棰嗘枡绫诲瀷蹇呴』鍏ㄩ儴涓�2");
+            }
+            if (resolvedDto.getProductionOrderId() == null) {
+                resolvedDto.setProductionOrderId(productionOrderId);
+            }
+            validateFeedingParam(resolvedDto, rowNo);
+
+            ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
+            if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
+                throw new ServiceException("绗�" + rowNo + "鏉¢鏂欒褰曚笉瀛樺湪鎴栦笉灞炰簬褰撳墠璁㈠崟");
+            }
+            addFeedingPick(resolvedDto, oldPick, rowNo);
+        }
+    }
+
+    private void addFeedingPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
+        if (dto.getProductModelId() != null && !Objects.equals(dto.getProductModelId(), oldPick.getProductModelId())) {
+            throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欎骇鍝佽鏍间笌棰嗘枡璁板綍涓嶄竴鑷�");
+        }
+        Long productModelId = oldPick.getProductModelId();
+        List<String> batchNoList = resolveBatchNoList(dto);
+        String inventoryBatchNo = batchNoList.isEmpty()
+                ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo())
+                : formatBatchNoStorage(batchNoList);
+        BigDecimal feedingQuantity = dto.getFeedingQuantity();
+
+        subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
+
+        BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId());
+        BigDecimal afterFeedingQty = beforeFeedingQty.add(feedingQuantity);
+        insertPickRecord(oldPick.getId(),
+                dto.getProductionOrderId(),
+                dto.getProductionOperationTaskId(),
+                productModelId,
+                inventoryBatchNo,
+                feedingQuantity,
+                beforeFeedingQty,
+                afterFeedingQty,
+                PICK_TYPE_FEEDING,
+                dto.getRemark(),
+                dto.getFeedingReason());
+
+        ProductionOrderPick updatePick = new ProductionOrderPick();
+        updatePick.setId(oldPick.getId());
+        updatePick.setFeedingQty(afterFeedingQty);
+        updatePick.setActualQty(calculateActualQty(oldPick, afterFeedingQty));
+        int affected = baseMapper.updateById(updatePick);
+        if (affected <= 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欐�婚噺鏇存柊澶辫触");
+        }
+        oldPick.setFeedingQty(afterFeedingQty);
+        oldPick.setActualQty(updatePick.getActualQty());
+    }
+
+    private void processReturnPickItems(ProductionOrderPickDto rootDto,
+                                        Map<Long, ProductionOrderPick> existingPickMap,
+                                        Long productionOrderId) {
+        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
+        for (int i = 0; i < pickItems.size(); i++) {
+            int rowNo = i + 1;
+            ProductionOrderPickDto resolvedDto = mergeDto(rootDto, pickItems.get(i));
+            if (isEmptyUpdateItem(resolvedDto)) {
+                continue;
+            }
+            if (resolvedDto.getProductionOrderId() == null) {
+                resolvedDto.setProductionOrderId(productionOrderId);
+            }
+            validateReturnParam(resolvedDto, rowNo);
+
+            ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
+            if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
+                throw new ServiceException("绗�" + rowNo + "鏉¢鏂欒褰曚笉瀛樺湪鎴栦笉灞炰簬褰撳墠璁㈠崟");
+            }
+            updateReturnPick(resolvedDto, oldPick, rowNo);
+        }
+    }
+
+    private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
+        ProductionOrderPick updatePick = new ProductionOrderPick();
+        updatePick.setId(oldPick.getId());
+        updatePick.setReturnQty(dto.getReturnQty());
+        updatePick.setActualQty(dto.getActualQty());
+        updatePick.setReturned(true);
+        int affected = baseMapper.updateById(updatePick);
+        if (affected <= 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢��鏂欎俊鎭洿鏂板け璐�");
+        }
+        oldPick.setReturnQty(updatePick.getReturnQty());
+        oldPick.setActualQty(updatePick.getActualQty());
+        oldPick.setReturned(true);
+    }
+
+    private void updateExistingPick(ProductionOrderPickDto dto,
+                                    int rowNo,
+                                    Map<Long, ProductionOrderPick> existingPickMap) {
+        ProductionOrderPick oldPick = existingPickMap.get(dto.getId());
+        if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), dto.getProductionOrderId())) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欒褰曚笉瀛樺湪鎴栦笉灞炰簬褰撳墠璁㈠崟");
+        }
+
+        Long oldProductModelId = oldPick.getProductModelId();
+        String oldBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo());
+        BigDecimal oldQuantity = defaultDecimal(oldPick.getQuantity());
+
+        Long newProductModelId = dto.getProductModelId();
+        List<String> newBatchNoList = resolveBatchNoList(dto);
+        String newBatchNo = pickInventoryBatchNo(newBatchNoList);
+        String newStoredBatchNo = formatBatchNoStorage(newBatchNoList);
+        BigDecimal newQuantity = dto.getPickQuantity();
+
+        boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId)
+                && Objects.equals(oldBatchNo, newBatchNo);
+        if (sameStockKey) {
+            BigDecimal delta = newQuantity.subtract(oldQuantity);
+            if (delta.compareTo(BigDecimal.ZERO) > 0) {
+                subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo);
+            } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
+                addInventory(oldProductModelId, oldBatchNo, delta.abs());
+            }
+        } else {
+            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
+            subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
+        }
+
+        oldPick.setProductModelId(newProductModelId);
+        oldPick.setBatchNo(newStoredBatchNo);
+        oldPick.setQuantity(newQuantity);
+        oldPick.setRemark(dto.getRemark());
+        oldPick.setOperationName(dto.getOperationName());
+        oldPick.setTechnologyOperationId(dto.getTechnologyOperationId());
+        if (dto.getDemandedQuantity() != null) {
+            oldPick.setDemandedQuantity(dto.getDemandedQuantity());
+        }
+        if (dto.getBom() != null) {
+            oldPick.setBom(dto.getBom());
+        }
+        int affected = baseMapper.updateById(oldPick);
+        if (affected <= 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐洿鏂板け璐�");
+        }
+
+        BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity;
+        if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) {
+            insertPickRecord(oldPick.getId(),
+                    dto.getProductionOrderId(),
+                    dto.getProductionOperationTaskId(),
+                    newProductModelId,
+                    newBatchNo,
+                    recordQuantity,
+                    oldQuantity,
+                    newQuantity,
+                    dto.getPickType(),
+                    dto.getRemark(),
+                    dto.getFeedingReason());
+        }
+    }
+
+    private void insertPickRecord(Long pickId,
+                                  Long productionOrderId,
+                                  Long productionOperationTaskId,
+                                  Long productModelId,
+                                  String batchNo,
+                                  BigDecimal pickQuantity,
+                                  BigDecimal beforeQuantity,
+                                  BigDecimal afterQuantity,
+                                  Byte pickType,
+                                  String remark,
+                                  String feedingReason) {
+        ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord();
+        pickRecord.setPickId(pickId);
+        pickRecord.setProductionOrderId(productionOrderId);
+        pickRecord.setProductionOperationTaskId(productionOperationTaskId);
+        pickRecord.setProductModelId(productModelId);
+        pickRecord.setBatchNo(batchNo);
+        pickRecord.setPickQuantity(defaultDecimal(pickQuantity));
+        pickRecord.setBeforeQuantity(defaultDecimal(beforeQuantity));
+        pickRecord.setAfterQuantity(defaultDecimal(afterQuantity));
+        pickRecord.setPickType(pickType == null ? PICK_TYPE_NORMAL : pickType);
+        pickRecord.setRemark(remark);
+        pickRecord.setFeedingReason(feedingReason);
+        productionOrderPickRecordMapper.insert(pickRecord);
+    }
+
+    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
+        BigDecimal deductQuantity = defaultDecimal(quantity);
+        if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        List<String> batchNoList = parseBatchNoValue(batchNo);
+        if (batchNoList.isEmpty()) {
+            batchNoList = Collections.singletonList(null);
+        }
+
+        Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>();
+        BigDecimal totalAvailableQuantity = BigDecimal.ZERO;
+        for (String currentBatchNo : batchNoList) {
+            StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo));
+            BigDecimal availableQuantity = BigDecimal.ZERO;
+            if (stockInventory != null) {
+                availableQuantity = defaultDecimal(stockInventory.getQualitity())
+                        .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
+                if (availableQuantity.compareTo(BigDecimal.ZERO) < 0) {
+                    availableQuantity = BigDecimal.ZERO;
+                }
+            }
+            availableQuantityMap.put(currentBatchNo, availableQuantity);
+            totalAvailableQuantity = totalAvailableQuantity.add(availableQuantity);
+        }
+
+        if (deductQuantity.compareTo(totalAvailableQuantity) > 0) {
+            BigDecimal shortQuantity = deductQuantity.subtract(totalAvailableQuantity);
+            throw new ServiceException("棰嗘枡鍙敤搴撳瓨涓嶈冻锛屽彲鐢ㄥ簱瀛樹负" + formatQuantity(totalAvailableQuantity)
+                    + "锛岃繕宸�" + formatQuantity(shortQuantity));
+        }
+
+        BigDecimal remainingQuantity = deductQuantity;
+        for (Map.Entry<String, BigDecimal> entry : availableQuantityMap.entrySet()) {
+            if (remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+                break;
+            }
+            BigDecimal availableQuantity = defaultDecimal(entry.getValue());
+            if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity);
+            StockInventoryDto stockInventoryDto = new StockInventoryDto();
+            stockInventoryDto.setProductModelId(productModelId);
+            stockInventoryDto.setBatchNo(entry.getKey());
+            stockInventoryDto.setQualitity(currentDeductQuantity);
+            int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
+            if (affected <= 0) {
+                throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐墸鍑忓簱瀛樺け璐�");
+            }
+            remainingQuantity = remainingQuantity.subtract(currentDeductQuantity);
+        }
+
+        if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐墸鍑忓簱瀛樺け璐ワ紝鍓╀綑寰呮墸鍑忔暟閲忎负" + formatQuantity(remainingQuantity));
+        }
+    }
+
+    private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) {
+        BigDecimal addQuantity = defaultDecimal(quantity);
+        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+        StockInventoryDto stockInventoryDto = new StockInventoryDto();
+        stockInventoryDto.setProductModelId(productModelId);
+        stockInventoryDto.setBatchNo(batchNo);
+        stockInventoryDto.setQualitity(addQuantity);
+        stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode()));
+        stockInventoryDto.setRecordId(0L);
+        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
+    }
+
+    private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
+        if (dto == null) {
+            throw new ServiceException("棰嗘枡鍙傛暟涓嶈兘涓虹┖");
+        }
+        if (dto.getPickList() != null && !dto.getPickList().isEmpty()) {
+            return dto.getPickList();
+        }
+        return Collections.singletonList(dto);
+    }
+
+    private List<ProductionOrderPickDto> resolveUpdateItems(ProductionOrderPickDto dto) {
+        if (dto.getPickList() != null) {
+            return dto.getPickList();
+        }
+        if (isEmptyUpdateItem(dto)) {
+            return Collections.emptyList();
+        }
+        return Collections.singletonList(dto);
+    }
+
+    private boolean isEmptyUpdateItem(ProductionOrderPickDto dto) {
+        return dto.getId() == null
+                && dto.getProductModelId() == null
+                && dto.getPickQuantity() == null
+                && StringUtils.isEmpty(dto.getBatchNo())
+                && (dto.getBatchNoList() == null || dto.getBatchNoList().isEmpty())
+                && dto.getPickType() == null
+                && dto.getFeedingQuantity() == null
+                && StringUtils.isEmpty(dto.getFeedingReason())
+                && dto.getReturnQty() == null
+                && dto.getActualQty() == null
+                && dto.getReturned() == null
+                && dto.getProductionOperationTaskId() == null
+                && dto.getTechnologyOperationId() == null
+                && StringUtils.isEmpty(dto.getOperationName())
+                && dto.getDemandedQuantity() == null
+                && dto.getBom() == null
+                && StringUtils.isEmpty(dto.getRemark());
+    }
+
+    private Long resolveProductionOrderId(ProductionOrderPickDto dto) {
+        if (dto.getProductionOrderId() != null) {
+            return dto.getProductionOrderId();
+        }
+        if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
+            return null;
+        }
+        return dto.getPickList().stream()
+                .filter(Objects::nonNull)
+                .map(ProductionOrderPickDto::getProductionOrderId)
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElse(null);
+    }
+
+    private ProductionOrderPickDto mergeDto(ProductionOrderPickDto rootDto, ProductionOrderPickDto itemDto) {
+        ProductionOrderPickDto merged = new ProductionOrderPickDto();
+        if (itemDto != null) {
+            merged.setId(itemDto.getId());
+            merged.setProductionOrderId(itemDto.getProductionOrderId());
+            merged.setProductionOperationTaskId(itemDto.getProductionOperationTaskId());
+            merged.setProductModelId(itemDto.getProductModelId());
+            merged.setBatchNo(itemDto.getBatchNo());
+            merged.setBatchNoList(itemDto.getBatchNoList());
+            merged.setPickQuantity(itemDto.getPickQuantity());
+            merged.setPickType(itemDto.getPickType());
+            merged.setRemark(itemDto.getRemark());
+            merged.setFeedingReason(itemDto.getFeedingReason());
+            merged.setFeedingQuantity(itemDto.getFeedingQuantity());
+            merged.setTechnologyOperationId(itemDto.getTechnologyOperationId());
+            merged.setOperationName(itemDto.getOperationName());
+            merged.setDemandedQuantity(itemDto.getDemandedQuantity());
+            merged.setBom(itemDto.getBom());
+            merged.setReturnQty(itemDto.getReturnQty());
+            merged.setActualQty(itemDto.getActualQty());
+            merged.setReturned(itemDto.getReturned());
+        }
+        if (merged.getId() == null) {
+            merged.setId(rootDto.getId());
+        }
+        if (merged.getProductionOrderId() == null) {
+            merged.setProductionOrderId(rootDto.getProductionOrderId());
+        }
+        if (merged.getProductionOperationTaskId() == null) {
+            merged.setProductionOperationTaskId(rootDto.getProductionOperationTaskId());
+        }
+        if (merged.getProductModelId() == null) {
+            merged.setProductModelId(rootDto.getProductModelId());
+        }
+        if (merged.getBatchNo() == null) {
+            merged.setBatchNo(rootDto.getBatchNo());
+        }
+        if (merged.getBatchNoList() == null || merged.getBatchNoList().isEmpty()) {
+            merged.setBatchNoList(rootDto.getBatchNoList());
+        }
+        if (merged.getPickQuantity() == null) {
+            merged.setPickQuantity(rootDto.getPickQuantity());
+        }
+        if (merged.getPickType() == null) {
+            merged.setPickType(rootDto.getPickType());
+        }
+        if (merged.getRemark() == null) {
+            merged.setRemark(rootDto.getRemark());
+        }
+        if (merged.getFeedingReason() == null) {
+            merged.setFeedingReason(rootDto.getFeedingReason());
+        }
+        if (merged.getFeedingQuantity() == null) {
+            merged.setFeedingQuantity(rootDto.getFeedingQuantity());
+        }
+        if (merged.getTechnologyOperationId() == null) {
+            merged.setTechnologyOperationId(rootDto.getTechnologyOperationId());
+        }
+        if (merged.getOperationName() == null) {
+            merged.setOperationName(rootDto.getOperationName());
+        }
+        if (merged.getDemandedQuantity() == null) {
+            merged.setDemandedQuantity(rootDto.getDemandedQuantity());
+        }
+        if (merged.getBom() == null) {
+            merged.setBom(rootDto.getBom());
+        }
+        if (merged.getReturnQty() == null) {
+            merged.setReturnQty(rootDto.getReturnQty());
+        }
+        if (merged.getActualQty() == null) {
+            merged.setActualQty(rootDto.getActualQty());
+        }
+        if (merged.getReturned() == null) {
+            merged.setReturned(rootDto.getReturned());
+        }
+        return merged;
+    }
+
+    private void validatePickParam(ProductionOrderPickDto dto, int rowNo) {
+        if (dto.getProductionOrderId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉$敓浜ц鍗旾D涓嶈兘涓虹┖");
+        }
+        if (dto.getProductModelId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉′骇鍝佽鏍糏D涓嶈兘涓虹┖");
+        }
+        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐暟閲忓繀椤诲ぇ浜�0");
+        }
+        if (dto.getPickType() != null && dto.getPickType() != PICK_TYPE_NORMAL && dto.getPickType() != PICK_TYPE_FEEDING) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欑被鍨嬪彧鑳芥槸1鎴�2");
+        }
+    }
+
+    private void validateFeedingParam(ProductionOrderPickDto dto, int rowNo) {
+        if (dto.getProductionOrderId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉$敓浜ц鍗旾D涓嶈兘涓虹┖");
+        }
+        if (dto.getId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂橧D涓嶈兘涓虹┖");
+        }
+        if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉℃湰娆¤ˉ鏂欐暟閲忓繀椤诲ぇ浜�0");
+        }
+        if (!isFeedingPick(dto)) {
+            throw new ServiceException("绗�" + rowNo + "鏉¤ˉ鏂欑被鍨嬪繀椤讳负2");
+        }
+    }
+
+    private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) {
+        if (dto.getProductionOrderId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉$敓浜ц鍗旾D涓嶈兘涓虹┖");
+        }
+        if (dto.getId() == null) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂橧D涓嶈兘涓虹┖");
+        }
+        if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢��鏂欐暟閲忎笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+        }
+        if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉″疄闄呮暟閲忎笉鑳戒负绌轰笖涓嶈兘灏忎簬0");
+        }
+    }
+
+    private boolean isFeedingRequest(ProductionOrderPickDto dto) {
+        if (isFeedingPick(dto)) {
+            return true;
+        }
+        if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
+            return false;
+        }
+        return dto.getPickList().stream()
+                .filter(Objects::nonNull)
+                .anyMatch(this::isFeedingPick);
+    }
+
+    private boolean isFeedingPick(ProductionOrderPickDto dto) {
+        return dto != null && Objects.equals(dto.getPickType(), PICK_TYPE_FEEDING);
+    }
+
+    private boolean isReturnRequest(ProductionOrderPickDto dto) {
+        if (isReturnPick(dto)) {
+            return true;
+        }
+        if (dto.getPickList() == null || dto.getPickList().isEmpty()) {
+            return false;
+        }
+        return dto.getPickList().stream()
+                .filter(Objects::nonNull)
+                .anyMatch(this::isReturnPick);
+    }
+
+    private boolean isReturnPick(ProductionOrderPickDto dto) {
+        return dto != null && Boolean.TRUE.equals(dto.getReturned());
+    }
+
+    private BigDecimal sumFeedingQuantity(Long productionOrderId, Long pickId) {
+        List<ProductionOrderPickRecord> feedingRecords = productionOrderPickRecordMapper.selectList(
+                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
+                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)
+                        .eq(ProductionOrderPickRecord::getPickId, pickId)
+                        .eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_FEEDING));
+        return feedingRecords.stream()
+                .map(ProductionOrderPickRecord::getPickQuantity)
+                .map(this::defaultDecimal)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private BigDecimal calculateActualQty(ProductionOrderPick pick, BigDecimal feedingQty) {
+        return defaultDecimal(pick.getQuantity())
+                .add(defaultDecimal(feedingQty))
+                .subtract(defaultDecimal(pick.getReturnQty()));
+    }
+
+    private String normalizeBatchNo(String batchNo) {
+        if (StringUtils.isEmpty(batchNo)) {
+            return null;
+        }
+        String trimBatchNo = batchNo.trim();
+        return trimBatchNo.isEmpty() ? null : trimBatchNo;
+    }
+    private List<String> resolveBatchNoList(ProductionOrderPickDto dto) {
+        List<String> normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList());
+        if (!normalizedBatchNoList.isEmpty()) {
+            return normalizedBatchNoList;
+        }
+        return parseBatchNoValue(dto.getBatchNo());
+    }
+
+    private String pickInventoryBatchNo(List<String> batchNoList) {
+        if (batchNoList == null || batchNoList.isEmpty()) {
+            return null;
+        }
+        return batchNoList.get(0);
+    }
+
+    private String resolveInventoryBatchNoFromStored(String storedBatchNo) {
+        return pickInventoryBatchNo(parseBatchNoValue(storedBatchNo));
+    }
+
+    private String formatBatchNoStorage(List<String> batchNoList) {
+        if (batchNoList == null || batchNoList.isEmpty()) {
+            return null;
+        }
+        if (batchNoList.size() == 1) {
+            return batchNoList.get(0);
+        }
+        return String.join(",", batchNoList);
+    }
+
+    private List<String> normalizeBatchNoList(List<String> batchNoList) {
+        if (batchNoList == null || batchNoList.isEmpty()) {
+            return Collections.emptyList();
+        }
+        LinkedHashSet<String> normalizedSet = new LinkedHashSet<>();
+        for (String batchNo : batchNoList) {
+            String normalizedBatchNo = normalizeBatchNo(batchNo);
+            if (!StringUtils.isEmpty(normalizedBatchNo)) {
+                normalizedSet.add(normalizedBatchNo);
+            }
+        }
+        return new ArrayList<>(normalizedSet);
+    }
+
+    private void fillBatchNoList(List<ProductionOrderPickVo> detailList) {
+        if (detailList == null || detailList.isEmpty()) {
+            return;
+        }
+        Map<String, LinkedHashSet<String>> batchNoGroupMap = new HashMap<>();
+        for (ProductionOrderPickVo detail : detailList) {
+            String key = buildBatchNoGroupKey(detail);
+            LinkedHashSet<String> batchSet = batchNoGroupMap.computeIfAbsent(key, k -> new LinkedHashSet<>());
+            batchSet.addAll(parseBatchNoValue(detail.getBatchNo()));
+            if (detail.getBatchNoList() != null && !detail.getBatchNoList().isEmpty()) {
+                batchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
+            }
+        }
+        for (ProductionOrderPickVo detail : detailList) {
+            String key = buildBatchNoGroupKey(detail);
+            LinkedHashSet<String> batchSet = batchNoGroupMap.get(key);
+            detail.setBatchNoList(batchSet == null ? Collections.emptyList() : new ArrayList<>(batchSet));
+        }
+    }
+
+    private void fillSelectableBatchNoList(List<ProductionOrderPickVo> detailList) {
+        if (detailList == null || detailList.isEmpty()) {
+            return;
+        }
+        Set<Long> productModelIdSet = detailList.stream()
+                .map(ProductionOrderPickVo::getProductModelId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (productModelIdSet.isEmpty()) {
+            return;
+        }
+        List<StockInventory> stockBatchList = stockInventoryMapper.listSelectableBatchNoByProductModelIds(
+                new ArrayList<>(productModelIdSet));
+        Map<Long, LinkedHashSet<String>> stockBatchMap = new HashMap<>();
+        for (StockInventory stockInventory : stockBatchList) {
+            if (stockInventory == null || stockInventory.getProductModelId() == null) {
+                continue;
+            }
+            String normalizedBatchNo = normalizeBatchNo(stockInventory.getBatchNo());
+            if (StringUtils.isEmpty(normalizedBatchNo)) {
+                continue;
+            }
+            stockBatchMap.computeIfAbsent(stockInventory.getProductModelId(), k -> new LinkedHashSet<>())
+                    .add(normalizedBatchNo);
+        }
+        for (ProductionOrderPickVo detail : detailList) {
+            LinkedHashSet<String> mergedBatchSet = new LinkedHashSet<>();
+            mergedBatchSet.addAll(normalizeBatchNoList(detail.getBatchNoList()));
+            LinkedHashSet<String> selectableBatchSet = stockBatchMap.get(detail.getProductModelId());
+            if (selectableBatchSet != null) {
+                mergedBatchSet.addAll(selectableBatchSet);
+            }
+            detail.setBatchNoList(new ArrayList<>(mergedBatchSet));
+        }
+    }
+
+    private String buildBatchNoGroupKey(ProductionOrderPickVo detail) {
+        return String.valueOf(detail.getProductionOrderId()) + "|"
+                + String.valueOf(detail.getProductModelId()) + "|"
+                + String.valueOf(detail.getTechnologyOperationId()) + "|"
+                + String.valueOf(detail.getOperationName());
+    }
+
+    private List<String> parseBatchNoValue(String rawBatchNoValue) {
+        String normalizedValue = normalizeBatchNo(rawBatchNoValue);
+        if (StringUtils.isEmpty(normalizedValue)) {
+            return Collections.emptyList();
+        }
+        if (normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) {
+            String value = normalizedValue.substring(1, normalizedValue.length() - 1);
+            if (StringUtils.isEmpty(value)) {
+                return Collections.emptyList();
+            }
+            List<String> parsed = Arrays.stream(value.split(","))
+                    .map(item -> item == null ? null : item.trim().replace("\"", "").replace("'", ""))
+                    .collect(Collectors.toList());
+            return normalizeBatchNoList(parsed);
+        }
+        if (normalizedValue.contains(",")) {
+            List<String> parsed = Arrays.stream(normalizedValue.split(","))
+                    .map(item -> item == null ? null : item.trim())
+                    .collect(Collectors.toList());
+            return normalizeBatchNoList(parsed);
+        }
+        return Collections.singletonList(normalizedValue);
+    }
+
+    private LambdaQueryWrapper<StockInventory> buildStockWrapper(Long productModelId, String batchNo) {
+        LambdaQueryWrapper<StockInventory> wrapper = Wrappers.<StockInventory>lambdaQuery()
+                .eq(StockInventory::getProductModelId, productModelId);
+        if (StringUtils.isEmpty(batchNo)) {
+            wrapper.isNull(StockInventory::getBatchNo);
+        } else {
+            wrapper.eq(StockInventory::getBatchNo, batchNo);
+        }
+        return wrapper;
+    }
+
+    private BigDecimal defaultDecimal(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
+
+    private String formatQuantity(BigDecimal value) {
+        return defaultDecimal(value).stripTrailingZeros().toPlainString();
+    }
 }

--
Gitblit v1.9.3