diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index 0f431473c..50e3a0a46 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -25,6 +25,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.beans.factory.annotation.Value; import feign.FeignException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.RoundingMode; @@ -37,6 +39,7 @@ import java.util.Set; /** 平台端入库测量、OCR 修正、重量确认和提交审核应用服务。 */ @Service public class InboundMeasurementService { + private static final Logger LOGGER = LoggerFactory.getLogger(InboundMeasurementService.class); /** 测量批次已提交,等待测量审核。 */ private static final String STATUS_SUBMITTED = "SUBMITTED"; /** OCR 或人工修正的重量已确认,可参与计费。 */ @@ -298,8 +301,7 @@ 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()); + ensureLocationWithoutBlocking(tenantId, inboundId, operatorId, weight.getId(), weight.getVersion()); return toWeight(weight); } BigDecimal value = effectiveValue(weight); @@ -329,11 +331,21 @@ public class InboundMeasurementService { insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit, grossWeightKg, netWeightKg, null, operatorId, operatorName); recalculateConfirmedWeight(inbound, operatorId); - locationOccupation.ensureForConfirmedWeight( - tenantId, inboundId, operatorId, weight.getId(), confirmedVersion); + ensureLocationWithoutBlocking(tenantId, inboundId, operatorId, weight.getId(), confirmedVersion); return toWeight(weight); } + private void ensureLocationWithoutBlocking(long tenantId, long inboundId, long operatorId, + long weightResultId, int weightVersion) { + try { + locationOccupation.ensureForConfirmedWeight( + tenantId, inboundId, operatorId, weightResultId, weightVersion); + } catch (RuntimeException exception) { + LOGGER.warn("称重确认已完成,但库位指引暂未生成,不阻断测量主流程: tenantId={}, inboundId={}, weightResultId={}", + tenantId, inboundId, weightResultId, exception); + } + } + @Transactional public void deleteWeightResult(long tenantId, long inboundId, long resultId, int version, long operatorId) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java index 19ea22ea6..a6ac28632 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementLocationOccupationService.java @@ -94,7 +94,16 @@ public class MeasurementLocationOccupationService { } } - public void requireOccupied(long tenantId, long inboundId) { + public void ensureForSubmission(long tenantId, long inboundId, long operatorId) { + var weights = confirmedWeights(tenantId, inboundId); + InboundWeightResultEntity latest = weights.stream() + .max(java.util.Comparator.comparing(InboundWeightResultEntity::getConfirmedAt) + .thenComparing(InboundWeightResultEntity::getId)) + .orElseThrow(() -> new BizException(ResultCode.CONFLICT, + "至少需要一张已确认的称重图片才能提交测量")); + ensureForConfirmedWeight(tenantId, inboundId, operatorId, latest.getId(), + latest.getVersion() == null ? 0 : latest.getVersion()); + InboundOrderEntity order = requireOrder(tenantId, inboundId); long warehouseId = requireWarehouse(order); var confirmedWeights = confirmedWeights(tenantId, inboundId); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementSubmissionService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementSubmissionService.java index dea6bd541..eff050be1 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementSubmissionService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementSubmissionService.java @@ -6,9 +6,12 @@ import com.baiyue.firstleg.dto.InboundMeasurementRequests; import com.baiyue.firstleg.dto.InboundMeasurementResponses; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Service public class MeasurementSubmissionService { + private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementSubmissionService.class); @Resource private InboundMeasurementService measurements; @Resource @@ -19,7 +22,12 @@ public class MeasurementSubmissionService { long tenantId, long inboundId, long operatorId, InboundMeasurementRequests.SubmitRequest request) { var result = measurements.submit(tenantId, inboundId, operatorId, request); - locations.requireOccupied(tenantId, inboundId); + try { + locations.ensureForSubmission(tenantId, inboundId, operatorId); + } catch (RuntimeException exception) { + LOGGER.warn("测量已提交,但库位指引暂未补齐,不阻断提交审核主流程: tenantId={}, inboundId={}", + tenantId, inboundId, exception); + } return result; } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 947781388..ab0c01789 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -579,6 +580,28 @@ class InboundMeasurementServiceTest { assertThat(result.slotStatus()).isEqualTo("BOUND"); } + @Test + void locationAllocationFailureDoesNotBlockWeightConfirmation() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + InboundWeightResultEntity weight = weight(20L, "30.000", "KG", "PENDING_CONFIRM", 0); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight); + when(repository.updateWeight(weight)).thenReturn(true); + when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight)); + doThrow(new IllegalStateException("warehouse unavailable")) + .when(locationOccupation).ensureForConfirmedWeight(1L, 10L, 9L, 20L, 1); + + InboundMeasurementResponses.WeightResult result = service.confirmWeightResult( + 1L, 10L, 20L, 9L, "操作员", + new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null)); + + assertThat(result.recognitionStatus()).isEqualTo("CONFIRMED"); + assertThat(result.grossWeightKg()).isEqualByComparingTo("30.000"); + assertThat(result.locationCode()).isNull(); + verify(repository).insertRevision(any(InboundWeightRevisionEntity.class)); + verify(repository).updateInboundWeightSummary(inbound); + } + @Test void confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight() { InboundOrderEntity inbound = inbound("WAIT_MEASURE"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java index 7e14d9d89..e9932f854 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementLocationOccupationServiceTest.java @@ -320,7 +320,7 @@ class MeasurementLocationOccupationServiceTest { when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights); when(allocations.list(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(first, second)); - service.requireOccupied(BUSINESS_TENANT, INBOUND); + service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR); verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()); @@ -330,6 +330,51 @@ class MeasurementLocationOccupationServiceTest { org.mockito.ArgumentMatchers.any()); } + @Test + void submissionReconcilesMissingBlockFromLatestConfirmedWeight() { + InboundOrderEntity inbound = inbound(); + inbound.setActualVolumeCbm(new BigDecimal("2.5000")); + List weights = confirmedWeights().subList(0, 1); + when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound); + when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights); + when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3))); + when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3))); + when(allocations.list(BUSINESS_TENANT, INBOUND)) + .thenReturn(List.of(), blockSlotsBoundTo(weights)); + + service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR); + + verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.argThat(request -> request.requiredSlotCount() == 3)); + verify(allocations).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, weights); + } + + @Test + void submissionExpandsUndersizedBlockWhenMeasuredVolumeGrows() { + InboundOrderEntity inbound = inbound(); + inbound.setActualVolumeCbm(new BigDecimal("2.5000")); + List weights = confirmedWeights().subList(0, 1); + var current = slot(1); + current.setWeightResultId(weights.get(0).getId()); + current.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(current), blockSlotsBoundTo(weights)); + when(warehouse.expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3))); + + service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR); + + verify(warehouse).expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"), + org.mockito.ArgumentMatchers.argThat(request -> request.blockId() == 500L + && request.requiredSlotCount() == 3)); + verify(allocations).replaceBlock(BUSINESS_TENANT, INBOUND, WAREHOUSE, + 20, new BigDecimal("2.5000"), block("OCCUPIED", 3)); + } + private static InboundProperties properties() { InboundProperties value = new InboundProperties(); value.setDefaultPalletCapacityCbm(new BigDecimal("1.200000")); @@ -377,6 +422,18 @@ class MeasurementLocationOccupationServiceTest { return value; } + private static List blockSlotsBoundTo( + List weights) { + return java.util.stream.IntStream.rangeClosed(1, 3).mapToObj(slotNo -> { + var slot = slot(slotNo); + if (slotNo <= weights.size()) { + slot.setWeightResultId(weights.get(slotNo - 1).getId()); + slot.setSlotStatus("BOUND"); + } + return slot; + }).toList(); + } + private static List confirmedWeights() { return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> { InboundWeightResultEntity weight = new InboundWeightResultEntity(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementSubmissionServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementSubmissionServiceTest.java index 65604b30f..d79d94af7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementSubmissionServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementSubmissionServiceTest.java @@ -9,6 +9,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,7 +36,7 @@ class MeasurementSubmissionServiceTest { assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted); - verify(locations).requireOccupied(10001L, 88L); + verify(locations).ensureForSubmission(10001L, 88L, 9L); } @Test @@ -44,7 +45,10 @@ class MeasurementSubmissionServiceTest { var submitted = new InboundMeasurementResponses.MeasurementSubmitted( 30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null); when(measurements.submit(10001L, 88L, 9L, request)).thenReturn(submitted); + doThrow(new IllegalStateException("warehouse unavailable")) + .when(locations).ensureForSubmission(10001L, 88L, 9L); + assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted); - verify(locations).requireOccupied(10001L, 88L); + verify(locations).ensureForSubmission(10001L, 88L, 9L); } }