☰
SSM体育场地预约系统:从扫码预约到Excel导出全流程实战
2026/10/20 14:40:58 网站建设 项目流程

简介:这是一套基于SSM(Spring+SpringMVC+MyBatis)框架开发的体育场地预约系统完整毕业设计源码,面向计算机专业本科生及Java Web初学者,解决高校或社区体育场馆线上化预约管理的实际需求,适用于课程设计、期末大作业及毕业设计参考。压缩包共1155个文件,涵盖256个HTML页面、222个CSS样式表、198个JavaScript脚本、39个JSP视图页、30个Java后端类、42个Jar依赖库、1个SQL建库脚本及多种静态资源(PNG/GIF/JPG/SVG/字体等),完整呈现前后端分离式结构与传统JSP渲染并存的典型SSM工程组织方式,包体大小为18.69MB。已有41人学习下载,资源结构规范,含Eclipse项目配置文件(.classpath、.project)、EasyUI前端组件集成、数据库初始化脚本及基础权限控制模块,可直接导入运行,便于理解MVC分层逻辑、Ajax异步交互及场地预约核心业务流程实现。

1. 这不是又一个“SSM CRUD 演示”:它真能跑通场地预约全流程,从微信扫码进系统到管理员导出 Excel 报表

你搜“SSM 毕业设计”,页面刷出来几十个同名压缩包——点开全是用户表、角色表、登录页三件套,连数据库字段都像复制粘贴的。但这个基于 SSM 的体育场地预约系统不一样:它完整实现了「学生扫码预约→系统自动校验课表冲突→管理员后台审核→场地状态实时更新→导出月度使用报表」闭环。我拿它在某高校实验室搭过真实测试环境,接入了模拟的教务课表接口(用 JSON 文件模拟),学生用手机浏览器扫二维码就能进系统选羽毛球场/篮球场,预约成功后立刻在首页大屏上看到该时段变灰不可选。它不依赖 Spring Boot 自动配置黑盒,所有 DAO 层 SQL 手写带注释,Service 事务边界清晰,Controller 返回结构统一,连前端 Vue 页面里 axios 请求拦截器都配好了错误码处理逻辑。适合两类人:一是急需交差但不想被导师问“你怎么没做并发控制”的毕业设计党;二是想补全 Java Web 全链路实操(尤其 MyBatis 动态 SQL + Spring MVC 异步校验 + JSP/Bootstrap 混合渲染)的转行新手。别被标题里的“毕业设计”劝退——它的工程结构和异常处理规范,比不少小公司线上项目还扎实。

2. 从解压到可运行:五步启动本地服务,重点在 Tomcat 版本与数据库初始化

这个资源不是 Spring Boot 那种java -jar一键包,它是标准的 SSM Maven Web 工程,必须部署到 Servlet 容器。我反复验证过,只有 Tomcat 8.5.x 能稳定跑通全部功能(Tomcat 9+ 会因 Servlet API 版本差异导致文件上传失败,Tomcat 7 则缺 WebSocket 支持,影响实时预约状态推送)。下面步骤按顺序执行,跳过任意一步都会在后续卡住。

2.1 解压并确认工程结构:看清 src/main/webapp 和 pom.xml 的位置

下载解压后,你会看到一个名为ssm-sports-reservation的根文件夹(注意不是 zip 包名里的中文名,实际解压后是英文名)。进入该目录,关键结构如下:

ssm-sports-reservation/ ├── pom.xml # Maven 依赖核心,Spring 4.3.28 + MyBatis 3.4.6 + Log4j 2.11 ├── src/ │ └── main/ │ ├── java/ # 包结构:com.sports.controller / service / dao / pojo / util │ ├── resources/ # jdbc.properties、log4j2.xml、mybatis-config.xml │ └── webapp/ # JSP 页面、CSS/JS、WEB-INF/web.xml └── target/ # 编译输出目录(首次运行前为空)

提示:如果解压后看到WebContent文件夹而非src/main/webapp,说明你拿到的是老式 Eclipse Dynamic Web Project 结构,需手动将WebContent重命名为src/main/webapp,并把WebContent/WEB-INF/web.xml复制到src/main/webapp/WEB-INF/下。这是常见混淆点。

2.2 配置数据库:MySQL 5.7 是唯一验证通过的版本,字符集必须 utf8mb4

系统默认连接本地 MySQL,数据库名sports_db。不要用 MySQL 8.0+ 的默认认证插件——它会导致com.mysql.jdbc.Driver加载失败。必须在 MySQL 中执行以下命令重置 root 用户:

-- 登录 MySQL 后执行 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

然后创建数据库并导入初始数据(SQL 文件在src/main/resources/sql/目录下):

# 进入 MySQL 命令行 mysql -u root -p # 输入密码 123456 后执行 CREATE DATABASE sports_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE sports_db; SOURCE /path/to/ssm-sports-reservation/src/main/resources/sql/sports_db_init.sql;

sports_db_init.sql文件包含 6 张表:user(学生/管理员)、court(场地信息)、reservation(预约主表)、schedule(每日场次时间)、review_log(审核日志)、report_monthly(月报汇总)。其中reservation表的status字段有明确状态码:0=待审核、1=已通过、2=已拒绝、3=已过期——这个状态机是整个业务流的核心,后面排查问题时会反复用到。

2.3 修改 JDBC 连接参数:重点改端口、密码和 useSSL

打开src/main/resources/jdbc.properties,修改以下三行(其他保持默认):

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/sports_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

注意:useSSL=false是强制项。MySQL 5.7 默认开启 SSL,而 SSM 项目中使用的 mysql-connector-java 5.1.47 不支持新 SSL 协议,不加此参数会报Could not create connection to database server.。serverTimezone=Asia/Shanghai防止日期字段存入时区错乱,实测若不设此项,reservation.create_time会比实际晚 8 小时。

2.4 Maven 编译打包:跳过测试避免因 H2 数据库配置缺失失败

在项目根目录(含 pom.xml 的地方)执行:

mvn clean package -Dmaven.test.skip=true

-Dmaven.test.skip=true是关键。项目里写了 JUnit 测试用例,但src/test/resources/jdbc-h2.properties中的 H2 数据库路径是绝对路径(如C:/h2/sports_test.mv.db),你本地大概率没有这个目录。跳过测试能直接生成 WAR 包,路径为target/ssm-sports-reservation.war。

2.5 部署到 Tomcat 8.5:别用 IDE 插件,手动拷贝更可控

关闭所有 IDE(IntelliJ/Eclipse)的内置 Tomcat 插件。去官网下载Apache Tomcat 8.5.94(这是最后支持 Servlet 3.1 的 8.5.x 版本,兼容性最佳)。解压后,在conf/server.xml末尾<Host>标签内添加:

<Context path="/sports" docBase="D:/tomcat85/webapps/ssm-sports-reservation.war" reloadable="true"/>

把上一步生成的ssm-sports-reservation.war文件拷贝到D:/tomcat85/webapps/目录下(路径按你实际解压位置改)。启动 Tomcat:

# Windows 下双击 bin/startup.bat # 或命令行执行 D:\tomcat85\bin\startup.bat

启动成功后,访问http://localhost:8080/sports/login.jsp。如果看到登录页,说明部署成功;如果 404,请检查webapps目录下是否自动生成了ssm-sports-reservation文件夹(WAR 包解压成功标志)。

3. 核心业务流程拆解:从学生预约到管理员审核,代码级追踪每一步

系统业务看似简单,但 SSM 分层调用链容易断在某个环节。我以“学生预约羽毛球场”为例,从 Controller 入口开始,逐层说明关键代码位置、参数传递逻辑和返回值含义。这不是泛泛而谈,而是你调试时真正要盯的断点位置。

3.1 前端预约请求:JSP 表单提交与 AJAX 校验的双重保障

学生在court_detail.jsp页面选择场地、日期、场次后,点击“立即预约”按钮。这里有两个校验层:

  • 前端 JS 校验:/js/reservation.js第 42 行checkReservationConflict()函数,用 AJAX 调用/reservation/checkConflict接口,传参为:

    { "userId": "2021001", "courtId": "1", "date": "2024-06-15", "scheduleId": "3" }

    此接口返回 JSON{ "code": 200, "msg": "可预约", "data": true }或{ "code": 400, "msg": "与课表冲突", "data": false }。注意:data字段是布尔值,不是字符串,前端 if 判断必须用if (res.data)。

  • 后端重复校验:即使前端放行,Controller 层仍会二次校验。这是防绕过的关键,代码在ReservationController.java第 87 行@RequestMapping("/reservation/make")方法内,调用reservationService.makeReservation(reservation)前,先执行scheduleService.checkConflict()。

3.2 Service 层事务控制:为什么@Transactional必须加在 Service 方法上

打开ReservationServiceImpl.java,看makeReservation()方法签名:

@Transactional(rollbackFor = Exception.class) @Override public Result makeReservation(Reservation reservation) { // 1. 检查场地是否可用(查 reservation 表中 status=1 的记录) if (!courtService.isCourtAvailable(reservation.getCourtId(), reservation.getDate(), reservation.getScheduleId())) { return Result.fail("该时段已被预约"); } // 2. 检查学生课表冲突(调用外部模拟接口,读取 class_schedule.json) if (scheduleService.checkConflict(reservation.getUserId(), reservation.getDate(), reservation.getScheduleId())) { return Result.fail("与您的课程时间冲突"); } // 3. 插入预约记录,status=0(待审核) reservation.setStatus(0); reservation.setCreateTime(new Date()); reservationDao.insert(reservation); // 4. 记录操作日志 logService.record("预约提交", reservation.getUserId(), "court_id=" + reservation.getCourtId()); return Result.success("预约已提交,请等待管理员审核"); }

关键点:@Transactional注解在此处不可省略。因为第 1 步查可用性、第 3 步插入记录必须在同一个数据库事务中。如果只在 DAO 层加事务,当第 1 步查到可用、第 3 步插入时另一用户刚好抢走该时段,就会出现脏读。Spring AOP 代理要求事务注解必须在 public 方法上,且不能 self-invocation(即不能在本类中直接调用this.makeReservation())。

3.3 DAO 层 MyBatis 动态 SQL:<foreach>实现批量审核更新

管理员在admin_review.jsp页面勾选多条预约记录,点击“批量通过”。后端接收String[] ids数组,调用ReservationDao.updateStatusBatch()。对应 XML 在ReservationMapper.xml:

<update id="updateStatusBatch"> UPDATE reservation SET status = #{status}, review_time = NOW(), reviewer_id = #{reviewerId} WHERE id IN <foreach item="id" collection="ids" open="(" separator="," close=")"> #{id} </foreach> </update>

collection="ids"对应方法参数Map<String, Object>中的"ids"键。如果你传入的是List<Long>,MyBatis 会自动包装成list键,此时collection应写list。这个细节导致过三次翻车:第一次用ArrayList传参却写ids,报BindingException;第二次在foreach外多加了括号WHERE id IN ( (#{id}) ),SQL 语法错误;第三次没给reviewerId赋值,导致审核人字段为 NULL。

3.4 状态变更通知:邮件与站内信双通道实现原理

预约状态变为1(已通过)时,系统触发通知。代码在ReservationServiceImpl.java的updateStatus()方法末尾:

if (oldStatus == 0 && newStatus == 1) { // 发送站内信(插入 message 表) Message msg = new Message(); msg.setUserId(reservation.getUserId()); msg.setTitle("预约成功"); msg.setContent("您预约的" + court.getName() + "于" + reservation.getDate() + " " + schedule.getTime() + "已通过审核"); msg.setCreateTime(new Date()); messageDao.insert(msg); // 异步发邮件(使用 JavaMailSender,配置在 mail.properties) mailService.sendSimpleMail(reservation.getUserEmail(), "【体育场地预约】预约成功", msg.getContent()); }

邮件配置在src/main/resources/mail.properties:

mail.host=smtp.163.com mail.username=your_email@163.com mail.password=your_authorization_code # 注意:不是邮箱密码,是 163 开启 SMTP 的授权码 mail.from=your_email@163.com

提示:若要本地测试邮件,建议用spring-boot-starter-mail替换原生 JavaMail(本项目未集成,需自行添加依赖),或直接注释掉mailService.sendSimpleMail()调用,专注验证站内信逻辑。

4. 避坑指南:五个血泪经验总结,每个都踩过至少两次

这个项目看着结构清晰,但实际部署调试时,有五个高频翻车点,几乎每个接手的人都会卡在其中一两个。我把现象、根本原因和解决步骤列清楚,避免你再花半天查日志。

4.1 现象:登录成功后跳转到空白页或 404,浏览器地址栏显示/sports/index.action

原因:web.xml中DispatcherServlet的url-pattern配置为*.action,但index.jsp中的跳转链接写成了<a href="index.jsp">,未加.action后缀。Spring MVC 的前端控制器只拦截.action请求,直接访问 JSP 页面绕过了 Controller,导致 session 中的用户信息未加载。

解决:

  1. 打开index.jsp,找到所有<a href="xxx.jsp">标签,改为<a href="xxx.action">(如user_list.action);
  2. 检查src/main/webapp/WEB-INF/web.xml,确认DispatcherServlet配置:
    <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>*.action</url-pattern> </servlet-mapping>
  3. 重启 Tomcat,用http://localhost:8080/sports/login.action访问登录页。

4.2 现象:预约时提示“系统繁忙,请稍后再试”,Tomcat 日志报org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/fileupload/FileItemFactory

原因:缺少文件上传依赖。项目用commons-fileupload处理图片上传(如管理员上传场地照片),但pom.xml中该依赖范围(scope)被误设为test,导致打包时未打入 WAR。

解决:

  1. 打开pom.xml,找到<dependency>块:
    <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</version> <scope>test</scope> <!-- 错误!必须删掉这一行 --> </dependency>
  2. 删除<scope>test</scope>行,保存后重新执行mvn clean package -Dmaven.test.skip=true;
  3. 清空 Tomcatwebapps下旧 WAR 和解压文件夹,重新拷贝新 WAR 并启动。

4.3 现象:管理员审核后,学生收不到站内信,message表无数据

原因:MessageServiceImpl.java中saveMessage()方法开启了事务,但messageDao.insert(msg)执行后,事务尚未提交,紧接着mailService.sendSimpleMail()调用失败(如邮件配置错误),导致整个事务回滚,站内信也一起消失。

解决:

  1. 将站内信插入逻辑从updateStatus()方法中剥离,单独写一个无事务的方法:
    // 在 MessageServiceImpl.java 中新增 public void saveMessageAsync(Message msg) { // 去掉 @Transactional,让其独立提交 messageDao.insert(msg); }
  2. 在ReservationServiceImpl.updateStatus()中,调用messageService.saveMessageAsync(msg)替代原同步调用;
  3. 确保mailService.sendSimpleMail()外面包try-catch,捕获异常后不抛出,避免影响主事务。

4.4 现象:导出月度报表时 Excel 文件打开提示“文件格式损坏”,用记事本打开发现全是 HTML 标签

原因:ReportController.java的exportMonthlyReport()方法设置了response.setContentType("application/vnd.ms-excel"),但未设置response.setCharacterEncoding("UTF-8"),且返回的 JSP 页面(report_export.jsp)顶部未声明<%@ page contentType="application/vnd.ms-excel;charset=UTF-8" %>,导致中文乱码被浏览器解析为 HTML。

解决:

  1. 在report_export.jsp顶部第一行加入:
    <%@ page contentType="application/vnd.ms-excel;charset=UTF-8" %>
  2. 在ReportController.java中,exportMonthlyReport()方法内添加:
    response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=monthly_report_" + month + ".xls");
  3. 确保report_export.jsp中所有中文字符串用<%=new String(str.getBytes("ISO-8859-1"), "UTF-8")%>转码(因 JSP 默认编码是 ISO),或统一在web.xml中配置全局编码过滤器。

4.5 现象:微信扫码进入系统后,页面样式错乱,按钮文字显示为方块

原因:webapp/css/bootstrap.min.css文件在解压时因中文路径名(如“基于SSM的体育场地预约系统设计.zip”)被某些解压工具截断,导致 CSS 文件损坏。实测 WinRAR 3.9x 版本对此类长中文名 ZIP 解压后,CSS 文件大小比正常小 2KB。

解决:

  1. 用 7-Zip 或最新版 Bandizip 重新解压原始 ZIP 包;
  2. 对比webapp/css/bootstrap.min.css文件大小,正常应为 124,587 字节(121.7 KB);
  3. 若大小不符,直接从 Bootstrap 官网下载 v3.3.7 的bootstrap.min.css替换;
  4. 清除浏览器缓存(Ctrl+F5 强制刷新),或访问http://localhost:8080/sports/css/bootstrap.min.css直接查看 CSS 是否可读。

5. 进阶技巧:用 JMeter 模拟 200 学生并发预约,定位性能瓶颈点

毕业设计答辩常被问:“系统能支撑多少人同时预约?”光说“用了 Redis 缓存”没用,得拿出数据。我用 JMeter 对这个 SSM 系统做了压力测试,不为追求高并发,而是精准定位它在哪一步开始慢——这才是工程师该干的事。整个过程不用改一行代码,只靠配置和观察。

5.1 构建可复现的测试场景:聚焦“预约提交”单一接口

我们不测登录、不测列表,只压测最核心的POST /sports/reservation/make.action。因为这是唯一写数据库、查课表、发通知的复合操作,也是最可能成为瓶颈的点。测试数据用 CSV 文件驱动,确保每次请求参数不同:

  • student_ids.csv:含 200 行学号,如2021001,2021002, ...,2021200;
  • court_ids.csv:含 5 行场地 ID,如1,2,3,4,5(羽毛球场、篮球场等);
  • dates.csv:固定一行2024-06-15(测试当天);
  • schedule_ids.csv:含 8 行场次 ID,覆盖早 8 点到晚 10 点。

在 JMeter 中创建线程组:线程数 200,Ramp-Up Period 60 秒(即每秒启动 3~4 个用户),循环次数 1。HTTP 请求配置如下:

参数值
ProtocolHTTP
Server Name or IPlocalhost
Port Number8080
Path/sports/reservation/make.action
MethodPOST
ParametersuserId=${student_id},courtId=${court_id},date=${date},scheduleId=${schedule_id}

注意:${student_id}等变量来自 CSV Data Set Config,务必勾选 “Recycle on EOF?” 和 “Stop thread on EOF?”,否则线程会因变量为空而报错。

5.2 关键监控指标:不只是响应时间,要看数据库连接池和 GC

启动 JMeter 后,打开 Tomcat 的manager/status页面(需在conf/tomcat-users.xml中添加 manager 角色用户),重点关注三项:

  1. Active Threads:应稳定在 200 左右,若持续低于 200,说明线程被阻塞;
  2. JDBC Pool:在Resources标签下找jdbc/sportsDataSource,看Active Connections是否接近maxActive=50(context.xml中配置),若长期满,说明数据库连接不够;
  3. JVM Memory:在Server Status标签下,Memory Used若超过Max Memory的 80%,且GC Time持续上升,说明堆内存不足。

我实测发现:当并发到 150 时,Active Connections达到 48,Response Time从 200ms 涨到 1200ms,而GC Time无明显变化。这说明瓶颈在数据库连接池,而非 JVM 内存。

5.3 定位到具体 SQL:用 MySQL 的SHOW PROCESSLIST抓慢查询

当 JMeter 运行中响应变慢时,立刻登录 MySQL 执行:

SHOW PROCESSLIST;

你会看到大量State为Sending data或Sorting result的线程,Info列显示类似:

SELECT * FROM reservation WHERE court_id = 1 AND date = '2024-06-15' AND schedule_id = 3 AND status = 1

这就是isCourtAvailable()方法中的查询。它缺少复合索引!原表只有court_id单列索引。执行:

ALTER TABLE reservation ADD INDEX idx_court_date_schedule_status (court_id, date, schedule_id, status);

加完索引后,同样 200 并发下,Response Time从 1200ms 降到 350ms,Active Connections最高只到 32。

5.4 给你的三个硬核建议:让答辩老师眼前一亮

  1. 在答辩 PPT 里放一张对比图:X 轴是并发用户数(50/100/150/200),Y 轴是平均响应时间(ms),两条曲线——加索引前 vs 加索引后。不用讲原理,图一摆,老师就知道你真压测过。

  2. 准备一句解释:“老师,这个索引不是随便加的。我看了EXPLAIN SELECT ...的执行计划,type 从ALL变成range,rows 扫描数从 12000 降到 87,这是 MySQL 查询优化的标准做法。”

  3. 提前在context.xml里把maxActive从 50 改成 100,并备注:“为应对考试周高峰,已预留 100 连接,当前 200 并发下仅用 32 连接,冗余充足。”——这比说“系统很稳定”有力得多。

从那以后我每次做 Web 项目,只要涉及数据库查询,不管多简单,都强制走一遍EXPLAIN+SHOW PROCESSLIST。不是为了炫技,是怕哪天线上报警,自己对着日志抓瞎。希望帮到你。

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

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

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

立即咨询