目标
了解运费的业务需求
了解运费模板表的设计
了解项目中的代码规范
实现运费计算的业务逻辑
完成部署服务以及功能测试
背景
完成开发一组网关鉴权服务,到开发二组实现运费服务(基本框架搭建完成)的业务逻辑
需求分析
接到开发任务后,首先需要了解需求,再动手开发。(需要问清楚需求)
运费的计算是分不同地区的,有不同的运费规则。
产品原型:(增删改查)



业务说明:
不同的模板类型只有一个,关联城市为全国。但经济区互寄有多个(有不同的关联城市)。
模板类型有常量维护,经济区有枚举类维护。
运送类型只有普快。
<1kg为首重。
(体积÷轻抛系数)和实际重量比谁大用谁。轻抛系数的值产品经理已给出
计算运费,根据提供的的参数与运费规则计算得到
如已设置同城寄、跨省寄、省内寄,则只可修改,不可再新增
如已设置经济区互寄某个城市,下次添加不可再关联此经济区城市
计费规则
重量计算方法:
取重量和体积两者间较大的数值,体积计算方法:长(cm)×宽(cm)×高(cm)/轻抛系数
普快的轻抛系数:
同城互寄:12000
城内寄件:12000
跨省寄件:12000
经济区互寄(京津翼、江浙沪皖、川渝):6000
经济区互寄(黑吉辽):9000
重量小数点收费规则:
不满1kg,按1kg计费;
10KG以下:以0.1kg为计重单位,四舍五入保留 1 位小数;
10-100KG:续重以0.5kg为计重单位,不足0.5kg按0.5kg算,四舍五入保留 1 位小数;
18.1kg按照18.5kg收费,18.7kg按照19kg收费
100KG及以上:四舍五入取整;
108.5kg按照109kg收费,108.6kg按照109kg收费
总运费小数点规则:按四舍五入计算,精确到小数点后一位
价格:(目前只有普快)
同城寄:首重(1.0kg)13.0元,续重2.0元/kg
省内寄:首重(1.0kg)14.0元,续重2.0元/kg
跨省寄:首重(1.0kg)18.0元,续重5.0元/kg
经济区互寄(江浙沪皖):首重(1.0kg)12.0元,续重3.0元/kg
经济区互寄(京津翼):首重(1.0kg)12.0元,续重3.0元/kg
经济区互寄(黑吉辽):首重(1.0kg)16.0元,续重6.0元/kg
经济区互寄(川渝):首重(1.0kg)18.0元,续重8.0元/kg
数据库表设计:

将表中原有属性加入,另外在加主键id(唯一标识),创建、修改时间,如果是逻辑删除在加状态
bigint即Long类型,tinyint比int小处理更快。
注:BIGINT主要用于数据库中定义字段类型。Long主要用于编程语言中定义变量类型。二者在存储范围上是一致的,都是-2^63 到 2^63 - 1。
实际建表语句:
CREATE TABLE `sl_carriage` (
`id` bigint NOT NULL COMMENT '运费模板id',
`template_type` tinyint NOT NULL COMMENT '模板类型,1-同城寄 2-省内寄 3-经济区互寄 4-跨省',
`transport_type` tinyint NOT NULL COMMENT '运送类型,1-普快 2-特快',
`associated_city` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '关联城市,1-全国 2-京津冀 3-江浙沪 4-川渝 5-黑吉辽',
`first_weight` double NOT NULL COMMENT '首重价格',
`continuous_weight` double NOT NULL DEFAULT '1' COMMENT '续重价格',
`light_throwing_coefficient` int NOT NULL COMMENT '轻抛系数',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='运费模板表';
说明:由于该表数据比较少,所以就不需要添加索引字段了。
开发环境
微服务开发规范
在神领物流项目中,微服务代码是独立的工程(非聚合项目结构)
1个微服务需要创建3个工程,分别是:
sl-express-ms-xxx-api(定义Feign接口)
每个微服务抽取一个API,调用API依赖即可调用该微服务
sl-express-ms-xxx-domain(定义DTO、枚举对象)
sl-express-ms-xxx-service(微服务的实现)
它们之间的依赖关系如下

拉取代码
将下面三个代码gitClone下来放到carriage文件夹下,并添加到maven

拉取代码后pom依赖又报错,拉取parent,common工程

注意:重新加载前看自己的maven配置是否正确

代码规范
枚举对象介绍

DTO对象介绍
本项目中,在接收参数,微服务间调用,返回参数都使用DTO对象。
在service中对数据库进行操作时用entity对象。
这里需要将DTO对象(比entity对象多几个属性)使用项目提供Utils转换为entity对象

有些项目可能多一个VO对象作为返回参数对象
数据校验
这里前端传来的参数可能已经做了一些校验(如某些数据不能为空),但无论前端做不做校验,后端都要校验。
方式一:采用hibernate-validator注解方式校验
如Max,Min(该属性的取值范围),NotNull(对象不能为空),若为字符串可以用NotEmpty,NotBlank

在Controller中需要增加@Validated注解,来开启校验。
在函数方法中的属性前也要写校验规则注解,(该属性内部也要写校验规则注解)。
使用AOP切面方法解决每个方法的属性前写校验规则注解(具体在common工程中的com.sl.transport.common.aspect.ValidatedAspect实现)
around:环绕通知可以在方法执行之前和之后执行自定义逻辑。

方式二:在程序中通过if()进行逻辑判断

进行代码测试(启动carriage时),发现错误
自定义异常
在神领物流项目中,我们统一做了自定义异常的处理。定义了2个异常:
- com.sl.transport.common.exception.SLException(用于微服务之前接口调用抛出的异常)
- com.sl.transport.common.exception.SLWebException(用于前后端交互时抛出的异常)
疑问:为什么不使用一个,而是要设置两个?
这个主要是前端和后端的设计不同,一般在微服务间接口调用时会采用标准的RESTful方式,按照RESTful的规范响应的状态码要使用标准的http状态码,成功->200,失败->500,没有权限->401等。
而前后端进行交互时,一般都是响应200,即使出错也是200,只是响应结果中通过msg和code进行表达是否成功。
使用@RestControllerAdvice注解和@ExceptionHandler注解为异常做统一接收controller抛出错误,(如果没有,异常会以mapper->service->controller->前端用户的顺序抛出)


@Resource代替@Autowired
在项目中,涉及到注入Spring容器中bean对象时,均使用@Resource,目前IDEA不推荐使用@Autowired,原因是它是Spring容器提供,并非是Java标准(换个容器就不能使用),而@Resource是Java标准中定义的,建议使用。
两者区别:
@Autowired:默认是ByType注入,可以使用@Qualifier指定Name,可以对构造器、方法、参数、字段使用。
@Resource:默认ByName注入,如果找不到则按ByType注入,只能对方法、字段使用,不能用于构造器。
@Autowired是Spring提供的,@Resource是JSR-250提供的。
总结:基本上@Resource可以完全替代@Autowired。
配置文件
SpringBoot配置文件

| 文件 | 说明 |
| bootstrap.yml | 通用配置项,服务名、日志文件、swagger配置等 |
| bootstrap-local.yml | 多环境配置,本地开发环境 |
| bootstrap-stu.yml | 多环境配置,学生101环境(jenkins中使用的都为stu环境,即150网段,可在配置中查看) |
| bootstrap-test.yml | 多环境配置,开发组测试环境(学习阶段忽略该文件) |
jenkins配置查看(shell读取一些设置脚本后,使用docker)

#启动dokcer命令
docker run -d -p $SERVER_PORT:8080 --name $SERVER_NAME -e SERVER_PORT=8080 -e SPRING_CLOUD_NACOS_DISCOVERY_IP=${SPRING_CLOUD_NACOS_DISCOVERY_IP} -e SPRING_CLOUD_NACOS_DISCOVERY_PORT=${port} -e SPRING_PROFILES_ACTIVE=stu $SERVER_NAME:$SERVER_VERSION
为了与101环境中服务互通,所以在local环境中固定设置了注册到注册中心的服务ip地址

具体的一些项目配置统一使用nacos的配置中心管理,并且在这里使用nacos的共享配置机制,这样可以在多个项目中共享相同的配置

seata配置
shared-spring-seata.yml:
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.150.101:8848
namespace: ecae68ba-7b43-4473-a980-4ddeb6157bdc
group: DEFAULT_GROUP
application: seata-server
username: nacos
password: nacos
tx-service-group: sl-seata # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
sl-seata: default
seata服务的配置:(seata-server.properties)
#指定seata存储的数据库
store.mode = db
store.db.datasource = druid
store.db.dbType = mysql
store.db.driverClassName = com.mysql.cj.jdbc.Driver
store.db.url = jdbc:mysql://192.168.150.101:3306/seata?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
store.db.user = root
store.db.password = 123
store.db.minConn = 5
store.db.maxConn = 100
store.db.globalTable = global_table
store.db.branchTable = branch_table
store.db.lockTable = lock_table
store.db.distributedLockTable = distributed_lock
store.db.queryLimit = 100
store.db.maxWait = 5000
seata服务地址: sl-express.com - sl express 资源和信息。 账号信息:seata/seata
mysql配置
spring:
datasource: #数据库的配置
driver-class-name: ${jdbc.driver:com.mysql.cj.jdbc.Driver}
url: ${jdbc.url}
username: ${jdbc.username}
password: ${jdbc.password}
具体的配置项在每个微服务自己的配置文件中,例如运费服务:
jdbc.url = jdbc:mysql://192.168.150.101:3306/sl_carriage?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
jdbc.username = root
jdbc.password = 123
需要说明的是,${jdbc.driver:com.mysql.cj.jdbc.Driver} 这种写法冒号后面的是默认值,如果不配置jdbc.driver就采用默认值。
mybatis-plus配置
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
db-config:
id-type: ASSIGN_ID
在配置文件中指定的默认的id策略为ASSIGN_ID,只当插入对象ID为空时,自动填充雪花id。
mybatis-plus与mybatis对比
自定义 SQL vs 内置 CRUD:MyBatis 允许手写 SQL,灵活性高;MyBatis-Plus 提供内置的 CRUD 操作,简化开发。
配置复杂度:MyBatis 需要手动配置 SQL 映射,适合复杂查询;MyBatis-Plus 提供自动化配置,适合快速开发。
内置功能:MyBatis-Plus 提供分页、逻辑删除等插件,MyBatis 需要自行实现。
日志

实现业务
1 查询运费模板列表
逻辑
写好ServiceImpl框架


M是一个BaseMapper<T>的子类,用于操作数据库的 Mapper。T是一个实体类,与数据库表对应。
通过继承 ServiceImpl<CarriageMapper, CarriageEntity>,CarriageServiceImpl 类就自动拥有了 MyBatis-Plus 提供的 CRUD 方法的实现
代码
/**
* 获取全部运费模板
*
* @return 运费模板对象列表
*/
@Override
public List<CarriageDTO> findAll() {
//LambdaQueryWrapper<>:这是 MyBatis-Plus 提供的一个查询条件包装器,使用 Lambda 表达式构造查询条件,类型为<>
LambdaQueryWrapper<CarriageEntity> queryWrapper = new LambdaQueryWrapper();
//指定排序的字段为 CarriageEntity 实体的 created 字段按降序排序
queryWrapper.orderByDesc(CarriageEntity::getCreated);
//调用父类 ServiceImpl 提供的 list 方法
List<CarriageEntity> list = super.list(queryWrapper);
return list.stream().map(CarriageUtils::toDTO).collect(Collectors.toList());
//list.stream():将 CarriageEntity 实体列表转换为流(Stream),便于后续的流式操作。
//.map(CarriageUtils::toDTO):使用 map 方法将 CarriageEntity 实体转换为 CarriageDTO 对象,CarriageUtils::toDTO 是一个方法引用,指向 CarriageUtils 类中的 toDTO 方法。
//.collect(Collectors.toList()):将转换后的 CarriageDTO 对象收集到一个列表中,并返回该列表。
//map 是 Java Stream API 中的一个中间操作,它接受一个函数作为参数,并将这个函数应用到流中的每个元素,产生一个新的流。
//在这里,map 方法将应用于 list 流中的每个 CarriageEntity 元素。转换为 CarriageDTO 对象,产生一个新的流
}
单元测试
carriageServic下得src中添加
为carriageService接口提供单元测试

勾选全部方法

创建勾选方法的单元测试

添加SpringBootTest注解

添加测试逻辑

debug查询结果

接口测试

点击18094/,在网页后缀加/doc.html进入swagger进行测试(注:此处未走网关,故不需要进行登录)

点击发送即可直接测试
2 新增或更新
逻辑

//校验运费模板是否存在,如果不存在直接插入运送类型(查询条件:模板,运送类型 如果是修改排除当前id)
LambdaQueryWrapper<>构造查询
//如果没有重复的模板,可以直接插入或更新操作(DT0 转entity 保存成功 entity 转 DTO)
//如果存在重复模板,需要判断此次插入的是否为经济区互寄,非经济区互寄是不可以重复的
//如果是经济区互寄类型,需进一步判断关联城市是否重复,通过集合取交集判断是否重复
.stream().map().map().flatMap.collect(Collectors.toList())构造实体关联城市的单个数组
//如果没有重复,可以新增或更新(DTO转entity 保存成功entity 转DTO)
CollUtil.intersection判断有无交集
将方法进行抽取为具体函数


代码
@Override
public CarriageDTO saveOrUpdate(CarriageDTO carriageDto) {
log.info("新增运费模板 --> {}", carriageDto);
//校验运费模板是否存在,如果不存在直接插入(查询条件: 模板类型 运送类型 如果是修改排除当前id)运输类型
//使用LambdaQueryWrapper<>构造查询。模板类型,运送类型相同,且id不同的数据
LambdaQueryWrapper<CarriageEntity> queryWrapper = new LambdaQueryWrapper<>();
//eq 方法用于在查询中添加等于条件
queryWrapper.eq(CarriageEntity::getTemplateType,carriageDto.getTemplateType())
.eq(CarriageEntity::getTransportType,carriageDto.getTransportType())
//ne 方法用于添加不等于条件,如果为修改原有数据,则查询id不是carriageDto.getId()的数据
//ObjectUtil.isNotEmpty(carriageDto.getId())判断非空时
.ne(ObjectUtil.isNotEmpty(carriageDto.getId()), CarriageEntity::getId, carriageDto.getId());
//使用LambdaQueryWrapper构造条件后,在数据库中进行查询,不存在直接插入
List<CarriageEntity> carriageEntityList = super.list(queryWrapper);
//如果没有重复的模板,可以直接插入或更新操作(DT0 转entity 保存成功 entity 转 DTO)
//CollUtil.isEmpty判断,而不是 == null判断
//CollUtil.isEmpty(collection) 更具体地判断集合是否为空集合(即集合对象不为 null 且集合大小为 0)。
//== null 只能检查对象是否为 null,无法判断对象是否为空集合或者空数组。
if(CollUtil.isEmpty(carriageEntityList)){
return saveOrUpdateCarriage(carriageDto);
}
//如果存在重复模板,判断此次插入的是否为经济区互寄.ECONOMIC_ZONE常量,非经济区互寄是不可以重复的
if(ObjectUtil.notEqual(carriageDto.getTemplateType(), CarriageConstant.ECONOMIC_ZONE)){
//抛出异常:非经济区互寄是不可以重复
throw new SLException(CarriageExceptionEnum.NOT_ECONOMIC_ZONE_REPEAT);
}
//如果是经济区互寄类型,需进一步判断关联城市是否重复,通过集合取交集判断是否重复
List<String> associatedCityList = carriageEntityList.stream().map(CarriageEntity::getAssociatedCity)
.map(associatedCity -> StrUtil.splitToArray(associatedCity, ","))
//防止出现(2,3)的组合,使用flat重新拆散为单个元素
.flatMap(Arrays::stream)
.collect(Collectors.toList());
//经济区重复
Collection<String> intersection = CollUtil.intersection(associatedCityList, carriageDto.getAssociatedCityList());
if (CollUtil.isNotEmpty(intersection)) {
throw new SLException(CarriageExceptionEnum.ECONOMIC_ZONE_CITY_REPEAT);
}
//如果没有重复,可以新增或更新(DTO转entity 保存成功entity 转DTO)
return saveOrUpdateCarriage(carriageDto);
}
@NotNull
private CarriageDTO saveOrUpdateCarriage(CarriageDTO carriageDto) {
CarriageEntity carriageEntity = CarriageUtils.toEntity(carriageDto);
//super为ServiceImpl<CarriageMapper, CarriageEntity>父类
super.saveOrUpdate(carriageEntity);
//经过saveOrUpdate后carriageEntity中存入了id(如果为插入),故用转换对象而不是原DTO
return CarriageUtils.toDTO(carriageEntity);
}
单元测试
测试数据

结果

在终端使用Docker代替jenkins启动微服务
方式1:docker start sl-express-ms-web-manager
方式2:docker ps -a -q --no-trunc --filter name=^/sl(查询指定名称,这里是sl开头的docker容器)
docker start ‘docker ps -a -q --no-trunc --filter name=^/sl)’(启动查询到的所有微服务)
可在nacos中查看是否启动
待解决问题:docker ps -a -q --no-trunc --filter name=^/sl在虚拟机中查不到任何微服务
原因分析:可能老师未关闭虚拟机,所以容器才一直存在于docker中,只是服务断开启动,但构建的容器一直在
解决:重新开机后可以docker ps -a -q --no-trunc --filter name=^/sl查询到所有sl开头微服务
使用docker start $(docker ps -a -q --no-trunc --filter name=^/sl)(启动查询到的所有微服务)
虚拟机中原有微服务
sl-express-ms-base-service:1.1-SNAPSHOT
sl-express-ms-web-manager:1.1-SNAPSHOT
sl-express-gateway:1.1-SNAPSHOT
管理员前端登录时输入验证码后接口异常
等待一段时间再登录可以解决
前后端测试

3 计算运费

主要分为 1找运费模板 2算重量 3算运费
1 找运费模板
代码
//根据参数查找运费模板
//根据同城(市),同省,同经济区,跨省依次寻找运费模板
private CarriageEntity findCarriage(WaybillDTO waybillDTO){
Long senderCityId = waybillDTO.getSenderCityId();
Long receiverCityId = waybillDTO.getReceiverCityId();
//同城(市)
if(ObjectUtil.equals(senderCityId,receiverCityId))
{
CarriageEntity carriageEntity = findByTemplateType(CarriageConstant.SAME_CITY);
if(ObjectUtil.isNotEmpty(carriageEntity)){
return carriageEntity;
}
}
//如果没查到或不是同城,则获取收,寄件地址省份id使用AreaFeign接口(base微服务中)查
Long senderProvinceId = areaFeign.get(senderCityId).getParentId();
Long receiverProvinceId = areaFeign.get(receiverCityId).getParentId();
if(ObjectUtil.equals(senderProvinceId,receiverProvinceId)){
//如果 收发件的省份id相同,查询同省的模板调用findByTemplateType方法
CarriageEntity carriageEntity = findByTemplateType(CarriageConstant.SAME_PROVINCE);
if(ObjectUtil.isNotEmpty(carriageEntity)){
return carriageEntity;
}
}
//如果设查到或不是同省,则查询是否为经济区互赛调用findEconomiccarriage方法查询
CarriageEntity carriageEntity = findEconomicCarriage(receiverProvinceId, senderProvinceId);
if(ObjectUtil.isNotEmpty(carriageEntity)){
return carriageEntity;
}
//如果没查到或不是经济区互寄,直接查跨省运费模板
carriageEntity = findByTemplateType(CarriageConstant.TRANS_PROVINCE);
if(ObjectUtil.isNotEmpty(carriageEntity)){
return carriageEntity;
}
//如果最后没查到、直接抛自定义异常,提示模板未找型
throw new SLException(CarriageExceptionEnum.NOT_FOUND);
}
//根据模板,运送类型查找模板
@Override
public CarriageEntity findByTemplateType(Integer templateType) {
LambdaQueryWrapper<CarriageEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CarriageEntity::getTemplateType,templateType);
//运送模板固定为普快(1)
queryWrapper.eq(CarriageEntity::getTransportType,CarriageConstant.REGULAR_FAST);
return super.getOne(queryWrapper);
}
//查询是否为经济区互寄
private CarriageEntity findEconomicCarriage(Long receiverProvinceId,Long senderProvinceId){
//通过工具类EnumUtil.getEnumMap 获取所有经济区城市配置枚举值
LinkedHashMap<String, EconomicRegionEnum> enumMap = EnumUtil.getEnumMap(EconomicRegionEnum.class);
//遍历所有经济区枚举值enumMap.values().iter
EconomicRegionEnum economicRegionEnum = null;
for (EconomicRegionEnum regionEnum : enumMap.values()) {
//通过ArrayUtil.containAll工具类 判断发件网点 和 收件网点是否在同一经济区(即判断两网点id是否在经济区的城市序号中)
//enumMap.values() 返回所有 EconomicRegionEnum 枚举实例。每个 EconomicRegionEnum 实例有一个方法 getValue(),该方法返回该枚举实例所代表的值
boolean b = ArrayUtil.containsAll(regionEnum.getValue(), receiverProvinceId, senderProvinceId);
//如果 true 得到对应经济区枚举
if(b == true){ economicRegionEnum = regionEnum; break;}
}
//循环遍历未发现所属经济区,方法直接返回null
if(ObjectUtil.isNotEmpty(economicRegionEnum)){
//如果有经济区根据 模板类型=经济区 运送类型=普快 关联城市=枚举code值 查询
LambdaQueryWrapper<CarriageEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CarriageEntity::getTemplateType,CarriageConstant.ECONOMIC_ZONE);
queryWrapper.eq(CarriageEntity::getTransportType,CarriageConstant.REGULAR_FAST);
//此处用like而非eq
queryWrapper.like(CarriageEntity::getAssociatedCity,economicRegionEnum.getCode());
return super.getOne(queryWrapper);
}
return null;
}
单元测试
数据
WaybillDTO waybillDTO = new WaybillDTO();
waybillDTO.setReceiverCityId(7363L); //天津
waybillDTO.setSenderCityId(2L); //北京
waybillDTO.setWeight(3.8); //重量
waybillDTO.setVolume(125000); //体积
CarriageDTO compute = this.carriageService.compute(waybillDTO);
System.out.println(compute);
结果

2算重量
//计算重量
private double getComputeWeight(WaybillDTO waybillDTO, CarriageEntity carriage) {
//判断参数中是否传入体积,未传入则计算
Integer volume = 0;
if(ObjectUtil.isEmpty(waybillDTO.getVolume())){
try {
volume = waybillDTO.getMeasureLong() * waybillDTO.getMeasureHigh() * waybillDTO.getMeasureWidth();
}
catch (Exception e){
//如果参数中有null,则捕获异常,设volume为0
volume = 0;
}
}
//NumberUtil工具类返回结果为BigDecimal类型,使用.doubleValue()转换为double
//计算体积重量 = 体积 / 轻抛系数 tips:使用NumberUtil.div工具类计算 保留一位小数
BigDecimal volumeWeight = NumberUtil.div(volume,carriage.getLightThrowingCoefficient(),1);
//重量 = 体积重量和实际重量取最大值 tips:使用NumberUtil工具类计算 保留一位小数
double computeWeight = NumberUtil.max(volumeWeight,NumberUtil.round(waybillDTO.getWeight(),1)).doubleValue();
//重量小数点规则:不满1kg,按1kg计费
if(computeWeight <= 1){
return 1;
}
//10KG一下以0.1kg计量保留1位小数
//注:此处不用写为else,if后会直接返return
if(computeWeight <= 10){
//计算结果保留一位小数
return computeWeight;
}
//100KG 以上四舍五入取整,举例108.4kg按照108收费 108.5kg 按照109KG收费
//tips:使用NumberUtil工具类,即hutool工具类计算。
if(computeWeight >100){
//零位小数,即四舍五入到最接近的整数
return NumberUtil.round(computeWeight, 0).doubleValue();
}
//10-100kg续重以0.5kg计量保留1位小数
//向下取整
double value = NumberUtil.round(computeWeight, 0, RoundingMode.DOWN).doubleValue();
//二者相减,=0原本为整数,<=0.5给value+0.5,>0.5给value+1
double sub = NumberUtil.sub(computeWeight,value);
if(sub == 0){ return value;}
if(sub <= 0.5){ return NumberUtil.add(value,0.5); }
return NumberUtil.add(value,1);
}
3算运费
代码
public CarriageDTO compute(WaybillDTO waybillDTO) {
//根据参数查找运费模板 调用finaCarriage方法
CarriageEntity carriage = findCarriage(waybillDTO);
//计算重量,最小重量为1kg 调用getComputeweight方法
double computeWeight = getComputeWeight(waybillDTO, carriage);
//计算运费 运费=首重价格+(实际重量-1)*续重价格
double expense = carriage.getFirstWeight() + (computeWeight -1) * carriage.getContinuousWeight();
//结果保留一位小数,用NumberUtil.round
expense = NumberUtil.round(expense,1).doubleValue();
//封装运费和计算重量到CarriageDTO,并返回
//使用CarriageUtils.toDTO()将carriage转换为carriageDTO
CarriageDTO carriageDTO = CarriageUtils.toDTO(carriage);
carriageDTO.setComputeWeight(computeWeight);
carriageDTO.setExpense(expense);
return carriageDTO;
}
单元测试
数据
void compute() {
WaybillDTO waybillDTO = new WaybillDTO();
waybillDTO.setReceiverCityId(7363L); //天津
waybillDTO.setSenderCityId(2L); //北京
waybillDTO.setWeight(3.8); //重量
waybillDTO.setVolume(125000); //体积
CarriageDTO compute = this.carriageService.compute(waybillDTO);
System.out.println(compute);
}
结果
本地workbench中没有sl_area表,重新连接即可

前后端联调(用户端wx小程序测试)
构建微服务

jenkins构建carriage—service时出错

解决:在pom.xml文件中指定java版本为11(本项目使用jdk11版本)

申请wx测试账号并在nacos配置

AppID:wx68629bd32e3a013c
AppSecret:da9c057fcd112c316ac871ece0a79fca
将AppID和AppSecret保存到nacos配置中心的 sl-express-ms-web-customer.properties中:

在finalshell进行重启并登录sl-express-ms-web-customer
重启:docker restart sl-express-ms-web-customer
登录:docker logs -f sl-express-ms-web-customer

出现502错误,发现后台gateway网关服务构建不了

使用docker logs -f sl-express-gateway找到错误

发现idea本地gateway错误,根据配置local.yml得应为role.courier。同理role.driver

改![]()
成功启动gateway微服务。
原因:之前写客户鉴权逻辑时没有进行测试,也没有本地启动过。因为微信小程序在这一章才用到
使用参考代码后进行上传
public AuthUserInfoDTO check(String token) {
// 普通用户的token没有权力对接权限系统,需要自定实现
//即无法使用AuthTemplate对象,AuthUserInfoDTO,TokenService对象中的方法
//自定义解析Token即可
try {
// 基于JwtUtils解析token获取Claims内容
Map<String, Object> claims = JwtUtils.checkToken(token,jwtProperties.getPublicKey());
//这里只需封装userId信息即可
Long userId = Long.valueOf(claims.get("userId").toString());
//AuthUserInfoDTO.bulider(),不可使用builder方法,AuthUserInfoDTO为拉取的依赖,未注入Bean容器
AuthUserInfoDTO authUserInfoDTO = new AuthUserInfoDTO();
authUserInfoDTO.setUserId(userId);
return authUserInfoDTO;
}catch (Exception e) {
log.error(">>>>>>>>>>>>>>>>>> 解析用户登录token失败 >>>>>>>>>>>>>>>>");
return null;
}
}
结果展示

另外:思考,本地启动gateway服务wx端仍报502错误,但上传构建服务后可成功使用。
练习
参考:神领物流day02-运费微服务_神领物流面试-优快云博客
说明
需求:计算运费的第一步就是根据参数查询运费模板,而这个动作会访问数据库,并且是比较频繁的,然而运费模板的变更并不频繁,需要可以将运费模板缓存起来(使用redis缓存优化findCarriage找运输模板方法),以提高效率。
提示:
- 需要引入redis相关的依赖
- 增加redis相关的配置
- 编码实现缓存相关逻辑
缓存结构(推荐hash结构)
大key定义 小key:发件城市id_收件城市id value: 模板数据
优化1:使用redis缓存查找模板
本地连接redis

redis配置
在sl-express-ms-carriage-service引入redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在local.yml中添加nacos配置文件位置

nacos中已经存在配置文件如下,默认即可:

redis优化代码
缓存中有数据:
//如果缓存中有数据,使用 JSONUtil.toBean() 将缓存数据转换为 CarriageEntity 对象
String CACHE_KEY = "CARRIAGE_CACHE";//大key
CarriageEntity carriageEntity;
//小key 发件城市id_收件城市id
String redisHasKey = StrUtil.format("{}_{}", waybillDTO.getSenderCityId(), waybillDTO.getReceiverCityId());
Object cacheData = this.stringRedisTemplate.opsForHash().get(CACHE_KEY, redisHasKey);
if (ObjectUtil.isNotEmpty(cacheData)) {
carriageEntity = JSONUtil.toBean(Convert.toStr(cacheData), CarriageEntity.class);
return carriageEntity;
}
缓存中无数据时:
//抽取方法,如果寻找到的运费模板非空,则写入redis缓存
private boolean notEmptyAndWriteRedis(String CACHE_KEY, CarriageEntity carriageEntity, String redisHasKey) {
if (ObjectUtil.isNotEmpty(carriageEntity)) {
this.stringRedisTemplate.opsForHash().put(CACHE_KEY, redisHasKey, JSONUtil.toJsonStr(carriageEntity));
this.stringRedisTemplate.expire(CACHE_KEY, 7, TimeUnit.DAYS);//缓存存放时间为7天
return true;
}
return false;
总体代码
//根据参数查找运费模板
//根据同城(市),同省,同经济区,跨省依次寻找运费模板
private CarriageEntity findCarriage(WaybillDTO waybillDTO){
//如果缓存中有数据,使用 JSONUtil.toBean() 将缓存数据转换为 CarriageEntity 对象
String CACHE_KEY = "CARRIAGE_CACHE";//大key
CarriageEntity carriageEntity;
//小key 发件城市id_收件城市id
String redisHasKey = StrUtil.format("{}_{}", waybillDTO.getSenderCityId(), waybillDTO.getReceiverCityId());
Object cacheData = this.stringRedisTemplate.opsForHash().get(CACHE_KEY, redisHasKey);
if (ObjectUtil.isNotEmpty(cacheData)) {
carriageEntity = JSONUtil.toBean(Convert.toStr(cacheData), CarriageEntity.class);
return carriageEntity;
}
Long senderCityId = waybillDTO.getSenderCityId();
Long receiverCityId = waybillDTO.getReceiverCityId();
//同城(市)
if(ObjectUtil.equals(senderCityId,receiverCityId))
{
carriageEntity = findByTemplateType(CarriageConstant.SAME_CITY);
if (notEmptyAndWriteRedis(CACHE_KEY, carriageEntity, redisHasKey)) return carriageEntity;
}
//如果没查到或不是同城,则获取收,寄件地址省份id使用AreaFeign接口(base微服务中)查
Long senderProvinceId = areaFeign.get(senderCityId).getParentId();
Long receiverProvinceId = areaFeign.get(receiverCityId).getParentId();
if(ObjectUtil.equals(senderProvinceId,receiverProvinceId)){
//如果 收发件的省份id相同,查询同省的模板调用findByTemplateType方法
carriageEntity = findByTemplateType(CarriageConstant.SAME_PROVINCE);
if (notEmptyAndWriteRedis(CACHE_KEY, carriageEntity, redisHasKey)) return carriageEntity;
}
//如果设查到或不是同省,则查询是否为经济区互赛调用findEconomiccarriage方法查询
carriageEntity = findEconomicCarriage(receiverProvinceId, senderProvinceId);
if (notEmptyAndWriteRedis(CACHE_KEY, carriageEntity, redisHasKey)) return carriageEntity;
//如果没查到或不是经济区互寄,直接查跨省运费模板
carriageEntity = findByTemplateType(CarriageConstant.TRANS_PROVINCE);
if (notEmptyAndWriteRedis(CACHE_KEY, carriageEntity, redisHasKey)) return carriageEntity;
//如果最后没查到、直接抛自定义异常,提示模板未找型
throw new SLException(CarriageExceptionEnum.NOT_FOUND);
}
//抽取方法,如果寻找到的运费模板非空,则写入redis缓存
private boolean notEmptyAndWriteRedis(String CACHE_KEY, CarriageEntity carriageEntity, String redisHasKey) {
if (ObjectUtil.isNotEmpty(carriageEntity)) {
this.stringRedisTemplate.opsForHash().put(CACHE_KEY, redisHasKey, JSONUtil.toJsonStr(carriageEntity));
this.stringRedisTemplate.expire(CACHE_KEY, 7, TimeUnit.DAYS);//缓存存放时间为7天
return true;
}
return false;
}
在数据库模板更新时删除缓存,防止不一致
//新增,修改运费模板 更新数据库
@NotNull
private CarriageDTO saveOrUpdateCarriage(CarriageDTO carriageDto) {
CarriageEntity carriageEntity = CarriageUtils.toEntity(carriageDto);
//super为ServiceImpl<CarriageMapper, CarriageEntity>父类
super.saveOrUpdate(carriageEntity);
//使用redis清除 整个“CARRIAGE_TEMPALTE_CACHE”哈希键
stringRedisTemplate.opsForHash().delete("CARRIAGE_TEMPALTE_CACHE");
//经过saveOrUpdate后carriageEntity中存入了id(如果为插入),故用转换对象而不是原DTO
return CarriageUtils.toDTO(carriageEntity);
}
测试结果


优化2:对热门城市/经济区的快递直接使用缓存
优化3:使用责任链模式写查找不同运输模板
面试问
问题
- 你们的运费是怎么计算的?体积和重量怎么计算,到底以哪个为准?(轻抛系数,四舍五入怎么计算的,重量的不同范围划分等等)
- 详细聊聊你们的运费模板是做什么的?
- 有没有针对运费计算做什么优化?
逻辑
从下表出发

后介绍找模板的业务逻辑,和计算运费的业务逻辑

706






