☰
SpringBoot实训室管理系统:预约并发冲突处理与架构实现
2026/10/10 17:31:07 网站建设 项目流程

1. 项目概览与适用人群

做计算机专业毕业设的同学应该都有体会:选“管理系统”这类题的人最多,但答辩时真正能讲出技术亮点的没几个。这个SpringBoot计算机实训室管理系统,名字看着普通,实际上是一个把“资源管理、预约调度、并发冲突处理、权限控制”全串起来的中型Java Web项目,非常适合拿来当毕设,也很适合刚学完SpringBoot想练手的开发者反复拆解。

先把这个系统到底干什么说清楚。计算机实训室就是学校里那些装了电脑、装了专业软件、按机房分配的教室。以前的管理方式基本都是“纸质登记表 + 任课老师口头协调”,一到考试周、实训周就乱成一锅粥:某个老师在Excel里排了课,但学生想课后练习找不到空机房;设备坏了没人及时发现;领导问机房使用率,只能手工数表。这套系统要解决的就是这几件事:实训室课表可视化、上机预约、设备/耗材管理、使用记录统计。

从技术栈看,后端主体是SpringBoot + Java,毋庸置疑就是当前Java开发岗位最主流的那套组合,网上岗位需求里“熟练使用SpringBoot”已经是标配了。对毕设来说,选SpringBoot而不是SSH(Struts + Spring + Hibernate)或纯Servlet,核心原因只有一个:起步快、结构清晰、资料多。SpringBoot内置Tomcat,不用单独部署Web容器,一个main方法就能把整个应用跑起来,这种“约定优于配置”的体验对需要同时写论文、画图、调代码的毕业生来说太重要了。

适合谁阅读这篇文章?如果你是正在选题的计算机/软件工程专业学生,可以参考完整的设计思路和数据库方案;如果你已经在写代码阶段,可以直接照着第4章、第5章的接口设计和冲突处理代码开工;如果你是Java初学者想积累项目经验,这个项目涉及的AOP、Redis、定时任务、状态机等知识点,面试时都能当谈资。

接下来我会从需求设计开始,逐步讲清楚整个系统的核心实现,所有内容都基于我在实际带毕设和开发中积累的经验,该给的代码、SQL、参数计算过程都会给到。

2. 需求分析与整体设计思路拆解

2.1 三类用户角色与权限边界

管理系统的第一件事不是写代码,而是搞清楚谁在用、能用什么。实训室管理系统跑不出三类人:管理员、教师、学生。我见过不少同学把权限设计得很玄幻,什么RBAC模型、什么五张表八个角色,其实完全没有必要。

这个项目里我采用“三角色 + 菜单级权限”的方案。管理员管全局:实训室信息维护、设备台账、课表审核、预约审批(如果开启审批流)、数据统计;教师核心诉求是排课和预约实训室带学生做实验;学生只能查空闲机房、提交预约申请、查看自己的上机记录。

权限控制的落地方式很简单,SpringBoot + Sa-Token或Spring Security都行。我用的是Sa-Token,它的@SaCheckLogin、@SaCheckRole("admin")注解比Shiro配置轻量得多。毕设答辩时,老师大概率会问“不同角色的权限是怎么控制的”,你回答“基于拦截器 + 注解,登录后颁发令牌,接口层通过角色注解做访问控制”就已经合格了。

2.2 功能模块拆解:从课表到设备的闭环

这个系统的功能不要做成“大杂烩”,我建议按“资源约束”这条主线来组织。核心业务是实训室预约,但预约并不是一个孤立的动作,它依赖课表排期,又受设备状态影响,完成之后还会产生统计报表。这条链路想清楚,模块就自然出来了:

  • 实训室管理:机房的楼栋、门牌号、座位数、是否配备特殊软件(如Matlab、EDA工具)、开放时段。
  • 设备管理:每台电脑的资产编号、IP、硬件配置、维修记录,和实训室是“一对多”关系。
  • 课表管理:教师上传/录入每周课程安排,形成“硬性占用”时段,这些时段学生不可预约。
  • 预约管理:学生或教师提交预约,核心字段是实训室ID、开始时间、结束时间、用途。系统最关键的任务是判断提交的时间段是否与已有课表或预约冲突。
  • 签到与使用记录:预约通过后,实际到场签到,系统记录实际使用时长。
  • 统计大屏/报表:按周/月统计机房利用率、设备故障率、热门时段,这是答辩时能拿得出手的可视化亮点。

这六个模块里,技术上最值得展开的就是预约模块,因为它是典型的并发冲突处理场景,后面第4章会单独立章节重点讲。

2.3 为什么SpringBoot是这个项目的最佳选择

标题里“SpringBoot架构下Java驱动”这半句话,其实就是在划定技术基调。有同学纠结要不要上Spring Cloud微服务,答案很明确:不需要。实训室管理系统是一个数据量在万级、用户量在千级的单体应用,上微服务只会增加部署和调试负担,答辩时反而容易暴露“技术选型不合理”的短板。

SpringBoot在这个项目里承担的角色可以拆成几点:

  • 自动配置:数据源、JPA/MyBatis、Redis等依赖通过spring-boot-starter-*一键引入,省掉了大量XML配置。
  • 内嵌容器:项目打成Jar包直接运行,答辩现场演示部署非常方便。
  • 生态成熟:配合MyBatis-Plus操作数据库、配合Redis做缓存和分布式锁、配合Spring Task做定时清理过期预约,都是现成的解决方案,遇到问题搜索资料一抓一大把。

一句话总结我的选型思路:用主流但不过度设计的技术,把业务逻辑做扎实,这比堆砌冷门框架更能体现工程能力。

3. 数据库设计与核心表结构规划

3.1 六张核心表的字段与关系

数据库设计是答辩提问的重灾区,很多同学只会在Navicat里点“新建表”,问起字段为什么这么设计就说不出所以然。实训室管理系统的表结构其实很好讲,因为它贴合实际业务场景,我用六张核心表带你捋一遍。

第一张tb_user用户表。字段:id、username、password(BCrypt加密存储)、real_name、role(1管理员/2教师/3学生)、college、phone。注意role用int不用字符串,省空间也好判断。

第二张tb_lab实训室表。字段:id、name、location、seat_count、is_available(是否开放预约)、open_time(如08:00)、close_time(如21:30)、software_desc(特殊软件说明)、description。座位数这个字段是预约容量判断的关键。

第三张tb_computer设备表。字段:id、lab_id(外键关联实训室)、asset_no(资产编号)、ip_address、cpu、memory、disk、status(1正常/2维修/3报废)。为什么要单独建设备表而不直接塞在实训室表里?因为设备有独立的生命周期,一台电脑坏了要维修记录,如果塞在实训室里,查维修历史和统计故障率都非常痛苦。

第四张tb_course课表/课程占用表。字段:id、lab_id、course_name、teacher_id、week_day(星期几,1-7)、start_slot(第几节课开始)、end_slot、semester。这里是用“节次”来表示时间,而不是直接用时间戳,因为高校作息时间是固定节次的,第1节到第2节就是一个槽位,这样设计最贴合学校场景。

第五张tb_reservation预约表。这是核心表,字段:id、user_id、lab_id、reservation_date(预约日期)、start_time、end_time、status(0待审核/1已通过/2已拒绝/3已取消/4已完成/5已违约)、purpose、create_time。预约时间用reservation_date和start_time/end_time拆开存,是为了方便查询某一天某个机房的所有占用时段。

第六张tb_usage_log使用记录表。字段:id、reservation_id、lab_id、user_id、actual_start、actual_end、seat_count_used。这张表主要给统计报表供数。

3.2 表关系与索引设计要点

表关系上,tb_computer多对一关联tb_lab,tb_reservation多对一关联tb_user和tb_lab,tb_course多对一关联tb_lab和tb_user。没有复杂到要建中间表的“多对多”关系,这个数据模型已经很干净了。

但数据库设计最关键的其实是索引。预约冲突查询是这样的SQL:

SELECT COUNT(*) FROM tb_reservation WHERE lab_id = ? AND reservation_date = ? AND status IN (1, 4) AND start_time < ? AND end_time > ?;

这个查询如果不加索引,一旦数据量上来就是全表扫描。我的做法是建两个联合索引:

ALTER TABLE tb_reservation ADD INDEX idx_lab_date_time (lab_id, reservation_date, start_time, end_time); ALTER TABLE tb_course ADD INDEX idx_lab_week (lab_id, week_day, start_slot, end_slot);

另外非常重要的一步:对并发预约做防重约束。我在tb_reservation上加了唯一索引,字段是(lab_id, reservation_date, start_time, end_time),这样即使两个用户同时提交相同时间段的预约,数据库层面也会拦住一条。当时我这么设计的原因是:仅靠Java代码里的“先查再插”无法百分百防冲突,必须数据库兜底。这个问题在答辩时被老师追问了,我把“乐观锁 + 唯一索引兜底”的思路讲清楚之后,老师直接点头。”

之前提到“状态字段status”,我补充一下状态流转:学生提交预约后状态为0,管理员/教师端审核后变为1或2;使用时签到后进入执行中状态;实际结束变为4;预约了但没来签到超过一定时间自动变为5。这个状态机可以用一个简单的枚举类管理,后面第4章会给出代码示意。

4. 预约调度与并发冲突处理的完整实现

4.1 时间片与节次映射方案

预约系统最难的不是CRUD,而是如何判断“这个机房这个时间到底空不空”。我在设计时做了个映射方案:系统虽然存的是具体时间(如09:00-09:45),但为了跟课表节次对照,又维护了一张“节次时间映射表”。

比如学校作息是:第1节08:00-08:45,第2节08:55-09:40,第3节10:00-10:45……那我在配置表里就存这些节次的起止时间。课表占用的是一段连续节次,比如“周一第1-2节”,转成时间就是08:00-09:40。学生预约写了具体起止时间,我同样映射到节次区间去比较。

这样做的好处是:课表管理的“节次思维”和预约管理的“时间思维”能统一到一个比较模型里。代码里我写了一个TimeSlotUtil工具类,核心方法就是把节次字符串解析成时间范围:

public class TimeSlotUtil { public static TimeRange parseCourseSlots(List<Integer> slots, Integer weekDay) { // 根据节次列表和星期几,从配置表查出对应时间 // 例如 slots = [1, 2] => 08:00-09:40 } public static boolean isOverlap(TimeRange occupied, TimeRange request) { // 区间重叠判断:occupied.start < request.end && occupied.end > request.start return occupied.getStart().isBefore(request.getEnd()) && occupied.getEnd().isAfter(request.getStart()); } }

判断两段时间是否重叠的关键式就是a.start < b.end && a.end > b.start。这个公式很多人记不住,我换个生活说法:两个人时间段“打架”的唯一条件就是——你先结束的时间比我后开始的时间要晚,同时我先开始的时间比你后结束的时间要早。记不住公式没关系,画出时间轴对比最直观。

4.2 基于Redis预占与数据库唯一索引配合的并发控制

这是整个项目里最有含金量的地方,也是答辩时最能展示技术深度的地方。

场景还原:期末考试前一周,某个热门机房开放了预约入口,500个学生在同一秒点“提交预约”。如果你用最简单的方式——先查数据库有没有冲突,没有就insert——那么一瞬间大量请求会查到“数据库还是空的”,然后全部插进去,造成超卖。这就是经典的并发问题。

我的方案分两层:

第一层,Redis预占。用户提交预约时,先在Redis里用lab_id + 日期 + 开始时间 + 结束时间拼一个唯一Key,执行setIfAbsent(即SETNX)。这个Key一旦写入成功,说明这个时段的“预占权”拿到了,才允许继续去操作数据库;拿不到就直接返回“该时段已被预约”,不用去查库。

String key = "reservation:lock:" + labId + ":" + date + ":" + startTime + ":" + endTime; Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, userId, Duration.ofMinutes(10)); if (Boolean.FALSE.equals(locked)) { throw new BizException("该时段刚刚被预约,请选择其他时间"); }

注意这个Key设置了10分钟过期时间。为什么不设永久?因为用户很可能中途放弃、网络断掉,如果不自动过期,这个时段就会被一个“幽灵预占”卡住永远无法释放。我遇到过因为没设过期时间导致某个机房连续三天无法预约的Bug,排查花了两个小时才定位到是Redis里的死Key。

第二层,数据库唯一索引兜底。Redis预占解决的是“瞬时高并发”问题,但极端情况下Redis和数据库状态可能不一致(比如Redis过期了但事务还没提交)。我在数据库层面继续用唯一索引约束,插入预约记录时如果同一个时段的记录已存在,数据库直接抛DuplicateKeyException,代码捕获后回滚并友好提示。这就是前面提到的兜底方案,层层设防,保证系统的正确性压倒一切。

4.3 可复用的预约提交Service方法设计

把以上逻辑串成一个完整的createReservation方法,贴核心代码:

@Transactional(rollbackFor = Exception.class) public ReservationVO createReservation(ReservationRequest req) { // 1. 校验用户身份 User user = userService.getById(req.getUserId()); if (user == null || user.getRole() != 3) { throw new BizException("仅学生角色可提交预约"); } // 2. 校验实训室状态 Lab lab = labService.getById(req.getLabId()); if (lab == null || !lab.getIsAvailable()) { throw new BizException("该实训室当前不可预约"); } // 3. 校验预约时段合法性 if (req.getStartTime().isBefore(localTimeNow) || !req.getEndTime().isAfter(req.getStartTime())) { throw new BizException("预约时间不合法"); } // 4. 检查课表占用 List<Course> courses = courseMapper.findConflicts(req.getLabId(), req.getReservationDate(), req.getStartTime(), req.getEndTime()); if (!courses.isEmpty()) { throw new BizException("该时段与课表冲突,请选择其他时间"); } // 5. Redis预占 + 数据库唯一索引兜底插入 String lockKey = buildLockKey(req); boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, req.getUserId(), Duration.ofMinutes(10)); if (!locked) { throw new BizException("手速太快了,该时段刚刚被预约"); } try { Reservation reservation = new Reservation(); // 组装字段... boolean saved = reservationService.save(reservation); if (!saved) { throw new BizException("预约失败,请重试"); } return new ReservationVO(reservation); } catch (DuplicateKeyException e) { throw new BizException("该时段已被他人抢先预约"); } finally { // 事务提交成功后删除Redis预占Key,交给后续状态标记 redisTemplate.delete(lockKey); } }

注意第5步里redisTemplate.delete(lockKey)为什么放在finally里?因为如果忘了删Key,10分钟内所有相同时段的预约都会被Redis挡住,即使数据库里这条记录已经因为用户取消而被删除了。这里有一个更稳妥的做法:不直接删除Key,而是把Key的值从“预约中”改成“已占用”,让后续判断逻辑更灵活。我为了省事直接删掉了,你们实现时可以根据完整流程细化。

4.4 定时任务清理过期未签到记录

预约通过后,如果学生没来签到,这条记录的状态就会一直停在“已通过”,占着茅坑不拉屎。我的方案是用Spring自带@Scheduled定时任务,每10分钟扫描一次:找到所有status = 1(已通过)且当前时间已超过预约开始时间30分钟的预约记录,把状态改为5(已违约)。

@Scheduled(fixedDelay = 600000) public void handleTimeoutReservations() { LocalDateTime now = LocalDateTime.now(); List<Reservation> timeouts = reservationMapper.findTimeoutReservations(now.minusMinutes(30), 1); for (Reservation r : timeouts) { r.setStatus(5); reservationService.updateById(r); // 可选的附加动作:给学生发送站内信提醒违约记录 } }

这里值得注意的点是:定时任务一定要幂等,也就是同一批数据即使被扫描两次,结果也是一样的。我通过findTimeoutReservations(threshold, status)只查状态为1的记录,更新完之后状态变5,下次扫描就不会再命中,天然幂等。

5. 前后端交互与核心功能落地细节

5.1 接口设计与统一返回结构

前后端分离是这个项目的默认形态:SpringBoot提供JSON接口,前端用Vue开发。为了让前端处理数据时不迷路,我定义了统一的返回结构Result<T>:

public class Result<T> { private Integer code; // 200成功,400业务异常,401未登录,500系统错误 private String message; private T data; }

所有Controller直接返回Result.success(data)或Result.error("xxx")。这里我踩过一个坑:一开始图省事直接返回裸数据,前端拿到response后形态不一,有的地方取res.data,有的地方取res.result,联调的时候改来改去非常痛苦。后来统一了返回结构,再配合一个全局异常处理器@RestControllerAdvice,代码瞬间清爽很多。

关键的接口清单如下:

功能点请求方式路径说明
登录POST/api/auth/login参数username+password,返回Token
查询空闲实训室GET/api/lab/available参数date、startTime、endTime、seatCount
提交预约POST/api/reservation参数见ReservationRequest
取消预约PUT/api/reservation/{id}/cancel状态流转 1/0 -> 3
课表导入POST/api/course/importExcel批量导入,省得手工录入
统计报表GET/api/statistics/usage返回机房利用率趋势

5.2 空闲实训室查询的SQL优化

“查空闲机房”是一个高频接口,前端页面里学生一进来就要调。我第一版实现得很朴素:把某天某时段的所有预约和课表都查出来,然后在内存循环里过滤出冲突的labId,剩下的返回给前端。数据量小的时候没问题,但我用脚本灌了1万条预约数据做压测,这个接口平均耗时跑到1.2秒,明显不合格。

后来我换成“排除法”SQL:先查所有实训室,然后用NOT EXISTS子查询排除掉冲突时段,一次性出结果:

SELECT l.* FROM tb_lab l WHERE l.is_available = 1 AND l.seat_count >= #{seatCount} AND NOT EXISTS ( SELECT 1 FROM tb_reservation r WHERE r.lab_id = l.id AND r.reservation_date = #{date} AND r.status IN (1, 4) AND r.start_time < #{endTime} AND r.end_time > #{startTime} ) AND NOT EXISTS ( SELECT 1 FROM tb_course c WHERE c.lab_id = l.id AND c.week_day = #{weekDay} AND c.start_slot < #{endSlot} AND c.end_slot > #{startSlot} )

这样数据库层面就过滤掉了冲突的实训室,接口响应压到了200毫秒以内。优化完自己都舒服了,答辩的时候还能顺势讲一下“我在压测中发现了性能瓶颈,并通过子查询优化解决了问题”,这可是妥妥的加分项。

5.3 数据可视化与报表模块

报表模块不只是画几个图表,更重要的是背后的统计SQL。实训室使用率是核心指标:某机房某个月的使用率 = 实际被占用的总时长 ÷ 当月总开放时长。我在usage_log表里记录了每次签入签出时间,直接聚合:

SELECT lab_id, SUM(TIMESTAMPDIFF(MINUTE, actual_start, actual_end)) AS used_minutes, COUNT(*) AS usage_count FROM tb_usage_log WHERE actual_start >= #{monthStart} AND actual_end <= #{monthEnd} GROUP BY lab_id ORDER BY used_minutes DESC;

拿到这些聚合数据,前端用ECharts画柱状图、饼图都很简单。我个人建议在仪表盘页面放三个核心指标:今日预约人次、机房实时占用率(已签到/总共)、设备故障总数,这三个数字最有说服力,也最容易被答辩老师一眼看懂。

6. 项目搭建流程与实操经验

6.1 从零开始的项目初始化步骤

如果你打算照着这个思路自己搭,我给你一套流程,省得走弯路:

  1. 用IDEA的Spring Initializr创建项目,Java版本建议8或11,SpringBoot版本用稳定版(当前2.7.x系或3.x都行,注意JDK匹配)。
  2. 引入依赖:spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter、mysql-connector-j、spring-boot-starter-data-redis、sa-token-spring-boot-starter、lombok。
  3. 配置application.yml:数据库连接、Redis连接、MyBatis-Plus的逻辑删除和驼峰映射开关。
  4. 先跑一个HelloController确认环境通了,再开始建表、写实体。
  5. 按模块顺序开发:用户登录 → 实训室CRUD → 设备CRUD → 课表管理 → 预约核心逻辑 → 报表。我特别推荐这个顺序,因为每一步都可以独立测试,前一模块稳定了再往下做,出bug时定位范围很小。

MyBatis-Plus的配置有个细节需要注意:如果表名和实体类名不对应(如tb_lab对应Lab),一定要在实体类上加@TableName("tb_lab")注解,或者统一配置table-prefix: tb_。我当时忘了配前缀,结果全表查询报“表不存在”,排查了半天才发现是这个问题。

6.2 答辩时值得展开讲的技术亮点

毕设答辩不是代码审查,老师不可能一行一行看你的代码,但他们喜欢追问“你系统里最有技术含量的地方”。我建议你准备三个讲故事的点:

第一,预约并发的处理策略。你直接说:我用了Redis分布式锁预占 + 数据库唯一索引兜底 + 事务回滚,三层保护防止超卖和冲突。老师听到Redis和分布式锁,兴趣一下就上来了。

第二,时间重叠判断模型。你解释课表和预约如何统一映射到时间区间,边界条件怎么处理的(比如恰好一端相等并不算冲突,因为<和>已经排除了相等情况)。这是测试用例设计的能力体现。

第三,性能优化案例。你说:我压测发现空闲查询接口从1.2秒优化到200毫秒,手段是子查询过滤代替内存过滤。这说明你有性能意识,不是只会CRUD。

6.3 踩坑记录:那些文档里没有的细节

最后分享几个我实际踩过的坑,这些才是经验值所在。

第一个坑:时区问题导致预约时间错乱。MySQL连接串里如果不加serverTimezone=Asia/Shanghai,存进去的时间可能比实际时间早8小时。学生预约了下午3点,数据库里存的是早上7点,第二天管理员看数据一头雾水。解决方式:连接串加参数,同时Java实体里用LocalDateTime而不是java.util.Date,后者在Jackson序列化时也会有各种奇奇怪怪的格式问题。

第二个坑:跨天预约的场景忘了处理。实训室开放时间是08:00-21:30,正常情况下没有跨天预约,但如果哪天遇到特殊情况允许预约到晚上22:30,你的reservation_date和start_time组合就会产生歧义——到底是哪天的时间?我的处理方式很简单:业务层面禁止跨天预约,接口校验阶段直接拒绝跨天的请求,不给系统引入歧义。这也是一个产品思维的点:与其把逻辑做复杂,不如在规则上限制条件。

第三个坑:测试数据要贴近真实。很多同学为了演示方便,把预约记录的时间都集中在当天,结果报表模块一看,整张图全是零点到两点的数据,根本不真实。我建议写一个DataInitializer,用代码生成过去30天的随机预约数据,保证空闲机房、高峰时段、故障设备这些情况在界面上都能看到。演示时数据越真实,老师越觉得你系统是“用过”的,不是纯玩具。

7. 个人实操体会与后续方向

这套系统做下来,我最大的体会是:管理系统类毕设的分数差异不在“功能多不多”,而在“边界条件处理得好不好”。预约冲突、并发兜底、时间重叠判断、超时未签到清理——这些才是区分“会写代码”和“用了心写代码”的地方。我见过太多同学的预约模块就两句SQL,一提交就直接insert,答辩时老师抽两个并发场景就直接露怯了。

在这里也给你一个思路扩展:如果你学有余力,可以给系统加一个基于Redis + ECharts的“实训室热力图”实时展示页面,再给设备模块接一个简单的二维码报修流程(用Hutool生成二维码,手机扫码弹出报修表单)。这两点不用动系统主体架构,却能明显提升完整度和创新性。特别是二维码报修,每次实训室技术员来修完设备,在管理员后台更新状态,这个闭环听起来就比纯手工录入吸引人。

最后再分享一个小建议:做这种系统,千万别到最后两周才熬夜赶工。按我前面推荐的模块顺序,每天推进一个小模块,一个月时间足够你把代码、测试、论文初稿都搞定。重点是把预约核心逻辑先做扎实再谈界面美化,因为这才是整个系统的灵魂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询