☰
基于SSM的体育场地预约系统毕业设计:从选型到部署完整指南
2026/10/29 16:44:34 网站建设 项目流程

简介:这份资源是基于SSM框架的体育场地预约系统毕业设计完整项目包,面向计算机相关专业需要完成毕业设计、课程设计或期末大作业的本科与专科学生,尤其适合正在寻找可运行、可参考的Java Web实战项目的开发者。压缩包共1155个文件,约18.69MB,其中html、css、js与png、gif等前端资源占比较大,用于构建预约界面与后台管理页面;jsp、java与jar文件构成SSM核心业务逻辑与依赖库,xml、properties、sql及db文件则负责配置与数据库支撑,整体结构完整、层次清晰。目前已有41人学习下载。项目围绕体育场地预约场景,涵盖用户管理、场地信息维护、预约下单与后台审核等典型模块,读者可据此理解SSM整合流程、MVC分层设计与数据库表结构,并在此基础上进行功能扩展或二次开发,为毕业设计答辩与课程考核提供可直接参考的完整方案。

1. 体育场地预约系统:从 SSM 选型到能跑通的毕业设计

体育场地预约系统这个题目,几乎每年毕业设计季都会被翻出来做一遍。原因很直接:业务闭环清晰——用户查场地、选时段、下单、支付、核销,管理员管场地、管订单、管用户,一套 CRUD 加上时间冲突校验就能撑起一个完整系统。但真正动手时,很多人卡在第一步:SSM 三个框架怎么捏到一起,配置文件写多少,前端页面怎么和接口对上。这篇笔记按我实际带过的做法,把基于 SSM 的体育场地预约系统从环境搭建、数据库设计、核心预约逻辑到部署排错完整走一遍。适合正在做计算机毕业设计、软件工程毕业设计选题的同学,也适合想用 SSM 练手 Java Web 的开发者。读完你能拿到一套可复现的工程结构和关键代码,而不是只停留在“知道要用 SSM”。

2. SSM 三层怎么拆:体育场地预约系统的工程骨架

2.1 为什么选 SSM 而不是 Spring Boot

先说选型理由。SSM 是 Spring + Spring MVC + MyBatis 的缩写,在毕业设计场景里它有一个被低估的优势:分层显式、配置可见。Spring Boot 把大量配置藏进自动装配,答辩时老师问“你的事务是怎么生效的”,很多人答不上来。SSM 的applicationContext.xml、spring-mvc.xml、mybatis-config.xml三份配置文件摆在那里,IoC 容器、DispatcherServlet、SqlSessionFactory 的装配过程一目了然,写论文时也容易画出架构图。

体育场地预约系统的业务特点决定了它适合 SSM:读多写少、事务边界清晰(下单扣时段)、查询条件组合多(按场地类型、日期、时段筛选)。MyBatis 手写 SQL 在这种多条件动态查询场景下比 JPA 更可控,<if>标签拼条件比 Criteria API 直观。

常见做法是 Maven 多模块或单模块分层。我一般用单模块,包结构按职责分:

com.sport.reserve ├── controller // 接收请求,参数校验 ├── service // 业务逻辑,事务边界 │ └── impl ├── mapper // MyBatis 接口 ├── entity // 数据库实体 ├── dto // 前后端传输对象 ├── vo // 视图返回对象 └── util // 日期、分页等工具

这个结构不是摆设。controller 只做参数接收和结果包装,不写业务判断;service 里处理“这个时段是否已被预约”“会员折扣怎么算”;mapper 只负责 SQL。分层乱了,后期改一个预约规则要动三四个文件。

2.2 环境依赖与 pom.xml 关键配置

JDK 用 1.8,Tomcat 8.5,MySQL 5.7 或 8.0 都行。版本不要追新,SSM 生态在 JDK 8 上最稳。下面是 pom.xml 的核心依赖,版本号我写的是长期验证过的组合:

<properties> <spring.version>5.3.20</spring.version> <mybatis.version>3.5.9</mybatis.version> <mybatis.spring.version>2.0.6</mybatis.spring.version> <mysql.version>8.0.28</mysql.version> <druid.version>1.2.11</druid.version> </properties> <dependencies> <!-- Spring 核心:IoC 和事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring MVC:Web 层 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <!-- 数据库驱动与连接池 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <!-- Servlet API,scope 必须是 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>

参数说明:spring-tx和spring-jdbc必须显式引入,否则DataSourceTransactionManager找不到类。servlet-api的 scope 设成provided,因为 Tomcat 自带,打进 war 包会冲突。Druid 连接池不是必须,但它自带监控页面,调试连接泄漏时比默认池好用。

2.3 三份配置文件的职责边界

SSM 最容易翻车的地方就是配置文件互相引用出错。记住一条:web.xml是入口,它加载 Spring 父容器和 Spring MVC 子容器,父容器管 service 和 dao,子容器只管 controller。

<!-- web.xml 关键片段 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

applicationContext.xml里配数据源、SqlSessionFactory、MapperScannerConfigurer 和事务管理器。spring-mvc.xml里配注解驱动、静态资源放行、视图解析器。两者扫描的包必须错开:父容器扫com.sport.reserve.service和mapper,子容器只扫controller。如果都扫全包,事务会失效——因为 controller 被父容器也实例化了一份,请求走的是子容器那份没有事务代理的 bean。这个坑我在答辩现场见过不止一次。

3. 数据库设计与预约冲突校验:把业务规则落到表结构

3.1 五张核心表与字段取舍

体育场地预约系统的表不用多,五张够用:用户表、场地表、场地类型表、预约订单表、时段表。关键在字段设计要能支撑冲突校验和状态流转。

CREATE TABLE `venue` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL COMMENT '场地名称', `type_id` INT NOT NULL COMMENT '类型外键', `price_per_hour` DECIMAL(10,2) NOT NULL COMMENT '每小时价格', `status` TINYINT DEFAULT 1 COMMENT '1可用 0维护中', `open_time` TIME NOT NULL COMMENT '开放开始时间', `close_time` TIME NOT NULL COMMENT '开放结束时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `reservation` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `venue_id` INT NOT NULL, `reserve_date` DATE NOT NULL COMMENT '预约日期', `start_time` TIME NOT NULL, `end_time` TIME NOT NULL, `total_price` DECIMAL(10,2) NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已核销 3已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_venue_slot` (`venue_id`, `reserve_date`, `start_time`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:uk_venue_slot这个唯一索引是防重复预约的最后一道防线。有人会问为什么不用start_time和end_time联合唯一——因为同一场地不同开始时间但重叠的时段(比如 9:00-10:00 和 9:30-10:30)唯一索引拦不住,必须在 service 层做区间重叠判断。唯一索引只解决“完全相同的开始时间”这种并发场景。

status用 TINYINT 而不是 ENUM,方便扩展。价格用 DECIMAL 不用 FLOAT,避免 0.1+0.2 那种精度问题导致对账差一分钱。

3.2 时段重叠校验的 SQL 与 Java 双层实现

冲突校验是预约系统的核心。逻辑是:同一场地、同一天,新预约的[start, end)不能和已有有效订单的时段有交集。区间重叠的判定条件是new.start < old.end AND new.end > old.start。

先看 Mapper 层的查询:

<select id="countConflict" resultType="int"> SELECT COUNT(*) FROM reservation WHERE venue_id = #{venueId} AND reserve_date = #{reserveDate} AND status IN (0, 1, 2) AND start_time &lt; #{endTime} AND end_time &gt; #{startTime} </select>

注意status IN (0,1,2)排除了已取消的订单,否则用户取消后时段永远释放不出来。&lt;和&gt;是 XML 转义,直接写<会解析报错。

Service 层不能只靠这条 SQL,因为并发下两个请求可能同时查到 0 条然后都插入。我一般用“先查后插 + 数据库唯一索引兜底 + 捕获异常”三段式:

@Service public class ReservationServiceImpl implements ReservationService { @Autowired private ReservationMapper reservationMapper; @Override @Transactional(rollbackFor = Exception.class) public Result createReservation(ReservationDTO dto) { // 1. 基础校验:时间合法性 if (!dto.getStartTime().before(dto.getEndTime())) { return Result.fail("结束时间必须晚于开始时间"); } // 2. 查询冲突 int conflict = reservationMapper.countConflict( dto.getVenueId(), dto.getReserveDate(), dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { return Result.fail("该时段已被预约,请选择其他时间"); } // 3. 插入,唯一索引冲突时抛异常回滚 try { reservationMapper.insert(dto.toEntity()); } catch (DuplicateKeyException e) { return Result.fail("手慢了,该时段刚被预订"); } return Result.success(); } }

逻辑说明:@Transactional的rollbackFor = Exception.class必须写,默认只回滚 RuntimeException,DuplicateKeyException 是 RuntimeException 的子类所以默认能回滚,但显式写更保险。捕获DuplicateKeyException后返回友好提示,而不是让 500 页面糊到用户脸上。

参数说明:countConflict的四个参数顺序要和 XML 里的#{}对应,MyBatis 多参数要么用@Param注解,要么按顺序传。我习惯在 Mapper 接口上加@Param("venueId")这种,避免顺序错位。

3.3 订单状态机与超时释放

预约订单有四个状态:待支付、已支付、已核销、已取消。状态流转必须单向,不能从已核销跳回待支付。常见做法是在 service 层写一个updateStatus方法,内部判断当前状态是否允许目标状态:

private static final Map<Integer, Set<Integer>> ALLOWED = new HashMap<>(); static { ALLOWED.put(0, new HashSet<>(Arrays.asList(1, 3))); // 待支付 -> 已支付/已取消 ALLOWED.put(1, new HashSet<>(Arrays.asList(2, 3))); // 已支付 -> 已核销/已取消 ALLOWED.put(2, Collections.emptySet()); // 已核销终态 ALLOWED.put(3, Collections.emptySet()); // 已取消终态 }

超时释放是毕业设计里容易漏的点。用户下单后 15 分钟不支付,时段应该释放。实现方式有两种:定时任务扫描,或者延迟队列。SSM 项目里用 Spring 的@Scheduled最简单:

@Scheduled(cron = "0 */5 * * * ?") public void releaseTimeoutOrders() { // 查出创建超过15分钟且状态为待支付的订单 List<Reservation> list = reservationMapper.selectTimeout(15); for (Reservation r : list) { r.setStatus(3); reservationMapper.updateStatus(r); } }

cron表达式每 5 分钟跑一次。注意要在applicationContext.xml里加<task:annotation-driven/>和命名空间声明,否则@Scheduled不生效。这个坑很隐蔽,代码没错但任务就是不跑,查半天才发现配置漏了。

4. 从登录到下单:Controller 与前端联调的完整链路

4.1 统一返回结构与全局异常处理

前后端联调最烦的是返回格式不统一,前端一会儿要判code,一会儿要判success。我一般定义一个Result<T>类,固定code、msg、data三个字段:

public class Result<T> { private int code; // 200成功,其他失败 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "ok"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }

配合@ControllerAdvice做全局异常捕获,把DuplicateKeyException、NullPointerException统一转成Result.fail,前端永远只处理一种结构。这样联调时前端不用为每个接口写不同的错误处理。

4.2 登录拦截器与 Session 管理

体育场地预约系统需要区分普通用户和管理员。用 Session 存登录态是最简单的做法,配一个拦截器:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { HttpSession session = req.getSession(); if (session.getAttribute("userId") == null) { resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}"); return false; } return true; } }

在spring-mvc.xml里注册拦截器,排除登录、注册、场地列表这些公开接口:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/user/login"/> <mvc:exclude-mapping path="/user/register"/> <mvc:exclude-mapping path="/venue/list"/> <bean class="com.sport.reserve.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

注意exclude-mapping的路径要和 Controller 的@RequestMapping完全对应,少一个斜杠就拦不住。返回 401 时直接写 JSON,不要sendRedirect,否则前端 ajax 拿到的是登录页 HTML,解析报错。

4.3 场地列表分页与多条件查询

场地列表要支持按类型、日期、价格区间筛选,还要分页。MyBatis 的分页用 PageHelper 插件最省事:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.1</version> </dependency>

在applicationContext.xml里配成 SqlSessionFactory 的 plugin:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value> helperDialect=mysql reasonable=true </value> </property> </bean> </array> </property> </bean>

reasonable=true的作用是页码超出范围时返回第一页或最后一页,而不是空列表。前端传pageNum=0或负数时不会白屏。

Controller 里调用:

@GetMapping("/list") public Result<PageInfo<VenueVO>> list(VenueQuery query, @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize) { PageHelper.startPage(pageNum, pageSize); List<VenueVO> list = venueService.queryByCondition(query); return Result.success(new PageInfo<>(list)); }

PageHelper.startPage必须紧挨着查询方法,中间不能插其他数据库操作,否则分页会串到别的查询上。这个坑我踩过,表现为分页数量对不上,查了半天 SQL 没问题,最后发现是中间多调了一次 count。

5. 部署与排错:那些让毕业设计答辩翻车的坑

5.1 中文乱码的三处来源

乱码是 SSM 项目的高频问题,来源有三个,要逐个排查。第一处是数据库连接 URL 没加字符集:

jdbc:mysql://localhost:3306/sport_reserve?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

characterEncoding=utf8和serverTimezone缺一不可,MySQL 8 不配时区会报The server time zone value错误。第二处是 web.xml 没配CharacterEncodingFilter:

<filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

第三处是 Tomcat 的server.xml里 Connector 没加URIEncoding="UTF-8"。三处都配了还乱码,检查数据库表和字段的字符集是不是utf8mb4。

5.2 事务不生效的四种典型场景

现象:service 方法抛异常了,但数据库数据没回滚。原因通常是这四种之一。一是@Transactional标在 private 方法上,Spring AOP 代理拦不住 private 方法。二是同类内部方法调用,比如this.createOrder()调this.updateStock(),后者的事务注解不生效,因为走的是原始对象不是代理对象。三是异常被 catch 了没重新抛,事务管理器感知不到。四是applicationContext.xml里没开<tx:annotation-driven/>。

排查方法:在@Transactional方法里打个断点,看注入的 service 对象是不是代理类(类名带$$EnhancerBySpringCGLIB)。如果不是,说明代理没生成,回头查配置。

5.3 静态资源 404 与 DispatcherServlet 拦截

现象:CSS、JS、图片全部 404,页面裸奔。原因是web.xml里 DispatcherServlet 的url-pattern配了/,所有请求包括静态资源都进了 Spring MVC,但没配静态资源映射。

解决方式是在spring-mvc.xml里加:

<mvc:default-servlet-handler/> <mvc:resources mapping="/static/**" location="/static/"/>

default-servlet-handler把找不到的请求交回给 Tomcat 默认 Servlet 处理,resources显式映射静态目录。两个都加最稳。注意location的路径是相对于 webapp 根目录的,不是 classpath。

5.4 日期类型转换失败

现象:前端传2024-06-01到 Controller 的Date参数,报Failed to convert value of type String to Date。原因是 Spring MVC 默认不支持这种格式的自动转换。

解决方式是在 Controller 字段上加@DateTimeFormat:

public class ReservationDTO { @DateTimeFormat(pattern = "yyyy-MM-dd") private Date reserveDate; @DateTimeFormat(pattern = "HH:mm") private Date startTime; }

或者在spring-mvc.xml里注册全局转换器。我倾向用注解,精确到字段,不会影响其他接口。注意@DateTimeFormat对TIME类型要写HH:mm,写HH:mm:ss前端传09:00会解析失败。

5.5 连接池耗尽与 Druid 监控

现象:系统跑一段时间后所有请求卡死,日志报GetConnectionTimeoutException。原因是连接泄漏,某个查询用完没关,或者事务方法里有远程调用阻塞了连接。

排查方式:打开 Druid 监控页面,在applicationContext.xml的数据源里配:

<property name="filters" value="stat"/> <property name="maxActive" value="20"/> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/>

访问/druid/index.html看活跃连接数和等待线程数。maxActive毕业设计设 20 够用,设太大反而掩盖泄漏问题。testWhileIdle配合validationQuery能在连接空闲时检测有效性,避免 MySQL 8 小时断连后拿到死连接。

6. 让预约系统经得起答辩追问的两个进阶点

第一个进阶点是接口幂等。答辩老师常问“用户连点两次提交按钮怎么办”。除了前端置灰,后端要做幂等。最简单的方式是下单时生成一个requestId,用 Redis 或数据库唯一索引去重。SSM 项目里不引入 Redis 的话,可以在reservation表加一个request_id唯一索引,前端每次进入下单页生成一个 UUID,提交时带上。重复提交会触发唯一索引冲突,返回“请勿重复提交”。这个改动小,但答辩时能体现你考虑过并发。

第二个进阶点是预约日历的查询优化。用户选场地后要看到未来 7 天哪些时段可约。如果每天循环查一次数据库,7 次查询加 7 次冲突判断,响应慢。我一般用一条 SQL 查出该场地未来 7 天所有有效订单,在 Java 里按日期分组,再用位图或布尔数组标记已占用时段。代码大致是:

// 一次查出7天订单 List<Reservation> orders = reservationMapper.selectByVenueAndDateRange( venueId, today, today.plusDays(7)); // 按日期分组 Map<LocalDate, List<Reservation>> grouped = orders.stream() .collect(Collectors.groupingBy(r -> r.getReserveDate().toLocalDate())); // 对每天生成时段占用表 for (int i = 0; i < 7; i++) { LocalDate date = today.plusDays(i); List<Reservation> dayOrders = grouped.getOrDefault(date, Collections.emptyList()); // 根据场地开放时间切分小时段,标记哪些被占 // ... }

这样数据库只查一次,Java 内存计算,7 天日历渲染在 50ms 内完成。答辩演示时点日期切换很流畅,不会卡顿。

最后说个习惯:每次改完 Mapper XML,我一定重启 Tomcat 而不是热部署。MyBatis 的 XML 映射文件在热部署时经常不重新加载,改了 SQL 没生效,白白 debug 半小时。这个血泪经验希望帮到你。

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

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

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

立即咨询