Java毕设实战:从零搭建健身房会员管理系统(Spring Boot+MySQL)
作为一个带过不少毕设项目、也被周围同学反复问过“Java毕设到底做什么好”的人我经常遇到一种情况很多人打开 IDEA新建了一个 Spring Boot 项目跑通了自带的 HelloWorld然后就开始慌了——因为接下来要做的“系统”不是一句 HelloWorld 能糊弄过去的。健身房会员管理系统就是这类选题里非常典型的代表。它看起来简单但牵扯到会员信息管理、课程安排、私教预约、续费提醒、数据统计等真实业务场景既能覆盖数据库设计、增删改查、权限控制这些 Java 基础能力又不会像电商系统那样复杂到让你无从下手。选这个题目本质上是用一个“麻雀虽小、五脏俱全”的业务闭环把 Java 后端开发里最常用的一套东西完整过一遍。但这里我想给一个明确判断这类管理系统能否拿高分核心不在页面炫不炫而在数据建模和代码结构能不能撑起业务逻辑。很多同学把 80% 的时间花在折腾 Bootstrap 模板上最后数据库只建了两张表功能全靠前端按钮堆——答辩时老师问一句“会员续费记录怎么查一个月到期的会员”就答不上来。真正的功夫恰恰在你没注意的数据库设计和代码分层里。这篇文章我会从零开始带你完成一个可运行的健身房会员管理系统先梳理业务模块再设计数据库表结构然后给出后端代码实现、前端页面调用方式和完整的运行验证步骤最后补充常见问题和毕业设计答辩加分建议。每个环节我都会直接给出可复制的代码你不需要一上来就追求完美跑通一个最小版本再逐步加功能才是比较稳的节奏。1. 这篇文章真正要解决的问题这类项目最尴尬的地方在于网上下载的源码往往结构混乱把 Controller 写成“上帝类”所有业务逻辑塞在一个方法里数据库脚本缺约束外键一概不建时间字段连默认值都没有最要命的是没有一份完整的数据流说明你拿到源码根本跑不起来。所以本文要解决的问题有三个怎么从业务需求出发设计数据库而不是先拍脑袋建表。怎么用清晰的 Java 分层结构实现核心功能让代码能拆能改、不粘在一起。怎么把数据库、后端、前端串起来进行验证并准备好答辩可能被追问的细节。如果你是刚学完 Java SE、正在找第一个完整项目练手或者正在为毕业设计、数据库课程设计发愁这篇文章都适合你。读完你不仅能跑通项目更重要的是理解这套“分析业务—设计表—写后端—验证功能”的完整方法。这个方法比项目本身更值钱因为换一个题目比如图书管理系统、快递管理系统你依然能套用同样的思路做出来。2. 业务分析与核心概念这套系统到底在管理什么2.1 系统的三个核心角色健身房会员管理系统首先要搞清楚谁在用系统、有什么权限。以常见的校园毕设级别系统为例我们至少需要三个角色角色核心诉求典型操作管理员掌握会员整体情况、课程安排、营收概览查看仪表盘、管理教练、查看预约记录前台/员工接待会员、办理办卡和续费、约课新增会员、续费、登记课程预约会员查询自己的卡信息、课程表、训练记录查看个人资料、查看剩余次数、查看预约在毕设答辩时你可以说自己做了“基于角色的访问控制”。但落到代码层面最简单的做法是用户表加一个role字段拦截器根据角色判断是否放行。不用引入 Spring Security 这种重量级框架先把基础逻辑做明白后续想加权限框架也方便。这里真正容易踩坑的地方是很多同学把“会员”和“用户”当成一张表。从展示角度来说你可以让会员直接通过用户名密码登录但更规范的做法是拆成user账号信息和member会员档案两张表通过 user_id 关联。原因有两层第一会员的卡信息、到期时间经常变账号信息很少变拆开后各自维护更清晰第二以后如果新增“教练也登录系统”的需求教练可以有自己独立的用户账号而会员表不需要为此改动字段。2.2 会员的生命周期是数据设计的灵魂“会员”在健身房场景里不是一个静态对象它有状态流转潜在客户 - 已办卡会员 - (会员卡到期/续费) - 续费会员或过期成员如果数据库里只设计一张member表存手机号和姓名你确实能完成“增删改查”演示但老师一问“有多少会员这个月到期”你就要临时改代码、加字段。更合理的做法是把“会员”和“会员卡/套餐”关联起来通过状态字段表达生命周期。后面我会给出一个尽量接近真实业务、但控制在一定复杂度内的表结构。核心原则是数据库表不是字段越少越好而是刚好支撑你需要的业务查询。2.3 为什么说增删改查只是表象建模才是真功夫CSDN 上大量课程设计项目Java 代码无非就是几家框架套来套去。但你会发现会写select * from member的人很多能讲清楚“为什么要加create_time索引”“为什么会员手机号要加唯一约束”“为什么课时记录不能只存一个课程名字”的人很少。这套系统的难点和分数差距就在这里。举例来说会员预约一节团课如果只在预约表里存了member_id和course_id那么你想知道“这个会员是不是重复约了同一节课”“这节课是否已经约满”就需要额外的业务判断。如果课程表里加上“最大人数”字段预约表里加上“约课时间”你就能很容易地查出这节团课当前预约了几人某个会员是否已约过这节课哪些会员约了课但没来可以加一个 status 字段。这就是从“写接口”到“设计业务”的区别。3. 数据库设计与建表脚本附 SQL3.1 表结构总览前面我们确定了角色划分和会员生命周期下面给出一个不超过 6 张表、但能完整支撑业务闭环的设计方案表名用途关键字段user登录账号表id, username, password, rolemember会员档案表id, user_id, name, phone, gender, birthdaycard会员卡表id, member_id, card_no, card_type, start_date, end_date, statuscoach教练表id, name, phone, specialtycourse课程表id, coach_id, course_name, max_people, course_date, start_timebooking预约记录表id, member_id, course_id, booking_time, status其中member和card的关系是“一个会员可以有多张卡记录历史上的旧卡”但当前有效的卡通常只有一张。为了简化毕设里也可以设计成会员表里直接带上card_no和end_date但我更推荐拆分成独立表因为这样后续做“续费记录”会非常自然。3.2 建表 SQL 脚本下面是一个可以直接在 MySQL 中执行的建表脚本。为了便于理解字段中加了注释方便你自己调整。-- 文件路径sql/gym_system.sql CREATE DATABASE IF NOT EXISTS gym_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE gym_system; -- 用户表存登录账号 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL COMMENT 建议存储加密后的密码, role VARCHAR(20) NOT NULL DEFAULT MEMBER COMMENT ADMIN/COACH/MEMBER, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 会员档案表 CREATE TABLE member ( id INT NOT NULL AUTO_INCREMENT, user_id INT DEFAULT NULL, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, gender TINYINT DEFAULT 0 COMMENT 0未知 1男 2女, birthday DATE DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 会员卡表 CREATE TABLE card ( id INT NOT NULL AUTO_INCREMENT, member_id INT NOT NULL, card_no VARCHAR(50) NOT NULL, card_type VARCHAR(20) NOT NULL COMMENT 月卡/季卡/年卡, start_date DATE NOT NULL, end_date DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT 1有效 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_card_no (card_no), KEY idx_member_id (member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教练表 CREATE TABLE coach ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT NULL, specialty VARCHAR(100) DEFAULT NULL COMMENT 主攻方向, hire_date DATE DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, coach_id INT NOT NULL, course_name VARCHAR(100) NOT NULL, max_people INT DEFAULT 20, course_date DATE NOT NULL, start_time VARCHAR(10) DEFAULT NULL COMMENT 例如 19:00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_course_date (course_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约记录表 CREATE TABLE booking ( id INT NOT NULL AUTO_INCREMENT, member_id INT NOT NULL, course_id INT NOT NULL, booking_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0已预约 1已取消 2已完成, PRIMARY KEY (id), KEY idx_member_course (member_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节值得说明为什么user表要加唯一索引uk_usernamemember表要加唯一索引uk_phone原因很简单健身房办卡时手机号是重要标识同一家门店不应该允许两个会员用同一个手机号注册。数据库层的唯一约束比你在 Java 代码里先查询再判断更可靠并发场景下不会出现重复数据。3.3 索引使用的基本原则有些同学会在每个字段上都加索引这其实不对。索引能加速查询但也会拖慢插入和更新。对于这张规模的表常规做法是主键上自然有索引频繁用于where条件的字段如member.phone、booking.member_id、course.course_date建立普通索引唯一业务标识如card_no建立唯一索引不要在text或过长的varchar字段上建索引。如果你在答辩时能讲出“为什么给course_date加索引”会比只会执行 SQL 的人更有说服力。4. 技术选型与环境准备4.1 技术栈建议对于毕设或练习项目我推荐一套最稳妥的组合层级技术选型理由后端框架Spring Boot简化配置适合快速开发持久层Spring JDBC / MyBatis重点理解 SQL 和 Java 的映射关系前端HTML Bootstrap jQuery不用引入复杂前端工程好学易演示数据库MySQL 5.7 或 8.0网上资料多排错容易构建工具Maven导入依赖方便IDEIntelliJ IDEA学生使用广泛这里有个取舍要讲清楚。很多教程直接上 MyBatis-Plus确实能少写很多 SQL但如果你正处于“练 Java 基础”的阶段我更建议先用原生 JDBC 或 MyBatis 把 SQL 写明白再用 MyBatis-Plus 感受“少写代码”的便利。否则你很容易陷入“框架帮你做了所有事但数据是怎么查出来的你完全没概念”的尴尬状态。4.2 环境准备清单在开始写代码前先确认你的电脑里有以下环境JDK 8 及以上版本以你本机安装为准本文示例代码使用 Java 8 语法兼容后续版本Maven 3.6 及以上MySQL 5.7 或 8.0IntelliJ IDEA 社区版或专业版一个能运行浏览器的现代浏览器。环境安装我不展开细说但有一个点容易踩坑IDEA 里新建 Spring Boot 项目时如果网络不稳定Maven 依赖会一直下载失败。解决办法是给 Maven 配置国内镜像源一般在自己的settings.xml中增加阿里云镜像即可。4.3 项目结构规划为了后面写代码不混乱先规划好目录结构gym-system/ ├── pom.xml ├── sql/ │ └── gym_system.sql └── src/main/ ├── java/com/example/gym/ │ ├── GymApplication.java │ ├── controller/ │ ├── service/ │ ├── dao/ │ ├── entity/ │ └── config/ └── resources/ ├── application.properties ├── static/ └── templates/这个结构是标准的 Spring Boot 分层Controller 负责接收请求Service 负责业务逻辑Dao 负责数据库访问Entity 负责实体映射。这样的好处是如果你要换数据库或改前端不需要大改业务代码。5. 核心代码实现从 Maven 配置到后端接口5.1 Maven 依赖配置pom.xml为了让项目能跑起来先准备好 Maven 依赖。下面是关键依赖!-- 文件路径pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 启动器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 模板用于渲染页面 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- JDBC 支持配合 mysql 驱动即可使用 JdbcTemplate -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency !-- 简化实体类代码可选 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies如果你不想用 Lombok也可以不用手动写 getter/setter 即可。这里保留它是为了演示常见的项目依赖但你要知道 Lombok 只是编译期简化工具不是必须的。5.2 数据库连接配置application.properties# 文件路径src/main/resources/application.properties spring.datasource.urljdbc:mysql://localhost:3306/gym_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password你的数据库密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.thymeleaf.cachefalse server.port8080注意serverTimezoneAsia/Shanghai如果不加容易出现时间相关的时区报错。数据库密码一定要改成你自己本机的 MySQL 密码。5.3 实体类 Member实体类跟数据库表字段一一对应。以Member为例// 文件路径src/main/java/com/example/gym/entity/Member.java package com.example.gym.entity; import java.time.LocalDate; import java.time.LocalDateTime; public class Member { private Integer id; private Integer userId; private String name; private String phone; private Integer gender; private LocalDate birthday; private LocalDateTime createTime; public Member() { } public Member(String name, String phone) { this.name name; this.phone phone; } public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId userId; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone phone; } public Integer getGender() { return gender; } public void setGender(Integer gender) { this.gender gender; } public LocalDate getBirthday() { return birthday; } public void setBirthday(LocalDate birthday) { this.birthday birthday; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime createTime; } }第一次写实体类的时候有人会问为什么不用java.util.Date用LocalDate和LocalDateTime是更现代的做法类型语义更明确生日只需要日期不需要时分秒所以用LocalDate创建时间需要精确到秒所以用LocalDateTime。如果你的数据库版本或驱动兼容性有问题也可以统一换成java.util.Date但要保证跟表结构能对应上。5.4 Dao 层新增会员和查询会员列表这里我先用JdbcTemplate演示它比纯 JDBC 写起来简洁又能让你清楚看到 SQL 的执行过程。如果你后面想换 MyBatis业务逻辑和 Service 层基本不用改动改 Dao 层实现即可。// 文件路径src/main/java/com/example/gym/dao/MemberDao.java package com.example.gym.dao; import com.example.gym.entity.Member; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; Repository public class MemberDao { Autowired private JdbcTemplate jdbcTemplate; /** * 查询所有会员 */ public ListMember findAll() { String sql SELECT id, user_id, name, phone, gender, birthday, create_time FROM member ORDER BY id DESC; return jdbcTemplate.query(sql, new BeanPropertyRowMapper(Member.class)); } /** * 根据手机号查询会员 */ public Member findByPhone(String phone) { String sql SELECT id, user_id, name, phone, gender, birthday, create_time FROM member WHERE phone ?; ListMember members jdbcTemplate.query(sql, new BeanPropertyRowMapper(Member.class), phone); return members.isEmpty() ? null : members.get(0); } /** * 新增会员 */ public int insert(Member member) { String sql INSERT INTO member(user_id, name, phone, gender, birthday) VALUES(?, ?, ?, ?, ?); return jdbcTemplate.update(sql, member.getUserId(), member.getName(), member.getPhone(), member.getGender(), member.getBirthday()); } }这里有一个 Spring JDBC 的常见坑BeanPropertyRowMapper会自动把数据库字段user_id映射为userId。如果你的实体类属性命名不规范或者数据库字段有别名映射就会失败。排查时优先看两个地方字段名是否一致、Java 类型是否能被 JdbcTemplate 转换。5.5 Service 层处理业务逻辑Service 层的职责是“调用 Dao 完成业务”。以新增会员为例我们要做的不是简单调用insert而是先判断手机号是否已经存在。这个业务判断放在 Service 层比放在 Controller 层更合理因为将来如果要在别的地方添加会员比如批量导入也会复用这里的逻辑。// 文件路径src/main/java/com/example/gym/service/MemberService.java package com.example.gym.service; import com.example.gym.dao.MemberDao; import com.example.gym.entity.Member; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; Service public class MemberService { Autowired private MemberDao memberDao; public ListMember listAll() { return memberDao.findAll(); } public Member getByPhone(String phone) { return memberDao.findByPhone(phone); } /** * 新增会员手机号重复时抛出异常并回滚 */ public boolean addMember(Member member) { Member exist memberDao.findByPhone(member.getPhone()); if (exist ! null) { return false; } int rows memberDao.insert(member); return rows 0; } }5.6 Controller 层接收请求并响应Controller 层做的是“翻译”把前端传来的参数转成 Java 对象调用 Service 处理然后把结果返回给页面或 JSON。// 文件路径src/main/java/com/example/gym/controller/MemberController.java package com.example.gym.controller; import com.example.gym.entity.Member; import com.example.gym.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; Controller public class MemberController { Autowired private MemberService memberService; /** * 会员列表页 */ GetMapping(/member/list) public String list(Model model) { model.addAttribute(members, memberService.listAll()); return member_list; } /** * 新增会员页面表单提交 */ PostMapping(/member/add) public String add(Member member) { memberService.addMember(member); return redirect:/member/list; } /** * 新增会员JSON 接口方便前端用 Ajax 调用 */ PostMapping(/api/member/add) ResponseBody public MapString, Object addByJson(RequestParam String name, RequestParam String phone) { MapString, Object result new HashMap(); Member member new Member(name, phone); boolean success memberService.addMember(member); result.put(success, success); result.put(message, success ? 新增成功 : 该手机号已存在); return result; } }这里的POST /api/member/add接口你可以直接放到 Postman 里测试不用刷新页面也能知道后端逻辑是否正常。JdbcTemplate 新增成功后返回的是受影响行数所以rows 0就是判断成功的准绳。6. 前端页面与交互用 Bootstrap 快速搭一个会员列表6.1 页面需求说明后端接口写完以后剩下的事情就是把页面串起来。我见过很多人一上来就搞 Vue Element UI结果项目一运行发现 Node 环境没配好、跨域问题一堆、源码看起来特别复杂。对于毕设项目我更推荐用模板引擎渲染页面或者直接用一个静态 HTML Ajax 调接口。这里我提供一个最简单的会员列表页面用 Bootstrap 4 CDN 和 jQuery 完成交互。你没有看错这段代码没有 Webpack、没有 npm非常适合快速验证和课堂演示。6.2 会员列表页面member_list.html!DOCTYPE html html langzh-CN xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title会员管理 - 健身房管理系统/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap4.6.2/dist/css/bootstrap.min.css /head body div classcontainer stylemargin-top: 30px; h3 classmb-4会员列表/h3 form classform-inline mb-3 idaddForm input typetext idmemberName classform-control mr-2 placeholder会员姓名 input typetext idmemberPhone classform-control mr-2 placeholder手机号 button typesubmit classbtn btn-primary新增会员/button /form table classtable table-bordered thead tr thID/th th姓名/th th手机号/th th性别/th th创建时间/th /tr /thead tbody idmemberTableBody tr th:eachmember : ${members} td th:text${member.id}1/td td th:text${member.name}张三/td td th:text${member.phone}13800000000/td td th:text${member.gender 1 ? 男 : (member.gender 2 ? 女 : 未知)}未知/td td th:text${member.createTime}2024-01-01 10:00:00/td /tr /tbody /table /div script srchttps://cdn.jsdelivr.net/npm/jquery3.6.0/dist/jquery.min.js/script script $(function () { $(#addForm).submit(function (event) { event.preventDefault(); var name $(#memberName).val().trim(); var phone $(#memberPhone).val().trim(); if (!name || !phone) { alert(姓名和手机号不能为空); return; } $.ajax({ url: /api/member/add, type: POST, data: {name: name, phone: phone}, dataType: json, success: function (res) { if (res.success) { alert(res.message); location.reload(); } else { alert(res.message); } }, error: function () { alert(请求失败请检查后端服务); } }); }); }); /script /body /html这里有几个点需要留意th:each是 Thymeleaf 的循环语法它会把数据库里已有的会员渲染到表格中新增会员用 Ajax 调用 JSON 接口成功后再刷新页面显示最新列表如果你想测试后端接口完全可以不用这个页面直接在浏览器地址栏输入不现实因为 POST 需要工具推荐用 Postman 或 curl。6.3 为什么前端不建议过度设计这里我想再说一次做毕设项目前端功能满足演示和操作即可重心尽量放在系统和数据逻辑上。答辩老师通常关注的是有没有完成业务闭环、数据是怎么流动的、表设计是否合理而不是你的按钮动画做得有多流畅。学会用 Bootstrap 快速搭后台管理页面比花两周时间研究前端工程化性价比高得多。7. 运行与验证确保项目真的能跑通7.1 启动项目在 IDEA 中打开项目根目录等待 Maven 依赖下载完成后直接运行GymApplication的主方法。控制台出现类似下面的日志就说明 Spring Boot 已经启动成功Tomcat started on port(s): 8080 (http) Started GymApplication in 3.12 seconds (JVM running for 3.45)如果启动失败优先检查application.properties里的数据库配置和 MySQL 服务是否已经启动。日志里如果有Access denied for user说明用户名或密码不对如果有Unknown database说明你没有执行建表 SQL。7.2 用数据库验证核心查询启动成功后拿一个最常用的业务问题来验证数据设计是否合理“如何查询当前有效的会员卡”假设今天是 2025 年某个日期我们需要查所有end_date在今天之后且状态为 1 的会员卡并关联出会员姓名和手机号。SQL 可以这样写SELECT m.name, m.phone, c.card_no, c.card_type, c.end_date FROM card c JOIN member m ON c.member_id m.id WHERE c.status 1 AND c.end_date CURRENT_DATE ORDER BY c.end_date ASC;这条 SQL 在数据库客户端里执行成功说明你的表结构设计能支撑“会员到期提醒”这个常见功能。如果你的表结构里没有独立的卡表而是把卡信息直接写在会员表里这条 SQL 的写法就会不同扩展性也会差一些。7.3 用 Postman 验证后端接口启动项目后可以用 Postman 测试新增会员接口请求类型POST请求地址http://localhost:8080/api/member/addBody 参数x-www-form-urlencodedname李四、phone13900001111预期返回{ success: true, message: 新增成功 }再次用相同手机号提交预期返回{ success: false, message: 该手机号已存在 }这两次测试说明接口能正常写库业务校验也生效了。第一次做这类项目的人经常漏掉“重复校验”这一步导致数据库里出现多条相同手机号的会员这在健身房场景里是不允许的。7.4 如果我连数据库导入都不会怎么办这一步也许是很多新手最头疼的地方。简单说打开 MySQL 命令行或 Navicat执行sql/gym_system.sql文件里的全部 SQL就在gym_system库下建好了所有表。如果你用的是 Navicat更简单的操作是右键连接 - 运行 SQL 文件 - 选择脚本 - 开始。如果执行过程报错大概率是 SQL 脚本里有语法和你 MySQL 版本不兼容的地方可以逐段执行定位问题。8. 常见问题与排查思路下面列出的问题都是这类管理系统开发过程中很常见的。我按“问题现象 - 可能原因 - 排查方式 - 解决方案”的方式整理成表格方便你遇到问题直接对照。问题现象可能原因排查方式解决方案项目启动失败报数据库连接错误MySQL 服务未启动 / 用户名密码错误 / 库名不存在查看控制台异常信息检查 MySQL 服务状态启动 MySQL确认配置项和密码正确执行建表脚本查询会员列表时报 500表不存在或字段跟实体类不匹配查看日志里的 SQL 异常用 Navicat 执行对应 SQL检查表名、字段名是否与实体类一致新增会员时中文乱码数据库字符集或连接字符集不对查看数据表字符集检查 URL 中是否带characterEncodingutf8将数据库和表统一为 utf8mb4连接串加编码参数手机号重复但系统没有拦截两张会员档案手机号都能插入检查 Service 层是否判断了findByPhone在 Dao 层查询后再插入数据库层也加唯一约束Thymeleaf 页面报错提示属性找不到实体类缺少 getter/setter查看页面上th:text引用的属性名检查实体类字段和 getter 方法是否完整使用 Lombok 时编译报错IDEA 未安装 Lombok 插件检查 Build 日志安装 Lombok 插件或在 pom 中排除 Lombok手动写 getter/setterAjax 请求跨域前端服务端口和后端不一致查看浏览器控制台 Network使用同源访问或用 Spring Boot 配置跨域过滤器如果你用的是 Spring Boot 2.7.18 这个版本Maven 提示lombok报错时除了装插件也可以选择去掉 Lombok。去掉之后完全不影响系统功能只是实体类会变得稍微啰嗦。对于学习阶段我其实推荐亲手写一遍 getter/setter更有助于理解 Java 的基础语法。9. 工程化建议与毕设答辩加分点9.1 代码规范是隐形加分项很多同学交上来的源码代码缩进不统一、命名靠拼音缩写、Controller 里塞了几百行业务逻辑。老师打开源码第一眼印象分就下来了。这里建议你做到三条类名、方法名、变量名都用语义化英文比如findByPhone、addMember不要用a、b、c这类命名Service 层不要出现具体 SQLSQL 统一在 Dao 层维护常量要定义不要到处写魔法值比如会员卡类型可以用String CARD_TYPE_MONTH MONTH。9.2 至少实现一个“带业务含义”的功能只会增删改查答辩时很容易被问住。建议你至少加一个业务功能比如会员卡到期自动计算剩余天数会员约课时判断人数是否已满首页展示今日预约人数和会员总数。以“会员卡到期自动计算剩余天数”为例它并不复杂只需要在查询会员时用 SQL 的DATEDIFF(end_date, CURRENT_DATE)或者 Java 的LocalDate计算差值但这是一个完整的业务逻辑能向老师证明你不是只会机械地写 SQL。public long getRemainingDays(LocalDate endDate) { if (endDate null) { return 0; } return ChronoUnit.DAYS.between(LocalDate.now(), endDate); }9.3 稍微提一下安全性能让你从人群中区分出来如果时间允许给密码加密会是一个很好的加分点。最简单的方案是用 Spring 自带的BCryptPasswordEncoder它比 MD5 更安全因为内置了随机盐。作为毕设只要你能在答辩中说清楚“用户密码不能明文存储所以我用了 BCrypt 加密”就已经比很多项目高一个层次了。// 示意密码加密 String rawPassword 123456; String encodedPassword new BCryptPasswordEncoder().encode(rawPassword);9.4 保留一份操作说明书最后给你的项目附上一份README.md或《运行说明.docx》写明数据库如何导入、账号密码是什么、后端如何启动、前端页面访问路径是什么、主要功能如何演示。这不仅是为了让老师能复现运行也是在模拟真实团队里的交接文档。对于一个毕设项目这是最实在、最容易被忽略的工程化能力。10. 小结与下一步建议健身房会员管理系统是一个非常适合 Java 初学和毕设阶段的综合性项目。通过它你能完整走一遍“业务分析、数据库设计、后端分层、前端联调、运行验证”的开发流程而且这套流程可以迁移到很多其他管理系统选题上。这篇文章里给出的建表脚本、实体类、Dao 层、Service 层、Controller 层和前端页面已经可以组成一个能跑通的最小闭环。建议你拿到代码后不要只复制粘贴而是亲手把Member、Card、Booking这几条链路都走一遍新增一个会员给他办一张卡然后预约一节团课最后查一次预约列表。复现过一遍完整流程你对项目整体结构的理解会比看十篇文章都有用。下一步可以从三个方向深化一是给系统加入会员卡到期提醒和课程预约人数统计二是引入 MyBatis-Plus 简化持久层代码三是结合 Spring Security 做实登录和角色权限控制。每个方向都够你再写一篇博客了。把这个项目吃透你就不是“能跑通一个 Demo”而是真的有信心在面试或答辩时说清楚自己做了哪些事、为什么这么设计。先跑通它再改造它这个项目才会真正变成你自己的东西。