Files
baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java
T
2026-11-05 16:04:10 +08:00

581 lines
34 KiB
Java

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<LoadingPickItemEntity> 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<Long, Integer> pickedByInbound = pickedByInbound(taskItems);
Set<Long> 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<PdaPickSettlementPlan.SplitLine> 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<LoadingPickItemEntity> taskItems = picks.items(taskId);
Map<Long, Integer> 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<LoadingPickItemEntity> 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<InboundPalletAllocationEntity> 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<MeasurementItemRow> 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<Long, Integer> pickedByDetail = dropshipWholePallet
? Map.of() : pickedByDetail(taskItems, parentInboundId);
if (!dropshipWholePallet
&& pickedByDetail.values().stream().mapToInt(Integer::intValue).sum() != pickedPackageCount) {
throw conflict("PDA 已拣明细与入库单已拣箱数不一致");
}
List<MeasurementSplitLine> 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<Long, Integer> 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<Long, Integer> 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<OrderFulfillmentSplitClient.QuantityLine> 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<OrderFulfillmentSplitClient.Response> 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<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
Map<Long, Integer> result = new HashMap<>();
Set<Long> 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<Long, List<LoadingPickItemEntity>> 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<DropshipPalletizationMapper.PlanPalletMemberRow> physical =
dropshipPallets.palletInboundMembersByPalletIds(pickedPalletIds);
Set<Long> 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<Long, Integer> pickedByDetail(List<LoadingPickItemEntity> taskItems, long inboundId) {
Map<Long, Integer> 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<MeasurementSplitLine> measurementLines(List<MeasurementItemRow> measurements,
Map<Long, Integer> pickedByDetail) {
Set<Long> measuredDetailIds = measurements.stream().map(MeasurementItemRow::inboundDetailId)
.collect(Collectors.toSet());
if (!measuredDetailIds.containsAll(pickedByDetail.keySet())) {
throw conflict("PDA 已拣明细未匹配已审核测量明细");
}
Map<Long, Integer> remainingPickedByDetail = new HashMap<>(pickedByDetail);
List<MeasurementSplitLine> 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<MeasurementSplitLine> wholePalletMeasurementLines(List<MeasurementItemRow> 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<MeasurementSplitLine> 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<Long, Integer> measurementQuantities(List<MeasurementSplitLine> 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<InboundPalletAllocationEntity> 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<InboundPalletAllocationEntity> 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<InboundPalletAllocationEntity> 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<WarehousePalletLocationClient.BlockResult> 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) {
}
}