每年毕设季,后台问我要课表管理系统的人特别多。课表管理系统这个东西,听起来是个老掉牙的CRUD,但真正动手做的时候你就会发现,排课逻辑、时间冲突、教室资源分配、前后端联调,每一个环节都能把人折腾到怀疑人生。这次分享一套可直接运行的完整源码,SpringBoot做后端、Vue做前端、MySQL存数据,以西安工商学院的课表管理场景为原型,功能上覆盖了课程管理、班级管理、教师管理、教室管理、课表查询和冲突检测这几个核心模块。不管你是准备交毕设的学生,还是想快速上手全栈项目的开发者,这篇文章都能让你少走不少弯路。
我先说结论:这套项目的代码风格偏工程化,不是那种为了应付检查堆出来的玩具代码。后端用的是SpringBoot + MyBatis Plus,前端用的是Vue 2 + Element UI,数据库脚本和初始化数据都给你备好了,理论上clone下来改改数据库密码就能跑。但“能跑”和“跑得顺”是两回事,下文我会把项目的设计思路、每个模块的实现细节、启动步骤和常见坑全部拆开讲,包括我在实际运行中踩过的那些文档里根本不会写的坑。
1. 项目整体设计与技术选型拆解
1.1 课表管理系统到底在解决什么问题
先说业务。高校的课表管理远比想象中复杂。一个学期的课表,涉及专业、年级、班级、课程、教师、教室、时间槽七要素的交叉匹配。同一时间同一个教室不能排两门课,同一个老师同一时间不能出现在两个教室,一个班也不能同时上两门课。人工排课靠Excel和记忆力,在课程数量少的时候勉强能撑住,一旦到了两百门课、几十个班级的规模,冲突几乎是必然的。
这套系统的核心价值就是把排课和查询这件事从“Excel手工维护”变成“系统化数据管理”。管理员在系统里录入课程、维护教师和班级信息,然后通过课表管理模块把课程分配到具体的时间槽位上,系统自动检测时间冲突和教室冲突,学生和教师登录之后只能查看跟自己相关的课表。这个模型不算复杂,但它是绝大多数高校教务系统的基础单元,做好这一个,其他管理系统的套路你也就摸清了。
1.2 为什么是SpringBoot + Vue + MySQL这套组合
很多同学会纠结技术选型,问为什么不选SSH、不选Flask、不选微服务。我的观点很直接:毕设和中小型项目,SpringBoot + Vue + MySQL就是性价比最高的组合,没有之一。
SpringBoot的优势在于零XML配置和自动装配。传统SSH项目光写配置文件就能花掉一天时间,SpringBoot通过starter机制把常用场景的依赖打包好,引入即用。对于课表管理系统这种典型的CRUD + 业务校验项目,SpringBoot的开发效率是最高的。同时SpringBoot的生态太成熟了,集成MyBatis、Redis、JWT、Swagger都有现成的starter,社区资料多到查不完。
Vue选择前后端分离架构,主要考虑到两个场景:课表的周视图和列表视图需要前端做灵活的交互切换,而管理端的表单校验、弹窗提示用Vue + Element UI写起来非常顺手。前后端分离之后,后端只需要提供RESTful API,前端通过axios调用,开发时用Node代理解决跨域,部署时后端打包成Jar包、前端构建成静态文件放在一起,整个流程清晰且可控。
MySQL就不多说了,课表数据是典型的结构化关系数据,课程、班级、教师、教室、时间槽之间存在明确的外键关联,用关系型数据库表达最自然。MySQL 5.7以上版本对JSON类型的支持也很好,如果后续想扩展课表的自定义属性,也有空间。
选这套组合还有一个现实原因:市面上关于这仨的资料和解决方案覆盖非常全,你遇到任何问题,基本都能在网上找到对应的答案。做项目最怕的不是技术难,而是踩了坑没人告诉你坑在哪。
1.3 拿到源码之后,先从哪个文件开始看
我必须给第一次接触全栈项目的同学一条阅读路线。不要一上来就打开application.yml开始改配置,也不要直接npm run dev,那样你只会收获一堆报错。
建议按照这个顺序阅读:先打开数据库脚本,看表结构,搞明白课表数据是怎么组织的,这是整个系统最核心的部分。然后打开后端项目,从实体类看起,对照数据库表结构理解字段映射关系。接着看Controller层,了解系统对外暴露了哪些接口。再接着看Service层,理解排课冲突检测这类核心业务逻辑是怎么实现的。最后再打开前端,从router看起,搞清楚页面路由和接口的对应关系。按这条路线走一遍,你基本就能看懂整个项目了。
2. 功能模块与数据库设计的核心逻辑
2.1 三种角色,各取所需
系统的用户角色分为管理员、教师、学生三类,对应的是三种完全不同的使用场景。
管理员负责基础数据维护和排课管理。教师基础信息的管理、班级的增删改查、课程的创建、教室资源的维护,这些基础数据全部由管理员录入。排课管理是管理端的核心操作,管理员选择学期、选择班级、选择课程、指定教师、指定教室、选择时间槽,系统在保存前自动做冲突检测。
教师端的功能相对简单,登录后可以查看自己的代课课表,按周次浏览每天的上课时间和地点。部分学校有调课申请的需求,这套系统的教师端也预留了申请调课的入口,后端有对应的接口,前端页面可以直接对接。
学生端就是纯粹的课表查询,选完班级后,系统按周展示这个班的全部课程安排。这里要注意一个业务细节:一个学生属于哪个班级,决定了学生端看到什么课表;一个课程安排绑定的是班级而不是单个学生,所以学生端不需要维护个人的课程订阅关系,逻辑上简单很多。
2.2 核心功能模块一览
从功能清单上看,这套系统的模块划分是标准的MVC思路。
课程管理模块负责课程信息的增删改查,字段包括课程名称、课程编号、学分、课时、课程类型。课程是课表系统的基础实体,课程信息一旦发生变化,已经排好的课表可能全部要跟着调整,所以在设计上,课程表的数据要保证稳定性,修改课程编号这种操作应该禁掉。
班级管理模块维护专业、年级、班级名称等信息。班级在整个课表体系里是一个关键的“聚合单位”,课表本质上就是班级 × 周次 × 星期 × 节次 的矩阵。
教师管理和教室管理比较好理解,教师表主要存姓名、工号、职称、所属学院,教室表存教室编号、容纳人数、所在校区、教室类型(普通教室/机房/多媒体教室)。
课表管理模块是整个系统的核心。管理员选择班级后,系统按周展示这个班的课表,空白槽位可以点击添加课程安排。保存时后端做逻辑校验,检查教师在同一时间是否有其他课程安排,检查教室在同一时间是否被其他班级占用。校验通过才允许写入数据库。
查询模块提供多维度检索能力。学生登录查自己的班级课表,教师登录查自己的代课课表,管理员可以按班级、按教师、按教室三个维度查看课表占用情况。
2.3 数据库表设计的关键细节
很多初学者在做表设计的时候容易犯一个错误:把所有信息塞在一张巨大的表里。这套系统用了细分表的方式,核心表总共有八张:用户表、教师表、学生表、班级表、课程表、教室表、学期表、课表记录表。
课表记录表(schedule)是整张数据关系网的枢纽,我单独说一下。
schedule表的核心字段包括:id、class_id(关联班级)、course_id(关联课程)、teacher_id(关联教师)、classroom_id(关联教室)、semester_id(关联学期)、week_day(星期几,1-7)、start_section(开始节次)、end_section(结束节次)、week_start(开始周次)、week_end(结束周次)、week_interval(周间隔,默认1表示每周都有)。
这个设计的精妙之处在于用week_start和week_end描述课程的“周次范围”。比如一门课从第1周到第16周,每周四上午3-4节上课,那么week_start=1,week_end=16,week_interval=1。如果一门课隔周上,把week_interval改成2就可以了。这种设计比直接用“周次字符串”或者逐周拆成多条记录要灵活得多,排课查询的时候通过时间范围判断就能快速过滤。
还有一个设计细节值得提:week_day、start_section、end_section 这三列尽量用int类型,不要用varchar。一方面是为了查询效率,更重要的是后端做冲突检测的时候可以直接用数值比较,避免字符串解析的麻烦。
数据库字符集一定要用utf8mb4,不要用utf8。utf8mb4是utf8的超集,能存储emoji和生僻字,MySQL 8.0默认就是这个。如果你用的MySQL 5.7,建库的时候手动指定一下DEFAULT CHARACTER SET utf8mb4。这一步看似无所谓,真到了课程名称里出现生僻字导致页面乱码的时候,你就知道什么叫欲哭无泪了。另外所有表都要加上create_time和update_time字段,MyBatis Plus的自动填充功能可以直接用,后续排查数据问题的时候这两个字段能帮你定位很多麻烦。
3. 从零到一运行项目:环境配置与实操细节
3.1 版本选型,别让环境坑了你
这个项目能“直接运行”是有前提的,就是版本别搞错。我把经过验证的版本组合放在下面这张表里,照着配基本不会出问题。
| 组件 | 推荐版本 | 注意事项 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot 2.x 不建议用JDK 17,会有兼容性问题 |
| Maven | 3.6.3+ | 配置阿里云镜像,否则依赖下载慢到怀疑人生 |
| SpringBoot | 2.3.x - 2.7.x | 不要用3.x,依赖和用法有较大变化 |
| MySQL | 5.7+ 或 8.0 | 8.0要注意驱动版本和时区配置 |
| Node.js | 14.x - 16.x | Vue CLI 4.x 在Node 18+上会有OpenSSL报错 |
| Vue CLI | 4.x | 不要用5.x,4.x生态最稳定 |
特别注意两个点。第一,SpringBoot 2.7.x搭配MyBatis Plus 3.5.x是我测试过最稳的组合,你如果非要追求最新版SpringBoot 3.x,会发现很多starter的坐标都变了,网上查到的资料大多不适用。第二,前端用的Vue CLI 4.x在Node 16环境下跑得很顺畅,但如果你电脑装的是Node 18或20,启动开发服务器的时候大概率会报ERR_OSSL_EVP_UNSUPPORTED。解决办法是在package.json的scripts里加上"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",不过这个变量只在Node 17+才有意义。
3.2 数据库初始化的三个步骤
第一步创建数据库。打开MySQL命令行或者Navicat,执行数据库脚本。这套源码里带了两个SQL文件,一个建表、一个初始化数据。建议先执行建表脚本,再执行初始数据脚本,顺序不要搞反。
这里有个容易忽略的操作:如果MySQL是8.0版本,建完库之后记得检查一下默认字符集。有时候客户端连接设置的字符集和数据库实际字符集不一致,中文字段写入数据库会变成乱码。稳妥的做法是在连接参数里加上characterEncoding=utf8mb4,JDBC连接串里用serverTimezone=Asia/Shanghai指定时区,这个参数不写,连8.0的数据库会直接报时区相关的异常。
初始化数据脚本里预置了一个admin账号,密码是加密存储的。很多同学拿到手发现数据库里存的是一串密文,不知道怎么登录。这里说明一下,这套源码的密码加密用的是BCrypt,你可以直接拿源码里PasswordEncoder生成的新密文替换,也可以保留初始密文,对应的明文密码在项目文档里一般会注明。如果文档里没写,最省事的方法是在后端启动时加一段临时代码,用PasswordEncoder生成一条新密文再写进数据库,搞完再删掉这段代码。
3.3 后端启动:改三个地方就能跑
后端项目的配置集中在src/main/resources/application.yml这个文件里。你需要修改的无非三处。
第一处是数据源配置。把url改成你的数据库地址,用户名和密码改成你自己的。url格式是jdbc:mysql://localhost:3306/课表数据库名?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。注意mysql驱动版本如果用的是8.x,driverClassName要写成com.mysql.cj.jdbc.Driver,写成com.mysql.jdbc.Driver会直接报ClassNotFoundException。
第二处是MyBatis Plus的配置。主要检查mapper-locations路径,默认是classpath*:mapper/*.xml,如果你的SQL文件放在resources目录下的mapper文件夹里,就不用改。如果改了路径,记得把XML文件放到对应位置。
第三处是端口配置。server.port默认是8080,如果本机端口被占用,改成一个不冲突的端口就行。前端代码里如果写死了后端地址,这一步的改动会导致接口调用失败,要注意联动修改。
改完这三处,运行SpringBoot主类,控制台出现Spring的Banner和“Started xxxApplication in x.xxx seconds”就说明后端起来了。启动过程中如果看到“Failed to configure a DataSource”的报错,不用慌,99%是数据源配置有问题,去检查数据库密码和URL。
3.4 前端启动:npm install是第一个拦路虎
前端项目启动的正确姿势分两步。
第一步安装依赖。npm install这个命令对网络环境极其敏感,国内环境建议先配置npm镜像源。在终端执行npm config set registry https://registry.npmmirror.com,再执行npm install,速度会快很多。安装过程中如果报permissions相关的错误,不要用sudo硬怼,大概率是node_modules目录权限问题,删掉node_modules重新安装一次往往就好了。
第二步启动开发服务器。运行npm run serve,默认端口是8080,如果和后端冲突,Vue CLI会提示你换端口。这里有个前后端联调的经典问题:前端开发服务器跑在8080,后端跑在8080,端口冲突了,怎么处理?最优雅的方案不是硬改后端端口,而是让前端跑另一个端口(比如8081),然后通过vue.config.js里的devServer.proxy配置,把前端对/api的请求转发到后端的真实地址。这样前端代码里所有请求都写成相对路径,联调的时候不用改代码,部署的时候也不用改代码。
3.5 前端配置代理的完整写法
在vue.config.js里这样配置:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这段配置的意思是:浏览器发起的所有以/api开头的请求,都会被开发服务器转发到http://localhost:8080,并且把URL里的/api前缀去掉。比如前端请求/api/course/list,后端实际收到的请求是/course/list。如果你的后端Controller类上没加/api前缀,这种写法正好匹配。
这里有个细节要提醒:后端接口的路径设计,要么统一带/api前缀,要么统一不带,不要混着来。混着来会导致代理配置和路径重写永远对不上,排查起来非常痛苦。
前端项目用的是Vue Router,路由模式建议保留hash模式(默认就是),不要轻易改成history模式。改history模式不仅需要后端做路径重定向配置,部署到Linux服务器上的时候还要处理Nginx的try_files参数,不是不行,是麻烦。课表管理系统没有那么严格的URL美观需求,hash模式完全够用。
4. 前端实现的关键环节:从路由到组件
4.1 路由设计思路
前端路由一共分三层:登录页、管理端主布局、功能页面。
第一层是/login,未登录用户访问任何页面都会被路由守卫拦截并重定向到登录页。登录成功之后会从后端拿到token,前端把token存到localStorage,写进axios的请求拦截器。这个逻辑是用Vue生态混得久的人都懂的标准套路。
第二层是管理端主布局,也就是layout。主布局是一个左侧菜单栏加右侧内容区的结构,菜单项按管理员、教师、学生三种角色动态渲染。这里就涉及动态路由的概念:不同角色登录之后能看到的菜单不同,路由表也是登录之后根据角色信息动态添加的。这套源码用了一个简单高效的办法——在router.beforeEach守卫里判断用户角色,根据角色去router.addRoutes动态挂载不同权限的路由表。
第三层是具体业务页面。课程管理、班级管理、教师管理、教室管理、课表管理、课表查询,每个页面都是一个独立组件。页面之间的切换用的是Vue Router的懒加载,import语句用函数返回组件,代码分包加载,首屏不会慢。
4.2 课表周视图的表格渲染套路
课表管理页面是整套系统前端部分最“有技术含量”的页面。它的核心结构是:行表示星期一到星期日,列表示节次,每个单元格是一个可点击的槽位。
前端实现上,用Element UI的el-table或者手写一层grid布局都可以。这套源码用的是el-table,把表格的行数据定义成一个二维数组,一维是节次,二维是星期。每个单元格的数据结构是:
{ courseName: '高等数学', teacherName: '张老师', classroomName: 'A101', weekRange: '1-16周', color: '#409EFF' }如果这个时间槽已经排了课,单元格里就显示课程信息卡片;如果没排课,显示加号按钮,点击弹出排课表单。这里的要点是处理单元格的合并和跨行展示,一门课占了第3-4节,视觉上要合并两个单元格。el-table的span-method属性提供了单元格合并的回调,返回值是个数组,[rowspan, colspan],按这个逻辑计算即可。
课程颜色是另一个容易忽略的细节。不同课程给不同颜色,前端做一个映射表,根据课程ID取模映射颜色,这样同一个课程在不同周次、不同班级显示的颜色是一致的,视觉上找起来非常方便。纯前端计算颜色的好处是不用后端加字段,逻辑上也不复杂。
4.3 Vue的组件通信方式和插槽使用
项目里组件通信用得很常规,父组件往子组件传数据用props,子组件往父组件发消息用$emit。排课弹窗组件,数据的输入输出就是这么一个闭环:课表页面是父组件,点某个空槽位,把当前班级ID、星期、节次传给弹窗子组件,子组件里选完课程、教师、教室后,把新排好的课表数据emit回去,父组件更新页面数据,再调用后端接口做保存。
这个交互流程里的插槽用得比较浅,主要是布局层面的插槽。Element UI的表格列、弹窗的footer区域都是默认支持的插槽用法。如果你要扩展功能,比如在课程卡片上加一个“调课”按钮,直接在课表单元格组件里加一个具名插槽,外部通过template填充按钮内容就行。Vue的插槽机制本质上就是“占位 + 内容分发”,课表卡片这种高频变化的区域最适合用插槽做扩展。
4.4 axios封装和登录状态管理
axios的封装决定了整个前端代码的规范程度。源码里把axios实例单独抽了一个文件,配置了baseURL、请求超时时间、请求拦截器、响应拦截器。
请求拦截器做一件事:从localStorage拿token,加上Authorization头。响应拦截器做两件事:如果状态码是401,说明token失效,清除本地登录状态,跳转登录页;如果状态码是200且业务码非0,弹出Element UI的Message提示错误信息。
登录状态管理没有用Vuex做持久化,而是直接存在localStorage,刷新页面后通过router.beforeEach检查本地有没有token。这个方案对于课表管理系统这种对权限要求不极端的场景完全够用,也简化了状态管理的复杂度。如果你以后要扩展更多动态权限,再把Vuex或者Pinia引入也不迟。
5. 联调、打包与部署:从开发环境到生产环境
5.1 前后端联调的标准流程
项目开发完,前后端联调是必经之路。联调不是一个动作,而是一套流程。
先确认后端接口文档。源码里如果集成了Swagger,启动后访问/swagger-ui.html就能看到所有接口的说明和参数。没有Swagger的话,直接打开后端的Controller源码,看方法上的@RequestMapping注解。建议联调之前先用Postman把每个接口测一遍,确认数据返回格式正常,再去前端页面里点按钮调接口,这样能把问题定位在前端还是后端,不用两头Debug。
然后是数据格式的对接。后端统一返回结果是{code: 200, message: "success", data: {...}}这种格式。如果后端返回的data结构是List,前端就不要按Object去取;如果后端日期字段是时间戳,前端展示层要转换格式。这类问题在联调阶段出现频率最高,解决办法就是前后端约定统一的JSON结构,而这套源码已经在公共返回类里把这些都封装好了,直接用即可。
最后处理跨域问题。开发环境通过webpack的proxy解决跨域已经说过了,生产环境前后端部署在同一域名下就不存在跨域了。如果生产环境仍然需要前后端分离部署在不同域名,后端要额外配置CorsFilter,否则浏览器会拦截非同源的响应。
5.2 前端打包产物怎么放进SpringBoot
这是很多同学都不太清楚的一个环节,同时也是部署的关键一步。前端npm run build完成之后,dist目录里会生成index.html和一堆js/css静态资源。
SpringBoot的静态资源目录默认包含src/main/resources/static。把dist目录里的所有内容拷贝到static目录下,然后启动SpringBoot的Jar包,直接访问http://IP:端口/就能看到页面。这个方案相当于用SpringBoot托管前端静态资源,一个Jar包搞定前后端所有内容。部署到服务器上只需要装一个JDK和MySQL,连Nginx都不用装。
这里有个路由相关的坑必须提醒:如果你在开发环境用的是history模式,做上面的合并操作后,直接访问根路径没问题,但刷新一个深层路径(比如访问/course管理页刷新),后端没有对应的路由映射,会返回404。解决办法有三条:一是改回hash模式;二是在后端写一个route转发Controller,把所有非API的请求转发到index.html;三是用Nginx配置try_files。最省事的是第一种,所以我前面反复强调hash模式。
5.3 Linux服务器部署实操记录
把项目部署到Linux服务器,核心步骤就五步:安装JDK、安装MySQL、导入SQL脚本、上传Jar包、启动。
JDK安装用yum或者apt就行,版本一定和本地开发保持一致。MySQL安装之后记得把root密码确认好,用Navicat远程连接前要开3306端口,还得给root账号授权远程访问。生产环境要用root远程连接,一般是为了图方便,但你最好建一个专用账号只授权本项目库的权限,安全性能好一些。
启动命令稍微讲讲。常规做法是nohup java -jar xxx.jar > logs.log 2>&1 &,这样关闭终端后服务依然在运行。查看日志用tail -f logs.log,动态观察启动过程。如果启动失败,翻日志看异常信息是最快的排查手段。数据库连不上就去查网络和密码,端口被占就杀掉占用的进程。
内存方面,SpringBoot默认的JVM堆内存是物理内存的1/4,一台2G内存的小服务器跑一个课表系统绰绰有余,不需要额外调优。如果你发现启动特别慢,可以在启动命令里加-Xms512m -Xmx512m,强制指定初始和最大堆内存,减少动态扩容的次数。
6. 排课冲突检测的算法思路和业务实现
6.1 冲突检测的本质是维度交叉判断
排课功能是本系统的核心业务点,但我发现很多拿到源码的同学并没有真正读懂冲突检测这段代码。我从实现层面拆开讲一下。
冲突检测的本质,是在课程安排保存之前,检查拟新增的时间槽是否和已有安排发生重叠。三个关键维度:时间维度(学期、周次、星期、节次)、资源维度(教室)、人员维度(教师、班级)。
时间维度的重叠要同时满足四个条件才叫重叠:同一学期、周次范围有交集、星期相同、节次范围有交集。听起来很绕,实际判断代码核心就是区间重叠判断。两个区间[a, b]和[c, d]有交集的充要条件是a <= d 且 c <= b。比如已有课程占第1到16周,新课程拟排第8到10周,条件a <= d(1 <= 10)且c <= b(8 <= 16)成立,时间上就重叠了。同理,节次区间[1, 2]和[2, 2]也有交集,因为2 >= 2且2 <= 2。
教室维度的冲突就更好理解了,同一时间同一教室不能分配两个班级。查数据库时按教室ID + 时间条件去比对,查到了就说明冲突。人员维度的冲突逻辑同理。
6.2 一个完整的保存校验流程
后端Service层的保存方法执行的校验步骤是这样的:
第一步查班级时间冲突。以class_id + semester_id为条件,查出这个班级已有的全部课程安排,逐个比对时间和节次是否有交集,有交集直接返回“该班级在所选时间段已有课程”。
第二步查教师时间冲突。同样逻辑,换成teacher_id去查,有交集返回“该教师在所选时间段已有教学安排”。
第三步查教室时间冲突。以classroom_id + 时间段为条件查询,有交集返回“该教室在所选时间段已被占用”。
这里还要注意一个细节:修改课程安排(编辑排课记录)的时候,校验要排除自身。因为编辑的时候,当前这条记录本身就存在数据库里,如果不排除自己,系统会把这次修改误判成和自身冲突。很多同学第一次写这个逻辑都会漏掉,源码里用了一个id != currentId的条件把这个问题处理掉了。
排课业务还有周次间隔的判定。week_interval是2表示隔周上课,判断某周是否有课时,用weekNumber % week_interval等于0来判定。注意周次的起始点从week_start算起,不要从第1周全量算,不然隔周课的对错完全反过来。
6.3 前端周次选择器的联动处理
前端排课表单里,选择周次范围是一个联动逻辑。点开周次选择器,显示1到20周,多选。比如选了第1周、第3周、第5周,后端要怎么理解?前端先把选择的周数组排序,判断是否为连续区间,把连续的部分合并成一个区间,比如[1,3,5]变成[1,1]、[3,3]、[5,5]存在week_start和week_end里,week_interval设为1。这种情况课程每周都有课,周间隔性不存在。如果用户选了1,3,5,7,9,间隔是2,把它合并成start=1、end=9、interval=2一条,数据量少而且查询效率高。
这个合并逻辑放在前端做还是后端做都行。源码放在前端处理,因为Element UI的表单组件直接操作的是数组,处理成区间之后再传给后端,后端落库的字段结构非常规范。
7. 常见问题与排查技巧实录
7.1 后端启动失败的典型场景
我把实际运行这套源码时最容易翻车的场景罗列一下,你可以直接对照排查。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| Failed to configure a DataSource | 数据源配置错误或数据库没启动 | 检查application.yml的url、用户名、密码 |
| Access denied for user 'root'@'localhost' | 数据库账号密码不对 | 用Navicat确认本地MySQL密码 |
| Unknown database 'xxx' | 数据库名写错或没创建库 | 执行CREATE DATABASE语句 |
| Port 8080 was already in use | 端口被占用 | 换端口或者杀掉占用进程 |
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | MySQL驱动版本和连接串不匹配 | 更新驱动依赖版本并修改driverClassName |
| AllowPublicKeyRetrieval=true needed | MySQL 8.0 的caching_sha2_password认证问题 | 在连接串加allowPublicKeyRetrieval=true |
这里我多说一个很多人都会踩的坑:MySQL 8.0默认的认证插件是caching_sha2_password,老版本的JDBC驱动不识别这种认证方式,会报Public Key Retrieval is not allowed。解决方案是在JDBC连接串末尾加上allowPublicKeyRetrieval=true&useSSL=false,同时把mysql-connector-java的版本更新到8.0.33以上,这个版本对8.0数据库的兼容性最好。
7.2 前端run dev之后白屏或报错
前端启动过程中遇到的坑比后端多,我这里挑典型的说。
npm install慢或者卡住不动,解决办法前面说了换镜像源,但还有个小技巧:如果换源之后还是慢,检查一下npm config get registry生效没有。有时候你在项目的.npmrc文件里写了一个registry,和全局配置冲突,实际走的还是旧地址。
另一个高频问题是BrowserRouter导致的刷新404,我前面已经给过解决方案:改hash模式。
还有一类问题比较隐蔽:Vue打包之后接口请求404。开发环境走代理一切正常,打包部署之后页面能打开,但接口全部404。原因很可能是后端接口的context-path配置不对,或者前端请求的baseURL写的是开发环境的代理地址,打包后依然请求localhost。排查思路很简单,打开浏览器的Network面板,看看实际请求的URL是什么,跟后端的接口路径对比,就知道是哪一层出了问题。
7.3 业务数据异常的处理经验
数据这一层的问题往往比环境问题更让人头疼。
课表显示错位是最常见的数据问题。比如某门课显示在星期三,但数据库里存的week_day是3,页面上却跑到星期四去了。这种问题十有八九是前后端对“星期几”的约定不一致。后端约定周一对应1,但前端用Date对象的getDay()获取周几,得到的值是0到6,0表示周日。两个体系对不上,数据就错位了。解决办法是以数据库字段的int值作为唯一标准,前端展示前统一做一次转换。
另一个问题是班级和课程调换导致的历史数据残留。比如一门课程被删除了,但课表记录表里还有它的外键指向,查询的时候如果直接做JOIN,会出现查不到课程名的情况。处理办法是课表记录表的外键字段不要加物理外键约束,逻辑外键即可,删除课程时先清掉对应的课表记录,或者在查询时做null判断兜底。
还有一个数据库性能方面的问题。课表记录表的数据量不会特别大,但在做冲突检测时会频繁按班级、教师、教室维度查询,建议在schedule表上建三组联合索引:(semester_id, class_id)、(semester_id, teacher_id)、(semester_id, classroom_id, week_day)。索引不是越多越好,针对查询频率最高的三个维度建索引,性能就完全够用了。
8. 这套源码的扩展方向和使用建议
8.1 代码层面的优化点
如果你打算在这个项目基础上继续做,我建议先做三件事。第一件事是把后端返回的日期格式统一。现在的实现是LocalDateTime直接序列化,前端拿到的格式是带T的ISO格式,建议在application.yml里配置spring.jackson.date-format,输出成年月日 时分秒的格式,前端展示会友好很多。第二件事是把接口返回类做成泛型,统一ResponseResult ,避免每个Controller都返回Map。第三件事是在Swagger配置里把API文档完善一下,这个工作在一个项目初期做收益最高,中期补文档的效率远低于边写边注释。
8.2 功能层面的扩展思路
课表管理系统后续做功能扩展的空间很大。比较实用的方向有这么几个:第一个是学生和教师的导出功能。把课表导出成PDF或者Excel图片,是老师和学生都高频使用的功能。后端用POI或者EasyExcel生成Excel文件,前端放一个导出按钮,需求明确,技术也不难。第二个是公告消息模块,管理员发布调课通知,前端页面提示。第三个是智能排课算法,这个方向比较进阶,把排课问题建模成约束满足问题,用遗传算法去搜索合法的排课方案,作为毕设的亮点来写,非常加分。第四个是登录增加验证码和操作日志,完善一下系统审计能力。
8.3 给拿着源码做毕设的同学几点提醒
最后以我的个人经验说几句掏心窝的话。这年头拿到一套可以直接运行的源码并不难,难的是你能不能把它讲清楚、改明白。答辩的时候老师最常问的三个问题:你为什么选择这个技术栈?课表冲突是怎么检测的?你在这个项目里独立完成了哪些部分?如果你只是把项目跑起来,连课表冲突检测的代码在哪个文件里都说不出来,那答辩基本就是灾难。
建议你拿到源码之后,至少把课表冲突检测的Server实现类完整读一遍,把MyBatis Plus的查询逻辑搞明白,能自己画一个系统的架构图。在此基础上,哪怕你只是加了一个Excel导出功能,或者完善了调课申请模块,答辩的时候你都多了一个可以主动展示的亮点。不要做“只跑通不改一行代码”的人,那样对不起你这几个月的付出。
这套项目后续怎么用,还是看你的目标。想快点交差,就按部署流程跑通,改数据库里的学校名和初始化数据;想认真学全栈,就按我给的阅读路线一行行过代码,把每个Controller、Service、Mapper的角色弄清楚。我自己的习惯是,拿到任何一套新代码,第一时间不是急着运行,而是先把数据库表结构和接口文档通读一遍,脑子里有了地图,后面不管改代码还是排错,都会顺很多。希望这套课表管理系统能成为你全栈路上的第一块完整的台阶。