修改提交

This commit is contained in:
2026-09-03 11:59:06 +08:00
parent a3715f618f
commit f561e6bb09
5 changed files with 131 additions and 21 deletions
@@ -534,18 +534,36 @@ public class InboundMeasurementService {
} }
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) { 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( return new InboundMeasurementResponses.Item(
item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(), item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(), item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(),
item.getUnitVolumeCbm(), item.getUnitVolumeCbm(),
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(), item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
item.getOverLimitWarning(), item.getOuterLengthCm(), item.getOuterWidthCm(), item.getOverLimitWarning(), lengthOverridden ? item.getOuterLengthCm() : null,
item.getOuterHeightCm(), item.getChargeRuleId(), item.getCoefficientMode(), widthOverridden ? item.getOuterWidthCm() : null,
heightOverridden ? item.getOuterHeightCm() : null,
item.getChargeRuleId(), item.getCoefficientMode(),
item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(), item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
item.getPackageCount(), item.getPackageCount(),
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt()); 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, private void requireConfirmedPackageTotal(InboundOrderEntity inbound,
List<InboundMeasurementItemEntity> items) { List<InboundMeasurementItemEntity> items) {
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) { if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
@@ -167,26 +167,26 @@ public class MeasurementBatchService {
boolean hasOuterOverride = source.outerLengthCm() != null boolean hasOuterOverride = source.outerLengthCm() != null
|| source.outerWidthCm() != null || source.outerWidthCm() != null
|| source.outerHeightCm() != null; || source.outerHeightCm() != null;
if (hasOuterOverride && (source.outerLengthCm() == null if ((source.outerLengthCm() != null && source.outerLengthCm().signum() <= 0)
|| source.outerWidthCm() == null || source.outerHeightCm() == null || (source.outerWidthCm() != null && source.outerWidthCm().signum() <= 0)
|| source.outerLengthCm().signum() <= 0 || source.outerWidthCm().signum() <= 0 || (source.outerHeightCm() != null && source.outerHeightCm().signum() <= 0)) {
|| source.outerHeightCm().signum() <= 0)) {
throw new BizException(ResultCode.VALIDATION_FAILED, throw new BizException(ResultCode.VALIDATION_FAILED,
"仓库修改外箱长、宽、高必须同时填写且大于0,明细 " + source.itemNo()); "仓库修改外箱尺寸必须大于0,明细 " + source.itemNo());
} }
if (hasOuterOverride) { if (coefficientRule != null) {
// 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) {
BigDecimal specificationCoefficient = resolveSpecificationCoefficient( BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
coefficientRule, source.lengthCm(), source.widthCm(), source.heightCm()); coefficientRule, source.lengthCm(), source.widthCm(), source.heightCm());
outerLength = applyCoefficient(outerLength, specificationCoefficient); outerLength = applyCoefficient(outerLength, specificationCoefficient);
outerWidth = applyCoefficient(outerWidth, specificationCoefficient); outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
outerHeight = applyCoefficient(outerHeight, 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) BigDecimal unitVolumeRaw = outerLength.multiply(outerWidth).multiply(outerHeight)
.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP); .divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride
@@ -222,7 +222,7 @@ public class MeasurementBatchService {
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING)); item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
item.setChargeRuleId(coefficientRule == null ? null : coefficientRule.getId()); item.setChargeRuleId(coefficientRule == null ? null : coefficientRule.getId());
item.setCoefficientMode(coefficientMode); item.setCoefficientMode(coefficientMode);
item.setCalculationRuleVersion(hasOuterOverride ? "WAREHOUSE_OUTER_OVERRIDE_V1" item.setCalculationRuleVersion(hasOuterOverride ? warehouseOverrideVersion(source)
: coefficientRule != null : coefficientRule != null
? "CUSTOMER_COEFFICIENT_V" + coefficientRule.getRuleVersion() + "_MULTIPLY" ? "CUSTOMER_COEFFICIENT_V" + coefficientRule.getRuleVersion() + "_MULTIPLY"
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1"); : useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
@@ -237,6 +237,18 @@ public class MeasurementBatchService {
}).toList(); }).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) { private String normalizeMode(String mode) {
if (mode == null || mode.isBlank()) return null; if (mode == null || mode.isBlank()) return null;
String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT); String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT);
@@ -1526,9 +1526,9 @@ components:
nullable: true nullable: true
description: 当前尺寸测量明细的 File 文件 ID;图片类型固定为 DIMENSION description: 当前尺寸测量明细的 File 文件 ID;图片类型固定为 DIMENSION
overLimitWarning: {type: string, nullable: true} overLimitWarning: {type: string, nullable: true}
outerLengthCm: {type: number, format: double, nullable: true} outerLengthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱长度;未保存人工修正时为空}
outerWidthCm: {type: number, format: double, nullable: true} outerWidthCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱宽度;未保存人工修正时为空}
outerHeightCm: {type: number, format: double, nullable: true} outerHeightCm: {type: number, format: double, nullable: true, description: 仓库手工保存的外箱高度;未保存人工修正时为空}
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true} coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true}
remark: {type: string, nullable: true} remark: {type: string, nullable: true}
@@ -1642,9 +1642,9 @@ components:
heightCm: {type: number, minimum: 0.001} heightCm: {type: number, minimum: 0.001}
intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} intelligentImageFileId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
overLimitWarning: {type: string, maxLength: 500, nullable: true} overLimitWarning: {type: string, maxLength: 500, nullable: true}
outerLengthCm: {type: number, minimum: 0, nullable: true} outerLengthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱长度修正;保存后参与联动计算}
outerWidthCm: {type: number, minimum: 0, nullable: true} outerWidthCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱宽度修正;保存后参与联动计算}
outerHeightCm: {type: number, minimum: 0, nullable: true} outerHeightCm: {type: number, minimum: 0, nullable: true, description: 可单独提交的仓库外箱高度修正;保存后参与联动计算}
chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} chargeRuleId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true} coefficientMode: {type: string, enum: [ADD, MULTIPLY], nullable: true}
remark: {type: string, maxLength: 500, nullable: true} remark: {type: string, maxLength: 500, nullable: true}
@@ -189,6 +189,10 @@ class InboundMeasurementServiceTest {
item.setTenantId(1L); item.setTenantId(1L);
item.setMeasurementId(30L); item.setMeasurementId(30L);
item.setSourceImageFileId(140L); 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); InboundWeightResultEntity weight = weight(50L, "30.000", "KG", "CONFIRMED", 2);
weight.setGrossWeightKg(new BigDecimal("30.000")); weight.setGrossWeightKg(new BigDecimal("30.000"));
weight.setPalletTareWeightKg(new BigDecimal("18.500")); weight.setPalletTareWeightKg(new BigDecimal("18.500"));
@@ -206,6 +210,9 @@ class InboundMeasurementServiceTest {
assertThat(result.items()).singleElement().satisfies(dimension -> { assertThat(result.items()).singleElement().satisfies(dimension -> {
assertThat(dimension.id()).isEqualTo(40L); assertThat(dimension.id()).isEqualTo(40L);
assertThat(dimension.intelligentImageFileId()).isEqualTo(140L); assertThat(dimension.intelligentImageFileId()).isEqualTo(140L);
assertThat(dimension.outerLengthCm()).isNull();
assertThat(dimension.outerWidthCm()).isNull();
assertThat(dimension.outerHeightCm()).isNull();
}); });
assertThat(result.weightResults()).singleElement().satisfies(confirmedWeight -> { assertThat(result.weightResults()).singleElement().satisfies(confirmedWeight -> {
assertThat(confirmedWeight.id()).isEqualTo(50L); assertThat(confirmedWeight.id()).isEqualTo(50L);
@@ -217,6 +224,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 @Test
void detailComparesLatestDeclarationWithoutMutatingFrozenMeasurementFacts() { void detailComparesLatestDeclarationWithoutMutatingFrozenMeasurementFacts() {
InboundOrderEntity inbound = inbound("WAIT_SPLIT"); InboundOrderEntity inbound = inbound("WAIT_SPLIT");
@@ -246,6 +246,29 @@ class MeasurementBatchServiceTest {
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("WAREHOUSE_OUTER_OVERRIDE_V1"); 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<List<InboundMeasurementItemEntity>> 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 @Test
void staleDraftVersionIsRejectedBeforeReplacingItems() { void staleDraftVersionIsRejectedBeforeReplacingItems() {
InboundMeasurementEntity existing = new InboundMeasurementEntity(); InboundMeasurementEntity existing = new InboundMeasurementEntity();