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
@@ -20,6 +20,14 @@ class InboundMeasurementEvidenceOpenApiContractTest {
Map<String, Object> detailPath = stringMap(paths.get(
"/api/platform/v1/first-leg/inbounds/{inboundId}/measurement"));
assertThat(detailPath).containsKey("get");
Map<String, Object> confirmOperation = stringMap(stringMap(paths.get(
"/api/platform/v1/first-leg/inbounds/{inboundId}/weight-results/{resultId}/confirm")).get("post"));
assertThat(confirmOperation.get("description").toString())
.contains("首张图片确认成功时", "原子扩容原块", "确认响应立即返回");
Map<String, Object> submitOperation = stringMap(stringMap(paths.get(
"/api/platform/v1/inbounds/{inboundId}/measurements/submit")).get("post"));
assertThat(submitOperation.get("description").toString())
.contains("只校验并复用", "不会首次分配");
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
@@ -61,6 +61,7 @@ class InboundMeasurementServiceTest {
@MockBean private MeasurementImageClient imageClient;
@MockBean private WarehousePalletLocationClient warehousePallets;
@MockBean private InboundPalletAllocationRepository palletAllocations;
@MockBean private MeasurementLocationOccupationService locationOccupation;
@Resource private InboundMeasurementService service;
@BeforeEach
@@ -555,7 +556,7 @@ class InboundMeasurementServiceTest {
}
@Test
void confirmingAWeightBindsItToTheNextEmptySlotWhenTheTicketAlreadyHasABlock() {
void confirmingAWeightReturnsTheAuthoritativeSlotBoundByTheLocationOrchestrator() {
InboundOrderEntity inbound = inbound("MEASURE_REJECTED");
InboundWeightResultEntity weight = weight(20L, "30.000", "KG", "PENDING_CONFIRM", 0);
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
@@ -564,11 +565,18 @@ class InboundMeasurementServiceTest {
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
when(palletAllocations.list(1L, 10L)).thenReturn(List.of(
locationSlot(1, 801L, "A01", "RESERVED_EMPTY", null)));
when(palletAllocations.findByWeightResult(1L, 10L, 20L)).thenReturn(
locationSlot(1, 801L, "A01", "BOUND", 20L));
service.confirmWeightResult(1L, 10L, 20L, 9L, "操作员",
InboundMeasurementResponses.WeightResult result = service.confirmWeightResult(
1L, 10L, 20L, 9L, "操作员",
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null));
verify(palletAllocations).bindNextEmpty(1L, 10L, weight);
verify(locationOccupation).ensureForConfirmedWeight(1L, 10L, 9L, 20L, 1);
assertThat(result.blockId()).isEqualTo(700L);
assertThat(result.slotNo()).isEqualTo(1);
assertThat(result.locationCode()).isEqualTo("A01");
assertThat(result.slotStatus()).isEqualTo("BOUND");
}
@Test
@@ -57,7 +57,7 @@ class MeasurementLocationOccupationServiceTest {
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
ArgumentCaptor<WarehousePalletLocationClient.BlockReserveRequest> reserve =
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReserveRequest.class);
@@ -74,14 +74,14 @@ class MeasurementLocationOccupationServiceTest {
assertThat(confirm.getValue().idempotencyKey())
.isEqualTo(reserve.getValue().idempotencyKey() + "-confirm");
verify(allocations, times(2)).replaceBlock(
verify(allocations).replaceBlock(
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
org.mockito.ArgumentMatchers.eq(20),
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.0000")),
org.mockito.ArgumentMatchers.any());
verify(allocations, times(2)).bindConfirmedWeights(
verify(allocations).bindConfirmedWeights(
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
org.mockito.ArgumentMatchers.eq(INBOUND), org.mockito.ArgumentMatchers.anyList());
}
@@ -96,7 +96,7 @@ class MeasurementLocationOccupationServiceTest {
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
ArgumentCaptor<WarehousePalletLocationClient.BlockReserveRequest> reserve =
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReserveRequest.class);
@@ -108,7 +108,7 @@ class MeasurementLocationOccupationServiceTest {
verify(warehouse).confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
verify(allocations, times(2)).replaceBlock(
verify(allocations).replaceBlock(
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
org.mockito.ArgumentMatchers.eq(INBOUND),
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
@@ -129,11 +129,11 @@ class MeasurementLocationOccupationServiceTest {
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3);
verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.argThat(request -> request.requiredSlotCount() == 3));
verify(allocations, times(2)).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, twoWeights);
verify(allocations).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, twoWeights);
}
@Test
@@ -143,7 +143,7 @@ class MeasurementLocationOccupationServiceTest {
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(weight));
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
assertThatThrownBy(() -> service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
.isInstanceOf(BizException.class)
.hasMessageContaining("缺少确认时间");
@@ -157,7 +157,7 @@ class MeasurementLocationOccupationServiceTest {
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(weight));
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
assertThatThrownBy(() -> service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
.isInstanceOf(BizException.class)
.hasMessageContaining("缺少有效净重");
@@ -171,7 +171,7 @@ class MeasurementLocationOccupationServiceTest {
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(weight));
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
assertThatThrownBy(() -> service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
.isInstanceOf(BizException.class)
.hasMessageContaining("缺少有效托盘");
@@ -191,15 +191,64 @@ class MeasurementLocationOccupationServiceTest {
when(warehouse.releaseBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RELEASED", 3)));
assertThatThrownBy(() -> service.occupy(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
assertThatThrownBy(() -> service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, MEASUREMENT, 3))
.isSameAs(failure);
ArgumentCaptor<WarehousePalletLocationClient.BlockReleaseRequest> release =
ArgumentCaptor.forClass(WarehousePalletLocationClient.BlockReleaseRequest.class);
verify(warehouse).releaseBlock(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
verify(allocations, never()).replaceBlock(org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyInt(), org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any());
}
@Test
void expandsOccupiedBlockWithoutChangingExistingSlotsWhenConfirmedCountGrows() {
InboundOrderEntity inbound = inbound();
List<InboundWeightResultEntity> weights = confirmedWeights();
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current =
java.util.stream.IntStream.rangeClosed(1, 2).mapToObj(MeasurementLocationOccupationServiceTest::slot)
.toList();
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound);
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
when(allocations.list(BUSINESS_TENANT, INBOUND)).thenReturn(current);
when(warehouse.expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
service.ensureForConfirmedWeight(BUSINESS_TENANT, INBOUND, OPERATOR, 803L, 1);
verify(warehouse).expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.argThat(request -> request.blockId() == 500L
&& request.businessId() == INBOUND && request.requiredSlotCount() == 3
&& request.idempotencyKey().equals("measure-block-expand-88-803-1")));
verify(allocations).replaceBlock(BUSINESS_TENANT, INBOUND, WAREHOUSE,
20, new BigDecimal("1.0000"), block("RESERVED", 3));
20, new BigDecimal("1.0000"), block("OCCUPIED", 3));
verify(allocations).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, weights);
}
@Test
void submitValidationReusesExistingBoundBlockWithoutWarehouseMutation() {
List<InboundWeightResultEntity> weights = confirmedWeights().subList(0, 2);
var first = slot(1);
var second = slot(2);
first.setWeightResultId(weights.get(0).getId());
second.setWeightResultId(weights.get(1).getId());
first.setSlotStatus("BOUND");
second.setSlotStatus("BOUND");
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
when(allocations.list(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(first, second));
service.requireOccupied(BUSINESS_TENANT, INBOUND);
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
verify(warehouse, never()).confirmBlock(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
verify(warehouse, never()).expandBlock(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
}
private static InboundProperties properties() {
@@ -235,6 +284,20 @@ class MeasurementLocationOccupationServiceTest {
.toList());
}
private static com.baiyue.firstleg.entity.InboundPalletAllocationEntity slot(int slotNo) {
var value = new com.baiyue.firstleg.entity.InboundPalletAllocationEntity();
value.setBlockId(500L);
value.setSlotNo(slotNo);
value.setWarehouseId(WAREHOUSE);
value.setReservationId(600L + slotNo);
value.setLocationId(700L + slotNo);
value.setLocationCode("A-00" + slotNo);
value.setSharedOverflow(false);
value.setAllocationStatus("CONFIRMED");
value.setSlotStatus("RESERVED_EMPTY");
return value;
}
private static List<InboundWeightResultEntity> confirmedWeights() {
return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> {
InboundWeightResultEntity weight = new InboundWeightResultEntity();
@@ -35,7 +35,7 @@ class MeasurementSubmissionServiceTest {
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
verify(locations).occupy(10001L, 88L, 9L, 30L, 3);
verify(locations).requireOccupied(10001L, 88L);
}
@Test
@@ -45,6 +45,6 @@ class MeasurementSubmissionServiceTest {
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
when(measurements.submit(10001L, 88L, 9L, request)).thenReturn(submitted);
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
verify(locations).occupy(10001L, 88L, 9L, 30L, 3);
verify(locations).requireOccupied(10001L, 88L);
}
}