496 lines
28 KiB
Java
496 lines
28 KiB
Java
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, "测量数据已被其他人修改,请刷新后重试");
|
||
}
|
||
}
|