news 2026/9/15 12:01:27

SpringBoot体育馆预约系统实战:从表设计到并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot体育馆预约系统实战:从表设计到并发控制

简介:这套基于SpringBoot框架实现的体育馆预约管理系统,是一份面向计算机科学与技术、电子信息工程等专业学生的完整项目参考方案,适用于毕业设计、课程项目或期末作业等场景。系统采用浏览器与服务器(B/S)结构,综合运用Java语言、SpringBoot框架、MyBatis持久化技术、Ajax异步交互和Vue前端框架,并配合MySQL数据库与Tomcat应用服务器,完整覆盖场馆信息展示、预约时间选择、订单生成与管理等典型业务场景,还内置了一键安装、启动脚本,方便本地快速部署运行。资源压缩包共包含900个文件,以SVG图标、JavaScript脚本、Java源码、Vue组件、CSS样式、HTML页面及数据库备份文件为主,压缩后整体约17.61MB,目录结构清晰,下载后即可对照学习。目前已有58人学习,代码经过充分测试,功能稳定,不仅适合学生直接参考、二次开发或用于答辩准备,也能帮助开发者快速掌握SpringBoot前后端分离项目的实际落地方式。

1. 从排队登记到手机订场:SpringBoot把体育馆预约做成闭环

大多数体育馆还在用微信群接龙或前台Excel表排场地,高峰期漏记、重复订、退款对不上账是常态。基于SpringBoot的体育馆预约管理系统,核心是让用户在线选定日期和时段、提交订单、支付或锁场,同时管理员能在后台看每个场地的空闲情况。这套系统不复杂,但很能检验工程能力:实体关系设计、接口参数校验、定时任务、并发控制都得依次落地。适合拿来做毕设、中小型场馆的数字化管理,或者作为SpringBoot全家桶的练习项目。本文按我通常做这类系统的思路,从表结构搭起,到预约接口、防超卖、最后上线前要处理的边界问题,都会给出能直接抄的代码和参数。

2. 预约系统的数据模型与SpringBoot工程骨架搭建

2.1 场地、时段与预约单:先拆清楚三张核心表

体育馆预约的领域模型不复杂,但很多新手一上来就把所有信息塞进一张预约表,导致后面查询时段是否可订非常别扭。常见做法是分成三张主表:场地表(venue)、时段表(time_slot)、预约订单表(booking_order)。

表名关键字段作用
venueid、name、location、type、status记录羽毛球馆、篮球馆、健身房等场地基础信息
time_slotid、start_time、end_time、price预定义固定时段,例如 10:00-12:00
booking_orderid、venue_id、time_slot_id、booking_date、user_id、status、create_time记录某天某时段某场地的预约情况

这里最值得注意的关系是:一个场地在同一个日期和同一个时段只能被一个有效订单占用。也就是订单表需要唯一约束(venue_id, time_slot_id, booking_date),而且这个约束要尽量包含状态判断。状态字段一般用PENDING(待支付)、PAID(已支付)、CANCELLED(已取消)、COMPLETED(已完成)。只有待支付和已支付算占用,已取消的订单不锁时段。我的设计习惯是加一个status字段后,用数据库唯一性去防重复,但唯一约束不能作用于部分状态,所以通常另加occupied_flag或干脆用一个独立的时间段状态表。如果项目不大,可以直接在查询里以status IN ('PENDING','PAID')判断,配合分布式锁,后面第4章会讲。

2.2 SpringBoot版本怎么选:JDK版本直接决定依赖兼容性

版本选择是这类项目第一个坑。现在搜索引擎里大量 “SpringBoot版本太高”“用的是JDK21想回退到1.8” 的提问,本质都是没弄明白 SpringBoot 与 JDK 的对应关系。SpringBoot 3.x 要求 JDK 17 起跳,并且javax.*改成jakarta.*,很多网上抄来的老资料里的import javax.servlet在 3.x 里直接编译失败。

如果是毕业设计或者中小型项目,我通常建议直接选 SpringBoot 2.7.x(例如 2.7.18),JDK 用 8 或 11 都行。原因不是 3.x 不行,而是 2.7 的资料量大、和 MyBatis-Plus、Redis 客户端等中间件兼容问题最少。如果本机已经装了 JDK 21,也可以用 SpringBoot 3.2.x,后面代码里注意用jakarta.*包即可。

技术栈SpringBoot 2.7.xSpringBoot 3.2.x
JDK 最低版本817
命名空间javax.*jakarta.*
推荐搭配MyBatis-Plus 3.5.xMyBatis-Plus 3.5.5+
适合场景毕设、快速开发、老服务器部署新项目、团队已熟悉 JDK17

2.3 IDEA创建SpringBoot项目后,pom里必须出现的依赖

在 IDEA 中新建项目时选 Spring Initializr,地域选阿里云镜像通常更快。如果是在 start.spring.io 网页生成,生成的工程默认就是 SpringBoot 3.x,想用 2.7 需要手动切换版本。下面是我做体育馆预约项目常用的pom.xml片段:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段配置里要说明的是:MyBatis-Plus 的mybatis-plus-boot-starter版本不是随便加的。使用 2.7.x 时,3.5.3.1 及以下的兼容性更稳;如果用了 SpringBoot 3.x,请在 3.5.5 及以上版本中选择,否则启动时会报ClassNotFoundException: javax.sql.DataSource或 MyBatis 相关类找不到。Lombok 加<optional>true</optional>是因为它只在编译期生效,不需要进入最终 jar。

2.4 用 MyBatis-Plus 一分钟写出场地查询接口

业务还没做之前,先用一个场地列表接口验证工程能跑通。实体类Venue直接用注解映射字段,Mapper 接口继承BaseMapper就行。下面是典型的 Controller、Service 和 Mapper 写法。

@RestController @RequestMapping("/api/venue") public class VenueController { @Resource private VenueService venueService; @GetMapping("/list") public R<List<Venue>> list() { return R.ok(venueService.listUsefulVenue()); } }
@Service public class VenueServiceImpl implements VenueService { @Resource private VenueMapper venueMapper; @Override public List<Venue> listUsefulVenue() { LambdaQueryWrapper<Venue> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Venue::getStatus, 1) .orderByAsc(Venue::getSortOrder); return venueMapper.selectList(wrapper); } }

LambdaQueryWrapper的好处是不用拼字符串列名,编译期就能发现字段名写错的问题。.eq(Venue::getStatus, 1)表示只查状态为1的场地,状态的含义我约定为:0禁用,1启用。.orderByAsc可以让球馆排序不乱跳。前端是 Vue 项目时,也可以直接约定这个接口返回的 JSON,后端不掺和页面渲染。

3. 预约业务闭环:参数校验、库存扣减与定时回滚

3.1 提交预约:业务上先查后插还不够

预约接口是整个系统中最容易出错的入口。最基础的实现是:先查该场地该时段该日期是否存在有效订单,没有则插入新订单。代码如下:

@Transactional(rollbackFor = Exception.class) public BookingOrder createOrder(BookingRequest request) { LambdaQueryWrapper<BookingOrder> query = new LambdaQueryWrapper<>(); query.eq(BookingOrder::getVenueId, request.getVenueId()) .eq(BookingOrder::getTimeSlotId, request.getTimeSlotId()) .eq(BookingOrder::getBookingDate, request.getBookingDate()) .in(BookingOrder::getStatus, Arrays.asList("PENDING", "PAID")); Long count = bookingOrderMapper.selectCount(query); if (count > 0) { throw new BizException("该时段已被预约"); } BookingOrder order = new BookingOrder(); order.setVenueId(request.getVenueId()); order.setTimeSlotId(request.getTimeSlotId()); order.setBookingDate(request.getBookingDate()); order.setUserId(request.getUserId()); order.setStatus("PENDING"); order.setCreateTime(new Date()); bookingOrderMapper.insert(order); return order; }

这段代码看起来没有错,但并发场景下会出大问题:两个人同时读到 count 为 0,随后都能插入成功,预约超卖。真正的兜底是数据库层面的唯一约束,不能只靠应用层查询。解决思路是:给booking_order表加唯一索引,但唯一索引不能建在普通 status 上。我一般单独加一个字段表示占用标记occupy_key,值组合为venueId + timeSlotId + bookingDate,只有当订单处于有效状态时才写入,取消时置空。这个方案在 MySQL 下可行,如果你用 PostgreSQL,可以改用部分唯一索引,一行语句搞定。

3.2 用 @EnableScheduling 自动清理超时未支付订单

用户提交了预约单但迟迟不付款,这种待支付记录会一直占着时段,让其他用户无法预约。常见的做法是设置超时时间,比如 15 分钟未支付自动取消。SpringBoot 里可以用自带的@Scheduled定时任务解决,不需要额外引入 Quartz。

@Component public class OrderTimeoutTask { @Resource private BookingOrderMapper bookingOrderMapper; @Scheduled(fixedDelay = 60000) public void cancelExpiredOrders() { Date deadline = new Date(System.currentTimeMillis() - 15 * 60 * 1000); LambdaUpdateWrapper<BookingOrder> update = new LambdaUpdateWrapper<>(); update.eq(BookingOrder::getStatus, "PENDING") .lt(BookingOrder::getCreateTime, deadline) .set(BookingOrder::getStatus, "CANCELLED"); bookingOrderMapper.update(null, update); } }

启动类上需要加上@EnableScheduling,否则任务不生效。fixedDelay = 60000表示上一次任务执行完成后再等 60 秒继续执行,适合这种低频清理任务。注意.lt(BookingOrder::getCreateTime, deadline)的含义是找出创建时间早于截止时间的数据,也就是已经超时的单子。这里有一个容易踩的坑:如果服务器与数据库处于不同时区,new Date()在 JDBC 传参会按服务器时区转换,应用和 MySQL 连接参数必须显式设置serverTimezone=Asia/Shanghai,否则清理时间可能偏差数小时。

3.3 用 Swagger 把接口文档直接交给前端

预约系统通常要对接小程序或 Vue 页面,接口列表不能靠手写文档。在 SpringBoot 2.7 中使用springfox的 2.9.2 版本已经是过去式,那个版本存在 swagger-ui 未授权访问的问题,而且和 SpringBoot 2.6+ 的请求匹配策略冲突,需要额外改配置。我更推荐使用 springdoc-openapi:

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.7.0</version> </dependency>

启动项目后访问/swagger-ui/index.html就能看到可用接口列表。为了不把接口文档暴露在生产环境,我一般在配置里加:

springdoc: api-docs: enabled: true swagger-ui: enabled: true

生产环境则手动把这两个值改成false,或者在网关层面限制/swagger-ui/**/v3/api-docs/**的访问。停车场、体育馆这类系统通常在内网使用,但仍要防止外网直接访问文档泄露接口字段结构。

4. 热门时段被秒抢:并发一致性与SpringBoot兜底方案

4.1 超卖问题到底是怎么发生的

同一个热门时段,比如周五晚上 7 点到 9 点的羽毛球场,一旦开放预约,QPS 不会特别高,但并发足够触发经典的超卖。原因不是代码写得差,而是“检查-插入”这个组合操作不是原子的。应用层经过了网络、线程调度,两个请求可能在同一个数据库快照上做检查,然后各自插入成功。

要彻底解决,必须先想明白锁粒度。synchronized 和 ReentrantLock 只对单实例进程内的线程有效,一旦系统以后部署了多个实例做负载均衡,本地锁就形同虚设。所以分布式环境下的常见做法是 Redis 分布式锁,数据库乐观锁作为最后一道防线。

4.2 用 Redis 分布式锁给预约时段加锁

这里用 Redisson 封装的锁,因为它自带看门狗,不用手动处理锁过期和续期问题。先引入依赖,再在预约逻辑外层加锁。

<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.20.0</version> </dependency>

业务代码中构建锁的 key 时,要把场地、时段、日期都拼进去,锁粒度越小并发能力越高。不要只锁 venueId,那样会导致同一个场馆的所有时段全部串行化:

@Resource private RedissonClient redissonClient; public BookingOrder createOrderWithLock(BookingRequest request) { String lockKey = "BOOKING_LOCK:" + request.getVenueId() + ":" + request.getTimeSlotId() + ":" + request.getBookingDate(); RLock lock = redissonClient.getLock(lockKey); boolean locked = lock.tryLock(2, 30, TimeUnit.SECONDS); if (!locked) { throw new BizException("当前预约人数较多,请稍后重试"); } try { return createOrder(request); } finally { lock.unlock(); } }

tryLock(2, 30, TimeUnit.SECONDS)的意思是:最多等待 2 秒获取锁,获取成功后自动释放时间为 30 秒。体育馆预约操作非常轻量,30 秒完全够用,但也要注意如果业务里出现慢 SQL,超时释放有可能在事务结束前发生,导致其他线程拿到锁但读到旧状态。所以更稳的做法是事务提交后再释放锁,或者把锁放到 Controller 层,Service 只用事务。

4.3 数据库乐观锁兜底:宁可失败也不重复

分布式锁可以挡住绝大多数并发请求,但不能完全依赖外部中间件。最后的防线是给订单或时段表增加乐观锁字段。以时段占用为例,给venue_time_slot表加一个version字段,更新时检查版本号:

@Update("UPDATE venue_time_slot SET occupied = 1, version = version + 1 " + "WHERE id = #{slotId} AND version = #{version}") int tryOccupySlot(@Param("slotId") Long slotId, @Param("version") Integer version);

这里的关键是 SQL 的WHERE version = #{version}:如果期间有其他事务修改过这行数据的 version,本次更新会匹配不到任何记录,影响行数为 0,然后业务层抛出“该时段刚刚被抢”的提示,顺带触发用户重新选择时段。乐观锁不能避免无意义的并发请求占用数据库连接,但它能保证最多只有一个请求真正写入成功。实际使用时,我建议把乐观锁和分布式锁叠加:优先拿分布式锁挡流量,乐观锁防止极端情况或锁失效后的脏写。

5. 验证与上线:压测、日志与接口安全细节

5.1 用 JMeter 快速压出预约接口的吞吐量

系统上线前至少要知道接口的承受能力。JMeter 创建一个线程组,设置 100 个线程、Ramp-Up 10 秒、循环 5 次,就能模拟 100 个用户同时点击预约。重点关注聚合报告中的吞吐量和 p90 响应时间。体育馆预约场景正常规模下单机 300 并发足够,如果压测发现大量请求卡在等待分布式锁,要调整的是锁等待时间和 Redis 连接池配置,而不是盲目加机器。吞吐量低于预期时,先看数据库连接池是否成为瓶颈。

5.2 上线前关掉 Actuator 暴露的 HeapDump 和 Swagger 文档

SpringBoot 项目依赖spring-boot-starter-actuator后,默认暴露的 endpoint 有限,但一旦配置被错误放开,/actuator/heapdump可能让攻击者下载应用的对内存快照,从日志、数据库密码到业务 ID 全部泄露。安全配置如下:

management: endpoints: web: exposure: include: health,info

这样生产环境只暴露健康检查接口。与此同时,Swagger 文档在生产环境应当关闭。注意不能只隐藏入口页面,要直接禁用/v3/api-docs这个数据接口,否则别人拼接 URL 仍能拿到 JSON 格式的完整接口文档。

5.3 HikariCP 三处连接池参数调整

SpringBoot 默认使用 HikariCP,体育馆预约系统的特点是并发峰值集中、闲时几乎无流量。针对这种场景,我一般做三处配置调整:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000

maximum-pool-size: 20是对应数据库实例规格的合理值,开太大反而增加 MySQL 的上下文切换;minimum-idle: 5保证闲时保留少量连接,避免冷启动慢;connection-timeout: 3000让业务方在数据库连接被占满时快速失败,而不是无限等下去。预约业务本身单次只做一次插入和一次 update,这个配置在 8 核 16G 的机器上能支撑常规场馆的使用量。上线后如果日志出现connection is not available,优先看慢 SQL,再看是不是连接池最大值被业务长事务占满。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:01:01

Mermaid时序图进阶:4个关键字画清并发、分支与关键路径

Mermaid时序图进阶&#xff1a;4个关键字画清并发、分支与关键路径 【免费下载链接】mermaid Generation of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid 一条…

作者头像 李华
网站建设 2026/9/15 11:59:46

JDK28值类与Spring Boot4.1响应式升级实战指南

1. 这不是一份“新闻简报”&#xff0c;而是一份JVM生态实战者的手记Java周刊2026W35这个标题&#xff0c;表面看是时间戳版本号的堆砌&#xff0c;但如果你真在一线写业务、搭平台、调性能&#xff0c;就会立刻意识到&#xff1a;这一期不是更新日志&#xff0c;而是JVM技术栈…

作者头像 李华
网站建设 2026/9/15 11:59:16

用WeChatMsg导出微信聊天记录并生成年度报告

用WeChatMsg导出微信聊天记录并生成年度报告 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeChatMsg 换手机或者…

作者头像 李华