简介:一套基于SSM(Spring+SpringMVC+MyBatis)框架与JSP、MySQL开发的羽毛球交流平台毕业设计项目,主要面向计算机相关专业正在准备毕业设计的学生、需要课程设计或期末大作业的Java学习者,以及希望借助完整项目提升SSM开发能力的初学者。系统分为用户和管理员两类角色:前台用户可注册登录、浏览网站首页与站内新闻、按编号或名称查询场地并预约、发表评论和在线留言、查看系统简介,还能在个人后台修改资料、查看或删除预约信息;后台管理员可对系统用户与业务信息进行管理。项目经过严格调试,附带项目源码、数据库脚本、开发说明文档与常用软件工具,可直接作为毕设或课设使用。资源为约48.8MB的zip压缩包,其中以Java源码、SQL脚本与Word文档为主,文件总数暂未标注;目前已有百余人学习下载,是掌握SSM整合、MVC分层结构与预约类业务开发的实用参考。
1. 基于SSM+JSP+MySQL的羽毛球交流平台:毕业设计到底能拿来干什么
每年到了毕设季,SSM(Spring + Spring MVC + MyBatis)配 JSP 的项目就会霸占各大资源站的热搜。这套组合确实老,但架不住它“稳”——学校教材讲它、答辩老师熟它、网上资料多到查什么都有人踩过坑。所以我才推荐你在做选题时优先看这类“看起来传统”的 Java Web 项目。这份基于 SSM + JSP + MySQL 的羽毛球交流平台,属于典型的信息管理加互动交流二合一系统,前台做球场动态、活动发起与约球信息展示,后台做用户、场地和帖子管理。拿到之后你不是面对一个空壳 demo,而是源码、数据库脚本、部署文档都齐全的一个可跑项目,你只需要在本地把环境匹配上,它就能完整地转起来。适合的人群很明确:Java 基础薄弱的应届生、想短时间把 SSM 流程彻底过一遍的自学者、以及需要快速拿出一套“能演示、有逻辑”的毕设项目去参加答辩的人。
这个平台虽然业务不复杂,但它的技术栈覆盖面恰好踩中 Java Web 面试和课程考察的核心点——Spring 的 IoC 与事务管理、Spring MVC 的请求映射与参数绑定、MyBatis 的动态 SQL 和结果映射、JSP 与 JSTL 的页面渲染、MySQL 的表关系设计与简单 SQL 优化,全都能在讲解或者答辩时对上号。如果你正在为毕设题目犹豫,这篇笔记值得你花十分钟看完。我拆过不少同类型项目,会先给你一个整体的系统结构与模块划分,然后一步一步带你走通 Tomcat 部署和 IDEA 导入流程,再把核心代码从前台到后台捋一遍,最后把我在拆解过程中遇到的坑一次性列出来。中间还藏了一个对答辩特别有用的技巧——怎么不用改数据库结构就给平台加一个“分类检索”字段,这个后面细说。
2. 系统模块与数据库设计:拿到项目先看清这张表关系图
2.1 项目目录结构与模块职责
解压之后先用 IDEA 以 Maven 项目的方式打开根目录。一个标准的 SSM 项目结构在 src/main/java 下面会按 controller、service、mapper、entity、config 这几层来分包,resources 下放着 jdbc.properties、spring 和 mybatis 的 XML 配置,webapp 目录里才是 JSP 页面。模块划分围绕“羽毛球”这个主题,核心是这些:用户模块负责注册、登录、个人资料维护以及头像上传;约球模块能够发布打球邀请、查询附近的球友和回复约球帖;场地信息模块提供场地列表、场地详情、管理员后台增删改查;活动模块管理线下羽毛球活动,用户可以报名和取消;留言评论模块给帖子或者场地写评论,后台还能把违规评论直接删除。看明白这几块,答辩时被问到“那你这个系统的核心功能是什么”就非常清晰了。
2.2 数据库表结构拆解:从 SQL 脚本反推业务关系
数据库脚本一般是 sql 目录下的ball_platform.sql,导入到 MySQL 后你会发现表设计遵循了比较经典的范式。其中user表字段建议重点关注u_id主键、u_username唯一索引、u_pwd加密存储以及u_role角色字段;badminton_court表里设置有场地名称、开放时间、每小时价格和状态字段;activity_info表包含活动主题、活动时间、活动地点、最大人数与当前报名人数;post表记录帖子标题、正文内容、发帖人外键以及回帖数。这几张表之间的外键关系,本质上是业务上的数据约束,比如评论表要关联post_id和user_id,报名记录要关联activity_id和user_id。打开 SQL 文件,把每个外键字段找出来,逐条理解为什么要关联,答辩讲数据库设计时直接说“我是通过外键和索引来处理数据一致性与查询效率的”,比背概念有用得多。
2.3 为什么 SSM 加 JSP 在毕设阶段反而最稳妥
很多同学看不上 JSP,觉得太老。但你如果只求“项目能跑、答辩能过、代码讲得清楚”,JSP 反而是加分项。Spring MVC 的视图解析器把 Controller 返回的逻辑视图名拼上/WEB-INF/views/前缀和.jsp后缀,页面里再通过 JSTL 的c:forEach循环输出数据。这种写法让大家一眼就能看懂前端是怎么拿到后台数据的,也方便你做局部调整。Spring 的 Service 层通过@Transactional注解对约球报名这种写操作做事务管理,MyBatis 的 XML 中则直接手写 SQL。这个组合的一个最大优点:每一层边界都特别清晰,Controller 里不写业务逻辑,Service 只管事务,Mapper 只做 SQL 映射。你在讲解项目时,就可以把代码分层当作展示点来展开,整个过程不需要额外引入过于偏门的框架。
2.4 Maven 依赖版本与 JDK/Tomcat 的匹配
这是一个最容易被忽略、也是翻车率极高的地方。pom.xml里的 Spring 若使用 5.x,那么要求 JDK 8 或以上;MyBatis 3.5.x 需要和 mybatis-spring 2.x 搭配,否则会出现org.mybatis.spring.SqlSessionFactoryBean的 NoSuchMethodError 一类的错误。JSP 与 Servlet 依赖则必须与内嵌 Tomcat 版本对应,比如 Tomcat 9 要使用javax.servlet-api4.x,Tomcat 10 则要求jakarta.servlet-api5.x,混搭就会启动报错。建议项目中依赖一个最新的 spring-webmvc 5.3.x、mybatis 3.5.x、mysql-connector-java 8.0.x 的组合,再配一个 Tomcat 9.0。这种组合属于最不容易出问题的稳妥搭配,我下面做部署演示时也以它为基准。
3. 搭建运行环境与导入部署:从零到能跑通仅需这几步
3.1 本地环境准备清单
动手之前,先把环境对齐。JDK 安装 1.8 及以上版本,IDEA 使用 Ultimate 版本因为社区版对 Tomcat 集成支持比较弱,MySQL 安装 5.7 或 8.0 版本,Navicat 或 MySQL Workbench 任选其一用来导入 SQL 数据。Tomcat 单独再下载一个 9.0 版本,部署时尽量使用本地 Tomcat 而不要靠 IDEA 内置的 Tomcat,因为内置实例对 JSP 的兼容性有时候会产生很多额外问题。环境变量方面,JAVA_HOME需要用 JDK 安装根目录,CATALINA_HOME指定到 Tomcat 根目录,Path里把这两个的 bin 目录也加上。
3.2 数据库初始化与连接配置修改步骤
打开 Navicat,新建一个名为ball_platform的数据库,字符集选择utf8mb4排序规则选utf8mb4_general_ci,然后运行 SQL 脚本完成导入。修改jdbc.properties里的数据库账号和密码时,有一点值得特别留意:如果你本机 MySQL 是 8.x,驱动地址必须写成com.mysql.cj.jdbc.Driver,URL 里面还要增加useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true,否则连接阶段会报时区错误和 SSL 错误。修改完成后的配置大概长这样:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ball_platform?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的数据库密码这里值得多说几句:characterEncoding=utf8解决页面中文乱码问题,serverTimezone解决 MySQL 8.x 与 JDBC 驱动之间的时区不一致。如果在导入脚本时报错,检查 SQL 文件开头有没有CREATE DATABASE语句,有的话建议直接删掉,只在 Navicat 里手动建库并选中后再导入,我碰到过很多次脚本自带的库名和实际不一致导致导入中断的情况。
3.3 IDEA 中配置 Tomcat 并启动项目的完整过程
在 IDEA 里打开项目后,先等待 Maven 依赖全部下载完成。然后在 Run Configuration 里新增 Tomcat Server Local,在 Deployment 页签中添加war exploded类型的 artifact,Application context 填写/ball。添加完成后启动 Tomcat,控制台出现 “Connected to server” 和 Spring 容器加载日志就说明启动成功。接着浏览器访问http://localhost:8080/ball就能看到系统首页。值得提醒的是,每次修改 Java 代码重新编译后,IDEA 有时候不会自动把新 class 同步到 Tomcat 的部署目录,这时手动重启一次 Tomcat 就能解决,千万不要去怀疑代码逻辑出了问题。
3.4 前端首页运行验证与三种常见启动产物核查
首页加载后,第一件事看页面上的 CSS 和图片是否正常。SSM 项目里静态资源由 Spring MVC 的<mvc:resources>标签放行,如果样式错乱,多半是 web.xml 的DispatcherServlet拦截了.css或.js请求。此时打开浏览器 F12 开发者工具,观察网络请求中是否有 404 或 403 的静态资源请求,锁定后再去spring-mvc.xml中检查mapping配置是否写到了resources/static路径。启动过程中控制台如果出现“ClassNotFound”字样的异常,逐条核对 Maven 依赖是否已完整下载;如果出现 “Table 'ball_platform.user' doesn't exist”,则是数据库没有导入正确,去数据库客户端执行一次全量 SQL 脚本就好。这套检查思路适用于绝大多数 SSM 项目的启动排错。
4. 核心业务代码实现剖析:从“约球”这个主流程看透 SSM
4.1 用户登录与权限拦截器的工作机制
平台的登录逻辑可以当成一个标准模板来学。用户提交账号密码后,Controller 调用 Service 层完成校验,成功后把user对象塞进 session。“未登录不能访问某些页面”这类需求通过 Spring MVC 拦截器HandlerInterceptor实现。它的核心思想是:重写preHandle方法读取 session,判断是否为 null,把不需要拦截的 URL(比如登录接口、首页、静态资源)做成白名单列表,剩余请求全部拦截,未登录用户直接重定向到登录页。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }这段代码的核心价值在于前两行:从 session 取出登录态对象再判断。第一处容易犯的错是把request.getSession()写成了request.getSession(true),前者拿不到现成 session 会创建新对象,这会导致拦截器永远认为用户未登录。拦截器写好后还需要在spring-mvc.xml里注册,并配置<mvc:exclude-mappings>指定放行路径,注册方式我习惯这样写:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/register"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.platform.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>4.2 约球帖发布与 MyBatis 动态 SQL 拼装
约球模块的查询条件通常比较多:按球馆、按时间、按水平等级,这时 MyBatis 动态 SQL 就派上大用场。<where>标签会自动处理掉第一个多出来的AND,这一招在实现模糊搜索时非常实用。我以一个典型查询为例:
<select id="selectPostList" parameterType="map" resultType="com.platform.entity.Post"> SELECT * FROM post <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="courtId != null"> AND court_id = #{courtId} </if> <if test="level != null and level != ''"> AND level = #{level} </if> </where> ORDER BY create_time DESC </select><where>在这里的价值很明显:如果所有条件都为空,生成的 SQL 就是SELECT * FROM post ORDER BY create_time DESC,不会出现语法错误。传参使用Map的好处是,页面表单增加筛选条件时,Service 层只需要往 Map 里多塞一个键值即可,不需要频繁修改 Mapper 接口方法签名。
4.3 JSP 页面列表渲染与分页:从 JSTL 到 PageHelper 的取舍
列表页我建议直接用 JSTL 渲染核心信息。这段代码在 JSP 页面中的实际写法是:
<c:forEach items="${postList}" var="post"> <div class="post-item"> <h3>${post.title}</h3> <p>发帖人:${post.username} | 时间:<fmt:formatDate value="${post.createTime}" pattern="yyyy-MM-dd HH:mm"/></p> <p>${post.content}</p> </div> </c:forEach>其中fmt:formatDate是 JSTL 的格式化标签,必须保证 controller 存入 Model 的createTime是java.util.Date类型,字符串类型日期无法被格式化,这一点要格外注意。分页方面,如果项目中已经集成了 PageHelper,只要在 Service 层调用前加一行PageHelper.startPage(pageNum, pageSize),MyBatis 会自动拦截下一条查询 SQL 并生成 limit 语句。若不想引入插件,也可以在 MySQL 查询中直接用LIMIT #{offset}, #{pageSize}手动拼分页,但需要特别注意offset和pageSize的数值类型,如果传入的是字符串,SQL 执行时会产生隐式转换问题导致查询结果偏差。
4.4 MySQL 索引与外键使用上的误区
数据库脚本里,badminton_court表的court_name建议加一个普通索引,约球人搜索场地时基本都会按名字模糊查询;post表里的user_id建议和create_time建立一个联合索引,因为列表页的常见查询是“找某个人的帖子并按时间排序”。这里有个容易犯的误区——并不是外键就一定要建索引,也并不是索引越多越好。一张表超过六七个索引后,写入性能反而会明显下降。MyISAM 与 InnoDB 在毕设阶段不需要深入区分,但在外键约束上,InnoDB 支持真正的外键约束,MyISAM 不支持。如果 SQL 里出现FOREIGN KEY相关报错,第一时间确认表引擎是不是 InnoDB。
5. 实战踩坑与排查清单:环境、中文乱码与依赖冲突的解决实录
5.1 修改代码后页面没有变化
现象:在 JSP 或 Java 文件里改了内容,重启 Tomcat 后浏览器看到的还是老页面。 原因:IDEA 的 Tomcat 部署方式若是war,每次都会重新打整包,比较慢;若是war exploded,又可能出现编译输出目录与实际部署路径不同步的情况。 解决:我个人的做法是,配置 Deployment 时直接选择war exploded,并在 Server 页签里把 “On frame deactivation” 设置为Update classes and resources。改了代码后只需要按Ctrl + F10或者从 Build 菜单执行 Recompile,Tomcat 会自动同步新 class。学会这个小技巧后,你根本不需要反复重启整个 Tomcat。
5.2 数据库中文乱码问题
现象:页面显示正常,但往数据库插入中文后变成???问号,或者后台读出数据后再显示到页面上变成乱码。 原因:多半是连接 URL 没有指定characterEncoding=utf8,或者数据库本身的字符集是 latin1。 解决:先把 MySQL 客户端的show variables like 'char%'看一下,确保character_set_database为utf8mb4。然后核对jdbc.properties里的 URL 是否已带上useUnicode=true&characterEncoding=utf8。最后再排查 JSP 页面第一行是否写了<%@ page contentType="text/html;charset=UTF-8" language="java" %>,三个环节都对齐之后乱码问题才可能彻底解决,这一步能过滤掉绝大多数乱码情况。
5.3 Maven 依赖下载不下来或 jar 包冲突
现象:pom.xml中引入的依赖在 IDEA 的 Maven 面板里打红叉,或者启动时出现NoClassDefFoundError、AbstractMethodError。 原因:第一个可能是本地 Maven 仓库里缓存了不完整的 jar,或者私服地址不通。第二个通常是 jar 版本冲突,常见于 spring-web 与 spring-webmvc 小版本不一致,或 mybatis-spring 和 mybatis 版本不匹配。 解决:优先在 IDEA 的 Maven 设置里检查本地仓库路径,手动删除repository下对应文件夹后重新导入。版本冲突上,最快的方法是使用mvn dependency:tree命令查看依赖树,找到冲突的 jar 后利用exclusion标签把不需要的传递依赖排除掉。启动项目时如果出现AbstractMethodError,直接升级 mybatis-spring 到 2.0.6 以上版本,我这个组合里基本能规避这一类问题。
5.4 Tomcat 启动端口被占用
现象:启动报错提示Port 8080 was already in use。 原因:后台有残留的 java 进程,或者本机开了其他 Web 服务占用了 8080。 解决:Windows 下用netstat -ano | findstr 8080找到占用进程的 PID,再到任务管理器结束它。如果你不想结束进程,也可以在 Tomcat 的server.xml里把 Connector port 改成 8081,然后浏览器访问http://localhost:8081/ball,效果完全一样,这套操作通常马上就能解决。
5.5 IDEA 运行时突然报 “Consider renaming one of the beans or enabling overriding”
现象:Spring 容器启动到一半报错,提示有两个bean名称冲突,通常是 service 实现类或 mapper 出现重复定义。 原因:spring-mvc.xml和applicationContext.xml扫描同一个包,导致同一个 Service 被注册了两次。 解决:把两个配置文件的context:component-scan扫描范围分开。Spring 根容器只扫描service和dao包,Spring MVC 子容器只扫描controller包,重叠部分处理干净之后基本不会再冒出这个异常。
6. 扩展思路:给平台加上场地“热门程度”筛选
很多人的毕设拿到手里,展示了几个基本功能就交差了,答辩老师一问“你做过哪些优化拓展”就无话可说。这里分享一个成本极低、但答辩很好讲的小功能:给场地列表页加“热门程度”排序字段,按照每个场地下的约球活动数量进行排序。这个方法最大的价值在于,你的数据表完全不需要改动,只需要一个多表联查的 SQL 加一个 JSP 页面的排序链接就可以实现。
先在BadmintonCourtMapper.xml中新增一个查询方法,通过左连接activity_info表按场地分组统计活动数量,用于计算热度:
<select id="selectCourtListWithHot" resultType="map"> SELECT c.*, COUNT(a.act_id) AS activity_count FROM badminton_court c LEFT JOIN activity_info a ON c.court_id = a.court_id WHERE c.status = 1 GROUP BY c.court_id ORDER BY activity_count DESC, c.court_id ASC </select>这段 SQL 的关键点有两个。LEFT JOIN保证没有活动的场地依然会出现在列表里,活动数为 0,不会出现“无活动就没场地”的数据缺口;GROUP BY后的ORDER BY activity_count DESC则把活动数量最多的场地排在最前面。如果后续想加入时间条件只统计最近一个月的活动量,可以在WHERE里加上a.start_time > DATE_SUB(NOW(), INTERVAL 1 MONTH),但注意这时WHERE条件会过滤掉没有活动的场地,需要把LEFT JOIN改成LEFT JOIN ... AND a.start_time > ...才能保持所有场地都显示。这里也解释一个小坑:最低价的排序和这种“自由热度排序”没法共用一条 SQL,因为排序字段的优先级不同,需要单独扩展查询方法。
然后写一个 Controller 方法:
@RequestMapping("/court/list") public String courtList(Model model) { List<Map<String, Object>> courtList = courtService.getCourtListWithHot(); model.addAttribute("courtList", courtList); return "court/list"; }返回的Map里既包含场地字段,又包含计算出的activity_count,在 JSP 页面中直接用${court.activity_count}就能显示这个热度数字。不过这里有一个隐藏的问题需要处理好:Map传给前端时字段名大小写取决于 MySQL 返回的列名,建议在 SQL 里给字段起一个明确的别名,比如将聚合结果命名为activity_count,这样 JSP 和 Java 侧的代码就不会出现取不到值的诡异问题。
有了这个扩展之后,你再对自己的经历进行阐述就更有说服力:“我在做这个平台时,通过分析用户需求把场地列表做成按照热门活动数量来排序,活动越多的场地排名越靠前”,这就足以证明你不仅会调用现成框架,而且能依据业务场景去设计查询逻辑和索引,分数自然会高不少。
我拆过的毕设项目中,能在原始核心代码基础上多做一层这类思考的,基本都能让老师眼前一亮。从那以后,我每次拿到别人项目的第一件事,就是先看它有没有值得扩展的缺口,再看核心业务代码是否完整,这两点看完才会考虑下载。把环境反复搞好,再在心里把主流程过一遍,这次应该能帮到你。
本文还有配套的精品资源,点击获取