ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot+Vue线上学习平台毕设全攻略:从架构到排错

Spring Boot+Vue线上学习平台毕设全攻略:从架构到排错

线上学习平台这种选题,在毕业设计里出现频率极高,几乎是每年都有人问。但绝大多数人拿到源码也只是跑通完事,答辩时被问两句就卡壳。这篇博文我尽量把关键环节的为什么、怎么做讲透,从项目骨架到数据库设计,从JWT鉴权到前端路由守卫,再到一套我自己踩过坑才总结出来的排错清单。适合正在准备开题或者马上要答辩的在校学生,也适合想快速搭一套前后端分离项目练手的前端/后端入门开发者。

1. 项目整体设计与需求拆解

1.1 为什么这个选题经久不衰

Spring Boot + Vue的组合在今天依然是中小型项目的经典搭配,尤其在高校毕业设计场景里,几乎是统治级的存在。Spring Boot负责后端接口,内置Tomcat,不用额外部署Servlet容器,起步成本低;Vue负责前端SPA,配合Element UI这类组件库,半天时间就能把后台管理的界面搭出来。

线上学习平台这个业务场景本身就非常适合教学演示。第一,它天然包含最基础的登录、用户、课程、视频、评论这类CRUD操作,这是毕业设计检查的核心——增删改查做得完整清晰,答辩基本稳了。第二,它有一个相对完整的业务流:学生选课->在线学习->记录进度->完成测验->查看成绩,这个流程串起来之后,系统的雏形就具备了,比单纯做一个图书管理系统或者学生管理系统有说服力得多。第三,如果做得精细一点,还能加入权限控制、文件上传、数据统计这类功能,作为加分项展示。

所以这类项目不是靠花哨的架构取胜,而是靠“完整、规范、可演示”打动评委。对你来说,最大的价值在于可以基于一份成熟的源码,快速理解前后端分离项目的常用套路,然后根据自己的想法改造,既降低了毕设工作量,又能真正学到东西。

1.2 标准功能模块拆解

一套合格的线上学习平台,至少要拆成两个端:前台学习端和后台管理端。前台面向学生,后台面向管理员和教师。参考众多同类项目的通用设计,我习惯把功能拆成如下几个模块:

  • 用户侧:用户注册、登录、个人信息维护。这一步最好接入JWT做无状态鉴权,这也是答辩时的高频提问点。
  • 课程模块:课程分类管理、课程列表、课程详情、课程搜索。后台负责发布课程,前台负责展示和查看。
  • 视频学习模块:视频列表、视频播放、学习进度记录。这一块是这个项目的业务核心,能不能演示出“继续学习”的效果就看这里。
  • 测试模块:题目管理、测验发布、答题提交、分数计算。毕设里不用做得太复杂,单选+判断就足够撑起业务闭环。
  • 公告模块:发布公告、展示公告,属于典型的占位模块,但是有比没有好,能体现系统的完整性。
  • 后台管理:用户管理、课程审核、数据统计(用户数、课程数、学习次数),用ECharts画两个图表,视觉效果很好。

这个功能清单对应到数据库里,核心表大概就是:用户表、课程表、课程分类表、视频表(或者章节表)、学习记录表、题目表、测验表、成绩表、公告表。如果源码里额外带了评论表,也算锦上添花。

1.3 项目目录结构怎么读

拿到源码后,第一步不是急着运行,而是先快速浏览目录结构,建立整体认知。后端典型结构是:

src/main/java ├── com.xxx.education │ ├── controller # 接口层 │ ├── service # 业务层 │ ├── mapper # 数据访问层 │ ├── entity # 实体类 │ ├── config # 配置类(跨域、拦截器等) │ ├── common # 通用返回结果、异常处理 │ └── utils # 工具类(JWT、文件上传等)

前端典型结构是:

src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # Vuex状态管理 ├── views # 页面视图 │ ├── admin # 后台管理页面 │ └── student # 前台学习页面 └── utils # 请求拦截、token处理等工具

看懂这个结构后,再去看具体代码,你就能很快定位到某个功能对应的文件在哪。比如改登录接口,先找controller里的AuthController,再找service里的UserServiceImpl,最后改mapper里的SQL,一条线走到底。

2. 技术选型与核心实现思路

2.1 为什么是Spring Boot 2.x + MyBatis Plus

Spring Boot在这类项目里的地位不必多说,自动配置、依赖管理、内嵌容器,对毕设选手非常友好。但组件选型上有两个容易纠结的地方:持久层框架用JPA还是MyBatis Plus,鉴权用Session还是JWT。

我的建议是:MyBatis Plus。原因很实际——代码量少。单表CRUD基本不用写XML,继承BaseMapper就能用现成的selectPage、selectById、deleteById。对于课程查询、分页列表这种最常见的接口,写起来效率极高。答辩时被问到“怎么实现分页”,回答“MyBatis Plus的物理分页插件,通过拦截器自动拼接LIMIT语句”,这句话一出口就能显示你是认真研究过的。

JWT和Session的选择,属于典型的“新项目用新方案”场景。传统Session依赖服务器端存储,Tomcat下没问题,但分布式场景下要考虑Session共享,对单体毕设来说倒不是痛点。JWT的优势在于无状态,服务端不保存会话,客户端每次请求把Token带在Authorization头里就行。实际上毕设项目里,JWT能让前后端分离更彻底,前端拿Token存LocalStorage,后端用拦截器统一验证,逻辑清晰,答辩也好讲。

2.2 数据库表设计说明

数据库设计对毕设来说专项权重很高,但不需要太复杂。以下是一套比较通用的表设计参考,涵盖了主要业务:

  • user(用户表):id、username、password(BCrypt加密存储)、nickname、avatar、role(1=学生,2=教师,3=管理员)、status、create_time
  • course(课程表):id、title、cover、category_id、teacher_id、brief、status(0=未发布,1=已发布)、create_time
  • course_category(课程分类表):id、name
  • video(视频表,或chapter表):id、course_id、title、url、duration、sort
  • learn_record(学习记录表):id、user_id、video_id、progress、learn_time、update_time
  • question(题目表):id、quiz_id、content、option_a、option_b、option_c、option_d、answer、type
  • quiz(测验表):id、title、course_id、questions、create_time
  • exam_record(测验成绩表):id、user_id、quiz_id、score、create_time
  • announcement(公告表):id、title、content、create_time

这里我特别推荐大家重视user表的password字段。明文存密码在毕设项目里偶尔能看到,但如果答辩老师翻了源码,这种情况会显得很不专业。用Spring Security的BCryptPasswordEncoder或者hutool的BCrypt工具类,存哈希值,登录时校验,代码量只多两行,观感完全不一样。

2.3 后端核心接口设计

后端接口遵循RESTful风格写就好,路径用名词复数表示资源,方法用HTTP动词表达操作。一个典型的Controller写法如下:

@RestController @RequestMapping("/api/course") public class CourseController { @Resource private CourseService courseService; @GetMapping("/page") public Result getCoursePage(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.success(courseService.getCoursePage(pageNum, pageSize, keyword)); } @GetMapping("/{id}") public Result getCourseDetail(@PathVariable Long id) { return Result.success(courseService.getCourseDetail(id)); } @PostMapping("/save") public Result saveCourse(@RequestBody Course entity) { return Result.success(courseService.saveCourse(entity)); } }

注意几个细节:

  • 统一返回Result对象,也就是一个通用JSON结构,包含code、message、data三个字段。前端拿到后根据code判断业务是否成功,而不是根据HTTP状态码。这样处理的好处是业务异常和HTTP异常分离,前端逻辑统一。
  • login接口写在AuthController里,路径类似/api/auth/login,参数接收DTO对象而不是Map。用DTO能避免前端传多余字段时造成混乱,属性校验也能用@NotBlank这类注解直接完成。
  • 所有需要登录才能访问的接口,不要每个方法都手动判断,而是在SpringMVC的拦截器里统一处理。拦截器从request头里取token,解析失败或不存在就返回401,配合跨域配置一起使用,这也是一个专门的加分点。拦截器代码很简单,核心是注册到WebMvcConfigurer里,并做好放行配置,比如/api/auth/login和/api/course/page这类公开接口不拦。

2.4 前端模块与路由设计

前端的基本盘是Vue Router + Vuex。路由分两个区域,一块是前台页面,一块是后台管理。学生端页面包括首页、课程列表、课程详情、视频学习、我的测试、个人中心。管理端包括课程管理、用户管理、测验管理、公告管理、数据统计。

路由守卫是必须好好写的地方。都说Vue的鉴权核心是靠路由守卫而不是后端,这话不完全对,但从用户体验角度说,前端确实应该先做一层拦截,让未登录用户跳转到登录页。典型写法:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

每个需要登录的页面在meta里声明requiresAuth即可。这里我踩过一个坑:一开始只在需要权限的页面加了meta,但忘记处理“已登录用户访问登录页”的情况,结果用户登录后再访问/login还能看到登录表单,体验很怪。后来加了判断,如果token已存在就强制跳首页。

axios请求封装也一样,所有接口请求统一走一个封装函数,在请求拦截器里把token放进Header,在响应拦截器里全局处理业务code:如果code是401说明token过期或无效,直接清除本地token,然后跳到登录页。这套逻辑写一次,后面所有接口自动生效,比每个页面单独判断优雅得多,也省去大量重复代码。

3. 实操过程:从源码到本地跑通

3.1 环境准备与工具清单

开工之前,先把环境对齐。不同版本的Node、JDK、MySQL都可能导致项目启动失败,建议按以下版本准备:

  • JDK 1.8,绝大多数Spring Boot 2.x项目都基于这个版本,不要轻易上JDK 11或17,虽然多数情况下能兼容,但个别依赖的老版本cglib、javassist可能出问题。
  • Node 14或16,前端项目如果基于Vue2和webpack4,Node版本过高会出现OpenSSL错误,这是经典问题。
  • MySQL 5.7或8.0,注意8.0以上需要把驱动改成com.mysql.cj.jdbc.Driver,并且连接串里要加serverTimezone参数。
  • Redis非必需,如果源码里带了验证码、Token黑名单这类功能才需要启动它,普通的小项目一般不需要。

工具方面推荐:IDEA(后端)、VS Code或WebStorm(前端)、Navicat(数据库可视化管理)、Postman(接口测试)。这些没有硬性要求,用你自己顺手的就行。

3.2 导入数据库与后端配置

拿到源码后,第一件事是在项目根目录或doc目录下找.sql文件。这个文件就是整个数据库的初始化脚本,包含了建库、建表、初始化数据。如果没有sql文件,只有application.yml,那就需要自己到application.yml里看spring.datasource配置,找到数据库名,手动创建空库,然后用同步表结构的工具生成表。

导入SQL的操作用Navicat非常简单:新建连接 -> 新建数据库(注意字符集选utf8mb4) -> 右键选择“运行SQL文件” -> 选择目标sql文件 -> 执行。执行完成后查看表列表,确认表都建好了。

然后打开后端项目,重点检查application.yml里的配置项,包括端口(默认8080)、数据库账号密码、日志级别、文件上传路径等。这里有一个高频坑:他人项目的数据库密码肯定和你本地不同,开跑之前必须先改成自己的密码。我第一次开发类似项目时就没注意,直接点击run,结果控制台报Communications link failure,愣是查了半小时才想起密码早改了。

3.3 启动后端服务和前端页面

后端启动步骤比较机械,但有几个小细节值得留意:

  • 如果IDEA自动下载Maven依赖速度很慢,可以去配置里把Maven镜像源改成国内仓库,这一条能省下一个小时。
  • 启动类如Application.java必须放在某个包路径下,不要把它直接放在java目录根下,否则扫不到Mapper和Service组件。
  • 启动成功后看控制台是否打印“Started Application in x.xxx seconds”以及Tomcat on port 8080的日志。

前端启动步骤是:

npm install # 安装依赖,确保网络稳定 npm run serve # 启动开发服务器

npm install阶段如果报错,优先检查node_modules是否存在缓存问题,可以先删除再重装。踩过的一个典型坑是项目里package.json锁了旧的依赖版本,需要先执行npm install --legacy-peer-deps才能过。Vue2项目常见这个问题,Vue3相对少一些。

前端启动后浏览器访问本地开发服务器地址(一般是localhost:8080或8081),如果端口被占用就换个端口,或者把后端端口改成8082。后端端口一旦改了,要记得同步修改前端api目录下请求baseURL里的端口,否则所有请求都会发送错误。

3.4 接口联调与登录验证

前后端都启动后,先用已有的管理员账号(通常是admin/admin123之类)登录。如果登录成功并跳转后台,说明后端接口通、前端路由通、JWT生成和验证逻辑都没问题。

再用学生账号测试课程列表和视频学习。如果登录正常但列表加载不出来,打开浏览器开发者工具(F12)的Network面板,看具体请求报什么错:

  • 404说明路径不对,检查controller里的@RequestMapping和前端api文件里的url是否一致。
  • 500说明后端抛异常了,切到IDEA控制台看完整堆栈,最常见的是数据库字段名匹配问题。
  • CORS错误说明跨域没配置好,重点检查后端WebMvcConfigurer里的allowedOriginPatterns配置,以及前端是否用了代理。

经验之谈:联调阶段不要凭感觉猜测,先看F12里Preflight请求(OPTIONS)是否正常。跨域配置好的项目,Preflight请求应返回200;如果连这个都过不了,大概率是拦截器把OPTIONS请求也拦截了。

4. 常见问题与排查技巧实录

4.1 前端依赖安装大坑:Node版本与OpenSSL

这是我在实际使用中碰到最多的问题,没有之一。前端项目基于webpack4 + Vue2时,用Node 17以上版本执行npm run serve,大概率会报错:error:0308010C:digital envelope routines::unsupported。

原因很简单:Node 17的默认OpenSSLProvider从OpenSSL 1.1升级到了OpenSSL 3.0,而webpack4还在用原始的md4哈希算法,运行时不认。

解决办法有三种:

  • 更换Node版本。用nvm安装并使用Node 14或16,这是最稳妥的方案。
  • 不换Node,在启动命令前加NODE_OPTIONS=--openssl-legacy-provider参数(macOS/Linux)或cross-env NODE_OPTIONS=--openssl-legacy-provider(Windows)。
  • 把webpack4升级到webpack5,但涉及一堆插件兼容性改动,代价比较大,毕设阶段不建议。

顺便说一句,很多老项目遇到这种版本问题,第一反应是重装依赖或者删node_modules,但真正的.rootcaus往往在于运行环境,不在依赖本身。排查问题一定要先从环境入手。

4.2 后端启动失败排查顺序

后端启动报错时,不要一上来就看代码,按顺序排查:

第一步看数据库状态。MySQL服务是否启动、端口3306是否被占用、账号密码对不对、权限够不够(远程连接是否被拒)。这一步能过滤掉一半的问题。

第二步看端口冲突。8080端口如果被其他进程占用,报错会是Port 8080 was already in use。解决办法是换端口,或者在IDEA配置里改VM options里的server.port,最直接的是改application.yml。

第三步看依赖是否完整。Maven本地仓库里是否真的下载了全部jar包,某些私服依赖如果没有asnapshot版本会报Could not find artifact。此时需要检查idEstoE配置里的镜像源。

最后才是看代码错误。大部分代码层面报错都会给出清晰的堆栈行号,定位到具体文件就能解决,Spring Boot的错误信息已经比早期的框架友好太多了。

4.3 跨域问题标准解法

前后端分离项目遇到跨域,几乎是必经的一课。后端解决方法是写一个WebMvcConfigurer配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意几个细节:

  • allowedOrigins在Spring Boot 2.4以上的版本中推荐改用allowedOriginPatterns,因为前者对allowCredentials有兼容性限制。
  • 如果配置了拦截器,不要拦截OPTIONS请求。跨域预检请求走OPTIONS方法,如果被拦截器拦了,整个跨域流程直接断掉。常见的表现是F12里看到Preflight请求返回401或404,而实际接口却什么都没发出去。
  • 如果用Spring Security,还需要在SecurityConfig里放行OPTIONS请求的匿名访问,不然双重拦截很折磨人。

4.4 文件上传路径的坑

视频封面、用户头像这类上传功能在毕设里很常见。这里有一个经典坑:开发时文件上传到本地路径没问题,但演示时项目换了个电脑,路径失效,图片全部加载不出来。

后端在保存上传文件时,不要硬编码绝对路径,比如D:/upload/这类,一旦换环境路径就崩。更好的做法是配置一个相对路径,比如在application.yml里定义:

upload: path: ./upload/

然后写一个静态资源映射配置,把/upload/**映射到本地磁盘路径。这样源码拿到任何电脑上都能直接用,也比较贴合真实项目的部署习惯。之前有同学直接把文件Base64存数据库,虽然兼容性好,但数据表会迅速膨胀,答辩时也不好解释,不太推荐。

5. 答辩准备与二次开发建议

5.1 前端路由守卫与Token刷新

上面代码里我提到路由守卫的基本用法,但这里再补充一个更具体的问题:Token过期后怎么处理。很多毕设项目里最简单粗暴的做法是登录后Token存LocalStorage,过期了用户就再登录一次,没有自动刷新机制。这个做法本身没有错,但答辩被问到“Token失效怎么办”就会卡壳。

可以在项目里做一个简单的Token过期自动跳转:

// 响应拦截器 instance.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') this.$router.push('/login') this.$message.error('登录状态已过期,请重新登录') return Promise.reject(new Error('401')) } return res }, error => { return Promise.reject(error) } )

这段代码的价值不在于实现多复杂的效果,而在于展示出你对“会话状态”这件事有思考。答辩老师听到你主动讲“过期自动清理token并跳转登录页”,加分效果比回答“调用后端接口前手动检查token是否存在”要好得多。

5.2 数据统计模块的图表实现

在后台管理首页做一个数据统计看板,是性价比极高的加分项。用ECharts展示用户注册趋势、课程分类分布、学习次数Top5课程,占代码量不大,但视觉效果很能打。

接口设计上,可以新增一个统计Controller,用SQL的GROUP BY聚合数据,示例:

@GetMapping("/statistics/user-register-trend") public Result userRegisterTrend(@RequestParam Integer days) { return Result.success(statisticsService.getUserRegisterTrend(days)); }

对应的SQL可以写成:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS count FROM user WHERE create_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY) GROUP BY day

前端用ECharts的折线图和饼图渲染两个div,模板网上到处都是,但展示效果确实能让整个系统看起来完整许多。如果时间充裕,这个模块值得加。

5.3 如何准备毕业答辩的演示流程

答辩演示建议按下面这条线来走,流程完整且逻辑自洽:

  1. 先展示首页,介绍系统定位和面向用户。
  2. 注册一个新学生账号,走一遍注册流程。
  3. 学生登录,选课,播放一个视频,展示学习进度更新,这个环节重点讲“继续学习”的业务闭环。
  4. 进入测验模块,做一次测验,提交后展示成绩。
  5. 切到后台admin账号,展示用户管理、课程管理、数据统计看板。
  6. 最后展示核心代码,挑三个地方讲:JWT登录鉴权、学习进度记录、数据统计聚合查询。

答辩前必须自己完整走至少三遍演示,尤其要避免这几种翻车:打开页面时白屏(多为前端依赖或路由问题)、点播放按钮时视频不加载(多为上传路径失效)、查询列表时接口报500(多为数据库连接失败)。出现一次就会影响整体印象,宁可提前半小时进考场模拟一遍,也不要裸奔上台。

5.4 二次扩展的三个方向

如果做完基础功能还有富余时间,可以从下面三个方向里选一个做扩展:

  • 评论与评分功能。给课程增加评价机制,涉及一对多关联查询,能体现关联表设计能力。
  • 在线笔记功能。学生在看视频时记录学习笔记,这属于典型的多表业务关联,涉及用户、课程、视频、笔记四张表。
  • 权限精细化。引入Spring Security替代手写拦截器,做到按钮级权限控制,这个方向适合对框架有一定吃透想上难度的同学。

这三个方向都不会破坏原有项目结构,只是在现有模块上增量开发,适合打磨加分。

最后再分享一个小技巧

整个项目跑通之后,建议大家做的第一件事不是继续加功能,而是把项目的接口清单梳理成一份接口文档。项目级别不需要用Swagger自动生成,也不用引入过多依赖,直接在项目根目录写一个markdown,列出每个接口的路径、请求方式、参数、返回结果。这份文档不仅方便你自己调试前端,答辩时老师追问某个业务是怎么串起来的,你翻开文档就能准确回答哪个Controller哪个方法对应哪个功能,逻辑清晰,印象分会好很多。线上学习平台这个项目谈不上什么高深技术,但胜在业务链路完整、层次清晰,把这套东西吃透并讲清楚,其实就达到了毕业设计训练的初衷。

RELATED READING

延伸阅读

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