资讯动态

Java游泳馆管理系统源码:会员卡、场次容量与闸机核销落地

发布时间:2026/10/29 11:20:58 来源:尧图企业网站定制
简介游泳馆管理系统是一套面向Java初学者与数据库课程设计学习者的项目源码围绕会员、预约、场地与收费四条业务线展开适合练习ER建模、关系表设计、图形界面开发以及Java业务逻辑实现。压缩包共1286个文件约3.81MB以htm页面、gif素材、class字节码和java源码为主辅以SQL建库脚本、数据库文件及少量jar、js与配置文件能还原较完整的课程设计工程结构。已有1536人学习下载具备一定参考热度。项目包含会员信息维护、消费记录查询、预约冲突处理、场地状态调度、收费记账与报表等模块代码中可见VIP管理、DAO数据访问、主界面加载和收费示例等实现线索便于读者对照理解分层组织、数据库连接方式与界面交互逻辑也能为同类课程设计提供功能拆解与排错参考读者可据此梳理实体关系、接口调用与界面事件响应。1. 游泳馆管理系统一套 Java 单馆后台能不能直接接你馆里的闸机中小型游泳馆的前台多数还在用纸质签到本加一张 Excel 记次卡会员报手机号后四位前台翻页划勾月底对账全靠人眼。储值卡刷了几次、月卡还剩几天、旺季哪几个场次爆满、哪个教练带的班退课率高全凭记忆和几张手写表。这套游泳馆管理系统就是冲着这个场景做的Java 单体后台把会员卡、场次排班、泳道容量、闸机核销、充值流水串成一条能查、能导出、能对账的线。它不是 SaaS也不绑定任何平台数据库和上传目录都在自己机器上适合想拿源码改一套自用系统的开发者、做场馆信息化的小团队以及拿 Java 课程设计或毕设当骨架的学生。下面按「拆结构 → 跑起来 → 调参数 → 躲坑 → 接设备」的顺序走一遍。2. 拆包先拆表六张核心表定死了这套系统的业务边界拿到一套不熟悉的 Java 后台源码我一般不从 Controller 开始读。Controller 是入口但业务规则藏在表和 Service 里表名和字段名是需求方一条条拍板过的比代码注释可信得多。先把建表脚本打开业务边界基本就出来了这套系统里一个会员可以有多张卡一张卡对应一种卡型卡型的计费规则决定核销时扣什么场次是时间维度的核心单位核销流水把卡和场次连起来。中间任何一环缺失账就对不平。2.1 六张表撑起一条完整业务线先把最关键的几张表列出来字段做了删减真实脚本里还有审计字段和软删除标记。表名职责关键约束member会员主档一个手机号对应一个人phone 唯一索引card_type卡型定义次卡 / 时长卡 / 储值卡决定扣减字段member_card会员持有的具体卡实例card_no 唯一绑 member_idswim_session场次含日期、起止时间、容量(session_date, start_time) 联合索引checkin_record核销流水一次入场一条idempotent_key 唯一recharge_record充值/退款流水用于对账与 member_card 关联下面这段是删减后的建表语句重点是几个约束和金额字段的类型这两处是最容易在二次开发时被改坏的地方。-- 会员主表手机号既是登录凭据也是查询凭据必须唯一 CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR(20) NOT NULL COMMENT 手机号唯一, name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0 COMMENT 0未知 1男 2女, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 卡型计费规则的唯一来源不要在 Java 里写 if (type 1) CREATE TABLE card_type ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 30次卡 / 季卡 / 储值卡, charge_mode TINYINT NOT NULL COMMENT 1按次 2按时长 3按金额, price DECIMAL(10,2) NOT NULL, validity_days INT NOT NULL COMMENT 有效天数0表示长期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 会员卡三选一扣减余额与次数互斥 CREATE TABLE member_card ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, card_type_id BIGINT NOT NULL, card_no VARCHAR(32) NOT NULL COMMENT 实体卡号或虚拟卡号, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 储值余额非储值卡恒为0, remain_times INT DEFAULT 0 COMMENT 次卡剩余次数, valid_from DATE NOT NULL, valid_to DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT 1有效 2冻结 3过期, UNIQUE KEY uk_card_no (card_no), KEY idx_member (member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 场次booked 是冗余字段用空间换并发后面 4.2 会讲 CREATE TABLE swim_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_date DATE NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, capacity INT NOT NULL COMMENT 该场最大人数, booked INT NOT NULL DEFAULT 0 COMMENT 已占用名额, lane_count INT NOT NULL DEFAULT 8 COMMENT 可用泳道数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可约 2已满 3停开, KEY idx_date_start (session_date, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 核销流水idempotent_key 是这套系统的后悔药 CREATE TABLE checkin_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_id BIGINT NOT NULL, session_id BIGINT NOT NULL, checkin_at DATETIME NOT NULL, operator_id BIGINT COMMENT 前台人员ID闸机自助为空, source TINYINT NOT NULL DEFAULT 1 COMMENT 1前台 2闸机 3手机端, idempotent_key VARCHAR(64) NOT NULL COMMENT 设备流水号或UUID, UNIQUE KEY uk_idem (idempotent_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数和约束上有三处值得单独说。金额一律DECIMAL(10,2)不用DOUBLE这是对账能不能对上的分水岭后面第 5 章会有一个真实翻车例子。charge_mode放在card_type里而不是写死在代码里是为了后面加「亲子卡」「年卡」时不用改核销逻辑只加一行数据。idempotent_key建了唯一索引这不是保险而是这套系统对接闸机的唯一正确姿势让数据库来兜底去重比在 Java 里查一遍再插一遍可靠得多。2.2 为什么是 Spring Boot 单体加 MyBatis而不是一上来就微服务单馆的日均客流通常在两三百人次节假日峰值也就五百上下换算成接口 QPS 峰值不到 20。这个量级上微服务是给自己找事多一套注册中心、多一次网络调用、多一份链路排查成本换来的横向扩容能力根本用不上。项目实际用的是 Spring Boot 打一个可执行 jar扔到馆里那台小主机或者一台云主机上配 MySQL 就能跑运维只需要会systemctl restart和看日志。持久层选 MyBatis 而不是 JPA理由很实在这套系统最重的查询是报表。日报、月卡到期提醒、场次上座率、教练课时统计全都是多表关联加动态条件SQL 手写出来清楚改一个WHERE就能调口径。JPA 在这种场景下要么写原生 SQL 绕回去要么被方法名拼接过早。MyBatis 的代价是 XML 多但对一个单人维护的项目来说改得动比写得爽更重要。方案适合场景在这个项目里的取舍Spring Boot 单体单馆、QPS 50、运维人力 0~1 人采用打包即部署微服务拆分连锁多馆、独立计费中心不采用成本高于收益MyBatis报表多、SQL 需频繁调口径采用XML 集中管理JPA / Hibernate领域模型稳定、CRUD 为主不采用复杂查询会绕2.3 目录结构先看哪几个类能省两天时间包结构是标准的 controller / service / mapper / entity 四层外加一个common放统一返回和异常处理。swim-pool/ ├── sql/ │ ├── schema.sql # 建表先跑这个 │ └── data.sql # 初始卡型、管理员、示例场次 ├── src/main/java/com/example/swim/ │ ├── controller/ # 只做参数校验和组装不写业务 │ ├── service/ │ │ ├── CardService.java # 办卡、续费、退卡 │ │ ├── CheckinService.java # 核销最该先读的类 │ │ └── SessionService.java # 排场次、容量控制 │ ├── mapper/ # 接口 resources/mapper 下的 XML │ ├── entity/ │ └── common/ # Result、GlobalExceptionHandler、AuthInterceptor ├── src/main/resources/ │ ├── application.yml │ └── mapper/*.xml └── pom.xml读的顺序建议是sql/schema.sql→CheckinService→CardService→SessionService。核销是整个系统里唯一同时碰卡、场次、流水三张表的地方把它读透其余模块基本是它的变体。common/GlobalExceptionHandler也值得扫一眼业务异常最后都收敛成什么返回码写前端联调时省事。3. 跑起来从建库到第一条核销流水四步验证很多源码包卡在第一步不是代码有问题而是环境版本和 SQL 脚本对不上。这一章给的是能照着敲的顺序每一步都有可验证的结果跑不通就停在当前步骤排查不要往下走。3.1 环境与版本对齐项目本身对版本不敏感但 JDK 8 和 JDK 17 在日期序列化和switch语法上有差别先用表里的组合跑通再考虑升级。组件建议版本说明JDK8 或 1117 也能跑需确认 Lombok 版本Maven3.6依赖里没有私服坐标公共仓库可拉全MySQL5.7 或 8.08.0 注意驱动类名与时区参数Redis可选只用在做登录态缓存不装则走内存Node16可选仅当前端分离版本需要后端不依赖MySQL 8.0 要额外注意连接串里的serverTimezone这是第 5 章第一个坑的源头。JDK 17 加 Lombok 老版本会在编译期报IllegalAccessError把 Lombok 升到 1.18.24 以上即可。3.2 建库与导入初始化数据脚本里没有CREATE DATABASE需要自己建字符集必须是utf8mb4否则会员姓名里的生僻字会截断。# 1) 建库字符集与排序规则显式指定别依赖默认值 mysql -uroot -p -e CREATE DATABASE swim_pool DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 2) 建表 mysql -uroot -p swim_pool sql/schema.sql # 3) 导入初始数据3 种卡型、1 个管理员、14 天示例场次 mysql -uroot -p swim_pool sql/data.sql # 4) 验证应该能看到 card_type 三行、swim_session 若干行 mysql -uroot -p swim_pool -e SELECT id,name,charge_mode FROM card_type; SELECT COUNT(*) FROM swim_session;data.sql里默认管理员的账号密码是明文的方便首次登录第一次进后台务必改掉或者直接改脚本里的密码哈希再导入。示例场次是按脚本执行当天往后推 14 天生成的如果你的机器时间和数据库时间不一致会出现「场次全是过去时间」的情况先对一下SELECT NOW();。3.3 配置文件里必须改的三处server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/swim_pool?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 hikari: maximum-pool-size: 10 # 单馆够用别调大 connection-timeout: 3000 swim: checkin: allow-early-minutes: 30 # 开场前多久允许核销 allow-late-minutes: 20 # 开场后多久仍可入场 offline-cache-size: 500 # 闸机断网本地缓存条数 booking: advance-days: 7 # 最多提前几天预约 cancel-before-hours: 2 # 开场前几小时可免费取消三处必改数据库密码、serverTimezone、swim.checkin下的时间窗口。前两个不改直接连不上或时间错乱第三个不改会出现「会员提前一小时到场刷不进去」的现场投诉。时间窗口这两个参数没有标准答案常规做法是按泳馆的实际开门流程定比如提前 30 分钟、迟到 20 分钟超过就转到前台人工处理。3.4 跑通第一条业务线登录 → 办卡 → 核销启动命令就是标准的mvn spring-boot:run或者java -jar target/*.jar。起来之后不要急着点前端先用 curl 把后端链路走通这样出问题能立刻分清是前端还是后端。# 0) 启动 mvn clean package -DskipTests java -jar target/swim-pool-1.0.0.jar # 1) 登录拿 token默认账号见 data.sql TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} | grep -o token:[^]* | cut -d -f4) # 2) 给 1 号会员办一张卡cardTypeId2 为 30 次卡 curl -s -X POST http://localhost:8080/api/card/open \ -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {memberId:1,cardTypeId:2,validFrom:2024-06-01,validTo:2025-05-31} # 3) 核销入场idempotentKey 由调用方生成重放同一个 key 不会重复扣次 curl -s -X POST http://localhost:8080/api/checkin \ -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {cardNo:C20240601001,sessionId:12,idempotentKey:DEV-20240601-0001,source:2}第 2 步返回的cardNo要记下来第 3 步用它。第 3 步把同一个请求体原样再发一次如果返回的是「已核销」而不是「次数不足」说明幂等生效了这是交付前必做的一次验证比看代码可靠。source字段区分前台、闸机、手机端后面出报表按渠道统计人次就靠它别图省事全填 1。4. 参数怎么设卡型、场次容量与核销原子性的三组数字代码能跑起来只是及格线真正决定这套系统能不能在馆里活下来的是几个参数卡型怎么扣、容量怎么控、核销怎么保证不重不漏。这三件事都有明确的实现方式不需要靠感觉。4.1 三种卡型的扣减映射charge_mode决定核销时动哪个字段逻辑集中在CardService.deduct()一个方法里别分散到各个 Controller。卡型charge_mode核销时扣什么有效期判断退卡处理次卡30 次1remain_times - 1valid_to 当日 23:59 前按剩余次数折算时长卡季卡2不扣字段只记流水valid_from ~ valid_to按剩余天数折算储值卡3balance - 单次单价长期或按配置余额原路退时长卡不扣字段这点常被误解成「漏写了」。它的可用性完全由valid_from / valid_to与当前时间比较决定核销只写流水。副作用是同一张时长卡一天可以进多次如果业务上要求一天一次需要在核销前按card_id DATE(checkin_at)做一次存在性判断这个开关建议做成配置项不同馆的要求差别很大。4.2 场次容量不要用 count(*) 现算早期版本是核销时SELECT COUNT(*) FROM checkin_record WHERE session_id ?然后和capacity比。数据量一上来一个旺季几万条流水这个查询会开始拖慢核销而且并发下两个人同时查到 49 50都能进超卖就是这么来的。改成冗余booked字段用一条带条件的 UPDATE 同时完成判断和更新。-- 占位影响行数为 1 才算占位成功为 0 直接抛「场次已满」 UPDATE swim_session SET booked booked 1 WHERE id #{sessionId} AND status 1 AND booked capacity;这条语句依赖 InnoDB 的行锁WHERE里的booked capacity是条件更新的关键不能先查后改。返回 0 时不一定是满了也可能是status被改成停开异常信息里要把这两种情况分开提示不然前台会一直以为是满场。取消预约时的booked - 1要加AND booked 0防止异常数据把计数打成负数。4.3 核销的原子性一张卡不能被两个人同时扣核销要同时完成三件事扣卡、占场次名额、写流水。三件事必须在同一个事务里且扣卡这一步要防并发。Transactional(rollbackFor Exception.class) public CheckinResult checkin(CheckinCmd cmd) { // 1) 幂等前置检查唯一索引兜底这里只是快速失败减少无效事务 if (checkinMapper.existsByIdemKey(cmd.getIdempotentKey())) { return CheckinResult.duplicated(); } // 2) 条件扣减扣不动就是次数/余额不足直接返回 int affected cardMapper.deduct(cmd.getCardId(), cmd.getChargeMode()); if (affected 0) { throw new BizException(卡无效、已过期或余额不足); } // 3) 占场次名额失败则整个事务回滚卡上的扣减自动撤销 int seated sessionMapper.occupy(cmd.getSessionId()); if (seated 0) { throw new BizException(该场次已满或已停开); } // 4) 写流水唯一索引冲突会抛 DuplicateKeyException由全局异常处理转成「重复核销」 checkinMapper.insert(buildRecord(cmd)); return CheckinResult.ok(); }对应的 Mapper SQL 长这样remain_times 0和balance price是两条不同的扣减路径按chargeMode分支!-- 次卡剩余次数大于 0 才扣影响行数 0 表示扣不动 -- update iddeduct choose when testchargeMode 1 UPDATE member_card SET remain_times remain_times - 1 WHERE id #{cardId} AND status 1 AND remain_times gt; 0 /when when testchargeMode 3 UPDATE member_card SET balance balance - #{price} WHERE id #{cardId} AND status 1 AND balance gt; #{price} /when otherwise UPDATE member_card SET status status WHERE id #{cardId} AND status 1 AND CURDATE() BETWEEN valid_from AND valid_to /otherwise /choose /updateotherwise分支看着奇怪但它是时长卡的正确写法只做有效性校验不改数据用影响行数来表达「这张卡当前可用」。这种「用 UPDATE 的返回值当布尔判断」的写法在 MyBatis 里很好用代价是必须开事务否则校验和写流水之间会有空窗。4.4 把经验参数固化成配置参数建议起点调整依据advance-days7暑期旺季可放宽到 14cancel-before-hours2低于 1 会导致爽约率上升allow-early-minutes30跟开门时间对齐allow-late-minutes20超时转人工别硬拦hikari.maximum-pool-size10单馆够用调大反而争抢offline-cache-size500按闸机本地存储上限的 60% 设这些值统一放application.yml的swim前缀下别散落在代码里。改配置重启就能生效比重新打包发版省事得多。5. 避坑与排查五个把账做平的血泪记录下面五条都是我在部署和联调时真实撞过的按「现象 → 原因 → 解决」写照着排查能省不少时间。5.1 所有场次时间整体差 8 小时现象数据库里存的是2024-06-01 10:00:00页面上显示成2024-06-01 18:00:00核销时提示「不在可入场时间窗口内」。原因JDBC 连接串没指定serverTimezone驱动按 UTC 解析DATETIME而业务代码里用的是东八区的LocalDateTime两边差 8 小时。MySQL 8.0 的驱动对这点比 5.7 敏感。解决连接串加serverTimezoneAsia/Shanghai同时确认 MySQL 服务端time_zone与system_time_zone一致。改完后历史数据不会自动修正需要用一条 UPDATE 批量平移或者清库重导。5.2 同一张次卡被扣成 -1现象对账时发现某张 30 次卡的remain_times是 -1流水却只有 29 条。原因核销逻辑写成了「先 SELECT 查剩余次数再 if 判断再 UPDATE 减一」。两个前台同时操作同一张卡都查到了 1都判断通过都执行了减一。解决改成 4.3 里的条件更新WHERE remain_times 0交给数据库保证原子性影响行数为 0 就抛异常。已经出现的负值用一条 UPDATE 归零并补一条人工调整流水别直接改数不留痕。5.3 夜场跨天日报少统计一半现象22:00 到次日 01:00 的夜场核销流水记在第二天但场次的session_date是第一天导致日报按session_date统计时人次对不上。原因统计 SQL 用了DATE(checkin_at)分组而业务口径应该按场次归属日分组。这两个口径在跨天场次上必然冲突。解决报表统一按swim_session.session_date关联统计不用流水的日期字段。同时在场次创建时加校验end_time跨天必须显式确认避免运营批量生成时把 23:00-23:59 的场次错配到第二天。5.4 月底对账差几分钱现象储值卡充值 500 元消费若干次后余额显示 12.34手工按单价乘次数算出来是 12.33。原因早期版本balance字段用的DOUBLE浮点累加误差在小数位上滚雪球。解决全表金额字段改DECIMAL(10,2)Java 侧统一用BigDecimal除法必须指定setScale(2, RoundingMode.HALF_UP)。已经产生误差的历史数据做一次全量重算并生成调整流水不要手工改。5.5 闸机断网重连后重复核销现象馆里网络抖动闸机本地缓存了 3 条核销请求恢复后补传其中 2 条被重复扣次。原因补传时设备重新生成了流水号idempotent_key每次都不一样唯一索引形同虚设。解决约定idempotent_key由设备在「产生核销动作的那一刻」生成并持久化补传时复用原值不能重新生成。服务端收到后先查唯一索引冲突就返回成功而不是报错让设备清掉本地缓存。这条约定必须写进对接文档光靠口头说一定会出问题。6. 二次开发把核销接口接到自有闸机上的三个约定源码里的闸机对接留了一个DeviceAdapter接口默认实现是 HTTP 回调实际落地时硬件厂商给的多半是串口协议或者一个私有 TCP 报文中间需要一层网关做协议转换。我的做法是网关只负责收报文、转 JSON、转发到/api/checkin业务判断全部留在 Java 侧这样换硬件不用动业务代码。接口定义大概是这样public interface DeviceAdapter { /** 设备上报核销返回结果会被原样回给设备 */ CheckinResult onCheckin(DeviceCheckinMsg msg); /** 设备心跳用于在线状态展示5 秒一次即可 */ void onHeartbeat(String deviceSn); } // 实现类里只做三件事验签、转换、调 CheckinService Component public class HttpDeviceAdapter implements DeviceAdapter { Override public CheckinResult onCheckin(DeviceCheckinMsg msg) { // 1) 验签deviceSn timestamp nonce 用密钥做一次 HMAC防伪造 signVerifier.verify(msg); // 2) 转换后复用前台同一套核销逻辑绝不另写一份 return checkinService.checkin(msg.toCmd()); } }三个约定必须在联调前写进文档不然一定会在现场返工。第一幂等键由设备生成并持久化断网补传必须复用服务端遇到重复直接返回成功。第二时间戳允许的偏差不超过 5 分钟超了直接拒绝避免重放。第三设备端必须能缓存至少 500 条待传记录这个数按旺季单闸机一小时的最大通行量估算再留一倍余量。联调做完之后我会跑一个「断网 30 分钟 补传 200 条」的压测检查三件事流水条数是否等于实际通行人次、卡上扣减是否一致、场次booked是否与流水数吻合。这三条对上了才敢让馆里把纸质签到本撤掉。从那以后我每次接设备都先把幂等键和离线补传这两件事写进接口文档再去碰硬件。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑