SpringBoot+Vue教务排课系统实战:冲突校验与课表渲染
2026/9/16 4:47:36 网站建设 项目流程

简介:本资源是一套基于SpringBoot与Vue的Web版课表管理系统完整源码,面向计算机专业本科生毕业设计与Java全栈入门学习者,解决课程安排、教师排课、学生课表查询等教务管理核心需求。压缩包共556个文件,含115个Java后端逻辑文件、86个Vue组件页面、41个JS交互脚本、39个JPG/PNG素材图及15个XML配置与CSS样式文件,辅以SQL建表语句、启动批处理(bat)、数据库备份与项目文档(PDF/DOCX),整体15.2MB,结构清晰,覆盖前后端分离典型开发流程。已有116人学习下载,资源附带完整论文目录(含绪论、技术选型、可行性分析、系统设计与实现章节)及可运行工程,开箱即用,支持Eclipse/IDEA+MySQL 5.7+JDK1.8环境快速部署,适合毕设开发、课程设计实践与SpringBoot+Vue技术栈综合训练。

1. 这不是又一个“增删改查课表”,而是一套能跑通教务排课逻辑的 SpringBoot+Vue 全栈闭环

很多同学拿到“课表系统毕业设计”源码,第一反应是点开run.bat看能不能弹出登录页——能弹出来就交差。但真正卡住人的,从来不是登录页,而是「周三下午第3-4节,计算机楼205教室,同时被三门课抢着排」这种冲突校验;是「张老师本学期最多带6学时,已排4学时,再加一节就超限」这类业务规则落地;更是「导出Excel课表时,合并单元格逻辑错位导致打印变形」这种前端渲染细节。这套源码之所以在 Java 毕业设计圈里被反复传阅,核心在于它把「排课约束建模→后端校验拦截→前端可视化反馈→导出格式保真」这条链路做实了,不是只画UI、不跑逻辑的Demo。适合正在做教务类Web项目、需要快速验证SpringBoot+Vue协同开发流程、且对MyBatis-Plus动态SQL和Element UI表单联动有实操需求的开发者。JDK 1.8 + MySQL 5.7 + Vue 2.6 是硬性门槛,跨版本强行升级会触发Invalid bound statement (not found)Cannot read property 'length' of undefined这类典型报错。

2. SpringBoot 后端:从 MyBatis-Plus 动态条件构建到排课冲突拦截器

2.1 排课核心实体与 MyBatis-Plus 的条件构造器实战

课表系统最关键的实体不是Course(课程),而是Schedule(课表安排记录)。源码中Schedule.java定义了teacherId,courseId,classroomId,weekDay,sectionStart,sectionEnd,weekType(单双周)等字段,其中weekTypeTINYINT(1)存储(0=全周,1=单周,2=双周),这是规避VARCHAR存储导致索引失效的关键设计。MyBatis-Plus 的QueryWrapperScheduleServiceImpl.java中被高频使用:

// 查询某教师在指定周次、星期、节次是否存在冲突 public List<Schedule> findConflictByTeacher(Long teacherId, Integer weekNum, Integer weekDay, Integer sectionStart, Integer sectionEnd) { QueryWrapper<Schedule> wrapper = new QueryWrapper<>(); wrapper.eq("teacher_id", teacherId) .eq("week_num", weekNum) .eq("week_day", weekDay) .apply("section_start <= {0} AND section_end >= {1}", sectionEnd, sectionStart); return this.list(wrapper); }

注意apply()方法直接拼接 SQL 片段,此处用于实现「节次重叠判断」——若新课节次为3-4节(sectionStart=3, sectionEnd=4),则需检查已有记录是否满足section_start <= 4 AND section_end >= 3。不能用between,因为between 3 and 4无法覆盖section_start=2, section_end=5这种跨节情况。

2.2 自定义拦截器实现排课业务规则强校验

单纯数据库唯一索引无法覆盖「教师周课时超限」「教室时段占用」「课程类型冲突(如实验课必须配实验室)」等复合规则。源码在config/InterceptorConfig.java中注册了ScheduleCheckInterceptor

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("POST") && request.getRequestURI().contains("/schedule/save")) { String jsonBody = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); Schedule schedule = JSON.parseObject(jsonBody, Schedule.class); // 规则1:检查教师周课时 int currentHours = scheduleService.getTeacherWeekHours(schedule.getTeacherId(), schedule.getWeekNum()); if (currentHours + getSectionCount(schedule) > 6) { response.getWriter().write("{\"code\":400,\"msg\":\"教师周课时超限\"}"); return false; } // 规则2:检查教室时段占用(调用2.1中的findConflictByRoom方法) if (!scheduleService.checkRoomAvailable(schedule)) { response.getWriter().write("{\"code\":400,\"msg\":\"教室已被占用\"}"); return false; } } return true; }

提示getSectionCount()是工具方法,根据sectionStartsectionEnd计算实际节数(如3-4节=2节)。拦截器返回false会中断请求,前端axios需捕获400状态码并提示具体错误,而非显示“请求失败”。

2.3 数据库初始化与 SQLyog/Navicat 导入关键步骤

MySQL 5.7 必须启用sql_mode=STRICT_TRANS_TABLES,否则INSERT INTO schedule (...) VALUES (...)可能因NULL值插入成功但数据异常。建库脚本db/schedule.sql包含三张核心表:

  • teacher(教师表):id,name,max_week_hours(最大周课时,默认6)
  • room(教室表):id,name,type(类型:普通/实验室/机房)
  • schedule(课表表):外键关联teacher_id,room_id,course_id,并建立联合索引(teacher_id, week_num, week_day)(room_id, week_num, week_day)

使用 SQLyog 导入时,必须勾选「执行所有语句」和「忽略错误」,因为脚本中包含DROP TABLE IF EXISTS语句。导入后执行SELECT COUNT(*) FROM schedule;应返回非零值,若为0,检查db/data_init.sql是否被遗漏——该文件预置了测试教师、教室和课程数据,是run.bat启动后能登录的关键。

3. Vue 前端:Element UI 表单联动与课表可视化渲染

3.1 动态表单联动:课程类型决定教室类型筛选

IndexMain.vue.bak中的课程录入表单,通过el-select@change事件实现「课程类型→教室类型」联动:

<el-form-item label="课程类型"> <el-select v-model="form.courseType" placeholder="请选择" @change="fetchRoomsByType"> <el-option label="理论课" value="LECTURE"></el-option> <el-option label="实验课" value="LAB"></el-option> <el-option label="上机课" value="COMPUTER"></el-option> </el-select> </el-form-item> <el-form-item label="教室"> <el-select v-model="form.roomId" placeholder="请选择"> <el-option v-for="room in roomOptions" :key="room.id" :label="room.name" :value="room.id"> </el-option> </el-select> </el-form-item>

对应 JS 方法:

methods: { fetchRoomsByType() { const typeMap = { 'LECTURE': ['普通'], 'LAB': ['实验室'], 'COMPUTER': ['机房'] }; this.$http.get(`/room/list?types=${typeMap[this.form.courseType].join(',')}`) .then(res => { this.roomOptions = res.data; }); } }

注意:后端RoomController.java/room/list接口接收types参数,用FIND_IN_SET查询(MySQL 5.7 支持),避免IN子查询性能下降。若返回空数组,检查room.type字段值是否为'普通'而非'普通教室'——源码约定值必须严格匹配。

3.2 课表网格渲染:二维数组生成与跨行合并逻辑

课表主视图ScheduleGrid.vue(由IndexMain.vue.bak引入)将weekDay(1-7)和section(1-12)映射为二维数组:

computed: { gridData() { // 初始化7×12空数组 const grid = Array.from({ length: 7 }, () => Array(12).fill(null)); // 填充真实课表数据 this.schedules.forEach(item => { const row = item.weekDay - 1; // 星期一对应索引0 const col = item.sectionStart - 1; // 第1节对应索引0 // 计算跨行数:sectionEnd - sectionStart + 1 const rowspan = item.sectionEnd - item.sectionStart + 1; // 在grid[row][col]位置存对象,并标记rowspan grid[row][col] = { ...item, rowspan }; // 清空后续列,避免重复渲染 for (let i = 1; i < rowspan; i++) { if (col + i < 12) grid[row][col + i] = null; } }); return grid; } }

模板中使用v-for渲染:

<el-table :data="gridData" border> <el-table-column v-for="(day, dayIndex) in ['一','二','三','四','五','六','日']" :key="dayIndex"> <template #header> <div>{{ day }}</div> </template> <template #default="{ row }"> <div v-for="(cell, colIndex) in row" :key="colIndex" :rowspan="cell?.rowspan || 1" v-if="cell"> {{ cell.courseName }}<br>{{ cell.teacherName }} </div> <div v-else>&nbsp;</div> </template> </el-table-column> </el-table>

提示rowspan属性必须绑定到<td>:rowspan,而非<div>。源码中IndexMain.vue.bak的原始实现用el-table组件,其rowspanspan-method函数控制,但.bak文件保留了更易调试的手动二维数组方案。

3.3 Excel 导出:Apache POI 与单元格样式保真

导出功能在ScheduleExport.js中实现,关键点在于合并单元格与字体设置:

// 创建单元格样式 CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontHeightInPoints((short) 10); font.setBold(true); style.setFont(font); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); // 合并周一第1-2节单元格 CellRangeAddress region = new CellRangeAddress( 1, // 起始行(0-indexed) 1, // 结束行 1, // 起始列 2 // 结束列 ); sheet.addMergedRegion(region);

注意CellRangeAddress的行列索引从0开始,addMergedRegion()必须在设置单元格值之前调用,否则合并无效。导出后若出现「合并区域重叠」错误,检查是否对同一区域多次调用addMergedRegion()

4. 工程启动与常见故障排查:从run.bat到端口冲突解决

4.1run.bat执行流程与 JDK 1.8 环境验证

双击run.bat实质执行三步:

  1. mvn clean compile:清理并编译 Java 代码
  2. mvn resources:resources:拷贝src/main/resources下配置文件到target/classes
  3. java -jar target/schedule-0.0.1-SNAPSHOT.jar:启动 SpringBoot 内嵌 Tomcat

验证 JDK 1.8 是否生效:

# 在命令行执行 java -version # 正确输出应为: # java version "1.8.0_291" # Java(TM) SE Runtime Environment (build 1.8.0_291-b10) # Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode)

若显示11.0.x17.0.x,需修改系统环境变量JAVA_HOME指向 JDK 1.8 安装目录,并重启命令行。否则mvn compile会报错Unsupported class file major version 61(Java 17 的版本号)。

4.2 端口冲突与 MySQL 连接失败诊断

启动时若控制台快速滚动Failed to bind to 0.0.0.0:8080,说明 8080 端口被占用。解决方案:

  • 查看占用进程:netstat -ano | findstr :8080(Windows)或lsof -i :8080(Mac/Linux)
  • 杀死进程:taskkill /PID <PID> /F(Windows)或kill -9 <PID>(Mac/Linux)

MySQL 连接失败常见于application.yml配置错误:

spring: datasource: url: jdbc:mysql://localhost:3306/schedule_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: 123456 # 此处密码必须与MySQL实际root密码一致

提示:若 MySQL 5.7 使用mysql_native_password加密方式,需在url后追加&allowPublicKeyRetrieval=true&useSSL=false,否则报错Client does not support authentication protocol requested by server

4.3 前端资源加载失败:IndexHeader.vue.bak的静态资源路径修正

IndexHeader.vue.bak中引用了static/img/logo.png,但实际路径为src/assets/img/logo.png。需修改:

<!-- 错误 --> <img src="/static/img/logo.png" alt="logo"> <!-- 正确 --> <img :src="require('@/assets/img/logo.png')" alt="logo">

同时检查vue.config.js是否配置了public目录别名:

module.exports = { configureWebpack: { resolve: { alias: { '@': path.resolve(__dirname, 'src'), 'static': path.resolve(__dirname, 'public') } } } }

若未配置,/static/会被解析为根路径,导致 404。

5. 进阶技巧:基于update-password.vue.bak实现密码强度策略与加密迁移

5.1 密码强度校验与后端加密一致性

update-password.vue.bak中的密码修改表单,前端校验仅用正则^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$(至少8位,含大小写字母和数字)。但后端UserController.javaupdatePassword方法必须同步校验:

@PostMapping("/updatePassword") public Result updatePassword(@RequestBody Map<String, String> params) { String newPassword = params.get("newPassword"); // 复用前端正则 if (!newPassword.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$")) { return Result.fail("密码必须包含大小写字母和数字,长度不少于8位"); } // 加密存储:使用BCryptPasswordEncoder(Spring Security提供) String encodedPassword = passwordEncoder.encode(newPassword); user.setPassword(encodedPassword); userService.updateById(user); return Result.success(); }

注意passwordEncoderBean 需在SecurityConfig.java中声明为@Bean,且User实体类的password字段必须为String类型,不可用char[]——MyBatis-Plus 无法自动处理char[]

5.2 从 MD5 迁移至 BCrypt 的平滑过渡方案

若历史数据用 MD5 加密(如application.ymlpassword-encoder: md5),需支持新旧密码共存。在UserDetailsService实现中:

@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); if (user == null) throw new UsernameNotFoundException("用户不存在"); // 判断密码是否为BCrypt格式(以$2a$开头) if (user.getPassword().startsWith("$2a$")) { return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), new ArrayList<>()); } else { // 旧MD5密码,先验证再更新 if (MD5Util.verify(params.get("password"), user.getPassword())) { user.setPassword(passwordEncoder.encode(params.get("password"))); userMapper.updateById(user); } } }

此方案允许用户首次登录时自动升级密码哈希,无需批量数据迁移。

5.33-build.bat2-run.bat的差异化用途表格

脚本名执行时机核心命令典型用途注意事项
2-run.bat日常开发调试mvn spring-boot:run启动热部署服务,修改Java代码后自动重启依赖spring-boot-devtools,需在pom.xml中启用
3-build.bat发布部署前mvn clean package -Dmaven.test.skip=true打包成target/schedule-0.0.1-SNAPSHOT.jar跳过测试用例,确保打包速度;生成的 JAR 包可直接java -jar运行
build.bat旧版构建脚本mvn clean compile package兼容早期 Maven 版本若报错No plugin found for prefix 'package',改用3-build.bat

提示2-run.bat启动后访问http://localhost:8080,若页面空白,检查浏览器控制台是否有Failed to load resource: net::ERR_CONNECTION_REFUSED——这表示后端未启动成功,而非前端问题。

本文还有配套的精品资源,点击获取

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

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

立即咨询