基于SSM的运动场地预约系统实战:数据库设计与并发防重
发布时间:2026/9/16 17:47:59 锦皓数字建站

简介基于SSM的大学运动场地管理系统源代码包是一套面向Java毕设与课程设计的完整前后端项目覆盖管理员侧的用户管理、场地信息与预约、器材信息与借用、系统管理等核心模块同时也提供用户侧首页、场地/器材浏览、新闻通知、个人中心等常规功能。压缩包共含1299个文件其中364个js、146个css、176个png与141个gif构成前端交互与静态资源128个java与117个jsp承载后端逻辑与页面另含2个sql数据库脚本、说明文档及Eclipse/IDEA工程配置整体大小29.71MB。资源自带JDK1.8、Maven3.3、Tomcat7、MySQL5.7等环境说明可帮助毕业设计或课程设计者快速理解SSM框架下的分层开发与场地预约、器材借用等典型业务流。已有54人浏览学习对于需要完整可运行项目作为参考或二次开发起点的读者而言这套源码在结构完整性和上手便利性上都有不错价值。1. 别把「基于 SSM 的运动场地管理系统」只看成一个增删改查练习高校体育场地永远是稀缺资源羽毛球馆、篮球场到了晚上靠运气抢管理员排期靠微信群接龙。这个标题里那份「基于 SSM 的大学运动场地管理系统源代码」拆开看就是 Spring SpringMVC MyBatis 三件套配合 MySQL做成一个学生查场地、预约、取消管理员排期、审批、统计的 Web 系统。很多人在课设、毕设或者接手老项目时犯同一个错以为难点在增删改查真正要花最多时间处理的是并发下「同一时段被两个人抢到」和「页面状态与 MySQL 数据对不上」这两件事。下面我不复述项目文档直接按数据表设计、后端流程、联调部署、进阶验证的顺序把这个系统讲透。2. SSM 骨架与 MySQL 表设计先稳定数据再写业务代码2.1 三层框架各自管哪一段SSM 是典型的 Java 传统三层架构不少开源项目、课程设计和毕业设计都选它原因是学习曲线适中、资料多而且框架思路能直接平移到 Spring Boot。三个组件各自边界Spring 管对象Service、Mapper 的实例创建与依赖注入事务边界也在这一层声明。SpringMVC 管请求前端发来的 HTTP 请求由 DispatcherServlet 分发到 ControllerController 只负责接参数、调 Service、返回视图或 JSON。MyBatis 管 SQLMapper 接口定义方法XML 里写 SQL把结果集映射成 Java 对象。MySQL 在这套系统里就是存储层。常见做法是把数据库脚本拆成lw_schema.sql和lw_seed.sql两份前者建表后者灌初始数据。拿到源代码后先别急着启动 Tomcat把这两份 SQL 按顺序导入能避免掉后面一半的报错。2.2 用「用户—场地—时段」三个表撑起预约模型运动场地预约的核心模型不复杂三张表就够。用户表区分管理员和学生场地表描述场地订单表记录「谁在什么时间用了哪块场地」。参考建表语句CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, user_no VARCHAR(20) UNIQUE COMMENT 学号/工号, user_name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, role TINYINT DEFAULT 2 COMMENT 1管理员 2学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE field_info ( field_id INT PRIMARY KEY AUTO_INCREMENT, field_name VARCHAR(50) NOT NULL COMMENT 如羽毛球1号场, location VARCHAR(100), max_people INT DEFAULT 0 COMMENT 同时容纳人数, field_type VARCHAR(20) COMMENT 篮球/羽毛球/网球 ); CREATE TABLE booking_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, field_id INT NOT NULL, book_date DATE NOT NULL, begin_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_field_time (field_id, book_date, begin_time) );参数说明sys_user.password用 64 位是因为常见做法是存 MD5 或 SHA-256 摘要不要存明文booking_order.status用数字是为了后续扩展比如加一个「使用完成」状态uk_field_time唯一索引是数据库层的第一道防重闸门。导入这份 schema 后用DESC booking_order;查看表结构确认索引列顺序是 field_id、book_date、begin_time走索引时才能命中。2.3 不允许重复的时段约束建在表上唯一索引只能挡「同一天同一场地同一个开始时间」的完全重复挡不住「18:00-19:00」和「18:30-19:30」这种互相重叠的区间。这是整个系统最容易出 bug 的地方。处理重叠的正确姿势是表上加唯一索引做最后兜底业务层用区间判断拦截。查询任何一段「已被预约」的订单SQL 核心条件是begin_time 结束时间 AND end_time 开始时间只要两个时间段有交集就会查出记录。这个判断必须写进 Service 层不能只靠前端校验否则别人绕过页面直接发 HTTP 请求就能把同一时段刷成两单。3. 后端核心流程SpringMVC 控制器加 MyBatis 把「预定」做严3.1 Controller 只做分发业务判断交给 Service拿到代码后第一个要看清的地方是 Controller 层有没有写业务逻辑。典型的问题写法是直接在 Controller 里查数据库、拼个返回对象看着省事但后续加权限、加事务、加缓存都要动 Controller耦合太重。正常的分层是 Controller 从 Session 拿当前登录用户把请求参数传给 ServiceService 返回统一结果对象。参考写法Controller RequestMapping(/booking) public class BookingController { Autowired private BookingService bookingService; PostMapping(/submit) ResponseBody public Result submit(SessionAttribute(loginUser) SysUser user, RequestParam Integer fieldId, RequestParam String date, RequestParam String begin, RequestParam String end) { return bookingService.book(user.getUserId(), fieldId, date, begin, end); } }逻辑说明SessionAttribute(loginUser)表示当前用户是从 Session 里取出来的不信任前端传的 userId防止学生把订单挂在别人名下。ResponseBody表示直接返回 JSON由 Jackson 序列化前端 AJAX 接收后渲染结果。Result是统一返回体一般包含success和msg两个字段失败时页面上直接弹msg。3.2 时段重叠查询与并发下的双保险Service 里的预定方法是整个系统核心中的核心。先查重叠再插入订单这两步之间如果有两个请求同时进来各自都查到「无冲突」双双插入成功数据就坏了。处理方式分两档。如果你部署的是单机 Tomcat用synchronized锁住方法就够如果将来拆成多节点部署必须换成数据库悲观锁SELECT ... FOR UPDATE或者 Redis 分布式锁。下面是单机版本也是这类 SSM 系统最常见的实现Service public class BookingServiceImpl implements BookingService { Autowired private BookingMapper bookingMapper; Override public synchronized Result book(Integer userId, Integer fieldId, String date, String begin, String end) { // 1. 区间重叠判断status 只要待确认或已确认都算占用 int count bookingMapper.countOverlap(fieldId, date, begin, end); if (count 0) { return Result.fail(该时段已被预定); } // 2. 再查一次场地是否开放防止绕过前端传了闭馆时间 int open bookingMapper.countOpen(fieldId, begin, end); if (open 0) { return Result.fail(该时段不在场地开放时间内); } // 3. 插入订单 bookingMapper.insert(userId, fieldId, date, begin, end); return Result.success(预定成功); } }调用链路上真正查重用的是 Mapper XML 里的这一段select idcountOverlap resultTypeint SELECT COUNT(*) FROM booking_order WHERE field_id #{fieldId} AND book_date #{date} AND status IN (0, 1) AND begin_time lt; #{end} AND end_time gt; #{begin} /select说明begin_time #{end}意味着一个订单的开始时间早于当前请求的结束时间end_time #{begin}意味着已订订单的结束时间晚于当前请求的开始时间两个条件同时成立就是时间段相交。lt;和gt;是 XML 里转义后的小于、大于号。如果这段 SQL 出现在注解里也用同样逻辑但 XML 的好处是改 SQL 不用重新编译直接替换 class 文件里的 XML 即可。3.3 管理端一周排期视图的 SQL 写法管理后台最常见的功能是「以周为单位看每个场地每天被订了几场」。这个需求不要写循环查数据库一条分组 SQL 就能出结果SELECT field_id, book_date, COUNT(*) AS booked_count FROM booking_order WHERE status IN (0, 1) AND book_date BETWEEN #{startDate} AND #{endDate} GROUP BY field_id, book_dateMapper 返回ListMapString, Object前端拿到后嵌套循环渲染到表格里。性能上只要book_date有索引这个查询在一分钟内就完成校园几千条订单的聚合。有人习惯写 MySQL 存储过程来做这类统计我的看法是单表聚合没必要动用存储过程SQL 能表达清楚的就不要增加维护负担。真正值得写存储过程的场景是「多个步骤必须一次性完成且不可拆分」而排期统计只是一个只读查询。4. 前后端联调与 MySQL 部署把项目跑通需要的那套最小配置4.1 前端 JSP 与 AJAX页面怎么把请求发给后端这类系统最常见的形态是 JSP 做页面内嵌 EL 表达式和 JSTL 标签部分交互用 jQuery 的 AJAX 刷局部数据。学生端「提交预约」就是一个典型例子$.post(/booking/submit, { fieldId: $(#fieldId).val(), date: 2025-04-10, begin: 18:00, end: 19:00 }, function (res) { if (res.success) { $(#result).text(预约成功等待管理员确认); loadWeekTable(); } else { $(#result).text(res.msg); } }, json);逻辑说明$.post默认表单编码SpringMVC 用RequestParam接收没有压力loadWeekTable()是页面里复用的函数负责重新拉取本周排期这样用户订完当场就能看到自己占住的格子。对比一下现在流行的前后端分离架构Spring Boot Vue Token 认证这套 SSM 老系统的模式是 Session 登录 服务端渲染 局部刷新部署更轻但跨域、并发会话管理要自己注意。4.2 Tomcat 与 MySQL 的本地部署清单拿到 ZIP 解压后最快跑通的路径是先装 MySQL再导入 SQL最后把项目打成 WAR 丢进 Tomcat。MySQL 安装时注意版本与驱动对应最常配错的就是这个表场景驱动类JDBC URLMySQL 8.xcom.mysql.cj.jdbc.Driverjdbc:mysql://localhost:3306/sport?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiMySQL 5.7com.mysql.jdbc.Driverjdbc:mysql://localhost:3306/sport?useUnicodetruecharacterEncodingutf8数据库连接信息一般放在src/main/resources/jdbc.properties里部署时改成你自己的账号密码。本地开发图省事可以用 MySQL 免安装版解压后mysqld --initialize-insecure初始化再net start mysql启动服务。接下来是 Tomcat 侧。默认 8080 端口如果被占改conf/server.xml里的 Connectorport8081。项目打包成 WAR 后丢进webapps/目录启动 Tomcat 访问http://localhost:8080/sport/。注意上下文路径来自 WAR 包文件名改成sport.war就是/sport/。4.3 部署阶段出现频率最高的 4 个报错Access denied for user rootlocalhost账号密码或权限不匹配先到 MySQL 里用该账号手动登录验证排除密码问题再查授权。数据库中文乱码MySQL 驱动 URL 加characterEncodingutf8同时 MySQL 建库时指定DEFAULT CHARSETutf8mb4。Invalid bound statement (not found)Mapper 接口与 XML 的 namespace 对不上或者 XML 没被 Maven 打包进 classes。检查mybatis-config.xml里的 mapper 扫描路径以及接口方法名是否与 XML 的 id 一致。NoClassDefFoundError框架依赖 jar 缺失确认 Tomcat 解压后的WEB-INF/lib里有spring-webmvc、mybatis-spring、mysql-connector-java这些关键包。提示前两个报错占了这类系统启动失败原因的七成改配置时一次只改一项然后重启验证。5. 进阶用法让这个系统从「能跑」到「耐看」5.1 登录拦截器补齐权限漏洞很多源码的首页可以直接访问订单接口也能裸调这在课程设计里能交差但要写到简历或者演示给老师看至少加一个登录拦截器。用 SpringMVC 的HandlerInterceptor实现几十行代码public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(loginUser) ! null) { return true; } response.sendRedirect(request.getContextPath() /login); return false; } }注册到spring-mvc.xmlmvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ mvc:interceptor bean classcom.sport.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptor /mvc:interceptors说明/**拦所有路径/login放行/static/**放行静态资源。这样新增页面无需每个 Controller 里重复写 Session 判断。5.2 超时未确认订单自动取消学生提交后管理员一直不确认场地就被占着。用 Spring 的Scheduled定时任务每十分钟扫描一次Scheduled(cron 0 */10 * * * ?) public void autoCancel() { bookingMapper.cancelExpired(); }对应 SQLUPDATE booking_order SET status 2 WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 30 MINUTE)。记得在 Spring 配置文件里加task:annotation-driven/。5.3 用一条 SQL 和一条命令验证系统竣工验收不要只看页面点两下要看你改完的代码到底稳不稳。场地使用率统计是最容易向别人讲清楚「系统有价值」的指标SELECT fi.field_name, COUNT(bo.order_id) AS total_orders, SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(bo.end_time, bo.begin_time)))) AS total_hours FROM field_info fi LEFT JOIN booking_order bo ON fi.field_id bo.field_id AND bo.status IN (0, 1) GROUP BY fi.field_id ORDER BY total_orders DESC;如果相关字段建了索引这张报表在几千条数据量级都是毫秒返回可以直接做成管理端首页图表。最后用压测命令验证高并发下没有超卖模拟 20 个并发用户同时提交同一场地同一时段的预约ab -n 100 -c 20 -p book_data.txt -T application/x-www-form-urlencoded http://localhost:8080/sport/booking/submit观察返回结果里failed requests必须是 0同时到 MySQL 里查该时段订单数只有 1 条双端一致说明你的防重逻辑真正生效了。调试完毕后顺手在管理页面加一个「按日期导出当天预约表」的按钮用前端表格做导出即可这类演示型功能加一个就能明显提升完成度。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。