物流项目第五期(运费计算实现、责任链设计模式运用)

前四期:

物流项目第一期(登录业务)-CSDN博客

物流项目第二期(用户端登录与双token三验证)-CSDN博客

物流项目第三期(统一网关、工厂模式运用)-CSDN博客

物流项目第四期(运费模板列表实现)-CSDN博客

运费计算

运费计算的实现基本是三个步骤:

第一步,根据收件人、发件人的地址,查找对应的模板

第二步,计算实际计费的重量(使用轻抛系数将体积转化为重量,与实际重量相比,取大值)

第三步,按照首重 + 续重的方式计算出总价

基本的流程如下:

功能实现

java 复制代码
    /**
     * 运费计算
     *
     * @param waybillDTO 运费计算对象
     * @return 运费模板对象,不仅包含模板数据还包含:computeWeight、expense 字段
     */
    CarriageDTO compute(WaybillDTO waybillDTO);

    /**
     * 根据模板类型查询模板,经济区互寄不通过该方法查询模板
     *
     * @param templateType 模板类型:1-同城寄,2-省内寄,4-跨省
     * @return 运费模板
     */
    CarriageEntity findByTemplateType(Integer templateType);
java 复制代码
/**
 * 根据运单信息计算运费(主方法)
 *
 * @param waybillDTO 运单数据对象,包含发件城市、收件城市、重量、体积等信息
 * @return 返回包含运费结果的 CarriageDTO 对象
 */
@Override
public CarriageDTO compute(WaybillDTO waybillDTO) {
    // 1. 根据传入的运单信息查找匹配的运费模板(根据同城、省内、经济区、跨省等规则判断)
    CarriageEntity carriage = this.findCarriage(waybillDTO);

    // 2. 计算实际计费重量:
    // - 如果有体积,则按体积换算成"体积重量";
    // - 否则取实际重量;
    // - 取两者最大值作为最终计费重量;
    double computeWeight = this.getComputeWeight(waybillDTO, carriage);

    // 3. 开始计算运费:
    // 公式:首重费用 + (计费重量 - 1kg) × 续重单价
    double expense = carriage.getFirstWeight() + ((computeWeight - 1) * carriage.getContinuousWeight());

    // 使用 NumberUtil.round 方法保留一位小数(四舍五入)
    expense = NumberUtil.round(expense, 1).doubleValue();

    // 4. 构造返回结果对象:
    // 将数据库实体对象转换为 DTO,并设置计算出的运费和计费重量
    CarriageDTO carriageDTO = CarriageUtils.toDTO(carriage);
    carriageDTO.setExpense(expense);         // 设置运费金额
    carriageDTO.setComputeWeight(computeWeight); // 设置实际计费重量

    // 5. 返回封装好的 DTO 结果
    return carriageDTO;
}
java 复制代码
/**
 * 查找适用的运费模板
 *
 * @param waybillDTO 运单信息,用于判断是否同城、同省、经济区互寄等
 * @return 匹配的运费模板实体对象
 */
private CarriageEntity findCarriage(WaybillDTO waybillDTO) {

    // 1. 判断是否是"同城"快递:
    // 比较收件城市 ID 和发件城市 ID 是否相同
    if (ObjectUtil.equals(waybillDTO.getReceiverCityId(), waybillDTO.getSenderCityId())) {
        // 同城模板类型常量:CarriageConstant.SAME_CITY
        CarriageEntity carriageEntity = this.findByTemplateType(CarriageConstant.SAME_CITY);
        if (ObjectUtil.isNotEmpty(carriageEntity)) {
            return carriageEntity; // 找到就直接返回
        }
    }

    // 2. 判断是否是"同省"快递:
    // 获取收件人所在城市的父级行政区划(省份ID)
    Long receiverProvinceId = this.areaFeign.get(waybillDTO.getReceiverCityId()).getParentId();
    // 获取寄件人所在城市的父级行政区划(省份ID)
    Long senderProvinceId = this.areaFeign.get(waybillDTO.getSenderCityId()).getParentId();

    // 如果收发省份一致,说明是省内快递
    if (ObjectUtil.equal(receiverProvinceId, senderProvinceId)) {
        // 查询同省模板
        CarriageEntity carriageEntity = this.findByTemplateType(CarriageConstant.SAME_PROVINCE);
        if (ObjectUtil.isNotEmpty(carriageEntity)) {
            return carriageEntity; // 找到就返回
        }
    }

    // 3. 判断是否属于"经济区互寄":
    // 获取所有经济区枚举配置(比如华东、华南、华北等)
    LinkedHashMap<String, EconomicRegionEnum> EconomicRegionMap = EnumUtil.getEnumMap(EconomicRegionEnum.class);
    EconomicRegionEnum economicRegionEnum = null;

    // 遍历每个经济区,检查当前收发省份是否都属于该区域
    for (EconomicRegionEnum regionEnum : EconomicRegionMap.values()) {
        boolean result = ArrayUtil.containsAll(regionEnum.getValue(), receiverProvinceId, senderProvinceId);
        if (result) {
            economicRegionEnum = regionEnum; // 找到匹配的经济区
            break;
        }
    }

    if (ObjectUtil.isNotEmpty(economicRegionEnum)) {
        // 构建查询条件:
        // 模板类型为经济区(CarriageConstant.ECONOMIC_ZONE)
        // 快递类型为常规速递(CarriageConstant.REGULAR_FAST)
        // 关联城市字段中包含经济区编码
        LambdaQueryWrapper<CarriageEntity> queryWrapper = Wrappers
                .lambdaQuery(CarriageEntity.class)
                .eq(CarriageEntity::getTemplateType, CarriageConstant.ECONOMIC_ZONE)
                .eq(CarriageEntity::getTransportType, CarriageConstant.REGULAR_FAST)
                .like(CarriageEntity::getAssociatedCity, economicRegionEnum.getCode());

        // 查询模板
        CarriageEntity carriageEntity = super.getOne(queryWrapper);
        if (ObjectUtil.isNotEmpty(carriageEntity)) {
            return carriageEntity; // 找到就返回
        }
    }

    // 4. 最终兜底策略:跨省快递
    return this.findByTemplateType(CarriageConstant.TRANS_PROVINCE);
}
java 复制代码
/**
 * 根据体积参数与实际重量计算最终的计费重量
 *
 * @param waybillDTO 运单信息(含重量、长宽高等)
 * @param carriage   运费模板(含轻抛系数)
 * @return 返回最终计费重量(double 类型)
 */
private double getComputeWeight(WaybillDTO waybillDTO, CarriageEntity carriage) {

    // 1. 获取体积参数:
    Integer volume = waybillDTO.getVolume(); // 用户可能已经传了体积

    if (ObjectUtil.isEmpty(volume)) {
        try {
            // 如果没有传体积,则根据长宽高计算体积(单位:立方厘米)
            volume = waybillDTO.getMeasureLong() * waybillDTO.getMeasureWidth() * waybillDTO.getMeasureHigh();
        } catch (Exception e) {
            // 出错时设为0,防止异常中断
            volume = 0;
        }
    }

    // 2. 计算体积重量(用于轻泡货):
    // 体积 ÷ 轻抛系数 → 得到体积重量(保留一位小数)
    BigDecimal volumeWeight = NumberUtil.div(volume, carriage.getLightThrowingCoefficient(), 1);

    // 3. 获取实际重量(可能带小数),并保留一位小数
    double realWeight = NumberUtil.round(waybillDTO.getWeight(), 1).doubleValue();

    // 4. 取体积重量与实际重量中的较大者作为基础计费重量
    double computeWeight = NumberUtil.max(volumeWeight.doubleValue(), realWeight);

    // 5. 根据不同区间,对计费重量进行"续重规则"处理:

    // 规则一:≤1kg 的,按 1kg 计费
    if (computeWeight <= 1) {
        return 1;
    }

    // 规则二:1kg ~ 10kg 的,保留原始数值(精确到 0.1kg)
    if (computeWeight <= 10) {
        return computeWeight;
    }

    // 规则三:≥100kg 的,四舍五入取整数
    if (computeWeight >= 100) {
        return NumberUtil.round(computeWeight, 0).doubleValue();
    }

    // 规则四:10kg ~ 100kg 的,以 0.5kg 为一个计价单位
    int integer = NumberUtil.round(computeWeight, 0, RoundingMode.DOWN).intValue(); // 取整数部分
    double decimalPart = NumberUtil.sub(computeWeight, integer); // 小数部分

    if (decimalPart == 0) {
        return integer; // 整数,直接返回
    }

    if (decimalPart <= 0.5) {
        return NumberUtil.add(integer, 0.5); // 0.5以内加0.5
    }

    return NumberUtil.add(integer, 1); // 超过0.5,进位
}
java 复制代码
/**
 * 根据模板类型查询运费模板
 *
 * @param templateType 模板类型(如:同城、省内、跨省)
 * @return 返回匹配的运费模板实体对象
 */
@Override
public CarriageEntity findByTemplateType(Integer templateType) {
    // 如果调用的是经济区类型的模板,抛出异常(因为 findCarriage 方法已单独处理经济区情况)
    if (ObjectUtil.equals(templateType, CarriageConstant.ECONOMIC_ZONE)) {
        throw new SLException(CarriageExceptionEnum.METHOD_CALL_ERROR);
    }

    // 构建查询条件:
    // 模板类型 = templateType
    // 快递类型 = 常规速递(REGULAR_FAST)
    LambdaQueryWrapper<CarriageEntity> queryWrapper = Wrappers
            .lambdaQuery(CarriageEntity.class)
            .eq(CarriageEntity::getTemplateType, templateType)
            .eq(CarriageEntity::getTransportType, CarriageConstant.REGULAR_FAST);

    // 查询唯一一条记录并返回
    return super.getOne(queryWrapper);
}
java 复制代码
    @PostMapping("compute")
    @ApiOperation(value = "运费计算")
    public CarriageDTO compute(@RequestBody WaybillDTO waybillDTO) {
        return carriageService.compute(waybillDTO);
    }

代码优化

在上述的运费计算的代码中,通过条件查找运费模板的方法中,判断了很多种情况,如果后续要再增加不同类型的模板或者调整模板之间的优先级,就必须改动代码,所以这样的实现扩展性并不好,也不够灵活。这里可以通过【责任链设计模式】来优化。

解释:

责任链模式是一种行为模式,把多个处理器组成一条链,但具体由哪个处理器来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个处理器,直到有处理器处理它为止。

之所以采用【责任链】模式,是因为在查找模板时,不同的模板处理逻辑不同,并且这些逻辑组成了一条处理链,有开头有结尾,只要能找到符合条件的模板即结束。

定义处理链抽象类

java 复制代码
package com.sl.ms.carriage.handler;

import com.sl.ms.carriage.domain.dto.WaybillDTO;
import com.sl.ms.carriage.entity.CarriageEntity;

/**
 * 运费模板处理链的抽象定义
 *
 * 该抽象类定义了一个运费模板处理链的基本结构,允许通过链式调用来查找适用的运费模板。
 * 每个具体的处理器(Handler)需要继承此类并实现 doHandler 方法。
 */
public abstract class AbstractCarriageChainHandler {

    /**
     * 下一个处理器对象,用于形成处理链。
     * 如果当前处理器无法找到合适的运费模板,则将请求传递给下一个处理器。
     */
    private AbstractCarriageChainHandler nextHandler;

    /**
     * 抽象方法:执行过滤方法,根据输入参数查找运费模板。
     *
     * @param waybillDTO 输入参数,包含运单的相关信息(如发件城市、收件城市等)
     * @return 返回匹配的运费模板实体对象,如果没有找到则返回 null
     */
    public abstract CarriageEntity doHandler(WaybillDTO waybillDTO);

    /**
     * 执行下一个处理器的方法。
     *
     * 当前处理器未能找到运费模板时,可以调用此方法将请求传递给下一个处理器。
     * 如果下游处理器为空或当前处理器已经找到了运费模板,则直接返回当前结果。
     *
     * @param waybillDTO     输入参数,包含运单的相关信息
     * @param carriageEntity 上一个处理器处理得到的运费模板对象,如果未找到则为 null
     * @return 返回下一个处理器处理后的结果,或者直接返回当前的 carriageEntity(如果已找到)
     */
    protected CarriageEntity doNextHandler(WaybillDTO waybillDTO, CarriageEntity carriageEntity) {
        // 如果没有设置下一个处理器 或者 当前处理器已经找到了运费模板,则直接返回当前结果
        if (nextHandler == null || carriageEntity != null) {
            return carriageEntity;
        }
        // 否则继续调用下一个处理器进行处理
        return nextHandler.doHandler(waybillDTO);
    }

    /**
     * 设置下一个处理器。
     *
     * 通过此方法可以构建处理链,每个处理器可以指定它的下一个处理器,从而形成一条完整的处理链。
     *
     * @param nextHandler 下游处理器对象
     */
    public void setNextHandler(AbstractCarriageChainHandler nextHandler) {
        this.nextHandler = nextHandler;
    }
}

同城寄

java 复制代码
/**
 * 同城寄
 */
@Order(100) //定义顺序
@Component
public class SameCityChainHandler extends AbstractCarriageChainHandler {

    @Resource
    private CarriageService carriageService;

    @Override
    public CarriageEntity doHandler(WaybillDTO waybillDTO) {
        CarriageEntity carriageEntity = null;
        if (ObjectUtil.equals(waybillDTO.getReceiverCityId(), waybillDTO.getSenderCityId())) {
            //同城
            carriageEntity = this.carriageService.findByTemplateType(CarriageConstant.SAME_CITY);
        }
        return doNextHandler(waybillDTO, carriageEntity);
    }
}

省内寄

java 复制代码
/**
 * 省内寄
 */
@Order(200) //定义顺序
@Component
public class SameProvinceChainHandler extends AbstractCarriageChainHandler {

    @Resource
    private CarriageService carriageService;
    @Resource
    private AreaFeign areaFeign;

    @Override
    public CarriageEntity doHandler(WaybillDTO waybillDTO) {
        CarriageEntity carriageEntity = null;
        // 获取收寄件地址省份id
        Long receiverProvinceId = this.areaFeign.get(waybillDTO.getReceiverCityId()).getParentId();
        Long senderProvinceId = this.areaFeign.get(waybillDTO.getSenderCityId()).getParentId();
        if (ObjectUtil.equal(receiverProvinceId, senderProvinceId)) {
            //省内
            carriageEntity = this.carriageService.findByTemplateType(CarriageConstant.SAME_PROVINCE);
        }
        return doNextHandler(waybillDTO, carriageEntity);
    }
}

经济区互寄

java 复制代码
/**
 * 经济区互寄
 */
@Order(300) //定义顺序
@Component
public class EconomicZoneChainHandler extends AbstractCarriageChainHandler {

    @Resource
    private CarriageService carriageService;
    @Resource
    private AreaFeign areaFeign;

    @Override
    public CarriageEntity doHandler(WaybillDTO waybillDTO) {
        CarriageEntity carriageEntity = null;

        // 获取收寄件地址省份id
        Long receiverProvinceId = this.areaFeign.get(waybillDTO.getReceiverCityId()).getParentId();
        Long senderProvinceId = this.areaFeign.get(waybillDTO.getSenderCityId()).getParentId();

        //获取经济区城市配置枚举
        LinkedHashMap<String, EconomicRegionEnum> EconomicRegionMap = EnumUtil.getEnumMap(EconomicRegionEnum.class);
        EconomicRegionEnum economicRegionEnum = null;
        for (EconomicRegionEnum regionEnum : EconomicRegionMap.values()) {
            //该经济区是否全部包含收发件省id
            boolean result = ArrayUtil.containsAll(regionEnum.getValue(), receiverProvinceId, senderProvinceId);
            if (result) {
                economicRegionEnum = regionEnum;
                break;
            }
        }

        if (ObjectUtil.isNotEmpty(economicRegionEnum)) {
            //根据类型编码查询
            LambdaQueryWrapper<CarriageEntity> queryWrapper = Wrappers.lambdaQuery(CarriageEntity.class)
                    .eq(CarriageEntity::getTemplateType, CarriageConstant.ECONOMIC_ZONE)
                    .eq(CarriageEntity::getTransportType, CarriageConstant.REGULAR_FAST)
                    .like(CarriageEntity::getAssociatedCity, economicRegionEnum.getCode());
            carriageEntity = this.carriageService.getOne(queryWrapper);
        }

        return doNextHandler(waybillDTO, carriageEntity);
    }
}

跨省寄

java 复制代码
/**
 * 跨省
 */
@Order(400) //定义顺序
@Component
public class TransProvinceChainHandler extends AbstractCarriageChainHandler {

    @Resource
    private CarriageService carriageService;

    @Override
    public CarriageEntity doHandler(WaybillDTO waybillDTO) {
        CarriageEntity carriageEntity = this.carriageService.findByTemplateType(CarriageConstant.TRANS_PROVINCE);
        return doNextHandler(waybillDTO, carriageEntity);
    }
}

组装处理链

java 复制代码
/**
 * 查找运费模板处理链 @Order注解指定handler顺序
 *
 * 该类用于组装和管理一系列的运费模板处理器(AbstractCarriageChainHandler),通过Spring的依赖注入机制,
 * 按照@Order注解指定的顺序自动注入到List中,并构建处理链。
 */
@Component
public class CarriageChainHandler {

    /**
     * Spring注入的处理器列表,按照@Order注解从小到大排序。
     * 
     * 利用Spring的@Resource注解自动注入实现了AbstractCarriageChainHandler接口的所有bean实例,
     * 并按照@Order注解指定的顺序进行排序。
     */
    @Resource
    private List<AbstractCarriageChainHandler> chainHandlers;

    /**
     * 处理链的第一个处理器。
     * 
     * 在构造处理链时设置,指向处理链中的第一个处理器对象。
     */
    private AbstractCarriageChainHandler firstHandler;

    /**
     * 组装处理链。
     * 
     * 使用@PostConstruct注解标记的方法,在Spring容器初始化完成后自动调用。
     * 此方法负责将各个处理器按顺序连接起来,形成一条完整的处理链。
     */
    @PostConstruct
    private void constructChain() {
        // 检查chainHandlers是否为空,如果为空则抛出异常提示未找到处理器
        if (CollUtil.isEmpty(chainHandlers)) {
            throw new SLException("not found carriage chain handler!");
        }

        // 设置处理链的第一个节点为chainHandlers列表中的第一个元素
        firstHandler = chainHandlers.get(0);

        // 遍历chainHandlers列表,依次设置每个处理器的下一个处理器
        for (int i = 0; i < chainHandlers.size(); i++) {
            if (i == chainHandlers.size() - 1) {
                // 对于最后一个处理器,设置其下游处理器为null,表示没有后续处理器
                chainHandlers.get(i).setNextHandler(null);
            } else {
                // 对于非最后一个处理器,设置其下游处理器为下一个处理器
                chainHandlers.get(i).setNextHandler(chainHandlers.get(i + 1));
            }
        }
    }

    /**
     * 根据运单信息查找运费模板。
     * 
     * 从处理链的第一个处理器开始处理,逐级传递直到找到匹配的运费模板或遍历完所有处理器。
     *
     * @param waybillDTO 运单数据传输对象,包含发件城市、收件城市等信息
     * @return 返回匹配的运费模板实体对象
     */
    public CarriageEntity findCarriage(WaybillDTO waybillDTO) {
        // 从处理链的第一个处理器开始处理
        return firstHandler.doHandler(waybillDTO);
    }
}
相关推荐
68岁扶墙肾透8 分钟前
Java安全-Servlet内存马
java·安全·web安全·网络安全·系统安全·网络攻击模型
码农爱java10 分钟前
Elasticsearch 深入分析三种分页查询【Elasticsearch 深度分页】
java·大数据·spring boot·后端·elasticsearch·全文检索
_extraordinary_1 小时前
Java 继承
java·开发语言·继承
小鹭同学_1 小时前
Java基础 Day17
java·开发语言
设计师小聂!1 小时前
Spring ---IOC容器和DI的具体应用
java·后端·spring
飞人博尔特的摄影师2 小时前
C#开发利器:SharpBoxesCore全解析
开发语言·设计模式·系统架构·c#·.net·.net core
徐子宸2 小时前
docker面试题(4)
java·spring cloud·docker
潇凝子潇2 小时前
IntelliJ IDEA设置编码集
java·ide·intellij-idea
❀͜͡傀儡师2 小时前
IntelliJ IDEA 接入 DeepSeek帮助你更好编码
java·ide·intellij-idea
我命由我123452 小时前
IDEA - Windows IDEA 代码块展开与折叠(基础折叠操作、高级折叠操作)
java·笔记·后端·java-ee·intellij-idea·学习方法·intellij idea