资讯详情 ThinkPHP+uniapp上门预约源码:从部署到订单并发控制实战
📅 2026/10/13 19:46:44
简介面向同城家政、上门按摩等本地生活服务场景的预约系统完整源码后端基于ThinkPHP与FastAdmin框架前端采用Uniapp实现微信H5和小程序端适合具备PHP基础、希望快速搭建可商用预约平台的开发者或创业团队。系统已完整跑通并通过测试包内包含公众号通知模块、后端主程序、前端Uniapp源码、数据库SQL脚本及通知音频文件运行环境为LinuxNginxphp7.2mysql5.6需安装fileinfo与redis扩展H5属于公众号H5须在微信内访问并配合认证服务号、微信支付、小程序账号完成部署。资源共2000个文件以JavaScript、Vue组件、Markdown文档为主辅以JSON、HTML、CSS等配置样式文件以及SQL、Python辅助脚本整体66.31MB目录按功能模块划分便于快速定位与二次开发。已有255人学习下载参照包内说明即可完成环境搭建、支付与服务号配置明显降低从零开发联调的踩坑成本。1. 为什么一套thinkphpuniapp的上门预约源码能把三个月工期压到两周接到一个上门家政按摩的需求时多数团队的第一反应是拆端用户端一个小程序技师端一个微信小程序后台一个管理系统再来一套H5做分享四端联动光联调就得一个月。而thinkphpmysqluniappfastadminuniapp这套组合的思路是后端只有一份PHP负责接口和后台前端只有一份uniapp代码同时编译成H5、微信小程序和App。后台管理用fastadmin二次开发RBAC权限、菜单管理、数据表格CRUD这些重复劳动直接点出来用。也就是说你真正要写的只有业务层的预约、派单、支付回调、结算这几个模块。这个方案适合的正是中小本地生活团队、接外包的独立开发者以及想用最小成本快速验证同城上门模式的初创团队。它不追求架构上的炫技追求的是从拿源码到跑通首单的速度。2. 选型逻辑这套技术组合凭什么能扛住上门预约业务2.1 thinkphpfastadmin为什么还是上门预约类项目的主力选型上门预约系统这类项目核心业务是订单而订单系统的首要要求是开发快、能维护、招人容易。thinkphp在国内中小型项目里的存量非常大中文文档和社区问答足够覆盖开发中遇到的绝大多数问题。对这个场景来说生态比性能重要——你招一个PHP开发大概率不用从零教框架你在搜索引擎里搜一个问题大概率能直接搜到别人踩过的坑。这就是为什么很多同城服务源码、外卖源码、预约源码都在thinkphp里做市场已经用脚投票了。fastadmin则是建立在thinkphp之上的后台开发框架。它解决的痛点和上门预约项目高度相关后台要管理用户、技师、订单、服务项目、优惠券、提现申请……这些本质上都是数据表格。fastadmin提供了一键生成CRUD的功能你在数据库建好表后台就能自动生成对应的菜单、控制器、模型和视图模板。对于技师审核、订单列表、资金流水这类管理页面几乎不用手写。我一般接手这类项目时第一步是先把fastadmin后台跑起来看它的菜单结构再决定哪些表直接生成CRUD哪些表需要写自定义接口。这比从零搭后台至少省一半工作量。还有一个关键点是权限。上门预约系统需要区分总管理、商家/门店管理、财务、客服这些角色。fastadmin自带RBAC权限管理每个角色分配菜单和操作权限这套机制是现成的。如果自己写角色-权限-菜单三张关联表加上中间的逻辑校验又是两三天的活。省下来的时间投到业务里去。2.2 uniapp一套代码覆盖H5、微信小程序和App的编译链路上门预约系统的前端有一个特点用户可能在微信里搜小程序进来也可能从朋友圈的H5链接进来技师端又往往需要装App。如果用原生开发这三端各写一遍维护压力巨大。uniapp的价值就在这里——一套vue语法的代码通过不同的编译配置分别产出微信小程序包、H5页面和原生App包。我在实际项目中一般把前端分成两个uniapp工程一个是用户端包含首页、服务分类、下单、订单列表、支付、评价另一个是技师端包含接单大厅、订单详情、导航、结算记录、提现。两个工程共用公共组件和接口封装但页面和登录逻辑分开。用户端用微信授权登录H5端用手机号验证码登录不同端之间的差异通过条件编译处理。条件编译是uniapp里比较重要的一个机制。比如支付逻辑小程序端需要调起wx.requestPaymentH5端走的是公众号支付或扫码支付。代码里可以这样处理// #ifdef MP-WEIXIN uni.requestPayment({ provider: wxpay, orderInfo: res.data.payParams, success: () this.loadOrderDetail() }); // #endif // #ifdef H5 window.location.href res.data.payUrl; // #endif这段代码的逻辑是编译到微信小程序时只保留MP-WEIXIN块调用小程序原生的支付接口编译到H5时只保留H5块直接跳转到支付链接。这样支付这种强平台差异的逻辑就分开了页面结构和业务逻辑则完全复用。新人在接这类项目时最容易出问题的地方就是分不清哪些代码是跨端通用的哪些是端特有的。我的经验是只管接口和渲染的代码尽量写通用一碰到登录、支付、地理位置、文件上传这类边界能力马上用条件编译隔离。2.3 mysqlredis的分工哪些数据必须走数据库哪些走缓存上门预约系统的数据可以分成两层一层是订单、用户账户、技师资料、交易流水这些是核心业务数据必须保证强一致一律放mysql另一层是验证码、token会话、在线状态、首页热门技师缓存这些允许短暂过期和延迟放redis是常规做法。mysql这边有一个值得注意的点所有的订单状态变更都应该用事务条件更新来保证安全。比如用户取消订单、技师接单、平台关闭订单三者的本质都是把某个状态的订单变成另一个状态必须确保同一时刻只有一个操作能成功。事务在这里是底线。redis的作用则更偏提速和限流。上门预约系统里最典型的redis应用是短信验证码的发送间隔控制同一个手机号60秒内只能发一次直接在redis里存一个key设置有效期60秒发送前先查这个key。另一个典型应用是技师端接单大厅的订单列表缓存——新订单写入mysql后同步写一条到redis列表技师端轮询接口优先读redis减轻mysql的查询压力。缓存和数据库之间的一致性我的做法是只在非关键路径上用缓存订单状态、账户余额这种敏感数据绝不先进缓存再落库直接写mysql读也直接读mysql。为省那么一点查询压力引入数据不一致的坑不值得。2.4 为什么不用微服务单体架构对这个业务到底够不够很多开发者拿到这类项目后会问订单、用户、支付、技师管理是不是应该拆成微服务这里要给个明确的结论同城上门预约的流量模型是典型的脉冲式峰值QPS大概率在几十到几百之间单体架构完全扛得住。拆微服务带来的服务发现、分布式事务、链路追踪这些问题对一支三五个人的开发团队来说是负担而不是收益。我倾向于这样看架构业务量级没到一个模块单独部署一台机器之前保持单体但把模块在代码层面拆清楚。比如fastadmin里的应用目录按模块组织admin后台、api接口、job队列各自独立逻辑上已经分离了。等哪天真需要拆先把api模块抽出来独立部署其他模块保持原样迁移成本是可控的。这套组合选型的最终理由是它能让你把有限的开发资源全部投到业务逻辑里而不是基础设施里。3. 把上门预约系统从源码跑成本地可用的完整步骤3.1 环境准备PHP版本、扩展和nginx伪静态上门预约系统的源码跑起来之前先确认本机环境。PHP建议用7.2到7.4之间的版本这既是thinkphp 5.x系列最稳妥的运行区间也兼容fastadmin第三方的绝大多数插件。需要用到的扩展包括pdo_mysql、redis、fileinfo、openssl、gd。其中fileinfo在部分环境下默认没开thinkphp的文件上传和图片处理会用到漏了会报Class finfo not found。redis扩展也常被忽略但验证码和token依赖它后面会专门说。PHP环境就绪后设置php.ini里的两个参数date.timezone Asia/Shanghai post_max_size 100M upload_max_filesize 20M时区必须显式配置。PHP默认是UTCmysql默认又是系统时区两个不一致订单创建时间、技师接单时间全都会歪8个小时查数据时翻车概率很高。upload相关的两个参数是为了后台上传服务项目图片、技师头像时不至于被大小限制卡住。接下来是nginx的伪静态配置。thinkphp的URL访问方式是入口文件index.php加上路由参数nginx默认配置下访问https://域名/order/detail?id1这类地址时nginx会尝试找order目录下的detail文件结果404。必须在server块里加上重写规则server { listen 80; server_name your.domain.com; root /path/to/your-project/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 30d; } }这段配置的逻辑是当请求的文件不存在时把路径交给index.php处理并作为参数s传递php文件由fastcgi处理静态资源单独设置30天缓存减轻接口服务器压力。参数方面root指向的是项目下的public目录而不是项目根目录这是thinkphp的安全要求public之外的目录不允许被直接访问。fastcgi_pass的端口要和你本机php-fpm的listen端口一致默认9000如果改动过要同步。3.2 导入数据库与后台初始化从SQL文件到第一个管理员账号源码包一般会带一个.sql格式的数据库备份文件。操作前先创建一个utf8mb4编码的数据库编码用utf8mb4是为了正确存储用户昵称里的emoji字符。然后导入SQL文件mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS house_service DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -uroot -p house_service house_service.sql命令说明第一条命令创建数据库第二条把备份文件里的表结构和初始数据灌入。导入过程如果报错常见原因是sql文件里包了创建数据库语句或者是mysql版本差异导致语法不兼容。遇到这种情况直接用可视化工具导入更省心导入后再确认一下表数量和数据量避免中途中断导致缺表。数据库导入后配置项目根目录下的.env文件或application/database.php。核心是数据库连接信息// application/database.php 中的连接配置 return [ type mysql, hostname 127.0.0.1, database house_service, username root, password your_password, hostport 3306, prefix fa_, charset utf8mb4, ];注意prefix这个参数它对应数据表前缀。fastadmin的数据表默认前缀是fa_例如fa_order、fa_technician。如果导入的表还是源码自带的前缀这里就不动如果手动改过表名前缀这里必须保持同步否则模型查询时找不到表。表前缀不一致是拿到新项目后最常见的启动报错原因看到table not exists先查这里。后台初始化在浏览器访问http://127.0.0.1/install.phpfastadmin会引导你设置管理员账号和密码。安装完成后后台入口一般是http://127.0.0.1/admin.php。管理员登录后第一件事是检查菜单管理是否完整其次是权限管理里的角色是否可用。如果登录后空白页或者报权限错误基本都是后台菜单缓存或权限缓存没生成清掉runtime目录下的缓存文件再刷新。3.3 前端uniapp编译到H5和小程序manifest配置与接口域名前端工程是uniapp项目用HBuilderX或命令行工具打开。编译到H5之前必须修改manifest.json里的配置。H5模块需要设置路由模式小程序模块需要填AppID——如果还没有小程序AppID在微信公众平台注册一个测试号即可。另一个容易漏的是小程序后台的request合法域名配置这个放到避坑章节详细说。接口请求地址统一封装在一个工具文件里下面是实际项目里常用的写法// utils/request.js const BASE_URL https://api.yourdomain.com; export function request(path, data {}, method POST) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Token: uni.getStorageSync(token) }, success: (res) { resolve(res.data); }, fail: (err) { reject(err); } }); }); }这个封装把三件事统一了接口前缀统一、token自动带上、返回结果统一走Promise。实际开发中所有业务模块都引用这个request函数不要在每个页面里直接调uni.request否则后续要改接口地址或者加统一错误处理时就得全局搜索替换。BASE_URL对应的是后端api模块的地址本地联调时先指向本机IP加端口编译发布时再改成正式域名。3.4 前后端联调打通从登录到首单的接口路径环境全准备好后按一条链路验证整个前后端是否打通用户登录、获取服务项目、提交预约、技师接单、生成订单。这条链路走通系统就算跑起来了。我一般会先在后端api模块里写一个ping接口验证网络链路// application/api/controller/Test.php public function ping() { return json([code1, msgpong, timetime()]); }前端浏览器或微信开发者工具请求http://localhost/thinkphp/public/index.php/api/test/ping能拿到JSON就说明后端可用。这里要提醒一点如果是本地联调H5浏览器直接请求http://localhost的php接口通常是通的但小程序开发者工具有自己的限制后面避坑章节展开讲。接下来按业务流程逐项测试手机号验证码登录、服务项目列表、提交预约单、模拟技师端接单。每测试一个接口都在mysql里查一下对应表的数据变化确认写库成功再进入下一步。前后端联调的常见问题十有八九出在参数名不一致上php端读的是tech_id前端传成technician_id接口返回参数错误。我的习惯是先打开php的DEBUG模式把真实报错暴露出来再逐个接口对齐参数名。联调通过之后再把DEBUG关掉避免线上暴露敏感信息。4. 上门预约系统核心业务订单状态、抢单逻辑与同城距离排序4.1 订单状态机让状态的每一次流转都有据可查上门预约系统的订单和标准电商订单有本质差异多了一个上门的物理过程。一次完整的服务要经过用户提交预约、平台或技师接单、技师上门、正在服务、服务完成待评价、结算完成中间还穿插用户取消、技师取消、超时未接自动取消等多种分支。把这个流程管理好订单模块就稳了一半。状态清单和允许的流转方向如下状态含义允许流转到0待支付1-待接单、-1-已取消1待接单2-待服务、-1-已取消2待服务3-服务中、-1-已取消3服务中4-待评价4待评价5-已完成5已完成无-1已取消无注意待支付可以直接跳待接单而不是经过已支付这个状态。这里是为了和支付回调对齐用户支付成功回调里直接把状态改成待接单同时更新支付时间和第三方流水号。把已支付省掉能减少一次状态流转也避免出现已支付但没进待接单的中间态。我在项目里见过这个坑问题不大但排查时很绕。状态流转的校验必须在后端做不能依赖前端传state直接update。一个简单的状态机控制类// application/common/model/OrderStatus.php class OrderStatus { const UNPAID 0; const PENDING 1; const TO_SERVE 2; const SERVING 3; const WAIT_COMMENT 4; const COMPLETED 5; const CANCELED -1; private static $flows [ self::UNPAID [self::PENDING, self::CANCELED], self::PENDING [self::TO_SERVE, self::CANCELED], self::TO_SERVE [self::SERVING, self::CANCELED], self::SERVING [self::WAIT_COMMENT], self::WAIT_COMMENT [self::COMPLETED], ]; public static function canChange($from, $to) { return isset(self::$flows[$from]) in_array($to, self::$flows[$from]); } }这段代码定义了订单状态的全集和每个状态允许跳转到的目标状态。所有修改订单状态的地方先调canChange校验不通过就直接拒绝。这样即使前端被改过、接口被恶意调用订单也不会进入非法状态。值得注意的是状态变更接口对外提供时只能接受当前状态目标状态订单号绝不能直接把整个订单对象传上来让你自由更新字段那是自寻死路。4.2 技师抢单的并发控制为什么直接update比先select再update可靠技师端接单大厅的核心操作是抢单而抢单的本质是并发竞争一个资源。两个技师同时看到同一个订单都点了接单如果代码写成先select查状态再update改状态就会出大问题。两个请求几乎同时读到status为1都认为订单可接然后各自执行update后执行的覆盖先执行的一个订单被两个技师同时接走。这属于典型的竞态条件。正确做法是单条SQL原子更新把状态检查放进update的where条件里// application/api/controller/Order.php 技师接单 public function accept() { $orderId $this-request-post(order_id); $techId $this-request-post(tech_id); $result Db::name(order) -where(id, $orderId) -where(status, OrderStatus::PENDING) -update([ status OrderStatus::TO_SERVE, tech_id $techId, accept_time time(), ]); if ($result 1) { // 影响行数为1说明本次更新成功订单归这位技师 return json([code 1, msg 接单成功]); } // 影响行数为0说明订单状态已不是待接单被别的技师抢了 return json([code 0, msg 手慢了订单已被接走]); }这个写法的精妙之处在于where里带了status条件数据库行锁在更新时会再次校验where条件只有状态仍是1的记录才会被更新返回值影响行数是1否则影响行数是0。不需要事务不需要redis锁一条SQL就把并发问题解决了。这是这类项目里最值得记住的写法比引入分布式锁简单得多也稳得多。再说一下redis锁在什么情况下才需要当一个操作涉及多张表的多个步骤且必须在整个过程中独占资源时才需要考虑锁。比如用户余额提现到微信钱包需要冻结余额、创建提现单、调第三方接口这串操作需要一把锁保护。而单纯抢单这种单表单字段的变更原子update就够了。不要把所有并发问题都甩给redis很多问题的正解就在数据库本身。4.3 同城距离计算经纬度排序在mysql里的高效写法同城两个字是这类系统的业务核心。上门预约服务里用户看到技师列表时一般有两个诉求只看到同一个城市的技师并且按照距离从近到远排序。城市过滤用city_id字段即可难点在距离排序。同城业务的覆盖范围通常在一个城市内部经纬度差异不大这时可以用haversine公式的简化形式在mysql里直接算距离并排序。下面这段SQL在几千技师的数据量下性能可以接受SELECT id, name, avatar, lat, lng, 6371 * 2 * ASIN(SQRT( POWER(SIN((:user_lat - lat) * PI() / 360), 2) COS(:user_lat * PI() / 180) * COS(lat * PI() / 180) * POWER(SIN((:user_lng - lng) * PI() / 360), 2) )) AS distance FROM fa_technician WHERE city_id :city_id AND status 1 HAVING distance 10 ORDER BY distance ASC LIMIT 20;参数说明user_lat和user_lng是用户当前定位的经纬度city_id是用户所选城市distance的单位是公里HAVING distance 10表示只显示10公里内的技师。这段SQL的代价是每条记录都做三角函数运算在技师表数据量超过几万时响应会变慢但同城业务单城市技师数通常不会到这个量级所以前期完全够用。如果技师数增长到十万级再考虑引入按经纬度网格分区或使用空间索引不要一开始就上重型方案。数据量上来后常见的优化手段是粗筛后精算第一步用经纬度差做一个方形框过滤把计算限定在一个小矩形区域里第二步只对框内记录做精准的距离计算。方形框的边界值用固定比例估算一个经度约111公里纬度约111乘以cos纬度公里来算。这个优化可以直接在一开始就写进SQL里把三角函数计算量降一个量级。5. 上门预约系统避坑记录5个上线前后最容易翻车的现场5.1 小程序请求接口直接失败连失败回调都不触发现象H5端一切正常换成微信开发者工具后所有接口请求都报request:fail。原因微信小程序对网络请求有域名限制正式环境要求request的url必须是HTTPS且在微信公众平台后台配置过request合法域名开发环境下虽然可以勾选不校验合法域名但前端代码里的接口地址如果是http://localhost或http://192.168.x.x这类本机地址在真机预览时依然会被拦截。解决本地调试时在微信开发者工具的详情设置里勾选不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书要真机预览则需把接口部署到一台有公网地址且已配置HTTPS证书的服务器上并在小程序管理后台添加合法域名。另外注意小程序要求TLS版本不能低于1.2部分老服务器上自签证书或旧openssl配置会在这里卡住。5.2 thinkphp在nginx下访问任何非首页路由都404现象首页能打开但访问http://域名/order/list这类地址时nginx直接返回404后端接口全挂。原因nginx没有配置thinkphp的伪静态规则nginx在找不到对应文件时按自身默认行为处理没有把路径转交给index.php。解决在nginx的server配置里加上rewrite规则核心是if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; }。改完配置后reload nginx不要忘记。还有一个容易踩的细节点thinkphp的入口文件是public/index.php如果root配置指向了项目根目录而不是public目录即使rewrite正确也会报错因为index.php不在根目录下也等于把非public目录里的文件暴露了出去。5.3 mysql时间差8小时订单时间、技师服务时间全错位现象后台看到的订单创建时间比实际时间慢8个小时技师端显示的预约时间也偏移。原因PHP的默认时区是UTCmysql的默认时区取决于服务器系统配置。PHP写入的时间戳在mysql里显示出来会因为时区换算产生8小时偏移。解决先确保php.ini中配置date.timezone Asia/Shanghai再确认mysql的时区设置可以执行SQL查询select now();如果结果和服务器本地时间不一致在mysql配置文件的[mysqld]段加一行default-time-zone08:00。两个地方都对齐之后再测试一遍。这个问题玄学的地方在于本地开发可能不出现一部署到云服务器就出现因为云服务器默认时区往往是UTC。5.4 技师端轮询接口打垮服务器没有增量拉取的抢单大厅现象上线第一天技师端接单大厅接口的响应从50毫秒涨到3秒服务器CPU持续跑满mysql连接数被打满。原因技师端每3秒轮询一次订单列表每次都返回全量待接单订单几百个技师同时轮询瞬间打出的查询量远超mysql承受能力。解决轮询接口改成增量拉取。前端记录上一次请求返回的最大订单ID下一次请求带上这个ID后端只返回ID大于它的新订单和状态有变更的订单。这个方案能在不引入websocket的前提下把服务器压力降低一个量级。改造后同样规模的并发接口响应时间恢复到100毫秒以内。这里要特别强调不要一上来就上websocket方案先把轮询的增量逻辑做对大多数同城预约场景的实时性要求根本用不上长连接。5.5 并发抢单出现一单多接先查后改的代码写错了位置现象两个技师都收到接单成功的通知后台查询订单发现tech_id字段被后操作的技师覆盖。原因接单接口写成了先select查询订单状态、判断可接、再update更新状态的三段式。两个并发请求同时读到status为待接单都通过了判断先后执行update后执行的覆盖先执行的。解决按第4章讲的方式改写为单条update语句将状态判断放进where条件用影响行数判断是否抢单成功。代码改完后可以做一次并发压测验证简单方式是用两个浏览器同时点接单按钮也能复现。这是一条典型的血泪经验我在早期项目里踩过直接在线上出现过一单多接的运维事故。修完之后要同步检查其它可能的并发点用户余额扣减、优惠券核销、技师时间表占用凡是存在先查后写模式的地方一律改成条件更新。6. 上线前的最后一道工序把演示源码变成能商用的可用系统演示源码跑通和真正对外接单之间还隔着几道不得不做的工序。第一道是支付和结算确认微信支付/支付宝的小程序商户号配置在后端配置项里支付回调地址必须是公网HTTPS地址回调逻辑要处理支付成功但订单状态没更新的重复通知——支付回调可能被微信多次推送幂等处理必须做。第二道是把用户协议、隐私协议、技师入驻协议加上这是这类平台的合规底线。第三道是接口的鉴权和限流后台登录、下单、提现、验证码发送这几个接口至少要加频率限制redis计数器就能实现避免被刷。真正进入运营后有个技巧值得优先投入把技师端抢单做成可配置的策略。前期单量少可以先用平台派单——管理员在后台直接把订单分配给指定技师单量上来了再切先到先得的抢单模式。这个开关的核心逻辑就是一个配置项// 配置项dispatch_mode值为manual表示后台派单compete表示抢单 $mode config(site.dispatch_mode); if ($mode manual) { // 后台指派管理员选技师接口直接绑定订单 Db::name(order)-where(id, $orderId) -where(status, OrderStatus::PENDING) -update([status OrderStatus::TO_SERVE, tech_id $techId]); } else { // 抢单模式走第4章的原子更新逻辑 }参数说明dispatch_mode这个配置放在后台的系统配置里运营人员可以不用改代码直接切换。不同模式下前端技师端的交互也要配合——抢单模式显示立即接单按钮派单模式只显示订单已指派的提示。这是从源码走向商业运营最值得做的改造之一能让运营策略的试错成本降到最低。我自己的经验是做这类系统卡住进度的从来不是技术难点而是以为演示能跑就等于能上线的心态。演示源码解决的是从无到有的问题从有到可用还需要把并发、支付、合规这三件事挨个磨一遍。每多提前一天处理这些问题上线后就少一分被用户骂的风险。希望这篇笔记对你有点帮助。本文还有配套的精品资源点击获取