基于Spring Boot的云音乐平台设计与实现

news2025/6/8 8:48:34

基于Spring Boot的云音乐平台设计与实现——集成协同过滤推荐算法的全栈项目实战

📖 文章目录

  • 项目概述
  • 技术选型与架构设计
  • 数据库设计
  • 后端核心功能实现
  • 推荐算法设计与实现
  • 前端交互设计
  • 系统优化与性能提升
  • 项目部署与测试
  • 总结与展望

项目概述

🎯 项目背景

随着数字音乐产业的快速发展,个性化音乐推荐成为提升用户体验的关键技术。本项目基于Spring Boot框架,设计并实现了一个集成协同过滤推荐算法的云音乐平台,旨在为用户提供个性化的音乐推荐服务。

🎨 功能特性

  • 用户管理系统:支持用户注册、登录、权限管理
  • 音乐资源管理:音乐上传、分类、搜索功能
  • 个性化推荐:基于协同过滤算法的智能推荐
  • 播放历史追踪:用户行为数据收集与分析
  • 后台管理系统:数据统计、用户管理、系统监控

🛠️ 技术亮点

  • 采用前后端分离架构,提高系统可维护性
  • 实现改进的协同过滤推荐算法,提升推荐准确率
  • 集成播放行为分析,支持实时个性化推荐
  • 响应式UI设计,支持多端适配

技术选型与架构设计

💻 技术栈

技术分类具体技术版本作用描述
后端框架Spring Boot2.7.x快速开发、自动配置
持久层MyBatis3.5.xORM映射、SQL优化
数据库MySQL8.0.x数据存储、事务管理
前端框架Layui2.6.xUI组件、表单验证
前端技术HTML5/CSS3/JSES6+用户界面、交互逻辑
构建工具Maven3.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='用户播放历史表';

📊 数据库优化策略

  1. 索引优化:为高频查询字段建立复合索引
  2. 分区策略:播放历史表按月分区,提高查询效率
  3. 数据归档:历史数据定期归档,控制表大小

后端核心功能实现

🔐 用户认证与授权

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();
    }
}

运行效果图

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

总结与展望

🎯 项目成果

通过本项目的开发,成功实现了以下目标:

  1. 技术架构:构建了可扩展的前后端分离架构
  2. 核心功能:实现了完整的音乐平台基础功能
  3. 智能推荐:集成了有效的协同过滤推荐算法
  4. 用户体验:提供了现代化的UI界面和流畅的交互
  5. 性能优化:通过缓存、异步处理等手段提升了系统性能

📈 技术亮点

  • 推荐算法优化:结合时间衰减和播放完成度的权重计算
  • 实时数据收集:精准的用户行为追踪机制
  • 响应式设计:适配多端的现代化UI界面
  • 性能优化:多层次的缓存策略和异步处理

🔮 未来展望

  1. 算法优化:引入深度学习模型,提升推荐精度
  2. 功能扩展:添加社交功能、歌单分享等特性
  3. 技术升级:微服务架构改造,提升系统可扩展性
  4. AI增强:集成自然语言处理,支持智能音乐搜索

💡 学习心得

  1. 系统设计:良好的架构设计是项目成功的基础
  2. 算法实现:理论与实践相结合,注重实际应用效果
  3. 性能优化:从多个维度考虑系统性能问题
  4. 用户体验:技术服务于用户,体验至上

🔗 参考资源

  • Spring Boot官方文档
  • MyBatis官方文档
  • 协同过滤算法详解
  • MySQL性能优化指南

如果这篇文章对你有帮助,请点赞、收藏、关注!也欢迎分享给更多需要的同学~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2403940.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Neovim - 打造一款属于自己的编辑器(一)

文章目录 前言&#xff08;劝退&#xff09;neovim 安装neovim 配置配置文件位置第一个 hello world 代码拆分 neovim 配置正式配置 neovim基础配置自定义键位Lazy 插件管理器配置tokyonight 插件配置BufferLine 插件配置自动补全括号 / 引号 插件配置 前言&#xff08;劝退&am…

RAG检索系统的两大核心利器——Embedding模型和Rerank模型

在RAG系统中&#xff0c;有两个非常重要的模型一个是Embedding模型&#xff0c;另一个则是Rerank模型&#xff1b;这两个模型在RAG中扮演着重要角色。 Embedding模型的作用是把数据向量化&#xff0c;通过降维的方式&#xff0c;使得可以通过欧式距离&#xff0c;余弦函数等计算…

CLion社区免费后,使用CLion开发STM32相关工具资源汇总与入门教程

Clion下载与配置 Clion推出社区免费&#xff0c;就是需要注册一个账号使用&#xff0c;大家就不用去找破解版版本了&#xff0c;jetbrains家的IDEA用过的都说好&#xff0c;这里嵌入式领域也推荐使用。 CLion官网下载地址 安装没有什么特别&#xff0c;下一步就好。 启动登录…

第21讲、Odoo 18 配置机制详解

Odoo 18 配置机制详解&#xff1a;res.config.settings 与 ir.config_parameter 原理与实战指南 在现代企业信息化系统中&#xff0c;灵活且可维护的系统参数配置是模块开发的核心能力之一。Odoo 作为一款高度模块化的企业管理软件&#xff0c;其参数配置机制主要依赖于两个关…

【计算机网络】Linux下简单的TCP服务器(超详细)

服务端 创建套接字 &#x1f4bb;我们将TCP服务器封装成一个类&#xff0c;当我们定义出一个服务器对象后需要马上对服务器进行初始化&#xff0c;而初始化TCP服务器要做的第一件事就是创建套接字。 TCP服务器在调用socket函数创建套接字时&#xff0c;参数设置如下&#xff1…

最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

html+css+js趣味小游戏~Cookie Clicker放置休闲(附源码)

下面是一个简单的记忆卡片配对游戏的完整代码&#xff0c;使用HTML、CSS和JavaScript实现&#xff1a; html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"wid…

SDC命令详解:使用set_propagated_clock命令进行约束

相关阅读 SDC命令详解https://blog.csdn.net/weixin_45791458/category_12931432.html?spm1001.2014.3001.5482 目录 指定端口列表/集合 简单使用 注意事项 传播时钟是在进行了时钟树综合后&#xff0c;使用set_propagated_clock命令可以将一个理想时钟转换为传播时钟&#x…

win32相关(消息Hook)

消息Hook 要想实现消息Hook需要使用到三个相关的Api SetWindowsHookEx // 设置钩子CallNextHookEx // 将钩子信息传递到当前钩子链中的下一个子程序UnhookWindowsHookEx // 卸载钩子 我们编写的消息钩子需要将设置钩子的函数写到dll里面&#xff0c;当钩住一个线程后&#xff…

mysql 页的理解和实际分析

目录 页&#xff08;Page&#xff09;是 Innodb 存储引擎用于管理数据的最小磁盘单位B树的一般高度记录在页中的存储 innodb ibd文件innodb 页类型分析ibd文件查看数据表的行格式查看ibd文件 分析 ibd的第4个页&#xff1a;B-tree Node类型先分析File Header(38字节-描述页信息…

构建 MCP 服务器:第 2 部分 — 使用资源模板扩展资源

该图像是使用 AI 图像创建程序创建的。 这个故事是在多位人工智能助手的帮助下写成的。 这是构建MCP 服务器教程&#xff08;共四部分&#xff09;的第二部分。在第一部分中&#xff0c;我们使用基本资源创建了第一个 MCP 服务器。现在&#xff0c;我们将使用资源模板扩展服务…

【算法设计与分析】实验——汽车加油问题, 删数问题(算法实现:代码,测试用例,结果分析,算法思路分析,总结)

说明&#xff1a;博主是大学生&#xff0c;有一门课是算法设计与分析&#xff0c;这是博主记录课程实验报告的内容&#xff0c;题目是老师给的&#xff0c;其他内容和代码均为原创&#xff0c;可以参考学习&#xff0c;转载和搬运需评论吱声并注明出处哦。 4-1算法实现题 汽车…

【C++进阶篇】C++11新特性(下篇)

C函数式编程黑魔法&#xff1a;Lambda与包装器实战全解析 一. lambda表达式1.1 仿函数使用1.2 lambda表达式的语法1.3 lambda表达式使用1.3.1 传值和传引用捕捉1.3.2 隐式捕捉1.3.3 混合捕捉 1.4 lambda表达式原理1.5 lambda优点及建议 二. 包装器2.1 function2.2 bind绑定 三.…

全生命周期的智慧城市管理

前言 全生命周期的智慧城市管理。未来&#xff0c;城市将在 实现从基础设施建设、日常运营到数据管理的 全生命周期统筹。这将避免过去智慧城市建设 中出现的“碎片化”问题&#xff0c;实现资源的高效配 置和项目的协调发展。城市管理者将运用先进 的信息技术&#xff0c;如物…

echarts柱状图实现动态展示时报错

echarts柱状图实现动态展示时报错 1、问题&#xff1a; 在使用Echarts柱状图时&#xff0c;当数据量过多&#xff0c;x轴展示不下的时候&#xff0c;可以使用dataZoom实现动态展示。如下图所示&#xff1a; 但是当鼠标放在图上面滚动滚轮时或拖动滚动条时会报错&#xff0c;…

408第一季 - 数据结构 - 线性表

只能用C/C&#xff01; 顺序表 闲聊 线性表的逻辑顺序和物理顺序相同 都是1234 顺序表的优点&#xff1a; 随机访问&#xff0c;随机访问的意思是访问的时间 和位置没有关系&#xff0c;访问下标1和100一样的&#xff0c;更深层就是直接计算 a100 * 数组大小&#xff0c;随便…

第23讲、Odoo18 邮件系统整体架构

目录 Odoo 邮件系统整体架构邮件发送方式邮件模板配置SMTP 邮件服务器配置邮件发送过程开发中常见邮件发送需求常见问题排查提示与最佳实践完整示例&#xff1a;审批通过自动发邮件门户表单自动邮件通知案例邮件队列与异步发送邮件添加附件邮件日志与调试多语言邮件模板邮件安…

HarmonyOS:Counter计数器组件

一、概述 计数器组件&#xff0c;提供相应的增加或者减少的计数操作。 说明 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 二、属性 除支持通用属性外&#xff0c;还支持以下属性。 enableInc enableInc(value: b…

sqlsugar WhereIF条件的大于等于和等于查出来的坑

一、如下图所示&#xff0c;当我用 .WhereIF(input.Plancontroltype > 0, u > u.Plancontroltype (DnjqPlancontroltype)input.Plancontroltype) 这里面用等于的时候&#xff0c;返回结果一条数据都没有。 上图中生成的SQL如下&#xff1a; SELECT id AS Id ,code AS …

Pandas 技术解析:从数据结构到应用场景的深度探索

序 我最早用Python做大数据项目时&#xff0c;接触最早的就是Pandas了。觉得对于IT技术人员而言&#xff0c;它是可以属于多场景的存在&#xff0c;因为它的本身就是数据驱动的技术生态中&#xff0c;对于软件工程师而言&#xff0c;它是快速构建数据处理管道的基石&#xff1…