day2-神领物流-运费服务研发

目标

了解运费的业务需求
了解运费模板表的设计
了解项目中的代码规范
实现运费计算的业务逻辑
完成部署服务以及功能测试

背景

完成开发一组网关鉴权服务,到开发二组实现运费服务(基本框架搭建完成)的业务逻辑

需求分析

接到开发任务后,首先需要了解需求,再动手开发。(需要问清楚需求)

运费的计算是分不同地区的,有不同的运费规则。

产品原型:(增删改查)

业务说明:

不同的模板类型只有一个,关联城市为全国。但经济区互寄有多个(有不同的关联城市)。

模板类型有常量维护,经济区有枚举类维护。

运送类型只有普快。

<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='运费模板表';

说明:由于该表数据比较少,所以就不需要添加索引字段了。

MySQL两大引擎InnoDB和MyISAM区别

开发环境

微服务开发规范

在神领物流项目中,微服务代码是独立的工程(非聚合项目结构)
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时),发现错误

java: 错误: 无效的源发行版:16

自定义异常

在神领物流项目中,我们统一做了自定义异常的处理。定义了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查询结果

接口测试

idea开启底部services窗口(小技巧随记)

点击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:使用责任链模式写查找不同运输模板

面试问

问题

  • 你们的运费是怎么计算的?体积和重量怎么计算,到底以哪个为准?(轻抛系数,四舍五入怎么计算的,重量的不同范围划分等等)
  • 详细聊聊你们的运费模板是做什么的?
  • 有没有针对运费计算做什么优化?

逻辑

从下表出发

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值