跑腿小程序智能派单系统全开源:算法骨架与调优实战 简介这份开源资源是面向同城跑腿与校园配送场景的智能派单系统源码基于Fastadmin、ThinkPHP与Uniapp开发包含用户端、骑手端和运营后台三端适合跑腿创业团队、外包开发者及需要私有化部署的技术人员参考。系统支持帮取、帮送两种模式覆盖按距离与重量计价、临时加价、预约取件、跑腿小费、物品保价、地图选点导航等业务能力派单侧提供一键抢单、主动接单、自由开工并可按骑手距离、送货地址与等级进行智能派单同时兼容兼职与全职骑手佣金结算。资源以zip压缩包形式提供整体约64.82MB为无加密全开源代码便于二次开发与私有化落地。目前已有94人学习关注读者可从中获取完整的三端项目结构、派单与计价逻辑实现以及可复用的同城配送技术方案。1. 跑腿小程序智能派单系统从下单到骑手接单这套全开源代码到底能不能跑同城配送和校园跑腿这类业务表面看是「用户下单、骑手接单」两件事真落到代码里最要命的是派单逻辑。我见过太多团队用「先到先得」硬扛结果午高峰骑手扎堆抢近单、远单没人碰用户端显示「已接单」却半小时没人动。这套跑腿小程序智能派单系统全开源包把用户端、骑手端和后台派单引擎都摊开了核心价值就在于它给了一套可改的派单策略骨架而不是一个只能演示的壳子。它适合两类人一类是想快速搭一套同城配送 MVP 的小团队另一类是手里有校园跑腿场景、需要按楼栋和时段调权重的开发者。下面我按「先看懂派单怎么算、再动手把两端跑起来、最后说清楚哪些参数一改就翻车」的顺序拆一遍。2. 智能派单的算法骨架订单池、骑手状态和权重打分怎么串起来派单系统不是「谁先点谁拿单」这么简单。这套代码里派单引擎的核心是一个定时触发的匹配循环把待分配订单放进订单池把在线且空闲的骑手拉进候选集然后对每一对「订单-骑手」算一个综合得分取最高分配。理解这个骨架后面改参数才不会瞎改。2.1 订单池与骑手状态机的数据结构先看订单和骑手在数据库里长什么样。订单表里跟派单强相关的字段有取件坐标、送达坐标、期望送达时间、订单重量、是否加急。骑手表里则是当前坐标、接单上限、当前在手单量、服务范围半径、是否在线。派单引擎每次跑先做一次过滤骑手在线、在手单量小于上限、订单取件点在骑手服务半径内三条同时满足才进候选。-- 订单表关键字段简化 CREATE TABLE order_info ( id BIGINT PRIMARY KEY, pickup_lat DECIMAL(10,6), -- 取件纬度 pickup_lng DECIMAL(10,6), -- 取件经度 deliver_lat DECIMAL(10,6), -- 送达纬度 deliver_lng DECIMAL(10,6), -- 送达经度 expect_time DATETIME, -- 期望送达时间 weight INT DEFAULT 1, -- 重量等级影响骑手载具匹配 urgent TINYINT DEFAULT 0, -- 是否加急 status TINYINT DEFAULT 0 -- 0待派 1已派 2已取 3已送达 ); -- 骑手状态表关键字段 CREATE TABLE rider_status ( rider_id BIGINT PRIMARY KEY, lat DECIMAL(10,6), lng DECIMAL(10,6), online TINYINT DEFAULT 0, max_orders INT DEFAULT 3, -- 同时接单上限 current_orders INT DEFAULT 0, radius INT DEFAULT 3000 -- 服务半径单位米 );这里有个容易忽略的点current_orders不是实时从订单表 count 出来的而是骑手端每次接单/完单后主动上报更新。好处是派单循环不用做聚合查询坏处是骑手端掉线时这个值会失真。常见做法是加一个兜底任务每 5 分钟用订单表实际在手单量校正一次。2.2 权重打分公式与可调参数候选集出来之后对每个「订单-骑手」对算分。这套代码默认的公式是加权求和def score(order, rider): # 距离分取件点距离越近分越高归一化到 0-1 dist haversine(order.pickup_lat, order.pickup_lng, rider.lat, rider.lng) dist_score max(0, 1 - dist / rider.radius) # 负载分在手单越少分越高 load_score 1 - rider.current_orders / rider.max_orders # 时效分期望送达越紧分越高 left (order.expect_time - now()).seconds time_score 1 if left 1800 else max(0, 1 - left / 7200) # 加急加权 urgent_bonus 0.15 if order.urgent else 0 return (0.5 * dist_score 0.3 * load_score 0.2 * time_score urgent_bonus)三个权重 0.5 / 0.3 / 0.2 是默认值分别代表距离优先、负载均衡、时效兜底。校园场景我一般会把距离权重降到 0.35负载提到 0.4因为校园里骑手往往一次带好几单负载不均比距离远更致命。参数说明radius决定候选范围设太小会派不出去设太大会让远单被近骑手抢走max_orders是硬上限超过就不再进候选别指望打分能拦住超载。提示改权重之前先把score的中间值打到日志里跑一天真实数据看分布否则你调完根本不知道是权重起作用还是订单量变了。3. 用户端与骑手端跑起来环境、配置和一次完整下单链路看懂算法只是第一步真正要验证这套代码能不能用得把两端跑起来走一遍「用户下单 → 引擎派单 → 骑手接单 → 状态回传」的完整链路。这一章按我实际部署的顺序写踩过的坑直接标出来。3.1 后端服务与数据库初始化后端一般是 Spring Boot 或 Node 二选一这套包给的是前后端分离结构后端提供 REST 接口派单引擎作为独立模块定时跑。先建库、导表、改配置。# 1. 建库并导入表结构 mysql -uroot -p -e CREATE DATABASE errand DEFAULT CHARSET utf8mb4; mysql -uroot -p errand sql/schema.sql # 2. 导入基础配置数据服务范围、计费规则等 mysql -uroot -p errand sql/init_data.sql # 3. 改后端配置里的数据库和 Redis 连接 vim application.ymlapplication.yml里要重点确认三项数据库连接、Redis 地址派单引擎用 Redis 做订单池的临时队列、派单定时任务的 cron 表达式。cron 默认是每 10 秒跑一次订单量小的校园场景可以放到 5 秒订单量大的同城场景反而要放到 15 秒以上否则数据库压力顶不住。dispatch: cron: */10 * * * * ? # 派单循环触发频率 batch-size: 50 # 单次最多处理订单数 max-retry: 3 # 派单失败重试次数batch-size这个参数很关键。设太小高峰期订单积压设太大单次循环耗时过长骑手位置更新跟不上。我一般按「在线骑手数 × 2」来估50 个骑手就设 100 左右。3.2 用户端下单与骑手端接单的联调两端都是小程序用户端负责下单和查状态骑手端负责上下线、接单、上报位置。联调时最容易卡在 WebSocket 或轮询上——骑手位置要实时传给后端后端派单结果要实时推给骑手端。// 骑手端上报位置小程序端 wx.startLocationUpdate({ success: () { wx.onLocationChange(res { wx.request({ url: ${baseUrl}/rider/location, method: POST, data: { riderId: app.globalData.riderId, lat: res.latitude, lng: res.longitude, timestamp: Date.now() } }) }) } })这段代码的逻辑是开启小程序后台定位位置一变就上报。参数说明riderId必须和登录态绑定别信前端传的任意 ID后端要校验timestamp用来丢弃过期位置网络抖动时可能收到几秒前的位置后端按时间戳只保留最新的。联调时如果骑手端一直收不到派单推送先查后端派单引擎有没有真的把订单状态从 0 改成 1再看推送通道是不是被小程序后台挂起了。注意小程序后台定位在部分机型上会被系统限制测试阶段建议同时保留一个「手动刷新位置」的按钮别把联调卡死在定位权限上。4. 派单参数调优与常见翻车现场排查代码能跑通不代表派单结果合理。这一章是我在实际调这套系统时攒下的踩坑记录每条都按「现象 → 原因 → 解决」写你对着查能省不少时间。4.1 订单派不出去或反复重派现象订单在待派状态停留超过 5 分钟或者同一个订单被反复分配给不同骑手又撤回。原因候选集为空或者打分结果并列导致每次循环选中的骑手不同。候选集为空通常是radius设太小或者骑手online状态没同步反复重派则是订单状态更新和派单循环之间有竞态引擎还没写回状态下一轮又把它捞出来了。解决先查候选集日志确认是过滤太严还是真没骑手给订单加一个「派单锁」用 Redis 的 setnx 在派单前占位处理完再释放避免同一订单被两轮循环同时处理。4.2 骑手端接单后用户端状态不更新现象骑手点了接单骑手端显示成功用户端还停在「等待接单」。原因状态回传走了两个通道——骑手端写数据库、后端推用户端中间任何一步失败都会导致不一致。常见的是推送通道断了但数据库写成功了。解决用户端加一个兜底轮询每 15 秒拉一次订单状态别纯依赖推送。同时后端在写状态成功后发一条 MQ 消息推送服务消费这条消息再推解耦写入和推送。4.3 高峰期派单循环耗时暴涨现象午高峰派单延迟从 2 秒涨到 30 秒以上骑手端接单明显卡顿。原因batch-size设太大单次循环里对每个订单遍历所有骑手算分复杂度是订单数 × 骑手数。100 单 × 200 骑手就是 2 万次距离计算每次都查数据库坐标。解决把骑手坐标缓存到 Redis循环里只读缓存对订单按区域分片每个分片只跟该区域的骑手匹配把笛卡尔积降下来。batch-size回调到 50 以内宁可多跑几轮。4.4 加急订单反而没人接现象加了urgent标记的订单派单得分反而不如普通订单。原因urgent_bonus是固定加 0.15但如果距离分和负载分差距很大0.15 根本拉不回来。比如加急单取件点远、骑手又满载总分还是低。解决加急不应该只做加法应该做乘法或者硬优先级。我一般改成加急订单先单独跑一轮派单只对负载低于 50% 的骑手开放跑完再进普通池。这样加急单不会因为距离被挤掉。4.5 骑手位置漂移导致派单距离算错现象派单显示骑手距离取件点 200 米实际骑手在 2 公里外。原因小程序定位在室内或信号差时会有漂移上报的坐标精度不够haversine算出来的距离自然不准。解决后端对骑手位置做平滑处理连续几个点的坐标取加权平均突变超过 500 米的点直接丢弃。同时给radius留一点余量别卡着实际距离设。5. 把派单引擎接上真实地图与压测验证它到底扛不扛得住代码跑通、参数调完最后一步是验证。我一般分两层先用真实地图接口替换掉代码里的直线距离再用模拟订单压一遍派单循环看它在目标单量下的表现。5.1 用真实路径距离替换直线距离代码默认用haversine算直线距离实际配送走的是路网直线 1 公里可能实际要绕 2 公里。替换成地图路径规划接口后派单准确度会明显提升但接口调用有配额和延迟不能每轮循环对每个「订单-骑手」对都调一次。import requests def road_distance(origin, dest): # 调用路径规划接口返回实际骑行距离米 url https://restapi.amap.com/v3/direction/riding params { origin: f{origin[1]},{origin[0]}, # 经度,纬度 destination: f{dest[1]},{dest[0]}, key: MAP_KEY } resp requests.get(url, paramsparams, timeout2) data resp.json() if data.get(route) and data[route][paths]: return data[route][paths][0][distance] return None # 接口失败时回退到直线距离逻辑说明接口失败或超时返回None调用方回退到haversine保证派单不中断。参数说明timeout设 2 秒别让地图接口拖垮派单循环MAP_KEY走配置中心别硬编码。实际用的时候我会加一层本地缓存同一个「取件点-骑手区域」组合 5 分钟内只调一次把接口调用量压下来。5.2 压测脚本与关键指标压测不用真起几百个骑手端直接往数据库灌模拟骑手和模拟订单然后手动触发派单循环看耗时和分配率。指标含义合格线校园场景单轮派单耗时一次循环处理完所有待派订单的时间 3 秒分配率成功派出的订单 / 待派订单 95%平均接单距离骑手到取件点的实际距离 1.5 公里负载标准差骑手在手单量的离散程度 0.8压测时重点看负载标准差。这个值大说明派单只顾距离不顾均衡高峰期一定有人累死有人闲死。调大负载权重、缩小radius都能降这个值但要和分配率一起看别为了均衡把分配率压下去。# 灌 200 个模拟骑手、500 个模拟订单后触发派单 python scripts/mock_data.py --riders 200 --orders 500 curl -X POST http://localhost:8080/dispatch/trigger从那以后我每次改派单权重都强制先跑一遍这个压测表指标不达标就不上线。这套全开源代码的价值不在于它默认参数多好而在于它把订单池、状态机、打分公式和两端联调都摊开了你能按自己的场景改。希望帮到你。本文还有配套的精品资源点击获取