498 lines
28 KiB
Java
498 lines
28 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.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 java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
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 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;
|
|
@Value("${baiyue.security.internal-service-token}")
|
|
private String internalToken;
|
|
|
|
/** 每个柜次生成一张 LOADED 分片;存在余量时另生成可继续分柜的 REMAINDER 分片。 */
|
|
@Transactional
|
|
public void settle(long taskId, long operatorId, String operatorName) {
|
|
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),
|
|
"部分拣货装柜明细");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
List<Long> releasedReservationIds = releasedReservationIds(confirmedAllocations);
|
|
List<WarehousePalletLocationClient.SettlementTransferItem> transferItems =
|
|
remainingTransferItems(confirmedAllocations);
|
|
int allocationRemaining = transferItems.stream()
|
|
.mapToInt(WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount).sum();
|
|
if (allocationRemaining != childPackageCount) {
|
|
throw conflict("父入库单库位剩余箱数与未装柜箱数不一致");
|
|
}
|
|
long warehouseId = warehouseId(confirmedAllocations);
|
|
if (childPackageCount == 0 && (!transferItems.isEmpty() || releasedReservationIds.isEmpty())) {
|
|
throw conflict("全量拣货后库位资源结算不完整");
|
|
}
|
|
|
|
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("主入库单存在多个已审核测量批次,不能自动拆分");
|
|
}
|
|
Map<Long, Integer> pickedByDetail = pickedByDetail(taskItems, parentInboundId);
|
|
int pickedByMeasurement = pickedByDetail.values().stream().mapToInt(Integer::intValue).sum();
|
|
if (pickedByMeasurement != pickedPackageCount) {
|
|
throw conflict("PDA 已拣明细与入库单已拣箱数不一致");
|
|
}
|
|
List<MeasurementSplitLine> measurementLines = measurementLines(measurements, pickedByDetail);
|
|
int measuredRemaining = measurementLines.stream().mapToInt(MeasurementSplitLine::remainingPackageCount).sum();
|
|
if (measuredRemaining != childPackageCount) {
|
|
throw conflict("主入库单测量明细剩余箱数与未装柜箱数不一致");
|
|
}
|
|
|
|
if (childPackageCount > 0 && transferItems.isEmpty()) {
|
|
throw conflict("父入库单可转移库位剩余箱数与未装柜箱数不一致");
|
|
}
|
|
|
|
if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在");
|
|
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, measurementLines,
|
|
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId,
|
|
"pda-task-" + taskId, operatorId, operatorName);
|
|
String settlementKey = "PDA-SETTLE-" + taskId + '-' + parentInboundId;
|
|
long remainderChildId = childPackageCount > 0 ? existingSettlementTarget(parentInboundId, settlementKey) : 0L;
|
|
if (childPackageCount > 0 && remainderChildId == 0L) {
|
|
remainderChildId = IdWorker.getId();
|
|
}
|
|
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), "余量子入库单");
|
|
}
|
|
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.copyRemainingToChild(parent.tenantId(), remainderChildId, confirmedAllocations);
|
|
}
|
|
allocations.markTransferred(parent.tenantId(), parentInboundId, confirmedAllocations,
|
|
releasedReservationIds);
|
|
allocations.markReleased(parent.tenantId(), parentInboundId, releasedReservationIds);
|
|
settleWarehouseResources(parentInboundId, remainderChildId,
|
|
remainderChildCode == null ? root.inboundCode() : remainderChildCode,
|
|
taskId, operatorId, operatorName,
|
|
warehouseId, parentAvailable, childPackageCount, releasedReservationIds, transferItems);
|
|
return true;
|
|
}
|
|
|
|
private OrderFulfillmentSplitClient.Response createOrderSplit(
|
|
ParentRow source, ParentRow root, List<MeasurementSplitLine> lines,
|
|
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 = lines.stream()
|
|
.map(line -> new OrderFulfillmentSplitClient.QuantityLine(
|
|
line.item().inboundDetailId(), line.item().packageCount()))
|
|
.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 static Map<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
|
|
return taskItems.stream().filter(item -> "PICKED".equals(item.getItemStatus())
|
|
|| "PARTIAL".equals(item.getItemStatus()))
|
|
.collect(Collectors.groupingBy(LoadingPickItemEntity::getInboundId,
|
|
Collectors.summingInt(item -> count(item.getPickedPackageCount(), "PDA已拣箱数"))));
|
|
}
|
|
|
|
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 已拣明细未匹配已审核测量明细");
|
|
}
|
|
List<MeasurementSplitLine> result = new ArrayList<>();
|
|
for (MeasurementItemRow item : measurements) {
|
|
int measured = count(item.packageCount(), "测量明细箱数");
|
|
int picked = pickedByDetail.getOrDefault(item.inboundDetailId(), 0);
|
|
if (picked > measured) {
|
|
throw conflict("PDA 已拣箱数超过测量明细箱数");
|
|
}
|
|
result.add(new MeasurementSplitLine(item, picked, measured - picked));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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 List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
|
|
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
|
|
.sorted().toList();
|
|
}
|
|
|
|
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
|
|
List<InboundPalletAllocationEntity> allocations) {
|
|
return allocations.stream().filter(item -> remaining(item) > 0)
|
|
.map(item -> new WarehousePalletLocationClient.SettlementTransferItem(reservationId(item), remaining(item)))
|
|
.toList();
|
|
}
|
|
|
|
private static int remaining(InboundPalletAllocationEntity allocation) {
|
|
return count(allocation.getAllocatedPackageCount(), "库位已分配箱数")
|
|
- (allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount());
|
|
}
|
|
|
|
private static long reservationId(InboundPalletAllocationEntity allocation) {
|
|
if (allocation.getReservationId() == null || allocation.getReservationId() <= 0) {
|
|
throw conflict("库位资源缺少唯一reservationId,请先重新确认托盘库位分配");
|
|
}
|
|
return allocation.getReservationId();
|
|
}
|
|
|
|
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 settleWarehouseResources(long parentInboundId, long childInboundId, String childCode,
|
|
long taskId, long operatorId, String operatorName, long warehouseId,
|
|
int sourcePackageCount, int targetPackageCount,
|
|
List<Long> releasedReservationIds,
|
|
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
|
|
ApiResponse<WarehousePalletLocationClient.SettlementResult> response;
|
|
try {
|
|
response = warehouse.settle(internalToken, new WarehousePalletLocationClient.SettlementRequest(
|
|
warehouseId, SOURCE_SERVICE, parentInboundId, childInboundId,
|
|
sourcePackageCount, targetPackageCount, childCode, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
|
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
|
releasedReservationIds, remainingItems));
|
|
} catch (FeignException exception) {
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
|
|
}
|
|
if (response == null || !response.successful() || response.data() == null
|
|
|| response.data().released() == null || response.data().transferred() == null
|
|
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|
|
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
|
|
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
|
throw new BizException(code, response == null || response.message() == null
|
|
? "仓储服务原子结算PDA库位资源失败" : response.message());
|
|
}
|
|
}
|
|
|
|
private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) {
|
|
try {
|
|
ApiResponse<Long> response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey);
|
|
return response != null && response.successful() && response.data() != null
|
|
? response.data() : 0L;
|
|
} catch (FeignException exception) {
|
|
return 0L;
|
|
}
|
|
}
|
|
|
|
private static boolean sameReleasedReservations(List<Long> expected,
|
|
List<WarehousePalletLocationClient.SelectiveReleaseResult> actual) {
|
|
return expected.equals(actual.stream().map(WarehousePalletLocationClient.SelectiveReleaseResult::reservationId)
|
|
.sorted().toList());
|
|
}
|
|
|
|
private static boolean sameTransferredResources(List<WarehousePalletLocationClient.SettlementTransferItem> expected,
|
|
List<WarehousePalletLocationClient.TransferResult> actual) {
|
|
Map<Long, Integer> expectedByLocation = expected.stream().collect(Collectors.toMap(
|
|
WarehousePalletLocationClient.SettlementTransferItem::reservationId,
|
|
WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount));
|
|
Map<Long, Integer> actualByLocation = actual.stream().collect(Collectors.toMap(
|
|
WarehousePalletLocationClient.TransferResult::reservationId,
|
|
WarehousePalletLocationClient.TransferResult::remainingPackageCount,
|
|
(left, right) -> -1));
|
|
return expectedByLocation.equals(actualByLocation);
|
|
}
|
|
|
|
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) {
|
|
}
|
|
|
|
}
|