☰
实验室排课系统设计与实战:SpringBoot+Vue下的冲突检测与时间片模型
2026/9/30 3:08:55 网站建设 项目流程

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 8,SpringBoot 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 2(springfox)好用得多,因为 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意这几处设计都留了后手:

  • 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 节这种相邻时间也误判成冲突,因为 end=2 和 start=3 的关系是 2 < 3,正确的 SQL 应该判断为不冲突。

单双周的处理用了一个简化技巧:当已有记录是"每周"(week_type=1),或者新排的是"每周"时,都会产生交集,所以条件简化为三者中任一是每周,或两者模式相同。周次集合取交集的更通用逻辑是用位图,比如用 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-schedule,Nginx 配置的重心在两块。

第一是 Vue 路由 history 模式的刷新问题。如果直接暴露前端目录,用户访问/schedule页面后一刷新,Nginx 会去找/schedule这个文件,发现不存在就报 404。解决方式是用 try_files 回退到 index.html:

location / { 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 SuccessExitStatus=143 Restart=on-failure

-Xms512m -Xmx1024m是经验值。默认的 JVM 堆大小在服务器上可能偏保守,给的太大会影响同机其他服务,实验排课系统的数据量,1G 堆完全够了。SuccessExitStatus=143是为了让 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 并没有多少炫技空间,真正让这个系统从"能跑"变成"能用"的,是业务上对实验室排课约束的准确把握。你甚至可以不用任何复杂设计模式,只要把冲突检测、容量校验、单双周判断这三个核心规则写扎实,系统就已经满足九成实验室的需求。

后续如果要扩展,优先做"实验室开放预约 + 设备报修联动"。排课系统管的是教学计划内的课程,但实验室课后的空闲时间其实有大量开放实验、竞赛训练、社团活动需求。把这部分预约流程加进来,再把实验设备的损坏报修状态同步到排课逻辑里,系统的粘性和实用性会再上一个台阶。

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

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

立即咨询