SSM实验室设备预约系统实战:从表设计到冲突检测与审批流

发布时间:2026/10/6 10:22:14
SSM实验室设备预约系统实战:从表设计到冲突检测与审批流 简介这份资源是基于SSM框架的实验室设备预约系统完整设计包面向高校计算机相关专业的毕业设计、课程设计与期末大作业场景帮助解决传统实验室设备预约效率低、易出错、难管理的问题。压缩包共1166个文件约18.63MB以html、css、js、png等前端页面与静态资源为主配合51个java源文件、31个jsp页面、42个jar依赖及sql脚本、xml配置等完整呈现前后端分离的开发结构。系统采用Spring、SpringMVC、MyBatis整合开发MySQL负责存储预约数据、用户与设备信息涵盖需求分析、系统设计、编码实现到测试的软件工程流程。已有43人学习关注。读者可据此掌握SSM整合配置、数据库表设计、预约流程实现与页面交互逻辑适合作为可运行的参考项目用于快速搭建环境、理解模块划分并完成二次开发与答辩准备。1. 从一张 Excel 排班表说起SSM 实验室设备预约系统到底解决什么问题如果你在高校实验室待过大概率见过这样的场景一台价值几十万的液相色谱仪预约靠微信群喊话谁先回复谁先用设备使用记录写在一张被咖啡渍染黄的 Excel 里月底统计机时全靠人工翻聊天记录。更麻烦的是设备冲突、超时占用、权限混乱这三件事几乎每周都在发生。基于 SSM 的实验室设备预约系统就是冲着这些具体问题去的——它把设备台账、时间段预约、审批流、使用记录四件事收进一个 Web 系统里用 Spring SpringMVC MyBatis 这套经典组合做后端前端通常配 JSP 或 Thymeleaf数据库用 MySQL。这套方案适合谁一是高校实验室管理员需要一个能落地的内部工具二是正在做课程设计或毕业设计的计算机专业学生SSM 是绕不开的经典架构三是刚入行的 Java 开发者想找一个业务闭环完整、又不至于太复杂的项目练手。它不追求高并发也不上微服务核心价值在于把「预约」这个业务逻辑讲清楚谁能约、约哪个时段、冲突怎么判、审批怎么走。把这套逻辑跑通比堆一堆花哨技术更有用。2. 预约系统的核心业务模型设备、时段、用户三张表怎么设计2.1 为什么先定业务模型再写代码很多人拿到 SSM 项目第一反应是打开 IDE 建工程结果写到一半发现表结构不对返工重来。血泪经验是预约系统的复杂度不在框架在业务模型。核心就三个实体——设备Equipment、预约时段Reservation、用户User。难点在于「时段」怎么表达是用开始时间和结束时间两个字段还是把一天切成固定节次这直接决定冲突检测怎么写。我一般推荐固定节次方案比如把一天切成 8:00-10:00、10:00-12:00 这样的时段用一张 time_slot 表存。好处是冲突检测变成简单的等值判断不用处理时间区间重叠的边界情况。代价是灵活性差一点但实验室场景下够用。2.2 三张核心表与关联关系设备表存设备基本信息和使用状态预约表是核心业务表用户表区分角色。下面给出关键字段设计省略通用字段如 create_time、update_time。-- 设备表 CREATE TABLE equipment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 设备名称, model VARCHAR(100) COMMENT 型号, location VARCHAR(200) COMMENT 存放位置, status TINYINT DEFAULT 1 COMMENT 1可用 0维修 2停用, need_approval TINYINT DEFAULT 1 COMMENT 是否需要审批 ); -- 时段表 CREATE TABLE time_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, slot_name VARCHAR(50) NOT NULL COMMENT 如 08:00-10:00, start_time TIME NOT NULL, end_time TIME NOT NULL ); -- 预约表 CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, equipment_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, reserve_date DATE NOT NULL COMMENT 预约日期, status TINYINT DEFAULT 0 COMMENT 0待审批 1已通过 2已拒绝 3已取消 4已完成, apply_reason VARCHAR(500), approve_remark VARCHAR(500), UNIQUE KEY uk_equip_date_slot (equipment_id, reserve_date, slot_id) );这里最关键的是最后那个唯一索引uk_equip_date_slot。它把「同一设备、同一天、同一时段」设成数据库层面的唯一约束意味着即使应用层并发判断出问题数据库也会兜底拒绝重复插入。这是防超约的最后一道防线比在 Java 里写 synchronized 靠谱得多。2.3 状态流转与角色划分预约状态不是随便改的得定义清楚流转路径用户提交后是「待审批」管理员审批后变「已通过」或「已拒绝」用户可主动「取消」使用完成后管理员标记「已完成」。角色上至少两种普通用户提交预约、查看自己的记录和管理员审批、管理设备、查看全部记录。如果实验室有多个可以加「超级管理员」管设备台账。提示状态字段用数字还是枚举字符串数据库存数字省空间但 Java 里一定要用枚举类映射别在代码里到处写if (status 1)否则三个月后你自己都不记得 1 代表什么。3. 用 SSM 搭起后端骨架从 Maven 依赖到第一个接口跑通3.1 Maven 依赖与版本选择SSM 的依赖版本是新手第一个坑。Spring 5.x 配 MyBatis 3.5.x 是稳定组合别去追 Spring 6它要求 JDK 17 且和旧版 MyBatis 整合方式有变化。下面是我常用的依赖清单版本号按你本地仓库实际情况调整。dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency !-- MyBatis 与整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency !-- 数据库驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies参数说明mybatis-spring的版本必须和 MyBatis 主版本匹配2.1.x 对应 MyBatis 3.5.x。Druid 连接池不是必须但它的监控面板在排查连接泄漏时很好用。MySQL 驱动 8.x 的连接 URL 要加时区参数否则启动报时区错误。3.2 Spring 与 MyBatis 整合配置整合的核心是把 SqlSessionFactory 交给 Spring 管理再用 MapperScannerConfigurer 扫描 Mapper 接口。下面这段配置放在 spring-dao.xml 里。!-- 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lab_booking?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyour_password/ /bean !-- SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.lab.entity/ /bean !-- 扫描 Mapper 接口 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.lab.mapper/ /bean逻辑说明mapperLocations指定 XML 映射文件位置typeAliasesPackage让实体类在 XML 里可以用短名。MapperScannerConfigurer 会自动为接口生成代理实现你不需要写实现类。注意amp;是 XML 里的转义写法直接写会导致解析失败。3.3 第一个接口查询可预约设备列表从最简单的查询入手验证整合是否成功。Controller 层用RestController返回 JSONService 层做业务Mapper 层查库。RestController RequestMapping(/api/equipment) public class EquipmentController { Autowired private EquipmentService equipmentService; GetMapping(/available) public Result listAvailable(RequestParam String date, RequestParam Long slotId) { // 查询指定日期和时段下状态可用且未被预约的设备 ListEquipmentVO list equipmentService.listAvailable(date, slotId); return Result.success(list); } }参数说明date是预约日期格式 yyyy-MM-ddslotId是时段 ID。Service 层的 SQL 逻辑是「设备状态为可用」且「不存在该日期该时段的有效预约」。这里用 NOT EXISTS 子查询比 LEFT JOIN 更直观也避免重复行。select idlistAvailable resultTypecom.lab.vo.EquipmentVO SELECT e.* FROM equipment e WHERE e.status 1 AND NOT EXISTS ( SELECT 1 FROM reservation r WHERE r.equipment_id e.id AND r.reserve_date #{date} AND r.slot_id #{slotId} AND r.status IN (0, 1) ) /select注意status IN (0, 1)只排除待审批和已通过的预约已拒绝和已取消的不占时段。这个细节如果写错用户会发现明明没人用的设备显示被占用这就是典型的业务逻辑翻车点。4. 预约冲突检测与审批流把并发和状态机写对4.1 冲突检测的三层防线预约系统最核心的逻辑就是「别让两个人约到同一台设备的同一时段」。我一般布三层防线第一层是前端提交时禁用已占用时段提升体验但不安全第二层是 Service 层查询后判断覆盖大部分场景第三层是数据库唯一索引兜底并发。三层缺一不可只靠第二层在并发下必然出问题。Service 层的检测代码要放在事务里并且用SELECT ... FOR UPDATE锁住相关行或者直接依赖唯一索引捕获异常。Service public class ReservationServiceImpl implements ReservationService { Autowired private ReservationMapper reservationMapper; Override Transactional(rollbackFor Exception.class) public Result submit(ReservationDTO dto) { // 先查是否已有有效预约 int count reservationMapper.countActive( dto.getEquipmentId(), dto.getReserveDate(), dto.getSlotId()); if (count 0) { return Result.fail(该时段已被预约); } try { reservationMapper.insert(dto); } catch (DuplicateKeyException e) { // 唯一索引兜底并发下第二个请求会走到这里 return Result.fail(手慢了该时段刚被占用); } return Result.success(); } }参数说明countActive统计状态为 0 或 1 的预约数。DuplicateKeyException是 Spring 对唯一约束冲突的封装捕获它比在 SQL 里写 ON DUPLICATE KEY 更符合业务语义。注意事务注解的rollbackFor Exception.class默认只回滚运行时异常加上它更保险。4.2 审批流的状态机实现审批不是简单改个字段要校验当前状态是否允许该操作。比如只有「待审批」才能被审批「已通过」才能被标记完成。我习惯把状态流转规则集中写在一个方法里而不是散落在各个 Service。public enum ReservationStatus { PENDING(0, 待审批), APPROVED(1, 已通过), REJECTED(2, 已拒绝), CANCELLED(3, 已取消), COMPLETED(4, 已完成); private final int code; private final String desc; // 构造和 getter 省略 public static boolean canApprove(int current) { return current PENDING.code; } public static boolean canCancel(int current) { return current PENDING.code || current APPROVED.code; } }审批接口先调canApprove校验不通过直接返回错误。这样即使前端传了非法状态后端也能挡住。审批通过后如果设备需要审批还要发通知——简单场景下写一条站内消息记录即可不必上消息队列。4.3 分页查询与条件筛选管理端需要按设备、用户、状态、日期筛选预约记录并分页。用 PageHelper 插件最省事但要注意它和 MyBatis 版本的兼容性。Override public PageInfoReservationVO pageQuery(ReservationQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListReservationVO list reservationMapper.selectByCondition(query); return new PageInfo(list); }参数说明pageNum从 1 开始pageSize建议限制上限比如 100防止有人传 10000 把数据库拖垮。selectByCondition的 XML 里用if动态拼条件注意reserve_date的范围查询要用gt;和lt;转义。注意PageHelper 的startPage必须紧挨着查询语句中间不能插入其他数据库操作否则分页会作用到错误的 SQL 上。这是最常见的翻车点之一。5. 避坑与排查SSM 预约系统上线前必须过的五道坎5.1 中文乱码从数据库到页面的全链路排查现象设备名称在数据库里正常页面上显示问号。原因通常是字符集不统一。解决数据库建库时指定utf8mb4连接 URL 加characterEncodingutf8Tomcat 的 server.xml 里 Connector 加URIEncodingUTF-8JSP 页面首行% page contentTypetext/html;charsetUTF-8 %。四处都对了才不会乱码。如果用了ResponseBody返回 JSON还要在 spring-mvc.xml 里配StringHttpMessageConverter的默认编码。5.2 事务不生效Service 自己 new 了自己现象预约插入失败后前面的查询操作没有回滚。原因在 Controller 里手动new ReservationServiceImpl()这样创建的对象不受 Spring 事务代理管理。解决始终用Autowired注入让 Spring 容器管理 Bean。另外同类内部方法调用也会绕过代理比如this.submit()调this.otherMethod()事务注解不生效。需要的话用 AopContext.currentProxy() 或拆到另一个 Bean。5.3 唯一索引冲突没捕获用户看到 500 错误页现象并发预约时第二个用户看到 Whitelabel Error Page。原因DuplicateKeyException 没被捕获直接抛到容器。解决在 Service 层 try-catch 捕获并返回友好提示或者在全局异常处理器里统一处理。我一般写一个ControllerAdvice的 GlobalExceptionHandler把 DuplicateKeyException 映射成「该时段已被占用」的提示。5.4 时间字段差 8 小时时区配置漏了一处现象预约日期存进去是对的但查询出来少了 8 小时。原因MySQL 驱动 8.x 默认用 UTC 时区而服务器在东八区。解决连接 URL 加serverTimezoneAsia/Shanghai同时确认 MySQL 的time_zone参数。如果用了JsonFormat注解pattern 和 timezone 都要写全比如JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。5.5 分页查询慢没走索引的全表扫描现象预约记录到几万条后管理端列表加载要好几秒。原因selectByCondition里对reserve_date用了函数比如DATE_FORMAT导致索引失效。解决用范围查询代替函数reserve_date #{start} AND reserve_date #{end}并在reserve_date和equipment_id上建联合索引。用 EXPLAIN 看执行计划type 是 ALL 就说明全表扫描了。6. 让预约系统更好用几个我踩过坑才总结出的进阶技巧第一个技巧是「预约日历」的渲染优化。前端如果用表格逐格渲染一个月的数据设备一多页面就卡。我的做法是后端一次性返回该月所有预约记录前端用对象做索引key 是设备ID_日期_时段ID渲染时直接查对象避免嵌套循环。这个改动让日历加载从 3 秒降到 300 毫秒。第二个技巧是审批超时自动处理。实验室管理员不可能随时在线待审批的预约如果一直挂着时段就被占死了。我加了一个定时任务每天凌晨扫描超过 24 小时未审批的记录自动置为「已拒绝」并释放时段。用 Spring 的Scheduled注解就能实现cron 表达式写0 0 1 * * ?。注意定时任务要加分布式锁否则多实例部署时会重复执行。第三个技巧是操作日志。预约系统涉及审批和取消出了纠纷要能追溯。我一般用 AOP 切面拦截 Service 的增删改方法把操作人、操作类型、目标 ID、时间写进日志表。切点表达式用annotation自定义注解更精准避免把查询也记进去。Aspect Component public class OperationLogAspect { Around(annotation(com.lab.annotation.LogOperation)) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); // 异步写入日志表避免影响主流程 logService.asyncSave(pjp, result, System.currentTimeMillis() - start); return result; } }参数说明Around环绕通知能拿到方法参数和返回值pjp.proceed()执行原方法。日志写入用异步线程池别在主线程里同步写库否则每次操作都多一次数据库往返。最后一个习惯每次改完预约逻辑我都会手动构造三个测试用例——同一用户重复约同一时段、两个用户并发约同一时段、审批后再取消。这三个用例覆盖了最容易出问题的边界。跑通了再提交代码比事后救火省心得多。希望帮到你。本文还有配套的精品资源点击获取