尧图精选

电影推荐系统毕设实战:SpringBoot+Vue前后端分离与协同过滤算法实现

🕒 发布时间:2026/10/1 9:16:55 📁 来源:尧图网络
简介这是一套面向高校计算机专业学生与Java全栈学习者的电影推荐系统毕业设计源码采用SpringBoot后端与Vue前端分离架构适合作为高分毕设参考或课程设计实战项目。压缩包共171个文件约8.77MB其中33个Java文件承载后端业务逻辑与推荐算法实现59个JavaScript与4个Vue文件构成前端交互界面另有27张图片资源、12份Markdown说明文档、7个JSON配置及SQL建表脚本、yml与properties配置等覆盖从数据库设计到前后端联调的完整链路。项目已通过导师指导认可并经过严格调试可正常运行读者可据此掌握推荐系统的基本实现思路、接口分层设计与前端组件化开发方式也能借鉴其目录组织与配置管理经验快速搭建自己的开发环境。目前已有259人学习关注适合需要完整可运行案例来梳理开发流程、撰写论文或准备答辩的读者参考使用。1. 电影推荐系统毕设从协同过滤到前后端分离一套能跑通的工程骨架很多同学做毕业设计时选题定了「电影推荐系统」真正动手才发现难点不在算法而在把算法塞进一个能演示、能答辩、能写进论文的完整工程里。基于 SpringBoot Vue 的电影推荐系统本质上是一套前后端分离的 Web 应用后端负责用户、影片、评分数据和推荐计算前端负责把推荐结果用列表、海报墙、评分交互呈现出来。它适合计算机相关专业的本科或专科毕设也适合想练手全栈开发、把协同过滤从公式落到接口的开发者。这套骨架的价值在于推荐算法不是孤立的脚本而是被封装成服务通过 REST 接口暴露给前端数据库里存着真实的用户-物品评分矩阵。你拿到源码后重点不是照抄而是理解数据怎么流、算法在哪一层、接口怎么设计这样才能在答辩时讲清楚也才能改成自己的东西。2. 推荐算法选型为什么协同过滤仍是毕设首选2.1 三类推荐策略的适用边界做电影推荐常见路线有三条基于内容的推荐、协同过滤、以及混合推荐。基于内容的推荐依赖影片的标签、类型、导演、演员等元数据计算物品相似度优点是冷启动时只要有影片信息就能推缺点是无法挖掘用户潜在兴趣推荐结果容易同质化。协同过滤分用户基UserCF和物品基ItemCF核心思想是「相似的人喜欢相似的东西」或「喜欢相似东西的人口味相近」它不需要影片内容特征只依赖评分矩阵效果在数据量足够时通常更好但面临冷启动和稀疏性问题。混合推荐把两者加权融合效果上限高但实现复杂毕设周期内不容易调稳。对于毕业设计我一般建议以 ItemCF 为主辅以基于内容的兜底。原因是ItemCF 的相似度矩阵可以离线计算并缓存线上响应快电影数量通常远小于用户数量物品相似度矩阵规模可控而且 ItemCF 可解释性强答辩时能说清楚「因为你看过 A而 A 和 B 相似所以推荐 B」。UserCF 虽然经典但用户数量大时实时计算压力大且用户兴趣变化快工程上不如 ItemCF 稳定。2.2 评分矩阵与相似度计算的关键参数协同过滤的输入是用户-物品评分矩阵。假设有 m 个用户、n 部电影矩阵 R 中 R[u][i] 表示用户 u 对电影 i 的评分。实际系统中这个矩阵非常稀疏因为一个用户通常只看过几十部电影而电影库可能有几千部。稀疏度通常在 95% 以上这意味着直接算相似度会大量依赖偶然共现需要设置共同评分阈值来过滤噪声。相似度计算常用余弦相似度或皮尔逊相关系数。余弦相似度对评分绝对值不敏感适合评分尺度不统一的场景皮尔逊相关系数会减去用户平均分能消除用户打分偏严或偏松的影响。我在毕设里通常用调整余弦相似度它同时考虑了用户平均分和物品平均分公式如下# 调整余弦相似度计算示例 # R[u][i] 表示用户 u 对物品 i 的评分 # avg_u 是用户 u 的平均评分avg_i 是物品 i 的平均评分 import numpy as np def adjusted_cosine_similarity(R, avg_u, avg_i): # R: 评分矩阵行是用户列是物品未评分为 0 # avg_u: 每个用户的平均评分向量 # avg_i: 每个物品的平均评分向量 num_items R.shape[1] sim_matrix np.zeros((num_items, num_items)) for i in range(num_items): for j in range(i 1, num_items): # 找到同时评价了 i 和 j 的用户 common_users np.where((R[:, i] 0) (R[:, j] 0))[0] if len(common_users) 3: # 共同评分用户少于 3 个相似度置 0 sim_matrix[i][j] sim_matrix[j][i] 0 continue numerator 0.0 denom_i 0.0 denom_j 0.0 for u in common_users: r_ui R[u][i] - avg_u[u] r_uj R[u][j] - avg_u[u] numerator r_ui * r_uj denom_i r_ui ** 2 denom_j r_uj ** 2 if denom_i 0 or denom_j 0: sim_matrix[i][j] sim_matrix[j][i] 0 else: sim_matrix[i][j] sim_matrix[j][i] numerator / (np.sqrt(denom_i) * np.sqrt(denom_j)) return sim_matrix这段代码里common_users是同时评价过物品 i 和 j 的用户集合阈值设为 3 是经验值低于 3 个共同评分用户时相似度不可靠直接置零。avg_u[u]是用户 u 的平均评分减去它是为了消除用户打分偏好的影响。分母是两个向量调整后的模长乘积。实际工程中这个双重循环在物品数量上千时会很慢需要改成向量化计算或使用稀疏矩阵库但毕设数据量通常几百部电影直接跑也能接受。参数方面共同评分阈值建议 3 到 5太小噪声大太大相似度矩阵会过于稀疏。推荐列表长度一般取 10 到 20答辩演示够用。相似邻居数量 K 取 20 到 50K 越大推荐越多样但可能引入不相关物品。这些参数没有绝对最优需要根据你的数据集用留出法或交叉验证调。2.3 冷启动与数据稀疏的工程兜底新用户没有评分记录协同过滤无法工作。常见做法是注册时让用户选择至少 5 部喜欢的电影作为初始评分或者默认推荐热门电影和最新上架电影。新电影没有评分则用基于内容的相似度根据类型、导演、演员找相似影片推给看过相似影片的用户。这些兜底逻辑在毕设里不需要太复杂但要有否则答辩时被问到「新用户怎么办」会卡壳。数据稀疏的另一个缓解手段是降维比如用矩阵分解SVD把评分矩阵分解成用户隐向量和物品隐向量但 SVD 调参和解释成本高毕设里如果时间紧不建议作为主线可以作为对比实验写进论文。3. 后端工程搭建SpringBoot 分层与推荐服务接口3.1 项目结构与依赖配置后端用 SpringBoot 搭建典型分层是 controller、service、mapper、entity、utils。推荐算法相关代码放在 service 层下的 recommend 包独立于业务逻辑方便替换和测试。数据库用 MySQLORM 用 MyBatis 或 MyBatis-Plus后者在毕设里更省事单表增删改查不用写 XML。pom.xml 关键依赖包括spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、hutool工具类、以及可选的 spring-boot-starter-data-redis缓存相似度矩阵。版本选择上SpringBoot 2.7.x 比较稳定JDK 用 1.8 或 11 都行MySQL 5.7 或 8.0 均可。注意 MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver连接 URL 要加时区参数serverTimezoneAsia/Shanghai否则启动报错。# application.yml 关键配置 server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/movie_recommend?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: your_password redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case开启后数据库的user_id会自动映射到实体类的userId省去手动写 resultMap。log-impl打印 SQL 方便调试上线前关掉。Redis 用来缓存物品相似度矩阵因为矩阵计算一次要几秒到几十秒每次请求都算不现实。缓存 key 可以设计成sim:item:movievalue 用序列化后的二维数组或 Map。3.2 推荐服务接口设计与实现推荐服务对外暴露两个核心接口一个是给当前用户生成推荐列表一个是记录用户评分。评分记录接口在用户提交评分时调用写入评分表同时异步更新相似度矩阵或标记缓存失效。// RecommendController.java RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private RecommendService recommendService; // 获取当前用户的推荐列表默认 10 部 GetMapping(/list) public ResultListMovieVO getRecommendList( RequestParam Long userId, RequestParam(defaultValue 10) Integer size) { ListMovieVO list recommendService.recommendForUser(userId, size); return Result.success(list); } // 提交评分 PostMapping(/rate) public ResultVoid rateMovie(RequestBody RateDTO rateDTO) { recommendService.rateMovie(rateDTO.getUserId(), rateDTO.getMovieId(), rateDTO.getScore()); return Result.success(); } }recommendForUser方法内部逻辑先从 Redis 取物品相似度矩阵取不到则从数据库加载评分数据重新计算并回写然后查当前用户已评分的电影集合对每部已评分电影找相似度最高的 K 部未评分电影按相似度加权预测评分合并所有候选按预测分排序取前 size 部返回。这里要注意已评分电影本身不能出现在推荐结果里否则用户会觉得系统「傻」。// RecommendService.java 核心方法片段 public ListMovieVO recommendForUser(Long userId, int size) { // 1. 获取物品相似度矩阵 double[][] simMatrix getItemSimilarityMatrix(); // 2. 获取用户已评分电影 ListRating userRatings ratingMapper.selectByUserId(userId); if (userRatings.isEmpty()) { // 冷启动返回热门电影 return movieMapper.selectHotMovies(size); } MapLong, Double candidateScores new HashMap(); for (Rating rating : userRatings) { int itemIdx movieIdToIndex.get(rating.getMovieId()); double[] simRow simMatrix[itemIdx]; for (int j 0; j simRow.length; j) { if (simRow[j] 0) continue; Long candidateMovieId indexToMovieId.get(j); if (userRatedMovieIds.contains(candidateMovieId)) continue; double predicted simRow[j] * rating.getScore(); candidateScores.merge(candidateMovieId, predicted, Double::sum); } } // 3. 排序取 topN return candidateScores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(size) .map(e - movieMapper.selectVOById(e.getKey())) .collect(Collectors.toList()); }getItemSimilarityMatrix里做缓存判断userRatedMovieIds是用户已评分电影 ID 集合用于过滤。candidateScores用merge累加预测分同一候选电影可能被多个已评分电影关联分数累加更合理。最后按分数降序取前 size 部。这个实现是简化版没有做评分归一化实际可以除以相似度之和避免热门物品分数虚高。3.3 数据库表设计与评分数据准备核心表四张用户表user、电影表movie、评分表rating、以及可选的推荐日志表recommend_log。电影表字段包括 id、title、genres、director、actors、release_year、poster_url、avg_score。评分表字段包括 id、user_id、movie_id、score、create_time并在(user_id, movie_id)上建唯一索引防止重复评分。数据准备是毕设里最容易被忽视的一环。没有真实评分数据推荐算法跑不出效果。常见做法是用 MovieLens 数据集它提供 ml-latest-small 版本包含约 10 万条评分、600 个用户、9000 部电影格式是 CSV。导入时写个脚本解析ratings.csv和movies.csv批量插入数据库。注意 MovieLens 的电影 ID 和你的自增 ID 要建立映射否则评分对不上。-- 评分表建表语句 CREATE TABLE rating ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, movie_id bigint(20) NOT NULL, score double NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_movie (user_id,movie_id), KEY idx_movie (movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一索引uk_user_movie保证一个用户对一部电影只有一条评分重复提交时用INSERT ... ON DUPLICATE KEY UPDATE更新分数。idx_movie加速按电影查评分。导入数据时用批量插入每 1000 条提交一次否则 10 万条逐条插入会很慢。4. 前端 Vue 工程推荐列表渲染与评分交互4.1 Vue 项目初始化与接口联调前端用 Vue 2 或 Vue 3 都行毕设里 Vue 2 Element UI 资料多、踩坑少。用 vue-cli 创建项目安装 axios、vue-router、element-ui。开发时配置 proxy 解决跨域在vue.config.js里把/api代理到后端 8081 端口。// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }changeOrigin设为 true 让后端收到的 Host 是目标地址避免某些安全校验拦截。pathRewrite这里保持原样因为后端接口本身就带/api前缀。如果后端没加前缀可以重写成空。推荐列表页面核心逻辑进入页面时调/api/recommend/list拿到电影数组用v-for渲染卡片每张卡片显示海报、标题、类型、平均分以及一个评分组件。用户点击评分后调/api/recommend/rate提交成功后可以局部刷新推荐列表或者提示「评分成功推荐已更新」。template div classrecommend-list el-row :gutter20 el-col :span6 v-formovie in movies :keymovie.id el-card :body-style{ padding: 10px } img :srcmovie.posterUrl classposter / div classtitle{{ movie.title }}/div div classgenres{{ movie.genres }}/div el-rate v-modelmovie.userScore changerate(movie) / /el-card /el-col /el-row /div /template script import axios from axios export default { data() { return { movies: [], userId: 1 } }, created() { this.loadRecommend() }, methods: { loadRecommend() { axios.get(/api/recommend/list, { params: { userId: this.userId, size: 12 } }) .then(res { this.movies res.data.data }) }, rate(movie) { axios.post(/api/recommend/rate, { userId: this.userId, movieId: movie.id, score: movie.userScore }).then(() { this.$message.success(评分成功) this.loadRecommend() }) } } } /scriptel-rate的v-model绑定到movie.userScorechange触发提交。提交后重新加载推荐列表用户能立刻看到推荐变化演示效果好。注意userId这里写死为 1实际应该从登录态取毕设里可以简化成登录后存 localStorage。4.2 海报墙布局与评分组件细节海报墙用 Element UI 的el-row和el-col做栅格每行 4 张卡片:span6。海报图片建议统一尺寸比如宽 200px、高 280px用object-fit: cover裁剪避免变形。如果海报加载失败加error回退到默认图。评分组件除了el-rate也可以自己用星星图标实现但el-rate自带半星、只读、颜色配置省事。注意el-rate的change事件在用户点击时触发如果只是展示平均分用disabled属性设为只读。推荐列表的排序和分页毕设演示通常一页展示 12 到 20 部不需要分页。如果数据多可以加「换一批」按钮重新调接口并传随机种子或偏移量。后端可以在推荐结果里加一点随机扰动避免每次刷新都一样但扰动幅度要小不能破坏推荐相关性。4.3 登录态与用户 ID 传递毕设里登录可以简化用户表存用户名密码登录成功后后端返回 token 或直接返回 userId前端存 localStorage。后续请求在 axios 拦截器里统一带上 userId 或 token。不要在前端硬编码 userId否则答辩时被问到「多个用户怎么办」不好回答。// axios 请求拦截器 axios.interceptors.request.use(config { const userId localStorage.getItem(userId) if (userId) { config.params { ...config.params, userId } } return config })这样每个请求自动带 userId后端从参数里取。更规范的做法是用 JWT但毕设里参数传递足够实现简单调试也方便。5. 避坑与排查推荐系统毕设里最容易翻车的五件事5.1 相似度矩阵计算超时导致接口 504现象调用推荐接口时浏览器转圈很久最后返回 504 或超时错误。原因每次请求都重新计算物品相似度矩阵双重循环在电影数量几百时就要几秒上千时几十秒。解决把相似度矩阵计算做成离线任务或启动时计算一次结果存 Redis 或内存缓存。评分数据变化时不要立即重算全量可以标记缓存过期下次请求时异步重算或者只更新受影响物品的相似度。5.2 评分数据导入后推荐结果为空现象数据库里明明有评分数据但推荐接口返回空列表。原因常见有三种。一是 MovieLens 的电影 ID 和数据库自增 ID 没建立映射评分表里的 movie_id 在电影表里查不到二是用户已评分电影集合把候选全过滤了比如用户只评了 3 部电影而相似度阈值太高没有候选三是相似度矩阵全为零因为共同评分用户数低于阈值。解决先查评分表和电影表的 ID 是否能关联再打印候选集大小和相似度矩阵非零元素个数逐步定位。5.3 前端跨域请求被拦截现象前端调接口报 CORS 错误浏览器控制台提示Access-Control-Allow-Origin。原因开发时前端 8080、后端 8081端口不同即跨域。解决用 vue.config.js 的 proxy 代理或者后端加CrossOrigin注解。代理方式更干净生产环境用 Nginx 转发。注意 proxy 配置后要重启前端 dev server否则不生效。5.4 评分提交后推荐列表不更新现象用户评分后推荐列表还是旧的。原因前端提交评分后没有重新调推荐接口或者后端缓存没失效。解决前端在评分成功的回调里重新调loadRecommend后端在评分写入后删除 Redis 里的相似度矩阵缓存 key下次请求重新计算。如果用了本地内存缓存需要加版本号或时间戳判断是否过期。5.5 中文乱码与日期格式错误现象电影标题显示问号或者日期显示成时间戳。原因数据库字符集不是 utf8mb4或者连接 URL 没加characterEncodingutf-8日期字段用Date类型但前端直接展示没有格式化。解决建库时指定DEFAULT CHARSETutf8mb4连接 URL 加编码参数实体类日期字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)或者前端用 moment.js 格式化。6. 让推荐结果可解释一个提升答辩通过率的小技巧推荐系统毕设答辩时老师最常问的两个问题是「为什么推荐这些电影」和「推荐效果怎么评估」第一个问题如果答不上来会显得算法是黑匣子。我的习惯是给每部推荐电影附带一条推荐理由比如「因为你喜欢《肖申克的救赎》而《阿甘正传》和它相似度 0.87」。实现上在推荐服务返回结果时除了电影信息再带上reason字段记录是哪个已评分电影关联过来的、相似度多少。// 在候选分数累加时记录来源 MapLong, RecommendReason reasonMap new HashMap(); for (Rating rating : userRatings) { // ... 遍历相似物品 if (predicted 0) { candidateScores.merge(candidateMovieId, predicted, Double::sum); // 记录相似度最高的来源 RecommendReason reason reasonMap.get(candidateMovieId); if (reason null || simRow[j] reason.getSimilarity()) { reasonMap.put(candidateMovieId, new RecommendReason( rating.getMovieId(), simRow[j])); } } } // 返回时组装 reason MovieVO vo movieMapper.selectVOById(movieId); RecommendReason reason reasonMap.get(movieId); if (reason ! null) { String sourceTitle movieMapper.selectTitleById(reason.getSourceMovieId()); vo.setReason(因为你喜欢《 sourceTitle 》相似度 String.format(%.2f, reason.getSimilarity())); }这样前端展示时每张卡片下面加一行小字推荐理由答辩演示时老师一眼就能看懂推荐逻辑。评估方面毕设里不需要做大规模 A/B 测试用留出法就够了把评分数据按 8:2 分成训练集和测试集在测试集上算 RMSE 或 MAE再算一下推荐列表的准确率和召回率。RMSE 衡量评分预测误差准确率衡量推荐列表中用户实际喜欢的比例。这些指标写进论文比只贴几张截图有说服力。我踩过的一个坑是一开始只算了 RMSE结果老师问「推荐列表准不准」RMSE 低不代表推荐列表好因为 RMSE 是评分预测指标不是排序指标。后来补了准确率和召回率才把评估闭环补上。建议你至少算两个指标一个评分误差一个排序质量。另外相似度矩阵的缓存不要用 Java 对象序列化直接存 Redis不同版本 JDK 可能反序列化失败。用 JSON 字符串存或者用 Redis 的 Hash 结构存每个物品的相似邻居。我一般用 JSON简单直接出问题也好排查。最后说个习惯每次改完推荐算法参数不要只看接口返回把中间结果打印出来——相似度矩阵的非零元素个数、候选集大小、每个候选的预测分。这些日志在排查「为什么推荐结果奇怪」时比任何调试工具都管用。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →