课程安排
- 什么是智能调度
- 实现订单转运单(运单中包含路线的一些信息)
- 美团Leaf使用入门
- 完善运单服务
- 合并运单
1、背景说明
一个好的调度系统可以高效的管理着运单、运输任务、司机作业单、快递员取派件任务等
2、智能调度
2.1、为什么需要调度?
用户下单后,应该哪个网点的哪个快递员上门呢?
1 发件人地址信息定位到所属服务范围内的网点进行服务
2 对快递员的工作情况以及排班情况进行分析,才能确定哪个快递员进行服务。
快递员把快件拿回到网点后,假设这个快件是从上海寄往北京的,是网点直接开车送到北京吗
1 车辆尽可能的满载,集中化运输。
2 一般物流公司会有很多的车辆、路线、司机,而每个路线都会设置不同的车次,如何能够将快件合理的分配到车辆上,分配时需要参考车辆的载重、司机的排班,车辆的状态以及车次等信息
快件到收件人地址所在服务范围内的网点了,系统该如何分配快递员?
还有一些其他的情况,比如:快件拒收应该怎么处理?车辆故障不能使用怎么处理?一车多个司机,运输任务是如何划分?等等
2.2、整体核心业务流程
实线为主流程触发,虚线为mq等机制触发
关键流程说明:
- 用户下单后,会产生取件任务,该任务也是由调度中心进行调度的
- 订单转运单后,会发送消息到调度中心,在调度中心中对相同节点的运单进行合并(这里是指最小转运单元)
- 调度中心同样也会对派件任务进行调度,用于生成快递员的派件任务
- 司机的出库和入库操作也是流程中的核心动作,尤其是入库操作,是推动运单流转的关键
- 订单在网点的流转也是由调度中心进行调度的
3、订单转运单
订单和运单的区别:
订单:与电商平台物品有关的购买信息
运单(数据库表为sl_transpport_order):货物运输时的路径,成本等的信息
介绍:快递员已经接收到订单,使用mq往消息队列中发送消息。
现在任务为将订单转运单,补充消息
1查询订单相关的订单是否已经创建(mq多次发送,或网络延迟造成多次发送)
2 下单消息发送过来,在转换为运单的过程中,用户又取消了订单等原因导致订单不存在。故须判断订单是否存在
3 本项目的订单号有规定为sl+13位数字。假若使用数据库进行创建的自增id,性能差,这里使用美团leaf算法进行计算
4 使用mq发消息通知其他系统
3.2、运单表结构
运单表是在sl_work数据库中,表名为:sl_transport_order,结构如下:
其中当运单和某辆车产生关联时,为以装车。当车装满开始运送时为运输中
往mq中发消息就变成了待调度的货物
CREATE TABLE `sl_transport_order` (
`id` varchar(18) CHARACTER SET utf16 COLLATE utf16_general_ci NOT NULL DEFAULT '' COMMENT 'id',
`order_id` bigint NOT NULL COMMENT '订单ID',
`status` int DEFAULT NULL COMMENT '运单状态(1.新建 2.已装车 3.运输中 4.到达终端网点 5.已签收 6.拒收)',
`scheduling_status` int DEFAULT NULL COMMENT '调度状态(1.待调度2.未匹配线路3.已调度)',
`start_agency_id` bigint DEFAULT NULL COMMENT '起始网点id',
`end_agency_id` bigint DEFAULT NULL COMMENT '终点网点id',
`current_agency_id` bigint DEFAULT NULL COMMENT '当前所在机构id',
`next_agency_id` bigint DEFAULT NULL COMMENT '下一个机构id',
`transport_line` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '完整的运输路线',
`total_volume` decimal(32,4) DEFAULT NULL COMMENT '货品总体积,单位:立方米',
`total_weight` decimal(32,2) DEFAULT NULL COMMENT '货品总重量,单位:kg',
`is_rejection` tinyint(1) DEFAULT NULL COMMENT '是否为拒收运单',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `order_id` (`order_id`) USING BTREE,
KEY `created` (`created`) USING BTREE,
KEY `status` (`status`) USING BTREE,
KEY `scheduling_status` (`scheduling_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='运单表';
给经常查询的数据构建索引。查询时加快速度
索引构建太多的缺点:1占用空间2增删改时候需重新构建结构,操作复杂
3.3、揽收成功的消息
快递员揽件成功后,发出消息,这个逻辑是在sl-express-ms-web-courier
工程的com.sl.ms.web.courier.service.impl.TaskServiceImpl#pickup()
方法中实现的。
如下:
消息中包含了订单id,取到消息后根据id查询订单其他消息构建出对应的运单消息
/**
* 快递员取件成功
*
* @param msg 消息
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = Constants.MQ.Queues.WORK_COURIER_PICKUP_SUCCESS),
exchange = @Exchange(name = Constants.MQ.Exchanges.COURIER, type = ExchangeTypes.TOPIC),
key = Constants.MQ.RoutingKeys.COURIER_PICKUP
))
public void listenCourierPickupMsg(String msg) {
log.info("接收到快递员取件成功的消息 >>> msg = {}", msg);
//解析消息
CourierMsg courierMsg = JSONUtil.toBean(msg, CourierMsg.class);
//消费消息,调用transportOrderService的订单转运单方法
this.transportOrderService.orderToTransportOrder(courierMsg.getOrderId());
//TODO 发送运单跟踪消息
}
消息的交换机名称、路由key均是在sl-express-common工程中的Constants.MQ常量类中定义的。
3.4、生成运单号
对于运单号的生成有特殊的要求,格式:SL+13位数字,例如:SL1000000000760,对于这个需求,如果采用MP提供的雪花id生成是19位,是不能满足需求的,所以我们需要自己生成id,并且要确保唯一不能重复。
在这里我们采用美团的Leaf作为id生成服务,其源码托管于GitHub:
GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service
这里有个美团的技术播客,专门介绍了Leaf:
目前Leaf覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM基础上,通过公司RPC方式调用,QPS压测结果近5w/s,TP999 1ms。
Leaf 提供两种生成的ID的方式(segment模式,snowflake模式改进了雪花算法强依赖系统时钟的方式),我们采用segment模式(号段自增)来生成运单号。
订单号中还包括手机后四位验证,图片验证码验证。从而较好的保证了订单的安全性
3.4.1、号段模式
服务集群每次取一个号段(step规定号段长度),max_id为所有集群下最大的id。
biz_tag为leaf算法为对应某个表单设置的属性项,如运单表
号段模式采用的是基于MySQL数据生成id的,它并不是基于MySQL表中的自增长实现的,因为基于MySQL的自增长方案对于数据库的依赖太大了,性能不好,Leaf的号段模式是基于一张表来实现,每次获取一个号段,生成id时从内存中自增长,当号段用完后再去更新数据库表,如下:
缺点:
Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。
双buffer优化:
两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。以此解决号段用完时的阻塞。
采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。
- 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS(秒处理事务数)的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
3.4.2、部署服务(了解)
我们只用到了号段的方式,并没有使用雪花方式,所以只需要创建数据库表即可,无需安装ZooKeeper。
Leaf官方是没有docker镜像的,我们将其进行了镜像制作,并且上传到阿里云仓库,可以直接下载使用。目前已经在101机器部署完成。
双buffer实现测试:当网页中的号段id发出超过100的10%时,maxid会增加一个号段的值。此时即为另一个buffer中在存储号段
shell命令
docker run \
-d \
-v /itcast/meituan-leaf/leaf.properties:/app/conf/leaf.properties \
--name meituan-leaf \
-p 28838:8080 \
--restart=always \
registry.cn-hangzhou.aliyuncs.com/itheima/meituan-leaf:1.0.1
配置
leaf.name=leaf-server
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://192.168.150.101:3306/sl_leaf?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
leaf.jdbc.username=root
leaf.jdbc.password=123
leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=
创建sl_leaf数据库脚本:
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
`max_id` bigint NOT NULL DEFAULT '1',
`step` int NOT NULL,
`description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 插入运单号生成规划数据
INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`, `update_time`) VALUES ('transport_order', 1000000000001, 100, 'Test leaf Segment Mode Get Id', '2022-07-07 11:32:16');
测试:
# transport_order 与 biz_tag字段的值相同
http://192.168.150.101:28838/api/segment/get/transport_order
#监控
http://192.168.150.101:28838/cache
3.4.3、封装服务
在项目中,已经将Leaf集成到sl-express-common
工程中,代码如下:
package com.sl.transport.common.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.sl.transport.common.enums.IdEnum;
import com.sl.transport.common.exception.SLException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* id服务,用于生成自定义的id
*/
@Service
public class IdService {
@Value("${sl.id.leaf:}")
private String leafUrl;
/**
* 生成自定义id
*
* @param idEnum id配置
* @return id值
*/
public String getId(IdEnum idEnum) {
String idStr = this.doGet(idEnum);
return idEnum.getPrefix() + idStr;
}
private String doGet(IdEnum idEnum) {
if (StrUtil.isEmpty(this.leafUrl)) {
throw new SLException("生成id,sl.id.leaf配置不能为空.");
}
//访问leaf服务获取id
String url = StrUtil.format("{}/api/{}/get/{}", this.leafUrl, idEnum.getType(), idEnum.getBiz());
//设置超时时间为10s
HttpResponse httpResponse = HttpRequest.get(url)
.setReadTimeout(10000)
.execute();
if (httpResponse.isOk()) {
return httpResponse.body();
}
throw new SLException(StrUtil.format("访问leaf服务出错,leafUrl = {}, idEnum = {}", this.leafUrl, idEnum));
}
}
本项目只有运单服务使用,故此处枚举只有一个
package com.sl.transport.common.enums;
public enum IdEnum implements BaseEnum {
TRANSPORT_ORDER(1, "运单号", "transport_order", "segment", "SL");
private Integer code;
private String value;
private String biz; //业务名称
private String type; //类型:自增长(segment),雪花id(snowflake)
private String prefix;//id前缀
IdEnum(Integer code, String value, String biz, String type, String prefix) {
this.code = code;
this.value = value;
this.biz = biz;
this.type = type;
this.prefix = prefix;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getValue() {
return this.value;
}
public String getBiz() {
return biz;
}
public String getType() {
return type;
}
public String getPrefix() {
return prefix;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("IdEnum{");
sb.append("code=").append(code);
sb.append(", value='").append(value).append('\'');
sb.append(", biz='").append(biz).append('\'');
sb.append(", type='").append(type).append('\'');
sb.append(", prefix='").append(prefix).append('\'');
sb.append('}');
return sb.toString();
}
}
使用步骤:
- 在配置文件中进行配置
sl.id.leaf
为: http://192.168.150.101:28838 - 在Service中注入IdService,调用getId()方法即可,例如:
idService.getId(IdEnum.TRANSPORT_ORDER)
3.5、编码实现
订单转运单
订单转运单的实现是在sl-express-ms-work-service
微服务中完成的,git地址:http://git.sl-express.com/sl/sl-express-ms-work-service.git
上文中提到快递员通过mq接收消息,消费消息,从中获得订单id,传入transportOrderService的orderToTransportOrder方法中进行订单转换运单的操作
transportOrderService.orderToTransportOrder()具体编码实现:
逻辑:
1使用orderId通过TransportOrderService中.findByOrderId()方法获取运单实体
若非空,说明运单已存在,return
2幂等性判断
(订单,订单货物的体积和重量,位置信息),查询数据库表中对应的信息为非空,否则return
使用如下方法查询 orderFeign.findById(orderId) cargoFeign.findByOrderId(orderId)
orderFeign.findOrderLocationByOrderId(orderId)
分别对应数据库sl_oms的三张表
3 从查询到的位置信息的orderLocationDTO.getReceiveAgentId()中获取起点和终点网点的id
使用Convert.toLong将字符串转换为Long类型
4 定义是否参与调度的bool变量(起始和结束网点相同的不参与调度)boolean isDispatch = true
定义路线信息的变量 TransprotLineNodeDTO
5 判断起始和终点是否为一个网点,是 将idDispatch设为false
否 进行路线策略查询transportLineFeign.queryPathByDispatchMethod(sendAgentId, receiveAgentId)。这里固定写死为距离最短查询,后期将根据redis中的内容选择策略成本或距离
并判断transportLineNodeDTO和CollUtil.isEmpty(transportLineNodeDTO.getNodeList())是否为空
transportLineNodeDTO.getNodeList()为路线中的各个节点
空则报错
6创建新的运单对象
设置属性 id (根据美团leaf获得 idService.getId(IdEnum.TRANSPORT_ORDER) )
OrderId,StartAgencyId,EndAgencyId,CurrentAgencyId(当前所在机构id刚开始为起始机构id)
判断transportLineNodeDTO是否为空
空(下个网点就是当前网点,不参与调度)set NextAgencyId,Status运单状态,SchedulingStatus调度状态
非空(进行转运) set Status运单状态,SchedulingStatus调度状态
NextAgencyId( transportLineNodeDTO.getNodeList().get(1).getId() )下标为1是路线中的第二个
setTransportLine(JSONUtil.toJsonStr(transportLineNodeDTO))
设置信息 setTotalVolume(cargoDto.getVolume()),setTotalWeight(cargoDto.getWeight()),setIsRejection(false)默认非拒收订单
7保存boolean result = super.save(transportOrder)
8保存成功后发消息,失败时抛异常
isDispatch为true 发消息到调度中心sendTransportOrderMsgToDispatch(transportOrder)
并 //使用mq接口发消息通知其他系统,运单已经生成
为false 更新订单状态sendUpdateStatusMsg(ListUtil.toList(transportOrder.getId()), TransportOrderStatus.ARRIVED_END),
生成派件任务sendDispatchTaskMsgToDispatch(transportOrder)
mq:交换机根据路由键的规则将消息路由到适当的队列
发送消息设置延迟:服务端代码过长,需要时间运行。保证在其他服务查询时服务端订单转换运单,数据更新已经完成。
String msg = TransportOrderMsg.builder()
.id(transportOrder.getId())
.orderId(transportOrder.getOrderId())
.created(DateUtil.current())
.build().toJson();
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED,
Constants.MQ.RoutingKeys.TRANSPORT_ORDER_CREATE, msg, Constants.MQ.NORMAL_DELAY);
return transportOrder;
代码:
@Resource
private OrderFeign orderFeign;
@Resource
private MQFeign mqFeign;
@Resource
private CargoFeign cargoFeign;
@Resource
private TransportLineFeign transportLineFeign;
@Resource
private OrganFeign organFeign;
@Resource
private IdService idService;
@Transactional(rollbackFor = Exception.class,timeout = 120000)
@Override
public TransportOrderEntity orderToTransportOrder(Long orderId) {
//幂等性校验
TransportOrderEntity transportOrderEntity = this.findByOrderId(orderId);
if (ObjectUtil.isNotEmpty(transportOrderEntity)) {
return transportOrderEntity;
}
//查询订单
OrderDTO orderDTO = this.orderFeign.findById(orderId);
if (ObjectUtil.isEmpty(orderDTO)) {
throw new SLException(WorkExceptionEnum.ORDER_NOT_FOUND);
}
//查询货物的重量和体积数据
OrderCargoDTO cargoDto = this.cargoFeign.findByOrderId(orderId);
if (ObjectUtil.isEmpty(cargoDto)) {
throw new SLException(WorkExceptionEnum.ORDER_CARGO_NOT_FOUND);
}
//查询位置信息
OrderLocationDTO orderLocationDTO = orderFeign.findOrderLocationByOrderId(orderId);
if (ObjectUtil.isEmpty(orderLocationDTO)) {
throw new SLException(WorkExceptionEnum.ORDER_LOCATION_NOT_FOUND);
}
Long sendAgentId = Convert.toLong(orderLocationDTO.getSendAgentId());//起始网点id
Long receiveAgentId = Convert.toLong(orderLocationDTO.getReceiveAgentId());//终点网点id
//默认参与调度
boolean isDispatch = true;
TransportLineNodeDTO transportLineNodeDTO = null;
if (ObjectUtil.equal(sendAgentId, receiveAgentId)) {
//起点、终点是同一个网点,不需要规划路线,直接发消息生成派件任务即可
isDispatch = false;
} else {
//根据起始机构规划运输路线
transportLineNodeDTO = this.transportLineFeign.queryPathByDispatchMethod(sendAgentId, receiveAgentId);
if (ObjectUtil.isEmpty(transportLineNodeDTO) || CollUtil.isEmpty(transportLineNodeDTO.getNodeList())) {
throw new SLException(WorkExceptionEnum.TRANSPORT_LINE_NOT_FOUND);
}
}
//创建新的运单对象
TransportOrderEntity transportOrder = new TransportOrderEntity();
transportOrder.setId(this.idService.getId(IdEnum.TRANSPORT_ORDER)); //设置id
transportOrder.setOrderId(orderId);//订单ID
transportOrder.setStartAgencyId(sendAgentId);//起始网点id
transportOrder.setEndAgencyId(receiveAgentId);//终点网点id
transportOrder.setCurrentAgencyId(sendAgentId);//当前所在机构id
if (ObjectUtil.isNotEmpty(transportLineNodeDTO)) {
transportOrder.setStatus(TransportOrderStatus.CREATED);//运单状态(1.新建 2.已装车 3.运输中 4.到达终端网点 5.已签收 6.拒收)
transportOrder.setSchedulingStatus(TransportOrderSchedulingStatus.TO_BE_SCHEDULED);//调度状态(1.待调度2.未匹配线路3.已调度)
transportOrder.setNextAgencyId(transportLineNodeDTO.getNodeList().get(1).getId());//下一个机构id
transportOrder.setTransportLine(JSONUtil.toJsonStr(transportLineNodeDTO));//完整的运输路线
} else {
//下个网点就是当前网点
transportOrder.setNextAgencyId(sendAgentId);
transportOrder.setStatus(TransportOrderStatus.ARRIVED_END);//运单状态(1.新建 2.已装车 3.运输中 4.到达终端网点 5.已签收 6.拒收)
transportOrder.setSchedulingStatus(TransportOrderSchedulingStatus.SCHEDULED);//调度状态(1.待调度2.未匹配线路3.已调度)
}
transportOrder.setTotalVolume(cargoDto.getVolume());//货品总体积,单位m^3
transportOrder.setTotalWeight(cargoDto.getWeight());//货品总重量,单位kg
transportOrder.setIsRejection(false); //默认非拒收订单
boolean result = super.save(transportOrder);
if (result) {
if (isDispatch) {
//发送消息到调度中心,进行调度
this.sendTransportOrderMsgToDispatch(transportOrder);
//发消息通知其他系统,运单已经生成
String msg = TransportOrderMsg.builder()
.id(transportOrder.getId())
.orderId(transportOrder.getOrderId())
.created(DateUtil.current())
.build().toJson();
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED,
Constants.MQ.RoutingKeys.TRANSPORT_ORDER_CREATE, msg, Constants.MQ.NORMAL_DELAY);
} else {
// 不需要调度 发送消息更新订单状态
this.sendUpdateStatusMsg(ListUtil.toList(transportOrder.getId()), TransportOrderStatus.ARRIVED_END);
//不需要调度,发送消息生成派件任务
this.sendDispatchTaskMsgToDispatch(transportOrder);
}
return transportOrder;
}
//保存失败
throw new SLException(WorkExceptionEnum.TRANSPORT_ORDER_SAVE_ERROR);
}
发送消息的代码实现(了解):
sendTransportOrderMsgToDispatch:通过map绑定参数,进行mq消息传递
sendDispatchTaskMsgToDispatch:如果是中午12点到的快递,当天22点前,否则,第二天22点前。使用offset 0或1进行添加
sendPickupDispatchTaskMsgToDispatch:
//(1)运单为空:取件任务取消,取消原因为返回网点;重新调度位置取寄件人位置
//(2)运单不为空:生成的是派件任务,需要根据拒收状态判断位置是寄件人还是收件人
// 拒收:寄件人 其他:收件人
sendUpdateStatusMsg:运单只要创建就发送消息,等待其他微服务调用运单消息
/**
* 发送运单消息到调度中,参与调度
*/
private void sendTransportOrderMsgToDispatch(TransportOrderEntity transportOrder) {
Map<Object, Object> msg = MapUtil.builder()
.put("transportOrderId", transportOrder.getId())
.put("currentAgencyId", transportOrder.getCurrentAgencyId())
.put("nextAgencyId", transportOrder.getNextAgencyId())
.put("totalWeight", transportOrder.getTotalWeight())
.put("totalVolume", transportOrder.getTotalVolume())
.put("created", System.currentTimeMillis()).build();
String jsonMsg = JSONUtil.toJsonStr(msg);
//发送消息,延迟5秒,确保本地事务已经提交,可以查询到数据
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED,
Constants.MQ.RoutingKeys.JOIN_DISPATCH, jsonMsg, Constants.MQ.LOW_DELAY);
}
/**
* 发送生成取派件任务的消息
*
* @param transportOrder 运单对象
*/
private void sendDispatchTaskMsgToDispatch(TransportOrderEntity transportOrder) {
//预计完成时间,如果是中午12点到的快递,当天22点前,否则,第二天22点前
int offset = 0;
if (LocalDateTime.now().getHour() >= 12) {
offset = 1;
}
LocalDateTime estimatedEndTime = DateUtil.offsetDay(new Date(), offset)
.setField(DateField.HOUR_OF_DAY, 22)
.setField(DateField.MINUTE, 0)
.setField(DateField.SECOND, 0)
.setField(DateField.MILLISECOND, 0).toLocalDateTime();
//发送分配快递员派件任务的消息
OrderMsg orderMsg = OrderMsg.builder()
.agencyId(transportOrder.getCurrentAgencyId())
.orderId(transportOrder.getOrderId())
.created(DateUtil.current())
.taskType(PickupDispatchTaskType.DISPATCH.getCode()) //派件任务
.mark("系统提示:派件前请与收件人电话联系.")
.estimatedEndTime(estimatedEndTime).build();
//发送消息
this.sendPickupDispatchTaskMsgToDispatch(transportOrder, orderMsg);
}
/**
* 发送消息到调度中心,用于生成取派件任务
*
* @param transportOrder 运单
* @param orderMsg 消息内容
*/
@Override
public void sendPickupDispatchTaskMsgToDispatch(TransportOrderEntity transportOrder, OrderMsg orderMsg) {
//查询订单对应的位置信息
OrderLocationDTO orderLocationDTO = this.orderFeign.findOrderLocationByOrderId(orderMsg.getOrderId());
//(1)运单为空:取件任务取消,取消原因为返回网点;重新调度位置取寄件人位置
//(2)运单不为空:生成的是派件任务,需要根据拒收状态判断位置是寄件人还是收件人
// 拒收:寄件人 其他:收件人
String location;
if (ObjectUtil.isEmpty(transportOrder)) {
location = orderLocationDTO.getSendLocation();
} else {
location = transportOrder.getIsRejection() ? orderLocationDTO.getSendLocation() : orderLocationDTO.getReceiveLocation();
}
Double[] coordinate = Convert.convert(Double[].class, StrUtil.split(location, ","));
Double longitude = coordinate[0];
Double latitude = coordinate[1];
//设置消息中的位置信息
orderMsg.setLongitude(longitude);
orderMsg.setLatitude(latitude);
//发送消息,用于生成取派件任务
this.mqFeign.sendMsg(Constants.MQ.Exchanges.ORDER_DELAYED, Constants.MQ.RoutingKeys.ORDER_CREATE,
orderMsg.toJson(), Constants.MQ.NORMAL_DELAY);
}
private void sendUpdateStatusMsg(List<String> ids, TransportOrderStatus transportOrderStatus) {
String msg = TransportOrderStatusMsg.builder()
.idList(ids)
.statusName(transportOrderStatus.name())
.statusCode(transportOrderStatus.getCode())
.build().toJson();
//将状态名称写入到路由key中,方便消费方选择性的接收消息
String routingKey = Constants.MQ.RoutingKeys.TRANSPORT_ORDER_UPDATE_STATUS_PREFIX + transportOrderStatus.name();
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED, routingKey, msg, Constants.MQ.LOW_DELAY);
}
测试:
测试订单转运单功能,需要启动所必须的一些服务,base、oms、transport服务,启动命令如下:
#在101机器执行如下命令
docker start sl-express-ms-base-service
docker start sl-express-ms-oms-service
docker start sl-express-ms-transport-service
编写测试用例:
快递员揽收成功后,发出消息(中包含订单id的信息),再使用订单转运单的方法
package com.sl.ms.work.mq;
import cn.hutool.json.JSONUtil;
import com.sl.transport.common.vo.CourierMsg;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class CourierMQListenerTest {
@Resource
private CourierMQListener courierMQListener;
@Test
void listenCourierTaskMsg() {
}
@Test
void listenCourierPickupMsg() {
CourierMsg courierMsg = new CourierMsg();
//目前只用到订单id
courierMsg.setOrderId(1564170062718373889L);
String msg = JSONUtil.toJsonStr(courierMsg);
this.courierMQListener.listenCourierPickupMsg(msg);
}
}
测试时,需要确保在sl_oms数据库中的sl_order、sl_order_cargo、sl_order_location表中有对应的订单数据。如果没有数据,可以通过以下SQL插入测试数据或者通过用户端进行下单
use `sl_oms`;
INSERT INTO `sl_order` (`id`, `trading_order_no`, `trading_channel`, `payment_method`, `payment_status`, `amount`, `refund`, `is_refund`, `order_type`, `pickup_type`, `create_time`, `member_id`, `receiver_member_id`, `receiver_province_id`, `receiver_city_id`, `receiver_county_id`, `receiver_address`, `receiver_address_id`, `receiver_name`, `receiver_phone`, `sender_province_id`, `sender_city_id`, `sender_county_id`, `sender_address`, `sender_address_id`, `sender_name`, `sender_phone`, `current_agency_id`, `estimated_arrival_time`, `mark`, `estimated_start_time`, `distance`, `status`, `created`, `updated`) VALUES (1590586236289646594, 0, '0', 1, 1, 12.00, 0.00, NULL, 1, 0, '2022-11-10 14:04:45', 1555110960890843137, NULL, 545532, 545533, 545763, '西华大道16号', 1575682056178180097, '吴思涵', '15645237852', 545532, 545533, 545669, '光华大道一段1666号', 1575763704869625857, '邓诗涵', '15745678521', 1024771753995515873, '2022-11-14 14:04:45', NULL, '2022-11-10 15:04:00', 11265, 23000, '2022-11-10 14:04:45', '2022-11-10 14:04:45');
INSERT INTO `sl_order` (`id`, `trading_order_no`, `trading_channel`, `payment_method`, `payment_status`, `amount`, `refund`, `is_refund`, `order_type`, `pickup_type`, `create_time`, `member_id`, `receiver_member_id`, `receiver_province_id`, `receiver_city_id`, `receiver_county_id`, `receiver_address`, `receiver_address_id`, `receiver_name`, `receiver_phone`, `sender_province_id`, `sender_city_id`, `sender_county_id`, `sender_address`, `sender_address_id`, `sender_name`, `sender_phone`, `current_agency_id`, `estimated_arrival_time`, `mark`, `estimated_start_time`, `distance`, `status`, `created`, `updated`) VALUES (1590586360180998146, 0, '0', 1, 1, 12.00, 0.00, NULL, 1, 0, '2022-11-10 14:05:15', 1555110960890843137, NULL, 545532, 545533, 545669, '光华大道一段1666号', 1575763704869625857, '邓诗涵', '15745678521', 545532, 545533, 545669, '光华大道一段1666号', 1575681460301799425, '李成百', '15812357412', 1024771753995515873, '2022-11-14 14:05:15', NULL, '2022-11-10 15:05:00', 1, 23000, '2022-11-10 14:05:15', '2022-11-10 14:05:15');
INSERT INTO `sl_order` (`id`, `trading_order_no`, `trading_channel`, `payment_method`, `payment_status`, `amount`, `refund`, `is_refund`, `order_type`, `pickup_type`, `create_time`, `member_id`, `receiver_member_id`, `receiver_province_id`, `receiver_city_id`, `receiver_county_id`, `receiver_address`, `receiver_address_id`, `receiver_name`, `receiver_phone`, `sender_province_id`, `sender_city_id`, `sender_county_id`, `sender_address`, `sender_address_id`, `sender_name`, `sender_phone`, `current_agency_id`, `estimated_arrival_time`, `mark`, `estimated_start_time`, `distance`, `status`, `created`, `updated`) VALUES (1590587504731062273, 0, '0', 1, 1, 18.00, 0.00, NULL, 2, 0, '2022-11-10 14:09:47', 1555110960890843137, NULL, 161792, 161793, 165026, '上海迪士尼度假区', 1590587449528274946, '吕奉先', '18512345678', 545532, 545533, 545669, '光华大道一段1666号', 1575763704869625857, '邓诗涵', '15745678521', 1024771753995515873, '2022-11-14 14:09:47', NULL, '2022-11-10 15:09:00', 1990898, 23000, '2022-11-10 14:09:47', '2022-11-10 14:09:47');
INSERT INTO `sl_order_cargo` (`id`, `order_id`, `tran_order_id`, `goods_type_id`, `name`, `unit`, `cargo_value`, `cargo_barcode`, `quantity`, `volume`, `weight`, `remark`, `total_volume`, `total_weight`, `created`, `updated`) VALUES (1590586236767797249, 1590586236289646594, NULL, '1552846618315661320', '单肩包', NULL, NULL, NULL, 1, 1.0000000000, 1.0000000000, NULL, 1.0000000000, 1.0000000000, '2022-11-10 14:04:45', '2022-11-10 14:04:45');
INSERT INTO `sl_order_cargo` (`id`, `order_id`, `tran_order_id`, `goods_type_id`, `name`, `unit`, `cargo_value`, `cargo_barcode`, `quantity`, `volume`, `weight`, `remark`, `total_volume`, `total_weight`, `created`, `updated`) VALUES (1590586360294244354, 1590586360180998146, NULL, '1552846618315661321', '项链', NULL, NULL, NULL, 1, 1.0000000000, 1.0000000000, NULL, 1.0000000000, 1.0000000000, '2022-11-10 14:05:15', '2022-11-10 14:05:15');
INSERT INTO `sl_order_cargo` (`id`, `order_id`, `tran_order_id`, `goods_type_id`, `name`, `unit`, `cargo_value`, `cargo_barcode`, `quantity`, `volume`, `weight`, `remark`, `total_volume`, `total_weight`, `created`, `updated`) VALUES (1590587504747839490, 1590587504731062273, NULL, '1552846618315661323', '跑步鞋', NULL, NULL, NULL, 1, 1.0000000000, 1.0000000000, NULL, 1.0000000000, 1.0000000000, '2022-11-10 14:09:47', '2022-11-10 14:09:47');
INSERT INTO `sl_order_location` (`id`, `order_id`, `send_location`, `receive_location`, `send_agent_id`, `receive_agent_id`, `status`, `created`, `updated`) VALUES (1590586238537793537, 1590586236289646594, '103.960686,30.671626', '104.034504,30.721027', '1024771753995515873', '1024771466287232801', '1', '2022-11-10 14:04:46', '2022-11-10 14:04:46');
INSERT INTO `sl_order_location` (`id`, `order_id`, `send_location`, `receive_location`, `send_agent_id`, `receive_agent_id`, `status`, `created`, `updated`) VALUES (1590586360315215874, 1590586360180998146, '103.960686,30.671626', '103.960686,30.671626', '1024771753995515873', '1024771753995515873', '1', '2022-11-10 14:05:15', '2022-11-10 14:05:15');
INSERT INTO `sl_order_location` (`id`, `order_id`, `send_location`, `receive_location`, `send_agent_id`, `receive_agent_id`, `status`, `created`, `updated`) VALUES (1590587504756228097, 1590587504731062273, '103.960686,30.671626', '121.661735,31.141333', '1024771753995515873', '1024981295454874273', '1', '2022-11-10 14:09:47', '2022-11-10 14:09:47');
结果
第二个订单发送和接收地相同,故不参加调度,没有路线
4、完善运单服务
前面已经完成了订单转运单的功能,接下来我们将完善运单中的其他基本的实现,这部分代码以阅读、理解为主。
其中pageQueryByTaskId()、updateByTaskId()方法在学习运输任务相关业务时进行实现。
4.1、获取运单分页数据
mybatisPlus中可以将bool类型参数放入查询语句中,如下
实现
@Override
public Page<TransportOrderEntity> findByPage(TransportOrderQueryDTO transportOrderQueryDTO) {
Page<TransportOrderEntity> iPage = new Page<>(transportOrderQueryDTO.getPage(), transportOrderQueryDTO.getPageSize());
//设置查询条件
LambdaQueryWrapper<TransportOrderEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getId()), TransportOrderEntity::getId, transportOrderQueryDTO.getId());
lambdaQueryWrapper.like(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getOrderId()), TransportOrderEntity::getOrderId, transportOrderQueryDTO.getOrderId());
lambdaQueryWrapper.eq(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getStatus()), TransportOrderEntity::getStatus, transportOrderQueryDTO.getStatus());
lambdaQueryWrapper.eq(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getSchedulingStatus()), TransportOrderEntity::getSchedulingStatus, transportOrderQueryDTO.getSchedulingStatus());
lambdaQueryWrapper.eq(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getStartAgencyId()), TransportOrderEntity::getStartAgencyId, transportOrderQueryDTO.getStartAgencyId());
lambdaQueryWrapper.eq(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getEndAgencyId()), TransportOrderEntity::getEndAgencyId, transportOrderQueryDTO.getEndAgencyId());
lambdaQueryWrapper.eq(ObjectUtil.isNotEmpty(transportOrderQueryDTO.getCurrentAgencyId()), TransportOrderEntity::getCurrentAgencyId, transportOrderQueryDTO.getCurrentAgencyId());
lambdaQueryWrapper.orderByDesc(TransportOrderEntity::getCreated);
return super.page(iPage, lambdaQueryWrapper);
}
注:如果订单id不为空,根据订单id进行模糊查询。此时是单个订单的精确查询。
不输入id时,根据page和pagesize进行查询多个运单进行显示
4.2、订单id获取运单信息
@Override
public TransportOrderEntity findByOrderId(Long orderId) {
LambdaQueryWrapper<TransportOrderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TransportOrderEntity::getOrderId, orderId);
return super.getOne(queryWrapper);
}
@Override
public List<TransportOrderEntity> findByOrderIds(Long[] orderIds) {
LambdaQueryWrapper<TransportOrderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(TransportOrderEntity::getOrderId, orderIds);
return super.list(queryWrapper);
}
4.3、运单ids获取运单列表
@Override
public List<TransportOrderEntity> findByIds(String[] ids) {
LambdaQueryWrapper<TransportOrderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(TransportOrderEntity::getId, ids);
return super.list(queryWrapper);
}
4.4、根据运单号搜索运单
用户端输入一部分订单号后进行模糊查询
@Override
public List<TransportOrderEntity> searchById(String id) {
LambdaQueryWrapper<TransportOrderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(TransportOrderEntity::getId, id);
return super.list(queryWrapper);
}
4.5、统计状态的数量
统计所有枚举类设置初值为0,只更改后续查询到的状态数量。这样保证了没有的状态可以设置为0显示
逻辑
获取枚举类中所有的值抓换为数组
TransportOrderStatus[] values = TransportOrderStatus.values()
将数组使用流进行处理转换为TransportOrderStatusCountDTO对象。使用map的方法将每一个枚举的值转换为DTO对象
Arrays.stream(TransportOrderStatus.values()).map()
map中将每一个枚举的值转换为DTO对象
transportOrderStatus -> TransportOrderStatusCountDTO.builder()
.status(transportOrderStatus)
.statusCode(transportOrderStatus.getCode())
.count(0L)
.build()
或
将他们收集为集合(转换为List形式)
这里的baseMapper为函数头继承的mapper类型
数据库语句查询
查询到的结果使用两层循环,如果有(即.getStatusCode得到的code相同),则将查询到的状态数量更改
实现
@Override
public List<TransportOrderStatusCountDTO> findStatusCount() {
//将所有的枚举状态放到集合中
List<TransportOrderStatusCountDTO> statusCountList = Arrays.stream(TransportOrderStatus.values())
.map(transportOrderStatus -> TransportOrderStatusCountDTO.builder()
.status(transportOrderStatus)
.statusCode(transportOrderStatus.getCode())
.count(0L)
.build())
.collect(Collectors.toList());
//将数量值放入到集合中,如果没有的数量为0
List<TransportOrderStatusCountDTO> statusCount = super.baseMapper.findStatusCount();
for (TransportOrderStatusCountDTO transportOrderStatusCountDTO : statusCountList) {
for (TransportOrderStatusCountDTO countDTO : statusCount) {
if (ObjectUtil.equal(transportOrderStatusCountDTO.getStatusCode(), countDTO.getStatusCode())) {
transportOrderStatusCountDTO.setCount(countDTO.getCount());
break;
}
}
}
return statusCountList;
}
测试
4.6、更新状态
在更新运单状态时需要考虑三件事:
- 如果更新运单为拒收状态,需要将快递退回去,也就是原路返回
- 在更新状态时,需要同步更新物流信息,通过发送消息的方式完成(先TODO,后面实现)
- 更新状态后需要通知其他系统(消息通知)
- 一车货有很多订单,需要批量修改List<String> ids
逻辑:
1 List<String> ids非空
2 订单状态不能为新建状态
3 运单为拒收状态时
3.1 查询所有订单super.listByIds(ids) ,for循环订单
3.2 查询起始和终点,将终点网点设置为起点,将起点网点设置为终点,拒收时原路返回
3.3 默认调度isDispatch设置为true。
3.4 若起终点相同,isDispatch设置为false,不参与调度
3.5 参与调度时 this.transportLineFeign.queryPathByDispatchMethod(sendAgentId, receiveAgentId)查询路线。判断是否为空
注:这里查询的transportLineNodeDTO为拒收路线。
之前根据ids查询的transportOrderEntity为发送到这个网点的路线
3.6 删除掉拒收路线的第一个机构,需要和之前的路线拼接为完整线路,故删去一个重复的D。
transportLineNodeDTO.getNodeList().remove(0)
3.7 设置 调度状态:待调度,当前所在机构id,
下一个机构id transportLineNodeDTO.getNodeList().get(0).getId()。
3.8合并为完整路线
//获取原发送路线
TransportLineNodeDTO transportLineNode = JSONUtil.toBean(transportOrderEntity.getTransportLine(), TransportLineNodeDTO.class);
//将拒收路线追加到节点列表中
transportLineNode.getNodeList().addAll(transportLineNodeDTO.getNodeList());
//合并成本
transportLineNode.setCost(NumberUtil.add(transportLineNode.getCost(), transportLineNodeDTO.getCost()));
//完整的运输路线给transportOrderEntity作为返回对象
transportOrderEntity.setTransportLine(JSONUtil.toJsonStr(transportLineNode));
3.9设置运单状态为拒收。isDispatch判断是否需要发送消息参与调度
//不需要调度,发送消息生成派件任务
transportOrderEntity.setStatus(TransportOrderStatus.ARRIVED_END);
this.sendDispatchTaskMsgToDispatch(transportOrderEntity);
4 运单非拒收状态时
4.1 使用stream流循环ids( ids.stream().map(id->{}).collect(Collectors.toList()) ),每一个id构建一个transprotOrderEntity实体(设置id和状态),最后收集为List集合转换为transprotOrderList
4.2 Day10时订单只要发生变化,使用mq发送消息,这样用户端可以一直观看进度
4.3 批量更新运单数据super.updateBatchById(transportOrderList),和发送运单更新消息super.updateBatchById(transportOrderList)
代码:
@Override
public boolean updateStatus(List<String> ids, TransportOrderStatus transportOrderStatus) {
if (CollUtil.isEmpty(ids)) {
return false;
}
if (TransportOrderStatus.CREATED == transportOrderStatus) {
//修改订单状态不能为 新建 状态
throw new SLException(WorkExceptionEnum.TRANSPORT_ORDER_STATUS_NOT_CREATED);
}
List<TransportOrderEntity> transportOrderList;
//判断是否为拒收状态,如果是拒收需要重新查询路线,将包裹逆向回去
if (TransportOrderStatus.REJECTED == transportOrderStatus) {
//查询运单列表
transportOrderList = super.listByIds(ids);
for (TransportOrderEntity transportOrderEntity : transportOrderList) {
//设置为拒收运单
transportOrderEntity.setIsRejection(true);
//根据起始机构规划运输路线,这里要将起点和终点互换
Long sendAgentId = transportOrderEntity.getEndAgencyId();//起始网点id
Long receiveAgentId = transportOrderEntity.getStartAgencyId();//终点网点id
//默认参与调度
boolean isDispatch = true;
if (ObjectUtil.equal(sendAgentId, receiveAgentId)) {
//相同节点,无需调度,直接生成派件任务
isDispatch = false;
} else {
TransportLineNodeDTO transportLineNodeDTO = this.transportLineFeign.queryPathByDispatchMethod(sendAgentId, receiveAgentId);
if (ObjectUtil.hasEmpty(transportLineNodeDTO, transportLineNodeDTO.getNodeList())) {
throw new SLException(WorkExceptionEnum.TRANSPORT_LINE_NOT_FOUND);
}
//删除掉第一个机构,逆向回去的第一个节点就是当前所在节点
transportLineNodeDTO.getNodeList().remove(0);
transportOrderEntity.setSchedulingStatus(TransportOrderSchedulingStatus.TO_BE_SCHEDULED);//调度状态:待调度
transportOrderEntity.setCurrentAgencyId(sendAgentId);//当前所在机构id
transportOrderEntity.setNextAgencyId(transportLineNodeDTO.getNodeList().get(0).getId());//下一个机构id
//获取到原有节点信息
TransportLineNodeDTO transportLineNode = JSONUtil.toBean(transportOrderEntity.getTransportLine(), TransportLineNodeDTO.class);
//将逆向节点追加到节点列表中
transportLineNode.getNodeList().addAll(transportLineNodeDTO.getNodeList());
//合并成本
transportLineNode.setCost(NumberUtil.add(transportLineNode.getCost(), transportLineNodeDTO.getCost()));
transportOrderEntity.setTransportLine(JSONUtil.toJsonStr(transportLineNode));//完整的运输路线
}
transportOrderEntity.setStatus(TransportOrderStatus.REJECTED);
if (isDispatch) {
//发送消息参与调度
this.sendTransportOrderMsgToDispatch(transportOrderEntity);
} else {
//不需要调度,发送消息生成派件任务
transportOrderEntity.setStatus(TransportOrderStatus.ARRIVED_END);
this.sendDispatchTaskMsgToDispatch(transportOrderEntity);
}
}
} else {
//根据id列表封装成运单对象列表
transportOrderList = ids.stream().map(id -> {
//获取将发往的目的地机构
Long nextAgencyId = this.getById(id).getNextAgencyId();
OrganDTO organDTO = organFeign.queryById(nextAgencyId);
//构建消息实体类
String info = CharSequenceUtil.format("快件发往【{}】", organDTO.getName());
String transportInfoMsg = TransportInfoMsg.builder()
.transportOrderId(id)//运单id
.status("运送中")//消息状态
.info(info)//消息详情
.created(DateUtil.current())//创建时间
.build().toJson();
//发送运单跟踪消息
this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_INFO, Constants.MQ.RoutingKeys.TRANSPORT_INFO_APPEND, transportInfoMsg);
//封装运单对象
TransportOrderEntity transportOrderEntity = new TransportOrderEntity();
transportOrderEntity.setId(id);
transportOrderEntity.setStatus(transportOrderStatus);
return transportOrderEntity;
}).collect(Collectors.toList());
}
//批量更新数据
boolean result = super.updateBatchById(transportOrderList);
//发消息通知其他系统运单状态的变化
this.sendUpdateStatusMsg(ids, transportOrderStatus);
return result;
}
测试:
5、合并运单
5.1、逻辑
使用xxl job定时任务查询redis,进行合并订单的操作
将待调度的运单按照不同的路线放入redis的List中,使用redis set解决幂等性问题。
运单在运输过程中,虽然快件的起点与终点都不一定相同,但是在中间转运环节有一些运输节点是相同的,如下
根据以上两点的需求,很容易想到队列的存储方式。在这里我们采用Redis的List作为队列,将相同节点转运的订单放到同一个队列中,可以使用其LPUSH
放进去,RPOP
弹出数据,这样就可以确保先进先出,并且弹出后数据将删除,因此符合我们的需求。
redis优点: 1:内存中访问快
2:持久化机制,系统宕机时,数据不会丢失
redis使用的键值构造为A_B,每两点之间都有。
mq发消息可能重复发送,考虑幂等性时,同时使用List集合和Set集合存储货物。
每次List存储货物时,先在Set集合中查看是否已经存在,若存在,则不进行List的第二次存储
5.2、代码实现
5.2.1、准备环境
合并运单与调度的业务逻辑都放到sl-express-ms-dispatch-service
工程中,git地址:http://git.sl-express.com/sl/sl-express-ms-dispatch-service.git,检出代码如下:
sl-express-ms-dispatch-service:只处理调度相关的任务,代码结构中无entity,数据库层的代码
5.2.2、逻辑
逻辑:
1从调度中获取消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = Constants.MQ.Queues.DISPATCH_MERGE_TRANSPORT_ORDER),
exchange = @Exchange(name = Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED, type = ExchangeTypes.TOPIC, delayed = Constants.MQ.DELAYED),
key = Constants.MQ.RoutingKeys.JOIN_DISPATCH
))
2 判断消息非空
3 获取当前机构id,下一个机构id,运单id
4 .getSetRedisKey(startId, endId)获取redis的setReidsKey
redis获取List,set key的代码
public String getListRedisKey(Long startId, Long endId) {
return StrUtil.format("DISPATCH_LIST_{}_{}", startId, endId);
}
public String getSetRedisKey(Long startId, Long endId) {
return StrUtil.format("DISPATCH_SET_{}_{}", startId, endId);
}
5幂等性判断,stringRedisTemplate.opsForSet().isMember(setRedisKey, transportOrderId)
判断该运单是否为set成员,若是则表示已经合并过,return
注:stringRedisTemplate与redisTemplate区别:string为string类型数据,原redisTemplate为jdk序列化数据
6 若未处理过,则添加到List集合中.getListRedisKey(startId, endId)
设置value
String value = JSONUtil.toJsonStr(MapUtil.builder()
.put("transportOrderId", transportOrderId)
.put("totalVolume", dispatchMsgDTO.getTotalVolume())
.put("totalWeight", dispatchMsgDTO.getTotalWeight())
.put("created", dispatchMsgDTO.getCreated()).build()
);
7存储到List和set中。存数据用leftPush,取数据用rightPop
stringRedisTemplate.opsForList().leftPush(listRedisKey, value)
stringRedisTemplate.opsForSet().add(setRedisKey, transportOrderId)
5.2.3、代码
@Slf4j
@Component
public class TransportOrderDispatchMQListener {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 处理消息,合并运单到Redis队列
*
* @param msg
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = Constants.MQ.Queues.DISPATCH_MERGE_TRANSPORT_ORDER),
exchange = @Exchange(name = Constants.MQ.Exchanges.TRANSPORT_ORDER_DELAYED, type = ExchangeTypes.TOPIC, delayed = Constants.MQ.DELAYED),
key = Constants.MQ.RoutingKeys.JOIN_DISPATCH
))
public void listenDispatchMsg(String msg) {
// {"transportOrderId":"SL1000000000560","currentAgencyId":100280,"nextAgencyId":90001,"totalWeight":3.5,"totalVolume":2.1,"created":1652337676330}
log.info("接收到新运单的消息 >>> msg = {}", msg);
// {"transportOrderId":"SL1000000000560","currentAgencyId":100280,"nextAgencyId":90001,"totalWeight":3.5,"totalVolume":2.1,"created":1652337676330}
log.info("接收到新运单的消息 >>> msg = {}", msg);
DispatchMsgDTO dispatchMsgDTO = JSONUtil.toBean(msg, DispatchMsgDTO.class);
if (ObjectUtil.isEmpty(dispatchMsgDTO)) {
return;
}
Long startId = dispatchMsgDTO.getCurrentAgencyId();
Long endId = dispatchMsgDTO.getNextAgencyId();
String transportOrderId = dispatchMsgDTO.getTransportOrderId();
//消息幂等性处理,将相同起始节点的运单存放到set结构的redis中,在相应的运单处理完成后将其删除掉
String getSetRedisKey = this.getSetRedisKey(startId, endId);
if (this.stringRedisTemplate.opsForSet().isMember(getSetRedisKey, transportOrderId)) {
//重复消息
return;
}
//存储数据到redis,采用list结构,从左边写入数据,读取数据时从右边读取
//key => DISPATCH_CurrentAgencyId_NextAgencyId
//value => {"transportOrderId":111222, "totalVolume":0.8, "totalWeight":2.1, "created":111222223333}
String listRedisKey = this.getListRedisKey(startId, endId);
String value = JSONUtil.toJsonStr(MapUtil.builder()
.put("transportOrderId", transportOrderId)
.put("totalVolume", dispatchMsgDTO.getTotalVolume())
.put("totalWeight", dispatchMsgDTO.getTotalWeight())
.put("created", dispatchMsgDTO.getCreated()).build()
);
//存储到redis
this.stringRedisTemplate.opsForList().leftPush(listRedisKey, value);
this.stringRedisTemplate.opsForSet().add(getSetRedisKey, transportOrderId);
}
public String getListRedisKey(Long startId, Long endId) {
return StrUtil.format("DISPATCH_LIST_{}_{}", startId, endId);
}
public String getSetRedisKey(Long startId, Long endId) {
return StrUtil.format("DISPATCH_SET_{}_{}", startId, endId);
}
}
测试:
6、练习
6.2、练习二:阅读代码
难度系数:★★★☆☆
需求:阅读快递员服务中的【取件】业务功能,主要阅读代码逻辑如下:
1)理解取件业务的逻辑
2)理解实名认证的方法
3)理解更新订单状态的业务逻辑
取件业务、实名认证业务逻辑都放到sl-express-ms-web-courier
工程中,git地址:http://git.sl-express.com/sl/sl-express-ms-web-courier.git,检出代码如下:
找到其中 TaskController
7、面试连环问
面试官问:
- 物流项目中你参与了核心的功能开发吗?能说一下核心的业务逻辑吗?
- 你们的运单号是怎么生成的?如何确保性能?
- 能说一下订单转运单的业务逻辑吗?生成运单后如何与调度中心整合在一起的?
- 合并运单为什么使用Redis的List作为队列?如何确保消息的幂等性的?
- 项目中在哪些地方使用到了redis