
最近整理源码仓库时翻出来一套之前给高校做的课表管理系统技术栈是SpringBootVueMyBatisMySQL前后端分离的架构功能完整代码也整理得比较规范。想起不少读者正在找这类项目的完整源码做参考或者准备拿它当毕业设计、课程设计的基础干脆把这套系统的设计思路、核心实现、部署过程、还有我踩过的坑一次性整理出来。这套课表管理系统说白了就是解决高校里排课、查课、调课这一堆琐事。教务管理员维护班级、教室、课程和教师信息系统自动检测排课冲突学生和教师登录后能查到自己一周的课表还支持按周展示。如果你正在学Java全栈想找一个功能完整、不是那种只有登录注册的demo项目或者你接了一个排课管理的私活不知道怎么下手这份拆解会很有参考价值。1. 项目整体设计与思路拆解1.1 为什么选SpringBootVueMyBatis这套组合先说后端。SpringBoot现在就是Java Web开发的事实标准没什么好争论的。它最直观的好处是摆脱了以前SSM时代那一大堆XML配置一个Application类启动内置Tomcat打jar包直接跑。课表管理系统这种业务场景几十个接口用SpringBoot来写会非常清爽。选MyBatis而不是JPA是有原因的。排课管理里的SQL非常复杂涉及多表关联查询比如查某天某教室是否被占用、查某教师某时间段有没有课这些都需要写精确控制的SQL。MyBatis的半自动特性正好契合这种需求SQL你控制得住不会出现JPA那种自动生成的奇怪查询。再说SpringBoot整合MyBatis也就是加一个依赖、配一个MapperScan的事。前端用Vue理由也很实在。课表展示本身就是一种组件化很强的界面周几、第几节、课程名称、教师、教室每个单元格都是一个独立组件。Vue的响应式和组件复用能力天然适合做这种表格密集型应用。我用的是Vue2加Element UI这套组合在高校项目里非常成熟组件齐全文档多遇到坑也容易查。如果你用Vue3换成Element Plus也类似主体逻辑不受影响。MySQL的话没什么悬念。高校这类项目数据量不大一张课表几万条数据撑死了MySQL完全够用社区版免费安装也简单个人开发者最顺手的就是它。1.2 三类角色与功能模块划分课表系统不像电商那类项目角色逻辑很清晰主要就是三种管理员、教师、学生。管理员的权限最大负责基础数据维护和排课管理。基础数据指的是班级、教师、教室、课程这几张表的增删改查。排课管理是核心操作指定一个班级、某门课程、某位教师、在某个教室、某个时间段上课系统要自动验证有没有冲突。教师的权限小很多登录后能查看自己的课表按周切换查看有的版本还会加上调课申请提交功能。不过这套源码的调课功能是通过管理员修改排课记录来实现的没有做成单独的工作流模块做毕业设计完全够用。学生是最纯粹的使用者登录后按自己所在的班级查看课表筛选周次把课表打印出来或者截图发给同学。整个系统的权限控制就是靠后端每个接口校验当前登录用户的角色来实现的没引入Spring Security那套复杂的东西自己写了一个简单的拦截器就搞定了。这对初学者更友好代码看得懂。1.3 数据库设计与关键表结构这部分我把核心表结构列出来你拿着SQL就能初始化项目。第一张表是sys_user用户表。字段有id、username、password、name、role、class_id。学生的class_id关联班级表教师的class_id为空。密码存的是MD5加盐后的值不是明文这一点源码里做得很规范。第二张是course课程表。字段有id、course_name、course_code、credit、hours。课程表管的是学校开设了哪些课不涉及具体哪个班在上它和排课表是分离的这样数据更干净。第三张是teacher教师表。存在单独的教师表而不是并入用户表是因为大部分系统里教师有工号、职称、所属院系这些属性独立成表更好扩展。第四张是classroom教室表。核心字段是room_name、campus、capacity、type。type用来标记是普通教室还是多媒体教室或者机房。第五张是class_info班级表。字段是class_name、grade、major、head_teacher。一个班级对应一个专业一个专业有多个班级。最后是核心的course_schedule排课表。这张表是整个系统的关键字段是course_id、class_id、teacher_id、classroom_id、week_day、start_section、end_section、week_start、week_end、odd_even。week_day是周几1到7start_section和end_section是第几节课到第几节课比如上午第一大节就是1到2week_start和week_end表示这学期第几周到第几周有课odd_even用来标记是单周上课还是双周上课值为0表示每周都有。需要注意的是course_schedule和course之间是逻辑关联没有物理外键。这种做法在真实项目里很常见外键约束影响插入和删除性能而且一旦数据错位排查起来很麻烦宁可多写一个JOIN也不要整FK链。2. 核心业务逻辑与实现要点2.1 排课冲突检测的思路课表管理系统的灵魂一定是排课冲突检测这也是面试官最容易追问的部分。这套源码里用的是一种比较直观的检测方式在新增或编辑排课记录时遍历所有已有的排课记录逐条判断时间、教室、教师三者是否重叠。先解释时间重叠怎么写。两条排课记录在同一个week_day下一个的start_section为1、end_section为2另一个的start_section为2、end_section为3那么这两条记录在第2节是重叠的。判断公式是A.start_section B.end_section AND B.start_section A.end_section。这个公式能精确判断区间交集不会漏判。判断周次逻辑稍微复杂一点。两条排课记录要判断它们在满足week_day重叠的基础上是不是在同一周上课。处理方式是先判断公共周次区间再用odd_even做单双周的筛查。如果一条是每周上课另一条也是每周上课那就看区间有没有交集如果一条是单周另一条是双周即使区间有交集也不会冲突因为它们在时间上错开了。代码层面的实现逻辑是这样的插入新排课记录时先根据course_id和class_id查出完整的排课信息然后把所有参数封装成一个ScheduleParam对象调用checkConflict方法。这个方法里会用Java的List加一层Stream流对已有记录做过滤过滤条件是上面提到的时间重叠和周次重叠再分组判断教室冲突和教师冲突。返回的提示信息也很精确直接告诉操作者是在第几周、星期几、第几节和某个教室或者某位教师发生了冲突。这套实现算不上算法优化数据量大了性能会下降但是对于一门普通高校的课表一个学期排出几百条排课记录毫秒级速度完全够用。2.2 用户认证与角色权限控制没有用Spring Security是自己写的一个基于Token的认证机制配合拦截器做权限校验。用户登录成功后后端会生成一个UUID作为token存到数据库的一张token表里然后把token返回给前端。前端把token存到localStorage里每次请求时在axios拦截器中加在请求头上后端再通过拦截器取出来进行校验。用户信息从token查出来后放在ThreadLocal里后续的业务方法直接调用CurrentUserHolder.getUser()就能拿到当前登录的人是谁。这个设计很轻量代码量不大适合做小型后台管理系统。权限校验这一步就是在拦截器里加角色判断。举个例子排课相关的接口路径以/admin开头的拦截器会先从token里取出用户角色如果不是管理员直接返回401状态码前端登录页收到401后自动跳转回去。像学生和教师能访问的只有课表查询和基础信息查看接口。这里提醒一下MD5加盐做登录密码存储是在2015年之前比较主流的做法现在有条件的项目还是应该换成BCrypt。这套源码里保留的是MD5加盐就是因为代码直观好懂适合做教学项目。真要商用建议替换PasswordEncoder。2.3 前端Vue组件化设计与课表渲染前端用Vue Router做页面跳转有三类主要页面登录页、管理员后台页面、课表展示页面。课表展示页是亮点它用Element UI的el-table搭建了一个8行乘7列的课表矩阵。行是节次列是星期一到星期日单元格里展示课程名、教师、教室。由于单元格的rowspan和colspan在动态渲染时很麻烦源码里做了一层数据转换把后端返回的排课记录List转换成前端课表需要的二维数组结构。转换逻辑用JavaScript写了一个专门的scheduleUtil.js工具类输入是排课数据输出是一个二维对象行索引是节次列索引是星期几对象里有课程信息。这个转换过程有一个坑一节大课通常会跨两个小节比如第一二节连上前端要展示的就是一个高两行的单元格。对应到数组里就是处理跨行时后一行要把当前行的数据合并掉不能简单循环填充。接口调用统一走request.js里面封装了axios实例配置好baseURL和请求拦截器、响应拦截器。响应拦截器里判断HTTP状态码非200的就用el-message弹出错误提示这种统一封装减少了很多重复代码。3. 环境搭建与核心配置实操3.1 本地开发环境的最低版本建议项目拿到手第一步是搭环境这一步出错后面全是麻烦。我给出一套我实测过完全没问题的版本组合照这个来最省事。JDK建议1.8以上如果项目pom里指定的是Java 8那就装JDK 8不要上来装JDK 17。SpringBoot旧版本在JDK17上经常遇到反射报错折腾半天不值得。Maven3.6以上主要用来拉依赖。如果网速不好一定把阿里云镜像配置到settings.xml里别用默认中央仓库硬扛。Node.js建议14到16Vue2项目用太新的Node版本容易在node-sass上卡住。这里有一个省时间的小技巧Node.js 16配好之后前端依赖用npm install --registryhttps://registry.npmmirror.com来装会快很多。MySQL5.7或者8.0都行。源码默认用的8.0驱动如果用5.7需要把pom里的mysql-connector-java版本降级同时把application.yml里的驱动类名改回com.mysql.jdbc.Driver。数据库导入没什么特殊的把源码包里的sql目录下的init.sql导入到MySQL即可。导入成功后库里会出现七张表如果只看到六张也不要慌有的版本里token表是后加进去的你再执行一下token.sql就行。3.2 后端SpringBoot配置文件的几个关键点application.yml是我们日常打交道最多的配置。数据源部分长这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/school_schedule?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.schedule.entity configuration: map-underscore-to-camel-case: true有三个细节值得注意。第一serverTimezoneAsia/Shanghai必须加不然MySQL 8.0会报时区错误。第二map-underscore-to-camel-case建议设成true这样数据库字段create_time能自动映射到实体类的createTime不然你写实体类属性就得迁就数据库字段名。第三mapper-locations指向XML文件目录如果你的Mapper查询都写在注解里那一行可以注释掉不影响项目启动。日志配置我也会在开发阶段打开在application.yml里加上logging: level: com.example.schedule.mapper: debug这样每次执行SQL都会打印到控制台调接口时能一眼看出SQL执行情况。等你熟悉SqlSessionFactory的日志打印机制后再把这行关掉防止生产环境日志太吵。3.3 前端项目初始化与接口联调代理前端项目是用Vue CLI创建的目录结构是标准化的src/api放接口请求src/router放路由src/views放页面src/components放公用组件。拿到源码后先npm install依赖装完再npm run serve默认端口8081。开发阶段前后端分开跑存在跨域问题。解决跨域有两种方式一种是后端加CORS配置另一种是在前端vue.config.js里配置代理。我更推荐前端代理因为部署阶段Nginx也要做同样的反向代理开发和生产环境行为一致不容易出偏差。vue.config.js里的配置是module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这里的/api有两种理解方式取决于后端接口有没有带这个前缀。如果后端Controller的RequestMapper写的是RequestMapping(/api/user)那就不要配pathRewrite如果后端没有统一加前缀才用pathRewrite把/api剥掉再转发。这个细节很多人配置完发现404就是没理清前后端路径的前缀问题。4. 部署与上线实操记录4.1 后端打包与运行参数调优开发环境跑通后距离上线就差打包这一步。后端打包直接mvn clean package打出来的是可执行jar文件我习惯把它放在服务器上的/opt/app/schedule目录下。启动时用的命令是nohup java -jar schedule-admin.jar --spring.profiles.activeprod schedule.log 21 注意这里的--spring.profiles.activeprod对应的就是在application-prod.yml里配置生产数据库连接和端口。开发环境和生产环境分开配是基本功。打包之前有一步容易漏把application.yml里的MySQL地址改成生产数据库的地址。如果你打出来的包连接的是本地数据库部署到服务器上一定连不上。我在源码包里会同时提供application-dev.yml和application-prod.yml两份配置部署时只需要在启动命令里加一个profile参数就行。服务器上的MySQL记得单独建一个普通账号别直接用root连项目。这个账号只授权给school_schedule库CREATE USER schedule_user% IDENTIFIED BY YourPassword; GRANT ALL PRIVILEGES ON school_schedule.* TO schedule_user%; FLUSH PRIVILEGES;这不仅是安全问题也是防呆设计。万一数据被误删至少不会是全库范围的事故。4.2 前端构建产物部署到Nginx前端构建执行npm run build完成后在dist目录下生成静态文件。把dist目录上传到服务器然后用Nginx托管。我的Nginx配置里有一个很重要的细节是解决页面刷新404问题的try_files指令。Vue Router用history模式时刷新某个路由路径Nginx会按路径去找文件找不到就404。加上try_files $uri $uri/ /index.html;后所有请求都会回退到index.html交给前端路由处理。server { listen 80; server_name schedule.example.com; root /var/www/schedule/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里的location /api/是个关键点。开发环境我用的是vue.config.js的代理生产环境则是Nginx把/api前缀的请求转发到本地的SpringBoot两种手段本质一样。唯一要注意的是proxy_pass后面有没有带路径带不带/会导致不同的转发结果这是Nginx使用中的一个高频坑。配好Nginx后执行nginx -t检查语法再nginx -s reload重载配置。这个流程跑通后前端页面能正常访问接口请求也能通整个系统就算真正运行起来了。4.3 数据库备份策略课表数据不像交易数据那么密集变化但学期初集中录入排课时掉数据也够喝一壶的。建议写一个简单的Shell脚本每周日凌晨做一次全量备份。#!/bin/bash BACKUP_DIR/data/backup/mysql DATE$(date %Y%m%d_%H%M%S) mysqldump -u schedule_user -pYourPassword school_schedule $BACKUP_DIR/schedule_$DATE.sql find $BACKUP_DIR -name *.sql -mtime 30 -exec rm {} \;这段脚本保留三十天以内的备份更早的自动清掉。如果你的服务器配置过cron加上一行0 2 * * 0 bash /opt/scripts/backup_schedule.sh就行。MySQL备份有逻辑备份和物理备份两种思路mysqldump是逻辑备份胜在简单可靠每天生成一个几十KB的SQL文件对这种规模的应用来说毫无压力。5. 常见问题与排查技巧实录5.1 后端启动失败的典型案例这个项目最常见的启动失败场景是端口占用和数据库连不上。SpringBoot默认8080端口本地如果已经启动了别的服务启动日志会报Port 8080 was already in use。这时候要么把其他服务停掉要么在application.yml里改成8081。实际开发中我经常把其他服务留在8080这个项目的端口调成8090避免开关机后哪个服务先启动造成的冲突。数据库连不上的报错五花八门常见的有两种。一种是Access denied for user说明账号密码不对或者授权地址不对另一种是Unknown database说明数据库名字没对上。排查时先手工在命令行用同样的账号连一下数据库确认账号能连上、库存在再回来看项目配置。这个排查顺序能过滤掉一半以上的问题。还有一个很容易被忽略的是MySQL 8.0的认证插件问题。旧项目驱动连接MySQL 8.0有时会报Authentication plugin caching_sha2_password cannot be loaded。解决方法是创建用户时指定mysql_native_password或者在当前用户上修改认证方式。源码里我建议直接创建用户时用8.0默认的caching_sha2_password然后保证mysql-connector-java版本在8.0以上这样就不用纠结兼容问题了。5.2 前端常见的依赖安装与页面白屏问题前端最烦人的依赖安装问题是node-sass。Vue2项目里styled-component是node-sass的典型场景安装时经常因为下载二进制文件失败而报错。解决方法是用npm镜像源安装或者直接用sass的dart版本替代node-sass。页面白屏问题多半由两种情况造成一种是没有配置try_files导致路由404这种已经在前文说过了另一种是请求接口报错后页面没有错误处理axios默认在非200状态码下会throw exception而页面没有catch结果整个Vue实例崩掉白屏。解决方法是给axios的响应拦截器加上统一处理非200状态码先弹提示再决定是否跳转登录页。比如401就清空localStorage并跳转到登录页500就提示服务端异常。这样至少用户能看到明确反馈不会一脸懵。5.3 排课数据重复提交的隐患这个问题我是在测试阶段偶然发现的。快速点击保存排课按钮两次系统生成了两条一模一样的排课记录没有任何提示。要解决这个问题在后端接口加幂等判断是第一道关卡修改排课时先查一下同一班级、同一星期、同一节次区间是否已经存在记录存在就不允许再次保存。如果追求更高可靠性可以给course_schedule表加一个联合唯一索引字段是class_id week_day start_section。这样即便应用层漏了校验数据库层面也能兜底插入重复数据时直接抛异常。我对这种做法比较推崇原因很简单系统的核心数据完整性不只是靠代码逻辑来保证的数据库约束是最后一道防线。6. 源码之外的经验与扩展建议6.1 这份源码真正值钱的是可读性市面上确实有不少课表管理系统的源码淘宝、GitHub一搜一大把但很多项目代码被魔改得面目全非命名混乱、逻辑冗长、参数满天飞看着都头疼。这份源码最大的优点是可读性包名按功能模块划分每个类只干一件事Service层的接口和实现分离SQL语句全部集中在Mapper的XML文件里不在Java代码里拼字符串。你照着源码读一遍能很清晰地理解一个前后端分离项目从接口设计到数据落库的完整链路。对我自己来说这份源码的价值在于很多接口范式可以直接复用。用户管理模块、权限拦截器、axios统一封装、Element UI表格组件这些都是一套成熟的中后台系统里反复出现的基础设施。你是拿去做毕设还是接私活二次开发这些代码都能直接改改就用。6.2 可以继续扩展的几个方向如果你有精力这个系统还有几个非常有价值的扩展方向。第一个是接入Spring Security和JWT替换现在自己写的token机制。这样能学会主流的企业级认证授权方案也是面试中的高频考点。第二个是把排课引擎从简单的冲突检测升级为智能排课。现在的思路是管理员手动指定每个班每门课的时间和教室系统只做校验。升级思路是先录入所有班级的教学计划再通过算法自动分配时间和教室管理员只需要微调和审批。这个方向如果用了遗传算法或者约束满足算法写论文和做答辩素材都有底气。第三个是增加调课申请和工作流审批。教师登录后可以提交调课申请说明原因、目标时间、目标教室教务管理员在后台审批。这个功能涉及一个简单的工作流设计也很适合做成独立模块。第四个是加一个导入导出功能用EasyExcel或者POI封装课程表导入模板批量导入排课数据再支持导出PDF版课表。高校办公场景里这几乎是刚需功能。6.3 个人最想分享的一个实操技巧最后分享一个我在部署项目时养成的习惯拿到任何一份源码永远先读README.md和sql目录不要一上来就npm install和mvn package。README里通常会写环境要求sql目录里的建库脚本你一条一条看下来整个系统的数据模型已经在你脑子里了。带着这个数据模型去读代码速度会快非常多。这个习惯帮我避免过很多坑也让我能在较短时间内评估一份源码的质量好坏。希望你在用这份课表管理系统时也先用这个方法来读一遍你会有更深的体会。