尧图精选

SpringBoot3+Vue3+MySQL健身房管理系统全栈开发实战

🕒 发布时间:2026/9/5 18:29:00 📁 来源:尧图网络
这次我们从零拆一套健身房管理系统后端 Java SpringBoot3前端 Vue.js3数据库 MySQL。这个组合覆盖了课程设计、毕业设计和 Java 全栈练手最常见的技术点也不用硬上微服务、消息队列这些偏重的中间件。如果你正在纠结毕设题目或者想找一个能完整练到“接口、权限、分页、报表、导出”的前后端分离项目这套系统可以作为首选模板。先看核心设计约束SpringBoot3 不是 SpringBoot2它基于 Spring Framework 6 和 Jakarta EE启动环境要求 JDK17如果用 JDK8 或 JDK11会直接遇到版本不兼容。前端采用 Vite Vue3 工程化方式比 Vue2 的 webpack 模板轻量很多。MySQL 端建议直接使用 MySQL 8.0连接驱动用com.mysql.cj.jdbc.Driver建库字符集用utf8mb4否则中文插入和统计容易出现乱码。数据库设计上要切分成会员、会员卡、课程、预约、售卡订单、系统用户几个核心模块先不碰商品库存、多门店、多人商城这些扩展功能。下面会按“功能规划 - 数据库建表 - 后端工程 - 核心接口 - 登录认证 - Vue3 前端 - 联调验证 - 批量导入导出 - 常见问题 - 部署上线”的顺序展开给出的建表 SQL、Java 代码、Vue 代码和排查清单都能直接复制到本地项目里改。1. 系统核心能力速览能力项方案说明系统定位健身房会员、办卡、约课、收入报表的后台管理系统后端技术栈Java 17 SpringBoot3 MyBatis-Plus MySQL前端技术栈Vue3 Vite Vue Router Pinia Element Plus Axios数据库要求MySQL 8.0字符集 utf8mb4默认端口后端 8080前端开发 5173MySQL 3306接口风格RESTful统一前缀/api统一返回体 Result登录方案简易 Token 会话方案生产可替换成 Spring Security JWT 或 Sa-Token批量能力会员 Excel 导入导出、月度收入统计、分页查询适合场景课设、毕设、前后端分离入门训练、小型健身工作室内部管理原型不建议直接承担大型连锁会员系统、涉及支付资金清结算的正式生产系统从规划角度讲这套系统最重要的不是页面做得多花哨而是数据关系是否正确一个会员可以拥有多张会员卡会员可以预约多节课程每一笔售卡订单都要有订单号和支付状态会员查询要支持分页和手机号模糊搜索。把这几个关系理清楚整个项目骨架就是健康的。技术选型上MyBatis-Plus 可以省掉大量单表 CRUD 代码官方已经提供了 Spring Boot3 对应的 starter包名基于jakarta体系不能把旧版mybatis-plus-boot-starter直接复制到 SpringBoot3 项目里使用这是一个很典型的启动报错来源。2. 功能模块规划与使用边界2.1 先做最小可用版本不要一上来就规划十几个模块。健身房管理系统的最小闭环是三条链第一条会员进店后先建档系统生成唯一的会员编号姓名、手机号、性别、生日是基础字段。这里要特别注意体重、身高、健康备注属于健康隐私信息不是核心管理必须项能不做就不做做了就必须做权限控制和脱敏展示。第二条会员办卡。会员可以购买时长卡、次数卡系统要记录卡号、开卡时间、到期时间和支付状态。如果是一个课程设计项目不需要真的接微信/支付宝支付用“模拟支付成功”或者在订单表里维护支付状态即可重点是把订单状态流转写清楚。第三条课程预约。健身房里通常有团课和私教课会员看到课程时间后预约到课后再确认签到。此时需要把“上课人数不能超过单节课程最大人数”的业务校验写进后端而不是只在前端按钮上禁用。商品零售、私教排班、员工工资、租客管理这些模块在第一个版本里全部砍掉。项目不是越多功能越好把会员、卡、约课、订单四条主流程闭环跑通就足以证明你对 SpringBoot3 Vue3 MySQL 的掌握程度。2.2 使用边界与合规提醒这个系统会收集会员的手机号码、生日、身体数据。在页面展示、Excel 导出、接口返回时都要遵循“最少必要”原则列表页不展示完整手机号时可以脱敏导出文件要限制为管理员操作数据库备份文件不能泄露到公开网盘。涉及人脸识别门禁、声音或图像采集的功能必须在明确获得授权、且有安全存储方案的前提下再扩展。涉及课程照片、教练肖像、音乐版权素材时也要确认是否允许在学员端或公开展示页面中使用。简单说技术是实现手段数据边界必须在架构一开始就画好。本文后面的接口和表结构均按学习演示版本实现正式商用前还需要补审计日志、密码加密、角色权限和数据备份。3. 数据库设计与建表 SQL3.1 核心表关系说明前端界面再复杂底层都靠 MySQL 表承载。可以先在纸上画出下面这些关系sys_user系统登录用户成员主要分管理员、前台、教练初期不做复杂 RBAC。member会员主表member_no是业务编号带唯一索引。membership_card会员卡表一个会员可以有多张卡。card_order售卡订单表记录办卡来源、支付状态、支付金额。course课程表包含团课或私教课的基础信息。course_appointment课程预约记录表会员与课程是多对多关系。sys_login_session轻量登录会话表保存前端 Token 和过期时间避免在初期就引入 Redis。建表脚本如下直接放到 MySQL 中执行即可。数据库名统一使用gym_db。CREATE DATABASE IF NOT EXISTS gym_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE gym_db; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(128) NOT NULL COMMENT 密码请使用BCrypt加密, real_name VARCHAR(50) NULL COMMENT 真实姓名, role_code VARCHAR(20) NOT NULL DEFAULT OPERATOR COMMENT ADMIN/OPERATOR/COACH, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL, update_time DATETIME NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_no VARCHAR(32) NOT NULL UNIQUE COMMENT 会员编号, name VARCHAR(50) NOT NULL COMMENT 会员姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, birthday DATE NULL COMMENT 生日, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0冻结, source VARCHAR(20) NULL COMMENT 来源WALK_IN/ONLINE/REFERRAL, remark VARCHAR(255) NULL COMMENT 备注, create_time DATETIME NOT NULL, update_time DATETIME NULL, INDEX idx_phone(phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表; CREATE TABLE membership_card ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(32) NOT NULL UNIQUE COMMENT 卡号, member_id BIGINT NOT NULL COMMENT 会员ID, card_type VARCHAR(20) NOT NULL COMMENT TIME_CARD时长卡/COUNT_CARD次数卡, total_count INT NULL COMMENT 总次数次数卡专用, used_count INT NOT NULL DEFAULT 0 COMMENT 已用次数, begin_date DATE NOT NULL COMMENT 开卡日期, expire_date DATE NULL COMMENT 到期日期, status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效 0停用, create_time DATETIME NOT NULL, INDEX idx_member_id(member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员卡表; CREATE TABLE card_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, member_id BIGINT NOT NULL, card_id BIGINT NULL COMMENT 关联会员卡, card_type VARCHAR(20) NOT NULL, pay_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 订单金额, pay_status VARCHAR(20) NOT NULL DEFAULT UNPAID COMMENT UNPAID/PAID/REFUNDED, pay_time DATETIME NULL COMMENT 支付时间, source VARCHAR(20) NULL COMMENT 订单来源, create_time DATETIME NOT NULL, INDEX idx_member_id(member_id), INDEX idx_pay_status(pay_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT售卡订单表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, course_type VARCHAR(20) NOT NULL COMMENT GROUP团课/PRIVATE私教, coach_name VARCHAR(50) NULL, course_date DATE NOT NULL, start_time VARCHAR(10) NOT NULL COMMENT 例如 18:30, end_time VARCHAR(10) NULL, max_count INT NOT NULL DEFAULT 10 COMMENT 最大预约人数, current_count INT NOT NULL DEFAULT 0 COMMENT 当前已预约人数, price DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可预约 0下架, create_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE course_appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, course_id BIGINT NOT NULL, appointment_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0已预约 1已签到 2已取消, UNIQUE KEY uk_member_course(member_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程预约表; CREATE TABLE sys_login_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, token VARCHAR(64) NOT NULL UNIQUE, expire_time DATETIME NOT NULL, create_time DATETIME NOT NULL, INDEX idx_token(token) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT登录会话表;这里有几个容易踩的数据库细节。金额字段不要用float或double办卡订单统一用DECIMAL(10,2)否则多次统计后面会出现 0.1 这类精度误差。字符集使用utf8mb4不是utf8因为早期utf8在 MySQL 中无法完整存储部分生僻字和扩展表情符号。会员卡和课程预约里不要用“大宽表”把多个值用逗号拼在一个字段里例如member_ids 1,2,3这种设计在统计时非常痛苦正确做法是拆出course_appointment关联表。订单号建议用独立规则生成不要依赖数据库自增主键暴露业务量。可以在后端写入订单时拼接日期加随机流水例如20250130 毫秒时间戳后8位 随机数这样在数据量不大时基本够用。4. SpringBoot3 工程搭建与环境准备4.1 本地环境检查后端运行前先确认三样东西JDK 必须是 17 或更高版本SpringBoot3 无法在 JDK8 或 JDK11 上运行。Maven 建议 3.8。MySQL 8.0 已启动并且能通过 root 账号连接。如果你还没有 MySQL这里给出一个适合本地开发环境的 Docker 启动方式docker run -d \ --name gym-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ -e MYSQL_DATABASEgym_db \ mysql:8.0命令行登录 MySQL 后执行第 3 节的 SQL确认表已经建好mysql -uroot -p -h127.0.0.1 -P3306 gym_db USE gym_db; SHOW TABLES;如果连接时出现Public Key Retrieval is not allowed需要在 JDBC 连接串上加allowPublicKeyRetrievaltrue。出现Access denied for user rootlocalhost大概率是密码错误或者没有允许当前 host 登录优先检查 MySQL 用户权限。4.2 Maven 核心依赖在 Spring Initializr 中生成 SpringBoot3 项目依赖选择 Web、MySQL Driver。分页和单表 CRUD 需要引入 MyBatis-Plus注意 SpringBoot3 必须使用官方兼容 Boot3 的 MyBatis-Plus starter不能直接复制旧项目中的mybatis-plus-boot-starter。!-- pom.xml 核心依赖示例 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId !-- 版本号以 Maven 仓库实际稳定版本为准 -- /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency4.3 application.yml 配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/gym_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false需要注意数据库连接串中serverTimezoneAsia/Shanghai不是可有可无的参数。如果服务器和本地时区不一致查询DATETIME字段时会出现时间偏移甚至直接抛时区异常。生产环境数据库密码不要写在明文配置里至少使用环境变量替换。启动后端mvn spring-boot:run如果编译阶段报“程序包 javax.servlet 不存在”或“无法访问 jakarta.servlet”说明项目里混入了 SpringBoot2 时代的依赖需要检查依赖树并排除。5. 会员核心接口实现5.1 统一返回体前后端分离项目里统一返回值能大幅减少前端异常判断成本。下面定义ResultTData public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }5.2 实体类和 Mapper对应 MySQL 的member表实体类字段采用驼峰风格MyBatis-Plus 打开map-underscore-to-camel-case后会自动映射member_no到memberNo。Data TableName(member) public class Member { TableId(type IdType.AUTO) private Long id; private String memberNo; private String name; private String phone; private Integer gender; private LocalDate birthday; private Integer status; private String source; private String remark; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper 不需要写 XML 也能完成单表增删改查Mapper public interface MemberMapper extends BaseMapperMember { }5.3 新增会员和分页查询新增会员时要做两件事生成会员编号判断手机号是否已经存在。分页查询时支持关键词按姓名或手机号模糊搜索。RestController RequestMapping(/api/member) RequiredArgsConstructor public class MemberController { private final MemberMapper memberMapper; PostMapping public ResultMember add(RequestBody Member member) { if (!StringUtils.hasText(member.getPhone())) { return Result.error(手机号不能为空); } Long exists memberMapper.selectCount( Wrappers.MemberlambdaQuery() .eq(Member::getPhone, member.getPhone()) ); if (exists ! null exists 0) { return Result.error(该手机号已存在会员档案); } member.setMemberNo(M System.currentTimeMillis()); member.setStatus(1); member.setCreateTime(LocalDateTime.now()); memberMapper.insert(member); return Result.ok(member); } GetMapping(/page) public ResultPageMember page(RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword) { LambdaQueryWrapperMember query Wrappers.lambdaQuery(Member.class); query.orderByDesc(Member::getId); if (StringUtils.hasText(keyword)) { query.and(w - w.like(Member::getName, keyword) .or() .like(Member::getPhone, keyword)); } PageMember page memberMapper.selectPage(new Page(current, size), query); return Result.ok(page); } }这里选择的是 MyBatis-Plus 的LambdaQueryWrapper好处是字段名在编译期检查不会因为字符串拼错而查询失败。也可以用传统 XML 写动态 SQL但工程效率会低很多。后端接口启动后可以直接用 curl 做最基础的接口验证curl -X POST http://localhost:8080/api/member \ -H Content-Type: application/json \ -d {name:张三,phone:13800001111,gender:1} curl http://localhost:8080/api/member/page?current1size10keyword张三第一次跑通“新增 查询”说明后端数据链路已经通了。接下来再补会员卡和订单业务。6. 登录认证与接口权限管理系统不能裸奔至少要做登录校验。这里不引入 Spring Security 全量配置先用“登录生成 Token - 写入 sys_login_session - 拦截器校验 Token”的方式实现等系统规模增大后再替换为 Spring Security JWT。登录接口RestController RequestMapping(/api/auth) RequiredArgsConstructor public class AuthController { private final SysUserMapper sysUserMapper; private final SysLoginSessionMapper sessionMapper; PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO dto) { SysUser user sysUserMapper.selectOne( Wrappers.SysUserlambdaQuery() .eq(SysUser::getUsername, dto.getUsername()) .eq(SysUser::getStatus, 1) ); if (user null) { return Result.error(用户不存在或已停用); } // 正式项目中不要用明文密码比较这里省略 BCrypt 匹配代码 if (!user.getPassword().equals(dto.getPassword())) { return Result.error(密码错误); } String token UUID.randomUUID().toString().replace(-, ); SysLoginSession session new SysLoginSession(); session.setUserId(user.getId()); session.setToken(token); session.setExpireTime(LocalDateTime.now().plusHours(24)); session.setCreateTime(LocalDateTime.now()); sessionMapper.insert(session); return Result.ok(new LoginVO(token, user.getRealName(), user.getRoleCode())); } }登录成功后前端每个请求都会在 Header 中携带Authorization: Bearer token。后端通过拦截器校验Component RequiredArgsConstructor public class AuthInterceptor implements HandlerInterceptor { private final SysLoginSessionMapper sessionMapper; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authorization request.getHeader(Authorization); if (authorization ! null authorization.startsWith(Bearer )) { String token authorization.substring(7); SysLoginSession session sessionMapper.selectOne( Wrappers.SysLoginSessionlambdaQuery() .eq(SysLoginSession::getToken, token) ); if (session ! null session.getExpireTime().isAfter(LocalDateTime.now())) { return true; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }再把拦截器注册到 WebMvc 配置中Configuration RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login); } }这里演示的是轻量登录方案学习项目完全够用。生产环境建议将 Token 换成 JWT 并由网关统一校验或者直接引入 Sa-Token、Spring Security以减少每次请求都查一次数据库的开销。7. Vue3 前端工程与接口联调7.1 创建 Vite 工程前端需要 Node.js 18 及以上版本。命令行创建项目并安装依赖npm create vitelatest gym-web -- --template vue cd gym-web npm install npm install axios element-plus vue-router pinia npm run dev默认页面地址是http://localhost:5173。Vite 开发服务器里配置代理把所有/api请求转发到后端 8080 端口这样可以绕开浏览器跨域限制// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })7.2 Axios 请求封装封装一个带 Token 的 request 实例以后每个页面调用接口都统一走它// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(gym_token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code !
上一篇/下一篇内容由系统自动关联 返回资讯列表 →