简介:面向高校计算机专业学生与Django入门开发者,这份毕业论文完整呈现了基于Python与Django构建社交音乐分享平台的设计与实现过程。全包1个doc文件约16.19MB,为完整学位论文,涵盖摘要、目录及系统开发技术等章节。已有73人学习下载,适合选题参考、方案借鉴或毕业设计素材需求。文档围绕用户注册登录、音乐上传、个人资料、社交关系、评论点赞等功能展开,并分析了Django的MTV架构、内置认证系统与ORM数据库交互机制。性能层面还涉及数据库优化、缓存机制、负载均衡及功能测试与可扩展性验证,能为社交类Web应用的架构设计与工程实践提供具体参考。
1. 把一份社交音乐分享平台当真实项目做,卡点不在建表而在音频
拿「django 基于 Python 的社交音乐分享平台的设计与实现」这个题目动手的人,十个里有八个会在同一个地方停住:模型类写完了、admin 后台也能点,但一上传 MP3 就发现首页加载变慢,进度条拖不动,点一下播放按钮服务器内存往上飙。真正决定这个平台好不好用的,从来不是 User、Track 这些表叫什么名字,而是音频文件存哪儿、怎么响应 Range 请求、动态流在几百条数据下用几条 SQL 拉回来。这篇内容按一个能上线的音乐社区来做:上传、播放、关注、动态、部署五段。适合已经写过 Hello World 想往实战走的新手,也适合带学生做毕设、想看看生产端参数怎么设的人。
2. Django 的 MTV 分层怎么承载社交音乐分享平台的数据模型
标题里的「设计」两个字,落到 Django 上就是 MTV 三层怎么切分。不少毕设项目把所有逻辑塞进一个 app,结果 models.py 两千行、views.py 三千行,改一个字段要提心吊胆。社交音乐分享平台天然有边界:账号体系、音乐资源、动态信息流,正好对应三个 app。
2.1 用 django-admin 和 startapp 切出三个应用
先把工程骨架搭出来,注意 startapp 生成的是应用不是项目,Django 教程里最容易混的就是这两个命令:
# 建虚拟环境,避免和系统 Python 冲突 python -m venv venv source venv/bin/activate # Windows 下换成 venv\Scripts\activate # 装 Django 和 Pillow,Pillow 负责处理封面图 pip install "django>=5.0,<5.1" pillow # 建项目,再建三个业务 app django-admin startproject musichub cd musichub python manage.py startapp accounts # 用户与音乐人主页 python manage.py startapp music # 专辑、歌曲、上传 python manage.py startapp feed # 关注、动态、播放记录startproject产出的是settings.py、urls.py、wsgi.py这套项目级配置,startapp产出的是一份带apps.py、models.py、views.py、migrations/的应用模板。三个 app 建好后,第一件事是把它们写进INSTALLED_APPS,否则makemigrations完全看不到你的模型。
2.2 MTV 模式里 M、T、V 各干什么活
Django 的 MTV 和常见 MVC 名字不同,职责划分也不一样,做音乐分享场景时对应关系是这样:
| 层级 | 在本平台中的载体 | 典型文件 | 容易混淆的点 |
|---|---|---|---|
| Model | Track、Album、Follow、Share | music/models.py | 只管数据结构和约束,不要写业务判断 |
| Template | 播放页、动态流、个人主页 | templates/*.html | 只做渲染,别塞查询逻辑 |
| View | 上传接口、流式播放、Feed 接口 | music/views.py | 组织流程,不直接拼 SQL |
MTV 真正的作用是把「数据长什么样」和「数据怎么展示」解耦。播放计数该放 Model 的字段还是 View 里算,答案很明确:它是一个持久化状态,必须是字段。而「这个用户能不能听 VIP 曲目」是规则,应该放在 View 或独立的 service 层。搞混这两者的项目,后期加一个「试听 30 秒」的需求就要改十几处。
2.3 五张核心表与关联字段设计
音乐分享平台的表不多,但要提前把级联和索引想清楚,否则删一个音乐人会把整个 feed 拖死:
# music/models.py from django.conf import settings from django.db import models class Artist(models.Model): """音乐人主页,与登录用户一对一,区分普通听众和创作者""" user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="artist") nickname = models.CharField(max_length=50, unique=True) bio = models.TextField(blank=True, default="") class Album(models.Model): artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name="albums") title = models.CharField(max_length=120) cover = models.ImageField(upload_to="covers/%Y/%m/", blank=True) released_at = models.DateField(null=True, blank=True) class Track(models.Model): album = models.ForeignKey(Album, on_delete=models.CASCADE, related_name="tracks") title = models.CharField(max_length=120, db_index=True) # 支持按标题搜索 audio = models.FileField(upload_to="audio/%Y/%m/") # 不要用 ImageField duration = models.PositiveIntegerField(default=0, help_text="单位:秒") play_count = models.PositiveIntegerField(default=0) # 用整数,别用字符串 uploaded_at = models.DateTimeField(auto_now_add=True)on_delete是必须显式写的参数。做社交场景我一般用CASCADE:删掉音乐人,他的专辑和歌曲一并消失,避免出现孤儿记录。但关注关系表要小心,用CASCADE会让「取消关注再重新关注」丢掉历史,需要留痕的话得换SET_NULL加一个is_active字段。
2.4 用 ORM 执行查询与删除对象
Django ORM 的删除不是 SQL 里那种直接DELETE就完事,它默认会把关联对象一起拉出来做级联判断。给 Track 加一个清理逻辑时,这两种写法差别很大:
# 方式一:删除单条,会触发级联 Track.objects.filter(pk=track_id).delete() # 方式二:批量删除某个音乐人的全部作品,减少查询次数 from music.models import Track Track.objects.filter(album__artist_id=artist_id).delete() # 方式三:只删符合条件的,先看会删掉什么 qs = Track.objects.filter(play_count=0, uploaded_at__lt=cutoff) print(qs.count(), list(qs.values_list("title", flat=True))) qs.delete()filter(...).delete()返回一个元组(删除总数, {模型名: 数量}),把它打进日志能快速确认影响范围。注意delete()不能和切片一起用,qs[:10].delete()会直接抛异常,必须先把 id 取出来再按pk__in删。另外级联删除会触发pre_delete、post_delete信号,如果你的项目里挂了信号做通知,删 1000 首歌就是 1000 次信号触发,线上要改成批量处理。
3. 音频上传与 StreamingHttpResponse 流式播放的参数配置
音乐平台最核心的体验点在播放。用FileResponse读整个文件返回,30 秒试听没问题,一首 8 MB 的曲子并发 50 个请求就能把 worker 占满,因为文件内容全被读进内存再发给客户端。正确做法是流式响应,并且把 content_type 和 content_disposition 设对。
3.1 音频存哪里:MEDIA_ROOT 与 upload_to 的配合
FileField只存相对路径,真实文件落在MEDIA_ROOT下面,这个字段一旦有历史数据就不要再改:
# settings.py import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") # 上传体积控制,音频文件普遍偏大 DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 超出走临时文件 FILE_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024upload_to="audio/%Y/%m/"会按年月分目录,单目录文件超过几千个之后ls和备份都会变慢。上传接口还要校验 MIME 而不是只看扩展名,把.mp3改名的可执行文件直接存进去,是最常见的坑。
3.2 StreamingHttpResponse 的 content_type 与 content_disposition 怎么设
这两个参数决定浏览器是「内联播放」还是「弹下载框」,设错的表现很直观:
| 参数 | 取值示例 | 效果 |
|---|---|---|
| content_type | audio/mpeg | 浏览器用内置播放器接管 |
| content_type | application/octet-stream | 浏览器无法识别,触发下载 |
| content_disposition | inline; filename="x.mp3" | 页面内播放 |
| content_disposition | attachment; filename="x.mp3" | 强制下载,保存为 x.mp3 |
下面是一个能直接用的流式播放视图,同时处理 Range 请求:
# music/views.py import os, re, mimetypes from django.conf import settings from django.http import StreamingHttpResponse from django.shortcuts import get_object_or_404 from .models import Track RANGE_RE = re.compile(r"bytes=(\d*)-(\d*)") def stream_audio(request, pk, chunk_size=65536): track = get_object_or_404(Track, pk=pk) path = track.audio.path size = os.path.getsize(path) # 根据扩展名推断 MIME,mp3 得到 audio/mpeg content_type = mimetypes.guess_type(path)[0] or "application/octet-stream" filename = os.path.basename(path) range_header = request.META.get("HTTP_RANGE") if not range_header: resp = StreamingHttpResponse(open(path, "rb"), content_type=content_type) resp["Content-Length"] = str(size) resp["Accept-Ranges"] = "bytes" resp["Content-Disposition"] = f'inline; filename="{filename}"' return resp m = RANGE_RE.match(range_header) start = int(m.group(1)) if m.group(1) else 0 end = int(m.group(2)) if m.group(2) else size - 1 # 缺省到文件末尾 end = min(end, size - 1) length = end - start + 1 def file_iter(path, start, length, chunk_size): with open(path, "rb") as f: f.seek(start) # 跳到客户端要求的起点 remaining = length while remaining > 0: data = f.read(min(chunk_size, remaining)) if not data: break remaining -= len(data) yield data resp = StreamingHttpResponse( file_iter(path, start, length, chunk_size), status=206, # 部分内容必须是 206 content_type=content_type, ) resp["Content-Range"] = f"bytes {start}-{end}/{size}" resp["Content-Length"] = str(length) resp["Accept-Ranges"] = "bytes" resp["Content-Disposition"] = f'inline; filename="{filename}"' return resp逻辑上分两条路线:没有Range头就整文件流式返回,有的话按区间返回 206。chunk_size我一般取 64 KB,太小会让 await 次数暴涨,太大则首字节延迟变高。Content-Disposition用inline是为了让<audio src>直接播,如果是「下载原曲」按钮就换成attachment,文件名里的中文要按 RFC 5987 做编码,否则会乱码。
3.3 拖动进度条为什么会失败
浏览器发Range: bytes=1048576-表示「从 1 MB 处开始播」。如果服务端忽略这个头、始终返回 200 和完整文件,进度条拖完会跳回开头,或者干脆无法播放。断点续传类播放器的判断顺序是:先看响应码是不是 206,再看Content-Range是否与请求区间匹配,最后才读 body。所以线上排查拖动问题,第一步永远是抓响应头,而不是去改前端 JS。
4. 动态流、关注关系与播放计数:Django ORM 查询优化实战
平台有内容之后,性能瓶颈会从文件 IO 转到数据库。首页动态流是典型的多表场景:要取关注的人、他们的分享、每条分享关联的歌曲和作者,写不好就是 N+1 查询。
4.1 关注关系表与 Feed 拉取
关注是自关联之外最常见的关系模型,加一个唯一约束防止重复关注:
# feed/models.py from django.conf import settings from django.db import models class Follow(models.Model): follower = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="following") followee = models.ForeignKey("music.Artist", on_delete=models.CASCADE, related_name="followers") created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint(fields=["follower", "followee"], name="uniq_follow") ] indexes = [models.Index(fields=["follower", "-created_at"])]UniqueConstraint比在视图里get_or_create更可靠,因为并发下两个请求可能同时通过存在性检查。索引顺序是follower在前、时间倒序在后,正好匹配「我关注的人的最新动态」这个查询模式。
4.2 select_related 与 prefetch_related 的取舍
多对一用select_related走 JOIN,一对多用prefetch_related发第二条 SQL 再在内存拼装。写反了要么 SQL 数量爆炸,要么 JOIN 出笛卡尔积:
from django.db.models import Prefetch from feed.models import Follow, Share def build_feed(user_id, limit=20): followee_ids = Follow.objects.filter(follower_id=user_id)\ .values_list("followee_id", flat=True) shares = (Share.objects .filter(track__album__artist_id__in=followee_ids) .select_related("user", "track", "track__album", "track__album__artist") .order_by("-created_at")[:limit]) return sharesvalues_list(..., flat=True)只取一列,避免把整个 Follow 对象实例化。select_related里用双下划线穿透三层外键,Django 会生成一条带多个 JOIN 的 SQL,取 20 条动态从 61 次查询降到 1 次。判断是否生效的方法很简单,在settings.py里开日志,数一数一次请求打了几条 SELECT。
4.3 播放计数用 F 表达式避免丢更新
播放量是最容易写错的地方。track.play_count += 1; track.save()在并发下会互相覆盖,正确写法是让数据库自己加:
from django.db.models import F from music.models import Track # 原子自增,不读回内存 Track.objects.filter(pk=track_id).update(play_count=F("play_count") + 1) # 同一个 F 表达式可以跨字段运算,比如热度分 Track.objects.filter(pk=track_id).update(score=F("play_count") * 2 + F("like_count"))update()不会触发save()方法,也不会触发post_save信号,需要同步缓存就得手动处理。另外高频播放场景下每条都写一次数据库压力很大,常见做法是先用 Redis 累加,定时任务回写,这里不展开。
4.4 用 Python 筛选出一样的记录做去重
动态流里同一个人连续分享同一首歌,展示多条观感很差。这类「筛选出一样的、只保留一条」的需求,Python 侧的写法比 SQL 的DISTINCT更灵活:
rows = (Share.objects.filter(track__album__artist_id__in=followee_ids) .order_by("-created_at") .values("id", "user_id", "track_id", "created_at")[:200]) seen = set() unique_rows = [] for row in rows: key = (row["user_id"], row["track_id"]) # 去重键 if key in seen: continue seen.add(key) unique_rows.append(row)用元组做 key 比拼字符串稳,字段多了也不会串味。如果用的是 PostgreSQL,也可以写成.distinct("user_id"),但 MySQL 不支持按字段去重,换数据库时这段代码会静默失效,所以按(user_id, track_id)在 Python 里去重更保险。数据量大到几万条时,把这段逻辑改成一次性values_list加集合运算,能再省一半内存。
4.5 用 explain 看 SQL 有没有走索引
写完不看执行计划,等于闭眼调优。取一下查询集的 SQL 再交给数据库分析:
qs = Share.objects.filter(track__album__artist_id__in=[1, 2, 3]).order_by("-created_at")[:20] print(qs.explain(analyze=False)) # 看预估计划 print(qs.query) # 看生成的 SQL 全文输出里重点看三处:type有没有变成ALL(全表扫描)、key是不是NULL、rows估算量是不是远超实际需要。created_at上建了索引却没用上,常见原因是在它外面套了函数,或者排序方向与索引定义相反。
5. 用 waitress 加 nginx 部署 Django 音乐平台并验证 Range 播流
开发服务器不能扛音频文件,runserver单线程处理几 MB 的流就会阻塞。Windows 上没法直接用 gunicorn,我用 waitress 顶上;Linux 上 waitress 同样能跑,选它不是将就,而是它在 Windows 下线程模型稳、零依赖。
在启动应用之前,先把静态文件收拢,并把媒体目录交给 nginx 直出,别让 Python 进程去读文件:
python manage.py collectstatic --noinput pip install waitress waitress-serve --listen=127.0.0.1:8000 --threads=8 musichub.wsgi:application--threads=8要结合机型调,流式响应每个连接会占用一个线程直到读完,给太少会排队,给太多则在磁盘 IO 上互相抢。实际生产更推荐让 nginx 直接服务/media/,或者用X-Accel-Redirect把鉴权交给 Django、把传输交给 nginx:
server { listen 80; server_name music.example.com; client_max_body_size 50m; # 与上传限制保持一致 location /static/ { alias /srv/musichub/staticfiles/; } location /protected_media/ { internal; # 只允许内部重定向访问 alias /srv/musichub/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; # 流式响应必须关掉缓冲 proxy_read_timeout 300s; } }internal和alias是最容易踩的两个点:internal意味着外部直接访问/protected_media/xxx.mp3会拿到 404,只能由 Django 在视图里返回带X-Accel-Redirect头的响应触发;alias结尾的斜杠要和 location 保持一致,否则会出现路径拼接错误。proxy_buffering off不设的话,nginx 会先把整段音频缓冲到自己这边,Range 的秒开效果就没了。
部署完别只看首页能不能打开,回到第 3 章那套响应头,用 curl 直接验证 Range 行为:
# 检查是否返回 206、Content-Range 是否正确 curl -s -D - -o /dev/null -H "Range: bytes=0-1023" \ http://127.0.0.1:8000/api/tracks/1/stream/ | head -n 12 # 检查 Content-Disposition 是 inline 还是 attachment curl -s -I http://127.0.0.1:8000/api/tracks/1/stream/ | grep -i disposition第一行返回里只要看到206 Partial Content加上Content-Range: bytes 0-1023/xxxx,说明断点续传链路是通的。如果这里是 200,问题多半出在反向代理把Range头吃掉了,检查 nginx 是否透传该请求头。
本文还有配套的精品资源,点击获取