简介:基于Python与Django框架实现的视频点播网站系统,是一套面向毕业设计或课程实践的完整可运行项目,覆盖在线教育、视频分享等常见应用场景。项目前后端功能齐备:前端支持视频列表展示、播放详情、用户评论与个人中心,后端提供视频管理、评论审核、用户权限及反馈处理,便于理解Django MTV架构、ORM操作、内置认证与Admin后台的实战用法。压缩包共128个文件,以48个html页面模板、40个py逻辑文件、20个js脚本为主,辅以css样式表与png/jpg图片素材,整体仅3.23MB,结构清晰、可直接部署运行。目前已有244人学习浏览,适合需要快速上手Django全栈开发、完成毕设或巩固Web开发技能的读者。通过该项目可掌握从数据库设计到前后端联调的关键流程,并获得一套可用于扩展二次开发的代码基础。
1. 为什么毕业设计选视频点播系统而不是“管理系统”
三次毕业设计开题下来,有个感受很强烈:做后台管理系统的同学最后都在演示点按钮,做视频点播网站系统的同学却在讲用户上传、播放器取流、播放量累加、分类推荐,老师提问的方向完全不在一个层次。视频点播本身覆盖文件存储、媒体播放、权限控制、后台管理等多条链路,展示维度多,技术边界清楚,适合在有限时间内交付。这套基于 Python + Django 开发的视频点播网站系统,代码和数据库文件都是现成的,虚拟环境建好、依赖装上就能跑。适合两类人:一类是 Django 基础还没吃透,需要一套能运行的完整代码保底;另一类是已经会写简单视图,但想在存储设计、播放器接入和答辩扩展点上做文章的。下面先从项目骨架开始拆。
2. 项目骨架与数据模型:先把三张核心表讲透
2.1 MVT 分层与视频站点的模块划分
Django 做视频点播系统,最容易出现的问题不是代码写不出来,而是所有逻辑堆在 views.py 里,出问题以后不知道去哪个文件排查。这个项目的结构走的是标准 Django MVT 路线:根 urls.py 统一路由分发,把 admin、用户、视频三个入口拆开;video 这个 app 承载核心业务,models.py 定义数据结构,views.py 写列表、详情、搜索、上传处理的视图函数;用户系统直接复用 Django 自带的 auth 模块,注册、登录、改密码不重复造轮子;后台管理通过继承 admin.ModelAdmin 配置列表字段和搜索框;前端直接用服务端模板渲染页面,不额外引入复杂前端框架。
这样分层有个明显好处:功能出问题时定位面很小。比如播放页打开 404,先查 video/urls.py 和 video/views.py 里的视频详情视图,再看模板文件名是否拼错;后台传不了视频,查 admin.py 里 ModelAdmin 注册项和字段权限就行。
为什么这个场景选 Django 而不是 Flask?核心原因是 Django 自带 Admin 管理和数据模型迁移机制。视频点播这种“后台要管理、前台要展示”的双端项目,Admin 直接省掉了一整套后台管理页的重复开发量。Flask 要做出同样的后台效果,需要额外接入 Flask-Admin 和 SQLAlchemy,自己补的代码多,依赖链长了跑不起来时排查也费劲。另外 Django ORM 在写跨表查询时更顺,比如按分类名筛视频,一条 category__name 就能拿到分类的中文名,不用手动写 join。
2.2 数据模型设计:Video、Category、User 三张表的关键字段
数据表是整个系统最核心的地基。这套下载包里虽然已经带了初始化好的数据库文件,但只有读懂表结构,后面加功能、改逻辑才动得了。下面这段模型代码就是这套系统最常见的表结构:
# video/models.py:视频点播系统的三张核心表 from django.db import models class Category(models.Model): name = models.CharField('分类名称', max_length=50) sort = models.IntegerField('排序权重', default=0) class Meta: verbose_name = '视频分类' verbose_name_plural = verbose_name ordering = ['sort', 'id'] def __str__(self): return self.name class Video(models.Model): title = models.CharField('视频标题', max_length=200) category = models.ForeignKey( Category, on_delete=models.PROTECT, verbose_name='所属分类' ) cover = models.ImageField('封面图', upload_to='cover/%Y/%m/') video_file = models.FileField('视频文件', upload_to='videos/%Y/%m/') duration = models.CharField('视频时长', max_length=20, blank=True) desc = models.TextField('视频简介', blank=True) view_count = models.IntegerField('播放量', default=0) is_published = models.BooleanField('是否上架', default=True) created_at = models.DateTimeField('发布时间', auto_now_add=True) class Meta: verbose_name = '视频' verbose_name_plural = verbose_name ordering = ['-view_count'] def __str__(self): return self.title这里有两个字段细节建议特别注意。
第一个是 category 外键的 on_delete 参数。很多人习惯写 models.CASCADE,意思是分类被删掉,这个分类下面所有视频全部跟着删除。毕设阶段如果误点了删除分类,视频数据整批消失,找都找不回来。用 PROTECT 的话,Django 会在删分类时主动拦截并报错,逼你先手动把该分类下的视频转移或下架,这个约束放着就是一道保险。
第二个是 cover 和 video_file 的 upload_to 参数。这里没有简单写 upload_to='cover',而是带上了 %Y/%m 子目录,Django 会按上传年份和月份自动创建目录。这样处理有两个直接收益:一是文件名即使重复也不会互相覆盖,二是后台和播放页拿到的是相对路径,迁移项目时目录结构清晰。
对应的后台注册代码一般长这样,逻辑简单但必不可少:
# video/admin.py:把视频和分类注册进 Django 后台 from django.contrib import admin from .models import Video, Category @admin.register(Video) class VideoAdmin(admin.ModelAdmin): list_display = ('id', 'title', 'category', 'view_count', 'is_published') list_filter = ('category', 'is_published') search_fields = ('title', 'desc') @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'sort')list_display 决定后台列表展示哪些列;list_filter 在右侧生成分类和上下架的筛选项;search_fields 开启标题和简介的关键词搜索。这三个参数是管理后台最常用的配置,答辩时如果老师追问“后台是怎么实现的”,直接从这个注册类入手讲就行。
2.3 字段的坑:ImageField 和 FileField 的落盘规则
上传文件有两条路。第一种是直接把文件对象存到模型字段里,Django 自动按 upload_to 的规则拼接目录;第二种是先用 FileSystemStorage 手动保存,再把返回路径写到数据库。这套项目的视频上传走的是第一种,也是最推荐的方式。用 FileSystemStorage 手动处理还容易出 bug,最常见的是保存时没处理文件重名,同名学生视频传两次,后一个把前一个覆盖掉。
另一个容易踩的是 MEDIA_ROOT 和 STATIC_ROOT 混为一谈。MEDIA_ROOT 是用户上传的视频、封面存放的物理目录,STATIC_ROOT 是项目自带的 CSS、JS、图片在部署时收集到一起的目录。两个目录必须分开配置,否则 collectstatic 时会把用户上传的视频也一起复制走,实际部署时会拖慢构建流程。
3. 从环境到界面:一倍速跑通本地全流程
3.1 Python 环境与依赖安装:虚拟环境无论如何都要建
打开下载包,第一件事不是直接双击 manage.py,而是先建一套干净的 Python 虚拟环境。很多拿到代码就说跑不起来的人,十有八九是在全局 Python 环境里直接装依赖,某次升级把 Django 版本改掉了,项目就跟着坏了。
# 在项目根目录创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows 使用下面的命令) venv\Scripts\activate # macOS / Linux 使用下面的命令 source venv/bin/activate激活之后终端提示符前面会出现 (venv) 字样,之后的 pip 和 python 命令都会落在虚拟环境里,不会污染全局环境。接着安装依赖:
# 安装项目运行所需的依赖 pip install -r requirements.txtrequirements.txt 里一般会包含 django、pillow 这两个核心依赖。pillow 是 ImageField 处理封面图的底层库,漏装的话执行数据库迁移时会直接报 ModuleNotFoundError。如果下载包里没有 requirements.txt,可以用 pip install django pillow 手动补上,版本不用太新,Django 3.2 到 4.2 之间都可以,太新的版本某些第三方库还没跟上。
安装完成后验证环境是否正常,用 python -c "import django; print(django.get_version())",能输出版本号说明 Django 已经就位。
3.2 数据库迁移、初始数据导入和管理员账号
这套系统包了一个现成的 db.sqlite3 文件。能不能直接用它?分两种情况:如果你不对代码做任何改动,直接跳过迁移跑服务器是可以的;但一旦你改了模型字段、增删了表,就要重新处理迁移。保守做法是先把原始数据库备份一份,再按下面的流程从头迁移一次:
# 在虚拟环境激活状态下执行 python manage.py makemigrations video python manage.py migratemakemigrations 会根据 models.py 的变化生成一个新的迁移文件,migrate 会把迁移真正应用到数据库。第一次跑如果提示“No changes detected”,说明模型没有变化,数据库已经是最新的,不用紧张。
如果系统带了初始分类和演示视频数据,通常会有一个 data.json 文件,迁移后执行:
# 把初始分类与视频数据导入数据库 python manage.py loaddata data.jsonloaddata 会按 JSON 里记录的模型和字段逐一回填。导入报错最常见的原因是数据库里已经有重复的主键 id,可以先清空 video_video 和 video_category 两张表再导入。接着创建后台管理员:
# 交互式创建管理员账号 python manage.py createsuperuser按提示输入用户名、邮箱、密码。这里特别提醒:密码不要太复杂,答辩现场输入时容易出错,我一般设成 Admin@123456 这种兼顾大小写和数字还能记住的密码,演示完再改。
3.3 启动开发服务器,访问首页和管理后台
数据库准备好以后,在项目根目录执行:
python manage.py runserver看到“Starting development server at http://127.0.0.1:8000/”就是启动成功。浏览器打开首页,应该能看到视频列表和分类导航;打开 /admin 路径,用刚创建的管理员账号登录,能看到视频、分类的管理入口。
如果首页能开但所有图片和视频都不显示,这是 media 路由没有配置。开发环境下需要在根 urls.py 里加上一行:
# urls.py:开发环境下的媒体文件访问路由 from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... 项目已有路由 ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这行代码只应该在 DEBUG=True 时生效,否则公网上所有人都能访问你的上传目录,是个安全隐患。部署后要通过 Django 部署文档里的正规静态文件服务方案来替代。
4. 核心功能实现:上传、播放、搜索与用户逻辑
4.1 视频上传:格式校验、大小限制与落盘处理
视频上传是这套系统里最值得讲的模块。很多毕设项目只做后台表单上传,不管文件格式,结果用户传一个 .txt 改名成 .mp4 也能入库。正规做法是在视图里做校验,至少拦掉扩展名明显不对的文件:
# video/views.py:视频发布处理视图 import os from django.shortcuts import render, redirect from django.contrib import messages from .models import Video, Category ALLOWED_EXT = ['.mp4', '.avi', '.mkv', '.mov'] MAX_SIZE_MB = 500 def publish_video(request): if request.method == 'POST': title = request.POST.get('title') category_id = request.POST.get('category_id') desc = request.POST.get('desc', '') cover = request.FILES.get('cover') video_file = request.FILES.get('video_file') if not title or not video_file: messages.error(request, '标题和视频文件为必填项') return render(request, 'publish.html', { 'categories': Category.objects.all() }) ext = os.path.splitext(video_file.name)[1].lower() if ext not in ALLOWED_EXT: messages.error(request, '仅支持 mp4 / avi / mkv / mov 格式') return render(request, 'publish.html', { 'categories': Category.objects.all() }) if video_file.size > MAX_SIZE_MB * 1024 * 1024: messages.error(request, '文件大小不能超过 500MB') return render(request, 'publish.html', { 'categories': Category.objects.all() }) Video.objects.create( title=title, category_id=category_id, cover=cover, video_file=video_file, desc=desc, ) return redirect('video_list') return render(request, 'publish.html', { 'categories': Category.objects.all() })这段逻辑里要注意三点。
第一,ext = os.path.splitext(video_file.name)[1].lower() 这段是在切出文件名的后缀并转成小写,避免用户传 .MP4 时被误判成不支持的格式。第二,video_file.size 是 Django 上传文件对象自带的属性,单位是字节,所以 MAX_SIZE_MB 要乘以 1024 再乘以 1024,这里漏一处就相当于没限制。第三,Video.objects.create 传 category_id 而不是 category 对象,能少一次查询,Django 在 create 时会自动处理外键的 id 赋值。
真实项目里格式校验不能只看扩展名。扩展名是 mp4 的文件,内部也可能是 H.265 编码,浏览器播放不了。这个后面避坑章节会展开讲,这里先记住一个结论:扩展名校验只是第一道门,真正的兼容性要靠转码解决。
4.2 播放页设计:HTML5 video 标签与模板变量传递
播放页是整套系统里用户唯一能直观感受到的模块。这里的核心是把 Video 对象的文件地址正确交给前端播放器。最简单的方案是直接用 HTML5 原生 video 标签,不引入第三方播放器库,减少依赖:
<!-- video/templates/video/detail.html --> {% extends 'base.html' %} {% block content %} <div class="player-wrapper"> <video controls preload="metadata" poster="{{ video.cover.url }}" class="video-player"> <source src="{{ video.video_file.url }}" type="video/mp4"> 抱歉,您的浏览器不支持 HTML5 视频播放。 </video> </div> <div class="video-meta"> <h1>{{ video.title }}</h1> <p>分类:{{ video.category.name }} | 播放量:{{ video.view_count }}</p> <p>{{ video.desc }}</p> </div> {% endblock %}模板里的 {{ video.cover.url }} 和 {{ video.video_file.url }} 是 FileField 和 ImageField 自带的 url 属性,Django 会把 MEDIA_URL 和文件路径拼接成完整可访问的地址。这里最容易犯的错误是直接写 {{ video.video_file.path }},path 返回的是服务器物理绝对路径,浏览器拿到这个路径是访问不了的,必须用 url。
preload="metadata" 这个属性值得说一下。它告诉浏览器先只加载视频的元数据,比如时长和封面信息,用户真正点击播放时才拉取视频内容。如果一个页面加载大量视频数据,会直接把本地带宽打满,加上这个属性可以明显加快列表页和详情页的响应速度。
前端如果想做更丰富的控制栏,比如倍速、画中画、进度缩略图,可以在此基础上叠加 video.js 之类的播放器库,但毕设阶段原生 video 已经能覆盖绝大多数演示需求。
4.3 播放量统计和搜索筛选:QuerySet 的三个常用写法
播放量统计看起来简单,但实现位置不同效果也完全不一样。有些版本把播放量递增写在 JavaScript 里,页面一加载就发请求,用户只是快速滑过去,播放量照样加一。更合理的做法是在详情视图里递增,保证用户真正打开了播放页才算一次:
# video/views.py:播放量递增与相似推荐逻辑 from django.shortcuts import render, get_object_or_404 from .models import Video def video_detail(request, video_id): video = get_object_or_404(Video, pk=video_id, is_published=True) video.view_count += 1 video.save(update_fields=['view_count']) similar = Video.objects.filter( category=video.category, is_published=True ).exclude(pk=video.pk)[:6] return render(request, 'video/detail.html', { 'video': video, 'similar': similar, })这里的 save(update_fields=['view_count']) 是个被忽略的小优化。如果不指定 update_fields,Django 会把整条记录的所有字段重新写一遍,包括可能很大的 desc 文本字段;指定后只更新 view_count 一个字段,SQL 语句更短,并发更新时的锁粒度也更小。
搜索和分类筛选功能通常会放在同一个视图里,用 request.GET 接收参数:
# video/views.py:搜索与分类筛选逻辑 from django.shortcuts import render from .models import Video, Category def search(request): keyword = request.GET.get('q', '').strip() category_id = request.GET.get('category_id', '') videos = Video.objects.filter(is_published=True) if keyword: videos = videos.filter(title__icontains=keyword) if category_id: videos = videos.filter(category_id=category_id) videos = videos.order_by('-created_at')[:60] return render(request, 'video/search.html', { 'videos': videos, 'keyword': keyword, 'categories': Category.objects.all(), })title__icontains 是 Django ORM 里的“包含且不区分大小写”查询。它有两个好处:一是能匹配标题中任意位置的关键词,二是默认忽略英文大小写,搜索“django”和“Django”结果一致。keyword 先做 .strip() 处理,是为了去掉用户输入的首尾空格,避免搜索时因为多打了一个空格而查不到结果。
一个反向的坑:如果 keyword 为空,videos.filter(title__icontains='') 会把所有视频都捞出来。所以代码里用 if keyword 包了一层,没有关键词时只按分类过滤或直接返回最新列表。这属于最常见的权限集合顺序:先拿到基础 QuerySet,再按条件逐层追加筛选。
5. 避坑记录:跑通这个 Django 视频站常见的 5 个坎
5.1 首页能打开,但视频和封面全部 404
现象:runserver 启动成功,首页 HTML 正常,但所有封面图和小视频链接点击后返回 404,控制台能看到大量 /media/ 开头的请求失败。
原因:settings.py 里没有配置 MEDIA_URL 和 MEDIA_ROOT,或者配置了但没有在 urls.py 中增加媒体访问路由。开发服务器默认只处理 Django 应用本身的路由,不会自动映射 MEDIA_ROOT 目录。
解决:在 settings.py 里补上 MEDIA_URL = '/media/' 和 MEDIA_ROOT = os.path.join(BASE_DIR, 'media'),再按前面 3.3 节的方法,在根 urls.py 中用 static() 函数把路由挂进去。改完重启服务器,不要只刷新页面,Django 修改路由后必须重新加载。
5.2 迁移数据库时报 duplicate table 或已存在表
现象:用项目自带的 db.sqlite3 文件直接跑 python manage.py migrate,提示某些表已经存在,或者反过来,迁移已经记录为已执行,但访问页面时提示 no such table。
原因:项目自带的数据库文件和当前代码模型不一致。别人打包时可能用的是旧版本模型,迁移历史记录还在,但实际表结构和当前代码对不上。sqlite3 文件的表结构是写死在二进制文件里的,Django 只认迁移记录,不会自动帮你重建缺失表。
解决:把原始 db.sqlite3 备份到项目外,删除原文件,重新执行 makemigrations、migrate、createsuperuser,再用 loaddata 导入数据。从那以后我每次拿到新项目的数据库文件,第一件事就是把原始文件备份好再删掉重来,绝不直接拿别人数据库凑合跑。
5.3 页面没有任何样式,像纯文本浏览器
现象:视频列表和详情页能打开,也能看到文字和图片,但所有 CSS 样式完全没生效,页面排版全部错乱。
原因:开发环境下没有配置静态文件路由,或 STATICFILES_DIRS 指向的目录和实际模板里加载 css/js 的路径对不上。Django 默认只按 app 内的 static/ 目录查找,如果项目把静态文件放在根目录的 static/ 下,必须显式配置 STATICFILES_DIRS。
解决:在 settings.py 中指定 STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')],并确认模板里 {% load static %} 标签在最顶部且 {% static 'xxx.css' %} 里的路径和文件实际位置一致。改完重启 runserver 再看。
5.4 上传的 mp4 视频在播放器里黑屏
现象:后台能成功上传视频,封面正常,点击播放后只有进度条,画面一直黑屏,控制台也没有明显报错。
原因:视频编码不兼容。很多手机或剪辑软件输出的 mp4 是 H.265/HEVC 编码,而浏览器对 HEVC 的支持非常有限,大多只认 H.264 编码的 mp4。扩展名是 .mp4 只能保证容器格式,不能保证内部编码。
解决:用 FFmpeg 把视频转码成 H.264 加 AAC 编码:
# 使用 FFmpeg 把不兼容编码转成 H.264 + AAC ffmpeg -i input.mp4 -vcodec h264 -acodec aac output.mp4转码后重新上传再播放。如果项目内已经有大量视频,可以在后台做一层预处理,上传时用 subprocess 调用 FFmpeg 自动转码,这个功能答辩时提出来也比较加分。
5.5 中文乱码或显示成正方形
现象:后台分类名称、视频标题只要是中文的地方全部乱码,或者页面显示很多问号和小方块。
原因:模板文件保存时不是 UTF-8 编码,或者 HTML 中缺少 charset 声明。很多编辑器默认保存成 GBK,浏览器按 UTF-8 解析时中文就变成乱码。Django 默认输出编码是 UTF-8,但模板文件本身的编码不受 Django 控制。
解决:用编辑器把出问题的模板文件另存为 UTF-8 without BOM,并确保每个模板的 head 里都有 。另外 name 字段在 settings.py 中也可以补一行 DEFAULT_CHARSET = 'utf-8',双保险。
6. 答辩前必做的一件事:把系统从“能跑”变成“能演示”
6.1 五分钟演示脚本,先定好流程再上台
答辩现场最容易出的状况不是代码不会写,而是演示顺序混乱。我总结了一套比较顺的流程:先打开首页展示视频列表和分类导航,让老师看到整体结构;点开一个视频播放,展示播放器、封面、简介和播放量;切到搜索框,输入一个关键词验证筛选和搜索;再打开 /admin 登录,进入后台展示视频管理列表;最后新建一条测试视频,回到前台确认它出现在列表里。这套流程一共五分钟,每个动作对应一个技术点,老师问什么都能接得上。
6.2 用 Django TestCase 做一次回归验证
答辩前我在本地跑一套冒烟测试,确保修改代码后核心页面没有变坏。Django 自带测试客户端,不需要额外装工具:
# video/tests.py:验证播放详情页和搜索页可访问 from django.test import TestCase from django.urls import reverse class VideoPageTests(TestCase): def test_detail_page_ok(self): resp = self.client.get(reverse('video_detail', args=[1])) self.assertEqual(resp.status_code, 200) def test_search_page_ok(self): resp = self.client.get(reverse('search')) self.assertEqual(resp.status_code, 200)执行 python manage.py test 之后,两个用例全绿再上台。这个小环节能帮你拦掉很多低级错误,比如路由写错、模板加载失败这类一改代码就翻车的问题。测试用例不用写太多,覆盖打开首页、视频详情、搜索页、登录页四条主链路就够了。
6.3 数据准备:演示数据宁多勿乱,分类下放上 3 到 5 条视频
视频列表空荡荡的页面在答辩时会让项目效果大打折扣。上传前先规划好分类:比如按“热门电影、纪录片、编程公开课”三个方向,每个分类下放 3 到 5 条视频,封面图统一尺寸,让列表页视觉整齐一些。时长最好是几十秒到两分钟的短视频,完整长视频加载慢,演示时等缓冲反而尴尬。
答辩前一天我会强制自己把整条链路从头走一遍:建虚拟环境、装依赖、清数据库、重新迁移、导入数据、启动服务器、测试用例、按脚本演示。从那以后我每次拿到新的毕设代码,都先按这条路完整跑一遍再动任何代码,省掉了无数次临时翻车。希望这篇文章能帮到你。
本文还有配套的精品资源,点击获取