☰
同城跑腿源码:Fastadmin+ThinkPHP+Uniapp三端部署
2026/10/27 8:39:10 网站建设 项目流程

简介:面向跑腿团队、站长与PHP开发者的同城跑腿系统完整源码包,基于Fastadmin、ThinkPHP与Uniapp构建,同时提供用户端、骑手端与运营后台,覆盖帮取、帮送两种主流业务模式。压缩包共2000个文件,以1079个js、265个html、235个vue、213个json为核心,另含58个css、142个md说明文档及2个sql数据库脚本,整体43.97MB,目录划分清晰,便于按端查找与二次开发。目前已有107人学习下载。源码未加密、可私有化部署,内置按距离/重量计价、临时加价、预约取件、跑腿小费、物品保价、地图选点等完整业务逻辑;骑手端支持一键抢单、主动接单、自由开工与听单开关,后台可灵活切换抢单/派单模式,并依据距离、等级等条件智能派单。项目从用户下单、骑手接单到后台调度与计费配置均形成闭环,前端Uniapp可编译为小程序、App等多端运行,适合独立开发者、外包团队与本地生活服务商快速搭建自有跑腿平台,也可作为ThinkPHP+Uniapp全栈项目的学习范本。

1. 一套能直接商用的同城跑腿源码:从三端结构到上线部署

做跑腿业务的团队,最怕的不是没单,而是订单一多,微信群派单就彻底乱了。用户问单靠刷屏,骑手接单靠手速,账目对不上,体验更谈不上。这套基于 Fastadmin + ThinkPHP + Uniapp 的优创同城跑腿系统,本质是把「帮取、帮送」这类同城即时配送业务的完整闭环——用户端下单、骑手端抢单配送、运营后台管单管人管定价——全部用源码固定下来。它不是一个演示用的空壳,而是带用户端、骑手端、运营后台三个独立端点的可用工程。适合谁?想自建跑腿平台的技术负责人、接私活的开发者、以及看中了校园或社区跑腿场景但不想从零写起的创业者。下面我从代码结构、核心逻辑、联调部署、踩坑记录到上线发布,完整拆一遍。

2. 认识这套三端架构:Fastadmin 后台、ThinkPHP 接口、Uniapp 多端

2.1 Fastadmin + ThinkPHP:为什么这个组合适合跑腿业务

Fastadmin 本身就是基于 ThinkPHP 5 开发的后台管理框架,它最大的优势是 CRUD 快速生成、自带权限管理和后台皮肤。跑腿项目的运营后台需要的骑手审核、订单列表、计费规则配置、优惠券管理,本质上都是围绕数据表的增删改查,Fastadmin 的生成器能把这些后台页面在很短时间内搭出来。

ThinkPHP 在这套系统里承担的是 API 层职责。用户端和骑手端通过 HTTP 请求调用接口,订单创建、抢单、状态流转、位置上报这些操作都由 ThinkPHP 控制器里对应的方法处理。选择 ThinkPHP 而不是 Laravel 的原因,对做 Fastadmin 二次开发的人来说很实际:Fastadmin 底层就是 TP,你不需要维护两套框架,路由规则、数据库连接配置、中间件机制完全一致,出问题排查时只看一套代码就够了。

这套组合的短板也要清楚:ThinkPHP 的性能上限在小规模并发下没有问题,但如果做到城市级日单量过万,就需要引入队列和 Redis 做削峰,而不是改框架。跑腿业务初期用这套架构踩通商业模型,成本是最低的。

2.2 三个端的源码结构:拿到项目先看这几个目录

源码包解压后,我一般会先按端别把目录分清,避免在错误的位置找代码。跑腿项目通常是这种布局:

project/ ├── application/ # ThinkPHP 应用目录(后端 API 和后台) │ ├── admin/ # Fastadmin 运营后台控制器 │ ├── api/ # UniApp 调用的用户端/骑手端接口 │ └── config.php # 全局配置,数据库连接、URL 模式 ├── public/ # 入口目录,站点根目录指向这里 │ └── uploads/ # 用户头像、证件上传目录 ├── addons/ # Fastadmin 插件目录 └── uniapp/ # 前端工程,HBuilderX 直接导入 ├── pages/user/ # 用户端页面 ├── pages/rider/ # 骑手端页面 ├── manifest.json # 小程序/App 配置文件 └── common/ # 请求封装、工具函数

关键路径先说三个:application/api是接口层,联调时改的就是这里;uniapp/manifest.json是小程序 AppID、定位权限、App 打包配置所在;application/admin是运营后台的逻辑层,Fastadmin 生成的后台控制器都在这里。

一个容易忽视的细节是 Fastadmin 的伪静态配置。很多新手把站点根目录指向了application,结果页面能开但样式全丢,接口全部 404。正确做法是 Nginx 站点根目录指向public,并把伪静态规则配成 ThinkPHP 标准模式,不然index.php入口和路由解析会出各种问题。

3. 核心业务逻辑拆解:订单流转、计费规则与抢单实现

3.1 订单状态机:从下单到完单的七个状态

跑腿系统的核心不是页面,是订单状态机。用户下了单,订单到哪个环节、骑手能不能抢、用户能不能取消、钱什么时候结算,全由状态字段控制。这套系统的订单状态通常是按数字递增设计的,每个数字对应一个业务阶段:

CREATE TABLE `order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_sn` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int(11) NOT NULL COMMENT '下单用户', `rider_id` int(11) DEFAULT NULL COMMENT '接单骑手', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待支付,1待接单,2已接单,3配送中,4已完成,5已取消,6退款中', `pay_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付方式:1微信,2余额', `amount` decimal(10,2) NOT NULL COMMENT '订单金额', `distance` decimal(10,2) DEFAULT NULL COMMENT '配送距离(km)', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

对应到代码层,状态流转的合法性判断必须做在接口里,不能只靠前端按钮控制。比如骑手接单前要检查订单状态是否仍然是 1(待接单),否则会出现两个人同时抢到同一单的情况:

// application/api/controller/Order.php 中的抢单方法 public function grab() { $order_id = $this->request->post('order_id'); $rider_id = $this->request->post('rider_id'); // 使用事务 + 状态条件更新,避免并发抢单 $result = Db::name('order') ->where('id', $order_id) ->where('status', 1) // 必须是待接单状态 ->update([ 'rider_id' => $rider_id, 'status' => 2, // 置为已接单 'grab_time' => date('Y-m-d H:i:s') ]); if ($result) { Db::commit(); return json(['code' => 1, 'msg' => '接单成功']); } else { Db::rollback(); return json(['code' => 0, 'msg' => '手慢了,订单已被抢']); } }

这里的关键是where('status', 1)这个条件更新写法。在 MySQL 的 InnoDB 事务下,两个骑手同时请求时,只有一个请求能成功把 status 从 1 更新为 2,另一个会因为条件不满足更新 0 行而失败。这比先 SELECT 再 UPDATE 的写法安全得多,能省掉后续很多并发问题的排查。

3.2 距离计费规则:起步价、里程费与夜间加价

跑腿的定价直接决定毛利,所以计费逻辑往往不是写死在前端,而是放在后台运营端可配置。常见的实现方式是在数据库里放一张计价规则表,后台可以调整起步价、每公里单价、夜间服务费区间:

CREATE TABLE `fee_rule` ( `id` int(11) NOT NULL AUTO_INCREMENT, `start_km` decimal(4,1) NOT NULL DEFAULT '0.0' COMMENT '起步里程', `start_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '起步价', `per_km_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '每公里单价', `night_start` time DEFAULT NULL COMMENT '夜间加价开始', `night_end` time DEFAULT NULL COMMENT '夜间加价结束', `night_extra` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '夜间加价金额', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

下单接口计算价格时,先用腾讯地图或高德的距离计算接口拿到两个地址间的骑行或步行距离,再套计价规则。距离是取整还是截断、夜间时段怎么判断,这几个参数每个项目要求都不一样,二次开发时优先改这里:

// 计算订单费用的简化逻辑 public function calcFee($distance, $start_time) { $rule = Db::name('fee_rule')->find(); // 超过起步里程的部分按每公里单价计费 $extra_km = max(0, $distance - $rule['start_km']); $fee = $rule['start_price'] + $extra_km * $rule['per_km_price']; // 夜间时段判断,加价直接叠加 $hour = date('H:i', strtotime($start_time)); if ($hour >= $rule['night_start'] && $hour <= $rule['night_end']) { $fee += $rule['night_extra']; } return round($fee, 2); }

有一个常见的业务坑是距离单位。地图接口返回的距离一般是米,而计价规则表里存的是公里,换算的时候必须除以 1000。我见过因为忘记换算,用户下单显示 0.5 公里实收起步价,结果实际距离是 5 公里,骑手拒单的翻车情况。

3.3 骑手定位与轨迹:Uniapp 的后台持续定位

骑手端的定位上报是跑腿系统的另一个核心。Uniapp 在小程序端可以使用wx.startLocationUpdateBackground,App 端则用plus.geolocation.watchPosition,但这两者的触发条件和后台限额有很大差异。

小程序端的后台定位目前必须配置「requiredBackgroundModes」为location,并且在manifest.json中声明permission权限描述。App 端需要在 Android 上配置前台服务权限,否则 App 一退后台位置更新就断了。下面是骑手端在 App 场景下常用的持续监听写法:

// uniapp/pages/rider/deliver.vue 中的定位监听 let watcher = null; function startLocation() { // plus.geolocation 只在 App 端可用,小程序端的 watchPosition 不生效 watcher = plus.geolocation.watchPosition( (res) => { const { longitude, latitude } = res.coords; // 上报位置到后端接口,间隔内多次回调只发一次 uni.request({ url: '/api/rider/reportLocation', method: 'POST', data: { longitude, latitude, order_id: currentOrderId }, }); }, (err) => { console.log('定位监听失败', err); }, { enableHighAccuracy: true, maximumAge: 3000, timeout: 10000 } ); }

参数里的enableHighAccuracy: true表示使用 GPS 和基站混合定位,maximumAge: 3000表示 3 秒内的缓存位置直接复用,避免频繁回调消耗电量。骑手端切记不要每个回调都发 HTTP 请求,实际位置变化频率远低于回调频率,合理的做法是记录最后上报时间,超过 10 秒才发一次,这样服务器压力小很多,电池也不会 2 小时耗尽。

4. 部署与联调实操:从 Fastadmin 后台到 Uniapp 客户端的完整链路

4.1 Fastadmin 部署四件事:PHP 版本、伪静态、目录权限、上传限制

跑腿系统部署时,Fastadmin 的项目运行要求是 PHP 7.1 以上,推荐 7.4,兼容性最稳。PHP 8 也能跑 ThinkPHP 5.1,但个别 Fastadmin 老插件会有兼容问题,生产环境不建议冒险。数据库用 MySQL 5.7,utf8mb4 字符集,跑腿业务的用户昵称和地址包括特殊符号,utf8mb4 是必须的。

部署顺序我一般固定为四步。第一步,导入项目源码后把站点根目录交给 Nginx 的 root 指向public目录;第二步,配置 ThinkPHP 伪静态,Nginx 下写入:

location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }

注意是$1而不是$uri,很多文档写的是$uri,在部分 Nginx 版本下会导致带参数的路由全部丢失。第三步,给runtime目录设置 755 权限,并确保其属主是 www 用户,否则模板渲染和日志写入都会报错;第四步,修改public/uploads上传目录的权限,保证骑手证件上传、用户头像上传可用。

Fastadmin 后台的 URL 默认是你的域名/admin,首次访问会进入安装向导,数据库信息填好后后台即可登录,账号密码由安装向导生成。要注意的是安装完成后删除install目录或改名,避免重装风险。

4.2 Uniapp 端导入与 manifest.json 配置

Uniapp 前端工程直接拿 HBuilderX 打开,不用 npm install 那套流程,这点比原生小程序开发省事。导入后第一件事是改manifest.json,这是整个前端能否正常编译到小程序和 App 的关键文件。需要配置的有三处:微信小程序 AppID、定位权限描述、App 打包配置。

{ "mp-weixin": { "appid": "你自己的小程序AppID", "setting": { "urlCheck": false }, "permission": { "scope.userLocation": { "desc": "骑手配送需要获取你的位置信息" } }, "requiredBackgroundModes": ["location"] }, "app-plus": { "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\"/>" ] } } } }

这里的urlCheck: false是开发调试时关闭合法域名校验用的,上线前必须改为 true,否则真机预览能通过但小程序审核会被拒。requiredBackgroundModes声明了后台定位模式,编译时微信会自动把相关权限加进去。

4.3 一条完整联调链路:登录、发单、抢单、配送、完单

环境准备好之后,联调从用户端登录开始。跑腿系统一般用手机号加验证码方式登录,后端缓存验证码

// application/api/controller/User.php 发送验证码 public function sendSms() { $mobile = $this->request->post('mobile'); $code = rand(1000, 9999); // 验证码存入缓存,有效期 300 秒 cache('sms_' . $mobile, $code, 300); // 对接阿里云短信或腾讯云短信发送接口,这里省略具体实现 return json(['code' => 1, 'msg' => '验证码已发送']); }

开发阶段没有短信服务时,可以直接在缓存里读验证码。联调发单流程时,用户端选择帮送或帮取、填写取件地址和收货地址、系统计算距离和费用、用户支付、订单进入待接单池。骑手端登录后刷新订单列表,看到一个待接单订单,点击抢单,此时用户端状态应同步跳转为「已接单」。如果用户端状态没变,优先检查 API 接口返回的状态字段与前端映射表是否一致——这是最常出现的低级错误,前端判断条件写死了status === 2,后端给的是 2 但返回的是字符串,等等。

5. 避坑指南:Fastadmin 上传漏洞、小程序包体超限与定位失效

5.1 Fastadmin 上传文件漏洞:不升级等于裸奔

现象:Fastadmin 后台文件上传接口存在绕过风险,攻击者可构造恶意请求上传 PHP 文件到public/uploads目录,进而获取服务器权限。

原因:这是 Fastadmin 历史上真实出现过的安全漏洞,本质是上传接口对文件类型校验不严,同时部分部署没有禁止 uploads 目录的 PHP 执行权限。

解决:有三件事必须做。第一,把 Fastadmin 框架升级到最新版本,官方补丁已经修复了上传接口的校验逻辑;第二,在 Nginx 配置中禁止 uploads 目录执行 PHP:

location ~* /uploads/.*\.(php|php5)$ { deny all; }

第三,后台管理入口改用复杂路径,Fastadmin 默认管理入口是/admin,可以通过修改路由配置换成自定义字符串。这套跑腿系统涉及骑手证件上传、用户头像上传两个入口,任何一个泄露都会变成攻击面,上传漏洞的排查不能只信框架,还要自己验证一次。

5.2 小程序包体超限:2612kb 的瘦身方案

现象:使用 HBuilderX 云打包微信小程序时,编译结果提示source size 2612kb exceed max limit 2mb,上传失败。

原因:这是 uniapp 微信小程序打包最常见的问题。跑腿项目包含用户端、骑手端两套页面,加上地图组件、支付组件、UI 库后,主包体积很容易突破 2MB 限制。

解决:拆分包是唯一出路。把骑手端的页面全部放到subpackages/rider子包,用户端的支付和订单详情页也可以按需拆分。微信小程序的分包加载机制是首次进入只下载主包,进入子包页面时才加载对应代码,用户端和骑手端本来就不是同一时间被访问的,拆开完全不影响体验。manifest.json中分包配置如下:

{ "mp-weixin": { "subPackages": [ { "root": "pages/rider", "name": "rider", "pages": ["rider-list", "rider-deliver"] } ], "preloadRule": { "pages/index/index": { "network": "all", "packages": ["pages/rider"] } } } }

拆完分包后再看 static 目录,地图 SDK 如果用的是 WebView 版可以换成小程序原生地图组件,UI 框架没用到的高亮组件可以按需引入,这一步通常能再压掉 300-500KB。

5.3 后台定位失效与日志不打印:两个最容易忽略的点

现象:骑手端 App 退到后台后,后台运行监测定位userlocationbackground不触发,用户端看不到骑手位置更新;同时排查时发现console.log在 HBuilderX 控制台完全不打印信息。

原因:后台定位失效多半是 Android 的电池优化策略把 App 进程冻结了,或者是manifest.json里没有声明ACCESS_BACKGROUND_LOCATION权限;日志不打印则是因为 HBuilderX 的日志等级过滤开了 ERROR 级别,info/log 级别的输出被吞了。

解决:App 端定位监听改用plus.geolocation.watchPosition并配置force: true参数,同时引导用户关闭该应用的电池优化白名单。日志查看在 HBuilderX 的「运行」菜单里把日志级别切换为 VERBOSE,并开启真机调试模式的vConsole。小程序端则是在manifest.json的卸载设置里打开调试模式,微信开发者工具也能直接看到 log。

5.4 热更新不生效:版本号递增的坑

现象:App 端离线打包后发布新版本,用户更新热更新包后打开仍是旧页面。

原因:uniapp 的热更新(wgt 包)基于版本号比较,如果manifest.json中的 versionName 没有递增,客户端会认为无需更新,直接忽略新包。

解决:每次发版必须同时递增versionName和内部versionCode。versionCode是整数,每次 +1,versionName按语义化版本命名。wgt 包只更新前端资源,原生插件改动必须走整包更新,否则容易出现资源与原生代码不匹配的启动崩溃。

6. 上线发布与二次开发:把小程序的支付接到自己的商户号

6.1 微信小程序上架和安卓应用市场发布的操作要点

小程序端配置好 AppID 并完成分包瘦身后,HBuilderX 点击「发行 → 小程序-微信」,生成目录后用微信开发者工具打开、上传代码、提交审核。审核时需要注意几个问题,跑腿类小程序的类目需要选择「生活服务-跑腿」类目,否则会被拒;如果配置了requiredBackgroundModes: ["location"],审核时可能会额外要求说明持续定位的使用场景,这个在提审信息里写清楚即可。

安卓应用市场发布时,用 HBuilderX 的云打包生成 apk,需要确定包名和组织架构。应用市场如华为、小米、OPPO要求应用有软件著作权和相应的行业资质,跑腿业务如果涉及商家入驻,部分市场还会核查 ICP 备案。建议先发一个市场验证审核周期,别等到全渠道同时提交时才发现资质文件不齐全。

6.2 把跑腿订单的支付改成自己的微信商户号

这是二次开发里几乎必改的一项。演示环境或源码自带的支付配置用的是通用商户号,正式运营必须换成自己的微信支付商户号。支付下单逻辑在小程序端用uni.requestPayment发起,但前提是后端先生成预支付单拿到支付参数:

// 用户端支付页调用 uni.request({ url: '/api/pay/wxpay', method: 'POST', data: { order_sn: this.order_sn }, success: (res) => { // 后端返回的 payParams 直接传给 requestPayment uni.requestPayment({ provider: 'wxpay', timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: 'MD5', paySign: res.data.paySign, success: () => { uni.redirectTo({ url: '/pages/order/order-detail?id=' + this.order_id }); }, fail: (err) => { // 支付失败,订单仍处于待支付状态,允许用户取消或继续支付 }, }); }, });

这里最容易出问题的是签名算法不一致。MD5签名时需要把商户号、订单号、金额、随机字符串按文档顺序拼接,然后 md5 加密后转大写,很多联调失败都是大小写不匹配导致的。我自己的习惯是先跑一遍微信支付官方提供的签名校验工具,确认签名无误后再去调 API。

后端支付后的回调验签也不能省。微信支付服务器会以 POST 方式把支付结果推送到你在商户平台配置的回调地址,必须在回调里验证签名成功后才更新订单状态为已支付,否则恶意伪造回调就能免单。从那以后我每次部署跑腿类项目,都强制走一遍「下单 → 支付 → 回调 → 订单状态变更」的全链路验证,包括模拟支付成功和支付失败两种情况,确认订单状态变化正确才交给运营使用。这套流程看着繁琐,但能在一开始就把订单对不上的隐患掐掉,希望帮到你少踩几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询