做场馆预订系统,我这些年经手的需求真不算少。羽毛球馆、篮球场、共享会议室、自习室,甚至一些团建场地,核心诉求大同小异:用户要能在线看场次、选时段、下单支付,运营方要能管场地、管订单、看营收。最近整理了一套 PHP + UniApp 组合的智能场馆预订系统源码,把后端接口、后台管理和前端多端打包整个链路都跑通了,代码可以直接部署成微信小程序、支付宝小程序、H5 网页和安卓 App。这篇文章就是我从设计到落地的完整记录,表结构、核心 API、前端关键页面、多端打包、踩坑点都摊开讲,给想用低成本技术栈做场馆预约的朋友一份能直接照做的参考。
为什么我对这个组合特别有感触?因为这两年找我做预约类小程序的人越来越多,多数项目预算不高、周期又紧。PHP 做后端接口,UniApp 做前端多端编译,恰好把“开发成本”和“业务复杂度”压到了平衡点。很多朋友一听 PHP 觉得老,但老有老的好处:部署门槛低、资料多、云服务器上随便一套 nginx + php-fpm 就能跑;UniApp 则解决“今天要小程序、明天要 H5、后天又要 App”的反复需求,一套 Vue 语法代码多端编译,比原生开发省事太多。
1. 项目定位与技术选型:为什么是PHP+UniApp组合
1.1 这套源码到底解决什么问题
我整理这套场馆预订系统,业务原型是很常见的场景:一个场馆下有多个场地,比如羽毛球馆有8片场地,共享办公区有3间会议室;用户打开小程序,选日期、选时间段,能看到某片场地在某个时段是否空闲;选定后下单支付,到店后直接核销入场。后台要做的则是维护场馆和场地信息、设置不同时段的价格、查看订单流水、处理退款。
这套系统里“智能”主要体现在三块:一是库存自动管理,每个场地每天按预设时段生成库存,可约、锁定、已售状态实时更新;二是超时订单自动释放,用户下单后没付款,倒计时结束库存自动放回,不浪费场地资源;三是防并发超卖,同一个时段同时有两个人下单,系统只允许一个人锁单成功,另一人只能换时间。实现了这些,场馆线上预订的核心链路就算完整了。
1.2 PHP和UniApp的分工边界
用生活里的例子打比方:UniApp 是前台和菜单,PHP 是后厨,API 接口就是传菜窗口。用户看到的页面、点击的逻辑,归前端管;场地到底可不可订、订单金额怎么算、支付结果怎么确认,全部交给后端。两端通过 JSON 数据格式互相“传菜”,前端不直接操作数据库,后端的业务改动也不会牵连页面。
选 PHP 做后端,不是因为技术多花哨,而是因为这类 CRUD 型业务系统,PHP 的生态实在太成熟。PHP 8 之后加入了 JIT,性能比以前提升明显,写场馆预订这种量级的接口完全够用。ThinkPHP 8、Laravel 10 都有很完整的路由、模型、中间件、队列组件,数据校验、鉴权、支付回调都有现成方案。最重要的是部署成本低,个人开发者随便买一台低配云服务器甚至虚拟主机都能跑起来,这对预算有限的小场馆特别友好。
UniApp 的价值在另一边。它基于 Vue 语法,一次编写,代码可编译到微信小程序、支付宝小程序、百度小程序、H5、iOS App、安卓 App。做外包项目时,客户的需求经常变,今天只要小程序,明天看到别人有 App 又想要 App,用 UniApp 就不用推翻重来,前端主代码保持一套,差异用条件编译隔离即可。
1.3 多平台部署的关键认知:条件编译不是玄学
需要特别注意,多端部署不是“一个按钮全搞定”的魔法。不同端的登录方式、支付通道、分享逻辑、定位授权、甚至包体积限制都不一样。以登录为例,微信小程序里可以用uni.login拿 code 再向后端换 openid,App 端就得考虑一键登录、微信授权登录或账号密码登录的组合;支付也是,小程序端直接调wx.requestPayment,H5 端可能是跳转支付或 JSAPI 支付,App 端又要用支付宝 SDK、微信 SDK。
我前端代码里会大量出现#ifdef MP-WEIXIN、#ifdef H5、#ifdef APP-PLUS这类条件编译块。把所有平台差异塞进独立的小函数或条件分支,公共页面逻辑保持一套。实际跑起来后你会发现,这样写前期虽然稍微麻烦,后期维护多端版本时特别省心。真正部署时,只要把微信小程序、H5、App 对应的域名和密钥配好,编译产物各自上传到对应平台就行。
2. 数据库设计与接口设计:把“预订”这件事拆到表里
2.1 核心表结构说明
预订系统的数据模型其实不复杂,核心思路是把“场馆—场地—时段—库存—订单”拆成独立却又关联清晰的表。下面是我在实际项目中用的简化表结构,字段做了裁剪,但核心关系都在。
-- 场馆表 CREATE TABLE `venue` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '场馆名称', `address` varchar(255) NOT NULL DEFAULT '', `cover` varchar(255) NOT NULL DEFAULT '' COMMENT '封面图', `lat` decimal(10,7) DEFAULT NULL COMMENT '纬度', `lng` decimal(10,7) DEFAULT NULL COMMENT '经度', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1启用 0停用', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 场地表 CREATE TABLE `venue_place` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `venue_id` int NOT NULL COMMENT '所属场馆ID', `name` varchar(50) NOT NULL COMMENT '场地名称,如A1号场', `type` varchar(20) NOT NULL DEFAULT '' COMMENT '场地类型', `status` tinyint NOT NULL DEFAULT '1', PRIMARY KEY (`id`), KEY `venue_id` (`venue_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 时段规则表 CREATE TABLE `venue_slot_rule` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `venue_id` int NOT NULL, `start_time` time NOT NULL COMMENT '开始时间', `end_time` time NOT NULL COMMENT '结束时间', `price` decimal(10,2) NOT NULL COMMENT '该时段基础价格', `sort` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 场地库存表 CREATE TABLE `venue_slot_inventory` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `venue_id` int NOT NULL, `place_id` int NOT NULL COMMENT '场地ID', `slot_date` date NOT NULL COMMENT '日期', `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0可订 1锁定 2已售', `order_id` int unsigned NOT NULL DEFAULT '0' COMMENT '占用订单ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_place_time` (`place_id`,`slot_date`,`start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `venue_order` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int NOT NULL, `venue_id` int NOT NULL, `total_amount` decimal(10,2) NOT NULL, `pay_amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已取消 3已退款 4已完成', `pay_time` datetime DEFAULT NULL, `expire_time` datetime NOT NULL COMMENT '支付过期时间', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表,一个订单可能包含多个连续时段 CREATE TABLE `venue_order_detail` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `order_id` int NOT NULL, `place_id` int NOT NULL, `slot_date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, `price` decimal(10,2) NOT NULL, PRIMARY KEY (`id`), KEY `order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里的关键设计是venue_slot_inventory表。每一行代表“某天某片场地某个时间段”这种最小粒度的库存单元,status字段直接标记可订、锁定、已售。这样做的好处是用户查询当日场次时,一条 SQL 就能把可订状态拉出来,不需要在多个表之间做复杂计算,查询压力很小。时段规则表则用来批量生成每天的库存数据。
2.2 时段库存与并发防超卖
预订模块最容易出事故的地方是并发。用户点击支付时,如果后端先查一遍库存,发现状态为可订,再去生成订单,这个流程在两个人同时操作时一定会出问题。更合理的做法是直接执行一条带条件的原子更新语句,用数据库行锁保证只有一个请求能抢到库存。
我在下单接口里常用的是悲观锁或原子更新。ThinkPHP 里可以用lock(true)锁行,再判断库存状态;也可以用类似下面的 SQL 手法:
UPDATE `venue_slot_inventory` SET `status` = 1, `order_id` = ? WHERE `id` = ? AND `status` = 0执行后返回受影响行数,如果为 0,说明这个时段已经被别的用户抢占了,直接提示“手慢了,请换个时段”。这套逻辑就像抢火车票:先查票再买票会遇到超卖,直接拿着身份证去闸机口抢占,谁先刷进去谁就拿到票。
订单创建和库存锁定一定要放在同一个数据库事务里。先锁库存,再写订单主表和明细表,全部成功才提交事务。否则库存改了,订单没写成功,数据就对不上了。支付超时释放库存也很重要,我通常会让后端在订单表里记录一个expire_time,配合一个定时任务,每5分钟扫描一次待支付且超时的订单,把它改成已取消状态,同时把对应库存的status从 1 改回 0。
2.3 统一接口返回格式与基础代码示例
前端和后端约定统一的 JSON 返回格式,会让联调效率高很多。我习惯用code/msg/data三段式:
{ "code": 0, "msg": "ok", "data": { "list": [], "total": 100, "page": 1 } }code为 0 表示成功,非 0 表示业务失败;msg是给用户看的提示文案;data是业务数据。这样前端请求封装里只需要判断一次code就能处理成功和失败。下面是一段 ThinkPHP 8 风格的场馆列表接口:
public function venueList(Request $request) { $page = $request->param('page', 1); $limit = $request->param('limit', 10); $keyword = $request->param('keyword', ''); $query = Venue::where('status', 1); if (!empty($keyword)) { $query->where('name', 'like', "%{$keyword}%"); } $list = $query->page($page, $limit)->select(); $total = $query->count(); return json([ 'code' => 0, 'msg' => 'ok', 'data' => [ 'list' => $list, 'total' => $total, 'page' => $page, ] ]); }下单接口则是重头戏。一个完整的创建订单流程,包括验证场地是否可约、计算金额、锁库存、生成订单号、设置支付过期时间。注意订单号不要用自增 ID 直接暴露给前端,可以用date('YmdHis') . random_int(100000, 999999)生成一串可读性较强的订单号,避免被猜测和遍历。
3. 用UniApp实现前端:从页面到微信小程序打包
3.1 前端项目结构与manifest配置
UniApp 前端我一般按业务模块分目录,典型结构如下:
pages/ index/index.vue 首页场馆列表 venue/detail.vue 场馆详情 booking/booking.vue 预订选场次 order/list.vue 我的订单 order/detail.vue 订单详情 user/index.vue 个人中心 static/ images/ api/ request.js 请求封装 venue.js 接口模块manifest.json是多端配置的核心。微信小程序要在这里填mp-weixin.appid;H5 端要确认路由模式是不是history,否则部分页面刷新后会 404;App 端需要根据功能开启定位、相机等模块权限。我遇到过不少朋友把appid写成测试号,结果预览时一直提示“未找到对应小程序”,排查半天才发现配置没有同步。
请求封装建议单独拎出来。所有接口走同一个request函数,自动携带 token,统一处理 401 跳登录和网络错误提示。下面是我常用的一版:
const BASE_URL = 'https://api.example.com'; 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': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => { uni.showToast({ title: '网络异常,请重试', icon: 'none' }); reject(err); } }); }); }BASE_URL要在不同环境切换。我在开发环境用http://localhost:8000,打包上线时再改成 HTTPS 域名。需要注意的是,微信小程序正式版强制要求域名必须备案且支持 HTTPS,所以开发调试和上线部署通常要两套配置。
3.2 小程序登录与手机号获取
场馆预订一般建议先登录再下单,最简单的方式是微信小程序静默登录:调uni.login拿到临时code,传给后端,后端拿着code调微信code2Session接口,换回openid,再生成我们自己的登录态 token 返回给前端。
手机号获取现在跟早期逻辑不一样了,不是随便调接口就能拿全号。现在通常的做法是页面上放一个button,设置open-type="getPhoneNumber",用户点击授权后,前端拿到带有加密数据的code,再把code传给后端,由后端调用微信接口解密或换取手机号。前端代码大概是:
<button open-type="getPhoneNumber" @getphonenumber="handleGetPhoneNumber"> 获取手机号 </button>async handleGetPhoneNumber(e) { if (e.detail.code) { const res = await request('/api/user/bindPhone', 'POST', { code: e.detail.code }); uni.showToast({ title: '绑定成功', icon: 'success' }); } else { uni.showToast({ title: '已取消授权', icon: 'none' }); } }这里有个很常见的坑:开发者工具里可以模拟返回手机号,但真机上e.detail不一定跟开发工具完全一样,所以一定要用真机测试授权流程。否则你辛辛苦苦写完,客户一拿手机试就发现绑定不了。
3.3 场馆列表和预订时段选择
首页场馆列表我用uni.request拉接口,加上onReachBottom做分页加载。列表卡片展示封面图、场馆名称、地址、最低价格和距离。场馆详情页再请求一个详情接口,把场地数、营业时间、图片集、公告这些信息渲染出来。
预订页是整套前端里交互最复杂的页面。我一般分三块:
- 顶部日期选择器:展示未来7天,用横向
scroll-view滚动,每个日期显示星期几和日期号。 - 中间场地/时段矩阵:每一行是一个场地,每一列是一个时段,格子颜色表示状态,绿色可订、灰色锁定、红色已售。
- 底部订单栏:实时累计用户选中的时段和金额,点击“去支付”时把所有选中项一次性提交给后端。
前端拿到库存接口后,要把status字段直接映射到格子的禁用态。这里不能只做 UI 禁用,后端接口还必须再次校验,因为前端所有数据都是可以被修改的。前端的作用是体验优化,真正的“守门员”永远是后端。
3.4 微信小程序打包超2MB的经典问题
小程序平台限制主包大小不能超过 2MB,这是很多开发者的噩梦。我见过有人打包后提示source size 2612kb exceed max limit 2mb,第一反应是删代码,结果删完还是超。真正有效的方案是分包加载。
在pages.json里配置subPackages,把不常用的页面拆进分包。比如订单列表、订单详情、用户协议、关于我们这些功能使用率低,完全可以放到子包里:
{ "pages": [ { "path": "pages/index/index", "style": {} }, { "path": "pages/venue/detail", "style": {} } ], "subPackages": [ { "root": "pages/order", "pages": [ { "path": "list", "style": {} }, { "path": "detail", "style": {} } ] } ] }分包的意义是把首次打开必须用到的内容留在主包,把低频页面拆出去,小程序会按需加载。除了分包,还要做三件事:图片尽量放线上 CDN 而不是本地静态目录;UI 组件用到了哪个引哪个,不要全量引入整套组件库;在微信开发者工具里勾选“上传代码时自动压缩脚本文件”和“ES6 转 ES5”。
4. PHP后端开发与部署避坑
4.1 JWT登录鉴权与身份安全
场馆预订系统涉及订单和支付,不能把用户 ID 直接存到前端 storage 里当作登录凭证。我用 JWT 做用户认证,用户登录后后端签发一个 token,前端存在本地,每次请求放到Authorization头里。后端中间件校验 token,并把解析出的用户 ID 注入到请求对象中。
JWT 的过期时间我一般设置成 7 天。场馆预订用户不是天天打开小程序,Token 过期太短会让用户频繁重新登录;过期太长又有安全风险。如果需要“长期登录”,可以让前端在请求返回 401 时先调刷新 token 接口,再重放原请求。实现起来也不复杂,核心还是前后端遵守同一套约定。
接口越权是另一个不得不防的点。比如用户 A 想取消用户 B 的订单,如果后端只校验“订单存在”,不去比对user_id,就会出大事故。所有涉及订单、个人信息的接口,拿到订单后第一步必须是判断当前用户是否为订单归属人,否则直接返回无权限。
4.2 跨域处理与JSONP兼容
H5 端部署时最容易遇到跨域问题。小程序没有浏览器同源策略,但 H5 有。后端正则是在所有对外接口前统一设置 CORS 响应头:
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Authorization, Content-Type'); if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { http_response_code(204); exit; }如果接口需要携带 Cookie,Access-Control-Allow-Origin就不能用*,必须写成具体域名。以前老的 H5 项目里还会用 JSONP 解决跨域,到现在已经不建议了,JSONP 只能支持 GET,而且没法在请求头里加 token,类型的业务接口根本没法安全使用。新项目直接 CORS 就够了。
开发阶段 UniApp 也可以自己配代理转发,把/api开头的请求转发到后端地址,这样不需要后端额外处理跨域。但生产环境如果后端不设 CORS,H5 部署在 CDN 上照样会出问题,所以后端统一设置 CORS 是最省心的方案。
4.3 支付回调与订单状态机
支付环节是场馆预订系统的定盘星。用户前端发起支付前,后端要先调用微信支付或支付宝的“统一下单”接口,拿到预支付参数,然后返回给前端调起支付。支付结果不是靠前端跳转页判断的,而是微信或支付宝服务器主动请求我们后端的一个回调地址。
回调地址必须是一个外网可访问的 HTTPS 接口。回调逻辑表面看简单,实际坑很多。第一要验签,回调数据里的签名不合法直接忽略;第二要幂等,同一笔订单可能收到多次回调,后端要保证第二次回调不会重复处理;第三要返回微信规定的成功报文,否则微信会认为回调失败,反复通知:
public function payNotify() { $xml = file_get_contents('php://input'); // 1. 验证签名 // 2. 解析订单号、支付金额 // 3. 校验订单状态,只能从未支付更新为已支付 // 4. 修改订单状态、写支付时间 // 5. 返回微信成功报文 return response( '<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>', 200 ); }订单状态机我固定为:0 待支付,1 已支付,2 已取消,3 已退款,4 已完成。只有“待支付”状态可以变成“已支付”或“已取消”,已支付可以变成已退款或已完成。这样在代码里就杜绝了状态乱跳的问题。支付成功之后,如果之前锁的是「锁定」状态,要更新为「已售」,同时通知场地运营方。
4.4 常用开发环境和老旧环境问题实录
PHP 8 配合 PhpStorm 是我最喜欢的开发组合。PhpStorm 对 ThinkPHP 的跳转、补全都很友好,调试接口时可以用 Postman 或 Apifox 做接口测试。本地环境我常用小皮面板这种集成环境,省去手工配置 PHP、MySQL、Nginx 的过程,几秒钟就能起一个干净的环境。
不过 PHP 8 在 Windows 下有一个高频问题,就是php warning: 'c:\windows\system32\vcruntime140.dll' 14.0 is not compatible。这个报错多数时候是系统里的 VC++ 运行库版本太旧,PHP 8 需要的是较新的 Microsoft Visual C++ Redistributable。解决办法是把 2015-2022 版本的 x64 运行库装上,重开命令行再测试php -v就正常了。
还有一个是 Linux 下源码编译 PHP 时报no package 'libzip' found。这是因为新版 PHP 编译要求libzip扩展库,低于 1.3 版本都不认。用包管理器提前装好再 configure:
# CentOS yum install -y libzip-devel # Ubuntu/Debian apt install -y libzip-dev如果你完全用不到 ZipArchive 功能,也可以编译时加--without-zip跳过这个依赖,但一般服务器上部署站点都离不开 zip 解压,建议还是正装好依赖。
5. 常见问题排查与源码交付实用技巧
5.1 高频问题速查表
代码部署和交付过程中,我经常会遇到下面这些重复出现的问题,整理成一张速查表:
| 问题现象 | 常见原因 | 排查与解决 |
|---|---|---|
| uniapp 微信小程序打包提示 source size 2612kb exceed max limit 2mb | 主包过大,依赖和图片太多 | 启用分包,低频页面拆到 subPackages;图片放 CDN;按需引入组件库;勾选压缩脚本 |
| uniapp 不打印日志信息 | 控制台过滤级别或真机调试模式问题 | 在 HBuilderX 控制台切换日志级别,小程序端打开 vConsole,确认代码在非生产环境 |
| PHP 命令行报 vcruntime140.dll 版本不兼容 | VC++ 运行库版本过旧 | 安装 Microsoft Visual C++ 2015-2022 x64 运行库 |
| 编译 PHP 报 no package 'libzip' found | 缺少 libzip-devel 依赖 | 安装对应包,确认版本不低于 1.3,不需要 zip 时可加--without-zip |
| H5 端请求接口跨域失败 | 后端未设置 CORS | 后端加 CORS 响应头,或前端 devServer 代理 |
| 微信开发者工具打开项目空白 | 导错了目录 | 导入dist/dev/mp-weixin目录,不是整个 uni-app 工程 |
| 小程序真机获取不到手机号 | 未用 button 触发、未认证或后端未正确解密 | 确认按钮 open-type,开发者工具模拟数据与真机有差异,后端使用官方接口解密 |
| 上线小程序接口请求失败 | 域名未配置或未备案 | 小程序后台配置合法 request 域名,要求 HTTPS 已备案 |
这张表虽然不能覆盖所有细节,但基本把初学者高频撞墙的地方都列全了。遇到问题先按表格逐项排查,不要第一反应就去改业务逻辑。
5.2 拿到源码后的正确启动顺序
源码交付之后,最容易出现的情况是拿到手就傻眼。我这里给出一个标准的启动顺序,照着做能少走很多弯路:
- 准备后端环境:安装 PHP 8 + MySQL 5.7/8.0 + Nginx/Apache,本地可以直接用小皮面板。
- 导入数据库:把项目里的
*.sql文件导入 MySQL,确认表结构完整生成。 - 修改后端配置:数据库连接信息、Redis 配置、支付密钥等写在
.env或config/database.php里,按实际环境改掉。 - 启动后端接口:本地能访问
/api/venue/list之类接口并返回 JSON 即可。 - 用 HBuilderX 导入前端源码:不要直接双击
.vue文件,要用 HBuilderX 的“导入项目”功能。 - 修改前端
api/request.js里的BASE_URL,指向你的后端地址。 - 运行到微信开发者工具:HBuilderX 点击“运行到小程序模拟器”,微信开发者工具里查看效果。
- 线上部署:后端接口上传服务器,前端源码在 HBuilderX 里点击“发行”,分别生成小程序包、H5 压缩包或 App 安装包。
这个顺序只要走通一遍,你脑子里就会很清晰地知道哪一层负责什么。以后不管是改样式还是加功能,都能快速定位到具体文件。
5.3 从场馆预订扩展到更多场景
这套表结构和接口设计并不只适合运动场馆,很多预约类业务都能直接改。共享会议室、健身房私教课、自习室座位、甚至美容美发店的时段预约,核心都是“资源 + 时间 + 订单”三元组。二次开发时你只需要改场馆名称,把场地换成会议室、工位或教练,接口逻辑基本不用大动。
我还建议在此基础上逐步扩展会员体系。可以加 VIP 等级,不同等级享受不同折扣;加次卡和储值卡,用户购买后按次抵扣;加优惠券,按场馆、按品类、按金额门槛发放。这些都建立在现有用户表和订单表之上,只是多设计几张券表和核销记录,不会伤筋动骨。
预约业务做到后面,“核销”环节很重要。用户到店后,场馆前台可以用管理员账号或扫码枪扫用户订单二维码,核销成功后再把订单状态改为已完成。这个功能我一般会在订单详情页生成一个动态二维码,前端定时刷新,后端核销接口做状态校验,能有效防止订单截图重复使用。
做这套 PHP + UniApp 场馆预订系统,我最大的实际体会是:第一版千万不要贪大求全。先跑通“场馆列表—选时段—创建订单—模拟支付—后台看单”这条主链路,哪怕 UI 丑一点都没关系。主链路稳定了,用户信息、优惠券、多图展示、分享裂变,这些都是后续慢慢加锦上添花的功能。如果你正准备接手一个预约类小程序项目,抓住“库存状态原子更新”和“支付回调幂等处理”这两个核心,这个项目就不会翻车。剩下的细节,在跑代码的过程里自然会一点点暴露出来,比看一百篇文档都管用。