DeliveryFeeServiceImpl.java 15.4 KB
package com.diligrp.rider.service.impl;

import com.diligrp.rider.common.exception.BizException;
import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
import com.diligrp.rider.service.CityService;
import com.diligrp.rider.service.DeliveryFeeService;
import com.diligrp.rider.util.GeoUtil;
import com.diligrp.rider.vo.DeliveryFeeResultVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * 配送费计算引擎
 * Helpsend.computed() + City.checkTime() + City.getLength()
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DeliveryFeeServiceImpl implements DeliveryFeeService {

    private final CityService cityService;

    @Override
    public DeliveryFeeResultVO calcFee(DeliveryFeeCalcDTO dto) {
        DeliveryPricingConfigDTO pricingConfig = cityService.getConfig(dto.getCityId());
        return calcFeeByConfig(pricingConfig, dto);
    }

    @Override
    public DeliveryFeeResultVO calcFeeByConfig(DeliveryPricingConfigDTO pricingConfig, DeliveryFeeCalcDTO dto) {
        if (pricingConfig == null || pricingConfig.getType() == null) {
            throw new BizException("当前城市未开通服务");
        }

        int orderType = dto.getOrderType();
        if (!pricingConfig.getType().contains(orderType)) {
            throw new BizException("当前城市未开通该类型服务");
        }

        // 获取该订单类型的配送费配置
        DeliveryPricingRuleDTO typeConfig = getTypeConfig(pricingConfig, orderType);
        if (typeConfig == null) {
            throw new BizException("该服务类型未配置收费规则");
        }

        DeliveryFeeResultVO result = new DeliveryFeeResultVO();
        result.setMoneyBasic(BigDecimal.ZERO);
        result.setMoneyBasicTxt("");
        result.setMoneyDistance(BigDecimal.ZERO);
        result.setMoneyDistanceTxt("");
        result.setMoneyWeight(BigDecimal.ZERO);
        result.setMoneyWeightTxt("");
        result.setMoneyPiece(BigDecimal.ZERO);
        result.setMoneyPieceTxt("");
        result.setMoneyTime(BigDecimal.ZERO);
        result.setDistance(BigDecimal.ZERO);
        result.setWeight(dto.getWeight());
        result.setPieces(dto.getPieces());
        result.setMinFee(nvl(typeConfig.getMinFee()));
        result.setMinFeeApplied(0);

        // --- fee_mode=1:固定费用 ---
        if (typeConfig.getFeeMode() != null && typeConfig.getFeeMode() == 1) {
            BigDecimal fix = typeConfig.getFixMoney() != null ? typeConfig.getFixMoney() : BigDecimal.ZERO;
            result.setMoneyBasic(fix);
            result.setTotalFee(applyMinFee(fix, typeConfig, result));
            result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, BigDecimal.ZERO));
            return result;
        }

        // --- fee_mode=2:按距离/重量计费 ---
        BigDecimal moneyBasic = BigDecimal.ZERO;
        BigDecimal moneyDistance = BigDecimal.ZERO;
        BigDecimal moneyWeight = BigDecimal.ZERO;
        BigDecimal moneyPiece = BigDecimal.ZERO;
        String moneyBasicTxt = "";
        String moneyDistanceTxt = "";
        String moneyWeightTxt = "";
        String moneyPieceTxt = "";
        BigDecimal distanceKm = BigDecimal.ZERO;

        // 基础费
        if (typeConfig.getBaseSwitch() != null && typeConfig.getBaseSwitch() == 1) {
            moneyBasic = moneyBasic.add(nvl(typeConfig.getBaseFee()));
        }

        // 距离计费
        if (typeConfig.getDistanceSwitch() != null && typeConfig.getDistanceSwitch() == 1) {
            BigDecimal distanceBasicMoney = nvl(typeConfig.getDistanceBasicMoney());
            moneyBasic = moneyBasic.add(distanceBasicMoney);
            BigDecimal basicKm = nvl(typeConfig.getDistanceBasic());
            moneyBasicTxt = "(" + basicKm.stripTrailingZeros().toPlainString() + "km)";

            // 计算实际距离
            distanceKm = calcDistance(typeConfig, dto);

            // 超出距离费
            if (distanceKm.compareTo(basicKm) > 0) {
                BigDecimal moreKm = distanceKm.subtract(basicKm);
                if (typeConfig.getDistanceSteps() != null && !typeConfig.getDistanceSteps().isEmpty()) {
                    moneyDistance = calcDistanceStepFee(basicKm, distanceKm, typeConfig.getDistanceSteps());
                } else {
                    BigDecimal moreMoneyPerKm = nvl(typeConfig.getDistanceMoreMoney());
                    BigDecimal moreKmCeil = moreKm.setScale(0, RoundingMode.CEILING);
                    moneyDistance = moreKmCeil.multiply(moreMoneyPerKm).setScale(2, RoundingMode.HALF_UP);
                }
                moneyDistanceTxt = "(" + moreKm.setScale(1, RoundingMode.HALF_UP).toPlainString() + "km)";
            }
        }

        // 重量计费
        if (typeConfig.getWeightSwitch() != null && typeConfig.getWeightSwitch() == 1) {
            BigDecimal weightBasicMoney = nvl(typeConfig.getWeightFirstFee().compareTo(BigDecimal.ZERO) > 0
                    ? typeConfig.getWeightFirstFee() : typeConfig.getWeightBasicMoney());
            moneyBasic = moneyBasic.add(weightBasicMoney);

            BigDecimal weight = dto.getWeight() != null ? dto.getWeight() : BigDecimal.ZERO;
            BigDecimal weightAdj = adjustValue(weight, typeConfig.getWeightType());
            BigDecimal basicWeight = nvl(typeConfig.getWeightFirst().compareTo(BigDecimal.ZERO) > 0
                    ? typeConfig.getWeightFirst() : typeConfig.getWeightBasic());
            BigDecimal moreMoneyPerKg = nvl(typeConfig.getWeightUnitFee().compareTo(BigDecimal.ZERO) > 0
                    ? typeConfig.getWeightUnitFee() : typeConfig.getWeightMoreMoney());
            BigDecimal capFee = nvl(typeConfig.getWeightCapFee());

            if (weightAdj.compareTo(basicWeight) > 0) {
                BigDecimal moreWeight = weightAdj.subtract(basicWeight);
                moneyWeight = moreWeight.multiply(moreMoneyPerKg).setScale(2, RoundingMode.HALF_UP);
                if (capFee.compareTo(BigDecimal.ZERO) > 0) {
                    BigDecimal weightTotal = weightBasicMoney.add(moneyWeight);
                    if (weightTotal.compareTo(capFee) > 0) {
                        moneyWeight = capFee.subtract(weightBasicMoney).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
                    }
                }
                moneyWeightTxt = "(" + moreWeight.toPlainString() + "kg)";
            }
        }

        // 件数计费
        if (typeConfig.getPieceSwitch() != null && typeConfig.getPieceSwitch() == 1
                && typeConfig.getPieceRules() != null && !typeConfig.getPieceRules().isEmpty()) {
            int pieces = dto.getPieces() != null ? dto.getPieces() : 0;
            List<DeliveryPricingRuleDTO.PieceRuleDTO> rules = new ArrayList<>(typeConfig.getPieceRules());
            rules.sort(Comparator.comparing(rule -> rule.getListOrder() == null ? 0 : rule.getListOrder()));
            for (DeliveryPricingRuleDTO.PieceRuleDTO rule : rules) {
                int start = rule.getStartPiece() != null ? rule.getStartPiece() : 0;
                int end = rule.getEndPiece() != null ? rule.getEndPiece() : Integer.MAX_VALUE;
                if (pieces >= start && pieces <= end) {
                    moneyPiece = nvl(rule.getFee()).setScale(2, RoundingMode.HALF_UP);
                    moneyPieceTxt = "(" + start + "-" + end + "件)";
                    break;
                }
            }
        }

        // 时段附加费
        long serviceTime = dto.getServiceTime() != null && dto.getServiceTime() > 0
                ? dto.getServiceTime() : System.currentTimeMillis() / 1000;
        BigDecimal moneyTime = calcTimeMoney(typeConfig, serviceTime);

        result.setMoneyBasic(moneyBasic);
        result.setMoneyBasicTxt(moneyBasicTxt);
        result.setMoneyDistance(moneyDistance);
        result.setMoneyDistanceTxt(moneyDistanceTxt);
        result.setMoneyWeight(moneyWeight);
        result.setMoneyWeightTxt(moneyWeightTxt);
        result.setMoneyPiece(moneyPiece);
        result.setMoneyPieceTxt(moneyPieceTxt);
        result.setMoneyTime(moneyTime);
        result.setDistance(distanceKm);

        BigDecimal total = moneyBasic.add(moneyDistance).add(moneyWeight).add(moneyPiece).add(moneyTime);
        result.setTotalFee(applyMinFee(total, typeConfig, result));
        result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, distanceKm));

        return result;
    }

    @Override
    public boolean isServiceEnabled(Long cityId, int orderType) {
        DeliveryPricingConfigDTO config = cityService.getConfig(cityId);
        return config != null && config.getType() != null && config.getType().contains(orderType);
    }

    // ---- 私有方法 ----

    /**
     * 计算实际距离(km), getDistance() + distance_type取整
     */
    private BigDecimal calcDistance(DeliveryPricingRuleDTO typeConfig, DeliveryFeeCalcDTO dto) {
        double disKm;
        if (typeConfig.getDistanceMode() != null && typeConfig.getDistanceMode() == 2) {
            // 直接使用传入距离(米转km)
            disKm = 0; // 传入距离模式下 dto 中应有 distance 字段,此处简化为0
        } else {
            // 经纬度计算
            try {
                disKm = GeoUtil.calcDistanceKm(
                        Double.parseDouble(dto.getStartLat()),
                        Double.parseDouble(dto.getStartLng()),
                        Double.parseDouble(dto.getEndLat()),
                        Double.parseDouble(dto.getEndLng()));
            } catch (Exception e) {
                throw new BizException("地点经纬度信息错误");
            }
        }
        return adjustValue(BigDecimal.valueOf(disKm), typeConfig.getDistanceType());
    }

    /**
     * 根据取整方式调整数值
     * distanceType: 1=四舍五入(保留1位) 2=向上取整 3=向下取整
     */
    private BigDecimal adjustValue(BigDecimal value, Integer type) {
        if (type == null || type == 1) {
            return value.setScale(1, RoundingMode.HALF_UP);
        } else if (type == 2) {
            return value.setScale(0, RoundingMode.CEILING).setScale(1);
        } else {
            return value.setScale(0, RoundingMode.FLOOR).setScale(1);
        }
    }

    /**
     * 计算时段附加费
     *  City.checkTime()
     */
    private BigDecimal calcTimeMoney(DeliveryPricingRuleDTO typeConfig, long serviceTime) {
        if (typeConfig.getTimes() == null || typeConfig.getTimes().isEmpty()) {
            return BigDecimal.ZERO;
        }
        ZonedDateTime zdt = Instant.ofEpochSecond(serviceTime).atZone(ZoneId.of("Asia/Shanghai"));
        int minuteOfDay = zdt.getHour() * 60 + zdt.getMinute();

        BigDecimal total = BigDecimal.ZERO;
        for (DeliveryPricingRuleDTO.TimePeriodDTO period : typeConfig.getTimes()) {
            if (period.getIsOpen() == null || period.getIsOpen() != 1) continue;
            if (period.getStart() == null || period.getEnd() == null) continue;
            if (period.getStart() <= period.getEnd()) {
                if (minuteOfDay >= period.getStart() && minuteOfDay < period.getEnd()) {
                    total = total.add(period.getMoney() != null ? period.getMoney() : BigDecimal.ZERO);
                }
            } else {
                if (minuteOfDay >= period.getStart() || minuteOfDay < period.getEnd()) {
                    total = total.add(period.getMoney() != null ? period.getMoney() : BigDecimal.ZERO);
                }
            }
        }
        return total;
    }

    /**
     * 计算预计送达分钟数
     *  City.getLength()
     */
    private int calcEstimatedMinutes(DeliveryPricingConfigDTO pricingConfig, BigDecimal distanceKm) {
        if (pricingConfig.getDistanceBasicTime() == null) return 0;
        int basicTime = pricingConfig.getDistanceBasicTime();
        BigDecimal basicKm = pricingConfig.getDistanceBasic() != null ? pricingConfig.getDistanceBasic() : BigDecimal.ZERO;
        int moreTime = pricingConfig.getDistanceMoreTime() != null ? pricingConfig.getDistanceMoreTime() : 0;

        if (distanceKm.compareTo(basicKm) <= 0) return basicTime;
        BigDecimal moreKm = distanceKm.subtract(basicKm);
        return (int) (basicTime + moreKm.doubleValue() * moreTime);
    }

    private DeliveryPricingRuleDTO getTypeConfig(DeliveryPricingConfigDTO config, int orderType) {
        return switch (orderType) {
            case 1 -> config.getType1();
            case 2 -> config.getType2();
            case 6 -> config.getType6();
            default -> null;
        };
    }

    private BigDecimal nvl(BigDecimal v) {
        return v != null ? v : BigDecimal.ZERO;
    }

    private BigDecimal calcDistanceStepFee(BigDecimal basicKm, BigDecimal distanceKm,
                                           List<DeliveryPricingRuleDTO.DistanceStepDTO> steps) {
        List<DeliveryPricingRuleDTO.DistanceStepDTO> sorted = new ArrayList<>(steps);
        sorted.sort(Comparator.comparing(step -> step.getListOrder() == null ? 0 : step.getListOrder()));

        BigDecimal fee = BigDecimal.ZERO;
        BigDecimal prevThreshold = basicKm;
        for (DeliveryPricingRuleDTO.DistanceStepDTO step : sorted) {
            BigDecimal endDistance = nvl(step.getEndDistance());
            BigDecimal unitDistance = nvl(step.getUnitDistance());
            BigDecimal unitFee = nvl(step.getUnitFee());
            if (unitDistance.compareTo(BigDecimal.ZERO) <= 0 || unitFee.compareTo(BigDecimal.ZERO) < 0
                    || endDistance.compareTo(prevThreshold) <= 0) {
                continue;
            }

            BigDecimal capped = distanceKm.min(endDistance);
            if (capped.compareTo(prevThreshold) > 0) {
                BigDecimal segments = capped.subtract(prevThreshold)
                        .divide(unitDistance, 0, RoundingMode.CEILING);
                fee = fee.add(segments.multiply(unitFee));
                prevThreshold = endDistance;
            }
            if (distanceKm.compareTo(endDistance) <= 0) {
                return fee.setScale(2, RoundingMode.HALF_UP);
            }
        }

        if (!sorted.isEmpty() && distanceKm.compareTo(prevThreshold) > 0) {
            DeliveryPricingRuleDTO.DistanceStepDTO last = sorted.get(sorted.size() - 1);
            BigDecimal unitDistance = nvl(last.getUnitDistance());
            BigDecimal unitFee = nvl(last.getUnitFee());
            if (unitDistance.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal segments = distanceKm.subtract(prevThreshold)
                        .divide(unitDistance, 0, RoundingMode.CEILING);
                fee = fee.add(segments.multiply(unitFee));
            }
        }
        return fee.setScale(2, RoundingMode.HALF_UP);
    }

    private BigDecimal applyMinFee(BigDecimal total, DeliveryPricingRuleDTO typeConfig, DeliveryFeeResultVO result) {
        BigDecimal finalTotal = total.setScale(2, RoundingMode.HALF_UP);
        BigDecimal minFee = nvl(typeConfig.getMinFee()).setScale(2, RoundingMode.HALF_UP);
        result.setMinFee(minFee);
        if (minFee.compareTo(BigDecimal.ZERO) > 0 && finalTotal.compareTo(minFee) < 0) {
            result.setMinFeeApplied(1);
            return minFee;
        }
        result.setMinFeeApplied(0);
        return finalTotal;
    }
}