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 的组合式 APIComposition API配合script setup语法让组件逻辑的组织方式比 Vue2 的选项式 APIOptions 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、avatarsys_role角色表id、role_code、role_namecourse课程表id、name、cover_url、description、teacher_id、statuscourse_section课程章节/课时表id、course_id、title、section_type、sort_ordercourse_student选课记录表id、course_id、student_id、create_timeresource课件资源表id、course_id、uploader_id、file_name、file_url、file_sizehomework作业表id、course_id、title、content、deadlinehomework_submit作业提交表id、homework_id、student_id、content、attach_url、score、statusexam考试表id、course_id、title、start_time、end_time、durationexam_record考试记录表id、exam_id、student_id、score、submit_statusnotice公告表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。以课程为例编写一个继承IServiceCourse的接口和对应实现类然后可以直接写业务方法// 条件查询课程列表keyword 为空时不过滤名称 ListCourse 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直接静态调用ListCourse 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 vitelatest 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_PASSWORD123456 \ -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 默认 5173SpringBoot 默认 8080跨域问题一定会出现。解决办法是在 Vite 配置里启用代理而不是在后端粗暴地开启全局跨域server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这样前端代码里请求/api/v1/course/listVite 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。第二个是时区问题。连接串里不加serverTimezoneMySQL8.0 会报一个时区相关的错误提示。配置里加上serverTimezoneAsia/Shanghai即可解决spring: datasource: url: jdbc:mysql://localhost:3306/edu_platform?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4第三个是认证插件问题。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和实体属性createTimeMyBatis-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 事件手动赋值或者使用 autocompleteoff 并结合表单的onPasswordVisibilityChange等处理总之这类浏览器问题在真实开发中一定会遇到。最后给一个常见问题速查表方便快速定位问题现象排查方向启动报 ClassNotFoundException 找不到 MySQL 驱动检查 maven 依赖是否引入 mysql-connector-j版本是否 8.0连接数据库报 Public Key Retrieval is not allowed连接串加allowPublicKeyRetrievaltrueselectPage 查不出 total检查是否配置 MyBatisPlusInterceptor 分页插件接口返回 401检查 token 是否过期、拦截器放行路径是否正确前端请求 404检查 Vite 代理 rewrite 规则、后端接口路径是否带 /apiElement Plus 组件无样式检查按需引入插件配置或全局样式是否导入动态表单删除行后数据错乱检查列表 key 是否用了可变的 index改为唯一 id这套系统我整体跑下来最大的感受是“结构比功能更重要”。功能只是往里填内容而结构决定了一个项目能不能被看懂、能不能被别人接手、能不能在遇到问题的时候快速定位。SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这套组合就是这类教学系统最稳妥的底座。如果你也想拿这套框架练手或做二次开发我建议从数据库表设计开始看再看后端如何用 MyBatis-Plus 做通用 CRUD接着看前端路由和权限控制最后把 MySQL8.0 环境搭起来跑通整个项目。按照这个顺序你会比直接看页面代码更快建立起整体认知。另外如果你在准备面试这套系统里涉及的技术点——JWT 鉴权、统一异常处理、Vite 代理、MyBatis-Plus 分页、动态表单——都是中后台开发的高频题能把“为什么这样做”讲清楚比背概念有用得多。
返回列表