
今年干的一件大事是给自己搭了一个能打的音乐网站名字就叫“酷听音乐”。起因很简单各大音乐平台的版权互相不打通我想听的老歌散落好几家歌单没法合并连播放器的交互体验都不一样。与其继续忍受割裂不如自己动手写一个顺手的播放器。技术栈我选了 Python Vue开发工具用 PyCharm后端把 Django 和 Flask 都安排上了。Django 扛主站的用户、歌曲、歌单、评论这些核心业务Flask 单独拎出来跑推荐服务。这个组合不是说某个框架不行而是各干各最擅长的事。如果你正在学 Python Web 或者 Vue但还没碰过完整的前后端分离项目这篇内容应该能让你少走不少弯路从环境搭建到部署上线我把能踩的坑基本都踩了一遍。1. 项目全貌需求分析到技术选型1.1 需求挖掘音乐网站的标配功能是什么动手之前我先把需求列清楚避免写着写着变成“什么都想加”的半成品。对一个个人项目来说第一版不需要做得多花哨但核心闭环必须在。我圈定的 MVP 范围是这样的用户端注册登录、关键词搜索、歌曲播放、收藏、创建歌单、评论。管理端歌曲上传、歌手管理、歌单审核用来方便我自己维护曲库。个性化基于用户播放行为做简单的相似歌曲推荐。这就像一个社区的公共配套先把“路和水电”通好再考虑装修。如果一上来就奔着歌词滚动、桌面歌词、多端同步这种高级功能去大概率项目会烂尾。我的经验是先做减法把播放器“能搜到歌、能播、能存歌单”这件事跑顺其他都是锦上添花。1.2 为什么是 Django Flask 双后端而不是一个框架硬扛很多朋友问我一个音乐站Django 和 Flask 二选一不就行了吗为什么两个都用这里我把道理掰开讲。Django 是一个“全家桶”式的框架自带 ORM、Admin 后台、表单校验、认证系统生态里还有现成的 Django REST FrameworkDRF。做音乐站这种业务逻辑密集的系统用户、歌单、评论这些模型天然适合用 Django 的 ORM 管理数据库迁移、关联查询、后台管理都能省下大量时间。尤其是我这种一个人维护全部代码的情况Django Admin 可以直接当管理后台用歌单审核、歌曲上下架全都现成。Flask 则是“轻量自由”路线的代表。它不强制你用什么组件你想接什么库就接什么。我拿它单独跑推荐服务一个独立的进程输入歌曲 ID输出相似歌曲列表接口少、依赖干净、部署简单。这种边缘业务塞进 Django 里不是不行但会让主站的代码越来越重而且推荐算法经常要迭代独立部署意味着改了可以直接重启一个服务完全不影响主站。两个框架之间用 HTTP 接口通信Django 主站收到前端请求后如果需要推荐数据就去调 Flask 的接口。服务拆分不一定要上微服务那套重武器像这样按业务边界拆一个小服务个人项目完全够用了。1.3 整体架构和数据流转整个系统跑起来之后数据是这样流转的浏览器请求静态页面由 Nginx 直接托管 Vue 构建出来的文件。前端调 API比如 /api/songs、/api/auth/login这些请求通过 Nginx 反向代理到 Django默认跑在 8000 端口。Django 处理业务逻辑读写 MySQL 数据库Redis 用来缓存热门歌曲的播放信息和推荐结果。需要推荐时Django 把请求转发给 Flask跑在 5000 端口的推荐服务。音频文件和封面图存在对象存储里通过 CDN 分发前端拿到的播放地址直接指向 CDN。开发环境下更简单前端跑在 Vite 的 5173 端口我配置了代理把所有 /api 开头的请求转发到 Django/recommend 转发到 Flask前端代码里只需要写相对路径避免跨域问题。2. 环境准备PyCharm、Python 与 Vue 的初始化细节2.1 PyCharm 里配置 Python 虚拟环境别在全局环境里裸奔这一步看着简单但坑不少。我见过太多新手直接在全局 Python 环境里 pip install装到后面依赖冲突项目一多就乱套。我用的方案是 PyCharm 新建项目时选择 Virtualenv 或 uv 虚拟环境。以 PyCharm 为例新建项目后设置里找到 Python 解释器选择“添加解释器 新建虚拟环境”指定 Python 3.10 或 3.11 的路径PyCharm 会自动创建一个 .venv 目录。后面所有依赖都装到这个虚拟环境里跟系统 Python 互不干扰。装依赖的时候有一个强烈建议在国内网络环境下先把 pip 源换成国内镜像否则装 Django、DRF 这些包会慢到怀疑人生。在 PyCharm 终端执行pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple然后安装核心依赖pip install django djangorestframework djangorestframework-simplejwt flask flask-cors mysqlclient redis numpy gunicorn这里简单说下每个包的用途django 和 djangorestframework 是主站服务的基础simplejwt 用来做接口的 Token 认证flask 和 flask-cors 是推荐服务及其跨域配置mysqlclient 用于连接 MySQL如果编译报错可以换成 pymysql然后在 Django 配置里写上import pymysql; pymysql.install_as_MySQLdb()numpy 在推荐服务里算相似度用得上。PyCharm 版本方面社区版完全够用。很多人纠结要不要折腾专业版我的体会是做这个项目用到的功能Python 解释器、虚拟环境、Git、终端社区版全都支持。专业版多了数据库面板和自带 HTTP Client但那些功能我用 DataGrip 或直接浏览器也能替代。没必要在这个环节卡住。2.2 Django 工程与 App 划分我用 django-admin 命令创建工程命名为 coolmusic然后按业务边界拆成几个 Appdjango-admin startproject coolmusic cd coolmusic python manage.py startapp users python manage.py startapp songs python manage.py startapp playlists python manage.py startapp interactions这样拆的好处是每个 App 只管一类事users用户注册、登录、个人资料。songs歌曲、歌手、专辑的管理和查询。playlists歌单和歌单歌曲关联。interactions收藏、评论、播放记录。然后修改 coolmusic/settings.py把四个 App 和 DRF、simplejwt 加进 INSTALLED_APPS配置 MySQL 连接再设置 DRF 的默认认证方式为 JWT。最后执行python manage.py makemigrations python manage.py migratePyCharm 里可以直接右键 manage.py 运行也可以直接在终端跑看个人习惯。2.3 Vue 3 工程搭建与依赖安装前端我选的 Vue 3 Vite而不是 Vue 2 Webpack。Vite 冷启动速度快开发体验好太多。在项目根目录下执行npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios element-plus这里必须提醒一个高频踩坑点npm install 在国内网络环境下经常卡住或者下载到一半报错。解决方案是给 npm 配置镜像源npm config set registry https://registry.npmmirror.com装完之后把目录结构规划一下src/router路由配置src/storePinia 状态管理主要管播放状态src/api封装 axios 请求src/views页面组件src/components通用组件比如播放条、歌曲列表src/assets样式和静态资源再配置 vite.config.js加一个开发代理export default { server: { proxy: { /api: http://localhost:8000, /recommend: http://localhost:5000 } } }这样前端代码里请求 /api/songs 时开发环境下会自动转发到 Django不会有跨域问题。生产环境上 Nginx 也是同样思路。3. 数据库设计与后端 API 实现3.1 核心表结构与关键字段数据库是整站的基石表结构设计合理后面少改很多代码。我的模型设计是这样的users 用户表id、username、password_hash、email、avatar、created_at。密码必须存哈希值Django 默认用的 PBKDF2 算法已经够安全千万别存明文。artists 歌手表id、name、avatar、description。一个歌手可以有多首歌和歌曲表是一对多关系。songs 歌曲表id、title、artist外键、album、duration、audio_url、cover_url、lyric、play_count。播放地址我存的是对象存储的相对路径不是完整 URL这样切换域名或迁移存储时不用动数据库。duration 存秒数前端拿去格式化分钟。playlists 歌单表id、creator外键指向 users、name、cover、description。歌单和歌曲是多对多关系需要一个中间表 playlist_songs字段包括 id、playlist、song、sort_order、created_at。comments 评论表id、user、song、content、created_at。favorites 收藏表id、user、song、created_at这个表加了联合唯一约束防止重复收藏。这套设计没有设计得特别复杂但足够支撑一个真实可用的音乐站。还有一点值得注意播放量这种频繁更新的字段我建议单独放一张 counter 表或者用 Redis 暂时累积再定时写回 MySQL避免每次播放都更新一行导致数据库压力大。3.2 Django ORM 的查询与删除实战Django 的 ORM 写起来很爽但有一些细节新手容易踩。举几个真实场景场景一按歌手查歌曲。歌单详情页需要展示歌曲和歌手名如果直接查 Song.objects.all()然后在页面里对每首歌去查一次 artist.name就是典型的 N1 问题。一首歌查一次数据库100 首歌就是 101 次查询页面会明显变慢。正确用法是 select_relatedsongs Song.objects.select_related(artist).filter(playlist_id1)这样一条 SQL 通过 JOIN 把歌曲和歌手一起查出来性能差距非常明显。场景二删除歌手时的级联策略。Django 默认的 on_delete 参数决定了关联数据的命运。如果歌手下架了他的歌怎么处理我选的是on_deletemodels.CASCADE删除歌手时直接把他的歌曲一并删掉数据不会变孤儿。但这要非常小心因为 Cascade 是数据库层面的连带删除误操作是找不回来的。如果你想保留歌曲只删歌手就得用 SET_NULL 或者 PROTECT。实际工作中我倾向于 PROTECT即如果有歌曲关联歌手不允许被删除。这样业务上更安全真要删的时候先处理歌曲再说。删除操作本身很简单artist Artist.objects.get(id3) artist.delete()如果因为 PROTECT 抛了 ProtectedError就先把该歌手下面的歌曲转移或下架。场景三播放量自增。不要先读出 play_count再加一后写回去。多线程环境下会丢数据。正确姿势是用 F 表达式from django.db.models import F Song.objects.filter(idsong_id).update(play_countF(play_count) 1)F 表达式把加减操作放在数据库端执行不会出现并发覆盖。3.3 Flask 推荐服务从最简单的规则做起推荐算法很容易被包装得很玄但对我这个量级的项目来说先用最简单有效的方法就对了。我在 Flask 服务里做的事读取所有歌曲的播放记录矩阵用 numpy 计算歌曲之间的余弦相似度然后为每首歌维护一个“相似歌曲 Top10”列表。具体流程是从 MySQL 查出一首歌被哪些用户播放过构造一个 user-song 矩阵。对矩阵做归一化用 numpy 计算余弦相似度。把相似度列表缓存在 Rediskey 类似 rec:similar:song:12324 小时过期。提供接口 /recommend/ int:song_id 返回相似歌曲列表。为什么用余弦相似度而不是更复杂的协同过滤因为我的用户量小数据稀疏复杂模型容易过拟合到冷启动问题。余弦相似度简单、可解释性强而且 numpy 计算快个人站完全跑得动。Flask 接口长这样from flask import Flask, jsonify import numpy as np app Flask(__name__) app.route(/recommend/int:song_id) def recommend(song_id): # 从缓存读取相似度结果没有则重新计算 similar_ids get_similar_songs(song_id) return jsonify({song_id: song_id, similar_songs: similar_ids})开发时 Flask 默认跑在 5000 端口注意 Django 里如果要用 urllib 或 requests 调它网络地址要写成http://localhost:5000。跨域问题用 flask-cors 直接全局开掉from flask_cors import CORS CORS(app)推荐服务部署就更容易了用 gunicorn 起一个进程配一个 systemd 服务开机自启。因为不涉及用户上传不需要复杂文件存储整个服务非常轻。4. Vue 前端页面与播放器实战4.1 路由规划与页面骨架动态路由别硬编码前端路由我用 vue-router 4整体规划如下/首页推荐歌单和热门歌曲/search搜索结果页/playlist/:id歌单详情页/artist/:id歌手页/player正在播放的完整页面/login登录注册/admin管理后台入口这里用到的动态路由其实就是带参数的路由。比如路由定义/playlist/:id在页面里通过route.params.id拿到歌单 ID然后调接口。这种模式对音乐站特别合适因为歌单、歌手页都是同一套模板只是数据不同。只要后端接口设计好前端一行代码不用改。导航守卫必须加只有登录用户才能访问个人中心和创建歌单。我在 router/index.js 里写了一个简单的 beforeEach检查 localStorage 有没有 tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })4.2 播放器核心组件兼容 MP3 与 HLS 播放播放器是整个网站的灵魂组件。我的方案是做一个全局的 PlayerBar固定在页面底部不管用户在哪个页面播放条都在。播放状态放在 Pinia store 里跨页面共享。核心逻辑用原生的 audio 元素加少量封装const audio new Audio() audio.src currentSong.audio_url audio.play()然后监听 timeupdate、ended 事件分别用来更新进度条和自动切到下一首。播放队列我用一个数组管理支持列表循环和单曲循环切换逻辑几行代码就搞定。有一个点要提一下如果你有的音频是 m3u8 分片格式也就是 HLS 流浏览器是不能直接用原生 audio 播放的。这种情况下有两个方案用 hls.js 库在前端把 m3u8 转成普通流再喂给 audio用户不用装任何插件。在后端用 ffmpeg 把 m3u8 转成 MP3 或 AAC虽然中转一次但前端零依赖。我实测下来hls.js 的方案更省事播放到哪算哪不用等全部转完。安装很简单npm install hls.js然后在播放器里做一个兼容分支src 以 .m3u8 结尾时走 hls.js否则直接播放。音量控制、倍速播放、进度条拖拽这些都属于锦上添花。我给进度条做了一个简单的拖动逻辑按下时记录鼠标位置松开时把 audio.currentTime 设成对应秒数。这个小交互看起来不起眼但直接决定播放器好不好用。4.3 歌单、搜索与个人中心页面歌单详情页是整个站比较复杂的页面包含歌单信息、歌曲列表、评论区和收藏按钮。歌曲列表我用了一个通用表格组件这里正好用到 Vue 的插槽slot功能。表格负责渲染固定字段但操作列是外部的通过插槽传入按钮这样组件不用写死逻辑复用到首页、搜索页都行。插槽是 Vue 里非常实用但不复杂的功能只需要在子组件里写slot nameactions :songsong/slot在父组件使用时template #actions{ song } button clickaddToPlaylist(song)收藏/button /template这样的好处是表格组件完全通用歌单、搜索结果、排行榜都能用只是传进去的数据和操作按钮不同。把通用性和定制性隔离开开发效率提升非常明显。搜索页我做了输入防抖用户停止输入 300 毫秒后才发请求避免每敲一个字就请求一次后端给数据库省点压力。个人中心展示用户收藏的歌单、收藏的歌曲和最近播放记录都是列表页暴力堆接口就行没有太多技术含量主要处理好每个列表的 loading 状态和空数据展示。4.4 后端接口对接与状态管理前端和后端的对接我统一封装在 src/api 目录里。每个模块一个文件比如 songs.js、playlist.js、auth.js里面全部用 axios 发请求统一处理错误码和 loading 状态。状态管理用的是 Pinia。播放器 store 里放了五个字段currentSong、playQueue、playIndex、isPlaying、volume。这个 store 是全局的所以切换页面不会影响播放状态。页面底部 PlayerBar 就绑定这个 store页面里的“点击播放”按钮只是往 store 里塞数据然后调 play()。还有一点体验细节音频文件如果放在对象存储上并且配了 CDN跨域访问要小心。某些 CDN 会限制 Referer导致前端拿不到音频流表现就是audio.play()之后立刻跳到 ended 事件。这个坑我排查了很久最后在 CDN 控制台把跨域头 Access-Control-Allow-Origin 配成*才彻底解决。5. 部署上线与性能优化5.1 Django 与 Flask 各自的生产部署别用 runserver 裸奔开发时 Django 自带的 runserver 很方便但生产环境并发能力很弱必须换 gunicorn。启动命令很简单gunicorn coolmusic.wsgi:application -w 4 -b 127.0.0.1:8000-w 4 表示起 4 个 worker 进程个人站这个配置足够。Django 的静态文件比如 Admin 后台的 CSS需要 whitenoise 来处理不然 Nginx 静态文件目录还要单独映射麻烦。安装并配置好 whitenoise 之后静态资源访问就不依赖单独的目录了。Flask 推荐服务同理gunicorn recommand_server:app -b 127.0.0.1:5000两个服务都建议用 systemd 做成开机自启。网上有现成的 service 文件模板把 ExecStart 改一改就行。但我要多说一句个人项目也要把启动日志和错误日志分开存出事能快速定位。我就在 systemd 配置里把 StandardOutput 和 StandardError 分别指向两个 log 文件搜索 bug 时省了太多时间。5.2 前端构建与 Nginx 配置刷新不 404 是底线前端构建很简单npm run build产物在 dist 目录把这个目录整个同步到服务器上Nginx 托管静态文件就行。但这里有一个必须处理的细节Vue Router 默认是 history 模式用户在 /playlist/123 按 F5 刷新Nginx 会去查这个路径对应的文件结果 404。解法是 Nginx 的 try_files让所有路径都回退到 index.html。我最终的 Nginx 配置关键块长这样location / { root /var/www/coolmusic; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; } location /recommend/ { proxy_pass http://127.0.0.1:5000; }这样 /api 走 Django、/recommend 走 Flask、其余路径全部交给 Vue 应用处理刷新也不会白屏。5.3 流媒体优化与缓存策略音乐站的流量大头是音频文件。最开始的版本我把音频存在服务器本地磁盘上结果带宽和数据盘 IO 顶不住。后来改成对象存储加 CDN体验提升是质变级别的。音频文件上传到对象存储后会得到一个类似https://cdn.example.com/music/xxx.mp3的地址。这个地址直接配在歌里CDN 节点会自动缓存到边缘用户播放时下载速度飞快源站压力也很小。缓存层用的 Redis在几个地方派上用场热门歌曲的播放地址Redis 缓存 1 小时避免每次播放都穿透到 DB。推荐结果的相似歌曲列表24 小时过期。用户登录的 JWT Token做基于 Redis 的黑名单注销时把 Token 拉黑。还有一个细节图片和封面不要一次性加载太多。歌单页可能有几十张封面图如果全部同步加载首屏会卡。我给图片组件加了一个懒加载指令滚动到可视区域才开始加载。配合 CDN页面速度刷刷的。6. 常见问题排查与经验总结6.1 开发期踩坑记录给你当避坑手册CORS 跨域报错开发时前端 5173 端口访问 Django 8000 端口如果没走 Vite 代理而是直接写绝对路径就会触发跨域。报错信息通常是 “Access to XMLHttpRequest at ... has been blocked by CORS policy”。解法有两条路要么配置 Vite 代理推荐要么在 Django 里装 django-cors-headers 开白名单。生产环境走了 Nginx 同源之后跨域问题自动消失。刷新页面 404Vue Router history 模式 Nginx 未配 try_files这个就是上面提到的坑。症状是你从首页点进歌单正常但直接按 F5 刷新就 404。加一行try_files $uri $uri/ /index.html解决。JWT 过期后接口报 401用户登录后拿到的 Token 默认有效期很短如果前端不做静默刷新用户用着用着突然全部接口 401。我在 axios 拦截器里加了一个逻辑收到 401 时尝试用 refresh token 换新 token换成功就把失败请求重新发一遍换失败才跳登录页。这个逻辑可以让用户长时间停在页面上也不掉线。音频播放失败CDN 没有配置正确的跨域头和 Referer 白名单播放器一打开就异常。先看 DevTools 的网络面板确认音频请求是 200 还是 403403 基本都是 CDN 防盗链问题。PyCharm 无法识别虚拟环境里的依赖换一台电脑拉代码后PyCharm 的 Python 解释器忘了选虚拟环境导致 import 全飘红。很简单在解释器设置里重新指向 .venv 里的 python 可执行文件就行。MySQL 驱动安装失败mysqlclient 在 Windows 上编译特别容易报错。直接换成 pymysql然后改 Django 配置一分钟解决没必要死磕。npm install 卡住或报错除了换镜像源还可以试试先删掉 node_modules 目录再重新安装经常能解决依赖树不一致的问题。搜索接口慢如果你在歌曲表上用了 LIKE 前缀匹配比如WHERE title LIKE %关键词%数据量上来之后会越来越慢。基础做法是给 title 字段加索引但更推荐用全文索引或者单独引入搜索引擎个人项目可以先用索引过渡。6.2 我的个人经验做这类全栈项目的节奏比技术更重要这个项目踩过这么多坑之后我最大的体会是节奏比技术重要。一开始急着把功能堆全结果前端页面还没写几个后端接口已经因为设计不合理改了三轮。后来我换成先定义接口文档再并行走前后端项目的完成速度反而快了很多。具体做法是先用 Markdown 把核心接口列出来比如歌曲详情接口返回什么字段、歌单接口返回什么字段前端和后端都按这个文档干。这样两个人协作不扯皮一个人做也没问题。我写接口文档时会明确标注每个字段的类型、是否可空、关联关系前端照着类型定义 TypeScript 接口后端照着写 serializer两边天然对齐。另外音乐网站这种项目特别适合拿来练手因为它的功能足够丰富但又不像电商那样对并发、支付有变态要求。你可以完全掌控技术栈从数据库表设计到前端组件封装每个环节都能学到东西。做完这个站我对 Django ORM、Vue 状态管理、Nginx 部署的掌握程度比看十遍教程都深。6.3 后续还可以这样扩展项目基础跑通之后我下一步想加的是歌词滚动同步。原理是前端监听 audio 的 timeupdate 事件根据当前播放时间在 LRC 歌词数组里二分查找当前行然后让歌词块滚动到中间位置并高亮。逻辑不复杂但体验提升很明显。还有一块是现在挺火的 AI 歌单生成把你收藏的歌和听歌记录发给大模型让它根据情绪或场景生成歌单。这个扩展我计划做成一个独立的服务接入大模型 API然后把生成的歌单写入数据库关联到用户账号下。Django 主站这边只需要加一个“AI 生成歌单”的按钮和对应接口前端弹窗展示生成结果用户确认后保存。技术上没有难点关键是 API 成本和响应时间的控制建议放在 Flask 那边做异步任务避免阻塞主站。最后再提醒一句这种个人项目别追求一步到位。先把基础版的酷听音乐跑起来让朋友用上再根据真实反馈迭代。有人用你才会知道哪里卡、哪里难用那时候你的优化才有方向。我自己就是在朋友说“怎么搜索慢得像拨号上网”之后才认真优化了搜索接口的索引和缓存配置。好产品不是设计出来的是改出来的。