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

article/2025/7/27 18:46:59

本项目专栏:

物流项目_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;}


http://www.hkcw.cn/article/EusGaeEMUN.shtml

相关文章

React 项目中封装 Excel 导入导出组件:技术分享与实践

文章目录 前言一、为什么需要封装 Excel 组件&#xff1f;二、技术选型三、核心实现1. 安装依赖2. 封装Excel导出3. 封装导入组件 &#xff08;UploadExcel&#xff09; 总结 前言 在 React 项目中&#xff0c;处理 Excel 文件的导入和导出是常见的业务需求。无论是导出报表数…

用calibredrv提取版图中指定类型cell,保留位置信息并输出新的gds

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 现在有一个gds,其中的bump位置信息是我们需要的,如何从现有的gds中提取我们需要的部分呢? 需要用到工具calibredrv,如果数量少,可以用图形界面操作,方法如下: 01 打开gds calibredrv -m inp…

iOS 使用CocoaPods 添加Alamofire 提示错误的问题

Sandbox: rsync(59817) deny(1) file-write-create /Users/aaa/Library/Developer/Xcode/DerivedData/myApp-bpwnzikesjzmbadkbokxllvexrrl/Build/Products/Debug-iphoneos/myApp.app/Frameworks/Alamofire.framework/Alamofire.bundle把这个改成 no 2 设置配置文件

Python基本运算符

White graces&#xff1a;个人主页 &#x1f439;今日诗词:相恨不如潮有信&#xff0c;相思始觉海非深&#x1f439; ⛳️点赞 ☀️收藏⭐️关注&#x1f4ac;卑微小博主&#x1f64f; ⛳️点赞 ☀️收藏⭐️关注&#x1f4ac;卑微小博主&#x1f64f; 目录 &#x1f9ee; Pyt…

nginx: [emerg] bind() to 0.0.0.0:80 failed (10013: 80端口被占用

Nginx启动报错&#xff1a;nginx: [emerg] bind() to 0.0.0.0:80 failed (10013: An attempt was made to access a socket in a way forbidden by its access permissions) 这个报错代表80端口被占用 先查看占用80的端口 netstat -aon | findstr :80 把它杀掉&#xff0c;强…

vscode命令行debug

vscode命令行debug 一般命令行debug会在远程连服务器的时候用上&#xff0c;命令行debug的本质是在执行时暴露一个监听端口&#xff0c;通过进入这个端口&#xff0c;像本地调试一样进行。 这里提供两种方式&#xff1a; 直接在命令行中添加debugpy&#xff0c;适用于python…

(笔记+作业)第五期书生大模型实战营---L1G2000 OpenCompass 评测书生大模型实践

学员闯关手册&#xff1a;https://aicarrier.feishu.cn/wiki/QdhEwaIINietCak3Y1dcdbLJn3e 课程视频&#xff1a;https://www.bilibili.com/video/BV13U1VYmEUr/ 课程文档&#xff1a;https://github.com/InternLM/Tutorial/tree/camp4/docs/L0/Python 关卡作业&#xff1a;htt…

激光雷达的强度像和距离像误差与噪声分析(1)2025.5.30

激光雷达的强度像和距离像在测量过程中可能受到多种误差和噪声的影响&#xff0c;这些因素既包括硬件本身的物理特性&#xff0c;也涉及环境条件和算法处理等外部因素。以下是主要误差类型、噪声来源及其关键影响因素的综合分析&#xff1a; 一、强度像的误差与噪声 能量信号…

uboot移植之IOMUX介绍

本章节主要讲&#xff0c;如何将NXP官方i.MX6ULL EVK评估板的uboot源码移植适配到ELF 1开发板。本身uboot的作用就是启动内核&#xff0c;只要能成功启动内核&#xff0c;uboot使命便已完成。但是从开发调试的角度来讲&#xff0c;有时候我们需要在uboot阶段使用一些外设接口方…

3DMAX+Photoshop教程:将树木和人物添加到户外建筑场景中的方法

在本教程中&#xff0c;我将向您展示如何制作室外场景。我不会详细解释每一个细节&#xff0c;而是想快速概述一下我的方法。 在本教程中&#xff0c;我使用了一个相对简单的3D模型&#xff0c;并向您展示了在一些高质量纹理的帮助下可以做什么。此外&#xff0c;我将向您展示…

n8n 中文系列教程_25.在n8n中调用外部Python库

在n8n中使用Python处理复杂任务时&#xff0c;内置的Code节点由于运行在沙盒环境中&#xff0c;无法直接调用外部Python库&#xff08;如pandas、requests等&#xff09;&#xff0c;限制了工作流的扩展能力。本文将介绍一种持久化解决方案&#xff1a;通过Docker挂载目录虚拟环…

STM32单片机简介

1.基本情况 STM32单片机正如其名是32位微控制器&#xff0c;相较于51单片机的8位微控制器&#xff0c;性能会更好&#xff0c;但学习难度也会提高。 在stm32单片机中内核时核心部分&#xff0c;是ARM公司设计的&#xff0c;其在stm32单片机中占据极为重要的地位。(程序指令的…

安全帽目标检测

安全帽数据集 这里我们使用的安全帽数据集是HelmentDetection&#xff0c;这是一个公开数据集&#xff0c;里面包含5000张voc标注格式的图像&#xff0c;分为三个类别&#xff0c;分别是 0: head 1: helmet 2: person 安全帽数据集下载地址、 我们将数据集下载后&#xff0c…

气镇阀是什么?

01、阀门介绍&#xff1a; 油封机械真空泵的压缩室上开一小孔&#xff0c;并装上调节阀&#xff0c;当打开阀并调节入气量&#xff0c;转子转到某一位置&#xff0c;空气就通过此孔掺入压缩室以降低压缩比&#xff0c;从而使大部分蒸汽不致凝结而和掺入的气体一起被排除泵外起此…

1,QT的编译教程

目录 整体流程&#xff1a; 1&#xff0c;新建project文件 2,编写源代码 3&#xff0c;打开QT的命令行窗口 4&#xff0c;生成工程文件&#xff08;QT_demo.pro&#xff09; 5&#xff0c;生成Make file 6&#xff0c;编译工程 7&#xff0c;运行编译好的可执行文件 整体…

Linux操作系统 使用共享内存实现进程通信和同步

共享内存使用 //main.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <sys/shm.h> #include <string.h> int main() {int shmidshmget((key_t)1234,256,IPC_CREAT|0600);assert(shmid!-1);…

力扣HOT100之动态规划:322. 零钱兑换

这道题和上一道题279.完全平方数的套路是完全一样的&#xff0c;但是这道题不需要我们自己生成物品列表&#xff0c;函数的输入中已经给出了&#xff0c;但是这道题有一个坑&#xff0c;就是我们在初始化dp数组的时候&#xff0c;所有的位置不应该赋值为INT_MAX&#xff0c;因为…

工厂方法模式(Factory Method)深度解析:从原理到实战优化

作者简介 我是摘星&#xff0c;一名全栈开发者&#xff0c;专注 Java后端开发、AI工程化 与 云计算架构 领域&#xff0c;擅长Python技术栈。热衷于探索前沿技术&#xff0c;包括大模型应用、云原生解决方案及自动化工具开发。日常深耕技术实践&#xff0c;乐于分享实战经验与…

π0-FAST-针对VLA模型的高效动作token化技术-2025.1.16-开源

0. 前言 2025年2月4日&#xff0c;π0 和 π0-FAST 一并开源&#xff0c;这个系列许多研究者、企业人士认为落地潜力很大 项目页 论文页 GitHub页 之前已经做了 π0 论文的详解&#xff1a;π0-通用VLA模型-2024.11.13-开源 本文来详解一下 π0-FAST 1. π0-FAST&#xff1…

正点原子Z20 ZYNQ ​​​开发板​​发布!板载FMC LPC、LVDS LCD和WIFI蓝牙等接口,资料丰富!

正点原子Z20 ZYNQ ​​​开发板​​发布&#xff01;板载FMC LPC、LVDS LCD和WIFI&蓝牙等接口&#xff0c;资料丰富&#xff01; 正点原子新品Z20 ZYNQ开发板来啦&#xff01;核心板全工业级设计&#xff0c;主控芯片型号是XC7Z020CLG484-2I。开发板由核心板底板组成&…