Files
baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java
T
2026-08-13 14:37:51 +08:00

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) {
}
}