Files
baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementBatchService.java
T

496 lines
28 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 后台录入和第三方回传共用的测量批次持久化服务。
*
* <p>本服务只保存测量批次和明细,绝不推进入库单状态。第三方适配层应调用
* {@link #saveBatch(MeasurementBatchCommand)},由平台确认接口另行提交审核。</p>
*/
@Service
public class MeasurementBatchService {
/** 后台人工录入的测量来源。 */
static final String SOURCE_MANUAL = "MANUAL";
/** 第三方设备或开放接口回传的测量来源。 */
static final String SOURCE_THIRD_PARTY = "THIRD_PARTY";
/** 测量草稿,可继续编辑。 */
static final String STATUS_DRAFT = "DRAFT";
/** 已接收第三方测量数据,待平台提交审核。 */
static final String STATUS_RECEIVED = "RECEIVED";
private static final Set<String> EDITABLE_INBOUND_STATUSES = Set.of(
"PREPARING", "WAIT_MEASURE", "MEASURING", "PROCESSING", "MEASURE_REJECTED");
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
private static final Pattern SPECIFICATION_COEFFICIENT_PATTERN = Pattern.compile(
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)(?:;重量:(\\d+(?:\\.\\d+)?))?");
@Resource
private InboundMeasurementRepository repository;
@Resource
private CustomerDimensionWeightingService dimensionWeighting;
@Resource
private FbaPricingRepository pricingRepository;
/** 按已选算法保存测量批次及计算快照,不推进履约状态。 */
@Transactional
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
var inbound = repository.findInbound(command.tenantId(), command.inboundId());
if (inbound == null) {
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
}
if (!EDITABLE_INBOUND_STATUSES.contains(inbound.getProcessStatus())) {
throw new BizException(ResultCode.CONFLICT,
"当前入库状态不允许保存测量批次,已提交批次必须保持冻结");
}
validateSource(command);
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
command.tenantId(), command.items(), dimensionWeighting.load(command.tenantId()));
InboundMeasurementEntity measurement;
if (command.measurementId() == null) {
measurement = newMeasurement(command);
applyTotals(measurement, calculatedItems);
repository.insertMeasurement(measurement);
} else {
measurement = requireEditableBatch(command);
measurement.setStatus(STATUS_DRAFT);
applyHeader(measurement, command);
applyTotals(measurement, calculatedItems);
if (!repository.updateMeasurement(measurement)) {
throw concurrentModification();
}
}
preserveScannedTraceability(command, measurement, calculatedItems);
calculatedItems.forEach(item -> {
item.setTenantId(command.tenantId());
item.setMeasurementId(measurement.getId());
});
repository.replaceItems(command.tenantId(), measurement.getId(), calculatedItems);
return measurement;
}
/** 修改批次算法时,以实测及人工修正基数重算,避免重复叠加。 */
void recalculateStrategy(long tenantId, InboundMeasurementEntity measurement,
List<InboundMeasurementItemEntity> existingItems,
String coefficientMode, Long chargeRuleId) {
if (existingItems.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "当前测量批次没有可重算的明细");
}
MeasurementBatchCommand command = new MeasurementBatchCommand(
tenantId, measurement.getInboundId(), measurement.getId(), measurement.getVersion(),
measurement.getSourceType(), measurement.getSourceCode(), measurement.getExternalEventNo(),
measurement.getDeviceCode(), measurement.getWarehouseCode(), measurement.getMeasuredAt(),
measurement.getRawPayloadJson(), measurement.getOriginalDocumentNo(),
measurement.getOriginalGrossWeightKg(), measurement.getOriginalVolumeCbm(),
measurement.getActualGoodsName(), measurement.getCreatedBy(),
existingItems.stream().map(item -> new MeasurementBatchCommand.Item(
item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), null, item.getSourceImageFileId(),
item.getOverLimitWarning(), item.getManualOuterLengthCm(), item.getManualOuterWidthCm(),
item.getManualOuterHeightCm(), chargeRuleId, coefficientMode, item.getRemark())).toList());
List<InboundMeasurementItemEntity> recalculated = calculateItems(
tenantId, command.items(), dimensionWeighting.load(tenantId));
for (int index = 0; index < existingItems.size(); index++) {
InboundMeasurementItemEntity target = existingItems.get(index);
InboundMeasurementItemEntity source = recalculated.get(index);
applyRecalculatedItem(target, source, false);
}
applyTotals(measurement, existingItems);
}
/** 按保存的算法重算单行外箱修正,并更新批次汇总。 */
void recalculateOuterDimensions(long tenantId, InboundMeasurementEntity measurement,
List<InboundMeasurementItemEntity> existingItems, long itemId,
BigDecimal outerLengthCm, BigDecimal outerWidthCm,
BigDecimal outerHeightCm) {
InboundMeasurementItemEntity target = existingItems.stream()
.filter(item -> item.getId() != null && item.getId() == itemId)
.findFirst()
.orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "测量明细不存在"));
MeasurementBatchCommand command = new MeasurementBatchCommand(
tenantId, measurement.getInboundId(), measurement.getId(), measurement.getVersion(),
measurement.getSourceType(), measurement.getSourceCode(), measurement.getExternalEventNo(),
measurement.getDeviceCode(), measurement.getWarehouseCode(), measurement.getMeasuredAt(),
measurement.getRawPayloadJson(), measurement.getOriginalDocumentNo(),
measurement.getOriginalGrossWeightKg(), measurement.getOriginalVolumeCbm(),
measurement.getActualGoodsName(), measurement.getCreatedBy(),
List.of(new MeasurementBatchCommand.Item(
target.getInboundDetailId(), target.getItemNo(), target.getBoxSpecification(),
target.getPackageCount(), target.getLengthCm(), target.getWidthCm(), target.getHeightCm(),
null, target.getSourceImageFileId(), target.getOverLimitWarning(), outerLengthCm,
outerWidthCm, outerHeightCm, target.getChargeRuleId(), target.getCoefficientMode(),
target.getRemark())));
InboundMeasurementItemEntity recalculated = calculateItems(
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());
target.setVolumeCbm(source.getVolumeCbm());
target.setTotalVolumeCbm(source.getTotalVolumeCbm());
target.setVolumeWeightKg(source.getVolumeWeightKg());
target.setChargeRuleId(source.getChargeRuleId());
target.setCoefficientMode(source.getCoefficientMode());
target.setCalculationRuleVersion(source.getCalculationRuleVersion());
target.setOuterLengthCm(source.getOuterLengthCm());
target.setOuterWidthCm(source.getOuterWidthCm());
target.setOuterHeightCm(source.getOuterHeightCm());
}
/**
* 人工补录会整批替换可编辑测量项,但不能抹掉同一项已冻结的扫码身份与设备取证。
* 只按入库货物明细和明细号精确匹配,避免把一个箱码猜测性地复制给其他测量项。
*/
private void preserveScannedTraceability(MeasurementBatchCommand command,
InboundMeasurementEntity measurement,
List<InboundMeasurementItemEntity> replacement) {
if (command.measurementId() == null || replacement.isEmpty()) {
return;
}
Map<String, InboundMeasurementItemEntity> scannedByItem = new HashMap<>();
for (InboundMeasurementItemEntity existing : repository.listItems(command.tenantId(), measurement.getId())) {
if (existing.getBoxCode() != null && !existing.getBoxCode().isBlank()) {
scannedByItem.put(itemKey(existing.getInboundDetailId(), existing.getItemNo()), existing);
}
}
for (InboundMeasurementItemEntity item : replacement) {
InboundMeasurementItemEntity scanned = scannedByItem.get(itemKey(item.getInboundDetailId(), item.getItemNo()));
if (scanned == null) {
continue;
}
item.setExternalDetailNo(scanned.getExternalDetailNo());
item.setBoxCode(scanned.getBoxCode());
item.setScannedShipmentId(scanned.getScannedShipmentId());
item.setMachineNo(scanned.getMachineNo());
item.setDeviceMeasuredAt(scanned.getDeviceMeasuredAt());
}
}
private String itemKey(Long inboundDetailId, String itemNo) {
return String.valueOf(inboundDetailId) + ':' + itemNo;
}
private void validateSource(MeasurementBatchCommand command) {
if (!SOURCE_MANUAL.equals(command.sourceType()) && !SOURCE_THIRD_PARTY.equals(command.sourceType())) {
throw new BizException(ResultCode.BAD_REQUEST, "测量数据来源不支持");
}
if (SOURCE_THIRD_PARTY.equals(command.sourceType())
&& (command.externalEventNo() == null || command.externalEventNo().isBlank())) {
throw new BizException(ResultCode.BAD_REQUEST, "第三方测量必须提供事件号");
}
if (command.items() == null || command.items().isEmpty()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细不能为空");
}
}
private InboundMeasurementEntity newMeasurement(MeasurementBatchCommand command) {
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setTenantId(command.tenantId());
measurement.setInboundId(command.inboundId());
measurement.setSourceType(command.sourceType());
measurement.setStatus(SOURCE_MANUAL.equals(command.sourceType()) ? STATUS_DRAFT : STATUS_RECEIVED);
measurement.setCreatedBy(command.operatorId());
measurement.setVersion(0);
applyHeader(measurement, command);
return measurement;
}
private InboundMeasurementEntity requireEditableBatch(MeasurementBatchCommand command) {
InboundMeasurementEntity measurement = repository.findMeasurement(
command.tenantId(), command.inboundId(), command.measurementId());
if (measurement == null) {
throw new BizException(ResultCode.NOT_FOUND, "测量批次不存在");
}
if (!Integer.valueOf(command.expectedVersion()).equals(measurement.getVersion())) {
throw concurrentModification();
}
return measurement;
}
private void applyHeader(InboundMeasurementEntity measurement, MeasurementBatchCommand command) {
measurement.setSourceCode(command.sourceCode());
measurement.setExternalEventNo(command.externalEventNo());
measurement.setDeviceCode(command.deviceCode());
measurement.setWarehouseCode(command.warehouseCode());
measurement.setMeasuredAt(command.measuredAt());
measurement.setRawPayloadJson(command.rawPayloadJson());
measurement.setOriginalDocumentNo(command.originalDocumentNo());
measurement.setOriginalGrossWeightKg(command.originalGrossWeightKg());
measurement.setOriginalVolumeCbm(command.originalVolumeCbm());
measurement.setActualGoodsName(command.actualGoodsName());
}
/** 明确的行算法优先;未选择时仅使用客户默认算法,不因预置系数而自动启用。 */
private List<InboundMeasurementItemEntity> calculateItems(
long tenantId, List<MeasurementBatchCommand.Item> sourceItems,
CustomerDimensionWeightingService.RuleSet dimensionRules) {
Set<String> itemNumbers = new HashSet<>();
return sourceItems.stream().map(source -> {
if (!itemNumbers.add(source.itemNo())) {
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细号不能重复");
}
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());
}
boolean hasOuterOverride = source.outerLengthCm() != null
|| source.outerWidthCm() != null
|| source.outerHeightCm() != null;
BigDecimal baseLength = source.outerLengthCm() != null ? source.outerLengthCm() : source.lengthCm();
BigDecimal baseWidth = source.outerWidthCm() != null ? source.outerWidthCm() : source.widthCm();
BigDecimal baseHeight = source.outerHeightCm() != null ? source.outerHeightCm() : source.heightCm();
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
baseLength, baseWidth, baseHeight);
String requestedMode = normalizeMode(source.coefficientMode());
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));
boolean hasMatchedAddition = useDimensionRule && weighted.boxType() != null;
// ADD 只从匹配箱规取得加边值,外尺寸仍以本次实测尺寸为基准,
// 不能把箱规边界尺寸当成本次货物的实际外尺寸。
BigDecimal outerLength = hasMatchedAddition ? baseLength.add(addition) : baseLength;
BigDecimal outerWidth = hasMatchedAddition ? baseWidth.add(addition) : baseWidth;
BigDecimal outerHeight = hasMatchedAddition ? baseHeight.add(addition) : baseHeight;
if (coefficientRule != null) {
BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
coefficientRule, baseLength, baseWidth, baseHeight, source.unitWeightKg());
outerLength = applyCoefficient(outerLength, specificationCoefficient);
outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
outerHeight = applyCoefficient(outerHeight, specificationCoefficient);
}
BigDecimal unitVolumeRaw = outerLength.multiply(outerWidth).multiply(outerHeight)
.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride
? unitVolumeRaw.setScale(3, RoundingMode.HALF_UP)
: unitVolumeRaw.setScale(4, RoundingMode.CEILING);
BigDecimal packageCount = BigDecimal.valueOf(source.packageCount());
BigDecimal totalVolumeRaw = unitVolumeRaw.multiply(packageCount).setScale(6, RoundingMode.HALF_UP);
BigDecimal totalVolume;
try {
totalVolume = unitVolume.multiply(packageCount).setScale(4, RoundingMode.UNNECESSARY);
} catch (ArithmeticException exception) {
throw measurementRangeExceeded(source);
}
if (totalVolumeRaw.compareTo(MAX_RAW_VOLUME_CBM) > 0
|| totalVolume.compareTo(MAX_TOTAL_VOLUME_CBM) > 0) {
throw measurementRangeExceeded(source);
}
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setInboundDetailId(source.inboundDetailId());
item.setItemNo(source.itemNo());
item.setBoxSpecification(source.boxSpecification());
item.setPackageCount(source.packageCount());
item.setLengthCm(source.lengthCm());
item.setWidthCm(source.widthCm());
item.setHeightCm(source.heightCm());
BigDecimal girth = outerLength.add(outerWidth.add(outerHeight).multiply(BigDecimal.valueOf(2)));
item.setGirthCm(girth.setScale(3, RoundingMode.CEILING));
item.setUnitVolumeCbm(unitVolume);
item.setVolumeRawCbm(totalVolumeRaw);
item.setVolumeCbm(totalVolume);
item.setTotalVolumeCbm(totalVolume);
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
item.setChargeRuleId(coefficientRule == null ? null : coefficientRule.getId());
item.setCoefficientMode(coefficientMode);
item.setCalculationRuleVersion(hasOuterOverride ? warehouseOverrideVersion(source)
: coefficientRule != null
? "CUSTOMER_COEFFICIENT_V" + coefficientRule.getRuleVersion() + "_MULTIPLY"
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
item.setSourceImageFileId(source.intelligentImageFileId());
item.setOverLimitWarning(source.overLimitWarning());
// 人工外箱值是策略计算基数;最终外箱值单独落库,避免覆盖基数或重复叠加策略。
item.setManualOuterLengthCm(source.outerLengthCm());
item.setManualOuterWidthCm(source.outerWidthCm());
item.setManualOuterHeightCm(source.outerHeightCm());
item.setOuterLengthCm(outerLength.setScale(3, RoundingMode.HALF_UP));
item.setOuterWidthCm(outerWidth.setScale(3, RoundingMode.HALF_UP));
item.setOuterHeightCm(outerHeight.setScale(3, RoundingMode.HALF_UP));
item.setRemark(source.remark());
item.setCreatedAt(LocalDateTime.now());
return item;
}).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);
if (!Set.of("ACTUAL", "ADD", "MULTIPLY").contains(normalized)) {
throw new BizException(ResultCode.VALIDATION_FAILED, "系数模式仅支持 ADD 或 MULTIPLY");
}
return normalized;
}
private CustomerChargeRuleEntity requireCoefficientRule(long tenantId, Long ruleId) {
CustomerChargeRuleEntity rule = ruleId == null
? pricingRepository.findDefaultRule(tenantId, tenantId)
: pricingRepository.findRule(tenantId, ruleId);
if (rule == null || !"ACTIVE".equals(rule.getStatus())
|| !java.util.Objects.equals(rule.getTenantId(), tenantId)) {
throw new BizException(ResultCode.VALIDATION_FAILED, "计费规则不属于当前客户或已停用");
}
return rule;
}
private BigDecimal applyCoefficient(BigDecimal dimension, BigDecimal coefficient) {
if (coefficient == null) coefficient = BigDecimal.ONE;
BigDecimal result = dimension.multiply(coefficient);
if (result.compareTo(BigDecimal.ZERO) <= 0 || result.compareTo(new BigDecimal("999999999.999")) > 0) {
throw new BizException(ResultCode.VALIDATION_FAILED, "应用计费系数后的箱规尺寸超出允许范围");
}
return result;
}
private BigDecimal resolveSpecificationCoefficient(CustomerChargeRuleEntity rule,
BigDecimal length, BigDecimal width,
BigDecimal height, BigDecimal unitWeightKg) {
List<SpecificationCoefficient> tiers = parseSpecificationCoefficients(rule.getRemark());
if (tiers.isEmpty()) {
return rule.getLengthFactor() == null ? BigDecimal.ONE : rule.getLengthFactor();
}
List<BigDecimal> measured = sortedDimensions(length, width, height);
return tiers.stream()
.filter(tier -> tier.contains(measured, unitWeightKg))
.findFirst()
.orElse(tiers.get(tiers.size() - 1))
.coefficient();
}
private List<SpecificationCoefficient> parseSpecificationCoefficients(String remark) {
if (remark == null || remark.isBlank()) return List.of();
Matcher matcher = SPECIFICATION_COEFFICIENT_PATTERN.matcher(remark);
List<SpecificationCoefficient> tiers = new ArrayList<>();
while (matcher.find()) {
List<BigDecimal> dimensions = sortedDimensions(new BigDecimal(matcher.group(2)),
new BigDecimal(matcher.group(3)), new BigDecimal(matcher.group(4)));
BigDecimal coefficient = new BigDecimal(matcher.group(5));
BigDecimal weight = matcher.group(6) == null ? null : new BigDecimal(matcher.group(6));
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient, weight));
}
tiers.sort(Comparator.comparing(SpecificationCoefficient::volume));
return tiers;
}
private List<BigDecimal> sortedDimensions(BigDecimal length, BigDecimal width, BigDecimal height) {
List<BigDecimal> dimensions = new ArrayList<>(List.of(length, width, height));
dimensions.sort(Comparator.reverseOrder());
return dimensions;
}
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient, BigDecimal maxWeightKg) {
private BigDecimal volume() {
return dimensions.get(0).multiply(dimensions.get(1)).multiply(dimensions.get(2));
}
private boolean contains(List<BigDecimal> measured, BigDecimal unitWeightKg) {
return measured.get(0).compareTo(dimensions.get(0)) <= 0
&& measured.get(1).compareTo(dimensions.get(1)) <= 0
&& measured.get(2).compareTo(dimensions.get(2)) <= 0
&& (maxWeightKg == null || unitWeightKg == null || unitWeightKg.compareTo(maxWeightKg) <= 0);
}
}
private static BizException measurementRangeExceeded(MeasurementBatchCommand.Item source) {
return new BizException(ResultCode.VALIDATION_FAILED,
"箱规尺寸或箱数超出系统允许范围,明细 " + source.itemNo() + ":"
+ source.lengthCm() + " × " + source.widthCm() + " × " + source.heightCm()
+ " cm,箱数 " + source.packageCount());
}
private void applyTotals(InboundMeasurementEntity measurement,
List<InboundMeasurementItemEntity> items) {
long packages = items.stream().mapToLong(InboundMeasurementItemEntity::getPackageCount).sum();
if (packages > Integer.MAX_VALUE) {
throw new BizException(ResultCode.VALIDATION_FAILED, "测量总箱数超过系统上限");
}
BigDecimal rawVolume = items.stream().map(InboundMeasurementItemEntity::getVolumeRawCbm)
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(6, RoundingMode.HALF_UP);
BigDecimal volume = items.stream().map(InboundMeasurementItemEntity::getTotalVolumeCbm)
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.CEILING);
measurement.setTotalPackageCount((int) packages);
measurement.setTotalVolumeRawCbm(rawVolume);
measurement.setTotalVolumeCbm(volume);
measurement.setVolumeWeightKg(volume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
}
private BizException concurrentModification() {
return new BizException(ResultCode.CONFLICT, "测量数据已被其他人修改,请刷新后重试");
}
}