毕业设计选题季,每年这个时候我都会收到一堆学弟学妹的私信:"学长,Django的毕设到底该做什么?"、"想用Python做毕设但不知道选什么题"、"有没有不太卷、但是答辩又拿得出手的项目"。如果你也在纠结这个问题,那这篇关于"青听校园音乐平台"的整理值得你耐心看完。
先说清楚这是个什么东西:这是一个基于Python + Django开发的全栈校园音乐平台,前端用HTML/CSS/JS配合 BootstraP 和 ECharts,后端引入了分布式计算的思想,用 Celery + Redis 处理异步任务,用 Channels + WebSocket 做实时数据推送,最后再通过数据可视化把用户行为、歌曲热度、播放趋势这些数据画成图表。换句话说,它不是一个简单的 CRUD 音乐管理后台,而是一个把简历里"分布式"、"数据可视化"、"实时推送"这几个关键词全部落到实处的毕业设计项目。强烈建议你先收藏,后面写毕设或者做项目实训的时候肯定用得上。
下面我不光讲这个平台怎么搭,还会把每个技术点背后"为什么这么做""踩了哪些坑""答辩时怎么讲"一并给你拆开揉碎。
1. 毕业设计选题的幕后逻辑:为什么音乐平台值得做
1.1 题目覆盖面分析:一个题目对应简历上的三块拼图
很多人在毕设选题的时候会走两个极端:要么选纯理论、纯算法的题目,写完连个能点开的界面都没有;要么选一些烂大街的管理系统,比如"图书管理系统""超市管理系统",做完之后除了增删改查完全说不出技术亮点。选择音乐平台这个方向,本质上是在"业务复杂度"和"技术深度"之间找一个平衡点。
音乐平台的业务模型比普通管理系统要丰富得多。你要处理用户的注册登录、歌手专辑歌单的关联关系、播放记录的产生、评论的发布、音乐文件的上传与播放,甚至还有搜索、推荐、排行榜这些衍生功能。这些功能落到数据库上,就是多张表之间的外键关联、多对多关系、聚合统计查询,Django 的 ORM 在这里能发挥出很大优势。
更重要的是,这个题目天然适配三个"加分方向":第一是数据可视化,音乐平台每天会产生大量播放记录和用户行为数据,画趋势图、柱状图、饼图都合情合理;第二是分布式计算,歌曲的播放计数、热门榜单统计、消息推送这些任务如果全部同步处理,会占用大量请求线程,用消息队列削峰填谷就成了一件顺理成章的事;第三是实时通信,你完全可以做一个 WebSocket 推送,让后台一旦有新的播放数据,前台页面马上更新。这三块拼图凑在一起,在毕业答辩里具备了从"功能演示"升级到"系统设计讲解"的基础。
1.2 技术栈定型的思路:Django做主力框架的理由
先解决一个最常见的问题:为什么是 Django,而不是 Flask 或者 Spring Boot?Flask 确实轻量,配一个 ECharts 写数据可视化页面也很快,但 Flask 默认不自带 ORM、Admin 后台、用户认证体系,这些在毕设中都要自己拼装,做到后面反而更耗时。Spring Boot 在企业里很主流,但如果你主攻 Python,再用 Java 系框架,精力会被分散掉,而且对零基础同学来说门槛偏高。
Django 最吸引人的地方在于它的"全家桶"属性:自带的 Admin 后台能直接看到用户、歌曲、评论的数据管理;自带的 User 模型可以通过继承 AbstractUser 扩展头像、签名、会员字段;自带的 ORM 让你不用写原生 SQL 就能完成复杂的连表查询和聚合统计。换句话说,Django 帮你踩掉了大量重复性的坑,你能把宝贵的时间省下来,投入到分布式任务和数据可视化这两个最有答辩价值的点上。
再说分布式计算这个点。严格来说,一个毕业设计项目并不会真的去部署一套 Hadoop 或者 Spark 集群,那属于分布式计算里的"大数据"分支。在 Web 项目语境下,"分布式"的合理落地方式是把耗时任务拆出去,交给独立进程去跑,让 Web 服务器和任务执行器在逻辑上分离。青听平台采用的是Celery 分布式任务队列 + Redis 消息代理的方案:Web 进程收到请求后先把任务写入 Redis,然后由多个 Worker 进程并行消费任务。这样你能在答辩现场演示"并发越高、任务队列自动扩容"的分布式特性,比口头上说"我了解分布式原理"有说服力得多。
2. 平台的底层骨架:从数据模型到前后端交互
2.1 数据库模型设计:音乐平台的六个核心表
数据模型是整个平台的根基。我第一次做这类项目的时候犯过一个错误:一上来就写视图、写模板,等做到播放记录和榜单统计时才发现表结构设计得乱七八糟,返工重来了三天。所以如果你要复现这个项目,建议先花半天时间把模型定下来。
青听平台的模型,我按业务域拆成了用户、音乐、歌单、记录、评论这几块,核心表大概如下:
from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): # 扩展Django自带用户:头像、签名、会员标记 avatar = models.ImageField(upload_to='avatar/%Y/%m', default='avatar/default.png') signature = models.CharField(max_length=255, blank=True) is_vip = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Singer(models.Model): name = models.CharField(max_length=50, db_index=True) avatar = models.ImageField(upload_to='singer/') region = models.CharField(max_length=20, blank=True) # 地区,用于可视化 intro = models.TextField(blank=True) class Album(models.Model): title = models.CharField(max_length=100) singer = models.ForeignKey(Singer, on_delete=models.CASCADE, related_name='albums') cover = models.ImageField(upload_to='album_cover/') publish_date = models.DateField(null=True, blank=True) class Song(models.Model): title = models.CharField(max_length=100) singer = models.ForeignKey(Singer, on_delete=models.CASCADE, related_name='songs') album = models.ForeignKey(Album, on_delete=models.SET_NULL, null=True, related_name='songs') audio = models.FileField(upload_to='audio/') duration = models.IntegerField(default=0, help_text='时长,单位秒') play_count = models.BigIntegerField(default=0) # 冗余字段,减少实时聚合压力 lyric = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Playlist(models.Model): name = models.CharField(max_length=50) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='playlists') cover = models.ImageField(upload_to='playlist_cover/', null=True, blank=True) songs = models.ManyToManyField(Song, related_name='playlists') created_at = models.DateTimeField(auto_now_add=True) class PlayRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='play_records') song = models.ForeignKey(Song, on_delete=models.CASCADE, related_name='play_records') played_at = models.DateTimeField(auto_now_add=True) class Comment(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) song = models.ForeignKey(Song, on_delete=models.CASCADE, related_name='comments') content = models.TextField() created_at = models.DateTimeField(auto_now_add=True)这里有两个新手容易忽略的设计细节。第一个是Song.play_count字段,我用的是一个冗余计数,而不是在展示时去PlayRecord表里 Count。原因是播放记录表会随着用户操作无限膨胀,每次访问榜单都聚合 Count 一次,数据库压力会非常大。更好的做法是:播放时通过 Celery 异步把计数写入这个字段,展示时直接查这个字段就行了。第二个是related_name一定要写清楚,比如singer.playlists和song.playlists如果都叫playlists就会冲突,良好的命名习惯能避免后续 ORM 查询时一堆莫名其妙的报错。
2.2 ORM查询优化:select_related与prefetch_related
音乐平台里最常见的查询是"列表页展示歌曲,同时显示歌手名和专辑封面"。如果你用最原始的写法:
songs = Song.objects.all() for song in songs: print(song.singer.name, song.album.title)这段代码触发的 SQL 查询次数是 1 + 2N。歌曲有 100 条,就会额外发 200 次查询请求,页面响应时间会肉眼可见地变慢。解决方式很简单,在查询时用select_related把外键关系一次性 join 出来:
songs = Song.objects.select_related('singer', 'album').all()而如果是查询歌单,并且要取歌单里的所有歌曲,这就是一个多对多关系,select_related帮不上忙,得用prefetch_related:
playlists = Playlist.objects.prefetch_related('songs__singer').all()这类优化在功能开发阶段看不出来区别,因为测试数据只有几十条。但答辩的时候很可能会被评委问一句:"如果你的平台有十万首歌曲,现在的查询方式还撑得住吗?"你只要能答出select_related、prefetch_related和普通查询的 N+1 问题区别,这个加分项基本就稳了。
2.3 Django模板体系与路由组织
Django 的路由组织有一个习惯我特别推荐:每个 app 内部创建自己的 urls.py,然后在项目根路由里用 include 挂载。青听平台我拆了user、music、analysis、api这几个 app,根路由大概长这样:
from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('user/', include('user.urls')), path('music/', include('music.urls')), path('analysis/', include('analysis.urls')), path('api/', include('api.urls')), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这个设计的直接好处是:分析模块的可视化接口走/api/,业务页面走/music/,用户中心走/user/,职责清晰,后面加新功能基本不会动到根路由。模板方面,Django 默认的模板语法虽然不如 Vue 那样灵活,但应对这个项目已经足够了。我的建议是页面骨架用模板继承:
<!-- base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}青听校园音乐平台{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> {% block extra_head %}{% endblock %} </head> <body> {% include 'header.html' %} {% block content %}{% endblock %} {% include 'footer.html' %} <script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script> {% block extra_js %}{% endblock %} </body> </html>这样写的好处是:每个页面只需要写自己的{% block content %}部分,页面之间风格统一,也不会出现同一个导航栏在不同页面重复复制粘贴的问题。
3. 数据可视化落地:ECharts在Django里的正确打开方式
3.1 可视化数据从哪来:ORM聚合查询与JsonResponse
数据可视化最关键的并不是前端怎么画图,而是后端怎么把数据喂给前端。我在这个项目里没有采用复杂的 ECharts + 前后端分离方案,而是走 DjangO 模板页面 + Ajax 动态刷新,这样既保住了 Django 模板开发的便利性,又能让图表实时更新。
后端我先写了一个提供 JSON 数据的视图,放在analysis/views.py里。比如统计最近七天每天的播放量:
from django.db.models import Count from django.db.models.functions import TruncDate from django.http import JsonResponse from django.utils import timezone from datetime import timedelta def play_trend_data(request): days = int(request.GET.get('days', 7)) start = timezone.now() - timedelta(days=days) records = ( PlayRecord.objects .filter(played_at__gte=start) .annotate(day=TruncDate('played_at')) .values('day') .annotate(total=Count('id')) .order_by('day') ) dates = [item['day'].strftime('%m-%d') for item in records] counts = [item['total'] for item in records] return JsonResponse({'dates': dates, 'counts': counts})这段代码的核心在于TruncDate,它能把played_at这个DateTimeField截断成日期,配合annotate就能按天分组计数。很多新手会试图用filter(played_at__contains='2025-01-01')这种土办法来筛日期,SQL 效率低不说,代码还丑。用 TruncDate 才是 Django 官方推荐的做法。
再比如"歌手歌曲分布图"和"热门歌曲 Top10",本质上就是把聚合条件换一下:
def singer_songs_data(request): data = list( Singer.objects.annotate(song_count=Count('songs')) .values('name', 'song_count') .order_by('-song_count')[:10] ) return JsonResponse({'data': data}) def hot_rank_data(request): data = list( Song.objects.order_by('-play_count')[:10] .values('title', 'play_count') ) return JsonResponse({'data': data})这里有一个要点:values('name', 'song_count')之后返回的是一个字典列表,JsonResponse会自动帮你做 JSON 序列化,不需要手动json.dumps,也不需要ensure_ascii=False,因为 Django 已经处理好了中文编码。如果你在返回时遇到中文乱码,记得检查是不是自己手动做了多余的序列化。
3.2 ECharts图表在模板中的接入
后端准备好了 JSON 接口,前端页面的核心工作就是拿数据、渲染图表。在 DjangO 模板中存 ECharts,我强烈建议在{% block extra_js %}里写图表初始化代码,不要全部堆到页面底部。以首页的"播放趋势折线图"为例:
// 页面里的 script 标签 $(function () { $.getJSON('/api/play-trend/', function (res) { var chart = echarts.init(document.getElementById('trendChart')); var option = { title: { text: '最近7天播放量趋势' }, tooltip: { trigger: 'axis' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: res.dates }, yAxis: { type: 'value' }, series: [{ name: '播放量', type: 'line', smooth: true, areaStyle: {}, data: res.counts }] }; chart.setOption(option); // 浏览器窗口变化时自适应 window.addEventListener('resize', function () { chart.resize(); }); }); });这里最容易踩的一个坑是:echarts.init时绑定的 DOM 元素必须是可见的。如果你把图标放在一个默认隐藏的 Tab 页里面,等切换 Tab 的时候会发现图表宽高为 0。解决办法是在 Tab 切换事件里手动调用一次chart.resize()。另一个常见问题是直接把 ECharts 的echarts.min.js放在本地静态目录,如果你是从 CDN 下载的旧版本,可能会有兼容性问题,建议直接到 ECharts 官网下载当前最新版本,或者用官方 CDN。
3.3 几个高清分可视化案例:播放趋势、歌手分布、热榜Top10
除了播放趋势折线图,我还做了三个可视化页面,分别对应不同图表类型,答辩时展示效果会非常丰富:
歌手地区分布饼图,数据从Singer.region字段聚合而来,用饼图能直观看出校园音乐平台里本土歌手和国际歌手的占比。热门歌曲 Top10 横向柱状图,最能体现榜单效果,建议用横向柱状图(把 yAxis 和 xAxis 的 type 互换),歌名显示更完整。用户活跃时段热力图,这个稍微进阶一点,把一天 24 小时按小时分组,统计每个小时段的播放次数,用 ECharts 的 heatmap 展示,能够看出用户在午休和晚上两个时间段使用最频繁。
这三个可视化的数据接口全部通过上面的聚合查询方式获取,前端模板结构基本一致,只是option里的type不同。我建议你在开发时把每个图表封装成一个独立的renderXxxChart()函数,维护起来会舒服很多。
4. "分布式计算"真实落地场景:不是堆机器,而是拆任务
4.1 为什么Django项目需要Celery:耗时代码的归宿
聊到"分布式计算"之前,我先说一个很现实的问题:Django 的视图函数默认是同步阻塞的,如果一个请求里要执行耗时的操作,比如发送邮件、解析音乐文件的时长、批量更新播放计数、生成数据报表,这个请求的线程就会被占住,用户就要一直转圈等待,并发稍高一点,整个服务就容易被拖垮。
分布式计算在这个项目里最扎实的落地方式,就是把这类耗时不紧急的任务从请求链路中剥离出去,交给消息队列和后台 Worker 去处理。用到的工具就是Celery。
Celery 的基本模型可以类比成一个外卖平台:Django 视图就是下订单的顾客,Redis 是订单池,Celery Worker 是骑手。顾客点单之后不需要自己跑到厨房等菜,订单进入池子,空闲的骑手取单配送。对应到系统里:用户播放歌曲时,视图函数不需要立刻去写PlayRecord、去累加play_count,而是把任务发给 Redis,然后立刻返回"播放成功",后台 Worker 再去慢慢处理这些任务。
这里就体现出"分布式"的意味了:你可以开多个 Worker 进程,每个 Worker 独立消费任务队列,任务多了就横向扩展 Worker 数量,不依赖 Web 进程本身。
4.2 Celery + Redis集成全流程
我把我实际搭通的配置步骤完整列出来,照着做基本不会出错。
首先安装依赖:
pip install celery redis然后在项目根目录创建qingting/celery.py:
import os from celery import Celery os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'qingting.settings') app = Celery('qingting') app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks()接着在settings.py里补充配置:
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/1' CELERY_TASK_SERIALIZER = 'json' CELERY_TIMEZONE = 'Asia/Shanghai'然后写一个异步任务,放在music/tasks.py:
from celery import shared_task from django.core.cache import cache @shared_task def add_play_record(song_id, user_id): from .models import PlayRecord, Song PlayRecord.objects.create(song_id=song_id, user_id=user_id) Song.objects.filter(pk=song_id).update(play_count=F('play_count') + 1) # 删除排行榜缓存,让下次查询重新聚合 cache.delete('hot_rank')注意几个细节。第一,任务内部建议延迟导入模型,写在函数里面而不是写在文件顶部,避免 Celery 加载任务时因为 Django 模型未初始化而报错。第二,累加次数用F('play_count') + 1而不是先取再写,这样在并发情况下不会丢失计数。第三,创建 PlayRecord 之前,可以把"这个用户是否已经播放过这首歌""是否允许重复播放"这类校验逻辑放在视图层,任务层只做最简单的数据落库。
启动的时候分别开两个终端:
# 终端1:启动Django开发服务器 python manage.py runserver # 终端2:启动Celery Worker,Windows下推荐加 -P eventlet celery -A qingting worker -l info -P eventletWindows 下不加-P eventlet经常会报ValueError: not enough values to unpack,这是因为 Celery 默认的 prefork 模式在 Windows 上没有完整的 fork 机制,换成 eventlet 协程模式就能正常跑。需要先执行pip install eventlet。
4.3 WebSocket实时数据推送:Channels实现前台实时更新
热词里有一条很关键的:"django websocket实现后台有数据前端推送"。这正好是青听平台的亮点功能。Django 原生只支持 HTTP,要实现 WebSocket 需要引入Channels,它把 Django 扩展成了支持 ASGI(异步服务器网关接口)的框架。
安装和配置很简单:
pip install channels channels-redis# settings.py INSTALLED_APPS = [ # ... 'daphne', 'channels', ] ASGI_APPLICATION = 'qingting.asgi.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {'hosts': [('127.0.0.1', 6379)]}, } }然后写一个消费者(Consumer),作用是统计当前在线人数并实时推送给所有连接的前端页面:
# analysis/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from django.contrib.auth.models import User class OnlineConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name = 'online_users' await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() # 把当前在线人数推给所有连接的客户端 online_count = await self.get_online_count() await self.channel_layer.group_send( self.group_name, {'type': 'online_count', 'value': online_count} ) async def disconnect(self, code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def online_count(self, event): await self.send(text_data=json.dumps({'type': 'online_count', 'value': event['value']})) @database_sync_to_async def get_online_count(self): return User.objects.filter(last_login__gte=...).count()路由文件也要跟上:
# analysis/routing.py from django.urls import path from .consumers import OnlineConsumer websocket_urlpatterns = [ path('ws/online/', OnlineConsumer.as_asgi()), ]再把analysis/routing.py挂到项目的asgi.py里。前端页面只需要建立 WebSocket 连接:
var ws = new WebSocket('ws://' + window.location.host + '/ws/online/'); ws.onmessage = function (event) { var data = JSON.parse(event.data); if (data.type === 'online_count') { $('#onlineCount').text(data.value + ' 人在线'); } };做到这一步,你在后台用另一个浏览器登录,前台就能实时看到在线人数变化。这个功能演示起来非常直观,也是整个项目里"看起来最像企业级系统"的一个点。
4.4 数据库读写分离与缓存策略
我在部署版本里还做了一个"轻量级读写分离"的尝试:所有的写操作走主库,所有的读操作走从库。这虽然是数据库层的垂直拆分,但在 Django 里配置非常简单,只需要在settings.py里定义多套数据库:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'qingting', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', }, 'replica': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'qingting', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '192.168.1.4', # 从库地址 'PORT': '3306', } } DATABASE_ROUTERS = ['qingting.db_router.MasterSlaveRouter']然后写一个简单的路由类,实现db_for_read和db_for_write两个方法。核心逻辑就一句话:读操作给replica,写操作给default。在真实生产环境里,从库和主库之间是通过主从复制保持数据一致的,毕设环境如果没有条件搭主从,你可以用两个本机 MySQL 实例模拟,或者在答辩时讲清楚设计思路即可。
缓存方面,排行榜、歌手列表这种不会实时变化的数据,我用 Redis 做了缓存。查询时先读缓存,缓存里没有才查数据库,查完之后再写入缓存并设置过期时间。这就是典型的Cache Aside 模式,写代码不超过十行,但讲起来非常有体系感。
5. 开发期踩坑记录:每一个坑都可以是答辩加分项
5.1 静态文件404的老问题
Django 开发环境下静态文件 404 是出现频率最高的问题。很多人用python manage.py runserver启动后,访问页面发现 CSS、JS 全部加载不出来,控制台全是 404。原因通常是忘记在项目的urls.py里配置静态文件路由。开发环境下我建议直接在根路由里加一句:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)同时要检查settings.py里是否设置了STATICFILES_DIRS,并且保证模板里是用{% static 'css/style.css' %}而不是写死/static/css/style.css。写死路径在你本地开发可能没有问题,一旦部署到服务器,路径前缀发生变化,就会出现全部引用失效的情况。
5.2 ECharts图表渲染不出来:宽高为零的问题
ECharts 初始化时如果容器没有设置宽度和高度,图表是画不出来的。我第一次遇到这个坑是在把图表放进 Bootstrap 的 Tab 页时,只有一个 Tab 是默认显示的,到了第二个 Tab,聪明的折腾半天才发现不是数据的问题,而是容器被隐藏时宽度为 0。
解决办法有两个:要么给每个图表的div写死height: 400px; width: 100%,要么在 Tab 切换的回调里调用setTimeout(() => chart.resize(), 200)。我最后选择的是给容器写死高度,避免因为 resize 时机问题导致图表变形。
还有一个细节:$.getJSON请求后端接口时,如果接口返回 500,浏览器控制台只会显示一个红色的GET ... 500 (Internal Server Error),真正的报错信息在 Django 终端里。这时候要学会第一时间看终端堆栈,而不是在前端反复调试。
5.3 Celery在Windows下的兼容性问题
Windows 下启动 Celery 的坑我在前面提过一次,这里再展开讲。没有加-P eventlet时,Celery worker 启动后能正常接收任务,但一旦有任务真正执行,Windows 上容易进程卡死或抛出ValueError。这个问题在 macOS 和 Linux 上不存在,主要是 Windows 没有原生 fork 系统调用。解决方案就是安装eventlet并用协程模式启动:
pip install eventlet celery -A qingting worker -l info -P eventlet另外提醒一句:加入 Channels 之后,启动开发服务器不能再用python manage.py runserver,而要改用daphne qingting.asgi:application,或者使用 uvicorn。我实测下来用uvicorn qingting.asgi:application --reload更顺手,重载功能和 runserver 类似。
5.4 N+1查询导致的响应变慢
我在 2.2 节中已经讲解了select_related和prefetch_related。开发初期我没有太在意,一直等到数据量增加到几百条,发现"歌曲列表页"打开要将近两秒,才意识到这个问题的严重性。后来我用 Django 自带的一个调试工具django-debug-toolbar查看 SQL 执行次数,发现一个页面竟然发出去几百条 SQL,这才彻底修改查询写法。在这里想多强调一句:当你写完一个列表页,养成用 debug-toolbar 看一眼 SQL 次数的习惯,比什么都管用。
5.5 媒体文件上传与访问路径
音乐平台的用户头像、歌手封面、音乐文件都属于媒体文件。开发环境下要把MEDIA_ROOT和MEDIA_URL配置好,并且在根路由中手工添加静态路由,这样才能在页面里通过{{ song.audio.url }}访问到文件。这个问题的隐蔽性在于:Admin 后台里能看到上传成功的文件,但前台就是加载不出来。检查思路很简单,先看MEDIA_URL是否被模板正确引用,再看 Django 终端是否拦截了/media/开头的请求。
部署到生产环境后,媒体文件通常由 Nginx 直接托管,不再经过 Django,所以这些配置还要再调整一遍。
6. 项目收尾与答辩经验:从源码到优秀毕业设计的临门一脚
6.1 部署结构:Nginx + Gunicorn + Django
毕设如果需要现场演示,直接在本地用开发服务器跑完全没问题,但如果你想在服务器上部署一份,让评委通过公网域名访问,推荐的部署结构是:
- Nginx负责接收外部 HTTP 请求,托管静态文件和媒体文件
- Gunicorn负责运行业务代码,处理动态请求
- Django通过 WSGI 与 Gunicorn 对接
- Celery Worker独立进程处理异步任务
- Redis提供消息队列与缓存支撑
Gunicorn 启动命令:
gunicorn qingting.wsgi:application -w 4 -b 127.0.0.1:8000Nginx 的核心配置大概如下:
server { listen 80; server_name your_domain.com; location /static/ { alias /your_project/static/; } location /media/ { alias /your_project/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置我在多个项目里验证过,稳定性没问题。需要注意的是:部署前一定要执行python manage.py collectstatic收集静态文件,否则 Nginx 找不到任何 CSS 和 JS。
6.2 答辩演示的操作顺序建议
答辩的时候给评委演示项目,顺序和节奏非常关键。我不建议一上来就演示功能,而是先用一个数据可视化页面开场,比如播放趋势折线图和热门 Top10 柱状图,让评委在头三十秒就 get 到"这个项目有数据分析能力",而不是一个普通的管理系统。
接着演示核心业务:登录、站内搜索、听歌、创建歌单。这里不要贪多,把一条主链路走通就好。然后切换到"分布式与实时通信"这个技术亮点:打开后台,手动模拟一个用户播放歌曲,前台页面通过 WebSocket 立刻刷新在线人数和最新播放记录,同时解释这一过程中 Redis 队列和 Celery Worker 各自做了什么。最后打开 Admin 后台,展示数据模型设计和数据库表结构,回答评委关于"表与表之间关系"的提问。
这部分需要提前准备好一张系统架构图,图中清楚标注浏览器、Nginx、Django、Redis、Celery Worker、MySQL 六个组件之间的关系。不需要史实图或者复杂工具,用 PowerPoint 画一个简单的框图就足够了。
6.3 这版源码还能往哪些方向延伸
如果你做完上面这些功能之后还有富余时间,我非常推荐再往下面几个方向扩展一两个,能让项目含金量再上一个台阶:
第一个方向是推荐算法。基于用户播放记录构建"协同过滤"推荐,给用户推荐可能喜欢的歌曲。不用做到大厂那样复杂的深度模型,一个基于物品的协同过滤(ItemCF)算法就足以写清楚原理并实现。第二个方向是自动标签。从歌曲名和评论内容提取关键词,生成词云图,这是数据可视化里非常出效果的一个扩展。第三个方向是移动端适配。把前端页面用响应式布局重构,或者做成一个简单的微信小程序,通过后端 API 交互,这样系统就横跨了 PC 端和移动端两个平台。
我个人在这类项目里最感兴趣的其实是Celery 任务队列结合定时任务:每天凌晨自动汇总前一天的播放数据,生成日报,并通过站内消息推送给管理员。这个功能实现难度不大,但它把"任务队列 + 定时调度 + 数据聚合"串在了一条链路上,讲解起来特别容易引起评委的兴趣。
回到开头那个问题:毕业设计的项目到底该怎么选?我的结论很简单——选那些能把课本上几个重要概念落到真实代码里的题目。青听校园音乐平台这套组合(Python + Django + HTML/CSS/JS + 分布式计算 + 数据可视化)具备"功能完整、技术点密集、可演示性强"三个特点,这几年带学生复盘下来,走这条路线的最终答辩效果普遍都不错。你照着上面的结构和踩坑记录动手做一遍,等到答辩那天,你会感谢自己当初没有为了省事去选一个"图书管理系统"。