fix: allocate pallet locations on weight confirmation

This commit is contained in:
2026-09-04 13:21:37 +08:00
parent d6956d396c
commit 944ecb2bf3
21 changed files with 581 additions and 77 deletions
@@ -38,6 +38,10 @@ public interface WarehousePalletLocationClient {
ApiResponse<BlockResult> confirmBlock(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BlockConfirmRequest request);
@PostMapping("/pallet-locations/blocks/expand")
ApiResponse<BlockResult> expandBlock(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BlockExpandRequest request);
@PostMapping("/pallet-locations/blocks/transfer")
ApiResponse<BlockResult> transferBlock(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BlockTransferRequest request);
@@ -51,6 +55,8 @@ public interface WarehousePalletLocationClient {
long operatorId, LocalDateTime expiresAt) {}
record BlockConfirmRequest(long blockId, long businessId, String idempotencyKey,
long operatorId) {}
record BlockExpandRequest(long blockId, long businessId, int requiredSlotCount,
String idempotencyKey, long operatorId) {}
record BlockTransferRequest(long blockId, long sourceBusinessId, long targetBusinessId,
String targetInboundCode, String idempotencyKey, long operatorId) {}
record BlockReleaseRequest(long blockId, long businessId, String idempotencyKey,
@@ -67,6 +67,8 @@ public class InboundMeasurementService {
private WarehousePalletLocationClient warehousePallets;
@Resource
private InboundPalletAllocationRepository palletAllocations;
@Resource
private MeasurementLocationOccupationService locationOccupation;
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
@@ -296,6 +298,8 @@ public class InboundMeasurementService {
request.adjustedWeightValue(), request.adjustedWeightUnit(),
request.adjustmentReason(), operatorId, operatorName);
} else if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
locationOccupation.ensureForConfirmedWeight(
tenantId, inboundId, operatorId, weight.getId(), weight.getVersion());
return toWeight(weight);
}
BigDecimal value = effectiveValue(weight);
@@ -324,10 +328,9 @@ public class InboundMeasurementService {
weight.setVersion(confirmedVersion);
insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit,
grossWeightKg, netWeightKg, null, operatorId, operatorName);
if (!palletAllocations.list(tenantId, inboundId).isEmpty()) {
palletAllocations.bindNextEmpty(tenantId, inboundId, weight);
}
recalculateConfirmedWeight(inbound, operatorId);
locationOccupation.ensureForConfirmedWeight(
tenantId, inboundId, operatorId, weight.getId(), confirmedVersion);
return toWeight(weight);
}
@@ -23,7 +23,6 @@ import java.util.UUID;
@Service
public class MeasurementLocationOccupationService {
private static final String SOURCE_SERVICE = "firstleg-service";
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
@Resource
private WarehousePalletLocationClient warehouse;
@Resource
@@ -37,26 +36,43 @@ public class MeasurementLocationOccupationService {
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
public void occupy(long tenantId, long inboundId, long operatorId, long measurementId,
int measurementVersion) {
public void ensureForConfirmedWeight(long tenantId, long inboundId, long operatorId,
long weightResultId, int weightVersion) {
InboundOrderEntity order = requireOrder(tenantId, inboundId);
long warehouseId = requireWarehouse(order);
BigDecimal volume = requireVolume(order);
var confirmedWeights = confirmedWeights(tenantId, inboundId);
int requiredSlotCount = requiredSlotCount(volume, confirmedWeights.size());
int packageCount = requirePackageCount(order);
var current = allocations.list(tenantId, inboundId);
if (!current.isEmpty()) {
long blockId = requireActiveBlock(current, warehouseId);
if (current.size() < requiredSlotCount) {
String expandKey = "measure-block-expand-" + inboundId + "-" + weightResultId + "-" + weightVersion;
WarehousePalletLocationClient.BlockResult expanded = requireSuccess(call(() -> warehouse.expandBlock(
internalToken, new WarehousePalletLocationClient.BlockExpandRequest(blockId, inboundId,
requiredSlotCount, expandKey, operatorId))), "称重确认后整票库位块扩容失败");
requireBlockResult(expanded, requiredSlotCount, "OCCUPIED");
if (expanded.blockId() != blockId) {
throw new BizException(ResultCode.CONFLICT, "Warehouse扩容返回了不同整票库位块");
}
requirePreservedSlots(current, expanded);
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, expanded);
}
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
return;
}
// Warehouse retains released blocks for audit. A compensated attempt therefore needs a new
// reservation key, otherwise a retry would replay the released block instead of reserving again.
String key = "measure-block-" + inboundId + "-" + measurementId + "-" + measurementVersion
String key = "measure-block-" + inboundId + "-" + weightResultId + "-" + weightVersion
+ "-" + UUID.randomUUID();
WarehousePalletLocationClient.BlockResult reserved = requireSuccess(call(() -> warehouse.reserveBlock(
internalToken, new WarehousePalletLocationClient.BlockReserveRequest(warehouseId,
SOURCE_SERVICE, inboundId, order.getInboundCode(), requiredSlotCount, key, operatorId,
LocalDateTime.now().plus(properties.getReservationTtl())))),
"测量完成后库位预占失败");
int packageCount = requirePackageCount(order);
requireBlockResult(reserved, requiredSlotCount, "RESERVED");
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, reserved);
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
try {
WarehousePalletLocationClient.BlockResult occupied = requireSuccess(call(() -> warehouse.confirmBlock(
internalToken, new WarehousePalletLocationClient.BlockConfirmRequest(reserved.blockId(), inboundId,
@@ -75,6 +91,68 @@ public class MeasurementLocationOccupationService {
}
}
public void requireOccupied(long tenantId, long inboundId) {
InboundOrderEntity order = requireOrder(tenantId, inboundId);
long warehouseId = requireWarehouse(order);
var confirmedWeights = confirmedWeights(tenantId, inboundId);
int requiredSlotCount = requiredSlotCount(requireVolume(order), confirmedWeights.size());
var current = allocations.list(tenantId, inboundId);
requireActiveBlock(current, warehouseId);
if (current.size() < requiredSlotCount) {
throw new BizException(ResultCode.CONFLICT, "整票库位块容量不足,请重新确认称重图片");
}
requireAllWeightsBound(current, confirmedWeights);
}
private long requireActiveBlock(java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
long warehouseId) {
if (current.isEmpty() || current.get(0).getBlockId() == null || current.get(0).getBlockId() <= 0) {
throw new BizException(ResultCode.CONFLICT, "称重图片尚未建立整票库位块");
}
long blockId = current.get(0).getBlockId();
if (current.stream().anyMatch(slot -> !Long.valueOf(blockId).equals(slot.getBlockId())
|| !Long.valueOf(warehouseId).equals(slot.getWarehouseId())
|| !"CONFIRMED".equals(slot.getAllocationStatus())
|| slot.getSlotNo() == null || slot.getSlotNo() <= 0
|| slot.getReservationId() == null || slot.getReservationId() <= 0
|| slot.getLocationId() == null || slot.getLocationId() <= 0)) {
throw new BizException(ResultCode.CONFLICT, "整票库位块快照不完整或状态异常");
}
java.util.List<Integer> slotNos = current.stream().map(item -> item.getSlotNo()).sorted().toList();
if (!slotNos.equals(java.util.stream.IntStream.rangeClosed(1, current.size()).boxed().toList())) {
throw new BizException(ResultCode.CONFLICT, "整票库位块槽序不连续");
}
return blockId;
}
private static void requireAllWeightsBound(
java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
java.util.List<InboundWeightResultEntity> confirmedWeights) {
java.util.Set<Long> bound = current.stream()
.filter(item -> "BOUND".equals(item.getSlotStatus()))
.map(item -> item.getWeightResultId()).filter(java.util.Objects::nonNull)
.collect(java.util.stream.Collectors.toSet());
if (confirmedWeights.stream().map(InboundWeightResultEntity::getId).anyMatch(id -> !bound.contains(id))) {
throw new BizException(ResultCode.CONFLICT, "已确认称重图片未完整绑定整票库位槽");
}
}
private static void requirePreservedSlots(
java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
WarehousePalletLocationClient.BlockResult expanded) {
java.util.Map<Integer, WarehousePalletLocationClient.BlockResource> bySlot = expanded.resources().stream()
.collect(java.util.stream.Collectors.toMap(WarehousePalletLocationClient.BlockResource::slotNo,
java.util.function.Function.identity()));
if (current.stream().anyMatch(slot -> {
WarehousePalletLocationClient.BlockResource resource = bySlot.get(slot.getSlotNo());
return resource == null || resource.reservationId() != slot.getReservationId()
|| resource.locationId() != slot.getLocationId()
|| resource.sharedOverflow() != Boolean.TRUE.equals(slot.getSharedOverflow());
})) {
throw new BizException(ResultCode.CONFLICT, "Warehouse扩容改变了既有库位槽");
}
}
private InboundOrderEntity requireOrder(long tenantId, long inboundId) {
InboundOrderEntity order = workflow.findOwned(tenantId, inboundId);
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
@@ -154,7 +232,8 @@ public class MeasurementLocationOccupationService {
int requiredSlotCount, String expectedStatus) {
if (result.blockId() <= 0 || !expectedStatus.equals(result.blockStatus())
|| result.resources() == null || result.resources().size() != requiredSlotCount
|| result.resources().stream().anyMatch(resource -> resource.blockId() != result.blockId())) {
|| result.resources().stream().anyMatch(resource -> resource.blockId() != result.blockId()
|| !expectedStatus.equals(resource.resourceStatus()))) {
throw new BizException(ResultCode.CONFLICT, "Warehouse返回的整票库位块不完整");
}
java.util.List<Integer> slotNos = result.resources().stream()
@@ -19,7 +19,7 @@ public class MeasurementSubmissionService {
long tenantId, long inboundId, long operatorId,
InboundMeasurementRequests.SubmitRequest request) {
var result = measurements.submit(tenantId, inboundId, operatorId, request);
locations.occupy(tenantId, inboundId, operatorId, result.measurementId(), result.version());
locations.requireOccupied(tenantId, inboundId);
return result;
}
}