2026/10/20 3:48:51

基于Spring Boot的游泳馆管理系统:扫码入场与会员核销全链路实现

基于Spring Boot的游泳馆管理系统:扫码入场与会员核销全链路实现 简介这是一份面向高校计算机专业学生与Java初学者的数据库课程设计完整源码以游泳馆日常运营为业务背景帮助读者理解如何将数据库原理与Java编程落地为可运行的管理系统。项目围绕会员管理、预约管理、场地管理与收费管理四大模块展开涉及会员等级设置、预约冲突处理、场地状态调度、多方式支付记账及财务报表统计等典型业务场景并遵循ER模型完成实体与关系设计是练习需求分析、系统设计与编码测试全流程的合适素材。资源包共1286个文件以1132个htm页面、63个gif素材、28个java源码、29个class文件及js、css、sql、mdf等为主另含少量jar与dll依赖压缩包约3.81MB目录结构便于按模块查阅。目前已有1535人学习下载适合需要参考完整课设方案、梳理数据库设计思路与Java界面实现的学习者。1. 游泳馆管理系统从手写台账到扫码入场一套 Java 项目把日常运营串起来了夏天一到前台排队的场景我见过太多次家长带着孩子挤在窗口工作人员翻着纸质登记本找会员卡号手边还压着一沓现金和零钱。这套游泳馆管理系统要解决的就是这种“人一多就乱”的日常。它用 Java 写成把会员办卡、充值扣次、入场核销、泳池区域管理和营业统计收进一个后台前台扫码或输手机号就能完成一次入场。适合谁一是中小型游泳馆、健身中心里负责信息化改造的开发二是拿它当 Java Web 课程设计或毕业设计的同学。项目结构不复杂但业务闭环完整跑起来就能看到一条从办卡到入场的真实链路。2. 技术选型与目录结构为什么用 Spring Boot 而不是裸 Servlet2.1 技术栈拆解一套典型的 Java Web 组合这个项目没有走微服务路线选的是单体分层架构对游泳馆这种并发量不大、业务相对固定的场景来说单体反而更好维护。核心依赖如下层次技术作用控制层Spring MVC接收前端请求参数校验业务层Spring Service会员、入场、计费逻辑持久层MyBatis-Plus单表 CRUD 与条件查询数据库MySQL 8会员、订单、入场记录存储前端Thymeleaf Bootstrap后台管理页面渲染构建Maven依赖管理与打包为什么用 MyBatis-Plus 而不是 JPA我一般会看项目里“条件查询”多不多。游泳馆系统里查会员要按姓名、手机号、卡状态、到期时间组合筛选MyBatis-Plus 的LambdaQueryWrapper写起来比 JPA 的 Specification 直观改起来也快。Spring Boot 版本选 2.7.x 是常见做法JDK 用 8 或 11 都能跑新手不用折腾新版本兼容问题。2.2 目录结构包名就是业务地图项目根目录下是标准的 Maven 结构src/main/java里按功能分包com.swim ├── config # 拦截器、跨域、MyBatis 配置 ├── controller # MemberController、EntryController、OrderController ├── service │ ├── impl # 业务实现 ├── mapper # 继承 BaseMapper 的接口 ├── entity # Member、EntryRecord、RechargeOrder ├── dto # 前端传参与返回封装 ├── common # Result、异常处理、常量 └── SwimApplication.javaresources下放application.yml、mapper/*.xml复杂 SQL 才用 XML和templates页面。这种分包方式的好处是拿到项目先看controller有哪些类基本就知道系统能干什么。entity里的字段和数据库表一一对应改字段时两边一起改不容易漏。2.3 数据库表设计三张核心表撑起业务建表 SQL 在resources/sql/init.sql里核心三张表-- 会员表一张卡对应一条记录 CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(32) UNIQUE NOT NULL, -- 卡号扫码用 name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, balance DECIMAL(10,2) DEFAULT 0, -- 余额按次扣费时用 remain_times INT DEFAULT 0, -- 剩余次数 expire_date DATE, -- 到期日 status TINYINT DEFAULT 1 -- 1正常 0冻结 ); -- 入场记录每次核销写一条 CREATE TABLE entry_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, entry_time DATETIME DEFAULT CURRENT_TIMESTAMP, channel VARCHAR(20), -- 前台/闸机 cost_type TINYINT -- 1扣次 2扣费 ); -- 充值订单记录每一笔钱 CREATE TABLE recharge_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, pay_type TINYINT, -- 1现金 2扫码 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );card_no加唯一索引是为了扫码入场时能直接命中不用全表扫。entry_record不存冗余的会员姓名查的时候 join 一下就行避免会员改名后历史记录对不上。这是我在实际项目里踩过坑之后养成的习惯流水表只存 ID 和当时的状态展示层再去关联。3. 核心模块拆解一次入场核销的完整链路3.1 入场核销接口从扫码到扣次这是整个系统最核心的一段代码前台扫码或输入卡号后调用PostMapping(/entry/checkin) public ResultEntryVO checkin(RequestBody Valid EntryDTO dto) { // 1. 按卡号查会员查不到直接返回 Member member memberService.getByCardNo(dto.getCardNo()); if (member null) { return Result.fail(卡号不存在); } // 2. 状态校验冻结卡、过期卡不允许入场 if (member.getStatus() 0) { return Result.fail(该卡已冻结请联系前台); } if (member.getExpireDate().before(new Date())) { return Result.fail(会员卡已过期); } // 3. 扣次或扣费二选一 if (member.getRemainTimes() 0) { member.setRemainTimes(member.getRemainTimes() - 1); memberService.updateById(member); entryService.saveRecord(member.getId(), 1); } else if (member.getBalance().compareTo(BigDecimal.valueOf(30)) 0) { member.setBalance(member.getBalance().subtract(BigDecimal.valueOf(30))); memberService.updateById(member); entryService.saveRecord(member.getId(), 2); } else { return Result.fail(次数不足且余额不够请先充值); } return Result.ok(new EntryVO(member.getName(), member.getRemainTimes())); }逻辑说明先查会员再做状态和有效期校验最后按“优先扣次、次选扣费”的顺序处理。EntryDTO里只有cardNo一个必填字段Valid保证空值不会进业务。扣费金额 30 元是写死的常量实际改造时应该抽到配置表或application.yml里不同泳池定价不一样。entryService.saveRecord里会写一条entry_recordchannel字段区分是前台操作还是闸机自动上报。参数说明cardNo是会员卡唯一编号扫码枪扫出来的就是它costType在记录表里区分扣次还是扣费方便月底统计“有多少人是刷卡次数、多少人是临时付费”。3.2 会员充值余额和次数的双轨处理充值逻辑比入场多一层要同时更新会员余额和写订单。常见做法是用Transactional包住Transactional(rollbackFor Exception.class) public void recharge(Long memberId, BigDecimal amount, Integer payType) { Member member memberMapper.selectById(memberId); if (member null) { throw new BizException(会员不存在); } // 更新余额 member.setBalance(member.getBalance().add(amount)); memberMapper.updateById(member); // 写充值订单 RechargeOrder order new RechargeOrder(); order.setMemberId(memberId); order.setAmount(amount); order.setPayType(payType); orderMapper.insert(order); }rollbackFor Exception.class是血泪经验默认只回滚运行时异常如果业务里抛了受检异常订单写了但余额没加账就对不上。加上这个参数任何异常都回滚。payType区分现金和扫码月底对账时按这个字段分组求和和收银台的实际收款核对。3.3 会员列表查询多条件组合筛选后台最常用的页面是会员列表支持按姓名、手机号、卡状态筛选public PageMemberVO pageQuery(MemberQuery query) { LambdaQueryWrapperMember wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), Member::getName, query.getName()) .like(StringUtils.hasText(query.getPhone()), Member::getPhone, query.getPhone()) .eq(query.getStatus() ! null, Member::getStatus, query.getStatus()) .orderByDesc(Member::getId); PageMember page memberMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); return page.convert(this::toVO); }like条件前加了StringUtils.hasText判断参数为空时不拼进 SQL避免like %%全表扫。orderByDesc按 ID 倒序新会员排前面前台找刚办卡的人方便。分页用 MyBatis-Plus 的Page前端传pageNum和pageSize就行。4. 本地跑起来从建库到访问后台的完整步骤4.1 环境准备与数据库初始化先确认本机有 JDK 8、Maven 3.6、MySQL 8。建库语句mysql -u root -p -e CREATE DATABASE swim_db DEFAULT CHARSET utf8mb4; mysql -u root -p swim_db src/main/resources/sql/init.sqlinit.sql里除了建表还会插入一条测试会员数据卡号1001剩余次数 10 次方便跑通流程。字符集用utf8mb4是为了存中文姓名和可能的 emoji 备注用utf8在某些客户端会乱码。4.2 修改配置并启动打开application.yml改数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/swim_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone必须配否则 MySQL 8 的时区问题会让entry_time差 8 小时。改完在项目根目录执行mvn clean package -DskipTests java -jar target/swim-0.0.1-SNAPSHOT.jar启动成功后访问http://localhost:8080/login默认账号admin密码123456在init.sql的sys_user表里。登录后左侧菜单能看到会员管理、入场核销、充值订单、营业统计四个模块。4.3 验证核心流程按这个顺序点一遍能跑通说明环境没问题先在“会员管理”里新增一个会员卡号填2001剩余次数填 5然后进“入场核销”输入2001点核销提示“核销成功剩余 4 次”再回会员列表看剩余次数是不是变成 4最后进“充值订单”给这个会员充 100 元看余额和订单记录是否都更新。这一套走完说明数据库连接、事务、前后端交互都是通的。提示如果核销时报“卡号不存在”先检查member表里有没有数据再看card_no字段是不是被空格污染了扫码枪有时会带换行符。5. 避坑与排查部署和二次开发时最容易翻车的几个点5.1 时区导致入场时间差 8 小时现象核销后entry_record.entry_time比实际时间早 8 小时。原因MySQL 8 默认时区是 UTCJDBC 连接没指定serverTimezone。解决在url后面加serverTimezoneAsia/Shanghai同时确认 MySQL 服务器时区SELECT global.time_zone;返回SYSTEM或08:00。5.2 事务不生效导致余额和订单不一致现象充值后余额加了但订单表没记录或者反过来。原因Transactional方法被同类内部调用Spring AOP 代理没拦住。解决把充值方法放到独立的 Service 里或者从 Controller 直接调 Service 的recharge方法不要在一个 Service 里自己调自己。5.3 扫码枪输入带换行符现象扫码核销提示卡号不存在但手动输入同样的卡号能成功。原因扫码枪默认在末尾追加回车换行cardNo变成2001\r\n。解决在EntryDTO的cardNo字段上加Pattern(regexp ^[0-9A-Za-z]$)校验或者在 Controller 里dto.setCardNo(dto.getCardNo().trim())先清洗。5.4 分页查询总数不对现象会员列表显示 10 条但分页控件显示总共 1 条。原因MyBatis-Plus 分页插件没配selectPage返回的total是 0 或当前页条数。解决在config包下加一个MybatisPlusConfig注册PaginationInnerInterceptorBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }5.5 前端页面 404 但接口能通现象访问http://localhost:8080/member/list返回 JSON 正常但浏览器打开页面 404。原因Thymeleaf 模板没放在src/main/resources/templates下或者 Controller 返回的是ResponseBody而不是视图名。解决检查Controller和RestController的区别页面跳转用Controller返回member/list字符串接口数据用RestController。6. 二次开发把硬编码改成配置把单机改成能对账这个项目跑通之后最值得动手改的地方是“定价”和“对账”。原代码里扣费金额 30 元写死在EntryController里不同泳池、不同时段价格不一样我一般会抽一张price_config表字段包括id、biz_type入场/储物柜/泳具租赁、price、effective_time然后在核销时按当前时间查生效价格。改完之后运营改价不用重启服务前台也不用记“今天多少钱”。第二个改造点是营业统计。原项目只有entry_record和recharge_order两张流水表月底对账要手动加。可以加一个定时任务每天凌晨把前一天的入场次数、充值金额、扣次扣费金额汇总到daily_report表字段包括report_date、entry_count、recharge_amount、consume_amount。用 Spring 的Scheduled(cron 0 0 1 * * ?)挂在 Service 上跑完写日志。这样老板要看“这个月每天多少人”直接查报表不用扫全量流水。第三个技巧是给核销接口加幂等。现在同一张卡连续点两次核销会扣两次次数。常见做法是前端传一个requestId可以用 UUID后端在 Redis 里setnx一个 5 分钟过期的 keykey 是entry:{cardNo}:{requestId}设置成功才继续扣次失败直接返回“请勿重复提交”。没有 Redis 的话用数据库唯一索引也行在entry_record上加(member_id, entry_time)唯一约束但粒度太粗还是 Redis 更灵活。我印象最深的一次翻车是给一个泳池做演示时扫码枪扫了卡但页面没反应查了半天发现是前端fetch请求没带Content-Type: application/json后端RequestBody解析失败直接 400。从那以后我每次接扫码枪或读卡器都强制先用curl打一遍接口确认参数格式和编码没问题再联调前端。希望这些经验能帮到你少走点弯路。本文还有配套的精品资源点击获取