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.ReviewStatusEnum; import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum; import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum; 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.StockInRecord; import com.ruoyi.stock.pojo.StockInventory; import com.ruoyi.stock.pojo.StockOutRecord; import com.ruoyi.stock.service.StockInventoryService; import com.ruoyi.stock.service.StockInRecordService; import com.ruoyi.stock.service.StockOutRecordService; 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; /** * 生产订单领料服务实现。 * 负责领料新增、更新、补料、退料及库存联动。 */ @Service @RequiredArgsConstructor public class ProductionOrderPickServiceImpl extends ServiceImpl implements ProductionOrderPickService { private static final byte PICK_TYPE_NORMAL = 1; private static final byte PICK_TYPE_FEEDING = 2; private static final String PICK_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.PICK_STOCK_OUT.getCode(); private static final String FEED_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.FEED_STOCK_OUT.getCode(); private static final String PICK_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode(); private static final String FEED_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode(); private final ProductionOrderMapper productionOrderMapper; private final ProductionOperationTaskMapper productionOperationTaskMapper; private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper; private final StockInventoryMapper stockInventoryMapper; private final StockInventoryService stockInventoryService; private final StockInRecordService stockInRecordService; private final StockOutRecordService stockOutRecordService; @Override @Transactional(rollbackFor = Exception.class) public Boolean savePick(ProductionOrderPickDto dto) { // 领料新增总流程: // 1) 解析前端行数据并逐行合并参数; // 2) 校验参数与批次; // 3) 先保存领料主记录; // 4) 再走“出库申请 + 审批通过”完成库存扣减; // 5) 写入领料流水,记录数量变化轨迹。 List 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 batchNoList = resolveBatchNoList(resolvedDto); String inventoryBatchNo = pickInventoryBatchNo(batchNoList); String storedBatchNo = formatBatchNoStorage(batchNoList); // 保存领料主记录快照。 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); // 先新增出库申请,再审批通过,完成库存扣减。 subtractInventory(orderPick.getId(), resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE); // 记录本次领料流水(before=0,after=本次领料量)。 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) { // 领料更新入口(同接口兼容三类业务): // 1) 普通领料改量/增删; // 2) 补料(pickType=2); // 3) 退料(returned=true)。 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("生产订单不存在"); } // 查询订单下现有领料记录并构建ID索引。 List existingPickList = baseMapper.selectList( Wrappers.lambdaQuery() .eq(ProductionOrderPick::getProductionOrderId, productionOrderId)); // 转成Map便于后续按ID快速校验与更新。 Map 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 pickItems = resolveUpdateItems(dto); Set keepPickIdSet = new HashSet<>(); // keepPickIdSet 用于标记本次前端仍然保留的旧记录,后续用于识别“未回传即删除”的行。 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 listPickedDetail(Long productionOrderId) { // 查询订单领料明细,并补齐批次展示字段。 if (productionOrderId == null) { return Collections.emptyList(); } List detailList = baseMapper.listPickedDetailByOrderId(productionOrderId); fillBatchNoList(detailList); fillSelectableBatchNoList(detailList); return detailList; } private void processDeletePickIds(ProductionOrderPickDto rootDto, Map existingPickMap, Long productionOrderId) { // 处理前端显式删除ID: // 1) 校验删除目标是否属于当前订单; // 2) 回补库存; // 3) 删除主记录; // 4) 记录删除流水。 if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) { return; } Set 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.getId(), existingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); // 删除关联领料流水,避免遗留无主记录。 productionOrderPickRecordMapper.delete( Wrappers.lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, existingPick.getId()) ); 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(), rootDto.getFeedingReason()); existingPickMap.remove(deleteId); } } private void processMissingPickItems(ProductionOrderPickDto rootDto, Map existingPickMap, Long productionOrderId, Set keepPickIdSet) { // 处理“前端未回传”的旧行: // 对应场景是用户在前端删除行但未放入 deletePickIds。 // 这里兜底识别并执行回补库存 + 删除主记录 + 写流水。 if (rootDto.getPickList() == null) { return; } List 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.getId(), missingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); // 删除关联领料流水,避免遗留无主记录。 productionOrderPickRecordMapper.delete( Wrappers.lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, missingPick.getId()) ); 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(), rootDto.getFeedingReason()); existingPickMap.remove(missingPick.getId()); } } private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) { // 更新场景下新增一条领料: // 新增主记录 -> 出库申请并审批 -> 写流水。 List batchNoList = resolveBatchNoList(dto); String inventoryBatchNo = pickInventoryBatchNo(batchNoList); String storedBatchNo = formatBatchNoStorage(batchNoList); 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); // 先新增出库申请,再审批通过,完成库存扣减。 subtractInventory(orderPick.getId(), dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE); 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 existingPickMap, Long productionOrderId) { // 补料流程入口: // 逐行校验补料参数,校验原领料归属,再执行补料库存扣减和主记录回写。 List 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("补料请求中存在非补料类型数据"); } 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) { // 补料核心: // 1) 校验规格一致; // 2) 扣减补料库存; // 3) 写补料流水; // 4) 回写主单累计补料量和实际量。 if (dto.getProductModelId() != null && !Objects.equals(dto.getProductModelId(), oldPick.getProductModelId())) { throw new ServiceException("第" + rowNo + "行补料失败:产品规格与原领料记录不一致"); } Long productModelId = oldPick.getProductModelId(); List batchNoList = resolveBatchNoList(dto); String inventoryBatchNo = batchNoList.isEmpty() ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo()) : formatBatchNoStorage(batchNoList); BigDecimal feedingQuantity = dto.getFeedingQuantity(); subtractInventory(oldPick.getId(), productModelId, inventoryBatchNo, feedingQuantity, rowNo, FEED_STOCK_OUT_RECORD_TYPE); // 计算补料前后数量并写补料流水。 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 existingPickMap, Long productionOrderId) { // 退料流程入口: // 逐行校验退料参数与领料归属,再更新退料量与实际量字段。 List 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) { // 退料更新: // 1) returnQty 按“本次退料量”处理; // 2) 本次退料量回补到“生产退料入库”; // 3) 累加主记录退料总量并重算实际量。 BigDecimal oldReturnQty = defaultDecimal(oldPick.getReturnQty()); BigDecimal currentReturnQty = defaultDecimal(dto.getReturnQty()); BigDecimal totalReturnQty = oldReturnQty.add(currentReturnQty); if (currentReturnQty.compareTo(BigDecimal.ZERO) > 0) { String returnBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo()); addInventoryRecordOnly(oldPick.getId(), oldPick.getProductModelId(), returnBatchNo, currentReturnQty, FEED_RETURN_IN_RECORD_TYPE); } BigDecimal actualQty = defaultDecimal(oldPick.getQuantity()) .add(defaultDecimal(oldPick.getFeedingQty())) .subtract(totalReturnQty); if (actualQty.compareTo(BigDecimal.ZERO) < 0) { throw new ServiceException("第" + rowNo + "行退料失败:累计退料数量不能大于可用数量"); } ProductionOrderPick updatePick = new ProductionOrderPick(); updatePick.setId(oldPick.getId()); updatePick.setReturnQty(totalReturnQty); updatePick.setActualQty(actualQty); updatePick.setReturned(totalReturnQty.compareTo(BigDecimal.ZERO) > 0); int affected = baseMapper.updateById(updatePick); if (affected <= 0) { throw new ServiceException("第" + rowNo + "行退料失败:更新领料主记录失败"); } oldPick.setReturnQty(updatePick.getReturnQty()); oldPick.setActualQty(updatePick.getActualQty()); oldPick.setReturned(updatePick.getReturned()); } private void updateExistingPick(ProductionOrderPickDto dto, int rowNo, Map existingPickMap) { // 普通更新单行核心流程: // 1) 校验旧记录存在且属于当前订单; // 2) 比较新旧“规格+批次”,决定库存处理策略; // 3) 更新主记录; // 4) 写变更流水(记录前后数量变化)。 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 newBatchNoList = resolveBatchNoList(dto); String newBatchNo = pickInventoryBatchNo(newBatchNoList); String newStoredBatchNo = formatBatchNoStorage(newBatchNoList); BigDecimal newQuantity = dto.getPickQuantity(); // 判断规格+批次或数量是否变化,并按场景处理库存: // 1) 同规格同批次:按差值处理(增量扣减 / 减量回退); // 2) 规格或批次变化:回退旧领料后再重提新领料。 boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId) && Objects.equals(oldBatchNo, newBatchNo); boolean quantityChanged = oldQuantity.compareTo(newQuantity) != 0; boolean needReissuePickRecord = !sameStockKey || quantityChanged; if (sameStockKey) { BigDecimal deltaQuantity = newQuantity.subtract(oldQuantity); if (deltaQuantity.compareTo(BigDecimal.ZERO) > 0) { // 数量增加,只扣减新增部分。 subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, deltaQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE); } else if (deltaQuantity.compareTo(BigDecimal.ZERO) < 0) { // 数量减少,只回退差值部分。 addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, deltaQuantity.abs(), PICK_RETURN_IN_RECORD_TYPE); } } else { // 规格或批次变化:先全量回退旧领料,再全量扣减新领料。 addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE); subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, newQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE); } if (needReissuePickRecord) { // 正常领料流水按“最新领料量”重建,避免保留历史旧值。 deleteNormalPickRecord(oldPick.getId()); } oldPick.setProductModelId(newProductModelId); oldPick.setBatchNo(newStoredBatchNo); oldPick.setQuantity(newQuantity); oldPick.setRemark(dto.getRemark()); oldPick.setOperationName(dto.getOperationName()); oldPick.setTechnologyOperationId(dto.getTechnologyOperationId()); // 普通更新也要同步重算实际用量,避免沿用旧值。 // 规则:实际用量 = 领料数量 + 补料数量 - 退料数量。 oldPick.setActualQty(calculateActualQty(oldPick, oldPick.getFeedingQty())); 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 + "行更新失败:更新领料记录失败"); } // 如果发生领料重提,补写一条新的正常领料流水。 if (needReissuePickRecord) { insertPickRecord(oldPick.getId(), dto.getProductionOrderId(), dto.getProductionOperationTaskId(), newProductModelId, newBatchNo, newQuantity, BigDecimal.ZERO, 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 deleteNormalPickRecord(Long pickId) { // 删除该领料单历史上的“正常领料”流水,保留补料/退料流水。 if (pickId == null) { return; } productionOrderPickRecordMapper.delete( Wrappers.lambdaQuery() .eq(ProductionOrderPickRecord::getPickId, pickId) .eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_NORMAL) ); } private void subtractInventory(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, int rowNo, String stockOutRecordType) { // 扣减库存总流程: // 1) 解析批次列表; // 2) 计算每个批次可用量与总可用量; // 3) 按批次顺序逐笔“新增出库记录并审批通过”,直到扣完目标数量; // 4) 任一步失败即抛错并回滚事务。 BigDecimal deductQuantity = defaultDecimal(quantity); // 领料数量小于等于0时,不需要执行库存扣减。 if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) { return; } List batchNoList = parseBatchNoValue(batchNo); if (batchNoList.isEmpty()) { batchNoList = Collections.singletonList(null); } // 先计算各批次可用量,避免边扣边算导致判断不一致。 Map 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("第" + rowNo + "行扣减库存失败:可用库存不足,当前可用" + formatQuantity(totalAvailableQuantity) + ",仍缺少" + formatQuantity(shortQuantity)); } // 按批次顺序逐笔扣减库存。 BigDecimal remainingQuantity = deductQuantity; for (Map.Entry 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); createAndApproveStockOutRecord(recordId, productModelId, entry.getKey(), currentDeductQuantity, rowNo, stockOutRecordType); remainingQuantity = remainingQuantity.subtract(currentDeductQuantity); } if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) { throw new ServiceException("第" + rowNo + "行扣减库存失败:仍有未扣减数量" + formatQuantity(remainingQuantity)); } } private void createAndApproveStockOutRecord(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, int rowNo, String stockOutRecordType) { // 库存扣减改为两步: // 1) 先调用 addStockOutRecordOnly 新增待审批出库记录; // 2) 再调用出库审批,审批状态固定传 1(通过)。 try { StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setRecordId(recordId == null ? 0L : recordId); stockInventoryDto.setRecordType(stockOutRecordType); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(quantity); stockInventoryService.addStockOutRecordOnly(stockInventoryDto); LambdaQueryWrapper recordWrapper = Wrappers.lambdaQuery() .eq(StockOutRecord::getRecordId, stockInventoryDto.getRecordId()) .eq(StockOutRecord::getRecordType, stockOutRecordType) .eq(StockOutRecord::getProductModelId, productModelId) .eq(StockOutRecord::getType, "0") .orderByDesc(StockOutRecord::getId) .last("limit 1"); if (StringUtils.isEmpty(batchNo)) { recordWrapper.isNull(StockOutRecord::getBatchNo); } else { recordWrapper.eq(StockOutRecord::getBatchNo, batchNo); } StockOutRecord stockOutRecord = stockOutRecordService.getOne(recordWrapper, false); if (stockOutRecord == null || stockOutRecord.getId() == null) { throw new ServiceException("第" + rowNo + "行扣减库存失败:未找到对应出库申请记录"); } stockOutRecordService.batchApprove( Collections.singletonList(stockOutRecord.getId()), ReviewStatusEnum.APPROVED.getCode() ); } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException("第" + rowNo + "行扣减库存失败:" + ex.getMessage()); } } private void addInventory(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, String stockInRecordType) { // 回补库存改为两步: // 1) 先新增入库申请; // 2) 再审批通过,确保库存立刻回补生效。 BigDecimal addQuantity = defaultDecimal(quantity); if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) { return; } try { StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(addQuantity); stockInventoryDto.setRecordType(stockInRecordType); stockInventoryDto.setRecordId(recordId == null ? 0L : recordId); stockInventoryService.addStockInRecordOnly(stockInventoryDto); LambdaQueryWrapper recordWrapper = Wrappers.lambdaQuery() .eq(StockInRecord::getRecordId, stockInventoryDto.getRecordId()) .eq(StockInRecord::getRecordType, stockInventoryDto.getRecordType()) .eq(StockInRecord::getProductModelId, productModelId) .eq(StockInRecord::getType, "0") .orderByDesc(StockInRecord::getId) .last("limit 1"); if (StringUtils.isEmpty(batchNo)) { recordWrapper.isNull(StockInRecord::getBatchNo); } else { recordWrapper.eq(StockInRecord::getBatchNo, batchNo); } StockInRecord stockInRecord = stockInRecordService.getOne(recordWrapper, false); if (stockInRecord == null || stockInRecord.getId() == null) { throw new ServiceException("回补库存失败:未找到对应入库申请记录"); } stockInRecordService.batchApprove( Collections.singletonList(stockInRecord.getId()), ReviewStatusEnum.APPROVED.getCode() ); } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException("回补库存失败:" + ex.getMessage()); } } private void addInventoryRecordOnly(Long recordId, Long productModelId, String batchNo, BigDecimal quantity, String stockInRecordType) { // 仅记录入库申请,不做审核通过。 BigDecimal addQuantity = defaultDecimal(quantity); if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) { return; } try { StockInventoryDto stockInventoryDto = new StockInventoryDto(); stockInventoryDto.setProductModelId(productModelId); stockInventoryDto.setBatchNo(batchNo); stockInventoryDto.setQualitity(addQuantity); stockInventoryDto.setRecordType(stockInRecordType); stockInventoryDto.setRecordId(recordId == null ? 0L : recordId); stockInventoryService.addStockInRecordOnly(stockInventoryDto); } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException("退料入库记录保存失败:" + ex.getMessage()); } } private List 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 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) { // 优先从主DTO解析订单ID,不存在时再从子项中回退查找。 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) { // 合并规则: // - itemDto 优先承载行级输入; // - itemDto 缺失字段从 rootDto 兜底继承; // - 输出 merged 作为统一业务入参。 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 + "行参数错误:生产订单ID不能为空"); } if (dto.getProductModelId() == null) { throw new ServiceException("第" + rowNo + "行参数错误:产品规格不能为空"); } 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) { // 校验补料参数(订单、领料ID、补料数量、类型)。 if (dto.getProductionOrderId() == null) { throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空"); } if (dto.getId() == null) { throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空"); } 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) { // 校验退料参数(订单、领料ID、退料量)。 if (dto.getProductionOrderId() == null) { throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空"); } if (dto.getId() == null) { throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空"); } if (dto.getReturnQty() == null || dto.getReturnQty().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 feedingRecords = productionOrderPickRecordMapper.selectList( Wrappers.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) { // 标准化批次号(去空白、空串转null)。 if (StringUtils.isEmpty(batchNo)) { return null; } String trimBatchNo = batchNo.trim(); return trimBatchNo.isEmpty() ? null : trimBatchNo; } private List resolveBatchNoList(ProductionOrderPickDto dto) { // 优先解析 batchNoList,空则回退解析 batchNo 字符串。 List normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList()); if (!normalizedBatchNoList.isEmpty()) { return normalizedBatchNoList; } return parseBatchNoValue(dto.getBatchNo()); } private String pickInventoryBatchNo(List 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 batchNoList) { // 将批次集合格式化为数据库存储值。 if (batchNoList == null || batchNoList.isEmpty()) { return null; } if (batchNoList.size() == 1) { return batchNoList.get(0); } return String.join(",", batchNoList); } private List normalizeBatchNoList(List batchNoList) { // 批量标准化批次号并去重。 if (batchNoList == null || batchNoList.isEmpty()) { return Collections.emptyList(); } LinkedHashSet 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 detailList) { // 将同订单+同规格+同工序的数据按组聚合批次,便于前端统一展示。 if (detailList == null || detailList.isEmpty()) { return; } Map> batchNoGroupMap = new HashMap<>(); for (ProductionOrderPickVo detail : detailList) { String key = buildBatchNoGroupKey(detail); LinkedHashSet 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 batchSet = batchNoGroupMap.get(key); detail.setBatchNoList(batchSet == null ? Collections.emptyList() : new ArrayList<>(batchSet)); } } private void fillSelectableBatchNoList(List detailList) { // 合并“已选批次”和“库存可选批次”,用于前端下拉。 if (detailList == null || detailList.isEmpty()) { return; } // 先收集明细中涉及的规格ID,批量查询库存批次。 Set productModelIdSet = detailList.stream() .map(ProductionOrderPickVo::getProductModelId) .filter(Objects::nonNull) .collect(Collectors.toSet()); if (productModelIdSet.isEmpty()) { return; } List stockBatchList = stockInventoryMapper.listSelectableBatchNoByProductModelIds( new ArrayList<>(productModelIdSet)); Map> 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 mergedBatchSet = new LinkedHashSet<>(); mergedBatchSet.addAll(normalizeBatchNoList(detail.getBatchNoList())); LinkedHashSet selectableBatchSet = stockBatchMap.get(detail.getProductModelId()); if (selectableBatchSet != null) { mergedBatchSet.addAll(selectableBatchSet); } detail.setBatchNoList(new ArrayList<>(mergedBatchSet)); } } private String buildBatchNoGroupKey(ProductionOrderPickVo detail) { // 构建批次聚合分组键。 return detail.getProductionOrderId() + "|" + detail.getProductModelId() + "|" + detail.getTechnologyOperationId() + "|" + detail.getOperationName(); } private List parseBatchNoValue(String rawBatchNoValue) { // 批次解析兼容三种格式: // 1) 单值:A001 // 2) 逗号分隔:A001,A002 // 3) 类JSON数组字符串:["A001","A002"] String normalizedValue = normalizeBatchNo(rawBatchNoValue); if (StringUtils.isEmpty(normalizedValue)) { return Collections.emptyList(); } if (normalizedValue != null && normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) { String value = normalizedValue.substring(1, normalizedValue.length() - 1); if (StringUtils.isEmpty(value)) { return Collections.emptyList(); } List parsed = Arrays.stream(value.split(",")) .map(item -> item.trim().replace("\"", "").replace("'", "")) .collect(Collectors.toList()); return normalizeBatchNoList(parsed); } if (normalizedValue != null && normalizedValue.contains(",")) { List parsed = Arrays.stream(normalizedValue.split(",")) .map(item -> item.trim()) .collect(Collectors.toList()); return normalizeBatchNoList(parsed); } return Collections.singletonList(normalizedValue); } private LambdaQueryWrapper buildStockWrapper(Long productModelId, String batchNo) { // 构建库存查询条件(规格 + 批次)。 LambdaQueryWrapper wrapper = Wrappers.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) { // BigDecimal 空值兜底,统一按0处理。 return value == null ? BigDecimal.ZERO : value; } private String formatQuantity(BigDecimal value) { // 数量格式化输出(去除末尾无效0)。 return defaultDecimal(value).stripTrailingZeros().toPlainString(); } }