SpringBoot+Vue智能教室管理系统:课表冲突检测与设备联动全链路实战
简介这是一套面向高校计算机相关专业学生与Java全栈初学者的智能教室管理系统完整项目源码采用SpringBoot后端与Vue前端前后端分离架构可直接用于毕业设计、课程结课作业或全栈练手项目帮助解决从零搭建管理系统时缺少完整可运行案例的问题。压缩包共978个文件约108.36MB其中274个java文件承载后端业务逻辑与接口实现99个vue文件与82个js文件构成前端页面与交互另有58个xml、26个vm模板、7个yml配置及1个sql脚本配合293个class编译产物与bat启动脚本覆盖依赖配置、数据库建表到前后端启动的完整链路。资源已有723人学习下载具备一定参考热度。读者可获得一套结构清晰、模块划分明确的前后端分离工程便于理解权限管理、数据表格与接口联调等常见实现方式并在此基础上进行二次开发或撰写论文。1. 智能教室管理系统从课表到灯光的全链路拆解智能教室管理系统这个词第一次接触的人容易把它想成教室预约小程序。真做过一轮就会发现它其实是一个把课表、教室资源、设备状态、考勤数据揉在一起的中后台系统。基于 SpringBootVue 前后端分离的智能教室管理系统核心要解决的是三件事教室和课表的冲突检测、设备投影、空调、灯光、门禁的状态联动、以及按角色分层的权限控制。它适合两类人一类是正在做毕业设计、节课作业需要一个结构完整、能讲清楚技术选型的项目另一类是刚接触前后端分离想找一个业务不复杂但链路完整的系统练手。这个标题里的智能不是指 AI 算法而是指设备状态和业务数据能自动流转别被名字带偏。下面按我实际搭过一遍的顺序把选型、建表、接口、联调和踩坑讲清楚。2. 技术选型与工程骨架为什么是 SpringBoot Vue 而不是别的2.1 后端选 SpringBoot 的三个现实理由毕业设计或节课作业这类场景时间通常只有几周选型的首要标准不是性能极限而是能不能快速跑起来、出了问题好不好查。SpringBoot 在这个场景下的优势很具体内嵌 Tomcat打成 jar 直接java -jar就能起不用单独装容器starter 依赖把 MyBatis、Redis、Web 这些常用组件版本对齐了省掉大量版本冲突排查自动配置让一个最小可运行的后端只需要一个启动类加一个配置文件。我一般会用的依赖组合是这样的写在pom.xml里!-- SpringBoot 版本建议 2.7.x3.x 对 JDK 要求 17毕设环境不一定跟得上 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 层提供 REST 接口 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 持久层MyBatis-Plus 比原生 MyBatis 少写大量 XML -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- JWT 做无状态登录前后端分离必备 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies这里有个参数要说明SpringBoot 版本别盲目追新。3.x 强制 JDK 17很多学校机房还是 JDK 8 或 11装环境会卡住。2.7.18 是 2.x 最后一个稳定版兼容 JDK 8毕设够用。MyBatis-Plus 选 3.5.x 是因为它的LambdaQueryWrapper能避免手写字段名拼错这个在教室、课表这种多条件查询里省事很多。2.2 前端选 Vue 2 还是 Vue 3这是被问得最多的问题。我的判断标准很简单如果项目里要用 Element UI选 Vue 2如果用 Element Plus选 Vue 3。毕业设计场景下Vue 2 Element UI 的教程和现成代码更多遇到问题搜得到答案容错率高。Vue 3 的组合式 API 更现代但setup语法对新手有额外学习成本。前端初始化命令# 用 Vue CLI 创建 Vue2 项目比 Vite 在毕设环境里更稳 vue create classroom-front # 进入目录后装依赖axios 负责接口element-ui 负责组件 cd classroom-front npm install axios element-ui vue-router vuex --save # 启动开发服务器默认 8080 npm run serve参数说明vue-router用 history 模式还是 hash 模式前后端分离部署时建议 hash省掉 Nginx 的 try_files 配置vuex用来存登录后的 token 和用户角色别用 localStorage 直接读刷新页面会丢状态。跨域在开发阶段用vue.config.js的 proxy 解决别在后端加CrossOrigin到处撒上线会乱。2.3 前后端分离的目录结构约定工程骨架定下来之后目录结构要提前约定不然后面接口对不上。我一般这样分classroom-back/ src/main/java/com/example/classroom/ controller/ // 对外接口 service/ // 业务逻辑 mapper/ // 数据访问 entity/ // 数据库实体 config/ // 拦截器、跨域、JWT 配置 src/main/resources/ application.yml mapper/ // 复杂 SQL 的 XML classroom-front/ src/ api/ // 接口封装 views/ // 页面 router/ // 路由 store/ // 状态这个约定看着普通但它决定了后面联调时你能不能一眼定位问题。接口返回统一用{code, msg, data}结构前端 axios 拦截器统一处理 code 不等于 200 的情况这个习惯能省掉大量重复判断。3. 数据库设计与核心表教室、课表、设备三张表怎么建3.1 教室表和课表表的字段取舍智能教室管理系统的数据模型核心是教室和课表的多对多关系。教室表要存容量、位置、类型普通/多媒体/机房课表表要存课程、教师、时间段、占用教室。这里有个容易翻车的地方时间段别用start_time和end_time两个 datetime 存用第几周 星期几 第几节三个字段存因为排课冲突检测是按节次算的用 datetime 反而要来回转换。-- 教室表 CREATE TABLE classroom ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL COMMENT 教室编号如 A101, capacity INT DEFAULT 0 COMMENT 容纳人数, room_type TINYINT DEFAULT 1 COMMENT 1普通 2多媒体 3机房, building VARCHAR(50) COMMENT 所在楼栋, status TINYINT DEFAULT 1 COMMENT 1可用 0停用 ); -- 课表表week_day 1-7section 1-12 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, classroom_id BIGINT NOT NULL, course_name VARCHAR(100), teacher_id BIGINT, week_day TINYINT COMMENT 星期几, section_start TINYINT COMMENT 开始节次, section_end TINYINT COMMENT 结束节次, week_range VARCHAR(50) COMMENT 如 1-16 周, INDEX idx_room_time (classroom_id, week_day, section_start) );参数说明idx_room_time这个联合索引是必须的冲突检测的查询条件是同一教室 同一天 节次区间重叠没有索引在数据量上千后会明显变慢。week_range用字符串存是为了简单如果要精确到单双周可以再加一个week_type字段。3.2 设备表和状态流水表设备表存投影、空调、灯光这些硬件的基本信息状态流水表记录每次开关操作。为什么要单独一张流水表因为智能体现在能追溯——谁在什么时候开了哪间教室的空调这个在答辩时是加分项。CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, classroom_id BIGINT NOT NULL, device_type VARCHAR(20) COMMENT projector/ac/light/door, device_code VARCHAR(50) COMMENT 设备唯一编码, online_status TINYINT DEFAULT 0 COMMENT 0离线 1在线 ); CREATE TABLE device_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id BIGINT, action VARCHAR(20) COMMENT on/off, operator_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );3.3 冲突检测的 SQL 写法排课冲突检测是这个系统里最值得讲的一段逻辑。判断某教室在某天某节次是否被占用本质是区间重叠判断新排课的[section_start, section_end]和已有课表的区间有交集就算冲突。SELECT COUNT(*) FROM schedule WHERE classroom_id #{classroomId} AND week_day #{weekDay} AND section_start #{newEnd} AND section_end #{newStart}逻辑说明区间重叠的判定条件是已有开始 ≤ 新结束 且 已有结束 ≥ 新开始这个写法比用BETWEEN更严谨能覆盖新排课完全包含已有课、被已有课包含、部分重叠三种情况。参数newStart和newEnd是前端传来的节次注意前端传的是字符串要转 int这个转换漏了会报类型错误是常见坑。4. 后端接口与权限JWT 登录和角色分层的落地写法4.1 JWT 登录拦截器的完整实现前后端分离项目里登录态不能靠 Session要用 JWT。流程是登录接口校验账号密码通过后签发 token前端存起来之后每个请求在 header 里带上后端拦截器统一校验。// JWT 工具类核心方法 public class JwtUtil { private static final String SECRET classroom-secret-key; private static final long EXPIRE 24 * 60 * 60 * 1000L; // 24小时 public static String createToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parse(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }参数说明SECRET别硬编码在代码里正式点放application.yml用Value注入答辩时能讲出这个细节是加分的。EXPIRE设 24 小时是毕设场景的折中太短调试烦太长不安全。role放进 token 是为了拦截器里直接判断角色不用每次查库。拦截器注册Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/register); } }逻辑说明addPathPatterns拦截所有/api开头的请求excludePathPatterns放行登录注册。这个配置漏了 exclude 会导致登录接口自己被拦死循环是新手必踩的坑。4.2 角色分层的三种权限智能教室管理系统一般有三种角色管理员、教师、学生。管理员能管教室和设备教师能排课和查自己课表学生只能查课表和预约空闲教室。权限控制放在拦截器里做别散落在各个 Controller。// 拦截器里根据路径和角色判断 String role claims.get(role, String.class); String uri request.getRequestURI(); if (uri.startsWith(/api/admin) !ADMIN.equals(role)) { response.setStatus(403); return false; }参数说明用路径前缀区分权限是最简单的做法/api/admin/**只有管理员能访问。更细的粒度可以用注解 AOP但毕设场景下路径前缀够用别过度设计。4.3 设备状态联动的接口设计设备控制接口要设计成幂等的因为前端可能重复点击。开灯接口收到请求后先查设备当前状态如果已经是开的就直接返回成功不重复写流水。PostMapping(/device/{id}/toggle) public Result toggle(PathVariable Long id, RequestParam String action) { Device device deviceMapper.selectById(id); if (device null) return Result.fail(设备不存在); // 幂等判断状态一致直接返回 if (on.equals(action) device.getOnlineStatus() 1) { return Result.ok(已处于开启状态); } deviceMapper.updateStatus(id, on.equals(action) ? 1 : 0); deviceLogMapper.insert(new DeviceLog(id, action, getCurrentUserId())); return Result.ok(); }逻辑说明幂等判断这段是血泪经验没有它前端连点两下就会产生两条流水答辩时被问为什么有重复记录会很尴尬。getCurrentUserId()从 token 里取别从请求参数取否则可以伪造。5. 前端页面与联调从登录到课表渲染的完整链路5.1 axios 封装和统一错误处理前端第一步不是写页面是封装 axios。所有接口请求走同一个实例统一加 token、统一处理错误码。// src/api/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截带上 token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization token return config }) // 响应拦截统一处理 code service.interceptors.response.use(res { const { code, msg, data } res.data if (code 200) return data if (code 401) { Message.error(登录过期请重新登录) router.push(/login) } else { Message.error(msg || 请求失败) } return Promise.reject(msg) }) export default service参数说明baseURL设成/api是为了配合开发环境的 proxy上线后由 Nginx 转发。timeout设 10 秒设备控制接口如果超时说明后端卡了别让用户干等。401 单独处理跳登录其他错误统一弹提示这个分层能避免每个页面都写一遍错误处理。5.2 课表页面的渲染逻辑课表页面是前端最复杂的部分本质是把二维数据渲染成网格。后端返回的是课表列表前端要转成星期几 × 节次的矩阵。// 把后端列表转成 7 行 12 列的网格 buildGrid(scheduleList) { const grid Array.from({ length: 12 }, () Array(7).fill(null)) scheduleList.forEach(item { for (let s item.sectionStart; s item.sectionEnd; s) { grid[s - 1][item.weekDay - 1] { courseName: item.courseName, teacher: item.teacherName, span: item.sectionEnd - item.sectionStart 1 } } }) return grid }逻辑说明节次从 1 开始数组下标从 0 开始所以是s - 1和weekDay - 1这个减一是最容易漏的漏了课表整体错位一格。span字段用来做跨行合并一门课占两节就合并两行Element UI 的span-method配合这个字段用。5.3 跨域和部署联调开发阶段跨域用vue.config.js的 proxymodule.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }参数说明target指向后端端口后端application.yml里server.port设 8081前端 8080两个端口别冲突。changeOrigin设 true 是为了改请求头里的 host某些后端校验 host 时不设会 403。上线时前端npm run build出静态文件后端打成 jar用 Nginx 把/api转发到后端端口静态文件直接指向 dist 目录这是最常见的部署方式。6. 避坑与排查五个真实踩过的坑6.1 坑一MyBatis-Plus 字段映射驼峰失效现象数据库字段room_no实体类属性roomNo查询出来是 null。原因MyBatis-Plus 默认开启驼峰映射但如果application.yml里手动配了map-underscore-to-camel-case: false或者用了自定义 SQL 没走自动映射就会失效。解决检查配置项自定义 SQL 里显式写AS roomNo或者用resultMap手动映射。我一般直接开map-underscore-to-camel-case: true别关。6.2 坑二JWT token 过期后前端死循环现象token 过期前端每个请求都返回 401拦截器不停跳登录页页面闪。原因响应拦截器里 401 直接router.push(/login)但登录页本身可能也在发请求形成循环。解决加一个标志位判断当前是否已经在登录页是就不重复跳。或者 401 时先清 token 再跳避免带着过期 token 反复请求。6.3 坑三课表冲突检测漏了周次现象第 1 周排的课和第 5 周排的课被判为冲突。原因冲突检测 SQL 只比了week_day和节次没比week_range。解决week_range存成1-16这种格式检测时先解析出周次区间再判断是否有交集。如果嫌麻烦可以拆成week_start和week_end两个 int 字段SQL 里直接比大小。6.4 坑四设备状态更新后前端不刷新现象点了开灯后端数据库状态变了但页面还是显示关。原因前端更新成功后没重新拉列表或者拉的是缓存数据。解决更新接口成功后重新调一次查询接口别在前端手动改本地状态容易和后端不一致。Vue 的响应式对数组元素更新有坑用this.$set或者直接重新赋值整个数组。6.5 坑五打包后接口 404现象开发环境正常npm run build后部署所有接口 404。原因开发环境靠 proxy 转发生产环境没有 proxybaseURL还是/api但 Nginx 没配转发规则。解决Nginx 配置里加location /api/ { proxy_pass http://localhost:8081/; }注意proxy_pass结尾的斜杠带斜杠会去掉/api前缀不带会保留配错了路径就多一层或少一层。7. 进阶技巧把设备联动做成可配置的规则引擎基础版本里设备开关是手动点的。如果想在答辩时多讲点东西可以把什么条件下自动开什么设备做成配置。比如课表开始前 10 分钟自动开投影和空调这个用定时任务加规则表就能实现不用引入复杂的规则引擎。思路是建一张device_rule表存触发条件时间偏移、教室、设备类型和执行动作。后端用 Spring 的Scheduled每分钟扫一次匹配到规则就调设备控制接口。Scheduled(cron 0 * * * * ?) // 每分钟执行 public void checkRules() { ListDeviceRule rules ruleMapper.selectActiveRules(); LocalDateTime now LocalDateTime.now(); for (DeviceRule rule : rules) { // 查该教室当前时间是否有课且距离开始时间等于偏移量 Schedule next scheduleMapper.findNextClass(rule.getClassroomId(), now); if (next ! null Duration.between(now, next.getStartTime()).toMinutes() rule.getOffsetMinutes()) { deviceService.turnOn(rule.getClassroomId(), rule.getDeviceType()); } } }参数说明cron表达式0 * * * * ?是每分钟第 0 秒执行别写成每秒设备接口扛不住。offsetMinutes存负数表示提前正数表示延后。这个方案的好处是不用改代码就能调规则答辩时能演示改个配置就自动开灯比纯手动控制有说服力。验证方法把offsetMinutes设成 1手动改系统时间到课前 1 分钟看设备流水表有没有新增记录。这个测试比等真实时间快得多。我自己的习惯是任何定时任务都要加日志记录扫了几条规则、匹配了几条、执行了几条出问题时看日志比打断点快。这个项目从建表到联调真正花时间的不是写代码是排查那些明明配了却不生效的玄学问题而大部分玄学最后都落在配置和字段映射上。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →