本项目专栏:
整体核心业务流程
关键流程说明:
- 用户下单后,会产生取件任务,该任务也是由调度中心进行调度的
- 订单转运单后,会发送消息到调度中心,在调度中心中对相同节点的运单进行合并(这里是指最小转运单元)
- 调度中心同样也会对派件任务进行调度,用于生成快递员的派件任务
- 司机的出库和入库操作也是流程中的核心动作,尤其是入库操作,是推动运单流转的关键
智能分配快递员
实现分析
消息分析
/**
* 订单业务消息,接收到新订单后,根据快递员的负载情况,分配快递员
*/
@Slf4j
@Component
public class OrderMQListener {
@Resource
private CourierFeign courierFeign;
@Resource
private DispatchConfigurationFeign dispatchConfigurationFeign;
@Resource
private MQFeign mqFeign;
/**
* 如果有多个快递员,需要查询快递员今日的取派件数,根据此数量进行计算
* 计算的逻辑:优先分配取件任务少的,取件数相同的取第一个分配
* <p>
* 发送生成取件任务时需要计算时间差,如果小于2小时,实时发送;大于2小时,延时发送
* 举例:
* 1、现在10:30分,用户期望:11:00 ~ 12:00上门,实时发送
* 2、现在10:30分,用户期望:13:00 ~ 14:00上门,延时发送,12点发送消息,延时1.5小时发送
*
* @param msg 消息内容
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = Constants.MQ.Queues.DISPATCH_ORDER_TO_PICKUP_DISPATCH_TASK),
exchange = @Exchange(name = Constants.MQ.Exchanges.ORDER_DELAYED, type = ExchangeTypes.TOPIC, delayed = Constants.MQ.DELAYED),
key = Constants.MQ.RoutingKeys.ORDER_CREATE
))
public void listenOrderMsg(String msg) {
//{"orderId":123, "agencyId": 8001, "taskType":1, "mark":"带包装", "longitude":116.111, "latitude":39.00, "created":1654224658728, "estimatedEndTime": 1654224658728}
log.info("接收到订单的消息 >>> msg = {}", msg);
//1. 解析消息
OrderMsg orderMsg = JSONUtil.toBean(msg, OrderMsg.class);
Long agencyId = orderMsg.getAgencyId();
Double longitude = orderMsg.getLongitude();
Double latitude = orderMsg.getLatitude();
long epochMilli = LocalDateTimeUtil.toEpochMilli(orderMsg.getEstimatedEndTime());
//2. 查询有排班、符合条件的快递员,并且选择快递员
// List<Long> courierIds = this.courierFeign.queryCourierIdListByCondition(agencyId, longitude, latitude, epochMilli);
List<Long> courierIds = this.queryCourierIdListByCondition(agencyId, longitude, latitude, epochMilli);
Long selectedCourierId = null;
if (CollUtil.isNotEmpty(courierIds)) {
//选择快递员
selectedCourierId = this.selectCourier(courierIds, orderMsg.getTaskType());
}
//3. 如果是取件任务,需要计算时间差,来决定是发送实时消息还是延时消息
// 假设现在的时间是:10:30,用户期望上门时间是13:00 ~ 14:00
long between = LocalDateTimeUtil.between(LocalDateTimeUtil.now(), orderMsg.getEstimatedEndTime(), ChronoUnit.MINUTES);
DispatchConfigurationDTO dispatchConfiguration = this.dispatchConfigurationFeign.findConfiguration();
int dispatchTime = dispatchConfiguration.getDispatchTime() * 60;
int delay = Constants.MQ.DEFAULT_DELAY;
if (ObjectUtil.equals(orderMsg.getTaskType(), 1) && between > dispatchTime) {
//延迟消息 13:00 向前推 2小时,得到11:00
LocalDateTime date = LocalDateTimeUtil.offset(orderMsg.getEstimatedEndTime(), dispatchTime * -1L, ChronoUnit.MINUTES);
//延迟的时间,单位:毫秒 计算: 0.5小时 * 60分钟 * 60秒 * 1000
delay = Convert.toInt(LocalDateTimeUtil.between(LocalDateTime.now(), date, ChronoUnit.MILLIS));
}
//4. 发送消息,通知work微服务,用于创建快递员取派件任务
//4.1 构建消息
CourierTaskMsg courierTaskMsg = BeanUtil.toBeanIgnoreError(orderMsg, CourierTaskMsg.class);
courierTaskMsg.setCourierId(selectedCourierId);
courierTaskMsg.setCreated(System.currentTimeMillis());
//4.2 发送消息
this.mqFeign.sendMsg(Constants.MQ.Exchanges.PICKUP_DISPATCH_TASK_DELAYED,
Constants.MQ.RoutingKeys.PICKUP_DISPATCH_TASK_CREATE, courierTaskMsg.toJson(), delay);
}
private List<Long> queryCourierIdListByCondition(Long agencyId, Double longitude, Double latitude, long toEpochMilli) {
// TODO 暂时先模拟实现,后面再做具体实现
return ListUtil.of(1L);
}
/**
* 根据当日的任务数选取快递员
*
* @param courierIds 快递员列个表
* @param taskType 任务类型
* @return 选中的快递员id
*/
private Long selectCourier(List<Long> courierIds, Integer taskType) {
// TODO 暂时先模拟实现,后面再做具体实现
return courierIds.get(0);
}
}
根据位置查询快递员
@Service
@Slf4j
public class CourierUserServiceImpl implements CourierUserService {
@Resource
private WorkSchedulingFeign workSchedulingFeign;
@Resource
private ServiceScopeFeign serviceScopeFeign;
/**
* 条件查询快递员列表(结束取件时间当天快递员有排班)
* 如果服务范围内无快递员,或满足服务范围的快递员无排班,则返回该网点所有满足排班的快递员
*
* @param agencyId 网点id
* @param longitude 用户地址的经度
* @param latitude 用户地址的纬度
* @param estimatedEndTime 结束取件时间
* @return 快递员id列表
*/
@Override
public List<Long> queryCourierIdListByCondition(Long agencyId, Double longitude, Double latitude, Long estimatedEndTime) {
log.info("当前机构id为:{}", agencyId);
//1.根据经纬度查询服务范围内的快递员
List<ServiceScopeDTO> serviceScopeDTOS = serviceScopeFeign.queryListByLocation(2, longitude, latitude);
//1.1 如果服务范围内有快递员,则在其中筛选结束取件时间当天有排班的快递员
if (CollUtil.isNotEmpty(serviceScopeDTOS)) {
List<Long> bids = CollStreamUtil.toList(serviceScopeDTOS, ServiceScopeDTO::getBid);
log.info("根据经纬度查询到的快递员id有:{}", bids);
String bidStr = StrUtil.join(",", bids);
//1.2 查询排班数据,对满足服务范围、网点的快递员筛选排班
List<WorkSchedulingDTO> workSchedulingDTOS = workSchedulingFeign.monthSchedule(bidStr, agencyId, WorkUserTypeEnum.COURIER.getCode(), estimatedEndTime);
log.info("满足服务范围、网点的快递员排班:{}", workSchedulingDTOS);
if (CollUtil.isNotEmpty(workSchedulingDTOS)) {
List<Long> courierIds = StreamUtil.of(workSchedulingDTOS)
// 过滤出今日有排班的快递员
.filter(workSchedulingDTO -> workSchedulingDTO.getWorkSchedules().get(0))
.map(WorkSchedulingDTO::getUserId)
.collect(Collectors.toList());
log.info("服务范围、网点、排班均满足的快递员id有:{}", courierIds);
//1.3 存在同时满足服务范围、网点、排班的快递员,直接返回
if (CollUtil.isNotEmpty(courierIds)) {
return courierIds;
}
}
}
//2. 如果服务范围内没有快递员,或服务范围内的快递员没有排班,则查询该网点的任一有排班快递员
List<WorkSchedulingDTO> workSchedulingDTOS = workSchedulingFeign.monthSchedule(null, agencyId,
WorkUserTypeEnum.COURIER.getCode(), estimatedEndTime);
log.info("查询该网点所有快递员排班:{}", workSchedulingDTOS);
if (CollUtil.isEmpty(workSchedulingDTOS)) {
//该网点没有有排班的快递员
return null;
}
//2.1 对满足网点的快递员筛选排班
List<Long> courierIds = StreamUtil.of(workSchedulingDTOS)
// 过滤出今日有排班的快递员
.filter(workSchedulingDTO -> workSchedulingDTO.getWorkSchedules().get(0))
.map(WorkSchedulingDTO::getUserId)
.collect(Collectors.toList());
log.info("只满足网点、排班的快递员id有:{}", courierIds);
return courierIds;
}
}
选取快递员
/**
* 根据当日的任务数选取快递员
*
* @param courierIds 快递员列个表
* @param taskType 任务类型
* @return 选中的快递员id
*/
private Long selectCourier(List<Long> courierIds, Integer taskType) {
if (courierIds.size() == 1) {
return courierIds.get(0);
}
String date = DateUtil.date().toDateStr();
// List<CourierTaskCountDTO> courierTaskCountDTOS = this.pickupDispatchTaskFeign.findCountByCourierIds(courierIds,
// PickupDispatchTaskType.codeOf(taskType), date);
List<CourierTaskCountDTO> courierTaskCountDTOS = this.findCountByCourierIds(courierIds,
PickupDispatchTaskType.codeOf(taskType), date);
if (CollUtil.isEmpty(courierTaskCountDTOS)) {
//没有查到任务数量,默认给第一个快递员分配任务
return courierIds.get(0);
}
//查看任务数是否与快递员数相同,如果不相同需要补齐,设置任务数为0,这样就可以确保每个快递员都能分配到任务
if (ObjectUtil.notEqual(courierIds.size(), courierTaskCountDTOS.size())) {
List<CourierTaskCountDTO> dtoList = StreamUtil.of(courierIds)
.filter(courierId -> {
int index = CollUtil.indexOf(courierTaskCountDTOS, dto -> ObjectUtil.equals(dto.getCourierId(), courierId));
return index == -1;
})
.map(courierId -> CourierTaskCountDTO.builder()
.courierId(courierId)
.count(0L).build())
.collect(Collectors.toList());
//补齐到集合中
courierTaskCountDTOS.addAll(dtoList);
}
//按照任务数量从小到大排序
CollUtil.sortByProperty(courierTaskCountDTOS, "count");
//选中任务数最小的快递员进行分配
return courierTaskCountDTOS.get(0).getCourierId();
}
private List<CourierTaskCountDTO> findCountByCourierIds(List<Long> courierIds, PickupDispatchTaskType codeOf,
String date) {
//TODO 模拟实现
List<CourierTaskCountDTO> list = new ArrayList<>();
CourierTaskCountDTO courierTaskCountDTO = CourierTaskCountDTO.builder()
.courierId(courierIds.get(0))
.count(10L)
.build();
list.add(courierTaskCountDTO);
return list;
}


1070

被折叠的 条评论
为什么被折叠?



