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 9b9cad116..df644942b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -534,18 +534,36 @@ public class InboundMeasurementService { } private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) { + String overrideVersion = item.getCalculationRuleVersion(); + boolean legacyFullOverride = "WAREHOUSE_OUTER_OVERRIDE_V1".equals(overrideVersion); + boolean lengthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'L'); + boolean widthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'W'); + boolean heightOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'H'); return new InboundMeasurementResponses.Item( item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(), item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(), item.getUnitVolumeCbm(), item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(), - item.getOverLimitWarning(), item.getOuterLengthCm(), item.getOuterWidthCm(), - item.getOuterHeightCm(), item.getChargeRuleId(), item.getCoefficientMode(), + item.getOverLimitWarning(), lengthOverridden ? item.getOuterLengthCm() : null, + widthOverridden ? item.getOuterWidthCm() : null, + heightOverridden ? item.getOuterHeightCm() : null, + item.getChargeRuleId(), item.getCoefficientMode(), item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(), item.getPackageCount(), item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt()); } + private boolean warehouseOverrideContains(String calculationRuleVersion, char dimension) { + if (calculationRuleVersion == null + || !calculationRuleVersion.startsWith("WAREHOUSE_OUTER_OVERRIDE_") + || !calculationRuleVersion.endsWith("_V1")) { + return false; + } + String dimensions = calculationRuleVersion.substring( + "WAREHOUSE_OUTER_OVERRIDE_".length(), calculationRuleVersion.length() - 3); + return dimensions.indexOf(dimension) >= 0; + } + private void requireConfirmedPackageTotal(InboundOrderEntity inbound, List items) { if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java index fb701ccc6..e78048358 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java @@ -165,26 +165,26 @@ public class MeasurementBatchService { boolean hasOuterOverride = source.outerLengthCm() != null || source.outerWidthCm() != null || source.outerHeightCm() != null; - if (hasOuterOverride && (source.outerLengthCm() == null - || source.outerWidthCm() == null || source.outerHeightCm() == null - || source.outerLengthCm().signum() <= 0 || source.outerWidthCm().signum() <= 0 - || source.outerHeightCm().signum() <= 0)) { + if ((source.outerLengthCm() != null && source.outerLengthCm().signum() <= 0) + || (source.outerWidthCm() != null && source.outerWidthCm().signum() <= 0) + || (source.outerHeightCm() != null && source.outerHeightCm().signum() <= 0)) { throw new BizException(ResultCode.VALIDATION_FAILED, - "仓库修改外箱长、宽、高必须同时填写且大于0,明细 " + source.itemNo()); + "仓库修改外箱尺寸必须大于0,明细 " + source.itemNo()); } - if (hasOuterOverride) { - // Warehouse-confirmed outer dimensions are authoritative. Derived - // values below (girth, volume and volumetric weight) use them directly. - outerLength = source.outerLengthCm(); - outerWidth = source.outerWidthCm(); - outerHeight = source.outerHeightCm(); - } else if (coefficientRule != null) { + if (coefficientRule != null) { BigDecimal specificationCoefficient = resolveSpecificationCoefficient( coefficientRule, source.lengthCm(), source.widthCm(), source.heightCm()); outerLength = applyCoefficient(outerLength, specificationCoefficient); outerWidth = applyCoefficient(outerWidth, specificationCoefficient); outerHeight = applyCoefficient(outerHeight, specificationCoefficient); } + if (hasOuterOverride) { + // Each warehouse-confirmed dimension is authoritative on its own. + // Unspecified dimensions keep their strategy-calculated values. + if (source.outerLengthCm() != null) outerLength = source.outerLengthCm(); + if (source.outerWidthCm() != null) outerWidth = source.outerWidthCm(); + if (source.outerHeightCm() != null) outerHeight = source.outerHeightCm(); + } BigDecimal unitVolumeRaw = outerLength.multiply(outerWidth).multiply(outerHeight) .divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP); BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride @@ -220,7 +220,7 @@ public class MeasurementBatchService { item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING)); item.setChargeRuleId(coefficientRule == null ? null : coefficientRule.getId()); item.setCoefficientMode(coefficientMode); - item.setCalculationRuleVersion(hasOuterOverride ? "WAREHOUSE_OUTER_OVERRIDE_V1" + item.setCalculationRuleVersion(hasOuterOverride ? warehouseOverrideVersion(source) : coefficientRule != null ? "CUSTOMER_COEFFICIENT_V" + coefficientRule.getRuleVersion() + "_MULTIPLY" : useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1"); @@ -235,6 +235,18 @@ public class MeasurementBatchService { }).toList(); } + private String warehouseOverrideVersion(MeasurementBatchCommand.Item source) { + if (source.outerLengthCm() != null && source.outerWidthCm() != null + && source.outerHeightCm() != null) { + return "WAREHOUSE_OUTER_OVERRIDE_V1"; + } + StringBuilder dimensions = new StringBuilder(); + if (source.outerLengthCm() != null) dimensions.append('L'); + if (source.outerWidthCm() != null) dimensions.append('W'); + if (source.outerHeightCm() != null) dimensions.append('H'); + return "WAREHOUSE_OUTER_OVERRIDE_" + dimensions + "_V1"; + } + private String normalizeMode(String mode) { if (mode == null || mode.isBlank()) return null; String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT); diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 03e606c9e..50b38abed 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -1526,9 +1526,9 @@ components: nullable: true description: 当前尺寸测量明细的 File 文件 ID;图片类型固定为 DIMENSION overLimitWarning: {type: string, nullable: true} - outerLengthCm: {type: number, format: double, nullable: true} - outerWidthCm: {type: number, format: double, nullable: true} - outerHeightCm: {type: number, format: double, nullable: true} + outerLengthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱长度;未保存人工修正时为空} + outerWidthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱宽度;未保存人工修正时为空} + outerHeightCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱高度;未保存人工修正时为空} chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true} remark: {type: string, nullable: true} @@ -1642,9 +1642,9 @@ components: heightCm: {type: number, minimum: 0.001} intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} overLimitWarning: {type: string, maxLength: 500, nullable: true} - outerLengthCm: {type: number, minimum: 0, nullable: true} - outerWidthCm: {type: number, minimum: 0, nullable: true} - outerHeightCm: {type: number, minimum: 0, nullable: true} + outerLengthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱长度修正;保存后参与联动计算} + outerWidthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱宽度修正;保存后参与联动计算} + outerHeightCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱高度修正;保存后参与联动计算} chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true} remark: {type: string, maxLength: 500, nullable: true} 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 fd3b51d36..5eea7657b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -190,6 +190,10 @@ class InboundMeasurementServiceTest { item.setTenantId(1L); item.setMeasurementId(30L); item.setSourceImageFileId(140L); + item.setOuterLengthCm(new BigDecimal("64.000")); + item.setOuterWidthCm(new BigDecimal("54.000")); + item.setOuterHeightCm(new BigDecimal("54.000")); + item.setCalculationRuleVersion("CUSTOMER_DIMENSION_EXTRA_LARGE_V1"); InboundWeightResultEntity weight = weight(50L, "30.000", "KG", "CONFIRMED", 2); weight.setGrossWeightKg(new BigDecimal("30.000")); weight.setPalletTareWeightKg(new BigDecimal("18.500")); @@ -207,6 +211,9 @@ class InboundMeasurementServiceTest { assertThat(result.items()).singleElement().satisfies(dimension -> { assertThat(dimension.id()).isEqualTo(40L); assertThat(dimension.intelligentImageFileId()).isEqualTo(140L); + assertThat(dimension.outerLengthCm()).isNull(); + assertThat(dimension.outerWidthCm()).isNull(); + assertThat(dimension.outerHeightCm()).isNull(); }); assertThat(result.weightResults()).singleElement().satisfies(confirmedWeight -> { assertThat(confirmedWeight.id()).isEqualTo(50L); @@ -218,6 +225,56 @@ class InboundMeasurementServiceTest { }); } + @Test + void detailReturnsWarehouseOuterDimensionsOnlyAfterManualOverrideWasSaved() { + InboundOrderEntity inbound = inbound("MEASURING"); + InboundMeasurementEntity measurement = new InboundMeasurementEntity(); + measurement.setId(30L); + InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); + item.setId(40L); + item.setOuterLengthCm(new BigDecimal("65.000")); + item.setOuterWidthCm(new BigDecimal("55.000")); + item.setOuterHeightCm(new BigDecimal("55.000")); + item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_V1"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement); + when(repository.listItems(1L, 30L)).thenReturn(List.of(item)); + when(repository.listWeights(1L, 10L)).thenReturn(List.of()); + + InboundMeasurementResponses.Detail result = service.detail(1L, 10L); + + assertThat(result.items()).singleElement().satisfies(dimension -> { + assertThat(dimension.outerLengthCm()).isEqualByComparingTo("65.000"); + assertThat(dimension.outerWidthCm()).isEqualByComparingTo("55.000"); + assertThat(dimension.outerHeightCm()).isEqualByComparingTo("55.000"); + }); + } + + @Test + void detailReturnsOnlyTheIndividuallyOverriddenOuterDimension() { + InboundOrderEntity inbound = inbound("MEASURING"); + InboundMeasurementEntity measurement = new InboundMeasurementEntity(); + measurement.setId(30L); + InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); + item.setId(40L); + item.setOuterLengthCm(new BigDecimal("65.000")); + item.setOuterWidthCm(new BigDecimal("54.000")); + item.setOuterHeightCm(new BigDecimal("54.000")); + item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_L_V1"); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement); + when(repository.listItems(1L, 30L)).thenReturn(List.of(item)); + when(repository.listWeights(1L, 10L)).thenReturn(List.of()); + + InboundMeasurementResponses.Detail result = service.detail(1L, 10L); + + assertThat(result.items()).singleElement().satisfies(dimension -> { + assertThat(dimension.outerLengthCm()).isEqualByComparingTo("65.000"); + assertThat(dimension.outerWidthCm()).isNull(); + assertThat(dimension.outerHeightCm()).isNull(); + }); + } + @Test void detailComparesLatestDeclarationWithoutMutatingFrozenMeasurementFacts() { InboundOrderEntity inbound = inbound("WAIT_SPLIT"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java index f1e93860c..45bf02aad 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementBatchServiceTest.java @@ -246,6 +246,29 @@ class MeasurementBatchServiceTest { assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_V1"); } + @Test + void warehouseCanOverrideOnlyOneOuterDimension() { + MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item( + 80001L, "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"), + new BigDecimal("20"), 500L, null, new BigDecimal("50"), + null, null, null, "ADD", "仅修改长度"); + MeasurementBatchCommand command = new MeasurementBatchCommand( + 1L, 10L, null, 0, "MANUAL", "PLATFORM", null, null, null, + LocalDateTime.now(), null, null, null, null, null, 9L, List.of(source)); + + service.saveBatch(command); + + ArgumentCaptor> items = ArgumentCaptor.forClass(List.class); + verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L), + org.mockito.ArgumentMatchers.eq(100L), items.capture()); + InboundMeasurementItemEntity calculated = items.getValue().get(0); + assertThat(calculated.getOuterLengthCm()).isEqualByComparingTo("50.000"); + assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("32.000"); + assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("22.000"); + assertThat(calculated.getUnitVolumeCbm()).isEqualByComparingTo("0.0352"); + assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_L_V1"); + } + @Test void staleDraftVersionIsRejectedBeforeReplacingItems() { InboundMeasurementEntity existing = new InboundMeasurementEntity();