统一设备与人工测量默认算法和外箱计费快照
This commit is contained in:
@@ -44,21 +44,22 @@ public class CartonMeasurementService {
|
||||
private final ObjectProvider<CartonLabelBindingResolver> resolverProvider;
|
||||
private final MeasurementImageClient imageClient;
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
private final CustomerDimensionWeightingService dimensionWeighting;
|
||||
private final MeasurementBatchService measurementBatchService;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
/** 设备及手工单箱录入复用测量批次的计算规则。 */
|
||||
public CartonMeasurementService(InboundMeasurementRepository repository,
|
||||
ObjectProvider<CartonLabelBindingResolver> resolverProvider,
|
||||
MeasurementImageClient imageClient,
|
||||
TransactionTemplate transactionTemplate,
|
||||
CustomerDimensionWeightingService dimensionWeighting) {
|
||||
MeasurementBatchService measurementBatchService) {
|
||||
this.repository = repository;
|
||||
this.resolverProvider = resolverProvider;
|
||||
this.imageClient = imageClient;
|
||||
this.transactionTemplate = transactionTemplate;
|
||||
this.dimensionWeighting = dimensionWeighting;
|
||||
this.measurementBatchService = measurementBatchService;
|
||||
}
|
||||
|
||||
public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) {
|
||||
@@ -178,6 +179,7 @@ public class CartonMeasurementService {
|
||||
return result(measurement, item, false);
|
||||
}
|
||||
|
||||
/** 保存设备原始尺寸与证据,按客户默认算法或已保存的行算法生成计费快照。 */
|
||||
private UpsertOutcome persist(long tenantId, long operatorId,
|
||||
CartonLabelBindingResolver.Binding binding,
|
||||
String boxCode, CartonMeasurementCommand command, long imageFileId,
|
||||
@@ -228,7 +230,7 @@ public class CartonMeasurementService {
|
||||
item.setMachineNo(command.machineNo().trim());
|
||||
item.setDeviceMeasuredAt(command.measuredAt());
|
||||
item.setUpdatedAt(LocalDateTime.now());
|
||||
applyCalculatedValues(item, dimensionWeighting.load(tenantId));
|
||||
measurementBatchService.calculateCartonDimensions(item);
|
||||
|
||||
if (created) {
|
||||
repository.insertItem(item);
|
||||
@@ -265,33 +267,12 @@ public class CartonMeasurementService {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
private void applyCalculatedValues(InboundMeasurementItemEntity item,
|
||||
CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolveDeviceMeasurement(
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm());
|
||||
BigDecimal unitRaw = weighted.rawVolumeCbm();
|
||||
BigDecimal unitVolume = weighted.unitVolumeCbm();
|
||||
BigDecimal count = BigDecimal.valueOf(item.getPackageCount());
|
||||
BigDecimal totalRaw = unitRaw.multiply(count).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal totalVolume = unitVolume.multiply(count).setScale(4, RoundingMode.UNNECESSARY);
|
||||
item.setGirthCm(item.getLengthCm().add(item.getWidthCm().add(item.getHeightCm())
|
||||
.multiply(BigDecimal.valueOf(2))).setScale(3, RoundingMode.CEILING));
|
||||
item.setUnitVolumeCbm(unitVolume);
|
||||
item.setVolumeRawCbm(totalRaw);
|
||||
item.setVolumeCbm(totalVolume);
|
||||
item.setTotalVolumeCbm(totalVolume);
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
item.setCalculationRuleVersion(weighted.calculationRuleVersion());
|
||||
item.setOuterLengthCm(weighted.outerLengthCm());
|
||||
item.setOuterWidthCm(weighted.outerWidthCm());
|
||||
item.setOuterHeightCm(weighted.outerHeightCm());
|
||||
}
|
||||
|
||||
/** 仅修改确认箱数时复用已保存的单箱快照,不重新套用当前客户规则。 */
|
||||
private void applyCalculatedValuesFromSnapshot(InboundMeasurementItemEntity item) {
|
||||
if (!positive(item.getOuterLengthCm()) || !positive(item.getOuterWidthCm())
|
||||
|| !positive(item.getOuterHeightCm()) || !positive(item.getUnitVolumeCbm())
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion())) {
|
||||
applyCalculatedValues(item, dimensionWeighting.load(item.getTenantId()));
|
||||
measurementBatchService.calculateCartonDimensions(item);
|
||||
return;
|
||||
}
|
||||
BigDecimal count = BigDecimal.valueOf(item.getPackageCount());
|
||||
|
||||
+18
-20
@@ -25,6 +25,7 @@ public class CustomerDimensionWeightingService {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/** 加载完整四档箱规及客户明确选择的默认算法,配置存在不代表算法已启用。 */
|
||||
public RuleSet load(long tenantId) {
|
||||
List<CustomerDimensionConfigEntity> values = repository.findAll(tenantId);
|
||||
if (values == null || values.size() != CustomerDimensionBoxType.values().length
|
||||
@@ -33,7 +34,14 @@ public class CustomerDimensionWeightingService {
|
||||
.collect(java.util.stream.Collectors.toSet()).size() != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户箱规尺寸配置不完整,不能计算计费体积");
|
||||
}
|
||||
return new RuleSet(values.stream().map(Rule::from).toList());
|
||||
List<String> modes = values.stream().map(value -> value.getDefaultCalculationMode() == null
|
||||
|| value.getDefaultCalculationMode().isBlank() ? "ACTUAL"
|
||||
: value.getDefaultCalculationMode().trim().toUpperCase(java.util.Locale.ROOT))
|
||||
.distinct().toList();
|
||||
if (modes.size() != 1 || !List.of("ACTUAL", "ADD", "MULTIPLY").contains(modes.get(0))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户默认箱规计算方式配置不一致或无效");
|
||||
}
|
||||
return new RuleSet(values.stream().map(Rule::from).toList(), modes.get(0));
|
||||
}
|
||||
|
||||
private static boolean invalidRule(CustomerDimensionConfigEntity value) {
|
||||
@@ -48,14 +56,22 @@ public class CustomerDimensionWeightingService {
|
||||
|
||||
public static final class RuleSet {
|
||||
private final List<Rule> rules;
|
||||
private final String defaultCalculationMode;
|
||||
|
||||
private RuleSet(List<Rule> rules) {
|
||||
/** 保存同一客户的箱规和默认算法,供设备及人工测量统一使用。 */
|
||||
private RuleSet(List<Rule> rules, String defaultCalculationMode) {
|
||||
this.defaultCalculationMode = defaultCalculationMode;
|
||||
this.rules = rules.stream()
|
||||
.sorted(Comparator.comparing(Rule::innerVolume)
|
||||
.thenComparing(rule -> rule.boxType().ordinal()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 返回客户已启用的默认算法;ACTUAL 表示尺寸不调整。 */
|
||||
public String defaultCalculationMode() {
|
||||
return defaultCalculationMode;
|
||||
}
|
||||
|
||||
public WeightedDimensions resolve(BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm) {
|
||||
List<BigDecimal> measured = sortedDimensions(lengthCm, widthCm, heightCm);
|
||||
return rules.stream()
|
||||
@@ -66,24 +82,6 @@ public class CustomerDimensionWeightingService {
|
||||
.weightedActualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
|
||||
public WeightedDimensions resolveDeviceMeasurement(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
List<BigDecimal> measured = sortedDimensions(lengthCm, widthCm, heightCm);
|
||||
return rules.stream()
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(rule -> rule.weightedActualDimensions(lengthCm, widthCm, heightCm))
|
||||
.orElseGet(() -> actualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
|
||||
private WeightedDimensions actualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal rawVolume = lengthCm.multiply(widthCm).multiply(heightCm)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(null, null, null, lengthCm, widthCm, heightCm,
|
||||
rawVolume, rawVolume.setScale(3, RoundingMode.HALF_UP),
|
||||
"ACTUAL_DIMENSION_V1");
|
||||
}
|
||||
}
|
||||
|
||||
public record WeightedDimensions(
|
||||
|
||||
@@ -56,6 +56,7 @@ public class MeasurementBatchService {
|
||||
@Resource
|
||||
private FbaPricingRepository pricingRepository;
|
||||
|
||||
/** 按已选算法保存测量批次及计算快照,不推进履约状态。 */
|
||||
@Transactional
|
||||
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
|
||||
var inbound = repository.findInbound(command.tenantId(), command.inboundId());
|
||||
@@ -68,7 +69,7 @@ public class MeasurementBatchService {
|
||||
}
|
||||
validateSource(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
|
||||
command, dimensionWeighting.load(command.tenantId()));
|
||||
command.tenantId(), command.items(), dimensionWeighting.load(command.tenantId()));
|
||||
|
||||
InboundMeasurementEntity measurement;
|
||||
if (command.measurementId() == null) {
|
||||
@@ -93,6 +94,7 @@ public class MeasurementBatchService {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
/** 修改批次算法时,以实测及人工修正基数重算,避免重复叠加。 */
|
||||
void recalculateStrategy(long tenantId, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> existingItems,
|
||||
String coefficientMode, Long chargeRuleId) {
|
||||
@@ -112,7 +114,7 @@ public class MeasurementBatchService {
|
||||
item.getOverLimitWarning(), item.getManualOuterLengthCm(), item.getManualOuterWidthCm(),
|
||||
item.getManualOuterHeightCm(), chargeRuleId, coefficientMode, item.getRemark())).toList());
|
||||
List<InboundMeasurementItemEntity> recalculated = calculateItems(
|
||||
command, dimensionWeighting.load(tenantId));
|
||||
tenantId, command.items(), dimensionWeighting.load(tenantId));
|
||||
for (int index = 0; index < existingItems.size(); index++) {
|
||||
InboundMeasurementItemEntity target = existingItems.get(index);
|
||||
InboundMeasurementItemEntity source = recalculated.get(index);
|
||||
@@ -121,6 +123,7 @@ public class MeasurementBatchService {
|
||||
applyTotals(measurement, existingItems);
|
||||
}
|
||||
|
||||
/** 按保存的算法重算单行外箱修正,并更新批次汇总。 */
|
||||
void recalculateOuterDimensions(long tenantId, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> existingItems, long itemId,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm,
|
||||
@@ -143,14 +146,42 @@ public class MeasurementBatchService {
|
||||
outerWidthCm, outerHeightCm, target.getChargeRuleId(), target.getCoefficientMode(),
|
||||
target.getRemark())));
|
||||
InboundMeasurementItemEntity recalculated = calculateItems(
|
||||
command, dimensionWeighting.load(tenantId)).get(0);
|
||||
tenantId, command.items(), dimensionWeighting.load(tenantId)).get(0);
|
||||
applyRecalculatedItem(target, recalculated, true);
|
||||
applyTotals(measurement, existingItems);
|
||||
}
|
||||
|
||||
/** 设备首次测量与复测共用批次算法,保留已有人工修正及明确选择的算法。 */
|
||||
void calculateCartonDimensions(InboundMeasurementItemEntity item) {
|
||||
MeasurementBatchCommand.Item source = new MeasurementBatchCommand.Item(
|
||||
item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), item.getManualOuterLengthCm(), item.getManualOuterWidthCm(),
|
||||
item.getManualOuterHeightCm(), item.getChargeRuleId(), item.getCoefficientMode(), item.getRemark());
|
||||
InboundMeasurementItemEntity calculated = calculateItems(item.getTenantId(), List.of(source),
|
||||
dimensionWeighting.load(item.getTenantId())).get(0);
|
||||
copyCalculatedValues(item, calculated);
|
||||
}
|
||||
|
||||
/** 保存重算后的行,保留箱码、设备证据和批次身份。 */
|
||||
private void applyRecalculatedItem(InboundMeasurementItemEntity target,
|
||||
InboundMeasurementItemEntity source,
|
||||
boolean updateManualDimensions) {
|
||||
copyCalculatedValues(target, source);
|
||||
if (updateManualDimensions) {
|
||||
target.setManualOuterLengthCm(source.getManualOuterLengthCm());
|
||||
target.setManualOuterWidthCm(source.getManualOuterWidthCm());
|
||||
target.setManualOuterHeightCm(source.getManualOuterHeightCm());
|
||||
}
|
||||
target.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateItem(target)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
}
|
||||
|
||||
/** 统一复制尺寸、体积、材积重及算法快照,不覆盖原始测量值。 */
|
||||
private void copyCalculatedValues(InboundMeasurementItemEntity target,
|
||||
InboundMeasurementItemEntity source) {
|
||||
target.setGirthCm(source.getGirthCm());
|
||||
target.setUnitVolumeCbm(source.getUnitVolumeCbm());
|
||||
target.setVolumeRawCbm(source.getVolumeRawCbm());
|
||||
@@ -163,15 +194,6 @@ public class MeasurementBatchService {
|
||||
target.setOuterLengthCm(source.getOuterLengthCm());
|
||||
target.setOuterWidthCm(source.getOuterWidthCm());
|
||||
target.setOuterHeightCm(source.getOuterHeightCm());
|
||||
if (updateManualDimensions) {
|
||||
target.setManualOuterLengthCm(source.getManualOuterLengthCm());
|
||||
target.setManualOuterWidthCm(source.getManualOuterWidthCm());
|
||||
target.setManualOuterHeightCm(source.getManualOuterHeightCm());
|
||||
}
|
||||
target.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateItem(target)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,10 +279,12 @@ public class MeasurementBatchService {
|
||||
measurement.setActualGoodsName(command.actualGoodsName());
|
||||
}
|
||||
|
||||
/** 明确的行算法优先;未选择时仅使用客户默认算法,不因预置系数而自动启用。 */
|
||||
private List<InboundMeasurementItemEntity> calculateItems(
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
long tenantId, List<MeasurementBatchCommand.Item> sourceItems,
|
||||
CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
Set<String> itemNumbers = new HashSet<>();
|
||||
return command.items().stream().map(source -> {
|
||||
return sourceItems.stream().map(source -> {
|
||||
if (!itemNumbers.add(source.itemNo())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细号不能重复");
|
||||
}
|
||||
@@ -279,10 +303,9 @@ public class MeasurementBatchService {
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
|
||||
baseLength, baseWidth, baseHeight);
|
||||
String requestedMode = normalizeMode(source.coefficientMode());
|
||||
CustomerChargeRuleEntity coefficientRule = "MULTIPLY".equals(requestedMode)
|
||||
? requireCoefficientRule(command.tenantId(), source.chargeRuleId())
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId()) : null;
|
||||
String coefficientMode = coefficientRule != null ? "MULTIPLY" : requestedMode;
|
||||
String coefficientMode = requestedMode == null ? dimensionRules.defaultCalculationMode() : requestedMode;
|
||||
CustomerChargeRuleEntity coefficientRule = "MULTIPLY".equals(coefficientMode)
|
||||
? requireCoefficientRule(tenantId, source.chargeRuleId()) : null;
|
||||
boolean useDimensionRule = "ADD".equals(coefficientMode);
|
||||
BigDecimal addition = weighted.edgeAdditionCm() == null
|
||||
? null : weighted.edgeAdditionCm().multiply(BigDecimal.valueOf(2));
|
||||
@@ -374,10 +397,6 @@ public class MeasurementBatchService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity findDefaultCoefficientRule(long tenantId) {
|
||||
return pricingRepository.findDefaultRule(tenantId, tenantId);
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity requireCoefficientRule(long tenantId, Long ruleId) {
|
||||
CustomerChargeRuleEntity rule = ruleId == null
|
||||
? pricingRepository.findDefaultRule(tenantId, tenantId)
|
||||
|
||||
@@ -2245,7 +2245,7 @@ components:
|
||||
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: [ACTUAL, ADD, MULTIPLY], nullable: true}
|
||||
coefficientMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY], nullable: true, description: 明确选择的行算法优先;未指定时仅使用客户默认算法,ACTUAL 不调整修正后的实测尺寸,系数配置存在不代表启用 MULTIPLY}
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
InboundMeasurementDetailEnvelope:
|
||||
type: object
|
||||
|
||||
+68
-2
@@ -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();
|
||||
}
|
||||
|
||||
+11
-18
@@ -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
|
||||
|
||||
+94
-3
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user