ARTICLE DETAIL

资讯详情

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

实验室排课系统设计与实战:SpringBoot+Vue下的冲突检测与时间片模型

实验室排课系统设计与实战:SpringBoot+Vue下的冲突检测与时间片模型 1. 实验室排课系统和普通教务排课差在业务约束复杂度很多人在立项时会下意识把实验室排课系统当成一个缩小版的教务系统来做结果做出来以后实验室管理员根本不愿意用。我当初也是先犯了这个错误后来才明白实验室排课的核心难点不在 SpringBoot 或 Vue 的技术实现而在业务模型里多出来的那一堆约束条件。1.1 实验室排课要管的不只是时间不冲突普通教室排课主要冲突就是时间、班级、教师三个维度。实验室排课在这个基础上还要叠加四类特殊约束。第一是设备容量。一个实验室可能只有 40 台机器如果排进来 60 人的班就算时间不冲突实验过程中也会有人没机器可用教学效果基本为零。所以排课时必须实时校验班级人数 实验室座位数。第二是实验环境匹配。比如某些实验课要跑专门的仿真软件只能在装了特定系统的机房上电子类实验需要示波器、信号源化学类实验需要通风橱。如果排课系统里没有实验室类型 / 课程要求这个维度排出来的课表就是废纸一张。第三是特殊时段封锁。实验室不是所有时间都能排课比如每周五下午设备维护、期末设备检修、考试周封闭这些时段必须在系统层面提前锁定否则管理员每次都靠电话通知老师改时间。第四是设备分组问题。很多实验室里的设备并不是全校通用而是分散在不同实验分室。比如单片机实验室和嵌入式实验室虽然都属于计算机类但设备清单不同不能混排。这个维度如果建模不到位后面所有排课都只是表面不冲突。1.2 角色与流程设计谁申请、谁审核、谁排课实验室排课系统的角色我最终拆成了四类系统管理员、教师、实验员、学生。教师是排课需求的发起方。他们要提交的是课程 班级 人数 期望周次 对实验室类型的要求而不是具体指定某一个实验室——因为教师根本不知道哪个实验室空闲。实验员是关键审核岗。他们要干两件事一是确认教师提交的实验需求是否合理比如某个课程能不能用这个类型的实验室二是在系统自动推荐的时间片里再做人工确认因为有些冲突是机器规则覆盖不到的比如某位老师每周三下午有固定例会、某个实验室的指导老师只有周四在岗。系统管理员负责基础数据维护学期设置、实验室档案、教师档案、班级档案、系统参数。另外还要负责跑批量数据比如学期初把培养计划的实验课程一次性导入。学生的角色最轻基本只有课表查询但课表查询的并发压力在开学第一周非常高这个要考虑在接口设计里而不是真的做一个只能给管理员用的后台。1.3 把模糊业务规则翻译成需求清单这一步我建议在写任何代码之前做。我就是靠下面这张规则表才把所有开发任务拆清楚的。编号冲突规则说明R1教师时间冲突同一教师在同一时间片内只能安排一门实验课R2班级时间冲突同一班级不能同时出现在两个实验室R3实验室时间冲突同一实验室在同一时间片只能排一个班级R4容量约束班级人数必须小于等于实验室座位数R5环境约束课程要求的实验室类型必须匹配R6封锁时段实验室维护、考试周等时段默认不可排R7周次模式支持单周、双周、每周三种排课模式这些规则看着简单但实际开发时每一条都会衍生出边界问题。比如 R7 的单双周A 实验室周一 1-2 节第一周被占用第二周可能空闲那么第三周的同一时间能不能再排如果不能因为跟第一周占位有关系如果能那单双周之间的冲突判定要基于周次集合取交集而不是简单的星期几判断。这块我放在后面算法部分细说。2. 技术选型决策SpringBoot、Vue 版本和配套组件为什么这么定技术选型不能只看哪个流行要看团队维护能力和部署环境。这套系统我推荐并最终使用的是SpringBoot 2.7 Vue 3 Element Plus MySQL 8.0 MyBatis-Plus Sa-Token。下面逐一说理由。2.1 后端版本SpringBoot 2.7 比 3.x 更稳妥SpringBoot 3.x 已经发布很久但我在实验室项目里仍然建议 2.7.x核心原因有两个。第一是 JDK 版本兼容。实验室或学校服务器上很多还是 JDK 8SpringBoot 3.x 强制要求 JDK 17 起升级 JDK 本身就是一个需要申请流程的运维动作不值得为一个排课系统背这个包袱。第二是生态兼容性。很多网上的教程、毕设代码、老版本的 MyBatis-Plus 分页插件等在 SpringBoot 2.7 上踩坑最少。搜索热词里那个springboot版本太高说的就是这个痛点——版本高了以后各种第三方 starter 还在用旧的自动装配方式启动报错你一查就是半天。当然如果这是一个全新项目服务器可以自由装 JDK 17用 SpringBoot 3.x 问题也不大。只是从大概率要给别人维护的角度2.7 更宽厚。2.2 前端选型Vue 3 Element Plus而不是 Vue 2 老项目很多实验室系统还在用 Vue 2 Element UI原因是有大量现成模板。但如果你是从零开发新系统我不建议再开 Vue 2 的新坑。Vue 2 已经停止维护Element UI 也跟着进入冻结状态新开发的功能出问题后基本只能自己改源码。我这边最终用的是 Vue 3.2 Element Plus Pinia Vue Router 4。组件库用 Element Plus 是因为实验室排课系统里面有大量表单、表格、弹窗、日历类交互Element Plus 能覆盖超过八成的基础组件需求自己造轮子的成本很低。状态管理用 Pinia 而不是 Vuex原因很简单Vuex 4 的 API 设计更繁琐Pinia 更符合 Vue 3 的组合式 API 习惯。这个排课系统的全局状态其实也不多当前学期、当前选中的实验室、当前用户信息、权限路由表用 Pinia 管理起来很清爽。2.3 权限、接口文档和文件导出的配套选型权限部分我没有用 Spring Security因为对这个场景来说太重了。实验室排课系统的权限模型是典型的三种角色 接口路径级控制我用 Sa-Token 的登录认证 注解鉴权解决了开发效率高很多文档也全。接口文档用 springdoc-openapi 自动生成 OpenAPI 文档再对接 Apifox 做在线调试。这个组合比老旧的 Swagger 2springfox好用得多因为 springfox 很长一段时间不支持 SpringBoot 2.6 的路径匹配策略启动就报springfox.documentation.spring.web.WebMvcRequestHandlerProvider之类的错排查成本高。Excel 导入导出我直接用了 EasyExcel而不是 Apache POI。EasyExcel 在 POI 之上封装了流式读写100 人以上的班级名单导入不会把内存打爆导出课表也没有 POI 常见的样式兼容问题。3. 数据库建模排课系统真正的地基是时间片模型排课系统最怕的是把数据库设计成一张课表、一个 StartTime、一个 EndTime这种简单结构。实验室排课按学期循环跟普通会议室预约按绝对时间是完全不同的建模思路。3.1 核心表结构和字段设计我最后落地的核心表是这些每张表都尽量做到一张表只负责一类核心问题。第一张是lab_room。实验室档案表字段包括实验室名称、编号、类型计算机类/电子类/化学类等、座位数、所属实验分室、是否启用、状态正常/维护/封锁。最容易被忽略的字段是maintenance_weekday比如每周三下午维护这个字段直接给前端课表做灰色置灰比单独维护一张封锁表更直观。第二张是schedule排课记录表。这是整张表设计的灵魂。CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, semester_id BIGINT NOT NULL COMMENT 学期ID, lab_id BIGINT NOT NULL COMMENT 实验室ID, course_id BIGINT NOT NULL COMMENT 课程ID, teacher_id BIGINT NOT NULL COMMENT 教师ID, class_ids VARCHAR(255) NOT NULL COMMENT 班级ID集合逗号分隔, week_type TINYINT NOT NULL COMMENT 1每周 2单周 3双周, week_day TINYINT NOT NULL COMMENT 星期几 1-7, start_section TINYINT NOT NULL COMMENT 开始节次, end_section TINYINT NOT NULL COMMENT 结束节次, schedule_status TINYINT DEFAULT 0 COMMENT 0待审核 1已排 2已停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_schedule_unique (semester_id, lab_id, week_day, week_type, start_section, end_section) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这几处设计都留了后手class_ids用逗号分隔存储合并班级。因为一个教师的一门实验课经常是多个小班合并上课如果单独建关联表查询课表时要 join 三次性能差而且代码复杂。用逗号分隔配合FIND_IN_SET查询在这个量级完全够用。week_type字段特别加上了单双周支持。常规排课系统如果不支持单双周在实验教学场景会被直接否决。唯一索引把实验室 星期 单双周 节次区间作为一个整体这是数据库层防重复的最后一道闸门。第三张是semester。学期表要记录学期名称、开学日期、总周数、当前是否为默认学期。排课记录全部挂在 semester_id 下换学期一键切换不会出现上一年课表跟今年混在一起的情况。第四张是course_lab_requirement。这是实验室需求表和课程之间的关联表记录每个实验课程需要哪种实验室、三维目标是必修还是选修。排课推荐的核心逻辑会反复读这张表。3.2 冲突检测在数据库层面怎么落地冲突检测如果完全靠 Java 代码在内存里遍历数据量一上来就会很慢。我推荐大部分场景靠 SQL 查重只有自动推荐时才在内存里做二次过滤。以 R1 教师时间冲突为例核心 SQL 可以写成这样SELECT COUNT(*) FROM schedule WHERE semester_id #{semesterId} AND teacher_id #{teacherId} AND week_day #{weekDay} AND ( (week_type 1 OR #{weekType} 1 OR week_type #{weekType}) ) AND start_section #{endSection} AND end_section #{startSection};这段 SQL 里最关键的是区间重叠判断start_section 传入结束节次 AND end_section 传入开始节次。它排掉了完全包含、部分重叠、相邻但不相交之外的所有情况。如果你写成start_section #{endSection} AND end_section #{startSection}会把 1-2 节和 3-4 节这种相邻时间也误判成冲突因为 end2 和 start3 的关系是 2 3正确的 SQL 应该判断为不冲突。单双周的处理用了一个简化技巧当已有记录是每周week_type1或者新排的是每周时都会产生交集所以条件简化为三者中任一是每周或两者模式相同。周次集合取交集的更通用逻辑是用位图比如用 20 位整数表示整学期的周次冲突判断就是(bitmapA bitmapB) ! 0这个方案后面做自动推荐时我用到了。3.3 为什么我坚持加冗余字段而不是过度建模有经验的 DBA 可能会说class_ids、week_type这些字段不够范式化应该建关联表和周次明细表。理论没错但要考虑实验室排课系统的真实使用强度一个学期排课记录撑死几千条又是内网系统并发量极低。在这种量级下冗余字段带来的好处远大于范式化的好处查课表时一次 SQL 直接返回完整记录不需要 JOIN 班级表再拼接导出 Excel 时也不用在 Java 层做循环拼装。我当初按标准三范式设计了一版结果一个课表查询接口写了 80 行 SQL后来砍掉一堆关联表代码反而更短更容易维护。所以这里的经验是实验室排课系统不是高并发大数据系统你的表设计第一目标是人看着清楚、SQL 写着简单第二目标才是范式化。数据库的模型要服务于业务查询而不是服务于教科书理论。4. 后端核心实现排课接口、冲突算法与并发控制后端部分最关键的不是 CRUD而是三个点排课接口怎么设计得让前端好调用、自动排课算法怎么写出可用性、并发操作下怎么保证不产生重复排课。4.1 排课接口承载的核心逻辑排课接口我采用了统一入口 校验工厂的思路前端不管你是手动指定时间排课还是自动推荐排课最终都调用同一个接口POST /api/schedule/assign。请求体设计{ semesterId: 1, courseId: 12, teacherId: 23, classIds: [101, 102], labId: 3, weekType: 1, weekDay: 2, startSection: 3, endSection: 4, operatorRole: ADMIN }后端接收到请求后执行顺序是参数校验 → 实验室状态校验 → 容量校验 → 环境匹配校验 → 冲突检测 → 写入排课记录 → 更新实验室时间片缓存。参数校验里最容易漏的是节次区间。很多前端传过来 endSection 比 startSection 还小这种错误要在后端直接拦截不要等 SQL 执行完再报错。实验室状态校验要查lab_room.status如果实验室处于维护状态哪怕是数据库唯一索引没拦住也必须在业务层返回明确提示该实验室当前不可排课。整个方法上标了Transactional(rollbackFor Exception.class)因为后面更新缓存、记录日志等操作如果失败不能把排课记录留在库里产生脏数据。4.2 自动排课推荐贪心 打分自动排课功能是这个项目的加分项。实现思路不复杂给定课程需求遍历所有满足类型和容量的实验室在每个实验室的时间片矩阵里找到所有空闲时间片然后计算每个候选时间片的得分排序后返回前 5 个推荐项。打分规则我定义成几个维度教师当天已有课时太多减分。排课时间越靠近期望时间段加分。同一实验室前半周占用率高后半周空闲多给后半周加分。教室连续占用太久比如上午四节都是同一组班级减分。遍历的顺序是先按实验室类型和容量过滤再对每个实验室生成整学期的周次位图和候选时间片做交集判断。位图方案在这里非常快因为Java int就能表示 20 周位运算直接判断冲突boolean conflict (existingBitmap candidateBitmap) ! 0;这一步替代了逐条查 SQL走了内存计算。因为候选实验室和已有排课记录的总量都是可控的通常几十条到几百条内存遍历的复杂度完全可接受。实际效果测试下来200 条排课记录下推荐一个时间片大概 50ms 以内。4.3 并发排课下如何保证不冲突排课系统虽然并发量低但架不住管理员和实验员同时手动排。如果两个人同时间给同一个实验室排同一个时间片先查后插的流程会出现都查不到、都插入成功的问题。这个问题我是在一次线上验收时被领导现场演示出来的非常尴尬。解决分两层。第一层是数据库唯一索引就是建表那节里写的uk_schedule_unique。当重复插入时 MySQL 会报 Duplicate entry我们在异常处理里把它转换为业务提示该时间片已被占用。这是兜底防线。第二层是对实验室记录加悲观锁。排课操作先执行SELECT * FROM lab_room WHERE id #{labId} FOR UPDATE;拿到实验室行锁后再做业务判断和插入。这样第二个并发请求会被阻塞等第一个事务提交后再进入时就能查看到最新的排课记录不会产生重复数据。悲观锁在这个场景比乐观锁更合适因为排课冲突概率不算低一旦发生乐观锁重试用户会看到一个提交失败体验很差。4.4 Excel 批量导入导出要注意的两个细节学期初导入培养计划时老师们经常直接甩过来一个 Excel 文件里面几百行课程排期。EasyExcel 导入时我用的是逐行invoke监听器每 100 条 batch 提交一次避免一次性把所有行都塞进内存。导出课表同样用 EasyExcel但要注意一行数据里包含多个动态列一个实验室一周 7 天乘以每天的节次我直接构建一个二维 List 作为表头数组一次性写入比手动拼 CSV 再设置响应头要稳定得多。另外一个容易被忽略的点是导出文件名编码。设置响应头时这样写response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(fileName, StandardCharsets.UTF_8));如果不做 URLEncoder.encode中文文件名在部分浏览器里会变成乱码或者直接被拦截下载。这种小问题看着不起眼但现场演示时特别容易翻车。5. Vue 前端实现周课表组件和排课交互怎么做得顺手后端做得再好前端交互卡顿这个系统照样没人用。实验室排课系统前端最重要的是课表视图和排课操作我分别踩过不少坑。5.1 用 el-table 自建周课表而不是套 FullCalendar第一版我直接引了 FullCalendar想着日历组件功能齐全支持拖拽应该很快。结果发现实验室排课的课表语义和日历语义有很大差别日历是按自然日展开的而实验课表是按星期几 第几节展开的同一个格子可能显示一周里单双周不同的课程。FullCalendar 要定制成周节次视图反而要写很多覆盖样式和事件逻辑的代码维护成本很高。最后我直接用 el-table 定制。表格的列固定为周一至周日行按节次展开上午 1-2 节、3-4 节下午 5-6 节、7-8 节晚上 9-10 节每个 cell 里展示课程名、班级、教师再根据 schedule_status 展示不同颜色。这种实现的好处是数据模型和后端的 schedule 记录完全对齐遍历一个二维数组就能渲染。格子里的内容我用了一个小组件ScheduleCell.vue接收当前实验室某天的所有排课记录内部再按 startSection 排序最多显示 3 条超过的用N折叠点击展开弹窗。5.2 排课拖拽什么时候合适什么时候不要硬上很多人在看到排课系统时都会想我要做拖拽排课把课程从左边拖到右边格子里。这个功能确实好看但我建议分阶段第一期先做点击格子 弹窗表单排课第二期再考虑拖拽。直接拖拽的难点不在拖拽本身而在冲突提示。拖到格子上时如果该格子部分时间可用部分不可用比如 1-2 节空闲、3-4 节冲突业务上是允许把课程排在 1-2 节还是整个格子都拒绝这个语义如果不定义清楚前端实现会非常别扭。如果要做拖拽推荐用原生 HTML5 Drag API 配合 vuedraggable 简化。拖拽过程中实时调用后端的可用性预检接口传递课程、实验室、星期、节次、单双周返回可用状态可用/完全冲突/部分冲突然后在前端用绿色、红色、黄色表达。黄色情况下点击后还是要弹窗让用户确认不要直接放进去。5.3 前后端联调最容易翻车的时间格式问题这个坑我到现在仍记忆犹新。后端的排课记录里有 createTime 和 updateTime返回给前端时是 JSON 字符串2024-09-08T10:30:00。前端用new Date(value)解析在部分浏览器里能解析成功在部分浏览器里显示 Invalid Date页面直接泛白。解决方式是在后端统一加全局 Jackson 配置Bean public Jackson2ObjectMapperBuilderCustomizer globalJsonConfig() { return builder - { builder.serializers(new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; }前后端统一约定yyyy-MM-dd HH:mm:ss格式后所有时间字段不会再出现时区偏移和解析失败的问题。这一点在前后端联调阶段一定要提前约定不要等联调时再来补。5.4 动态路由和菜单权限因为系统有三种角色前端菜单和路由不能写死我在登录后从后端拉取当前用户的权限标识动态生成路由。const modules import.meta.glob(/views/**/*.vue); function buildRoutes(menus) { return menus.map((menu) ({ path: menu.path, component: modules[/views${menu.component}.vue], meta: { title: menu.title, icon: menu.icon }, children: menu.children ? buildRoutes(menu.children) : [] })); }这里有个细节Vue 3 的 Vite 环境下动态导入组件路径必须用import.meta.glob预扫描否则会出现找不到模块的运行时错误。我在开发模式下一次都没事打包部署后就遇到这个报错排查了很久才意识到是打包器静态分析的问题。6. 部署发布与半年使用后的避坑记录代码写完不算完真正让这个系统在实验室里稳定跑起来才算是项目完成。部署环节看起来只是 Nginx jar 一配就完但实际操作里有一堆细节。6.1 Nginx 配置前后端分离的经典结构前端项目执行npm run build生成 dist 目录上传到服务器/var/www/lab-scheduleNginx 配置的重心在两块。第一是 Vue 路由 history 模式的刷新问题。如果直接暴露前端目录用户访问/schedule页面后一刷新Nginx 会去找/schedule这个文件发现不存在就报 404。解决方式是用 try_files 回退到 index.htmllocation / { root /var/www/lab-schedule; index index.html; try_files $uri $uri/ /index.html; }第二是 API 反向代理。前端请求/api开头的路径时转发到后端服务。这样后端不需要开启 CORS 跨域减少了安全暴露面。location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }注意proxy_pass里的路径。proxy_pass http://127.0.0.1:8080/api/;这种带 URI 的写法会替换/api/前缀如果你写成proxy_pass http://127.0.0.1:8080;不带斜杠请求会原样转发。这个区别连很多老手都容易记混。6.2 后端部署jar 包放 systemd 还是容器实验室项目的后端部署我更推荐直接用 systemd 管理 SpringBoot 的 jar 包而不是一开始就上 Docker。原因很简单实验室服务器往往还要跑其他服务Docker 的镜像构建、端口映射、日志收集都增加了部署维度。systemd 的服务文件写一次以后每次更新 jar 包后systemctl restart lab-schedule就完事日志直接用journalctl -u lab-schedule查看符合服务器运维的基本直觉。systemd 服务文件里我特别指定了运行参数[Service] ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/lab-schedule/lab-schedule.jar SuccessExitStatus143 Restarton-failure-Xms512m -Xmx1024m是经验值。默认的 JVM 堆大小在服务器上可能偏保守给的太大会影响同机其他服务实验排课系统的数据量1G 堆完全够了。SuccessExitStatus143是为了让 systemd 正确识别 Java 进程被 kill 时收到 SIGTERM 的退出码。没有这一行每次重启服务 systemd 都认为服务异常退出排查起来很懵。6.3 我实际踩过的三个坑第一排课记录插入时一直报重复键。当时我以为并发问题加了锁还不行后来才发现是测试阶段手动插入了大量同一时间片的数据唯一索引本身就在拦脏数据而不是代码逻辑出错。教训是遇到 Duplicate entry 先查数据别急着改代码。第二课表页面白屏。排查后发现是后端返回了BigDecimal类型的字段前端渲染模板里直接做四舍五入格式化导致undefined参与计算整个单元格渲染报错。最后在数据转换层统一处理把所有金额、人数等数值字段用Number()包裹避免类型隐式转换问题。第三Excel 导出乱码。刚开始直接用 POI 的 SXSSFWorkbook 生成文件下载后中文表头全部乱码。原因是我没有在 response 头里指定Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。加上之后正常。这个坑虽小但演示时被发现非常掉链子。7. 这套系统真正的价值和一个可以扩展的方向做完这个项目后我最大的体会是技术上 SpringBoot 和 Vue 并没有多少炫技空间真正让这个系统从能跑变成能用的是业务上对实验室排课约束的准确把握。你甚至可以不用任何复杂设计模式只要把冲突检测、容量校验、单双周判断这三个核心规则写扎实系统就已经满足九成实验室的需求。后续如果要扩展优先做实验室开放预约 设备报修联动。排课系统管的是教学计划内的课程但实验室课后的空闲时间其实有大量开放实验、竞赛训练、社团活动需求。把这部分预约流程加进来再把实验设备的损坏报修状态同步到排课逻辑里系统的粘性和实用性会再上一个台阶。
返回列表