Django视频点播网站实战:从上传到播放的完整链路与避坑指南
简介:这是一套面向高校计算机及相关专业学生的视频点播网站系统完整方案,采用Python与Django框架开发,适用于毕业设计、课程设计及学期综合实践等学术场景,帮助学习者理解Web应用开发流程与视频内容管理系统的实现原理。资源包共156个文件,压缩后约3.89MB,以48个html页面模板、40个py后端逻辑文件、20个js脚本、5个css样式表及6张png图片为主,另含数据库备份、配置说明与项目文档,结构完整、模块清晰。已有82人学习下载。内容涵盖环境搭建、数据初始化、功能测试与基础定制等关键环节,读者可据此掌握Django后端架构、视频点播业务逻辑与前后端交互方式,并将理论知识有效转化为工程实践,适合作为规范化的学术项目参考。
1. 从零搭一个视频点播网站:Django 到底能扛住哪些活
很多人做毕业设计时,第一反应是"视频网站不就是上传个文件、点开播放吗",真动手才发现坑比想象中多:视频文件放哪、怎么边下边播、播放量怎么统计、后台怎么管理、部署到服务器为什么一刷新就 404。基于 Python 与 Django 的视频点播网站系统,本质就是用 Django 这套 MTV 框架,把视频的上传、转码、存储、播放、检索、后台管理串成一条完整链路。它适合两类人:一类是计算机毕业设计需要一套能跑通、能答辩、能讲清楚架构的项目;另一类是刚学完 Django 基础、想找一个比博客系统更有含金量的实战练手。这套系统真正值钱的地方不是页面多好看,而是你能讲清楚视频流怎么走、数据库怎么设计、权限怎么控。下面我按实际落地的顺序,把选型、建模、播放、后台、部署和踩坑一条条拆开讲。
2. 技术选型与项目骨架:为什么是 Django 而不是 Flask
2.1 视频点播场景下 Django 的真实优势
选 Django 做视频点播,核心不是因为它"火",而是它自带的东西刚好覆盖了这个场景的刚需。视频网站天然需要用户体系、后台管理、ORM 和文件处理,这四样 Django 全都内置。用户注册登录、权限分组用 django.contrib.auth 直接搞定;视频、分类、评论这些表的增删改查用自带 admin 后台几分钟就能配出来,答辩时演示后台管理特别省事;ORM 让视频和分类、用户之间的外键关系写起来很直观; FileField 和 ImageField 处理封面图和视频文件上传也是开箱即用。
对比 Flask,Flask 更轻,但用户系统、后台、表单验证都要自己拼轮子,毕业设计周期内很容易在"造轮子"上耗光时间。Django 的"重"在这个场景里反而是优势。常见做法是:用 Django 做主体,视频转码这种 CPU 密集的活交给独立脚本或 Celery 异步处理,不要塞进请求响应周期里。
需要提前想清楚的一点是:Django 默认的开发服务器 runserver 只能用来调试,它单线程处理请求,视频这种大文件传输会直接把并发打满。真正上线必须换成 Gunicorn 或 uWSGI 加 Nginx,这个后面部署章节会细讲。
2.2 用命令把项目骨架搭起来
先确认 Python 环境。建议用 3.10 或 3.11,太新的版本部分第三方库还没跟上。虚拟环境是必须的,别把依赖装到全局,否则换台机器就翻车。
# 创建项目目录并进入
mkdir video_site && cd video_site
# 创建虚拟环境(Windows 用 python -m venv venv)
python3 -m venv venv
source venv/bin/activate
# 安装核心依赖
pip install django==4.2 pillow
# 创建 Django 项目和 app
django-admin startproject config .
python manage.py startapp videos
这里 config . 表示把项目配置目录命名为 config 并放在当前目录,避免出现 video_site/video_site/ 这种套娃结构。 videos 这个 app 专门管视频相关的模型和视图。装 pillow 是因为封面图字段依赖它做图片处理。
创建完要在 config/settings.py 里注册 app,并配置好数据库、静态文件和媒体文件路径:
# config/settings.py 关键片段
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'videos', # 注册视频 app
]
# 媒体文件(用户上传的视频和封面)存放路径
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# 静态文件
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# 数据库先用 sqlite 跑通,后期换 mysql
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
MEDIA_ROOT 是用户上传文件的物理存储位置, STATIC_ROOT 是 collectstatic 收集静态文件的目标目录,这两个概念新手最容易混。记住一句话:静态文件是开发者写的 CSS/JS,媒体文件是用户上传的内容。开发阶段还要在 config/urls.py 里加上媒体文件的路由,否则封面图显示不出来:
# config/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('videos.urls')),
]
# 仅开发环境:让 Django 直接服务媒体文件
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这段 static() 只在 DEBUG=True 时生效,上线后必须由 Nginx 接管媒体文件,否则性能和安全性都有问题。很多人本地跑得好好的,一部署封面全裂,就是漏了这一步。
3. 数据建模与视频上传:把表设计对,后面少改十次
3.1 视频、分类、评论三张核心表怎么设计
视频点播系统的数据模型不复杂,但字段设计直接影响后面查询和播放。核心是三张表:分类、视频、评论。分类和视频是一对多,视频和评论是一对多。视频表里最关键的字段是文件路径、封面、时长、播放量和状态。
# videos/models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField('分类名', max_length=50, unique=True)
order = models.IntegerField('排序', default=0)
class Meta:
ordering = ['order']
verbose_name = '分类'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Video(models.Model):
STATUS_CHOICES = (
('draft', '待审核'),
('published', '已发布'),
('blocked', '已下架'),
)
title = models.CharField('标题', max_length=200)
desc = models.TextField('简介', blank=True)
cover = models.ImageField('封面', upload_to='covers/%Y/%m/')
file = models.FileField('视频文件', upload_to='videos/%Y/%m/')
category = models.ForeignKey(Category, on_delete=models.SET_NULL,
null=True, verbose_name='分类')
uploader = models.ForeignKey(User, on_delete=models.CASCADE,
verbose_name='上传者')
duration = models.IntegerField('时长(秒)', default=0)
views = models.PositiveIntegerField('播放量', default=0)
status = models.CharField('状态', max_length=10,
choices=STATUS_CHOICES, default='draft')
created = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
ordering = ['-created']
indexes = [models.Index(fields=['-created'])]
def __str__(self):
return self.title
class Comment(models.Model):
video = models.ForeignKey(Video, on_delete=models.CASCADE,
related_name='comments')
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField('内容', max_length=500)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created']
几个参数要重点说。 upload_to='videos/%Y/%m/' 让文件按年月分目录存放,避免所有视频堆在一个文件夹里,文件一多 ls 都能卡死。 on_delete=models.SET_NULL 表示分类被删时视频保留但分类置空,比级联删除安全。 views 用 PositiveIntegerField 而不是 IntegerField ,从数据库层面防止出现负数播放量。 indexes 给创建时间加索引,因为首页列表默认按时间倒序,数据量上万后没索引查询会明显变慢。
改完模型执行迁移:
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser # 创建后台管理员
3.2 上传表单与文件校验
视频上传不能只靠一个 FileField ,必须做类型和大小校验,否则用户传个 exe 或者几个 G 的文件进来,服务器直接崩。用 Django 的 Form 做校验最规范:
# videos/forms.py
from django import forms
from .models import Video
class VideoUploadForm(forms.ModelForm):
class Meta:
model = Video
fields = ['title', 'desc', 'cover', 'file', 'category']
def clean_file(self):
f = self.cleaned_data['file']
# 限制扩展名
if not f.name.lower().endswith(('.mp4', '.webm', '.ogg')):
raise forms.ValidationError('只支持 mp4/webm/ogg 格式')
# 限制大小 500MB
if f.size > 500 * 1024 * 1024:
raise forms.ValidationError('文件不能超过 500MB')
return f
def clean_cover(self):
img = self.cleaned_data['cover']
if img.size > 2 * 1024 * 1024:
raise forms.ValidationError('封面不能超过 2MB')
return img
clean_file 是 Django 表单的字段级校验钩子,方法名必须是 clean_ 加字段名。这里做了两层校验:扩展名白名单和大小上限。注意扩展名校验只是第一道防线,真正严谨的做法还要读文件头判断真实类型,但毕业设计层面扩展名加大小基本够用。500MB 这个阈值要结合服务器磁盘和带宽调整,如果只是本地演示,可以调到 100MB 以内加快上传测试。
视图里接收表单并保存:
# videos/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import VideoUploadForm
@login_required
def upload(request):
if request.method == 'POST':
form = VideoUploadForm(request.POST, request.FILES)
if form.is_valid():
video = form.save(commit=False)
video.uploader = request.user
video.status = 'draft' # 上传后待审核
video.save()
return redirect('video_list')
else:
form = VideoUploadForm()
return render(request, 'videos/upload.html', {'form': form})
commit=False 先不落库,是为了在保存前补上 uploader 和 status 这两个表单里没有的字段。 request.FILES 必须传,否则文件字段拿不到数据,这是新手最常见的漏写。 @login_required 保证只有登录用户能上传。
4. 播放、检索与后台:让视频真正能看能搜能管
4.1 视频播放页与播放量统计的正确姿势
播放页的核心是 <video> 标签指向媒体文件 URL,同时要处理播放量统计。播放量不能简单地在页面加载时加一,因为用户刷新一次就多一次,数据会虚高。常见做法是前端在视频真正开始播放时发一个异步请求。
<!-- templates/videos/detail.html -->
<video id="player" controls width="100%"
poster="{{ video.cover.url }}"
src="{{ video.file.url }}">
你的浏览器不支持 video 标签
</video>
<script>
const player = document.getElementById('player');
let counted = false;
player.addEventListener('play', function () {
if (counted) return; // 同一次访问只计一次
counted = true;
fetch("{% url 'add_view' video.id %}", {method: 'POST',
headers: {'X-CSRFToken': '{{ csrf_token }}'}});
});
</script>
poster 是视频未播放时显示的封面, controls 开启浏览器原生控制条。 counted 标志位保证同一次页面访问只上报一次,避免用户暂停再播放重复计数。 fetch 里带 X-CSRFToken 是 Django 的 CSRF 防护要求,漏了会返回 403。
对应的视图用 F 表达式原子更新,避免并发下的竞态:
# videos/views.py
from django.db.models import F
from django.http import JsonResponse
from django.views.decorators.http import require_POST
@require_POST
def add_view(request, pk):
Video.objects.filter(pk=pk).update(views=F('views') + 1)
return JsonResponse({'ok': True})
用 F('views') + 1 而不是先查出来加一再存回去,是因为前者在数据库层面完成自增,多个请求同时来也不会丢更新。 @require_POST 限制只接受 POST,防止被人用 GET 刷量。
4.2 视频检索与分页
检索要支持按标题模糊匹配和按分类筛选,同时分页。用 Django 的 Q 对象组合查询条件:
# videos/views.py
from django.core.paginator import Paginator
from django.db.models import Q
def video_list(request):
keyword = request.GET.get('kw', '').strip()
cat_id = request.GET.get('cat', '')
qs = Video.objects.filter(status='published')
if keyword:
qs = qs.filter(Q(title__icontains=keyword) |
Q(desc__icontains=keyword))
if cat_id:
qs = qs.filter(category_id=cat_id)
paginator = Paginator(qs, 12) # 每页 12 条
page = paginator.get_page(request.GET.get('page'))
return render(request, 'videos/list.html', {'page': page})
icontains 是不区分大小写的包含查询,中文场景下等价于模糊匹配。 Q 对象用 | 组合表示"或",标题或简介命中都算。 Paginator 每页 12 条适合三列布局。 get_page 比 page 更健壮,传入非法页码时自动回退到第一页而不是抛异常。
4.3 后台管理配置
Django admin 是毕业设计答辩的加分项,配置好能直接演示增删改查和审核流程:
# videos/admin.py
from django.contrib import admin
from .models import Category, Video, Comment
@admin.register(Video)
class VideoAdmin(admin.ModelAdmin):
list_display = ('title', 'category', 'uploader', 'views',
'status', 'created')
list_filter = ('status', 'category')
search_fields = ('title', 'desc')
list_editable = ('status',) # 列表页直接改状态
date_hierarchy = 'created'
admin.site.register(Category)
admin.site.register(Comment)
list_display 控制列表页显示哪些列, list_filter 右侧筛选器, search_fields 顶部搜索框, list_editable 让审核状态能在列表页直接改, date_hierarchy 加时间钻取。审核员在后台把 draft 改成 published ,前台就能看到了,这条流程答辩时讲出来很清晰。
5. 避坑与排查:这些坑我基本都踩过一遍
5.1 上传大视频报 413 或请求中断
现象:上传超过几十 MB 的视频,浏览器报 413 或者请求直接断掉。原因:Nginx 默认 client_max_body_size 只有 1MB,Django 也有 DATA_UPLOAD_MAX_MEMORY_SIZE 限制。解决:Nginx 配置里加 client_max_body_size 500m; ,Django settings 里设置 DATA_UPLOAD_MAX_MEMORY_SIZE = 524288000 和 FILE_UPLOAD_MAX_MEMORY_SIZE 。注意这两个值要和表单校验的大小上限保持一致,否则前端过了后端拦。
5.2 视频能上传但播放页 404
现象:文件确实传上去了, media/videos/ 目录里能看到,但播放页视频加载失败。原因:开发环境没配媒体文件路由,或者生产环境 Nginx 没配 media 的 location。解决:开发时确认 urls.py 里有 static(settings.MEDIA_URL, ...) ;生产环境在 Nginx 里加 location /media/ { alias /path/to/media/; } 。血泪经验是路径结尾的斜杠, alias 和 root 对斜杠的处理不一样,少一个斜杠就 404。
5.3 播放量并发下丢更新
现象:压测时播放量比实际请求数少。原因:用了"查询-加一-保存"的写法,并发时多个请求读到同一个旧值。解决:改用 F('views') + 1 让数据库原子自增。这个坑在单机测试时基本发现不了,一上并发就暴露。
5.4 静态文件在 DEBUG=False 后全部失效
现象:本地 DEBUG=True 一切正常,改成 False 后 CSS、JS 全 404。原因:Django 在 DEBUG=False 时不再自动服务静态文件。解决:跑 python manage.py collectstatic 把静态文件收集到 STATIC_ROOT ,然后由 Nginx 配置 location /static/ 指向该目录。这是部署阶段最经典的翻车点。
5.5 视频文件删除后磁盘没释放
现象:后台删了视频记录,但 media 目录里文件还在,磁盘越用越满。原因:Django 删除模型实例不会自动删物理文件。解决:用 post_delete 信号监听删除事件,手动删文件:
# videos/signals.py
from django.db.models.signals import post_delete
from django.dispatch import receiver
from .models import Video
@receiver(post_delete, sender=Video)
def delete_files(sender, instance, **kwargs):
if instance.file:
instance.file.delete(save=False)
if instance.cover:
instance.cover.delete(save=False)
记得在 app 的 apps.py 的 ready() 里导入这个 signals 模块,否则信号不生效。
6. 部署与进阶:从能跑到扛得住
6.1 用 Gunicorn 加 Nginx 把服务跑起来
开发服务器不能上生产,标准组合是 Gunicorn 跑 Django,Nginx 做反向代理和静态/媒体文件服务。先装 Gunicorn:
pip install gunicorn
# 在项目根目录启动,config.wsgi 是 wsgi 模块路径
gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000
-w 4 是 4 个 worker 进程,一般设为 CPU 核数乘 2 加 1。 -b 绑定本地回环,由 Nginx 转发过来,不要直接暴露到公网。Nginx 配置核心片段:
server {
listen 80;
server_name your_domain;
client_max_body_size 500m;
location /static/ {
alias /path/to/staticfiles/;
}
location /media/ {
alias /path/to/media/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
proxy_set_header 这两行是为了让 Django 拿到真实客户端 IP 和域名,否则后台日志里全是 127.0.0.1。静态和媒体文件交给 Nginx 直接返回,不经过 Django,性能差好几倍。
6.2 视频转码与 HLS 切片
原始 mp4 直接播放有个问题:用户要等整个文件头部加载完才能拖动进度条,大文件体验差。进阶做法是用 ffmpeg 转成 HLS 切片,边下边播。核心命令:
# 把 mp4 转成 HLS,切片 10 秒一个
ffmpeg -i input.mp4 -codec: copy -start_number 0 \
-hls_time 10 -hls_list_size 0 \
-f hls output.m3u8
-codec: copy 表示不重新编码直接封装,速度快但要求源文件编码兼容; -hls_time 10 是每个切片 10 秒; -hls_list_size 0 表示 m3u8 里保留所有切片。生成后前端用 hls.js 播放 m3u8。这个方案在毕业设计里属于加分项,能讲清楚"为什么大视频要切片"就比只会 <video> 标签高一个层次。
6.3 一个验证部署是否到位的小技巧
部署完别急着庆祝,按这个顺序验一遍:先 curl -I http://your_domain/static/xxx.css 看静态文件返回 200 还是 404;再上传一个视频看 media 路径能不能访问;然后开两个浏览器窗口同时播放同一个视频,看播放量是不是加了 2 而不是 1;最后把 DEBUG 确认成 False 再跑一遍全流程。我一般会把这些检查写成一个 checklist,每次部署照着过一遍,比事后救火省心得多。
这套系统真正拉开差距的地方,从来不是页面多花哨,而是你能不能把视频从上传到播放的每一步讲清楚、把并发和部署的坑提前填上。我自己做第一个版本时,光静态文件 404 就折腾了一晚上,后来才明白开发和生产是两套逻辑。把上面这些跑通,答辩时随便挑一个点都能讲出深度。希望帮到你。
更多推荐

所有评论(0)