☰
同城跑腿系统开发实战:Fastadmin+ThinkPHP+Uniapp 从选型到上线
2026/10/1 19:30:43 网站建设 项目流程

简介:这是一套面向同城跑腿创业团队与外包开发者的完整技术方案,基于Fastadmin、ThinkPHP与Uniapp构建,覆盖用户端、骑手端和运营后台三端,支持帮取、帮送两种业务模式,可私有化部署且源码无加密。包内共2000个文件,以1079个js、265个html、235个vue、213个json为主,另有142个md说明文档、58个css样式、2个sql建表脚本及少量xml、sh等配置,压缩包约43.97MB,前后端与小程序代码结构完整。功能层面涵盖按距离重量计价、临时加价、预约取件、跑腿小费、物品保价、地图选点导航,以及一键抢单、主动接单、自由开工、系统派单与智能派单等接单派单机制,并区分兼职与全职骑手佣金结算。目前已有105人学习下载,适合需要快速搭建跑腿平台、研究派单算法与计价规则的中高级开发者参考复用。

1. 同城跑腿系统选型:为什么 Fastadmin + ThinkPHP + Uniapp 是中小团队最稳的起手式

去年帮一个三线城市的本地生活团队做技术复盘,他们花了四个月用某开源商城改跑腿,最后卡在骑手端定位漂移和订单状态机混乱上,上线延期两个月。问题不在业务复杂度,而在选型:商城系统的订单模型和跑腿的「取送双地址 + 骑手实时轨迹 + 帮取帮送两种模式」根本对不上。后来换成 Fastadmin + ThinkPHP 做运营后台和 API,Uniapp 做用户端和骑手端,两周跑通核心链路。这套组合的价值在于:Fastadmin 自带 CRUD 生成和权限管理,ThinkPHP 的 ORM 和中间件生态成熟,Uniapp 一套代码覆盖微信小程序、H5 和 App,对预算有限、人手不足的团队来说,试错成本最低。这篇文章面向的是准备自建同城跑腿系统的开发者或技术负责人,从环境搭建、数据建模、双端对接到骑手调度和避坑,按可复现的路径讲清楚。如果你正在评估「帮取帮送」这类业务能不能用这套技术栈落地,下面的内容可以直接抄作业。

2. 环境搭建与 Fastadmin 后台初始化:从零到能登录的完整命令

2.1 ThinkPHP 运行环境的最低要求与版本选择

Fastadmin 基于 ThinkPHP 5.1 或 6.x 分支,当前主流稳定版本是 ThinkPHP 6.0 + Fastadmin 1.3.x。PHP 版本建议 7.4 或 8.0,MySQL 5.7+,Nginx 1.18+。不要用 PHP 8.1 以上,Fastadmin 部分依赖包在 8.1 会有兼容性告警。Composer 必须装,因为 Fastadmin 的插件机制和第三方 SDK 都走 Composer 管理。

我一般会先确认服务器时区和 MySQL 的sql_mode,跑腿系统对时间敏感,ONLY_FULL_GROUP_BY不关掉会在订单统计查询里翻车。命令如下:

# 查看 PHP 版本和扩展 php -v php -m | grep -E 'pdo_mysql|curl|gd|redis' # 关闭 MySQL 严格模式(在 my.cnf 的 [mysqld] 下添加) sql_mode=NO_ENGINE_SUBSTITUTION # 设置时区 timedatectl set-timezone Asia/Shanghai

逻辑说明:pdo_mysql是 ThinkPHP 连接数据库的基础,curl用于调用地图 API 和支付回调,gd用于生成海报和骑手接单凭证,redis用于订单队列和骑手位置缓存。参数上,sql_mode只保留NO_ENGINE_SUBSTITUTION是为了避免GROUP BY查询报错,跑腿后台的「区域订单统计」和「骑手绩效」都会用到聚合查询。

2.2 Fastadmin 下载与安装:三条命令跑通后台

Fastadmin 官方推荐用 Composer 创建项目,但国内网络直接拉取可能超时,我一般用 Gitee 镜像或完整包。以下命令假设你已经在项目根目录:

# 方式一:Composer 创建(网络好时用) composer create-project karsonzhang/fastadmin:1.3.4 fastadmin-run # 方式二:完整包解压后进入目录 cd fastadmin-run cp .env.sample .env # 修改 .env 数据库配置 # [database] # type = mysql # hostname = 127.0.0.1 # database = paotui # username = root # password = your_password # hostport = 3306 # prefix = fa_ # 导入初始 SQL mysql -uroot -p paotui < fastadmin.sql # 启动内置服务器测试 php think run --host 0.0.0.0 --port 8000

逻辑说明:.env文件是 ThinkPHP 6 的环境配置入口,Fastadmin 的数据库前缀默认fa_,跑腿业务表建议统一用pt_前缀区分。php think run是 ThinkPHP 自带的开发服务器,生产环境必须换成 Nginx + PHP-FPM。参数上,hostport默认 3306,如果 MySQL 改了端口要同步。安装完成后访问http://你的IP:8000/index.php/admin,默认账号admin,密码在安装时设置。

2.3 跑腿业务的数据表设计:订单表、骑手表、地址表

Fastadmin 的 CRUD 生成器可以快速建表,但跑腿的核心表结构必须手动设计。以下是最小可用的三张表:

-- 订单表:支持帮取和帮送两种模式 CREATE TABLE `pt_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int(11) NOT NULL COMMENT '下单用户ID', `rider_id` int(11) DEFAULT 0 COMMENT '骑手ID,0为未接单', `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1帮取 2帮送', `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0待接单 1已接单 2取件中 3配送中 4已完成 5已取消', `from_address` varchar(255) NOT NULL COMMENT '取件地址', `from_lat` decimal(10,7) DEFAULT 0 COMMENT '取件纬度', `from_lng` decimal(10,7) DEFAULT 0 COMMENT '取件经度', `to_address` varchar(255) NOT NULL COMMENT '送达地址', `to_lat` decimal(10,7) DEFAULT 0 COMMENT '送达纬度', `to_lng` decimal(10,7) DEFAULT 0 COMMENT '送达经度', `goods_info` varchar(500) DEFAULT '' COMMENT '物品信息', `fee` decimal(10,2) DEFAULT 0 COMMENT '配送费', `remark` varchar(255) DEFAULT '' COMMENT '备注', `create_time` int(11) DEFAULT 0, `accept_time` int(11) DEFAULT 0 COMMENT '接单时间', `finish_time` int(11) DEFAULT 0 COMMENT '完成时间', PRIMARY KEY (`id`), KEY `idx_rider_status` (`rider_id`,`status`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跑腿订单表'; -- 骑手表 CREATE TABLE `pt_rider` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '关联用户ID', `real_name` varchar(50) DEFAULT '' COMMENT '真实姓名', `phone` varchar(20) DEFAULT '' COMMENT '手机号', `id_card` varchar(20) DEFAULT '' COMMENT '身份证号', `status` tinyint(1) DEFAULT 0 COMMENT '0待审核 1正常 2禁用', `online` tinyint(1) DEFAULT 0 COMMENT '0离线 1在线', `lat` decimal(10,7) DEFAULT 0 COMMENT '当前纬度', `lng` decimal(10,7) DEFAULT 0 COMMENT '当前经度', `update_time` int(11) DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_online` (`online`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手表';

逻辑说明:type字段区分帮取和帮送,帮取是「取件地址 → 用户指定送达地址」,帮送是「用户地址 → 指定送达地址」,两者在订单状态流转上一致,但计价规则不同。status的状态机是跑腿系统的核心,0 到 4 的顺序不能乱,骑手端和用户端都依赖这个字段做 UI 切换。索引idx_rider_status用于骑手端「我的订单」查询,idx_online用于后台筛选在线骑手。参数上,经纬度用decimal(10,7)保证精度到厘米级,fee用decimal(10,2)避免浮点误差。

3. Uniapp 双端开发:用户端下单与骑手端接单的联调路径

3.1 Uniapp 项目初始化与 manifest 配置要点

用户端和骑手端建议放在同一个 Uniapp 项目里,通过角色路由区分,减少维护成本。用 HBuilderX 创建「默认模板」项目,然后修改manifest.json。关键配置项:

{ "name": "优创同城跑腿", "appid": "__UNI__XXXXXXX", "description": "同城跑腿系统", "versionName": "1.0.0", "versionCode": "100", "transformPx": false, "app-plus": { "usingComponents": true, "nvueStyleCompiler": "uni-app", "compilerVersion": 3, "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": true, "delay": 0 }, "modules": { "Geolocation": {}, "Maps": {}, "Payment": {} }, "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.CAMERA\"/>" ] }, "ios": { "privacyDescription": { "NSLocationWhenInUseUsageDescription": "需要获取您的位置用于计算配送距离", "NSCameraUsageDescription": "需要拍照上传物品信息" } } } }, "mp-weixin": { "appid": "wxXXXXXXXX", "setting": { "urlCheck": false, "es6": true, "postcss": true, "minified": true }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于跑腿下单" } }, "requiredPrivateInfos": ["getLocation", "chooseLocation"] } }

逻辑说明:transformPx设为 false 是为了用 Uniapp 的 rpx 单位,跑腿界面在手机端需要自适应。modules里必须声明 Geolocation 和 Maps,否则打包 App 后定位 API 返回失败。微信小程序的requiredPrivateInfos是 2022 年后新增的隐私协议要求,不配置getLocation会直接报错。参数上,versionCode每次发版必须递增,Android 应用市场审核会检查。

3.2 用户端下单页:地址选择与费用预估的实现

用户端核心是下单页,需要调用地图选点、计算距离、预估费用。以下是一个可复用的下单逻辑片段:

// pages/order/create.vue export default { data() { return { fromAddress: '', toAddress: '', fromLat: 0, fromLng: 0, toLat: 0, toLng: 0, distance: 0, fee: 0, orderType: 1 // 1帮取 2帮送 }; }, methods: { // 选择地址 async chooseAddress(type) { const res = await uni.chooseLocation({ latitude: this.fromLat || 39.908, longitude: this.fromLng || 116.397 }); if (type === 'from') { this.fromAddress = res.address; this.fromLat = res.latitude; this.fromLng = res.longitude; } else { this.toAddress = res.address; this.toLat = res.latitude; this.toLng = res.longitude; } this.calcFee(); }, // 计算距离和费用 calcFee() { if (!this.fromLat || !this.toLat) return; const distance = this.getDistance( this.fromLat, this.fromLng, this.toLat, this.toLng ); this.distance = distance; // 起步价5元含3公里,超出每公里2元 const basePrice = 5; const baseDistance = 3; const extraPrice = 2; if (distance <= baseDistance) { this.fee = basePrice; } else { this.fee = basePrice + (distance - baseDistance) * extraPrice; } // 帮取模式加收2元 if (this.orderType === 1) { this.fee += 2; } }, // 球面距离计算 getDistance(lat1, lng1, lat2, lng2) { const rad = Math.PI / 180; const a = rad * lat1; const b = rad * lat2; const theta = lng1 - lng2; const c = rad * theta; let d = Math.sin(a) * Math.sin(b) + Math.cos(a) * Math.cos(b) * Math.cos(c); d = Math.acos(Math.min(d, 1)); return d * 6371; // 地球半径6371公里 } } };

逻辑说明:uni.chooseLocation是 Uniapp 封装的原生地图选点,微信小程序和 App 都支持,H5 端需要配置地图 key。getDistance用球面余弦定理计算直线距离,实际配送距离应该用地图 API 的骑行路径规划,但直线距离作为预估足够。参数上,起步价和超出单价应该从后台配置接口读取,不要硬编码,方便运营调价。帮取模式加收 2 元是业务规则,可以在后台做成可配置项。

3.3 骑手端接单与状态流转:轮询还是 WebSocket

骑手端需要实时接收新订单,常见做法有两种:轮询和 WebSocket。轮询实现简单,但延迟高、耗电;WebSocket 实时性好,但需要服务端维护连接。我一般推荐混合方案:骑手在线时用 WebSocket 推送新订单,离线时用轮询兜底。

// 骑手端 WebSocket 连接 let socketTask = null; function connectSocket(riderId) { socketTask = uni.connectSocket({ url: `wss://your-domain.com/wss?rider_id=${riderId}`, success: () => { console.log('WebSocket 连接成功'); } }); socketTask.onMessage((res) => { const data = JSON.parse(res.data); if (data.type === 'new_order') { // 播放提示音并弹窗 uni.showModal({ title: '新订单', content: `取件:${data.order.from_address}\n送达:${data.order.to_address}`, success: (modalRes) => { if (modalRes.confirm) { acceptOrder(data.order.id); } } }); } }); socketTask.onClose(() => { console.log('WebSocket 断开,3秒后重连'); setTimeout(() => connectSocket(riderId), 3000); }); } // 接单 async function acceptOrder(orderId) { const res = await uni.request({ url: 'https://your-domain.com/api/rider/accept', method: 'POST', data: { order_id: orderId }, header: { 'token': uni.getStorageSync('token') } }); if (res.data.code === 1) { uni.showToast({ title: '接单成功' }); uni.navigateTo({ url: '/pages/rider/order-detail?id=' + orderId }); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); } }

逻辑说明:uni.connectSocket在 App 和微信小程序都可用,H5 端需要服务端支持 WSS。onClose里做重连是必须的,移动网络切换时 WebSocket 会断开。参数上,rider_id用于服务端识别骑手身份,接单接口的token从本地存储读取,服务端校验骑手状态和订单状态。注意:接单接口必须做并发控制,两个骑手同时点接单只能有一个成功,用 MySQL 的UPDATE ... WHERE status=0影响行数判断。

4. 运营后台与调度逻辑:Fastadmin 里的订单管理和骑手审核

4.1 用 Fastadmin CRUD 生成订单管理页面

Fastadmin 的 CRUD 生成器可以一键生成订单的增删改查,但跑腿订单不需要「新增」和「删除」,只需要「列表」和「详情」。在后台命令行执行:

# 生成订单管理 CRUD php think crud -t pt_order -c order/Order -m OrderModel # 生成骑手管理 CRUD php think crud -t pt_rider -c rider/Rider -m RiderModel # 生成菜单 php think menu -c order/Order php think menu -c rider/Rider

逻辑说明:-t指定表名,-c指定控制器路径,-m指定模型。生成后需要手动修改控制器,去掉add和del方法,因为订单不允许后台手动新增和删除。参数上,Fastadmin 的权限节点会自动注册,需要在「权限管理」里给运营角色分配「订单列表」和「骑手审核」权限。

4.2 骑手审核与在线状态管理

骑手注册后需要后台审核,审核通过才能接单。Fastadmin 的列表页自带「审核」按钮,但需要自定义操作。在application/admin/controller/rider/Rider.php里添加:

// 审核通过 public function pass($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error('骑手不存在'); } $row->status = 1; $row->save(); $this->success('审核通过'); } // 禁用骑手 public function forbid($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error('骑手不存在'); } $row->status = 2; $row->online = 0; $row->save(); $this->success('已禁用'); }

逻辑说明:$this->model->get($ids)获取骑手记录,status字段控制审核状态。禁用时同时把online设为 0,防止骑手继续接单。参数上,$ids是 Fastadmin 表格传过来的主键,支持批量操作时用逗号分隔,需要explode处理。

4.3 订单调度:手动派单与自动派单的取舍

小团队初期建议用「手动派单 + 骑手抢单」混合模式。后台可以手动指派订单给指定骑手,骑手端也可以主动抢单。自动派单算法复杂,需要综合考虑骑手距离、当前负载、历史评分,初期没必要上。

// 后台手动派单 public function assign($orderId, $riderId) { $order = OrderModel::get($orderId); if ($order->status != 0) { $this->error('订单已被接单'); } $rider = RiderModel::get($riderId); if ($rider->status != 1 || $rider->online != 1) { $this->error('骑手不在线或未审核'); } $order->rider_id = $riderId; $order->status = 1; $order->accept_time = time(); $order->save(); // 推送通知给骑手 $this->pushToRider($riderId, $order); $this->success('派单成功'); }

逻辑说明:派单前必须检查订单状态和骑手状态,避免重复派单。pushToRider是自定义方法,通过 WebSocket 或极光推送通知骑手。参数上,accept_time记录接单时间,用于计算骑手响应时长。

5. 避坑与排查:跑腿系统上线前必须处理的五个问题

5.1 定位漂移导致取送地址偏差超过 500 米

现象:骑手端显示的距离和用户端不一致,有时差出 1 公里。原因:Uniapp 的uni.getLocation默认返回 GCJ02 坐标系,但后台存储和地图 API 可能用 WGS84,坐标系不统一。解决:统一用 GCJ02,在manifest.json里配置"coordType": "gcj02",后台存储时不做转换,地图 API 调用时也传 GCJ02。

5.2 订单状态机混乱:骑手点「已送达」但用户没收到

现象:骑手端可以跳过「取件中」直接点「已送达」。原因:前端没有做状态校验,后端接口也没有校验前置状态。解决:后端在updateStatus接口里加状态流转判断,只允许0→1→2→3→4的顺序,非法流转直接返回错误。前端根据当前状态只显示下一个合法操作按钮。

5.3 微信小程序审核被拒:类目和隐私协议不符

现象:提交微信审核时提示「服务类目与功能不符」。原因:跑腿属于「生活服务 > 跑腿代购」,但很多开发者选了「工具 > 效率」。解决:在微信公众平台把类目改成「生活服务 > 跑腿代购」,并在manifest.json的mp-weixin里配置requiredPrivateInfos,包括getLocation、chooseLocation、chooseAddress。

5.4 骑手端 App 在后台被杀后收不到新订单

现象:骑手锁屏或切换应用后,WebSocket 断开,新订单推送丢失。原因:Android 和 iOS 对后台进程限制严格。解决:接入厂商推送通道(极光推送、个推),WebSocket 只在前台用,后台用推送通知。Uniapp 端在manifest.json里配置推送模块,服务端调用推送 API 发送通知。

5.5 订单金额计算出现 0.01 元误差

现象:用户端显示 12.30 元,后台统计 12.29 元。原因:JavaScript 浮点数计算精度问题,0.1 + 0.2 !== 0.3。解决:所有金额计算用整数分,前端显示时除以 100。后端 PHP 用bcmath扩展做精确计算,数据库存decimal(10,2)。

6. 从能跑到好用:跑腿系统的压测指标与骑手调度优化技巧

系统上线只是开始,真正决定留存的是高峰期能不能扛住。我一般会在上线前做一轮压测,重点看三个指标:订单创建接口的 P99 延迟、WebSocket 并发连接数、MySQL 订单表的写入 TPS。用ab或wrk对/api/order/create压测,目标是在 500 并发下 P99 低于 200ms。WebSocket 用websocket-bench模拟 1000 个骑手同时在线,观察服务端内存和连接稳定性。

骑手调度优化上,初期不要追求「最优派单」,先做到「不超时」。我的经验是:订单创建后 30 秒内没有骑手接单,后台自动把订单推送给距离取件地址 3 公里内、当前负载小于 3 单的骑手。这个逻辑用 Redis 的GEO命令实现:

# 骑手位置写入 Redis GEO GEOADD rider_online 116.397 39.908 "rider_1001" # 查询取件地址 3 公里内的骑手 GEORADIUS rider_online 116.400 39.910 3 km WITHDIST ASC

逻辑说明:GEOADD在骑手每次上报位置时更新,GEORADIUS按距离排序返回附近骑手。参数上,WITHDIST返回距离,ASC按由近到远排序。拿到骑手列表后,再过滤掉负载已满的,逐个推送。这个方案比全表扫描 MySQL 快一个数量级。

另一个技巧是订单状态变更时用 Redis 队列异步写 MySQL,避免高峰期直接写库导致锁等待。用 ThinkPHP 的think-queue扩展,把订单创建、状态变更、骑手位置更新都丢进队列,消费者进程慢慢写。这样接口响应时间能压到 50ms 以内。

最后说一个血泪教训:跑腿系统的「帮取」和「帮送」在计价上一定要分开配置,我见过一个团队把两者混在一起算,结果帮取订单多收了用户 3 块钱,客诉率直接翻倍。后台的计价规则表要支持按类型、按区域、按时间段配置,别偷懒写死在代码里。希望帮到你。

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

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

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

立即咨询