同城跑腿系统开发实战:Fastadmin+ThinkPHP+Uniapp架构拆解

发布时间:2026/10/7 20:18:29
同城跑腿系统开发实战:Fastadmin+ThinkPHP+Uniapp架构拆解 简介基于FastadminThinkPHP和Uniapp开发的优创同城跑腿系统是一套面向跑腿团队与同城即时配送创业者的完整源码方案覆盖用户端、骑手端、运营后台支持帮取、帮送两种核心模式既适合技术人员二次开发也适合运营团队快速搭建业务平台。压缩包共2000个文件大小约43.97MB以js、html、vue、json等类型为主前端页面、后端逻辑、接口配置与说明文档分布清晰便于按模块检索修改。资源在业务层面覆盖按距离/重量计价、临时加价、预约取件、跑腿小费、物品保价、地图选点、一键抢单、系统派单、智能派单、兼职/全职等常用规则基本满足同城跑腿实际运营场景。目前已有107人学习下载源码无加密且可私有化部署适合希望低成本获得可运营系统并进行二次扩展的个人或团队。1. 同城跑腿系统拆解Fastadmin、ThinkPHP、Uniapp各管什么如果你接过同城跑腿这类项目大概率会先遇到一个选择题运营后台用现成的后台框架还是从零写用户端和骑手端是套模板还是自己搭基于FastadminThinkPHP和Uniapp开发的优创同城跑腿系统给出的答案很明确——后台管理交给Fastadmin底层是ThinkPHP面向C端的两个App用Uniapp一套代码同时出微信小程序和安卓/iOS包。这个组合的优势在于开发效率Fastadmin把菜单、权限、CRUD都生成好了Uniapp把三端编译差异也处理掉了你真正要啃的是业务逻辑部分——帮取、帮送两种订单模式的状态流转、骑手定位回传、以及用户端和骑手端各自的结算规则。这篇笔记适合准备接同城跑腿外包、或者想快速验证这个方向的人我会把架构拆开再给到能直接抄的接口、配置和排错方案。2. 跑腿系统整体架构订单状态机与三端数据流跑腿业务不像商城那样商品是主角它核心是一笔订单从发起到完成的整个生命周期。同城跑腿系统虽然分用户端、骑手端、运营后台三个端口但所有端的操作都是在推进同一个订单状态。所以先把状态机想明白比先写任何一行代码都重要。2.1 为什么是FastadminThinkPHP而不是纯前后端分离常见做法是后台用Fastadmin原因很实际Fastadmin自带基于ThinkPHP 5.x/6.x的权限控制Auth、后台菜单管理、附件管理和一键生成CRUD跑腿系统的运营后台需求——骑手审核、订单列表、价格配置、提现审核——全是标准的表格表单操作用生成器能省一大半功夫。有人会觉得Fastadmin太重不如用Laravel或直接写API给前端用但跑腿系统有个特点运营后台的查询条件很多比如按骑手、按时间段、按订单状态组合筛选Fastadmin的searchlist机制几乎不用额外写SQL而ThinkPHP的ORM在查询构造这块也很顺手比如统计骑手今日完单数直接使用Db类加where条件就行。如果你非要用纯前后端分离理论上可行但成本和收益不成正比。Fastadmin后台的菜单、管理员、日志、配置管理都是现成的运营后台管理员的登录态、操作日志、权限拦截Fastadmin在中间件层面已经处理掉了你再从零搭一遍RBAC少说多写几百行代码。而且跑腿系统的后台不是给用户用的不需要那种极致的交互体验Fastadmin基于Bootstrap的后台界面完全够用。2.2 订单状态机设计帮取和帮送如何共用一个状态流帮取和帮送的区别只在任务内容帮取是去商家/代收点取东西送到指定人帮送是用户把东西给骑手送出去。但从订单状态看两者完全共用一条链路待支付 → 待接单 → 已接单骑手去取件→ 取件完成配送中→ 已完成 → 待评价/已评价外加两个终止状态用户取消支付前/已接单前和平台取消骑手长时间不接单超时。订单状态字段我一般用int型state配一个状态映射描述文件而不是用字符串枚举这样在Fastadmin的列表里可以直接用status渲染标签如success、danger、warning也不用在SQL里做字符串大小写匹配。订单表的核心字段如下字段类型说明order_snvarchar(32)订单号唯一索引user_idint用户IDrider_idint骑手ID接单前为空order_typetinyint1帮取 2帮送statetinyint0待支付 1待接单 2已接单 3配送中 4已完成 5已取消start_addressvarchar(255)取件地址帮送模式就是用户发件地end_addressvarchar(255)送达地址goods_weightdecimal(5,2)物品重量(kg)帮送必填delivery_feedecimal(10,2)配送费distancedecimal(10,2)预估距离(km)paid_atdatetime支付时间created_at / updated_atdatetime记录创建和更新时间帮送模式比帮取多一个goods_weight字段帮取是代买/代拿重量一般按实收取后台的价格配置里也能按重量阶梯计价。在写接口之前先把这张表的索引建对order_sn唯一索引用于支付回调查单statecreated_at联合索引用于运营后台按状态筛选订单列表。3. 用Fastadmin搭建运营后台菜单、权限与帮取帮送工单流转Fastadmin引入项目的第一步不是写业务代码而是先做后台基础配置。把Fastadmin部署好之后要用php think命令行工具生成订单表和骑手表的对应控制器/模型/视图然后做菜单授权。跑腿系统的运营后台角色一般有超级管理员看全量数据、运营审核骑手、处理订单异常、财务提现审核。这三类角色在Fastadmin里就是三个权限组每个组勾选对应菜单即可。在Fastadmin里手动创建一张业务表之后用一行命令生成接口和视图是很高效的php think crud -t order -c order -i order_sn,user_id,rider_id,state,order_type,start_address,end_address,delivery_fee,created_at -u 1-t是表名-c是控制器名-i是需要在列表页显示的字段-u 1表示强制覆盖已有控制器。这样生成的控制器位于application/admin/controller/Order.php模型在application/common/model/Order.php视图在application/admin/view/order/。它自带index/add/edit/del/forbidden接口和对应模板运营后台的订单管理列表就直接能用了。3.1 在Fastadmin里建订单表先设计状态字段的注释规范生成CRUD只是省了写监控面的时间真正的业务逻辑要落到控制器里改尤其是订单状态的变更。Fastadmin生成的控制器默认只有增删改查我需要重写order控制器的index方法增加按骑手和状态筛选的where条件。同时状态字段的值在数据库中只存int如1待接单要在模型里加一个getStateTextAttr的查询器或者用Fastadmin后端自带的状态渲染。在视图index.html里需要用Fastadmin的builddatagrid同时把state字段做成开关标签。运营后台的骑手审核是另一个重要管理模块骑手表要记录身份证号、行驶证信息、接单状态、今日完单量。骑手状态更新我建议用独立接口不要直接依赖Fastadmin的编辑按钮因为骑手表会被Uniapp端高频读取和更新位置而Fastadmin的常规编辑会走form表单跟API是有冲突面的。后台接口和客户端接口要分离Fastadmin的控制器用create/find等管理API处理运营后台请求另外在application/api/controller/Rider.php里写供Uniapp调用的接口。3.2 用Fastadmin的验证层和Token鉴权给Uniapp端提供API的正确姿势Uniapp端用户端和骑手端不能直接访问后台的admin路由因为Fastadmin的admin入口做了登录态和CSRF校验而客户端没有Cookie与后台会话。常见做法是在extend/fast/目录下使用Fastadmin的Api基类或者按官方推荐单独建立api模块。我先说一个最容易翻车的点很多人在api控制器里用了$this-request-post()读参数但在Fastadmin的API控制器里正确写法是继承think\Controller然后读取input()或$this-request-param()因为Fastadmin的基类做了不少后台特定处理。用户和骑手的登录态我用Fastadmin的token鉴权机制处理——用户登录成功后会返回一个token客户端后续每次请求header里带Authorization: Bearer token。Fastadmin的api基类里自带_initialize方法会校验token无需自己重写登录逻辑。?php namespace app\api\controller; use think\Controller; use think\Db; class Order extends Controller { /** * 创建订单帮取/帮送共用 * 参数: token, order_type, start_address, start_lng, start_lat, * end_address, end_lng, end_lat, contact_tel, remark * 帮送模式额外传: goods_weight */ public function create() { $user $this-getUser(); // 从token中解析用户信息 if (!$user) { return json([code 401, msg 请先登录]); } $orderType (int) input(order_type); if (!in_array($orderType, [1, 2])) { return json([code 400, msg 订单类型不合法]); } $params [ order_sn $this-buildOrderSn(), user_id $user[id], order_type $orderType, start_address input(start_address), start_lng input(start_lng), start_lat input(start_lat), end_address input(end_address), end_lng input(end_lng), end_lat input(end_lat), contact_tel input(contact_tel), remark input(remark, ), state 0, // 待支付 ]; if ($orderType 2) { $params[goods_weight] input(goods_weight, 1); } $fee $this-calcDeliveryFee($params); // 按距离和重量计算运费 $params[delivery_fee] $fee; $orderId Db::name(order)-insertGetId($params); return json([code 0, data [order_id $orderId, delivery_fee $fee]]); } }这段代码里最关键的设计是创建订单时先给state设0待支付用户微信支付成功后再通过微信支付回调把订单推到待接单状态。这里没有把支付动作写在create方法里是为了让客户端在拿到order_id后再拉起后续支付避免下单和支付耦合后因支付失败导致脏订单。calcDeliveryFee方法里一般取距离两个经纬度的直线距离乘1.2的路程系数再按基础价每公里单价重量加价计算这块逻辑不要写到控制器里放模型层或服务类会更好维护。运营后台改计价规则时改的是数据库配置不用动代码。3.3 骑手端接单、取件、送达的接口设计状态变更必须加并发保护跑腿系统最容易出现的数据不一致是骑手同时抢同一单。两个骑手同时请求接单如果代码只做简单的update操作会出现状态覆盖。这时的正解是在SQL里用条件更新而不是先查再更新// 接单操作 $result Db::name(order) -where(id, $orderId) -where(state, 1) // 必须是待接单状态 -where(rider_id, 0) // 且还没有骑手 -update([rider_id $riderId, state 2, accept_time time()]); if ($result false) { return json([code 400, msg 手慢了订单已被抢]); }更新后返回的行数为0即没抢到这是避免超卖的经典做法数据库行锁帮我们做了并发控制。这里的等待问题在ThinkPHP里由PDO的update操作自动处理不需要额外的队列和锁。取件完成state3、送达完成state4同理都是带where条件的状态流转更新。如果订单被骑手接受后又取消比如商家缺货需要区分是用户取消、骑手取消、还是超时未支付系统自动取消不同取消源走不同逻辑用户取消要原路退款骑手取消要标记骑手责任并可能扣信用分。在运营后台的订单列表里我通常会额外加一个操作列——「流转记录」。不是所有状态变化都改order表的state字段而是同时要写一张order_log表记录order_id、from_state、to_state、operator_type用户/骑手/系统/管理员和操作时间。这样出现纠纷时能翻出完整的操作轨迹不然用户说自己没下过单骑手说已经送达了你拿不出任何依据。4. Uniapp端实现用户下单、骑手接单与实时定位Uniapp的价值在于一套Vue语法同时编译成微信小程序、H5和App。跑腿系统真正复杂的不在界面而在两件事微信小程序的支付唤起用户端和骑手端实时定位上报App和微信小程序。4.1 用户端下单页与微信支付流程的封装用户端下单页无非是选地址、选类型、填备注、显示估算费用。前端要做的是把订单数据组装好调用后台create接口拿到orderId之后用uniapp的uni.requestPayment唤起微信支付或微信小程序内的wx.requestPaymentUniapp会自动转换。在支付结果处理上uniapp的requestPayment返回的成功并不代表支付一定成功——用户可能支付后立刻杀掉了App客户端收不到回调。所以后台必须实现一个订单状态查询接口让前端在支付成功后轮询或等支付回调来刷新订单状态。// api.js 统一封装请求 const BASE_URL https://yourdomain.com/api; export function request(url, method GET, data {}) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: Bearer uni.getStorageSync(token) }, timeout: 10000, success: (res) { if (res.statusCode 401) { // token过期跳回登录页 uni.navigateTo({ url: /pages/login/login }); } else if (res.data.code 0) { resolve(res.data.data); } else { reject(res.data.msg); } }, fail: (err) { reject(网络异常请检查网络); } }); }); }这段代码做了三层处理第一层是HTTP状态码401统一拦截因为token过期是所有会话型应用都会遇到的第二层是业务code错误信息提取这样调用处可以直接catch到中文错误提示第三层是fail回调里统一提示网络异常避免用户看到原始的connection refused之类的错误信息。在用户端创建订单的页面里调用create接口后拿到delivery_fee再显示给用户确认然后才拉起支付否则会出现预估费用和实际费用不一致时用户已经付款的纠纷。4.2 骑手端实时定位上报与后台运行监测骑手端的核心是实时定位。骑手App需要持续向后台发送经纬度让用户和运营后台能看到骑手轨迹。在Uniapp的App端使用plus.geolocation.watchPosition做持续监听微信小程序端则使用wx.startLocationUpdateBackground需用户授权并且要在manifest里声明requiredBackgroundModes为location。热词搜索里提到的“uniapp 后台运行监测定位 userlocationbackground”和“plus.geolocation.watchposition 配合 uni.startLocationUpdate”说明大家在这个功能上经常卡住。// 骑手端开启持续定位 // App端和微信小程序端需分别判断环境 if (uni.getSystemInfoSync().platform ios || uni.getSystemInfoSync().platform android) { // App端 plus.geolocation.watchPosition( (position) { const lat position.coords.latitude; const lng position.coords.longitude; uploadLocation(lng, lat); }, (err) { console.log(定位失败, err); }, { enableHighAccuracy: true, maximumAge: 5000, timeout: 10000 } ); } else { // 微信小程序端 uni.startLocationUpdateBackground({ success: () { uni.onLocationChange((res) { uploadLocation(res.longitude, res.latitude); }); }, fail: (err) { console.log(后台定位启动失败, err); } }); }App端这里有一个Windows风格的坑plus.geolocation.watchPosition的回调频率不是你能控制的有些Android机型在息屏或App切后台后回调会停止。常规做法是在manifest.json里添加requiredBackgroundModes: [location]同时要在App.vue里申请前台服务权限——Android上定位权限和高精度定位权限是两回事。还有一个学出来的教训后台定位的精度回调间隔太密会非常耗电但间隔太长又会让轨迹断裂。我一般会在恰当的位置设置一个节流变量只在上一次上报时间距今超过15秒时才调用uploadLocation同时上报时附带剩余电量让后台在低电量状态下把上报频率自动降到30秒一次。4.3 微信小程序打包超2MB的优化分包与代码瘦身用户端和骑手端打包成微信小程序后很容易撞上“source size 2612kb exceed max limit 2mb”这个限制。换一个角度看2MB上限是微信的老规矩但用Uniapp编译出来的包特别容易超限因为编译过程会把Vue运行时、Promise polyfill等公共代码打进主包。解决方案是分包加载这是所有Uniapp小程序项目的必修课。在pages.json里配置subPackages把骑手端、用户端的订单详情页、定位页面都放进去{ pages: [ { path: pages/index/index }, { path: pages/login/login } ], subPackages: [ { root: pagesRider, pages: [ { path: order-list/order-list }, { path: order-detail/order-detail } ] }, { root: pagesUser, pages: [ { path: order-submit/order-submit }, { path: order-pay/order-pay }, { path: order-track/order-track } ] } ] }分包的核心思想小程序启动时只加载主包pages下面的页面骑手端和用户端的其余页面在跳转时才去加载对应分包。需要注意的一个硬性规则分包不能互相引资源打包后的公共代码比如common目录里的js和css必须留在主包。另外地图组件高德/腾讯地图在Uniapp里是按需引入的但编译时地图SDK仍可能被打进主包建议把地图页面单独放一个分包同时在地图组件上设置lazy-load。还有一个让包体积缩一半的绝招在manifest.json的源码视图里关掉不需要的模块比如不用的支付渠道、统计SDK很多情况下超限是因为把推送、视频、分享这些模块统统编译了进去。热词里提到的“uniapp自定义分享好友”和“uniapp manifest配置”说的就是这个内容——manifest里配置的模块很多其实用不上留着只会让包变大。5. 本地联调与常见坑Fastadmin上传漏洞、ThinkPHP兼容、Uniapp不打印日志同城跑腿系统跨了三个技术栈每个栈都有自己的毛病。我按踩过的实际坑来排这几条能帮你至少省三天调试时间。5.1 Fastadmin上传文件漏洞必须处理的安全隐患Fastadmin历史上曝光过上传文件漏洞典型场景是后台附件管理被上传了可执行PHP文件导致服务器被拿权。根源是Fastadmin的附件上传接口在没有正确校验文件类型和文件名的情况下允许用户上传包含恶意代码的文件并且文件保存路径在Web可访问目录下。解决方案分两步第一步升级Fastadmin到修复后的版本第二步主动关掉不用的上传控制器或做二次校验。// application/admin/controller/Ajax.php 中重写upload方法 public function upload() { $file $this-request-file(file); if (!$file) { $this-error(未上传文件); } // 强制校验文件后缀白名单之外直接拒收 $ext strtolower($file-getExtension()); $allowed [jpg, jpeg, png, gif, webp, mp4, zip]; if (!in_array($ext, $allowed)) { $this-error(文件类型不允许上传); } // 上传路径放到一个禁止PHP执行的目录或加.htaccess规则 $info $file-rule(uniqid)-move(ROOT_PATH . public . DS . uploads); if ($info) { $this-success(上传成功, null, [url /uploads/ . $info-getSaveName()]); } $this-error($file-getError()); }记住一个关键点文件上传校验永远后端做永远用白名单而不要用黑名单黑名单永远有漏网之鱼。另外在Nginx配置里给uploads目录加一段location规则禁止PHP文件在这个目录执行location ~ ^/uploads/.*\.(php|php5|phtml)$ { deny all; }这样即使某个绕过校验的PHP文件被传上去了它也无法执行作为纵深防御可以兜底。5.2 ThinkPHP项目运行伪静态、PHP版本与调试开关Fastadmin基于ThinkPHP 5.x如果你本机或服务器用的是PHP 7.4以上版本很多项目跑不起来是因为Fastadmin老版本依赖的extend/fast/的一些函数比如each()函数在PHP 8.0被移除了。常见做法是开始之前先确认PHP版本和Fastadmin版本的匹配关系Fastadmin V1.2.0ThinkPHP 5.0.24配PHP 7.1最稳ThinkPHP 6配PHP 7.4/8.0也没大问题。如果你拿到的是老项目可以先在入口文件里加错误显示快速定位是否因语法兼容导致的白屏。运行阶段另一个高频问题是伪静态Fastadmin的URL重写不生效导致/index.php?s/admin/login变成404。Nginx下必须配好伪静态规则Apache下则需要开启mod_rewrite。我见过太多人把时间耗在“后台访问不了”上结果只是伪静态没配置或者重写规则里少了一个try_files语句。5.3 Uniapp不打印日志信息先确认是发布版还是开发版Uniapp项目经常出现console.log在真机调试时完全没输出的情况。原因通常有三个一是Uniapp在打包时会把console.log默认移除release模式这属于构建配置的问题二是手机系统版本对WebView的console输出做了拦截三是uni-app的日志输出到原生层而不是浏览器控制台你在HBuilderX的调试器里看不到。解决办法在manifest.json里把minify设为false或者在构建时用开发模式同时用HBuilderX真机运行不打包这样console会输出到HBuilderX的控制台如果真机运行都看不到日志那就直接在代码里用uni.showToast把错误信息弹出来暴力但有效。5.4 微信小程序打包超2MB后如何快速定位体积大头当遇到“source size 2612kb exceed max limit 2mb”时先别急着一通乱改在HBuilderX的发行-小程序-微信小程序选项里勾选“压缩代码”和“上传代码时自动去掉调试用的console”这一步通常能消掉20-30%的体积。如果还超用微信开发者工具的“代码依赖分析”功能看哪个npm包占了体积。跑腿系统最常见的大头是echarts或vant组件库——如果只是显示订单列表和地图不建议引UI框架Uniapp自带的组件够用了。如果你必须用vant把它也放进分包里主包只放登录页和首页这样基本都能压到2MB以内。5.5 骑手端定位后台不更新的排查清单骑手App切到后台后定位停止更新这个问题不能只靠一个watchPosition解决。排查时按步骤走第一步确认manifest里应用的权限声明是否包含ACCESS_BACKGROUND_LOCATIONAndroid 10以上必须在manifest里单独声明第二步确认是否调用了uni.startLocationUpdateBackground而不是uni.startLocationUpdate前者才是专门为后台场景设计的第三步检查手机厂商的白名单——小米、华为的系统在省电策略里会杀掉一切不带前台服务的App所以骑手端要在App里申请前台服务Foreground Service保活。连热词“uniapp实现app前台服务”都搜到了说明很多人栽在这里。前台服务的实现在Uniapp里没有直接的统一APIApp端要离线打包或用原生插件做云端打包的话需要选“Android前台服务”模块。但需要提醒的是由于各厂商后台限制差异极大骑手App的定位保活不存在通杀方案这时你在第2章做的状态机就派上用场了——即使定位没更新骑手手动点击“取件完成”“送达”也会触发一次位置上报配合状态流转不会造成业务卡死。6. 上线前做完这四件事少返工保收入这套系统要上线接真实订单我给你一套最直接的验证清单。第一用Mock数据跑通一条完整链路。自己在后台建一个测试骑手账号用户端下单一帮取任务骑手端接单、取件、送达然后看order_log表里的每一步记录是否完整、时间戳是否正确。这一步能同时验证状态机、日志、接口鉴权和前后端联调是性价比最高的一轮测试。第二用代理工具抓包看用户端支付流程。支付是最容易出黑匣子的环节。用户端在你手机上拉起微信支付后你把请求用工具记录下来确认回调接口有没有真正被微信服务器打到别只依赖客户端返回的成功。再测一次支付后杀进程、断网、延误回调的场景看订单状态会不会永远卡在待支付。如果卡住了那你要在后台加一个订单超时自动关闭的定时任务比如30分钟未支付自动取消并触发库存回滚和支付单作废。第三跑一次分账和提现演练。跑腿系统的收入来源主要是每单抽成和会员费。后台要给骑手设置提现规则比如满100元才能提现提现流程走一遍骑手发起提现→后台审核→打款→状态标记。注意资金相关操作绝不能用Fastadmin的普通编辑接口改字段必须单独写提现单和审核记录每一笔变动都要留下操作人。第四压测抢单接口。跑腿业务用刮刮乐式抢单高峰期可能同时几十个骑手在抢一单。我在第3章里写的条件更新就是为这个准备的但你仍然要测一下高并发下会不会死锁。用简单的ab或wrk工具压一下接单接口观察ThinkPHP的事务日志和数据库锁等待时间如果延迟过大回到代码里检查是否有在事务内请求外部接口比如推送、地图逆地理编码。回到我刚接这个项目时的教训我当时自己图省事把用户端、骑手端、后台三份代码放在同一个仓库里结果每次改后台都要重推一遍小程序联调时又经常因为分支混乱浪费半天。现在我会把三个端拆成三个独立项目后端拆成“API服务”和“管理后台”两部分骑手端的小程序包单独分包发布。这套习惯一直沿用到现在它让后期的迭代和问题定位都清晰很多。希望这一篇笔记能让你少走我走过的弯路祝你的跑腿系统早日上线赚到钱。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询