前四期:
物流项目第二期(用户端登录与双token三验证)-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);
}
}