Files
baiyue-firstleg/warehouse/src/main/java/com/baiyue/warehouse/service/PalletLocationAllocationService.java
T
2026-08-10 18:40:43 +08:00

557 lines
33 KiB
Java

package com.baiyue.warehouse.service;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.LocationDtos;
import com.baiyue.warehouse.dto.PalletAllocationDtos;
import com.baiyue.warehouse.entity.PalletEntity;
import com.baiyue.warehouse.entity.PalletLocationTransferOperationEntity;
import com.baiyue.warehouse.entity.PalletLocationReleaseOperationEntity;
import com.baiyue.warehouse.entity.PalletLocationSettlementOperationEntity;
import com.baiyue.warehouse.repository.PalletRepository;
import com.baiyue.warehouse.repository.PalletLocationTransferOperationRepository;
import com.baiyue.warehouse.repository.PalletLocationReleaseOperationRepository;
import com.baiyue.warehouse.repository.PalletLocationSettlementOperationRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 托盘拆分和一托一位编排。库位资源操作全部委托给任务6的 {@link LocationResourceService}。
*/
@Service
public class PalletLocationAllocationService {
/** 测量完成后默认推荐的普通托盘类型编码。 */
public static final String DEFAULT_PALLET_TYPE = "STANDARD";
/** 仓库、托盘和库位属于平台公共资源域。 */
private static final long PLATFORM_RESOURCE_TENANT_ID = 0L;
/** 仅头程服务可结算头程入库单占用的仓储资源。 */
private static final String FIRSTLEG_SOURCE_SERVICE = "firstleg-service";
@Resource
private PalletRepository palletRepository;
@Resource
private PalletVolumeSplitter splitter;
@Resource
private LocationResourceService locationResourceService;
@Resource
private PalletLocationTransferOperationRepository transferOperations;
@Resource
private PalletLocationReleaseOperationRepository releaseOperations;
@Resource
private PalletLocationSettlementOperationRepository settlementOperations;
@Resource
private ObjectMapper objectMapper;
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_RESERVE", resourceType = "INBOUND", resourceId = "#request.businessId", description = "预占托盘和库位", captureBefore = false)
public PalletAllocationDtos.BatchResult reserve(PalletAllocationDtos.ReserveRequest request) {
if (!Boolean.TRUE.equals(request.measurementCompleted())) {
throw new BizException(ResultCode.CONFLICT, "只能使用已完成测量的总体积拆分托盘并占用库位");
}
var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm());
List<LocationDtos.ActiveResource> existing = active(request.tenantId(), request.sourceService(), request.businessId());
if (!existing.isEmpty()) {
return fromActive(existing, split);
}
String palletType = org.springframework.util.StringUtils.hasText(request.palletType())
? request.palletType() : DEFAULT_PALLET_TYPE;
List<PalletEntity> pallets = palletRepository.findAvailable(request.tenantId(), request.warehouseId(),
palletType, request.palletCapacityCbm(), split.palletCount());
if (pallets.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "可用托盘数量不足");
}
List<LocationDtos.ReservationTarget> targets = new ArrayList<>(pallets.size());
for (int index = 0; index < pallets.size(); index++) {
PalletEntity pallet = pallets.get(index);
if (palletRepository.transitionStatus(request.tenantId(), request.warehouseId(), pallet.getId(),
List.of("AVAILABLE"), "RESERVED") != 1) {
throw new BizException(ResultCode.CONFLICT, "托盘被并发预占,请重试");
}
targets.add(new LocationDtos.ReservationTarget(pallet.getId(), pallet.getPalletCode(),
request.idempotencyKey() + ":" + (index + 1)));
}
List<LocationDtos.ReservationResult> reservations = locationResourceService.reserveAvailable(
new LocationDtos.ReserveCommand(request.tenantId(), request.warehouseId(), request.sourceService(),
request.businessId(), request.inboundCode(), request.operatorId(), request.expiresAt(), targets));
if (reservations.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "库位预占结果数量不一致");
}
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(reservations.size());
for (int index = 0; index < reservations.size(); index++) {
var item = reservations.get(index);
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1,
item.palletId(), item.palletCode(), item.locationId(), item.locationCode(),
split.allocations().get(index).allocatedVolumeCbm(), "RESERVED"));
}
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
}
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_CONFIRM", resourceType = "INBOUND", resourceId = "#request.businessId", description = "确认托盘和库位占用")
public PalletAllocationDtos.BatchResult confirm(PalletAllocationDtos.ConfirmRequest request) {
var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm());
List<LocationDtos.ActiveResource> active = active(request.tenantId(), request.sourceService(), request.businessId());
if (active.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致");
}
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
List<LocationDtos.OccupancyTarget> targets = new ArrayList<>(active.size());
int perAllocation = (int) Math.ceil((double) request.packageCount() / active.size());
int remainingPackages = request.packageCount();
for (int index = 0; index < active.size(); index++) {
int allocatedPackages = Math.min(perAllocation, remainingPackages);
remainingPackages -= allocatedPackages;
targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(),
split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages));
}
List<LocationDtos.OccupancyResult> occupied = locationResourceService.confirmOccupied(
new LocationDtos.ConfirmCommand(request.tenantId(), request.warehouseId(), request.sourceService(),
request.businessId(), request.operatorId(), targets));
for (LocationDtos.ActiveResource resource : active) {
if ("RESERVED".equals(resource.state())
&& palletRepository.transitionStatus(request.tenantId(), request.warehouseId(), resource.palletId(),
List.of("RESERVED"), "OCCUPIED") != 1) {
throw new BizException(ResultCode.CONFLICT, "托盘确认占用发生并发冲突");
}
}
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(occupied.size());
for (int index = 0; index < occupied.size(); index++) {
var item = occupied.get(index);
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), active.get(index).reservationNo(), index + 1,
item.palletId(), active.get(index).palletCode(), item.locationId(), item.locationCode(),
split.allocations().get(index).allocatedVolumeCbm(), "OCCUPIED"));
}
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
}
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_RELEASE", resourceType = "INBOUND", resourceId = "#request.businessId", description = "补偿释放托盘和库位预占")
public List<LocationDtos.ActiveResource> release(PalletAllocationDtos.ReleaseRequest request) {
List<LocationDtos.ActiveResource> active = active(request.tenantId(), request.sourceService(), request.businessId());
if (active.stream().anyMatch(item -> "OCCUPIED".equals(item.state()))) {
throw new BizException(ResultCode.CONFLICT, "已确认实物入库的占用不能通过补偿接口释放");
}
List<LocationDtos.ActiveResource> released = locationResourceService.release(
new LocationDtos.ReleaseCommand(request.tenantId(), request.sourceService(), request.businessId(),
request.operatorId(), request.reason()));
for (LocationDtos.ActiveResource resource : released) {
if (palletRepository.transitionStatus(request.tenantId(), resource.warehouseId(), resource.palletId(),
List.of("RESERVED"), "AVAILABLE") != 1) {
throw new BizException(ResultCode.CONFLICT, "托盘释放发生并发冲突");
}
}
return released;
}
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_RELEASE_SELECTED", resourceType = "INBOUND",
resourceId = "#request.businessId", description = "Release selected occupied locations for picking")
public List<PalletAllocationDtos.SelectiveReleaseResult> releaseSelected(
PalletAllocationDtos.SelectiveReleaseRequest request) {
String key = request.idempotencyKey().trim();
String requestHash = selectiveReleaseRequestHash(request);
boolean created = releaseOperations.createIfAbsent(request.tenantId(), request.sourceService(),
request.businessId(), key, requestHash);
PalletLocationReleaseOperationEntity operation = releaseOperations.lockByKey(request.tenantId(),
request.sourceService(), request.businessId(), key);
if (operation == null) {
throw new BizException(ResultCode.CONFLICT, "Selected release idempotency operation is missing");
}
if (!requestHash.equals(operation.getRequestHash())) {
throw new BizException(ResultCode.CONFLICT, "Idempotency key has already been used for a different selected release request");
}
if (!created) {
if ("COMPLETED".equals(operation.getOperationStatus())) {
return replaySelectiveReleaseResult(operation.getResponseSnapshot());
}
throw new BizException(ResultCode.CONFLICT, "Selected release request is being processed, retry later");
}
List<LocationDtos.ExactSelectiveReleaseResult> released = locationResourceService.releaseSelectedExactly(
new LocationDtos.ExactSelectiveReleaseCommand(request.tenantId(), request.sourceService(), request.businessId(),
request.operatorId(), request.reason(), request.locationIds()));
List<PalletAllocationDtos.SelectiveReleaseResult> result = new ArrayList<>(released.size());
for (LocationDtos.ExactSelectiveReleaseResult resource : released) {
if (palletRepository.transitionStatus(request.tenantId(), resource.warehouseId(), resource.palletId(),
List.of("OCCUPIED"), "AVAILABLE") != 1) {
throw new BizException(ResultCode.CONFLICT, "Pallet changed concurrently during selected release");
}
result.add(new PalletAllocationDtos.SelectiveReleaseResult(resource.reservationId(),
resource.occupancyId(), resource.allocationNo(), resource.palletId(), resource.locationId(),
resource.locationCode(), resource.allocatedPackageCount()));
}
List<PalletAllocationDtos.SelectiveReleaseResult> immutable = List.copyOf(result);
releaseOperations.complete(operation.getId(), serializeSelectiveReleaseResult(immutable));
return immutable;
}
/**
* PDA resource settlement is deliberately one transaction. A resource cannot be released
* successfully while the matching remaining resources fail to transfer to the child inbound.
*/
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_SETTLEMENT", resourceType = "INBOUND",
resourceId = "#request.sourceBusinessId", description = "PDA拣货后原子结算托盘库位资源")
public PalletAllocationDtos.SettlementResult settle(PalletAllocationDtos.SettlementRequest request) {
validateSettlementScope(request);
String key = request.idempotencyKey().trim();
String requestHash = settlementRequestHash(request);
boolean created = settlementOperations.createIfAbsent(request.sourceBusinessId(), request.targetBusinessId(),
key, requestHash);
PalletLocationSettlementOperationEntity operation = settlementOperations.lockByKey(request.sourceBusinessId(), key);
if (operation == null) {
throw new BizException(ResultCode.CONFLICT, "PDA settlement idempotency operation is missing");
}
if (!requestHash.equals(operation.getRequestHash())) {
throw new BizException(ResultCode.CONFLICT, "Idempotency key has already been used for a different PDA settlement request");
}
if (!created) {
if ("COMPLETED".equals(operation.getOperationStatus())) {
return replaySettlementResult(operation.getResponseSnapshot());
}
throw new BizException(ResultCode.CONFLICT, "PDA settlement request is being processed, retry later");
}
List<LocationDtos.OccupiedResource> sourceResources = locationResourceService.lockOccupiedResources(
PLATFORM_RESOURCE_TENANT_ID, FIRSTLEG_SOURCE_SERVICE, request.sourceBusinessId());
validateSettlement(request, sourceResources);
List<PalletAllocationDtos.SelectiveReleaseResult> released = releasePickedResources(request);
List<PalletAllocationDtos.TransferResult> transferred = transferRemainingResources(request);
PalletAllocationDtos.SettlementResult result = new PalletAllocationDtos.SettlementResult(released, transferred);
settlementOperations.complete(operation.getId(), serializeSettlementResult(result));
return result;
}
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_REBIND_TEMPORARY", resourceType = "LOCATION_OCCUPANCY",
resourceId = "#request.occupancyId", description = "A0临时库位换绑到普通库位")
public PalletAllocationDtos.RebindTemporaryLocationResult rebindTemporaryLocation(
PalletAllocationDtos.RebindTemporaryLocationRequest request) {
LocationDtos.RebindTemporaryLocationResult result = locationResourceService.rebindTemporaryLocation(
new LocationDtos.RebindTemporaryLocationCommand(request.tenantId(), request.occupancyId(),
request.targetLocationId(), request.operatorId()));
return new PalletAllocationDtos.RebindTemporaryLocationResult(result.occupancyId(), result.reservationId(),
result.warehouseId(), result.palletId(), result.previousLocationId(), result.previousLocationCode(),
result.locationId(), result.locationCode());
}
private List<PalletAllocationDtos.SelectiveReleaseResult> releasePickedResources(
PalletAllocationDtos.SettlementRequest request) {
if (request.releasedReservationIds().isEmpty()) {
return List.of();
}
List<LocationDtos.ExactSelectiveReleaseResult> released = locationResourceService.releaseSelectedReservations(
new LocationDtos.ReservationReleaseCommand(PLATFORM_RESOURCE_TENANT_ID, FIRSTLEG_SOURCE_SERVICE,
request.sourceBusinessId(), request.operatorId(), settlementReason(request), request.releasedReservationIds()));
List<PalletAllocationDtos.SelectiveReleaseResult> result = new ArrayList<>(released.size());
for (LocationDtos.ExactSelectiveReleaseResult resource : released) {
if (palletRepository.transitionStatus(PLATFORM_RESOURCE_TENANT_ID, resource.warehouseId(), resource.palletId(),
List.of("OCCUPIED"), "AVAILABLE") != 1) {
throw new BizException(ResultCode.CONFLICT, "Pallet changed concurrently during PDA settlement");
}
result.add(new PalletAllocationDtos.SelectiveReleaseResult(resource.reservationId(),
resource.occupancyId(), resource.allocationNo(), resource.palletId(), resource.locationId(),
resource.locationCode(), resource.allocatedPackageCount()));
}
return List.copyOf(result);
}
private List<PalletAllocationDtos.TransferResult> transferRemainingResources(
PalletAllocationDtos.SettlementRequest request) {
if (request.remainingItems().isEmpty()) {
return List.of();
}
List<LocationDtos.TransferResult> transferred = locationResourceService.transferToChildByReservation(
new LocationDtos.ReservationTransferCommand(PLATFORM_RESOURCE_TENANT_ID, FIRSTLEG_SOURCE_SERVICE,
request.sourceBusinessId(), request.targetBusinessId(), request.targetInboundCode(), request.operatorId(),
request.remainingItems().stream()
.map(item -> new LocationDtos.ReservationTransferItem(item.reservationId(),
item.remainingPackageCount()))
.toList()));
return transferResults(PLATFORM_RESOURCE_TENANT_ID, transferred);
}
/** Same idempotency key must represent the same resource selection, regardless of request list ordering. */
private static String selectiveReleaseRequestHash(PalletAllocationDtos.SelectiveReleaseRequest request) {
String locationIds = request.locationIds() == null ? "" : request.locationIds().stream()
.sorted()
.map(String::valueOf)
.collect(Collectors.joining(","));
String canonical = request.tenantId() + "|" + request.sourceService().trim() + "|"
+ request.businessId() + "|" + request.operatorId() + "|" + request.reason().trim() + "|"
+ locationIds;
return sha256(canonical);
}
/** 将未拣货的库位和托盘分配转给二级子入库单,物理资源继续保持占用。 */
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_TRANSFER", resourceType = "INBOUND",
resourceId = "#request.sourceBusinessId", description = "将未拣货库位转给子入库单")
public List<PalletAllocationDtos.TransferResult> transfer(PalletAllocationDtos.TransferRequest request) {
String key = request.idempotencyKey().trim();
String requestHash = transferRequestHash(request);
boolean created = transferOperations.createIfAbsent(request.tenantId(), request.sourceService(),
request.sourceBusinessId(), request.targetBusinessId(), key, requestHash);
PalletLocationTransferOperationEntity operation = transferOperations.lockByKey(request.tenantId(),
request.sourceService(), request.sourceBusinessId(), request.targetBusinessId(), key);
if (operation == null) {
throw new BizException(ResultCode.CONFLICT, "调拨幂等操作记录不存在");
}
if (!requestHash.equals(operation.getRequestHash())) {
throw new BizException(ResultCode.CONFLICT, "幂等键已用于不同的调拨请求");
}
if (!created) {
if ("COMPLETED".equals(operation.getOperationStatus())) {
return replayTransferResult(operation.getResponseSnapshot());
}
throw new BizException(ResultCode.CONFLICT, "调拨请求正在处理中,请稍后重试");
}
List<PalletAllocationDtos.TransferResult> immutable = transferResources(request.tenantId(), request.sourceService(),
request.sourceBusinessId(), request.targetBusinessId(), request.targetInboundCode(), request.operatorId(),
request.items());
transferOperations.complete(operation.getId(), serializeTransferResult(immutable));
return immutable;
}
private List<PalletAllocationDtos.TransferResult> transferResources(long tenantId, String sourceService,
long sourceBusinessId, long targetBusinessId, String targetInboundCode, long operatorId,
List<PalletAllocationDtos.TransferItem> items) {
List<LocationDtos.TransferResult> transferred = locationResourceService.transferToChild(
new LocationDtos.TransferCommand(tenantId, sourceService, sourceBusinessId, targetBusinessId,
targetInboundCode, operatorId, items.stream()
.map(item -> new LocationDtos.TransferItem(item.locationId(), item.remainingPackageCount()))
.toList()));
return transferResults(tenantId, transferred);
}
private List<PalletAllocationDtos.TransferResult> transferResults(long tenantId,
List<LocationDtos.TransferResult> transferred) {
List<PalletAllocationDtos.TransferResult> result = new ArrayList<>(transferred.size());
for (LocationDtos.TransferResult item : transferred) {
PalletEntity pallet = palletRepository.findOwned(tenantId, item.warehouseId(), item.palletId());
if (pallet == null || !"OCCUPIED".equals(pallet.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "转移托盘不存在或未处于占用状态");
}
result.add(new PalletAllocationDtos.TransferResult(item.reservationId(), item.reservationNo(),
item.occupancyId(), item.allocationNo(), tenantId, item.warehouseId(), item.palletId(),
item.palletCode(), pallet.getCapacityCbm(), item.locationId(), item.locationCode(),
item.allocatedVolumeCbm(), item.remainingPackageCount(), item.state()));
}
return List.copyOf(result);
}
/** 同一幂等键必须携带完全一致的调拨目标和库位箱数。 */
private static String transferRequestHash(PalletAllocationDtos.TransferRequest request) {
String items = request.items().stream()
.sorted(Comparator.comparingLong(PalletAllocationDtos.TransferItem::locationId))
.map(item -> item.locationId() + ":" + item.remainingPackageCount())
.collect(Collectors.joining(","));
String canonical = request.tenantId() + "|" + request.sourceService().trim() + "|"
+ request.sourceBusinessId() + "|" + request.targetBusinessId() + "|"
+ request.targetInboundCode().trim() + "|" + request.operatorId() + "|" + items;
return sha256(canonical);
}
private static String settlementRequestHash(PalletAllocationDtos.SettlementRequest request) {
String released = request.releasedReservationIds().stream().sorted().map(String::valueOf)
.collect(Collectors.joining(","));
String remaining = request.remainingItems().stream()
.sorted(Comparator.comparingLong(PalletAllocationDtos.SettlementTransferItem::reservationId))
.map(item -> item.reservationId() + ":" + item.remainingPackageCount())
.collect(Collectors.joining(","));
return sha256(request.warehouseId() + "|" + request.sourceService().trim() + "|"
+ request.sourceBusinessId() + "|" + request.targetBusinessId() + "|" + request.sourcePackageCount()
+ "|" + request.targetPackageCount() + "|" + normalize(request.targetInboundCode()) + "|"
+ request.operatorId() + "|" + normalize(request.operatorName()) + "|"
+ request.requestId().trim() + "|" + released + "|" + remaining);
}
private static void validateSettlementScope(PalletAllocationDtos.SettlementRequest request) {
if (!FIRSTLEG_SOURCE_SERVICE.equals(request.sourceService())) {
throw new BizException(ResultCode.FORBIDDEN, "PDA仓储资源仅允许头程服务结算");
}
}
private static String settlementReason(PalletAllocationDtos.SettlementRequest request) {
String operator = normalize(request.operatorName());
return operator.isEmpty() ? "PDA拣货资源结算:" + request.requestId().trim()
: "PDA拣货资源结算:" + request.requestId().trim() + ",操作人:" + operator;
}
private static void validateSettlement(PalletAllocationDtos.SettlementRequest request,
List<LocationDtos.OccupiedResource> sourceResources) {
java.util.Set<Long> releasedIds = new java.util.HashSet<>(request.releasedReservationIds());
if (releasedIds.size() != request.releasedReservationIds().size()) {
throw new BizException(ResultCode.BAD_REQUEST, "已拣满预约记录不能重复");
}
java.util.Map<Long, PalletAllocationDtos.SettlementTransferItem> remainingByReservation =
request.remainingItems().stream().collect(Collectors.toMap(
PalletAllocationDtos.SettlementTransferItem::reservationId, item -> item,
(first, duplicate) -> {
throw new BizException(ResultCode.BAD_REQUEST, "剩余预约记录不能重复");
}));
if (releasedIds.stream().anyMatch(remainingByReservation::containsKey)) {
throw new BizException(ResultCode.BAD_REQUEST, "同一预约记录不能同时释放和转移");
}
if (releasedIds.isEmpty() && remainingByReservation.isEmpty()) {
throw new BizException(ResultCode.BAD_REQUEST, "PDA结算必须指定已拣满或剩余的预约记录");
}
java.util.Map<Long, LocationDtos.OccupiedResource> sourceByReservation = sourceResources.stream()
.collect(Collectors.toMap(LocationDtos.OccupiedResource::reservationId, item -> item,
(first, duplicate) -> {
throw new BizException(ResultCode.CONFLICT, "同一预约记录存在多条活动占用");
}));
java.util.Set<Long> selectedIds = new java.util.HashSet<>(releasedIds);
selectedIds.addAll(remainingByReservation.keySet());
if (!sourceByReservation.keySet().equals(selectedIds)) {
throw new BizException(ResultCode.FORBIDDEN, "PDA结算预约记录必须完整且属于来源入库单");
}
int allocatedTotal = 0;
int pickedTotal = 0;
int remainingTotal = 0;
for (LocationDtos.OccupiedResource resource : sourceResources) {
if (resource.warehouseId() != request.warehouseId()) {
throw new BizException(ResultCode.FORBIDDEN, "PDA结算预约记录不属于指定仓库");
}
allocatedTotal = addPackages(allocatedTotal, resource.allocatedPackageCount());
PalletAllocationDtos.SettlementTransferItem remaining = remainingByReservation.get(resource.reservationId());
if (releasedIds.contains(resource.reservationId())) {
pickedTotal = addPackages(pickedTotal, resource.allocatedPackageCount());
continue;
}
if (remaining.remainingPackageCount() > resource.allocatedPackageCount()) {
throw new BizException(ResultCode.BAD_REQUEST, "剩余箱数不能超过预约记录原分配箱数");
}
remainingTotal = addPackages(remainingTotal, remaining.remainingPackageCount());
pickedTotal = addPackages(pickedTotal, resource.allocatedPackageCount() - remaining.remainingPackageCount());
}
if (request.sourcePackageCount() != allocatedTotal) {
throw new BizException(ResultCode.CONFLICT, "来源入库单箱数与当前库位分配箱数不一致");
}
if (request.targetPackageCount() != remainingTotal) {
throw new BizException(ResultCode.BAD_REQUEST, "目标入库单箱数必须等于剩余预约记录箱数合计");
}
if (request.sourcePackageCount() != addPackages(pickedTotal, request.targetPackageCount())) {
throw new BizException(ResultCode.BAD_REQUEST, "来源箱数必须等于已拣箱数与目标箱数之和");
}
if (request.targetPackageCount() > 0
&& (request.targetBusinessId() < 1 || request.sourceBusinessId() == request.targetBusinessId())) {
throw new BizException(ResultCode.BAD_REQUEST, "存在剩余箱数时必须指定不同的子入库单");
}
if (request.targetPackageCount() > 0 && !org.springframework.util.StringUtils.hasText(request.targetInboundCode())) {
throw new BizException(ResultCode.BAD_REQUEST, "存在剩余箱数时必须指定子入库单号");
}
}
private static int addPackages(int current, int increment) {
try {
return Math.addExact(current, increment);
} catch (ArithmeticException exception) {
throw new BizException(ResultCode.BAD_REQUEST, "箱数合计超出允许范围");
}
}
private static String normalize(String value) {
return value == null ? "" : value.trim();
}
private static String sha256(String canonical) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(canonical.getBytes(StandardCharsets.UTF_8));
StringBuilder value = new StringBuilder(64);
for (byte item : digest) {
value.append(String.format("%02x", item));
}
return value.toString();
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 算法不可用", exception);
}
}
private String serializeSelectiveReleaseResult(List<PalletAllocationDtos.SelectiveReleaseResult> result) {
try {
return objectMapper.writeValueAsString(result);
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "Selected release result snapshot could not be saved");
}
}
private List<PalletAllocationDtos.SelectiveReleaseResult> replaySelectiveReleaseResult(String snapshot) {
try {
return List.copyOf(objectMapper.readValue(snapshot,
new TypeReference<List<PalletAllocationDtos.SelectiveReleaseResult>>() { }));
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "Selected release idempotency result snapshot is invalid");
}
}
private String serializeSettlementResult(PalletAllocationDtos.SettlementResult result) {
try {
return objectMapper.writeValueAsString(result);
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "PDA settlement result snapshot could not be saved");
}
}
private PalletAllocationDtos.SettlementResult replaySettlementResult(String snapshot) {
try {
return objectMapper.readValue(snapshot, PalletAllocationDtos.SettlementResult.class);
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "PDA settlement idempotency result snapshot is invalid");
}
}
private String serializeTransferResult(List<PalletAllocationDtos.TransferResult> result) {
try {
return objectMapper.writeValueAsString(result);
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "调拨结果快照保存失败");
}
}
private List<PalletAllocationDtos.TransferResult> replayTransferResult(String snapshot) {
try {
return List.copyOf(objectMapper.readValue(snapshot,
new TypeReference<List<PalletAllocationDtos.TransferResult>>() { }));
} catch (Exception exception) {
throw new BizException(ResultCode.CONFLICT, "调拨幂等结果快照无效");
}
}
private List<LocationDtos.ActiveResource> active(long tenantId, String sourceService, long businessId) {
return new ArrayList<>(locationResourceService.findActiveResources(tenantId, sourceService, businessId));
}
private static PalletAllocationDtos.BatchResult fromActive(List<LocationDtos.ActiveResource> active,
PalletVolumeSplitter.SplitResult split) {
if (active.size() != split.palletCount()) {
throw new BizException(ResultCode.CONFLICT, "幂等请求与已有托盘拆分结果不一致");
}
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(active.size());
for (int index = 0; index < active.size(); index++) {
var item = active.get(index);
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1,
item.palletId(), item.palletCode(), item.locationId(), item.locationCode(),
split.allocations().get(index).allocatedVolumeCbm(), item.state()));
}
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
}
}