最近整理源码仓库时翻出来一套之前给高校做的课表管理系统,技术栈是SpringBoot+Vue+MyBatis+MySQL,前后端分离的架构,功能完整,代码也整理得比较规范。想起不少读者正在找这类项目的完整源码做参考,或者准备拿它当毕业设计、课程设计的基础,干脆把这套系统的设计思路、核心实现、部署过程、还有我踩过的坑,一次性整理出来。
这套课表管理系统,说白了就是解决高校里排课、查课、调课这一堆琐事。教务管理员维护班级、教室、课程和教师信息,系统自动检测排课冲突,学生和教师登录后能查到自己一周的课表,还支持按周展示。如果你正在学Java全栈,想找一个功能完整、不是那种只有登录注册的demo项目,或者你接了一个排课管理的私活不知道怎么下手,这份拆解会很有参考价值。
1. 项目整体设计与思路拆解
1.1 为什么选SpringBoot+Vue+MyBatis这套组合
先说后端。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到7;start_section和end_section是第几节课到第几节课,比如上午第一大节就是1到2;week_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上经常遇到反射报错,折腾半天不值得。
- Maven:3.6以上,主要用来拉依赖。如果网速不好,一定把阿里云镜像配置到settings.xml里,别用默认中央仓库硬扛。
- Node.js:建议14到16,Vue2项目用太新的Node版本容易在node-sass上卡住。这里有一个省时间的小技巧:Node.js 16配好之后,前端依赖用
npm install --registry=https://registry.npmmirror.com来装,会快很多。 - MySQL:5.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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.schedule.entity configuration: map-underscore-to-camel-case: true有三个细节值得注意。第一,serverTimezone=Asia/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.active=prod > schedule.log 2>&1 &注意这里的--spring.profiles.active=prod,对应的就是在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 -p'YourPassword' 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目录里的建库脚本你一条一条看下来,整个系统的数据模型已经在你脑子里了。带着这个数据模型去读代码,速度会快非常多。
这个习惯帮我避免过很多坑,也让我能在较短时间内评估一份源码的质量好坏。希望你在用这份课表管理系统时,也先用这个方法来读一遍,你会有更深的体会。