🎵 Spring Boot + Vue.js 打造個人化 Spotify 播放清單推薦系統

🎯 專案動機與背景

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}

### 🎵 音樂推薦核心演算法

```java
@Service
public class MusicRecommendationService {
    
    @Autowired
    private SpotifyApiService spotifyApiService;
    
    /**
     * 基於使用者偏好產生音樂推薦
     * 結合多種推薦策略提供個人化建議
     */
    public List<Track> generateRecommendations(String userId, RecommendationRequest request) {
        try {
            // 1. 獲取使用者歷史播放記錄
            List<Track> recentTracks = spotifyApiService.getRecentlyPlayed(userId, 50);
            
            // 2. 分析音樂特徵偏好
            AudioFeaturePreferences preferences = analyzeUserPreferences(recentTracks);
            
            // 3. 種子歌曲/藝人選擇
            RecommendationSeeds seeds = buildRecommendationSeeds(request, preferences);
            
            // 4. 呼叫 Spotify 推薦 API
            List<Track> spotifyRecommendations = spotifyApiService.getRecommendations(
                seeds.getArtists(),
                seeds.getTracks(),
                seeds.getGenres(),
                preferences.toTuneableAttributes()
            );
            
            // 5. 應用自定義過濾與排序
            List<Track> filteredTracks = applyCustomFiltering(
                spotifyRecommendations, 
                preferences,
                request.getExcludedArtists()
            );
            
            // 6. 多樣性增強處理
            return enhanceDiversity(filteredTracks, request.getDiversityLevel());
            
        } catch (Exception e) {
            log.error("音樂推薦生成失敗", e);
            throw new RecommendationException("推薦系統暫時無法使用", e);
        }
    }
    
    /**
     * 分析使用者音樂偏好模式
     * 從歷史播放記錄中提取音頻特徵趋势
     */
    private AudioFeaturePreferences analyzeUserPreferences(List<Track> recentTracks) {
        if (recentTracks.isEmpty()) {
            return AudioFeaturePreferences.getDefault();
        }
        
        // 批次獲取音頻特徵
        List<String> trackIds = recentTracks.stream()
            .map(Track::getId)
            .collect(Collectors.toList());
            
        List<AudioFeatures> audioFeatures = spotifyApiService.getAudioFeatures(trackIds);
        
        // 計算各項特徵的平均值與標準差
        DoubleSummaryStatistics energyStats = audioFeatures.stream()
            .mapToDouble(AudioFeatures::getEnergy)
            .summaryStatistics();
            
        DoubleSummaryStatistics valenceStats = audioFeatures.stream()
            .mapToDouble(AudioFeatures::getValence)
            .summaryStatistics();
            
        DoubleSummaryStatistics danceabilityStats = audioFeatures.stream()
            .mapToDouble(AudioFeatures::getDanceability)
            .summaryStatistics();
        
        // 建構偏好物件
        return AudioFeaturePreferences.builder()
            .targetEnergy((float) energyStats.getAverage())
            .energyRange(calculateOptimalRange(energyStats))
            .targetValence((float) valenceStats.getAverage())
            .valenceRange(calculateOptimalRange(valenceStats))
            .targetDanceability((float) danceabilityStats.getAverage())
            .danceabilityRange(calculateOptimalRange(danceabilityStats))
            .build();
    }
    
    /**
     * 多樣性增強演算法
     * 確保推薦結果具有適當的多樣性,避免推薦過於相似的音樂
     */
    private List<Track> enhanceDiversity(List<Track> tracks, DiversityLevel level) {
        if (level == DiversityLevel.LOW || tracks.size() <= 10) {
            return tracks.subList(0, Math.min(20, tracks.size()));
        }
        
        List<Track> diversifiedTracks = new ArrayList<>();
        Set<String> selectedArtists = new HashSet<>();
        Set<String> selectedGenres = new HashSet<>();
        
        // 第一輪:選擇不同藝人的高品質推薦
        for (Track track : tracks) {
            if (diversifiedTracks.size() >= 15) break;
            
            String primaryArtist = track.getArtists().get(0).getId();
            if (!selectedArtists.contains(primaryArtist)) {
                diversifiedTracks.add(track);
                selectedArtists.add(primaryArtist);
                
                // 記錄風格資訊(如果可用)
                if (track.getGenres() != null) {
                    selectedGenres.addAll(track.getGenres());
                }
            }
        }
        
        // 第二輪:在剩餘空間中加入多樣性選項
        if (level == DiversityLevel.HIGH && diversifiedTracks.size() < 20) {
            for (Track track : tracks) {
                if (diversifiedTracks.size() >= 20) break;
                if (diversifiedTracks.contains(track)) continue;
                
                // 優先選擇不同風格的歌曲
                boolean isDifferentGenre = track.getGenres() != null && 
                    track.getGenres().stream().noneMatch(selectedGenres::contains);
                    
                if (isDifferentGenre || diversifiedTracks.size() < 18) {
                    diversifiedTracks.add(track);
                }
            }
        }
        
        return diversifiedTracks;
    }
}

### 🔌 Spotify API 整合服務

```java
@Service
public class SpotifyApiService {
    
    private final SpotifyApi spotifyApi;
    private final TokenRefreshService tokenRefreshService;
    
    public SpotifyApiService(SpotifyConfiguration config) {
        this.spotifyApi = SpotifyApi.builder()
            .setClientId(config.getClientId())
            .setClientSecret(config.getClientSecret())
            .setRedirectUri(SpotifyHttpManager.makeUri(config.getRedirectUri()))
            .build();
    }
    
    /**
     * 搜尋藝人資訊
     * 提供模糊搜尋與自動完成功能
     */
    public List<Artist> searchArtists(String query, int limit) {
        try {
            ensureValidToken();
            
            SearchArtistsRequest request = spotifyApi.searchArtists(query)
                .limit(limit)
                .market(CountryCode.TW)
                .build();
                
            Paging<Artist> artistPaging = request.execute();
            return Arrays.asList(artistPaging.getItems());
            
        } catch (IOException | SpotifyWebApiException | ParseException e) {
            log.error("藝人搜尋失敗: query={}", query, e);
            throw new SpotifyServiceException("搜尋服務暫時無法使用", e);
        }
    }
    
    /**
     * 獲取藝人熱門歌曲
     * 用於推薦系統的種子選擇
     */
    public List<Track> getArtistTopTracks(String artistId, int limit) {
        try {
            ensureValidToken();
            
            GetArtistsTopTracksRequest request = spotifyApi.getArtistsTopTracks(artistId, CountryCode.TW)
                .build();
                
            Track[] tracks = request.execute();
            return Arrays.stream(tracks)
                .limit(limit)
                .collect(Collectors.toList());
                
        } catch (Exception e) {
            log.error("獲取藝人熱門歌曲失敗: artistId={}", artistId, e);
            throw new SpotifyServiceException("無法獲取藝人資訊", e);
        }
    }
    
    /**
     * Token 自動刷新機制
     * 確保 API 呼叫的持續有效性
     */
    private void ensureValidToken() {
        try {
            String currentToken = getCurrentAccessToken();
            if (tokenRefreshService.isTokenExpired(currentToken)) {
                String refreshedToken = tokenRefreshService.refreshAccessToken();
                spotifyApi.setAccessToken(refreshedToken);
                log.info("Spotify access token 已自動刷新");
            }
        } catch (Exception e) {
            log.error("Token 刷新失敗", e);
            throw new SpotifyServiceException("認證失效,請重新登入", e);
        }
    }
}

## 💻 前端實作重點

### 🧩 Vue.js 主要元件架構

```vue
// SpotifyAuth.vue - 認證元件
<template>
  <div class="spotify-auth">
    <div v-if="!isAuthenticated" class="auth-container">
      <h2>連接您的 Spotify 帳戶</h2>
      <p>授權後即可開始探索個人化音樂推薦</p>
      <button @click="initiateAuth" class="auth-button" :disabled="loading">
        <i class="fab fa-spotify"></i>
        {{ loading ? '連接中...' : '連接 Spotify' }}
      </button>
    </div>
    
    <div v-else class="auth-success">
      <h3>✓ Spotify 帳戶已連接</h3>
      <p>歡迎回來,{{ userProfile.display_name }}!</p>
      <button @click="logout" class="logout-button">登出</button>
    </div>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  name: 'SpotifyAuth',
  data() {
    return {
      loading: false
    }
  },
  computed: {
    ...mapState('spotify', ['isAuthenticated', 'userProfile'])
  },
  methods: {
    ...mapActions('spotify', ['authenticate', 'fetchUserProfile', 'clearAuth']),
    
    async initiateAuth() {
      try {
        this.loading = true
        const response = await this.$http.get('/api/spotify/auth')
        
        // 開啟新視窗進行 OAuth 認證
        const authWindow = window.open(
          response.data.authUrl,
          'spotify-auth',
          'width=600,height=700,scrollbars=yes,resizable=yes'
        )
        
        // 監聽認證完成訊息
        this.listenForAuthComplete(authWindow)
        
      } catch (error) {
        this.$toast.error('認證初始化失敗:' + error.message)
      } finally {
        this.loading = false
      }
    },
    
    listenForAuthComplete(authWindow) {
      const checkClosed = setInterval(() => {
        if (authWindow.closed) {
          clearInterval(checkClosed)
          this.checkAuthStatus()
        }
      }, 1000)
      
      // 監聽來自認證視窗的訊息
      window.addEventListener('message', (event) => {
        if (event.data.type === 'SPOTIFY_AUTH_SUCCESS') {
          clearInterval(checkClosed)
          authWindow.close()
          this.handleAuthSuccess()
        }
      })
    },
    
    async handleAuthSuccess() {
      await this.authenticate()
      await this.fetchUserProfile()
      this.$toast.success('Spotify 認證成功!')
      this.$router.push('/recommendations')
    },
    
    logout() {
      this.clearAuth()
      this.$toast.info('已登出 Spotify')
    }
  }
}
</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](https://github.com/yennanliu/SpringPlayground/tree/main/springSpotifyPlayList) |
| 🌐 **線上展示** | 即將推出 |
| 📖 **技術文件** | [API 文件](http://localhost:8888/swagger-ui.html) |
Yen

Yen

Yen