简介:基于Spring Boot与Vue的前后端分离在线教育平台毕业设计项目,主要面向计算机相关专业正在准备毕设的学生,也适合需要项目实战练习的开发者,可用于课程设计或期末大作业。平台围绕教师、学生的在线学习与课程管理场景,前端采用Vue.js构建交互界面,后端基于Spring Boot提供稳定API,MySQL存储用户、课程、成绩等核心数据。资源包共194个文件,其中165个Java源码、17个XML配置或MyBatis映射文件,同时包含SQL数据库脚本、YAML配置文件、PNG界面截图及Markdown辅助文档,压缩包约1.67MB。包内提供系统设计文档、数据库设计文档、接口文档和部署指南,并覆盖课程管理、教师管理、会员管理、权限管理、统计分析等核心模块,数据库脚本包含表结构与基础数据,初始化即可运行,代码分层清晰,便于从环境搭建到答辩说明完整走通流程。目前已有66人学习下载,适合需要参考完整毕设源码、理解前后端分离架构或在此基础上扩展功能的学习者。
1. 这个毕业设计题目在做什么:一条能从建表讲到上线的完整链路
基于 SpringBoot 和 Vue 前后端分离的在线教育平台,是毕业设计选题里被做烂、但很少被讲透的一类项目。标题里虽然带着 .zip,真正要交付的东西其实是一条完整链路:SpringBoot 负责接口,Vue 负责页面,MySQL 负责数据,前后端通过 HTTP 协议对话,最后打包部署成可访问的系统。源码从来不是重点,把它重新跑通、讲清楚、改出一点自己的东西,才是拿高分的关键。
这个项目适合三类人:需要完成本科毕业论文的学生,选了类似题、想绕开无脑复制粘贴的课程设计者,以及正在转行学习、想低成本搞懂一套主流全栈项目如何组织的开发者。一套常见在线教育平台大致包含学员端看课、教师端上传课程、管理端做审核与统计,再加上登录注册、选课下单、学习记录这些支撑模块。下面不按“先解压再照着抄”的顺序来,而是按“能通过答辩、能继续扩展、能现场演示”的目标,把这个题目拆开讲。
2. 环境准备与项目骨架:先把版本与目录定死,再谈功能
2.1 选型理由:为什么这套组合值得写进毕业设计
SpringBoot 加 Vue 是目前前后端分离项目里最“稳”的组合。SpringBoot 的自动配置负责把 Tomcat、数据源、JSON 序列化这些底层细节替你安排好,你要做的只是写 Controller、Service、Mapper,这对课程设计和毕业论文来说成本很低。Vue 的组件化开发配合脚手架,能在短时间内把页面拆成可维护的组件,尤其是课程列表、播放页、后台管理这种结构重复的界面,组件化之后不需要每个页面都复制一遍。
但选型也要看清边界。前后端分离带来两个隐藏成本:第一是跨域,本地联调时前端跑 8080、后端跑 8081,浏览器会把两边当成不同源,不处理就调不通接口;第二是部署,前端打包成静态资源,后端打包成 jar,二者要么交给 Nginx 转发,要么把前端产物并进后端一起启动,二者只能选一种,不能两头都做。这两点会在后面反复出现,也是答辩时老师最爱追问的地方。
版本这里要先泼一盆冷水:SpringBoot 不是越新越好。“springboot 版本太高”是这两年最常见的翻车现场,SpringBoot 3.x 把 javax 包名换成了 jakarta,很多网上的现成代码和毕业设计模板都基于 2.7 写的,你拿 3.2 创建工程后,一整片 import 全报红。我的建议是:如果项目文档明确写了 2.7.x,就老老实实用 SpringBoot 2.7.18,配 JDK 8,整个生态都不会和你作对。
| 端 | 技术选型 | 选它而不是另一个的理由 |
|---|---|---|
| 后端 | SpringBoot 2.7.x + MyBatis-Plus | 自动配置省心,MyBatis-Plus 的 LambdaQueryWrapper 写条件查询比手写 XML 快 |
| 前端 | Vue 3 + Vite 或 Vue CLI | 组件化清晰,生态成熟,遇到问题搜得到答案 |
| 数据库 | MySQL 5.7 或 8.0 | 学校机房和云服务器都装过,字符集改成 utf8mb4 后中文没有兼容问题 |
| 鉴权 | JWT | 前后端分离下无需在服务端维护 Session,接口天然无状态,便于扩展到微服务 |
后端选 2.7 还有一个原因:教学资料和答辩评委的认知基本都停在这个版本段。你写“使用 SpringBoot 2.7 完成接口开发”,评委不会有任何疑问;你写“使用 SpringBoot 3.2”,如果不解释清楚 jakarta 迁移,反而容易被追问到答不上来。
2.2 环境清单与版本约定:少了这些你连工程都建不起来
环境不一致是毕业设计项目最大的隐形炸弹。很多同学拿到别人的源码后第一个动作就是解压、导入、运行,然后看着一大堆报错发呆,却没想到问题可能根本不在代码里。我一般会先花半小时把工具链检查一遍,再决定是否打开工程文件。
最低要求是这样一套:JDK 1.8 配 SpringBoot 2.7.x,Maven 3.6 以上,MySQL 5.7 或 8.0,Node 16 或 18。如果你用的是 Vue CLI 创建的前端工程,Node 版本别超过 18,新版 Node 配合旧版本 node-sass 会出现编译失败,这个问题在 Windows 上尤其明显。IDEA 方面社区版足够用,后端导入 Maven 工程,前端直接打开文件夹。
| 组件 | 建议版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 与 SpringBoot 2.7.x 搭配最稳,3.x 才强制要求 17 |
| Maven | 3.6.3 以上 | 依赖下载失败时先检查镜像源 |
| MySQL | 5.7 / 8.0 | 8.0 的驱动要加时区参数 |
| Node | 16 或 18 | 避免与 node-sass 冲突 |
| IDEA | Community 即可 | 社区版支持 Maven 与 Spring 工程 |
检查命令可以放在终端里一次性跑完:
java -version mvn -v node -v npm -v mysql --version这里每个命令都有用:java -version 看主版本号,Maven 的 -v 会显示它是否依赖了错误的 JDK,node 和 npm 的版本必须匹配,mysql 的版本决定了 JDBC 驱动参数怎么配。执行完不要只看有版本号输出就放心,要认真核对 Java 主版本是不是 1.8。很多机器装了多个 JDK,命令行输出的是 17,但项目管理器里还是旧版本,启动时就会出现各种诡异报错。这种问题在论文里写不出来,却可能浪费你两三天。
2.3 初始化骨架:前端后端分开建目录,别把代码堆在一个文件夹里
拿到任何一份标题带“源码+数据库+文档说明”的项目,先看目录结构,这比看代码更能判断质量。一份组织合理的在线教育工程,通常长这样:
online-education/ ├── backend/ # SpringBoot 后端工程 ├── frontend/ # Vue 前端工程 ├── database/ # 建表 SQL 脚本 └── doc/ # 设计文档、论文素材、接口说明backend 和 frontend 必须分开,这是前后端分离的根本约定。有一类项目喜欢在前端的 public 目录下塞后端代码,或者在后端 resources 里堆 Vue 组件,这种工程往往是从单体项目硬改过来的,结构混乱,推荐直接放弃。database 目录放初始化脚本,doc 目录放文档说明,这两个文件夹的价值在于,答辩时你可以直接说“这是数据库设计脚本,这是接口说明”,比现场翻工程找佐证更有说服力。
创建骨架的常规做法是后端用 IDEA 的 Spring Initializr,前端用 Vue 脚手架:
# 后端工程关键依赖:web、mysql、mybatis-plus # 前端工程创建 npm create vite@latest frontend -- --template vue cd frontend npm install参数解释一下:Vite 创建工程时,--template vue 表示生成 Vue 3 的基础模板,比 Vue CLI 的交互式创建更适合新手快速上手。npm install 会根据 package.json 安装依赖,这一步耗时较长,如果中途失败,优先检查 Node 版本而不是反复重装。后端工程我建议包名用 com.example.education 这类带业务含义的命名,不要直接叫 demo,因为后面写论文时包结构是要截图放进文档里的。
骨架建立后先做一件事:初始化 Git 仓库并提交一次初始代码,这样后面无论改坏了什么,都有后悔药可吃。提交信息写成 init project 就够了,不要在这个阶段写任何业务代码。
3. 后端落地:从建表到登录鉴权,把核心功能写到能答辩
3.1 数据库设计:表之间的关系要能在论文里画成 ER 图
在线教育平台的核心表不需要很多,四张表足够撑起主流程:用户表、课程表、课程章节表、选课记录表。设计原则是“核心字段齐全、关系清晰、不过度设计”,表越多论文越难画清楚,表太少又撑不起业务闭环。下面是一份可以直接拿来改的建表脚本:
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色 0学员 1教师 2管理员', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0未删 1已删', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `course` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `teacher_id` BIGINT NOT NULL COMMENT '讲师ID,关联user.id', `title` VARCHAR(100) NOT NULL COMMENT '课程标题', `cover` VARCHAR(255) DEFAULT NULL COMMENT '封面图', `intro` TEXT COMMENT '课程简介', `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0未上架 1已上架', `deleted` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_title` (`title`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; CREATE TABLE `course_section` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `course_id` BIGINT NOT NULL COMMENT '所属课程ID', `section_name` VARCHAR(200) NOT NULL COMMENT '章节名', `video_url` VARCHAR(255) DEFAULT NULL COMMENT '视频地址', `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程章节表'; CREATE TABLE `user_course` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '学员ID', `course_id` BIGINT NOT NULL COMMENT '课程ID', `pay_amount` DECIMAL(10,2) NOT NULL COMMENT '实付金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0已选课 1已退款', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_course` (`user_id`, `course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';表设计里有三个容易被忽略的决策。第一个是逻辑删除,deleted字段是让数据保留而非物理删除,毕业设计里很有必要,因为你要在论文里写“数据可追溯”,物理删除没法证明。第二个是金额用DECIMAL(10,2),不要用 FLOAT,浮点数的精度问题会在打印结算单时暴露。第三个是联合索引idx_user_course,它的顺序是有讲究的,查询条件通常是“某个学员买了哪些课”,所以 user_id 放在前面。
物理外键我建议不建。课程表的 teacher_id 和选课表的 course_id 在逻辑上关联,但不需要真的加FOREIGN KEY约束,理由有两点:一是删除课程时物理外键会挡住操作,你得先删关联记录;二是答辩时讲“逻辑外键”比讲“物理外键”更符合企业真实做法。ER 图里把关系画出来,数据库层面用索引保证查询效率,这样论文和代码都不虚。
3.2 后端包结构:Controller、Service、Mapper 的边界先划清楚
工程结构决定了一行代码写在哪里。我见过不少毕业设计把业务逻辑直接写在 Controller 里,接口一多就变成一个几千行的类,答辩老师问“这段逻辑为什么放在这里”,你答不上来。规范的的做法是 Controller 只做参数接收和结果返回,Service 写业务规则,Mapper 负责数据库操作。下面是一个典型的包结构:
com.example.education ├── config/ # 跨域、拦截器、MyBatis-Plus 分页配置 ├── controller/ # 接收前端请求 ├── service/ # 业务逻辑接口与实现 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 数据库表对应的实体类 ├── dto/ # 接口入参与返回对象 ├── common/ # 统一结果封装、异常处理 └── EducationApplication.java以课程列表接口为例,Controller 只做三件事:收参数、调 Service、包装返回结果。代码长这样:
@RestController @RequestMapping("/api/course") public class CourseController { @Resource private CourseService courseService; @GetMapping("/list") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.ok(courseService.pageCourses(pageNum, pageSize, keyword)); } @GetMapping("/{id}") public Result detail(@PathVariable Long id) { return Result.ok(courseService.getCourseDetail(id)); } }这里的defaultValue参数很关键,前端如果不传 pageNum 和 pageSize,接口也能按默认值跑,避免空指针。Result.ok()是统一返回结构,里面包含 code、message、data 三个字段,前端拦截器只需要判断 code 就能知道这次请求成功与否,不用每个接口单独处理异常。Service 层负责真正的查询逻辑,Controller 不做任何数据库操作,这样职责边界清晰,测试也好写。
3.3 登录与权限:JWT 令牌为什么比 Session 更适合前后端分离
Session 方案在前后端分离下有两个固有缺陷:一是 Session 存在服务器内存里,前端请求如果打到另一台实例,Session 就不存在了,系统没法水平扩展;二是前端需要维护 Cookie,跨域时还要处理凭证传递。JWT 则完全不同,令牌本身携带用户信息,后端每次请求只需要验签,不查询任何会话状态。这段原理写进论文里非常加分,要让评委知道你不只是会用框架,而是知道为什么这样选。
生成 JWT 的工具类在 Java 生态里常用 jjwt:
public class JwtUtil { private static final String SECRET = "your-secret-key-please-change-at-least-32-bytes"; private static final long EXPIRE = 7L * 24 * 3600 * 1000; public static String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }两个参数必须说明白。SECRET 是签名密钥,HS256 算法要求密钥至少 32 字节,这是硬性要求,写短了启动时报错或者生成的令牌无法通过验签;EXPIRE 是过期时间,我一般设 7 天,太短会导致学员看课时频繁登录,太长又会让令牌泄露后的风险窗口变大。claim("role", role)把角色写进令牌,后面拦截器可以直接读这个字段做权限判断,不必每次查库。
有了令牌,再配一个拦截器:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { try { Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.getSubject()); return true; } catch (Exception e) { // 令牌无效,走下方 401 返回 } } response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } }拦截器里有三个细节值得注意。Bearer前缀是行业习惯,客户端拿到令牌后拼上这个前缀再放进 Header,拦截器解析时先去掉它;request.setAttribute把 userId 放进请求上下文,Controller 里可以直接取,避免每个接口都重复解析令牌;最后返回 401 时一定要手动设置 Content-Type 为 JSON,否则前端拿到的是一段 HTML 错误页,拦截器会报解析失败。
3.4 MyBatis-Plus 分页与条件查询:别把查询条件拼在 SQL 字符串里
MyBatis-Plus 的价值在于把单表 CRUD 从手写 SQL 中解放出来。分页查询是后台管理系统出现频率最高的功能,先配置分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }PaginationInnerInterceptor(DbType.MYSQL)告诉插件当前数据库是 MySQL,分页 SQL 会生成 LIMIT 语句。不配这个插件时,Page 对象不会生效,查询结果会返回全部数据,这是最常见的“明明分了页却没分页”的原因。条件查询推荐用 LambdaQueryWrapper,配合 lambda 引用可以避免字段名拼错:
@Override public Page<Course> pageCourses(Integer pageNum, Integer pageSize, String keyword) { LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Course::getStatus, 1) .like(StringUtils.hasText(keyword), Course::getTitle, keyword) .orderByDesc(Course::getCreateTime); return courseMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }wrapper.eq(Course::getStatus, 1)表示只查已上架的课程,like方法第一个参数是布尔值,只有当 keyword 不为空时才拼接这个条件,这样接口里不传关键词也不会报错。orderByDesc按创建时间倒序排列,让新课程排在最前面。这里要特别提醒:selectPage 会把实体所有字段都查出来,包括 intro 这种大文本字段,列表页并不需要。如果课程表数据量大,建议在实体上给 intro 加@TableField(select = false),详情接口再单独查全字段。
4. 前端工程与接口联调:路由守卫、axios 封装和两个教育业务点
4.1 axios 实例别裸用:baseURL 与拦截器一次配好
前端和后端联调的标准姿势,是封装一个 axios 实例,而不是在页面里每次 import axios 再单独发请求。后者的结果是每个页面都要写重复的 Header、重复的错误提示,代码量翻倍且难以维护。封装的核心是 baseURL 与请求拦截器:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { alert(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default requestbaseURL 写成/api而不是http://localhost:8080,这是本地联调不翻车的核心。前端的 5173 端口和后端的 8080 端口不同源,如果直接写死 IP,开发环境要靠后端开启 CORS 才能跑通;写成/api后交给 Vite 代理转发,生产环境再由 Nginx 把/api指到后端,代码本身不用改,环境怎么变都兼容。timeout 的 10000 毫秒适合在线教育这种以查询为主的业务,视频相关的接口另设超时时间。响应拦截器里的 401 处理必须放在这里统一做,不然每个页面的 axios 请求失败时都要自己判断一次登录状态。
4.2 路由守卫:权限控制前后端各做各的
很多刚接触前后端分离的开发者有个误区:以为前端配了路由守卫,后端就不用管鉴权了。实际上前端路由守卫只是用户体验层的兜底,防止未登录用户看到页面框架,后端的拦截器才是数据安全真正的防线,因为接口可以被绕过前端直接调用。两者是配合关系,不是替代关系。
路由守卫的典型写法:
import router from './router' router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else if (token) { next() } else { next('/login?redirect=' + encodeURIComponent(to.fullPath)) } })redirect参数很值得写,它的作用是用户登录成功后自动回跳到来时的页面。如果不加这个参数,用户访问一个需要权限的深链接时被踢到登录页,登录完又回到首页,体验很差。encodeURIComponent防止路径里的特殊字符破坏 URL。这里需要注意:路由守卫只能在前端控制页面可见性,真正的数据保护还是要靠后端拦截器,所以第 3.3 节的 LoginInterceptor 无论如何不能省略。
4.3 在线教育特有场景:视频播放与图片上传两个容易卡住的点
在线教育平台和普通管理系统最大的区别,在于要处理视频和富文本。视频播放这里有一个稳妥的路线:如果视频是 mp4 格式,直接交给<video>标签播放,后端只需要保证视频文件能被静态访问:
<video controls :src="section.videoUrl" style="width: 100%"></video>这个方案背后依赖的是 HTTP 的 Range 请求,浏览器拖动进度条时会向后端发起分段请求,SpringBoot 的静态资源处理默认支持 Range,所以拖动进度条不会有问题。如果评审老师要求播放 m3u8 流媒体格式,事情会复杂很多:前端不能直接
图片上传同样有讲究。富文本编辑器里插图片,最简单的实现是转 base64 字符串直接存进富文本内容,但对大图不友好,数据库会被撑大。正规做法是把图片上传到后端指定的存储目录,数据库只保存访问路径。上传接口返回路径字符串,富文本编辑器把这段路径作为图片 src 使用。毕设里不需要复杂的云存储,本地目录加一个静态资源映射就能讲清楚。
5. 部署与排查:前后端分离项目最容易翻车的几个环节
5.1 打包与源码交接:jar、dist 和 node_modules 的边界
代码写完后面临两个问题:怎么打包、怎么把项目源码完整交给别人。先说打包,后端用 Maven,前端用 Vite,两个命令一次搞定:
# 后端打包 cd backend mvn clean package -DskipTests # 前端打包 cd frontend npm run build-DskipTests跳过测试可以减少打包耗时,如果你写了单元测试但没配好测试环境,加上这个参数能避免打包时卡在测试阶段。前端 build 完成后会生成 dist 目录,这就是最终要上线的静态资源。后端产物是 target 目录下的 jar 文件,if 你的工程里配置了前后端合并部署,把 dist 里的文件复制到后端 resources/static 目录再打包,一次启动就能同时提供页面和接口。
源码交接时要特别注意 node_modules 这个目录。它是由 npm install 根据 package.json 生成的,体积动辄几百兆,完全不需要手动拷贝,也要避免把 node_modules 打进压缩包。正确做法是:压缩包里只保留源码、package.json、package-lock.json,对方拿到后在 frontend 目录执行一次 npm install 即可恢复环境。三个文件各司其职,src 是你的业务代码,package.json 是依赖清单,package-lock.json 锁定了依赖版本,加上它才能保证别人安装的版本和你的完全一致。
两种部署方式的选择,面试时也经常被问到,这里整理成对比:
| 方式 | 实现 | 适用场景 | 注意点 |
|---|---|---|---|
| SpringBoot 托管静态资源 | 把 dist 放进 static 目录 | 毕设部署在单台服务器 | 前后端同端口,无跨域问题 |
| Nginx 反向代理 | 前端 dist 交给 Nginx,接口转发到后端 | 更接近企业生产环境 | 需要配置 location 和 proxy_pass |
我更推荐先学会 SpringBoot 托管这种方式,因为只需要 Java 环境就能跑,服务器上少装一个 Nginx。等你把 jar 包跑通之后,再回头看 Nginx 方案,理解起来会容易很多。
5.2 跨域与代理:看到 CORS 报错先分清是预检失败还是代理没生效
跨域报错是前后端分离项目的门神,也是被误解最深的问题。很多同学一看到“Access-Control-Allow-Origin”就以为是后端配置问题,其实要先判断:你是用 Vite 代理联调,还是直接请求后端接口。本地开发正确姿势是走代理,配置写在 Vite 配置文件里:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这段配置的意思是:所有以 /api 开头的请求都被 Vite 转发到 8080 端口的后端服务。因为代理发生在 Node 服务端,浏览器只跟 5173 端口打交道,所以完全没有跨域问题。changeOrigin: true的作用是把请求头里的 Host 改成目标地址,有些后端会对 Host 做校验,不设置可能被拒绝。
如果你选择不走代理、直接在后端开 CORS,要注意预检请求。跨域请求分为简单请求和预检请求,带 Authorization 头的请求属于预检请求,浏览器会先发一个 OPTIONS 请求探测后端是否允许跨域。很多人只放行了 GET、POST,没有放行 OPTIONS,结果前端网络面板里能看到接口返回 200,但浏览器还是报 CORS 错,这就是典型的“后端开了 CORS 但仍然跨域”。解决方法是在跨域配置里允许所有 OPTIONS 请求直接通过,并配置允许的请求头列表。
5.3 常见问题排查:现象、原因、解决
问题一:登录接口一直 401,但用 Postman 测试却正常。现象是前端每次请求都报未登录,单独用接口工具调登录却能通。原因多半是后端解析 token 时读取的请求头名称不一致,前端封装 axios 时写成 Authorization,后端拦截器却从 request.getHeader("token") 取。解决办法:前后端约定统一使用 Authorization 头,后端解析时先判断 header 是否存在,再判断前缀。
问题二:中文写入数据库变成乱码。现象是前端提交的中文内容,在数据库里显示为问号或乱码。原因通常是数据库默认字符集是 latin1,而连接字符串里的 characterEncoding=utf8 只影响连接层,库表本身的字符集没改。解决办法:在数据库初始化脚本里加DEFAULT CHARSET=utf8mb4,建库时也显式指定,不要依赖 MySQL 默认值。
问题三:前端打包后页面空白或图片 404。现象是本地开发正常,npm run build 之后打开 dist 目录全是空白。原因是 Vite 默认资源路径是绝对路径根目录,而你访问时可能是在子路径下,资源路径对不上。解决办法:在 vite.config.js 里设置base: './',让资源加载走相对路径。这个改动只在打包阶段生效,不影响本地开发。
问题四:MySQL 8.0 连接时报错 Public Key Retrieval is not allowed。现象是后端启动时数据库连接失败。原因是 MySQL 8 的默认认证插件允许客户端抓取公钥,但 JDBC 默认关了这功能。解决办法:在 jdbc 连接串上加allowPublicKeyRetrieval=true&useSSL=false两个参数。如果用户名密码都没错,这个报错是八成的连库失败原因。
问题五:后端接口返回报错:Could not write JSON。现象是查询接口在浏览器里突然 500,Memory 里提示序列化问题。原因通常是实体类之间存在循环引用,课程表里关联了用户信息,用户信息又包含课程列表,Jackson 序列化时陷入死循环。解决办法有两个:一是在课程实体里查老师时只返回 id 和昵称,不返回完整对象;二是用 JSON 注解标记忽略某条关联路径。我更推荐第一种,因为返回给前端的字段越少,接口响应越快,数据也不容易暴露内部结构。
6. 答辩前做三件事:接口自测、文档对齐、把话讲短
6.1 把三个核心链路按顺序自测一遍
答辩翻车的重灾区不是“没做出来”,而是演示时业务流程跑不通。我保留的习惯是提前一天用 curl 把接口按业务顺序打一遍,确保核心链路是通的。登录、获取课程列表、选课,这三个动作必须连贯成功。
# 第一步:登录拿到 token curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"123456"}' # 第二步:携带 token 查询已上架课程 curl -X GET http://localhost:8080/api/course/list \ -H "Authorization: Bearer <上一步返回的token>" # 第三步:模拟选课下单 curl -X POST http://localhost:8080/api/user/course \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"courseId": 1}'这段自测的含义不是“接口能返回 200 就行”,而是验证一个完整用户故事:学员能不能注册登录、能不能看到课程、能不能选课成功。如果你连管理员登录的账号密码都没设置为固定测试账号,答辩现场临时注册浪费时间,提前在 SQL 脚本里初始化三个角色各一个账号,是最小的投入。
6.2 文档说明与演示话术对齐
文档说明不是写完了就放着,它要和代码严格对应。至少要检查三处:数据库设计里的表数量和字段是否与数据库脚本一致;接口清单里的路径是否与 Controller 里实际写的 RequestMapping 一致;部署文档写的启动命令能否在自己电脑上重新跑通。很多项目代码能跑,但文档还停留在最初版本,答辩时被评委现场挑出“文档里的表和实际表对不上”这种尴尬,非常扣分。
描述系统用三句话就够了:用户通过浏览器访问 Vue 前端页面,前端通过 HTTP 请求调用 SpringBoot 提供的 RESTful 接口,后端基于 MyBatis-Plus 操作 MySQL 数据库并返回 JSON 数据。三句话说清楚了项目架构、数据流向和技术选型,比背一大堆概念更让人信服。
6.3 扩展点只挑一个写深
如果论文需要“不足与展望”,不要写三个泛泛的方向,选一个能讲清技术方案的。我比较推荐订单超时关单,因为它在现有系统里加了状态和时间两个维度,可以用定时任务扫描超时未支付订单,再把状态改为已取消。这个扩展点不复杂,却能体现你理解业务状态流转,比写“引入 Redis 缓存”这种套话有说服力。引入缓存看似高端,但如果你说不清缓存和数据库一致性问题,反而容易给自己挖坑。
最后说一点个人习惯:毕业设计阶段我有两件后悔的事,一件是花在调整前端样式上的时间太多,另一件是没提前把“JWT 过期后前端该怎么跳转”这个细节想清楚。这两个问题代码量不大,但直接影响演示效果和答辩印象分。如果你能把核心流程提前按顺序自测一遍,把版本环境固定住,把每个关键配置讲出原因,这个项目大概率会成为你简历上最扎实的一条项目经历。希望帮到你。
本文还有配套的精品资源,点击获取