物流项目第十一期(智能调度之分配快递员)

本项目专栏:

物流项目_Auc23的博客-优快云博客

整体核心业务流程

关键流程说明:

  • 用户下单后,会产生取件任务,该任务也是由调度中心进行调度的
  • 订单转运单后,会发送消息到调度中心,在调度中心中对相同节点的运单进行合并(这里是指最小转运单元)
  • 调度中心同样也会对派件任务进行调度,用于生成快递员的派件任务
  • 司机的出库和入库操作也是流程中的核心动作,尤其是入库操作,是推动运单流转的关键

智能分配快递员 

实现分析

消息分析 

/**
 * 订单业务消息,接收到新订单后,根据快递员的负载情况,分配快递员
 */
@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;
    }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Auc23

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值