基于SpringBoot+Vue+MySQL的课表管理系统开发全流程实战

发布时间:2026/10/13 19:49:40

基于SpringBoot+Vue+MySQL的课表管理系统开发全流程实战 去年年底帮某高校信息中心做课表管理系统的时候我第一反应是这东西不就是把一张表放到网页上吗真做起来才发现课表管理系统跟普通CRUD项目完全是两码事。它要处理的不只是数据的增删改查而是同一门课每周上两次单周和双周安排不一样老师周五下午要去分院开会所以课不能排第三节之后A301教室周三晚有选修课这一大堆真实业务约束。这篇文章我会把这个基于SpringBootVueMySQLMyBatis的完整课表管理系统项目从需求、设计到实现的整个过程拆开讲包括我踩过的坑、返工过的表、以及最后总结出的实战经验。适合正在做毕业设计、课程设计或者想自己搭一个课表管理Demo的开发者参考尤其是那种源码在手但不知道怎么改成自己的情况。1. 需求拆解课表系统表面上是展示本质上是三类用户的权限博弈我一般拿到一个项目不会先开IDE而是先把角色和场景列清楚。课表管理系统最核心的一点就是它的用户不是一个人而是三种人而且三种人对课表的需求完全不同。1.1 三种角色三套视图教务管理员负责学期初始化、课程任务维护、教室调停课、课表发布。他们要的是一张全局表能看到某个教室在某个时间段是否空闲能看到某个老师一周排了多少课时。教师只关心我什么时候在哪上课。教师端课表查询其实是最简单的但要注意教师可能同时属于多个学院、有多门课程、有调课申请。学生/班级按班级查课表按学号查个人课表偶尔还要按老师名字或者教室号反查课表。这里有一个很典型的认知误区很多初学者会把课表做成一张写死的大表。实际上课表的查询维度至少有班级维度、教师维度、教室维度、课程维度四种每种维度的数据筛选逻辑不同后端接口和前端页面都要分开设计。1.2 需求优先级怎么定这个项目我建议按P0/P1/P2来排功能优先级不要一开始就想着做选课系统和成绩管理那是另一个项目了。P0登录鉴权、课表数据维护增删改查、按班级/教师/教室查询课表、课表预览P1教室冲突检测、调停课申请与审批、单双周/跨周次配置P2课表导出Excel、课程提醒、数据统计看板实际开发中P0是硬性要求P1决定了系统是否可用P2决定了系统是否好用。我当时就是在P1的冲突检测上卡了最久后面专门讲。1.3 明确系统边界做课表管理系统前一定要想清楚你做的不是排课系统而是课表管理系统。排课系统解决的是课怎么排的问题涉及复杂的算法和约束条件比如遗传算法、启发式搜索。课表管理系统解决的是课排完之后怎么发布、怎么查询、怎么调整的问题重点是数据的组织和展示。很多人毕设选题写着课表管理系统却试图做自动排课最后把项目拖死。我见过不止一个同学在自动排课上纠结了一个月最后手动录入数据照样能跑——因为项目标题里写的是管理而不是排课。2. 技术选型复盘SpringBoot Vue MySQL MyBatis这套组合为什么合适说实话这个技术栈组合在现在的Web项目里算是中规中矩的方案没有一个是特别新的也没有一个是会被淘汰的。关键是它恰好匹配课表管理系统这种中小型管理系统的所有需求。2.1 SpringBoot负责后端省掉一堆配置SpringBoot最大的价值是约定优于配置。内嵌Tomcat、自动化配置、起步依赖这些东西让项目从零到跑通只需要几分钟。对这个项目来说SpringBoot 2.7.x是比较稳妥的选择。如果你用的JDK是8或11那完全没问题如果非要上SpringBoot 3.x要注意里面用的是jakarta.servlet而不是javax.servlet很多老代码和教程里的写法都要改。我不建议在这类项目里追求版本最新够用、稳定、资料多才是硬道理。2.2 Vue做前端课表网格渲染起来灵活Vue的响应式数据和组件化开发非常适合做中后台页面。课表展示是一个典型的二维网格用Vue的computed属性去转换数据、用模板指令去遍历渲染开发效率比JQuery时代高太多了。版本上如果你熟悉组合式API用Vue3如果之前没怎么接触过用Vue2 Element UI也完全够用。我自己做这个项目用的是Vue2 Element UI Axios因为团队对这个组合最熟出问题能快速定位。前端还有一个重要选型点要不要用脚手架。我的建议是直接用Vue CLI创建项目不要在这个项目里手写Webpack配置不是不能写是没必要。课表管理系统的核心价值在后端的数据建模前端把页面写清楚、交互做流畅就行。2.3 MyBatis复杂查询手动控制SQL才有底MyBatis和JPASpring Data JPA都有人用但课表查询的场景里我坚定选MyBatis。原因有三点课表查询条件组合多变按学年学期、按周次、按班级、按教师、按教室、按星期几。用MyBatis动态SQL写if标签组合条件一眼就能看懂查询逻辑。多表联查频繁课程表、教师表、班级表、教室安排表几乎每个查询都要LEFT JOIN。MyBatis里SQL完全自己控制效率和高低心里有数。排错方便系统出问题大部分时候是SQL问题。MyBatis直接把SQL拿出来就能在数据库客户端里跑JPA的自动生成SQL排错起来绕了一大圈。再补充一个点很多教程喜欢在MyBatis之上再套MyBatis-Plus用BaseMapper直接CRUD。这个做法我不反对单表操作确实快但课表系统里最复杂的查询还是要自己写XML所以不要因为用了MyBatis-Plus就不去掌握XML里的动态SQL写法这不冲突只是分工。3. 课表数据模型设计一张排课表把多少人卡在原地说实话整个项目里最值得写进博客的就是数据模型设计。课表系统难的不是代码而是怎么用一张表其实是几张表把排课的所有约束表达清楚。3.1 核心数据表拆解我设计的时候把核心表拆成了四张用户表、班级表、课程表、排课表。不要试图把所有信息塞到一张表里那样后面调停课、查冲突的时候会哭。表名作用关键字段sys_user用户登录与基础信息username,password,real_name,role_typeclass_info班级信息class_name,grade,major,dept_idcourse课程基础信息course_name,credit,course_type,dept_idschedule排课结果记录course_id,class_id,teacher_id,classroom,day_of_week,start_section,end_section,week_type,week_start,week_end,semester,academic_year其中最核心的就是schedule这张排课表。你可以理解成一条记录代表某一个班级在某个学期的某个星期几的某几节课在某个教室里由某位老师上某门课。3.2 周次建模单双周、跨周次这个坑必须提前想清楚很多课表系统做崩是因为周次处理。真实高校课表里课程安排不只是每周都上还存在单周上、双周上、前八周上、第3到第15周上等情况。我采用的方案是两个字段组合week_start和week_end是区间比如第1周到第16周week_type是类型0表示全周1表示仅单周2表示仅双周。举个例子某门课从第1周到第16周但只在单周上数据库里就是week_start1, week_end16, week_type1。有人可能会问如果课程安排是第1-4周、第7-10周这种断断续续的怎么办我当时的解决方案是拆成多条记录第一条存1-4周第二条存7-10周两条记录的class_id、day_of_week、start_section完全相同。这样查询时逻辑最清晰只是维护的时候稍微麻烦一点。如果你是做毕设能覆盖全周单双周起止周这个范围答辩的时候已经可以重点展示了。3.3 节次设计别用时间字段用节次编号初学者容易踩的另一个坑是用具体时间来存课表比如start_time08:00, end_time09:45。这样做的麻烦在于——课表的节次是固定的全校统一划分节次时段比如第1-2节是8:00-9:40第3-4节是10:00-11:40。你直接用day_of_week1-7代表周一到周日start_section第几节开始end_section第几节结束就够了。前端的展示网格也是按节次来画的不需要真正去算时间。比如上午四大节课下午三大节晚上两节一天撑死了也就9-10个时间段画表格非常轻松。3.4 冲突检测的SQL逻辑课表系统如果没有冲突检测管理员手动录入排课信息的时候很容易出乱子——同一间教室同一时间排了两门课结果上课时两个班撞在一起。我做了一个checkConflict的校验逻辑核心SQL大概是这样的SELECT COUNT(*) FROM schedule WHERE academic_year #{academicYear} AND semester #{semester} AND day_of_week #{dayOfWeek} AND week_type IN (#{weekType}, 0) AND ((start_section #{endSection} AND end_section #{startSection})) AND classroom #{classroom}注意这里有个关键点判断两个时间段是否冲突不能简单用要用区间相交的判断也就是start_section 新记录的end_section AND end_section 新记录的start_section。这个逻辑对教室冲突、教师冲突、班级冲突都通用只需要替换后面的条件字段即可。提示这个接口不能只做后端校验前端录入表单时也要用blur事件或者提交前预校验。我在实际使用中发现前端先查一次、后端提交时再查一次才能完全杜绝并发提交产生的冲突数据。3.5 数据库额外的配置细节数据表字符集一定要用utf8mb4连接串里要带上characterEncodingutf8。这个项目虽然主要是中文数据但万一以后要存个emoji或者生僻字utf8就抓瞎了。另外连接串时间参数也很重要我后面在部署章节会单独讲。4. 核心功能实现拆解登录鉴权、三套查询接口、课表网格渲染4.1 登录鉴权为什么密码不能明文存为什么选用JWT先说密码存储。当年我见过有人把密码明文放数据库的然后被别人看到了直接把管理后台登进去改成绩——这种事在学校里可不是开玩笑的。这个项目我用的方案是BCryptPasswordEncoder加密。BCrypt的特点就是每次加密同一个密码得到的密文都不一样因为内置了随机盐值这样就算数据库泄露密码也无法反推还原。登录鉴权用JWT是因为前后端分离部署后端不需要维护Session登录成功之后服务器签发一个token前端每次请求带着这个token后端拦截器校验通过就放行。这里有个拦截器设计细节需要放行的请求要写在白名单里/api/login、/api/captcha如果有验证码、静态资源/static/**、以及错误页。其他接口全部拦截。JWT本身要注意的是token里只放必要信息比如用户ID和角色类型不要放密码这种敏感字段。token过期时间根据实际场景设置我当时设置了24小时学生上课期间不会反复登录但如果你的学校对安全性要求高可以设置短一点并结合刷新token机制。4.2 后端接口设计一个接口动态查询还是分三个接口之前说过课表有三个查询维度按班级查、按教师查、按教室查。在接口设计上我倾向于做一个统一的查询接口GET /api/schedule/query?viewTypeclasstargetIdxxweekNum5academicYear2024-2025semester1参数含义viewType查询维度可选class、teacher、classroomtargetId对应维度的ID比如班级ID、教师ID、教室名weekNum要查询的本学期第几周academicYear和semester限定学年学期之所以做成统一接口而不是三个接口核心原因是三种查询的业务逻辑完全一样——先确定条件然后查schedule表最后关联出课程和教师信息。区别只是WHERE条件不同而已。用一个Service方法内部根据viewType设置条件映射代码量最少后期也好维护。对应的MyBatis XML里的核心查询大概是这个形态select idqueryScheduleList resultTypecom.example.entity.ScheduleVO SELECT sc.id, c.course_name, c.credit, ci.class_name, u.real_name AS teacher_name, sc.classroom, sc.day_of_week, sc.start_section, sc.end_section, sc.week_type, sc.week_start, sc.week_end FROM schedule sc LEFT JOIN course c ON sc.course_id c.id LEFT JOIN class_info ci ON sc.class_id ci.id LEFT JOIN sys_user u ON sc.teacher_id u.id WHERE sc.academic_year #{academicYear} AND sc.semester #{semester} if testclassId ! null and classId ! 0 AND sc.class_id #{classId} /if if testteacherId ! null and teacherId ! 0 AND sc.teacher_id #{teacherId} /if if testclassroom ! null and classroom ! AND sc.classroom #{classroom} /if ORDER BY sc.day_of_week, sc.start_section /select看到没有这就是MyBatis动态SQL的价值页面传过来哪个维度就拼接哪个维度的条件。三个查询场景用同一个SQL搞定。4.3 前端课表网格渲染rowspan这个坎前端最核心的组件是课表网格组件。实现逻辑不复杂但需要细心。我先把后端返回的课程列表转换成一个二维矩阵行是节次列是星期几scheduleMatrix() { const matrix Array.from({ length: 11 }, () Array(8).fill(null)); this.scheduleList.forEach(item { for (let sec item.start_section; sec item.end_section; sec) { matrix[sec][item.day_of_week] { ...item, isFirst: sec item.start_section, rowSpan: item.end_section - item.start_section 1 }; } }); return matrix; }在HTML里渲染的时候只在isFirst为true的格子输出td并且设置:rowspanitem.rowSpan其他被占用的格子直接跳过。这样一来一门课占了两节就在表格里自动合并两行视觉上就是一张正常课表的样子。这个过程中比较容易踩的坑是如果不小心在非首节的格子里也渲染了td表格的列数就会错位整个课表看起来是歪的。我当时调试这个组件花了快两个小时最后发现问题出在遍历顺序上——必须先循环节次再循环星期如果反了同一个格子会被课程覆盖多次出现上一门课的数据被下一门课覆盖的bug。4.4 管理端的课表录入表单管理端录排课信息时表单至少要有这些字段班级下拉、课程下拉、教师下拉、教室输入、星期下拉、节次区间开始节次和结束节次、周次类型。提交前调用冲突检测接口。这里有一个从实操里总结出的经验节次区间最好用两个下拉选择器而不是一个多选组件因为用户必须清楚地知道我选的是2-4节还是3-4节多选组件容易误操作。5. 联调、部署与真实跑起来后的问题这个章节分享一些项目从开发机到服务器之间出现的具体问题和解决办法都是跑一遍才会遇到的。5.1 跨域问题一次配置解决别每次加注解前端开发服务器在8080端口后端在8081端口前端请求后端接口必然存在跨域。解决方式是在后端写一个全局配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }这里要注意allowCredentials(true)和allowedOrigins不能同时使用*通配符否则浏览器的CORS策略会直接拒绝请求。这个细节在做前后端联调时卡过我一下。5.2 数据库时区与中文乱码如果项目跑起来之后发现时间字段差了8小时或者插入中文数据变成问号绝大多数情况下是连接串的问题。我的MySQL连接串写法如下jdbc:mysql://localhost:3306/school_schedule?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue千万要把serverTimezone写成Asia/Shanghai而不是GMT否则数据库存的时间和你本地时间对不上。useSSLfalse是因为本地开发环境没有配置SSL证书后面部署到生产再按实际需要开启。5.3 前端打包后的部署方式前端项目执行npm run build之后会生成dist目录。最简单的部署方式是把dist里的所有文件拷贝到SpringBoot项目的src/main/resources/static目录下然后重新打包后端jar。这样前端页面和接口服务就是同一个端口不需要再配置Nginx又省了一层部署问题。但这个方案有一个大坑如果你前端用了Vue Router的history模式直接访问某个子路由会404比如用户刷新了/dashboard页面后端找不到这个路由。解决方案有两个一是部署时后端加一个404转发到index.html的Controller二是把前端路由改成hash模式。考虑到这就是个演示性质的项目用hash模式最省事代价是URL里多一个#但刷新不404。5.4 查询性能课表系统也会慢有人觉得课表数据量小不会慢。但如果你给查询接口加了一堆LEFT JOIN又没加索引到了学期初几千条排课数据一起查还是会有卡顿感。我给schedule表建了一个联合索引(academic_year, semester, day_of_week, classroom)查课表、查教室冲突、查教室占用率都用得上。加了索引之后查询耗时从几百毫秒降到了几十毫秒。注意不要给每个字段都加索引尤其不要在week_type这种重复率高的字段上单独加索引MySQL的优化器会放弃使用它纯粹浪费存储空间和维护成本。6. 这个项目还能怎么改从能用到好用还差这几步如果做完上面这些功能项目已经可以交工了。但每次我做完一个项目都会习惯性地想如果再多给我两周时间我会往哪些方向打磨。目前业务上最明显的短板是调停课流程。现在的设计是管理员直接在后台修改排课记录一旦涉及到某老师周三因院内会议需要临时停一次课这种场景直接改数据库记录是不合适的因为课表的历史记录被覆盖了。我设想的是增加一张调课记录表记录原排课ID、调整后的时间和教室、申请理由、审批状态。这样系统里既能查当前课表也能追溯调课历史如果将来接入教务系统的对接这个设计就有用武之地了。另外还有一个值得做的扩展是教室空闲查询学生自习需要知道某个教室当前有没有课。这个功能完全可以在现有数据模型上扩展复用前面的queryScheduleList接口按classroom维度查询然后前端把课表网格做成空闲时间段高亮的效果。这个功能实际做起来工作量不大但对系统的实用性提升非常明显。7. 写在最后如果让我重做一遍我会省下哪些弯路这个项目从启动到交付我在三个方面走了明显的弯路写下来给后面做类似项目的同学提个醒。第一不要急着写代码。项目需求调研真的只需要半天找教务的老师聊一聊把真实课表的Excel模板要一份问题基本迎刃而解。很多数据结构上的坑比如单双周、节次划分、连堂合并在Excel模板里都有答案看懂了模板再设计数据库能省一半时间。我一开始就是没要模板凭想象设计表结构后来拿到真实数据才发现自己缺了两个字段。第二一定要先搭通前后端最小链路再填充业务。也就是先做一个查全部排课记录的列表页把SpringBoot从Controller到Mapper再到前端页面的整个链路跑通然后在这个基础上逐步添加条件筛选、冲突检测、权限控制。这样出了问题很容易定位是前端还是后端而不是等到代码写了一大堆再去Debug。第三好好利用日志。我后面几天排查问题时在后端加了一个MyBatis的SQL日志输出配置logging.level.com.example.mapperdebug。这样每次执行查询都能直接看到最终的真实SQL语句一眼就能看出来动态SQL拼接的条件是否正确。这个习惯帮我解决了很多数据查不出来的诡异问题。最后再分享一个具体的经验教训。调试课表网格的rowspan时不要总是盯着浏览器页面看把数据在控制台里打印出来检查每门课的start_section、end_section、isFirst字段是否正确数据对了页面大概率就对了。很多前端怪现象追根到底都是后端返回的数据结构不够干净不要跟前端死磕回头看看接口返回的JSON也许问题出在SQL的排序上。课表管理系统就是这样——架构上用最主流的技术栈业务上把一张表的数据模型想清楚剩下的就是一遍遍打磨细节。希望这篇复盘能帮你少走我之前走过的弯路。
延伸阅读

更多相关文章

2026/10/13 19:49:40

Geant4光学仿真调试实战:端到端光子追踪与断点日志模板

简介:本资源是面向高能物理仿真与光学光子传播研究者的Geant4进阶实践示例,聚焦于G4Opticks光学扩展模块的集成与应用,适用于已掌握C基础及Geant4核心框架的科研人员与研究生。项目完整实现了光学光子在定制化几何介质中的生成、传输与探测模…

2026/10/13 19:44:38

背包问题:动态规划的心法入口与建模本质

1. 为什么背包问题不是“刷题模板”,而是动态规划的“心法入口”刚接触动态规划时,我见过太多人把背包问题当成一道“必须背下来的经典例题”——抄状态转移方程、默写二维数组初始化、硬套“for i in range(1, n1): for w in range(W, weight[i-1]-1, -…

2026/10/13 22:10:04

GPU租用平台实测:学生深度学习训练如何选型、省钱与避坑

上个月实验室的GPU排队排到了第三天,一个研二的同学拿着跑到一半的BERT微调任务来找我吐槽,说再等下去课题节点就要错过了。这个场景在高校里其实一点都不陌生:深度学习相关课程越来越多,竞赛和论文任务一个接一个,但实…

2026/10/13 22:10:04

基于Arduino与离线语音模块的智能垃圾桶DIY指南

如果你在厨房垃圾桶前犹豫过要不要伸手去掀那个沾满油渍的盖子,那你已经懂了这个项目最原始的动机。我做的这个基于Arduino的语音识别垃圾箱,说白了就是让普通垃圾桶听懂一句话就自动开盖、保持几秒再自动合上,全程不碰桶盖。比起那些挂在云端…

2026/10/13 22:10:04

昇腾适配DeepSeek V4全流程解析:算子、图编译与性能调优实践

前阵子我们组接到一个内部项目:把DeepSeek V4完整跑通在昇腾上。这里的"跑通"不是指能出几个token就算交差,而是从模型权重、算子适配、图编译、精度对齐到性能压测全部走完,最后给业务侧交付一个可用的推理接口。整件事前后花了几…

2026/10/13 22:10:04

基于SpringBoot+Vue的饮食健康管理系统设计与实现要点解析

最近刚把一个基于SpringBootVue的饮食健康管理系统完整走通了一遍。这个项目我一直惦记着——从需求梳理、表结构设计,到前后端联调、打jar包部署,前前后后用了两周多的时间。这类系统在课程设计和实际健康类产品里都很常见,核心思路并不复杂…

2026/10/13 22:10:04

Dyna-2.1物理智能体:URR与全身控制器的端到端工作流程解析

1. 从标题拆解Dyna-2.1到底在解决什么问题第一次看到“Dyna-2.1:面向端到端工作流程的物理智能体”这个标题,我脑子里蹦出来的第一个念头是:又来了一个“端到端”。这几年“端到端”这个词被用得太泛了,从自动驾驶到推荐系统&…

2026/10/13 22:05:03

语义词谜游戏设计:绕过拼写训练词汇提取能力

1. 项目概述:当词典变成游戏界面,我们到底在玩什么? “Show HN: Wick, a daily word game where you guess by meaning, not spelling”——这个标题一出现,我就在某开发者社区的首页多刷了三遍。不是因为它的UI有多炫&#xff0c…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑