ARTICLE DETAIL

资讯详情

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

SSM+MySQL开放性实验室预约系统:数据库设计与并发控制实战

SSM+MySQL开放性实验室预约系统:数据库设计与并发控制实战 简介这是一份基于SSM框架与MySQL的开放性实验室管理系统毕设资源适合JavaWeb学习者、毕业设计学生及需要快速搭建实验室管理系统的开发者。系统以后端SpringSpringMVCMyBatisMaven搭配前端Vue、CSS、JS覆盖用户登录注册、个人中心、用户/教师管理、实验室管理、实验室预约、实验项目、学生实验、公告管理等功能并按管理员、用户、教师三类角色划分权限。资源包共497个文件约25.51MB含127个Java源码、39个Vue页面、18个XML配置、2个SQL数据库脚本及1个doc毕业论文文档另有SVG图标、CSS、JS等前端素材支撑界面源码已经严格调试支持IDEA/Eclipse配合MySQL5.5以上即可运行目录清晰便于二次开发。目前已有143人学习下载。资源包同时提供项目源码、数据库脚本与毕业论文既能作为毕业设计完整素材也能用于理解SSM框架整合、权限角色设计及预约业务的数据流转方便在此基础上扩展实验管理相关功能。1. 实验室总是“看着开放实际进不去”SSMMySQL 开放性实验室管理系统到底解决什么实验室晚上八点之后基本都是空着的学生想进去做实验又没门禁权限老师又不想天天跑去开门——这是我第一次听到“开放性实验室管理系统”这个词时甲方描述的真实场景。所谓开放性就是不再按固定课表把学生塞进时间格子而是让实验室在开放时段里可以被预约、被自主使用、被记录回溯。基于 SSMSpring SpringMVC MyBatis和 MySQL 做这样一套系统核心不是把 CRUD 写完而是把“开放时段、预约申请、审批排程、自助签到、设备借用”这条链路做成不冲突、可统计、可验收的流程。适合拿来做 Java 毕设也适合刚接触 SSM 的新手把 Spring 容器、MyBatis 映射和 MySQL 事务处理在一个完整项目里串起来。难点不在增删改查而在于“同一间实验室、同一个时段不能被两拨人同时约走”以及“你怎么让管理员放心地把门打开”。2. 数据库先行开放性实验室的表怎么设计才经得起预约和统计2.1 用户、实验室、设备三张基础表别把“开放性”做成课表系统如果一开始就把实验室表设计成“第几节、星期几、哪个班”后面做预约和时间统计会非常别扭。开放性实验室的资源模型不是课程格子而是一段“可预约的空闲时间”。所以我会先建三张基础表用户表、实验室表、设备表。用户表里的 role_type 区分管理员、教师、学生这是整个权限判断的根。实验室表里除了实验室编号、名称、座位数必须有 open_start 和 open_end 两个时间字段代表这个实验室每天对外开放的窗口比如 18:00 到 22:00。设备表挂在实验室下面一个实验室可以有多台设备用 lab_id 做逻辑归属。另外每个基础表都放一个 is_open 或 status 字段而不是直接删记录这样历史数据不会断后面写论文统计使用率时才有据可查。CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 登录密码(MD5带盐), real_name VARCHAR(32) NOT NULL COMMENT 真实姓名, role_type TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员/2教师/3学生, dept VARCHAR(64) COMMENT 院系/部门, phone VARCHAR(20), email VARCHAR(64), status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用/0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;username 必须建唯一索引这是第一个容易翻车的细节。登录时 MyBatis 的 selectOne 如果查到两行会直接抛 TooManyResultsException而且数据脏了以后很难发现是哪里写重了。密码用 MD5 加盐是毕设项目里的常见做法如果这是真实公网系统建议至少换 BCrypt写论文时照实写“通过 MD5 加盐存储”即可导师不会在这个点上卡你。2.2 预约表 status 字段是系统的“命门”从 0 到 5 的状态流lab_info表的关键是开放时段的类型不能把 “18:00-22:00” 存成一个字符串。用 TIME 类型的好处是查询“当前还有哪些实验室开门”时可以直接拿CURTIME()和 open_start、open_end 比较不需要先截字符串再转类型。下面是我常用的建表语句CREATE TABLE lab_info ( lab_id INT PRIMARY KEY AUTO_INCREMENT, lab_no VARCHAR(16) NOT NULL COMMENT 实验室编号如 A-302, lab_name VARCHAR(64) NOT NULL, seat_count INT NOT NULL DEFAULT 40, campus VARCHAR(32), building VARCHAR(32), floor VARCHAR(8), open_start TIME NOT NULL DEFAULT 18:00:00, open_end TIME NOT NULL DEFAULT 22:00:00, is_open TINYINT NOT NULL DEFAULT 1 COMMENT 是否对外开放 1开/0关, description VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_lab_no (lab_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实验室表;预约表是整个系统里最重要的一张表。它的状态字段不能随意设计否则后面做审核、统计、过期处理都要连带返工。我给 lab_appointment 设计的字段和索引如下CREATE TABLE lab_appointment ( appt_id INT PRIMARY KEY AUTO_INCREMENT, lab_id INT NOT NULL COMMENT 预约实验室ID, user_id INT NOT NULL COMMENT 申请学生ID, appt_date DATE NOT NULL COMMENT 预约日期, time_start TIME NOT NULL COMMENT 预计开始时间, time_end TIME NOT NULL COMMENT 预计结束时间, purpose VARCHAR(255) COMMENT 实验内容, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核/1已通过/2已拒绝/3已取消/4已使用/5已过期, approver_id INT COMMENT 审批人ID, approve_time DATETIME COMMENT 审批时间, remark VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_appt_lab_date (lab_id, appt_date), KEY idx_appt_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实验室预约表;status 用数字而不是中文字符串因为中文字符串一旦改名历史数据全部要跟着迁。我通常在代码里写一个常量类AppointmentStatus.PENDING0、APPROVED1、REJECTED2、CANCELED3、USED4、EXPIRED5。这样在 Java 代码和 SQL 里都写数字不会出现“已通过”和“审核通过”这种同义不同词导致统计错乱的问题。2.3 搭建数据模型物理外键不要建逻辑外键要靠索引和应用层网上下载的源码包里很多表没有物理外键。这不是偷懒而是企业里更常见的做法。物理外键在删除和分库分表时非常碍事学生误操作一次就弹“违反外键约束”的报错排查起来很痛苦。我的做法是表与表之间只建索引不建 FOREIGN KEY在 Service 层做逻辑校验——插入预约前先确认 lab_id 对应的 lab_info 存在且 is_open1再继续后面的时间冲突判断。时间重叠判断的核心条件是time_start #{endTime} AND time_end #{startTime}这个条件成立说明两条预约在时间线上有交集。需要特别注意的是冲突查询必须带上status IN (0,1,4)因为已拒绝、已取消、已过期的预约不占资源而已使用4的时段已经实际发生过同样不能再被预约。统计报表尽量不要跨表 JOIN 太多层。使用记录表 lab_usage_record 里冗余 user_id、lab_id 字段报表 SQL 就能少关联两张基础表查询速度快很多。如果导师要求论文里体现 MySQL 存储过程可以把“按周统计使用率”的查询封装成一个存储过程作为加分项但系统正常运行不依赖存储过程这一点你要心里有数。3. 搭建 SSM 骨架Spring 管业务、MyBatis 管 SQL 的工程长什么样3.1 从 pom 到 web.xmlSSM 整合的依赖与前端控制器拿到一个新解压的 SSM 项目我第一件事不是看代码而是打开 pom.xml 和 web.xml确认 Spring 版本、JDK 版本、Tomcat 版本三者是否能对上。Spring 如果用了 6.x最低要求 JDK 17但大多数毕设开发机还是 JDK 8启动时各种 ClassNotFoundException 会接踵而来。比较稳妥的组合是 JDK 8 Tomcat 8.5/9 Spring 5.x MyBatis 3.5.x。web.xml 里的 DispatcherServlet 也是重灾区servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingurl-pattern 必须用/不能写成/*。写成/*会把 JSP 也拦截进 DispatcherServlet页面直接变成下载文件或 404。这个错误在网上下载的源码包里出现频率极高因为原作者可能一直在用某个特定 Tomcat 版本换一个环境就炸。3.2 用 MapperScannerConfigurer 把 Dao 接口和 XML 映射文件串起来SSM 整合的经典姿势是Dao 层只写接口不写实现类通过 MapperScannerConfigurer 把接口代理对象注册进 Spring 容器。核心配置如下context:component-scan base-packagecom.lab.service / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lab_open?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghaiamp;useSSLfalse/ property nameusername valueroot/ property namepassword value123456/ property nameinitialSize value5/ property namemaxActive value20/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.lab.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ property namelazyLoadingEnabled valuetrue/ property nameaggressiveLazyLoading valuefalse/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.lab.dao/ /bean这里几个参数直接决定你能不能跑起来。driverClassName 在 MySQL 8.0 必须是com.mysql.cj.jdbc.Driver老驱动才是com.mysql.jdbc.Driver用错报 ClassNotFoundException。url 里的amp;是 XML 转义后的如果你把连接串放到 properties 文件里直接写就行。mapUnderscoreToCamelCase 必须开数据库字段 lab_id 才能自动映射成实体里的 labId不开的话查询结果全是 null还找不到原因。注意如果这套配置运行在 MySQL 8.0 上serverTimezoneAsia/Shanghai 不能省略否则启动时会报“The server time zone value is unrecognized”。这是 MySQL 8.0 安装配置教程里出现率最高的报错之一。3.3 三层代码如何分工Controller 只做参数转换Service 只做事务SSM 项目最重要的不是代码多花哨而是分层清晰。Controller 层只负责接收参数、调 Service、返回 ModelAndView 或 JSONService 层承载业务逻辑和事务Dao 层只写接口和 SQL 映射。网上下载的“源码笔记”项目里经常看到把 SQL 写在 Controller 里的操作这种代码后期连原作者自己都维护不了。Service 层的事务要这样理解Spring 默认只对 RuntimeException 回滚如果 Service 方法抛的是受检异常事务不会自动回滚。这是 java 开发工程师面试题里很喜欢考的点。我一般不在 Service 里声明受检异常统一抛自定义的 BusinessException 继承 RuntimeException或者在 Transactional 里显式写rollbackFor Exception.class。还有 MyBatis 里#{}和${}的区别#{}生成预编译参数占位符能防 SQL 注入${}是字符串替换有注入风险。但排序字段和表名只能用${}这时候必须在代码里做白名单校验不能让前端把任意字段名传进来再拼进 ORDER BY。否则就是给攻击者留了个口子。这个知识点同样常出现在 SSM 面试八股里它的实际意义就在这里。4. 预约审批和并发控制让同一天同一间实验室不冲突4.1 预约流程的状态流转从学生申请到管理员审批正常流程是这样学生填写预约时间、选择实验室、填实验用途提交后状态为 PENDING0。管理员登录后台看到待审核列表通过后变 APPROVED1拒绝则变 REJECTED2并填写理由。学生也可以在审核前取消状态变 CANCELED3。预约当天学生签到后系统把状态置为 USED4过了预约日期且没签到则置为 EXPIRED5。这个流程里最忌讳的是把状态更新散落在多个 JSP 或多个 Controller 里。统一收敛到 AppointmentService 后审批逻辑就是一条带条件的状态更新// AppointmentServiceImpl.java Override Transactional(rollbackFor Exception.class) public void approve(Integer apptId, Integer approverId) { // 只有当前状态是 PENDING 的预约才能被审核通过 int rows appointmentDao.updateStatus(apptId, AppointmentStatus.APPROVED, AppointmentStatus.PENDING, approverId); if (rows 0) { throw new BusinessException(预约状态已变化请刷新后重试); } }对应的 Mapper XML 长这样update idupdateStatus UPDATE lab_appointment SET status #{targetStatus}, approver_id #{approverId}, approve_time NOW() WHERE appt_id #{apptId} AND status #{currentStatus} /update为什么要带AND status #{currentStatus}因为先 SELECT 再 UPDATE 在并发情况下会互相覆盖。两个管理员同时审核同一单后提交的人会把先提交的审核结果覆盖掉。带上当前状态条件后第二个人的 UPDATE 影响行数是 0直接抛出“状态已变化”的提示。这个思想跟秒杀系统的防超卖是一样的并发控制下沉到数据库而不是只在 Java 代码里加锁。4.2 防止“超约”用一条条件 INSERT 堵住并发窗口状态更新只是审批阶段的并发问题。更隐蔽的坑在预约提交阶段两个学生同时提交同一间实验室同一时段的预约如果先查“有没有冲突”再插入两个请求同时查到“无冲突”然后都插入成功这就超约了。解决方式常见有两种。第一种是把预约天然设计成固定时间片比如只有整点和半点可约然后对lab_id appt_date period建唯一索引靠数据库唯一约束直接拒绝重复预约。这个方案最简单但灵活性差。第二种是下面这种原子插入我更喜欢用在毕设里INSERT INTO lab_appointment (lab_id, user_id, appt_date, time_start, time_end, purpose, status) SELECT #{labId}, #{userId}, #{apptDate}, #{timeStart}, #{timeEnd}, #{purpose}, 0 FROM dual WHERE NOT EXISTS ( SELECT 1 FROM lab_appointment WHERE lab_id #{labId} AND appt_date #{apptDate} AND time_start #{timeEnd} AND time_end #{timeStart} AND status IN (0, 1, 4) );这条 SQL 返回影响行数为 1 表示插入成功为 0 说明撞了时段。INSERT...SELECT 在 InnoDB 里执行时会加锁配合 NOT EXISTS 子查询基本能挡住同一时段的并发插入。需要注意如果允许预约跨天比如 22:00 到次日 1:00就不能只存 TIME 类型建议把 appt_date 拆成 start_time 和 end_time 两个 DATETIME 字段否则跨天判断会错乱。4.3 实验记录与设备归还使用时长怎么算才准确预约通过只是开始开放性实验室要做完整闭环必须把“实际使用记录”也管起来。常见做法是管理员审批通过后系统给预约单生成一个签到状态学生到实验室由管理员或门卫确认后生成一条 lab_usage_record 记录写入 checkin_time。离开时写入 checkout_time实际使用时长用 TIMESTAMPDIFF 算SELECT TIMESTAMPDIFF(MINUTE, checkin_time, checkout_time) AS actual_use_minutes FROM lab_usage_record WHERE record_id #{recordId};用 MySQL 函数算时长而不是在 Java 里手动减是考虑到服务器时区和应用容器时区可能不一致统一交给数据库算能少一个变量。统计报表时也直接在 SQL 层聚合不用把数据拉到内存再算。设备借还同样要放在事务里借出时实验室设备的 available_quantity 减一归还时加一。归还时还要防止超还也就是归还数量不能大于已借数量。可以这样写UPDATE equipment_info SET available_quantity available_quantity #{quantity} WHERE equip_id #{equipId} AND available_quantity #{quantity} quantity;影响行数为 0 说明归还数量异常直接抛业务异常。这条条件更新本质上是乐观锁的一个变种把业务校验和状态修改放在同一条 SQL 里避免查改分离带来的数据漂移。5. 避坑记录SSMMySQL 实验室系统从导入到运行的五个翻车现场5.1 MySQL 8.0 连不上serverTimezone 与驱动版本的双重陷阱现象本地明明装了 MySQL 8.0IP 和密码都对Tomcat 启动却报The server time zone value й׼ʱ is unrecognizedJDBC 连接直接失败。原因MySQL 8.0 的 JDBC 驱动要求连接串里显式指定 serverTimezone另外驱动类名也从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver。很多从网上下载的源码包还是照着 5.7 写的不改配置就启动十有八九在这里翻车。解决JDBC URL 改成jdbc:mysql://localhost:3306/lab_open?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalsedriverClassName 改成com.mysql.cj.jdbc.Driver。如果是远程 MySQL 出现 SSL 连接错误先加useSSLfalse把加密握手跳过跑通后再考虑配置证书。注意连接串放在 Spring XML 里时必须写成amp;。5.2 中文乱码URL 设置了 utf8数据库还是 latin1现象JSP 页面显示中文正常但插入数据库后变成??或者查询出来的中文是乱码。原因这往往不是单点问题而是三层叠加。JDBC URL 没加 characterEncodingMySQL 建库时默认字符集是 latin1Tomcat 接收 POST 请求时默认按 ISO-8859-1 解码。解决三层都要处理。web.xml 里加 Spring 的字符编码过滤器filter filter-nameencoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping建库时用CREATE DATABASE lab_open DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。如果库已经建好执行ALTER TABLE sys_user CONVERT TO CHARACTER SET utf8mb4;转换。定位时先执行SHOW VARIABLES LIKE character_set%;看服务端默认字符集到底是 utf8mb4 还是 latin1再决定改哪里。不要一看到乱码就只改 web.xml那是白费功夫。5.3 事务失效Service 内部方法自调用导致审批记录丢失现象管理员审批通过后预约状态正常更新但同一 Service 里的“写操作日志”或“发站内通知”就是没执行。代码看起来没问题甚至单独调那个方法又是好的。原因Spring 的事务基于 AOP 代理。Controller 调的是 Spring 生成的代理对象代理对象开启事务但 Service 内部this.sendMessage()调用的是原始对象不是代理对象Transactional 注解完全不生效。这个也是经典的 java 面试题“Spring 事务失效场景”。解决把发通知、写日志拆到单独的 NotificationServiceController 注入 AppointmentService 和 NotificationService在 Service 里通过注入的 notificationService 调用。不要用this去调带事务的方法。如果一定要在内部完成可以注入 ApplicationContext 再取代理或者用 AopContext.currentProxy()但最常见、最好读的写法还是拆 Service。5.4 JSP 里访问 MyBatis 懒加载属性Session 关闭后抓瞎现象预约列表页 JSP 里写${appt.labInfo.labName}页面报 MyBatis 懒加载异常或者关联对象直接为 null用肉眼查 SQL 又发现查询是好的。原因MyBatis 的懒加载依赖 SqlSession 仍然存活。SSM 默认 SqlSession 在 Service 方法结束后就关闭了JSP 渲染时再去触发懒加载自然拿不到数据。解决最简单的做法是列表查询时就用 JOIN 把 labInfo 查出来通过 resultMap 的 association 立即加载或者页面只显示预约里冗余的 labName 字段不访问关联对象。千万不要指望在 JSP 里靠懒加载补数据。如果非要用懒加载解决性能问题只能把一部分逻辑放到一个保持 SqlSession 打开的 Spring 管理模式里但那会引入更多坑毕设阶段不值得。5.5 Tomcat 与 JDK 版本错配启动报 ClassNotFound/NoClassDefFoundError现象项目在自己电脑能跑拷到别人电脑就起不来报ClassNotFoundException: javax.servlet.jsp.JspFactory或者NoClassDefFoundError: javax/servlet/ServletOutputStream。原因web.xml 头声明的是 Servlet 3.1却用了一个 Servlet 2.5 的 Tomcat 6或者 JDK 版本高于 8模块化后 javax 包访问行为变了老 Tomcat 直接找不到类。Spring 6.x 最低要求 JDK 17和 JDK 8 的 Spring 5.x 混搭也会爆炸。解决统一版本再谈调试。开发机器装 JDK 8Tomcat 用 8.5 或 9Spring 用 5.xMyBatis 用 3.5.x。启动前先java -version看 JDKTomcat 版本看bin/version.sh或控制台日志。不要在 JDK 17 上硬跑 Spring 5 的老项目就算勉强跑起来JSP 编译也会有一堆兼容问题。把这四者版本对齐后大多数“换电脑就挂”的问题都能消失。6. 从“能跑”到“能答辩”用三条 SQL 验证系统并给论文补上验收证据项目跑通以后最有说服力的验收材料不是截图是 SQL 能出数。答辩时导师问“这个系统到底用起来怎么样”你直接现场跑三条查询比背十页功能清单都管用。第一条验证使用率SELECT DATE_FORMAT(checkin_time, %Y-%u) AS week_no, COUNT(DISTINCT user_id) AS active_users, SUM(TIMESTAMPDIFF(MINUTE, checkin_time, checkout_time)) AS total_minutes FROM lab_usage_record WHERE checkin_time DATE_SUB(NOW(), INTERVAL 8 WEEK) GROUP BY week_no ORDER BY total_minutes DESC;第二条验证预约审批通过率SELECT COUNT(CASE WHEN status IN (1,4,5) THEN 1 END) / COUNT(*) AS pass_rate FROM lab_appointment;第三条验证设备借用排行SELECT e.equip_name, COUNT(b.borrow_id) AS borrow_times FROM equipment_borrow b LEFT JOIN equipment_info e ON b.equip_id e.equip_id GROUP BY e.equip_id ORDER BY borrow_times DESC LIMIT 5;这三条 SQL 分别能证明使用记录完整、状态流转一致、设备借还闭环正好对应论文里的“系统测试”和“数据分析”章节。如果论文想再体现一个 MySQL 存储过程亮点可以把第一条查询封装成 proc_usage_stat()存储过程里加一个参数指定统计周数也算给技术加分项。演示顺序也建议固定先导入 SQL 脚本再启动项目登录管理员账号然后演示学生提交预约、系统拒绝同一时间段的冲突预约、管理员审批通过、学生签到签退、最后跑统计报表。别直接开项目答辩现场最容易翻车的就是数据库没连上。我自己以前踩过一次答辩前一周为了演示好看加了一个冗余字段却忘了同步导出 SQL 脚本现场在新机器导入旧脚本后表结构对不上整个演示直接卡在登录页。从那以后我养成了一个习惯——凡是改动表结构立刻重新导出一次完整 SQL 脚本并在本地开一个全新数据库导入验证同时把检查清单里“源码、数据库脚本、论文截图”三份材料压在同一时间点保证论文里的截图跟当前系统行为一致。希望这个习惯对你有用也希望你在答辩时不用再经历我那次手心冒汗的翻车。本文还有配套的精品资源点击获取
返回列表