Spring Boot游泳馆管理系统:手牌计费与会员订单业务建模实战

发布时间:2026/10/17 2:56:36
Spring Boot游泳馆管理系统:手牌计费与会员订单业务建模实战
简介这套游泳馆管理系统是一份基于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 里这个项目用了一个ResultT泛型类统一包装前端拿到的永远是{ 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 接口里只需要一个返回ListDailyStatVO的方法。这个查询的巧妙之处在于用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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 rootlocalhost这是账号密码不对检查application.yml里的配置即可。第二类是Table swim_pool.xxx doesnt exist说明schema.sql没有执行成功重新执行一次注意先执行DROP TABLE再CREATE TABLE。第三类是端口冲突日志里会显示Port 8088 was already in use把端口改成 8090 或者杀掉占用进程都能解决。5.3 避坑记录我在跑这个项目时踩到的四个坑第一个坑是数据库时区问题导致的日期偏移。系统里所有时间字段用LocalDateTime存储如果 JDBC 连接串没有设置serverTimezoneAsia/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 业务开发的跳板祝你好运。本文还有配套的精品资源点击获取