简介:基于Django的在线学习平台项目,附带完整源码与详细开发文档,适合计算机、人工智能、自动化等专业学生用于毕业设计、课程设计,也适合Django初学者对照学习。资源共783个文件,涵盖97个Python核心代码、118个HTML页面、182个JavaScript脚本、54个CSS样式表,以及200余张PNG/JPG图片素材,另外包含数据库文件、国际化翻译文件和配置文件,整套代码约5.33MB,目录结构清晰。该项目已通过导师指导认可,答辩评审分达95分,目前已有56人学习下载。代码经充分测试运行成功,可直接部署或在此基础上二次修改;配套文档可帮助理解路由配置、模板渲染、ORM模型、静态资源管理与多语言支持等实现细节,便于快速扩展为课程管理、在线考试等功能模块。无论是毕业设计、课程设计还是个人学习,都能获得可复用的工程参考。
1. 一份“资料齐全”的Django在线学习平台,到底能省下你多少事?
一份“资料齐全+详细文档”的Django在线学习平台项目压缩包,最近在课程设计、毕业设计甚至内部培训系统选型里被反复搜到。它对你的价值不是省去敲几千行代码,而是把“课程、章节、用户、评论、订单”这套在线教育业务,一开始就摆成可落地的数据模型和页面流。拿到手你能直接看到别人怎么设计表、怎么写权限、怎么做上传和支付回调,比对着零散教程拼一个多月要快得多。
但拿到 zip 不等于会用。我见过太多人解压后第一件事是双击 README,然后卡在环境装不上、数据库迁不过去、静态文件全丢这三座山上。真正决定这份资料能不能用的,是你有没有能力把它还原成一套能跑、能改、能上线的系统。这篇文章就按“解压立项 → 建模型 → 写查询 → 踩坑排查 → 验证部署”的顺序,把这个标题背后的项目完整拆开给你看。
2. 拆开项目压缩包:目录结构、环境还原与 Django 项目初始化
2.1 项目包的目录结构:先分清源码、依赖和说明文档
拿到压缩包后别急着点开 README,我一般先用tree或ls -R把顶层目录过一遍。一个典型的 Django 在线学习平台,无论做成单项目还是多 app 结构,通常都由四部分组成:项目配置根目录、业务 app 目录、static/media 资源目录、依赖声明和文档。先分清这四类,能避免你在文档和源码之间来回翻。
online_learning/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置根目录 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ # 业务模块 │ ├── course/ # 课程、章节、课时 │ ├── users/ # 用户、角色 │ └── order/ # 订单、支付回调 ├── static/ # 前端静态资源 ├── media/ # 用户上传的头像、课件 └── docs/ # 项目文档、SQL 脚本这段目录的意义在于帮你建立“入口意识”:manage.py是所有命令的入口,config/settings.py是配置入口,apps下的每个目录是一个独立可插拔的业务模块。看目录时我最先打开的是config/settings.py里的INSTALLED_APPS和DATABASES,前者决定项目启用了哪些功能模块,后者决定数据落到 SQLite 还是 MySQL。很多资料包自带的db.sqlite3不一定适配你的环境,先看清数据库配置再决定要不要沿用。
requirements.txt则记录了第三方依赖。注意这份文件里如果只有包名没有版本号,说明作者希望装最新版,这对老项目风险很大;反过来如果版本号写得很死,你换 Python 版本后很可能装不上。正确做法是看项目文档里有没有注明开发版本,没有的话先按当前环境能装的范围来,后面我会讲怎么处理依赖冲突。
2.2 用虚拟环境还原依赖:requirements.txt 不是摆设
网上那些“Django 项目实战新手”教程往往会让你直接在全局环境里pip install,这是解压这套资料后最容易翻车的地方。全局安装会把依赖版本冲突转嫁到系统 Python 里,最后你为了跑通一个课程平台,把机器上其他项目的环境搞坏了。我每次拿到这类 zip,第一件事永远是建虚拟环境。
python3 -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txt python manage.py runserver 0.0.0.0:8000这段命令的逻辑很简单:python3 -m venv venv生成一个隔离的 Python 环境,激活后所有pip install都只装进venv目录,不会污染系统。--upgrade pip是先把包管理工具升到新版,避免老 pip 在解析依赖时报语法错误。最后runserver 0.0.0.0:8000表示监听所有网卡,方便你拿手机或局域网其他电脑访问页面。
参数上真正要注意的是 Python 版本与 Django 版本的兼容关系。老项目 requirements 里写死 Django 3.x 的,你用 Python 3.11 或 3.12 经常会在装mysqlclient或Pillow时遇到 gcc 编译报错,那不是代码问题,是版本组合问题。常见做法是先用pyenv装一个项目文档里提到的 Python 版本,再重新建虚拟环境。如果文档没说,就按 requirements 里 Django 大版本号反查它官方支持的 Python 版本区间,宁可多花十分钟切版本,也不要硬着头皮编译。
装完依赖后先跑python manage.py check,这个命令不会启动服务,只检查配置和模型有没有明显错误。它能帮你区分“环境没装好”和“代码本身有问题”,避免一上来 runserver 报错就怀疑人生。
2.3 从零初始化一个 Django 项目:创建 app、注册、迁移
有些资料包只给了部分源码和文档,主工程是缺的,那你就需要自己把这份在线学习平台的骨架补起来。下面这套命令是 Django 创建 project 和 app 的标准动作,也是你在任何 Django 一站式教程里都会看到的第一步:
django-admin startproject config . python manage.py startapp course python manage.py startapp users python manage.py startapp order这里有个关键细节:startproject config .后面这个点不能漏,它表示在当前目录生成manage.py和config包,而不是再多套一层同名文件夹。startapp course则会在项目根目录下创建一个course目录,里面自动生成models.py、views.py、admin.py等文件。
创建完 app 之后,很多人忘记注册就直接写模型,结果makemigrations提示App 'course' could not be found。你需要打开config/settings.py,在INSTALLED_APPS列表里把三个 app 都加进去:
INSTALLED_APPS = [ # Django 自带的 app 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 业务 app 'course', 'users', 'order', ]注册完成后才能做迁移。迁移的命令是python manage.py makemigrations和python manage.py migrate,前者把你写的模型翻译成迁移文件,后者真正把表建到数据库里。这里要注意顺序:makemigrations必须在migrate之前,而且每次改了models.py都要重新执行一次makemigrations,否则数据库和模型不同步,后面查询时会出现no such column一类的错误。
3. 在线学习平台的四个核心模块:模型、权限、互动与支付
3.1 用户与权限:Student/Teacher/Admin三类角色怎么落地
任何在线学习平台,第一个要改的就是用户模型。Django 默认的User表只有用户名、密码、邮箱这些字段,没有昵称、头像、角色区分。你要做学生、讲师、管理员三类角色,最常见的方案不是再建一张 Profile 表去一对一关联,而是直接写一个继承AbstractUser的自定义用户模型,把角色和扩展字段直接挂在用户表上。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES = [ ("student", "学员"), ("teacher", "讲师"), ("admin", "管理员"), ] role = models.CharField("角色", max_length=20, choices=ROLE_CHOICES, default="student") avatar = models.ImageField("头像", upload_to="avatars/%Y/%m/", blank=True) phone = models.CharField("手机号", max_length=20, blank=True) class Meta: db_table = "user"这段模型里role用CharField加choices而不是单独建角色表,是因为角色是固定枚举,不需要前端动态增删。default="student"意味着通过注册接口进来的用户默认都是学员,讲师只能由管理员后台修改,这是学习平台最保守也最不容易被薅权限的默认值。avatar的upload_to按年月分目录,避免头像文件全堆在一个文件夹里。
注意一个关键点:使用AbstractUser自定义用户模型,必须在第一次migrate之前就设置好。如果项目已经迁移过并且有数据了,再改成自定义模型会非常痛苦,因为 Django 不提供自动迁移auth_user的路径。这也是为什么我拿到资料包后,会先检查它的用户模型是不是自定义的——如果不是,就趁数据还没灌进去赶紧改。
权限判断上,我习惯写一个装饰器来限制讲师或管理员接口:
from django.http import JsonResponse from functools import wraps def require_role(role): def decorator(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({"code": 401, "msg": "未登录"}, status=401) if request.user.role != role: return JsonResponse({"code": 403, "msg": "无权限"}, status=403) return view_func(request, *args, **kwargs) return wrapper return decorator这个装饰器解决的是“课程发布、学员管理”这类接口只允许讲师访问的场景。is_authenticated判断是否登录,role != role判断角色是否匹配,都不满足就直接返回 JSON 错误,前端拿到 401 或 403 后跳转登录页或提示无权限。相比在每个视图里重复写 if 判断,装饰器的好处是一行就能给视图加上权限,而且逻辑集中,改动只动一处。
3.2 课程内容模型:课程→章节→课时→资料
在线学习平台的核心是内容层级。我见过很多课程设计把课程内容全塞进一个 JSON 字段,结果课程详情页、学习进度、课时权限全都难做。标准做法是分三张表:课程、章节、课时,课程下面挂多个章节,章节下面挂多个课时,课时再关联视频地址和附件。
class Course(models.Model): title = models.CharField("课程名", max_length=200) teacher = models.ForeignKey("users.User", on_delete=models.PROTECT, related_name="courses") status = models.CharField("状态", max_length=20, choices=[("draft", "草稿"), ("published", "已发布"), ("offline", "已下线")], default="draft") created_at = models.DateTimeField("创建时间", auto_now_add=True) class Meta: db_table = "course" class Chapter(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="chapters") title = models.CharField("章节名", max_length=200) order = models.PositiveIntegerField("排序", default=0) class Meta: ordering = ["order"] class Lesson(models.Model): chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name="lessons") title = models.CharField("课时名", max_length=200) video_url = models.CharField("视频地址", max_length=500, blank=True) file = models.FileField("附件", upload_to="lesson/%Y/%m/", blank=True) class Meta: ordering = ["id"]这个模型设计里有两个容易被新手忽略的点。第一,Course.teacher用on_delete=models.PROTECT而不是CASCADE,意思是讲师被删除时,如果它名下还有课程,Django 会直接报错阻止删除,而不是连课程一起删掉。原因很简单:课程内容是有价值的,讲师离职应该做“课程转移”而不是“数据清空”。第二,Chapter里加了一个order字段,因为章节顺序在业务上是可以手动调整的,纯靠创建时间排序不满足“拖拽排序”的需求。
Lesson.video_url用CharField存字符串而不是FileField,是因为线上环境视频几乎都放在 OSS 或 CDN 上,项目里只需要存 URL,不需要把大文件同步到服务器。附件file保留FileField,用来上传 PDF、PPT 这类小体积学习资料。upload_to按年月分目录,和头像的策略一致,避免单目录文件过多影响 IO。
3.3 学习互动:课程评论、学习进度记录
评论和进度才是让这个项目“像在线学习平台”而不只是“课程展示站”的关键。评论最简单的做法是直接关联用户和课时,一个课时下面挂多条评论,按时间排序。进度记录则要一张选课表和一张课时完成表,选课表防止重复购买,课时完成表记录每个课时是否学完。
class Enrollment(models.Model): user = models.ForeignKey("users.User", on_delete=models.CASCADE) course = models.ForeignKey("course.Course", on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("user", "course") class LessonProgress(models.Model): user = models.ForeignKey("users.User", on_delete=models.CASCADE) lesson = models.ForeignKey("course.Lesson", on_delete=models.CASCADE) finished = models.BooleanField("是否学完", default=False) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ("user", "lesson")unique_together在这里的含义是:同一个用户对同一门课只能有一条选课记录,对同一个课时只能有一条进度记录。这比在查询时用filter(...).first()去重更省事,数据库层面就堵住了重复数据的可能。LessonProgress用BooleanField记录是否学完,而不是记录“学到了第几秒”,因为学习平台的实际需求通常是“已学完/未学完”和“最近学到哪”,后者可以用一个last_position整数字段补充,前端播放器每次暂停时上报。
查询时我一般这样取“用户在学课程”的列表:
courses = Course.objects.filter( status="published", enrollment__user=request.user ).distinct()这里enrollment__user是跨表查询的语法,Django 会自动从Course通过Enrollment反查到用户。由于一个用户可以选多门课,理论上不会产生重复,但一旦后续加了多条件过滤出现 join 多条记录,加上distinct()是最保险的写法,它可以防止关联表里有多条匹配记录时课程重复出现。
3.4 订单与支付:什么时候值得接微信支付
做课程设计或毕业设计时,订单模块做到“下单 → 模拟回调 → 支付成功”这一步就够了。真正接微信支付或支付宝需要商户号、证书和审核,个人开发者基本走不通这条路。我见过的成熟做法是:订单表保存金额和状态,后台提供一个“模拟支付成功”的接口,把订单状态直接改成已支付,前端演示时点一下按钮就能看到完整闭环。
from django.utils import timezone class Order(models.Model): user = models.ForeignKey("users.User", on_delete=models.PROTECT) course = models.ForeignKey("course.Course", on_delete=models.PROTECT) amount = models.DecimalField("金额", max_digits=10, decimal_places=2) status = models.CharField("状态", max_length=20, choices=[("pending", "待支付"), ("paid", "已支付"), ("refunded", "已退款")], default="pending") paid_at = models.DateTimeField(null=True, blank=True) def mark_paid(self): self.status = "paid" self.paid_at = timezone.now() self.save(update_fields=["status", "paid_at"])金额用DecimalField而不是FloatField,因为浮点数在比较时会出幺蛾子,比如0.1 + 0.2 != 0.3,这在账务上是不能接受的。DecimalField(max_digits=10, decimal_places=2)表示最多 8 位整数加 2 位小数,金额范围足够撑住绝大多数课程定价。mark_paid方法里用update_fields只更新status和paid_at两个字段,避免整行覆盖时把其他字段的并发修改冲掉,尤其是在回调接口里,这个细节能减少很多脏数据。
订单与选课表的联动一般放在mark_paid成功后:先判断该用户是否已选这门课,没有就创建Enrollment记录,用户才能进入课程详情页。这个动作不要放在下单时做,否则用户取消支付后选课记录还在,等于免费送了课。这个时序问题,是很多初版学习平台线上翻车的重灾区。
4. django 执行查询:ORM 的增删改查与常见翻车点
4.1 查询:filter/get/exclude 的边界
学 Django 的人几乎都踩过同一个坑:把get当filter用。filter返回的是QuerySet,是一个可继续链式调用的集合;get返回的是单个模型对象,如果查到 0 条或 2 条以上,都会抛异常。两者的边界在在线学习平台里分得很清:列表页用filter,详情页用get或get_object_or_404。
# 已发布的课程列表 courses = Course.objects.filter(status="published") # 当前用户已选的课程 my_courses = Course.objects.filter( status="published", enrollment__user=request.user ).distinct() # 排除标题里带“内测”的课程 available = Course.objects.filter(status="published").exclude(title__icontains="内测")第一行是最基础的查询,status="published"会翻译成 SQL 里的WHERE status = 'published'。第二行通过enrollment__user实现跨表关联查询,Django 自动帮你 join 了Enrollment表,不需要手写 SQL;这里distinct()的意义前面说过,是为了防止关联记录导致课程重复。第三行的exclude相当于NOT LIKE,icontains在 SQLite 和 MySQL 里都翻译成大小写不敏感的模糊匹配,比contains更符合搜索习惯。
get的正确打开方式是“明确只需要一条记录”的接口,比如课程详情页和订单详情页。因为查不到或查到多条都是异常情况,get的抛错行为正好帮你暴露问题。我一般不会让get的异常裸奔到浏览器,而是用get_object_or_404包装一层,让查不到时自动返回 404 页面,而不是 500 错误。
4.2 删除对象:delete() 的级联与软删除
删除对象是“django 执行查询”里操作风险最高的一类。你在模型里定义了on_delete=models.CASCADE,那course.delete()一执行,它名下所有章节、课时、评论、进度记录全部物理删除,这是真正意义上的“没有后悔药”。在线学习平台的内容数据一旦误删,重建成本极高,所以我在设计阶段就不太愿意让业务代码碰物理删除。
最常见的替代方案是软删除:给模型加一个is_active或is_deleted字段,删除时只是把字段置为 False,查询时默认过滤掉。这样数据还在数据库里,运营想要恢复随时可以改回来。
class Lesson(models.Model): # 省略其他字段 is_active = models.BooleanField("是否有效", default=True) def delete(self, *args, **kwargs): self.is_active = False self.save(update_fields=["is_active"])但这里有一个特别隐蔽的坑必须说:模型级delete()方法只在你调用instance.delete()时生效,如果你执行Lesson.objects.filter(title__icontains="测试").delete(),Django 走的是QuerySet.delete()批量删除,它不会调用模型里的delete()方法,而是直接生成DELETE FROM ... WHERE ...的 SQL,照样物理删除。
所以更稳的做法是写一个自定义 Manager,在默认查询集上过滤掉已删除的记录,并且凡是涉及删除的接口都走“UPDATE 标记”而不是 DELETE。示例:
class ActiveManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_active=True) class Lesson(models.Model): # 省略其他字段 is_active = models.BooleanField("是否有效", default=True) objects = ActiveManager() all_objects = models.Manager() # 需要真的查到已删课时时用这个这里objects默认只返回有效课时,业务代码里Lesson.objects.filter(...)不会拿到已删除数据;all_objects是原始管理器,留给后台恢复数据或导出报表用。这样做之后,删除操作的语义从“抹掉记录”变成了“下架记录”,用户端看到的效果一致,但数据安全性完全不同。
批量操作里还有一个被忽视的隐患:外键的级联删除。假设你删了一个用户,Django 会顺着它所有外键关系一层层删下去,可能把订单、评论、课程进度一起删掉。所以在线学习平台里,用户、课程这类核心业务外键,我一般不用CASCADE,而是用PROTECT或者SET_NULL,让删除在遇到关系数据时直接报错,逼着业务方先做转移或归档。
4.3 查询性能:select_related 和 prefetch_related 的差异
课程列表页是 N+1 查询的重灾区:页面要显示每门课程的讲师头像,如果你先查出 20 门课,再在模板里逐门课访问course.teacher.avatar,Django 会额外发起 20 次查询。解决这个问题的标准手段是select_related和prefetch_related,两者的区别必须搞清楚。
# select_related 适用于外键和一对一:一条 JOIN 把讲师带出来 courses = Course.objects.filter(status="published").select_related("teacher") # prefetch_related 适用于反向外键和多对多:先查课程,再查所有相关章节 courses = Course.objects.filter(status="published").prefetch_related("chapters")select_related的原理是 SQL 层直接 JOIN 课程表和用户表,一次查询把所有字段取回来,适合“查课程同时带出讲师”这种正向外键关系。prefetch_related则是分两步:先查课程列表,再查这些课程的所有章节,然后在 Python 内存里自动按外键分组拼装,适合“查课程列表同时带出每门课的章节”这种反向关系。
参数上有个容易翻车的点:prefetch_related("chapters")之后,如果你继续用chapters__title__icontains="xxx"去过滤,Django 会打破之前的 prefetch 计划,退回逐条查询。这是因为带过滤条件的跨表查询无法用简单的第二次查询完成。遇到这种情况,要用更细粒度的Prefetch对象指定过滤条件:
from django.db.models import Prefetch published_chapters = Prefetch("chapters", queryset=Chapter.objects.filter(order__gt=0).order_by("order")) courses = Course.objects.filter(status="published").prefetch_related(published_chapters)这里Prefetch允许你自定义“预取过来的章节”长什么样,它可以过滤、排序、甚至再 select_related 课程资料。使用Prefetch之后,模板里遍历course.chapters.all拿到的就是已经过滤并排序好的章节列表,不会再多打 SQL。对于课程详情页这种“课程 + 章节 + 课时 + 讲师”一次加载的大页面,把三个层级都用 prefetch 串起来,性能提升是肉眼可见的。
5. 避坑清单:从 zip 解压到上线的 5 个常见问题与排查
5.1 现象:runserver 能跑,但页面样式全丢、图片全裂
这是 Django 新手最容易遇到的“静态文件黑洞”。runserver在DEBUG=True时会帮你处理静态文件,但你一关掉 DEBUG、或者换到服务器上用 uwsgi 跑,Django 就会明确告诉你静态文件不是它该管的事。原因在于STATIC_URL和STATIC_ROOT没有正确配置,collectstatic也没跑过。
解决步骤是先检查 settings:
STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"]STATIC_URL是浏览器访问静态文件的 URL 前缀,STATIC_ROOT是collectstatic把所有 app 静态文件集中存放的目录,STATICFILES_DIRS是项目里手动放置的全局静态文件目录。配置完后执行python manage.py collectstatic --noinput,把分散在 app 里的静态资源全部收集到STATIC_ROOT,再交给 Nginx 托管这个目录。注意STATICFILES_DIRS和STATIC_ROOT不能指向同一个目录,否则会循环递归报错。
5.2 现象:pip install 报错,提示 gcc 编译失败或 Python 版本不兼容
老项目的 requirements.txt 里通常锁着特定版本的 Django、Django REST framework、Pillow 等,这些包的新版本对 Python 的 C 扩展编译要求很高。你用 Python 3.12 去装一个为 3.8 时代设计的mysqlclient,大概率会在安装阶段直接编译失败。解决方法是先看项目文档里有没有明确指定 Python 版本,没有就查 requirements 里 Django 大版本官方支持的 Python 版本区间,再用pyenv或 conda 切到对应版本,重新建虚拟环境安装。
不要硬扛编译报错。遇到gcc: error或zoneinfo相关问题,第一时间先怀疑版本组合,而不是去装 gcc 依赖或改系统库。换 Python 版本是最省事的路径,因为很多包的老版本根本没有针对新版 Python 的预编译 wheel,源代码编译需要一堆底层依赖,成本完全不成正比。
5.3 现象:数据库迁移报 “table already exists” 或 “column does not exist”
资料包里如果自带db.sqlite3,而你又在代码里跑了一遍makemigrations和migrate,就可能出现迁移状态和数据库结构对不上的情况。原因通常是源码里的迁移文件和数据库里的历史状态不同步,或者作者打包时清理了迁移目录但留下了数据库文件。
对本地开发环境,我的处理顺序是:先确认没有需要保留的数据,然后python manage.py migrate auth重建基础权限表,再删除旧的db.sqlite3后重新migrate。如果线上环境已经有不删的数据,就不要蛮干,先用python manage.py dumpdata > backup.json备份,再migrate --fake把迁移状态强行对齐,或者手动把缺的列补上。这里最忌讳的就是不看迁移文件直接删表,删完再想起来里面还有用户数据就晚了。
5.4 现象:MEDIA_URL 配置了,上传的图片还是访问不到
课程资料、头像上传成功,但页面访问/media/xxx.png返回 404。多数情况下是只配了MEDIA_ROOT忘了配MEDIA_URL,或者没有在根 URL conf 里挂载媒体目录。settings 里要同时有:
MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media"然后要在config/urls.py的urlpatterns里追加:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # 其他路由 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这样开发和调试阶段才能通过 Django 自带的静态服务访问到上传文件。注意static()这个辅助函数在DEBUG=False时不会生效,所以线上必须用 Nginx 单独配置/media/指向MEDIA_ROOT目录。
5.5 现象:在管理后台删一个讲师,结果整批课程都消失了
这是on_delete=models.CASCADE带来的典型连锁灾难。讲师作为外键挂在课程表上,你删讲师时 Django 会把它名下所有课程连同章节、课时、评论一起删掉,而且这个删除是数据库层面一条条执行的,没有确认弹窗能拦得住。课程是平台最核心的资产,讲师离职应该做课程转移或下架,而不是物理删除。
解决方法是回到模型设计,把业务上属于“核心资产”的外键全部改成on_delete=models.PROTECT。这样当有人尝试删除一个名下还有课程的讲师时,Django 会抛出ProtectedError,后台操作直接失败,逼着你先把课程转移走。如果你确实允许“讲师删除后课程保留”,可以改成on_delete=models.SET_NULL,同时让teacher字段允许为空,但前端要处理好课程显示“讲师已注销”的状态。
6. 验证项目真能扛住:Django 自查清单与本地压测
一个在线学习平台从“能跑”到“能交”之间,差距通常在验证环节。我拿到资料包改完之后,不会直接runserver看两眼就完事,而是按下面这套检查走一遍。先跑python manage.py check --deploy,它会扫描 settings 里不适合生产环境的配置。注意,这条命令在DEBUG=True时会给出大量警告,这是正常的,你要关注的是有没有SECRET_KEY硬编码、ALLOWED_HOSTS为空这类致命项。
之后是静态文件和媒体目录的实测:关闭 DEBUG,执行collectstatic,用 Nginx 托管静态目录和媒体目录,然后用浏览器直接访问一张图片确认 200 状态码。再测权限:用普通学员账号访问讲师的课程发布接口,预期返回 403 而不是出现 500。最后用python manage.py shell检查订单支付回调后选课记录是否创建,避免付了钱进不了课的尴尬。
本地压测我习惯用locust,不用写复杂脚本,直接在测试环境模拟 50 个并发用户同时访问课程列表和课程详情,看响应时间和错误率。如果课程详情页在并发下超过 1 秒,优先检查有没有 N+1 查询,再考虑加缓存。常见做法是给课程列表和详情页加cache_page,但注意加了缓存之后讲师改完课程内容不会立刻生效,要手动清缓存或设置短过期时间。
python manage.py check --deploy python manage.py collectstatic --noinput locust --host=http://127.0.0.1:8000 --users 50 --spawn-rate 5check --deploy是静态检查,collectstatic是构建发布产物,locust是动态压力验证。三步做完,这个项目才算真正达到“可以交付”的状态。我自己的习惯是把这套命令写进项目 README 的发布章节,每次部署前照着跑一遍,而不是靠脑子记。很多资料包里的项目最终翻车,不是因为代码差,而是因为没人跑过验证步骤。
希望这些从解压到上线踩过坑、填过土的经验能帮到你。拿到这类资料包,别急着表功,先跑通、再改深、最后压一遍,你才算真正吃透了这份 Django 在线学习平台。
本文还有配套的精品资源,点击获取