
走进实验室的时候管理老师又在翻那个被写满的登记本学生站在旁边排队等着签字。这种场景在高校里太常见了空闲时段实验室没人用热门时段却挤成一团管理员根本没法实时知道每个实验室此刻是空是满学生想用实验室要么跑到现场看要么靠口头问。要做的事其实很清楚把线下那套“填表—找人签字—排队等待”的流程搬到线上变成“查空闲—在线预约—自动审批”。这个基于Java SpringBoot SSM的实验室共享预约平台就是奔着这个痛点去的。我陆陆续续帮人改过几年这种高校毕设项目也亲手把其中一套从零搭到上线演示过。这套系统的技术选型很典型基于当前毕业生最熟悉的 SpringBoot 整合 SSM 三层架构后端用 Java 写业务逻辑前端走模板引擎或普通 Web 页面数据库用 MySQL 存用户、实验室、预约记录这些核心数据。它能解决的问题概括起来就三个一是把实验室资源可视化地展示给所有有预约需求的人二是将审批流程从线下改成线上减少人为沟通成本三是利用数据库层面的约束和业务校验防止同一个实验室同一时间被多人占用。这篇文章适合三种人看一是正在做 Java 课设/毕设、想找一个“能跑起来又讲得清”的项目模板的同学二是学校实验室或实训中心的管理员想了解这类系统能做到什么程度三是刚接触 SpringBoot SSM 整合想通过一个完整案例把前后端、数据库、权限串起来理解的初级开发者。1. 整体设计思路与技术选型解析1.1 为什么是 SpringBoot SSM 的组合先把这个概念说透。SSM 指的是 Spring SpringMVC MyBatis这是过去几年学校里教得最多的 Java Web 组合。Spring 管对象和业务逻辑SpringMVC 管请求路由MyBatis 管数据库操作各司其职。而 SpringBoot 是在这三者之上做了一层自动配置它并没有把 SSM 丢掉而是把 SSM 的配置过程简化到极致不用写繁琐的 XML 配置不用纠结 Jar 版本冲突一个启动类跑起来就完事。我见过不少同学纠结“到底选 SpringBoot 还是选 SSM”这个纠结没必要。SpringBoot 本身就是基于 SSM 的封装项目里照样有Controller、Service、Mapper三层照样写 MyBatis 的 Mapper 接口和 XML 映射文件。可以理解为SSM 是骨架SpringBoot 是让骨架自动拼装的人。在简历和论文里写“基于 SpringBoot SSM 框架”是严谨的因为这俩本来就是套着用的关系。1.2 单体应用的选择理由说实话现在动不动就聊微服务、Spring Cloud、分布式事务但对实验室预约这种场景单体应用是最合适的。原因很实在用户量级就是一所学校几百到几千人没什么高并发压力业务集中度高登录、预约、审批、统计全在一个应用里搞定部署和调试成本低一个 Jar 包扔到服务器或本地跑个 IDEA 就行毕设答辩阶段能把核心逻辑讲透比堆砌一堆用不上的中间件更得高分。所以这个项目架构我采用的是标准的单体分层设计Controller 层负责接收请求和返回 JSONService 层处理预约逻辑、权限校验、状态流转等业务规则Mapper 层通过 MyBatis 操作 MySQL 数据库。前端没有过度拆分用模板引擎渲染页面加 jQuery/Ajax 做异步交互简单直接。1.3 角色与功能模块梳理平台涉及三类用户功能点是围绕角色来铺的角色核心功能学生/教师注册登录、浏览实验室列表、查看空闲时段、预约实验室、取消预约、查看审批状态管理员用户管理、实验室信息维护、开放时间设置、预约审批/驳回、公告发布、使用记录统计超级管理员管理员账号分配、系统参数配置、数据备份模块划分上我习惯这样拆用户模块、实验室管理模块、预约管理模块、审批模块、公告模块、统计模块。如果只想做个基础毕设把前四个做扎实就够了统计和公告属于加分项优先级往后放。功能不要贪多把预约这条主链路跑通比做十个没深度的模块有价值得多。2. 数据库设计与核心流程拆解2.1 数据表关系设计这个项目的数据库是典型的“用户-资源-预约记录”三张核心表结构。我直接说设计思路表名和字段名按通用习惯来方便理解和改造。第一张是tb_user用户表字段主要有id、username、password、real_name、phone、role0 管理员、1 教师、2 学生、status启用/禁用、create_time。密码存的是 MD5 或 BCrypt 加密后的值不要明文存这是底线。第二张是tb_lab实验室表字段有id、lab_name、location、capacity、equipment设备描述、manager责任人、status开放/维护中、open_start和open_end开放时间段。有的系统会把“实验室的可预约时间段”单独拆表因为不同实验室可能开放时间不一样拆开更灵活。但毕设阶段不拆也能讲清楚看你自己决定。第三张是tb_reservation预约记录表这是整个系统的核心。字段至少要包含id、user_id、lab_id、reservation_date预约日期、start_time、end_time、purpose用途说明、status0 待审核、1 已通过、2 已拒绝、3 已取消、4 已完成、audit_user审批人、audit_time、create_time。辅助表还有tb_notice公告表、tb_equipment实验室设备表如果实验室设备要单独管理才需要。整体表数量控制在 5 到 7 张是最舒服的既能满足功能又能把 ER 图画清楚。2.2 预约流程与状态流转设计预约的核心流程听起来简单学生选实验室选时间填用途提交管理员审核通过或拒绝学生看到状态结果。但实际设计时要细想状态流转。我一般把预约状态定义为这样一条线待审核0→ 已通过1或已拒绝2待审核状态学生可以主动取消3已通过的预约到达预约日期后标记为已完成4。这里有两个容易忽略的点。第一个是“取消”的时机。如果已经通过了学生还能不能取消答案是应该允许否则学生临时有事就没办法释放资源。所以我在设计时规定只要预约还没到开始时间学生都可以取消过了开始时间就不能取消了只能算“爽约”管理员在后台可以酌情把这种状态改成已完成或违约。第二个是“完成”的触发方式。可以通过定时任务每天检查一次把status1且reservation_date 当前日期的记录批量置为status4。也可以用懒更新的方式查询用户预约记录时遇到已经过期的已通过记录顺手更新状态。定时任务虽然看着正规但毕设阶段我推荐懒更新原因是你不需要为了一个“值班状态刷新”引入 Quartz 或 Spring Task 的复杂度还能少写不少代码。2.3 时间冲突校验的真正做法实验室预约最大的坑就是时间冲突。两个学生同时预约同一实验室的同一时间段必须有一个被拦下来。这个校验不能只靠前端判断后端和数据库都得防。我的做法分两层第一层是业务层查询校验。在提交预约的 Service 方法里先查数据库看同样的lab_id、reservation_date下有没有已经存在的、状态为“待审核”或“已通过”的记录并且时间段有重叠。时间重叠判断的条件是新预约的开始时间小于已有预约的结束时间且新预约的结束时间大于已有预约的开始时间。这句 SQL 条件用代码表示就是// 时间冲突判断的核心条件 // 已有的预约: 开始时间 新预约.结束时间 并且 结束时间 新预约.开始时间 WHERE lab_id #{labId} AND reservation_date #{date} AND status IN (0, 1) AND start_time #{endTime} AND end_time #{startTime}第二层是数据库兜底。我给预约表加了一个逻辑校验让数据库在极端并发下也能挡住错误数据——通过联合唯一索引把“实验室 日期 开始时间”作为唯一键如果同一毫秒内两个人插入同一条第二个请求会抛DuplicateKeyException在 Service 层捕获并转成“该时段已被预约”的友好提示。这两层加在一起才算完整。很多同学只写了第一层自己测试没问题但答辩时老师一句“如果两个人同时提交怎么办”就被问住了。有了数据库唯一索引这个答案这个问题就闭环了。3. 核心功能实现与难点攻坚3.1 预约提交的并发防重实现上面提到了并发问题这里把代码逻辑展开。预约提交的 Service 层方法我用Transactional注解包起来数据源层配合唯一索引做兜底。可以理解为了两层保险。完整的关键代码如下Service public class ReservationServiceImpl implements ReservationService { Autowired private ReservationMapper reservationMapper; Override Transactional(rollbackFor Exception.class) public Result submitReservation(ReservationDTO dto, Long userId) { // 1. 检查用户在预约日期是否已有未结束的预约 int userConflict reservationMapper.countUserReservationOnDate( userId, dto.getReservationDate(), dto.getStartTime(), dto.getEndTime()); if (userConflict 0) { return Result.error(你在这个时间段已有预约请勿重复预约); } // 2. 检查实验室在目标时间段是否被占用 int labConflict reservationMapper.countLabTimeConflict( dto.getLabId(), dto.getReservationDate(), dto.getStartTime(), dto.getEndTime()); if (labConflict 0) { return Result.error(该实验室此时间段已被预约请更换时间或实验室); } // 3. 构建预约记录并插入 Reservation record new Reservation(); record.setUserId(userId); record.setLabId(dto.getLabId()); record.setReservationDate(dto.getReservationDate()); record.setStartTime(dto.getStartTime()); record.setEndTime(dto.getEndTime()); record.setPurpose(dto.getPurpose()); record.setStatus(ReservationStatus.PENDING); try { reservationMapper.insert(record); } catch (DuplicateKeyException e) { // 唯一索引兜底并发场景下极端冲突在这里被拦截 return Result.error(该时段预约冲突请刷新后重试); } return Result.success(预约提交成功等待管理员审核, record.getId()); } }这段逻辑的关键点有两个一是Transactional保证“查询冲突”和“插入记录”在同一个事务里避免查到没冲突后插入前被别人抢先二是DuplicateKeyException的捕获必不可少这是最后一道防线。我在实测中专门模拟过并发用两个浏览器无痕窗口同时点提交确实有极少数情况第一层查询都放行了全靠唯一索引挡住。3.2 权限控制的简与繁实验室预约平台里学生和管理员能做的事完全不同。实现权限控制常见两种方案一是引入 Spring Security 或 Shiro二是自己写拦截器加注解。我的建议是如果毕设想凸显技术含量用 Spring Security JWT 的组合如果想让项目更容易跑通、更快出成果用拦截器就够了。这个项目里我采用的是更轻的拦截器方案。实现方式很简单定义一个AuthInterceptor在preHandle方法里从 session 或请求头中取出登录信息检查请求的 URL 是否在白名单之外以及当前用户的角色是否有权限。配合一个RequireRole(admin)自定义注解标注在需要管理员权限的 Controller 方法上拦截器里通过反射读取注解做判断。整个实现比引入 Spring Security 少写很多配置但讲起来也完全能自圆其说。有一点要特别提醒前端的按钮隐藏不等于权限控制。真正控制权限必须在后端接口层面拦截。学生即便手动调管理员的删除接口后端也会返回“无权限”这才是合格的设计。3.3 前端交互方案的取舍页面交互这块我见过几种做法纯模板引擎服务端渲染所有页面防抖、刷新逻辑简单但页面切换会闪烁模板引擎 jQuery/Ajax局部刷新预约提交和状态查询不用整页跳转开发效率高Vue Element UI 前后端分离界面好看但要处理跨域、Token 存储、异步加载等一系列问题。毕设项目我一般推荐第二种。不用额外启动一个前端服务不用纠结跨域代码量也更少。人事、项目评审时更容易被看明白。整个项目结构如下src/main/java/com/example/labreservation ├── controller // Controller层接收请求 │ ├── UserController.java │ ├── LabController.java │ └── ReservationController.java ├── service // Service层业务逻辑 │ └── impl ├── mapper // MyBatis的Mapper接口 ├── entity // 实体类 ├── interceptor // 自定义拦截器 ├── config // 配置类 └── common // 通用返回对象、常量、异常处理 src/main/resources ├── mapper // MyBatis的XML映射文件 ├── static // CSS、JS、图片 ├── templates // Thymeleaf模板页面 └── application.yml // 核心配置前端页面我用 Thymeleaf 做服务端渲染页面里的预约表单通过 jQuery 的$.ajax提交到后端接口页面局部刷新。这样做的好处是整个项目的代码量可控学生能看明白每一行代码在干什么。3.4 别忘了写“取消预约”的连带逻辑取消预约看似简单就是改一个状态但如果预约通过后实验室的“开放时间”被别人看到这里会牵扯到一个连带设计预约通过后要不要在界面上标记“该时段已占用”我的做法是查询实验室空闲时间段时把“已通过的预约 待审核的预约”全部排掉。这样做的理由是就算学生还没真正进去用也该让其他人看到这个时段“被占用了”否则就会出现一个人看到有空位提交了另一个人也提交了然后第一个人被管理员驳回这种体验很差。查询空闲时间段的算法是把实验室开放时间按半小时切片遍历所有切片去掉已被预约的切片剩下的就是可预约时间。这个逻辑写起来简单跑起来直观答辩时也容易演示。4. 环境搭建、源码使用与调试技巧4.1 本地环境准备拿到这套源码之后第一步不是打开 IDEA 就敲代码而是先把环境对齐。我用的这套项目默认环境是组件版本说明JDK1.8版本太新反而容易出兼容问题Maven3.6 及以上依赖管理IDEA2020 以上版本社区版也够用MySQL5.7 或 8.0驱动选型不一样Navicat任意版本导入 SQL 脚本用项目导入后先等 Maven 把依赖下载完再去看配置文件。核心配置在src/main/resources/application.yml里主要改三个地方数据源地址、端口、MyBatis 配置。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/lab_reservation?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.labreservation.entity注意一个很常见的问题MySQL 8.0 的驱动类要写com.mysql.cj.jdbc.Driver而 5.7 可以写com.mysql.jdbc.Driver用错就会报ClassNotFoundException。另外serverTimezone必须显式设置不然插入日期时间时会差八个小时。4.2 源码导入三步走第一次导入项目时我建议按这个顺序来能少踩很多坑第一步在 Navicat 里新建数据库名字叫lab_reservation字符集选utf8mb4然后双击运行项目根目录下的sql/lab_reservation.sql脚本。这个脚本会把表结构、初始管理员账号、测试数据全部建好。第二步IDEA 里选择File - Open选中项目根目录的pom.xml以 Maven 项目方式打开。等着依赖下载完如果下载慢可以换国内镜像源。第三步改完配置文件后启动启动类LabReservationApplication.java看到 SpringBoot 启动成功日志后浏览器访问http://localhost:8080/login。初始账号我用的是admin / 123456学生测试账号student / 123456。运行不起来最常出现的问题就那么几个MySQL 服务没启动、防火墙拦了 3306 端口、配置文件的密码不对、IDEA 的 Maven 没有配置好。逐个排查十分钟之内能搞定。4.3 调试文档和 LW 的使用建议这套项目带的调试文档和 LW论文/设计文档是配套的很多人拿到后直接看正文效率很低。我建议换个顺序先跑通项目操作一遍所有功能心里有数再打开 LW 看系统设计、流程图、ER 图把“代码里的功能”和“文档里的描述”对照理解最后把 LW 里的设计图当成待补充的素材替换成你自己项目的截图或改过的思路。特别是答辩前一周强烈建议拿 LW 里的“核心功能模块设计”章节对着代码一行行走读。比如论文里写了“系统采用冲突检测算法防止时间重叠”你就去代码里找到冲突检测的 Mapper SQL理解它是怎么查重叠的。这样被问到“这里怎么实现的”时你能答得上细节而不仅仅是“会跑”。4.4 演示时最容易翻车的三个场景第一是并发预约演示。老师问“有没有处理并发”你点头说有然后现场打开两个页面同时提交。如果不提前设计演示数据很可能两个请求都提交成功了看起来像系统有 bug。正确的演示方式提前在数据库里造一条“待审核”状态的预约再演示学生提交时提示“该时段已被预约”这比现场拼手速要稳妥得多。第二是时间显示错乱。MySQL 连接串没加serverTimezoneAsia/Shanghai或者 JDK 版本不同前端显示的时间跟数据库存的时间差了八小时。演示前一定要检查前后台的日期时间显示是否一致。第三是状态流转演示顺序乱。演示时应该按照“学生提交 → 管理员通过 → 学生查看通过状态 → 设置已过期 → 状态变为已完成”的顺序走每一步都清晰可见。别上来先演示取消预约会把状态流转的完整链路打断。5. 常见问题与排查经验速查5.1 启动阶段高频报错错误现象可能原因解决方式端口被占用8080 端口被其他进程占用命令行执行netstat -ano查看占用 PID结束进程或改server.port无法连接数据库MySQL 没启动、驱动版本不匹配、密码错误依次检查 MySQL 服务、依赖版本、配置文件页面中文乱码数据库字符集不是 utf8mb4或页面未声明编码数据库和表改成 utf8mb4spring.datasource.url里加characterEncodingutf8Invalid bound statement (not found)Mapper 接口和 XML 映射文件没有绑定检查mapper-locations路径和 XML 里的 namespace 是否匹配扫描不到 Controller启动类位置放错启动类必须放在所有包的最外层第二个表格的Invalid bound statement这个问题我看着挺眼熟。它一般是MyBatis的XML里的namespace和Mapper接口的完整类名不一致或者mapper-locations的路径写错了。检查这两处基本都能解决。5.2 业务逻辑暗坑运行起来没事但业务上出问题的情况也有几个第一个是删除用户时没处理关联预约。如果一个用户已经存在预约记录甚至使用记录直接删用户会导致预约表里出现悬空引用。我给所有删除接口都加了限制只要该用户在预约表里存在记录只做禁用不做物理删除。这也是现实中管理系统的常规做法。第二个是实验室维护状态下仍然能被预约。管理员把实验室状态改成“维护中”后预约接口仍能查到并提交成功。这个 bug 我在初版里踩过。解决方式是在查询空闲实验室和提交预约时都过滤掉status0的实验室管理员维护操作才有实际意义。第三个是列表分页和条件查询的 SQL 拼接问题。如果用了分页插件 PageHelper需要注意它和自定义 SQL 的执行顺序count查询和page查询要分开写。为了避免这层麻烦这个项目的列表我建议只做条件查询 简单的 limit 分页对毕设场景完全够用。5.3 自学项目时的扩展方向如果你不满足于基础功能想往上走一步这里有三个性价比很高的扩展方向第一加入“实验室预约 门禁联动”的仿真设计。在预约通过后生成一个入场二维码管理员扫码核销这样就把“预约管理”延伸到了“到场验证”环节技术上可以引入 Google ZXing 生成二维码和扫码逻辑。第二做一个简单的使用率统计模块。按周按实验室统计各时段预约次数用表格或简单的 ECharts 柱状图展示。这个模块代码不多但很能在论文里多撑一节“系统应用效果分析”。第三增加邮件或站内信通知。预约审核通过或拒绝时给用户发送消息这里引入 Spring 的事件监听机制能体现你对解耦设计的理解而不用真的去集成复杂的消息队列。6. 最后分享一点我的体会把一套这种规模的系统从零写到能跑再用论文语言包装成一个可以答辩的完整项目我做过不止一次。如果非要说一个最重要的心得那就是不要把功能铺得太开把预约这条主链路做扎实。从提交预约、冲突校验、审批流转、取消释放、超时完成到异常情况下的并发兜底整个流程打通了这个项目就立住了。至于那些花哨的验证码、图表、第三方登录都是可以后期叠加的加分项。这套基于 Java SpringBoot SSM 的实验室共享预约平台在技术上是标准的 Web 开发练兵场三层架构、MyBatis 操作 MySQL、事务管理、事务下并发控制、拦截器鉴权、Thymeleaf 渲染每一块都是以后工作里天天要碰的东西。把这几样吃透了比追逐一个新的框架版本有用得多。如果你拿到源码后第一遍跑不起来别慌先看日志找到第一行ERROR从那里开始查。大多数问题都不是代码本身的问题而是环境和数据没对它。调通了第一遍后面就顺了。