做这套高校体育场馆智能预约系统的时候,我其实没想过它会变成一个三端同步的完整项目。最开始只是校学生会体育部的一个需求:羽毛球馆每天下午被各种社团占得乱七八糟,普通学生想打球根本抢不到场地,管理员手里还拿着一沓纸质登记表,经常出现同一时段被两个人同时约走的情况。后来需求从“能在线预约”一路变成“小程序、APP、PC后台三端同步,数据实时一致”,于是才有了这份带完整源码的项目版本。
这篇内容我打算从实际开发和部署角度来拆解这套系统:为什么做成三端同步,核心架构和技术选型怎么定,后端、前端、小程序端到底怎么部署才能跑通,二次开发时如果要加新场地类型、对接学校统一身份认证、调整预约规则,又该动哪些代码。适合正在做同类项目的开发团队、高校信息化部门的技术老师,以及准备拿这套源码做毕业设计后又想真正上线跑的同学。光看代码永远只能停留在“能跑”,搞清楚背后的设计逻辑和踩过的坑,才是这套源码真正值钱的地方。
1. 体育场馆预约系统的真实需求与设计边界
1.1 高校场馆管理的典型困境
我接触过的场地管理方式基本可以分为三个阶段。第一阶段是纯人工,管理员在办公室门口贴一张排期表,学生来了在纸上签名,遇到体育馆有两个门、登记表在管理员手里的时候就只能干等。第二阶段是微信群接龙,虽然能看出谁约了,但消息一多就乱,接龙顺序和实际到馆顺序经常不一致。第三阶段才轮到软件系统,但很多学校买来的商业系统只给管理员用,学生端体验极差,或者只做小程序、没有后台管理,导致管理员还得手动同步数据。
这套预约系统设计时,我的原则是先把“人”理清楚。学生要的是快速查看今天还有没有场地、一键预约、能取消不扣信用分;体育部老师要的是能排场地、看预约记录、统计使用率、发现谁在频繁爽约;部门领导要的是不用装复杂软件,手机上打开APP就能看到全校场馆今天的运行情况。三拨人的使用场景完全不同,所以三端不是噱头,是从真实角色里长出来的需求。
1.2 为什么非要做三端同步
很多初次接触这个项目的人会问:小程序就够用了,为什么还要APP和PC后台?我的回答是:小程序面向学生,APP面向管理层,PC后台面向日常运营管理,三者缺一不可。
小程序胜在轻量,微信扫码即用,不需要安装,学生预约、取消、查看记录都在这里完成。APP端虽然也需要登录,但它更多承担管理职能,比如体育部的值班老师要临时调整场地状态,在手机端直接操作比打开电脑快得多。PC后台则是管理员的“工作台”,场地管理、时段配置、订单查询、数据统计、信用分调整、公告发布,这些高频且复杂的操作放在PC端大屏上效率最高。
三端同步的核心不是“有三个界面”,而是“一套数据库、一套接口、一套业务规则”。小程序端和学生端看到的是同一个订单状态,APP端管理员把某块场地标记为“维护中”,小程序端该时段必须立刻不可约,不能出现一边显示可约一边被管理员锁定的脏数据。这个能力决定系统能不能真正上线用起来,而不是一个演示Demo。
1.3 核心功能清单怎么定
我在设计功能时把需求分成了三层。第一层是保命功能,没有这些系统根本没法用:场地管理、时段管理、在线预约、取消预约、我的预约、管理员审核。第二层是体验功能:信用分、签到、爽约记录、消息提醒、数据统计。第三层才是加分项:在线支付、退款、闸机联动、人脸识别、跨校区预约。
这套源码里第一层和第二层全部实现了,第三层留了接口和数据库字段,方便二次开发。我建议准备拿源码二次开发的同学也按这个优先级来改,不要一上来就加支付、加人脸识别,先把核心预约链路跑稳,不然很容易陷入“新增功能一堆 bug,核心预约却频频出错”的尴尬局面。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构与关键技术选型
2.1 技术栈选型的实际考量
后端我选的是 Spring Boot + MySQL + Redis。为什么用 Spring Boot?因为它在高校信息化团队里的认知度最高,遇到问题网上资料多,招人也容易,相比 Node.js 和 Go,Java 生态在处理复杂业务逻辑和事务管理时更稳妥。数据库用 MySQL,原因不必多说,数据关系型强、事务支持完整,预约订单这类数据绝对不能出现状态错乱。
前端 PC 后台用 Vue + Element UI,这是技术社区里最常见的组合,Vue 的组件化开发让页面复用率很高,Element UI 的表格、表单组件做管理后台几乎是现成的。小程序端和 APP 端我用了 uni-app,重点说一下这个选择。
uni-app 的核心价值是一套代码可以同时编译到微信小程序和 App。当时也纠结过是否用原生微信小程序 + Flutter 来做 App,但考虑到学校项目没有专职的移动端团队,原生双端开发的人力成本太高。uni-app 基于 Vue 语法,前端组员能直接上手,虽然复杂交互上不如原生流畅,但预约系统这类以表单、列表、支付为主的场景完全够用。实际跑下来,iOS 和 Android 的兼容性都还稳定,没有出现需要单独写原生插件的情况。
2.2 三端同步在接口层面如何实现
三端同步的底层逻辑其实不复杂,所有端都通过同一个后端接口层读写数据。我设计的接口规范是:
- 统一使用 RESTful 风格,资源用名词复数表示,比如
/api/app/venues获取场地列表,/api/app/orders提交预约订单。 - 请求和响应统一用 JSON,响应体固定包含
code、message、data三个字段,前端根据code判断成功失败,这样三端可以复用同一套错误处理逻辑。 - 登录态统一使用 token,小程序端通过微信登录换取 token,APP 端通过账号密码或统一身份认证换取 token,PC 后台使用管理员账号登录换取 token,后续每个接口在请求头里带上
Authorization。
一个典型的响应体结构是这样的:
json复制{
"code": 200,
"message": "预约成功",
"data": {
"orderId": "202506120001",
"venueName": "羽毛球馆 3 号场",
"startTime": "18:00",
"endTime": "19:00",
"status": "BOOKED"
}
}
接口层统一之后,小程序端、APP 端和 PC 后台只是换了一套 UI 样式和交互方式,背后的业务逻辑全部由后端保证。这样也方便后续增加新端,比如将来要做钉钉端、企业微信端,只要按同样规范调接口即可。
2.3 核心表结构设计与预约状态机
数据库设计是整个系统的基础。这是我不愿意省时间的地方,因为预约类系统涉及大量关联查询,表结构设计不好,后面写 SQL 会非常痛苦。
核心表我整理成下面这张表格:
| 表名 | 核心字段 | 说明 |
|---|---|---|
user |
id, student_no, name, phone, role, credit_score | 用户表,学生和管理员共用 |
venue |
id, name, type, location, status | 场地表,type 区分羽毛球、篮球等 |
venue_slot |
id, venue_id, start_time, end_time, max_count, booked_count, price | 场地时段表,每个场地每天生成不同时段 |
booking_order |
id, order_no, user_id, slot_id, status, create_time, cancel_time, sign_time | 预约订单表 |
credit_record |
id, user_id, type, score, description, create_time | 信用分变动记录 |
sys_config |
config_key, config_value, remark | 系统参数配置表,放预约提前天数等规则 |
预约订单的状态我设计成了状态机:PENDING_PAY(待支付)、BOOKED(已预约)、USED(已使用)、CANCELLED(已取消)、NO_SHOW(爽约)、EXPIRED(过期未签到自动释放)。每个状态只能走固定的流转路径,不能从“已取消”再跳回“已预约”。比如用户取消时必须判断当前状态是 BOOKED 且距离开始时间超过 X 小时,否则不允许取消;系统定时任务扫描到已到开始时间但未签到的订单,自动把状态改为 NO_SHOW 并把信用分扣减记录写入 credit_record。
如果不做状态机,直接用 if else 判断状态,代码会越写越乱,尤其是在定时任务和用户取消并发触发的时候,很容易出现一个订单被同时改两次的情况。
2.4 并发控制的核心逻辑
体育场馆预约有一个典型场景:热门时段比如工作日晚 18:00-19:00 的羽毛球场地,开放预约那一刻可能同时有几百个学生抢同一个时段。这时候必须做并发控制,不然 MySQL 容易出现超卖。
我采用的是 Redis 预占 + Lua 脚本的方式。大致逻辑是:每个场地时段在 Redis 里维护一个可预约数量,用户提交预约请求时先执行一段 Lua 脚本,检查当前已预约数量是否达到上限,如果没有则对计数加一并返回成功,否则直接返回“该时段已约满”。Lua 脚本在 Redis 中是原子执行的,比先查后改的 Java 代码安全得多。
lua复制-- 简化版预约扣减脚本
local key = KEYS[1]
local max = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or '0')
if current >= max then
return 0
else
redis.call('incr', key)
return 1
end
Redis 计数与数据库订单最终通过异步或事务保证最终一致。这个方案在大并发下实测稳定,秒杀式抢场场景中接口响应始终在 200ms 以内。
3. 实际部署过程中的完整步骤
3.1 环境准备清单
这是我在部署文档里给每个接手这个项目的同学列出来的环境清单,版本号都经过实测,不要随意升级大版本,否则可能踩到兼容性坑。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 17 | 项目基于 Spring Boot 2.7,1.8 最稳,17 需要改部分依赖 |
| Maven | 3.6+ | 后端依赖管理 |
| MySQL | 5.7 或 8.0 | 5.7 兼容性更好,8.0 需要调整驱动 |
| Redis | 5.0+ | 并发控制、缓存、验证码存储 |
| Node.js | 14+ | PC 后台前端构建 |
| HBuilderX | 3.x | 打包 APP 端,也可以直接用 CLI 方式 |
| 微信开发者工具 | 最新稳定版 | 运行小程序端 |
提前把环境装好,是最省时间的部署准备。我见过很多同学在部署阶段卡住,原因不是代码有问题,而是 MySQL 版本太高导致连接驱动不兼容,或者 Redis 默认配置没有密码导致启动时报错,这些环境问题排查起来比业务代码耗时得多。
3.2 后端部署与配置
拿到源码后,第一步是用 IntelliJ IDEA 打开后端工程,等待 Maven 下载完依赖。然后修改资源配置文件 application.yml:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/sports_booking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password: your_redis_password
database: 0
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
这里有几个容易翻车的细节。MySQL 连接串必须指定 serverTimezone=Asia/Shanghai,否则 8.0 版本的驱动会报时区错误;characterEncoding=utf8 不写的话,中文数据入库后会出现乱码;Redis 如果设置了密码但没有填 password,启动时连接会一直超时。
数据库初始化相对简单,项目里我已经打包了 sql/init.sql,里面包含建表语句和初始数据。部署时执行:
bash复制mysql -uroot -p sports_booking < sql/init.sql
然后启动后端:
bash复制mvn clean package
java -jar target/sports-booking.jar
看到控制台输出 “Started SportsBookingApplication” 并监听 8080 端口,后端就完成了基础部署。
3.3 前端和小程序端部署
PC 后台的前端是 Vue 工程,部署时先在工程目录下执行:
bash复制npm install
npm run build
生成 dist 目录后,把 dist 里的文件放到 Nginx 配置的静态目录下,并配置反向代理到后端接口。
小程序端不需要构建产物,直接用微信开发者工具导入 uni-app 工程下的小程序目录,修改 config.js 里的接口地址为后端服务器的公网地址,然后编译预览。注意微信小程序上线时必须在微信公众平台配置服务器合法域名,开发阶段可以在开发者工具里勾选“不校验合法域名”。
APP 端的部署相对麻烦一点,我用的是 HBuilderX 云打包方式:在 HBuilderX 中打开 uni-app 工程,点击“发行” -> “原生 App-云打包”,配置好包名和证书后,会生成 APK 或 IPA。云打包的好处是不需要本机安装 Android SDK 和 iOS 证书,缺点是需要排队等待。
3.4 服务器部署时最容易踩的坑
我在部署这套系统时踩过的坑,按出现频率排序大概是下面几个。
第一个是防火墙和云服务器安全组。后端接口在 8080 端口跑得好好的,但外部就是访问不到,原因是云服务器安全组没有放行 8080 端口。这个排查起来特别迷惑,因为本地测试完全正常,一上服务器就超时。后来我在部署文档里加了一条注意事项:部署前先检查安全组和操作系统防火墙。
第二个是 MySQL 8.0 的认证插件问题。MySQL 8.0 默认的认证插件是 caching_sha2_password,而很多旧版本的 Java 驱动不认识这个插件,连接池初始化就报错。解决方法是换用最新的 MySQL Connector/J,或者在创建用户时指定:
sql复制CREATE USER 'sports'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
第三个是文件上传路径问题。系统里用户头像、活动图片等上传后默认存在本地磁盘,如果项目是用 root 用户起的,而 Nginx 用普通用户运行,就会因为目录权限问题出现图片加载不出来。统一把上传目录设置到一个共享目录,并给该目录设置 755 权限能规避大部分问题。
第四个是微信小程序端登录失败:“获取登录后的微信用户失败”。这个很常见,原因通常是后台接口没有正确处理微信的 code 换 openid 流程,或者小程序的 appid 和后端配置的不一致。排查时先确认后端日志里是否收到 wx.login 返回的 code,如果没有收到,问题在前端;如果收到了但换 openid 失败,基本就是 appid 和密钥配置不正确。
4. 二次开发实战:从改需求到落地
4.1 新增“室外网球场”场地类型需要改哪几处
这套系统最常被问到的二次开发需求就是“我们学校还有网球场、游泳馆,要怎么加进来”。很多人以为只要在后台“场地管理”页面点新增就行了,但实际上一个场地类型要完整跑通,需要改好几个地方。
数据库层面,venue 表的 type 字段有一个字典,用来区分羽毛球馆、篮球馆、健身房、游泳馆等。新增类型时先在字典表里加一条记录,例如 type=tennis,name=室外网球场。后端层面,如果预约规则没有特殊差异,其实不需要改代码,因为场地类型不参与核心预约逻辑,只做筛选和展示。前端层面,这是最容易被忽略的地方:小程序端首页的场地筛选栏可能是硬编码了“羽毛球、篮球、健身”,新增类型时必须在这里把筛选项补上,否则用户永远点不开新场地。
一个比较稳妥的做法是把场地类型也做成动态配置,前端通过接口拉取类型列表,而不是写死。这样以后再加新场地,后台配置一次,三端都会自动显示,不用动代码。
4.2 对接学校统一身份认证(CAS/OAuth2)的改造思路
很多高校有自己的统一身份认证系统,学生希望用学号直接登录,而不是再注册一遍账号。这个改造不算难,但要动登录逻辑。
我的思路是:保留现有本地账号体系作为兜底,同时增加一个第三方认证登录入口。具体步骤是:前端登录页增加“统一身份认证登录”按钮,点击后跳转到学校 CAS 认证中心;认证成功后回调到我们系统,携带一个临时票据;后端用这个票据去认证中心换取用户信息(学号、姓名);再用学号去 user 表查找是否已经绑定过本地账号,如果找到就签发 token 并登录成功,如果没找到就自动创建一条新用户记录并将认证中心的 openid 绑定到该用户。
改造时最容易出问题的是回调地址配置。学校统一认证平台只允许配置有限个回调地址,本地开发环境和服务器环境必须分别申请,否则改完代码后回调地址不合法,会一直登录失败。另外要注意 token 有效期,建议首次登录后设置 30 天有效,避免学生在学期中反复登录。
4.3 预约规则从“提前 1 天”改成“提前 3 天可约”
这类规则调整看似简单,但如果预约天数写死在代码里,改起来就会牵扯很多地方。所以我在项目设计时特意把这类规则都放到了 sys_config 表里,包括提前预约天数、取消截止时间、每日预约上限、信用分扣减规则等。
以“提前 3 天可约”为例,后台管理员只需要在参数配置页面把 booking_advance_days 改成 3,前端在生成可预约日期时读取这个参数,后端在预约接口校验时也读取这个参数。两层都改了,才算真正生效。
如果你拿到的源码里这个值是写死的,我建议你在二次开发时顺手把它改成配置化,这种改动成本很低,但后续维护效率会提升很多。具体做法是在后端定义一个 ConfigService,提供 getInt("booking_advance_days") 方法,从数据库或 Redis 缓存中读取,然后所有需要的地方调用这个方法,而不是直接用硬编码的常量。
4.4 二次开发中必须避开的三个坑
我做过不少类似的定制项目,总结出三个在二次开发时最容易犯的错。
第一个是只改后端不改前端,或者反过来。预约流程是跨端的,改了后端校验规则,前端可能还在提示旧的限制条件。上线前一定要三端联调,重点检查数据库变更后缓存是否刷新。
第二个是忽略权限控制。给新页面添加接口时,如果直接复制其他接口,却忘了在接口上添加管理员角色校验,会造成越权访问。比如普通学生调用了 PC 后台的场地管理接口,就能随意修改场地状态。
第三个是改表结构后没有同步更新初始化 SQL。本地测试时数据库是自己手动改的,没问题,等拿到新环境重新跑 init.sql,发现缺字段,系统起不来。每次改完表结构,都记得把 init.sql 同步更新一遍。
5. 预约并发与数据一致性的实战处理
5.1 热门场地“秒没”的实现方案
前面提到了用 Redis + Lua 做预占计数,这里补充一下完整流程。用户在前端点击“提交预约”后,后端做的第一件事不是往数据库写订单,而是先调 Lua 脚本扣减 Redis 里的可预约数量。扣减成功,才创建订单;扣减失败,直接返回“该时段已约满”。
这里要注意一个细节:Redis 扣减成功但数据库写入失败时,要回滚 Redis 计数。最稳妥的方案是在一个事务里同时执行数据库操作和 Redis 操作,或者用定时任务定期比对 Redis 计数和数据库订单数,发现不一致时做补偿。我在项目里采用的是“先扣 Redis,再写数据库,失败则反向扣回”的补偿机制,日志里记录每一步操作,方便追踪。
对于并发量特别高的场景,比如全校几千人同时抢周末晚高峰时段的场地,还可以引入消息队列把预约请求削峰。先让所有请求进入队列,后端消费者按顺序处理。但考虑到大部分高校的并发量没有这么夸张,直接同步处理也能扛住,所以我没把 MQ 作为必选组件,而是留了一个扩展点。
5.2 用户取消预约后名额如何自动释放
用户取消预约后,必须立刻把 Redis 中的已约数量减一,否则这个时段即使有空余名额也没人约得进来。具体逻辑是:取消接口更新数据库订单状态为 CANCELLED,同时调用 Redis 脚本对计数减一。两个操作之间有一定时间差,如果取消后系统突然崩溃,可能出现数据库已取消但 Redis 计数没减的情况。
我在生产环境是这么处理的:取消请求先写数据库,成功后记录一条 Redis 的待补偿消息,再尝试扣减计数。如果扣减失败,定时任务会扫描数据库里已取消但 Redis 计数未减的订单,自动补偿。实现有点复杂,但保证了最终一致性。核心预约系统的数据一致性永远是第一优先级,不要为了追求代码简单而牺牲正确性。
5.3 定时任务处理超时未支付和未签到
整套系统里有几个定时任务很关键。第一个是超过 15 分钟未支付的订单自动取消,释放名额;第二个是预约开始时间已过但用户未签到的订单自动标记为 NO_SHOW,并扣减信用分;第三个是每天早上生成未来 N 天的场地时段。
定时任务我用的是 Spring 自带的 @Scheduled,配置了 cron 表达式。这里提醒一点:如果后端部署了多个实例,同一个定时任务会在每个实例上都执行一遍,导致重复释放或者重复生成时段。解决方法是引入分布式锁,比如用 Redis 的 setnx 实现一个简单的锁,每次任务执行前先获取锁,获取成功才执行。
java复制// 简化版 Redis 分布式锁
String lockKey = "task:release_order";
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(acquired)) {
try {
// 执行释放订单任务
} finally {
redisTemplate.delete(lockKey);
}
}
如果不做分布式锁,等部署到多实例环境后再来修这个问题,会非常痛苦,因为定时任务跑出来的脏数据很难追查。
6. 项目上线后的真实运营经验
6.1 我们实际遇到过的预约纠纷
系统上线后,真实场景中出现过几类预料之外的纠纷。
一类是学生预约了早上 8 点的场地,但 7 点 50 分发现临时有事,想取消却因为系统设置了“开场前 1 小时不可取消”而失败,结果被标记为爽约,影响信用分。这种规则在学生眼里显得特别不人性化。后来我在后台增加了管理员“手动取消”通道,并支持填写取消原因,由管理员在收到申诉后人工处理。
另一类是场地临时被学校活动征用。比如羽毛球馆突然要举办迎新晚会,管理员需要在 PC 后台一键关闭某天所有时段,同时给已预约的学生推送通知。这个功能在最初的版本里没有,上线后才补上,但对运营方来说非常重要。推荐你在二次开发时优先考虑这类“人工干预能力”,它比增加更多花哨功能更能减少日常运维压力。
第三类是极端情况下的重复签到。学生到达体育馆后,由于手机网络问题,签到请求被重复提交,导致系统插入两条签到记录。后来我在签到接口加了唯一索引,以 order_id 做唯一约束,重复提交直接报错,问题就解决了。
6.2 数据统计与报表给管理带来的价值
体育部老师使用 PC 后台后,最惊喜的不是预约功能,而是数据统计。以前场地使用率只能靠人工估算,现在系统可以按月输出每个场馆的使用率、各时段预约热度、爽约率排行榜、热门场地 TOP10。
建议你在做二次开发时,把统计报表也纳入重点。不需要做得太复杂,先按下述三个维度做:
- 按场馆维度统计:每个场馆每天/每周/每月的使用次数、总收入(如果有计费)。
- 按时段维度统计:哪些时段预约最火爆,方便学校动态调整开放时长。
- 按用户维度统计:哪些用户频繁爽约,为信用分管理提供依据。
这些数据做出来后,你会发现系统已经不只是“预约工具”,而是体育场馆精细化管理的数据基础。领导层看到报表后,对项目的认可度也会明显提升。
6.3 个人对这套源码的体会和后续扩展想法
做了这么多预约类项目,我最大的体会是:不要把“预约”本身想得太简单。预约背后涉及时间、空间、人、规则四者的组合约束,再加上支付、信用、消息通知,业务复杂度并不低。源码只是起点,真正考验功底的是能根据学校实际运营情况持续调整。
如果后续想继续扩展,我觉得有几个方向很值得做。一个是和校园卡系统对接,学生到馆后刷校园卡完成身份核验,免去手动签到。另一个是智能推荐,根据学生的历史预约习惯推荐他可能感兴趣的时段,降低场地空置率。还有计费功能,按学生/教职工身份设置不同价格,和校园支付平台打通。这些都需要在现有稳定底座上迭代,不建议一口气全塞进来。
这套系统能跑起来,最关键的是当时我们忍住没有一直加需求,先把预约链路做稳,再逐步做优化。现在把这个源码和踩坑过程分享出来,也是希望后来的同学少走一点弯路。拿到代码后,先从部署跑通开始,再慢慢理解每个模块为什么这样设计,最后结合自己的场景去二次开发,这条路是最省时间的。
