前阵子有位准备做毕设的同学问我,Java方向的选题想选推荐系统,是不是必须先啃深度学习。我当时有点哭笑不得——在很多人的印象里,“推荐系统”四个字已经和神经网络、大数据绑定了,好像不搞个GPU训练就不好意思写进论文。但实际上,如果要选一个最稳妥、最容易被答辩老师接受、又能在几个月内真正做出成果的题目,基于协同过滤的Java音乐推荐系统反而是个被严重低估的选择。这篇文章我不讲空话,直接把这类项目从选题、数据、系统设计到算法落地、评估答辩的完整链路梳理清楚,争取你看完就能拿着去搭自己的毕设。
为什么我说“协同过滤+Java+音乐”这个组合是黄金组合?先看算法:协同过滤是推荐系统里最经典的一类方法,原理一句话能讲清——“物以类聚,人以群分”,但往里钻又有UserCF、ItemCF、矩阵分解几条不同的路线可以写;再看技术栈:Java搭配Spring Boot是绝大多数计算机方向学生最熟悉的一套东西,答辩时被问到工程化问题完全不虚;最后看业务领域:音乐比电商、新闻更适合做推荐demo,因为歌曲数量可控、用户行为数据容易构造、推荐结果的对错还能靠直觉判断。这篇就把这个项目的完整实现过程拆开讲。
1. 为什么选协同过滤:这道题背后的逻辑链
1.1 毕设选题的第一原则是“可控”
毕设和真正的科研不一样,选题的第一原则不是“新颖”,而是“可控”。什么叫可控?老师考核的点不外乎三个:工作量是否饱满、技术上能不能自圆其说、最后能不能跑出可演示的结果。从这个角度看,基于协同过滤的音乐推荐系统几乎是完美答案:工作量足够撑起一篇论文,算法原理简单到可以在答辩PPT上画图讲清,而且数据可控、时间可控、出错时可排查。
很多同学一上来就想着深度学习,结果实际做的时候发现光是配环境就要折腾一周,训练一轮要几小时,笔记本风扇响得像飞机起飞,最后拿出来的还是一个类似黑盒的模型。老师问“为什么给这个用户推荐了这几首歌”,你反而讲不出所以然。协同过滤完全不同,它的每一步都可以被拆开解释:哪个用户和你口味相似?因为你们共同听过的歌有交集;为什么给你推这首歌?因为你口味相似的用户听过它。这种可解释性在答辩现场就是天然的加分项。
1.2 协同过滤的三条路线,总得选一条当主线
协同过滤听起来是一个词,实际分成三条路线:基于用户的UserCF、基于物品的ItemCF、以及基于矩阵分解的隐因子模型。它们各有利弊,选哪条直接决定你论文的核心章节怎么写。
| 路线 | 实现难度 | 推荐效果 | 可解释性 | 论文可写点 |
|---|---|---|---|---|
| UserCF | 低 | 尚可 | 高 | 相似度计算、近邻选择优化 |
| ItemCF | 低 | 尚可 | 高 | 物品相似矩阵、热门物品惩罚 |
| 矩阵分解SVD | 中 | 更好 | 低 | 隐因子模型、正则化调参 |
我当时选的主线是UserCF,副线是ItemCF。理由很实际:两条经典路线做对比,可以从指标上说明在音乐场景下到底哪个更合适;如果只做UserCF,内容偏薄;如果只做矩阵分解,Java实现里矩阵运算的代码会绕一些,答辩时数学表达也不够直观。UserCF和ItemCF代码高度同构、差异明显、实验结果还能对应到直觉,非常适合当论文的核心实验。
1.3 为什么是“Java环境下”,而不是用Python跑个算法了事
很多人觉得算法就该用Python写,Java只是用来做网页。这是对“Java环境下”这个题目的严重误解。对于毕设来说,Java环境反而是优势:大多数学校课程里Spring Boot是基础,Maven、MyBatis、MySQL这套东西你已经有了肌肉记忆,不用从零学新语言。
更重要的是,Java工程会让你的算法落在一个真实系统里:数据存在MySQL里、用户行为通过接口写入、推荐结果被Controller查询展示、前端有页面有交互。相比Python Notebook里甩一堆全局变量的demo,Java版本是一个完整闭环。答辩时老师关心的是“你有没有工程能力”,而不是“你调了哪个现成的库”。真正做完你会发现,“Java环境下”不是限制,而是加分项——它要求你理解数据如何在对象、数据表和接口之间流动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 没有用户行为数据,推荐系统拿什么算
2.1 数据集从哪来
很多第一次做推荐系统的同学会卡在第一步:算法看懂了,但不知道拿什么数据喂给它。这个项目的数据来源一般有三种:公开数据集、自己造数据、爬虫。最推荐前两种结合。
网络上可以找到一些公开的收听记录数据集,字段通常是“用户ID、物品ID、互动次数、时间戳”,规模从几万到几十万条不等。这类数据直接导入MySQL就能用,风险低、可复现。但公开数据集有个小问题:它可能不包含你想做的歌曲元信息(歌名、歌手、风格),需要额外处理。还有个更灵活的路子:自己写生成器造数据。很多人一听“造数据”就心虚,觉得不真实,但毕设阶段“可控”比“真实”更重要,只要造数据的方式符合真实世界的分布规律,实验结论依然成立。
2.2 建表:user、song、behavior三类表怎么设计
数据表是这个项目的地基,我建议按三张核心表来设计。第一张是用户表,第二张是歌曲表,第三张是用户行为表。行为表是整个推荐算法的输入源,设计的好坏直接决定算法代码好不好写。
sql复制CREATE TABLE `t_user` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`username` varchar(40) NOT NULL UNIQUE,
`password` varchar(100) NOT NULL,
`pref_style` varchar(20) DEFAULT NULL COMMENT '偏好风格',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_song` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`singer` varchar(50) NOT NULL,
`album` varchar(100),
`style` varchar(30) COMMENT '民谣/摇滚/电子等',
`duration` int COMMENT '时长秒',
`play_count` int DEFAULT 0 COMMENT '热度',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_behavior` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`song_id` bigint NOT NULL,
`play_cnt` int DEFAULT 1 COMMENT '播放次数',
`like_flag` tinyint DEFAULT 0 COMMENT '是否收藏',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_user` (`user_id`),
KEY `idx_song` (`song_id`),
UNIQUE KEY `uk_user_song` (`user_id`, `song_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
行为表我特意加了唯一键 (user_id, song_id),这样同一个用户对同一首歌的多次播放会合并成一条记录,用 play_cnt 累加次数。这个设计既符合真实场景,又能在算法阶段直接拿播放次数当隐式评分。索引方面,用户侧查询和歌曲侧查询都要覆盖,否则数据量大一点就会出现明显的慢查询。
2.3 造数据:用Zipf分布模拟真实收听行为
如果选择自造数据,千万不要用 new Random().nextInt(songCount) 均匀生成行为,否则推荐结果会“假”得离谱。真实世界的收听行为是高度长尾的:极少部分热门歌曲占据了绝大多数播放量,大部分冷门歌曲只有零星几次收听。
一种更接近真实的做法是用Zipf分布采样,让歌曲热度排名越靠前的被选中的概率越大。简化实现的思路是先算一个归一化分母(zeta),再按排名递减的概率累积采样:
java复制// 简化版Zipf生成器:rank越靠前,被选中的概率越大
public int sampleSongByZipf(int songCount, double skew) {
double zeta = 0.0;
for (int i = 1; i <= songCount; i++) {
zeta += 1.0 / Math.pow(i, skew);
}
double p = ThreadLocalRandom.current().nextDouble();
double cum = 0.0;
for (int rank = 1; rank <= songCount; rank++) {
cum += (1.0 / Math.pow(rank, skew)) / zeta;
if (p <= cum) return rank;
}
return songCount;
}
规模建议:自造数据按 2000用户、50000首歌、每个用户10到50条行为来生成。这样用户-歌曲矩阵虽然看起来很稀疏,但已经有足够的共同行为让算法工作,计算速度也能保证演示流畅。造数脚本里务必用固定随机种子,不然每次跑实验数据不一致,论文里的指标就复现不了。
2.4 冷启动问题不要想着一口气解决
新用户没有任何行为记录,UserCF根本找不到相似用户,这是协同过滤的天然缺陷。很多同学在这里硬刚,试图搞一个完美的冷启动方案,其实没必要。毕设阶段最合理的做法是分层兜底:推荐接口先查个性化推荐结果,如果用户是新用户或结果为空,直接返回全站热门歌曲TopN,保证页面永远有内容。
还可以在注册阶段让用户选感兴趣的歌曲风格,做一个简单的基于内容的冷启动。这样既解决了页面空白问题,又能在论文里名正言顺地写一段“冷启动是协同过滤的固有限制,本系统采用热门兜底和风格偏好结合的方式缓解”。答辩老师十有八九会问冷启动,你提前准备好这段话,回答起来会从容很多。
3. 系统拆分:前台、后台、推荐引擎准备怎么协作
3.1 技术栈和工程目录,别把推荐逻辑混在Controller里
这个项目的技术栈选择不必太激进。后端用Spring Boot 2.7配合MyBatis-Plus,数据库用MySQL 8.0,依赖管理用Maven;前端如果求稳可以用Vue加Element UI,如果只求快速可用,用Thymeleaf模板加Bootstrap也完全足够。要是Redis熟,可以加一个缓存层,不熟的话用Caffeine本地缓存也行。
我第一次给同学改这个项目代码时,发现最容易写砸的结构就是:把推荐算法逻辑直接写在Controller里,一个接口几百行,又算相似度又查数据库。正确的做法是把推荐算法独立成一个 recommend 包,让它只依赖数据查询结果,不直接依赖HTTP请求对象。
code复制music-recommend
├── src/main/java/com/recommend
│ ├── config
│ ├── controller
│ ├── service
│ │ ├── impl
│ │ └── recommend // 推荐算法独立包
│ ├── mapper
│ ├── entity
│ └── common
└── src/main/resources
├── mapper
└── application.yml
这样设计的好处是,答辩时讲代码结构非常清晰:Controller管接口、Service管业务、recommend包管算法。三个层次各司其职,老师一问就知道你没有把工程当成一锅粥。
3.2 功能模块与接口设计
系统的功能模块要围绕“能演示”来规划,不用贪多。核心是用户模块、音乐模块、行为模块、推荐模块和管理后台。接口设计可以参考下面这套,足够覆盖毕设演示场景。
| 模块 | 接口 | 说明 |
|---|---|---|
| 用户 | POST /api/user/register | 注册,包含偏好风格选择 |
| 用户 | POST /api/user/login | 登录,返回会话标识 |
| 音乐 | GET /api/song/search | 歌曲模糊搜索、分页 |
| 音乐 | GET /api/song/detail/ | 歌曲详情 |
| 行为 | POST /api/behavior | 记录播放或收藏行为 |
| 推荐 | GET /api/recommend/user/ | 个性化TopN推荐 |
| 推荐 | GET /api/recommend/hot | 热门榜单兜底 |
| 管理 | GET /api/admin/song/list | 后台歌曲列表 |
| 管理 | POST /api/admin/song/save | 后台新增/编辑歌曲 |
登录态用简单的令牌处理即可,毕设不需要引入特别复杂的权限框架。重要的是行为模块,前端页面每次播放或收藏时都要调用一次行为接口,否则算法没有输入数据。
3.3 推荐引擎的调用时机:离线算、在线查
推荐系统工程化有个核心原则:不要让用户请求去实时计算全量相似度。正确做法是拆分“离线计算”和“在线查询”两个阶段。
离线阶段,系统启动时或通过定时任务读取行为表,构建用户-歌曲矩阵,计算相似度,为每个用户生成TopN推荐列表,把结果写入一张 t_recommend_result 表。在线阶段,推荐接口直接查这张结果表返回。如果某用户没有缓存结果,就走热门兜底。为了演示效果,还可以在管理后台加一个“重新计算推荐”按钮,手动触发离线任务。这个按钮在答辩现场作用很大——点一下,等几秒,页面上的推荐结果发生变化,比纯静态页面直观得多。
4. 协同过滤核心算法:从相似度到TopN推荐的完整实现
4.1 代码里的数据表示:稀疏矩阵怎么存
协同过滤第一步是把用户行为转成算法能算的数据结构。从数学上讲,它是一个用户-歌曲二维矩阵,行是用户,列是歌曲,值是播放次数。但直接用二维数组存这个矩阵是灾难:5万首歌、2000个用户,就是1亿个格子,而且绝大多数是0。
Java里的经典做法是用嵌套Map表示稀疏矩阵。外层key是用户ID,内层key是歌曲ID,value是播放次数(或归一化后的权重)。这种表示和“稀疏矩阵”的算法描述完全对应,代码写起来也直观。
java复制public class SparseMatrix {
private final Map<Long, Map<Long, Double>> data = new HashMap<>();
public void put(long rowId, long colId, double value) {
data.computeIfAbsent(rowId, k -> new HashMap<>()).put(colId, value);
}
public Map<Long, Double> getRow(long rowId) {
return data.getOrDefault(rowId, Collections.emptyMap());
}
public double get(long rowId, long colId) {
return data.getOrDefault(rowId, Collections.emptyMap())
.getOrDefault(colId, 0.0);
}
}
在数据量十几万条、几千个用户的规模下,这种Map方案的内存开销完全可接受,而且实现简单、不容易出错。
4.2 余弦相似度:10来行Java说清楚
UserCF的核心是计算用户之间的相似度。最常见的指标是余弦相似度:把每个用户的播放向量看成高维空间里的一个点,两个用户越“同向”,相似度越高。对于播放次数这种隐式反馈,不需要做均值中心化,直接用原始向量计算即可。
java复制public class CosineSimilarity {
public static double similarity(Map<Long, Double> vecA, Map<Long, Double> vecB) {
if (vecA.isEmpty() || vecB.isEmpty()) {
return 0.0;
}
double dot = 0.0, normA = 0.0, normB = 0.0;
for (double v : vecA.values()) {
normA += v * v;
}
for (double v : vecB.values()) {
normB += v * v;
}
// 遍历较小的map,减少查询次数
Map<Long, Double> small = vecA.size() < vecB.size() ? vecA : vecB;
Map<Long, Double> large = small == vecA ? vecB : vecA;
for (Map.Entry<Long, Double> e : small.entrySet()) {
Double other = large.get(e.getKey());
if (other != null) {
dot += e.getValue() * other;
}
}
return Math.sqrt(normA) * Math.sqrt(normB) == 0 ? 0.0
: dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
}
两个容易被忽略的细节:一是遍历时跑较小的map,能显著减少HashMap查询次数;二是分母为0时直接返回0,避免除零异常。
4.3 UserCF推荐器:找相似用户,再聚合他们听过的歌
UserCF的推荐流程分三步:对目标用户计算其他所有用户的相似度,取TopK近邻,再把近邻们听过的、且目标用户没听过的歌曲按相似度加权累加,取分数最高的前N首。
java复制@Service
public class UserCFRecommender {
private Map<Long, Map<Long, Double>> userItems = new HashMap<>();
public void load(List<Behavior> behaviors) {
for (Behavior b : behaviors) {
userItems.computeIfAbsent(b.getUserId(), k -> new HashMap<>())
.merge(b.getSongId(), Math.log1p(b.getPlayCnt()), Double::sum);
}
}
public List<Long> recommend(Long userId, int topK, int n) {
Map<Long, Double> targetVec = userItems.getOrDefault(userId, Collections.emptyMap());
// 1. 计算目标用户与所有其他用户的余弦相似度
List<Map.Entry<Long, Double>> simUsers = new ArrayList<>();
for (Map.Entry<Long, Map<Long, Double>> entry : userItems.entrySet()) {
if (entry.getKey().equals(userId) || entry.getValue().isEmpty()) {
continue;
}
double sim = CosineSimilarity.similarity(targetVec, entry.getValue());
if (sim > 0) {
simUsers.add(new AbstractMap.SimpleEntry<>(entry.getKey(), sim));
}
}
// 2. 按相似度降序取TopK近邻
simUsers.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
// 3. 近邻物品加权累加,排除目标用户已听过的歌曲
Map<Long, Double> scores = new HashMap<>();
for (int i = 0; i < Math.min(topK, simUsers.size()); i++) {
Map.Entry<Long, Double> neighbor = simUsers.get(i);
Map<Long, Double> neighborItems = userItems.get(neighbor.getKey());
for (Map.Entry<Long, Double> item : neighborItems.entrySet()) {
if (targetVec.containsKey(item.getKey())) {
continue;
}
scores.merge(item.getKey(), neighbor.getValue() * item.getValue(), Double::sum);
}
}
// 4. 排序取TopN
return scores.entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(n)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
一个细节是加载行为时我对播放次数做了 Math.log1p(playCnt)。这是为了防止某个用户对一首歌疯狂刷播放次数导致权重失控,log变换能让数值分布更平稳,属于实用的小技巧。
4.4 ItemCF:同一套相似度代码,换个方向
ItemCF的推荐逻辑和UserCF对称。它先基于“同时被同一个用户听过”的共现关系,计算歌曲与歌曲之间的相似度,然后推荐时把用户历史歌曲的相似歌曲累加排序。核心代码只需要把矩阵反过来:外层key是歌曲ID,内层key是用户ID。
java复制public class ItemCFRecommender {
// songId -> userId -> playCnt
private Map<Long, Map<Long, Double>> itemUsers;
// songId -> similarSongId -> sim
private Map<Long, Map<Long, Double>> itemSim;
public void buildItemSimMatrix() {
// 遍历物品对,复用CosineSimilarity计算相似度
// 只保留每首歌相似度最高的K个近邻,控制矩阵规模
}
public List<Long> recommend(Long userId, int topN) {
Map<Long, Double> scores = new HashMap<>();
Map<Long, Double> userItems = getItemsByUser(userId);
for (Long itemId : userItems.keySet()) {
for (Map.Entry<Long, Double> e : itemSim.getOrDefault(itemId, Collections.emptyMap()).entrySet()) {
if (userItems.containsKey(e.getKey())) {
continue;
}
scores.merge(e.getKey(), e.getValue(), Double::sum);
}
}
return topN(scores, topN);
}
}
ItemCF有个工程上的坑:物品相似矩阵理论大小是歌曲数的平方,5万首歌全量计算会非常夸张。解决思路是每首歌只保留相似度最高的Top50个近邻,其余一律丢弃。这样既控制计算量,又对推荐效果影响很小,论文里还能写一句“稀疏化和相似度截断是协同过滤工程落地的关键手段”。
4.5 性能优化:预计算、并发与缓存
关于协同过滤的性能,必须认清一个事实:每次请求实时算全量相似度一定很慢。所以务必要把计算放到离线阶段。项目里我是这么处理的:启动时从行为表加载全部行为到内存,构建矩阵,然后立刻跑一次UserCF和ItemCF,把所有用户的TopN结果写入 t_recommend_result 表。之后在线查询只查表。
如果数据量再大一些,计算相似度矩阵可以用Java的并行流或线程池,例如 Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 按用户分片并行计算。再加一层Caffeine或Redis缓存,在线接口的响应耗时能做到毫秒级。答辩的时候把这些优化方案讲出来,老师会觉得你不只写了算法,还考虑了真实场景的性能问题。
5. 离线评估与答辩预案:让老师确信你的推荐是有效的
5.1 不要只演示功能,要把指标跑出来
毕设答辩最怕什么?最怕老师看完你的演示,问一句“你这个推荐效果到底怎么样”,你只能说“看起来还行”。要避免这种尴尬,必须做离线实验,用指标说话。
实验设置其实不难:把每个用户的行为按时间排序,前80%当训练集,后20%当测试集。用训练集为每个测试用户生成TopN推荐,再看推荐的歌曲里有多少命中测试集真实收听过的歌。这里的关键是评估代码和训练代码分开,训练代码见不到测试集任何信息。
指标定义记住四个就够:
- 精确率Precision@N = 命中数 / N
- 召回率Recall@N = 命中数 / 测试集真实歌曲数
- F1 = 2 * Precision * Recall / (Precision + Recall)
- 覆盖率Coverage = 被推荐到的歌曲数 / 总歌曲数
跑完一组实验,我的数据大概长这样(示意):
| 指标 | UserCF@10 | ItemCF@10 |
|---|---|---|
| 精确率 Precision | 0.113 | 0.096 |
| 召回率 Recall | 0.078 | 0.071 |
| F1 | 0.092 | 0.082 |
| 覆盖率 Coverage | 13.6% | 21.2% |
这个精确率看起来不高,但推荐系统评估里这是正常量级——用户历史上没听过的歌本来就远多于听过的歌,预测命中本身就是低概率事件。真正有说服力的是两个方法的对比趋势,而不是绝对数值。
5.2 实验结果怎么解释,才有论文深度
分数跑出来只是第一步,能解释清楚才算真懂。一组常见的解释逻辑是这样的:
播放行为天然长尾,大量歌曲只有零星几次行为,模型很难从稀疏数据里学到稳定模式,所以召回率普遍偏低。UserCF在精确率和召回率上略好于ItemCF,符合音乐场景的直觉——音乐口味有明显的社交传染性,和你口味相似的人群喜欢的歌,你更可能喜欢。ItemCF覆盖率更高,说明它更容易把推荐分散到更多小众歌曲上,但精确率会稍低。如果你把N从5调到20,会看到精确率下降、召回率上升,这个趋势本身就可以在论文里写两页讨论。
在论文里给出“指标随N变化”的小表格,再配合你的算法流程截图,评价部分的深度就有了。
5.3 答辩现场的常见质疑清单
答辩环节老师大概率不会深入看你的代码,而是挑几个典型问题来验证你是不是真懂。下面这几个问题出现的频率极高,我建议你提前把答案写成稿子:
| 老师可能问 | 建议答法 |
|---|---|
| 为什么不用深度学习? | 当前数据集规模在万级,深度模型容易过拟合;协同过滤可解释性好、实现成本低,毕设追求算法与场景匹配 |
| 你这个推荐和现在音乐App里的推荐有什么区别? | App用的是多路召回加排序的复杂架构,我实现的是经典协同过滤闭环;核心思想一脉相承,我侧重把单一路线做完整 |
| 新用户没有行为怎么办? | 冷启动问题是协同过滤固有限制,系统用热门榜兜底和注册时风格选择缓解,随着行为累积逐步进入个性化推荐 |
| 用户量大了系统是不是就扛不住了? | 离线预计算加缓存,用户请求只查结果表;未来可扩展分片存储和增量计算 |
| 为什么用Java实现? | Spring Boot生态成熟,工程可维护性高;算法通过离线预计算保证响应速度,Java完全胜任 |
回答的时候不用慌,核心原则是“承认局限,说明方案,再引向自己的实现”。一句“这是协同过滤的固有局限,我采用的方案是……”比支支吾吾好一百倍。
6. 最后想说的实在话:时间规划与避坑心得
如果从零开始做这个题目,我的建议时间线是这样的:第一周搭Spring Boot骨架,把注册登录跑通;第二周导入数据,完成歌曲列表、搜索、播放、收藏页面;第三周实现算法核心,先写一个独立的main方法验证推荐输出,再接进Service;第四周把推荐接口、热门榜、后台管理串起来整体联调;第五周跑评估实验、截图、整理结果表;第六周开始写论文、做PPT、录演示视频。六周完全不慌,你要是只有三周,压缩前面的页面开发,算法和评估部分千万别压缩。
再分享几个我实际踩过或帮别人排查过的坑。第一,MySQL连接串一定要带 useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,否则时区问题和SSL问题会浪费你半天。第二,中文乱码几乎都是连接串没带characterEncoding或者建库不是utf8mb4导致的。我记得有一次歌曲页面打开中文歌名全是问号,第一反应是改页面编码,改完没用,查数据库发现乱码已经入库了,最后定位到是建库时字符集没配好,重新导入数据才解决。排查顺序一般是从外到内:页面、接口、数据库、连接串。第三,推荐接口千万不要现场全量算相似度,内存一爆演示就砸了。第四,造数脚本记得固定随机种子,我见过有人每次跑实验指标都不一样,论文数据根本没法写。
最后说点体会。我见过太多同学想在毕设里证明自己“追得上新技术”,最后反而被技术拖累。协同过滤这种经典算法放到Java工程里认认真真做完,算法理解、数据库设计、Spring开发、实验评估,每一步都是实打实的积累,而这些恰恰是答辩老师最认可的东西。“Java环境下基于协同过滤的音乐推荐系统”这个题目看上去朴实,但把一个闭环完整走下来——数据、存储、算法、接口、评估、论文——比那些飘在空中的“智能XX系统”有价值得多。希望看到这里的你,也能踏踏实实把它做出来。
