Java毕设实战:基于Spring Boot+MyBatis的健身房会员管理系统

Java毕设实战:基于Spring Boot+MyBatis的健身房会员管理系统 你说的“健身房会员管理系统”是不是听起来很熟悉每年毕业季Java 方向的毕设选题里它几乎是“常驻嘉宾”。十个做管理系统的人里至少有三四个会选它。但问题是选这个题目的人多真正把它做扎实、能讲清楚的人少。我见过不少同学从网上扒下来一个“能运行”的源码跑起来看似正常结果答辩时被老师问一句“会员卡到期和续费的逻辑是怎么设计的”当场卡住。这个项目的价值恰恰不在于它能交差而在于它的技术覆盖度刚刚好CRUD、模糊查询、日期计算、状态管理、分页、一对多关联、统计报表这些 Java 后端开发里高频出现的知识点它全都能装进来。做完它你不仅能过毕设还能在回答“你做过什么项目”的时候真正有话可说。这篇文章不打算只给源码截几张图而是带你从需求、数据库、代码、运行到排查完整走一遍。你不需要是高手但需要跟着思路动手敲。我会把每一步为什么这样做、容易踩什么坑都讲清楚。最终你会得到一个能部署、能演示、能解释清楚的健身房会员管理系统。1. 这篇文章真正要解决的问题很多初学者做管理系统卡住的不是编码本身而是不知道自己写出来的每一块代码对应什么业务。随便举几个场景前台要给会员办一张年卡办卡日期是今天截止日期是明年今日这个“日期计算”在哪里实现会员卡快到期了系统要不要给出提醒提醒逻辑是查询时实时算还是每天定时任务更新状态会员想换卡升级旧卡余额怎么处理原卡冻结还是作废课程预约和会员卡余额怎么保证“余额不足不能约课”这个规则在代码里成立这些问题如果只是照着现成源码看很难看明白。因为它们横跨了数据库设计、Service 层业务逻辑、页面交互三个层面。读完本文你应该能收获三件事一张清晰的健身房会员管理系统功能地图明白每个模块存在的理由。一套规范的数据库设计思路不是随便建几张表而是能解释清楚字段含义和表关系。一份可以照着敲的代码骨架让你在拿到任何类似“xxx 管理系统”题目时都有重构和扩展的能力。适合读这篇文章的人正在准备 Java 毕业设计的学生、想找一个完整练手项目的初学者、已经工作但想巩固 Java Web 基础的后端新人。2. 健身房会员管理系统需求拆解与功能规划2.1 先理解健身房前台在干什么写系统之前先不要急着打开 IDEA先想想真实场景。一个健身房的日常业务可以拆成这几条线会员管理线新会员录入资料、老会员修改信息、会员查询、会员列表导出。会员卡线办卡买新卡、续费给旧卡延长时间或加次数、换卡升级卡类型、退卡结清费用后停用。课程线健身房发布课程表会员预约课程约满就不能再约上课前可以取消预约。教练线教练基本信息管理以及教练和课程的绑定关系。营收线每一笔办卡、续费、购课记录的流水以及每天的营收统计。理解了这五条线你就知道系统至少需要五个核心模块会员管理、会员卡管理、课程管理、教练管理、订单流水管理。再加上最基础的后台登录权限就是一个完整的业务闭环。2.2 功能模块怎么划分从一个面向学生的毕设项目角度出发我建议模块这样划分模块核心功能关键字段或状态管理员登录登录、退出、修改密码用户名、密码、角色会员管理新增会员、编辑信息、搜索、删除姓名、手机号、性别、身份证、状态会员卡管理开卡、续费、冻结、到期提醒卡号、卡类型、剩余天数/次数、状态课程管理课程发布、课程列表、修改状态课程名称、教练、人数上限、已约人数预约管理会员约课、取消约课、查看我的预约会员、课程、预约时间、状态统计看板今日营收、会员总数、到期提醒列表聚合查询上面这个表已经是很多毕设项目的标准答案了。做的时候不用贪多先把这六个模块做扎实比你堆十个没做完的功能更有说服力。2.3 明确角色边界毕设项目一般做两种角色就够了管理员和会员。管理员负责后台维护会员可以登录查看自己的卡和预约记录。如果你时间充足再加一个教练角色教练可以查看自己被预约的课程。但这里要提醒一句角色越多权限管理越复杂前期设计数据库时就要考虑 user 表和 role 表的关联不要做到后期再返工。3. 技术选型与项目亮点3.1 为什么推荐 Spring Boot MyBatis MySQL网上很多老毕设还在用 JSP Servlet JDBC 的结构。不是说它不能做而是它把大量精力耗在写模板代码上一个查询要写 Connection、PreparedStatement、ResultSet还要自己处理异常和关闭连接。今天的 Java 就业市场面试官更期待看到你掌握成熟的框架。我推荐这套组合Spring Boot简化配置内置 Tomcat打 jar 包就能跑适合演示和部署。MyBatisSQL 自己写可控性强。对管理系统这种查询逻辑丰富的项目比 JPA 更好理解也更容易让你讲清楚 SQL 逻辑。MySQL开源、免费、生态成熟网上资料多遇到问题容易找到答案。Thymeleaf服务端渲染页面对于非前后端分离的毕设项目比 Vue 方案更容易在短时间内做完。Bootstrap用现成样式避免写大量 CSS让页面看起来不至于太“原始”。这套技术栈对于毕业设计的作用就一句话用最小的成本把后端业务逻辑完整地展示出来。而且它和现阶段很多 Java 岗位的实际技术栈是接近的练完以后面试可以往深了聊 MyBatis 的缓存、Spring Boot 的自动装配原理。3.2 这个项目的“亮点”该写在哪里很多同学写论文里的“项目创新点”特别痛苦。其实一个项目的亮点不一定是算法多难而是建模是否合理、设计是否分层清晰、代码是否规范。对于健身房会员管理系统合格的亮点可以写在状态流转清晰会员卡有“正常/冻结/已过期”三种状态每次操作都有业务规则限制而不是随意改状态。到期提醒设计不依赖定时任务查询会员卡列表时自动计算剩余天数并标记“即将到期”。事务控制办卡操作同时写会员表和订单表要么全成功要么全失败用 Transactional 控制。搜索体验会员搜索支持姓名和手机号模糊匹配分页展示。这些亮点每一个都是可以量化的写进论文里时也不是空话。你可以在答辩时说“我的办卡功能里如果会员卡已存在且未过期系统会拦截再次办卡的操作。”这就是一个具体的、可测试的业务规则比“系统界面友好”要有说服力得多。4. 数据库设计表结构这样建才规范4.1 先看表数量再看字段健身房会员管理系统核心表我建议设计 7 张左右不要贪多也不要过少表名说明admin_user后台管理员表member会员表card_type会员卡类型表月卡、季卡、年卡member_card会员卡表记录某会员持有的一张卡course课程表coach教练表reservation预约记录表pay_order缴费订单表这是一个很标准的“主表 子表 关联表”结构。最核心的关系是member 和 member_card 是一对多关系会员可以先后持有多张卡member、course 和 reservation 是多对多关系一个成员可以预约多个课程一个课程可以被多个会员预约。4.2 核心建表 SQL下面给出核心表的 MySQL 建表语句。你可以直接复制到 Navicat 或命令行中执行。-- 创建数据库 CREATE DATABASE IF NOT EXISTS gym_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE gym_system; -- 管理员表 CREATE TABLE admin_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 后台管理员表; -- 会员表 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 会员姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, gender VARCHAR(4) DEFAULT 男 COMMENT 性别, age INT COMMENT 年龄, id_card VARCHAR(20) COMMENT 身份证号码, address VARCHAR(200) COMMENT 住址, join_date DATE COMMENT 入会日期, status TINYINT DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_phone (phone) ) COMMENT 会员表; -- 会员卡类型表 CREATE TABLE card_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT 卡名称如年卡、季卡, duration_days INT NOT NULL COMMENT 有效天数如365、90, price DECIMAL(10,2) NOT NULL COMMENT 售价, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) COMMENT 会员卡类型表; -- 会员卡表 CREATE TABLE member_card ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(50) NOT NULL UNIQUE COMMENT 卡号建议唯一, member_id INT NOT NULL COMMENT 所属会员, card_type_id INT NOT NULL COMMENT 卡类型, start_date DATE NOT NULL COMMENT 开卡日期, end_date DATE NOT NULL COMMENT 截止日期, status TINYINT DEFAULT 0 COMMENT 0正常 1冻结 2已过期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_card_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_card_type FOREIGN KEY (card_type_id) REFERENCES card_type(id) ) COMMENT 会员卡表; -- 教练表 CREATE TABLE coach ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 教练姓名, phone VARCHAR(20), specialty VARCHAR(100) COMMENT 擅长方向如增肌、减脂、瑜伽, status TINYINT DEFAULT 1 COMMENT 1在职 0离职 ) COMMENT 教练表; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, coach_id INT COMMENT 授课教练, start_time DATETIME NOT NULL COMMENT 上课时间, max_count INT DEFAULT 20 COMMENT 人数上限, booked_count INT DEFAULT 0 COMMENT 已约人数, status TINYINT DEFAULT 1 COMMENT 1可预约 0已取消, CONSTRAINT fk_course_coach FOREIGN KEY (coach_id) REFERENCES coach(id) ) COMMENT 课程表; -- 预约记录表 CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL COMMENT 预约会员, course_id INT NOT NULL COMMENT 预约课程, reserve_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 预约时间, status TINYINT DEFAULT 0 COMMENT 0已预约 1已取消 2已完成, UNIQUE KEY uk_member_course (member_id, course_id), CONSTRAINT fk_res_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_res_course FOREIGN KEY (course_id) REFERENCES course(id) ) COMMENT 预约记录表; -- 缴费订单表 CREATE TABLE pay_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50) NOT NULL UNIQUE COMMENT 订单号, member_id INT NOT NULL COMMENT 会员, card_id INT COMMENT 关联会员卡, amount DECIMAL(10,2) NOT NULL COMMENT 支付金额, pay_type VARCHAR(20) COMMENT 微信、支付宝、现金, pay_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(200) COMMENT 备注 ) COMMENT 缴费订单表;这段 SQL 有三个值得注意的设计会员卡要和卡类型分开因为同一个会员可能在不同时间购买不同类型的卡避免冗余。预约表加了UNIQUE KEY uk_member_course (member_id, course_id)从数据库层面防止同一个会员重复约同一节课。这是很多新手容易忽略的点。缴费订单表和会员卡表分开办卡和支付记录是独立业务以后查流水时不需要扫描会员卡表。4.3 字段类型和索引的细节手机号字段用VARCHAR(20)而不是BIGINT因为手机号可能包含 86 前缀而且不需要参与数值运算。金额一律用DECIMAL(10,2)不要用FLOAT或DOUBLE否则会出现 0.1 0.2 精度丢失问题。手机号、卡号、订单号这类频繁用于查询的字段加索引。主键统一用自增INT对毕设项目足够如果有大量数据扩展需求可以聊分布式 ID但面试时别提“我记得 order_no 是全局唯一ID”你得真讲得清原理才行。5. 环境准备与项目初始化5.1 需要准备的工具这一部分是多数人卡住的地方。你至少需要装好工具版本建议说明JDK8 或 11不要追求太新版本避免兼容问题Maven3.6项目依赖管理MySQL5.7 或 8.0建议 8.0字符集注意 utf8mb4IntelliJ IDEACommunity 版即可毕设项目不需要收费版功能Navicat 或 MySQL Workbench任意数据库客户端执行 SQL 脚本版本细节以你本机的实际安装情况为准重点不是版本多新而是整个链路能跑通。JDK 8 配 Spring Boot 2.x 是当前兼容性最好的组合之一。如果你本机是 JDK 17可以选 Spring Boot 3.x但要注意 MyBatis 相关的 starter 版本要匹配不然启动时容易出现奇怪的ClassNotFoundException。5.2 创建 Spring Boot 项目在 IDEA 中新建 Spring Initializr 项目填写 Group 为com.exampleArtifact 为gym-system然后添加以下依赖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 !-- MyBatis 整合 Spring Boot -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok减少 getter/setter 代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意mybatis-spring-boot-starter的版本不是 Spring Boot 官方托管的需要自己指定版本号。上面用的是 2.3.1对应 Spring Boot 2.x 场景。如果你使用 Spring Boot 3.x可能需要切换为mybatis-spring-boot-starter3.x 版本。5.3 配置文件 application.yml位置src/main/resources/application.yml。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/gym_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.gym.entity configuration: map-underscore-to-camel-case: true几个容易踩坑的点driver-class-name在 MySQL 8.0 里一定是com.mysql.cj.jdbc.Driver老教程里的com.mysql.jdbc.Driver已经不推荐。serverTimezoneAsia/Shanghai不能省否则数据库时间比本地时间差 8 小时。map-underscore-to-camel-case: true让数据库的start_date自动映射到 Java 的startDate省去大量取别名操作。6. 核心代码实现从 Entity 到 Controller6.1 实体类定义我先以 Member 为例。位置src/main/java/com/example/gym/entity/Member.java。package com.example.gym.entity; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; Data public class Member { private Integer id; private String name; private String phone; private String gender; private Integer age; private String idCard; private String address; private LocalDate joinDate; private Integer status; private LocalDateTime createTime; }这里用了 Lombok 的Data自动生成 getter/setter减少样板代码。如果你不想装 Lombok就自己手动生成 getter 和 setter功能完全一致。再看会员卡实体。位置src/main/java/com/example/gym/entity/MemberCard.java。package com.example.gym.entity; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; Data public class MemberCard { private Integer id; private String cardNo; private Integer memberId; private Integer cardTypeId; private LocalDate startDate; private LocalDate endDate; private Integer status; private LocalDateTime createTime; }6.2 Mapper 接口与 XML位置src/main/java/com/example/gym/mapper/MemberCardMapper.java。package com.example.gym.mapper; import com.example.gym.entity.MemberCard; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; Mapper public interface MemberCardMapper { ListMemberCard findByMemberId(Integer memberId); MemberCard findById(Integer id); int insert(MemberCard memberCard); int update(MemberCard memberCard); // 查询即将到期的会员卡7天内 ListMemberCard findExpiringCards(Param(dateNow) LocalDate dateNow, Param(dateAfter) LocalDate dateAfter); // 查询当前可用的会员卡未过期且状态正常 MemberCard findActiveCardByMemberId(Param(memberId) Integer memberId, Param(today) LocalDate today); }对应的 XML位置src/main/resources/mapper/MemberCardMapper.xml。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.gym.mapper.MemberCardMapper insert idinsert parameterTypecom.example.gym.entity.MemberCard useGeneratedKeystrue keyPropertyid INSERT INTO member_card(card_no, member_id, card_type_id, start_date, end_date, status) VALUES(#{cardNo}, #{memberId}, #{cardTypeId}, #{startDate}, #{endDate}, #{status}) /insert select idfindActiveCardByMemberId resultTypecom.example.gym.entity.MemberCard SELECT * FROM member_card WHERE member_id #{memberId} AND status 0 AND end_date #{today} ORDER BY end_date DESC LIMIT 1 /select select idfindExpiringCards resultTypecom.example.gym.entity.MemberCard SELECT * FROM member_card WHERE end_date BETWEEN #{dateNow} AND #{dateAfter} AND status 0 ORDER BY end_date ASC /select /mapper这个 XML 里有两条很关键的查询语句findActiveCardByMemberId判断某个会员当前是否还有有效的会员卡。约课时会用到。findExpiringCards查到期提醒列表。参数是“今天”和“7天后的日期”中间的时间区间就是即将到期的卡。6.3 Service 层业务规则在这里落地位置src/main/java/com/example/gym/service/MemberCardService.java。package com.example.gym.service; import com.example.gym.entity.MemberCard; import com.example.gym.mapper.MemberCardMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.UUID; Service public class MemberCardService { Autowired private MemberCardMapper memberCardMapper; Transactional(rollbackFor Exception.class) public void openCard(Integer memberId, Integer cardTypeId, Integer durationDays) { // 1. 生成唯一卡号 String cardNo CARD System.currentTimeMillis(); // 2. 计算开卡日期和截止日期 LocalDate startDate LocalDate.now(); LocalDate endDate startDate.plusDays(durationDays); // 3. 插入会员卡记录 MemberCard card new MemberCard(); card.setCardNo(cardNo); card.setMemberId(memberId); card.setCardTypeId(cardTypeId); card.setStartDate(startDate); card.setEndDate(endDate); card.setStatus(0); memberCardMapper.insert(card); } public long getRemainDays(Integer memberId) { MemberCard card memberCardMapper.findActiveCardByMemberId(memberId, LocalDate.now()); if (card null) { return 0L; } return ChronoUnit.DAYS.between(LocalDate.now(), card.getEndDate()); } public ListMemberCard getExpiringCards(int days) { LocalDate today LocalDate.now(); LocalDate after today.plusDays(days); return memberCardMapper.findExpiringCards(today, after); } }这里体现了业务规则的落地方式办卡不是简单 insert 一条记录而是先计算截止日期再生成唯一卡号最后写入。如果中途出现异常Transactional保证数据回滚不会出现“订单创建了但卡没办成功”的脏数据。剩余天数的计算不存字段而是每次实时用ChronoUnit.DAYS.between计算。这样做的好处是你永远不需要担心某个定时任务漏更新导致数据过期坏处是每天第一次查询时会多一次计算但这是毫秒级的对毕设项目完全无所谓。6.4 Controller 层控制请求流转位置src/main/java/com/example/gym/controller/MemberCardController.java。package com.example.gym.controller; import com.example.gym.entity.MemberCard; import com.example.gym.service.MemberCardService; 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.RequestMapping; import java.util.List; Controller RequestMapping(/card) public class MemberCardController { Autowired private MemberCardService memberCardService; GetMapping(/expiring) public String expiringList(Model model) { ListMemberCard expiringCards memberCardService.getExpiringCards(7); model.addAttribute(cards, expiringCards); return card/expiring; } }Controller 层不要写业务逻辑。Controller 的职责是接收参数、调用 Service、把结果放到 Model 里交给页面展示。不要看到别人在 Controller 里直接写 SQL 就跟着学那是代码坏味道。7. 前端页面与交互实现对于毕设项目前端不需要做得多惊艳但至少要干净、可用、看得出页面之间的跳转关系。用 Thymeleaf 实现一个会员列表页面位置src/main/resources/templates/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/bootstrap5.3.0/dist/css/bootstrap.min.css /head body div classcontainer mt-4 h2 classmb-3会员列表/h2 !-- 搜索 新增按钮 -- form th:action{/member/list} methodget classrow g-2 mb-3 div classcol-auto input typetext namekeyword classform-control placeholder输入姓名或手机号 th:value${keyword} /div div classcol-auto button typesubmit classbtn btn-primary搜索/button /div div classcol-auto a th:href{/member/add} classbtn btn-success新增会员/a /div /form !-- 会员数据表格 -- table classtable table-bordered table-hover thead classtable-light tr thID/th th姓名/th th手机号/th th性别/th th入会日期/th th状态/th th操作/th /tr /thead tbody tr th:eachmember : ${members} td th:text${member.id}1/td td th:text${member.name}张三/td td th:text${member.phone}13800138000/td td th:text${member.gender}男/td td th:text${member.joinDate}2024-01-01/td td span classbadge bg-success th:if${member.status 1}正常/span span classbadge bg-secondary th:if${member.status 0}停用/span /td td a th:href{/member/edit/{id}(id${member.id})} classbtn btn-sm btn-warning编辑/a a th:href{/member/delete/{id}(id${member.id})} classbtn btn-sm btn-danger onclickreturn confirm(确定要删除该会员吗)删除/a /td /tr /tbody /table /div /body /html这个页面做了三件事表单用 GET 提交把keyword参数传给后端实现搜索搜索后输入框保留关键字不丢条件。表格行用th:each循环遍历后端传来的会员列表。状态列根据member.status动态渲染不同样式的标签删除操作通过confirm弹窗做二次确认避免误删。页面里没有写任何 Java 逻辑只有数据和标签的绑定关系。这就是 Thymeleaf 的用法页面看得懂后端改起来也方便。8. 系统运行与功能验证代码写完后验证是重头戏。如果只是“点一下运行看到页面出来了”那不算验证通过。我建议按下面这个清单走一遍。8.1 启动项目在 IDEA 中直接运行主类GymApplication控制台出现类似下面的日志Started GymApplication in 3.12 seconds (JVM running for 3.5) Tomcat started on port 8080说明启动成功。如果启动失败看第一行抛出的异常九成是数据库连接或依赖问题。8.2 按业务流程走一遍验证步骤操作预期结果1访问http://localhost:8080/member/list进入会员列表页显示空表格或已有数据2点击“新增会员”填写姓名、手机号等信息并提交保存成功后跳回列表页新会员出现在表格第一行3在搜索框输入刚才的手机号点击搜索表格只显示该会员URL 变为?keywordxxx4点击编辑修改会员地址并提交列表页信息更新5点击删除确认弹窗中选“确定”会员从列表消失6访问http://localhost:8080/card/expiring能看到所有 7 天内到期的会员卡列表如果第 4 步出现问题不要直接改代码先看控制台有没有 SQL 报错。如果第 2 步报 500检查字段名和表单 name 是否一致这是新手最常见的问题。8.3 验证一些“看不到但很重要的逻辑”除了页面操作还要用 Navicat 或命令行验证数据是否正确-- 查一下新插入的会员是否在数据库里 SELECT * FROM member ORDER BY id DESC LIMIT 3; -- 查一下办卡之后会员卡表里有没有正确计算截止日期 SELECT card_no, start_date, end_date, status FROM member_card ORDER BY id DESC LIMIT 3;如果end_date计算错了比如比预计少了 1 天或多了一天回去检查plusDays(durationDays)的参数是从卡类型表传的还是页面直接传的。这一步能帮你发现“页面传参越权”和“日期边界”这两个隐藏问题。9. 常见问题与排查方法以下问题是我在带初学者调试时遇到频次最高的列成表格方便你对照排查。问题现象可能原因排查方式解决方案启动时报Failed to configure a DataSource数据库连接配置缺失或账号密码错误检查application.yml里的url/username/password确认 MySQL 已启动密码和配置一致服务端口 3306 未被占用页面访问报 404Controller 路径和页面模板路径不匹配看控制台日志确认请求映射是否注册检查RequestMapping路径和return member/list对应文件是否存在查询时报Invalid bound statement (not found)Mapper 接口和 XML 没有绑定确认 XML 的 namespace 和接口全限定名一致检查 XML 目录是否在classpath:mapper/*.xml指定范围内页面中文乱码数据库字符集或连接参数不对查看页面编码、数据库表字符集、连接 URL建库时用utf8mb4连接 URL 增加characterEncodingutf8删除会员时外键约束失败会员还有会员卡、订单、预约记录关联查看异常信息中提到的外键表先删除子表记录或改用逻辑删除加 status 字段端口 8080 被占用其他程序占用了 8080执行 netstat -anofindstr 8080 查看 PIDLombok 的 getter/setter 不生效IDEA 未安装 Lombok 插件或未启用注解处理打开 Settings → Plugins 搜索 Lombok安装插件并在 Settings → Build Tools → Compiler → Annotation Processors 开启注解处理这里最需要多说一句的是外键约束问题。很多初学者喜欢“先删主表”结果数据库直接报Cannot delete or update a parent row。毕设项目里我强烈建议对会员这种核心数据用逻辑删除也就是加一个status字段删除时只把status改为 0不实际 delete 记录。这样既保留历史数据又避免外键错误。10. 最佳实践、答辩要点与后续进阶10.1 代码组织规范写管理系统最容易乱的是“Controller 里直接写 SQL、页面里写业务逻辑”。如果你想通过这个项目建立好的工程习惯记住三条分层Controller 只做参数接收和页面跳转Service 只做业务逻辑Mapper 只做 SQL。统一返回值新增、修改、删除后都用redirect重定向避免表单重复提交。查询出错时统一跳转到错误页面或返回提示信息不要直接打印堆栈。校验前置手机号、金额、日期等字段前端做格式校验后端 Service 再校验一次。前端校验是体验后端校验才是安全。10.2 答辩时老师喜欢问什么这部分是很多同学最关心的。根据多年经验毕业论文答辩老师不太会纠结你的页面做得漂不漂亮更关心三件事数据怎么来、业务规则怎么实现、技术选型为什么是这几样。高频问题如下“会员卡到期是怎么实现的”回答思路会员卡表存end_date字段每次查询时实时判断end_date是否小于当天。到期提醒通过findExpiringCards查询 7 天内的记录。不需要定时任务因为数据量小实时计算成本低。“这个系统怎么保证数据一致性”回答思路办卡和写订单在同一个事务方法里用Transactional注解。如果中间步骤失败事务回滚数据库不会出现半成功状态。可以举例子余额足够但写订单失败时会员卡不会新开。“为什么用 MyBatis 而不是 JPA”回答思路管理系统的报表查询和模糊搜索很多MyBatis 用 SQL 直接控制查询逻辑优化和排查更直观。自己也对 SQL 更熟悉希望把数据库基础练扎实。“数据库为什么用单表查询不用视图或者存储过程”回答思路毕设项目数据量不大单表查询配合 Service 层组装即可满足业务需求。视图和存储过程会增加数据库复杂度和维护成本而且不利于代码版本管理。这些问题基本都能从前面写的代码里找到答案。所以务必在答辩前把自己写的核心代码再仔细过一遍特别是 Service 层的方法。10.3 后续怎么进阶如果你在毕设之后还想继续提升不要把它扔到一边而是按照这几个方向扩展加入定时任务用 Spring Schedule 每天凌晨扫描过期会员卡自动把状态改为 2替代现在“查询时实时判断”的方案。加入图形验证码登录模块用 Hutool 或 Google Kaptcha 生成验证码防止脚本暴力登录。改为前后端分离后端只提供 JSON 接口前端用 Vue 3 Element Plus 重写页面把权限控制改成 JWT 或 Spring Security。引入 Redis 缓存把课程预约量、到期提醒等热点数据缓存起来减少数据库压力。上线部署购买一台低配云服务器用 Docker 或者直接把 jar 包部署上去用 Nginx 做反向代理这样简历上可以写“项目已上线”。每一个方向都能对应一个真实项目中的痛点。比如“为什么需要 Redis因为高峰期会员频繁查课程列表数据库有压力”这种经验积累在面试时比背八股文强太多。总结健身房会员管理系统表面上是 CRUD实质上是一整套业务建模和工程规范训练。通过这个项目你可以把需求分析、数据库设计、Spring Boot 项目搭建、MyBatis 映射、Service 事务控制、前端页面渲染和常见问题排查完整地串起来。这些能力无论你以后是做毕设、进公司实习还是准备 Java 面试都是通用的底层能力。如果你准备拿这个题目做毕设我的建议是先把本文的数据库建好再把会员管理模块完整跑通然后逐步补上会员卡、课程预约和统计报表。每做完一个模块都试着把代码讲给身边同学听——讲得清楚才算真的会了。代码不是背出来的是会“用”的。希望这篇文章能帮你迈过那道坎。