SpringBoot3+Vue3+MySQL健身房管理系统全栈开发实战
2026/9/5 18:28:44 网站建设 项目流程

这次我们从零拆一套健身房管理系统:后端 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,前端开发 5173,MySQL 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. 数据库设计与建表 SQL

3.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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录会话表';

这里有几个容易踩的数据库细节。金额字段不要用floatdouble,办卡订单统一用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_PASSWORD=root \ -e MYSQL_DATABASE=gym_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 连接串上加allowPublicKeyRetrieval=true。出现Access denied for user 'root'@'localhost',大概率是密码错误或者没有允许当前 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <!-- 版本号以 Maven 仓库实际稳定版本为准 --> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

4.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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true 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

需要注意:数据库连接串中serverTimezone=Asia/Shanghai不是可有可无的参数。如果服务器和本地时区不一致,查询DATETIME字段时会出现时间偏移,甚至直接抛时区异常。生产环境数据库密码不要写在明文配置里,至少使用环境变量替换。

启动后端:

mvn spring-boot:run

如果编译阶段报“程序包 javax.servlet 不存在”或“无法访问 jakarta.servlet”,说明项目里混入了 SpringBoot2 时代的依赖,需要检查依赖树并排除。

5. 会员核心接口实现

5.1 统一返回体

前后端分离项目里,统一返回值能大幅减少前端异常判断成本。下面定义Result<T>

@Data public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } public static <T> Result<T> error(int code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }

5.2 实体类和 Mapper

对应 MySQL 的member表,实体类字段采用驼峰风格,MyBatis-Plus 打开map-underscore-to-camel-case后会自动映射member_nomemberNo

@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 BaseMapper<Member> { }

5.3 新增会员和分页查询

新增会员时要做两件事:生成会员编号;判断手机号是否已经存在。分页查询时支持关键词按姓名或手机号模糊搜索。

@RestController @RequestMapping("/api/member") @RequiredArgsConstructor public class MemberController { private final MemberMapper memberMapper; @PostMapping public Result<Member> add(@RequestBody Member member) { if (!StringUtils.hasText(member.getPhone())) { return Result.error("手机号不能为空"); } Long exists = memberMapper.selectCount( Wrappers.<Member>lambdaQuery() .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 Result<Page<Member>> page(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Member> 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)); } Page<Member> 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?current=1&size=10&keyword=张三"

第一次跑通“新增 + 查询”,说明后端数据链路已经通了。接下来再补会员卡和订单业务。

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 Result<LoginVO> login(@RequestBody LoginDTO dto) { SysUser user = sysUserMapper.selectOne( Wrappers.<SysUser>lambdaQuery() .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.<SysLoginSession>lambdaQuery() .eq(SysLoginSession::getToken, token) ); if (session != null && session.getExpireTime().isAfter(LocalDateTime.now())) { return true; } } response.setStatus(401); response.setContentType("application/json;charset=UTF-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 vite@latest 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 !==

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询