在线教育系统全栈实战:SpringBoot+Vue3+MyBatis+MySQL完整项目解析
做Java全栈这些年在线教育系统一直是个很耐打的项目方向它不单是把SpringBoot、Vue3、MyBatis、MySQL这几个词拼在一起而是把用户、课程、订单、学习进度这些真实业务串成完整闭环技术栈覆盖面恰好也是招聘市场最常问的那一批。这篇博文我以一套前后端分离的在线教育系统源码为例从架构设计到数据库表结构从后端关键代码到前端联调再到部署上线把完整链路拆开讲一遍。如果你正在准备毕业设计或者想给自己攒一个能扛住面试追问的项目这篇文章可以直接当手册用。1. 项目核心与整体设计1.1 在线教育系统到底在解决什么问题很多新手拿到这种项目会先纠结“我该写几个页面”但真正的问题不在页面数量而在于业务是否跑得通。在线教育系统的核心是让课程从“线下交付”变成“线上交付”并且让这个过程可售卖、可追踪、可运营。它至少覆盖三类角色学生要能注册登录、浏览课程、下单购买、观看视频、记录进度讲师要能上传课程、维护章节、查看学员数据管理员要能审核课程、管理订单、处理异常数据。这三类需求叠在一起就导致系统必须同时具备用户中心、课程中心、支付订单、学习记录、后台管理五个模块。这几个模块不是孤立存在的。学生下单之后服务端要把订单状态改成已支付同时给学生开通对应课程的访问权限学生开始看视频后前端要定时把学习进度上报到后端下次进入课程可以继续从上次位置播放讲师上传章节时又要调用文件上传接口把视频和封面存起来。这些交互才是在线教育系统真正有价值的地方也是面试官最喜欢追问的细节。从学习角度来看这个项目也很适合用来串知识点。它既有SpringBoot的接口开发、MyBatis的数据访问又有Vue3的组件化页面和路由权限控制还涉及MySQL的表设计和索引优化。做完这一套你对前后端分离的理解会比单纯刷八股文深得多。1.2 技术选型为什么是这四个先聊SpringBoot。在线教育系统有很多查询、事务、定时任务之类的需求SpringBoot的自动装配和starter机制能省掉大量重复配置一个内嵌Tomcat就能把服务跑起来。相比SSH或者纯Servlet时代SpringBoot把开发重心拉回到业务逻辑上。Vue3的优势在于组合式API。以前Vue2写业务逻辑容易把一段功能拆散在data、methods、computed里Vue3的setup函数可以让相关代码聚在一起可读性明显更好。配合Vite开发服务器热更新速度快到基本不用等待这对调试课程列表、章节树这种多级联动页面非常友好。MyBatis这个选择可能有人觉得老但在在线教育场景里课程列表需要多表联查订单统计需要写复杂SQLMyBatis的XML可以让你精确控制每一条SQL不像JPA那样被自动生成的查询绕来绕去。下面是几个常见对比你可以按自己项目的情况参考对比项MyBatisSpring Data JPASQL控制力XML手写适合复杂查询JPQL封装复杂查询较绕学习成本需要理解Mapper和XML映射上手快但深水区多性能调优SQL可见DBA好介入需要理解Hibernate缓存机制团队招聘传统项目广泛使用新项目也常见视团队而定MySQL就没什么争议了社区版免费、资料多、云厂商支持完善对中小型在线教育系统来说性能完全够用。真正要注意的是建表时别偷懒字符集用utf8mb4订单金额用DECIMAL索引根据查询条件来加这些我在后面数据库章节会详细讲。1.3 整体架构与请求流转这套源码的项目结构我建议按下面这个思路理解Nginx负责托管前端静态页面并把/api开头的请求反代到SpringBoot服务后端通过MyBatis访问MySQL。前端和后端通过JSON交互严格分离。具体一次请求的流转过程是用户在浏览器点击“课程列表”Vue3页面调用封装好的axios方法请求地址是/api/course/listNginx把请求转发到SpringBoot的ControllerController调用ServiceService调用Mapper接口Mapper执行XML里定义的SQL查询MySQL结果再一层层返回。前端拿到数据后渲染成卡片列表。这种分离架构最大的好处是前端和后端可以并行开发。后端用Swagger或者Apifox把接口文档定义好前端可以先写页面等联调阶段再对接真实接口。后期如果要把项目扩展成小程序端或者App端后端接口可以直接复用不用重写业务逻辑。2. 数据库设计与核心业务表2.1 业务模块拆解与数据流拿到在线教育系统源码建议先把数据流画出来再去看代码。一条完整的主线是学生注册登录后进入课程列表页挑选课程点击购买生成订单模拟支付成功之后系统给该学生开通对应课程权限然后学生进入课程详情页按章节观看视频前端每隔一段时间上报学习进度管理员登录后台可以上架、下架课程查看订单列表。为了支撑这条主线库里需要这几类表用户表、课程表、章节表、订单表、学习记录表、用户课程关联表。有些项目会把评论也加进来那还要有评论表。面试时你把这几个表之间的关系讲清楚基本就能证明你不是只写了个登录注册。有一点容易忽略订单支付成功后不要每次都通过查订单来判断学生有没有权限。更好的做法是单独建一张用户课程表比如course_user_rel支付成功后插入一条记录查询权限时直接扫这张表。这样后续做“我的课程”列表时只需要一次关联查询订单表也不会越来越臃肿。2.2 核心表的字段设计与建表SQL下面是我认为一个在线教育系统源码里最核心的几张表字段都是最简可用版本表名用途关键字段member用户表id, username, password, salt, role, status, created_atcourse课程表id, title, cover, price, lecturer_id, category_id, status, deletedcourse_lesson章节表id, course_id, parent_id, title, video_url, duration, sortorders订单表id, order_no, user_id, course_id, amount, pay_status, paid_atcourse_user_rel用户课程表id, user_id, course_id, expires_at, created_atstudy_record学习记录表id, user_id, course_id, lesson_id, progress, updated_at建表时有几个细节值得留意。第一个是用户表密码不要存明文要存加盐后的哈希值字段可以用password和salt两个字段或者直接集成BCrypt密码字段长度设置64以上。第二个是金额字段必须用DECIMAL(10,2)不要用DOUBLE或FLOAT浮点数在金额计算时会出现精度问题。第三个是创建时间和更新时间。建议统一使用created_at和updated_at类型用datetime默认值不要写死在SQL里而是在数据库层设置DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这样代码里就算忘了set时间数据库也能自动维护。这里给出课程表的完整建表语句可以作为源码里其他表的设计基准CREATE TABLE course ( id bigint NOT NULL AUTO_INCREMENT, title varchar(128) NOT NULL COMMENT 课程标题, subtitle varchar(255) DEFAULT COMMENT 副标题, cover varchar(255) DEFAULT COMMENT 封面图地址, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 售价, lecturer_id bigint NOT NULL COMMENT 讲师ID, category_id bigint DEFAULT NULL COMMENT 分类ID, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0下架1上架, deleted tinyint NOT NULL DEFAULT 0 COMMENT 软删除标记, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_category (status, category_id), KEY idx_lecturer (lecturer_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT课程表;很多人习惯用is_delete做逻辑删除我觉得没问题但要注意所有查询SQL里都记得加deleted 0条件不然很容易把已删除的数据查出来。源码里如果用了MyBatis可以在XML的公共SQL片段里统一加上这个条件避免每个查询都重复写。2.3 索引、事务与冗余设计索引不是越多越好而是要根据实际查询来设计。在线教育系统里高频查询有几种课程列表按分类和状态筛选所以course表可以建status category_id联合索引订单查询按用户查orders表要建user_id索引订单号要保证唯一直接建唯一索引uk_order_no。学习进度表经常按user_id course_id查建议建联合索引。写SQL时不要对索引列做函数运算比如WHERE DATE_FORMAT(created_at, %Y-%m-%d) 2024-01-01就废了索引应该改成范围查询created_at 2024-01-01 00:00:00 AND created_at 2024-01-02 00:00:00。这些小细节在慢查询日志里一抓一个准公司里最容易被DBA点名。事务设计主要围绕下单链路。学生点击购买之后至少要执行三步插入订单记录、支付成功后更新订单状态、写入用户课程关联表。这三步必须放在同一个事务里否则会出现“钱付了但课程没开通”的严重问题。源码里一般在订单Service上加Transactional然后把状态更新写在事务内。关于冗余字段我建议在业务最频繁读取的页面上适当冗余。比如课程列表页需要显示讲师姓名如果每次都去联查member表多表联查会让SQL变复杂。可以在course表冗余一个lecturer_name字段讲师改名的时候同步更新。对中小型系统来说这种以读为主的冗余非常实际。3. 后端源码结构与关键实现3.1 后端分层与通用组件拿到这套源码第一步先看包结构我建议的分层是这样的controller放接口service放业务逻辑mapper放MyBatis接口entity对应数据库表vo给前端返回视图对象config放拦截器、跨域等配置common放统一返回体和异常处理。统一返回体很关键。一个标准的Result要包含code、message、data三个字段成功返回200业务异常返回自定义错误码前端根据code做统一拦截。不要图省事直接返回Map或者裸数据否则后续前端联调会和迷茫。全局异常处理用RestControllerAdvice配合ExceptionHandler把参数校验异常、业务异常、系统异常分开处理。登录鉴权这里很多在线教育源码用的是JWT方案。学生登录成功后后端生成一个token返回前端拿到后存在localStorage里每次请求在请求头带Authorization: Bearer token。后端写一个拦截器在preHandle里校验token校验通过就把用户信息放进ThreadLocal后续Service层可以从上下文里直接拿到当前用户ID避免每个接口都手动传userId。拦截器注册别忘了实现WebMvcConfigurer的addInterceptors方法同时用excludePathPatterns放行登录注册、课程列表、课程详情这些无需登录的接口。如果拦截范围太大前端第一次加载页面拿不到课程数据排查起来会很困惑。3.2 MyBatis缓存机制与SQL打印MyBatis的缓存经常被面试问到也经常在实际项目里踩坑。一级缓存是SqlSession级别的同一个SqlSession内两次查询相同SQL会命中缓存减少一次数据库查询。在Spring集成下每次请求通常对应一个SqlSession所以一级缓存的生命周期很短。二级缓存是namespace级别的也就是Mapper级别多个SqlSession可以共享但默认不开启。在线教育源码里课程列表这种变动不频繁的数据可以考虑开二级缓存但要注意只要课程表发生增删改对应的缓存就必须清掉否则用户会一直看到旧数据。实际开发中我反而不太推荐开二级缓存热点数据用Redis更可控普通数据靠MySQL查询就够了。如果你在源码里看到cache/标签优先确认它的淘汰策略不要盲目保留。SQL打印一定要在开发阶段打开配置方式是在SpringBoot的application.yml里加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会直接输出SQL语句和参数值排查SQL语法错误、参数占位符不匹配都非常直观。生产环境记得换成logback按级别输出不然每个查询刷一堆日志磁盘容易被拖垮。还有一个开发效率小技巧IDEA里装一个MyBatisX插件Mapper接口方法和XML高亮联动点击方法名直接跳转到对应SQL查XML里标签写没写错特别方便。很多新人报Invalid bound statement错误其实就是接口方法和XML里的id没对应上用这个插件基本都能避免。3.3 分页插件用法与列表接口在线教育系统的课程列表、订单列表、学习记录列表都涉及分页。最常用的方案是PageHelper用法非常简单查询前调用PageHelper.startPage(pageNum, pageSize)接下来执行的第一条MyBatis查询就会被自动拼接LIMIT。查询返回结果可以用PageInfo包装取出total总数和list列表。示例代码结构是这样的GetMapping(/course/list) public ResultPageResultCourseVO list(RequestParam Integer pageNum, RequestParam Integer pageSize, RequestParam(required false) Long categoryId) { PageHelper.startPage(pageNum, pageSize); ListCourseVO list courseMapper.selectCourseList(categoryId); PageInfoCourseVO pageInfo new PageInfo(list); return Result.success(new PageResult(pageInfo.getTotal(), pageInfo.getList())); }这里有几个坑必须记住。第一startPage只对紧随其后的第一条查询生效中间如果穿插了其他查询分页可能加错地方。所以不要在循环里调用startPage也不要在查询前做其他数据库操作。第二多表联查时PageHelper生成的count语句偶尔有问题如果发现总数不对可以用PageHelper.startPage(pageNum, pageSize, true)强制重写count。第三如果源码里用的是MyBatis-Plus分页机制不一样它需要通过PaginationInnerInterceptor拦截器配合selectPage方法使用别把两套分页混在一起。后端返回分页结构时不要直接把PageInfo整个返回给前端里面字段太多而且有很多前端用不到的信息。更规范的做法是自定义PageResult只返回total和list前端拿到这两个字段足够渲染分页组件了。3.4 登录鉴权与下单事务在线教育系统的下单接口是最容易被面试官盘逻辑的模块我建议你看源码时重点看这一段。先看下单流程前端传课程ID后端先校验课程是否存在、是否已上架再判断当前用户是否已经购买过该课程如果已购买直接返回“请勿重复购买”的提示。新订单生成时订单号不能随便用自增ID推荐用时间戳加随机数或者雪花算法生成因为订单号在很多场景下需要对外展示不能暴露业务量。支付环节在纯源码项目里一般是模拟的前端调用pay接口后端直接生成一条支付成功记录。真实项目中这里要对接支付宝或者微信支付回调地址必须做验签。但不管模拟还是真实支付代码里都要做幂等处理通过订单号查询订单如果状态已经是已支付直接返回成功不重复开通课程权限。下单和开通权限必须放在同一个事务方法里Transactional(rollbackFor Exception.class) public PayResult payOrder(String orderNo, Long userId) { Orders order ordersMapper.selectByOrderNo(orderNo); if (order null || !order.getUserId().equals(userId)) { throw new BizException(订单不存在); } if (order.getPayStatus() 1) { return PayResult.success(订单已支付); } int rows ordersMapper.updatePayStatus(orderNo, userId, 1); if (rows 0) { throw new BizException(订单状态更新失败请重试); } CourseUserRel rel new CourseUserRel(); rel.setUserId(userId); rel.setCourseId(order.getCourseId()); courseUserRelMapper.insert(rel); return PayResult.success(支付成功); }看到没有updatePayStatus的理想执行会影响一行这也是幂等的关键。如果多线程环境下对同一个订单重复支付数据库更新条件里带上pay_status 0就能保证只有一个请求能更新成功。这种用“条件更新”代替先查后改的思路放面试里也很有亮点。4. 前端Vue3工程与联调实战4.1 Vue3工程从零搭建有些同学拿到后台系统项目喜欢直接用若依这类脚手架但我自己实际体验下来若依的Vue3TS版本对新人并不友好导入IDEA之后经常冒出一堆类型报错还得去调ESLint和依赖版本。与其花时间解决框架问题不如从零搭一个精简的Vue3工程更利于理解每个依赖是干什么用的。第一步直接Vite初始化npm create vitelatest edu-web -- --template vue然后安装vue-router、pinia、axios、element-plus。工程目录我建议按功能划分api目录放接口请求文件views放页面组件components放公共组件router放路由store放全局状态utils放axios封装和处理工具。Element Plus的引入方式有两种全量引入简单但打包体积大项目规模大了之后最好改成自动按需引入。在线教育这种管理系统用到的组件其实不多无脑全量引入也能跑但如果你想把项目作为面试作品按需引入这个点可以提一嘴显得有性能意识。组合式API写起来和Vue2差别很大最大的变化就是把数据、方法、生命周期钩子统一放到setup里。刚开始可能不习惯但代码一旦超过两百行你就会发现相关逻辑放在一起维护起来确实比Vue2舒服很多。源码里如果看到ref和reactive记住一条准则基本类型用ref复杂对象用reactive。4.2 路由守卫与角色权限在线教育系统有学生、讲师、管理员三种角色前端必须控制不同角色能看到的页面。学生端能看到“我的课程”“学习中心”讲师端能看到“课程管理”管理员端能看到“订单管理”“用户管理”。这不能只靠隐藏菜单路由层面也要拦截否则用户手输路径就能看到无权限页面。实现方式是在路由配置里给页面加meta信息比如meta: { roles: [admin, teacher] }然后在全局路由守卫里判断当前用户角色router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.meta.roles) { const userInfo JSON.parse(localStorage.getItem(userInfo)) if (!to.meta.roles.includes(userInfo.role)) { next(/403) } else { next() } } else { next() } })这种静态路由配合角色判断的方式对中小型系统够用了。只要登录接口返回用户角色每次刷新页面都能从localStorage里恢复用户信息。动态路由、按钮级权限这些属于进阶玩法真有必要再上新方案不要一开始就把自己绕晕。要注意localStorage里的用户信息不能太敏感密码绝对不要存。如果不需要持久化用户信息也可以考虑用Pinia保存刷新页面后重新请求用户详情接口拉一次。4.3 Axios封装与跨域处理前端不能每个组件里都直接axios.get一定要封装一个实例这样遇到统一错误处理、token过期、接口地址调整只需要改一个文件。核心代码大致如下const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )跨域问题是前后端分离最常见的坑。开发环境下我建议用Vite的proxy配置在vite.config.js里把/api代理到http://localhost:8080这样浏览器请求的始终是同源地址不会触发跨域。生产环境由Nginx做反向代理一样是转发/api到后端服务也不需要后端开启CORS。后端的CORS配置可以作为兜底方案但要注意allowedOrigins别配成*否则携带token的请求会出问题。如果前端加了代理后端完全不需要配跨域两边只通过相对路径访问能省很多麻烦。4.4 核心页面逻辑课程列表、课程详情、播放页课程列表页是最容易打开门的页面。数据从后端分页接口拉取前端用一个pageNum和一个pageSize控制分页组件切换页码时重新调接口。筛选条件比如分类、价格区间是拼在请求参数里的注意参数为空的字段不要传给后端减少后端判断成本。课程详情页要展示课程信息、讲师信息、章节目录。章节表是树形结构有parent_id后端接口会返回一个多级列表。前端拿到数据后用递归组件渲染目录树或者先用循环把一级章节渲染出来点开一级章节再显示下面的课时列表。递归组件记得设置name否则组件自己调用自己会报错。播放页是学习记录上报的地方。视频用HTML5的video标签播放地址来自后端返回的视频URL。前端监听timeupdate事件每隔15秒或者30秒上报一次当前播放进度上报时带上courseId、lessonId、progress字段。进度从0到100记录的是百分比。续播功能就是进入页面时请求一次最近的播放记录把currentTime设置到对应进度位置。上报进度不建议每次timeupdate都发请求会刷爆接口最好做节流。面试时把“为什么用节流”这个点说出来对面基本能确认你写过真实项目。5. MySQL安装部署与上线流程5.1 MySQL安装、字符集和时区初始化本地调试先要有MySQL环境。Windows去官网下载安装包Linux用包管理器安装就可以比如Ubuntu上执行apt install mysql-server。安装完之后有两件事必须做一是确认端口3306可以被访问二是设置合适的字符集。在线教育系统肯定要存中文MySQL的字符集我建议直接在配置文件里统一设成utf8mb4[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_general_ci不要在每张表建完之后再单独改字符集到时候字段和表层级不一致查中文偶尔会出乱码。还有时区问题如果后端连接串里没有设置时区经常会报The server time zone value Öйú±ê׼ʱ¼ä这类错误。解决方式是在JDBC连接串上加serverTimezoneAsia/Shanghai。初始化数据库很简单把源码里的edu.sql用source命令执行或者用Navicat直接导入。建完库后先用SHOW TABLES;确认一下表数量再查一下course表有没有测试数据避免后续接口查出来全是空的还以为自己代码写错了。5.2 SpringBoot打包与生产配置后端部署第一步是改配置。建议把配置拆成application-dev.yml和application-prod.yml生产环境用环境变量区分启动命令带上--spring.profiles.activeprod。数据库地址、密码、日志级别都不要硬编码可以通过环境变量注入。打包命令很简单mvn clean package -DskipTests打完包后在target目录下会生成一个edu-server.jar用java -jar edu-server.jar就能启动。生产服务器上为了让服务保持后台运行可以用nohup或systemd。这里插一个大家常问的点如果只拿到一个jar包要怎么反编译看源码工具可以用IDEA自带的反编译功能或者jd-gui能恢复大部分Java代码但注释、配置、资源文件可能会丢MyBatis的XML文件尤其容易看不到。反编译只能用来应急阅读不能当作项目维护的常规手段。源码管理还是要靠Git仓库别为了图省事丢掉原始工程。5.3 前端构建与Nginx部署前端部署前先执行npm run build生成dist目录。然后把dist里的文件放到服务器的Web目录由Nginx托管。Nginx配置里有两个关键点第一个是Vue3使用history路由模式后刷新页面会404需要在location /里加try_files $uri $uri/ /index.html;让所有未知路径都回退到入口页。第二个是把/api反向代理到后端服务server { listen 80; server_name edu.example.com; root /var/www/edu-web; index index.html; location / { 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; } }配置完执行nginx -t校验语法没问题再nginx -s reload生效。整套部署完成之后在浏览器访问域名正常能看到登录页面就算成功。如果接口报404先看代理路径有没有拼错如果报500大概率是后端数据库配置或者Mapper映射问题。6. 高频问题排坑与面试考点梳理6.1 MyBatis常见坑和SQL性能问题开发在线教育系统时最常碰到的错误是Invalid bound statement (not found)。这类错误通常有四个原因Mapper接口方法名和XML里id不一致、XML文件没放在resources对应目录、Maven没有把XML打包进classpath、接口类上忘了加Mapper或者没有配MapperScan。如果启动时没有报错调用才报这个错优先检查XML文件是否在target/classes目录下。另一个高频问题是我Batis缓存导致的数据不一致。如果系统开启了二级缓存后台管理员修改了课程标题用户端看到的可能还是旧标题。解决方法要么关闭二级缓存只依赖Redis要么在增删改SQL后显式清缓存。对在线教育系统来说我更推荐后者都不开直接用MySQL扛住简单查询。关于“MyBatis update执行慢”的问题通常不是MyBatis本身慢而是SQL逻辑有问题。比如更新语句没走索引或者更新时锁住了大量行。排查方法是打开慢查询日志把执行时间超过1秒的SQL捞出来用EXPLAIN看执行计划重点看type是不是ALLkey有没有命中索引。还有一个常见的低级错误update语句误写了WHERE 11导致全表更新这种事故在测试环境出一次就能长记性。6.2 前后端联调高频报错联调阶段最烦的是跨域和401。跨域问题我在4.3已经讲过了这里补充一个排查思路打开浏览器DevTools的Network面板看请求到底是发出去没收到响应还是压根没有发出。前者是后端CORS或者代理配置问题后者是前端axios配置或拦截器阻断。401报错通常是因为token过期或者根本没有传token。前端要统一处理当response拦截器捕获到401清除本地token跳转登录页然后提示用户重新登录。注意不要在拦截器里写死跳转window.location.href否则会刷新页面丢失所有全局状态应该用Vue Router实例做跳转。还有一个容易忽略的问题后端返回的时间格式。Java默认的时间格式有时候是时间戳有时候是2024-01-01T12:00:00前端直接展示很难看。建议在application.yml里统一配置Jackson的日期格式或者后端把LocalDateTime转成前端约定的字符串格式再返回。6.3 安全加固的几个必要动作在线教育系统涉及用户数据和支付信息安全不能不做。最少要搞定四件事密码加密、SQL注入防护、XSS过滤、接口防刷。密码加密用BCrypt不要自己写MD5加盐。BCrypt每次生成的哈希值都不同存储长度要留足。SQL注入防护这块MyBatis的#{}会自动使用预编译但如果你图省事写了${}做字符串拼接就可能有注入风险。源码里如果出现${}一定确认传入参数是内部可信数据否则改成#{}。前端防XSS的重点是富文本和用户输入内容课程评论、课程简介都可能注入脚本。后端对输入参数要做白名单校验输出到前端时要转义。SpringBoot项目可以在全局加一个过滤器对请求参数里的script、javascript:等关键词做过滤当然这只处理了一层真正的富文本必须用专门的XSS过滤组件。接口防刷比较轻量的做法是IP维度的限流比如同一个IP一分钟内最多请求10次登录接口超过就拒绝。另外视频播放地址不要直接给永久有效的URL可以生成带签名的临时播放地址过期自动失效这样能防止别人把视频地址抄走。6.4 面试官常问的项目问题清单如果你是拿这套在线教育系统去面试下面这些问题最好提前准备为什么用Redis如果项目里没引入Redis你可以说热点课程列表用Redis缓存缓存穿透怎么解决缓存和数据库一致性怎么处理。分页插件的工作原理是什么要说出MyBatis拦截器通过动态代理拦截Executor在执行查询前改写SQL拼接LIMIT。MyBatis的一级缓存和二级缓存区别二级缓存存在什么问题一般回答完就会把自己引到“实际项目中开Redis更可控”这个结论上。JWT和传统Session有什么区别JWT无状态、天然适合前后端分离但退出登录和主动失效比较麻烦。在线教育系统对安全要求不算极端JWT够用。下单接口怎么设计把第3.4节里的流程讲清楚再加上幂等条件更新这个回答基本就能过关。事务失效的场景有哪些常见的有Transactional加在非public方法上、方法内部自己调用不走代理、异常被catch吞掉、rollbackFor设置不对。面试官最爱听这些源码级的坑。数据库索引怎么设计在线教育系统的课程表、订单表、学习记录表分别有哪些索引为什么要用联合索引这道题背完理论再结合自己的表结构回答比空谈B树扎实得多。写在最后这套在线教育系统源码我前后带团队做过多个版本最深的感觉是项目本身不难难的是把技术栈和业务场景真正融合起来。框架代码大家都能跑拉开差距的是你是否想清楚了为什么这样设计表、为什么这样处理事务、为什么前端要节流上报学习进度。面试的时候把细节讲透比背一百道八股都管用。最后分享两个实用小习惯一是开发阶段用Apifox维护接口文档后端写完接口顺手测试前端也能实时看到示例返回联调效率高很多。二是本地开发记得开慢查询日志和SQL打印很多诡异问题其实看一眼真实SQL就有答案。这个系统后续如果想继续深挖可以往Redis缓存、消息队列推送课程通知、视频断点续传这几个方向扩展每一个都是能写进简历的亮点。