🎯 專案動機與背景
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) |