
简介这份资源是面向高校计算机专业毕业设计场景的完整项目源码主题为基于协同过滤算法的个性化音乐推荐系统适合正在准备Java方向毕设的本科生或需要SpringBootVue全栈练手的开发者。项目采用Java语言与SpringBoot框架前端使用Vue构建配套MySQL 5.7数据库JDK1.8、Tomcat7、Maven等环境均已验证可完美运行。压缩包共366个文件约10.61MB其中101个Java文件承载后端业务逻辑与协同过滤算法实现79个Vue文件构成前后端分离界面另有PNG、JPG等图片资源、JS脚本、CSS样式、XML配置及SQL建表脚本结构完整。系统覆盖用户行为跟踪、偏好学习与用户画像构建、相似性计算、推荐列表生成、用户反馈调整策略以及音乐库管理、用户管理、评论管理和数据可视化等模块管理员可增删改音乐信息与用户数据。目前已有70人学习适合需要完整赛题方案、算法落地思路与前后端联调参考的读者。1. 从一份毕设需求说起协同过滤怎么把歌单推对人每年带毕设总有几个学生拿着“个性化音乐推荐系统”来找我开口就是“老师我想用深度学习”。我一般会先泼一盆冷水你手上有多少条真实听歌记录如果只有几百个用户、几千条播放日志上深度模型就是拿大炮打蚊子训练不动、调参玄学、答辩还讲不清。这个标题里的协同过滤算法恰恰是数据量不大时最稳、最容易讲明白、也最容易做出效果的一档方案。它解决的事情很朴素我不知道你喜欢什么风格、什么年代、什么语种但我知道你和另外一批人听得差不多而那批人反复听的一首歌你还没听过——那就把它推给你。整套系统落到 Java 技术栈上就是 Spring Boot 提供接口、MyBatis-Plus 管数据、MySQL 存用户行为、离线算相似度、在线出推荐列表。适合谁适合数据规模在万级以内、想在一到两个月内跑通“登录—听歌—打分—推荐—反馈”闭环的毕设也适合想借这个题目把 Java 后端工程能力串一遍的人。下面我按自己带项目的顺序把选型、建表、算法实现、接口和踩坑一次讲透。2. 协同过滤在音乐场景里到底怎么算用户相似度与物品相似度的取舍2.1 为什么音乐推荐优先选 UserCF 而不是 ItemCF协同过滤分两条路基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。UserCF 算“和你口味像的人还听了什么”ItemCF 算“听了这首歌的人还听了什么”。在电商里 ItemCF 是主流因为商品数量相对稳定、用户兴趣漂移慢但音乐场景不一样歌曲库动辄几万首、每天还有新歌进来物品相似度矩阵维护成本高而且一首歌被共同收听的数据非常稀疏。毕设的数据量通常更小UserCF 反而更合适用户数少、相似度矩阵小、算得快而且“找相似用户”这个逻辑在答辩时特别好讲。我一般会这样定用户数在 5000 以内、行为数据在 10 万条以内直接上 UserCF如果用户量明显大于物品量再考虑 ItemCF。这不是绝对规则但能让你少走弯路。2.2 相似度公式余弦、皮尔逊和调整余弦怎么选最常用的是余弦相似度。把每个用户对歌曲的评分看成一个向量两个用户向量的夹角越小越相似sim(u, v) Σ(r_ui · r_vi) / (√Σr_ui² · √Σr_vi²)但音乐评分有个坑有人习惯全打 5 分有人最高只给 3 分这叫“评分尺度偏差”。余弦对绝对值不敏感皮尔逊会减去用户均分能缓解这个问题。我的经验是如果评分是 1~5 星且用户打分习惯差异大用皮尔逊如果行为是“播放/收藏/跳过”这种隐式反馈用余弦更直接。调整余弦减去物品均分在物品维度偏差大时有用音乐场景用得少。2.3 用 Java 实现用户相似度矩阵下面这段是我常用的核心计算输入是userId - (songId - score)的评分表输出用户两两相似度。// 计算用户相似度矩阵ratingMap: userId - (songId - score) public MapLong, MapLong, Double calcUserSimilarity( MapLong, MapLong, Double ratingMap) { MapLong, MapLong, Double simMatrix new HashMap(); ListLong userIds new ArrayList(ratingMap.keySet()); for (int i 0; i userIds.size(); i) { Long u userIds.get(i); MapLong, Double uRatings ratingMap.get(u); // 预计算 u 的模长避免内层循环重复开方 double uNorm Math.sqrt(uRatings.values().stream() .mapToDouble(v - v * v).sum()); for (int j i 1; j userIds.size(); j) { Long v userIds.get(j); MapLong, Double vRatings ratingMap.get(v); // 只遍历共同评分的歌曲降低稀疏数据下的无效计算 double dot 0.0; for (Map.EntryLong, Double e : uRatings.entrySet()) { Double vScore vRatings.get(e.getKey()); if (vScore ! null) { dot e.getValue() * vScore; } } if (dot 0) continue; // 无共同歌曲相似度为 0 double vNorm Math.sqrt(vRatings.values().stream() .mapToDouble(x - x * x).sum()); double sim dot / (uNorm * vNorm); simMatrix.computeIfAbsent(u, k - new HashMap()).put(v, sim); simMatrix.computeIfAbsent(v, k - new HashMap()).put(u, sim); } } return simMatrix; }逻辑说明外层双重循环只算上三角算完对称写入省一半计算量。内层遍历共同评分歌曲而不是全集是因为音乐评分矩阵极度稀疏一个用户平均可能只评过几十首遍历全集纯属浪费。uNorm提前算好避免在内层重复求模。参数说明ratingMap的 score 建议归一化到 0~1 或 1~5 统一量纲如果用的是播放次数记得先做对数压缩log(1count)否则一首循环播放 200 次的歌会主导整个相似度。相似度为 0 的用户对直接跳过不写入矩阵能显著省内存。2.4 生成推荐加权求和加去重有了相似度推荐就是“取最相似的 K 个用户把他们听过而我没听过的歌按相似度加权打分排序取 TopN”。// 为 targetUser 生成 TopN 推荐k 为最近邻数量 public ListLong recommend(Long targetUser, int k, int topN, MapLong, MapLong, Double ratingMap, MapLong, MapLong, Double simMatrix) { MapLong, Double targetRatings ratingMap.getOrDefault(targetUser, Map.of()); MapLong, Double scores new HashMap(); // 取相似度最高的 k 个邻居 ListMap.EntryLong, Double neighbors simMatrix .getOrDefault(targetUser, Map.of()).entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(k) .collect(Collectors.toList()); for (Map.EntryLong, Double nb : neighbors) { Long neighbor nb.getKey(); double sim nb.getValue(); for (Map.EntryLong, Double r : ratingMap.get(neighbor).entrySet()) { // 过滤掉目标用户已经听过的歌 if (targetRatings.containsKey(r.getKey())) continue; scores.merge(r.getKey(), sim * r.getValue(), Double::sum); } } return scores.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }逻辑说明sim * r.getValue()是加权核心相似度越高、邻居打分越高推荐分越高。merge把多个邻居对同一首歌的贡献累加。最后排序取前 N。参数说明K 是最近邻数量太小推荐不准太大引入噪声我一般取 20~50数据少时取 10~20。TopN 是返回条数接口层一般 10~20 首。注意这里没有做评分归一化如果邻居打分尺度差异大建议先减去各自均分再加权效果更稳。3. 从零搭起Spring Boot MyBatis-Plus MySQL 的工程骨架3.1 表结构设计用户、歌曲、行为三张核心表毕设不需要花哨的库表三张表就能撑起整个闭环。下面是我常用的建表 SQL字段够用且好扩展。-- 用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 歌曲表 CREATE TABLE song ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, artist VARCHAR(100), album VARCHAR(200), genre VARCHAR(50), duration INT COMMENT 时长秒, cover_url VARCHAR(500) ); -- 用户行为表评分/收藏/播放统一记录 CREATE TABLE user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, song_id BIGINT NOT NULL, score DOUBLE DEFAULT 0 COMMENT 评分或加权行为分, behavior_type TINYINT COMMENT 1播放 2收藏 3评分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_song (song_id) );逻辑说明user_behavior是算法唯一的数据来源把播放、收藏、评分统一成一张表用behavior_type区分、用score承载权重离线计算时直接查这张表就能构造评分矩阵。参数说明score用 DOUBLE 而不是 INT是为了兼容“播放次数加权”这类连续值。索引必须建在user_id和song_id上否则数据量上万后相似度计算会慢到怀疑人生。behavior_type的权重映射建议播放 1 分、收藏 3 分、评分按星级这样隐式反馈和显式反馈能混用。3.2 用 MyBatis-Plus 快速生成实体和 Mapper热词里提到的“mybatisplus 根据 java 实体类生成创建表的 sql 语句”其实是反过来的常见需求但 MyBatis-Plus 的代码生成器确实能省大量体力活。我一般用它的FastAutoGenerator直接生成 entity、mapper、service。// MyBatis-Plus 代码生成器核心配置 FastAutoGenerator.create(jdbc:mysql://localhost:3306/music_rec, root, 123456) .globalConfig(builder - builder .author(yourname) .outputDir(System.getProperty(user.dir) /src/main/java)) .packageConfig(builder - builder .parent(com.example.music) .entity(entity) .mapper(mapper) .service(service)) .strategyConfig(builder - builder .addInclude(user, song, user_behavior) // 指定要生成的表 .entityBuilder().enableLombok() .mapperBuilder().enableBaseResultMap()) .execute();逻辑说明addInclude指定表名避免把无关表也生成一遍。enableLombok让实体自动带 getter/setter省掉几百行样板代码。参数说明数据库连接、账号密码按本地环境改。outputDir指向你的源码目录。生成后记得检查主键策略MySQL 自增主键要配TableId(type IdType.AUTO)否则插入会报错。3.3 离线计算任务定时刷新相似度矩阵相似度矩阵不能每次请求都算那样接口会卡死。我的做法是写一个定时任务每天凌晨或数据更新后重算一次把结果缓存到内存或 Redis。Component public class SimilarityRefreshTask { Resource private UserBehaviorMapper behaviorMapper; Resource private RecommendCache recommendCache; // 每天凌晨 3 点刷新 Scheduled(cron 0 0 3 * * ?) public void refresh() { // 1. 从行为表构造评分矩阵 MapLong, MapLong, Double ratingMap buildRatingMap(); // 2. 计算用户相似度 MapLong, MapLong, Double simMatrix new UserCFService().calcUserSimilarity(ratingMap); // 3. 为每个用户预生成推荐列表并缓存 for (Long userId : ratingMap.keySet()) { ListLong recs new UserCFService() .recommend(userId, 30, 20, ratingMap, simMatrix); recommendCache.put(userId, recs); } } private MapLong, MapLong, Double buildRatingMap() { ListUserBehavior list behaviorMapper.selectList(null); MapLong, MapLong, Double map new HashMap(); for (UserBehavior b : list) { map.computeIfAbsent(b.getUserId(), k - new HashMap()) .merge(b.getSongId(), b.getScore(), Double::sum); } return map; } }逻辑说明先构造评分矩阵再算相似度最后为每个用户预生成推荐并写入缓存。接口层直接读缓存响应时间从秒级降到毫秒级。参数说明cron表达式按需调整数据更新频繁可以改成每小时。recommend的 K 取 30、TopN 取 20缓存 20 条是为了给接口层留出过滤已听歌曲的余量。缓存用ConcurrentHashMap就够毕设用上 Redis 更好但非必须。提示定时任务记得在启动类加EnableScheduling否则Scheduled不生效这是新手最常翻的车之一。4. 接口层与推荐闭环让推荐结果真正被用户看到4.1 推荐接口设计分页、过滤与冷启动兜底推荐接口不能只返回一个 ID 列表前端要展示歌名、歌手、封面所以要么联表查要么先查 ID 再批量查歌曲。我一般分两步缓存里拿 ID 列表再批量查歌曲详情。RestController RequestMapping(/api/recommend) public class RecommendController { Resource private RecommendCache recommendCache; Resource private SongMapper songMapper; GetMapping(/list) public ResultListSongVO list(RequestParam Long userId, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { ListLong ids recommendCache.get(userId); // 冷启动兜底新用户没有推荐返回热门歌曲 if (ids null || ids.isEmpty()) { return Result.ok(songMapper.selectHotSongs(page, size)); } // 分页截取 int from (page - 1) * size; if (from ids.size()) return Result.ok(List.of()); ListLong pageIds ids.subList(from, Math.min(from size, ids.size())); ListSongVO songs songMapper.selectByIds(pageIds); return Result.ok(songs); } }逻辑说明先取缓存 ID空则走热门兜底解决新用户冷启动。分页在内存里做因为推荐列表本身只有几十条没必要再查库分页。参数说明page、size控制分页。冷启动兜底查询selectHotSongs按播放量倒序需要你在 Mapper 里写一条聚合 SQL。注意selectByIds返回顺序不一定和传入 ID 顺序一致如果前端对排序敏感要在 Java 里按pageIds重新排一遍。4.2 行为上报接口把播放和收藏写回数据源推荐要越用越准就得让用户行为回流。播放、收藏、评分都通过一个接口写入user_behavior。PostMapping(/behavior) public ResultVoid report(RequestBody BehaviorDTO dto) { UserBehavior behavior new UserBehavior(); behavior.setUserId(dto.getUserId()); behavior.setSongId(dto.getSongId()); behavior.setBehaviorType(dto.getType()); // 按行为类型赋权重分 double score switch (dto.getType()) { case 1 - 1.0; // 播放 case 2 - 3.0; // 收藏 case 3 - dto.getScore(); // 评分 default - 0.0; }; behavior.setScore(score); behaviorMapper.insert(behavior); return Result.ok(); }逻辑说明把不同行为映射成不同权重分统一写入行为表离线任务下次刷新时自然会把新数据算进去。参数说明权重 1/3/评分 是我常用的经验值你可以按业务调。注意要做幂等或去重同一用户对同一首歌短时间内重复播放不该重复累加否则刷播放量会污染推荐。常见做法是加时间窗口判断比如 5 分钟内同一首歌只记一次。4.3 效果验证离线指标和在线反馈两手抓毕设答辩最怕被问“你怎么证明推荐有效”。离线用准确率、召回率、覆盖率在线看点击率和播放完成率。指标含义计算方式参考目标准确率推荐中用户真正喜欢的比例命中数 / 推荐总数0.15~0.3召回率用户喜欢的歌被推荐出的比例命中数 / 用户喜欢总数0.1~0.25覆盖率被推荐过的歌曲占总曲库比例推荐歌曲去重数 / 总歌曲数越高越好点击率在线推荐位点击比例点击数 / 曝光数看业务基线做法是把行为数据按时间切分前 80% 做训练、后 20% 做测试用测试集里的真实行为去比对推荐列表。覆盖率低说明推荐总集中在少数热门歌可以加随机扰动或降低 K 值缓解。注意离线指标好看不代表线上好用答辩时两个都讲比只报一个数字可信得多。5. 避坑与排查协同过滤毕设里最容易翻车的五件事5.1 相似度全是 0 或 NaN现象算出来的相似度矩阵几乎为空推荐结果为空或报错。 原因评分矩阵太稀疏用户之间没有共同评分的歌曲或者某个用户评分为空导致模长为 0除零产生 NaN。 解决先统计共同评分数量低于阈值比如 2 首的用户对直接跳过模长为 0 时提前continue。数据太少时改用热门推荐兜底别硬算。5.2 推荐结果永远不变现象用户听了新歌、收藏了新歌推荐列表纹丝不动。 原因相似度矩阵是定时任务算的缓存没刷新或者行为上报接口没真正写库。 解决确认Scheduled生效、cron 时间已过手动触发一次刷新任务验证检查行为表是否有新数据写入。开发阶段可以把刷新频率调高方便调试。5.3 接口响应慢到超时现象推荐接口几秒才返回数据量一大直接 504。 原因在接口里实时算相似度或者每次请求都全表扫描行为表。 解决相似度必须离线算、缓存读。行为表查询加索引推荐结果预生成。如果非要在接口里算至少把评分矩阵缓存到内存别每次查库。5.4 新用户和老用户待遇一样现象刚注册的用户推荐列表是空的或者推的全是热门歌体验差。 原因没有冷启动策略新用户不在相似度矩阵里。 解决新用户走热门推荐或基于歌曲标签的推荐等积累了几条行为后再切到协同过滤。可以在用户行为数达到阈值比如 5 条后才纳入 UserCF 计算。5.5 评分尺度不统一导致推荐偏斜现象推荐结果总偏向某几个“打分大方”的用户喜欢的歌。 原因不同用户评分习惯不同有人全 5 分有人全 3 分余弦相似度对绝对尺度不敏感但加权求和时会放大高分用户的影响。 解决加权前先减去用户均分做中心化或者改用皮尔逊相似度。隐式反馈场景下先对播放次数做对数压缩避免刷播放量的歌主导推荐。6. 让推荐更耐问混合策略与答辩加分技巧纯协同过滤有个绕不开的短板数据稀疏时效果断崖式下跌而且没法解释“为什么推这首歌”。我在带毕设时一般会加一层混合策略既提升效果也让答辩时有话可讲。第一个技巧是协同过滤 内容标签的加权融合。歌曲表里有genre字段用户听过的歌的流派分布可以算出一个偏好向量。推荐时把协同过滤得分和流派匹配得分按 7:3 加权既能缓解稀疏又能在答辩时说“我做了混合推荐”。实现上就是在recommend方法返回前对候选歌曲按流派命中情况加一个小的 bonus 分。第二个技巧是给推荐结果加可解释理由。前端展示“因为你听过 XX所以推荐 YY”做法是在生成推荐时记录贡献最大的那个邻居用户和他听过的共同歌曲。这个信息在recommend里顺手就能存下来答辩时演示一下比干巴巴报准确率有说服力得多。第三个技巧是用 A/B 思路做对比验证。把用户随机分成两组一组走协同过滤、一组走热门推荐统计两组的点击率差异。毕设不一定真跑线上实验但你可以用历史数据模拟取一部分用户的行为做测试分别用两种策略生成推荐对比命中率。这个表格往答辩 PPT 上一放工作量和方法论都立住了。最后说个我自己的习惯每次改完算法参数我都会固定用同一份测试集跑一遍把准确率、召回率、覆盖率记在一个表格里而不是凭感觉说“好像变好了”。参数调优最忌讳没有基线K 从 20 调到 50 到底有没有用数字说了算。这套流程跑下来一个能演示、能讲清、能复现的个性化音乐推荐系统就成型了。希望帮到你。本文还有配套的精品资源点击获取