简介:本资源是一套完整可用的基于Python与Django框架开发的在线音乐网站毕业设计项目,面向计算机专业本科生及Web开发初学者,满足课程设计、期末大作业与毕业设计等实践需求。项目已通过本地部署验证,源码稳定可运行,评审得分98分,内容经助教审定,难度适中且具备典型Web应用全栈结构。压缩包共137个文件,含54个核心Python后端逻辑文件、10个CSS与10个JS前端样式交互文件、13个M4A/MP3音频示例、8个PNG与18个JPG界面素材,以及SQLite3数据库文件和SQL建表脚本,整体大小为44.12MB。已有160人学习下载,项目目录结构规范,涵盖用户注册登录、音乐播放、排行榜、搜索、评论等完整模块,配套多份CSS样式文件(如play.css、ranking.css、user.css等)体现清晰的分层设计逻辑,便于理解Django MTV模式在实际业务中的落地方式。
1. 为什么用 Django 做在线音乐网站,比 Flask 或纯 Vue 更稳、更省毕业答辩时间?
这不是一个“炫技型”项目——它要跑在本地开发机上能播歌、上传 MP3、按歌手/专辑分类、用户注册登录后收藏歌曲,还要能导出数据库、打包交导师、答辩现场不蓝屏。很多同学用 Flask 搭了个首页加播放器,结果登录状态存不住、文件上传卡死、MySQL 连接池崩三次,答辩前两天还在重装 Python 环境;也有人前端用 Vue 写得飞起,后端却只靠json.dumps()返回数据,连用户权限都没做,导师一问“怎么防止 A 用户删掉 B 的收藏列表”,当场哑火。而 Django 天然带 ORM、Admin 后台、用户认证系统、文件上传处理、CSRF 防护、模板渲染能力——这些不是“可选功能”,是毕业设计里必须闭环的硬性需求点。你不需要从零造轮子,但得知道哪些模块必须启用、哪些配置不能跳过、哪些数据库字段类型一选错,后期改起来就得重写迁移脚本。本文就带你用 Django 4.2(LTS 版本)+ SQLite(兼容性好、免部署)+ Bootstrap 5(快速搭 UI),从django-admin startproject开始,到python manage.py runserver能完整听歌、搜歌、收藏、管理后台全通,所有代码可直接复制粘贴运行,所有坑我都替你踩过三遍。
2. 从零初始化:Django 项目结构、核心 App 拆分与数据库建模逻辑
2.1 创建项目并规划 App 职责边界:music、userprofile、playlist 三个 App 到底谁管什么?
毕业设计最常翻车的起点,就是把所有模型、视图、模板全塞进一个musicApp 里。等做到第 4 周,发现用户收藏功能要改用户表,结果models.py里混着 Song、Album、User、Favorite 一堆类,改一个字段牵动 17 个外键,迁移失败报错像天书。正确做法是按业务域拆 App:
music:专注音源本身——歌曲、专辑、歌手、分类、标签、试听时长、文件路径;userprofile:扩展 Django 默认 User 模型——头像、昵称、注册时间、最后登录 IP(答辩时能展示“用户行为分析”加分项);playlist:处理用户级操作——收藏夹、创建歌单、歌单内歌曲排序、公开/私有状态。
提示:不要用
django.contrib.auth.models.User直接加字段!必须通过AbstractUser继承或OneToOneField关联扩展,否则后续 Admin、登录逻辑全崩。
执行以下命令初始化结构(Python 3.10+,Django 4.2):
# 创建虚拟环境(强烈建议,避免包冲突) python -m venv venv_music source venv_music/bin/activate # Windows 用 venv_music\Scripts\activate.bat pip install django==4.2.13 # 初始化项目 + 三个 App django-admin startproject music_site . python manage.py startapp music python manage.py startapp userprofile python manage.py startapp playlist然后在settings.py的INSTALLED_APPS中注册:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 自定义 App(顺序重要!userprofile 必须在 auth 之后、music 之前) 'userprofile', 'music', 'playlist', ]2.2 数据库建模:为什么 Song 表用FileField而不用CharField存路径?SQLite 下如何规避 BLOB 性能陷阱?
很多开源音乐项目把 MP3 文件直接读成二进制塞进models.BinaryField,结果数据库文件暴涨到 2GB,sqlite3打开都卡死——这是典型误区。Django 的FileField不存文件内容,只存相对路径,真实文件放在MEDIA_ROOT目录下,数据库只记录uploads/songs/2024/05/track_123.mp3这种字符串。这才是生产级(哪怕只是毕设)的合理设计。
以下是music/models.py的最小可行建模(已通过makemigrations+migrate验证):
# music/models.py from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Singer(models.Model): name = models.CharField(max_length=100, verbose_name="歌手名") avatar = models.ImageField(upload_to="singers/", blank=True, null=True, verbose_name="头像") def __str__(self): return self.name class Album(models.Model): title = models.CharField(max_length=150, verbose_name="专辑名") cover = models.ImageField(upload_to="albums/", blank=True, null=True, verbose_name="封面") release_date = models.DateField(blank=True, null=True, verbose_name="发行日期") def __str__(self): return self.title class Song(models.Model): title = models.CharField(max_length=200, verbose_name="歌名") singers = models.ManyToManyField(Singer, verbose_name="演唱者") album = models.ForeignKey(Album, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属专辑") duration = models.DurationField(verbose_name="时长") # 自动转为 HH:MM:SS 格式 file = models.FileField(upload_to="songs/", verbose_name="音频文件") # 关键:存路径,非内容 cover = models.ImageField(upload_to="covers/", blank=True, null=True, verbose_name="歌曲封面") upload_time = models.DateTimeField(default=timezone.now, verbose_name="上传时间") def __str__(self): return f"{self.title} - {', '.join([s.name for s in self.singers.all()[:2]])}"参数说明:
upload_to="songs/":文件实际保存到MEDIA_ROOT/songs/目录,Django 自动创建子目录(如按年月分);duration = models.DurationField():比CharField存 "03:45" 更可靠——支持数据库级时长计算(如“总播放时长 > 1 小时”的查询);on_delete=models.SET_NULL:删专辑时歌曲不消失,只清空外键,避免数据丢失(答辩老师最爱问“删专辑会影响已收藏歌曲吗?”)。
2.3 用户扩展:用OneToOneField关联 UserProfile,而不是继承AbstractUser的血泪经验
继承AbstractUser看似干净,但会导致auth_user表结构变更,createsuperuser命令失效,Admin 登录页崩溃——尤其当你已经跑过几次migrate后再想改,几乎无解。毕设阶段最稳妥的是OneToOneField方案:
# userprofile/models.py from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') nickname = models.CharField(max_length=50, blank=True, verbose_name="昵称") avatar = models.ImageField(upload_to="avatars/", blank=True, null=True, verbose_name="头像") bio = models.TextField(blank=True, verbose_name="个人简介") def __str__(self): return f"{self.user.username}'s profile" # 自动创建 Profile(关键!否则注册新用户后 profile 为空) @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): instance.profile.save()然后在userprofile/admin.py中注册,让 Admin 后台能一键编辑:
# userprofile/admin.py from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from .models import UserProfile class UserProfileInline(admin.StackedInline): model = UserProfile can_delete = False verbose_name_plural = 'Profile' class CustomUserAdmin(UserAdmin): inlines = (UserProfileInline,) admin.site.unregister(User) admin.site.register(User, CustomUserAdmin)3. 前后端协同:模板渲染 + 视图逻辑 + 静态资源路径的三重校准
3.1settings.py全局配置:MEDIA_ROOT / STATIC_ROOT / TEMPLATES 路径必须严格对齐
90% 的“图片不显示、CSS 不加载、上传文件 404”问题,根源都在这三处路径没对齐。别抄网上的模糊配置,按以下绝对路径写死(以项目根目录为基准):
# settings.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # 注意:manage.py 在根目录,所以向上三级 # 静态文件(CSS/JS/Bootstrap) STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / "static", # 开发时:存放 bootstrap.min.css 等 ] STATIC_ROOT = BASE_DIR / "staticfiles" # 生产时 collectstatic 输出目录(毕设不用,但必须存在) # 媒体文件(用户上传的 MP3、封面图) MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / "media" # 实际文件存储位置,必须手动创建该文件夹! # 模板路径(HTML 文件) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # 所有 HTML 放这里 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]注意:
MEDIA_ROOT对应的media/文件夹必须手动创建!Django 不会自动建。执行:mkdir media mkdir media/songs media/albums media/singers media/covers media/avatars
3.2 URL 路由分层:主路由urls.py+ App 子路由music/urls.py的标准写法
新手常犯错误:把所有path()全写在根urls.py,导致后期维护混乱。正确做法是每个 App 自管自己的路由:
# music_site/urls.py(主路由) 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('', include('music.urls')), # 首页、搜索、详情 path('user/', include('userprofile.urls')), # 登录、注册、个人页 path('playlist/', include('playlist.urls')), # 收藏、歌单 ] # 开发阶段必须加这一行,否则 MEDIA 文件无法访问 if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)# music/urls.py(子路由) from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('song/<int:song_id>/', views.song_detail, name='song_detail'), path('search/', views.search, name='search'), path('album/<int:album_id>/', views.album_detail, name='album_detail'), ]3.3 播放页面模板:用<audio>标签 +{{ song.file.url }}实现零 JS 播放
别被网上“Vue + Web Audio API”的教程带偏。毕设要的是稳定、可演示、不依赖额外框架。Django 模板原生支持:
<!-- templates/music/song_detail.html --> {% extends 'base.html' %} {% load static %} {% block content %} <div class="container mt-4"> <div class="row"> <div class="col-md-4 text-center"> <img src="{{ song.cover.url }}" class="img-fluid rounded" alt="{{ song.title }}"> </div> <div class="col-md-8"> <h2>{{ song.title }}</h2> <p><strong>演唱:</strong>{% for s in song.singers.all %}{{ s.name }}{% if not forloop.last %}、{% endif %}{% endfor %}</p> <p><strong>专辑:</strong>{{ song.album.title }}</p> <p><strong>时长:</strong>{{ song.duration|time:"i:s" }}</p> <!-- 核心:audio 标签直接读取 MEDIA_URL --> <audio controls class="w-100 mt-3"> <source src="{{ song.file.url }}" type="audio/mpeg"> 您的浏览器不支持 audio 元素。 </audio> </div> </div> </div> {% endblock %}关键点:
{{ song.file.url }}会自动拼接MEDIA_URL + 文件相对路径,比如/media/songs/2024/05/track_123.mp3,只要MEDIA_URL和MEDIA_ROOT配对正确,就能播放。
4. 用户体系与权限控制:登录注册、收藏逻辑、Admin 后台定制化
4.1 注册登录视图:用 Django 内置LoginView/LogoutView,但必须重写模板和 redirect
自己手写login()函数极易漏 CSRF、密码明文传输、session 失效等问题。直接复用官方视图,只定制外观和跳转:
# userprofile/urls.py from django.urls import path from django.contrib.auth import views as auth_views from . import views urlpatterns = [ path('login/', auth_views.LoginView.as_view( template_name='userprofile/login.html', redirect_authenticated_user=True # 已登录用户访问 login 页面,自动跳首页 ), name='login'), path('logout/', auth_views.LogoutView.as_view( next_page='index' # 登出后跳首页 ), name='logout'), path('register/', views.register, name='register'), ]# userprofile/views.py from django.shortcuts import render, redirect from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import login from .models import UserProfile def register(request): if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() # 自动创建 UserProfile(上面 signal 已保证,但显式调用更可控) UserProfile.objects.create(user=user) login(request, user) # 注册完直接登录 return redirect('index') else: form = UserCreationForm() return render(request, 'userprofile/register.html', {'form': form})4.2 收藏功能实现:playlist/models.py中的Favorite模型与视图联动逻辑
收藏不是简单“存个 ID”,必须考虑:同一首歌被同一用户收藏多次怎么办?取消收藏是否要删记录?答案是:用unique_together保证唯一性,用get_or_create()避免重复插入:
# playlist/models.py from django.db import models from django.contrib.auth.models import User from music.models import Song class Favorite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) song = models.ForeignKey(Song, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'song') # 关键:防重复收藏 verbose_name = "收藏" verbose_name_plural = "收藏列表"# playlist/views.py from django.shortcuts import get_object_or_404, redirect from django.contrib.auth.decorators import login_required from .models import Favorite from music.models import Song @login_required def toggle_favorite(request, song_id): song = get_object_or_404(Song, id=song_id) favorite, created = Favorite.objects.get_or_create( user=request.user, song=song ) if not created: # 已存在,说明是取消收藏 favorite.delete() return redirect('song_detail', song_id=song_id)<!-- templates/music/song_detail.html 中添加按钮 --> <a href="{% url 'toggle_favorite' song.id %}" class="btn btn-outline-primary"> {% if user.is_authenticated %} {% if song in user.favorite_set.all|map:'song' %} <i class="bi bi-heart-fill"></i> 已收藏 {% else %} <i class="bi bi-heart"></i> 收藏 {% endif %} {% else %} <i class="bi bi-heart"></i> 登录后收藏 {% endif %} </a>注意:
user.favorite_set.all是反向查询,Django 自动生成;map:'song'是自定义模板过滤器(需在userprofile/templatetags/user_extras.py中定义),用于判断当前用户是否收藏了这首歌。
4.3 Admin 后台定制:让导师能 3 秒上传一首歌,而不是写 SQL 插入
默认 Admin 只显示Song object (1),毫无实用性。必须重写admin.py:
# music/admin.py from django.contrib import admin from .models import Singer, Album, Song @admin.register(Singer) class SingerAdmin(admin.ModelAdmin): list_display = ['name', 'avatar_tag'] # avatar_tag 是自定义方法 search_fields = ['name'] @admin.register(Album) class AlbumAdmin(admin.ModelAdmin): list_display = ['title', 'cover_tag', 'release_date'] list_filter = ['release_date'] date_hierarchy = 'release_date' @admin.register(Song) class SongAdmin(admin.ModelAdmin): list_display = ['title', 'singers_list', 'album', 'duration', 'file_link', 'upload_time'] list_filter = ['upload_time', 'album'] search_fields = ['title', 'singers__name'] date_hierarchy = 'upload_time' filter_horizontal = ['singers'] # 多对多字段用横向选择框 def singers_list(self, obj): return ', '.join([s.name for s in obj.singers.all()]) singers_list.short_description = '演唱者' def file_link(self, obj): if obj.file: return f'<a href="{obj.file.url}" target="_blank">下载</a>' return '-' file_link.allow_tags = True file_link.short_description = '音频文件'效果:导师登录
/admin/后,点 “Songs” → “ADD SONG” → 上传 MP3、选歌手、填专辑、设时长,点保存即上线,全程图形界面,无需碰代码。
5. 避坑指南:毕业答辩前必查的 5 个致命问题与修复方案
5.1 现象:python manage.py runserver启动后,点击歌曲封面图片 404
原因:MEDIA_URL和MEDIA_ROOT路径不一致,或未在主urls.py中添加static(...)开发路由。
解决:
- 检查
settings.py中MEDIA_ROOT是否指向真实存在的media/文件夹; - 确认主
urls.py中if settings.DEBUG:分支已启用; - 在浏览器地址栏直接访问
http://127.0.0.1:8000/media/covers/test.jpg,看能否下载——若不行,说明路径错;若能下载但模板里不显示,检查{{ song.cover.url }}是否拼写正确(注意是.url,不是.path)。
5.2 现象:注册新用户后,Admin 后台看不到 Profile 编辑入口
原因:UserProfileInline未正确注册到CustomUserAdmin,或post_savesignal 未触发(常见于migrate后新增 signal)。
解决:
- 进入 Django shell 手动触发:
python manage.py shell→from django.contrib.auth.models import User→u = User.objects.get(username='test')→u.profile.nickname = 'testnick'→u.profile.save(); - 若仍无效,删除
db.sqlite3,重新migrate,再注册新用户(毕设阶段可接受)。
5.3 现象:上传 MP3 文件后,Admin 显示 “No file chosen”,但数据库里file字段有值
原因:FileField的upload_to路径含非法字符(如中文、空格),或MEDIA_ROOT权限不足(Linux/macOS 下常见)。
解决:
- 将
upload_to="songs/"改为upload_to="songs/"(确保全是 ASCII 字符); - Linux/macOS 执行
chmod 755 media/;Windows 忽略此步。
5.4 现象:搜索功能返回空结果,但数据库明明有匹配歌曲
原因:icontains查询对 SQLite 不区分大小写,但若字段含\n或空格,icontains会失效;或未在SongAdmin.search_fields中加入singers__name。
解决:
- 搜索视图中改用
Q对象组合查询:from django.db.models import Q songs = Song.objects.filter( Q(title__icontains=query) | Q(singers__name__icontains=query) | Q(album__title__icontains=query) ).distinct() - 确保
search_fields包含singers__name,否则 Admin 搜索也不生效。
5.5 现象:答辩现场演示时,点击收藏按钮报CSRF verification failed
原因:模板中未加载{% csrf_token %},或settings.py中MIDDLEWARE里CsrfViewMiddleware被误删。
解决:
- 检查所有含
<form>的模板(如login.html,register.html,song_detail.html中的收藏 form),确认<form>内第一行是{% csrf_token %}; - 检查
settings.py中MIDDLEWARE是否包含'django.middleware.csrf.CsrfViewMiddleware'(默认存在,勿删)。
6. 毕设交付技巧:数据库导出、静态资源压缩、答辩演示包打包实操
6.1 导出 SQLite 数据库:用sqlite3命令生成.sql文件,而非直接拷贝.sqlite3
导师要的是“可验证、可重演”的数据,不是二进制文件。.sqlite3文件在不同系统可能因字节序、版本不兼容打不开;而.sql是纯文本,任何 SQLite 工具都能导入:
# 导出全部表结构 + 数据(不含 sqlite_master 系统表) sqlite3 db.sqlite3 ".dump" | grep -v "^CREATE TABLE \"sqlite_sequence\"" > music_data.sql # 验证:新建空库,导入测试 sqlite3 test.db < music_data.sql sqlite3 test.db "SELECT COUNT(*) FROM music_song;" # 应输出歌曲总数提示:
grep -v "sqlite_sequence"是为了去掉自增 ID 重置语句,避免导入后 ID 错乱。
6.2 静态资源压缩:用django-compressor一键合并 CSS/JS,减小答辩演示包体积
毕设演示包要发给导师,static/里 Bootstrap、jQuery、自定义 CSS 加起来 2MB+,压缩后可压到 300KB:
pip install django-compressor# settings.py INSTALLED_APPS += ['compressor'] COMPRESS_ENABLED = True COMPRESS_CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.cssmin.CSSMinFilter'] COMPRESS_JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter'] STATICFILES_FINDERS += ['compressor.finders.CompressorFinder']<!-- base.html 中替换原来的 <link>/<script> --> {% load compress %} {% compress css %} <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/custom.css' %}"> {% endcompress %} {% compress js %} <script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script> {% endcompress %}执行python manage.py compress后,static/CACHE/下生成压缩文件,collectstatic时自动包含。
6.3 打包答辩演示包:requirements.txt+README.md+run_demo.sh三位一体
别只交一个src/文件夹。我交导师的包结构是:
music_site_demo/ ├── README.md # 含:环境要求、启动命令、功能清单、截图 ├── requirements.txt # pip freeze > requirements.txt 生成(删掉 -e git+... 行) ├── db.sqlite3 # 已预置 10 首测试歌曲 + 3 个用户 + 收藏数据 ├── media/ # 含所有封面、MP3(已压缩为 128kbps MP3,单首 < 5MB) ├── run_demo.sh # Linux/macOS 一键启动(含虚拟环境激活) └── run_demo.bat # Windows 批处理(同理)run_demo.sh内容:
#!/bin/bash echo "正在启动在线音乐网站演示..." python -m venv venv_demo source venv_demo/bin/activate pip install -r requirements.txt python manage.py migrate echo "服务已启动,请访问 http://127.0.0.1:8000" python manage.py runserver我的习惯:答辩前夜,用另一台干净电脑(无 Python 环境)解压
music_site_demo.zip,双击run_demo.sh,30 秒内看到首页——这才是真正的“稳”。最后提醒一句:答辩时别讲“我用了 WebSocket 实现实时推送”,讲“我用 Django 的
messages框架,在用户收藏成功后显示绿色提示条”,前者容易被问倒,后者是真实、可演示、导师能看懂的价值。希望帮到你。
本文还有配套的精品资源,点击获取