☰
Spring Boot游泳馆预约管理系统:从数据库设计到并发防超卖
2026/10/21 9:43:30 网站建设 项目流程

站在一个踩过无数毕设坑的人的角度说一句:很多人选“游泳馆预约管理”这种题目,不是因为它简单,而是因为它“刚好卡在舒服的位置”——业务链条完整、技术点覆盖经典、工作量可控,但又不是一眼就能抄完的增删改查。

我手头上正好有一套完整跑通的源码项目,编号39899,是用 Spring Boot 做的游泳馆预约管理系统。这套系统从头到尾改过三轮,从数据库设计到并发防超卖都踩过真实的坑。这篇文章就把整个项目的设计思路、核心代码、实操过程和避坑记录完整拆开,讲的都是可以直接复现的东西。

1. 项目定位与需求拆解

1.1 业务场景与核心痛点

先想清楚一个问题:游泳馆为什么需要预约系统?

游泳馆的容量不是无限的,泳道数量、水深区域划分、同一时段的安全员配比都决定了“某个时段最多能放多少人进来”。高峰期泳道紧张,以前靠前台人工登记,客户到了现场才发现约满了,白跑一趟;淡季时段又没人来,泳道闲置,馆方也看不到数据。

站在游泳馆管理者角度,痛点是三件:一是场次容量不透明,客户体验差;二是预约和收费靠人工,容易出错扯皮;三是没有数据支撑,不知道哪些时段热门、哪些时段该做活动引流。

站在用户角度,痛点是:预约不方便,到场还要排队核验身份,临时想改也改不了。

所以这套系统的核心价值,就是把“场次发布→在线预约→余额扣费→签到核销→统计报表”串成一条完整链路。这也是它适合做毕设的根本原因——不是某个单点功能炫技,而是整个业务闭环能讲清楚、能写进论文、能在答辩时被追问不慌。

1.2 角色权限与模块划分

系统用了三个角色,权限边界必须提前划清楚:

角色核心权限典型操作
管理员全部权限场馆管理、场次排期、订单查看、数据统计
前台员工受限权限查看预约列表、扫码/报号签到核销
注册用户个人权限场次查询、在线预约、余额充值、取消预约

模块划分在代码层面是六个核心包:认证模块处理注册登录,场馆模块维护基础信息,场次模块负责生成每日可预约时段,预约模块处理占位和扣费,账户模块管充值与退款,统计模块产出营收和热门时段数据。

这里想给一个非常实用的建议:动手写代码之前,先把包结构定下来。很多同学一上来就堆文件,后面改需求的时候连自己都找不到对应的类,心态直接崩。按照 controller / service / mapper / entity / common / config 分层包结构,后面加功能会很顺手。

1.3 业务流程总览

一个普通用户的完整预约流程:注册登录 → 查看今天或明天的场次列表 → 选一个有余位的场次 → 系统校验余额和余位 → 扣费 → 生成预约订单 → 到馆后工作人员核销 → 状态变更为已签到。

管理员侧流程:维护场馆信息 → 按日期批量生成场次(比如每天分早、中、晚三场) → 实时查看各场次预约人数和营收 → 遇到紧急情况可下架异常场次。

把流程画出来就会发现,状态流转是整个系统的灵魂。预约单至少要有“待使用 / 已签到 / 已取消 / 已过期”四种状态,场次也要有“可预约 / 约满 / 已结束”等状态。状态设计不清晰,后面的取消、退费、统计全会乱。

2. 技术选型与方案取舍

2.1 为什么是 Spring Boot,而不是 SSM 或微服务

直接说结论:这套系统用 Spring Boot 单体应用是最稳的选择。

前几年毕设主流还是 SSM(Spring + Spring MVC + MyBatis)三件套,配置文件特别多。Spring Boot 把自动配置、内嵌容器、起步依赖这些问题全解决了,一个启动类就能跑起来,开发重心回到业务代码本身。对毕设而言,这意味着你把时间花在预约逻辑、事务控制这些有讲头的地方,而不是花在环境配置上。

为什么不建议上微服务?预约管理系统本质上就是一个单体业务,硬拆成服务注册中心、网关、分布式事务,除了增加答辩时解释不清的复杂度,没有任何收益。技术选型是为了解决问题,不是为了炫技。如果“用微服务解决了什么问题”这个问题答不上来,那就不该用。

版本上,建议用 Spring Boot 2.7.x。毕业设计场景下资料最多、踩坑案例最全,遇到任何问题基本都能搜到现成答案。3.x 虽然也稳定了,但很多教程插件还停留在 2.x 生态,查问题会多花时间。

2.2 持久层方案:MyBatis-Plus 的实用体验

持久层这套项目用的是 MyBatis-Plus,理由非常实在:单表的增删改查样板代码全免了。继承一个 BaseMapper 就有基础 CRUD,复杂条件用 LambdaQueryWrapper 两行写完,不用手写重复 SQL。

查某个场次已预约人数,一行代码的事:

Integer booked = reservationMapper.selectCount( new LambdaQueryWrapper<Reservation>() .eq(Reservation::getSessionId, sessionId) .eq(Reservation::getStatus, 0) );

以前写 SSM 这种查询要在 XML 里配 resultMap,现在直接方法调用。当然 MyBatis-Plus 不是万能的,多表关联的复杂统计务必要写自定义 XML SQL,性能和可读性都更好。这套系统里的营收统计报表就是用 XML 自定义 SQL 实现的。

2.3 前端方案:Thymeleaf 还是前后端分离

前端方案是个大分水岭。很多同学一上来选 Vue + Spring Boot 前后端分离,看着潮流,实际工作量翻倍,还要处理跨域、Token 失效、联调等问题,折腾到怀疑人生。

我的明确建议:如果毕设重点在后端业务逻辑,前端老老实实用 Thymeleaf + Bootstrap。服务端渲染的好处是,没有跨域问题、不用纠结接口鉴权怎么传给前端,页面直接用 th:each 循环渲染数据,单个 Spring Boot 应用打成一个 jar 包就能部署,省心得多。

要是确实想体现一点现代感,可以把 Vue 的构建产物放到 static 目录下直接同源部署,也能绕开跨域。但我个人认为对于这套系统,写清楚业务逻辑比前端技术栈好看重要得多。

2.4 环境版本与依赖管理细节

  • JDK:1.8 或 11,Spring Boot 2.7 都支持,JDK 8 最稳
  • Maven:3.6 以上,换国内镜像源,否则依赖下载慢到崩溃
  • MySQL:8.0,字符集用 utf8mb4
  • 连接池:默认的 HikariCP 就够
  • 工具类:Hutool,处理日期、生成订单号非常省事
  • Redis:可选,但建议不引入,理由后面细说

这里有个原则想强调:能用数据库事务解决的,就不要为了简历好看而引入中间件。很多同学喜欢一上来把 Redis、RabbitMQ 全塞进去,但对预约场景,单机事务完全够用,加 Redis 做锁反而引入缓存一致性、缓存穿透一系列新问题。架构越简单,越容易把自己的核心亮点讲清楚。

3. 数据库设计:预约系统的地基

3.1 核心表结构总览

预约系统的表不算多,但每一张都有设计讲究。直接过一遍:

用户表 sys_user:id、username、password、nickname、phone、balance(余额)、role、create_time、update_time。

场馆表 swimming_pool:id、name、address、lane_count、capacity、open_time、close_time、status。

场次表 pool_session:id、pool_id、session_date(日期)、start_time、end_time、max_count(总容量)、booked_count(已预约数)、price、status。

预约表 reservation:id、order_no(订单号)、user_id、session_id、amount、status、create_time、checkin_time(签到时间)。

流水表 account_log:id、user_id、type(1充值、2预约扣款、3退款)、amount、balance_before、balance_after、remark、create_time。

最容易犯的一个设计错误:把场次直接塞在场馆表里,比如给场馆表加一个“时间段”字段。这样设计之后,同一日期多时段的数据处理会非常痛苦。正确做法是把“场次”当成独立的核心实体,一个场馆每天可以有多条场次记录,预约表直接关联场次而不是场馆。

3.2 场次容量与状态字段设计

场次的容量有两种建模方式:按“总人数”算,或者按“泳道 × 时段”算。真实游泳馆里泳道还有功能区分,比如深水训练道和儿童区。但毕设项目不用一上来就建那么细的模型,用 max_count 和 booked_count 两个字段表达即可:booked_count 表示已预约人数,预约成功加 1,取消或过期回退减 1。

场次状态字段我设了这些值:

  • 0:未开始
  • 1:可预约
  • 2:约满,即 booked_count >= max_count
  • 3:已结束

这里有一个小优化需要提一下:不要依赖定时任务去改场次状态。查询列表时直接用当前时间和场次结束时间动态判断是否已结束,少一套定时任务,也就少很多状态不同步的 bug。

3.3 余额、充值与流水表设计

用户账上的钱我直接放 balance 字段在用户表里,配合 account_log 表记录每一笔资金变动。你可能会问为什么不单独设计账户表?从业务规模看,余额字段足够,毕设不需要引入复杂的账户体系。但流水表一定要有,这是对账和退款追溯的依据。

流水表的关键设计是 balance_before 和 balance_after 两个字段,记录每一笔资金变动前后的余额。这个设计在答辩时非常加分,因为你能讲清楚“审计”和“可追溯性”——任何一笔资金变动都能从流水里完整还原来龙去脉。

3.4 索引与防并发设计

数据库设计阶段就要把索引考虑进去,而不是等数据多了再补:

  • 预约表建 (user_id, session_id) 联合唯一索引,这是防重复预约的关键
  • 预约表 status、create_time 建索引,统计签到数和查询历史用得到
  • 场次表 (pool_id, session_date) 建索引,查询某天某馆场次列表必备
  • 流水表 user_id 建索引,查个人消费历史效率高

一个我用真金白银换来的经验:业务约束要下沉到数据库层面。比如“一个用户一个场次只能预约一次”,不能只靠 Service 层查一遍再判断。两个请求同时进来,Service 层的 if 判断存在并发窗口,只有数据库唯一索引能兜底。

4. 核心业务逻辑实现:预约流程拆解

4.1 一次预约的完整调用链

预约是高频操作,同时牵涉余额和场次名额。后端收到预约请求后,Service 层的执行顺序很关键,我的实现是:

  1. 查询场次信息,校验场次存在、未结束、状态为可预约
  2. 查询用户信息,校验余额充足
  3. 原子更新预占名额:update pool_session set booked_count = booked_count + 1 where id = ? and booked_count < max_count
  4. 扣减用户余额
  5. 插入预约订单,状态为待使用
  6. 写入扣费流水

这六步必须在同一个事务里,任何一步失败,前面的操作全部回滚。

@Transactional(rollbackFor = Exception.class) public ReservationVO book(Long userId, Long sessionId) { // 1. 场次校验 // 2. 用户余额校验 // 3. 原子预占名额 // 4. 扣款 // 5. 生成订单 // 6. 写流水 }

这里有一个新手几乎必踩的坑:rollbackFor 必须写清楚。Spring 的 @Transactional 默认只回滚 RuntimeException 和 Error,如果你代码里抛出的是受检异常,事务不会回滚。所以业务异常最好统一使用自定义运行时异常,配合全局异常处理器返回友好提示。

4.2 原子更新与并发方案选择

第三步的原子更新本质上是一种乐观锁思路:通过 where 条件保证只有满足剩余容量条件才更新成功。如果 update 返回影响行数 0,说明场次已满或状态不对,直接抛“该场次已满”。

对比一下常见方案:

  • 悲观锁(SELECT FOR UPDATE):简单直接,但对数据库连接占用时间长,并发一高就锁等待
  • 版本号乐观锁:每次更新带 version,失败后重试,适合读多写少场景
  • 条件更新(本系统方案):一条 SQL 完成检查和更新,天然并发安全

预约场景用原子 update 最省事也最可靠。因为 booked_count < max_count 这个条件本身就是并发安全的,数据库行锁保证同一时刻只有一个事务能更新成功。很多秒杀系统的库存扣减用的就是这个思路。

4.3 防重复预约:唯一索引兜底

刚才强调的业务约束下沉,落实到 SQL 上就是在建表时直接加唯一索引:

ALTER TABLE reservation ADD UNIQUE KEY uk_user_session (user_id, session_id);

用户第二次对同一场次发起预约时,插入被数据库拒绝,抛出 DuplicateKeyException。在 Service 层捕获这个异常,转成“您已预约过该场次”的提示即可。

但这里有个细节容易踩坑:如果用户先预约后取消,数据库里仍留着一条取消状态的记录,唯一索引会拦住用户再次预约。所以处理方式是把 status 也加进唯一索引:

ALTER TABLE reservation ADD UNIQUE KEY uk_user_session_status (user_id, session_id, status);

注意 status=已取消(2)的记录不参与唯一约束,因为用户取消之后可以重新预约。这个业务约定一定要在代码注释里写清楚,不然过两个月再读代码,没人记得当初为什么这么设计。

4.4 取消预约与退款逻辑

取消预约同样在一个事务里完成四件事:

  1. 查询预约单,校验归属人正确、状态为待使用
  2. 更新预约单状态为已取消,记录取消时间
  3. 场次表 booked_count 原子减 1
  4. 用户余额退回,写入退款流水(type=3)

用余额支付就直接退到余额。实际生产环境对接微信或支付宝的话需要调用支付平台退款接口,毕设一般用余额模拟支付就够了,但代码里可以预留 pay_type 字段,说明未来可对接第三方支付,答辩时这是一个加分点。

还有一个规则设计值得提:临近开场是否允许取消。真实游泳馆通常会有截止时间,比如开场前 1 小时不允许取消。这个规则建议做成配置项 cancel_deadline_minutes 放进系统参数表,而不是硬编码。硬编码一时爽,改需求的时候谁都找不到入口。

5. 实操记录:从环境搭建到跑通全流程

5.1 环境准备清单

我把这套项目从零跑起来的完整操作过一遍,跟着走基本不会卡壳。

第一步,装 JDK 8 和 Maven。JDK 配置好 JAVA_HOME 环境变量,Maven 的 settings.xml 里把镜像源换成国内常用源,默认中央仓库在国内网络环境下经常卡死在下载 jar 包的环节。

第二步,装 MySQL 8.0。安装完后做两件事:确认服务启动,把数据库账号密码记好。遇到连接失败,百分之七八十是时区或账号问题,不是代码问题。

第三步,导入项目到开发工具。用工具自带的 Maven 面板刷新依赖,首次导入会慢一点,等右下角进度条走完再运行,别着急。

第四步,建库。执行项目附带的 SQL 脚本前,先手动执行 create database,确认库名与连接串一致。执行完脚本后,再核对配置文件里的用户名、密码、库名有没有笔误,这是启动报错的高发区。

5.2 配置文件的几个关键点

application.yml 里这几个配置值得展开讲:

spring: datasource: url: jdbc:mysql://localhost:3306/swim_pool_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true server: port: 8080

serverTimezone=Asia/Shanghai 解决的是 MySQL 8.0 默认时区与本地时区不一致导致的启动报错。map-underscore-to-camel-case 让数据库的 user_name 自动映射到实体的 userName,省去手动指定列映射。log-impl 配成 StdOutImpl 可以在控制台打印 SQL,开发调试必需,但部署演示时建议去掉,避免 SQL 日志暴露表结构细节。

5.3 核心代码实现片段

场次列表查询是首页核心接口,用 MyBatis-Plus 的 Wrapper 就能写明白:

public List<SessionVO> listSessions(Long poolId, LocalDate date) { LocalTime now = LocalTime.now(); return sessionMapper.selectList( new LambdaQueryWrapper<PoolSession>() .eq(PoolSession::getPoolId, poolId) .eq(PoolSession::getSessionDate, date) .ge(PoolSession::getEndTime, now) .orderByAsc(PoolSession::getStartTime) ).stream().map(this::toVO).collect(Collectors.toList()); }

预约接口的 Controller 入口:

@PostMapping("/api/reservation") @PreAuthorize("hasRole('USER')") public Result<?> createReservation(@RequestBody ReservationRequest request) { ReservationVO vo = reservationService.book( SecurityUtils.getCurrentUserId(), request.getSessionId() ); return Result.success(vo); }

这个 @PreAuthorize 注解做的接口级权限控制,配合启动类上的方法级安全配置,比手写 if 判断优雅很多。答辩时如果能把“认证”和“授权”是两个层次这件事讲清楚,就是一个很好的亮点。

签到核销的接口逻辑也值得看:工作人员输入用户的订单号,后端校验订单状态为待使用,然后把状态改成已签到,写入签到时间。这个操作同样要加事务,因为签到和场次人数的数据联动不能出错。

6. 常见问题排查与避坑指南

6.1 我踩过的三个典型坑

第一批坑是时区与日期问题。开发时本地运行一切正常,打包部署后就发现预约日期全部不对,差 8 个小时。原因是服务器默认时区不同,MySQL 连接串没加 serverTimezone,或者 Jackson 序列化时没指定时区。排查方法很直接:前端传一个日期,后端打印出来对比,差 8 小时基本就是时区问题,按上面配置文件改就行。

第二批坑是事务不生效。Service 方法加了 @Transactional,数据异常时却没有回滚。排查发现是同类内部方法调用:一个方法调用同一个类里另一个带事务的方法,Spring 的代理机制没有介入,事务自然不生效。这就是典型的自调用问题,解决办法是把事务注解加到外部入口,或者注入自身代理对象。

第三批坑是懒加载异常。在 Service 里查完关联的懒加载字段,返回 DTO 给前端时才去访问,发现 Session 已经关闭。处理方式有两种:一是事务内把字段拷贝到 VO,事务关闭后不再访问实体;二是给整个查询方法加事务。我更推荐前者,职责清晰,而且能避免把多余字段暴露给前端。

6.2 高频报错速查表

整理成一张速查表,照着排查就行。

报错场景现象解决方案
启动无法连接数据库Could not create connection to database server检查 MySQL 服务是否启动、连接串、账号密码、serverTimezone
权限被拒绝Access denied for user核对配置文件用户名密码,注意是否有多余空格
端口占用Port 8080 was already in use改端口,或查进程占用并清理
Mapper 方法找不到Invalid bound statement检查 Mapper 接口全限定名、XML namespace、MapperScan 路径
SQL 语法错误SQLSyntaxErrorException多半是字段名用了保留字,加反引号处理
重复预约提示DuplicateKeyExceptionService 层捕获并转为友好提示
驱动找不到ClassNotFoundException: com.mysql.cj.jdbc.Driverpom.xml 引入 mysql-connector-j 依赖

6.3 答辩前容易被追问的点

最后聊几个导师大概率会问的问题,提前准备好,现场不慌。

“并发场景下如何防止超卖?”回答思路:预约时不先查再改,而是用一条原子更新 SQL 在 where 条件里加上 booked_count < max_count,数据库行锁保证同一场次只有一个事务能扣减名额,事务回滚保证数据一致。

“预约和扣费不是一个操作,怎么保证一致性?”回答思路:用声明式事务把预占名额、扣费、生成订单、写流水放在同一个事务里,任何一步失败全部回滚。这是本地事务保证的强一致,如果未来拆分分布式,再考虑分布式事务方案。

“余额不足但场次还有空位,点击预约会发生什么?”回答思路:事务内第一步校验余额,不足则抛业务异常,整个事务回滚,场次名额不会被占用。关键是要强调校验发生在事务内部,而不是事务外部。

“为什么用 Session 做认证,不换 JWT?”回答思路:Session 简单、服务端可控,单体部署没有会话共享问题;JWT 无状态但过期控制难、无法主动失效。毕设单体场景 Session 足够,代码里留了扩展空间。

最后再说几句实话

这套项目做完,我最真实的体会是:毕设选题的价值不在于技术新,而在于你能否把一个完整业务闭环讲透、做稳。预约管理系统的每个环节——数据库设计、事务控制、并发防超卖、状态流转——都能对应到真实业务场景,用心做了,答辩和简历上都有的写。

如果你准备照着这个思路自己写,我给三个建议。第一,数据库脚本先设计好再写代码,改表结构的成本远高于改代码。第二,核心预约接口第一版就带上事务和并发控制,别抱着“先实现功能再加锁”的想法,后面补并发处理要重构一大片。第三,写注释顺手一点,两周后你自己回头看代码,能省大量回忆成本。

这套系统后续还能扩日历视图、教练预约、短信通知、真实支付对接,从游泳池的预约管理拓展成通用的按时段预约平台,也不是没可能。不过那是后话,眼下最要紧的是先把手头这条业务链路跑通、跑稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询