10 天以前 35f74c2abd95e6913180fbccd864915486eecc48
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -1,20 +1,657 @@
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.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 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>
 * 订单领料线边仓 服务实现类
 * 璁㈠崟棰嗘枡绾胯竟浠?鏈嶅姟瀹炵幇绫?
 * </p>
 *
 * @author 芯导软件(江苏)有限公司
 * @author 鑺杞欢锛堟睙鑻忥級鏈夐檺鍏徃
 * @since 2026-04-21 03:55:52
 */
@Service
@RequiredArgsConstructor
public class ProductionOrderPickServiceImpl extends ServiceImpl<ProductionOrderPickMapper, ProductionOrderPick> implements ProductionOrderPickService {
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final StockInventoryMapper stockInventoryMapper;
    @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(), inventoryBatchNo, 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());
            baseMapper.insert(orderPick);
            insertPickRecord(orderPick.getId(),
                    resolvedDto.getProductionOrderId(),
                    resolvedDto.getProductionOperationTaskId(),
                    resolvedDto.getProductModelId(),
                    inventoryBatchNo,
                    resolvedDto.getPickQuantity(),
                    BigDecimal.ZERO,
                    resolvedDto.getPickQuantity(),
                    resolvedDto.getPickType(),
                    resolvedDto.getRemark());
        }
        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));
        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("删除领料失败,ID=" + deleteId);
            }
            insertPickRecord(existingPick.getId(),
                    existingPick.getProductionOrderId(),
                    rootDto.getProductionOperationTaskId(),
                    existingPick.getProductModelId(),
                    oldBatchNo,
                    oldQuantity,
                    oldQuantity,
                    BigDecimal.ZERO,
                    rootDto.getPickType(),
                    rootDto.getRemark());
            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()))
                .collect(Collectors.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("删除领料失败,ID=" + missingPick.getId());
            }
            insertPickRecord(missingPick.getId(),
                    missingPick.getProductionOrderId(),
                    rootDto.getProductionOperationTaskId(),
                    missingPick.getProductModelId(),
                    oldBatchNo,
                    oldQuantity,
                    oldQuantity,
                    BigDecimal.ZERO,
                    rootDto.getPickType(),
                    rootDto.getRemark());
            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(), inventoryBatchNo, 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());
        baseMapper.insert(orderPick);
        insertPickRecord(orderPick.getId(),
                dto.getProductionOrderId(),
                dto.getProductionOperationTaskId(),
                dto.getProductModelId(),
                inventoryBatchNo,
                dto.getPickQuantity(),
                BigDecimal.ZERO,
                dto.getPickQuantity(),
                dto.getPickType(),
                dto.getRemark());
    }
    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, newBatchNo, delta, rowNo);
            } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
                addInventory(oldProductModelId, oldBatchNo, delta.abs());
            }
        } else {
            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
            subtractInventory(newProductModelId, newBatchNo, 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());
        }
    }
    private void insertPickRecord(Long pickId,
                                  Long productionOrderId,
                                  Long productionOperationTaskId,
                                  Long productModelId,
                                  String batchNo,
                                  BigDecimal pickQuantity,
                                  BigDecimal beforeQuantity,
                                  BigDecimal afterQuantity,
                                  Byte pickType,
                                  String remark) {
        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 ? (byte) 1 : pickType);
        pickRecord.setRemark(remark);
        productionOrderPickRecordMapper.insert(pickRecord);
    }
    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            throw new ServiceException("第" + rowNo + "条领料对应库存不存在");
        }
        BigDecimal availableQuantity = defaultDecimal(stockInventory.getQualitity())
                .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
        if (quantity.compareTo(availableQuantity) > 0) {
            throw new ServiceException("第" + rowNo + "条领料可用库存不足");
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(quantity);
        int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条领料扣减库存失败");
        }
    }
    private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) {
        BigDecimal addQuantity = defaultDecimal(quantity);
        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(productModelId);
            newStockInventory.setBatchNo(batchNo);
            newStockInventory.setQualitity(addQuantity);
            newStockInventory.setLockedQuantity(BigDecimal.ZERO);
            newStockInventory.setVersion(1);
            stockInventoryMapper.insert(newStockInventory);
            return;
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(addQuantity);
        int affected = stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("库存回退失败,产品规格ID=" + productModelId);
        }
    }
    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.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.setTechnologyOperationId(itemDto.getTechnologyOperationId());
            merged.setOperationName(itemDto.getOperationName());
            merged.setDemandedQuantity(itemDto.getDemandedQuantity());
            merged.setBom(itemDto.getBom());
        }
        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.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());
        }
        return merged;
    }
    private void validatePickParam(ProductionOrderPickDto dto, int rowNo) {
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "条生产订单ID不能为空");
        }
        if (dto.getProductModelId() == null) {
            throw new ServiceException("第" + rowNo + "条产品规格ID不能为空");
        }
        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("第" + rowNo + "条领料数量必须大于0");
        }
        if (dto.getPickType() != null && dto.getPickType() != 1 && dto.getPickType() != 2) {
            throw new ServiceException("第" + rowNo + "条领料类型只能是1或2");
        }
    }
    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;
    }
}