简介:一份基于ThinkPHP、FastAdmin与uniapp技术栈的同城上门家政按摩预约系统源码,面向需要搭建同城上门业务的家政服务商、PHP开发者和小程序研发人员。系统后端采用FastAdmin框架搭建,前端使用uniapp进行跨端开发,覆盖预约下单、服务分类、订单管理、微信支付、公众号通知等核心闭环;资源包提供后端主程序、前端完整源码、数据库SQL脚本、公众号通知模板及音频文件,测试通过可直接部署,但需要Linux配合Nginx、PHP7.2与MySQL5.6环境并安装fileinfo、redis扩展,同时准备备案域名、认证服务号、微信支付及小程序账号,H5版本需在微信内访问。压缩包共2000个文件、66.31MB,主要文件类型包括JS脚本865个、Vue组件546个、Markdown文档350个、JSON配置91个、HTML页面78个、CSS样式53个,另有SQL备份与辅助脚本,目录结构清晰便于按模块查阅。已有255人浏览学习,适合具备PHP与uniapp基础的开发者,可完整理解同城上门预约系统的项目结构、接口设计、支付回调与公众号通知流程,并快速二次开发或上线运营。
1. 同城上门预约系统:用 ThinkPHP+uniapp 搭出的家政按摩预约闭环
很多人以为上门预约系统最难的是地图和支付,真上线后才发现最难的却是“同一技师的时段重叠”和“取消单后状态回退”这种细节。这套基于 ThinkPHP + MySQL + FastAdmin + uniapp 的同城上门家政按摩H5小程序源码,属于典型的 O2O 预约闭环:C端在 H5/小程序里选服务、选技师、下单,后台排单,服务完成后结算。适合手里有本地服务资源、想快速上线验证业务的人,也适合接单做二次开发的 PHP 工程师。下文我会把它拆到表结构和接口层面,给出可复现的步骤、参数和坑,让新手能跟着跑通,熟手能直接拿到调参边界。
2. 技术栈拆解:FastAdmin 与 uniapp 在这套源码里的分工
2.1 FastAdmin:不是框架,而是一套自带权限与 CRUD 的后台底座
先讲清楚 FastAdmin 在这个项目里的角色。它本质上是 ThinkPHP 5.1 之上的一个后台应用脚手架,自带管理员登录、RBAC 权限控制、菜单管理、系统配置,以及最值钱的一键 CRUD 生成。上门预约这种业务的后台要管的东西非常可预测:服务项目、技师信息、预约单、会员、评价。这些全部是标准的列表+表单+详情操作,用 FastAdmin 的生成器能省掉大量重复代码。
我选择往 FastAdmin 而不是裸 ThinkPHP 靠,核心考量就是时间。预约系统的业务重头在前台下单流程和接口,后台只要能管数据、能分权就行。FastAdmin 自带的一键生成菜单和权限绑定,让“加一张表”这个操作在 5 分钟之内完成,这是裸框架做不到的。代价是你要遵循它的 admin 表、auth_group 表这套约定,新写模块要挂在独立前缀下,别乱改它的基类,否则后台菜单会和权限对不上。
这套源码后台的典型界面结构是:左侧菜单树,每个菜单项对应一个数据表模块,右侧是表格搜索筛选,点操作列进入编辑。菜单顺序基本就是数据库表的顺序,所以你初次打开后台,先看菜单排序就能猜到业务模块的优先级:通常服务管理、技师管理、订单管理排在最前面。FastAdmin 里每个控制器对应一个菜单节点,路由地址写成 controller/action 的格式,这一步出错会直接导致“后台点击菜单打不开”。
2.2 uniapp 负责的一端:一套代码同时出 H5 和小程序
前端这部分源码一般是一个独立的 uniapp 工程,目录里有 pages 目录按业务模块分包、common 目录放请求封装、static 目录放静态资源、manifest.json 配置 appid 和 H5 路由。uniapp 的编译机制决定页面逻辑写一次,最后产物可以是 H5 页面,也可以是小程序包。对家政按摩这个业务来说很实际:用户不愿意为了按一次摩单独下载 App,小程序是流量入口,H5 用于微信内分享和广告落地页,两端共用同一个下单流程。
注意边界:uniapp 不是没有代价。小程序里很多能力要靠条件编译判断,比如微信小程序的登录要走 wx.login 拿 code 再换 openid,H5 则走手机号验证码或账号密码。支付也一样,微信支付在小程序端和 H5 端的参数来源不同。这类源码会在 common 目录的请求文件里用条件编译分开处理,比如下面这样:
// #ifdef MP-WEIXIN // 小程序端登录逻辑 uni.login({ provider: 'weixin', success: (loginRes) => { // 把 code 发给后端换取 openid 和 token } }) // #endif // #ifdef H5 // H5 端走账号密码或短信登录 // #endif这里必须提醒一句:公共请求方法里千万不要写死某一种端的逻辑,否则换端发布必挂。很多外包项目翻车就是改了一个端忘了另一个端,小程序正常、H5 登录报错,查了半天发现是公共文件里被写入了 wx 对象。H5 环境里根本没有 wx 全局对象,一处没包条件编译,整页白屏。
2.3 数据库设计:五张核心表的字段与关联关系
上门预约的核心数据模型逃不出这几张表:用户表(C端会员)、技师表(服务提供者)、服务项目表(按摩/家政的具体品类)、预约订单表(主表)、订单状态日志表(跟踪状态变化)。我建议先看技师表和服务项目表,再看订单表,因为订单表的外键都指向它们。
技师表比较关键的两个字段是 status(是否可接单)和经纬度字段 lng、lat,因为“同城”与“上门”的匹配大部分时间靠距离筛选。服务项目表至少要有标题、主图、价格、时长、分类 id。预约订单表是核心,常见结构是这样的:
CREATE TABLE `appointment_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL DEFAULT '' COMMENT '订单号', `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '下单用户ID', `technician_id` int(11) NOT NULL DEFAULT '0' COMMENT '技师ID', `service_id` int(11) NOT NULL DEFAULT '0' COMMENT '服务项目ID', `appoint_date` date NOT NULL COMMENT '预约日期', `appoint_time_start` time NOT NULL COMMENT '预约开始时间', `appoint_time_end` time NOT NULL COMMENT '预约结束时间', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待支付 1待服务 2服务中 3已完成 4已取消', `address` varchar(255) NOT NULL DEFAULT '' COMMENT '上门地址', `contact_name` varchar(30) NOT NULL DEFAULT '' COMMENT '联系人', `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话', `pay_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支付方式', `pay_time` int(11) NOT NULL DEFAULT '0' COMMENT '支付时间', `finish_time` int(11) NOT NULL DEFAULT '0' COMMENT '完成时间', `cancel_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '取消原因', `create_time` int(11) NOT NULL DEFAULT '0', `update_time` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_technician` (`technician_id`, `appoint_date`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单表';注意几个设计细节。appoint_date 和 appoint_time_start 分开存,而不是合并成一个 datetime,原因后面第 4 章讲冲突检测时会用到:按日期+时间段查询比按时间戳范围查询直观,索引也更友好。status 字段用整形不用字符串,方便程序里做枚举判断,也方便后台搜索下拉。订单号必须唯一,一般用时间戳加随机数生成,避免并发重复。
最后是订单状态日志表,它不在业务主链路里,但绝不能省。字段大致是:id、order_id、old_status、new_status、operator_id、operator_type(用户/后台管理员/系统)、remark、create_time。它的作用是记录订单每一次状态跳变的轨迹,用户申诉时能翻旧账,后台做数据统计时能精确知道“已取消”的订单是因为用户改主意还是技师拒单。我见过不少项目上线后才补这张表,结果历史订单根本无从追溯,这种后悔药没得吃。
3. 本地跑通这套源码:从 FastAdmin 后台到 uniapp H5 联调
3.1 环境准备:PHP 版本、MySQL 扩展与伪静态基础
先说环境,这套源码后端基于 ThinkPHP 5.1 和 FastAdmin,常见运行要求是 PHP 7.1~7.4,MySQL 5.7,Web 服务器推荐 Nginx。本地开发用 Apache 也没问题,但伪静态规则要换成 .htaccess 版本。PHP 必须开启的扩展有:pdo_mysql、curl、openssl、fileinfo、mbstring。其中 fileinfo 是最容易漏的,漏了的话 FastAdmin 后台上传图片会直接报错,提示找不到某个 fileinfo 相关方法,但你的代码明明没问题。
本地开发我一般直接用集成环境,创建站点时把站点根目录指向 FastAdmin 的 public 目录。原因很简单:ThinkPHP 的入口文件在 public/index.php,如果站点根目录指到项目根目录,用户可以直接访问 application 目录下的文件,存在路径泄露风险,而且伪静态规则也会错乱。在集成环境里设置 ThinkPHP 伪静态,Nginx 下的核心规则先提前验证,因为生产环境也用同一份:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=/$1 last; } }这条规则的作用是把不存在的文件路径全部交给 index.php 的 s 参数处理,ThinkPHP 拿到参数后分发到对应控制器。很多运到本地一打开就 404,八成是伪静态没生效,先确认这条规则在站点配置里,再考虑是不是路由问题。确认方法很简单:在浏览器访问 public 下的静态资源路径,能打开说明站点正常;访问业务链接 404,那就是 rewrite 没起作用。
3.2 导入数据库与后台菜单初始化
源码包一般会带一个 .sql 文件,放在 install.sql 或 database 目录下。常见做法是新建一个数据库,比如 fa_massage,然后导入 sql 文件。导入完成后修改 application/database.php 里的配置。FastAdmin 安装时填写的数据库信息会写入这个文件,我一般直接打开确认三项:hostname、database、password。
导入之后打开后台,地址通常是 http://你的域名/admin.php,默认账号是 admin。FastAdmin 的菜单和权限是存在数据库里的,首次导入后菜单会自动带出。如果你看到后台是空白页,先看 runtime 目录是否可写,再把调试模式打开,FastAdmin 的 config 里 app_debug 默认是 false,改成 true 能看到具体报错堆栈,这是排查后台白屏的第一手段,比瞎猜快得多。
这一步常见的坑是 sql 导入中途报错“Unknown collation”或者 utf8mb4_unicode_ci 不识别。原因多半是本地 MySQL 版本过低,5.5 及以下不支持部分排序规则。解决办法是全局搜索 sql 文件里的 utf8mb4_unicode_ci,替换成 utf8mb4_general_ci,或者直接把数据库版本升到 5.7。版本问题别硬扛,本地环境用新版 MySQL 可以省掉很多这类诡异报错。
3.3 uniapp 端修改接口地址并跑通 H5 登录
前端工程用 HBuilderX 导入,源码目录一般是 /backend(FastAdmin 后端)和 /src 或 /frontend(uniapp 前端)。先找到全局请求配置文件,通常叫 common/config.js 或 common/request.js,里面有一项 baseUrl,这个值必须改成本地后端的可访问地址。给你一个这类源码里最常见的请求封装片段:
// common/request.js 典型封装 const BASE_URL = 'http://localhost:8080' // 本机后端地址,联调时改这里 function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'token': uni.getStorageSync('token') || '' }, success: (res) => { // 后端统一返回 {code: 1, msg: 'ok', data: ...} 这种结构 if (res.data.code === 1) { resolve(res.data.data) } else if (res.data.code === 401) { uni.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => reject(err) }) }) } export default { request }这里要说明两点。BASE_URL 必须是完整的协议+域名+端口,不能只写域名不带 http,uniapp 在 H5 端会直接拿它发起请求,写错会报“request:fail”。header 里 token 的字段名必须和后端中间件里读取的字段一致,常见的是 token,也有用 Authorization 的,建议先去后端 api 的基类控制器里看它读哪个字段,再决定这里写什么。这两个问题都是联调阶段最高发的,现象都是“请求报错”或“登录后依然提示未登录”,原因却完全不同。
跑 H5 时还有个跨域问题。直接用 HBuilderX 内置浏览器运行,请求本地后端会出现 CORS 跨域,解决方法是后端入口处加跨域头,或者改用带跨域参数的 Chrome 启动模式。我一般更愿意加后端跨域头,因为后面上线 H5 部署在同域下这个头也不会有负面影响,反而联调阶段省事。跨域头的具体写法在第 5 章排查部分会再展开,这里先知道根因是浏览器的同源策略就行。
4. 上门预约核心链路:服务项目管理、下单状态机与时段冲突检测
4.1 后台服务项目管理:FastAdmin 的 CRUD 生成器用法
服务项目这块在后台就是标准 CRUD 模块。用 FastAdmin 的命令行生成器,一条命令就能生成控制器、模型、验证器和对应 JS 文件:
php think crud -t service -c service/Service -a add,edit,del,multi-t service 是数据库表名 service,-c service/Service 是生成到 application/admin/controller/service/Service.php,后面 -a 后面跟允许的操作。如果 -c 参数写错,生成的文件会落到错误目录,菜单路由就找不到控制器,表现就是后台点击模块报 404。所以生成前先确认表名和控制器路径的对应关系,别随手敲。
生成完去后台菜单管理里添加菜单,路由地址填 service/service 才能正确绑定权限。这一步值得展开的是字段类型判断:FastAdmin 的生成器会根据数据库字段类型自动生成对应表单控件,varchar 生成输入框,text 生成文本域,tinyint 生成下拉或开关。对大多数运营需求来说默认控件够用了:服务项目名称、价格、时长、排序、状态这些字段,后台编辑反正是给运营人员用的,交互朴素一点反而好维护,别一上来就想搞自定义富文本和拖拽排序,那会给后续升级埋雷。
4.2 下单接口与订单状态机:从待支付到已完成的五次跳转
下单接口的职责不只是 insert 一条订单记录,还要做三个前置校验:用户登录态有效、服务项目存在且已上架、技师当天该时段可预约。这类源码里下单接口一般写在 api 侧的 Order 控制器,参数通常包含 service_id、technician_id、appoint_date、appoint_time_start、address、contact_name、contact_phone。传参里 service_price 这种东西不允许客户端传,价格必须后端根据 service_id 反查,否则用户改个参数就能用低价下单,这是支付相关接口的底线。
状态机是预约系统最容易改乱的部分。先把所有状态枚举写清楚再写逻辑,常见定义:0 待支付、1 待服务、2 服务中、3 已完成、4 已取消。流转规则是:下单成功=0,支付成功变 1,技师点击开始服务变 2,服务结束变 3,用户或后台取消变 4。在代码里用映射表强制校验,不接受任意状态直接互转:
// Order.php 中状态流转的核心判断 $order = Db::name('appointment_order')->where('id', $id)->find(); $allowedMap = [ 1 => [0], // 待服务,只能从待支付来 2 => [1], // 服务中,只能从待服务来 3 => [2], // 已完成,只能从服务中来 4 => [0, 1], // 已取消,待支付和待服务都可以取消 ]; if (!isset($allowedMap[$newStatus]) || !in_array($order['status'], $allowedMap[$newStatus])) { return json(['code' => 0, 'msg' => '非法状态变更']); }状态变更必须配合事务:改状态和写日志要在一个事务里,否则中间崩了,日志和前台进度显示就对不上。这是很容易翻车的点,很多人只改 status 忘了写日志,用户一直看到“待服务”,后台看却已经是“已完成”,其实就是日志表没落库,前端读的是日志历史字段。所以说订单状态日志表不只是审计用的,它还直接决定了前端页面上的状态展示来源,不能省。
4.3 时段冲突检测:同一技师同一日期时段不能重叠
这是上门预约里面技术含量最高、也最像“看起来简单做起来玄学”的地方。订单表存的是日期加起止时间,冲突检测就变成区间重叠判断。两个预约区间 [a, b) 和 [c, d) 冲突的条件是:a < d 且 c < b。写成 SQL 就是:
-- 检查技师在某个日期时段是否已有预约 SELECT id FROM appointment_order WHERE technician_id = ? AND appoint_date = ? AND status IN (0,1,2) AND appoint_time_start < ? /* 新单结束时间 */ AND appoint_time_end > ? /* 新单开始时间 */ LIMIT 1;这条 SQL 把区间重叠判断压缩成两个半开区间比较,不用遍历技师当天的每一单来判断。参数顺序特别容易写反:第一处 < 的右边应该是新单的结束时间,第二处 > 的右边应该是新单的开始时间。写反了要么查不出冲突导致技师被重复预约,要么把换班的空闲时段全部误杀,用户看着明明有空却下不了单。我强烈建议写完这条 SQL 后手工造两条重叠数据验证一次,再写自动化测试用例,这种问题靠眼睛看很难发现。
提示:默认状态里包含 0(待支付),意味着用户下单还没付款也占着时段。如果业务想提高转化率,可以在待支付超过 15 分钟后自动释放时段,用定时任务把超时未付款订单改成已取消。这个功能要单独加,别把状态机写得太死。
这里的边界细节还要说透。如果业务允许的预约时长是整点,存 time 类型没问题;如果允许任意分钟数,查询时要考虑跨天边界。比如晚上 23:00 开始服务到次日 1:00,appoint_date 存的是开始那天的日期,但冲突检测要查两个日期,否则技师第二天的第一单会被漏掉。对这种复杂场景,我宁可把起止时间额外存成 int 时间戳,再保留 appoint_date 做界面展示,查询冲突时直接比对时间戳范围,虽然 SQL 没那么文雅,但最不容易出错。
5. 生产部署与常见问题排查:伪静态、跨域、时区与支付回调
5.1 生产环境部署清单:Nginx 伪静态与 PHP 配置项
上线第一步永远是伪静态。绝大多数用户访问预约系统走的是 H5 域名首页,如果 Nginx 配置里没有正确配 ThinkPHP 的 rewrite,会出现首页打不开、二级页面 404、刷新页面 404 这三种现象,原因都一样:rewrite 规则没生效或写错位置。
Nginx 里修改站点 server{} 内的 location / 配置后必须跑 nginx -t 检查语法再 reload。很多人改了配置文件忘了 reload,然后反复查程序代码查半天,最后发现是环境没生效。这是典型的“程序没错、环境没生效”的坑。PHP 侧还有个坑是 upload_max_filesize,后台传服务项目图片会受它限制,默认 2M 经常不够用,建议调到 20M 左右,同时改 post_max_size 和 memory_limit。这三项是配套的,只改一个等于没改,传大图依然会失败。
生产环境还要注意 runtime 目录权限。FastAdmin 的日志、缓存、临时文件都写在 runtime 下,如果目录属主不是 www 用户,页面会白屏或报“runtime directory is not writable”。排查方法很简单:看 web 运行用户是谁,然后 chown 给对应用户。这个问题在从本地复制代码到服务器时特别常见,本地是 root 跑的,服务器是 www 跑的,一上来就白屏。
5.2 前后端接口 404、跨域与 token 失效
接口 404 在下单环节最常见。现象是后台页面正常,uniapp 请求某个接口返回 404。先分清是 FastAdmin 后台路由还是前台 api 路由问题:后台 404 一般是控制器没生成或菜单没挂全;前台 404 多半是路由规则里没有对应的方法。这类源码的 api 路由一般在 application/api/config.php 里做了路由映射,下单 url 如果写的是 /api/order/create,但控制器的方法名不是这个,就会 404。排查方法很直接:用浏览器直接访问这个接口地址,看返回什么,能快速定位到是路由还是参数问题。
跨域问题更多出现在 H5 联调阶段,现象是请求发出去但控制台显示 CORS 错误。最佳处理方式是在后端 api 入口的公共基类里统一加响应头:
// 公共基类 Base 中统一设置跨域响应头 public function _initialize() { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: token, Content-Type'); if ($this->request->isOptions()) { exit(); } }注意最后那个 OPTIONS 处理。浏览器在发真实请求前会先发一个 OPTIONS 预检,如果后端不直接返回 200,后续请求会被拦截。这条是 H5 联调跨域问题里最容易被漏掉的:加了 Allow-Origin 但没处理 OPTIONS,控制台依然报错。生产环境如果把前端 H5 和后端部署在同一个域名下,跨域头可以去掉,省得接口暴露给第三方任意调用。
token 失效是另一个高频场景。现象是用户用着用着突然所有接口都报“请先登录”。这类系统通常把 token 存在 Redis 或 MySQL 表里,有效期设置几小时到几天。前端请求带 token,后端中间件校验。排查时先确认服务器时间和本地时间是否一致,时间差超过有效期会导致莫名登录过期;再确认 Redis 配置是否正确,如果 Redis 挂了而 token 校验依赖它,接口表现就是时而好时而不好的玄学卡顿,后台报错日志里全是连接超时,这时候别怀疑代码,先重启 Redis 再看。
5.3 订单时间异常:时区、date 函数与入库数据的三种差异
时区坑特别隐蔽,尤其部署在 UTC 默认时区的服务器时。现象是用户下午 4 点下单,后台显示下单时间是凌晨或前一天。根因是 PHP 默认时区没设置,或者 MySQL 的 time_zone 和 PHP 不一致。我一般的处理方式是三层统一:PHP 的 php.ini 设置 date.timezone 为东八区;FastAdmin 的 config 里也有默认时区配置,确认它没有被业务代码覆盖;MySQL 连接后执行 set time_zone = '+8:00'。三层只要有一层不一致,时间就乱。
排查方法是找一条订单,对比后台显示时间、数据库原生查询结果、PHP 代码里直接打印的当前时间,三个时间放在一起看就能定位是哪一层在捣乱。还有一类订单时间问题是“已完成订单的 finish_time 为空”,原因是服务完成接口里只 update 了 status,没顺手更新 finish_time,或者日志记录的是另一个字段。我的习惯是:任何状态变更接口,都把状态、时间字段、日志写入放在同一段代码里完成,不要拆成两步。拆开了,前面成功后面失败,数据库里就留下永久的半完成数据,这些脏数据后面对报表统计会造成连锁污染。
6. 把它推向运营:技师结算、评价闭环与二次开发的方向
这套系统的商用价值不在订单状态机本身,而在技师结算和评价闭环。结算逻辑常见做法是后台每日生成一张结算单,按订单实际完成数乘以服务项目结算价,再扣掉平台抽成。这里提醒一点:结算必须以订单日志表里“已完成”的日志时间为准,不要用订单表的 finish_time,后者可能因为人工补单而失真。评价就挂在订单上,一个订单只能评价一次,前端评价入口在订单详情页的“待评价”状态,评价完成后再算技师评分汇总,平均分建议取最近 30 天,别拉全量历史,否则老技师分数永远下不来,新技师又没有参考分。
二次开发我最推荐先做两件事。一是给预约时间加“时段表”,把一天切成固定时段来卖,替代目前这种任意时间选择,能显著降低冲突检测和排班的复杂度,用户也不用琢磨选哪个时间点合适。二是做一个简单的会员储值或次卡功能,家政按摩这类高频复购业务,储值能极大提高用户粘性,实现难度也只比现有订单表多“余额扣减”和“充值流水”两张表,后端控制好扣款事务和负数校验就行。
最后说个教训:这类源码最怕的是上线后不停堆“临时改需求”,加“技师自选服务范围”、加“多人拼单”,每加一个字段都要回到订单表看索引和查询语句是否还撑得住。同城业务一旦用户量上来,订单表迟早过百万行,那时再用 appoint_date 做范围查询就会明显变慢,必须提前按月份分表或者做数据归档。我见过不止一个项目在订单表几百行的时候跑得飞快,到几万行开始变慢,才回头补索引,那种血泪经验很不值得。希望这篇能帮到你,在做同城预约方向时把该避的坑提前绕过去。
本文还有配套的精品资源,点击获取