拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3在线教学平台全栈实战:从架构设计到部署踩坑

SpringBoot2+Vue3在线教学平台全栈实战:从架构设计到部署踩坑

“在线教学平台”这个方向的 Java Web 项目,我前后看过不少,也自己动手改过几套。说实话,大多数所谓的“完整源码”要么后端只挂了几个 demo 接口,要么前端还停留在 jQuery 时代,真正能把 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 这整套组合串起来,并且覆盖课程、作业、考试、用户权限这些教学核心流程的,并不算多。我最近完整梳理了这套信息化在线教学平台的源码,从数据库表设计到前端权限路由,从 MyBatis-Plus 的无状态 CRUD 到 MySQL8.0 的安装部署,每一层都拆开过了一遍,这篇文章就把这套系统的技术实现逻辑、实操细节和踩坑记录全部整理出来。

如果你正在准备毕业设计,或者想找一套能真正跑通前后端的中后台项目做参考,又或者刚接触 Vue3 和 SpringBoot 全栈开发、想看看实际项目里每个技术点是怎么配合的,这套系统都挺有参考价值。我会把它拆成架构设计、后端实现、前端工程化、数据库环境搭建、问题排查五个部分来写,尽量还原实际开发时的选择和取舍。

1. 先盘清楚整体架构:功能与角色都在做什么

1.1 三个端口的职责边界

任何教学类系统,第一步都不是写代码,而是把使用角色和权限边界理清楚。这套在线教学平台分了三个端口:管理员端、教师端、学生端。

管理员端的核心职责是基础数据维护和平台管控。包括用户管理(给教师和学生分配账号、重置密码)、课程审核(教师创建的课程需要管理员审核后才对学生可见)、公告发布,以及一些简单的数据统计,比如系统里有多少活跃用户、多少门课程、作业提交率之类的指标。这个端口的功能一般不会太复杂,但它是整个系统的“管理枢纽”。

教师端的核心场景围绕“教学内容的制作与交付”展开。教师可以创建课程、编辑课程简介和封面、维护课程章节和课时信息、上传课件资源,也可以给学生布置作业、查看学生提交情况并进行批改评分,还能发起考试或测验。这一端的功能密度最高,也是整个系统里业务逻辑最复杂的部分。

学生端则相对简单直接:浏览课程列表、查看课程详情、选课、学习课时内容、提交作业、查看考试成绩和教师反馈。加上个人中心里的选课记录、作业记录等信息。

三个角色分开之后,后端接口设计、数据库表结构设计、前端页面路由就都有了清晰的依据。很多初学者一上来就写页面,结果写到一半发现某个角色要什么数据、要什么权限完全没想清楚,最后返工成本非常高。

1.2 技术选型:为什么是这套组合

先说后端。SpringBoot2 放到现在已经是“成熟得不能再成熟”的阶段,网上资料极多,踩坑方案也齐全,基于 JDK8 就可以运行,对新手和老手都友好。相比之下 SpringBoot3 强制要求 JDK17,虽然新特性更多,但不少第三方组件的兼容性还需要打磨。用 SpringBoot2 做这类教学系统,最大的优势是“稳”:你不用花时间在环境兼容性上,可以把精力放在业务逻辑本身。

前端选 Vue3,在 2026 年这个时间点基本已经是默认选项了。Vue3 的组合式 API(Composition API)配合<script setup>语法,让组件逻辑的组织方式比 Vue2 的选项式 API(Options API)清晰太多。拿同一个“课程管理页面”来说,Vue2 时代你可能要把数据、方法、生命周期钩子分散到 data、methods、created 三个区域里,来回跳着看;Vue3 里一个<script setup>从上到下一气呵成,数据定义、函数声明、页面初始化的逻辑顺序和读代码的顺序完全一致。对于中后台管理系统这种以表格、表单、弹窗为主要交互形态的项目,Vue3 的开发效率提升很明显。

数据访问层用 MyBatis-Plus,说白了就是冲着“少写重复代码”去的。教学平台里有大量单表 CRUD 操作——用户增删改查、公告增删改查、课程分类增删改查,这些如果用原生 MyBatis 手写 SQL 和 ResultMap,工作量会非常可观。MyBatis-Plus 内置了通用的增删改查方法,还提供了 lambda 查询语法,配合分页插件,覆盖整个项目中百分之七八十的数据访问场景完全够用。

数据库选 MySQL8.0 是顺势而为。8.0 默认字符集已经是 utf8mb4,支持窗口函数、JSON 类型,性能和功能比 5.7 有明显提升。之前踩过 5.7 一些字符集和函数受限的坑,这次直接上 8.0,基本没有再为数据库能力发过愁。

1.3 前后端交互的整体链路

这套系统的整体交互链路并不复杂,但值得在动手前想明白。浏览器请求先到前端开发服务器(开发阶段是 Vite DevServer,生产阶段是 Nginx),再通过代理或反向代理转发到 SpringBoot 后端。后端按 REST 风格暴露/api/v1/...接口,Controller 层接收请求后调用 Service 层,Service 层通过 MyBatis-Plus 对 MySQL 做数据访问。登录认证采用 JWT 方案:用户登录成功后拿到 token,后续请求在 header 里携带这个 token,后端通过拦截器统一校验。

这套链路对于中小型教学系统来说是标准且高效的。如果项目规模再大一些,可以在前端和后端之间加入 Redis 做缓存和分布式会话,但在这个项目体量下属于过度设计。先把这条链路的每一环跑通,比什么都重要。

2. 后端实现细节:数据库、通用 CRUD 与鉴权

2.1 数据库表设计:先想清楚数据怎么放

数据库设计是这个项目里最需要提前规划的部分,后面所有业务代码都建立在表结构之上。这套系统的核心表大致有这些:

表名用途关键字段
sys_user用户表id、username、password、real_name、role_type、avatar
sys_role角色表id、role_code、role_name
course课程表id、name、cover_url、description、teacher_id、status
course_section课程章节/课时表id、course_id、title、section_type、sort_order
course_student选课记录表id、course_id、student_id、create_time
resource课件资源表id、course_id、uploader_id、file_name、file_url、file_size
homework作业表id、course_id、title、content、deadline
homework_submit作业提交表id、homework_id、student_id、content、attach_url、score、status
exam考试表id、course_id、title、start_time、end_time、duration
exam_record考试记录表id、exam_id、student_id、score、submit_status
notice公告表id、title、content、create_by、create_time

设计时有一个容易忽略的点:course_student这种关联表,千万记得给 (course_id, student_id) 加唯一索引,否则并发情况下同一学生可能重复选同一门课。另外,所有表尽量都带 create_time / update_time 字段,使用 MyBatis-Plus 的自动填充功能,会让后续统计维护都方便很多。

2.2 MyBatis-Plus 集成:通用 CRUD 怎么落地

MyBatis-Plus 在这个项目里扮演的是“数据访问层半自动管家”的角色。集成时先在pom.xml里引入依赖,然后在工程里加入分页插件配置。这里有一个非常容易踩的坑:只引入依赖不加分页插件,selectPage方法调用后返回的数据里 total 一直是 0,因为分页功能默认没有开启。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

配好分页插件之后,单表的 CRUD 基本不需要手写 SQL。以课程为例,编写一个继承IService<Course>的接口和对应实现类,然后可以直接写业务方法:

// 条件查询课程列表,keyword 为空时不过滤名称 List<Course> courses = courseService.lambdaQuery() .eq(Course::getStatus, 1) .like(StringUtils.hasText(keyword), Course::getName, keyword) .orderByDesc(Course::getCreateTime) .list();

如果你用的是 MyBatis-Plus 3.5.4 以上版本,还可以直接用内置的 Db 工具类,实现真正的“无状态”增删改查——不需要注入任何 service,直接静态调用:

List<Course> list = Db.lambdaQuery(Course.class) .eq(Course::getStatus, 1) .list(); Db.save(new Course()); Db.removeById(Course.class, id);

这个Db工具类适合那种零散、复用度不高的数据操作,可以减少在 Controller 和 Service 之间来回传对象的繁琐。不过我实际用下来感觉,项目里涉及的关联查询和复杂统计(比如作业提交率报表)还是要手写 XML 或使用@Select注解,所以 MyBatis-Plus 的定位是“解决百分之八十的简单数据操作”,剩下百分之二十还是得靠数据库功底。

2.3 用户登录与 JWT 鉴权

登录鉴权这部分,这套系统采用了典型的 JWT 方案,整体流程是:用户提交用户名密码,后端校验通过后生成 token 返回前端;前端存储 token,之后每个请求在 Authorization header 里带上;后端拦截器对需要登录的接口统一校验 token 的有效性。

JWT 的好处是服务端无状态,非常适合前后端分离架构,不需要像传统 Session 那样维护服务端会话状态。但要注意几点:token 中不要放敏感信息(就放 userId 和 roleType 这种非敏感标识就行了),密钥要独立配置且足够复杂,同时给 token 设置合理的过期时间。业务上用户可能一节课上很久,token 过期时间可以设长一些,再配合前端拦截 401 响应做重新登录跳转,体验比较完整。

密码存储我这里建议使用 BCrypt 加密,不要用 MD5。MD5 加盐虽然也能做,但 BCrypt 内置盐值处理,而且每次哈希结果不同,抗彩虹表攻击能力更强。对于这类会真实部署的教学系统来说,密码安全是最基本的要求。

2.4 统一返回格式与全局异常处理

后端接口如果各写各的返回格式,前端联调时会非常痛苦。这套系统的做法是统一封装一个 Result 对象,格式大致是:

{ "code": 200, "message": "操作成功", "data": ... }

所有 Controller 都返回这个 Result,成功时 data 放业务数据,失败时 code 用非 200 值并附上 message。配合@RestControllerAdvice做全局异常处理,把参数校验异常、业务异常、未知异常统一转换成这个格式,前端只用在一处处理错误逻辑即可。

这个设计看起来简单,但实际项目中特别省事。比如说用户提交的作业附件超限,后端抛一个业务异常,全局处理器把它转成统一格式返回,前端弹出对应的提示,整个过程不需要每一个接口单独写 try-catch。这也是我比较推荐新手参考的一个设计:先把统一返回和全局异常做好,再谈业务功能。

3. 前端工程化:Vue3 中后台系统的搭建与页面实现

3.1 用 Vite 初始化项目与目录规划

前端这一块,这套系统使用 Vite 做构建工具,和 Vue3 配合得最顺。初始化命令很简单:

npm create vite@latest edu-frontend -- --template vue

然后安装路由 vue-router、状态管理 pinia、UI 组件库 element-plus、HTTP 客户端 axios。这套组合做中后台管理系统几乎是标配,相关的 quasar、naive-ui 等虽然也很好用,但 Element Plus 在组件覆盖面和中文资料上更占优势。

目录结构规划上,我见过太多随手乱建目录的项目,组件满天飞,最后连作者自己都要靠搜索找文件。这套系统的目录划分思路值得参考:

src/ api/ # 所有接口请求定义 assets/ # 静态资源 components/ # 通用组件 layout/ # 布局组件(侧边栏、导航栏) router/ # 路由配置 store/ # pinia 状态 views/ # 页面 admin/ teacher/ student/ login/ utils/ # 工具函数

其中 api 目录按页面或模块拆文件,比如 course.js、homework.js、user.js,每个文件里定义对应模块所有接口请求。这样做的好处是页面组件里不直接写 axios 请求,所有网络层集中在 api 目录里,接口改动时只需要动一个文件。

3.2 路由守卫与权限控制

中后台管理系统里,路由不仅仅是页面路径,更是权限的体现。这套系统的做法是:路由配置里给每个页面标记需要的角色,比如meta: { roles: ['admin'] },然后在全局前置守卫里做校验。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && !to.meta.public) { next('/login') return } const roleType = localStorage.getItem('roleType') if (to.meta.roles && !to.meta.roles.includes(roleType)) { next('/403') return } next() })

权限控制只做前端路由守卫肯定不够,后端每个接口也需要做角色权限校验,前端守卫负责页面级拦截,后端接口负责数据级安全,两层配合才能真正保证安全。很多刚入门的朋友只做前端权限,后面接口被直接调用就是一个安全漏洞,这一点务必记住。

3.3 Axios 封装与状态管理

axios 这里我强烈建议做统一封装,而不是每个页面各自引用。请求拦截器里从 localStorage 取出 token 放到 header 里;响应拦截器里统一处理业务码错误、401 未登录、网络异常,然后弹出统一的消息提示。核心逻辑如下:

service.interceptors.request.use(config => { config.headers.Authorization = localStorage.getItem('token') return config }) service.interceptors.response.use( response => { if (response.data.code !== 200) { ElMessage.error(response.data.message) return Promise.reject(response.data) } return response.data.data }, error => { if (error.response?.status === 401) { router.push('/login') } ElMessage.error(error.message) return Promise.reject(error) } )

这样封装之后,业务页面里调用接口只需要关心成功的业务数据,异常和错误提示统一在拦截器处理,代码会干净很多。

状态管理用 pinia,主要存储用户信息、侧边栏折叠状态这类全局共享的数据。这个项目的实操经验是:不要什么状态都往 store 里丢,只有多个不相关组件需要共享的数据才放进 store,否则 store 会变成一个大杂烩,维护成本反而上升。

3.4 一个典型业务页面的拆解:课程管理

以教师端的课程管理页面为例,它是这套系统里最具代表性的页面,几乎涵盖了中后台系统的所有典型交互:表格展示、分页、条件搜索、弹窗表单、上传文件、动态表单行。

页面的主体是一个 el-table,数据来自后端分页接口,采用 el-pagination 组件控制分页,搜索区通过课程名称关键词过滤。点击“新增课程”弹出一个 el-dialog,内嵌表单可以填写课程名称、简介、封面上传等。章节维护则使用 el-tabs 或者嵌套表格的形式,每个课时作为一行,支持动态添加和删除——这里一定会用到一个经典交互:表单里的动态行。

Element Plus 里处理动态添加删除表单行,核心是操作一个数组,添加课时就是 push 一个空对象进数组,删除就是 splice 掉对应索引。但这里有个很隐蔽的问题:删除中间某一行后,后面行的字段绑定对象还好,如果用了 index 作为 key,会导致组件复用出错。所以动态行里我建议每行都用一个唯一的 id(可以用Date.now() + Math.random()生成),key 绑定这个 id,避免 Vue 渲染复用时数据错乱。

这个页面的代码量占整个系统前端代码的五分之一左右,但把它拆开看就是“表格 + 弹窗 + 表单 + 上传 + 分页”几个经典模式的组合。把这种页面完整实现一遍,Vue3 的中后台开发基本就入门了。另外,如果你用 Vite 开发,可以装一个能够从页面快速定位到源码的 Vite 插件(比如 vite-plugin-vue-devtools 之类),调试体验会好非常多。

4. MySQL8.0 环境搭建与部署三件套

4.1 安装 MySQL8.0 的三种方式

这套系统的数据库要求是 MySQL8.0,本地搭建时有几种常见路径,我这里分别说清楚。

最省事的绝对是 Docker 方式。只要机器上装好了 Docker,一条命令就能跑起一个 MySQL8.0 实例:

docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0

这里-v /data/mysql:/var/lib/mysql是数据目录挂载,把容器里的数据文件映射到宿主机,这样容器删了重建数据也还在。第一次用 Docker 跑 MySQL 时我踩过一个坑:忘了挂载数据卷,后来 Docker 清理时数据全没了。数据库这种东西,数据安全永远是第一位的,挂载卷一定要从第一天就做好。

不用 Docker 的话,Windows 上安装 MySQL8.0 可以去官网下载 MySQL Installer,目前 8.0 系的安装包做得很省心,选择 Server only 就可以一路 Next。需要注意安装过程中设置 root 密码,以及选择认证插件时,如果后续要连接的老版本客户端很多,可以选 mysql_native_password 兼容性更好;但如果全部使用新版驱动就保持默认的 caching_sha2_password 即可。Linux 上则可以通过 apt 或 yum 仓库安装,安装完用systemctl start mysql启动服务。

验证 MySQL 是否正常工作,命令行执行:

mysql -u root -p

输入密码后进入 MySQL 终端,执行SELECT VERSION();能看到版本号就说明安装成功了。

4.2 初始化数据库与账号配置

安装好 MySQL8.0 之后,需要创建项目数据库并导入表结构。先把项目里的初始化 SQL 脚本准备好,然后在 MySQL 终端里执行:

CREATE DATABASE edu_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

建库之后,用USE edu_platform;切换数据库,再执行项目 sql 脚本(可以用source命令或直接在客户端工具里执行)。表结构导入后,建议不要直接让应用用 root 账号连接数据库,而是创建一个专有账号,只授权这个库的权限:

CREATE USER 'edu_app'@'%' IDENTIFIED BY 'edu_pass_2024'; GRANT ALL PRIVILEGES ON edu_platform.* TO 'edu_app'@'%'; FLUSH PRIVILEGES;

用最小权限账号连接数据库,是生产环境数据库安全的基本要求。即使这个项目只是本地跑,养成分账号管理的习惯也没坏处。

4.3 前后端联调与生产部署

本地开发时,前端和后端端口不同(Vite 默认 5173,SpringBoot 默认 8080),跨域问题一定会出现。解决办法是在 Vite 配置里启用代理,而不是在后端粗暴地开启全局跨域:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }

这样前端代码里请求/api/v1/course/list,Vite DevServer 会把它转发到http://localhost:8080/api/v1/course/list(如果后端接口不带/api前缀,就通过 rewrite 去掉),浏览器视角看起来是同源的,跨域问题彻底规避。

生产部署时,后端打包用mvn clean package生成一个可执行的 jar 包,java -jar edu-platform.jar就能跑起来。前端执行npm run build会生成 dist 静态目录,部署到 Nginx 里,再在 Nginx 配置一个反向代理把/api转发到后端服务即可。这套部署链路是所有 Java Web 前后端分离项目的通用做法,学会一次,以后做任何项目都能用上。

5. 我在实操中踩过的坑(真实问题排查记录)

5.1 连不上 MySQL:驱动类、时区和认证插件

这套系统在别人机器上跑不起来,八成问题出在数据库连接上。这里把最常见的几个坑集中列一下。

第一个是驱动类名。MySQL5.7 时代用的是com.mysql.jdbc.Driver,到了 MySQL8.0 必须换成com.mysql.cj.jdbc.Driver,而且新版本的 MySQL Connector/J 驱动类名实际上推荐直接不写,由 SpringBoot 自动识别。但如果你的配置里还写着老驱动名,启动时就会报ClassNotFoundException。

第二个是时区问题。连接串里不加serverTimezone,MySQL8.0 会报一个时区相关的错误提示。配置里加上serverTimezone=Asia/Shanghai即可解决:

spring: datasource: url: jdbc:mysql://localhost:3306/edu_platform?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4

第三个是认证插件问题。MySQL8.0 默认的caching_sha2_password认证方式,如果项目用的 JDBC 驱动版本太老,会出现连接失败。解决办法是先确认驱动版本(使用 mysql-connector-j 8.0.x 以上),或是在创建用户时指定老认证插件。第三方数据库可视化工具连不上 MySQL8.0 时,通常也是这个原因。

5.2 MyBatis-Plus 的常见使用问题

MyBatis-Plus 用起来确实方便,但有三个高频问题值得提前知道。

第一个是分页失效。前面说过,分页插件必须配置,否则selectPage查询出来 total 永远为 0。很多人的代码逻辑没问题,就是漏了那个PaginationInnerInterceptor配置类。

第二个是字段更新问题。updateById默认会忽略实体里为 null 的字段,也就是说你想把一个字段改成 null(比如清空某个字段值),这种操作默认不会生效。如果确实需要更新 null 值,可以用LambdaUpdateWrapper显式调用set方法,或者修改字段的FieldStrategy策略。

第三个是驼峰映射问题。数据库字段create_time和实体属性createTime,MyBatis-Plus 默认开通了下划线转驼峰映射,但如果你改了全局配置开关,或者自己手写了不规范的 XML 映射,就会出现字段查出来全是 null 的情况。排查时先看map-underscore-to-camel-case这个配置是否为 true。

5.3 前端与联调场景的典型问题

前端这边我遇到比较多的是 Element Plus 按需引入导致的样式丢失。用 unplugin-vue-components 做按需引入时,如果样式文件没配好,组件功能正常但没有 CSS 效果。解决方案是安装unplugin-element-plus插件或者在入口文件显式引入element-plus/dist/index.css。

还有 token 过期的问题。JWT 过期之后,后端返回 401,前端响应拦截器里要统一做跳转登录页并清理本地存储的操作。这里要注意一点:不要在拦截器里对 401 做死循环重试,否则登录页还没跳过去,又发了一个带过期 token 的请求,页面会出现怪异表现。跳转前清空 localStorage 里的 token 和用户信息是必须的一步。

另外一个很隐蔽但实际的坑是浏览器自动填充表单的问题。登录页如果使用 Element Plus 的 el-input,浏览器自动填充的用户名密码会出现在输入框里,但 Vue 的 v-model 数据里可能没有同步到这个值,导致用户填了密码也登录失败。解决思路是监听 input 的 change 事件手动赋值,或者使用 autocomplete="off" 并结合表单的onPasswordVisibilityChange等处理,总之这类浏览器问题在真实开发中一定会遇到。

最后给一个常见问题速查表,方便快速定位:

问题现象排查方向
启动报 ClassNotFoundException 找不到 MySQL 驱动检查 maven 依赖是否引入 mysql-connector-j,版本是否 8.0+
连接数据库报 Public Key Retrieval is not allowed连接串加allowPublicKeyRetrieval=true
selectPage 查不出 total检查是否配置 MyBatisPlusInterceptor 分页插件
接口返回 401检查 token 是否过期、拦截器放行路径是否正确
前端请求 404检查 Vite 代理 rewrite 规则、后端接口路径是否带 /api
Element Plus 组件无样式检查按需引入插件配置或全局样式是否导入
动态表单删除行后数据错乱检查列表 key 是否用了可变的 index,改为唯一 id

这套系统我整体跑下来,最大的感受是“结构比功能更重要”。功能只是往里填内容,而结构决定了一个项目能不能被看懂、能不能被别人接手、能不能在遇到问题的时候快速定位。SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 这套组合就是这类教学系统最稳妥的底座。如果你也想拿这套框架练手或做二次开发,我建议从数据库表设计开始看,再看后端如何用 MyBatis-Plus 做通用 CRUD,接着看前端路由和权限控制,最后把 MySQL8.0 环境搭起来跑通整个项目。按照这个顺序,你会比直接看页面代码更快建立起整体认知。另外,如果你在准备面试,这套系统里涉及的技术点——JWT 鉴权、统一异常处理、Vite 代理、MyBatis-Plus 分页、动态表单——都是中后台开发的高频题,能把“为什么这样做”讲清楚,比背概念有用得多。

返回列表