这套系统刚拿到手的时候,我第一反应是"又一个课设级别的三代同堂项目"。但真正把源码跑起来、把前后端链路理清楚之后,我发现它远比想象中完整——不是那种只有一个CRUD壳子的演示品,而是把大学计算机基础课最琐碎的教学事务:选课、排课、题库、考试、成绩统计、通知公告,全部塞进了同一套可以独立运行的工程里。如果你正好在准备课程设计、毕业设计,或者帮老师搭一套临时可用的网络教学管理后台,这套SpringBoot + Vue + MySQL的组合值得你花一下午研究透。
先说结论:它的技术栈非常主流,SpringBoot负责后端接口,Vue负责管理端页面,MySQL存储所有业务数据,三者通过RESTful API通信,用JWT做登录态保持。整条链路没有用到冷门框架,对于基础一般的同学来说,能在一天内跑起来;对于想借此学习完整项目结构的同学来说,它又是一个恰好足够复杂的范本。
下面我从六个维度拆这套系统,前几节偏设计和原理,后两节直接讲运行和排错,你可以按需跳读。
1. 系统全景:从登录到成绩单,一条完整的教学管理闭环
1.1 它到底管了哪些事
大学计算机基础课有个特点:学生基数大、班次多、上机实验频繁、平时作业和章节测验穿插。靠老师手动登记成绩、靠学习委员转告通知,一旦超过两百人,管理成本就会迅速失控。这套系统要解决的,就是把这堆线下流程搬到线上,让三类角色各司其职。
- 管理员侧:负责基础数据维护,包括学生信息导入、教师账号开通、课程开设与学期设置。
- 教师侧:创建课程章节、上传课件资源、维护题库、布置作业、发起在线考试、批阅主观题、查看班级成绩分布。
- 学生侧:选课、查看课件、刷题练习、参加考试、查看成绩与错题记录。
从流程上看,选课 → 学习 → 练习 → 考试 → 评分 → 成绩反馈,形成了一个完整闭环。这比光有"学生管理"和"课程列表"两棵孤立的CRUD树要实用得多。
1.2 为什么是SpringBoot + Vue + MySQL这套组合
很多同学问过我一个问题:课设系统用什么栈最稳?我的回答常年是"你以后工作最可能遇到什么栈,就用什么栈"。SpringBoot是目前Java后端的事实标准,Vue是前端招聘需求量最大的框架之一,MySQL是关系型数据库里最普适的选择,这三个东西组合出来的项目,放到简历上不算出彩,但绝对不寒酸,而且对你理解"前端请求 → 后端处理 → 数据库持久化"这条主线特别有帮助。
更关键的是,这套组合的社区资料极多,你跑通之后想改功能、想加模块,随便一搜就能找到参考方案。换成小众框架虽然很酷,但你在答辩或项目迭代时遇到问题可能连问的地方都没有。
1.3 三个角色的权限边界
系统内部不是所有接口都能被随便调用,它采用基于角色的访问控制,三种角色对应三套菜单和三组接口权限。后端通过JWT把用户身份信息带到每个请求里,拦截器再根据请求路径前缀做粗粒度校验,服务层再根据业务数据做细粒度校验。
这个设计听起来简单,但实际非常重要。比如学生可以查看自己的成绩,但绝不能看全班的成绩单;教师可以发布考试,但不能修改自己的最终评分记录——这些规则不是靠前端隐藏按钮就能保证的,必须在后端有权限校验。这套系统在这点上做得比较规范,没有出现"前端挡了后端就裸奔"的常见课设毛病。
2. SpringBoot后端的核心设计:接口分层与身份认证
2.1 典型的四层结构
后端工程(我直接说代码层面的结构)分得很清楚:
- controller 层:只做参数接收、调用service、返回统一结果对象,不写任何业务逻辑。
- service 层:承载业务规则,比如"学生能否选这门课""考试是否在有效时间内""成绩是否符合正态分布"。
- mapper 层:数据访问,基于MyBatis操作MySQL。
- entity / dto / vo:实体对象、传输对象、视图对象分开。
很多初学者喜欢把代码全堆在controller里,觉得"反正能跑"。但一旦功能复杂到考试、成绩、选课这种多表联动的程度,堆代码的路子会迅速崩溃,改一个bug牵出三个新bug。这套系统的分层虽然不能说教科书级别,但至少是合格的——每个接口的链路大概是:Vue发来JSON → Controller接收 → Service处理 → Mapper查询/写入 → 返回统一Result对象。
2.2 JWT认证:无状态登录是怎么实现的
登录模块用的是JWT,不是传统的Session。JWT的原理说白了就是:用户登录成功后,后端把用户ID、角色、过期时间等信息签名生成一串加密字符串返回给前端,前端每次请求都把这串字符放在请求头里,后端验签通过就信任这个身份。
这样做的好处是后端不需要在内存或数据库里维护Session会话,特别适合前后端分离部署的场景。坏处是token一旦签发,在过期前很难主动吊销,所以这套系统里把token过期时间设得比较短(一般是2小时),同时要求前端在token失效后自动跳回登录页。
代码层面,后端通常通过拦截器(HandlerInterceptor)统一读取请求头中的Authorization字段,解析token,把用户信息放入ThreadLocal,业务代码随时取用当前登录人ID。这个套路非常经典,建议你把这段代码反复读几遍,以后工作里大概率还要用到。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } UserContext.setUserId(JwtUtil.getUserId(token)); return true; } }2.3 统一返回结构和全局异常处理
前后端分离项目最忌讳接口返回值五花八门。这套系统定义了一个统一返回结构,大概长这样:
{ "code": 200, "message": "success", "data": { } }前端axios拿到响应后,先看code是不是200,不是就直接报错提示。这样处理的好处是,前端不需要为每个接口单独编写错误处理逻辑,一个拦截器统一搞定。
与之配套的是后端全局异常处理器,用@RestControllerAdvice捕获所有未处理的异常,把"系统莫名报500"变成"前端看到明确的错误提示"。这一点看起来不起眼,实际开发体验差别巨大——没有全局异常处理的项目,出问题时报错信息经常是英文堆栈,学生在页面上看到只会一头雾水。
2.4 典型接口示例
以"学生提交考试答案"为例,后端接口逻辑大概长这样:校验考试是否在进行中、校验学生是否已选这门课、逐题读取学生答案并和标准答案比对、计算客观题分数、把主观题答案记录留给教师批阅、更新考试状态为"已交卷"。
这整个流程放在一个事务里,任何一个步骤失败,成绩和答案都不会写到一半留下脏数据。SpringBoot里用@Transactional注解解决,一行声明式事务,代码干净得多。
@Transactional(rollbackFor = Exception.class) public SubmitExamResult submitExam(Long studentId, Long examId, List<AnswerItem> answers) { // 1. 校验考试状态 // 2. 遍历答案并记录 // 3. 客观题自动打分 // 4. 更新考试状态 }这是我建议你重点观察的部分:一个"提交考试"动作会触发多张表的写入,如果不用事务,数据库一致性根本没法保证。
3. Vue前端的关键实践:从页面组件到权限路由
3.1 工程结构长什么样
前端工程用的是Vue全家桶,Vue + Vue Router + Pinia(或Vuex)+ Element Plus,外加axios做请求库。如果你打开源码,大概率会看到下面这种目录布局:
- src/api 下面按模块拆分的接口调用文件,比如 exam.js、course.js、user.js
- src/router 下面配置路由表,并且给每个路由标记了meta信息,比如 requiresAuth 和 role
- src/views 下面按页面组织,比如 views/admin/、views/teacher/、views/student/
- src/store 状态管理,保存当前用户信息、菜单权限、页面状态
这个布局的意义在于:页面、路由、接口、状态四者分离,想改哪个维度都不会牵连其他维度。
3.2 路由守卫与动态菜单
前端权限控制主要有两派做法:路由写死、在守卫里判断角色;或者后端返回菜单树、前端动态注册路由。这套系统用的是前者,更简单直接。
具体实现是:前端在路由的meta里标好可访问角色,全局前置守卫里读store里的用户角色,如果角色不在meta允许列表里,就强制跳转到登录页或404页。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { const role = store.state.user.role if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403') } else { next() } } })这样做有个好处:即使有人绕过前端页面直接改路由,也进不了没权限的页面,至少门面挡了一道。当然真正的安全还是靠后端接口校验,前端只是用户体验层面的拦截。
3.3 axios封装与token注入
axios拦截器几乎是每个Vue项目必备的公共逻辑。请求发出前把token注入请求头,拿到响应后统一处理业务码和登录失效。这套系统的封装思路你可以直接抄:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( res => { if (res.data.code === 200) { return res.data } if (res.data.code === 401) { router.push('/login') } return Promise.reject(res.data) }, err => { ElMessage.error(err.response?.data?.message || '网络异常') return Promise.reject(err) } )这段代码虽然短,却是整个前端工程的"血管"。所有请求都从这里过,统一出错提示、统一登录跳转,页面组件里就不需要反复写错误判断了。
3.4 表格、弹窗、表单的组合使用
在管理端页面里,最常用的交互模式就是"表格展示数据 + 弹窗新增/编辑 + 表单校验 + 分页查询"。这套系统几乎每个管理页面都在复用它。以题库管理为例:题库列表用表格展示,查询条件放在顶部,新增题目打开弹窗,弹窗里题型、难度、选项、答案做动态表单。
这种模式之所以普及,是因为它开发效率最高,而且Element Plus正好把这几个组件封装得很成熟,几乎不用写太多自定义样式。你如果基于这套源码二次开发,大概率也是在复制这个模式再做新模块。
4. MySQL库表设计:理清实体关系,别让系统卡在数据层
4.1 核心表一览
数据库设计是整个系统最见功底的部分。我打开源码里的初始化SQL,核心表大概有这些:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 登录账号 | id, username, password, role |
| student_info | 学生信息 | user_id, student_no, name, class_name |
| teacher_info | 教师信息 | user_id, teacher_no, name, title |
| course | 课程 | id, course_name, teacher_id, semester |
| course_selection | 选课关系 | id, student_id, course_id, selected_time |
| chapter | 章节 | id, course_id, title, sort |
| resource | 教学资料 | id, chapter_id, file_name, file_url |
| question | 题库 | id, course_id, type, difficulty, content, answer |
| exam | 考试 | id, course_id, title, start_time, end_time, status |
| exam_paper | 试卷题目关联 | id, exam_id, question_id, score |
| exam_record | 考试记录 | id, exam_id, student_id, status, auto_score |
| score | 成绩表 | id, student_id, course_id, exam_id, total_score |
| notice | 公告 | id, title, content, publish_time, publisher_id |
4.2 表关系怎么理
从上面这张表能看出几组关键关系:
- 用户信息采用"账号表 + 资料表"拆开的模式,sys_user只存登录凭证,student_info和teacher_info存业务资料。这样设计的好处是,如果有系统管理员角色,不需要硬塞进学生或教师表。
- 课程和教师是多对一,课程和学生是多对多,于是拆出course_selection关联表。
- 考试和题目是多对多,拆出exam_paper关联表,还能单独存每道题的分值。
- 成绩数据不应该只存最终分数,还得关联考试记录,方便教师回看答卷详情。
4.3 索引与事务要注意的点
有些细节,不运行一段时间你根本意识不到。比如考试记录表会随着学生交卷快速膨胀,如果没有给exam_id和student_id建立联合索引,查询"某场考试的全部记录"会变成全表扫描,数据量几百条时没感觉,几千条时明显变慢。
再比如成绩的统计SQL用了GROUP BY和聚合函数,这类查询尤其依赖索引。另外,外键字段的类型和长度必须完全一致,否则MySQL建不了索引或关联查询会失败。我检查过这套源码里的SQL脚本,索引和字段一致性做得都还不错,不用改就能直接用。
4.4 字符集选择与编码坑
数据库创建时强烈建议使用utf8mb4字符集,原因很简单:它会存储emoji表情和生僻字,UTF8字符集存不了四个字节的字符。大学教学系统里学生姓名偶尔会出现生僻字,使用utf8mb4能减少很多不必要的报错。
CREATE DATABASE IF NOT EXISTS teaching_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接字符串也要注意加上useUnicode=true&characterEncoding=utf8,否则Java读取数据时可能中文乱码。这种问题排查起来特别费时间,配置出错时页面上显示的全是问号,你说不清是数据库坏了还是后端坏了。
5. 环境准备与一键启动:让"可直接运行"真正落地
标题强调"可直接运行",这句话能不能兑现,关键看两点:环境版本选对没有、配置改对了没有。我按自己的实操顺序把步骤列一遍。
5.1 你需要准备的本地环境
| 组件 | 版本建议 | 备注 |
|---|---|---|
| JDK | 8或11 | 如果后端是SpringBoot 2.x,Java 8完全够用;3.x需要Java 17 |
| MySQL | 5.7或8.0 | 推荐8.0,注意用户权限配置 |
| Node.js | 14以上 | 前端构建依赖npm,版本太低会装不上依赖 |
| IDEA / VS Code | 最新即可 | 前端和后端可以分两个IDE窗口开 |
这套系统大概率基于SpringBoot 2.x,用JDK 8最稳。如果你本机装了JDK 17但报出不兼容,优先检查pom.xml里spring-boot版本和java.version的匹配关系。
5.2 数据库初始化分两步
源码包里的sql文件夹通常有两个文件:一个是schema.sql(建表),一个是data.sql(初始化数据)。你要先创建一个空数据库,再依次执行这两个脚本。
导入命令用Navicat、DataGrip、命令行都行,个人推荐DataGrip,因为可以看表关系执行日志更直观。执行完建议先随便查两张表,确认表已经建好、数据有内容,再进行下一步。这一步别跳过——很多人后端启动报错"Table doesn't exist",就是漏了导入SQL脚本。
5.3 后端启动前必须改的三个地方
打开application.yml或application.properties,最常改的就三处:
spring: datasource: url: jdbc:mysql://localhost:3306/teaching_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己的数据库密码 redis: # 如果项目用到Redis,也要核对地址和密码;如果没用到,直接删除这段配置改完之后直接运行主类,控制台看到"Started Application in x.xxx seconds"就说明后端起来了。后端默认端口一般是8080,如果被占用,去application.yml里改server.port。
5.4 前端启动三步走
前端没有那么多配置,核心是安装依赖。打开终端,进入前端目录:
npm install这一步在网速一般的环境里可能要几分钟,如果中途报错,先换成国内镜像源:
npm config set registry https://registry.npmmirror.com安装成功后启动开发服务器:
npm run dev看到Local: http://localhost:5173或http://localhost:8080打开浏览器访问即可。第一次访问会跳登录页,用初始化数据里的管理员账号登录,就算彻底跑通了。
5.5 前后端联调时的地址配置
前端开发服务器默认端口和后端不一样,所以Vue工程里一般会有个环境配置文件,比如.env.development,里面写明后端接口地址:
VITE_API_BASE_URL=http://localhost:8080/apiaxios的baseURL会读取这个变量。如果你改了后端端口,别忘了同步改这里。这是新手最容易忽略的坑——后端已经启动了,前端却一直报请求失败,检查半天才发现地址没配对。
6. 从头跑一遍的真实过程:那些手册里不会告诉你的坑
6.1 端口占用问题
后端端口8080是重灾区。开发机上有时候会安装一些默认占用8080的服务(比如其他Java进程、装某些数据库时自带的管理端),导致SpringBoot启动直接提示Port already in use。别急着改配置,先查一下是谁占的:
netstat -ano | findstr 8080 # Windows lsof -i:8080 # Mac / Linux找到PID后确认是无关进程再结束,或者干脆把后端端口改成8081,前端环境变量同步改,一分钟搞定。
6.2 登录后接口401
登录成功后跳转主页面,但所有请求全部401——这个现象九成是token没传对。可能的原因有三个:
- axios拦截器没有正确读取localStorage里的token;
- token名前后端约定不一致,前端存的是accessToken,后端找的是Authorization;
- 请求头格式不对,后端要求"令牌字符串",前端却传了"Bearer 令牌字符串"。
解决办法是打开浏览器F12,看具体某个请求的请求头长什么样,再对照后端JwtUtil和拦截器的读取方式,一行一行对齐。这种问题表面上看是代码bug,本质上是前后端没有商量好契约。
6.3 跨域问题(CORS)
前后端分离项目几乎百分之百会遇到跨域。Vue项目跑在5173,后端跑在8080,浏览器会拦截跨域请求。解决方式有前端代理和后端放行两种。前端方式是在vite.config.ts里配置代理:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }后端方式则是在SpringBoot里写一个CORS配置类,放行指定来源。两种方式都能用,实际项目里推荐前者,因为部署上线时Nginx本身就是一个天然的反向代理,前端代理配置可以无缝迁移。
6.4 页面刷新后404
前端开发模式下按F5刷新,经常出现"页面找不到"。这不是系统bug,而是Vue Router默认使用history模式,刷新时前端路由地址直接发给了后端,后端找不到对应的Controller就返回404。解决办法有二:
- 改用hash模式,路由地址变成#/xxx,刷新不会发送给后端,但对SEO不友好;
- 保留history模式,在部署环境配置"所有未匹配路径全部rewrite到index.html"。
开发阶段最省心的是告诉后端同事加一个路径转发,或者临时用hash模式顶着。生产部署用Nginx时,配置throw一个try_files就好:
location / { try_files $uri $uri/ /index.html; }这段配置我几乎在每一个Vue部署项目里都要写一遍,基本属于标配。
6.5 中文乱码
启动后页面上中文全部变成问号或者乱码。这个问题有三个源头:
- 数据库本身不是utf8mb4,建库时选错字符集;
- 后端连接数据库的URL缺少characterEncoding参数;
- Java源码文件本身的编码不是UTF-8。
排查顺序从数据库到连接串再到源码,一步步缩小范围。大多数情况是原因2,把application.yml里的连接URL加上characterEncoding=utf8就解决了。
7. 实用避坑清单:我把最常见的运行错误汇总成了表格
| 现象 | 可能原因 | 排查优先级 |
|---|---|---|
| 后端启动报ClassNotFoundException | JDK版本不匹配,或Maven未正确导入依赖 | 先检查JDK,再执行mvn clean package |
| 前端npm install报ERESOLVE | 依赖版本冲突,或Node版本过新/过旧 | 删除node_modules和package-lock.json重装 |
| 页面能打开但接口全部报网络错误 | 前端代理或baseURL配置不正确 | 看浏览器Network面板请求的最终URL |
| 登录接口提示密码错误 | 初始化数据里用户密码被改过,或加密码方式不一致 | 直接查sys_user表的密文 |
| 某些页面按钮消失且显示403 | 当前账号角色没有该页面的路由权限 | 换管理员账号登录验证 |
| 上传文件失败 | 后端文件存储路径不存在 | 检查配置文件里上传目录,手动创建文件夹 |
| 启动时卡在打印数据库连接失败 | MySQL没启动或密码错误 | 先在本机用客户端软件连一下数据库 |
这些坑没有一个是高级问题,但它们占据了调试工作的大部分时间。如果你打算把这套系统跑起来,我的建议是先别急着改功能,把能跑通作为第一目标,再把权限、数据库、前后端通信这三条链路的代码各读一遍,读懂之后再动手改。
8. 写在最后:这套代码最适合你怎么"榨干"它的价值
如果你只是需要交一个课程设计,跑通之后把系统截图、代码结构、数据库设计说明做成文档,工作量已经足够了。但如果你想让它变成简历上拿得出手的项目,我建议你在它基础上做三件小事:
第一,把成绩模块从"列表展示"升级成"图表分析",比如用ECharts画一个班级成绩分布直方图、各题正确率雷达图。这会让"数据分析"这个关键词出现在你的项目描述里,含金量明显提高。
第二,给文件上传模块加一个本地存储路径的配置项,并实现"上传后预览"功能。很多课设项目里的上传功能都只是个摆设,你把这个补好,实际带学生上课的老师会真的愿意用。
第三,把定时任务引入考试模块。比如设定考试开始后自动开放、结束后自动收卷,而不是靠老师手动改状态。SpringBoot里用@Scheduled注解就能实现,改动非常小。
我们这个行业有个很朴素的规律:能跑通的系统只是半成品,能被人实际使用的系统才算项目。这套系统能拿到的起点已经不错了,但最终它对你的价值,取决于你愿意往里面投入多少思考和修补。
我自己在跑这套代码时最大的体会是:读它的代码比我自己的课设代码要舒服得多,因为它抓到了"教学管理"这个场景真正该关心的几个核心问题——权限、流程、数据一致性。框架本身会过时,但这几个核心问题不会,理解了它们,以后换任何技术栈你都还有底气。