统一设备与人工测量默认算法和外箱计费快照

This commit is contained in:
2026-10-28 10:30:40 +08:00
parent 0b13c2cfb7
commit b2e4f0d246
8 changed files with 243 additions and 95 deletions
@@ -7,12 +7,14 @@ import com.baiyue.firstleg.dto.CartonMeasurementCommand;
import com.baiyue.firstleg.dto.CartonMeasurementResult;
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -46,13 +48,18 @@ class CartonMeasurementServiceTest {
private final CustomerDimensionConfigRepository dimensionConfigs = mock(CustomerDimensionConfigRepository.class);
private final CustomerDimensionWeightingService dimensionWeighting =
new CustomerDimensionWeightingService(dimensionConfigs);
private final MeasurementBatchService measurementBatchService = new MeasurementBatchService();
private final FbaPricingRepository pricingRepository = mock(FbaPricingRepository.class);
private CartonMeasurementService service;
private final AtomicReference<InboundMeasurementItemEntity> insertedItem = new AtomicReference<>();
@BeforeEach
/** 构造真实共享尺寸计算入口,设备回归不以模拟计算结果代替业务算法。 */
void setUp() {
ReflectionTestUtils.setField(measurementBatchService, "dimensionWeighting", dimensionWeighting);
ReflectionTestUtils.setField(measurementBatchService, "pricingRepository", pricingRepository);
service = new CartonMeasurementService(repository, resolverProvider, images, transactions,
dimensionWeighting);
measurementBatchService);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
when(resolverProvider.getIfAvailable()).thenReturn(resolver);
when(resolver.resolveByBoxCode(10001L, "RK-001#5-1"))
@@ -164,7 +171,9 @@ class CartonMeasurementServiceTest {
}
@Test
/** 超出四档尺寸且未启用调整规则时,仍按设备原尺寸计算。 */
void scanOutsideConfiguredWeightingRulesKeepsImageAndUsesActualDimensions() {
useDefaultMode("ACTUAL");
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
@@ -187,7 +196,7 @@ class CartonMeasurementServiceTest {
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("60.100");
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("50.100");
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("50.100");
assertThat(insertedItem.get().getUnitVolumeCbm()).isEqualByComparingTo("0.151");
assertThat(insertedItem.get().getUnitVolumeCbm()).isEqualByComparingTo("0.1509");
assertThat(insertedItem.get().getOverLimitWarning()).isNull();
verify(images, never()).delete(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE));
}
@@ -347,6 +356,61 @@ class CartonMeasurementServiceTest {
verify(repository).updateInbound(inbound);
}
@Test
/** 预置系数记录存在但未启用时,设备首次测量不加边也不乘系数。 */
void firstScanUsesActualDimensionsWhenCoefficientIsNotEnabled() {
useDefaultMode("ACTUAL");
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setLengthFactor(new BigDecimal("1.25"));
when(pricingRepository.findDefaultRule(10001L, 10001L)).thenReturn(rule);
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get()));
when(repository.updateMeasurement(measurement)).thenReturn(true);
service.upsert(10001L, 90001L, acceptanceCommand());
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("59.500");
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("41.200");
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("31.300");
assertThat(insertedItem.get().getCoefficientMode()).isEqualTo("ACTUAL");
assertThat(insertedItem.get().getUnitVolumeCbm()).isEqualByComparingTo("0.0768");
verify(pricingRepository, never()).findDefaultRule(10001L, 10001L);
}
@Test
/** 客户明确启用系数后,设备首次录入与人工批次使用相同系数算法。 */
void firstScanAppliesExplicitlyEnabledMultiplyDefault() {
useDefaultMode("MULTIPLY");
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setId(77L);
rule.setTenantId(10001L);
rule.setStatus("ACTIVE");
rule.setRuleVersion(2);
rule.setLengthFactor(new BigDecimal("1.25"));
when(pricingRepository.findDefaultRule(10001L, 10001L)).thenReturn(rule);
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get()));
when(repository.updateMeasurement(measurement)).thenReturn(true);
service.upsert(10001L, 90001L, acceptanceCommand());
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("74.375");
assertThat(insertedItem.get().getOuterWidthCm()).isEqualByComparingTo("51.500");
assertThat(insertedItem.get().getOuterHeightCm()).isEqualByComparingTo("39.125");
assertThat(insertedItem.get().getCoefficientMode()).isEqualTo("MULTIPLY");
assertThat(insertedItem.get().getChargeRuleId()).isEqualTo(77L);
assertThat(insertedItem.get().getUnitVolumeCbm()).isEqualByComparingTo("0.1499");
}
/** 设置客户默认算法,与是否保存系数规则分开。 */
private void useDefaultMode(String mode) {
List<CustomerDimensionConfigEntity> rules = dimensionRules(10001L);
rules.forEach(rule -> rule.setDefaultCalculationMode(mode));
when(dimensionConfigs.findAll(10001L)).thenReturn(rules);
}
private CartonMeasurementCommand command() {
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("59.100"),
new BigDecimal("40.200"), new BigDecimal("30.300"),
@@ -388,6 +452,7 @@ class CartonMeasurementServiceTest {
return item;
}
/** 既有加边用例显式启用 ADD,避免依赖配置存在即生效的旧行为。 */
private List<CustomerDimensionConfigEntity> dimensionRules(long tenantId) {
return java.util.Arrays.stream(CustomerDimensionBoxType.values()).map(type -> {
CustomerDimensionConfigEntity rule = new CustomerDimensionConfigEntity();
@@ -395,6 +460,7 @@ class CartonMeasurementServiceTest {
rule.setInnerLengthCm(type.defaultLengthCm()); rule.setInnerWidthCm(type.defaultWidthCm());
rule.setInnerHeightCm(type.defaultHeightCm());
rule.setEdgeAdditionCm(new BigDecimal("2.000"));
rule.setDefaultCalculationMode("ADD");
return rule;
}).toList();
}
@@ -39,15 +39,9 @@ class CustomerDimensionWeightingServiceTest {
}
@Test
void deviceMeasurementsUseActualDimensionsAndAddEachSideWhenRuleMatches() {
var weighted = service.load(10001L).resolveDeviceMeasurement(
new BigDecimal("10"), new BigDecimal("10"), new BigDecimal("10"));
assertThat(weighted.boxType()).isEqualTo(CustomerDimensionBoxType.SMALL);
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("14.000");
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("14.000");
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("14.000");
assertThat(weighted.calculationRuleVersion()).isEqualTo("CUSTOMER_DIMENSION_SMALL_V3");
/** 未勾选默认调整算法时,四档尺寸配置不能隐式启用加边。 */
void defaultCalculationModeRemainsActualWhenNoAdjustmentWasSelected() {
assertThat(service.load(10001L).defaultCalculationMode()).isEqualTo("ACTUAL");
}
@Test
@@ -81,16 +75,15 @@ class CustomerDimensionWeightingServiceTest {
}
@Test
void deviceDimensionsOutsideConfiguredRulesRemainUnchanged() {
var weighted = service.load(10001L).resolveDeviceMeasurement(
new BigDecimal("60.001"), new BigDecimal("50"), new BigDecimal("50"));
/** 默认算法必须在四档配置中一致,防止各录入入口取得不同计算方式。 */
void inconsistentCustomerDefaultModesAreRejected() {
List<CustomerDimensionConfigEntity> rules = defaultRules(10001L);
rules.get(0).setDefaultCalculationMode("MULTIPLY");
when(repository.findAll(10001L)).thenReturn(rules);
assertThat(weighted.boxType()).isNull();
assertThat(weighted.edgeAdditionCm()).isNull();
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("60.001");
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("50.000");
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("50.000");
assertThat(weighted.calculationRuleVersion()).isEqualTo("ACTUAL_DIMENSION_V1");
assertThatThrownBy(() -> service.load(10001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("默认箱规计算方式配置不一致");
}
@Test
@@ -52,6 +52,7 @@ class MeasurementBatchServiceTest {
}
@Test
/** 未选择调整算法时,第三方与人工批次均保存实际外箱快照。 */
void manualAndExternalUseSameTablesAndExternalSaveNeverAdvancesInbound() {
MeasurementBatchCommand command = new MeasurementBatchCommand(
1L, 10L, null, 0, "THIRD_PARTY", "DEVICE_VENDOR", "EVENT-001",
@@ -73,10 +74,10 @@ class MeasurementBatchServiceTest {
assertThat(items.getValue().get(0).getOuterHeightCm()).isEqualByComparingTo("21.000");
assertThat(items.getValue().get(0).getCalculationRuleVersion())
.isEqualTo("WAREHOUSE_OUTER_OVERRIDE_V1");
assertThat(items.getValue().get(0).getCoefficientMode()).isNull();
assertThat(items.getValue().get(0).getCoefficientMode()).isEqualTo("ACTUAL");
assertThat(items.getValue().get(0).getChargeRuleId()).isNull();
assertThat(items.getValue().get(0).getGirthCm()).isEqualByComparingTo("145.000");
verify(pricingRepository).findDefaultRule(1L, 1L);
verify(pricingRepository, never()).findDefaultRule(1L, 1L);
verify(repository, never()).updateInbound(any());
}
@@ -174,7 +175,42 @@ class MeasurementBatchServiceTest {
}
@Test
/** 客户未启用算法时,预置系数不能放大仓库修正的实际外箱体积。 */
void savedCoefficientDoesNotOverrideActualDefaultMode() {
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setId(77L);
rule.setTenantId(1L);
rule.setCustomerId(1L);
rule.setStatus("ACTIVE");
rule.setDefaultRule(true);
rule.setLengthFactor(new BigDecimal("1.25"));
when(pricingRepository.findDefaultRule(1L, 1L)).thenReturn(rule);
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
80001L, "001", "A", 22, new BigDecimal("62"), new BigDecimal("52"),
new BigDecimal("52"), 500L, null, new BigDecimal("63"), new BigDecimal("53"),
new BigDecimal("53"), null, null, "仓库修正");
service.saveBatch(new MeasurementBatchCommand(
1L, 10L, null, 0, "MANUAL", "PLATFORM", null, null, null,
LocalDateTime.now(), null, null, null, null, null, 9L, List.of(source)));
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("63.000");
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("53.000");
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("53.000");
assertThat(calculated.getTotalVolumeCbm()).isEqualByComparingTo("3.8940");
assertThat(calculated.getCoefficientMode()).isEqualTo("ACTUAL");
assertThat(calculated.getChargeRuleId()).isNull();
verify(pricingRepository, never()).findDefaultRule(1L, 1L);
}
@Test
/** 只有客户明确启用系数算法,才自动选取当前默认系数配置。 */
void automaticallyAppliesCustomerDefaultRuleToActualMeasurement() {
useDefaultMode("MULTIPLY");
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setId(77L);
rule.setTenantId(1L);
@@ -212,7 +248,9 @@ class MeasurementBatchServiceTest {
}
@Test
/** 系数模式按当前尺寸匹配档位,同一系数作用于三个维度。 */
void usesOneMatchedTierCoefficientForAllDimensionsOfEachSpecification() {
useDefaultMode("MULTIPLY");
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setId(78L);
rule.setTenantId(1L);
@@ -251,6 +289,7 @@ class MeasurementBatchServiceTest {
}
@Test
/** 无默认系数且未选择调整算法时,显式保存 ACTUAL 计算方式。 */
void keepsActualMeasurementUnchangedWhenCustomerHasNoDefaultRule() {
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
80001L, "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"),
@@ -269,7 +308,7 @@ class MeasurementBatchServiceTest {
assertThat(calculated.getOuterWidthCm()).isEqualByComparingTo("30.000");
assertThat(calculated.getOuterHeightCm()).isEqualByComparingTo("20.000");
assertThat(calculated.getChargeRuleId()).isNull();
assertThat(calculated.getCoefficientMode()).isNull();
assertThat(calculated.getCoefficientMode()).isEqualTo("ACTUAL");
assertThat(calculated.getCalculationRuleVersion()).isEqualTo("ACTUAL_DIMENSION_V1");
}
@@ -530,6 +569,58 @@ class MeasurementBatchServiceTest {
assertThat(saved.getDeviceMeasuredAt()).isEqualTo(LocalDateTime.of(2026, 9, 2, 10, 0));
}
@Test
/** 行明确选择实测算法后,即使客户默认系数已启用,也不能被覆盖。 */
void explicitActualModeOverridesCustomerMultiplyDefault() {
useDefaultMode("MULTIPLY");
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setTenantId(1L);
item.setItemNo("B1");
item.setPackageCount(22);
item.setLengthCm(new BigDecimal("62"));
item.setWidthCm(new BigDecimal("52"));
item.setHeightCm(new BigDecimal("52"));
item.setManualOuterLengthCm(new BigDecimal("63"));
item.setManualOuterWidthCm(new BigDecimal("53"));
item.setManualOuterHeightCm(new BigDecimal("53"));
item.setCoefficientMode("ACTUAL");
service.calculateCartonDimensions(item);
assertThat(item.getTotalVolumeCbm()).isEqualByComparingTo("3.8940");
assertThat(item.getCoefficientMode()).isEqualTo("ACTUAL");
assertThat(item.getChargeRuleId()).isNull();
verify(pricingRepository, never()).findDefaultRule(anyLong(), anyLong());
}
@Test
/** 客户明确启用加边规则后,未指定行算法的设备及人工录入均采用该默认方式。 */
void appliesCustomerAddDefaultWhenRowModeIsNotSelected() {
useDefaultMode("ADD");
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setTenantId(1L);
item.setItemNo("B1");
item.setPackageCount(1);
item.setLengthCm(new BigDecimal("30"));
item.setWidthCm(new BigDecimal("20"));
item.setHeightCm(new BigDecimal("20"));
service.calculateCartonDimensions(item);
assertThat(item.getOuterLengthCm()).isEqualByComparingTo("34.000");
assertThat(item.getOuterWidthCm()).isEqualByComparingTo("24.000");
assertThat(item.getOuterHeightCm()).isEqualByComparingTo("24.000");
assertThat(item.getCoefficientMode()).isEqualTo("ADD");
verify(pricingRepository, never()).findDefaultRule(anyLong(), anyLong());
}
/** 为用例设置客户真正启用的算法,独立于系数配置是否存在。 */
private void useDefaultMode(String mode) {
List<CustomerDimensionConfigEntity> rules = dimensionRules(1L);
rules.forEach(rule -> rule.setDefaultCalculationMode(mode));
when(dimensionConfigs.findAll(1L)).thenReturn(rules);
}
private MeasurementBatchCommand.Item item(BigDecimal unitWeight) {
return new MeasurementBatchCommand.Item(
80001L, "001", "A", 2, new BigDecimal("40"), new BigDecimal("30"),