ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue网络教学系统开发:从数据库设计到部署实战

SpringBoot+Vue网络教学系统开发:从数据库设计到部署实战 1. 网络教学系统的需求边界与核心模块划定先说结论这个题目看着常规但每年答辩翻车的人都不少。原因大多不是代码跑不起来而是需求边界划得太散——学生端、教师端、后台管理端三个入口做成一锅粥功能堆了一堆最后连自己都说不清系统到底为用户解决了什么问题。1.1 我当时是怎么梳理功能清单的这个项目叫大学生计算机基础网络教学系统关键词是计算机基础不是计算机全专业课程平台。所以核心场景就三个学生在线看课件、看视频、交作业教师上传课件、布置作业、批作业管理员管账号管课程。其他花里胡哨的功能比如在线考试、直播答疑、论坛社交在毕设周期内完全可以砍掉或做成预留扩展点。我最终落地的功能模块如下模块学生端教师端管理端用户认证登录注册登录登录课程学习查看课程列表、进入章节、观看视频、下载课件维护课程和章节审核课程上下架作业查看作业、在线提交、查看成绩布置作业、批改打分查看整体数据公告查看公告发布公告全局管理个人中心修改密码、查看学习记录管理自己的课程用户管理你可能会问那学习记录和课程上下架要不要做我的建议是看时间和精力。这两个功能其实共享一张表就能实现后面数据库设计里我会写清楚做进去以后答辩时能聊的东西会多很多。1.2 一个容易被忽略的模块公告系统很多毕设项目一开始根本没想到公告。等你做到前后端联调时发现首页空荡荡的没有内容才想起来要填充数据。公告表实现成本极低却能同时出现在学生端首页和教师端后台是一个性价比很高的模块。实际写的时候就是一张表加两个接口前端一个轮播或列表组件搞定。2. 技术选型这套三件套好在哪以及替代方案的取舍这个题目能成为毕业设计常青树很大原因是 SpringBoot Vue MySQL 这套组合对大多数本科生的学习曲线足够平缓同时又能覆盖企业级开发的大部分基础概念。你答辩时可以讲的东西非常多RESTful 接口设计、前后端分离、ORM 映射、鉴权方案、文件存储、部署策略每一个都能展开聊。2.1 关于 Spring Boot 的版本选择别再踩版本坑我看到很多同学的 pom.xml 直接粘贴了 Spring Initializr 上最新的 3.x 版本然后发现项目起不来。这里有一个非常现实的兼容性问题Spring Boot 3.x 强制要求JDK 17很多学校机房和笔记本装的是 JDK 8 或 11Spring Boot 3.x 中 javax.* 包改成了 jakarta.*网上很多老教程和组件示例直接复制会编译报错部分自己写的工具类、第三方 starter 还没适配 3.x。如果你只是做毕设我强烈建议留在 2.7.x。别觉得版本落后毕业设计院的重点是完整度和逻辑清晰。我当时用的是 Spring Boot 2.7.14 JDK 1.8配套 MyBatis Plus 3.5.3跑起来非常稳。等答辩结束、想自己再折腾再上 3.x 不迟。2.2 为什么用 Vue 而不是 JSP这个题必须会答答辩时老师大概率会问为什么不直接用 JSP Spring Boot 一套搞定标准答法是前后端分离能让前端开发和后端开发并行推进Vue 的组件化思想适合复用课程卡片、视频播放器这类 UI 模块同时前后端通过 JSON 交互数据结构清晰后续要接小程序或移动端也不用重写后端。还有一层原因你可以提Vue 生态对视频播放这类场景支持好配合 hls.js 能轻松处理 m3u8 格式的视频流用 JSP 做的话这块要费不少劲。这句话一出来懂行的老师会点头。3. 数据库设计六张核心表如何支撑整个教学链路数据库设计是最先动手也最不能返工的部分。后期改表结构等于改接口、改前端字段牵一发动全身。我最后把表收敛到了六张加上两张辅助表所有功能都能覆盖。3.1 user 表角色字段别用字符串用户的角色我用的是 tinyint 类型0 学生、1 教师、2 管理员。前端的路由守卫和后端的拦截器直接比对数字即可性能好、代码也好写。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 学号/工号, password varchar(255) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0学生 1教师 2管理员, avatar varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码我用了 BCryptPasswordEncoder别用 MD5。这个点答辩时讲出来老师会认为你考虑到了安全问题。3.2 course 表 chapter 表课程与章节的父子关系课程表需要包含封面图、简介、属性和教师 id。教师 id 外联回 user 表。当时我加了一个 status 字段用于上下架默认 1 上架CREATE TABLE course ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, cover varchar(255) DEFAULT NULL, description text, teacher_id int(11) NOT NULL, status tinyint(4) NOT NULL DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;章节表则要存视频地址和课件地址视频地址我直接存相对路径比如/file/video/chapter1.m3u8而不是存完整 URL。这样可以避免服务器地址变了导致老数据全失效。CREATE TABLE chapter ( id int(11) NOT NULL AUTO_INCREMENT, course_id int(11) NOT NULL, title varchar(100) NOT NULL, video_path varchar(255) DEFAULT NULL, courseware_path varchar(255) DEFAULT NULL, sort int(11) DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.3 homework、submission 和 announcement状态值设计作业表的核心字段是 title、content、deadline以及关联的 course_id。提交表 submission 则是整个系统的数据流转枢纽——学生提交后教师批改成绩写在这张表里CREATE TABLE submission ( id int(11) NOT NULL AUTO_INCREMENT, homework_id int(11) NOT NULL, student_id int(11) NOT NULL, content text, file_path varchar(255) DEFAULT NULL, score int(11) DEFAULT NULL, comment varchar(255) DEFAULT NULL, submit_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_homework_student (homework_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我加了一个唯一联合索引 (homework_id, student_id)作用是让同一份作业一个学生只能提交一次。后端接口里再做第二次提交即覆盖的逻辑前端判断一下就行。这个设计细节很加分。至于 announcement 表字段就是 title、content、publisher_id、create_time非常简单但别漏掉。4. 后端落地接口设计、权限控制与文件存储后端部分我不打算贴完整代码那是源代码该干的事。这里讲的是思路和关键写法以及为什么这样写。4.1 项目目录结构按功能分包还是按层分包按层分包controller/service/mapper适合小项目但教学系统功能一多你会发现 controller 下面堆了十几个类维护起来难受。我按功能分包目录长这样com.example.teachingsystem ├── config ├── controller │ ├── admin │ ├── teacher │ └── student ├── entity ├── mapper ├── service │ └── impl ├── common │ ├── Result │ ├── JwtUtil │ └── GlobalExceptionHandler统一返回体 Result 是我自己封装的包含 code、message、data 三个字段。所有接口统一返回它前端 axios 拦截器只处理这个格式联调会顺畅很多。public class ResultT { private Integer code; private String message; private T data; // 静态方法 success() error() ... }4.2 登录鉴权拦截器 JWT别引 SecuritySpring Security 不是不能用但学习成本高、配置繁琐在毕设阶段容易把时间耗在过滤器链和权限表达式上。我自己用 JWT HandlerInterceptor 做了个轻量方案效果足够登录成功后签发 token有效期为 24 小时前端把 token 放在请求头Authorization里拦截器解析 token把用户 id 和角色放入 request attribute再写一个角色校验方法教师接口校验 role 1管理员校验 role 2。这个方案的核心代码其实就是 JwtUtil 工具类加一个拦截器注册public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 注意我的前端会带 Bearer 前缀 // 解析失败直接返回 401 // 解析成功把 userId 塞进 request.setAttribute(userId, ...) return true; } }需要注册到 WebMvcConfigurer 中并设置放行路径比如登录接口、注册接口、静态资源路径。拦截器 注解的方式写起来比 Security 直观答辩时你可以说考虑到项目规模我用轻量级的 JWT 方案替代了完整的安全框架后续如果要引入更全面的权限体系可以平滑迁移——这句话非常稳。4.3 文件上传与视频播放的现实方案视频播放是这种系统里最容易卡壳的地方。我的经验是不要在毕设里做真正的视频转码和流媒体分发。你只需要用MultipartFile.transferTo()把视频存到本地某个目录比如/upload/video/在 Spring Boot 里配置虚拟路径映射让http://ip:8080/file/**能直接访问本地磁盘文件视频格式直接用浏览器能播放的 mp4 格式或者转成 hlsm3u8后用 hls.js 播放。m3u8 播放是网上搜索的热点我也做了一版。做法很简单用 FFmpeg 把 mp4 转成 m3u8 ts 切片放到同一个目录前端引入 hls.js当视频地址以.m3u8结尾时先判断Hls.isSupported()然后交给 hls 实例加载。这个方案的好处是切片文件是流式的可以手动控制码率并且浏览器兼容性比直接播放 mp4 更稳。文件类型校验别只在前端做后端也要做。我当时只校验了扩展名结果有学生把 .exe 改名成 .mp4 传上来了。后来再加了文件头魔数校验比如 mp4 文件开头通常是ftyp。这个安全细节讲讲也是加分项。5. 前端落地Vue 路由、用户界面与 axios 封装Vue 这边我用了 Vue 3 Element Plus。如果你对 Vue 2 更熟用 Vue 2 也没问题但新项目我还是建议 Vue 3毕竟面试和后续学习都用得上。5.1 项目目录和路由设计src ├── api │ ├── course.js │ ├── homework.js │ └── auth.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── student │ ├── teacher │ ├── admin │ └── login.vue ├── components └── utils └── request.js路由设计上我按角色写了三个独立的路由模块student.js、teacher.js、admin.js根路由/动态判断用户角色后重定向到对应首页。这里有个点必须先想清楚路由守卫里不要只判断是否登录还要判断角色和路由 meta.role 是否匹配。不然学生直接改地址栏访问教师后台页面会白屏或报一堆权限错误。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.meta.role to.meta.role ! store.state.user.role) { ElMessage.error(无权访问) next(false) } else { next() } })5.2 axios 封装统一处理 token 和错误码request.js 里我用 axios 实例请求拦截器统一加Authorization: Bearer ${token}响应拦截器统一处理 codeservice.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error Promise.reject(error) )一个值得注意的点是文件流下载的接口不能走这个统一的响应拦截器。当时我下载作业附件时发现文件被存成了 JSON排查半天最后是给下载接口单独发了一个不带拦截器的 axios 实例才解决。你写的时候也可以提前把这种情况考虑进去。5.3 课程卡片、视频播放和分页筛选课程列表页我用 Element Plus 的卡片栅格布局每个卡片展示封面、课程名、教师名点击进入课程详情里面左侧是章节列表右侧是视频播放器。这一步的实现难度不大但要注意视频加载失败时的用户提示——我加了 error 事件监听播放失败就显示一个请下载课件学习占位组件至少不会黑屏僵在那。分页筛选我后端用 MyBatis Plus 的 Page 分页插件前端封装了一个 Pagination 组件统一接收 total、page、size然后触发查询。记住一点条件筛选不要靠前端 filter要拼在后端查询条件里。有的同学把全部课程拉到前端再 filter课程一多页面就卡。6. 前后端联调与打包部署几个常见的坑这部分是全网搜索热词的集中区域。我把自己实际踩过的坑挑几个高频的写出来每一条都能省你半天时间。6.1 Vue 打包放进 Spring Boot 的三种姿势第一种也是我推荐的方式前端执行npm run build生成 dist 目录然后把里面的文件复制到 Spring Boot 项目的src/main/resources/static下重新打包启动。访问同一个端口就能同时打开页面和调接口。第二种直接在前端项目里装 Vite 插件打出 jar 包时自动拷贝 dist 到 target/classes/static省得手动复制。这个适合你想让一键构建更完整的情况。第三种前端的 dist 部署到 Nginx后端单独跑一个 8080 端口Nginx 里配置反向代理/api到后端。这种方式更接近生产环境但毕设答辩现场网络环境不确定我建议还是用第一种最保守。这里有个经典坑Vue 路由用 history 模式时页面刷新会 404。解决方法是改回 hash 模式或者在后端写一个转发规则把非 /api 的请求转发到 index.html。我图省事直接用的是 hash 模式。别觉得 hash 丑它最稳。6.2 MySQL 连接三个高频报错和对应解法网上搜mysql ssl连接错误的人特别多我也遇到了。Spring Boot 连接 MySQL 8 时报错大概是SSL connection error解决JDBC URL 加?useSSLfalseThe server time zone value报错 解决加serverTimezoneAsia/Shanghai驱动类加载不到确认用的com.mysql.cj.jdbc.Driver同时数据库驱动依赖里不要排除mysql-connector-java我当时把完整的 JDBC URL 统一写成了这样jdbc:mysql://localhost:3306/teaching_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue注意allowPublicKeyRetrievaltrueMySQL 8 的 caching_sha2_password 认证模式下没有这个参数会偶发连接失败。这也是一个能帮别人省时间的细节。6.3 版本过高引起的问题Maven 依赖冲突与 Spring Boot 启动失败如果你坚持用了 Spring Boot 3.x大概率会遇到这种问题原来 2.x 下正常的 MyBatis Plus 版本在 3.x 下启动直接报ClassNotFoundError。这是因为高版本 Spring Boot 用了 Jakarta EE而某些 starter 还是旧规范。应对方案有二一是换掉不兼容的依赖二是整体降级。我给的建议是后者。毕设周期就那么长别把时间浪费在让一个老starter适配新框架上。降级到 2.7.x 以后所有问题迎刃而解。再有一个常见问题是Spring Boot 端口被占用启动报Port 8080 was already in use。解决方式很简单启动命令换成mvn spring-boot:run -Dspring-boot.run.arguments--server.port8081或者改配置文件。这个虽然不高级但现场演示时经常救急。7. 最终体会与复盘——如何让答辩组长多看一眼整个项目做完我最大的感触是毕设考察的不是你用了多炫的技术而是你有没有能力把一个模糊的题目拆成可落地的功能并且把它们串起来。这个系统里的每条链路——从建表到后端接口再到前端页面——都有不少边界情况需要考虑你可以把这些作为答辩时主动展示的点。我当时答辩时主动提了三个细节老师们反应都很好提交作业的覆盖策略用唯一索引拦截重复提交同时提供再次提交则覆盖原文件的能力业务上更贴近真实教学场景视频播放方案用 hls.js 播放 m3u8 流解释清楚了为什么用切片流而不是直接播放一个几百 MB 的 mp4 文件权限控制自己封装了拦截器实现 JWT 校验说明白了和直接用 Security 的取舍。另外一个小技巧答辩演示前准备一份干净的测试数据把课程视频时长控制在 30 秒以内避免现场等进度条。这个细节看似小但能让整个演示节奏顺畅非常多。最后再分享一个实际测试中的经验不要只在自己电脑上跑换一台没有开发环境的机器测试前端打包后的 jar 包。这一步能提前发现很多路径写死、依赖缺失、上传文件目录不存在的问题。我第一次打包后在室友电脑上跑就发现上传目录在我代码里写的是绝对路径D:/upload换机器就崩了。后来改成从配置文件读取上传根路径才算真正可移植。你提前做一次就能省去演示现场的手忙脚乱。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进