接手过不少Django项目,也帮人排查过很多次线上故障,我发现在线教育类系统里,“跑起来”从来不是难点,真正拉开差距的是数据模型设计、视频权限控制和部署调试这一整条链路。这篇文章就以“基于Django的网络课程在线学习平台”为例,从技术选型开始,到核心功能拆解,再到远程调试和部署实战,把我实际做过、踩过、最后验证过的方案完整写出来。适合正在做毕业设计、想快速搭建课程平台原型、或者准备在Django项目上做二次开发的同学参考。
1. 为什么选Django做在线课程平台:一次完整的技术选型复盘
1.1 在线教育平台的典型需求画像
网络课程在线学习平台,单看名字好像是一个很常见的CRUD项目,但只要你把需求展开,就会发现它天然带有“多角色、内容重、权限严”这三个特征。
先说多角色。一个完整的课程平台至少有三种身份:管理员要维护课程分类、审核课程内容、查看平台数据;讲师要上传视频、编辑章节、管理自己名下的课程;学生要注册登录、浏览课程、选课学习、记录学习进度。这就意味着用户体系不能只做一张表存用户名密码,还要有角色区分和权限控制。
再说内容重。课程平台的核心资产是视频,而视频跟普通文本、图片不一样,它涉及文件上传、格式校验、存储路径、播放权限、防盗链、进度记录等一系列问题。很多初学者在做好了课程标题、课程简介的增删改查之后,以为项目已经完成80%,实际上视频处理这块才是真正的深水区。
最后是权限严。一个付费或半付费的课程平台,必须保证只有选了课的用户才能看视频,访客最多只能看到课程封面和简介。这个权限校验不能只靠前端隐藏播放按钮,必须在后端每个视频请求里都做真实校验。需求一旦梳理到这个程度,你对技术栈的要求就很清楚了。
1.2 Django凭什么合适:与Flask、Spring Boot的对比
在做技术选型的时候,很多人会纠结Django、Flask、Spring Boot这几个框架怎么选。我的观点很直接:如果是做一个课程平台这种“业务模块多、需要后台管理、需要快速出成果”的项目,Django几乎是性价比最高的方案。
Django自带Admin后台,这是它最大的杀手锏。课程分类、课程信息、用户管理、订单记录,这些后台管理页面在Django里几乎不用写代码,注册一下Model就能直接用。对于毕设或者内部系统来说,省下的时间非常可观。
Django的ORM也是实打实的生产力工具。课程、章节、选课记录这些表关系,用ORM外键描述起来非常直观,迁移工具还能帮你从模型自动生成数据库表结构,不用手写SQL。
相比之下,Flask确实足够轻量,但轻量也意味着组件要靠自己拼装。你需要自己选择SQLAlchemy还是Peewee,自己接Flask-Login做登录,自己写后台管理页面,项目规模一大,容易在细节上失控。Spring Boot在企业级开发里很强,生态完整、性能可靠,但学习曲线明显更陡,对于以Python为主、或者想在短时间内完成项目的开发者来说,用Spring Boot做课程平台多少有点杀鸡用牛刀。
我把选型对比整理成一个表,方便你按自己的场景判断。
| 对比维度 | Django | Flask | Spring Boot |
|---|---|---|---|
| 开发效率 | 高,自带Admin和ORM | 中,组件需自己组合 | 中低,配置项多 |
| 学习成本 | 中,概念统一,教程多 | 低,入门容易,进阶难 | 高,Java体系复杂 |
| 后台管理 | 开箱即用 | 需要自建 | 需集成或自建 |
| 适合场景 | 内容管理、后台系统、课程平台 | 小型API、微服务 | 大型企业级系统 |
1.3 这套毕设项目的整体架构
这套网络课程在线学习平台,我建议采用经典的Django MTV架构,前端用Django模板加Bootstrap,后端用Django原生ORM,数据库用MySQL,文件存储先用本地目录的MEDIA_ROOT。
浏览器访问的完整链路是这样的:URL路由根据地址分发到对应的视图函数,视图函数通过ORM查询MySQL拿到课程数据,把数据丢给模板渲染成HTML,再返回给浏览器。如果是视频文件的请求,会由Nginx直接处理静态媒体文件,同时在后端做权限校验。
这个架构的好处是每一层职责清晰,出了问题容易定位,而且对毕设来说,不用引入Redis、Celery这类中间件就可以满足大部分需求。如果后续要优化,再把课程播放量统计放到Redis里异步处理就行,扩展路径很平滑。
2. 核心业务模块拆解:从用户体系到课程权限
2.1 用户角色的设计与权限控制
用户模块是整个平台的基石。我见过很多项目在这里走了弯路,最大的问题是自己建了一张User表,然后从登录注册到Session管理全部手写。实际上Django自带的认证系统已经非常完整,正确做法是继承AbstractUser扩展自己的用户表。
扩展之后,通过一个用户类型字段区分学生、讲师和管理员,配合Django自带的装饰器做视图级权限控制。看课的学生只能访问学习相关的页面,讲师进不了后台管理,管理员通过Django Admin管理所有数据。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE_CHOICES = ( ('student', '学生'), ('teacher', '讲师'), ('admin', '管理员'), ) user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES, default='student') nickname = models.CharField(max_length=50, blank=True) avatar = models.ImageField(upload_to='avatar/%Y/%m/', blank=True)在视图里做权限控制的时候,我一般这样处理:先判断用户是否登录,再判断用户类型,最后判断是否选了这门课。三个条件缺一不可。很多项目只做了登录校验,选课权限和后端播放校验都没做,结果浏览器里直接输入视频文件地址就能看,这在答辩的时候是很容易被追问的问题。
2.2 课程与章节的数据模型设计
课程和章节是一对多的关系,这是整个系统最核心的数据结构。设计的时候要注意几个细节:课程要有封面图、排序权重,因为首页要展示推荐课程;章节要有视频文件和时长,因为播放器需要展示总时长;课程和用户之间要有选课关系表,因为这是判断用户有没有权限看视频的依据。
from django.db import models from django.contrib.auth import get_user_model User = get_user_model() class CourseCategory(models.Model): name = models.CharField(max_length=50, unique=True) class Course(models.Model): title = models.CharField(max_length=200) cover = models.ImageField(upload_to='course/cover/%Y/%m/', blank=True) category = models.ForeignKey(CourseCategory, on_delete=models.SET_NULL, null=True) teacher = models.ForeignKey(User, on_delete=models.CASCADE, related_name='courses') desc = models.TextField(blank=True) is_published = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Chapter(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='chapters') title = models.CharField(max_length=200) video = models.FileField(upload_to='course/video/%Y/%m/') video_duration = models.IntegerField(default=0, help_text='视频时长(秒)') sort = models.IntegerField(default=0) class UserCourse(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_courses') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='user_courses') created_at = models.DateTimeField(auto_now_add=True)这里有个容易被忽略的点:章节里面的sort字段。很多初次设计的人会忽略排序字段,结果章节顺序只能按照创建时间排,一旦中途插入新章节,顺序就乱了。同样的情况在课程列表里也存在,所以课程表里也可以加sort或者一个推荐权重字段,展示页的排序逻辑才可控。
外键的on_delete策略也要想清楚。课程分类被删除时课程怎么办,我一般用SET_NULL保留课程本身,避免误删分类导致整个课程列表炸掉。讲师删除账号时课程怎么办,这里用CASCADE,等于讲师账号注销时同步清理其名下课程,语义上比较合理。
2.3 课程分类与检索的实现思路
课程列表页是所有用户进入平台后的第一站,它既要支持分类筛选,也要支持关键词搜索,还要处理分页。Django的Paginator做分页很顺手,搜索用Q对象可以同时查标题、简介和讲师名称。
from django.db.models import Q from django.core.paginator import Paginator def course_list(request): keyword = request.GET.get('keyword', '') category_id = request.GET.get('category', '') courses = Course.objects.filter(is_published=True) if keyword: courses = courses.filter( Q(title__icontains=keyword) | Q(desc__icontains=keyword) | Q(teacher__username__icontains=keyword) ) if category_id: courses = courses.filter(category_id=category_id) courses = courses.order_by('-created_at') paginator = Paginator(courses, 9) page = paginator.get_page(request.GET.get('page')) return page用ORM而不是裸SQL的一大好处是防注入。Q对象加上icontains筛选,Django会帮你转义特殊字符,不需要手动拼接字符串。不过要注意,ORDER BY和大量数据的全文搜索用ORM就不太够用了,那是搜索引擎的活。对毕设或者中小型课程平台来说,这个方案简单可靠,完全够用。
3. 视频处理与播放权限控制:最容易翻车的技术点
3.1 视频上传的格式与存储方案
视频上传是整个平台里最容易出问题的环节,而且问题往往出现在你意想不到的地方。首先是格式,很多同学在自己电脑上测试时用的是MP4,没有问题,但一换到线上,用户传了一个MOV或者MKV,浏览器直接无法播放。我的建议是上传时做格式校验,服务端只接受MP4、WebM这类适合Web播放的格式,在上传接口里对扩展名做白名单判断。
存储方面,数据库里绝对不要存视频二进制数据,而是存文件路径。Django的FileField上传后会保存到MEDIA_ROOT配置的目录中,数据库里存的是相对路径。
视频文件往往比较大,本地上传时要注意几个问题:一是Nginx默认有client_max_body_size限制,默认是1M,不调的话大视频直接传不上去;二是Django处理大文件上传时会把文件先写临时目录,要确保磁盘空间充足;三是文件命名不要用原始文件名,中文名、空格都会带来麻烦,最好用uuid重命名。
import uuid import os def upload_video_path(instance, filename): ext = os.path.splitext(filename)[1].lower() filename = f'{uuid.uuid4().hex}{ext}' return os.path.join('course/video', str(instance.course_id), filename)这段代码里的upload_video_path会作为FileField的upload_to参数,每次上传时自动生成一个新的UUID文件名,既避免了重名覆盖,也杜绝了路径穿越这类低级安全问题。
3.2 播放权限与防盗链的常见做法
视频权限控制是整个平台安全性的核心,也是最容易被答辩老师提问题的地方。很多同学在播放页用Django模板的if判断一下用户是否登录、是否选了课,然后决定展示还是隐藏视频标签,以为这样就安全了。实际上视频地址已经暴露在页面源码里,别人拿到URL直接就能下载。
正确的做法是后端拦截。我采用的方案是:所有视频播放请求都先走一个视图函数,在这个视图里做登录校验、选课校验、章节所属课程校验,全部通过后才把视频文件内容返回给浏览器。也就是说视频文件的真实路径永远不出现在前端的video标签里,前端播放的URL是类似 /media/play/?chapter_id=1 这样的接口地址。
from django.http import FileResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required @login_required def play_video(request): chapter_id = request.GET.get('chapter_id') chapter = Chapter.objects.filter(id=chapter_id).first() if not chapter: return HttpResponseForbidden('章节不存在') enrolled = UserCourse.objects.filter( user=request.user, course=chapter.course ).exists() if not enrolled and request.user.user_type != 'teacher': return HttpResponseForbidden('请先选课') video_path = chapter.video.path response = FileResponse(open(video_path, 'rb'), content_type='video/mp4') return response这个方案的优点是权限和文件读取都在后端完成,前端拿不到真实路径。缺点是需要后端配合做流式响应,Django的FileResponse其实已经处理了分块读取,所以性能上问题不大。如果追求更高的性能,可以在Nginx层做X-Accel-Redirect内部跳转,但这是后续优化的事了。
3.3 视频转码与切片:从本地视频到流畅播放
直接播放MP4文件在局域网或者测试环境里问题不大,但生产环境里会遇到两个问题:一是视频文件太大,用户拖动进度条时浏览器要下载完对应区间才能播放,体验差;二是编码格式五花八门,浏览器兼容性无法保证。
我的建议是引入ffmpeg做一次转码和切片,把视频转换为H.264编码的MP4,或者更进一步切片成HLS格式。HLS把视频切成一个个几秒的ts小文件,播放时按需加载,拖动进度条几乎不卡顿,而且天然支持自适应码率。
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict -2 -b:v 1500k output.mp4如果要做HLS切片,命令类似下面这样:
ffmpeg -i output.mp4 -codec copy -hls_time 10 -hls_list_size 0 output.m3u8切片生成后,前端用hls.js库播放,视频加载速度和拖动流畅度都会明显提升。在毕设项目里,这个能力可以作为加分项写在文档里,因为大多数同学做的平台还停留在直接扔一个MP4给浏览器播放的阶段。
4. 学习进度与数据统计:让平台“活”起来的关键
4.1 学习进度记录的数据结构与断点续看逻辑
前面做的课程、章节、选课,本质上还是静态的内容管理,学习进度记录才让整个平台具备“在线学习”的属性。用户在某个章节看到一半退出,下次登录应该能接着看,而不是重新从头播放。
实现方式不算复杂,关键是数据结构要设计好。我给每个用户和每个章节创建一条学习记录,记录里保存已经播放的时长和视频总时长。前端播放器定时把当前进度上报到后端,后端更新这条记录。
class StudyRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE) watched_seconds = models.IntegerField(default=0) duration = models.IntegerField(default=0) is_finished = models.BooleanField(default=False) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ('user', 'chapter')unique_together保证了同一个用户对同一个章节只会有一条记录,上报接口反复调用也只会更新,不会产生垃圾数据。前端拿到记录后,可以判断watch_seconds是否大于0,如果大于0就把播放器的当前时间设置到这个位置,实现断点续看。
上报进度时要注意频率。每秒钟上报一次太浪费请求,我一般让前端每5秒上报一次,在暂停、切换章节、离开页面时再补一次。还要加一个防刷策略:只有播放时长达到视频总时长的80%以上,才把is_finished标记为True,避免用户直接把进度条拖到末尾刷完成率。
4.2 课程评价与互动功能的设计
一个课程平台如果只有视频没有互动,学习氛围会差很多。常见的互动功能是课程评价和评论区。评论的数据结构很简单,核心是用户、课程、内容、星级这些字段。
这里我要提醒一个问题:用户输入的内容默认是不可信任的。Django模板渲染时会自动转义HTML,但只要你在某个地方用了mark_safe,或者把用户评论存成了HTML再原样输出,就会产生XSS漏洞。我的建议是内容一律按纯文本存储和展示,评论里如果有表情符号就用Unicode字符,不要允许用户提交任何HTML标签。
评论区还要考虑排序和分页。热门评论按点赞数排,最新评论按时间排,这个逻辑不复杂,但要在设计之初就想好,否则后面加字段会比较痛苦。
如果课程需要评分功能,可以单独建一张评分表,或者直接在评论表里加一个score字段,统计时取平均值展示在课程详情页。
4.3 后台数据统计与分析思路
平台跑起来之后,管理员最关心的是这些数据:注册用户数、课程数量、选课人数、视频播放量。Django Admin本身已经能展示这些数据,但要想更直观,可以做一个简单的数据卡片页面。
我一般用一个Dashboard视图,把核心指标通过ORM聚合查出来,再用模板渲染成卡片。选课最多的课程排行、播放量最高的章节排行,这些都可以用annotate加Count或者Sum实现。
from django.db.models import Count, Sum top_courses = Course.objects.annotate( student_count=Count('user_courses') ).order_by('-student_count')[:10]播放量统计这个指标有个坑:如果每次进入章节都累加,会出现一个用户反复进出导致播放量虚高的情况。我采用的方案是同一个用户对同一个章节在当天只记一次播放,或者用StudyRecord的updated_at来判断,两次播放间隔超过一定时间才算一次新的有效播放。
这些统计逻辑不需要很复杂,但一定要有防刷意识。答辩老师问到“平台上线后怎么判断数据是否可信”,你能回答出防刷策略,整个项目的完成度会明显上一个台阶。
5. 远程调试与部署实战:毕设项目从本机到服务器
5.1 用远程调试定位线上的棘手Bug
本地能跑不代表线上能跑,这是部署环节最让人头疼的地方。我遇到过DEBUG=False之后静态文件加载不出来、MySQL字符集导致中文乱码、服务器上Pillow库缺失导致图片上传500,这些问题如果不掌握远程调试手段,只能靠猜来解决问题。
PyCharm专业版提供了远程解释器功能,可以通过SSH连接服务器、同步代码、在服务器端运行和调试项目。这样你在本地打的断点会真实地在服务器环境里命中,看变量、看堆栈、单步执行,效率比print大法高得多。
配置路径是:PyCharm的Settings里选择Python Interpreter,点击Add Interpreter选On SSH,填上服务器的IP、端口、用户名密码,再选择服务器上虚拟环境里的Python解释器即可。
如果没有专业版,也可以用最朴素的日志排查法。在代码的关键位置加上logging或者print,把错误信息输出到日志文件,再用journalctl或者tail命令查看。关键是日志要分级、要带上模块名和行号,这样能快速定位到具体代码位置。
我印象最深的一次排查经历是这样的:部署后所有接口都正常,但只要一上传图片就报500。远程调试打上断点才发现,报错发生在Pillow读取图片的EXIF信息时,因为服务器上的Pillow版本是8.2,而本地是9.1,两者对某些损坏图片的处理逻辑不一样。升级服务器版本之后问题立刻消失。
5.2 服务器环境配置与项目部署流程
部署这套Django项目,我推荐用Linux服务器加Nginx加uWSGI(或者Gunicorn)加MySQL的组合。Python环境用虚拟环境隔离,不要直接把项目装到系统Python里,否则后面升级依赖时非常容易冲突。
部署的完整流程大概是这样的:
- 在服务器上安装Python 3.8+、MySQL、Nginx。
- 创建虚拟环境并安装项目依赖。
- 配置MySQL的数据库、账号、字符集。
- 修改settings.py里的数据库配置、DEBUG、ALLOWED_HOSTS。
- 执行makemigrations和migrate,创建数据库表。
- 用collectstatic收集静态文件。
- 用uWSGI启动项目,再用Nginx反向代理。
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py collectstatic --noinput python manage.py createsuperusersettings.py里有一个必须改的配置,就是ALLOWED_HOSTS。不填上服务器的域名或者IP,Django会直接拒绝请求并返回DisallowedHost。另一个必须改的是SECRET_KEY,不要使用仓库里自带的默认值,部署前重新生成一份。
5.3 静态文件、媒体文件与数据库的迁移
很多从本机搬到服务器上的Django项目,第一个报错就是静态文件全挂,页面没有任何样式。原因是Django的runserver会自动处理静态文件,但一旦换到uWSGI加Nginx的模式,就要靠collectstatic把静态文件收集到STATIC_ROOT指定的目录,再由Nginx来处理这些文件的访问。
Nginx配置的要点是这样两块:
location /static/ { alias /home/project/static_root/; } location /media/ { alias /home/project/media/; }一个对应Django的STATIC_URL,一个对应MEDIA_URL。视频文件、课程封面、用户头像都在media目录下,如果不配置Nginx直接访问,所有上传文件都会404。
数据库迁移这块,最常见的坑是本地用的SQLite,到了线上换成MySQL。开发时SQLite确实省事,但SQLite和MySQL在字段类型、事务行为上有差异,最好的做法是一开始就用MySQL,这样部署的时候就不用经历一次数据库类型切换。如果已经用了SQLite,可以通过Django的dumpdata和loaddata命令备份和恢复数据。
6. 拿到源码之后怎么学:跑通、读懂、二次开发
6.1 五分钟跑通本地环境的全流程
拿到一套完整的Django项目源码,第一件事不是打开代码逐行阅读,而是把它跑起来。只有程序动起来了,你对代码的感知才是真实的。
跑通项目的标准流程大概是:创建虚拟环境,安装依赖,配置数据库,执行迁移,创建超级管理员,然后启动开发服务器。
python -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver这几步做完,访问http://127.0.0.1:8000应该能看到首页,访问/admin用超级管理员登录后能进入后台。
这个过程中最容易出问题的就是安装依赖。如果项目的requirements.txt里有版本过旧的包,当前Python版本可能会安装失败。比较稳妥的做法是先把全部依赖升级或者调整到与当前Python版本兼容的版本,但要注意Django主版本之间差异比较大,比如Django 2.x和Django 4.x的urls写法、中间件配置都不一样,不要盲目升级到最新版。
6.2 读懂Django项目源码的阅读顺序
项目跑起来后,我建议按照这样的顺序读代码:先看settings.py了解配置,再看urls.py了解路由,然后按app逐个读models、views、templates。
读代码最忌讳的是从头到尾一个个文件啃,很容易迷失在细节里。我的方法是找一条主线:比如点开首页、选一门课、点进播放页,然后顺着这条用户操作链路去追代码。从浏览器地址栏的URL开始,找到路由规则,找到对应的视图函数,再看视图里查询了哪些模型,最终渲染的是哪个模板。一条链路走通了,项目整体结构自然就清楚了。
这个过程里你会发现,Django的MVC(准确说是MTV)分层非常规整,models管数据结构,views管业务逻辑,templates管页面展示,urls负责把请求路由到正确的视图。只要掌握了这条链路,任何市面上常见的Django开源项目你都能用同一种方法快速读进去。
6.3 从毕设到真正可用产品的扩展方向
如果这套系统不是为了答辩,而是真正打算给学校或者外部机构使用,有几个方向值得优先投入。
第一个是视频存储和分发。本地目录存视频的问题是磁盘空间有限、出口带宽有限,多人同时看视频时卡顿严重。可以换成云存储加CDN分发,视频上传到云端,播放入口走CDN,Django只管权限和签名。同时配合云厂商的视频转码服务,省去自己折腾ffmpeg的时间。
第二个是异步任务。课程数据量大了之后,选课成功通知、视频转码、数据报表生成,这些耗时任务都可以放进Celery里异步处理,避免同步等待阻塞请求。
第三个是前端体验。纯Django模板加Bootstrap能满足基本需求,但动态交互比较弱。可以把课程列表、播放页改造成Vue或者React单页应用,Django作为纯API后端,用DRF提供接口,体验会明显提升。
第四个是数据安全。线上的课程平台一定要做访问限流、文件下载控制、敏感操作审计。之前提到的视频防盗链只是一个起点,更完整的方案包括登录态校验、签名URL、IP限制等多层配合。
我个人的体会是,这套基于Django的课程平台,本身是一个麻雀虽小五脏俱全的项目,它涵盖了用户权限、内容管理、文件上传、在线播放、数据统计这些通用的业务场景。认真把它做完一遍,你对Django的理解会有一个质变,后续再接触任何Web项目,都会觉得清晰很多。