简介一份基于Java技术的游泳馆管理系统课程设计源码包面向计算机专业学生及需要完成数据库课程设计的开发者。系统涵盖会员管理、预约管理、场地管理、收费管理四大核心模块设计上处理预约冲突、场地调度、多方式计费等典型业务场景可帮助学习者理解ER模型设计、关系型数据库操作与Java桌面端实现。压缩包共1286个文件以htm页面、java源码、class字节码及sql脚本为主另含少量配置文件、图片与MDF数据库文件整体约3.81MB便于直接导入工程查看。已有1535人学习此资源适合作为毕业设计或课程项目参考从中可获取完整的数据库建表脚本、功能模块实现思路及JavaSwing/JavaFX界面布局示范。1. 游泳馆管理系统一个能讲清楚业务闭环的 Java 全栈实战项目做课程设计或毕设的同学最怕的不是代码写不出来而是答辩时讲不清业务逻辑——尤其是游泳馆这种有「会员卡余额、课时扣费、场次预约」三个核心业务叠加的系统一旦数据对不上整套系统就成了摆设。这套基于 Java 的游泳馆管理系统正好能把这三个环节串成一个完整闭环前台办卡、会员预约、到场签到扣费、余额流水自动记录。它适合正在做 Java Web 课程设计、毕业设计或者想用 Spring Boot 练手完整业务链路的人。2. 系统怎么拆角色权限、核心实体与 MySQL 表设计的取舍2.1 角色与权限三种角色为什么要分开建模游泳馆的实际使用场景里至少有三类人管理员负责全局配置和统计前台负责办卡和收银会员负责预约场次和查询余额。如果全部揉在一个入口里权限控制就是一笔糊涂账。常见做法是用一个 user 表加 role 字段区分角色而不是引入 Spring Security 那套完整的权限框架。原因很简单课程设计和练手项目里用户量级在几十到几百之间引入 Security 反而把 UserDetailsService、过滤器链、密码加密器的配置成本抬高了不少答辩时还容易被追问「你这权限模型是怎么设计的」。用一个 role 字段加一个拦截器既能把角色边界讲清楚代码量也控制在合理范围。表设计上我会这样处理user 表只存登录账号、密码、角色、昵称、联系电话管理员和前台也是 user只是 role 不同会员的卡信息单独放 member_card 表因为一张卡有独立的余额、有效期和状态和 user 是一对一关系每张卡的消费流水放 card_consumption 表方便月末对账。角色虽然只有三种但各自能看到的菜单完全不一样。管理员看全场次和营收统计前台只能办卡和登记场次会员只能预约和查自己的流水。这个「菜单按角色渲染」的逻辑后面前端联调章节会具体说。2.2 核心表设计会员卡、场次、预约记录怎么关联先把核心表结构列出来这是整套系统的骨架。我一般会在一开始就把这些表的 SQL 建好而不是写代码写到一半再补表。-- 会员卡表 CREATE TABLE member_card ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 关联 user 表, card_no VARCHAR(32) NOT NULL COMMENT 卡号前台显示用, card_type TINYINT NOT NULL DEFAULT 1 COMMENT 1-次卡 2-月卡 3-年卡, balance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 余额或剩余次数, valid_until DATE NOT NULL COMMENT 有效期至, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 2-冻结 3-过期 4-注销, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_card_no (card_no) ); -- 场次表 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL COMMENT 课程名如蛙泳基础, coach_name VARCHAR(30) NOT NULL COMMENT 教练姓名, start_time DATETIME NOT NULL COMMENT 开课时间, end_time DATETIME NOT NULL COMMENT 结束时间, max_count INT NOT NULL DEFAULT 10 COMMENT 最大容纳人数, booked_count INT NOT NULL DEFAULT 0 COMMENT 已预约人数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-可约 2-已满 3-已结束 4-已取消 ); -- 预约记录表 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, card_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-已预约 2-已签到 3-已取消 4-已过期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_schedule (user_id, schedule_id) );这里有几个设计取舍值得展开。appointment 表里的 uk_user_schedule 唯一索引是防重复预约的关键数据库层面就把同一会员约同一场次这个操作堵死了不用完全依赖后端代码判断。schedule 表里有 booked_count 这个冗余字段虽然违背了三范式但在这种场景下是对的——每次预约只要UPDATE schedule SET booked_count booked_count 1 WHERE id ? AND booked_count max_count一条 SQL 就完成了防超卖不用先查再用程序判断性能和安全性都好很多。2.3 状态机与预留字段怎么避免后期改表很多课程设计项目翻车不是翻在功能没写出来而是翻在做到一半发现表结构不够用要加字段、加状态。我自己的习惯是凡是涉及「卡」和「预约」这种生命周期比较长的业务一开始就把状态字段和流转关系设计好。会员卡的状态流转是这样的正常 → 冻结 → 注销正常 → 过期。不能出现「从过期直接变正常」这种反直觉的跳跃除非是后台管理员手动延期。场次状态是可约 → 已满 → 已结束或者可约 → 已取消。预约记录的状态最复杂已预约 → 已签到 → 已过期已预约 → 已取消。这几个状态在前端的表现就是按钮可不可点、列表里显示什么标签。如果状态值只当成一个普通 int 存进去前端到后端到处是散落的 if 判断后期最痛苦。我的做法是统一用一个状态工具类管理public class CardStatus { public static final int NORMAL 1; public static final int FROZEN 2; public static final int EXPIRED 3; public static final int CANCELED 4; public static String desc(int status) { switch (status) { case NORMAL: return 正常; case FROZEN: return 冻结; case EXPIRED: return 已过期; case CANCELED: return 已注销; default: return 未知; } } }调用的时候只从常量取值不会出现「1 代表什么来着」这种记忆负担。预留字段方面每张表我都强制带上 create_time 和 update_timeorder by 和排查问题的时候都是救命稻草。另外凡是金额、次数相关的字段统一用 DECIMAL千万别用 Float 和 DoubleJava 侧再用 BigDecimal 接收浮点精度问题在这种业务系统里是必须提前避开的坑。3. 接口链路与事务边界从 controller 到 mapper 的关键代码3.1 项目结构与启动配置这套系统的工程结构是标准的 Spring Boot 分层。我拿到源码的第一件事是先看 pom.xml 里的依赖版本再看 application.yml 里的数据库配置最后才看代码。很多同学拿到项目先点 Run结果起不来回来才发现是 JDK 版本和 MySQL 连接配置不对。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependenciesSpring Boot 2.7.18 是 2.x 系列里比较稳妥的版本mybatis-spring-boot-starter 用 2.3.1 跟它匹配。如果你的 JDK 是 17用 Spring Boot 2.7 没问题如果是 JDK 8那就更没问题了。mysql-connector-j 是 MySQL 官方驱动的新坐标老版本的 mysql-connector-java 在 Spring Boot 2.7 里也能用但新项目没必要再用旧坐标。application.yml 里最容易出问题的是 MySQL 的连接参数spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/swimming?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .htmlserverTimezoneAsia/Shanghai 这个参数后面避坑章节会专门讲这里先记住不配它时间大概率会差 8 小时。allowPublicKeyRetrievaltrue 是 MySQL 8.0 连接时常见的一个报错点密码用 caching_sha2_password 加密方式连接时经常会遇到先加上能省不少事。3.2 controller-service-mapper以办卡为例的完整链路选「办卡」这个场景来走一条完整链路因为它是整套系统里最有代表性的入口操作要查 user 是否存在、要生成卡号、要初始化余额、要写 member_card 表、要初始化一条流水记录。Controller 层只做参数接收和返回业务逻辑全部放 ServiceRestController RequestMapping(/api/card) public class CardController { Autowired private CardService cardService; PostMapping(/issue) public Result issueCard(RequestBody IssueCardRequest req) { if (req.getUserId() null || req.getCardType() null) { return Result.error(参数不完整); } try { cardService.issueCard(req); return Result.success(办卡成功); } catch (BizException e) { return Result.error(e.getMessage()); } } }Service 层核心逻辑长这样Service public class CardService { Autowired private CardMapper cardMapper; Autowired private UserMapper userMapper; Transactional(rollbackFor Exception.class) public void issueCard(IssueCardRequest req) { User user userMapper.selectById(req.getUserId()); if (user null) { throw new BizException(用户不存在); } // 每个用户只能有一张有效卡 Integer exist cardMapper.countValidByUserId(req.getUserId()); if (exist ! null exist 0) { throw new BizException(该用户已有有效会员卡); } MemberCard card new MemberCard(); card.setUserId(req.getUserId()); card.setCardNo(generateCardNo()); card.setCardType(req.getCardType()); card.setStatus(CardStatus.NORMAL); // 次卡初始余额为次数月卡/年卡初始余额为金额 if (req.getCardType() 1) { card.setBalance(new BigDecimal(req.getInitCount())); } else { card.setBalance(new BigDecimal(req.getInitAmount())); } card.setValidUntil(calcValidUntil(req.getCardType())); cardMapper.insert(card); // 写入初始流水 consumptionMapper.insertInit(card.getId(), 开卡, card.getBalance()); } }两个点值得细看。第一是 Transactional(rollbackFor Exception.class)——默认情况下 Spring 只在 RuntimeException 时回滚checked exception 是不回滚的所以这里的 rollbackFor 必须显式声明否则开卡成功但写流水失败数据就脏了。第二是生成的卡号要保证唯一我用时间戳加随机数拼成 16 位private String generateCardNo() { return S System.currentTimeMillis() String.format(%04d, new Random().nextInt(10000)); }虽然理论上存在极小概率碰撞但数据库里已经建了 uk_card_no 唯一索引兜底是够的。3.3 事务边界与并发扣费和预约防超卖怎么做办卡还只是单条链路扣费和预约才是真正考验事务边界的地方。扣费发生在「签到」动作里会员预约场次后到前台签到此时要同时做三件事——把 appointment 状态改成已签到、扣减 member_card 余额、写入一条扣费流水。这三件事必须在一个事务里中间任何一步失败都要全部回滚。Transactional(rollbackFor Exception.class) public void signIn(Long appointmentId) { Appointment app appointmentMapper.selectById(appointmentId); if (app null || app.getStatus() ! AppointmentStatus.BOOKED) { throw new BizException(预约状态异常无法签到); } // 检查场次是否已结束 if (scheduleMapper.isExpired(app.getScheduleId())) { throw new BizException(场次已结束不能签到); } MemberCard card cardMapper.selectById(app.getCardId()); if (card.getStatus() ! CardStatus.NORMAL) { throw new BizException(会员卡状态异常); } BigDecimal balance card.getBalance(); BigDecimal fee new BigDecimal(30.00); if (balance.compareTo(fee) 0) { throw new BizException(余额不足); } cardMapper.deductBalance(app.getCardId(), fee); appointmentMapper.updateStatus(appointmentId, AppointmentStatus.SIGNED); consumptionMapper.insert(app.getCardId(), 签到扣费, fee.negate()); }预约的防超卖在 SQL 层面实现比锁更有说服力// 乐观锁思路只有当前预约数小于上限时才更新 int rows scheduleMapper.increaseBookedCount(scheduleId, maxCount); if (rows 0) { throw new BizException(该场次已满预约失败); }update idincreaseBookedCount UPDATE schedule SET booked_count booked_count 1 WHERE id #{scheduleId} AND booked_count lt; #{maxCount} AND status 1 /update这里的关键是 where 条件里带上了 booked_count maxCount 和 status 1数据库行锁会串行化同一条 schedule 记录的并发修改后到的事务会因为影响行数为 0 而失败。用程序先查询再判断是做不到这点的——两个请求同时查到剩余 1 个名额就超卖了。这个细节在答辩时讲出来比背 JVM 调优更能证明你真的理解了并发问题。4. 前端联调细节时间选择、表单校验与状态展示4.1 场次冲突的交互处理前端把预约页面的时间选择做好了后端能少一半的异常处理。场次冲突分两种一是会员在同一时间段约了两个场次二是约了一个场次后没取消又去约另一个同时段的场次。前端能做的是提交前检查后端必须做的是最终裁决。我一般会在预约表单里用 datetime-local 输入控件让用户选「日期 时间」再用 JS 在提交时做一次基础校验function checkScheduleConflict(scheduleId) { let selectedDate $(#startTime).val(); if (!selectedDate) { alert(请选择场次时间); return false; } $.ajax({ url: /api/appointment/check-conflict, method: GET, data: { scheduleId: scheduleId, selectedDate: selectedDate }, async: false, success: function (res) { if (res.code ! 0) { alert(res.msg); flag false; } } }); return flag; }后端对应做一个查询当前用户在该场次开始时间和结束时间区间内是否已有状态为「已预约」或「已签到」的记录。有就直接拒绝提示「该时间段已有预约」。这个校验放在 Service 里用一条 count 查询完成不要加载所有预约记录到内存里再遍历判断。4.2 会员卡状态展示与余额刷新会员卡状态是整套系统里用户最关心的信息。前端列表里我习惯用标签加颜色区分正常显示绿色「正常」冻结显示橙色「冻结」过期和注销显示灰色。Thymeleaf 模板里这样写td span th:if${card.status 1} classbadge bg-success正常/span span th:if${card.status 2} classbadge bg-warning冻结/span span th:if${card.status 3} classbadge bg-secondary已过期/span span th:if${card.status 4} classbadge bg-dark已注销/span /td余额更新的交互有一个容易漏掉的细节签到扣费成功后会员卡列表页的余额必须实时刷新不能用「重新查询」这种最笨的办法掩盖问题。做法是在扣费接口里直接返回新的余额前端拿到后用 JS 更新对应位置的文本节点同时把一条消费记录追加到流水表。如果每次扣费都整页刷新演示的时候会显得很不跟手答辩观感差不少。4.3 权限菜单按角色渲染菜单渲染不是什么高深技术但很多课程设计只做了后端接口的权限拦截前端菜单却不区分角色管理员打开页面能看到会员的操作入口点击后弹出「无权限」体验很差。正确的做法是登录成功后把当前用户的 role 存进 session模板里用 th:if 控制菜单项显示li th:if${session.user.role 1} classnav-item a href/admin/stats classnav-link营收统计/a /li li th:if${session.user.role 1 || session.user.role 2} classnav-item a href/schedule/list classnav-link场次管理/a /li li th:if${session.user.role 3} classnav-item a href/appointment/my classnav-link我的预约/a /li这里 role 的约定1 是管理员2 是前台3 是会员。前后端都要用同一套约定后端拦截器校验的是接口路径前缀比如/api/admin/**只有 role1 能访问。两处都别写散落的魔法数字用常量类统一管起来不然改一处漏一处最后前后端权限对不上排查起来非常痛苦。5. 运行部署与避坑排查本机到演示环境的高频问题5.1 环境准备与数据初始化先确认三个东西JDK 版本、MySQL 版本、Maven 配置。我的建议是 JDK 8 或 JDK 17MySQL 5.7 或 8.0Maven 用 3.6 以上。源码里如果带了 sql 文件先按文件名排序挨个执行注意执行顺序父表在前、子表在后。数据库初始化完成后项目能不能起来只需要看两个日志位置第一个是 Spring Boot 启动时的数据源初始化日志第二个是 MyBatis mapper 的加载日志。如果启动过程中报Invalid bound statement那就是 XML 文件没被扫描到检查 application.yml 里 mapper-locations 配置是不是指向了 classpath:mapper/*.xml。5.2 启动即失败端口、版本、依赖冲突现象一启动时提示Port 8080 was already in use。 原因本机有其他程序占用了 8080。 解决在 application.yml 里改端口或者找出占用进程。Windows 下netstat -ano | findstr 8080然后到任务管理器结束对应 PID 的进程。一般改了端口就没事。现象二连接数据库报Public Key Retrieval is not allowed。 原因MySQL 8.0 默认认证插件是 caching_sha2_passwordJDBC 驱动在非 SSL 连接下需要显式允许获取公钥。 解决连接串里加allowPublicKeyRetrievaltrue和useSSLfalse。这两个参数官方文档里明确写了加了就不报。现象三启动后发现时间比实际时间早了 8 小时。 原因MySQL 驱动连接字符串里没有指定时区JVM 取的是操作系统时区MySQL 服务端用的又是 UTC。 解决连接串加serverTimezoneAsia/Shanghai。这只是连接层面的修复MySQL 服务端的 time_zone 也要确认是 SYSTEM 还是 08:00两处最好保持一致。现象四MyBatis 的 XML 里写号导致编译报错。 原因XML 里是标签开头必须转义。 解决用lt;代替。前面 3.3 节里的预约防超卖 SQL 就是典型场景。5.3 业务数据异常余额对不上、预约状态残留现象一会员卡余额扣多了流水记录却是对的。 原因扣费逻辑里没有把扣费和写流水放到同一个事务里扣费成功后写流水失败事务只回滚了流水余额已经扣了。 解决检查 Service 方法上是否有Transactional(rollbackFor Exception.class)且该方法内部调用的 mapper 方法不能被同类内部调用绕过代理。同类内部调用导致事务注解失效是 Spring 的一个老坑务必注意。现象二会员取消预约后场次的可预约人数没恢复。 原因取消时只改了 appointment 表的 status没有把 schedule 表的 booked_count 减回去。 解决取消预约的逻辑要和预约一样写成事务两步一起提交appointment 改状态schedule 减计数。现象三签到时间已经过了还能签到。 原因后端校验只查了 appointment 的状态没查 schedule 的开始时间。 解决签到前先判断当前时间是否在 start_time 和 end_time 区间内或者至少判断当前时间不能晚于 end_time。这个逻辑放在 Service 里别放在 Controller 里——接口如果被直接拿工具调Controller 的校验是挡不住的。5.4 部署演示打包配置与外部访问课程设计答辩大多需要现场演示。本地 Run 起来是最稳妥的但若要在别的机器上跑可以把项目打成 jar 包。Spring Boot 项目用 Maven 打包非常顺mvn clean package -DskipTests java -jar target/swimming-system.jar-DskipTests是跳过测试阶段省得打包时因为测试用例报错中断。打包出来的是一个可执行 jar内置了 Tomcat直接 java -jar 就能跑。需要的表结构用数据库客户端初始化或者把初始化 SQL 放到 spring.sql.init 配置里启动时自动建表。答辩演示时如果网络环境不稳定别把前端依赖的 CDN 写在页面里Bootstrap 和 jQuery 全部本地化不然断网时页面样式全崩。6. 十分钟验收技巧用一组 Demo 数据跑通核心业务拿到这套系统源码后先别急着翻代码。我习惯的做法是先建库建表启动项目然后用一组预置数据把核心业务闭环走一遍。十分钟足够验证系统能不能用也能看出你对业务的理解程度。第一步在 admin 用户下创建三个测试账号一个前台、一个会员。会员账号再开一张次卡初始次数设 10 次。第二步用管理员账号创建一个场次设最大容纳人数 2 人。第三步用会员账号预约这个场次然后到后台把预约记录标记为签到。第四步回到会员卡列表确认余额从 10 次变成了 9 次消费流水里多了一条「签到扣费」。第五步再去预约同一个场次此时应该提示人数已满预约失败。这套流程走完系统的核心业务全部验证到了角色登录、办卡、建场次、预约、签到扣费、防超卖。如果某一处断掉了直接从日志里的异常堆栈往上摸比满屏代码里瞎找快得多。验证完功能后有个小技巧把数据恢复到初始状态再演示。很多同学演示时翻车就是因为前面乱点了一堆数据场次满了、卡余额扣光了轮到正式演示时点哪个按钮都报错。我的习惯是准备一个 reset.sql 脚本里面只有 TRUNCATE 加重新 INSERT 初始数据演示前跑一遍保证每次演示都从干净状态开始。还有个小经验演示用的账号密码不要设复杂了admin / admin123 这种就很好避免现场输入错误。demo 数据里的会员余额、场次时间也要提前确认别到了答辩现场才发现场次全过期了。从那以后我每次拿到一个新的课程设计项目都会先强制走一遍「登录 → 开卡 → 预约 → 扣费 → 查流水」的闭环验证流程再决定是直接改代码还是先补测试数据。这样操作的效率远比先看代码要高确实帮我在验收和改需求的时候少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取