
简介这是一套面向计算机相关专业学生与Java学习者的健身房管理系统毕业设计项目采用SpringBoot与Vue前后端分离架构后端整合MyBatis前端基于Vue.js构建可直接用于毕设、课程设计或期末大作业。项目已通过导师指导并高分通过配套源码、数据库脚本、开发说明文档、部署视频与代码讲解视频环境基于JDK1.8、MySQL5.7、Maven3.3兼容Eclipse与IDEA。压缩包共767个文件约22.29MB以197个Java后端源码、139个Vue前端组件、51个JavaScript脚本、25个XML配置及SQL脚本、说明文档与运行批处理为主另含少量图片与字体资源目录结构清晰便于按模块阅读与二次开发。目前已有58人学习下载。读者可据此掌握前后端分离项目的完整实现流程理解会员、课程、器材等业务模块的接口设计与页面交互并借助部署与讲解视频快速跑通项目、排查环境问题为答辩与实战练习提供完整参考。1. 健身房管理系统到底在管什么从一张会员卡说起前台小姐姐刷一下会员卡系统要同时完成四件事查这个人是不是有效会员、扣一次入场次数、记录进场时间、如果卡快到期就弹窗提醒续费。这四件事背后牵扯到会员、卡、订单、员工、课程五张表任何一环掉链子前台就得拿纸质本子手动记。这就是「Java 基于 SpringBoot Vue 的健身房管理系统」要解决的真实场景也是计算机毕业设计里被选得最多、但翻车率同样最高的题目之一。它适合两类人一类是正在做计算机毕业设计、需要一套能跑通、能答辩、能讲清楚技术选型的同学另一类是想用 SpringBoot Vue 这套组合拳练手全栈开发的新人。整套系统的核心不是界面多花哨而是会员状态流转、订单金额计算、课程名额扣减这三条业务线能不能扛住并发和脏数据。下面我按「先立住选型、再动手复现、最后讲坑」的顺序把一套能直接照着做的方案拆开讲。2. 技术选型与数据库设计为什么是 SpringBoot Vue 而不是别的2.1 后端选 SpringBoot 的三个现实理由毕业设计的时间窗口通常只有两三个月后端框架的选型标准就一条能让你把精力花在业务上而不是花在配置上。SpringBoot 的自动装配把数据源、事务管理器、Web 容器这些基础设施都预置好了一个application.yml加几个 starter 依赖就能跑起来。相比之下传统 SSM 要手写一堆 XML光是applicationContext.xml和spring-mvc.xml的路径配置就能耗掉两天。第二个理由是生态。健身房管理系统绕不开权限控制、文件上传会员头像、教练证书、定时任务会员到期提醒这些在 SpringBoot 里都有成熟方案Spring Security 或 Sa-Token 做鉴权MinIO 或本地存储做文件Scheduled做定时。第三个理由是答辩友好面试八股文里 SpringBoot 的自动装配原理、starter 机制、循环依赖处理是高频题你做完这个项目顺带就把这些知识点吃透了。版本选择上有个血泪经验别追最新版。SpringBoot 3.x 要求 JDK 17 起步而很多学校的实验环境还停在 JDK 8。我一般建议用 SpringBoot 2.7.x 配 JDK 8 或 11这个组合稳定、资料多、出问题能搜到答案。如果你学校明确要求 JDK 17那就上 3.x但要注意javax.*全部换成了jakarta.*老教程里的代码直接抄会编译不过。2.2 前端选 Vue 而不是 React 或 Thymeleaf前端这块Thymeleaf 服务端渲染虽然简单但做健身房管理系统的会员列表、课程排期这种交互密集的页面会很痛苦每次筛选都要刷新整页。Vue 的响应式数据绑定和组件化刚好适配这类后台管理场景而且 Element Plus 这套 UI 库提供了现成的表格、表单、弹窗、日期选择器会员管理页面的增删改查基本是拼组件。Vue 2 和 Vue 3 怎么选如果你的项目周期紧、不想折腾Vue 2 Element UI 是最稳的组合网上教程铺天盖地。如果想在答辩时体现技术前瞻性Vue 3 Element Plus Vite 是更好的选择组合式 API 写起来逻辑更清晰而且 Vite 的冷启动速度比 Webpack 快一个量级。注意 Vue 3 的setup语法和 Vue 2 的optionsAPI 差异较大别混着抄代码。前后端分离后跨域是第一个要解决的问题。开发阶段在 Vue 的vite.config.js里配代理生产环境用 Nginx 反向代理这两种方式下面都会给具体配置。2.3 数据库表设计五张核心表与字段说明健身房管理系统的表不少但真正撑起业务的就五张。下面这张表是我反复调整后的字段设计直接可以用表名关键字段说明member会员id, name, phone, card_id, balance, expire_date, statusstatus: 0 正常 1 冻结 2 过期card会员卡id, card_type, price, duration_days, descriptioncard_type: 月卡/季卡/年卡/次卡order订单id, member_id, card_id, amount, pay_type, create_timepay_type: 1 现金 2 微信 3 支付宝course课程id, name, coach_id, start_time, capacity, bookedbooked 为已预约人数booking预约id, member_id, course_id, status, create_timestatus: 0 已预约 1 已取消 2 已签到建表时有两个容易忽略的点。第一member表的phone字段要加唯一索引否则同一个手机号能注册出多个会员前台查的时候直接懵。第二course表的booked字段和booking表的记录数必须保持一致这是后面讲并发时要用乐观锁处理的核心。CREATE TABLE member ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 会员姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, card_id BIGINT DEFAULT NULL COMMENT 当前会员卡ID, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 账户余额, expire_date DATE DEFAULT NULL COMMENT 到期日期, status TINYINT DEFAULT 0 COMMENT 0正常 1冻结 2过期, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表;这段建表语句里DECIMAL(10,2)用于金额字段别用FLOAT或DOUBLE浮点数算钱会出现0.1 0.2 0.30000000000000004这种玄学问题。utf8mb4字符集是为了支持 emoji会员昵称里带表情符号的情况比你想象的多。3. 后端接口开发从会员注册到课程预约的完整链路3.1 项目骨架搭建与核心依赖配置先用 Spring Initializr 或 IDE 自带的脚手架建项目勾选 Spring Web、MyBatis-Plus、MySQL Driver、Lombok 四个依赖。MyBatis-Plus 比原生 MyBatis 省事单表增删改查不用写 XML复杂查询再手写 SQL。pom.xml里关键依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesMyBatis-Plus 的版本要和 SpringBoot 版本匹配2.7.x 的 SpringBoot 配 3.5.x 的 MyBatis-Plus 没问题。如果你用的是 SpringBoot 3.xMyBatis-Plus 要升到 3.5.3 以上才支持jakarta命名空间。Lombok 的Data注解能省掉 getter/setter但注意在 IDEA 里要装 Lombok 插件并开启注解处理器否则编译报「找不到符号」。application.yml的配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/gym?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezoneAsia/Shanghai必须加否则插入的时间会差 8 小时会员到期日期算错一天就是事故。map-underscore-to-camel-case让数据库的expire_date自动映射到 Java 的expireDate省掉手动配resultMap。3.2 会员注册与会员卡绑定的接口实现会员注册不是简单插一条记录要同时处理三件事校验手机号是否已注册、根据卡类型计算到期日期、生成订单记录。这三步必须在一个事务里任何一步失败都要回滚。Service public class MemberServiceImpl implements MemberService { Autowired private MemberMapper memberMapper; Autowired private CardMapper cardMapper; Autowired private OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) public void register(MemberRegisterDTO dto) { // 1. 校验手机号唯一 Long count memberMapper.selectCount( new LambdaQueryWrapperMember().eq(Member::getPhone, dto.getPhone())); if (count 0) { throw new BusinessException(该手机号已注册); } // 2. 查询卡类型计算到期日期 Card card cardMapper.selectById(dto.getCardId()); if (card null) { throw new BusinessException(会员卡类型不存在); } LocalDate expireDate LocalDate.now().plusDays(card.getDurationDays()); // 3. 插入会员 Member member new Member(); member.setName(dto.getName()); member.setPhone(dto.getPhone()); member.setCardId(card.getId()); member.setExpireDate(expireDate); member.setStatus(0); memberMapper.insert(member); // 4. 生成订单 Order order new Order(); order.setMemberId(member.getId()); order.setCardId(card.getId()); order.setAmount(card.getPrice()); order.setPayType(dto.getPayType()); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); } }Transactional(rollbackFor Exception.class)里的rollbackFor必须显式写Exception.class因为 Spring 默认只对RuntimeException回滚如果抛的是受检异常事务不会回滚会员插进去了但订单没生成数据就脏了。这是答辩时老师最爱问的点之一。LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器用方法引用Member::getPhone代替字符串字段名重构时改字段名不会漏改 SQL。3.3 课程预约的并发扣减乐观锁与库存防超卖课程预约是整套系统里唯一有并发风险的地方。假设一节瑜伽课容量 20 人同一时刻有 30 个人点预约如果不做控制booked字段会被覆盖成 20 但实际插入了 30 条预约记录超卖就发生了。常见做法有两种悲观锁SELECT ... FOR UPDATE和乐观锁版本号或 CAS。健身房场景并发量不大乐观锁足够且不会阻塞读请求。MyBatis-Plus 内置了乐观锁插件配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }然后在Course实体的booked字段上加Version注解Version private Integer booked;预约接口的逻辑Override Transactional(rollbackFor Exception.class) public void bookCourse(Long memberId, Long courseId) { Course course courseMapper.selectById(courseId); if (course.getBooked() course.getCapacity()) { throw new BusinessException(课程名额已满); } // 更新已预约人数Version 会自动带上版本条件 course.setBooked(course.getBooked() 1); int rows courseMapper.updateById(course); if (rows 0) { throw new BusinessException(预约冲突请重试); } Booking booking new Booking(); booking.setMemberId(memberId); booking.setCourseId(courseId); booking.setStatus(0); booking.setCreateTime(LocalDateTime.now()); bookingMapper.insert(booking); }updateById返回的影响行数为 0 时说明版本号已被其他线程改过当前更新失败直接抛异常让用户重试。这里有个细节Version字段的值在每次更新后会自动加 1所以course.setBooked(course.getBooked() 1)之后不需要手动改版本号。注意乐观锁只保证更新不冲突不保证「先查后改」这个间隙的数据一致性。如果并发极高还是要在数据库层加唯一索引兜底比如booking表加(member_id, course_id)唯一索引防止同一个人重复预约同一节课。4. 前端页面与前后端联调Vue 页面怎么接上 SpringBoot 接口4.1 Vue 项目初始化与 Axios 封装用 Vite 创建 Vue 3 项目npm create vitelatest gym-frontend -- --template vue cd gym-frontend npm install npm install axios element-plus vue-router piniaAxios 的封装是前后端联调的关键统一处理请求头、响应拦截、错误提示// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带上 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理业务错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default requestbaseURL: /api配合 Vite 的代理配置开发时把/api开头的请求转发到localhost:8080// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })changeOrigin: true是为了让后端收到的请求头里的 Host 是localhost:8080而不是localhost:5173有些后端框架会校验这个。rewrite把/api前缀去掉这样后端接口不用每个都加/api前缀。4.2 会员管理页面的增删改查实现会员管理页面是后台系统的典型 CRUD用 Element Plus 的el-table和el-dialog组合template div classmember-page el-input v-modelquery.phone placeholder按手机号搜索 stylewidth: 200px / el-button typeprimary clickloadMembers查询/el-button el-button typesuccess clickopenDialog新增会员/el-button el-table :datamemberList border stylemargin-top: 16px el-table-column propname label姓名 / el-table-column propphone label手机号 / el-table-column propexpireDate label到期日期 / el-table-column label状态 template #default{ row } el-tag :typerow.status 0 ? success : danger {{ row.status 0 ? 正常 : 过期 }} /el-tag /template /el-table-column el-table-column label操作 template #default{ row } el-button sizesmall clickeditMember(row)编辑/el-button el-button sizesmall typedanger clickdeleteMember(row.id)删除/el-button /template /el-table-column /el-table /div /template script setup import { ref, onMounted } from vue import request from /utils/request import { ElMessage, ElMessageBox } from element-plus const memberList ref([]) const query ref({ phone: }) const loadMembers async () { const data await request.get(/member/list, { params: query.value }) memberList.value data } const deleteMember async (id) { await ElMessageBox.confirm(确认删除该会员, 提示, { type: warning }) await request.delete(/member/${id}) ElMessage.success(删除成功) loadMembers() } onMounted(loadMembers) /scriptscript setup是 Vue 3 的组合式 API 写法ref定义响应式数据onMounted在组件挂载时加载数据。ElMessageBox.confirm返回一个 Promise用户点取消会 reject所以要用await配合 try-catch 或者直接让 reject 冒泡这里没写 catch实际项目里要处理。4.3 登录鉴权与路由守卫后台系统必须登录才能访问用 Vue Router 的全局前置守卫拦截// src/router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/Login.vue) }, { path: /member, component: () import(/views/Member.vue), meta: { requiresAuth: true } }, { path: /course, component: () import(/views/Course.vue), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } }) export default router后端登录接口用 JWT 生成 token前端存localStorage每次请求通过 Axios 拦截器带上。JWT 的过期时间设 2 小时比较合适太长有安全风险太短用户频繁掉线。如果要做「记住我」可以加 refresh token 机制但毕业设计里通常不需要这么复杂。5. 避坑与排查那些让我熬夜到三点的真实问题5.1 跨域配置了但还是报 CORS 错误现象前端请求后端接口浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy但后端明明加了CrossOrigin注解。原因跨域是浏览器行为CrossOrigin加在 Controller 上只对单个接口生效如果加了全局拦截器或者 Spring Security预检请求OPTIONS会被拦截器先拦下来根本没走到CrossOrigin。解决配一个全局 CORS 配置类并且确保拦截器放行 OPTIONS 请求Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns(*)而不是allowedOrigins(*)因为后者在allowCredentials(true)时会报错。如果用了 Spring Security还要在 Security 配置里.cors()开启。5.2 时间字段差 8 小时现象前端传2024-01-01 10:00:00数据库存进去变成2024-01-01 02:00:00。原因JDBC 连接串没指定时区MySQL 驱动默认用 UTC 解析时间。解决连接串加serverTimezoneAsia/Shanghai同时检查实体类的时间字段类型。用LocalDateTime比Date更省心Date在不同时区下toString结果不一样。如果已经用了Date在application.yml里加spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT85.3 MyBatis-Plus 更新字段为 null 时不生效现象想把会员的card_id置空调用updateById后数据库里还是旧值。原因MyBatis-Plus 默认的FieldStrategy是NOT_NULLnull 字段不参与更新。解决在字段上加TableField(updateStrategy FieldStrategy.IGNORED)或者全局配置mybatis-plus: global-config: db-config: update-strategy: ignored全局配置要谨慎改成ignored后所有 null 字段都会更新可能误伤。我一般只在需要的字段上加注解。5.4 前端打包后刷新页面 404现象npm run build后部署到 Nginx首页能打开点进/member再刷新就 404。原因Vue Router 的 history 模式需要服务端把所有路径都指向index.htmlNginx 默认按文件路径找找不到就 404。解决Nginx 配置加try_fileslocation / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }如果不想改 Nginx把 Vue Router 改成 hash 模式createWebHashHistoryURL 会带#但刷新不会 404。毕业设计里两种都行看部署环境。5.5 课程预约后已预约人数没变现象用户预约成功booking表插入了记录但course表的booked字段没加 1。原因乐观锁更新失败但异常被吞了或者Version注解没生效插件没注册。解决先确认MybatisPlusConfig里注册了OptimisticLockerInnerInterceptor再检查Course实体的booked字段有没有Version。如果都配了还不行打开 MyBatis-Plus 的 SQL 日志看 update 语句有没有带WHERE booked ?条件。没有的话就是插件没生效。6. 让系统在答辩时加分三个进阶技巧与验证方法第一个技巧是给会员到期做定时提醒。用Scheduled每天凌晨扫一遍member表把 7 天内到期的会员查出来往message表插一条通知记录。这个功能代码量不大但答辩时能体现你考虑了业务的完整闭环。Component public class ExpireRemindTask { Autowired private MemberMapper memberMapper; Scheduled(cron 0 0 1 * * ?) public void remind() { LocalDate deadline LocalDate.now().plusDays(7); ListMember list memberMapper.selectList( new LambdaQueryWrapperMember() .le(Member::getExpireDate, deadline) .eq(Member::getStatus, 0)); // 批量插入提醒记录略 System.out.println(即将到期会员数 list.size()); } }cron 0 0 1 * * ?表示每天凌晨 1 点执行。注意Scheduled默认单线程如果任务多要配线程池否则任务会排队。验证方法很简单把 cron 改成每分钟执行一次看控制台输出。第二个技巧是用 Postman 或 Apifox 做接口回归测试。把会员注册、课程预约、订单查询这几个核心接口存成集合每次改完代码跑一遍比手动点页面快得多。特别是乐观锁这种并发逻辑用 Postman 的 Runner 并发跑 50 次预约请求看booked最终值是不是等于capacity能直接验证防超卖有没有生效。第三个技巧是答辩演示时准备两条数据线一条正常流程注册→买卡→预约→签到一条异常流程重复手机号注册、预约已满课程、余额不足买卡。老师最爱问「如果用户重复提交怎么办」你当场演示异常流程的拦截效果比背概念有说服力。最后说个我自己的习惯每次改完数据库表结构一定同步更新一份schema.sql放在项目根目录答辩前用这份脚本在干净环境里重建一次数据库确保没有依赖本地残留数据。这个习惯帮我避免过至少三次「本地能跑、换台电脑就崩」的翻车。希望帮到你。本文还有配套的精品资源点击获取