ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

同城跑腿系统智能派单与订单状态机实现细节拆解

同城跑腿系统智能派单与订单状态机实现细节拆解 简介一套基于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_sendstatus字段保存上面状态。前端 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.7PHP 需要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会直接拒绝请求并返回登录过期。本文还有配套的精品资源点击获取
返回列表