ARTICLE DETAIL

资讯详情

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

在线教育平台全栈开发:SpringBoot+Vue3前后端分离实战经验

在线教育平台全栈开发:SpringBoot+Vue3前后端分离实战经验 前阵子我把一个在线学习教育平台从零到一完整搭了起来技术栈就是这个组合Java SpringBoot Vue3前后端完全分离。这类项目在面试和实际业务里都挺常见既能体现后端接口设计能力又能展示前端的工程化水平。今天不打算只晒源码我想把当时的思考过程、模块拆分、核心代码写法、前后端联调里踩过的坑以及最终部署上线的完整链路都梳理一遍希望能给正在做类似系统或者准备拿这类项目当毕业设计/作品集的朋友一些能直接用的参考。1. 项目背景与整体定位1.1 这套在线教育平台到底解决什么问题先把这个项目说清楚。在线学习教育平台从使用角色上看通常包含四类人学生、教师、管理员、运营人员。核心业务链路也很直观学生浏览课程列表、查看课程详情、下单购买、在线学习、记录学习进度教师在后台创建课程、上传视频、管理章节管理员负责用户管理、课程上下架、订单退款审核。如果只是把页面堆出来那其实不算一个完整的系统。真正决定工作量的是业务状态的处理比如订单从待支付到已支付再到已取消学习进度怎么按章节粒度记录课程下架之后已购买用户还能不能看这些都需要通过合理的数据模型和接口设计来兜底。我之所以选SpringBoot Vue3前后端分离而不是像传统的JSP或者Thymeleaf服务端渲染核心原因就一句话把前端展示逻辑和后端数据能力彻底解耦这样前端可以独立部署后端可以按接口维度做权限控制和缓存团队协作的边界也清晰得多。1.2 为什么是SpringBoot配Vue3而不是其他组合我并不是说这个组合是唯一答案但在2024年、2025年这个时间点它确实是国内中小型项目里性价比极高的默认选择。后端SpringBoot内置Tomcat、自动配置、生态丰富整合MyBatis-Plus、Spring Security、Redis都非常顺手社区资料多遇到问题基本都能搜到答案。前端Vue3相比Vue2组合式APIComposition API写复杂业务逻辑更清晰配合Vite构建开发冷启动秒开而且TypeScript支持更好团队规范化程度能提升一截。前后端分离部署时可以前端丢Nginx、后端跑Java进程各自扩容互不干扰开发和调试也能通过代理直接转发不必被传统模板引擎的耦合逻辑绑架。如果你还在纠结用不用微服务我的建议是这个阶段先别碰。教育平台的核心逻辑还是围绕课程、订单、用户、学习记录展开单体内聚、按模块分包完全够用微服务只会让开发和部署复杂度翻倍。等到用户量真的达到瓶颈再按课程服务、订单服务去拆分也来得及业务边界现在先通过模块划分留好就行。2. 系统模块划分与数据库设计2.1 核心模块清单与业务边界我在设计时没有一上来就写代码而是先画清楚模块边界。整个系统的后端按业务域分为下面几个模块用户模块注册、登录、手机号/邮箱绑定、个人信息修改、角色管理学生/教师/管理员。课程模块课程分类、课程详情、章节管理、视频资源关联、课程上下架。订单模块购物车或直接下单、订单状态流转、支付回调处理、退款。学习模块学习进度记录、最近学习课程、我的课程列表、播放权限校验。内容模块轮播图、公告、课程评价用于运营位展示。后台管理统计分析、用户列表、订单列表这部分单独用一套Vue3管理端页面。实际实现的时候我把这些模块对应到后端工程里的controller、service、mapper包并没有拆成多个Maven模块因为一个可运行、可演示的项目单体比多模块更能快速交付。但模块边界在包里分得很清楚后期真要拆分代码迁移成本也很低。2.2 数据库表设计思路数据库我用的MySQL 8.0字符集utf8mb4。主要表结构我列一下你可以参考表名核心字段说明userid, username, password, nickname, avatar, phone, role用户表角色字段区分学生/教师/管理员course_categoryid, name, parent_id, sort课程分类支持两级courseid, title, cover, teacher_id, category_id, price, status, description课程表status表示上架/下架/审核course_sectionid, course_id, section_name, video_url, sort, duration章节表一门课多个章节course_orderid, order_no, user_id, course_id, amount, status, pay_time订单表study_progressid, user_id, course_id, section_id, watch_seconds, last_time, updated_at学习进度按章节记录course_evaluationid, user_id, course_id, content, star, status课程评价设计时最需要注意的几点订单号不要用数据库自增ID我用的yyyyMMddHHmmss 随机数生成唯一单号方便后续对账。支付金额用Decimal类型存分避免浮点误差如果金额单位是元接口返回时再转换成两位小数。学习进度表加联合唯一索引(user_id, course_id, section_id)保证一个用户对同一章节只有一条进度记录更新时走insert ... on duplicate key update。课程表里的teacher_id关联user表但教师信息和用户信息我放在同一张表通过role区分这样登录认证逻辑可以复用。2.3 权限模型选择权限模型我直接用RBAC基于角色的访问控制没有引入细粒度权限点。原因很简单在线教育平台的接口权限粒度到角色这一个级别基本够了学生角色只能访问学习、下单、评价相关接口教师角色可以创建和维护自己的课程管理员角色拥有所有权限。后端在Spring Security里配置hasRole前端再根据用户角色动态渲染菜单和按钮两层判断既不复杂也能挡住越权操作。具体的配置方式我放到后面后端实现部分详细说。3. 后端SpringBoot关键实现3.1 工程结构与统一响应后端工程我采用的是经典的三层架构RestController - Service - Mapper。另外加了一个common包放统一响应对象、全局异常处理器、工具类避免每个接口都重复写返回结构。一个简单的ResultT大概长这样Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }再加上一个RestControllerAdvice全局异常处理器把业务异常、参数校验异常统一转换成语义明确的返回体。这个动作看起来很小但实际联调时非常省心前端axios可以统一在拦截器里判断code不用每个接口单独写报错处理。3.2 JWT认证与Spring Security集成前后端分离之后Session这种方式就不合适了因为前端和后端域名/端口都可能不同Cookie跨域处理起来麻烦而且后端要做成无状态接口更容易横向扩展。所以认证方案我选的是JWT。流程是登录成功 - 后端生成JWT返回给前端 - 前端把Token放到请求头Authorization: Bearer token- 后端网关这里直接是Spring Security过滤器链解析Token把用户信息放入SecurityContext。核心配置大致如下Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .requestMatchers(/api/auth/**, /api/course/**, /api/home/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }JWT过滤器里做的事情就是从请求头取出Token校验签名和过期时间然后解析出用户名和角色往SecurityContextHolder里塞一个UsernamePasswordAuthenticationToken。这里有个我一开始容易忽略的细节JWT的密钥绝不能用默认值写在代码里我最后是放到application.yml的jwt.secret配置里使用ConfigurationProperties读取。密码存储也要多说一句一定用BCrypt加密不要存明文注册和登录时分别用passwordEncoder.encode(...)和matches(...)这样即使数据库泄露密码也无法被直接还原。3.3 课程分页与缓存优化课程列表算是一个读多写少的典型场景。我并没有一上来就把所有课程查询结果全缓存而是先做MySQL层面的优化使用MyBatis-Plus自带的分页插件配合索引(status, category_id, updated_at)把已上架课程按更新时间倒序分页查询。在数据量增长后再加上Redis缓存。我的缓存策略是把首页轮播、分类列表、前N条热门课程缓存到Rediskey设计成home:course:hot缓存时间设30分钟。更新课程上下架状态时手动删除对应缓存key保证数据最终一致。这里要特别提醒用Redis缓存列表数据时不要在缓存里存对象数组再整体序列化最好分别缓存每个课程的简要信息这样单条数据变更时删除单个key即可不会造成缓存击穿。分页接口的Controller这么写就够用GetMapping(/api/course/page) public ResultPageCourseVO page(RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size) { PageCourse coursePage courseService.lambdaQuery() .eq(Course::getStatus, 1) .orderByDesc(Course::getUpdatedAt) .page(new Page(page, size)); // 转换为VO隐藏内部字段 return Result.success(coursePage.convert(CourseVO::new)); }3.4 支付回调与订单事务控制支付这块我并没有真实对接微信或支付宝因为需要商户号。我在代码里实现了标准支付流程的模拟用户下单 - 生成待支付订单 - 前端跳转支付页 - 后端提供一个“模拟支付成功”的接口校验订单状态后调用paySuccess逻辑。接口预留了真实的支付回调入口实际接入时只需把伪支付替换成微信支付回调解析即可业务代码不用改。订单创建这个操作我给它加上了Transactional(rollbackFor Exception.class)因为一个订单创建会涉及三件事插入订单记录、给课程库存这里可没有库存概念但可能涉及优惠券核销、记录订单流水。任何一步失败都不能留下脏数据。事务里还要处理并发问题用户重复提交订单时要在事务内根据order_no加锁或利用数据库唯一索引兜底防止生成重复订单。重点提一下支付回调的幂等性回调接口必须做订单状态判断如果当前订单已经处于“已支付”状态直接返回成功不再触发后续逻辑否则用户点一次支付按钮订单日志可能被重复写入。4. 前端Vue3端到端实现4.1 工程搭建与目录规划前端我用的Vite Vue3 TypeScript Pinia Element Plus。用Vue3创建项目的命令是npm create vitelatest edu-web -- --template vue-ts然后安装依赖npm install npm install vue-router4 pinia element-plus axios目录结构我按功能组织而不是按组件类型堆砌src/ api/ // 接口请求模块按业务域拆分 assets/ components/ // 通用组件 router/ // 路由配置 stores/ // Pinia状态仓库 views/ // 页面级组件 utils/ // axios封装、工具函数这里有个容易踩的坑Vite脚手架默认会把src下的文件按引用关系打包如果views目录里有没被路由引用的页面它不会自动打包所以开发时注意别把测试页直接扔进views又不配路由否则最后build出来的产物可能缺失页面。4.2 axios封装与Token拦截axios我单独封装了一个实例统一设置基础URL然后在请求拦截器里携带Token响应拦截器里做统一错误提示和登录失效跳转。import axios from axios import { useUserStore } from /stores/user import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { useUserStore().clear() router.push(/login) } return Promise.reject(new Error(res.message || 请求失败)) }, error Promise.reject(error) ) export default servicebaseURL设成/api不是乱来的开发环境配合Vite proxy生产环境配合Nginx反向代理都是一个路径这样不用在环境切换时改代码。4.3 路由守卫与动态权限前端路由我分为两部分公开页面首页、课程列表、课程详情和需要登录的页面学习页、个人中心、订单页另外还有一套独立的admin管理端路由。全局前置守卫用来判断登录状态router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.path.startsWith(/admin) userStore.role ! ADMIN) { next(/) } else { next() } })当然这只是一层简单的控制真正可靠的权限校验一定在后端接口前端只是优化交互体验。我在实际项目里遇到过只在前端做判断导致有人直接改了按钮文字绕过限制的情况所以这句话一定要牢记前端是面子后端才是底子。4.4 课程学习页的进度上报学习页是整套系统里交互最复杂的页面之一。页面主体是视频播放器需要在上方展示章节列表右侧或底部展示当前章节信息。用户每次切章节时触发一次进度上报接口播放过程中每30秒再上报一次最近学习的章节和观看时长。视频我接的是标准HLS或MP4直链前端用video标签播放。如果想做得更专业可以用vue-video-player但要注意为了体积考虑只按需加载播放器组件别在首屏引入。进度上报接口设计成后端接收courseId、sectionId、watchSeconds然后执行INSERT ... ON DUPLICATE KEY UPDATE。同时这个接口一定要做权限校验确保当前用户已经购买了这门课程否则未购买的用户可以通过直接调接口白嫖学习记录。4.5 前后端联调与跨域处理开发环境最常见的跨域问题Vite里处理得很简单在vite.config.ts里配一个proxy就行export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/course/pageVite会把请求转发到http://localhost:8080/api/course/page浏览器里看不到跨域后端也不需要专门开启CORS。生产环境就用Nginx做同样映射。不过要注意如果后端接口直接部署在另一个域名下前端axios的baseURL就需要写成完整域名并且后端要用CorsFilter放行对应域名。我在开发过程中经常碰到的问题是改完Vite配置后必须重启dev server否则proxy不生效这个细节很容易被忽略排查了大半天。5. 打包部署与问题排查5.1 前端dist交给Nginx还是SpringBoot很多同学习惯把前端构建产物dist直接扔进SpringBoot的resources/static目录然后打包成一个jar运行这样确实最省事一个jar搞定所有。但我不推荐在生产环境这么做原因有两个前端静态资源和后端接口混在一个进程里后续前端更新必须重新打包后端发布耦合严重。一旦接口流量上来静态资源和接口争抢同一个Tomcat线程池性能会互相拖累。推荐的方案是前端dist目录部署到Nginx后端jar独立启动。Nginx配置大致如下server { listen 80; server_name edu.example.com; location / { root /opt/edu-web/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那句一定要写否则刷新页面时Vue Router的history模式会直接404这是我最常看到的一个部署坑。如果不想用history模式也可以改成hash模式但URL会多一个#我个人更喜欢history模式配Nginx处理。5.2 一个可跑的Docker部署方案我后来把整套系统容器化写了一个docker-compose.yml把MySQL、Redis、后端、前端四个服务编排起来。大致结构services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: edu_platform ports: - 3306:3306 redis: image: redis:7 ports: - 6379:6379 backend: build: ./backend depends_on: - mysql - redis ports: - 8080:8080 frontend: image: nginx volumes: - ./dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf ports: - 80:80 depends_on: - backend后端Dockerfile也没什么特别FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuild /app/target/edu-backend.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]部署的时候只要docker compose up -d --build整体链路就能跑起来。这里提醒一个细节Docker容器里的后端要连着宿主机或容器的MySQL/Redisjdbc:mysql://mysql:3306这种写法要用服务名而不是localhost否则容器一启动就连不上库。5.3 常见问题TOP5与解决实录开发过程中我确实攒了不少问题挑五个最典型的说问题现象原因解决方案跨域浏览器提示No Access-Control-Allow-OriginVite proxy没生效或后端CORS没配置确认Dev Server重启接口路径是否带/api前缀如果直连后端配置CorsFilter允许来源刷新页面404部署后访问子路由404Nginx没有try_files配置加上try_files $uri $uri/ /index.html;登录后请求401Token失效或未被携带axios拦截器没取到Token或者Token过期检查Pinia持久化方式推荐用pinia-plugin-persistedstate存localStorageToken有效期设短一点配合refresh_token刷新SpringBoot版本太高导致依赖不兼容启动报错或Bean创建失败SpringBoot 3.x要求Java17以上部分旧版MyBatis-Plus不支持要么升Java17要么用适配SpringBoot3的MyBatis-Plus版本或者直接把SpringBoot降级到2.7中文乱码接口返回中文变问号后端编码问题或数据库字符集问题确认application.yml中server.servlet.encoding.forcetrueMySQL连接URL加characterEncodingutf8mb4这里想特别展开一下SpringBoot版本的问题。很多教程还是基于SpringBoot 2.x如果你直接用了3.x版本像javax.servlet会变成jakarta.servletMyBatis-Plus需要3.5.3.1以上的版本才支持SpringBoot3否则会出现mapper扫描不到、自动注入失败这类问题。我的建议是如果你是照着旧教程搭项目优先用SpringBoot 2.7.x如果是新项目且团队熟悉Java17可以用3.x但依赖版本要自己核对。5.4 一些性能和安全优化建议项目跑起来只是第一步上线后你还会面临很多实际问题。我把自己做过的优化按收益排序列下来懒加载路由。Vue3里用() import(/views/course/CourseDetail.vue)按需加载页面首屏包体积能从1.8MB降到800KB左右。课程详情页的播放地址不要直接返回真实URL而是生成一个有时效性的签名URL后端校验过期时间防止视频链接被外部盗链。接口层面用Spring AOP记录关键操作日志特别是订单创建、退款这种高风险操作出问题时能定位到人。MySQL里的course_order表加索引user_id和status订单查询页避免全表扫描。Redis的key统一加前缀比如edu:order:paidsuccess避免和其他业务混在一起难排查。前后端接口都要做参数校验不要相信前端传过来的数据。我用ValidatedNotNull对入参做了强校验比自己手写if判断强很多。这些优化不复杂但每一项都能在真实场景里派上用场。比如懒加载我一开始图省事全部静态引入结果首屏白屏时间接近2秒优化后降到600毫秒体感差距非常明显。5.5 若依Vue3框架能不能用来改造我在项目调研时也看过若依这个老牌后台框架现在它有Vue3版本。若依的好处是已经集成了用户管理、角色管理、菜单管理、代码生成拿来改造成教育平台可以省不少基础功能开发时间。如果你的目标是快速交付后台管理系统完全可以基于若依Vue3二次开发只需要新增课程、订单、学习记录这几张业务表即可。但我这次没直接用若依主要原因是我希望把核心流程自己控制一遍尤其是JWT认证、Vite构建、Pinia状态管理这几个环节自己写一遍对理解前端工程化更有帮助。如果你是用来做毕设或者短期交付若依是一个值得参考的加速起点前提是熟练处理它那一套权限路由和代码生成器的使用习惯。6. 从这套项目里沉淀下来的几点经验代码写到这里功能算是完整了。最后我想分享几个我个人觉得比代码更有价值的体会。第一个体会是做这种全栈项目一定要先把数据模型想清楚再动手。我第一版数据库设计时漏了学习进度表导致学习记录只能用日志表硬扛后面前端展示“最近学习”这个功能时只能全量扫描日志聚合效率惨不忍睹。后来我补上study_progress表再配合索引这个接口从1秒多降到几十毫秒。数据模型就是你系统的地基地基歪了上层功能再好也站不稳。第二个体会是前后端分离的调试一定要有一套统一约定的返回格式。我在项目初期后端同学返回{code:200, data:...}有时候又返回{success:true, result:...}前端axios拦截器就得写一堆兼容逻辑。后来统一成ResultT所有接口都是{code, message, data}前端只用判断code 200后期维护成本骤降。第三个体会是安全这个话题千万别忽视。不仅要防SQL注入、XSS更要防水平越权和垂直越权。具体到这个系统里学生A购买课程后不能通过直接改接口参数去请求学生B的订单详情未购买用户不能访问课程视频播放接口。这些接口级权限校验是上线前的红线检查项我甚至写了一个简单的越权测试用例包用不同角色跑一遍核心接口。这套系统做完之后我又在它的基础上扩展了一个“课程问答”模块和“直播预约”功能。直播预约其实就是在课程详情里加一个预约活动到点给用户发站内提醒本质仍然是一个状态机问题和订单状态流转的思路完全一致。这也说明只要你把基础架构搭扎实后续业务的扩展真的会越来越顺手。如果你正在写自己的教育平台项目建议别停留在“能跑就行”的阶段多把时间花在接口健壮性、权限设计和部署易用性上这些才是真正能让面试官或者团队同事眼前一亮的细节。
返回列表