我最早接到这个课表管理系统的需求时,项目方只给了一句话——把学校的排课、调课、查课搬到线上。当时的技术方案其实已经比较固定了:前后端分离架构,后端用SpringBoot+MyBatis+MySQL,前端用Vue,这也是目前JavaWeb毕业设计和中小型管理系统中非常成熟的一套组合。前后端分离、SpringBoot、Vue、MyBatis、MySQL这套技术栈,既能满足课表管理系统这种典型CRUD业务,又能在答辩或面试时讲清楚每一个模块的分工,性价比很高。
这篇内容不只是讲项目怎么搭,我会把整个项目的设计思路、核心表结构、接口约定、前端页面实现、联调排错、部署上线的完整链路都拆开讲一遍。无论你是拿来做毕业设计,还是想练手一个完整的前后端分离项目,只要能跟着把思路捋一遍,课表管理系统这个题目基本不会再有什么盲区。
1. 项目整体设计与技术选型为什么这么搭
1.1 课表管理系统的核心需求拆解
先把业务看清楚再谈技术,这是我一贯的做法。课表管理系统表面上是"增删改查",但实际拆开之后,至少包含三个角色、四类核心数据、六个核心场景。
三个角色分别是:管理员(负责排课、调课、发布学期课表)、教师(查看个人课表、申请调课)、学生(按班级或学号查看课表)。四类核心数据是:班级、教师、课程、教室,加上一个维护周次节次的时间维度。六个核心场景分别是:管理员排课、课表冲突检测、班级课表查询、教师课表查询、教室占用查询、调课申请与审批。
这些需求决定了系统的功能边界。你在做需求分析时,最容易犯的错误是一上来就写代码。我建议先用半小时把角色和用例图列出来,哪怕只是画在白纸上,后面建表、设计接口都会顺畅很多。课表管理系统尤其不要在"排课算法"上过度设计,大多数实际项目只要做"冲突检测+人工调整"就够用了,贪心排课算法可以放到论文或文档里作为进阶讨论,但不应该成为核心交付物。
1.2 为什么选SpringBoot+Vue+MyBatis+MySQL这套组合
选型这件事,我不太推荐追新。SpringBoot在目前Java后端项目里几乎成了默认起点,它内置Tomcat、自动配置、生态完善,适合快速搭建RESTful接口。MyBatis作为数据访问层,相比Spring Data JPA来说SQL更可控,课表管理涉及多表联查、周次判断、教室时段冲突判断这类复杂SQL,MyBatis的XML写法能把SQL逻辑完整暴露出来,方便优化和排查。MySQL则是中小型系统最稳妥的选择,部署简单,社区资料多,出问题随便一搜就有答案。
前端选择Vue,主要看中两点:一是组件化和响应式数据非常适合课表这种"数据结构固定但交互状态多"的页面;二是Vue脚手架和生态成熟,Element UI等组件库能直接提供表格、弹窗、表单校验,省去大量UI重复劳动。这套组合本质上是在"开发效率"和"可控性"之间找平衡。
不过选型时有一个容易忽略的坑:版本兼容。SpringBoot 2.x和3.x差别很大,3.x要求JDK17,很多学校机房或云服务器还在用JDK8。网上不少教程是SpringBoot 2.x写的,你如果直接上手最新版3.x,MyBatis、PageHelper这些老牌依赖经常出现不兼容。稳妥做法是SpringBoot用2.7.x,对应JDK8或JDK11,Vue用2.6.x(配Element UI)或Vue3(配Element Plus)。这篇内容里的配置方案按SpringBoot 2.7.x + MyBatis + MySQL 5.7/8.0 + Vue2来写,是当前兼容性最稳的组合。
2. 后端核心模块设计与实现要点
2.1 数据库表设计与MyBatis映射关系
课表管理系统的核心表,我建议这样设计:用户表、教师表、学生表、班级表、课程表、教室表、教学任务表(也就是排课结果表)。下面给出我实际项目里用过的精简版表结构,字段去掉冗余,只保留能跑通完整业务的部分。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| sys_user | id, username, password, role, teacher_id | 登录账号,角色区分管理员/教师/学生 |
| teacher | id, name, title, department | 教师基础信息 |
| student | id, name, class_id, major | 学生信息,关联班级 |
| class | id, name, grade, major | 班级信息 |
| course | id, name, credit, hours, course_type | 课程信息 |
| classroom | id, name, building, capacity, type | 教室信息 |
| schedule | id, class_id, course_id, teacher_id, classroom_id, week_start, week_end, day_of_week, section_start, section_end, semester | 排课结果,一条记录代表某班某课程在哪些周、星期几、哪几节上课 |
这里重点说schedule表。课表的"时间"维度由三个字段组合表达:week_start/week_end表示起始周和结束周,day_of_week表示星期几,section_start/section_end表示第几节到第几节。这种设计比"周次用逗号分隔字符串"更规范,可以直接用SQL做区间判断,比如判断某教师是否在指定时间冲突:
SELECT COUNT(*) FROM schedule WHERE semester = #{semester} AND teacher_id = #{teacherId} AND day_of_week = #{dayOfWeek} AND (#{sectionStart} < section_end AND #{sectionEnd} > section_start) AND ((#{weekNum} BETWEEN week_start AND week_end) OR (week_start BETWEEN #{weekNum} AND #{weekEnd}))这条SQL是冲突检测的核心,逻辑是区间重叠判断:新排课程的节次区间和已有记录的节次区间相交,并且周次也相交,就判定冲突。很多初学者会把冲突检测写在Java代码里循环判断,数据量小没什么问题,但数据量上来之后,写SQL比Java循环高效得多,也更清晰。
MyBatis映射方面,我习惯用XML而不是注解。像schedule表联查course、teacher、class、classroom这种场景,注解拼接SQL可读性很差,XML里可以把表关系、WHERE条件、resultMap都写得清清楚楚。一个常用技巧是写一个查询课表详情的resultMap,把四张关联表的字段映射好,前端拿到的就是一个完整的课表条目对象,不需要再二次请求。
2.2 SpringBoot业务层:权限校验与业务边界
课表管理系统的后端controller层我做得比较薄,只负责接收参数、调用service、返回统一结果。业务逻辑放在service层,这样接口复用性好,比如管理员排课和课程导入共用同一个service方法。
权限这块,我推荐用SpringBoot内置的拦截器或者Spring Security做简单版本。如果不想引入太重的东西,一个HandlerInterceptor就能解决80%的权限问题。我在项目里是这么做的:用户登录成功后生成一个Token,用拦截器拦截需要鉴权的路径,从Token解析出用户角色,再用注解或手工判断当前接口允许哪些角色访问。
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // 解析token,判断用户是否登录、角色是否匹配 if (token == null || !TokenUtils.verify(token)) { response.setStatus(401); return false; } return true; } }当然,如果你项目时间宽裕,或者论文需要体现安全性设计,直接上Spring Security也是合理的。但要注意一点:Spring Security的配置复杂度和版本坑很多,如果只是做一个课表管理系统,Interceptor方案在答辩时反而更好讲清楚,因为它直观。
业务层的另一个重点是事务。排课时要同时插入schedule记录、更新教室占用状态、记录操作日志,必须加上@Transactional,避免中途失败导致数据不一致。我踩过这样一个坑:排课时教室冲突检测通过,但插入时教室表更新失败,结果schedule表里多了记录,教室状态没变,导致后续排课数据错乱。加了事务之后,这类问题就从根源上消失了。
2.3 接口设计规范:统一返回结构与前端约定
前后端分离项目,接口规范是协作的地基。如果每个接口返回结构都不一样,前端写起来会疯掉。我的统一返回结构是:
{ "code": 200, "message": "操作成功", "data": { } }code的取值范围自己约定,200表示成功,401表示未登录,403表示无权限,500表示服务器异常。前端Axios拦截器统一判断code,非200就直接弹错误提示,省去每个页面单独处理错误逻辑的重复劳动。
课表管理系统的核心接口大致如下:
- POST /api/login 登录
- GET /api/schedule/teacher/{teacherId} 教师个人课表
- GET /api/schedule/student/{studentId} 学生班级课表
- GET /api/schedule/classroom/{classroomId} 教室占用情况
- POST /api/schedule 新增排课(管理员)
- PUT /api/schedule 调课(管理员)
- DELETE /api/schedule/{id} 删除排课(管理员)
- GET /api/schedule/conflict/check 冲突检测
接口命名要统一用小写复数名词,路径参数用花括号,查询条件用query参数。前端和后端接口联调的时候,最好维护一份接口文档,哪怕是简单的Markdown表格也行。我见过太多项目前后端各干各的,最后联调时对不上参数名,白白浪费两三天时间。
3. 前端Vue项目搭建与页面实现
3.1 Vue环境配置与项目初始化完整过程
前端环境配置是新手特别容易卡住的地方,这里把完整流程写一遍。首先安装Node.js,建议装14.x或16.x,这两个版本对Vue2和大部分依赖的兼容性很稳定。安装完成后,命令行验证:
node -v npm -vnpm是Node自带的包管理器。但因为国内网络原因,直接用npm下载依赖经常很慢甚至失败,我建议先换镜像源:
npm config set registry https://registry.npmmirror.com换完之后再用npm config get registry验证一下,显示的是国内镜像地址就说明生效了。接下来用Vue CLI创建项目:
npm install -g @vue/cli vue create course-system-web脚手架会问你选择默认配置还是手动配置,建议选手动(Manually select features),勾选Router和Vuex。项目创建完成后,安装UI组件库:
npm install element-ui --save npm install axios --save装依赖的过程可能会报各种warning,只要不出现npm ERR一般不用管。真的遇到版本冲突,优先方案是删掉node_modules目录和package-lock.json,重新执行npm install。
3.2 路由、Vuex状态管理与Axios请求封装
前端代码的骨架是路由。课表管理系统的页面不多,我划分成这样几个模块:登录页、管理员首页(排课管理、教室管理、课程管理、教师管理)、教师端课表页、学生端课表页。路由用Vue Router的懒加载写法:
const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/admin/schedule', component: () => import('@/views/admin/ScheduleManage.vue'), meta: { role: 'admin' } }, { path: '/teacher/schedule', component: () => import('@/views/teacher/TeacherSchedule.vue'), meta: { role: 'teacher' } } ];路由守卫里判断登录状态和角色,未登录跳转登录页,角色不匹配拦截并提示无权限。这一步配合后端拦截器,实现了双层权限控制。
Vuex用来保存登录用户的token和角色信息,刷新页面后从localStorage恢复。这个细节很关键:如果你的token只存在Vuex里,刷新页面就丢了,用户必须重新登录,体验很差。
Axios请求封装是我比较重视的部分。我会创建一个request.js,设置baseURL、请求拦截器(自动携带token)、响应拦截器(统一处理code):
import axios from 'axios'; import { Message } from 'element-ui'; import router from './router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res.data; } Message.error(res.message || '请求失败'); return Promise.reject(res); }, error => { if (error.response && error.response.status === 401) { router.push('/login'); } Message.error('网络异常,请稍后重试'); return Promise.reject(error); } );这里有个细节:baseURL我配置成/api而不是完整的后端地址。开发环境下由Vue CLI的devServer代理转发到后端端口,生产环境由Nginx统一转发。这样前端代码里不会写死任何IP和端口,换环境部署只需要改代理配置,不用改代码。
3.3 课表展示与排课交互的核心实现
课表展示是前端最核心的组件。我用的方案是Element UI的el-table改造成的"周课表":行是节次(第1节到第12节),列是星期一到星期日,每个单元格里渲染课名、教师、教室、周次范围。数据从后端接口拿到的是schedule列表,前端按day_of_week和section_start进行分组,渲染成二维数组。
这里有几个体验细节值得注意。同一门课如果跨两个节次,比如第3-4节,单元格应该合并或者显示在起始行并设置rowspan,否则表格看起来会错乱。我处理的方式是在表格数据构造阶段,将同一星期连续节次的课程合并成一条信息,放在起始节次的单元格里,显示"第3-4节 大学英语 张老师 教1-101"。
排课页面我做成一个"可拖拽或点击选择"的交互:管理员选择学期、年级、班级后,左侧显示该班已有课表,右侧是可选的课程、教师、教室列表,点击课程后弹出时间选择器,选择完成后前端先调用冲突检测接口,后端返回无冲突才允许提交。这个交互比传统的表单填写直观得多,实测用了这个方案之后,排课录入效率明显提升,也避免了大量无效提交。
4. 前后端联调与常见问题排查实录
4.1 跨域问题的几种处理方式
前后端分离项目联调时,跨域是绕不开的第一个坎。前端跑在8080端口,后端跑在8081端口,浏览器会拦截跨域请求。我常用的解决方式有三种,按推荐程度排序。
第一种是后端配置CORS全局跨域。SpringBoot里加一个配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }这种方式联调阶段最简单,但上线时要收紧origin白名单,不能长期使用通配符。
第二种是前端开发环境用devServer代理,这也是我推荐的方式。在vue.config.js里配置:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };前端请求/api/schedule,devServer自动转发到后端的/api/schedule,浏览器看到的只是同源请求,不会有跨域报错。第三种是上线后由Nginx反向代理统一处理,这个放到部署章节细说。
4.2 日期时间格式与JSON序列化的坑
前后端联调最容易翻车的还有日期格式。Java后端LocalDateTime默认序列化出来是一串类似2025-01-15T10:30:00的字符串,前端要展示成"2025-01-15 10:30:00",这里经常出现格式不匹配。我通常在后端统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8如果用了LocalDateTime,光配这个还不够,需要在pom里引入jackson-datatype-jsr310模块,并对LocalDateTime做序列化转换器配置。这属于典型的"配置三分钟,排查两小时"问题,提前配好能省很多事。
课表系统还有一个时间相关的坑:周次是数字类型,但前端传过来可能是字符串。比如排课时选择"第1-8周",后端如果定义成Integer就会报类型转换错误。我统一约定:前端传参全部按后端字段类型匹配,后端接收参数用@RequestBody接JSON对象,不要散装接收多个参数,这样能减少大部分类型不匹配问题。
4.3 高频报错速查与排障思路
| 报错现象 | 可能原因 | 解决思路 |
|---|---|---|
| 前端请求一直404 | 后端接口路径拼错,或前端代理未生效 | 先看后端控制台是否收到请求;再用Postman直接调后端接口验证 |
| 请求返回401 | token缺失或已过期 | 检查前端请求拦截器是否携带token,后端token解析是否成功 |
| 数据库中文乱码 | 连接串没加编码参数 | jdbc url加useUnicode=true&characterEncoding=utf8 |
| MyBatis查询结果为空 | 实体字段和表字段映射不上 | 开启mybatis驼峰映射,或明确写resultMap |
| SpringBoot启动报端口占用 | 8081被占用 | 换端口,或杀掉占用进程 |
| Nginx部署后前端资源404 | 前端路由是history模式,刷新时Nginx找不到对应路径 | 配置try_files回退到index.html |
再单独说一个MyBatis的经典问题:一级缓存和二级缓存。默认情况下MyBatis开启一级缓存(同一个SqlSession内有效),二级缓存默认不开启。课表系统里如果多次查询同一课表,其实没必要手动开启二级缓存,因为数据更新频繁,缓存失效问题反而更麻烦。我个人的建议是:这个项目别在MyBatis缓存上花太多精力,把SQL性能和索引做好更实际。
5. 部署上线完整流程(从源码到可访问)
5.1 后端打包与前端构建
部署前先要把代码打包。后端在项目根目录执行:
mvn clean package -DskipTests打包产物在target目录下,是一个可执行的jar包。打开这个jar里的application.yml,确认数据库连接参数是服务器上的配置,端口按照生产环境调整。SpringBoot内置Tomcat,不需要额外安装,直接java -jar就能跑。
前端构建命令:
npm run build构建产物在dist目录,里面是静态文件。到这里要特别注意:dist里引用资源的路径,取决于Vue项目的publicPath配置。如果你的页面部署在服务器根路径下,publicPath用默认的/;如果放在子路径,要改成对应路径,否则资源加载404。这个是我见过的高频坑,部署自查时优先检查。
5.2 服务器环境准备:JDK、MySQL、Nginx
服务器我以CentOS为例。先把基础环境装好:
yum install -y java-1.8.0-openjdk yum install -y mysql-server yum install -y nginx systemctl start mysqld systemctl start nginxMySQL安装完,创建数据库和用户,然后导入项目里的sql文件:
mysql -uroot -p create database course_system default charset utf8mb4; use course_system; source /root/course_system.sql;数据库导入的时候容易踩编码的坑。如果是Windows本机导出的sql文件,拿到Linux上导入中文乱码,多半是文件本身编码不是UTF-8。解决办法是导出时就明确指定UTF-8,或者导入前先用file命令确认sql文件的编码。
后端jar包和前端dist都上传到服务器后,后端直接运行:
nohup java -jar course-system.jar --spring.profiles.active=prod > course.log 2>&1 &用nohup让进程在后台运行,日志输出到course.log。验证后端接口是否正常,用curl测一下:curl http://localhost:8081/api/health,有响应说明后端起来了。
5.3 Nginx反向代理配置与域名访问
Nginx配置是整个部署的关键一环。我的做法是前端静态资源直接由Nginx托管,后端接口通过反向代理转发。一个完整的server配置如下:
server { listen 80; server_name yourdomain.com; root /var/www/course-system/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }/api/路径转发到后端jar,其余路径由前端路由接管。try_files $uri $uri/ /index.html这句尤其重要,否则Vue Router的history模式下,登录后按F5刷新页面就会404。配置完后执行nginx -t检查语法,然后nginx -s reload重载。
到这里,整个系统就能通过浏览器访问了。记住一个排查原则:前端报错先看浏览器Network面板;后端报错看course.log;数据库报错看MySQL日志。定位问题按这个顺序来,基本不会抓瞎。
6. 个人实操体会与几点建议
课表管理系统项目的核心价值不在"功能多新奇",而在于把一套完整的前后端分离开发流程走通。我在做这个项目的过程中,最深的体会有三点。
第一,数据库设计别贪多。schedule表我一开始设计的时候加了多余的"课程类型""学分"字段,后来发现完全用不上,反而因为冗余字段让查询变慢。字段够用就行,等真有需求再加。
第二,前后端接口约定必须文档化。哪怕只有一张Excel表,把接口路径、请求参数、返回结构写清楚,联调效率能提升一倍。我吃过一次亏:排课接口最初叫/api/course/arrange,后来统一改成/api/schedule,前端所有调用处都要跟着改,浪费了不少时间。
第三,版本搭配要克制。SpringBoot不要追最新版,Vue也不要盲目升级到Composition API风格,除非你对生态兼容有充分把握。稳定运行比花哨版本重要得多,尤其是毕业设计或演示类项目,确保"开箱即跑"才是第一目标。
最后分享一个小技巧:如果这个系统后续还要扩展,可以考虑加一个"课表导入导出Excel"功能,用Apache POI或EasyExcel实现,管理员可以从教务系统导出的Excel批量导入课程数据,能省掉大量手工录入时间。另外,课表周次展示可以做成"单周/双周"的Color标志,这个视觉效果很好,排课数据里只要加一个week_type字段就能实现。希望这套完整的项目拆解能帮你少走点弯路。