跑腿小程序全开源:智能派单与系统派单双模式源码解析
简介:这是一套基于Fastadmin+ThinkPHP与Uniapp开发的全开源同城跑腿系统源码,面向跑腿创业团队、校园配送运营者及需要二次开发的技术人员,覆盖用户端、骑手端与运营后台三端,支持帮取、帮送两种业务模式,可私有化部署且无加密。功能上包含按距离与重量自动计价的规则引擎、夜间或恶劣天气临时加价、预约取件、跑腿小费、物品保价、地图选点与路线导航,并同时提供一键抢单、接单大厅主动接单、自由开工听单,以及依据骑手距离、送货地址与等级进行智能推送的系统派单模式,兼职与全职骑手均可参与佣金结算。资源包共2000个文件,以1079个js、265个html、235个vue、213个json为主,另含142个md说明、58个css样式及sql建表脚本等,压缩包约59.12MB,前后端与数据库结构完整。目前已有247人学习下载,适合作为同城配送项目的落地参考与二次开发基础。
1. 跑腿小程序全开源:一套能跑通智能派单的源码到底长什么样
同城配送和校园跑腿这两年的需求一直在涨,我身边不少做本地生活的朋友都在琢磨自己搭一套系统。但真去搜「小程序源码」,翻出来的要么是只有用户端下单页面的空壳,要么是派单逻辑写死在代码里的半成品,骑手端和调度端根本对不上。这套全开源的跑腿小程序源码,把用户端、骑手端和后台调度三块都放出来了,核心是那套智能派单和系统派单的双模式调度逻辑。它适合谁?想快速验证同城配送商业模式的小团队、需要给校园跑腿项目做二次开发的技术负责人,以及想拆解派单算法怎么落地到实际订单流的后端工程师。预约取件这种非即时场景也覆盖了,不是只能跑外卖那种即时单。
2. 拆开源码包:用户端、骑手端与调度后台的模块划分
2.1 三端目录结构与技术栈对应关系
拿到源码包先别急着往服务器上扔,花十分钟把目录结构捋清楚,后面改代码能省掉大量翻文件的时间。这套项目按端拆得很干净,根目录下通常能看到user-app、rider-app、admin-server和dispatch-core四个主目录。用户端和骑手端都是小程序形态,走的是原生小程序语法加组件化封装,后台调度是独立服务,负责订单池管理和派单计算。
# 典型的目录结构,不同版本可能有细微差异 ├── user-app/ # 用户端小程序:下单、支付、订单追踪 │ ├── pages/ │ │ ├── order/ # 下单流程页 │ │ └── track/ # 骑手位置追踪页 │ └── utils/ │ └── request.js # 统一请求封装,改 baseURL 就在这里 ├── rider-app/ # 骑手端小程序:接单、抢单、状态上报 │ ├── pages/ │ │ ├── hall/ # 接单大厅 │ │ └── route/ # 配送路线页 │ └── utils/ │ └── location.js # 位置上报逻辑,派单精度依赖它 ├── admin-server/ # 后台管理:订单监控、骑手管理、财务对账 │ ├── controllers/ │ └── models/ └── dispatch-core/ # 派单核心:订单池、骑手池、匹配算法 ├── strategy/ # 派单策略,智能派单和系统派单在这里切换 └── config/ └── dispatch.yaml # 派单参数配置文件这个划分的好处是调度逻辑独立成模块,你改派单规则不用动小程序端的代码。dispatch-core里的strategy目录是重点,智能派单和系统派单的差异基本都在这几个文件里。dispatch.yaml是调参入口,后面讲参数怎么设会专门说。
2.2 数据库表设计与订单状态流转
订单状态机是跑腿系统的骨架,状态定义错了后面全是坑。这套源码的订单表设计得比较规范,核心字段包括订单 ID、用户 ID、骑手 ID、订单类型(即时单/预约单)、取件地址、送达地址、预约时间、订单状态和派单模式。状态流转大致是:待支付 → 待派单 → 已派单 → 骑手已接单 → 取件中 → 配送中 → 已送达 → 已完成。预约取件单在「待派单」之前会多一个「待生效」状态,到预约时间前若干分钟才进入派单池。
-- 订单表核心字段(简化版,实际字段更多) CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `rider_id` bigint DEFAULT NULL COMMENT '派单后写入', `order_type` tinyint NOT NULL DEFAULT '1' COMMENT '1即时单 2预约单', `dispatch_mode` tinyint NOT NULL DEFAULT '1' COMMENT '1智能派单 2系统派单', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待支付 1待派单 2已派单...', `appoint_time` datetime DEFAULT NULL COMMENT '预约单生效时间', `pickup_addr` varchar(255) NOT NULL, `deliver_addr` varchar(255) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status_type` (`status`, `order_type`), KEY `idx_rider` (`rider_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;idx_status_type这个联合索引很关键,派单服务轮询待派单订单时会频繁按状态和类型筛选,没这个索引订单一多查询就拖慢整个调度周期。appoint_time字段只在预约单里有值,即时单为 NULL,派单逻辑里靠这个字段区分是否到了可派单时间。
2.3 派单核心模块的入口与配置加载
dispatch-core启动时会先加载dispatch.yaml,把派单模式、骑手筛选半径、最大并行单量这些参数读进内存。入口文件一般叫dispatcher.js或dispatch_service.py,里面有个定时轮询或者消息队列触发的调度循环。每次调度做三件事:从订单池捞待派单订单、从骑手池筛可用骑手、按策略做匹配并写回结果。
// dispatch-core/strategy/index.js 派单策略入口(示意) const config = require('../config/dispatch.yaml'); // 加载派单参数 const smartStrategy = require('./smart'); // 智能派单:多因子评分 const systemStrategy = require('./system'); // 系统派单:规则轮询 async function dispatch(order) { // 根据订单的 dispatch_mode 字段选择策略 const strategy = order.dispatchMode === 1 ? smartStrategy : systemStrategy; const riders = await getAvailableRiders(order, config.radius); // 按半径筛骑手 if (!riders.length) { await pushToRetryQueue(order); // 无可用骑手,进重试队列 return; } const matched = strategy.match(order, riders, config); await assignOrder(order.id, matched.riderId); // 写回骑手 ID }config.radius控制骑手筛选半径,默认值通常在 3 到 5 公里,校园场景可以调小到 1 公里因为骑手密度高。pushToRetryQueue是无骑手可用时的兜底,避免订单卡在待派单状态一直空转。智能派单的match方法内部会算距离、骑手当前负载、历史准时率等因子,系统派单则简单按轮询或先到先得分配。
3. 智能派单与系统派单:两种模式的参数配置与切换
3.1 智能派单的多因子评分逻辑
智能派单不是玄学,本质是把「谁最合适送这单」拆成几个可量化的因子,加权求和后取最高分。这套源码里常见的因子有四个:骑手到取件点的距离、骑手当前在手单量、骑手历史准时率、订单方向与骑手当前行进方向的一致性。每个因子归一化后乘权重,权重在dispatch.yaml里配。
# dispatch-core/config/dispatch.yaml 派单参数配置 dispatch: mode: 1 # 1智能派单 2系统派单,全局默认 radius: 3000 # 骑手筛选半径,单位米 max_parallel_orders: 5 # 骑手最大并行单量 smart: weight_distance: 0.4 # 距离权重,越高越倾向就近派单 weight_load: 0.3 # 负载权重,越高越倾向给空闲骑手 weight_punctuality: 0.2 # 准时率权重 weight_direction: 0.1 # 方向一致性权重 system: assign_rule: round_robin # 系统派单规则:round_robin 或 first_come retry: interval: 30 # 无骑手可用时重试间隔,单位秒 max_retry: 10 # 最大重试次数,超过转人工weight_distance设 0.4 意味着距离是主导因素,这适合订单密度均匀的城区。校园跑腿场景骑手都在校内,距离差异不大,可以把weight_load提到 0.4 让单量分配更均匀。max_parallel_orders设 5 是经验值,设太高骑手容易超时,设太低运力浪费。retry.interval和max_retry配合用,30 秒重试一次、最多 10 次,超过就转人工调度,避免订单无限期挂着。
3.2 系统派单的规则轮询实现
系统派单比智能派单简单,适合骑手数量少、订单量稳定的场景,比如固定几个骑手跑一个校园。它的逻辑是按规则轮流分配,不计算复杂评分。round_robin模式维护一个骑手队列,每来一单分配给队首骑手然后该骑手移到队尾;first_come模式则是谁先上报空闲就派给谁。
// dispatch-core/strategy/system.js 系统派单实现(示意) let riderQueue = []; // 轮询队列 function roundRobinMatch(order, riders) { // 过滤掉已达最大并行单量的骑手 const available = riders.filter(r => r.currentLoad < config.max_parallel_orders); if (!available.length) return null; // 取队首骑手,分配后移到队尾 const picked = available.shift(); available.push(picked); riderQueue = available; // 更新队列 return { riderId: picked.id }; } function firstComeMatch(order, riders) { // 按骑手最后空闲上报时间排序,取最早的 const sorted = riders .filter(r => r.currentLoad < config.max_parallel_orders) .sort((a, b) => a.freeReportTime - b.freeReportTime); return sorted.length ? { riderId: sorted[0].id } : null; }currentLoad是骑手当前在手单量,每次派单前都要从骑手端同步最新值,否则会超派。freeReportTime依赖骑手端定时上报空闲状态,上报间隔太长会导致first_come不准。系统派单的优点是公平、可预测,缺点是没法应对骑手位置和订单方向的动态变化,订单高峰期容易出现「近的骑手没单、远的骑手跑断腿」。
3.3 两种模式的切换时机与混合使用
实际运营中很少从头到尾只用一种模式。常见做法是高峰期用智能派单提效率,平峰期用系统派单保公平。切换可以在dispatch.yaml里改全局mode,也可以按订单类型或区域动态切。比如校园跑腿的预约取件单,因为时间确定、骑手可提前规划,用系统派单轮询反而更稳;即时单用智能派单抢时效。
// 按订单类型动态选择派单模式的示例 function selectStrategy(order) { // 预约单用系统派单,即时单用智能派单 if (order.orderType === 2) { return systemStrategy; } // 高峰期(11:00-13:00, 17:00-19:00)用智能派单 const hour = new Date().getHours(); if ((hour >= 11 && hour < 13) || (hour >= 17 && hour < 19)) { return smartStrategy; } return systemStrategy; // 平峰期系统派单 }这个动态选择逻辑可以放在dispatch-core的入口处,也可以做成配置项让运营在后台改。注意切换模式时骑手端的接单大厅展示逻辑要跟着变,智能派单下骑手看到的是系统推来的单,系统派单下可能需要骑手主动抢单,两端逻辑要对齐,不然会出现骑手看不到单的情况。
4. 本地跑起来:环境搭建、依赖安装与首单验证
4.1 后端服务与数据库初始化
先把后端跑通再碰小程序端,这样接口调试有目标。后端一般是 Node.js 或 Java 服务,依赖装完后要初始化数据库。源码包里通常带一个init.sql或者 migration 脚本,导入后建表并插入测试数据。
# 后端服务启动流程(以 Node.js 为例) cd admin-server npm install # 安装依赖 cp .env.example .env # 复制环境变量模板 # 编辑 .env,填入数据库连接、Redis 地址、小程序 AppID 等 mysql -u root -p running_errand < init.sql # 导入建表脚本 npm run dev # 启动开发服务,默认监听 3000 端口.env里几个必须改的项:数据库 host/port/user/password、Redis 地址(派单队列和骑手位置缓存依赖它)、小程序 AppID 和 Secret(用户端登录用)。init.sql导入后检查orders、riders、users三张表是否都有数据,测试数据能帮你快速验证派单流程。npm run dev启动后访问http://localhost:3000/health看服务是否正常,返回 ok 再往下走。
4.2 小程序端配置与真机调试
用户端和骑手端都是小程序,用开发者工具打开对应目录,改request.js里的baseURL指向你本地或测试服务器的地址。本地调试时小程序开发者工具需要勾选「不校验合法域名」,否则请求本地 IP 会被拦。
// user-app/utils/request.js 请求配置 const BASE_URL = 'http://192.168.1.100:3000/api'; // 改成你的后端地址 // 真机调试时不能用 localhost,要用局域网 IP function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data, header: { 'Authorization': wx.getStorageSync('token') }, success: res => res.statusCode === 200 ? resolve(res.data) : reject(res), fail: reject }); }); }BASE_URL真机调试必须用局域网 IP,localhost在手机上指向手机自己。Authorization头带的是登录后存的 token,没登录时为空,后端要放行登录和注册接口。两个小程序端的BASE_URL都要改,改完先跑用户端下单流程,再跑骑手端接单流程。
4.3 首单全流程验证与派单日志查看
验证分四步:用户端下单、后台看到待派单订单、派单服务触发、骑手端收到单。每步都有对应的日志或数据变化可以确认。
# 查看派单服务日志,确认派单是否触发 tail -f logs/dispatch.log # 正常派单成功会输出类似: # [dispatch] order=1001 mode=smart riders=8 matched=rider_003 score=0.87 # [dispatch] order=1001 assigned to rider_003 # 直接查数据库确认订单状态和骑手绑定 mysql -u root -p -e "SELECT id, status, rider_id, dispatch_mode FROM orders WHERE id=1001;"dispatch.log里matched=rider_003 score=0.87说明智能派单算出了最高分骑手并分配成功。如果日志里只有riders=0说明筛选半径内没骑手,检查骑手端是否上报了位置、radius是否设得太小。数据库里status应该从 1(待派单)变成 2(已派单),rider_id从 NULL 变成具体骑手 ID。首单跑通后,再测预约取件单,确认appoint_time到了才进派单池。
5. 派单不准、订单卡住、骑手收不到单:排查清单
5.1 订单一直停在待派单状态
现象是用户下了单,后台也能看到,但状态一直不变成已派单,骑手端没反应。原因通常是派单服务没启动、或者订单没进派单池。先看dispatch-core进程在不在,再看订单的status是不是 1、appoint_time是不是已经过了。预约单如果appoint_time设成了未来时间,派单服务会跳过它,这是正常的,但如果是即时单还卡着,就是服务问题。解决方法是重启派单服务,并检查dispatch.yaml里的retry配置,max_retry设太小会导致重试几次就放弃。
5.2 智能派单总是派给同一个骑手
现象是明明有多个骑手在线,订单却集中派给某一个人。原因是评分因子里weight_load权重太低,或者骑手的currentLoad没实时更新。智能派单算分时如果负载因子不生效,距离最近的骑手会一直赢。解决方法是把weight_load从 0.3 提到 0.4 以上,并检查骑手端上报currentLoad的频率,建议每完成一单或每 60 秒上报一次。另外确认max_parallel_orders没设得过大,设成 10 的话骑手接满之前都不会被过滤。
5.3 骑手端接单大厅空白
现象是骑手登录后接单大厅没有订单展示。原因可能是派单模式与骑手端展示逻辑不匹配。智能派单下订单是系统推给指定骑手的,骑手端应该展示「待接单」列表;系统派单下如果是抢单模式,骑手端展示的是「可抢单」池。如果后端配的是智能派单但骑手端按抢单池去拉数据,自然拉不到。解决方法是检查dispatch.yaml的mode和骑手端请求的接口是否对应,智能派单对应/api/rider/pushed-orders,系统派单对应/api/rider/available-orders。
5.4 预约取件单提前被派出去
现象是用户约了明天上午取件,结果今天骑手就收到单了。原因是appoint_time的判断逻辑没生效,或者时区处理有问题。派单服务捞单时应该加条件appoint_time IS NULL OR appoint_time <= NOW() + INTERVAL 30 MINUTE,只提前半小时把预约单放进池子。如果服务器时区和数据库时区不一致,NOW()算出来的时间会偏,导致提前派单。解决方法是统一用 UTC 时间存储,展示时再转本地时区,并在派单查询里显式加时间条件。
5.5 派单日志报骑手池为空但骑手在线
现象是日志显示riders=0,但骑手端明明显示在线。原因是骑手位置上报的坐标格式或坐标系不匹配。小程序端wx.getLocation默认返回 gcj02 坐标系,如果后端按 wgs84 去算距离,或者骑手位置压根没写进 Redis,筛选半径内就查不到人。解决方法是确认骑手端上报位置时带了坐标,后端存 Redis 的 key 没过期,以及距离计算用的坐标系和上报的一致。校园场景还要注意围栏设置,骑手如果被围栏挡在外面也会筛不到。
6. 派单权重调优:用历史订单回放验证参数改得对不对
参数改完不能直接上生产,得先验证。我一般会拿历史订单做回放,把过去一周的订单按时间顺序重新喂给派单服务,看新参数下的派单结果和实际结果的差异。这套源码的dispatch-core支持回放模式,在dispatch.yaml里把replay.enabled设成 true,指定订单时间范围就行。
# 回放模式配置 replay: enabled: true start_time: "2024-01-01 00:00:00" end_time: "2024-01-07 23:59:59" output: "logs/replay_result.csv" # 回放结果输出,含每单派给谁、评分多少回放跑完后看replay_result.csv,重点对比三个指标:平均派单距离、骑手单量标准差、预估超时率。平均派单距离降了说明weight_distance调对了;单量标准差降了说明weight_load起作用了;预估超时率要结合骑手历史准时率算,如果超时率升了说明权重调得太激进。
# 分析回放结果的简单脚本 import pandas as pd df = pd.read_csv('logs/replay_result.csv') # 平均派单距离(米) avg_distance = df['dispatch_distance'].mean() # 骑手单量标准差,越小越均衡 load_std = df.groupby('rider_id')['order_id'].count().std() # 预估超时率:派单距离 / 骑手历史均速 > 承诺时效 算超时 df['est_time'] = df['dispatch_distance'] / df['rider_avg_speed'] timeout_rate = (df['est_time'] > df['promise_minutes'] * 60).mean() print(f"平均派单距离: {avg_distance:.0f}m") print(f"骑手单量标准差: {load_std:.2f}") print(f"预估超时率: {timeout_rate:.2%}")这个脚本跑完,如果平均距离和超时率都降了、单量标准差没升太多,参数就可以上。如果超时率降了但单量标准差飙了,说明骑手之间分配太不均,得把weight_load往回拉一点。调参是个来回试的过程,别指望一次到位。
血泪经验是回放用的骑手历史均速要按骑手分别算,不能用全局平均值,否则预估超时率会失真。另外回放时要把retry逻辑也打开,不然无骑手可用的订单会被跳过,统计出来的超时率偏低。从那以后我每次改派单参数都强制走一遍回放,确认三个指标都没恶化才敢推到线上。希望这套源码和调参思路能帮到你。
本文还有配套的精品资源,点击获取