走进实验室的时候,管理老师又在翻那个被写满的登记本,学生站在旁边排队等着签字。这种场景在高校里太常见了:空闲时段实验室没人用,热门时段却挤成一团;管理员根本没法实时知道每个实验室此刻是空是满;学生想用实验室,要么跑到现场看,要么靠口头问。要做的事其实很清楚:把线下那套“填表—找人签字—排队等待”的流程,搬到线上变成“查空闲—在线预约—自动审批”。这个基于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 层负责接收请求和返回 JSON,Service 层处理预约逻辑、权限校验、状态流转等业务规则,Mapper 层通过 MyBatis 操作 MySQL 数据库。前端没有过度拆分,用模板引擎渲染页面加 jQuery/Ajax 做异步交互,简单直接。
1.3 角色与功能模块梳理
平台涉及三类用户,功能点是围绕角色来铺的:
| 角色 | 核心功能 |
|---|---|
| 学生/教师 | 注册登录、浏览实验室列表、查看空闲时段、预约实验室、取消预约、查看审批状态 |
| 管理员 | 用户管理、实验室信息维护、开放时间设置、预约审批/驳回、公告发布、使用记录统计 |
| 超级管理员 | 管理员账号分配、系统参数配置、数据备份 |
模块划分上,我习惯这样拆:用户模块、实验室管理模块、预约管理模块、审批模块、公告模块、统计模块。如果只想做个基础毕设,把前四个做扎实就够了;统计和公告属于加分项,优先级往后放。功能不要贪多,把预约这条主链路跑通,比做十个没深度的模块有价值得多。
2. 数据库设计与核心流程拆解
2.1 数据表关系设计
这个项目的数据库是典型的“用户-资源-预约记录”三张核心表结构。我直接说设计思路,表名和字段名按通用习惯来,方便理解和改造。
第一张是tb_user用户表,字段主要有id、username、password、real_name、phone、role(0 管理员、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(用途说明)、status(0 待审核、1 已通过、2 已拒绝、3 已取消、4 已完成)、audit_user(审批人)、audit_time、create_time。
辅助表还有tb_notice公告表、tb_equipment实验室设备表(如果实验室设备要单独管理才需要)。整体表数量控制在 5 到 7 张是最舒服的,既能满足功能又能把 ER 图画清楚。
2.2 预约流程与状态流转设计
预约的核心流程听起来简单:学生选实验室选时间填用途,提交;管理员审核,通过或拒绝;学生看到状态结果。
但实际设计时要细想状态流转。我一般把预约状态定义为这样一条线:待审核(0)→ 已通过(1)或已拒绝(2);待审核状态学生可以主动取消(3);已通过的预约到达预约日期后标记为已完成(4)。
这里有两个容易忽略的点。
第一个是“取消”的时机。如果已经通过了,学生还能不能取消?答案是应该允许,否则学生临时有事就没办法释放资源。所以我在设计时规定:只要预约还没到开始时间,学生都可以取消;过了开始时间就不能取消了,只能算“爽约”,管理员在后台可以酌情把这种状态改成已完成或违约。
第二个是“完成”的触发方式。可以通过定时任务每天检查一次,把status=1且reservation_date < 当前日期的记录批量置为status=4。也可以用懒更新的方式:查询用户预约记录时,遇到已经过期的已通过记录,顺手更新状态。定时任务虽然看着正规,但毕设阶段我推荐懒更新,原因是你不需要为了一个“值班状态刷新”引入 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 就敲代码,而是先把环境对齐。我用的这套项目默认环境是:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 版本太新反而容易出兼容问题 |
| Maven | 3.6 及以上 | 依赖管理 |
| IDEA | 2020 以上版本 | 社区版也够用 |
| MySQL | 5.7 或 8.0 | 驱动选型不一样 |
| Navicat | 任意版本 | 导入 SQL 脚本用 |
项目导入后,先等 Maven 把依赖下载完,再去看配置文件。核心配置在src/main/resources/application.yml里,主要改三个地方:数据源地址、端口、MyBatis 配置。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/lab_reservation?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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 连接串没加serverTimezone=Asia/Shanghai,或者 JDK 版本不同,前端显示的时间跟数据库存的时间差了八小时。演示前一定要检查前后台的日期时间显示是否一致。
第三是状态流转演示顺序乱。演示时应该按照“学生提交 → 管理员通过 → 学生查看通过状态 → 设置已过期 → 状态变为已完成”的顺序走,每一步都清晰可见。别上来先演示取消预约,会把状态流转的完整链路打断。
5. 常见问题与排查经验速查
5.1 启动阶段高频报错
| 错误现象 | 可能原因 | 解决方式 |
|---|---|---|
| 端口被占用 | 8080 端口被其他进程占用 | 命令行执行netstat -ano查看占用 PID,结束进程,或改server.port |
| 无法连接数据库 | MySQL 没启动、驱动版本不匹配、密码错误 | 依次检查 MySQL 服务、依赖版本、配置文件 |
| 页面中文乱码 | 数据库字符集不是 utf8mb4,或页面未声明编码 | 数据库和表改成 utf8mb4,spring.datasource.url里加characterEncoding=utf8 |
Invalid bound statement (not found) | Mapper 接口和 XML 映射文件没有绑定 | 检查mapper-locations路径和 XML 里的 namespace 是否匹配 |
| 扫描不到 Controller | 启动类位置放错 | 启动类必须放在所有包的最外层 |
第二个表格的Invalid bound statement这个问题我看着挺眼熟。它一般是MyBatis的XML里的namespace和Mapper接口的完整类名不一致,或者mapper-locations的路径写错了。检查这两处,基本都能解决。
5.2 业务逻辑暗坑
运行起来没事,但业务上出问题的情况也有几个:
第一个是删除用户时没处理关联预约。如果一个用户已经存在预约记录甚至使用记录,直接删用户会导致预约表里出现悬空引用。我给所有删除接口都加了限制:只要该用户在预约表里存在记录,只做禁用不做物理删除。这也是现实中管理系统的常规做法。
第二个是实验室维护状态下仍然能被预约。管理员把实验室状态改成“维护中”后,预约接口仍能查到并提交成功。这个 bug 我在初版里踩过。解决方式是在查询空闲实验室和提交预约时,都过滤掉status=0的实验室,管理员维护操作才有实际意义。
第三个是列表分页和条件查询的 SQL 拼接问题。如果用了分页插件 PageHelper,需要注意它和自定义 SQL 的执行顺序,count查询和page查询要分开写。为了避免这层麻烦,这个项目的列表我建议只做条件查询 + 简单的 limit 分页,对毕设场景完全够用。
5.3 自学项目时的扩展方向
如果你不满足于基础功能,想往上走一步,这里有三个性价比很高的扩展方向:
第一,加入“实验室预约 + 门禁联动”的仿真设计。在预约通过后生成一个入场二维码,管理员扫码核销,这样就把“预约管理”延伸到了“到场验证”环节,技术上可以引入 Google ZXing 生成二维码和扫码逻辑。
第二,做一个简单的使用率统计模块。按周按实验室统计各时段预约次数,用表格或简单的 ECharts 柱状图展示。这个模块代码不多,但很能在论文里多撑一节“系统应用效果分析”。
第三,增加邮件或站内信通知。预约审核通过或拒绝时给用户发送消息,这里引入 Spring 的事件监听机制,能体现你对解耦设计的理解,而不用真的去集成复杂的消息队列。
6. 最后分享一点我的体会
把一套这种规模的系统从零写到能跑,再用论文语言包装成一个可以答辩的完整项目,我做过不止一次。如果非要说一个最重要的心得,那就是:不要把功能铺得太开,把预约这条主链路做扎实。从提交预约、冲突校验、审批流转、取消释放、超时完成,到异常情况下的并发兜底,整个流程打通了,这个项目就立住了。至于那些花哨的验证码、图表、第三方登录,都是可以后期叠加的加分项。
这套基于 Java + SpringBoot + SSM 的实验室共享预约平台,在技术上是标准的 Web 开发练兵场:三层架构、MyBatis 操作 MySQL、事务管理、事务下并发控制、拦截器鉴权、Thymeleaf 渲染,每一块都是以后工作里天天要碰的东西。把这几样吃透了,比追逐一个新的框架版本有用得多。
如果你拿到源码后第一遍跑不起来,别慌,先看日志,找到第一行ERROR,从那里开始查。大多数问题都不是代码本身的问题,而是环境和数据没对它。调通了第一遍,后面就顺了。