做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这个选择可能有人觉得老,但在在线教育场景里,课程列表需要多表联查,订单统计需要写复杂SQL,MyBatis的XML可以让你精确控制每一条SQL,不像JPA那样被自动生成的查询绕来绕去。下面是几个常见对比,你可以按自己项目的情况参考:
| 对比项 | MyBatis | Spring Data JPA |
|---|---|---|
| SQL控制力 | XML手写,适合复杂查询 | JPQL封装,复杂查询较绕 |
| 学习成本 | 需要理解Mapper和XML映射 | 上手快,但深水区多 |
| 性能调优 | SQL可见,DBA好介入 | 需要理解Hibernate缓存机制 |
| 团队招聘 | 传统项目广泛使用 | 新项目也常见,视团队而定 |
MySQL就没什么争议了,社区版免费、资料多、云厂商支持完善,对中小型在线教育系统来说性能完全够用。真正要注意的是建表时别偷懒,字符集用utf8mb4,订单金额用DECIMAL,索引根据查询条件来加,这些我在后面数据库章节会详细讲。
1.3 整体架构与请求流转
这套源码的项目结构我建议按下面这个思路理解:Nginx负责托管前端静态页面,并把/api开头的请求反代到SpringBoot服务,后端通过MyBatis访问MySQL。前端和后端通过JSON交互,严格分离。
具体一次请求的流转过程是:用户在浏览器点击“课程列表”,Vue3页面调用封装好的axios方法,请求地址是/api/course/list,Nginx把请求转发到SpringBoot的Controller,Controller调用Service,Service调用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_at |
| course | 课程表 | id, title, cover, price, lecturer_id, category_id, status, deleted |
| course_lesson | 章节表 | id, course_id, parent_id, title, video_url, duration, sort |
| orders | 订单表 | id, order_no, user_id, course_id, amount, pay_status, paid_at |
| course_user_rel | 用户课程表 | id, user_id, course_id, expires_at, created_at |
| study_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`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 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 Result<PageResult<CourseVO>> list(@RequestParam Integer pageNum, @RequestParam Integer pageSize, @RequestParam(required = false) Long categoryId) { PageHelper.startPage(pageNum, pageSize); List<CourseVO> list = courseMapper.selectCourseList(categoryId); PageInfo<CourseVO> 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工程从零搭建
有些同学拿到后台系统项目喜欢直接用若依这类脚手架,但我自己实际体验下来,若依的Vue3+TS版本对新人并不友好,导入IDEA之后经常冒出一堆类型报错,还得去调ESLint和依赖版本。与其花时间解决框架问题,不如从零搭一个精简的Vue3工程,更利于理解每个依赖是干什么用的。
第一步直接Vite初始化:npm create vite@latest 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-server=utf8mb4 collation-server=utf8mb4_general_ci不要在每张表建完之后再单独改字符集,到时候字段和表层级不一致,查中文偶尔会出乱码。还有时区问题,如果后端连接串里没有设置时区,经常会报The server time zone value 'Öйú±ê׼ʱ¼ä'这类错误。解决方式是在JDBC连接串上加serverTimezone=Asia/Shanghai。
初始化数据库很简单,把源码里的edu.sql用source命令执行,或者用Navicat直接导入。建完库后先用SHOW TABLES;确认一下表数量,再查一下course表有没有测试数据,避免后续接口查出来全是空的,还以为自己代码写错了。
5.2 SpringBoot打包与生产配置
后端部署第一步是改配置。建议把配置拆成application-dev.yml和application-prod.yml,生产环境用环境变量区分,启动命令带上--spring.profiles.active=prod。数据库地址、密码、日志级别都不要硬编码,可以通过环境变量注入。
打包命令很简单:
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是不是ALL,key有没有命中索引。还有一个常见的低级错误:update语句误写了WHERE 1=1,导致全表更新,这种事故在测试环境出一次就能长记性。
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缓存、消息队列推送课程通知、视频断点续传这几个方向扩展,每一个都是能写进简历的亮点。