package com.baiyue.firstleg.service; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.client.OrderFulfillmentSplitClient; import com.baiyue.firstleg.entity.InboundPalletAllocationEntity; import com.baiyue.firstleg.entity.LoadingPickItemEntity; import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper; import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.MeasurementItemRow; import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.ParentRow; import com.baiyue.firstleg.mapper.DropshipPalletizationMapper; import com.baiyue.firstleg.repository.InboundPalletAllocationRepository; import com.baiyue.firstleg.repository.LoadingPickRepository; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import feign.FeignException; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** PDA 任务结算:按柜次生成实装/余量履约分片,并结算测量与库位资源。 */ @Service public class PdaPickSettlementService { private static final Logger log = LoggerFactory.getLogger(PdaPickSettlementService.class); private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L; private static final String SOURCE_SERVICE = "firstleg-service"; @Resource private LoadingPickRepository picks; @Resource private InboundSplitSettlementMapper splits; @Resource private InboundPalletAllocationRepository allocations; @Resource private WarehousePalletLocationClient warehouse; @Resource private OrderFulfillmentSplitClient orderSplits; @Resource private InboundHoldGuard inboundHoldGuard; @Resource private DropshipPalletizationMapper dropshipPallets; @Resource private com.baiyue.firstleg.repository.DropshipSplitRepository dropshipSplits; @Value("${baiyue.security.internal-service-token}") private String internalToken; /** 每个柜次生成一张 LOADED 分片;存在余量时另生成可继续分柜的 REMAINDER 分片。 */ @Transactional public void settle(long taskId, long operatorId, String operatorName) { inboundHoldGuard.requireTaskNotHeld(taskId); if (!picks.allTaskItemsResolved(taskId)) { return; } List taskItems = picks.items(taskId); PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems); for (Long planItemId : resolution.skippedPlanItemIds()) { // 重复 settle(例如复核)时该行已逻辑删除,0 行是可接受的幂等结果。 int changed = splits.removeSkippedPlanItem(planItemId, operatorId, operatorName); if (changed != 0 && changed != 1) { throw conflict("跳单明细已被并发修改,请刷新后重试"); } } Map pickedByInbound = pickedByInbound(taskItems); Set newlySplitInbounds = new java.util.HashSet<>(); pickedByInbound.entrySet().stream().sorted(Map.Entry.comparingByKey()) .forEach(entry -> { if (settleInbound(entry.getKey(), entry.getValue(), taskItems, taskId, operatorId, operatorName)) { newlySplitInbounds.add(entry.getKey()); } }); for (List lines : resolution.partialByInbound().values()) { for (PdaPickSettlementPlan.SplitLine line : lines) { if (newlySplitInbounds.contains(line.inboundId())) { requireAffected(splits.convergePartialPlanItem(line.loadingPlanItemId(), operatorId, operatorName), "部分拣货装柜明细"); } } } } /** Worker boundary: one inbound owner per transaction, so a cabinet never holds 15 owner locks at once. */ @Transactional public void settleOwner(long taskId, long inboundId, long operatorId, String operatorName) { inboundHoldGuard.requireTaskNotHeld(taskId); if (!picks.allTaskItemsResolved(taskId)) { throw conflict("PDA任务仍有未处理明细"); } List taskItems = picks.items(taskId); Map pickedByInbound = pickedByInbound(taskItems); Integer pickedPackageCount = pickedByInbound.get(inboundId); if (pickedPackageCount == null) { throw conflict("PDA任务不包含指定入库单"); } PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems); for (Long planItemId : resolution.skippedPlanItemIds()) { int changed = splits.removeSkippedPlanItem(planItemId, operatorId, operatorName); if (changed != 0 && changed != 1) { throw conflict("跳单明细已被并发修改,请刷新后重试"); } } boolean newlySplit = settleInbound(inboundId, pickedPackageCount, taskItems, taskId, operatorId, operatorName); if (newlySplit) { for (PdaPickSettlementPlan.SplitLine line : resolution.partialByInbound() .getOrDefault(inboundId, List.of())) { requireAffected(splits.convergePartialPlanItem(line.loadingPlanItemId(), operatorId, operatorName), "部分拣货装柜明细"); } } } /** 按柜次实拣数量结算,一件代发保留未装整托供后续柜次使用。 */ private boolean settleInbound(long parentInboundId, int pickedPackageCount, List taskItems, long taskId, long operatorId, String operatorName) { InboundSplitSettlementMapper.Lineage lineage = splits.lineage(parentInboundId); if (lineage == null) throw conflict("待拆分入库单不存在"); if ("CHILD".equals(lineage.inboundRole()) && !("REMAINDER".equals(lineage.splitPurpose()) && "NOT_SPLIT".equals(lineage.splitStatus()))) { throw conflict("仅未消费的余量子单可以继续分柜"); } if (!"MASTER".equals(lineage.inboundRole()) && lineage.rootInboundId() == null) { throw conflict("余量子单缺少根主单"); } long rootInboundId = "MASTER".equals(lineage.inboundRole()) ? parentInboundId : lineage.rootInboundId(); ParentRow root = splits.lockParent(rootInboundId); ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId); if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0 || !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole())) || !("WAIT_SPLIT".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus())) || ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) { throw conflict("待拆分主入库单不在可拆分状态"); } String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId; Long appliedBatchId = splits.appliedBatch(parent.tenantId(), operationId); if (appliedBatchId != null && appliedBatchId > 0) return false; var task = picks.task(taskId); if (task == null || task.getLoadingPlanId() == null) throw conflict("PDA任务缺少真实装柜计划"); long loadingPlanId = task.getLoadingPlanId(); Long containerBookingId = splits.containerBookingId(loadingPlanId); if (containerBookingId == null) throw conflict("PDA任务装柜计划缺少真实柜次"); int parentAvailable = availablePackageCount(parent); if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) { throw conflict("本次已拣箱数与主入库单可用箱数不一致"); } int childPackageCount = parentAvailable - pickedPackageCount; List confirmedAllocations = new ArrayList<>(allocations.lockConfirmed( parent.tenantId(), parentInboundId)); confirmedAllocations.sort(Comparator.comparing(InboundPalletAllocationEntity::getAllocationNo, Comparator.nullsLast(Integer::compareTo))); consumePickedAllocations(parent.tenantId(), parentInboundId, confirmedAllocations, pickedPackageCount); long blockId = blockId(confirmedAllocations); int allocationRemaining = confirmedAllocations.stream().mapToInt(PdaPickSettlementService::remaining).sum(); if (allocationRemaining != childPackageCount) { throw conflict("父入库单库位剩余箱数与未装柜箱数不一致"); } warehouseId(confirmedAllocations); List measurements = splits.lockLatestApprovedItemsForInbound( parent.tenantId(), parentInboundId); if (measurements.isEmpty()) { throw conflict("主入库单缺少已审核测量明细,不能生成子入库单"); } long sourceMeasurementId = measurements.get(0).measurementId(); if (measurements.stream().anyMatch(item -> !Objects.equals(sourceMeasurementId, item.measurementId()))) { throw conflict("主入库单存在多个已审核测量批次,不能自动拆分"); } boolean dropshipWholePallet = "ONE_CLICK_DROPSHIP".equals( splits.fulfillmentType(parent.tenantId(), parentInboundId)); Map pickedByDetail = dropshipWholePallet ? Map.of() : pickedByDetail(taskItems, parentInboundId); if (!dropshipWholePallet && pickedByDetail.values().stream().mapToInt(Integer::intValue).sum() != pickedPackageCount) { throw conflict("PDA 已拣明细与入库单已拣箱数不一致"); } List measurementLines = dropshipWholePallet ? wholePalletMeasurementLines(measurements, pickedPackageCount, parentAvailable) : measurementLines(measurements, pickedByDetail); int measuredRemaining = measurementLines.stream().mapToInt(MeasurementSplitLine::remainingPackageCount).sum(); if (measuredRemaining != childPackageCount) { throw conflict("主入库单测量明细剩余箱数与未装柜箱数不一致"); } if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在"); Map orderQuantities = measurementQuantities(measurementLines); OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, orderQuantities, pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId, "pda-task-" + taskId, operatorId, operatorName); long remainderChildId = childPackageCount > 0 ? IdWorker.getId() : 0L; long batchId = IdWorker.getId(); String idempotencyKey = "pda-task-" + taskId; int rootVersion = root.version() == null ? 0 : root.version(); int sourceVersion = parent.version() == null ? 0 : parent.version(); int sequence = splits.nextSplitSequence(parent.tenantId(), rootInboundId); String remainderChildCode = childPackageCount > 0 ? childInboundCode(root.inboundCode(), sequence) : null; // Consume the current slice before inserting its successor so the single-active-remainder // constraint is released. A local failure rolls this back; the Order split is idempotent on retry. requireAffected(splits.markSourceLoaded(parent.tenantId(), parentInboundId, pickedPackageCount, operatorId, sourceVersion), "来源分片状态"); if (parentInboundId != rootInboundId) { requireAffected(splits.touchRootSplit(parent.tenantId(), rootInboundId, operatorId, rootVersion), "根单拆分状态"); } if (remainderChildId != 0L) { requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), parentInboundId, rootInboundId, remainderChildId, orderSplit.remainderChild().orderId(), orderSplit.remainderChild().orderNo(), remainderChildCode, childPackageCount, sequence, "REMAINDER", operatorId, operatorName), "余量子入库单"); if (dropshipWholePallet) { var pickedPalletIds = taskItems.stream().filter(item -> "PICKED".equals(item.getItemStatus()) && "SEALED_PALLET".equals(item.getUnitType())).map(LoadingPickItemEntity::getPalletId) .filter(Objects::nonNull).distinct().toList(); // 托内箱按实际箱唛移动,不能按箱序号截取,否则会移错已装托盘。 dropshipSplits.transferRemainder(parent.tenantId(), parentInboundId, remainderChildId, orderSplit.remainderChild().orderId(), remainderChildCode, childPackageCount, pickedPalletIds); } else { splits.transferRemainingCartonLabels(parent.tenantId(), parentInboundId, remainderChildId, orderSplit.remainderChild().orderId(), remainderChildCode, pickedPackageCount, childPackageCount); } splits.normalizeLoadedCartonLabels(parent.tenantId(), parentInboundId, pickedPackageCount); } requireAffected(splits.insertBatchWithChildren(batchId, parent.tenantId(), rootInboundId, operationId, parentInboundId, idempotencyKey, rootVersion, parentAvailable, null, remainderChildId == 0L ? null : remainderChildId, pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, "PDA拣货后生成实装/余量子单", operatorId, operatorName), "拆分批次"); long childMeasurementId = childPackageCount > 0 ? IdWorker.getId() : 0L; if (childPackageCount > 0) { requireAffected(splits.cloneMeasurement(parent.tenantId(), parentInboundId, sourceMeasurementId, remainderChildId, childMeasurementId, batchId, childPackageCount, operatorId), "余量子入库测量批次"); } int lineNo = 1; for (MeasurementSplitLine line : measurementLines) { if (line.remainingPackageCount() <= 0) { continue; } long splitItemId = IdWorker.getId(); requireAffected(splits.insertSplitItem(splitItemId, parent.tenantId(), batchId, rootInboundId, remainderChildId, lineNo, line.item().inboundDetailId(), sourceMeasurementId, line.item().measurementItemId(), line.remainingPackageCount()), "拆分明细"); requireAffected(splits.cloneMeasurementItem(parent.tenantId(), sourceMeasurementId, line.item().measurementItemId(), childMeasurementId, splitItemId, IdWorker.getId(), lineNo, line.remainingPackageCount()), "子入库测量明细"); lineNo++; } requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效"); for (MeasurementSplitLine line : measurementLines) { requireAffected(splits.updateMeasurementItemForLoaded(parent.tenantId(), line.item().measurementItemId(), line.pickedPackageCount()), "来源测量明细"); } requireAffected(splits.updateMeasurementForLoaded(parent.tenantId(), parentInboundId, sourceMeasurementId, pickedPackageCount), "来源测量批次"); requireAffected(splits.normalizeChildMetrics(parent.tenantId(), parentInboundId, sourceMeasurementId), "来源单重量体积归一化"); if (remainderChildId != 0L) { requireAffected(splits.normalizeChildMetrics(parent.tenantId(), remainderChildId, childMeasurementId), "余量子单重量体积归一化"); } requireAffected(splits.insertHistory(IdWorker.getId(), parent.tenantId(), batchId, operationId + "-APPLY", "{\"sourceInboundId\":" + parentInboundId + ",\"loadedChildInboundId\":null" + ",\"remainderChildInboundId\":" + (remainderChildId == 0L ? "null" : remainderChildId) + ",\"loadedPackageCount\":" + pickedPackageCount + ",\"remainingPackageCount\":" + childPackageCount + ",\"loadingPlanId\":" + loadingPlanId + ",\"containerBookingId\":" + containerBookingId + "}", operatorId, operatorName, "PDA拣货后来源单承接实装并生成余量子入库单"), "拆分审计"); if (remainderChildId != 0L) { allocations.copyBlockToChild(parent.tenantId(), remainderChildId, confirmedAllocations); allocations.markBlockTransferred(parent.tenantId(), parentInboundId, blockId); } else { allocations.markBlockReleased(parent.tenantId(), parentInboundId, blockId); } settleWarehouseBlock(parentInboundId, remainderChildId, remainderChildCode == null ? root.inboundCode() : remainderChildCode, taskId, operatorId, blockId); return true; } private OrderFulfillmentSplitClient.Response createOrderSplit( ParentRow source, ParentRow root, Map quantitiesByDetail, int loadedPackageCount, int remainingPackageCount, long loadingPlanId, long containerBookingId, String operationId, String idempotencyKey, long operatorId, String operatorName) { if (root.orderId() == null || source.orderId() == null) { throw conflict("入仓单缺少真实订单引用"); } List quantities = quantitiesByDetail.entrySet().stream() .map(entry -> new OrderFulfillmentSplitClient.QuantityLine(entry.getKey(), entry.getValue())) .toList(); OrderFulfillmentSplitClient.CreateRequest request = new OrderFulfillmentSplitClient.CreateRequest( source.tenantId(), root.orderId(), source.orderId(), loadingPlanId, containerBookingId, loadedPackageCount + remainingPackageCount, loadedPackageCount, remainingPackageCount, null, idempotencyKey, operationId, quantities, operatorName); ApiResponse response; try { response = orderSplits.split(internalToken, operatorId, request); } catch (FeignException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务不可用,无法生成真实履约子订单"); } if (response == null || !response.successful() || response.data() == null) { ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code()); throw new BizException(code, response == null || response.message() == null ? "订单服务生成真实履约子订单失败" : response.message()); } OrderFulfillmentSplitClient.Response split = response.data(); boolean remainderMatches = remainingPackageCount == 0 ? split.remainderChild() == null : validOrderChild(split.remainderChild(), "REMAINDER", source.orderId(), loadingPlanId, containerBookingId, remainingPackageCount); boolean sourceLoadedMatches = split.loadedChild() == null || (Objects.equals(split.loadedChild().orderId(), source.orderId()) && validOrderChild(split.loadedChild(), "LOADED", source.orderId(), loadingPlanId, containerBookingId, loadedPackageCount)); if (!Objects.equals(split.parentOrderId(), root.orderId()) || !Objects.equals(split.sourcePackageCount(), loadedPackageCount + remainingPackageCount) || !Objects.equals(split.loadedPackageCount(), loadedPackageCount) || !Objects.equals(split.remainingPackageCount(), remainingPackageCount) || !Objects.equals(split.loadingPlanId(), loadingPlanId) || !Objects.equals(split.containerBookingId(), containerBookingId) || !sourceLoadedMatches || !remainderMatches) { throw conflict("订单服务返回的父子订单、柜次或数量不一致"); } return split; } private static boolean validOrderChild(OrderFulfillmentSplitClient.Child child, String role, Long sourceOrderId, long loadingPlanId, long containerBookingId, int packageCount) { return child != null && child.orderId() != null && child.orderId() > 0 && child.orderNo() != null && !child.orderNo().isBlank() && role.equals(child.splitRole()) && Objects.equals(child.packageCount(), packageCount) && Objects.equals(child.sourceOrderId(), sourceOrderId) && Objects.equals(child.loadingPlanId(), loadingPlanId) && Objects.equals(child.containerBookingId(), containerBookingId); } private Map pickedByInbound(List taskItems) { Map result = new HashMap<>(); Set pickedPalletIds = new java.util.LinkedHashSet<>(); for (LoadingPickItemEntity item : taskItems) { if (!("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus()))) continue; if ("SEALED_PALLET".equals(item.getUnitType()) && item.getPalletId() != null) { pickedPalletIds.add(item.getPalletId()); } else { result.merge(item.getInboundId(), count(item.getPickedPackageCount(), "PDA已拣箱数"), Integer::sum); } } if (pickedPalletIds.isEmpty()) return result; Map> palletItems = taskItems.stream() .filter(item -> item.getPalletId() != null && pickedPalletIds.contains(item.getPalletId())) .collect(Collectors.groupingBy(LoadingPickItemEntity::getPalletId)); if (palletItems.values().stream().anyMatch(items -> items.stream() .anyMatch(item -> !"PICKED".equals(item.getItemStatus())))) { throw conflict("一件代发整托存在未完成的 SKU 明细,不能结算物理箱数"); } List physical = dropshipPallets.palletInboundMembersByPalletIds(pickedPalletIds); Set resolvedPallets = physical.stream().map(DropshipPalletizationMapper.PlanPalletMemberRow::getPalletId) .collect(Collectors.toSet()); if (!resolvedPallets.equals(pickedPalletIds)) { throw conflict("一件代发托盘缺少物理箱明细,不能完成装柜结算"); } physical.forEach(row -> result.merge(row.getInboundId(), count(row.getAvailablePackageCount(), "托盘物理箱数"), Integer::sum)); return result; } private static Map pickedByDetail(List taskItems, long inboundId) { Map result = new HashMap<>(); for (LoadingPickItemEntity item : taskItems) { if (item.getInboundId() == null || item.getInboundId() != inboundId || !("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus()))) { continue; } if (item.getInboundDetailId() == null) { throw conflict("PDA 已拣明细缺少入库单明细标识"); } result.merge(item.getInboundDetailId(), count(item.getPickedPackageCount(), "PDA已拣箱数"), Integer::sum); } return result; } private static List measurementLines(List measurements, Map pickedByDetail) { Set measuredDetailIds = measurements.stream().map(MeasurementItemRow::inboundDetailId) .collect(Collectors.toSet()); if (!measuredDetailIds.containsAll(pickedByDetail.keySet())) { throw conflict("PDA 已拣明细未匹配已审核测量明细"); } Map remainingPickedByDetail = new HashMap<>(pickedByDetail); List result = new ArrayList<>(); for (MeasurementItemRow item : measurements) { int measured = count(item.packageCount(), "测量明细箱数"); int requested = remainingPickedByDetail.getOrDefault(item.inboundDetailId(), 0); int picked = Math.min(measured, requested); remainingPickedByDetail.put(item.inboundDetailId(), requested - picked); result.add(new MeasurementSplitLine(item, picked, measured - picked)); } if (remainingPickedByDetail.values().stream().anyMatch(remaining -> remaining != 0)) { throw conflict("PDA 已拣箱数超过测量明细箱数"); } return result; } /** 整托箱数守恒,测量行按箱数分摊重量体积;真实SKU组成仍由封托箱账本冻结。 */ private static List wholePalletMeasurementLines(List measurements, int pickedPackageCount, int parentAvailable) { int measuredPackages = measurements.stream() .mapToInt(item -> count(item.packageCount(), "测量明细箱数")).sum(); if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable || measuredPackages != parentAvailable) { throw conflict("一件代发整托箱数与入库单、测量批次不守恒"); } int left = pickedPackageCount; List result = new ArrayList<>(); for (var item : measurements) { int total = count(item.packageCount(), "测量明细箱数"); int picked = Math.min(total, left); result.add(new MeasurementSplitLine(item, picked, total - picked)); left -= picked; } return List.copyOf(result); } private static Map measurementQuantities(List lines) { return lines.stream().collect(Collectors.groupingBy(line -> line.item().inboundDetailId(), LinkedHashMap::new, Collectors.summingInt(line -> count(line.item().packageCount(), "测量明细箱数")))); } private void consumePickedAllocations(long accountId, long inboundId, List confirmed, int pickedCount) { int toConsume = pickedCount; for (InboundPalletAllocationEntity allocation : confirmed) { int allocated = count(allocation.getAllocatedPackageCount(), "库位已分配箱数"); int picked = allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount(); int available = allocated - picked; if (available <= 0 || toConsume == 0) { continue; } int consumed = Math.min(available, toConsume); if (allocation.getId() == null) { throw conflict("父入库单库位分配快照缺少标识"); } allocations.consume(accountId, inboundId, allocation.getId(), consumed); allocation.setPickedPackageCount(picked + consumed); toConsume -= consumed; } if (toConsume != 0) { throw conflict("父入库单已确认库位可用箱数不足以扣减本次已拣箱数"); } } private static int remaining(InboundPalletAllocationEntity allocation) { return count(allocation.getAllocatedPackageCount(), "库位已分配箱数") - (allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount()); } private static long blockId(List allocations) { if (allocations.isEmpty() || allocations.get(0).getBlockId() == null || allocations.get(0).getBlockId() <= 0 || allocations.stream().anyMatch(item -> !Objects.equals(allocations.get(0).getBlockId(), item.getBlockId()))) { throw conflict("PDA结算要求父入库单资源属于同一整票库位块"); } return allocations.get(0).getBlockId(); } private static long warehouseId(List allocations) { if (allocations.isEmpty() || allocations.get(0).getWarehouseId() == null || allocations.get(0).getWarehouseId() <= 0 || allocations.stream().anyMatch(item -> !Objects.equals(allocations.get(0).getWarehouseId(), item.getWarehouseId()))) { throw conflict("PDA结算要求父入库单分配资源属于同一仓库"); } return allocations.get(0).getWarehouseId(); } private void settleWarehouseBlock(long parentInboundId, long childInboundId, String childCode, long taskId, long operatorId, long blockId) { ApiResponse response; String key = "PDA-BLOCK-" + taskId + '-' + parentInboundId; try { response = childInboundId > 0 ? warehouse.transferBlock(internalToken, new WarehousePalletLocationClient.BlockTransferRequest( blockId, parentInboundId, childInboundId, childCode, key, operatorId)) : warehouse.releaseBlock(internalToken, new WarehousePalletLocationClient.BlockReleaseRequest( blockId, parentInboundId, key, operatorId, "ALL_PACKAGES_PICKED")); } catch (FeignException exception) { log.warn("PDA warehouse block settlement transport failure taskId={} sourceInboundId={} " + "blockId={} operationId={} httpStatus={}", taskId, parentInboundId, blockId, key, exception.status()); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源"); } if (response == null || !response.successful() || response.data() == null || response.data().blockId() != blockId || response.data().resources() == null || response.data().resources().isEmpty() || response.data().resources().stream().anyMatch(resource -> resource.blockId() != blockId) || !(childInboundId > 0 ? "OCCUPIED" : "RELEASED").equals(response.data().blockStatus()) || response.data().resources().stream().anyMatch(resource -> !(childInboundId > 0 ? "OCCUPIED" : "RELEASED").equals(resource.resourceStatus()))) { log.warn("PDA warehouse block settlement rejected taskId={} sourceInboundId={} blockId={} " + "operationId={} responseCode={} requestId={} traceId={}", taskId, parentInboundId, blockId, key, response == null ? null : response.code(), response == null ? null : response.requestId(), response == null ? null : response.traceId()); ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code()); throw new SettlementFailure(code, response == null || response.message() == null ? "仓储服务原子结算PDA库位资源失败" : response.message(), response == null ? null : response.requestId(), response == null ? null : response.traceId()); } } /** Carries only correlation facts needed by the durable command diagnosis record. */ public static final class SettlementFailure extends BizException { private final String requestId; private final String traceId; SettlementFailure(ResultCode code, String message, String requestId, String traceId) { super(code, message); this.requestId = requestId; this.traceId = traceId; } public String requestId() { return requestId; } public String traceId() { return traceId; } } static String childInboundCode(String rootInboundCode, int splitSequence) { if (splitSequence <= 0) { throw conflict("子入库单拆分序号不合法"); } String prefix = rootInboundCode == null ? "INBOUND" : rootInboundCode.trim(); if (prefix.isEmpty()) { prefix = "INBOUND"; } String suffix = "-" + splitSequence; return prefix.substring(0, Math.min(prefix.length(), 40 - suffix.length())) + suffix; } private static int count(Integer value, String resource) { if (value == null || value < 0) { throw conflict(resource + "不合法"); } return value; } private static int availablePackageCount(ParentRow parent) { if ("FULLY_SPLIT".equals(parent.splitStatus())) return 0; Integer available = parent.packageCount(); if (available != null && available > 0) return available; if (available != null && available == 0 && !"NOT_SPLIT".equals(parent.splitStatus())) return 0; return count(parent.totalPackageCount(), "主入库单可用箱数"); } private static void requireAffected(int affected, String resource) { if (affected != 1) { throw conflict(resource + "已被并发修改,请刷新后重试"); } } private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); } private record MeasurementSplitLine(MeasurementItemRow item, int pickedPackageCount, int remainingPackageCount) { } }