Building a Spotify Playlist Application with Spring Boot and Vue.js

🎯 專案動機與背景

Spotify 作為全球最受歡迎的音樂串流平台之一,雖然擁有強大的推薦演算法,但往往會陷入推薦相似歌曲的循環中,使用者缺乏主動探索新音樂的有效途徑。因此,我開發了這個全端應用程式,讓使用者能夠更主動地控制音樂發現過程。

💡 核心理念: “讓使用者主動參與音樂推薦過程,而不是被動接受演算法的建議”

🏗️ 系統架構總覽

🔧 技術堆疊

 1Frontend (前端)
 2├── Vue.js 3.x
 3├── Vue Router
 4├── Axios (HTTP Client)
 5└── Bootstrap/CSS3
 6
 7Backend (後端)  
 8├── Spring Boot 2.x
 9├── Spring Security (OAuth2)
10├── Spring Web MVC
11├── Spotify Web API Java Client
12└── Maven
13
14External Services (外部服務)
15├── Spotify Web API
16├── Spotify OAuth 2.0
17└── Machine Learning 推薦引擎

🗺️ 系統架構流程圖

graph TD
    A[使用者] --> B[Vue.js Frontend]
    B --> C[Spring Boot Backend]
    C --> D[Spotify OAuth Server]
    C --> E[Spotify Web API]
    C --> F[ML Recommendation Engine]
    D --> G[Access Token]
    G --> C
    E --> H[音樂資料]
    H --> C
    F --> I[個人化推薦]
    I --> C
    C --> B
    B --> A
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#fff3e0
    style F fill:#fce4ec

⭐ 核心功能特色

🤖 1. 智能音樂推薦系統

  • 基於機器學習的推薦演算法
  • 多維度音樂特徵分析 (節拍、能量、舞蹈性等)
  • 使用者偏好學習與適應

🔍 2. 互動式音樂探索

  • 藝人/歌曲智能搜尋
  • 專輯預覽與試聽功能
  • 相關音樂發現

🔐 3. Spotify 深度整合

  • OAuth 2.0 安全認證
  • 即時播放清單同步
  • 使用者音樂庫存取

🖥️ 後端核心實作

🔐 Spotify OAuth 認證流程

 1@RestController
 2@RequestMapping("/api/spotify")
 3public class SpotifyController {
 4    
 5    @Autowired
 6    private SpotifyService spotifyService;
 7    
 8    /**
 9     * 初始化 Spotify OAuth 認證流程
10     * 引導使用者至 Spotify 授權頁面
11     */
12    @GetMapping("/auth")
13    public ResponseEntity<?> authenticateSpotify(HttpServletRequest request) {
14        try {
15            // 生成隨機 state 參數防止 CSRF 攻擊
16            String state = UUID.randomUUID().toString();
17            request.getSession().setAttribute("spotify_state", state);
18            
19            // 構建 Spotify 授權 URL
20            String authUrl = spotifyService.getAuthorizationUrl(state);
21            
22            return ResponseEntity.ok(Map.of(
23                "authUrl", authUrl,
24                "message", "請前往此 URL 進行 Spotify 授權"
25            ));
26        } catch (Exception e) {
27            return ResponseEntity.status(500)
28                .body(Map.of("error", "授權初始化失敗: " + e.getMessage()));
29        }
30    }
31    
32    /**
33     * 處理 Spotify OAuth 回調
34     * 交換授權碼取得存取權杖
35     */
36    @GetMapping("/callback")
37    public ResponseEntity<?> handleCallback(
38            @RequestParam("code") String code,
39            @RequestParam("state") String state,
40            HttpServletRequest request) {
41        
42        try {
43            // 驗證 state 參數
44            String sessionState = (String) request.getSession().getAttribute("spotify_state");
45            if (!state.equals(sessionState)) {
46                throw new SecurityException("State 參數驗證失敗");
47            }
48            
49            // 交換授權碼取得 access token
50            SpotifyTokens tokens = spotifyService.exchangeCodeForTokens(code);
51            
52            // 儲存 tokens 到 session 或資料庫
53            request.getSession().setAttribute("spotify_tokens", tokens);
54            
55            return ResponseEntity.ok(Map.of(
56                "message", "Spotify 授權成功",
57                "expiresIn", tokens.getExpiresIn()
58            ));
59            
60        } catch (Exception e) {
61            return ResponseEntity.status(400)
62                .body(Map.of("error", "授權處理失敗: " + e.getMessage()));
63        }
64    }
65}

🎵 音樂推薦核心演算法

  1@Service
  2public class MusicRecommendationService {
  3    
  4    @Autowired
  5    private SpotifyApiService spotifyApiService;
  6    
  7    /**
  8     * 基於使用者偏好產生音樂推薦
  9     * 結合多種推薦策略提供個人化建議
 10     */
 11    public List<Track> generateRecommendations(String userId, RecommendationRequest request) {
 12        try {
 13            // 1. 獲取使用者歷史播放記錄
 14            List<Track> recentTracks = spotifyApiService.getRecentlyPlayed(userId, 50);
 15            
 16            // 2. 分析音樂特徵偏好
 17            AudioFeaturePreferences preferences = analyzeUserPreferences(recentTracks);
 18            
 19            // 3. 種子歌曲/藝人選擇
 20            RecommendationSeeds seeds = buildRecommendationSeeds(request, preferences);
 21            
 22            // 4. 呼叫 Spotify 推薦 API
 23            List<Track> spotifyRecommendations = spotifyApiService.getRecommendations(
 24                seeds.getArtists(),
 25                seeds.getTracks(),
 26                seeds.getGenres(),
 27                preferences.toTuneableAttributes()
 28            );
 29            
 30            // 5. 應用自定義過濾與排序
 31            List<Track> filteredTracks = applyCustomFiltering(
 32                spotifyRecommendations, 
 33                preferences,
 34                request.getExcludedArtists()
 35            );
 36            
 37            // 6. 多樣性增強處理
 38            return enhanceDiversity(filteredTracks, request.getDiversityLevel());
 39            
 40        } catch (Exception e) {
 41            log.error("音樂推薦生成失敗", e);
 42            throw new RecommendationException("推薦系統暫時無法使用", e);
 43        }
 44    }
 45    
 46    /**
 47     * 分析使用者音樂偏好模式
 48     * 從歷史播放記錄中提取音頻特徵趋势
 49     */
 50    private AudioFeaturePreferences analyzeUserPreferences(List<Track> recentTracks) {
 51        if (recentTracks.isEmpty()) {
 52            return AudioFeaturePreferences.getDefault();
 53        }
 54        
 55        // 批次獲取音頻特徵
 56        List<String> trackIds = recentTracks.stream()
 57            .map(Track::getId)
 58            .collect(Collectors.toList());
 59            
 60        List<AudioFeatures> audioFeatures = spotifyApiService.getAudioFeatures(trackIds);
 61        
 62        // 計算各項特徵的平均值與標準差
 63        DoubleSummaryStatistics energyStats = audioFeatures.stream()
 64            .mapToDouble(AudioFeatures::getEnergy)
 65            .summaryStatistics();
 66            
 67        DoubleSummaryStatistics valenceStats = audioFeatures.stream()
 68            .mapToDouble(AudioFeatures::getValence)
 69            .summaryStatistics();
 70            
 71        DoubleSummaryStatistics danceabilityStats = audioFeatures.stream()
 72            .mapToDouble(AudioFeatures::getDanceability)
 73            .summaryStatistics();
 74        
 75        // 建構偏好物件
 76        return AudioFeaturePreferences.builder()
 77            .targetEnergy((float) energyStats.getAverage())
 78            .energyRange(calculateOptimalRange(energyStats))
 79            .targetValence((float) valenceStats.getAverage())
 80            .valenceRange(calculateOptimalRange(valenceStats))
 81            .targetDanceability((float) danceabilityStats.getAverage())
 82            .danceabilityRange(calculateOptimalRange(danceabilityStats))
 83            .build();
 84    }
 85    
 86    /**
 87     * 多樣性增強演算法
 88     * 確保推薦結果具有適當的多樣性,避免推薦過於相似的音樂
 89     */
 90    private List<Track> enhanceDiversity(List<Track> tracks, DiversityLevel level) {
 91        if (level == DiversityLevel.LOW || tracks.size() <= 10) {
 92            return tracks.subList(0, Math.min(20, tracks.size()));
 93        }
 94        
 95        List<Track> diversifiedTracks = new ArrayList<>();
 96        Set<String> selectedArtists = new HashSet<>();
 97        Set<String> selectedGenres = new HashSet<>();
 98        
 99        // 第一輪:選擇不同藝人的高品質推薦
100        for (Track track : tracks) {
101            if (diversifiedTracks.size() >= 15) break;
102            
103            String primaryArtist = track.getArtists().get(0).getId();
104            if (!selectedArtists.contains(primaryArtist)) {
105                diversifiedTracks.add(track);
106                selectedArtists.add(primaryArtist);
107                
108                // 記錄風格資訊(如果可用)
109                if (track.getGenres() != null) {
110                    selectedGenres.addAll(track.getGenres());
111                }
112            }
113        }
114        
115        // 第二輪:在剩餘空間中加入多樣性選項
116        if (level == DiversityLevel.HIGH && diversifiedTracks.size() < 20) {
117            for (Track track : tracks) {
118                if (diversifiedTracks.size() >= 20) break;
119                if (diversifiedTracks.contains(track)) continue;
120                
121                // 優先選擇不同風格的歌曲
122                boolean isDifferentGenre = track.getGenres() != null && 
123                    track.getGenres().stream().noneMatch(selectedGenres::contains);
124                    
125                if (isDifferentGenre || diversifiedTracks.size() < 18) {
126                    diversifiedTracks.add(track);
127                }
128            }
129        }
130        
131        return diversifiedTracks;
132    }
133}

🔌 Spotify API 整合服務

 1@Service
 2public class SpotifyApiService {
 3    
 4    private final SpotifyApi spotifyApi;
 5    private final TokenRefreshService tokenRefreshService;
 6    
 7    public SpotifyApiService(SpotifyConfiguration config) {
 8        this.spotifyApi = SpotifyApi.builder()
 9            .setClientId(config.getClientId())
10            .setClientSecret(config.getClientSecret())
11            .setRedirectUri(SpotifyHttpManager.makeUri(config.getRedirectUri()))
12            .build();
13    }
14    
15    /**
16     * 搜尋藝人資訊
17     * 提供模糊搜尋與自動完成功能
18     */
19    public List<Artist> searchArtists(String query, int limit) {
20        try {
21            ensureValidToken();
22            
23            SearchArtistsRequest request = spotifyApi.searchArtists(query)
24                .limit(limit)
25                .market(CountryCode.TW)
26                .build();
27                
28            Paging<Artist> artistPaging = request.execute();
29            return Arrays.asList(artistPaging.getItems());
30            
31        } catch (IOException | SpotifyWebApiException | ParseException e) {
32            log.error("藝人搜尋失敗: query={}", query, e);
33            throw new SpotifyServiceException("搜尋服務暫時無法使用", e);
34        }
35    }
36    
37    /**
38     * 獲取藝人熱門歌曲
39     * 用於推薦系統的種子選擇
40     */
41    public List<Track> getArtistTopTracks(String artistId, int limit) {
42        try {
43            ensureValidToken();
44            
45            GetArtistsTopTracksRequest request = spotifyApi.getArtistsTopTracks(artistId, CountryCode.TW)
46                .build();
47                
48            Track[] tracks = request.execute();
49            return Arrays.stream(tracks)
50                .limit(limit)
51                .collect(Collectors.toList());
52                
53        } catch (Exception e) {
54            log.error("獲取藝人熱門歌曲失敗: artistId={}", artistId, e);
55            throw new SpotifyServiceException("無法獲取藝人資訊", e);
56        }
57    }
58    
59    /**
60     * Token 自動刷新機制
61     * 確保 API 呼叫的持續有效性
62     */
63    private void ensureValidToken() {
64        try {
65            String currentToken = getCurrentAccessToken();
66            if (tokenRefreshService.isTokenExpired(currentToken)) {
67                String refreshedToken = tokenRefreshService.refreshAccessToken();
68                spotifyApi.setAccessToken(refreshedToken);
69                log.info("Spotify access token 已自動刷新");
70            }
71        } catch (Exception e) {
72            log.error("Token 刷新失敗", e);
73            throw new SpotifyServiceException("認證失效,請重新登入", e);
74        }
75    }
76}

💻 前端實作重點

🧩 Vue.js 主要元件架構

 1// SpotifyAuth.vue - 認證元件
 2<template>
 3  <div class="spotify-auth">
 4    <div v-if="!isAuthenticated" class="auth-container">
 5      <h2>連接您的 Spotify 帳戶</h2>
 6      <p>授權後即可開始探索個人化音樂推薦</p>
 7      <button @click="initiateAuth" class="auth-button" :disabled="loading">
 8        <i class="fab fa-spotify"></i>
 9        {{ loading ? '連接中...' : '連接 Spotify' }}
10      </button>
11    </div>
12    
13    <div v-else class="auth-success">
14      <h3> Spotify 帳戶已連接</h3>
15      <p>歡迎回來{{ userProfile.display_name }}</p>
16      <button @click="logout" class="logout-button">登出</button>
17    </div>
18  </div>
19</template>
20
21<script>
22import { mapState, mapActions } from 'vuex'
23
24export default {
25  name: 'SpotifyAuth',
26  data() {
27    return {
28      loading: false
29    }
30  },
31  computed: {
32    ...mapState('spotify', ['isAuthenticated', 'userProfile'])
33  },
34  methods: {
35    ...mapActions('spotify', ['authenticate', 'fetchUserProfile', 'clearAuth']),
36    
37    async initiateAuth() {
38      try {
39        this.loading = true
40        const response = await this.$http.get('/api/spotify/auth')
41        
42        // 開啟新視窗進行 OAuth 認證
43        const authWindow = window.open(
44          response.data.authUrl,
45          'spotify-auth',
46          'width=600,height=700,scrollbars=yes,resizable=yes'
47        )
48        
49        // 監聽認證完成訊息
50        this.listenForAuthComplete(authWindow)
51        
52      } catch (error) {
53        this.$toast.error('認證初始化失敗:' + error.message)
54      } finally {
55        this.loading = false
56      }
57    },
58    
59    listenForAuthComplete(authWindow) {
60      const checkClosed = setInterval(() => {
61        if (authWindow.closed) {
62          clearInterval(checkClosed)
63          this.checkAuthStatus()
64        }
65      }, 1000)
66      
67      // 監聽來自認證視窗的訊息
68      window.addEventListener('message', (event) => {
69        if (event.data.type === 'SPOTIFY_AUTH_SUCCESS') {
70          clearInterval(checkClosed)
71          authWindow.close()
72          this.handleAuthSuccess()
73        }
74      })
75    },
76    
77    async handleAuthSuccess() {
78      await this.authenticate()
79      await this.fetchUserProfile()
80      this.$toast.success('Spotify 認證成功!')
81      this.$router.push('/recommendations')
82    },
83    
84    logout() {
85      this.clearAuth()
86      this.$toast.info('已登出 Spotify')
87    }
88  }
89}
90</script>

🎛️ 音樂推薦介面元件

  1// MusicRecommendations.vue - 推薦系統主介面
  2<template>
  3  <div class="recommendations-container">
  4    <!-- 推薦參數控制面板 -->
  5    <div class="recommendation-controls">
  6      <h2>個人化音樂推薦</h2>
  7      
  8      <form @submit.prevent="generateRecommendations" class="controls-form">
  9        <!-- 種子藝人選擇 -->
 10        <div class="form-group">
 11          <label>喜愛的藝人 (最多 5 )</label>
 12          <ArtistSelector 
 13            v-model="seedArtists"
 14            :max-selections="5"
 15            @artists-changed="onArtistsChanged"
 16          />
 17        </div>
 18        
 19        <!-- 音樂特徵調整 -->
 20        <div class="form-group">
 21          <label>音樂風格偏好</label>
 22          <div class="feature-sliders">
 23            <FeatureSlider
 24              v-for="feature in audioFeatures"
 25              :key="feature.key"
 26              :label="feature.label"
 27              :value="feature.value"
 28              :description="feature.description"
 29              @input="updateFeature(feature.key, $event)"
 30            />
 31          </div>
 32        </div>
 33        
 34        <!-- 多樣性控制 -->
 35        <div class="form-group">
 36          <label>推薦多樣性</label>
 37          <select v-model="diversityLevel" class="diversity-select">
 38            <option value="LOW">相似風格為主</option>
 39            <option value="MEDIUM">平衡探索</option>
 40            <option value="HIGH">最大化多樣性</option>
 41          </select>
 42        </div>
 43        
 44        <button type="submit" class="generate-btn" :disabled="generating">
 45          {{ generating ? '生成中...' : '生成推薦清單' }}
 46        </button>
 47      </form>
 48    </div>
 49    
 50    <!-- 推薦結果展示 -->
 51    <div v-if="recommendations.length > 0" class="recommendations-results">
 52      <h3>為您推薦的音樂</h3>
 53      <div class="tracks-grid">
 54        <TrackCard
 55          v-for="track in recommendations"
 56          :key="track.id"
 57          :track="track"
 58          @play="playTrack"
 59          @add-to-playlist="showPlaylistModal"
 60          @like="toggleLike"
 61        />
 62      </div>
 63      
 64      <!-- 批次操作 -->
 65      <div class="batch-actions">
 66        <button @click="createPlaylist" class="create-playlist-btn">
 67          建立為新播放清單
 68        </button>
 69        <button @click="exportRecommendations" class="export-btn">
 70          匯出推薦結果
 71        </button>
 72      </div>
 73    </div>
 74    
 75    <!-- 載入中狀態 -->
 76    <div v-if="generating" class="loading-container">
 77      <div class="loading-spinner"></div>
 78      <p>正在分析您的音樂偏好請稍候...</p>
 79    </div>
 80  </div>
 81</template>
 82
 83<script>
 84import ArtistSelector from '@/components/ArtistSelector.vue'
 85import FeatureSlider from '@/components/FeatureSlider.vue'
 86import TrackCard from '@/components/TrackCard.vue'
 87
 88export default {
 89  name: 'MusicRecommendations',
 90  components: {
 91    ArtistSelector,
 92    FeatureSlider,
 93    TrackCard
 94  },
 95  data() {
 96    return {
 97      seedArtists: [],
 98      diversityLevel: 'MEDIUM',
 99      generating: false,
100      recommendations: [],
101      audioFeatures: [
102        {
103          key: 'energy',
104          label: '能量感',
105          value: 0.5,
106          description: '音樂的強度與活力程度'
107        },
108        {
109          key: 'valence',
110          label: '情感僾向',
111          value: 0.5,
112          description: '正面情感 vs 憂鬱情感'
113        },
114        {
115          key: 'danceability',
116          label: '舞蹈性',
117          value: 0.5,
118          description: '適合跳舞的程度'
119        },
120        {
121          key: 'acousticness',
122          label: '原聲比例',
123          value: 0.5,
124          description: '原聲樂器 vs 電子音樂'
125        }
126      ]
127    }
128  },
129  methods: {
130    async generateRecommendations() {
131      if (this.seedArtists.length === 0) {
132        this.$toast.warning('請至少選擇一位喜愛的藝人')
133        return
134      }
135      
136      try {
137        this.generating = true
138        
139        const requestData = {
140          seedArtists: this.seedArtists.map(artist => artist.id),
141          audioFeatures: this.getAudioFeaturesValues(),
142          diversityLevel: this.diversityLevel,
143          limit: 20
144        }
145        
146        const response = await this.$http.post('/api/recommendations', requestData)
147        this.recommendations = response.data.tracks
148        
149        // 記錄推薦成功事件
150        this.$analytics.track('recommendation_generated', {
151          seed_artists_count: this.seedArtists.length,
152          diversity_level: this.diversityLevel,
153          results_count: this.recommendations.length
154        })
155        
156      } catch (error) {
157        this.$toast.error('推薦生成失敗:' + error.message)
158      } finally {
159        this.generating = false
160      }
161    },
162    
163    getAudioFeaturesValues() {
164      return this.audioFeatures.reduce((features, feature) => {
165        features[feature.key] = feature.value
166        return features
167      }, {})
168    },
169    
170    updateFeature(key, value) {
171      const feature = this.audioFeatures.find(f => f.key === key)
172      if (feature) {
173        feature.value = value
174      }
175    },
176    
177    async createPlaylist() {
178      try {
179        const trackUris = this.recommendations.map(track => track.uri)
180        const playlistName = `個人推薦 - ${new Date().toLocaleDateString()}`
181        
182        await this.$http.post('/api/playlists', {
183          name: playlistName,
184          tracks: trackUris,
185          description: '由智能推薦系統生成的個人化播放清單'
186        })
187        
188        this.$toast.success('播放清單建立成功!')
189      } catch (error) {
190        this.$toast.error('播放清單建立失敗')
191      }
192    }
193  }
194}
195</script>

🚀 部署與配置

🐳 Docker 容器化部署

 1# docker-compose.yml
 2version: '3.8'
 3
 4services:
 5  # Spring Boot 後端服務
 6  backend:
 7    build:
 8      context: ./backend/SpotifyPlayList
 9      dockerfile: Dockerfile
10    container_name: spotify-backend
11    ports:
12      - "8888:8888"
13    environment:
14      - SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID}
15      - SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET}
16      - SPOTIFY_REDIRECT_URI=${SPOTIFY_REDIRECT_URI}
17      - SERVER_PORT=8888
18    volumes:
19      - ./logs:/app/logs
20    depends_on:
21      - redis
22    networks:
23      - spotify-network
24    restart: unless-stopped
25
26  # Vue.js 前端服務
27  frontend:
28    build:
29      context: ./frontend/spotify-playlist-ui
30      dockerfile: Dockerfile
31    container_name: spotify-frontend
32    ports:
33      - "3000:80"
34    environment:
35      - VUE_APP_API_BASE_URL=http://localhost:8888/api
36      - VUE_APP_SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID}
37    depends_on:
38      - backend
39    networks:
40      - spotify-network
41    restart: unless-stopped
42
43  # Redis 快取服務
44  redis:
45    image: redis:7-alpine
46    container_name: spotify-redis
47    ports:
48      - "6379:6379"
49    volumes:
50      - redis-data:/data
51    command: redis-server --appendonly yes
52    networks:
53      - spotify-network
54    restart: unless-stopped
55
56  # Nginx 反向代理
57  nginx:
58    image: nginx:alpine
59    container_name: spotify-nginx
60    ports:
61      - "80:80"
62      - "443:443"
63    volumes:
64      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
65      - ./nginx/ssl:/etc/nginx/ssl
66    depends_on:
67      - frontend
68      - backend
69    networks:
70      - spotify-network
71    restart: unless-stopped
72
73volumes:
74  redis-data:
75
76networks:
77  spotify-network:
78    driver: bridge

⚙️ 環境配置檔案

 1# application.properties (Spring Boot 配置)
 2
 3# 伺服器配置
 4server.port=8888
 5server.servlet.context-path=/api
 6
 7# Spotify API 配置
 8spotify.client-id=${SPOTIFY_CLIENT_ID:your-client-id}
 9spotify.client-secret=${SPOTIFY_CLIENT_SECRET:your-client-secret}
10spotify.redirect-uri=${SPOTIFY_REDIRECT_URI:http://localhost:3000/callback}
11
12# 快取配置
13spring.cache.type=redis
14spring.redis.host=${REDIS_HOST:localhost}
15spring.redis.port=${REDIS_PORT:6379}
16spring.redis.timeout=2000ms
17spring.redis.lettuce.pool.max-active=10
18
19# 日誌配置
20logging.level.com.yen.spotify=DEBUG
21logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
22logging.file.name=./logs/spotify-app.log
23
24# CORS 配置
25app.cors.allowed-origins=${CORS_ORIGINS:http://localhost:3000}
26app.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
27app.cors.allowed-headers=*
28
29# 推薦系統配置
30recommendation.default-limit=20
31recommendation.max-seed-artists=5
32recommendation.cache-duration=30m
33
34# 安全配置
35app.jwt.secret=${JWT_SECRET:your-jwt-secret}
36app.jwt.expiration=86400000

💎 系統特色與創新點

🧠 1. 智能化推薦策略

  • 多維度特徵分析: 不只依賴風格標籤,深入分析音頻特徵
  • 動態偏好學習: 根據使用者行為持續調整推薦模型
  • 多樣性平衡: 在相關性與探索性之間找到最佳平衡

🎨 2. 使用者體驗優化

  • 直觀的參數控制: 讓一般使用者也能輕鬆調整推薦參數
  • 即時預覽功能: 在生成完整推薦前提供快速預覽
  • 個人化介面: 根據使用者偏好自動調整介面主題

🏛️ 3. 技術架構優勢

  • 微服務設計: 前後端分離,便於獨立擴展
  • 容器化部署: 支援 Docker,簡化部署流程
  • 快取優化: 減少 Spotify API 呼叫,提升回應速度

🔮 未來發展規劃

📋 短期目標 (3-6 個月)

  • 整合 ChatGPT API: 提供自然語言音樂描述搜尋
  • UI/UX 重新設計: 更現代化的使用者介面
  • 行動端 App: React Native 跨平台應用
  • 社交功能: 朋友間的播放清單分享與推薦

🎯 中期目標 (6-12 個月)

  • 機器學習模型升級: 自建深度學習推薦系統
  • 多平台整合: 支援 Apple Music、YouTube Music
  • 情境感知推薦: 基於時間、天氣、活動的智能推薦
  • CI/CD 流水線: 自動化測試與部署

🌟 長期願景 (1-2 年)

  • 雲端原生架構: 遷移至 AWS/GCP
  • 大數據分析: 音樂趨勢分析與預測
  • 商業化功能: 藝人推廣與音樂行銷工具
  • 國際化支援: 多語系與全球音樂市場適應

🎉 總結與心得

這個 Spotify 播放清單推薦系統不僅展示了全端開發的完整流程,更重要的是體現了以使用者為中心的產品思維。透過深度整合 Spotify API 與自建的推薦演算法,我們成功打破了傳統音樂平台的推薦局限性。

🔧 技術收穫

  • OAuth 2.0 深度實作: 深入理解第三方 API 整合的安全性考量
  • 推薦系統設計: 學習了從資料收集到演算法實現的完整流程
  • 前後端協作: Vue.js 與 Spring Boot 的無縫整合經驗
  • 容器化實戰: Docker 在複雜應用架構中的實際運用

💎 產品價值

  • 使用者自主性: 讓使用者主動參與音樂發現過程
  • 個人化體驗: 基於深度學習的個人偏好建模
  • 探索樂趣: 在熟悉與新奇之間找到完美平衡

這個專案證明了技術創新與使用者需求的完美結合,未來將持續迭代優化,為音樂愛好者帶來更豐富的聽覺體驗。


🔗 相關連結

項目連結
📂 專案原始碼GitHub - SpringPlayground/springSpotifyPlayList
🌐 線上展示即將推出
📖 技術文件API 文件
Yen

Yen

Yen