简介:一套基于FastAdmin、ThinkPHP与uniapp开发的全开源同城跑腿小程序系统,主要面向需要快速搭建同城配送、校园跑腿、预约取件等业务的技术团队与独立开发者。系统包含用户端、骑手端与运营后台三端,支持智能派单、系统派单、一键接单、抢单等完整流程,可帮助团队低成本实现跑腿业务的私有化部署与二次开发。资源包共2000个文件,以1080个js、235个vue、265个html、213个json为主要构成,覆盖前后端业务逻辑、页面结构与配置数据,另有142个md文档用于说明与部署指引,整体压缩包约43.26MB,结构清晰便于检索。目前已有646人学习下载,适合具备一定PHP与Vue基础的开发者参考使用。由于源码无加密,使用者可自由修改与分发,并针对校园、社区或商圈等不同场景定制派单规则与配送流程。运营后台还提供订单管理、骑手管理、用户管理、财务统计等功能,兼顾业务运转与日常维护需求,是一款实用性较强的开源跑腿系统方案。
1. 为什么跑腿系统最终要落到智能派单而不是抢单
跑腿业务最难受的环节不是缺单,而是订单密度上来之后,骑手全在抢好送的订单,没人接爬楼和偏远区域的单。早期版本用纯抢单模式,结果校园场景里 5 楼以下订单响应时间平均 3 分钟,5 楼以上直接没人接,最后只能靠运营人工打电话。这套基于 FastAdmin + ThinkPHP + uniapp 的优创同城跑腿系统,核心看点是把「用户端 + 骑手端 + 运营后台」完整拆开,同时提供智能派单和系统派单两套调度逻辑,支持帮取、帮送、预约取件。对想私有化部署并二次开发的团队来说,无加密 PHP 源码和 uniapp 前端意味着从下单到派单、从支付到结算的整条链路都能改得动,而不是被 SaaS 平台绑死。本文拆解它的订单状态机、派单策略、后台权限和部署环节的关键参数,适合正在评估跑腿系统底层实现的开发者本人,也适合准备从抢单模式切换到系统派单的运营方。
2. 用户端与骑手端的订单流:从下单到完成的链路实现
2.1 帮取/帮送两种模式的订单状态机
跑腿订单的核心不是支付,而是状态流转。帮取和帮送在业务上差一个「取件」动作,但状态机必须分开设计,否则骑手端的「已取件」按钮在帮送模式下会变成一个无效操作。这套系统里,我把状态机拆成两条主线:
| 状态 | 帮送模式 | 帮取模式 |
|---|---|---|
| 待支付 | 用户在用户端提交配送地址和货物类型 | 用户提交取件地址和送达地址 |
| 待接单 | 支付完成后进入派单池 | 支付完成后进入派单池 |
| 已接单 | 骑手接单,系统锁定订单 | 骑手接单,系统锁定订单 |
| 已取件 | 骑手到达寄件点取货 | 骑手到达取件点取货 |
| 配送中 | 骑手开始配送 | 同左 |
| 已完成 | 用户确认或系统自动完成 | 同左 |
| 已取消 | 支付前用户取消,或超时未接单 | 同左 |
从 FastAdmin 后台看,订单表的order_type字段区分help_take和help_send,status字段保存上面状态。前端 uniapp 页面拿到状态值后直接映射按钮文案和操作权限,而不是在页面里写多个if判断业务逻辑。我一般会把状态机常量单独放在common/constant/OrderStatus.php,避免后端和前端各维护一套字符串。
2.2 uniapp 跨端页面与接口层封装
用户端和骑手端在代码库上不是两个独立项目,而是同一个 uniapp 工程里用角色标识区分入口。常见的做法是在App.vue里根据登录接口返回的user_type跳转到不同的 tabBar,但这样会在小程序冷启动时多一次白屏等待。更好的做法是给用户端和骑手端分别创建独立的pages.json,通过编译条件加载:
// 用户端 pages.json 片段 { "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "同城跑腿", "navigationBarBackgroundColor": "#ff6b35", "navigationBarTextStyle": "white" } }, { "path": "pages/order/submit", "style": { "navigationBarTitleText": "发布订单", "navigationBarTextStyle": "black" } } ] }这段配置的作用是让微信小程序编译时只打包用户端页面,骑手端页面放在另一个pages-rider.json中,用uni-app的subPackages拆包加载。这样既能降低主包体积,也能避免骑手端代码暴露给普通用户。实际开发里还要注意navigationBarTitleText的静态限制:微信小程序不支持运行时通过uni.setNavigationBarTitle设置超过 8 个中文字的标题,所以订单编号这类信息不要直接拼到标题里,而是放在页面内部展示。
2.3 微信支付 v3 对接与回调幂等
支付环节是跑腿系统里最容易踩坑的位置。这个项目走的是微信支付 v3 接口,和旧的 v2 相比,关键变化是证书认证方式从MD5 签名变成了RSA 签名 + 平台证书验签。在 ThinkPHP 里,我会用官方 SDKwechatpay-php,但要注意回调地址必须能处理重复通知。写一个幂等检查:
public function notify() { // 微信支付 v3 回调 $decrypt = $this->payment->decrypt($GLOBALS['HTTP_RAW_POST_DATA'] ?? ''); $orderNo = $decrypt['out_trade_no']; $transactionId = $decrypt['transaction_id']; // 幂等检查:已处理的订单直接返回成功,避免重复改状态 $order = OrderModel::where('order_no', $orderNo)->find(); if ($order && $order['pay_status'] == 1) { return json(['code' => 'SUCCESS', 'message' => 'OK']); } // 更新订单支付状态后,再触发派单逻辑 $order->pay_status = 1; $order->pay_time = time(); $order->save(); dispatch_order($order->id); }这里强制在更新订单状态之前查询一次pay_status,是为了防止微信在弱网环境下对同一笔订单推送多条回调时,派单任务被执行两次。实际线上环境还要把transactionId存到订单流水表,方便对账。
2.4 骑手端抢单与一键接单的消息推送
骑手端在 uniapp 里监听实时订单使用 WebSocket 或定时轮询两种方式。这套系统没有内置即时通讯服务,所以常见的做法是骑手端小程序通过uni.connectSocket连接后台的think-worker服务。后台派单时向指定骑手推送order.assign事件,前端拿到事件后播放震动并弹窗。但要注意:微信小程序在后台运行时会挂起 WebSocket,所以还要配合订阅消息作为兜底。
一键接单的核心接口很简单,但并发控制必须做。用 ThinkPHP 的Db::transaction+ 行锁防止两个骑手同时抢同一单:
public function grab() { $orderId = input('order_id'); $riderId = $this->riderId; $order = OrderModel::where('id', $orderId) ->lock(true) ->find(); if ($order['status'] != 2) { return error('订单已被接走'); } $order->status = 3; $order->rider_id = $riderId; $order->accept_time = time(); $order->save(); return ok('接单成功'); }lock(true)在 MySQL InnoDB 引擎下会生成SELECT ... FOR UPDATE行锁,保证同一时刻只有一个骑手的事务能读到待接单状态的记录。这套写法在秒杀场景里被验证过,单个订单上锁耗时约 1ms,完全扛得住跑腿高峰期的并发。
3. 智能派单与系统派单:算法参数和调度策略细节
3.1 派单权重计算:距离、订单类型、骑手负载
智能派单不是随机分配,而是给每个在线骑手算一个分数,取最高分派单。这套系统在DispatchController里实现了权重评分,核心公式是:
score = 距离得分 * 0.5 + 负载得分 * 0.3 + 完成率得分 * 0.2距离得分根据订单起点和骑手当前位置的直线距离计算,超过 2 公里直接淘汰。负载得分看骑手当前待配送订单数,0 单为 100 分,每多一单减 20 分。完成率得分取近 7 天订单完成百分比。实际调用时用 FastAdmin 后台的计划任务每分钟扫一次待派单池:
public function autoDispatch() { $pendingOrders = OrderModel::where('status', 2)->limit(20)->select(); foreach ($pendingOrders as $order) { $riders = RiderModel::where('online', 1)->field('id, lat, lng, order_count')->select(); $bestRider = null; $bestScore = 0; foreach ($riders as $rider) { $distance = getDistance($order->start_lat, $order->start_lng, $rider->lat, $rider->lng); if ($distance > 2) continue; $score = calcScore($distance, $rider->order_count, $rider->finish_rate); if ($score > $bestScore) { $bestScore = $score; $bestRider = $rider; } } if ($bestRider) { assignOrder($order->id, $bestRider->id); } } }这里有一个关键点:必须先排除超过配送范围的骑手,再计算分数。如果把距离得分直接做成惩罚项,会出现「距离 3 公里但完成率极高」的骑手被选中,用户取货体验会非常差。
3.2 智能派单 vs 系统派单的适用场景
很多团队分不清「智能派单」和「系统派单」,其实在这个项目里是两个独立模块。智能派单是上面说的自动计算权重,适合订单密度稳定的城市区域;系统派单更强调运营后台的人工干预,比如高峰期优先派给指定骑手,或者处理用户打电话投诉「为什么没人接单」时,管理员直接在后台手动把订单指派给某个骑手。
后台手动派单的表单很简单:选择订单、选择骑手、填写备注。但要注意权限设计,不是所有管理员都能手动派单。在 FastAdmin 的权限节点里,我把「系统派单」挂到dispatch/manual节点,只分配给调度员角色,避免客服误点导致骑手和用户之间的冲突。
3.3 超时未接单的重新分配策略
派单成功不等于订单就一定能被接。骑手可能会因为手头订单太多、手机没电等原因忽略推送。这套系统的默认策略是:智能派单推送后,等待 60 秒骑手未确认则自动转入抢单池,同时给第二顺位骑手推送。重派次数上限为 3,超过 3 次后订单变成「待人工处理」状态,后台会高亮显示。
重派逻辑里最容易出错的是订单状态回滚。如果第一顺位骑手已经点了「接单」按钮但还没跳转页面,第二顺位骑手同时抢单成功,就会出现两个骑手持有同一订单。我的处理方式是给接单操作加一层Redis 分布式锁,键名为order:lock:{orderId},过期时间 5 秒,谁先拿到锁谁才能更新订单状态。
3.4 派单效果怎么看:订单响应时间和取消率
部署后不能只看「单量变多了」,要盯着两个指标:订单响应时间(从支付完成到骑手接单的时间差)和派单取消率。以下是我在这套系统后台用 SQL 统计响应时间的方法:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, ROUND(AVG(TIMESTAMPDIFF(MINUTE, create_time, accept_time)), 1) AS avg_accept_minute, COUNT(*) AS order_count FROM fa_rider_order WHERE accept_time IS NOT NULL GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day DESC;如果某天平均响应时间超过 5 分钟,基本可以断定是派单权重里距离阈值设置太小,或者在线骑手数量不足。另外还要单独看「预约取件」订单的响应时间,这类订单的expect_time跟当前时间可能相差几个小时,不应该进入自动派单池,否则骑手被预约订单占住,会挤压实时单的处理能力。预约单的派发时机一般放在预计送达前 30 分钟,通过计划任务触发。
4. FastAdmin 后台:订单、骑手、财务配置的落地细节
4.1 基于权限节点的角色划分
FastAdmin 自带权限节点管理,但默认的节点粒度只到控制器和方法级别,不能满足跑腿后台的精细需求。我实际把节点细化到按钮级别,比如「订单管理」下面的「取消订单」「重新派单」「标记异常」是三个独立节点。这样客服只能查看和标记异常,不能取消订单,调度员才能操作重新派单。
后台菜单表fa_auth_rule里,每个节点有一个ismenu字段区分是菜单还是按钮。创建节点时建议遵循controller/action的命名规则,比如:
| 节点标识 | 节点名称 | 类型 |
|---|---|---|
| order/index | 订单列表 | 菜单 |
| order/cancel | 取消订单 | 按钮 |
| order/redispatch | 重新派单 | 按钮 |
| rider/audit | 骑手审核 | 菜单 |
设置好节点后,在角色管理里勾选对应权限,骑手审核人员和财务人员看到的左侧菜单就是完全隔离的。这个做法能防止运营后台因为权限过大被误操作。
4.2 骑手注册审核与派单区域绑定
校园跑腿和同城配送的骑手管理逻辑不一样:校园场景骑手大多是兼职学生,需要审核学生证;同城场景则要求骑手有交通工具。这套系统在骑手端提交入驻资料后,后台骑手列表会出现待审核数据。审核通过后还需要绑定派单区域,因为智能派单时只会在该骑手对应的area_id范围内搜索。
区域绑定用 FastAdmin 的widget\Form多选组件实现,生成的中间表fa_rider_area结构很简单:rider_id和area_id。派单时,先查骑手绑定的区域,再查订单起点是否在此区域内。如果骑手没有绑定任何区域,则视为全城接单,适合单量较少的起步期。
4.3 财务结算:跑腿费与平台抽成设置
后台财务模块必须支持两种模式:固定抽成和比例抽成。固定抽成适用于订单金额较小的校园跑腿,每单抽 1 元;比例抽成适用于同城配送,按照订单金额的 8%~12% 抽取。我在fa_config里新增了两个配置项:
// application/extra/biz.php return [ 'commission_type' => 'ratio', // fixed 固定金额, ratio 比例抽成 'commission_fixed' => 1.00, // 固定抽成金额 'commission_ratio' => 10, // 比例抽成 10% ];骑手结算页面调用这个配置,乘以订单金额得到平台抽成,剩余部分进入骑手待结算余额。这里要注意:抽成计算必须基于订单的实际支付金额,而不是订单金额。因为用户可能使用优惠券,如果按原价抽成,会出现平台抽成大于骑手实际收入的情况。
4.4 运营后台的财务统计与对账
后台财务统计页最核心的是一个汇总查询,按日、周、月展示营收、订单数、平均客单价、骑手佣金总和。这个页面容易写重查询,建议用 MySQL 的临时表或者 ThinkPHP 的field聚合方法,避免循环查询数据库。对于数据量超过 10 万条的系统,我一般会加一层 Redis 缓存,设定 10 分钟过期。
"营收" = 订单总支付金额 "平台收入" = 营收 - 骑手佣金 - 退款金额 "骑手佣金" = sum(订单实付金额 * (1 - 抽成比例))对账时如果发现平台收入为负数,优先检查退款订单的状态。因为退款单如果已经结算给骑手,系统需要生成一条「骑手扣款」记录,否则财务永远对不平。
5. 私有化部署、无加密源码改造与微信小程序环境踩坑
5.1 部署环境核对与一键安装
这套源码是 FastAdmin 标准目录结构,部署时先确认 PHP 版本和扩展。我建议用 PHP 7.4 而不是 8.0+,因为 ThinkPHP 5.1 的某些模型事件在 PHP 8 下会触发Deprecated警告,虽然不影响运行,但后台日志会被刷得很难看。MySQL 用 5.7,PHP 需要fileinfo、redis扩展,Redis 用来做派单锁和缓存。
上传源码后访问你的域名/install.php,填数据库信息即可完成安装。安装完成记得删除install.php文件,否则 FastAdmin 会提示重新安装并可能清空数据。
5.2 扫码登录的小程序跳转链接坑
uniapp 编译到微信小程序后,骑手端分享出来的订单卡片要求能直接跳转到订单详情页。这里最容易踩的是微信的新版跳转方式:weixin://dl/business这种 URL scheme 已经在大部分 iOS 场景失效,现在必须用wx.openBusinessView或小程序码。我在改造时放弃自定义跳转,改为生成订单小程序码:
// uni-app 端生成小程序码 uni.request({ url: 'https://api.weixin.qq.com/wxa/getwxacodeunlimit', method: 'POST', data: { scene: 'order_no=' + orderNo, page: 'pages/order/detail', check_path: false, env_version: 'trial' // 体验版用 trial,正式版用 release }, success: (res) => { // 保存返回的 buffer 到后端并转成图片 } });这里要注意scene参数长度限制在 32 个字符以内,不能直接把整个订单号拼进去,我一般用订单 ID 的反向字符串,后端再还原。
5.3 微信支付 v3 报错:证书序列号不匹配
对接支付 v3 时最常见报错是apiclient_cert_serial_no和请求头里的序列号不一致。原因是微信支付后台有多个 API 证书,开发者从「微信支付商户平台 -> API 安全」下载证书后,序列号是下载的那个证书的,但代码里如果加载的是预留在服务器上的旧证书,就会报错。排查命令:
openssl x509 -in apiclient_cert.pem -noout -serial把输出的序列号跟后台API v3 密钥管理里的证书序列号对比,不一致就重新上传证书文件。另外还要确认apiclient_key.pem的权限,PHP 进程如果无法读取私钥文件,会报failed to open stream: Permission denied。
5.4 用微信开发者工具抓包定位前端口口问题
跑腿小程序在联调阶段经常出现「用户订单列表有数据,但骑手端看不到」。这时不要急着改后端,先用微信开发者工具打开骑手端项目,在「网络」面板筛选order/list请求,看返回的 JSON 里data是否为空。如果为空,检查骑手端的rider_id是否在订单查询条件里被隐式过滤掉了。这种问题的根源大多数是where('rider_id', $this->riderId)里$this->riderId是null,因为登录后 token 没有正确传递到请求头。
在main.js里给uni.request封装统一的 token 注入逻辑:
uni.request({ url, header: { 'X-Token': uni.getStorageSync('token'), 'Content-Type': 'application/json' }, success: (res) => { if (res.data.code === 401) { uni.reLaunch({ url: '/pages/login/login' }) } } })App 端和小程序端的 header 命名要一致,否则 FastAdmin 的UserToken中间件识别不到 token,会直接拒绝请求并返回登录过期。
本文还有配套的精品资源,点击获取