本地外卖运力调度模块开发:O2O配送订单分配逻辑实现
本地外卖平台业务中,运力调度模块是连接订单与骑手的核心组件。单纯实现下单、支付功能无法保证履约体验,合理的订单分配逻辑,能够解决骑手匹配、抢单/自动派单切换、订单兜底、并发冲突等问题,保障订单可以高效流转完成配送。
本文将从调度整体架构、数据表设计、抢单与自动派单逻辑、状态流转、地图位置处理、冲突控制、统计指标、技术选型、扩展方向讲解运力调度模块开发实现。
一、运力调度模块整体架构设计
调度模块与用户、商户、骑手、支付模块解耦,采用异步任务处理订单分配。整体业务端包含用户小程序、商家端、骑手端、平台管理后台,全部业务通过API层访问调度服务;骑手实时位置存储在Redis,降低数据库查询压力。
1. 用户端小程序
- 下单支付,查看订单调度、配送状态
2. 商家管理端
- 接单之后,订单流入调度池
- 查看订单分配骑手信息
3. 骑手配送端
- 接收系统派单推送,参与抢单
- 更新取餐、配送、送达履约状态
4. 平台管理后台
- 调度模式配置(自动派单/抢单模式)
- 订单调度监控、人工改派异常订单
- 调度成功率、超时率等指标查看
整体架构示意:
用户小程序
|
商家端 ---- API服务层 ——【调度服务】 ---- 管理后台
|
骑手端
|
MySQL(业务数据) + Redis(骑手位置、抢单锁、调度缓存)
二、调度相关数据表设计
订单表增加调度相关字段,同时维护骑手表,记录骑手在线、负载、位置信息。
订单表:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT COMMENT '用户ID',
shop_id INT COMMENT '商家ID',
rider_id INT COMMENT '分配骑手ID',
goods_amount DECIMAL(10,2),
amount DECIMAL(10,2),
status TINYINT COMMENT '订单主状态',
dispatch_status TINYINT COMMENT '调度子状态',
shop_lat DECIMAL(10,6) COMMENT '商家纬度',
shop_lng DECIMAL(10,6) COMMENT '商家经度',
user_lat DECIMAL(10,6) COMMENT '用户收货纬度',
user_lng DECIMAL(10,6) COMMENT '用户收货经度',
create_time DATETIME
);
骑手信息表:
CREATE TABLE rider (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
phone VARCHAR(20),
is_online TINYINT DEFAULT 0 COMMENT '0离线 1在线',
max_load INT DEFAULT 3 COMMENT '最大同时承接订单数',
current_load INT DEFAULT 0 COMMENT '当前手中订单数量',
avg_score DECIMAL(3,2) COMMENT '骑手服务评分',
status TINYINT DEFAULT 1 COMMENT '账号状态'
);
调度状态常量定义(Node.js)
const dispatchStatus = {
WAIT_DISPATCH:1, //待调度,商家已接单
AUTO_ASSIGN:2, //自动派单处理中
GRAB_POOL:3, //进入抢单池
RIDER_ACCEPT:4, //骑手已接单
DELIVERING:5, //配送中
FINISH:6, //订单已完成
MANUAL_INTERVENE:7 //需要人工介入处理
}
三、抢单模式功能实现
抢单模式适合骑手数量不多的中小平台,订单进入抢单池,周边在线骑手可以主动接单;使用Redis分布式锁,防止多个骑手同时抢到同一个订单。
骑手抢单接口示例:
router.post('/rider/grabOrder',async(req,res)=>{
const {
orderId,riderId}=req.body;
//分布式锁,防止并发抢单冲突
const lockKey = `grab:order:${
orderId}`;
const lockResult = await redis.set(lockKey,riderId,'NX','EX',30);
if(!lockResult){
return res.json({
code:500,message:"订单已被其他骑手抢占"});
}
try{
const orderInfo = await Order.findByPk(orderId);
if(orderInfo.dispatch_status !== dispatchStatus.GRAB_POOL){
await redis.del(lockKey);
return res.json({
code:500,message:"该订单不允许抢单"});
}
//校验骑手在线、当前负载未超限
const riderInfo = await Rider.findByPk(riderId);
if(riderInfo.is_online!==1 || riderInfo.current_load >= riderInfo.max_load){
await redis.del(lockKey);
return res.json({
code:500,message:"骑手不可接单"});
}
//更新订单调度状态,绑定骑手
await Order.update({
rider_id:riderId,
dispatch_status:dispatchStatus.RIDER_ACCEPT
},{
where:{
id:orderId}});
//骑手负载+1
await Rider.increment('current_load',{
where:{
id:riderId}});
return res.json({
code:200,message:"抢单成功"});
}catch (e){
await redis.del(lockKey);
return res.json({
code:500,message:"抢单异常"});
}
})

四、自动派单核心逻辑实现
自动派单模式,订单商家接单完成之后进入调度;利用Redis GEO检索商家周边在线骑手,综合距离、骑手负载、服务评分做加权筛选;没有合适骑手时自动降级流转到抢单池。
/**
* 自动派单主逻辑
* @param {Number} orderId 订单ID
*/
async function autoDispatchOrder(orderId){
const orderInfo = await Order.findByPk(orderId);
const {
shop_lat,shop_lng} = orderInfo;
//从Redis获取商家周边在线骑手列表
const nearRiderList = await getNearOnlineRider(shop_lat,shop_lng);
if(nearRiderList.length === 0){
//无匹配骑手,降级进入抢单池
await Order.update({
dispatch_status:dispatchStatus.GRAB_POOL},{
where:{
id:orderId}});
return false;
}
//加权排序:距离、负载、服务评分综合打分
const sortedRider = calcRiderScoreSort(nearRiderList);
const targetRider = sortedRider[0];
await Order.update({
rider_id:targetRider.id,
dispatch_status:dispatchStatus.RIDER_ACCEPT
},{
where:{
id:orderId}});
await Rider.increment('current_load',{
where:{
id:targetRider.id}});
//推送消息通知骑手
await pushRiderNotice(targetRider.id,orderId);
return true;
}
业务说明:实际项目可增加延迟队列,自动派单超时未接单,自动流转抢单池;全部无人承接标记为人工介入,平台运营手动分配骑手。
骑手接单确认接口:
router.post('/rider/acceptDispatch',async(req,res)=>{
let {
orderId,riderId}=req.body;
await Order.update({
dispatch_status:dispatchStatus.RIDER_ACCEPT
},{
where:{
id:orderId,rider_id:riderId}});
res.json({
message:"接单成功"});
})
完整订单调度流转:
商家接单
|
待调度
|
【自动派单】→匹配骑手成功→骑手已接单→配送中→已完成
↓(无骑手/超时)
【降级】进入抢单池
↓(抢单成功)骑手已接单→配送中→已完成
↓(全部无人接单)标记人工介入
五、骑手位置与地图能力
调度依赖骑手实时位置,骑手端定时上报经纬度写入Redis GEO,不频繁写MySQL数据库。
获取周边骑手示例函数:
async function getNearOnlineRider(lat,lng){
//redis GEO查询指定半径范围内骑手
const result = await redis.georadius("rider:geo",lng,lat,5,'KM','WITHCOORD');
return result;
}
//路线规划接口
function getDeliveryRoute(start,end){
return axios.get(
"[https://api.map.com/direction](https://api.map.com/direction)",
{
params:{
origin:start,
destination:end
}
}
)
}
六、调度相关后台数据统计功能
平台需要统计调度成功率、抢单占比、订单人工介入数量,评估运力健康状态。
统计每日订单与调度成功率SQL:
SELECT
DATE(create_time) AS day,
COUNT(*) AS total_order,
SUM(IF(dispatch_status in (4,5,6),1,0)) AS dispatch_success
FROM orders
GROUP BY day;
统计需要人工介入订单:
SELECT COUNT(*) AS manual_count
FROM orders
WHERE dispatch_status =7;
骑手维度统计接单与完成量:
SELECT rider_id,COUNT(id) AS accept_num
FROM orders
WHERE dispatch_status IN (4,5,6)
GROUP BY rider_id;
通过统计数据,运营人员可以掌握平台运力充足程度,判断是否需要补充骑手资源。
七、开发技术选型
小程序端
- 微信小程序原生开发
- UniApp
- Taro
后端
- Java Spring Boot
- Node.js
- PHP Laravel
数据库&缓存
- MySQL:存储订单、骑手、调度记录
- Redis:GEO地理位置、分布式抢单锁、骑手在线状态缓存
- 消息队列:实现异步调度任务,削峰处理高峰订单
服务部署
- Linux服务器
- Nginx
- Docker
SpringBoot项目目录参考:
takeout‑system
├── user 用户模块
├── shop 商家模块
├── order 订单基础模块
├── dispatch 运力调度核心模块
├── rider 骑手管理模块
├── payment 支付模块
└── admin 平台后台
八、后期扩展方向
基础调度模块开发完成之后,可迭代增强调度能力。
调度算法增强
- 多维度加权派单(距离、负载、评分、顺路度)
- 拒单、转单完整业务流程
- 延迟队列处理派单超时、订单超时
业务策略扩展
- 分片区调度,限定骑手服务区域
- 高峰时段调度参数动态调整
- 异常订单人工改派操作日志留存
骑手业务扩展
- 骑手上下班、小休状态管理
- 骑手接单上限动态配置
- 骑手接单拒单行为统计
数据运营
- 调度成功率报表
- 骑手接单、超时、拒单数据分析
- 订单积压告警监控
总结
本地外卖运力调度模块,核心不是简单找到距离最近骑手,而是构建一套完整订单分配状态机,兼顾自动派单、抢单模式、降级兜底,同时处理并发抢单冲突。
开发时要注意业务解耦,使用Redis处理地理位置、分布式锁,消息队列异步处理调度任务,减轻数据库压力。在此基础上可以持续迭代加权派单、片区管理、转单改派能力,适配不同规模的本地外卖平台运力场景。