本项目专栏:
物流项目_Auc23的博客-CSDN博客
整体核心业务流程
关键流程说明:
- 用户下单后,会产生取件任务,该任务也是由调度中心进行调度的
- 订单转运单后,会发送消息到调度中心,在调度中心中对相同节点的运单进行合并(这里是指最小转运单元)
- 调度中心同样也会对派件任务进行调度,用于生成快递员的派件任务
- 司机的出库和入库操作也是流程中的核心动作,尤其是入库操作,是推动运单流转的关键
智能分配快递员
实现分析
消息分析
/*** 订单业务消息,接收到新订单后,根据快递员的负载情况,分配快递员*/
@Slf4j
@Component
public class OrderMQListener {@Resourceprivate CourierFeign courierFeign;@Resourceprivate DispatchConfigurationFeign dispatchConfigurationFeign;@Resourceprivate 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:00long 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:00LocalDateTime date = LocalDateTimeUtil.offset(orderMsg.getEstimatedEndTime(), dispatchTime * -1L, ChronoUnit.MINUTES);//延迟的时间,单位:毫秒 计算: 0.5小时 * 60分钟 * 60秒 * 1000delay = 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 {@Resourceprivate WorkSchedulingFeign workSchedulingFeign;@Resourceprivate ServiceScopeFeign serviceScopeFeign;/*** 条件查询快递员列表(结束取件时间当天快递员有排班)* 如果服务范围内无快递员,或满足服务范围的快递员无排班,则返回该网点所有满足排班的快递员** @param agencyId 网点id* @param longitude 用户地址的经度* @param latitude 用户地址的纬度* @param estimatedEndTime 结束取件时间* @return 快递员id列表*/@Overridepublic 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;}