做一个毕业设计选题,最怕的不是技术难,而是方向太泛、最后做出来一个“啥都会但啥都不精”的演示品。这阵子刚好在帮学生梳理课表管理系统这类选题,SpringBoot + Vue + MySQL 的组合可以说是目前 Java 后端方向最稳的毕业设计配方了。今天这篇就从我实际带项目的角度,把“西安工商学院课表管理系统”从选题拆解、表结构设计、后端接口实现到前端页面联调、最后写论文和打包部署的完整链路捋一遍,重点说那些坑和“为什么这么做”的部分,给正在做或者准备做类似系统的同学一个可以直接参照的路线。
先把这个项目一句话讲清楚:这是一个基于 SpringBoot 2.x + Vue 2.x + MySQL 8 的 Web 课表管理系统,核心解决学校排课数据可视化、教师课程查询、学生个人课表导出、教室空余时段统计这些场景。适合的读者是:正在做毕业设计但代码经验不多、需要一套完整可复现方案的计算机专业学生,以及想快速上手前后端分离项目的初学者。
1. 内容整体设计与思路拆解
1.1 为什么选“课表管理”这个方向
课表管理这个业务,看起来普普通通,但它覆盖了一个 Web 系统几乎所有核心环节:数据的增删改查、多表关联(学生、教师、课程、班级、教室)、时间段的状态判断、前端列表渲染与条件筛选,还有权限区分(管理员排课、教师查看、学生查询)。这些能力正好对应毕业设计评审时最常被问到的几个点:数据库设计是否合理、后端接口是否规范、前端交互是否完整、系统有没有实际使用价值。
相比网上烂大街的“图书管理”“学生管理”这类纯 CRUD 项目,课表系统多了一层“排课校验”的业务逻辑,能体现出工作量,但又不会复杂到一个人做不完。尤其像“某工商学院”这种具体校名场景,可以把专业、校区、教学楼编号都做成可配置项,展示时更有真实感。
1.2 技术栈选型背后的逻辑
SpringBoot 选型几乎不用犹豫,它内置 Tomcat、自动配置、starter 体系让开发效率很高,而且毕业设计答辩时评委普遍熟悉这套生态。Vue 2 + Element UI 是当前毕业设计中最稳妥的前端组合,组件丰富、文档多、问题答案容易搜到,比 Vue 3 + TypeScript + Composition API 上手门槛低不少。MySQL 8 提供更完整的窗口函数和 JSON 支持,但实际这个项目用到的也就是基本 SQL,即使学校机房装的 MySQL 5.7 也完全兼容。
需要重点说明的是,项目用了前后端分离结构,而不是传统的 JSP/Thymeleaf 模板方案。分离式开发意味着前端 8080 端口、后端 8081 端口,通过接口交互。这个设计不仅是技术趋势,更是为了让论文里能单独写“前后端交互设计”和“RESTful API 设计”两章,凑工作量很自然。
1.3 功能模块划分与核心用例
我把系统拆成四个主要模块:
- 管理员端:登录、教师管理、班级管理、教室管理、课程管理、排课管理(新增/调课/删除)、全校课表总览。
- 教师端:查看个人课表、按周切换、导出个人课表。
- 学生端:按班级/学号查询课表、按周切换、导出课表。
- 公共功能:登录鉴权(JWT)、统一异常处理、跨域配置。
权限这块千万不要做成简单的页面隐藏,因为前端隐藏菜单并不能挡住别人直接访问接口。正确做法是后端在每次请求时从 token 解析用户角色,在 Controller 层做接口级别校验。Spring Security 或者拦截器都可以,毕业设计用拦截器 + JWT 已经足够,代码量少、逻辑透明,论文也好解释。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 数据库表结构设计:这是论文答辩的高频雷区
很多同学上来就建一张 big 表,把所有字段堆在一起,最后调课逻辑乱成一团。课表系统的核心不是课表本身,而是排课的资源约束。我这里的表设计遵循“实体拆分 + 时间冗余”两个原则。
建议最小表集合:
- 用户表
sys_user:id、username、password(BCrypt 加密存储)、real_name、role(admin/teacher/student)、teacher_id/student_id 作为关联外键。 - 学生表
student:id、student_no(学号)、name、class_id。 - 教师表
teacher:id、teacher_no、name、title(职称)。 - 班级表
class_info:id、class_name、grade(年级)、major(专业)。 - 课程表
course:id、course_name、course_code、credit、hours、teacher_id。 - 教室表
classroom:id、room_no、building、capacity、is_available。 - 课表主表
schedule:id、student_class_id / teacher_id / course_id / classroom_id / day_of_week / start_section / end_section / week_start / week_end / semester。
schedule 表里的 day_of_week 表示周几(1-7),start_section 表示从第几节开始,end_section 表示到第几节结束(比如 1-2 节,start=1, end=2),week_start 和 week_end 表示第几周到第几周,这样能支持单双周和分段排课。千万别把每一节课都存成独立记录,否则 18 周 * 每天 8 节 * 几十个班,数据量爆炸而且调课要批量更新,非常痛苦。
主键一律使用自增 id,不搞业务主键。关联字段要加索引:schedule 表加 (course_id, day_of_week, start_section) 联合索引、(class_id, semester) 联合索引。这个在论文数据库设计章节里写出来很加分。
2.2 排课冲突校验:项目的隐藏得分点
排课模块最核心的校验就是“冲突检测”——同一个班级、同一个学期,在同一时间段不能有两门课;同一个教室同理;同一个教师也要避免时间重叠。这听起来简单,但很容易写漏。
我刚带学生的时候他写的校验是每次只查已存在记录里有没有 day_of_week 相等且 start_section 相等的条目。这明显不够:一门课是 1-2 节,另一门课是 2-3 节,虽然 start_section 不同,但第 2 节冲突了。
正确做法是把时间段当作区间判断。插入前查出所有候选记录,逐条判断新时间区间 [start, end] 与旧区间 [old_start, old_end] 是否存在交集,条件就是 start <= old_end && end >= old_start。注意要同时考虑 week_start 和 week_end,如果两门课分别在单双周上课,区间相交也不能算冲突。这里需要在 Java 代码里写两层循环,或者用 SQL 写一个关联查询,两种方案我都试过,数据量不大时直接查出来在 Java 里判断更清晰,而且方便在冲突时返回具体冲突对象。
2.3 周次显示与学期管理
实际学校课表是按教学周展示的,所以还要维护一个 semester_config 表,切分学期名称、当前周次、总周数。前端切换“第 X 周”时,后端要根据周数判断单双周,然后过滤课表记录。我这里的做法是:schedule 表不直接存周一/周二,而是存固定的 day_of_week;查询时带 week 参数,后端检查 week 是否在 [week_start, week_end] 之内,同时如果排课标记了“单周”,就用 week % 2 判断。
这个逻辑其实不复杂,但很多人忽略了一个细节:学期跨年时周次不会清零,比如 2023-2024 学年第二学期从第 1 周开始到第 20 周,系统当前周应该由管理员手动维护,而不是根据日期自动算,因为学校经常因为节假日调休导致某周停课,自动算容易出错。
3. 实操过程与核心环节实现
3.1 项目初始化和依赖配置
后端我用的是 SpringBoot 2.7.x,Java 8。之所以不用 Spring Boot 3,因为 3 要求 JDK 17,很多学生的电脑和学校服务器还是 JDK 8,且 3 的 javax 包改成 jakarta,网上大部分教程不兼容,踩坑成本高。依赖就三个核心:spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java,另外加 jjwt 做 token,hutool 做日期工具。
MyBatis-Plus 是毕业设计神器,内置 CRUD,减少大量 mapper XML 编写。不过要注意,复杂查询还是建议手写 SQL,尤其是课表查询这种多条件关联查询,不要无脑用 LambdaQueryWrapper 拼接,可读性差且性能不易优化。
application.yml 里几个关键配置:
yaml复制server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/course_schedule?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: delFlag
logic-delete-value: 1
logic-not-delete-value: 0
这里有个容易踩的坑:MySQL 8 的驱动类必须是 com.mysql.cj.jdbc.Driver,不是旧的 com.mysql.jdbc.Driver。同时连接 URL 要加 serverTimezone=Asia/Shanghai,否则会报时区错误。另外我开了 MyBatis-Plus 的 SQL 日志,开发时能看到每条 SQL,排错非常有用,正式部署时记得关掉。
前端我用 Vue 2.6 + Element UI + axios。工程通过 Vue CLI 创建,npm run serve 启动后代理到后端:
js复制// vue.config.js
module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
请求路径统一带 /api 前缀,开发环境走代理,生产环境把前端打包后的静态文件直接交给 SpringBoot 托管,后端 Controller 统一写 /api 前缀,这样部署时只需要打一个 jar,前端资源放 src/main/resources/static 下即可,非常省事。
3.2 后端核心接口设计与实现
以“课表查询”为例,这是系统最重要的接口。
java复制@GetMapping("/schedule/list")
public Result getScheduleList(@RequestParam Integer role,
@RequestParam(required = false) Long teacherId,
@RequestParam(required = false) Long studentClassId,
@RequestParam(required = false) Integer week) {
List<ScheduleVO> list = scheduleService.querySchedule(role, teacherId, studentClassId, week);
return Result.success(list);
}
核心 SQL 是这样的:
xml复制<select id="querySchedule" resultType="com.example.entity.vo.ScheduleVO">
SELECT
s.id,
s.day_of_week,
s.start_section,
s.end_section,
s.week_start,
s.week_end,
s.semester,
c.course_name,
c.credit,
t.real_name AS teacher_name,
cr.building,
cr.room_no,
ci.class_name
FROM schedule s
LEFT JOIN course c ON s.course_id = c.id
LEFT JOIN teacher t ON s.teacher_id = t.id
LEFT JOIN classroom cr ON s.classroom_id = cr.id
LEFT JOIN class_info ci ON s.class_id = ci.id
<where>
<if test="teacherId != null">AND s.teacher_id = #{teacherId}</if>
<if test="studentClassId != null">AND s.class_id = #{studentClassId}</if>
<if test="week != null">
AND #{week} BETWEEN s.week_start AND s.week_end
</if>
</where>
ORDER BY s.day_of_week, s.start_section
</select>
注意我用 LEFT JOIN 而不是 INNER JOIN,保证即使有脏数据也能查出主记录,前端列表不会整行消失。排课新增接口需要事务,因为要先校验冲突,再插入记录。我在 ScheduleServiceImpl 中加 @Transactional(rollbackFor = Exception.class),同时校验逻辑里把“教室冲突”“班级冲突”“教师冲突”分类返回提示信息,这样前端可以明确弹出“该教室第 X 节已有课,冲突课程为……”之类的提示,比单一提示“排课失败”体验好很多,也方便论文里写异常处理章节。
3.3 前端页面实现:从蹩脚组件到顺畅交互
前端页面我用了经典的后台布局:左侧菜单、顶部导航、主内容区。核心页面是课表网格组件。Element UI 没有现成日历网格,我直接用 el-table 来做:行是节次(第 1 节到第 8 节),列是星期一到星期日,每个单元格渲染课程卡片。用 table 的好处是天然支持边框和对齐,缺点是行列合并逻辑要自己控制。
做法是:后端返回一个一维数组,前端先按 day_of_week 和 start_section 分组,然后对 end_section > start_section 的记录做 rowspan 合并。这个计算在 computed 里完成,核心代码如下:
js复制formatSchedule(list) {
const map = {};
list.forEach(item => {
const key = `${item.dayOfWeek}-${item.startSection}`;
if (!map[key]) map[key] = [];
map[key].push(item);
});
// 返回表格行结构...
}
这里有个特别需要注意的点:同一个格子可能有多条课(比如某天 1-2 节同时存在两组实验课),不能直接覆盖,而是要堆叠显示。我在渲染时限制了每个格子最多显示两条,超出部分显示“+N”并提供弹窗查看详情。这个细节虽然工作量不大,但做出来后很提质感,答辩演示操作时特别有说服力。
周次切换我放在顶部下拉框,选择周次后重新请求接口。同时做了个简单的单双周标记,如果当前课程属于单周或双周,课程卡片右上角会显示一个“单/双”标签。这里的核心是后端把 weekType 字段返回,前端根据当前选择周做过滤,否则把全部课程都塞进同一个周次展示,逻辑会很乱。
教师端和学生端页面复用同一个课表组件,只是查询参数不同。教师端默认查当前登录教师 id,学生端通过班级查询。导出课表我用的是前端 table 转 Excel,实际上就是遍历表格行生成 CSV 字符串,然后用 Blob 下载。CSV 方案比 poi 后端导出简单很多,而且导出后中文不乱码的关键是添加 \uFEFF BOM 头。这个技巧很小,但很实用,论文里也能提一笔。
3.4 登录鉴权和路由守卫
前端路由配置了 meta: { roles: ['admin'] },然后在全局前置守卫里读 localStorage 中的 token,再通过后端 /auth/info 接口获取用户角色,比对路由要求的角色。但后端接口必须重复做校验,不能指望前端。
我写了一个简单的拦截器:
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("OPTIONS")) {
return true;
}
String token = request.getHeader("Authorization");
// 校验并解析...
}
}
需要注意跨域问题。前后端分离开发时,前端在 8080,后端在 8081,浏览器会发起跨域请求。我在后端配置了 CorsFilter,允许本地前端地址访问。部署后同源了,这个配置也不会影响正常访问。
JWT 的密钥不要硬编码在业务代码里,我放在配置文件,并且设置了过期时间 2 小时。毕业设计虽然不必做到生产级安全,但至少不能让用户随便伪造 token 拿到管理员权限。密码存储用 BCrypt 加密,网上很多项目用 MD5,答辩时容易被追问,BCrypt 简单且安全观感好。
4. 常见问题与排查技巧实录
这部分全是实际带项目过程中学生反复踩的坑,我直接列成速查表,每一项都是血泪经验。
4.1 数据库连接失败与时区问题
症状:启动项目后访问接口报 Access denied for user 'root'@'localhost' 或者 Could not create connection to database server。
排查:
- 确认 MySQL 服务有没有启动:
net start mysql/service mysql status。 - 确认密码是否正确,MySQL 8 默认加密方式是 caching_sha2_password,高版本 jdbc 驱动才支持,所以驱动要够新。
- 连接 URL 必须加
serverTimezone=Asia/Shanghai,否则即使连上也报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。 - 端口如果是云服务器,记得在安全组开发 3306。本地开发建议直接用 root,不要新建用户,省得权限配置出幺蛾子。
4.2 前端跨域请求失败
症状:浏览器控制台报 CORS policy 或者 Proxy error。
处理方案:开发环境配置 devServer proxy 后,前端请求相对路径 /api/login,而不是绝对地址 http://localhost:8081/login。只要不写全地址,就不会触发跨域。后端也可以配置跨域过滤器,但更推荐前端代理,因为部署后无需改动。
4.3 排课时间段冲突校验漏判
这是逻辑 bug,最容易翻车。我前面已经讲了区间相交的判断。这里再补一个坑:有些学生在校验时只查了 start_section 相等,实际上两门课 1-2 节和 2-3 节的第 2 节是冲突的。务必用区间判断,并写单元测试去覆盖这个场景。
同时要注意星期字段的取值,MySQL 日期函数 DAYOFWEEK() 返回的是 1=Sunday, 2=Monday,和国内习惯不同。我这里统一用数字 1-7 代表周一至周日,查询时不经过日期函数转换,避免混乱。
4.4 MyBatis-Plus 分页插件失效
用了 PageHelper 的同学要注意:MyBatis-Plus 和 PageHelper 分页插件冲突时,会出现总数不对或者只能查出一页数据。MyBatis-Plus 有自己的一套分页插件,直接配置 PaginationInnerInterceptor,不要两个同时用。
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
课表列表通常不一定要分页,但“课程管理”“教师管理”这些列表需要。实现完记得检查 total 值,如果一直等于 0,大概率就是没配分页插件。
4.5 前端打包后部署到 SpringBoot 的坑
npm run build 后生成 dist 文件夹,很多人直接把 dist 扔到服务器上用 Nginx。其实如果只是为了毕设演示,可以直接把 dist 里的文件复制到 src/main/resources/static/ 目录下,然后重新打包后端 jar。这样只需启动一个 Java 进程,省事多了。
但这里有一个大坑:Vue Router 默认是 history 模式,打包后刷新页面会 404。解决方法是把路由改成 hash 模式(new Router({ mode: 'hash' })),或者在后端写一个转发 controller 把所有非 API 请求转发到 index.html。毕业设计图省事就选 hash 模式,地址栏带个 #,不影响功能。
4.6 导出课表中文乱码
CSV 导出的编码问题我前面提过一次,这里再强调:用 \uFEFF 加 BOM 头,或者后端响应头设置 Content-Type: text/csv; charset=utf-8,同时文件名做 URL 编码。如果文件名直接是中文,部分浏览器会乱码,用 encodeURIComponent 处理即可。
其实还有一个常见错误:window.open 下载文件被浏览器拦截。所以导出按钮最好不要用 open,而是创建一个隐藏 a 标签,设置 download 属性再 click(),同时捕获 blob。这样不会被拦截,体验更稳定。
5. 论文撰写与部署交付的实操建议
5.1 毕业论文结构怎么排
有些同学代码做出来了,论文却不知道写什么。我的建议是不要照抄网上的模板,最好按自己实际编码思路来写。推荐大纲:
- 第一章 绪论:背景、国内外现状、研究意义、主要工作。
- 第二章 相关技术介绍:SpringBoot、Vue、MySQL、MyBatis-Plus、JWT,每个写 1-2 页,配架构图。
- 第三章 系统分析:可行性分析、需求分析(用用例图)、功能模块分析。
- 第四章 系统设计:总体架构设计、功能设计、数据库设计(ER 图 + 表结构)、接口设计。
- 第五章 系统实现:每个模块放截图 + 核心代码片段 + 实现过程说明。
- 第六章 系统测试:测试环境、功能测试用例表、性能简单测试、结论。
数据库设计部分一定要画 ER 图,我推荐用 draw.io 或者 ProcessOn,导出高清图插到论文里。表的字段列表要用三线表,不要截 Navicat 的图,特别不专业。接口设计部分列一个接口表格,写明 URL、请求方法、参数、返回结果,这个工作量不大,也能很明显地增加论文字数和专业度。
5.2 部署文档和演示环境的准备
部署文档不要到最后才写,开发过程中每完成一个模块就记录一下步骤。我给学生整理的最小化部署手册包括三部分:
- 环境准备:JDK 1.8、MySQL 8、Node 14(仅构建用)。
- 初始化数据库:执行
init.sql,修改application.yml数据库密码。 - 启动后端:
mvn spring-boot:run或java -jar schedule-system.jar。 - 启动前端:开发模式
npm install && npm run serve;生产模式打包 dist 到 static。
同时建议打包一份“答辩演示清单”:管理员账号密码、教师账号密码、学生账号密码,以及几个预置好的排课数据。演示前把数据状态调整好,避免现场排课冲突弹窗导致紧张卡壳。
5.3 答辩时如何讲清楚系统亮点
课表管理系统的答辩高频问题是“你的系统哪里复杂了?”这时候不要只说 CRUD,要主动讲排课算法。比如“在同一天同一节次内,如何保证老师、教室、班级两两不冲突”,这是你代码里最值得讲的部分。
另一个加分项是“周次安排的设计”。我做了单双周和起止周区间存储,答辩时直接说:一般的系统只能存固定周几和节次,我的系统可以处理“第 2-16 周每周一第 1-2 节”这种复杂安排,而且通过区间交集判断,可以轻松支持调课。这个说法一下子就把系统从“增删改查”提升到了“有业务深度”。
6. 我的实操体会与后续可扩展方向
这个系统前前后后带了几轮学生做,每次都能发现新的坑,但核心框架很稳。我个人最大的体会是:毕业设计项目不需要炫技,重点是“完整闭环 + 业务逻辑自洽”。哪怕只用一个最简单的注解和 SQL,只要能把“排课冲突检测”“周次过滤”“权限区分”这三件事讲得清清楚楚,成绩一般不会差。反而有些人堆了 Redis、RabbitMQ、分布式文件存储,结果答辩被追问一个都答不上来,得不偿失。
后续想扩展的话,比较实际的方向有三个:一是加一个“调课审批流”,老师提交调课申请、管理员审批后才生效;二是做“教室冲突热力图”,用前端日历插件把整周空闲教室可视化;三是加一个“课程表订阅”功能,把 iCal 格式导出,学生可以直接导入手机日历。这几个方向都很贴合原有模块,又能体现工作量,比重新做一个系统值钱得多。
最后分享一个小技巧:无论代码还是论文,一定要自己从头到尾跑通一遍部署流程。我见过太多学生在我电脑上能跑、到自己电脑上就扑街的案例,基本都是环境变量、依赖版本的问题。建议把整个项目打成压缩包,在另一台干净虚拟机里按文档操作,能跑通,答辩现场基本就稳了。
