简介:这是一套面向高校计算机相关专业学生与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. 智能教室管理系统:从课表到灯光的全链路拆解
智能教室管理系统这个词,第一次接触的人容易把它想成"教室预约小程序"。真做过一轮就会发现,它其实是一个把课表、教室资源、设备状态、考勤数据揉在一起的中后台系统。基于 SpringBoot+Vue 前后端分离的智能教室管理系统,核心要解决的是三件事:教室和课表的冲突检测、设备(投影、空调、灯光、门禁)的状态联动、以及按角色分层的权限控制。它适合两类人:一类是正在做毕业设计、节课作业,需要一个结构完整、能讲清楚技术选型的项目;另一类是刚接触前后端分离,想找一个业务不复杂但链路完整的系统练手。这个标题里的"智能"不是指 AI 算法,而是指设备状态和业务数据能自动流转,别被名字带偏。下面按我实际搭过一遍的顺序,把选型、建表、接口、联调和踩坑讲清楚。
2. 技术选型与工程骨架:为什么是 SpringBoot + Vue 而不是别的
2.1 后端选 SpringBoot 的三个现实理由
毕业设计或节课作业这类场景,时间通常只有几周,选型的首要标准不是性能极限,而是"能不能快速跑起来、出了问题好不好查"。SpringBoot 在这个场景下的优势很具体:内嵌 Tomcat,打成 jar 直接java -jar就能起,不用单独装容器;starter 依赖把 MyBatis、Redis、Web 这些常用组件版本对齐了,省掉大量版本冲突排查;自动配置让一个最小可运行的后端只需要一个启动类加一个配置文件。
我一般会用的依赖组合是这样的,写在pom.xml里:
<!-- SpringBoot 版本建议 2.7.x,3.x 对 JDK 要求 17,毕设环境不一定跟得上 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 层,提供 REST 接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 持久层,MyBatis-Plus 比原生 MyBatis 少写大量 XML --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- JWT 做无状态登录,前后端分离必备 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.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-7,section 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的 proxy:
module.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 转发,生产环境没有 proxy,baseURL还是/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() { List<DeviceRule> 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 分钟,看设备流水表有没有新增记录。这个测试比等真实时间快得多。
我自己的习惯是,任何定时任务都要加日志,记录"扫了几条规则、匹配了几条、执行了几条",出问题时看日志比打断点快。这个项目从建表到联调,真正花时间的不是写代码,是排查那些"明明配了却不生效"的玄学问题,而大部分玄学最后都落在配置和字段映射上。希望帮到你。
本文还有配套的精品资源,点击获取