尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Spring Boot与Vue的在线教育个性化推荐系统实战

发布时间:2026/9/10 17:26:54

资讯中心
01
ARTICLE

基于Spring Boot与Vue的在线教育个性化推荐系统实战

基于Spring Boot与Vue的在线教育个性化推荐系统实战
做在线教育平台最头疼的事情往往不是把课程录好、上架而是用户进来之后看到一屏又一屏的课程列表根本不知道从哪下手。尤其当平台课程数量超过几百门之后用户停留时间和完课率会明显下降这不是内容质量问题是“找课成本”太高。我前前后后参与过几个教育类项目对这个痛点感触很深。所以做一个带有个性化推荐能力的在线教育系统并不是为了炫技而是真实业务需要。这篇内容围绕“基于Spring Boot与Vue的在线教育个性化推荐系统”这个项目来写适合正在做毕业设计、准备秋招项目经历或者中小企业想给自家网课平台增加推荐能力的开发者参考。我会把推荐引擎的落地思路、前后端的关键实现、部署联调过程中的坑以及一些在搜索引擎里被高频搜索的注意事项都过一遍。代码层面不会贴完整工程但核心逻辑和关键配置我会交代清楚保证你照着能搭出自己的版本。1. 先聊清楚这个推荐系统到底要解决什么问题1.1 在线教育平台里的“推荐”和电商推荐不是一回事很多人一听个性化推荐第一反应就是电商的“猜你喜欢”。但教育场景有个很明显的特点用户的学习行为是低频、长周期、强目的性的。一个用户可能一个月就学两三门课每次学习时长半小时到两小时不等不像电商那样有高频点击、加购、下单的丰富行为流。所以直接把电商那套协同过滤搬过来效果往往很差。在这个项目里我定义推荐目标是帮助用户更快找到适合当前学习阶段和兴趣方向的课程提升完课率和学习时长。推荐结果不是越多越好而是要精准、克制。首页推荐位大约展示12到20门课分“猜你喜欢”“同类用户也在学”“本分类热门”几个模块。1.2 系统的核心角色和业务边界系统按使用角色划分核心是三种学生端前台注册登录、浏览课程、搜索、观看视频、记录学习进度、收藏/评分课程、查看个性化推荐。教师/管理员端后台课程管理、视频上传与转码状态查看、分类管理、用户管理、推荐效果数据看板。系统端推荐引擎离线计算用户画像和课程相似度在线提供推荐接口实时接收学习行为事件。我在项目里把推荐引擎做成了独立模块没有跟业务CRUD代码混在一起。好处是后面替换算法、调整参数都不需要动业务代码这也是企业级项目里推荐系统的常见做法。1.3 冷启动问题在项目里的定位推荐系统绕不开冷启动新用户没有行为数据新课没有用户反馈怎么办这个项目里我用了一套组合策略新用户先按注册时选择的兴趣标签推分类热门再逐步加入协同过滤结果。新课内容相似度兜底按标题、分类、标签做基于内容的推荐保证新课也能被曝光。行为稀疏用户用热门榜兜底同时降低推荐列表刷新频率避免用户每次进来看到完全不同的内容。这套策略不复杂但很实用。比起一上来就堆深度学习模型先用规则把体验稳住才是真实项目应该有的节奏。2. 技术选型与系统骨架为什么是Spring Boot Vue2.1 前后端分离是现阶段最稳妥的选型选Spring Boot Vue与其说是在“选技术”不如说是选了一个生态最成熟、招人最容易、踩坑资料最多的组合。Spring Boot负责后端接口、业务逻辑、推荐算法服务化Vue负责前端SPA应用两者通过RESTful API交互天然适合前后端分离开发。前后端分离不只是部署形态问题更重要的是团队协作模式的改变。前端不用等后端把页面模板写好后端不用关心浏览器渲染细节。在这个项目里我和前端同学约定好接口文档用Swagger实时维护两边并行开发联调效率非常高。如果是单人开发前后端分离可以让注意力更聚焦写后端时专心抠接口和数据结构写前端时专心抠交互和状态管理。2.2 项目整体的模块划分我把系统按Maven多模块方式组织这里给出结构供参考online-edu-recommend/ ├── edu-common/ // 通用工具、统一返回结果、异常处理 ├── edu-admin/ // 后台管理端接口模块 ├── edu-portal/ // 学生前台端接口模块 ├── edu-recommend/ // 推荐引擎核心模块 ├── edu-video/ // 视频上传转码对接模块 └── sql/ // 数据库初始化脚本前端部分基于Vue 3 Vite Pinia Vue RouterUI库选了Ant Design Vue。Vite的开发体验比Webpack轻快很多项目启动和热更新速度对日常开发幸福感提升非常明显。2.3 数据库和中间件怎么选型数据存储的核心是MySQL存储用户、课程、分类、学习记录、评分等结构化数据。课程视频文件本身不直接入库存的是转码后的M3U8地址和封面图URL。缓存用Redis承担三个职责会话Token存储、推荐结果缓存、学习行为消息队列。这里我特别要提的是Redis Stream项目里学习行为的上报比如用户看了哪节课、看了多久、有没有收藏都是通过Redis Stream异步处理的而不是直接写数据库。原因很简单学习行为产生的频率远高于课程查询如果每个播放进度都同步写库数据库压力会非常大。Redis Stream的消费端在Spring Boot里实现时核心是使用StreamMessageListenerContainer注册消费者组通过XREADGROUP方式拉取消息。网上很多人搜“spring boot redis stream 如何拉取队列消息”其实就是这个场景。下面给一个核心配置片段Bean public StreamMessageListenerContainerString, MapRecordString, Object, Object streamMessageListenerContainer( RedisConnectionFactory connectionFactory) { StreamMessageListenerContainer.StreamMessageListenerContainerOptionsString, MapRecordString, Object, Object options StreamMessageListenerContainer.StreamMessageListenerContainerOptions .builder() .pollTimeout(Duration.ofSeconds(2)) .batchSize(50) .build(); return StreamMessageListenerContainer.create(connectionFactory, options); }然后为每个消费者组注册监听器处理学习行为事件异步落库到学习记录表。这样做不仅削峰还能保证推荐引擎用的行为数据是异步聚合好的不阻塞业务主流程。2.4 统一返回结构和异常处理别在最基础的地方翻车这个项目里我给所有接口定义了一个统一的ResultT结构包含code、message、data三个字段。配合全局异常处理器RestControllerAdvice业务异常、参数校验异常、未知异常都能返回统一格式。这一步看似简单实际联调时能省掉大量扯皮。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; } }前端axios响应拦截器里直接判断code不等于200就统一弹错误提示不需要每个页面单独写错误处理。这是所有前后端分离项目都应该有的基础规范很多新手项目忽视这一点后面联调时痛不欲生。3. 推荐引擎核心从用户行为到TopN课程怎么算出来3.1 用户行为数据的采集范围推荐引擎的原料是行为数据所以第一步要把行为定义清楚。这个项目里采集以下行为学习时长视频播放器每隔15秒上报一次进度记录课程ID、用户ID、播放秒数、总时长。完课事件视频播放到95%以上视为完课。收藏课程显式正反馈权重最高。评分1到5分显式反馈权重最高。搜索关键词记录用户最近搜索词用于短期兴趣识别。课程浏览用户点击课程详情视为轻度兴趣。每类行为在用户画像里有不同权重。我的经验是显式行为权重是隐式行为的5到10倍学习时长要折算成相对值播放比例而不是绝对值否则长视频天然占便宜。3.2 基于用户的协同过滤找相似的人协同过滤是推荐系统最经典的方法。基于用户的协同过滤核心思想找到和目标用户兴趣相似的其他用户把这些用户喜欢的、目标用户没学过的课程推荐给他。工程化落地时核心步骤分三步构建用户-课程评分矩阵行是用户列是课程值是行为加权分。学习完成算5分收藏算4分评分按实际值浏览算1分。计算用户相似度用余弦相似度或皮尔逊相关系数。项目里我用余弦相似度计算简单、效果稳定。生成推荐列表取TopK相似用户加权聚合他们学过且目标用户没学过的课程按预测分数排序。公式上用户u和用户v的余弦相似度是sim(u,v) sum(r_ui * r_vi) / (sqrt(sum(r_ui^2)) * sqrt(sum(r_vi^2)))预测用户u对课程i的评分pred(u,i) sum(sim(u,v) * r_vi) / sum(|sim(u,v)|)这些计算在离线任务里跑。课程数量几百门时直接用Python脚本算好写入MySQL推荐结果表就行如果课程过万就要考虑Spark或向量化计算了。这个项目的数据量用不到Spark但我在设计时把相似度矩阵存成了Redis Hash方便后续扩展。3.3 基于内容的推荐解决冷启动和长尾问题协同过滤解决“热门推热门”很容易但新课程和新用户就没办法。基于内容的推荐不依赖用户行为只看课程自身特征。课程表里维护了分类ID、标签列表、标题、简介。构建课程画像时把标签通过TF-IDF方式向量化然后用余弦相似度计算课程之间的相似度。对一个新用户如果注册时选了“Java”和“Spring Boot”两个兴趣标签系统就找出同时命中这两个标签的课程按分类热门度排序。实现上其实就是SQL加一点逻辑SELECT c.* FROM course c JOIN course_tag ct ON c.id ct.course_id WHERE ct.tag_id IN (SELECT tag_id FROM user_interest WHERE user_id ?) GROUP BY c.id ORDER BY c.study_count DESC LIMIT 20;基于内容和基于用户的协同过滤两者结果做加权融合这个项目里权重给的是内容0.4、协同过滤0.6。这个比例不是拍脑袋我先后跑过0.3/0.7、0.5/0.5、0.4/0.6三组离线评测按推荐列表的点击率和收藏率对比0.4/0.6在测试集上表现最好。当然这个数值换一个数据集就要重新调不要直接照搬。3.4 推荐结果缓存与更新策略推荐结果不能每次请求都现算否则数据库和CPU都扛不住。我的策略是离线任务每天凌晨计算一次全量用户推荐写入recommendation_result表。在线接口优先查Rediskey格式为recommend:user:{userId}缓存时间2小时。用户产生新的关键行为收藏、评分后主动删除该用户的推荐缓存触发重新计算。这样用户短期内看到的内容是稳定的不会因为一两次点击导致推荐列表大换血体验会舒服很多。4. 后端关键实现课程、学习记录与消息异步处理4.1 Spring Boot工程里推荐的接口怎么设计前端首页推荐流只需要一个接口GET /api/portal/recommend/courses参数是当前页码和每页数量。返回结构里包含课程ID、标题、封面、分类名、标签、学习人数、评分。控制层代码大致是这样RestController RequestMapping(/api/portal/recommend) public class RecommendController { Resource private RecommendService recommendService; GetMapping(/courses) public ResultPageResultCourseVO recommendCourses( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 12) Integer size, RequestAttribute(userId) Long userId) { return Result.success(recommendService.recommend(userId, page, size)); } }userId是JWT拦截器解析Token后放进去的业务层直接拿。推荐服务内部流程是先查Redis缓存命中则返回未命中则从推荐结果表分页查询如果表里没有新用户走基于兴趣标签的热门课程兜底SQL。4.2 视频模块M3U8播放地址的生成与管理在线教育系统离不开视频播放。项目里视频不是直接用MP4文件地址而是经过转码后生成M3U8索引文件加TS分片。这样做的好处是支持HLS协议iOS和Android都能播而且可以按清晰度切换。前端播放M3U8主流方案是video.js加videojs-contrib-hls插件或者轻量的hls.js。如果只有Web端用hls.js就够而且包体积小很多。项目我是Web端为主所以用了hls.jsimport Hls from hls.js; function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(url); hls.attachMedia(videoElement); hls.on(Hls.Events.MANIFEST_PARSED, () { videoElement.play(); }); } else if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // Safari原生支持 videoElement.src url; videoElement.addEventListener(loadedmetadata, () videoElement.play()); } }后端在保存课程视频时记录M3U8文件的完整URL。注意M3U8里引用的TS分片地址如果是相对路径播放器会基于M3U8所在目录拼接这里要保证分片文件和索引文件在同一层目录结构下否则会出现视频加载失败但网络请求200的诡异问题。4.3 学习进度上报与断点续播视频播放页需要记录用户看到第几秒下次进来接着播。我设计的接口是POST /api/portal/study/progress请求体包含courseId、videoId、playSecond、totalSecond、isFinished。这个接口里做了两个关键动作判断当前用户是否有该课程的学习记录没有则创建有则更新播放进度和最近学习时间。把事件写入Redis Stream用于推荐引擎的异步用户画像更新。断点续播的查询接口是GET /api/portal/study/progress/{videoId}返回上次播放秒数前端视频初始化后通过currentTime跳转。这个体验做得好不好直接影响用户对平台专业度的感知建议优先实现。4.4 Spring Boot 2.6 集成Springfox 3.0.0的冲突处理这个项目里有个绕不开的坑网上大量教程用的是Springfox 3.0.0来生成Swagger接口文档但Spring Boot 2.6版本开始Spring MVC的路径匹配策略从AntPathMatcher改成了PathPatternParser两者不兼容启动时直接报错java.lang.IllegalStateException: Spring.factories entries: org.springframework.boot.autoconfigure.解决办法是在application.yml里显式切换回旧策略spring: mvc: pathmatch: matching-strategy: ant_path_matcher这个配置我不会建议长期保留升级到springdoc-openapi兼容OpenAPI 3才是正道但如果只是做项目或短期上线一行配置就能救急。我在项目初期就被这个坑卡了半天报错信息在搜索引擎里全是英文讨论排查成本不低。4.5 Tomcat部署的两种方式Spring Boot项目部署到Tomcat有两条路。第一是内嵌Tomcat直接mvn package打成JAR包java -jar跑起来这是默认方式。第二是打成WAR包放到外置Tomcat的webapps目录下。打成WAR包需要三步修改pom.xml的packagingwar/packaging。排除内嵌Tomcat依赖scopeprovided/scope。启动类继承SpringBootServletInitializer并重写configure方法。如果只是自己用我建议直接JAR包方式省心。外置Tomcat的好处是可以复用已有的Tomcat运维体系比如用catalina.sh做托管适合已有服务器的团队。5. 前端Vue的落地细节推荐流、视频播放与状态管理5.1 Vue项目初始化与环境配置前端工程基于Vue 3 Vite创建npm create vitelatest edu-web -- --template vue cd edu-web npm install npm install vue-router4 pinia axios ant-design-vue hls.js开发环境需要配置Vite代理解决跨域问题。在vite.config.js里export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这里有个细节changeOrigin必须设为true否则后端接口接收到的请求头里的Host还是前端地址部分鉴权逻辑会出现诡异问题。生产环境部署时前端打包成dist目录放到Nginx由Nginx配置/api反向代理到后端服务跨域问题同样解决。5.2 首页推荐流的实现卡片列表与分流加载首页推荐区域我用三个模块展示顶部Banner轮播位放运营推荐课中间“猜你喜欢”放个性化推荐底部“大家都在学”放热门榜。推荐接口返回的是分页数据前端用无限滚动加载。每个课程卡片展示封面、标题、分类标签、学习人数、评分。课程封面的懒加载用v-lazy指令或者IntersectionObserver实现图片一旦多了懒加载对页面性能的提升非常明显。5.3 Vue Router路由和登录守卫路由配置里需要区分公开页面和登录页面。课程列表、课程详情可以公开访问但学习中心、个人中心必须登录。Vue Router 4的全局前置守卫这样处理router.beforeEach((to, from, next) { const token localStorage.getItem(edu_token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });登录后跳回原页面的逻辑靠query.redirect实现这个细节对用户体验影响很大。很多项目忽略了这一步用户登录完直接丢回首页之后还要手动找刚才想看的课程特别影响转化。5.4 用computed处理推荐列表的衍生数据前端拿到推荐列表后有时候需要在前端做二次加工。比如根据课程分类对推荐结果做分组展示或者对评分字段做格式统一。用Vue的computed来处理比在组件里写一堆watch干净得多。const recommendGrouped computed(() { const groups {}; props.recommendList.forEach(course { const category course.categoryName || 其他; if (!groups[category]) { groups[category] []; } groups[category].push(course); }); return groups; });注意computed的缓存特性只有依赖的响应式数据变化时才会重新计算。如果列表数据量大这个特性还能避免不必要的重复计算。5.5 前端调试Vue DevTools这个钱不能省做Vue项目前端调试Vue DevTools插件基本是必备的。它最大的价值不是看组件层级而是直接查看组件的props、data、computed实际值快速定位“页面空白但接口有数据”的问题。查看Vuex/Pinia状态变化调试登录态、权限控制逻辑非常直观。时间旅行调试回放状态变更过程。Vue DevTools可以从Chrome应用商店安装如果安装不了参考项目的GitHub仓库用开发者模式加载vue-devtools的shells/chrome目录即可。5.6 两个前端开发中容易卡住的现象项目开发里有两件事让前端同学特别头疼我在这里一并说了。第一是npm依赖安装慢或卡住。解决办法是换镜像源npm config set registry https://registry.npmmirror.com。装完之后确认一下是否生效npm config get registry应该返回镜像地址。第二是Vite启动时卡在“98% after emitting CopyPlugin”不动。这个百分比看着吓人其实大部分情况不是死循环而是打包插件在做文件复制或压缩等待时间较长。如果项目引用了大量静态资源首次打包几十秒到一两分钟都正常。但如果每次改动都卡住很长时间要检查是不是有构建插件配置了不合理的glob规则把node_modules里的文件也纳入copy范围了。6. 部署联调和那些值得记录的坑6.1 前后端联调时最容易忽视的接口文档维护前后端分离后接口文档就是双方的契约。项目里我用Swagger自动生成接口文档在Controller方法上写清楚ApiOperation描述和参数含义。但这还不够我强烈建议接口变更时前端负责人要在群里同步一份变更说明因为Swagger文档不会主动告诉接口消费方“我改了字段名”。多人协作时这份“人工通知”比工具本身更能避免联调事故。6.2 Nginx部署前端后播放M3U8视频要注意Content-Type前端打包后部署到Nginx播放M3U8视频时如果浏览器报错“Failed to load resource: the server responded with a status of 404 (Not Found)”但后端日志显示请求确实到了那很可能是Nginx没配置M3U8和TS的MIME类型。需要在Nginx配置里加上location ~ \.(m3u8)$ { add_header Content-Type application/vnd.apple.mpegurl; } location ~ \.(ts)$ { add_header Content-Type video/mp2t; }不加这个的话部分浏览器会拒绝解析M3U8文件。这个坑在本地开发时不会出现Vite开发服务器通常已配置好上线才暴露排查起来特别迷惑。6.3 前后端分离部署跨域配置到底怎么做如果前端和后端不在同一个域下跨域是绕不开的问题。项目里我用了Nginx反向代理解决生产环境跨域开发环境用Vite代理。这两种方式都比在后端直接配置CrossOrigin要干净。后端的CORS配置适合临时联调但不建议作为长期方案因为后面加拦截器、Token鉴权时CORS预检请求容易和自定义拦截器打架。6.4 推荐效果怎么评估从“看起来有推荐”到“推荐确实有用”系统上线后推荐效果好坏不能靠感觉。这个项目里我设计了三个指标的统计推荐位点击率曝光在推荐位的课程被点击的比例。推荐课程完课率用户通过推荐位进入课程后完成学习的比例。推荐覆盖率推荐结果中长尾课程学习人数低于平台均值的占比。前两个衡量推荐效果第三个衡量推荐是否只会推热门。理想状态下三个指标要一起看。如果点击率高但完课率低说明推荐内容有标题党嫌疑用户点进去发现不是自己想要的这时要检查课程标签和画像的准确度。6.5 用户反馈闭环让用户告诉系统“推错了”最后说一个容易被忽略却又非常重要的功能推荐结果的反馈按钮。在推荐卡片上放“不感兴趣”选项用户点掉之后该课程在后续推荐里降低权重。这个功能一开始我以为是锦上添花后来数据证明它对推荐精准度提升非常明显因为用户主动告诉我们“我不要什么”比算法猜“他要什么”更直接。实现上在后端增加一张recommend_feedback表记录用户ID、课程ID、反馈类型不感兴趣/已学过/举报。推荐结果过滤阶段把这些课程ID从候选集里剔除。这个过程不复杂但对用户体验的提升立竿见影。7. 个人实操中的几点体会最后分享几个我在这个项目里反复调整才想明白的点。第一个是关于推荐算法的复杂度。很多人一说个性化推荐就想着深度学习、知识图谱但实际情况是数据量几百到几千的在线教育平台用协同过滤加内容推荐就能做得很好复杂模型带来的提升可能不到5%但工程复杂度翻好几倍。先把推荐体验的完整链路跑通比追新算法有价值得多。第二个是推荐系统要尽早考虑“效果数据”的埋点。我一开始没有把推荐位的曝光和点击数据单独记录结果后面想评估推荐效果时没有数据可用只能补日志非常被动。建议从第一天起就在推荐接口返回数据里带上recommendId推荐场景ID前端上报曝光和点击事件时把这个ID带上这样后期可以做完整的漏斗分析。第三个体会是推荐系统本质上是一个反馈系统不是一个预测系统。它不是在猜用户下一秒想干什么而是在跟用户持续对话用户反馈行为系统调整策略用户再反馈系统再调整。所以不需要追求一次性推得完美而是要保证有反馈、有修正的机制。这个认知对我做推荐需求时的取舍帮助最大。这个项目做下来前端、后端、算法、部署都有涉及覆盖面比较广但也正因为如此我对整体系统的把握比以前做纯后端接口要清楚得多。希望这篇文章能帮到正在做类似项目的人少走弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。