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

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
@@ -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());
@@ -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)