ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+MySQL运动场地预约系统:从数据库设计到并发控制

SSM+MySQL运动场地预约系统:从数据库设计到并发控制 简介基于SSMMySQL的大学运动场地管理系统是一套适合毕业设计或课程设计的完整项目资源面向需要快速上线场地预约功能的学生开发者与高校管理员。系统覆盖个人中心、用户管理、场地信息管理、场地预约管理、器材信息管理与借用管理、系统简介及系统管理等模块支持从场地录入、预约审核到日程查看、统计报表的全流程操作具备清晰的权限分层与友好交互界面。资源包共含1302个文件压缩包约30.37MB其中Java源码、JSP页面、JS脚本、CSS样式占据主体另有MySQL建表SQL、项目配置文件及简明的部署说明文档便于本地环境搭建与二次开发。同时附有完整设计文档与视频演示可辅助理解代码结构、数据库关系及运行效果适合需要系统性完成课题展示或学习SSM框架整合的读者。目前已有96人学习下载适合作为项目参考或答辩素材。1. 大学运动场地管理系统SSMMySQL能解决什么一套大学的羽毛球馆、篮球场、网球场预约靠人工登记表管理到了期末基本就是「先到先得、迟到必吵」的状态。痛点不在场地本身而在预约信息的流动谁约了、什么时段、有没有冲突、爽约了怎么处理。基于 SSMMySQL 的运动场地管理系统正是用 Spring 管业务对象、SpringMVC 收 HTTP 请求、MyBatis 操作 MySQL 数据把「场地查询—在线预约—后台审核—使用记录」这条链路做成可用的 Web 系统。对于正在做 Java Web 课程设计或毕业设计的开发者这套组合最大价值是分层清晰SpringMVC 处理页面请求Service 层集中写预约规则MyBatis 用 XML 管理 SQLMySQL 负责最终的数据一致。本文会从数据库设计讲到并发预约的兜底方案每一步给的代码和 SQL 都能直接复制进你自己的工程里改着用。2. SSMMySQL下的运动场地数据模型与核心表设计2.1 SSM 在各层里到底管什么SSM 不是框架是三个框架按职责叠起来的使用方式。Spring 是容器负责创建 Service、Mapper 这些对象同时管理数据库事务SpringMVC 是 Web 层框架把用户提交的预约表单映射成 Controller 方法的参数MyBatis 是持久层框架把 Java 方法名和 XML 里的 SQL 绑定到一起。在运动场地管理系统里典型请求路径是用户打开预约页 → SpringMVC 的DispatcherServlet找到对应Controller→Controller调Service接口 →Service里用 Spring 声明式事务控制多步操作 → 调用 MyBatis 的Mapper接口 → 执行 MySQL 的INSERT/SELECT/UPDATE。MySQL 在其中的角色不只是存储。场地预约最怕“同一时段被两个人抢”而 MySQL 的 InnoDB 引擎提供行级锁和唯一索引可以在数据层面防止重复插入。设计表结构时就要把这个约束考虑进去而不是把所有防冲突逻辑都写在 Java 代码里。2.2 核心表用户、场地、预约、时段一个能跑通的管理系统最少需要四张表。用户表存放学生和教师账号场地表记录场馆名称、位置、开放时间时段表定义每天的可约时间段预约表把用户、场地、时段关联起来同时记录状态。下面是直接可执行的建表 SQL字符集统一用utf8mb4避免存入中文表情或特殊符号时出现乱码。CREATE DATABASE IF NOT EXISTS sports_field DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE sports_field; CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5密文, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, user_type TINYINT NOT NULL DEFAULT 1 COMMENT 1学生 2教师 3管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT用户表; CREATE TABLE field_info ( id INT AUTO_INCREMENT PRIMARY KEY, field_name VARCHAR(100) NOT NULL COMMENT 场地名称如羽毛球1号场, location VARCHAR(200) COMMENT 场地位置, field_type VARCHAR(50) COMMENT 羽毛球/篮球/网球等, open_time VARCHAR(20) DEFAULT 08:00 COMMENT 开放时间, close_time VARCHAR(20) DEFAULT 22:00 COMMENT 关闭时间, status TINYINT DEFAULT 1 COMMENT 1可用 0维护中 ) ENGINEInnoDB COMMENT场地表; CREATE TABLE time_slot ( id INT AUTO_INCREMENT PRIMARY KEY, slot_start VARCHAR(10) NOT NULL COMMENT 如 08:00, slot_end VARCHAR(10) NOT NULL COMMENT 如 09:00, sort_no INT DEFAULT 0 COMMENT 排序号 ) ENGINEInnoDB COMMENT时段表; CREATE TABLE field_reserve ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT 预约人, field_id INT NOT NULL COMMENT 场地, slot_id INT NOT NULL COMMENT 时段, reserve_date DATE NOT NULL COMMENT 预约日期, status TINYINT DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_field_slot_date (field_id, slot_id, reserve_date), CONSTRAINT fk_reserve_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_reserve_field FOREIGN KEY (field_id) REFERENCES field_info(id), CONSTRAINT fk_reserve_slot FOREIGN KEY (slot_id) REFERENCES time_slot(id) ) ENGINEInnoDB COMMENT预约表;上述 SQL 中UNIQUE KEY uk_field_slot_date是最关键的一行。它对(field_id, slot_id, reserve_date)三个字段建唯一索引意味着同一个场地、同一天、同一个时段只能存在一条预约记录。这个约束配合INSERT ... ON DUPLICATE KEY UPDATE或 Java 层捕获DuplicateKeyException就能实现秒级并发下的防重。2.3 索引设计不只是主键MySQL 在数据量几千条时不建索引也能跑但大学场地管理系统到了学期末会有几万条预约记录查询条件通常是「某日期 某场地 状态」所以除了唯一索引还应该按查询习惯补充普通索引。比如user_id用于查「我的预约」reserve_date用于查「当天所有预约」。ALTER TABLE field_reserve ADD INDEX idx_user_date (user_id, reserve_date); ALTER TABLE field_reserve ADD INDEX idx_status (status);这里有个常见误区UNIQUE KEY本身已经是一个索引联合索引(field_id, slot_id, reserve_date)已经覆盖了「按场地查」和「按场地日期查」的场景不需要再单独给field_id建索引。MySQL 创建索引不是越多越好写操作要维护索引多余索引只会拖慢预约提交速度。实际上在很多课程设计里学生会给每个字段都建索引最后数据量不大看不出问题但到了大促式抢场地的并发场景插入延迟会明显上升。此外time_slot表建议用sort_no而不是直接按slot_start排序。因为如果时段是08:00-09:00、10:00-11:00字符串排序没问题但出现9:00这类不带前导零的脏数据时排序就会错乱。sort_no手动维护查询时ORDER BY sort_no最稳。3. 用SSM跑通运动场地预约的完整请求链路3.1 工程结构与关键配置运动场地管理系统的 Maven 工程结构不需要很复杂遵循 SSM 的标准分包即可。我一般按controller / service / mapper / entity四层组织资源文件放在src/main/resources下。核心配置有三个spring.xmlSpring 容器与 MyBatis 整合、spring-mvc.xmlSpringMVC 注解驱动、视图解析器、db.properties数据库连接参数。src/main/java/com/example/sports ├── controller │ ├── LoginController.java │ ├── ReserveController.java │ └── FieldController.java ├── service │ ├── ReserveService.java │ └── impl/ReserveServiceImpl.java ├── mapper │ ├── FieldMapper.java │ ├── ReserveMapper.java │ └── xml/ReserveMapper.xml ├── entity │ ├── Field.java │ ├── Reserve.java │ └── User.java └── common └── Result.javaspring.xml中需要配置数据源、SqlSessionFactoryBean和MapperScannerConfigurer。数据源建议用druid它能输出 SQL 执行时间排错时很直观。db.properties里最需要注意的是serverTimezoneAsia/ShanghaiMySQL 8.x 驱动默认连接 UTC 时区如果不显式指定预约日期的DATE字段可能比本地时间少 8 小时导致用户看到「预约日期减一天」。3.2 预约提交从 Controller 到 Mapper 的参数传递预约操作的前端表单一般只提交三个业务参数fieldId、slotId、reserveDate。用户 ID 从登录后的session中取不能信任前端传值。Controller 层只做参数接收和简单的格式校验真正的业务规则放在 Service 里。这里给出核心方法Controller RequestMapping(/reserve) public class ReserveController { Resource private ReserveService reserveService; PostMapping(/add) ResponseBody public Result add(RequestParam Integer fieldId, RequestParam Integer slotId, RequestParam String reserveDate, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { return Result.error(未登录); } Reserve reserve new Reserve(); reserve.setUserId(loginUser.getId()); reserve.setFieldId(fieldId); reserve.setSlotId(slotId); reserve.setReserveDate(java.sql.Date.valueOf(reserveDate)); reserve.setStatus(0); try { reserveService.addReserve(reserve); return Result.success(预约提交成功等待审核); } catch (DuplicateKeyException e) { return Result.error(该时段已被预约请选择其他时间); } catch (RuntimeException e) { return Result.error(e.getMessage()); } } }DuplicateKeyException是 Spring 对 MySQL 唯一键冲突的包装异常。这里没有用全局异常处理器直接在 Controller 捕获是为了让前端能立刻看到「该时段已被预约」的提示而不是抛出一大段堆栈。真正的核心逻辑在ReserveServiceImpl.addReserve中Service public class ReserveServiceImpl implements ReserveService { Resource private ReserveMapper reserveMapper; Override Transactional public void addReserve(Reserve reserve) { // 1. 校验日期早于今天直接拒绝 if (reserve.getReserveDate().before(new java.sql.Date(System.currentTimeMillis()))) { throw new RuntimeException(不能预约过去的日期); } // 2. 查询该时段是否被自己重复预约 Reserve exist reserveMapper.selectByUserFieldSlot( reserve.getUserId(), reserve.getFieldId(), reserve.getSlotId(), reserve.getReserveDate()); if (exist ! null) { throw new RuntimeException(您已预约该场地时段请勿重复提交); } // 3. 插入由数据库唯一索引兜底 reserveMapper.insertSelective(reserve); } }Transactional确保这个操作要么全部成功要么全部回滚。这里的查询 插入不是原子操作但因为有 MySQL 的uk_field_slot_date唯一索引做兜底即使两个请求同时通过了第 2 步的检查第 3 步也只有一个能插入成功另一个会抛出DuplicateKeyException。这也是我强调在数据库层建唯一索引的原因——Java 层的if判断在并发下根本没保障。3.3 MyBatis 映射文件里写什么Service 层调用了reserveMapper的两个方法对应ReserveMapper.java接口public interface ReserveMapper { Reserve selectByUserFieldSlot(Param(userId) Integer userId, Param(fieldId) Integer fieldId, Param(slotId) Integer slotId, Param(reserveDate) Date reserveDate); int insertSelective(Reserve reserve); }XML 中需要特别注意参数类型和 SQL 的命名。下面的insertSelective用了动态 SQL只插入非空字段避免数据库NOT NULL字段被传成null时直接报错insert idinsertSelective parameterTypecom.example.sports.entity.Reserve INSERT INTO field_reserve trim prefix( suffix) suffixOverrides, if testuserId ! nulluser_id,/if if testfieldId ! nullfield_id,/if if testslotId ! nullslot_id,/if if testreserveDate ! nullreserve_date,/if if teststatus ! nullstatus,/if create_time, /trim trim prefixVALUES ( suffix) suffixOverrides, if testuserId ! null#{userId},/if if testfieldId ! null#{fieldId},/if if testslotId ! null#{slotId},/if if testreserveDate ! null#{reserveDate},/if if teststatus ! null#{status},/if NOW(), /trim /insertselectByUserFieldSlot的 SQL 实际是对唯一索引之外的重复预约做提前拦截select idselectByUserFieldSlot resultTypecom.example.sports.entity.Reserve SELECT * FROM field_reserve WHERE user_id #{userId} AND field_id #{fieldId} AND slot_id #{slotId} AND reserve_date #{reserveDate} LIMIT 1 /select这里其实可以拆成两条 SQL一条查「自己是否重复约」一条查「该时间是否被任何人约」。但「自己是否重复约」的查询条件已经包含在唯一索引的字段里即使不查也只会让用户看到「该时段已被预约」而不是「您已预约」。所以上面的查询本质上是提升用户体验而不是防重手段。真正防重必须靠数据库索引。4. 本地部署SSMMySQL项目的配置步骤与排错4.1 环境版本与基础准备SSM 项目最稳的本地组合是 JDK 1.8、Tomcat 8.5、MySQL 8.0用 Maven 3.6 以上打包。很多同学下载到的源码里 pom.xml 用的是旧版 MySQL 驱动com.mysql.jdbc.Driver在 MySQL 8.0 下会直接报ClassNotFoundException。正确做法是把驱动换成dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency同时db.properties中的驱动类名也要改成com.mysql.cj.jdbc.Driver并加上时区参数jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/sports_field?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码allowPublicKeyRetrievaltrue是 MySQL 8.0 连接时常见的坑。如果不开第一次连接会报Public Key Retrieval is not allowed。原因在于 MySQL 8.0 默认使用caching_sha2_password认证插件客户端需要获取服务端公钥。开发环境直接加这个参数最省事。4.2 部署到Tomcat的完整命令拿到项目后我习惯先 maven 打包再部署到 Tomcat 的 webapps 目录。下面是从零启动的步骤# 1. 启动 MySQL 服务并执行项目提供的 sports_field.sql mysql -u root -p sports_field sports_field.sql # 2. 进入项目根目录跳过测试打包 mvn clean package -DskipTests # 3. 把生成的 war 包复制到 Tomcat webapps cp target/sports-field.war /path/to/tomcat/webapps/ # 4. 启动 Tomcat cd /path/to/tomcat/bin ./startup.sh # 5. 查看日志确认无异常 tail -f ../logs/catalina.out访问http://localhost:8080/sports-field/就能看到系统登录页。如果项目没有打成 war 而是 IDE 里直接右键 Run也可以但要注意 Tomcat 的 Application context 必须和数据库连接配置里的库名一致否则页面跳转会出现 404。4.3 高频报错对照与处理部署阶段的错误来来去去就那么几个把对应关系整理成一张表排查时直接按表操作。报错信息原因解决方案Unknown database sports_field没有执行建库 SQL 或库名不一致检查db.properties里的库名执行CREATE DATABASEAccess denied for user rootlocalhostMySQL 密码或权限错误确认jdbc.password或用GRANT授权Table field_reserve doesnt existSQL 中表名或字段名大小写不一致Linux 下 MySQL 表名区分大小写统一小写Invalid bound statement (not found)Mapper XML 没有被 Spring 扫描检查mapper-locations配置XML 放在资源目录同路径Failed to configure a DataSourceSpring Boot 风格配置混入 SSM确认使用的是spring.xml手动配置数据源而不是 Boot 的自动配置Server returns invalid timezoneMySQL 时区问题URL 增加serverTimezoneAsia/Shanghai这里要额外提醒有些网上下载的 SSM 项目自带一个sql文件夹但里面的 SQL 用了CREATE TABLE IF NOT EXISTS如果你之前建过表但结构不一致执行时不会报错可后面查询一直异常。建议在导入 SQL 前先DROP TABLE IF EXISTS或者直接连到一个新建的数据库里。4.4 用Navicat验证数据而不是只靠页面部署成功后不要只盯着页面看预约是否成功还要用 Navicat for MySQL 直接查表。SSM 系统最常见的问题是页面显示「预约成功」但field_reserve表里没有数据或者user_id是 0。这是因为实体类的id属性没有正确封装或者 MyBatis 的resultType字段名和数据库列名不一致。用 Navicat 执行SELECT * FROM field_reserve ORDER BY id DESC LIMIT 5;能直接看到插入语句的真实效果。这比在 Java 代码里打断点更快。5. 预约冲突在MySQL层用唯一索引兜底并发5.1 为什么Java代码锁不住请求很多学生在做这个系统时会写这样的代码查field_reserve有没有记录没有就插入。单线程没问题但两个请求同时到达时两个线程都「查到没有记录」然后都去插入最终出现两条同场地同时段的数据。就算给 Service 方法加synchronizedTomcat 集群下也没用因为每个 JVM 的锁是独立的。正确做法是把约束放到 MySQL利用 InnoDB 的唯一索引在提交时强制检查。UNIQUE KEY uk_field_slot_date (field_id, slot_id, reserve_date)创建后第二个插入请求会主键冲突。MyBatis 执行插入时抛出DuplicateKeyExceptionController 捕获后返回友好提示。整个过程不需要SELECT ... FOR UPDATE也不需要悲观锁性能损耗最小。5.2 用「状态唯一索引」实现审核周期防重场地管理系统通常有「待审核」状态。如果用户提交预约后审核尚未通过另一个用户看到同一时段仍可预约就会产生两条待审核记录。唯一索引此时会拒绝第二条这未必符合业务预期——有些学校允许先提交占位再由管理员选择通过哪一条。如果希望「待审核不占位通过后才锁定时段」就不能用固定字段的唯一索引而应该引入「排他字段」技巧。MySQL 8.0 中可以用生成列实现条件唯一约束ALTER TABLE field_reserve ADD COLUMN field_slot_date_exclusive INT GENERATED ALWAYS AS ( IF(status 1, CONCAT(field_id, _, slot_id, _, reserve_date), NULL) ) STORED, ADD UNIQUE INDEX uk_exclusive_reserve (field_slot_date_exclusive);这个生成列只有在状态为 1已通过时才生成非 NULL 值唯一索引只对非 NULL 生效。这样多个待审核记录可以共存但一旦有一条记录被管理员通过其他同场地同时段的待审核记录再想通过就会冲突。这个方法比在 Service 层写一堆if判断要省事得多也是在高并发的预约场景里经常使用的一种「部分唯一索引」替代方案。5.3 预约成功后的性能检查方式部署完成后可以用 MySQL 的EXPLAIN检查关键查询是否走索引EXPLAIN SELECT * FROM field_reserve WHERE field_id 1 AND slot_id 2 AND reserve_date 2025-06-01;输出结果中key列应该是uk_field_slot_date或idx_field_slot_datetype至少是ref。如果看到ALL说明查询是全表扫描索引没有生效。常见的失效原因是reserve_date列是DATETIME类型但 Java 层传入的字符串直接拼接进了 SQL导致 MySQL 做隐式转换。解决方式是用#{reserveDate}参数绑定MyBatis 会按java.sql.Date处理避免转换。最后再确认一下SHOW INDEX FROM field_reserve中的Cardinality值不是全部为 1否则说明表里样本数据太少MySQL 优化器觉得走索引不如全扫这在实际数据量大后会自动纠正不用强行通过FORCE INDEX干预。本文还有配套的精品资源点击获取
返回列表