Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-03 12:01:00 +08:00
5 changed files with 131 additions and 21 deletions
@@ -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<InboundMeasurementItemEntity> items) {
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
@@ -201,26 +201,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
@@ -256,7 +256,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");
@@ -271,6 +271,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);
@@ -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}
@@ -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");
@@ -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<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
void staleDraftVersionIsRejectedBeforeReplacingItems() {
InboundMeasurementEntity existing = new InboundMeasurementEntity();