简介:这套游泳馆管理系统是一份基于Java的数据库课程设计完整项目,面向计算机相关专业学生、Java初学者及需要快速搭建管理系统的开发者。系统围绕游泳馆日常运营,实现会员管理、预约管理、场地管理、收费管理四大核心模块,涉及数据库ER模型设计、预约冲突处理、多支付方式与财务报表等功能,可帮助读者理解Java与关系型数据库的联动开发流程。压缩包共1286个文件,约3.81MB,核心内容以htm页面、java源码、class编译文件为主,同时包含SQL数据库脚本、MDF/LDF数据库文件以及少量配置与图片资源,便于直接导入项目或参考界面与逻辑实现。资源已有1535人学习下载,适合用于课程设计答辩、毕业设计参考或自学提升,通过研究源码和数据库结构,可快速掌握会员信息管理、预订调度、费用结算等业务场景的编码思路。
1. 项目定位:先聊清楚它到底解决什么问题
游泳馆的运营,和普通健身房完全是两种逻辑。健身房的会员办卡之后,进门刷个脸就行;游泳馆不一样,顾客要拿手牌、记入场时间、离场时按小时计费,中间还可能去淋浴、买泳具、请教练,每个环节都要跟一个“会话状态”绑定。几年前我在某健身集团做系统重构时接手过一个游泳馆的运营系统,业务方提的需求整整二十页,最痛的就一句话:前台结账排队排到门口,手牌和订单对不上账。
今天要拆的这个游泳馆管理系统,就是一个典型的 Java Web 单体应用,把场馆运营中最核心的三件事做了闭环:会员管理、手牌计费、订单结算。系统采用经典的 Spring Boot + MyBatis 分层架构,前端页面用服务端渲染,数据库用关系型存储,适合拿来学习、二次开发,也适合小体量场馆直接部署使用。它的可贵之处不在技术多新,而在业务模型完整——手牌从发放到回收的生命周期、超时补费的计算规则、会员卡余额与次卡的扣费差异,这些都是真实场馆每天都在发生的场景,比网上泛滥的教学 demo 更能训练“把业务翻译成代码”的能力。
2. 整体架构拆解:单体分层 + 状态机设计
先建立全局认知,再进代码。这个项目采用经典的 Spring Boot 单体架构,前后端不分离,页面渲染交给服务端模板,适合小团队快速交付和维护。只要并发量不夸张,单体架构的开发和排查效率远高于微服务,这也是我接手这类场馆系统时优先选择它的原因。
2.1 分层结构与模块边界
项目整体分为四个层级,从上到下依次是 controller、service、mapper、entity,这是非常标准的 SSM 分层。控制层只做参数接收和响应封装,业务逻辑全部下沉到服务层,数据访问通过 MyBatis 的 Mapper 接口完成,实体类与数据库表一一对应。这样的分层方式在团队协作时边界清晰:前端工程师只关心 controller 层的接口定义,后端开发在 service 里写业务,不会互相干扰。
除了基础的分层,项目里还单独抽了一个common包,专门放统一返回结果、异常处理、常量定义和工具类。很多教学项目会把返回结果散落在各个 controller 里,这个项目用了一个Result<T>泛型类统一包装,前端拿到的永远是{ code, message, data }三段式结构。这个习惯建议保留,后面接小程序或第三方系统时,统一响应结构能省掉无数联调时间。
2.2 手牌状态机:业务核心的设计思路
游泳馆业务里最容易出问题的不是会员表或订单表,而是手牌状态。一个手牌从入场开始要经历空闲、已发放、使用中、已回收四个状态,任何一步状态错乱都会导致前台对不上账。项目中用定义一个整型字段status来表示手牌状态,配合状态流转方法统一管理。
public class LockerCard { private Integer id; private String cardNo; private Integer status; // 0空闲 1已发放 2使用中 3已回收 private Long memberId; private LocalDateTime startTime; private LocalDateTime endTime; public boolean isAvailable() { return status == 0; } public void issue(Long memberId) { if (!isAvailable()) { throw new BusinessException("手牌当前不可用"); } this.memberId = memberId; this.status = 1; this.startTime = LocalDateTime.now(); } public void startUse() { this.status = 2; } public void recycle() { this.status = 3; this.endTime = LocalDateTime.now(); } }这段代码的核心在于把状态流转收敛成方法,而不是在 service 里到处setStatus。所有对状态的修改都必须经过对应的业务方法,方法内部先做前置校验,不满足条件直接抛异常。这种写法能拦住大量低级错误:比如手牌还没回收就再次发放、会员还在馆内就办了离场结算。参数上startTime在发放时写入,endTime在回收时写入,计费模块只需要计算这两个时间差就能得出时长。
3. 核心模块实战:计费怎么算、会员卡怎么扣、看板数据怎么查
前面把整体架构理顺了,这一章进入实际业务代码。游泳馆管理系统最核心的就是计费引擎,也就是从“用户拿手牌入场”到“离场结算”这段时间内,系统如何计算应缴费用。这块逻辑牵扯到会员折扣、超时规则、夜场特殊时段,稍有不慎账就算错。
3.1 计费引擎:按分钟计费 + 时段规则
计费模块我建议先看BillingService这个类。它不直接对数据库操作,而是把“费用计算”做成一个独立服务,输入是手牌记录,输出是应付金额和结算明细。这样设计的价值在于,将来想支持不同的计费策略(比如包月、按次、小时计费),只需要扩展这个服务,不需要动 controller 层。
@Service public class BillingService { private static final BigDecimal HOURLY_RATE = new BigDecimal("30.00"); private static final BigDecimal MEMBER_DISCOUNT = new BigDecimal("0.85"); private static final int FREE_MINUTES = 15; public BillingResult calculate(LockerCard card, Member member) { long minutes = Duration.between(card.getStartTime(), card.getEndTime()).toMinutes(); BigDecimal amount = BigDecimal.ZERO; if (minutes <= FREE_MINUTES) { amount = BigDecimal.ZERO; } else { BigDecimal chargeMinutes = BigDecimal.valueOf(minutes - FREE_MINUTES); BigDecimal hours = chargeMinutes.divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING); amount = hours.multiply(HOURLY_RATE); } if (member != null && member.isVip()) { amount = amount.multiply(MEMBER_DISCOUNT) .setScale(2, RoundingMode.HALF_UP); } return new BillingResult(amount, minutes); } }这里有几个容易被忽略的细节。第一是前 15 分钟免费,这是很多场馆的引流策略,但代码里必须单独判断,否则会出现 0 分钟也收费的 bug;第二是计费时长向上取整,游了 61 分钟按 2 小时计费,RoundingMode.CEILING就是为了处理这种边界;第三是会员折扣只在原价基础上算,不能先折扣再加时间,否则金额会对不上。这段代码堪称全项目最容易被改坏的地方,我之前见过有同事把折扣写进单价里,导致后续所有报表金额错误。
3.2 会员卡扣费:余额卡与次卡的双轨逻辑
会员管理模块里最有意思的是卡类型设计。系统区分了储值卡(存钱,按次扣费)和次卡(存次数,按时段限制),两种卡在结算时的处理逻辑完全不同。储值卡要走账户余额扣款,次卡要校验剩余次数和有效期,两套逻辑不能混在一个 if 里。
public void settleOrder(Order order) { MemberCard card = memberCardMapper.selectById(order.getMemberCardId()); if (card.getType() == 1) { // 储值卡:扣余额 BigDecimal balance = card.getBalance().subtract(order.getAmount()); if (balance.compareTo(BigDecimal.ZERO) < 0) { throw new BusinessException("余额不足"); } card.setBalance(balance); } else if (card.getType() == 2) { // 次卡:扣次数 if (card.getRemainingTimes() <= 0) { throw new BusinessException("次数已用尽"); } if (card.getExpireTime().isBefore(LocalDate.now())) { throw new BusinessException("次卡已过期"); } card.setRemainingTimes(card.getRemainingTimes() - 1); } memberCardMapper.updateById(card); }这段代码把两个关键业务规则落在实处:储值卡必须在扣款前判断余额是否充足,用compareTo而不是直接相减再判断,是为了避免浮点运算误差带来的误判;次卡不仅看次数,还要看有效期,不少场馆的次卡是季度卡或半年卡,过期未使用次数直接作废。业务规则虽然是死的,但代码逻辑必须有明确的分支,否则后期加卡种时只能在 main 方法里堆 if,最终难以维护。
3.3 数据看板:一张 SQL 搞定经营统计
场馆老板最关心的不是单个订单,而是今天的营收、入场人次、高峰时段。项目里用一个DashboardService承担统计查询,它的实现不走 ORM 逐条查询,而是直接拼 SQL 聚合,效率比在 Java 里循环计数高一个数量级。
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS order_count, SUM(amount) AS total_amount, AVG(DATEDIFF(end_time, start_time)) AS avg_duration FROM orders WHERE create_time BETWEEN #{startDate} AND #{endDate} GROUP BY day ORDER BY day;对应 Mapper 接口里只需要一个返回List<DailyStatVO>的方法。这个查询的巧妙之处在于用DATE_FORMAT按天分组,数据库端直接完成聚合,返回的行数最多几十条,内存压力可以忽略。DATEDIFF算的是天数差,适合看日均时长趋势;如果业务上需要更细的按小时统计,可以把粒度改成DATE_FORMAT(create_time, '%Y-%m-%d %H'),查询基本不用改结构。
4. 数据库模型与关键 DDL:先把表结构吃透
泳馆管理系统的表量不大,一共十来张表,但表之间的关联关系踩过不少坑。这一章直接讲透订单表、会员表、手牌表三张核心表的设计思路,以及它们之间如何通过外键逻辑而非物理外键来关联。物理外键在互联网项目中基本是被抛弃的,性能损耗大、迁移麻烦,这个项目也没有用,而是靠业务代码保证数据一致性。
4.1 三张核心表的结构解读
先看最核心的 orders 表。这张表是整个系统的交易源头,每一次结算都会在这里插入一条记录,它的字段设计直接决定了后续报表统计的可行性。
CREATE TABLE `orders` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `member_id` BIGINT DEFAULT NULL COMMENT '会员ID,散客为NULL', `card_id` BIGINT NOT NULL COMMENT '手牌ID', `amount` DECIMAL(10,2) NOT NULL COMMENT '应付金额', `discount_amount` DECIMAL(10,2) DEFAULT '0.00' COMMENT '折扣金额', `pay_type` TINYINT NOT NULL DEFAULT '1' COMMENT '支付类型 1微信 2支付宝 3现金', `status` TINYINT NOT NULL DEFAULT '1' COMMENT '1已支付 2已退款', `start_time` DATETIME NOT NULL, `end_time` DATETIME NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_member` (`member_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';member_id允许为空是有意的设计,因为散客(临时入场者)不归属任何会员,这与会员业务解耦。discount_amount单独存储是因为系统要同时记录原始应收和实际实收,方便月底核对账单。两张索引idx_member和idx_create_time分别服务会员订单查询和日结报表,这也是最常见的两条查询路径。创建订单号时从不在代码里手动拼——用数据库时间戳 + 序列号最简单,但为了避免并发重复,库表上建了唯一索引uk_order_no。
然后是 hand_card 手牌表,它的核心是状态字段和管理员操作记录。每次发牌、还牌都会更新这张表,同时插入操作流水表hand_card_log,这样出现对账差异时可以回溯整个过程。
4.2 会员卡与订单之间的关联查询
会员卡表member_card上有一个容易忽略的点:字段stored_balance和member_id之间的对应关系。按照业务设计,一个会员可以同时拥有多张卡(一张储值卡 + 一张次卡都是可能的),所以关联关系是 member 一对多 member_card,再通过 member_card_id 关联订单。查询“某会员最近十笔订单”时,必须通过 card 表中转,不能直接从 member 表 join orders。
SELECT o.order_no, o.amount, o.create_time, mc.card_no FROM orders o INNER JOIN member_card mc ON o.card_id = mc.id WHERE mc.member_id = #{memberId} ORDER BY o.create_time DESC LIMIT 10;从性能角度看,这张 join 的查询命中了两张表的聚集索引,实际执行成本极低。重点在于理解为什么要多关联一层:因为订单表中的 card_id 并不是绝对对应会员,散客订单没有 member 关系;通过 member_card 中转,既保证了查询结果的准确性,也不破坏 orders 表“宁可冗余字段也不强行多表关联”的设计原则。
5. 本地环境搭建与运行:从拉代码到跑通首页
看代码是一回事,把项目跑起来又是另一回事。这一章直接按步骤走,从环境准备到最终启动成功,每一步都标注了可能翻车的点。
5.1 环境准备与配置修改
项目基于 JDK 1.8 以上 + MySQL 5.7/8.0 + Maven 3.6+,这是最基本的运行条件。在动手之前,先把项目根目录下的application.yml打开,检查数据源配置是否正确。
server: port: 8088 spring: datasource: url: jdbc:mysql://localhost:3306/swim_pool?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.swimpool.entity configuration: map-underscore-to-camel-case: true推荐使用 MySQL 8.0 搭配com.mysql.cj.jdbc.Driver,如果本地是 MySQL 5.7,要把 driver 换成com.mysql.jdbc.Driver,同时去掉serverTimezone参数。这个坑我踩过一次,当时项目在同事电脑上跑得好好的,到我这里启动直接报Public Key Retrieval is not allowed错误,原因就是 MySQL 8.0 以下的驱动版本不兼容新的认证插件。
修改完配置文件后,打开 MySQL 命令行,执行根目录下提供的schema.sql建库脚本。脚本里除了建表语句,还插入了管理员初始账号和测试数据,如果不需要演示数据,注释掉对应的 INSERT 语句即可。
5.2 启动命令与常见报错处理
所有配置就绪后,在项目根目录执行以下命令:
mvn spring-boot:run第一次运行会自动下载依赖,需要耐心等待。启动成功后控制台会打印 Spring Boot 的启动日志,看到Started Application in x.xxx seconds就说明服务已经起来了。浏览器访问http://localhost:8088,应该能跳转到登录页面,初始账号密码在schema.sql的注释里有备注,也可以直接查数据库中的sys_user表。
实际操作中,最常遇到三类报错。第一类是Access denied for user 'root'@'localhost',这是账号密码不对,检查application.yml里的配置即可。第二类是Table 'swim_pool.xxx' doesn't exist,说明schema.sql没有执行成功,重新执行一次,注意先执行DROP TABLE再CREATE TABLE。第三类是端口冲突,日志里会显示Port 8088 was already in use,把端口改成 8090 或者杀掉占用进程都能解决。
5.3 避坑记录:我在跑这个项目时踩到的四个坑
第一个坑是数据库时区问题导致的日期偏移。系统里所有时间字段用LocalDateTime存储,如果 JDBC 连接串没有设置serverTimezone=Asia/Shanghai,订单的开始时间和实际入场时间可能相差 8 小时,计费时长直接多算。解决方式是统一在所有环境里使用 MySQL 的CST配置,并且在代码中统一用Asia/Shanghai时区创建时间对象,不要依赖服务器默认时区。
第二个坑是 MyBatis XML 中使用了 PostgreSQL 语法。这个项目的 Mapper XML 里有一个统计查询用了date_trunc函数,这是 PostgreSQL 的写法,在 MySQL 上直接报语法错误。换成DATE_FORMAT并用GROUP BY day解决,这提醒我们在复用网上代码时一定要先确认数据库方言。
第三个坑是target/classes目录下的旧配置文件残留。有时候改了application.yml但没生效,排查半天发现 target 目录里有缓存的旧配置。执行mvn clean再重新启动,问题立刻消失。这属于 Maven 项目的经典翻车点,不是代码逻辑问题。
第四个坑是浏览器缓存导致前端页面样式错乱。项目用了 Thymeleaf 模板渲染,改了 HTML 和 CSS 后浏览器总是显示旧页面。解决方法是打开开发者工具勾选 Disable cache,或者在后端配置里关闭模板缓存,不然每次调试都怀疑自己代码没保存。
6. 二次开发与改造方向:这三处值得动手
看完前面这些内容,你对这个项目已经比大多数下载它的人更熟了。这一章聊聊改造思路,我挑三个最容易落地、也最能体现价值的方向,每个都给出具体的改法建议,你可以根据自己的业务场景选一个下手。
第一个方向是支付对接。目前订单是手动结算,代码里写死了三种支付类型,但你可以在PayService接口里增加一个实现类,对接微信支付或支付宝支付。只需要把pay_type字段改成回调通知触发更新订单状态,新增一张payment_record表记录第三方返回的交易流水号。建议先对接支付宝的沙箱环境,官方文档完整,不需要营业执照也能拿到测试账号,联调成本最低。
第二个方向是短信通知。游泳馆有这个需求:顾客入场时发一条入场通知,离场结算时推送消费明细。你可以在结算成功后加一个 MQ 异步消息,或者先用最简单的方式——在线程池里调用短信网关接口。需要新增sms_config表存模板 ID 和签名,然后把Member实体的phone字段利用起来。这个改动不碰核心计费逻辑,风险很小,但运营感知极强。
第三个方向是数据大屏。前面已经有一个DashboardService做统计,你可以把数据指标推给前端,做一个今日营收、实时在场人数、手牌使用率的可视化页面。技术栈上不用引入重型框架,用 ECharts 加载 JSON 接口就能实现。接口可以复用现有的DashboardController,返回的数据结构已经按天聚合好了,再加一个实时在线数的查询即可。
第四个方向是软删除优化。目前的删除操作是物理删除,手牌表和订单表的关联数据一旦删除就找不回来了。我给所有核心表都加了deleted字段,配合 MyBatis-Plus 的@TableLogic实现逻辑删除,改造成本极低。这属于工程习惯的改良,虽然不是业务刚需,但对后续数据维护帮助很大。
整个项目最让我吃惊的反而是它的业务建模能力,而不是技术栈本身。计费引擎、手牌状态机、会员卡双轨逻辑,这三块是游泳馆系统真正的门槛,也是这个项目最值得反复阅读的地方。如果你拿到源码后能把这部分逻辑讲给别人听,说明你已经把它吃透了。从那以后,我每接手一个新系统,都强制自己先画一遍核心业务的状态流转图再碰代码,这个过程能避开太多返工。希望这个项目也能成为你理解 Java Web 业务开发的跳板,祝你好运。
本文还有配套的精品资源,点击获取