当前位置: 首页 > news >正文

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

前四期:

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

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

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

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

运费计算

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

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

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

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

基本的流程如下:

功能实现

    /*** 运费计算** @param waybillDTO 运费计算对象* @return 运费模板对象,不仅包含模板数据还包含:computeWeight、expense 字段*/CarriageDTO compute(WaybillDTO waybillDTO);/*** 根据模板类型查询模板,经济区互寄不通过该方法查询模板** @param templateType 模板类型:1-同城寄,2-省内寄,4-跨省* @return 运费模板*/CarriageEntity findByTemplateType(Integer templateType);
/*** 根据运单信息计算运费(主方法)** @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;
}
/*** 查找适用的运费模板** @param waybillDTO 运单信息,用于判断是否同城、同省、经济区互寄等* @return 匹配的运费模板实体对象*/
private CarriageEntity findCarriage(WaybillDTO waybillDTO) {// 1. 判断是否是“同城”快递:// 比较收件城市 ID 和发件城市 ID 是否相同if (ObjectUtil.equals(waybillDTO.getReceiverCityId(), waybillDTO.getSenderCityId())) {// 同城模板类型常量:CarriageConstant.SAME_CITYCarriageEntity 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);
}
/*** 根据体积参数与实际重量计算最终的计费重量** @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,进位
}
/*** 根据模板类型查询运费模板** @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);
}
    @PostMapping("compute")@ApiOperation(value = "运费计算")public CarriageDTO compute(@RequestBody WaybillDTO waybillDTO) {return carriageService.compute(waybillDTO);}

代码优化

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

解释:

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

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

 定义处理链抽象类

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;}
}

同城寄

/*** 同城寄*/
@Order(100) //定义顺序
@Component
public class SameCityChainHandler extends AbstractCarriageChainHandler {@Resourceprivate CarriageService carriageService;@Overridepublic 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);}
}

省内寄

/*** 省内寄*/
@Order(200) //定义顺序
@Component
public class SameProvinceChainHandler extends AbstractCarriageChainHandler {@Resourceprivate CarriageService carriageService;@Resourceprivate AreaFeign areaFeign;@Overridepublic CarriageEntity doHandler(WaybillDTO waybillDTO) {CarriageEntity carriageEntity = null;// 获取收寄件地址省份idLong 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);}
}

经济区互寄

/*** 经济区互寄*/
@Order(300) //定义顺序
@Component
public class EconomicZoneChainHandler extends AbstractCarriageChainHandler {@Resourceprivate CarriageService carriageService;@Resourceprivate AreaFeign areaFeign;@Overridepublic CarriageEntity doHandler(WaybillDTO waybillDTO) {CarriageEntity carriageEntity = null;// 获取收寄件地址省份idLong 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()) {//该经济区是否全部包含收发件省idboolean 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);}
}

跨省寄

/*** 跨省*/
@Order(400) //定义顺序
@Component
public class TransProvinceChainHandler extends AbstractCarriageChainHandler {@Resourceprivate CarriageService carriageService;@Overridepublic CarriageEntity doHandler(WaybillDTO waybillDTO) {CarriageEntity carriageEntity = this.carriageService.findByTemplateType(CarriageConstant.TRANS_PROVINCE);return doNextHandler(waybillDTO, carriageEntity);}
}

组装处理链

/*** 查找运费模板处理链 @Order注解指定handler顺序** 该类用于组装和管理一系列的运费模板处理器(AbstractCarriageChainHandler),通过Spring的依赖注入机制,* 按照@Order注解指定的顺序自动注入到List中,并构建处理链。*/
@Component
public class CarriageChainHandler {/*** Spring注入的处理器列表,按照@Order注解从小到大排序。* * 利用Spring的@Resource注解自动注入实现了AbstractCarriageChainHandler接口的所有bean实例,* 并按照@Order注解指定的顺序进行排序。*/@Resourceprivate List<AbstractCarriageChainHandler> chainHandlers;/*** 处理链的第一个处理器。* * 在构造处理链时设置,指向处理链中的第一个处理器对象。*/private AbstractCarriageChainHandler firstHandler;/*** 组装处理链。* * 使用@PostConstruct注解标记的方法,在Spring容器初始化完成后自动调用。* 此方法负责将各个处理器按顺序连接起来,形成一条完整的处理链。*/@PostConstructprivate 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);}
}

相关文章:

  • 自动驾驶中的预测控制算法:用 Python 让无人车更智能
  • 第六章 Freertos智能小车循迹模块
  • 2025.05.21华为暑期实习机考真题解析第二题
  • jenkins数据备份
  • 改写视频生产流程!快手SketchVideo开源:通过线稿精准控制动态分镜的AI视频生成方案
  • Spring Boot AI 之 Chat Client API 使用大全
  • Spring Boot + +小程序, 快速开发零工市场小程序
  • 游戏引擎学习第303天:尝试分开对Y轴和Z轴进行排序
  • GPU加速Kubernetes集群助力音视频转码与AI工作负载扩展
  • 野火鲁班猫(arrch64架构debian)从零实现用MobileFaceNet算法进行实时人脸识别(四)安装RKNN Toolkit Lite2
  • DataGridView中拖放带有图片的Excel,实现数据批量导入
  • 【JAVA】比较器Comparator与自然排序(28)
  • 【Java】泛型在 Java 中是怎样实现的?
  • PostgreSQL 日常维护
  • VLA模型:自动驾驶与机器人行业的革命性跃迁,端到端智能如何重塑未来?
  • 【C++】移动语义与move()实用性教学
  • Docker网关冲突导致容器启动网络异常解决方案
  • 蓝桥杯3503 更小的数
  • Podman(Pod Manager)简介
  • Zabbix开源监控的全面详解!
  • 前沿设计公司网站/凡科建站的免费使用
  • 做网站都有什么成本/怎么在百度上发布信息广告
  • 网站建设公司厦门有哪些/网络广告营销的典型案例
  • 三门峡做网站公司/seo综合查询什么意思
  • 合肥瑶海区寒假兼职工网站建设/新手做电商怎么起步
  • 杭州商标设计/seo服务 收费