基于Spring Boot的云音乐平台设计与实现——集成协同过滤推荐算法的全栈项目实战
📖 文章目录
- 项目概述
- 技术选型与架构设计
- 数据库设计
- 后端核心功能实现
- 推荐算法设计与实现
- 前端交互设计
- 系统优化与性能提升
- 项目部署与测试
- 总结与展望
项目概述
🎯 项目背景
随着数字音乐产业的快速发展,个性化音乐推荐成为提升用户体验的关键技术。本项目基于Spring Boot框架,设计并实现了一个集成协同过滤推荐算法的云音乐平台,旨在为用户提供个性化的音乐推荐服务。
🎨 功能特性
- 用户管理系统:支持用户注册、登录、权限管理
- 音乐资源管理:音乐上传、分类、搜索功能
- 个性化推荐:基于协同过滤算法的智能推荐
- 播放历史追踪:用户行为数据收集与分析
- 后台管理系统:数据统计、用户管理、系统监控
🛠️ 技术亮点
- 采用前后端分离架构,提高系统可维护性
- 实现改进的协同过滤推荐算法,提升推荐准确率
- 集成播放行为分析,支持实时个性化推荐
- 响应式UI设计,支持多端适配
技术选型与架构设计
💻 技术栈
技术分类 | 具体技术 | 版本 | 作用描述 |
---|---|---|---|
后端框架 | Spring Boot | 2.7.x | 快速开发、自动配置 |
持久层 | MyBatis | 3.5.x | ORM映射、SQL优化 |
数据库 | MySQL | 8.0.x | 数据存储、事务管理 |
前端框架 | Layui | 2.6.x | UI组件、表单验证 |
前端技术 | HTML5/CSS3/JS | ES6+ | 用户界面、交互逻辑 |
构建工具 | Maven | 3.8.x | 依赖管理、项目构建 |
🏗️ 系统架构
┌─────────────────────────────────────────────────────────┐
│ 表现层 (Presentation Layer) │
├─────────────────────────────────────────────────────────┤
│ 前端页面 │ 管理后台 │ 移动端适配 │ API接口 │
├─────────────────────────────────────────────────────────┤
│ 业务层 (Business Layer) │
├─────────────────────────────────────────────────────────┤
│ 用户服务 │ 音乐服务 │ 推荐服务 │ 播放历史服务 │ 统计服务 │
├─────────────────────────────────────────────────────────┤
│ 数据访问层 (Data Access Layer) │
├─────────────────────────────────────────────────────────┤
│ MyBatis Mapper │ 数据缓存 │ 连接池 │
├─────────────────────────────────────────────────────────┤
│ 数据层 (Data Layer) │
└─────────────────────────────────────────────────────────┘
│ MySQL数据库集群 │
└─────────────────────────────────────────┘
📁 项目结构
src
├── main
│ ├── java
│ │ └── com.example.music
│ │ ├── controller # 控制层
│ │ ├── service # 业务层
│ │ ├── mapper # 数据访问层
│ │ ├── entity # 实体类
│ │ ├── config # 配置类
│ │ ├── algorithm # 推荐算法
│ │ └── utils # 工具类
│ └── resources
│ ├── static # 静态资源
│ ├── templates # 模板文件
│ └── mapper # SQL映射文件
└── test # 测试代码
数据库设计
🗄️ 核心表结构
1. 用户表 (users)
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`role` tinyint DEFAULT '0' COMMENT '角色:0-普通用户,1-管理员',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2. 歌曲表 (songs)
CREATE TABLE `songs` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '歌曲ID',
`name` varchar(100) NOT NULL COMMENT '歌曲名称',
`singer` varchar(100) NOT NULL COMMENT '歌手',
`album` varchar(100) DEFAULT NULL COMMENT '专辑',
`duration` int DEFAULT NULL COMMENT '时长(秒)',
`file_path` varchar(255) NOT NULL COMMENT '文件路径',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图片',
`play_count` int DEFAULT '0' COMMENT '播放次数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_singer` (`singer`),
KEY `idx_name` (`name`),
KEY `idx_play_count` (`play_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌曲表';
3. 播放历史表 (user_play_history)
CREATE TABLE `user_play_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int NOT NULL COMMENT '用户ID',
`song_id` int NOT NULL COMMENT '歌曲ID',
`play_time` datetime NOT NULL COMMENT '播放时间',
`play_duration` int DEFAULT '0' COMMENT '播放时长(秒)',
`play_percentage` decimal(5,2) DEFAULT '0.00' COMMENT '播放完成度(%)',
`device_type` varchar(20) DEFAULT 'web' COMMENT '设备类型',
`ip_address` varchar(45) DEFAULT NULL COMMENT 'IP地址',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_song_id` (`song_id`),
KEY `idx_play_time` (`play_time`),
KEY `idx_user_song` (`user_id`, `song_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户播放历史表';
📊 数据库优化策略
- 索引优化:为高频查询字段建立复合索引
- 分区策略:播放历史表按月分区,提高查询效率
- 数据归档:历史数据定期归档,控制表大小
后端核心功能实现
🔐 用户认证与授权
JWT令牌实现
@Service
public class JwtTokenService {
private static final String SECRET_KEY = "music_platform_secret";
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 24小时
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("role", user.getRole());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
权限拦截器
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtTokenService jwtTokenService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new UnauthorizedException("未登录或token已过期");
}
try {
Claims claims = jwtTokenService.getClaimsFromToken(token);
// 将用户信息存储到ThreadLocal
UserContext.setCurrentUser(claims);
return true;
} catch (Exception e) {
throw new UnauthorizedException("token验证失败");
}
}
}
🎵 音乐服务实现
音乐上传与处理
@Service
@Transactional
public class MusicServiceImpl implements MusicService {
@Autowired
private SongMapper songMapper;
@Value("${file.upload.path}")
private String uploadPath;
public Result<Song> uploadMusic(MultipartFile file, SongUploadDTO songDTO) {
try {
// 1. 文件格式验证
validateAudioFile(file);
// 2. 保存文件
String fileName = saveFile(file);
// 3. 提取音频信息
AudioMetadata metadata = extractAudioMetadata(file);
// 4. 保存到数据库
Song song = new Song();
song.setName(songDTO.getName());
song.setSinger(songDTO.getSinger());
song.setAlbum(songDTO.getAlbum());
song.setDuration(metadata.getDuration());
song.setFilePath(fileName);
songMapper.insert(song);
return Result.success(song);
} catch (Exception e) {
log.error("音乐上传失败", e);
return Result.error("上传失败:" + e.getMessage());
}
}
private AudioMetadata extractAudioMetadata(MultipartFile file) throws Exception {
// 使用FFmpeg或其他音频处理库提取元数据
// 这里简化处理
return new AudioMetadata();
}
}
📈 播放历史记录服务
@Service
public class PlayHistoryServiceImpl implements PlayHistoryService {
@Autowired
private UserPlayHistoryMapper playHistoryMapper;
@Async
public void recordPlayHistory(PlayHistoryDTO playDTO) {
UserPlayHistory history = new UserPlayHistory();
history.setUserId(playDTO.getUserId());
history.setSongId(playDTO.getSongId());
history.setPlayTime(new Date());
history.setPlayDuration(playDTO.getPlayDuration());
history.setPlayPercentage(calculatePercentage(playDTO));
history.setDeviceType(playDTO.getDeviceType());
history.setIpAddress(playDTO.getIpAddress());
playHistoryMapper.insert(history);
// 异步更新歌曲播放统计
updateSongPlayStats(playDTO.getSongId());
}
private BigDecimal calculatePercentage(PlayHistoryDTO playDTO) {
if (playDTO.getTotalDuration() == 0) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(playDTO.getPlayDuration())
.divide(BigDecimal.valueOf(playDTO.getTotalDuration()), 2, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
}
}
推荐算法设计与实现
🧠 协同过滤算法核心实现
1. 用户相似度计算
@Service
public class RecommendationServiceImpl implements RecommendationService {
@Autowired
private UserPlayHistoryMapper playHistoryMapper;
@Autowired
private FavoriteMapper favoriteMapper;
/**
* 计算用户相似度(余弦相似度)
*/
public double calculateUserSimilarity(Integer userId1, Integer userId2) {
// 获取用户行为数据
Map<Integer, Double> user1Preferences = getUserPreferences(userId1);
Map<Integer, Double> user2Preferences = getUserPreferences(userId2);
// 计算余弦相似度
return calculateCosineSimilarity(user1Preferences, user2Preferences);
}
/**
* 获取用户偏好向量(结合收藏和播放历史)
*/
private Map<Integer, Double> getUserPreferences(Integer userId) {
Map<Integer, Double> preferences = new HashMap<>();
// 1. 从收藏获取偏好(权重0.6)
List<Favorite> favorites = favoriteMapper.findByUserId(userId);
for (Favorite favorite : favorites) {
preferences.put(favorite.getSongId(), 0.6);
}
// 2. 从播放历史获取偏好(权重0.4)
List<UserPlayHistory> playHistory = playHistoryMapper.findByUserId(userId);
Map<Integer, Double> playWeights = calculatePlayHistoryWeights(playHistory);
for (Map.Entry<Integer, Double> entry : playWeights.entrySet()) {
Integer songId = entry.getKey();
Double weight = entry.getValue() * 0.4;
preferences.merge(songId, weight, Double::sum);
}
return preferences;
}
/**
* 计算播放历史权重(考虑时间衰减)
*/
private Map<Integer, Double> calculatePlayHistoryWeights(List<UserPlayHistory> playHistory) {
Map<Integer, PlayStats> songStats = new HashMap<>();
Date now = new Date();
for (UserPlayHistory history : playHistory) {
Integer songId = history.getSongId();
PlayStats stats = songStats.computeIfAbsent(songId, k -> new PlayStats());
// 时间衰减因子
long daysDiff = (now.getTime() - history.getPlayTime().getTime()) / (24 * 60 * 60 * 1000);
double timeDecay = Math.exp(-daysDiff / 30.0); // 30天衰减
// 播放完成度权重
double completionWeight = history.getPlayPercentage().doubleValue() / 100.0;
// 综合权重
double weight = timeDecay * completionWeight;
stats.addWeight(weight);
stats.incrementPlayCount();
}
// 计算最终权重
Map<Integer, Double> weights = new HashMap<>();
for (Map.Entry<Integer, PlayStats> entry : songStats.entrySet()) {
PlayStats stats = entry.getValue();
// 结合播放次数和平均权重
double finalWeight = Math.log(1 + stats.getPlayCount()) * stats.getAverageWeight();
weights.put(entry.getKey(), finalWeight);
}
return weights;
}
/**
* 余弦相似度计算
*/
private double calculateCosineSimilarity(Map<Integer, Double> vector1,
Map<Integer, Double> vector2) {
Set<Integer> commonItems = new HashSet<>(vector1.keySet());
commonItems.retainAll(vector2.keySet());
if (commonItems.isEmpty()) {
return 0.0;
}
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
for (Integer item : commonItems) {
double score1 = vector1.get(item);
double score2 = vector2.get(item);
dotProduct += score1 * score2;
norm1 += score1 * score1;
norm2 += score2 * score2;
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
}
2. 推荐生成算法
public List<Song> generateRecommendations(Integer userId, int limit) {
// 1. 找到相似用户
List<UserSimilarity> similarUsers = findSimilarUsers(userId, 50);
// 2. 获取候选歌曲
Map<Integer, Double> candidateScores = new HashMap<>();
for (UserSimilarity similar : similarUsers) {
List<Integer> userSongs = getUserLikedSongs(similar.getUserId());
List<Integer> currentUserSongs = getUserLikedSongs(userId);
// 过滤已知歌曲
userSongs.removeAll(currentUserSongs);
for (Integer songId : userSongs) {
double score = similar.getSimilarity() * getSongWeight(similar.getUserId(), songId);
candidateScores.merge(songId, score, Double::sum);
}
}
// 3. 排序并返回Top N
return candidateScores.entrySet().stream()
.sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
.limit(limit)
.map(entry -> songMapper.findById(entry.getKey()))
.collect(Collectors.toList());
}
🎯 冷启动问题解决
@Service
public class ColdStartService {
/**
* 新用户推荐策略
*/
public List<Song> getNewUserRecommendations(Integer userId) {
// 1. 热门歌曲推荐
List<Song> hotSongs = songMapper.findHotSongs(20);
// 2. 多样性推荐(不同风格)
List<Song> diverseSongs = songMapper.findDiverseSongs(10);
// 3. 最新歌曲推荐
List<Song> latestSongs = songMapper.findLatestSongs(10);
// 组合推荐结果
List<Song> recommendations = new ArrayList<>();
recommendations.addAll(hotSongs);
recommendations.addAll(diverseSongs);
recommendations.addAll(latestSongs);
// 去重并随机排序
return recommendations.stream()
.distinct()
.sorted((a, b) -> (int) (Math.random() * 3 - 1))
.limit(30)
.collect(Collectors.toList());
}
}
前端交互设计
🎨 现代化UI实现
1. 响应式登录界面
/* 渐变背景与毛玻璃效果 */
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 40px 30px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.1);
animation: slideUp 0.8s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
2. 播放历史追踪
class PlayHistoryTracker {
constructor() {
this.trackingInterval = null;
this.startTime = null;
this.currentSong = null;
}
startTracking(songInfo) {
this.currentSong = songInfo;
this.startTime = Date.now();
// 每30秒记录一次播放进度
this.trackingInterval = setInterval(() => {
this.recordProgress();
}, 30000);
}
recordProgress() {
if (!this.currentSong || !this.startTime) return;
const playDuration = Math.floor((Date.now() - this.startTime) / 1000);
const totalDuration = this.currentSong.duration;
const playPercentage = Math.min((playDuration / totalDuration) * 100, 100);
// 只记录播放时长超过5秒的记录
if (playDuration > 5) {
this.sendPlayHistory({
songId: this.currentSong.id,
playDuration: playDuration,
playPercentage: playPercentage.toFixed(2),
deviceType: this.getDeviceType(),
timestamp: new Date().toISOString()
});
}
}
sendPlayHistory(data) {
fetch('/api/play-history/record', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('token')
},
body: JSON.stringify(data)
}).catch(error => {
console.warn('播放历史记录失败:', error);
});
}
getDeviceType() {
const userAgent = navigator.userAgent.toLowerCase();
if (/mobile|android|iphone|ipad/.test(userAgent)) {
return 'mobile';
}
return 'web';
}
stopTracking() {
if (this.trackingInterval) {
clearInterval(this.trackingInterval);
this.trackingInterval = null;
}
// 记录最终播放记录
if (this.currentSong && this.startTime) {
this.recordProgress();
}
this.currentSong = null;
this.startTime = null;
}
}
// 全局播放追踪器
const playTracker = new PlayHistoryTracker();
3. 个人中心数据可视化
class PersonalCenter {
constructor() {
this.initializeCharts();
this.loadUserStats();
}
async loadUserStats() {
try {
const response = await fetch('/api/user/stats', {
headers: {
'Authorization': localStorage.getItem('token')
}
});
const stats = await response.json();
this.updateStatsCards(stats.data);
this.renderPlayTimeChart(stats.data.playTimeData);
this.renderGenreChart(stats.data.genreData);
} catch (error) {
console.error('加载用户统计失败:', error);
}
}
updateStatsCards(stats) {
document.getElementById('total-plays').textContent = stats.totalPlays;
document.getElementById('total-songs').textContent = stats.totalSongs;
document.getElementById('total-duration').textContent = this.formatDuration(stats.totalDuration);
document.getElementById('avg-completion').textContent = stats.avgCompletion + '%';
}
renderPlayTimeChart(data) {
const ctx = document.getElementById('playTimeChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [{
label: '播放时长(分钟)',
data: data.map(d => d.duration),
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}小时${minutes}分钟`;
}
}
系统优化与性能提升
⚡ 数据库优化
1. 索引优化策略
-- 播放历史表复合索引
CREATE INDEX idx_user_time ON user_play_history(user_id, play_time DESC);
CREATE INDEX idx_song_time ON user_play_history(song_id, play_time DESC);
-- 用户相似度计算专用索引
CREATE INDEX idx_user_song_weight ON user_play_history(user_id, song_id, play_percentage);
-- 分区策略 - 按月分区
ALTER TABLE user_play_history PARTITION BY RANGE (YEAR(play_time)*100 + MONTH(play_time)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
-- ... 更多分区
PARTITION pmax VALUES LESS THAN MAXVALUE
);
2. 缓存策略
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String USER_SIMILARITY_KEY = "user:similarity:{}:{}";
private static final String HOT_SONGS_KEY = "songs:hot";
private static final String USER_RECOMMENDATIONS_KEY = "user:recommendations:{}";
/**
* 缓存用户相似度
*/
public void cacheUserSimilarity(Integer userId1, Integer userId2, Double similarity) {
String key = USER_SIMILARITY_KEY.replace("{}", userId1.toString()).replace("{}", userId2.toString());
redisTemplate.opsForValue().set(key, similarity, Duration.ofHours(6));
}
/**
* 缓存热门歌曲
*/
@Scheduled(fixedRate = 3600000) // 每小时更新
public void updateHotSongsCache() {
List<Song> hotSongs = songMapper.findHotSongs(100);
redisTemplate.opsForValue().set(HOT_SONGS_KEY, hotSongs, Duration.ofHours(1));
}
/**
* 缓存用户推荐
*/
public void cacheUserRecommendations(Integer userId, List<Song> recommendations) {
String key = USER_RECOMMENDATIONS_KEY.replace("{}", userId.toString());
redisTemplate.opsForValue().set(key, recommendations, Duration.ofMinutes(30));
}
}
🔄 异步处理优化
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Service
public class AsyncRecommendationService {
@Async("taskExecutor")
@Retryable(value = Exception.class, maxAttempts = 3)
public CompletableFuture<List<Song>> generateRecommendationsAsync(Integer userId) {
List<Song> recommendations = recommendationService.generateRecommendations(userId, 30);
cacheService.cacheUserRecommendations(userId, recommendations);
return CompletableFuture.completedFuture(recommendations);
}
}
项目部署与测试
🚀 Docker部署配置
# Dockerfile
FROM openjdk:11-jre-slim
MAINTAINER developer@musicplatform.com
VOLUME /tmp
COPY target/music-platform-1.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: music_platform
volumes:
- mysql_data:/var/lib/mysql
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3306:3306"
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
REDIS_HOST: redis
volumes:
mysql_data:
redis_data:
🧪 单元测试
@SpringBootTest
@Transactional
class RecommendationServiceTest {
@Autowired
private RecommendationService recommendationService;
@MockBean
private UserPlayHistoryMapper playHistoryMapper;
@Test
void testCalculateUserSimilarity() {
// 准备测试数据
Integer userId1 = 1;
Integer userId2 = 2;
List<UserPlayHistory> user1History = createMockPlayHistory(userId1);
List<UserPlayHistory> user2History = createMockPlayHistory(userId2);
when(playHistoryMapper.findByUserId(userId1)).thenReturn(user1History);
when(playHistoryMapper.findByUserId(userId2)).thenReturn(user2History);
// 执行测试
double similarity = recommendationService.calculateUserSimilarity(userId1, userId2);
// 验证结果
assertThat(similarity).isBetween(0.0, 1.0);
}
@Test
void testGenerateRecommendations() {
Integer userId = 1;
int limit = 10;
List<Song> recommendations = recommendationService.generateRecommendations(userId, limit);
assertThat(recommendations).hasSize(limit);
assertThat(recommendations).allMatch(song -> song.getId() != null);
}
private List<UserPlayHistory> createMockPlayHistory(Integer userId) {
// 创建模拟播放历史数据
return Arrays.asList(
createPlayHistory(userId, 1, 180, new BigDecimal("85.5")),
createPlayHistory(userId, 2, 240, new BigDecimal("92.0")),
createPlayHistory(userId, 3, 150, new BigDecimal("78.2"))
);
}
}
📊 性能测试
@Component
public class PerformanceMonitor {
private final MeterRegistry meterRegistry;
public PerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@EventListener
public void handleRecommendationGenerated(RecommendationGeneratedEvent event) {
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("recommendation.generation.time")
.description("推荐算法执行时间")
.register(meterRegistry));
meterRegistry.counter("recommendation.generated.count",
"user_type", event.isNewUser() ? "new" : "existing")
.increment();
}
}
运行效果图
总结与展望
🎯 项目成果
通过本项目的开发,成功实现了以下目标:
- 技术架构:构建了可扩展的前后端分离架构
- 核心功能:实现了完整的音乐平台基础功能
- 智能推荐:集成了有效的协同过滤推荐算法
- 用户体验:提供了现代化的UI界面和流畅的交互
- 性能优化:通过缓存、异步处理等手段提升了系统性能
📈 技术亮点
- 推荐算法优化:结合时间衰减和播放完成度的权重计算
- 实时数据收集:精准的用户行为追踪机制
- 响应式设计:适配多端的现代化UI界面
- 性能优化:多层次的缓存策略和异步处理
🔮 未来展望
- 算法优化:引入深度学习模型,提升推荐精度
- 功能扩展:添加社交功能、歌单分享等特性
- 技术升级:微服务架构改造,提升系统可扩展性
- AI增强:集成自然语言处理,支持智能音乐搜索
💡 学习心得
- 系统设计:良好的架构设计是项目成功的基础
- 算法实现:理论与实践相结合,注重实际应用效果
- 性能优化:从多个维度考虑系统性能问题
- 用户体验:技术服务于用户,体验至上
🔗 参考资源
- Spring Boot官方文档
- MyBatis官方文档
- 协同过滤算法详解
- MySQL性能优化指南
如果这篇文章对你有帮助,请点赞、收藏、关注!也欢迎分享给更多需要的同学~