☰
SSM+JSP+MySQL健身俱乐部网站毕业设计实战:从环境搭建到部署排错
2026/10/8 16:10:17 网站建设 项目流程

简介:本资源为基于SSM+JSP+MySQL的健身俱乐部网站毕业设计完整项目包,面向计算机相关专业正在做毕设的学生及需要Java项目实战练习的学习者,也可用于课程设计与期末大作业。项目已通过导师指导并高分通过,可直接作为毕设使用。压缩包约56.45MB,内含项目源码、数据库脚本、开发说明文档、LW、演示视频及代码注释等,源码与文档相互配套,便于理解整体架构与业务逻辑。系统分为管理员与用户两大模块,管理员可添加课程种类、教练、课程、器材及教室信息,用户则可浏览网站、安排课程、办卡与选课,功能覆盖健身俱乐部日常运营的主要环节。开发环境采用JDK1.8、Tomcat7+、MySQL5.7+、IDEA与Maven3.3.9+,项目经过严格调试,确保可运行。目前已有146人学习,适合需要完整赛题方案、排错思路与可运行代码的读者参考。

1. 健身俱乐部网站从零到跑通:SSM+JSP+MySQL 这套老组合为什么还在被反复选

健身俱乐部网站这类题目,看着像“随便做做就能过”的毕业设计,真动手才知道坑不少:会员卡到期怎么算、课程预约怎么防重复、教练排班和前台收银怎么串起来,每一个都能把数据库表结构推翻重来。SSM(Spring + SpringMVC + MyBatis)加 JSP、MySQL 这套组合,是 JavaWeb 方向最经典的落地路径,也是很多高校毕业设计选题里出现频率最高的技术栈之一。它不新,但胜在资料全、结构清晰、答辩时老师听得懂。这篇文章面向正在做 Java 毕业设计、需要一套能跑通、能讲清、能改得动的健身俱乐部网站的同学,把从环境搭建、表结构设计、核心功能实现到部署排错的完整链路拆开讲。你拿到的不只是一份能交差的代码思路,而是一套遇到报错能自己定位的排查习惯。

2. 环境与工程骨架:把 SSM+JSP+MySQL 的依赖关系一次理顺

2.1 版本选型:为什么我建议 JDK 8 + MySQL 5.7 而不是追新

做毕业设计最怕的不是功能难,是环境装到一半卡住。SSM 这套东西对版本比较敏感,尤其是 Spring 和 MyBatis 的整合包,版本错一位就是NoSuchMethodError或者ClassNotFoundException。我一般会固定一套经过大量项目验证的组合:JDK 8、Tomcat 8.5、MySQL 5.7、Spring 5.2.x、MyBatis 3.5.x、Druid 1.2.x。JDK 8 和 Tomcat 8.5 的兼容性最稳,MySQL 5.7 在 Windows 上的安装教程遍地都是,遇到问题搜得到答案。MySQL 8.0 也能用,但驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,连接串还要加时区参数,新手容易在这里翻车。如果你的学校要求必须用 MySQL 8.0,那就把驱动换成mysql-connector-java 8.0.x,URL 写成jdbc:mysql://localhost:3306/gym?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,其余不变。

2.2 Maven 依赖清单:pom.xml 里必须锁死的几个坐标

工程用 Maven 管理依赖,pom.xml是整个项目的黑匣子,配错了后面全是玄学问题。下面这份依赖清单是我在多个 SSM 项目里沉淀下来的,直接抄,注意版本号不要随意升级。

<!-- pom.xml 核心依赖,版本锁定,不要随意升级 --> <properties> <spring.version>5.2.8.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> </properties> <dependencies> <!-- Spring 核心:IoC 容器和 AOP 支持 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- SpringMVC:处理 web 层请求映射 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring JDBC:事务管理依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 核心 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <!-- MyBatis 与 Spring 整合包,缺这个 SqlSessionFactory 注入不进来 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.5</version> </dependency> <!-- MySQL 驱动,5.7 用这个版本 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- Druid 连接池,比 DBCP 稳定,监控页面也好用 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency> <!-- JSTL 标签库,JSP 页面循环和判断全靠它 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- Servlet API,scope 必须是 provided,否则和 Tomcat 冲突 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>

这段配置里最容易出问题的是mybatis-spring的版本,它和 MyBatis 主包版本有对应关系,2.0.5 配 3.5.6 是稳的。servlet-api的 scope 写成provided是因为 Tomcat 自带这个包,如果打成compile会报类冲突。Druid 连接池的监控 Servlet 在 web.xml 里注册后可以访问/druid/index.html看 SQL 执行情况,调优阶段很有用。

2.3 配置文件三件套:jdbc.properties、spring-mybatis.xml、spring-mvc.xml

SSM 整合的核心就是把三个配置文件写对。jdbc.properties放数据库连接信息,spring-mybatis.xml管数据源和事务,spring-mvc.xml管 Controller 扫描和视图解析。分开写的好处是改数据库密码不用动逻辑配置。

# jdbc.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/gym?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=你的密码
<!-- spring-mybatis.xml 关键片段 --> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 连接池参数:初始 5,最大 20,毕业设计够用 --> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <!-- 扫描 Mapper 接口,生成代理对象 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.gym.mapper"/> </bean> <!-- 事务管理器,Service 层加 @Transactional 生效 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

MapperScannerConfigurer的basePackage必须和你的 Mapper 接口包路径一致,写错了启动时报No qualifying bean。事务管理器的dataSource引用要和上面数据源的 id 对上。spring-mvc.xml里配<mvc:annotation-driven/>和视图解析器前缀后缀,Controller 返回"member/list"就会去找/WEB-INF/jsp/member/list.jsp。

3. 数据库设计:会员、课程、预约三张核心表怎么定字段

3.1 表结构设计:从业务动作倒推字段,而不是先建表再想功能

健身俱乐部的核心业务动作有三个:办卡入会、预约课程、签到核销。表结构要从这三个动作倒推。会员表member存基本信息和卡状态,课程表course存课程信息和教练,预约表reservation存预约记录。下面给出建表 SQL,字段类型和索引都经过实际项目验证。

-- 会员表:卡状态用 tinyint 枚举,0 未激活 1 正常 2 过期 3 冻结 CREATE TABLE `member` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL COMMENT '会员姓名', `phone` varchar(16) NOT NULL COMMENT '手机号,登录账号', `password` varchar(64) NOT NULL COMMENT 'MD5 加密存储', `card_type` tinyint(2) DEFAULT '1' COMMENT '1月卡 2季卡 3年卡', `card_status` tinyint(2) DEFAULT '0' COMMENT '0未激活 1正常 2过期 3冻结', `expire_date` date DEFAULT NULL COMMENT '到期日期', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 课程表:教练名先冗余存着,避免多表联查拖慢列表页 CREATE TABLE `course` ( `id` int(11) NOT NULL AUTO_INCREMENT, `course_name` varchar(64) NOT NULL, `coach_name` varchar(32) NOT NULL, `course_time` datetime NOT NULL COMMENT '开课时间', `capacity` int(11) DEFAULT '20' COMMENT '容量上限', `booked` int(11) DEFAULT '0' COMMENT '已预约人数', PRIMARY KEY (`id`), KEY `idx_course_time` (`course_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 预约表:member_id + course_id 建唯一索引,防止重复预约 CREATE TABLE `reservation` ( `id` int(11) NOT NULL AUTO_INCREMENT, `member_id` int(11) NOT NULL, `course_id` int(11) NOT NULL, `status` tinyint(2) DEFAULT '1' COMMENT '1已预约 2已签到 3已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_member_course` (`member_id`,`course_id`), KEY `idx_member` (`member_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

member表的phone加了唯一索引,注册时重复插入会直接报错,比在代码里先查再插更可靠。reservation表的uk_member_course唯一索引是防重复预约的关键,同一个会员对同一节课只能有一条记录,取消后把status改成 3,再次预约时用INSERT ... ON DUPLICATE KEY UPDATE更新状态即可。course表的booked字段是冗余计数,每次预约成功加一,取消减一,用事务包住,避免并发时超卖。

3.2 预约防超卖:用 SQL 原子更新代替先查后改

课程预约最典型的坑是超卖:两个会员同时看到还剩 1 个名额,都点预约,结果两个都成功。根因是“先查库存再更新”不是原子操作。正确做法是把判断和更新合并到一条 SQL 里。

-- 原子更新:只有 booked < capacity 时才加一,返回受影响行数 UPDATE course SET booked = booked + 1 WHERE id = #{courseId} AND booked < capacity;

Mapper 接口里返回int,Service 层判断返回值:等于 1 说明抢到名额,等于 0 说明已满,直接抛业务异常提示“该课程已约满”。这条 SQL 依赖 InnoDB 的行锁,WHERE条件命中主键,锁粒度小,并发下不会互相阻塞太久。注意booked字段不要用int之外的浮点类型,避免精度问题。预约成功后插入reservation记录,两步操作放在同一个@Transactional方法里,任何一步失败都回滚。

3.3 卡到期判断:日期比较放在 SQL 里还是 Java 里

会员卡是否过期,判断逻辑很简单:expire_date < 当前日期就是过期。但放在哪里判断有讲究。放在 Java 里需要每次查出来再比较,列表页几十条记录就要循环几十次。放在 SQL 里用WHERE expire_date >= CURDATE()直接过滤有效会员,数据库层面完成,效率更高。如果要做定时任务批量更新卡状态,写一条UPDATE member SET card_status = 2 WHERE expire_date < CURDATE() AND card_status = 1,配合 Spring 的@Scheduled每天凌晨跑一次。注意CURDATE()返回的是数据库服务器日期,如果数据库和 Tomcat 不在同一台机器,要确认时区一致,否则会出现“明明没过期却显示过期”的玄学问题。

4. 核心功能落地:JSP 页面、Controller 和 MyBatis 映射怎么写

4.1 会员登录与拦截器:Session 存什么、拦截器放什么

登录功能是入口,JSP 表单提交到 Controller,查库比对密码(MD5 加密后比对),成功则把会员对象放进HttpSession。拦截器负责拦住未登录访问,但要注意放行登录页、注册页和静态资源。

// LoginInterceptor.java 关键逻辑 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 放行登录、注册、静态资源 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/register") || uri.contains("/static/")) { return true; } // 未登录跳转登录页 if (session.getAttribute("member") == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; }

拦截器在spring-mvc.xml里注册,<mvc:interceptor>配mapping为/**,exclude-mapping排除登录注册路径。Session 里只存会员 id 和姓名,不要存整个对象,避免序列化问题。密码用 MD5 加盐存储,虽然 MD5 现在不安全,但毕业设计层面够用,答辩时能说清“生产环境应换 BCrypt”反而是加分项。

4.2 课程列表分页:PageHelper 一行代码搞定,但参数要配对

课程列表数据量大,必须分页。MyBatis 生态里 PageHelper 是最省事的方案,但版本和 MyBatis 要对上,5.2.x 配 MyBatis 3.5.x 没问题。

// CourseServiceImpl.java 分页查询 public PageInfo<Course> listCourses(int pageNum, int pageSize) { // 这行代码必须紧跟在查询方法前,中间不能插其他数据库操作 PageHelper.startPage(pageNum, pageSize); List<Course> list = courseMapper.selectAll(); return new PageInfo<>(list); }

PageHelper.startPage只对紧接着的第一条查询生效,如果中间插了别的查询,分页会作用到错误的 SQL 上,这是最常见的翻车点。PageInfo对象封装了总页数、总记录数、当前页数据,JSP 页面用 JSTL 循环渲染列表,分页条用pageInfo.pageNum和pageInfo.pages控制。JSP 里遍历用<c:forEach items="${pageInfo.list}" var="course">,注意 EL 表达式的大小写,pageInfo要和 Controller 里model.addAttribute("pageInfo", pageInfo)的 key 一致。

4.3 预约功能前后端配合:表单提交、事务控制和结果反馈

预约按钮在课程列表页,点击后提交courseId到 Controller。Controller 从 Session 取memberId,调用 Service 的预约方法,返回结果用 JSON 给前端提示。

// ReservationController.java 预约接口 @RequestMapping("/reserve") @ResponseBody public Map<String, Object> reserve(Integer courseId, HttpSession session) { Map<String, Object> result = new HashMap<>(); Member member = (Member) session.getAttribute("member"); if (member == null) { result.put("code", 401); result.put("msg", "请先登录"); return result; } try { reservationService.reserve(member.getId(), courseId); result.put("code", 200); result.put("msg", "预约成功"); } catch (BusinessException e) { result.put("code", 500); result.put("msg", e.getMessage()); } return result; }

Service 层的reserve方法加@Transactional,先执行原子更新课程名额,返回 0 则抛BusinessException("课程已约满"),返回 1 则插入预约记录。前端用 jQuery 的$.post提交,根据code判断成功失败,成功则刷新页面或更新按钮状态。注意@ResponseBody要配合 Jackson 依赖,pom.xml里加jackson-databind,否则返回 JSON 会报 406 错误。

5. 避坑与排查:SSM+JSP 项目里最容易翻车的 5 个地方

5.1 中文乱码:GET 和 POST 要分开治

现象:表单提交中文变成问号,或者数据库里存进去是乱码。原因:JSP 页面编码、Tomcat 编码、数据库编码三者不一致。解决:JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>,web.xml 里配CharacterEncodingFilter拦截所有请求设 UTF-8,数据库连接串加characterEncoding=utf8,建表时指定DEFAULT CHARSET=utf8。GET 请求的中文乱码还要改 Tomcat 的server.xml,在 Connector 标签加URIEncoding="UTF-8"。四个地方都对齐,乱码问题基本绝迹。

5.2 Mapper 接口找不到:扫描路径和命名空间对不上

现象:启动报Invalid bound statement (not found)。原因:MyBatis 的 Mapper XML 文件位置和MapperScannerConfigurer扫描的包不一致,或者 XML 里的namespace写错了。解决:确认spring-mybatis.xml里basePackage是 Mapper 接口所在包,XML 文件放在resources/mapper/下且namespace是全限定接口名。如果 XML 和接口同名同包,Maven 默认不打包 XML,需要在pom.xml的build里加resources配置把**/*.xml包含进去。

5.3 事务不生效:方法内部调用是元凶

现象:预约失败后课程名额没有回滚,数据不一致。原因:@Transactional基于代理,同一个类里方法 A 调用方法 B,B 上的事务注解不生效。解决:把事务方法抽到单独的 Service 类里,或者用AopContext.currentProxy()获取代理对象再调用。更简单的做法是确保事务方法只被外部类调用,Controller 直接调 Service 的事务方法,不要在 Service 内部自调用。

5.4 JSP 页面 EL 表达式不解析:isELIgnored 惹的祸

现象:页面上直接显示${member.name}而不是具体值。原因:JSP 2.0 以上默认解析 EL,但如果 web.xml 的web-app版本声明低于 2.4,或者页面isELIgnored="true",EL 就不解析。解决:web.xml 用 Servlet 3.0 的 schema,页面不要设isELIgnored,JSTL 标签库的 uri 写http://java.sun.com/jsp/jstl/core。如果用了 JSTL 但没引依赖,会报Unable to find taglib,检查pom.xml里jstl和standard两个包是否都在。

5.5 连接池耗尽:连接没关还是泄漏了

现象:运行一段时间后报GetConnectionTimeoutException。原因:Druid 连接池最大连接数被占满,通常是代码里手动获取 Connection 没关闭,或者事务方法里做了远程调用导致连接持有时间过长。解决:检查所有dataSource.getConnection()的地方,确保finally块里close()。用 Druid 监控页面看活跃连接数,如果持续增长不下降,就是泄漏。毕业设计层面把maxActive设成 20,maxWait设成 3000 毫秒,超时快速失败比一直等更好排查。

6. 从能跑到能答辩:三个验证技巧和一套改需求的习惯

项目跑通只是第一步,答辩时老师会问“你怎么保证数据一致性”“并发下会不会超卖”“密码为什么这么存”。这些问题的答案不在代码里,在你对边界的理解里。第一个验证技巧:用 JMeter 开 50 个线程同时预约同一节课,看booked是否超过capacity,如果没超过,说明原子更新生效了。第二个验证技巧:手动把系统时间调到会员卡到期日之后,看列表页是否还显示“正常”,如果显示“过期”,说明 SQL 里的日期比较生效了。第三个验证技巧:在 Service 的事务方法里手动抛异常,看数据库里的booked是否回滚,如果回滚了,说明@Transactional配置正确。

改需求是毕业设计常态,导师说“加个教练管理模块”,你不要推翻重来。SSM 的好处是分层清晰,加模块就是加一张表、一个 Mapper、一个 Service、一个 Controller、几个 JSP。照着course模块复制一遍,改字段名和页面文案,半天就能跑通。我自己的习惯是每加一个功能,先在数据库里把表建好,写一条测试 SQL 确认能查能改,再写 Mapper 和 Service,最后写 JSP。顺序反了,页面写完了发现字段不够,返工更痛苦。还有一点血泪经验:每次改完pom.xml先跑mvn clean install,确认依赖没冲突再启动 Tomcat,能省下大量“明明代码没错却报 ClassNotFound”的排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询