Django 教程

Django 是一个高级的 Python Web 框架,以"大而全"著称:自带 ORM、Admin 后台、认证系统、表单处理等全套组件,让你"开箱即用"地构建完整网站。

Django 的核心特点

特点说明
全能框架ORM、模板、表单、Admin、认证一应俱全
MTV 架构Model-Template-View,职责清晰
自带 Admin自动生成后台管理界面
安全默认内置防 XSS、CSRF、SQL 注入保护
生态成熟20 年历史,文档完善,社区庞大

一个 Django 应用的样子

# models.py - 定义数据模型
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
# views.py - 定义视图
from django.shortcuts import render

def article_list(request):
    articles = Article.objects.all()
    return render(request, "list.html", {"articles": articles})

Django 与 FastAPI/Flask 的定位差异

框架定位适合场景
Django全栈、大而全内容站、后台系统、大型应用
FastAPI高性能 API前后端分离、高并发 API
Flask轻量灵活小项目、微服务、原型

谁在用 Django

  • Instagram(照片分享)
  • Pinterest(灵感收藏)
  • Mozilla(Firefox 相关站点)
  • Disqus(评论系统)
💡 提示

Django 的哲学是"为完美主义者而生的框架"——约定优于配置,把常见 Web 开发问题都替你考虑好了。

Django 简介

深入了解 Django 的设计理念、MTV 架构,以及它与传统 MVC 的区别。

MTV 架构

Django 采用 MTV 模式(与 MVC 类似但命名不同):

Django 术语传统 MVC职责
Model(模型)Model数据层:数据库表映射
Template(模板)View表现层:HTML 渲染
View(视图)Controller逻辑层:处理请求、调数据

一次请求的生命周期

  1. 浏览器发起请求,Django 的 URLconf 匹配路由
  2. 调用对应的 View 函数
  3. View 通过 Model 查询数据库
  4. View 把数据传给 Template 渲染
  5. 返回 HTML 响应给浏览器
浏览器 → urls.py → views.py → models.py → 数据库
                 ↓
浏览器 ← template.html ← views.py

Django 的设计哲学

  • DRY(Don't Repeat Yourself):避免重复代码
  • 约定优于配置:遵循约定即可快速开发
  • 可插拔应用:项目由多个 app 组成
  • 安全默认:默认开启各类防护

项目 vs 应用

概念说明示例
项目(Project)整个网站myblog
应用(App)项目中的功能模块articles、users、comments
💡 提示

理解 MTV 是 Django 开发的第一步——知道"数据去哪、逻辑在哪、页面怎么出",后面学起来就顺了。

Django 安装

安装 Django 并创建虚拟环境,准备开发环境。

环境要求

  • Python 3.8+(推荐 3.10+)
  • pip
  • 推荐虚拟环境

创建虚拟环境

python3 -m venv venv
source venv/bin/activate        # macOS/Linux
venv\Scripts\activate          # Windows

安装 Django

pip install django

# 验证版本
python -m django --version
# 例如:5.1

数据库驱动(MySQL 为例)

pip install mysqlclient   # 或 pymysql

安装后验证

python -c "import django; print(django.get_version())"

常见问题

问题解决
mysqlclient 编译失败改用 pymysql:pip install pymysql,在 __init__.py 中 import pymysql; pymysql.install_as_MySQLdb()
pip 下载慢用国内镜像:-i https://pypi.tuna.tsinghua.edu.cn/simple
Python 版本过低升级 Python 3.8+
💡 提示

使用虚拟环境隔离项目依赖,是 Django 开发的标准做法;部署到服务器时也会用到同样的 venv。

Django 创建第一个项目

创建你的第一个 Django 项目,跑通"创建 → 启动 → 访问"全流程。

创建项目

django-admin startproject myblog
cd myblog

# 创建应用
python manage.py startapp articles

项目结构

myblog/
├── manage.py          # 管理命令入口
├── myblog/            # 项目配置
│   ├── __init__.py
│   ├── settings.py    # 全局配置
│   ├── urls.py        # 根路由
│   └── wsgi.py        # WSGI 入口
└── articles/          # 应用
    ├── models.py      # 数据模型
    ├── views.py       # 视图
    ├── urls.py        # 应用路由
    └── apps.py        # 应用配置

注册应用

# myblog/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    ...
    'articles',        # 添加自己的应用
]

初始化数据库

python manage.py migrate

启动开发服务器

python manage.py runserver

# 访问 http://127.0.0.1:8000
# 看到 "The install worked successfully!" 即成功

第一个视图

# articles/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, Django!")

# articles/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

# myblog/urls.py
from django.urls import include, path

urlpatterns = [
    path('', include('articles.urls')),
]
💡 提示

manage.py 是 Django 的瑞士军刀:启动、迁移、创建应用、创建管理员等操作都通过它完成。

Django django-admin

掌握 django-admin 与 manage.py 的常用命令,管理 Django 项目。

两个命令入口

命令用途
django-admin全局命令:创建项目等
manage.py项目内命令(含全部 django-admin 能力)

常用命令速查

命令说明
django-admin startproject myblog创建项目
python manage.py startapp articles创建应用
python manage.py runserver启动开发服务器
python manage.py migrate应用数据库迁移
python manage.py makemigrations根据模型生成迁移文件
python manage.py createsuperuser创建管理员账号
python manage.py shell交互式 Python 环境(带 Django 上下文)
python manage.py collectstatic收集静态文件(部署用)
python manage.py check检查项目配置

查看全部命令

python manage.py help

shell 调试数据

python manage.py shell
>>> from articles.models import Article
>>> Article.objects.create(title="测试", content="内容")
>>> Article.objects.all()

创建管理员

python manage.py createsuperuser
# 用户名:admin
# 邮箱:(可跳过)
# 密码:****
💡 提示

开发时常用 runserver + shell;上线部署用 collectstatic + migrate。多敲命令,自然就熟了。

Django 项目结构解析

逐层解析 Django 项目结构,明白每个文件和目录的职责。

项目根目录

myblog/                 # 项目根目录
├── manage.py           # 管理入口
├── myblog/             # 项目配置包
│   ├── __init__.py     # 包标记
│   ├── settings.py     # 核心配置
│   ├── urls.py         # 根 URL 路由
│   ├── asgi.py         # ASGI 入口(异步)
│   └── wsgi.py         # WSGI 入口(同步部署)
├── articles/           # 应用包
│   ├── __init__.py
│   ├── admin.py        # Admin 注册
│   ├── apps.py         # 应用配置类
│   ├── models.py       # 数据模型
│   ├── views.py        # 视图
│   ├── urls.py         # 应用路由(自建)
│   ├── migrations/     # 迁移文件
│   ├── templates/      # 模板目录(自建)
│   └── static/         # 静态文件(自建)
├── templates/          # 项目级模板(自建)
├── static/             # 项目级静态文件(自建)
└── db.sqlite3          # 默认 SQLite 数据库

settings.py 关键配置

INSTALLED_APPS       # 已安装的应用列表
MIDDLEWARE           # 中间件列表
DATABASES            # 数据库配置
TEMPLATES            # 模板配置
STATIC_URL           # 静态文件 URL 前缀
MEDIA_URL            # 上传文件 URL 前缀
LANGUAGE_CODE        # 语言(zh-hans 中文)
TIME_ZONE            # 时区(Asia/Shanghai)

配置中文与 MySQL

# settings.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myblog',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

urls.py 路由层级

# 根 urls.py 负责分发
urlpatterns = [
    path('admin/', admin.site.urls),
    path('articles/', include('articles.urls')),
]

# 应用 urls.py 负责细节
urlpatterns = [
    path('', views.index),
    path('/', views.detail),
]
💡 提示

先弄清"配置在哪、路由怎么分、代码放哪",Django 项目的骨架就印在脑子里了。

Django 模板

Django 模板(Template)是 MTV 中的 T,负责把数据渲染成 HTML 页面。

模板基础语法

<!-- 输出变量 -->
<h1>{{ article.title }}</h1>

<!-- if 判断 -->
{% if user.is_authenticated %}
  <p>欢迎,{{ user.username }}</p>
{% else %}
  <p>请登录</p>
{% endif %}

<!-- for 循环 -->
<ul>
  {% for a in articles %}
    <li>{{ a.title }}</li>
  {% empty %}
    <li>暂无文章</li>
  {% endfor %}
</ul>

创建模板

articles/templates/
└── articles/
    ├── list.html
    └── detail.html
# views.py
from django.shortcuts import render

def list_view(request):
    articles = Article.objects.all()
    return render(request, 'articles/list.html', {'articles': articles})

模板标签速查

标签作用
{{ var }}输出变量
{% if %}条件判断
{% for %}循环
{% url 'name' %}按名称生成 URL
{% extends %}模板继承
{% block %}块定义/填充
{% include %}包含其他模板
{% csrf_token %}CSRF 令牌(表单必加)

过滤器

{{ title | upper }}        <!-- 转大写 -->
{{ content | truncatechars:30 }}  <!-- 截断 -->
{{ date | date:"Y-m-d" }}   <!-- 日期格式化 -->
{{ count | default:"0" }}   <!-- 默认值 -->

模板继承

<!-- base.html -->
<html><body>
  <nav>网站导航</nav>
  {% block content %}{% endblock %}
</body></html>

<!-- list.html -->
{% extends "base.html" %}
{% block content %}
  <h1>文章列表</h1>
{% endblock %}
💡 提示

模板自动转义变量内容,防 XSS;确实需要输出 HTML 时用 |safe 过滤器(仅限可信内容)。

Django 模型

Django 模型(Model)是 MTV 中的 M,用 Python 类描述数据库表,实现"以对象方式操作数据"。

定义模型

# articles/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    sort_order = models.IntegerField(default=0)

    class Meta:
        ordering = ['sort_order']

    def __str__(self):
        return self.name

class Article(models.Model):
    category = models.ForeignKey(Category,
    title = models.CharField(max_length=100)
    content = models.TextField()
    views = models.IntegerField(default=0)
    is_published = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

常用字段类型

字段用途
CharField(max_length)短文本
TextField()长文本
IntegerField()整数
FloatField()浮点数
BooleanField()布尔
DateField() / DateTimeField()日期时间
ForeignKey()外键(多对一)
ManyToManyField()多对多
OneToOneField()一对一

生成迁移

python manage.py makemigrations
python manage.py migrate

常见字段参数

参数说明
unique=True唯一约束
null=True数据库允许 NULL
blank=True表单允许为空
default=值默认值
on_delete=models.CASCADE外键关联删除(级联)
auto_now_add=True创建时自动填时间
auto_now=True每次保存更新为当前时间
💡 提示

改模型后一定要执行 makemigrations + migrate,否则数据库不会同步你的改动。

Django 表单

Django 表单处理用户输入:渲染表单、校验数据、显示错误。本节介绍基础用法。

用 Django Form 定义表单

# forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=50, label="姓名")
    email = forms.EmailField(label="邮箱")
    message = forms.CharField(widget=forms.Textarea, label="留言")

视图处理

# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():          # 校验
            # 取出清洗后的数据
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            # 业务处理(发邮件、存库等)
            return redirect('success')
    else:
        form = ContactForm()
    return render(request, 'contact.html', {'form': form})

模板渲染

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}          <!-- 自动渲染所有字段 -->
  <button type="submit">提交</button>
</form>

自定义校验

class RegisterForm(forms.Form):
    username = forms.CharField(min_length=3, max_length=20)
    password = forms.CharField(widget=forms.PasswordInput)
    confirm = forms.CharField(widget=forms.PasswordInput)

    def clean_confirm(self):
        # 字段级校验:clean_字段名
        if self.cleaned_data['password'] != self.cleaned_data['confirm']:
            raise forms.ValidationError("两次密码不一致")
        return self.cleaned_data['confirm']

ModelForm(最常用)

from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content', 'category']
        # 或 exclude = ['views']
def create_article(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            form.save()            # 直接保存到数据库
            return redirect('list')
    else:
        form = ArticleForm()
    return render(request, 'form.html', {'form': form})
💡 提示

模板中的表单必须加 {% csrf_token %},否则 POST 会被 Django 的安全中间件拦截(403)。

Django 视图

Django 视图(View)是 MTV 中的 V,负责"接收请求 → 处理逻辑 → 返回响应"。

最简视图

# views.py
from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello, World!")

常用响应类型

响应用途
HttpResponse返回文本
render()渲染模板返回 HTML
redirect()重定向
JsonResponse返回 JSON
Http404抛 404 异常

典型视图

from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from .models import Article

# 列表
def article_list(request):
    articles = Article.objects.filter(is_published=True)
    return render(request, 'list.html', {'articles': articles})

# 详情
def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    return render(request, 'detail.html', {'article': article})

# 重定向
def publish(request, pk):
    article = get_object_or_404(Article, pk=pk)
    article.is_published = True
    article.save()
    return redirect('article_detail', pk=pk)

# JSON 接口
def api_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    return JsonResponse({
        'id': article.id,
        'title': article.title,
    })

视图中的请求对象

def debug_request(request):
    print(request.method)        # GET / POST
    print(request.GET)           # 查询参数
    print(request.POST)          # 表单数据
    print(request.user)          # 当前用户
    print(request.session)       # Session
    return HttpResponse("ok")

FBV 与 CBV

视图有两种写法:函数视图(FBV)和类视图(CBV)。本节先掌握 FBV,类视图在后续章节介绍。

💡 提示

get_object_or_404 是常用技巧:对象不存在时自动返回 404,省去手动判断。

Django 路由

URL 路由(URLconf)把 URL 映射到视图函数,是 Django 请求分发的核心。

基本路由

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('articles/', views.article_list, name='article_list'),
    path('articles//', views.article_detail, name='article_detail'),
]

路径转换器

转换器匹配示例
<int:pk>整数/articles/42/
<str:slug>字符串/articles/hello/
<slug:slug>字母数字横线/articles/my-post/
<uuid:uid>UUID/articles/xxxx/
<path:rest>含斜杠路径/files/a/b.txt

路由命名与反向解析

# 给路由命名(name),模板/视图用名称引用
path('articles//', views.detail, name='article_detail')

# 视图中使用
from django.urls import reverse
url = reverse('article_detail', args=[42])

# 模板中使用
<a href="{% url 'article_detail' article.pk %}">查看</a>

include 包含子路由

# 根 urls.py
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('articles.urls')),   # 分发到应用
    path('users/', include('users.urls')),
]

re_path 正则路由(旧式)

from django.urls import re_path

urlpatterns = [
    re_path(r'^articles/(?P\d+)/$', views.detail),
]

默认 404/500 页面

Django 自动处理 404(Http404)与 500 错误,生产环境可自定义 templates/404.html、templates/500.html。

💡 提示

给每个路由起 name,模板和视图都用名称而不是硬编码 URL,改地址时只改 urls.py 一处即可。

Django Admin 管理工具

Django Admin 是自动生成的后台管理界面,无需写代码即可管理数据,是 Django 的招牌功能。

启用 Admin

# 创建管理员账号
python manage.py createsuperuser

# 启动后访问
# http://127.0.0.1:8000/admin/

注册模型

# articles/admin.py
from django.contrib import admin
from .models import Article, Category

admin.site.register(Article)
admin.site.register(Category)

自定义显示列

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'category', 'views', 'is_published', 'created_at')
    list_filter = ('is_published', 'category')      # 右侧筛选
    search_fields = ('title', 'content')            # 搜索框
    ordering = ('-created_at',)                     # 排序
    list_editable = ('is_published',)               # 列表页直接编辑
    date_hierarchy = 'created_at'                   # 日期钻取
    readonly_fields = ('views',)                    # 只读字段

自定义表单布局

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    fieldsets = (
        ('基本信息', {'fields': ('name', 'sort_order')}),
    )
    list_display = ('id', 'name', 'sort_order')

Admin 常用选项

选项作用
list_display列表显示的列
list_filter右侧筛选栏
search_fields搜索框搜索字段
list_per_page每页条数
ordering默认排序
actions批量操作

Admin 界面中文化

# settings.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
💡 提示

Admin 适合内部管理;面向用户的后台建议自己写(用 Django 表单 + 视图),界面和权限更可控。

Django ORM - 单表实例

通过单表实例掌握 Django ORM 的增删改查(CRUD),用 Python 代码操作数据库。

创建数据

# 方式一:create()
Article.objects.create(title="第一篇", content="内容1")

# 方式二:先建后存
a = Article(title="第二篇", content="内容2")
a.save()

# 方式三:get_or_create(存在则取,否则建)
obj, created = Article.objects.get_or_create(title="第三篇",
    defaults={'content': '内容3'})

查询数据

# 全部
Article.objects.all()

# 过滤
Article.objects.filter(is_published=True)

# 排除
Article.objects.exclude(category_id=1)

# 单个
Article.objects.get(pk=1)                    # 不存在报错
Article.objects.filter(pk=1).first()         # 不存在返回 None

# 排序
Article.objects.order_by('-created_at')      # 倒序
Article.objects.order_by('title')            # 正序

# 限制数量
Article.objects.all()[:10]                   # 前 10 条

更新数据

# 单条
a = Article.objects.get(pk=1)
a.title = "修改后的标题"
a.save()

# 批量更新
Article.objects.filter(is_published=False).update(is_published=True)

# 自增操作
Article.objects.filter(pk=1).update(views=models.F('views') + 1)

删除数据

# 单条
a = Article.objects.get(pk=1)
a.delete()

# 批量删除
Article.objects.filter(is_published=False).delete()

其他常用查询

# 数量
Article.objects.count()

# 存在性
Article.objects.filter(pk=1).exists()

# 去重 / 值列表
Article.objects.values_list('title', flat=True)

# 聚合
from django.db.models import Count, Sum, Avg
Article.objects.aggregate(total=Count('id'), avg_views=Avg('views'))
💡 提示

ORM 查询是惰性的:只有真正使用时才执行 SQL。加 print(queryset.query) 可查看生成的 SQL 语句,便于优化。

Django ORM - 多表实例

通过多表实例掌握 Django ORM 的关系查询:一对多、多对多、反向查询。

一对多(外键)

class Category(models.Model):
    name = models.CharField(max_length=50)

class Article(models.Model):
    category = models.ForeignKey(Category,
        related_name='articles')
    title = models.CharField(max_length=100)
# 正向查询:文章 → 分类
a = Article.objects.get(pk=1)
a.category.name

# 反向查询:分类 → 文章(related_name)
c = Category.objects.get(pk=1)
c.articles.all()

# 带条件反向查询
c.articles.filter(is_published=True)

# 通过外键过滤
Article.objects.filter(category__name='Python')

多对多

class Tag(models.Model):
    name = models.CharField(max_length=30)

class Article(models.Model):
    tags = models.ManyToManyField(Tag, related_name='articles')
    # ...
# 添加标签
a = Article.objects.get(pk=1)
t = Tag.objects.get(name='python')
a.tags.add(t)

# 移除
a.tags.remove(t)

# 查询
a.tags.all()                              # 文章的标签
Tag.objects.filter(articles__id=1)        # 含标签的文章

一对一

class Profile(models.Model):
    user = models.OneToOneField(User,
    bio = models.TextField(blank=True)

# profile.user 正向;user.profile 反向

跨表过滤(双下划线)

# 文章按分类名过滤
Article.objects.filter(category__name__icontains='py')

# 文章按标签名过滤
Article.objects.filter(tags__name='ai')

# 评论文章的用户
Comment.objects.filter(article__author__username='admin')

# 反向外键字段过滤
Category.objects.filter(articles__is_published=True).distinct()

select_related 与 prefetch_related

# select_related:外键(一对一/多对一)JOIN 预取
articles = Article.objects.select_related('category')

# prefetch_related:多对多/反向 预取
categories = Category.objects.prefetch_related('articles')

# 避免 N+1 查询,大幅提升性能
💡 提示

related_name 控制反向查询名;遇到列表页性能问题,优先加 select_related/prefetch_related。

Django ORM - 聚合查询

使用 Django ORM 聚合查询:分组统计、求和、计数、最大值最小值等。

聚合函数

from django.db.models import Count, Sum, Avg, Max, Min

# 总数
Article.objects.count()

# 求和(浏览量)
Article.objects.aggregate(total_views=Sum('views'))

# 平均
Article.objects.aggregate(avg_views=Avg('views'))

# 最大/最小
Article.objects.aggregate(max_views=Max('views'), min_views=Min('views'))

annotate 分组统计

from django.db.models import Count

# 每个分类下的文章数
categories = Category.objects.annotate(
    article_count=Count('articles')
)
for c in categories:
    print(c.name, c.article_count)

分组聚合示例

# 按是否发布分组统计
Article.objects.values('is_published').annotate(
    total=Count('id'), total_views=Sum('views')
)

# 按分类分组统计浏览量
Article.objects.values('category__name').annotate(
    total_views=Sum('views')
).order_by('-total_views')

条件聚合

from django.db.models import Count, Q

# 每个分类已发布文章数
Category.objects.annotate(
    published_count=Count('articles', filter=Q(articles__is_published=True))
)

# 或先过滤再统计
Article.objects.filter(is_published=True).values('category').annotate(
    count=Count('id')
)

实战:首页统计面板

def stats(request):
    from django.db.models import Count, Sum
    data = {
        'total_articles': Article.objects.count(),
        'total_views': Article.objects.aggregate(s=Sum('views'))['s'] or 0,
        'top_categories': list(
            Category.objects.annotate(
                n=Count('articles')
            ).order_by('-n')[:5].values('name', 'n')
        ),
    }
    return JsonResponse(data)
💡 提示

annotate 生成 GROUP BY 查询,配合 values() 可实现任意分组统计;注意空值处理(用 or 0 兜底)。

Django Form 组件

深入 Django Form 组件:字段类型、Widget、样式定制与高级校验。

常用字段类型

字段HTML 控件
CharField单行输入
EmailField邮箱输入(自动校验格式)
URLFieldURL 输入
IntegerField数字输入
DecimalField小数输入
BooleanField复选框
ChoiceField下拉选择
MultipleChoiceField多选
DateField / DateTimeField日期输入
FileField / ImageField文件/图片上传

下拉选择

class ArticleForm(forms.Form):
    CATEGORY_CHOICES = [
        ('frontend', '前端'),
        ('backend', '后端'),
        ('ai', 'AI'),
    ]
    category = forms.ChoiceField(choices=CATEGORY_CHOICES)

Widget 定制样式

class StyledForm(forms.Form):
    title = forms.CharField(
        max_length=100,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '请输入标题',
        }),
    )
    content = forms.CharField(
        widget=forms.Textarea(attrs={
            'rows': 8,
            'class': 'form-control',
        }),
    )
    publish = forms.BooleanField(
        required=False,
        widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
    )

表单整体校验

class RegisterForm(forms.Form):
    username = forms.CharField(min_length=3)
    password = forms.CharField(min_length=6)
    confirm = forms.CharField()

    def clean(self):
        # 跨字段校验
        cleaned = super().clean()
        if cleaned.get('password') != cleaned.get('confirm'):
            self.add_error('confirm', '两次密码不一致')
        return cleaned

动态下拉(查询数据库)

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'category', 'content']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 数据库分类作为下拉选项
        self.fields['category'].queryset = Category.objects.filter(is_active=True)
💡 提示

用 widget=forms.TextInput(attrs={...}) 加 Bootstrap/Tailwind 类名,表单样式就能和前端框架融合。

Django Auth

Django Auth 是内置的用户认证系统,提供用户模型、登录、注册、权限等完整能力。

认证系统的组成

  • User 模型:默认用户模型(username/password/email)
  • 登录/登出视图:LoginView / LogoutView
  • 权限系统:permission + group
  • 密码管理:哈希存储、重置

登录视图(内置)

# urls.py
from django.contrib.auth.views import LoginView, LogoutView

urlpatterns = [
    path('login/', LoginView.as_view(
        template_name='registration/login.html'), name='login'),
    path('logout/', LogoutView.as_view(), name='logout'),
]

登录模板

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">登录</button>
</form>

手动登录逻辑

from django.contrib.auth import authenticate, login

def my_login(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect('home')
        return render(request, 'login.html', {'error': '用户名或密码错误'})
    return render(request, 'login.html')

注册功能

from django.contrib.auth.models import User
from django.contrib.auth import login

def register(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        if User.objects.filter(username=username).exists():
            return render(request, 'register.html', {'error': '用户名已存在'})
        user = User.objects.create_user(username=username, password=password)
        login(request, user)      # 注册后自动登录
        return redirect('home')
    return render(request, 'register.html')

模板中判断登录

{% if user.is_authenticated %}
  <p>你好,{{ user.username }}</p>
  <a href="{% url 'logout' %}">退出</a>
{% else %}
  <a href="{% url 'login' %}">登录</a>
{% endif %}

视图权限控制

from django.contrib.auth.decorators import login_required

@login_required          # 未登录跳转到登录页
def profile(request):
    return render(request, 'profile.html')

# 类视图写法
from django.contrib.auth.mixins import LoginRequiredMixin

class ProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'profile.html'

自定义用户模型(推荐)

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    bio = models.TextField(blank=True)

# settings.py
AUTH_USER_MODEL = 'accounts.User'   # 第一个迁移前就要设置
⚠️ 注意

自定义用户模型必须在第一次 migrate 之前配置好,否则后续更换会非常麻烦。

Django Cookie/Session

使用 Cookie 与 Session 实现状态保持,记录用户会话。

Cookie 与 Session 的关系

  • Cookie:存在浏览器的小数据,自动随请求携带
  • Session:存在服务器端的数据,通过 Cookie 中的 sessionid 关联

读取 Session

def view_session(request):
    # 读取
    count = request.session.get('visit_count', 0)

    # 写入
    request.session['visit_count'] = count + 1
    request.session['user_id'] = 42

    # 删除
    # del request.session['user_id']

    return HttpResponse(f"访问次数:{count + 1}")

登录状态保持

def login_with_session(request):
    user = authenticate(...)
    if user:
        login(request, user)          # Django 自动写 session
        return redirect('home')
    return render(request, 'login.html')

操作 Cookie(直接)

def set_cookie(request):
    response = HttpResponse("ok")
    response.set_cookie(
        'theme', 'dark',
        max_age=3600,        # 1 小时
        httponly=True,       # JS 不可读
        path='/',
    )
    return response

def get_cookie(request):
    theme = request.COOKIES.get('theme', 'light')
    return HttpResponse(f"主题:{theme}")

Session 配置

# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db'    # 默认:存数据库
# 可选:cache / file / cookie

SESSION_COOKIE_AGE = 86400       # 会话时长(秒),默认 2 周
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_HTTPONLY = True   # 防 XSS 窃取

实战:购物车(Session)

def add_to_cart(request, product_id):
    cart = request.session.get('cart', {})
    cart[product_id] = cart.get(product_id, 0) + 1
    request.session['cart'] = cart
    return redirect('cart')

def show_cart(request):
    cart = request.session.get('cart', {})
    return render(request, 'cart.html', {'cart': cart})

清空会话

from django.contrib.auth import logout

def logout_view(request):
    logout(request)          # 清空 session 并登出
    return redirect('home')
💡 提示

敏感数据存 Session(服务器端),非敏感偏好(主题等)可存 Cookie;不要用 Cookie 存密码等敏感信息。

Django 中间件

Django 中间件(Middleware)是请求/响应处理链上的钩子,在视图前后执行,用于日志、认证、安全等横切逻辑。

中间件工作原理

请求 → 中间件1 → 中间件2 → ... → 视图
响应 ← 中间件1 ← 中间件2 ← ... ← 视图

内置中间件

# settings.py 默认的 MIDDLEWARE
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',       # 安全头
    'django.contrib.sessions.middleware.SessionMiddleware', # Session
    'django.middleware.common.CommonMiddleware',            # 常用
    'django.middleware.csrf.CsrfViewMiddleware',            # CSRF 防护
    'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户
    'django.contrib.messages.middleware.MessageMiddleware',  # 消息
]

自定义中间件

# middleware.py
import time

class RequestTimeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.time()

        # 请求处理前
        response = self.get_response(request)   # 调用下一个

        # 响应处理后
        duration = time.time() - start
        response['X-Request-Time'] = f'{duration:.3f}s'
        return response
# settings.py 注册
MIDDLEWARE += [
    'articles.middleware.RequestTimeMiddleware',
]

登录拦截中间件

class LoginRequiredMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # 白名单路径直接放行
        allowed = ['/admin/', '/login/', '/static/']
        if not request.user.is_authenticated            and not any(request.path.startswith(p) for p in allowed):
            return redirect('/login/')
        return self.get_response(request)

维护模式中间件

class MaintenanceMiddleware:
    def __call__(self, request):
        if MAINTENANCE_MODE:
            return HttpResponse("网站维护中,请稍后再来", status=503)
        return self.get_response(request)

中间件执行顺序

请求按 MIDDLEWARE 列表从上到下执行 __call__,响应从下到上返回。加中间件时注意顺序(安全类放前面)。

💡 提示

中间件是全局的,影响所有请求;只针对部分接口的逻辑用装饰器(@login_required)更合适。

Django 视图 - FBV 与 CBV

Django 视图的两种写法:函数视图(FBV)与类视图(CBV),以及各自的适用场景。

FBV 函数视图

from django.shortcuts import render, redirect

def article_list(request):
    articles = Article.objects.all()
    return render(request, 'list.html', {'articles': articles})

CBV 类视图

from django.views import View
from django.shortcuts import render

class ArticleListView(View):
    template_name = 'list.html'

    def get(self, request):
        # GET 请求
        articles = Article.objects.all()
        return render(request, self.template_name, {'articles': articles})

    def post(self, request):
        # POST 请求
        Article.objects.create(title=request.POST['title'])
        return redirect('list')

通用类视图(Generic CBV)

Django 提供了大量开箱即用的通用视图,极大减少样板代码:

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView

class ArticleList(ListView):
    model = Article
    template_name = 'list.html'
    context_object_name = 'articles'
    paginate_by = 10

class ArticleDetail(DetailView):
    model = Article
    template_name = 'detail.html'

class ArticleCreate(CreateView):
    model = Article
    fields = ['title', 'content', 'category']
    template_name = 'form.html'
    success_url = '/articles/'

class ArticleUpdate(UpdateView):
    model = Article
    fields = ['title', 'content']
    template_name = 'form.html'
    success_url = '/articles/'

class ArticleDelete(DeleteView):
    model = Article
    success_url = '/articles/'
    template_name = 'confirm_delete.html'
# urls.py 对应
path('', ArticleList.as_view()),
path('/', ArticleDetail.as_view()),
path('create/', ArticleCreate.as_view()),
path('/update/', ArticleUpdate.as_view()),
path('/delete/', ArticleDelete.as_view()),

FBV vs CBV 怎么选

对比项FBVCBV
上手难度简单直观需理解继承体系
代码复用装饰器继承/混入(更强)
标准 CRUD手写多通用视图极简
复杂逻辑更直接覆盖方法较绕
💡 提示

新手建议先用 FBV 理解流程;熟悉后对标准 CRUD 用 CBV 通用视图,效率翻倍。

Django Nginx+uwsgi

生产环境部署 Django:Nginx + uwsgi(或 gunicorn)的经典组合。

部署架构

浏览器 → Nginx(静态文件 + 反向代理)→ uwsgi → Django

安装 uwsgi

pip install uwsgi

# 测试 uwsgi 能否启动 Django
uwsgi --http :8000 --module myblog.wsgi

uwsgi 配置文件

# uwsgi.ini
[uwsgi]
chdir = /www/wwwroot/myblog
module = myblog.wsgi:application
master = true
processes = 4
threads = 2
socket = 127.0.0.1:8001
chmod-socket = 664
vacuum = true
die-on-term = true
# 日志
logto = /www/wwwroot/myblog/logs/uwsgi.log
# 启动
uwsgi --ini uwsgi.ini

收集静态文件

# settings.py
STATIC_ROOT = '/www/wwwroot/myblog/staticfiles'
STATIC_URL = '/static/'

# 收集所有应用静态文件到 STATIC_ROOT
python manage.py collectstatic

Nginx 配置

server {
    listen 80;
    server_name www.laowang.click;

    # 静态文件由 Nginx 直接服务
    location /static/ {
        alias /www/wwwroot/myblog/staticfiles/;
        expires 7d;
    }

    # 动态请求转发到 uwsgi
    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:8001;
    }
}

gunicorn 替代方案

pip install gunicorn
gunicorn myblog.wsgi:application -w 4 -b 127.0.0.1:8001

# nginx 反代
# proxy_pass http://127.0.0.1:8001;

settings.py 生产配置

DEBUG = False                    # 必须关闭!
ALLOWED_HOSTS = ['www.laowang.click', 'laowang.click']

# 安全设置
SECURE_SSL_REDIRECT = True       # 强制 HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

宝塔面板部署

  1. 创建 Python 项目,上传代码
  2. 安装依赖,运行迁移 python manage.py migrate
  3. 收集静态文件 python manage.py collectstatic
  4. 启动命令设为 uwsgi/gunicorn
  5. 添加 Nginx 反代 + SSL 证书
⚠️ 注意

生产环境必须 DEBUG=False 并配置 ALLOWED_HOSTS,否则存在严重安全风险(信息泄露、伪造 Host 攻击)。

Django 博客项目

综合运用 Django 全部知识,构建一个完整的博客项目。

项目规划

功能清单:
1. 文章列表(分页 + 分类筛选)
2. 文章详情(含浏览量统计)
3. 分类管理
4. 用户注册/登录
5. 文章发布(表单)
6. 收藏功能
7. 后台管理(Admin)

项目结构

myblog/
├── myblog/            # 项目配置
├── articles/          # 文章应用
│   ├── models.py      # Article/Category
│   ├── views.py       # 视图
│   ├── urls.py        # 路由
│   ├── forms.py       # 表单
│   └── admin.py
├── users/             # 用户应用
│   ├── views.py       # 注册/登录
│   └── urls.py
├── templates/         # 模板
├── static/            # 静态文件
└── manage.py

核心模型

# articles/models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=50)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

class Article(models.Model):
    title = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    category = models.ForeignKey(Category, related_name='articles')
    author = models.ForeignKey(User, related_name='articles')
    views = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

class Favorite(models.Model):
    user = models.ForeignKey(User,
    article = models.ForeignKey(Article, related_name='favorites')

    class Meta:
        unique_together = ('user', 'article')

列表视图(分页 + 分类)

from django.core.paginator import Paginator

def article_list(request, category_slug=None):
    articles = Article.objects.select_related('category', 'author').all()

    if category_slug:
        articles = articles.filter(category__slug=category_slug)

    paginator = Paginator(articles, 10)          # 每页 10 篇
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, 'list.html', {
        'page_obj': page_obj,
        'categories': Category.objects.all(),
    })

详情视图(浏览量+1)

from django.shortcuts import get_object_or_404

def article_detail(request, slug):
    article = get_object_or_404(Article, slug=slug)
    Article.objects.filter(pk=article.pk).update(views=models.F('views') + 1)
    article.refresh_from_db()
    return render(request, 'detail.html', {'article': article})
💡 提示

分页用 Django 内置 Paginator;列表页用 select_related 预取外键,避免 N+1 查询。

模型与数据库

深入博客项目的模型与数据库设计:字段选择、关系、索引与查询优化。

完善的模型设计

class Article(models.Model):
    title = models.CharField(max_length=100, db_index=True)  # 标题索引
    slug = models.SlugField(unique=True, db_index=True)      # 唯一+索引
    summary = models.CharField(max_length=200, blank=True)   # 摘要
    content = models.TextField()
    cover = models.ImageField(upload_to='covers/', blank=True)  # 封面图
    status = models.CharField(max_length=10, choices=[
        ('draft', '草稿'), ('published', '已发布'),
    ], default='draft', db_index=True)
    views = models.PositiveIntegerField(default=0)
    category = models.ForeignKey('Category', related_name='articles')
    tags = models.ManyToManyField('Tag', blank=True, related_name='articles')
    author = models.ForeignKey(User, related_name='articles')
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['status', '-created_at']),   # 联合索引
        ]

on_delete 的选择

选项行为适用
CASCADE级联删除文章删了评论跟着删
PROTECT有引用禁止删除分类下有文章不可删
SET_NULL置空需 null=True
SET_DEFAULT设默认值需 default=...

迁移与回滚

python manage.py makemigrations articles
python manage.py migrate
python manage.py migrate articles 0001_initial    # 回滚到指定迁移

查询优化技巧

# N+1 问题:循环里查库
# 坏:for a in articles: a.category.name  → N 次查询
# 好:select_related 预取外键
articles = Article.objects.select_related('category', 'author')

# 多对多预取
articles = Article.objects.prefetch_related('tags')

# 只取需要的字段
Article.objects.only('title', 'slug')

# 查看 SQL
print(Article.objects.all().query)

媒体文件配置

# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# urls.py(开发环境)
from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
💡 提示

数据库设计先想清楚关系(1:N / M:N / 1:1)和查询路径,再加索引,比事后优化高效得多。

Django Admin

博客项目的 Django Admin 后台配置:让内容管理更方便。

完整 Admin 配置

# articles/admin.py
from django.contrib import admin
from .models import Article, Category, Tag

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'slug', 'article_count')
    prepopulated_fields = {'slug': ('name',)}    # slug 自动生成

    def article_count(self, obj):
        return obj.articles.count()
    article_count.short_description = '文章数'

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ('id', 'name')

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'category', 'status', 'views', 'author', 'created_at')
    list_filter = ('status', 'category', 'created_at')
    search_fields = ('title', 'content')
    prepopulated_fields = {'slug': ('title',)}
    list_editable = ('status',)
    date_hierarchy = 'created_at'
    autocomplete_fields = ('category', 'tags')
    readonly_fields = ('views', 'created_at', 'updated_at')

    # 表单分组
    fieldsets = (
        ('基本信息', {'fields': ('title', 'slug', 'summary', 'cover')}),
        ('正文', {'fields': ('content',)}),
        ('分类与标签', {'fields': ('category', 'tags')}),
        ('发布', {'fields': ('status', 'author', 'views')}),
    )

    def save_model(self, request, obj, form, change):
        if not obj.author_id:
            obj.author = request.user    # 自动填当前作者
        super().save_model(request, obj, form, change)

Admin 常用装饰器

装饰器作用
@admin.register(Model)注册模型(替代 admin.site.register)
@admin.display(description='...')自定义列名
@admin.action(description='...')自定义批量操作

自定义批量操作

@admin.action(description='批量发布所选文章')
def make_published(modeladmin, request, queryset):
    queryset.update(status='published')

class ArticleAdmin(admin.ModelAdmin):
    actions = [make_published]

Inline 内联编辑

class CommentInline(admin.TabularInline):   # 或 StackedInline
    model = Comment
    extra = 1

class ArticleAdmin(admin.ModelAdmin):
    inlines = [CommentInline]   # 文章编辑页直接管理评论
💡 提示

prepopulated_fields 自动根据标题生成 slug;autocomplete_fields 大数据量外键选择体验极佳。

视图与模板

博客项目的视图与模板:列表、详情、表单页的完整实现。

列表页视图

from django.views.generic import ListView

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        qs = super().get_queryset()            .filter(status='published')            .select_related('category', 'author')
        # 分类筛选
        slug = self.kwargs.get('category_slug')
        if slug:
            qs = qs.filter(category__slug=slug)
        # 搜索
        q = self.request.GET.get('q')
        if q:
            qs = qs.filter(title__icontains=q)
        return qs

列表模板

{% extends "base.html" %}
{% block content %}
  <h1>文章列表</h1>

  {% for article in articles %}
    <article class="card">
      <h2><a href="{% url 'detail' article.slug %}">{{ article.title }}</a></h2>
      <p>{{ article.summary }}</p>
      <small>
        {{ article.category.name }} · {{ article.author.username }}
        · {{ article.created_at|date:"Y-m-d" }} · {{ article.views }} 次浏览
      </small>
    </article>
  {% empty %}
    <p>暂无文章</p>
  {% endfor %}

  <!-- 分页 -->
  {% if is_paginated %}
    <div class="pagination">
      {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">上一页</a>
      {% endif %}
      <span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
      {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">下一页</a>
      {% endif %}
    </div>
  {% endif %}
{% endblock %}

详情页视图与模板

from django.views.generic import DetailView

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'articles/detail.html'
    slug_field = 'slug'

    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        Article.objects.filter(pk=obj.pk).update(views=models.F('views') + 1)
        return obj
<h1>{{ article.title }}</h1>
<div>{{ article.content|linebreaks }}</div>
<p>浏览量:{{ article.views }}</p>

路由

urlpatterns = [
    path('', ArticleListView.as_view(), name='list'),
    path('category/<slug:category_slug>/', ArticleListView.as_view(), name='list_by_category'),
    path('<slug:slug>/', ArticleDetailView.as_view(), name='detail'),
]
💡 提示

CBV 的 get_queryset() 是过滤逻辑的入口;模板中 {% empty %} 处理空列表,分页模板可直接复用。

URL 路由

博客项目的 URL 路由 设计:命名空间、动态参数、反向解析。

带命名空间的路由

# articles/urls.py
from django.urls import path
from . import views

app_name = 'articles'      # 命名空间

urlpatterns = [
    path('', views.article_list, name='list'),
    path('category//', views.article_list, name='list_by_category'),
    path('/', views.article_detail, name='detail'),
    path('create/', views.article_create, name='create'),
    path('/update/', views.article_update, name='update'),
    path('/delete/', views.article_delete, name='delete'),
    path('/favorite/', views.toggle_favorite, name='favorite'),
]

# 根 urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('articles/', include('articles.urls')),
    path('', include('articles.urls')),   # 首页即文章列表
]

反向解析(视图中)

from django.urls import reverse

# 带命名空间
url = reverse('articles:detail', args=[article.slug])
url = reverse('articles:list')

# 带查询参数
url = reverse('articles:list') + '?page=2'

反向解析(模板中)

<a href="{% url 'articles:detail' article.slug %}">详情</a>
<a href="{% url 'articles:list_by_category' article.category.slug %}">{{ article.category.name }}</a>

<!-- 带 GET 参数 -->
<a href="{% url 'articles:list' %}?q=python">搜索 Python</a>

路由匹配规则

  • 按顺序匹配,第一个匹配成功的生效
  • 动态路由(<slug:slug>)放静态路由(create/)之后
  • 否则 create/ 会被当作 slug 匹配

URL 命名最佳实践

  • 小写 + 下划线:article_detail
  • 命名空间避免 app 间冲突:articles:detail
  • 模板永远用 {% url %},不硬编码路径
💡 提示

用 app_name 声明命名空间后,模板/视图里的 url 都要带前缀(articles:detail)。

分类筛选

博客项目的分类筛选:按分类过滤文章,展示分类列表与计数。

模型

class Category(models.Model):
    name = models.CharField(max_length=50)
    slug = models.SlugField(unique=True)

    class Meta:
        ordering = ['id']

分类筛选视图

from django.views.generic import ListView

class ArticleListByCategory(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        return Article.objects.filter(
            category__slug=self.kwargs['slug'],
            status='published',
        ).select_related('category')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['current_category'] = get_object_or_404(
            Category, slug=self.kwargs['slug'])
        context['categories'] = Category.objects.all()
        return context

分类带计数(annotate)

from django.db.models import Count

def sidebar_categories():
    return Category.objects.annotate(
        article_count=Count('articles', filter=Q(articles__status='published'))
    ).order_by('-article_count')

模板侧边栏

<aside>
  <h3>分类</h3>
  <ul>
    <li>
      <a href="{% url 'articles:list' %}">全部</a>
    </li>
    {% for cat in categories %}
      <li>
        <a href="{% url 'articles:list_by_category' cat.slug %}"
           {% if current_category.id == cat.id %}class="active"{% endif %}>
          {{ cat.name }} ({{ cat.article_count }})
        </a>
      </li>
    {% endfor %}
  </ul>
</aside>

筛选 + 搜索 + 分页组合

class ArticleFilterView(ListView):
    model = Article
    template_name = 'articles/list.html'
    paginate_by = 10

    def get_queryset(self):
        qs = Article.objects.filter(status='published')
        # 分类
        if slug := self.kwargs.get('slug'):
            qs = qs.filter(category__slug=slug)
        # 搜索词
        if q := self.request.GET.get('q'):
            qs = qs.filter(title__icontains=q)
        # 排序
        order = self.request.GET.get('order', 'newest')
        qs = qs.order_by('-created_at' if order == 'newest' else 'created_at')
        return qs
💡 提示

侧边栏分类用 annotate 一次性带出文章计数,避免循环中 cat.articles.count() 的 N+1 查询。

用户系统

博客项目的用户系统:注册、登录、个人中心、权限控制。

注册视图

# users/views.py
from django.contrib.auth.models import User
from django.contrib.auth import login
from django.shortcuts import render, redirect
from .forms import RegisterForm

def register(request):
    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if form.is_valid():
            user = User.objects.create_user(
                username=form.cleaned_data['username'],
                password=form.cleaned_data['password'],
                email=form.cleaned_data.get('email', ''),
            )
            login(request, user)        # 注册后自动登录
            return redirect('articles:list')
    else:
        form = RegisterForm()
    return render(request, 'users/register.html', {'form': form})

注册表单

# users/forms.py
from django import forms

class RegisterForm(forms.Form):
    username = forms.CharField(min_length=3, max_length=20)
    email = forms.EmailField(required=False)
    password = forms.CharField(widget=forms.PasswordInput, min_length=6)
    confirm = forms.CharField(widget=forms.PasswordInput)

    def clean_username(self):
        username = self.cleaned_data['username']
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError('用户名已被占用')
        return username

    def clean(self):
        cleaned = super().clean()
        if cleaned.get('password') != cleaned.get('confirm'):
            self.add_error('confirm', '两次密码不一致')
        return cleaned

个人中心

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    my_articles = Article.objects.filter(author=request.user)
    my_favorites = Article.objects.filter(favorites__user=request.user)
    return render(request, 'users/profile.html', {
        'my_articles': my_articles,
        'my_favorites': my_favorites,
    })

发布文章权限

@login_required
def article_create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user    # 作者取当前用户
            article.save()
            return redirect('articles:detail', slug=article.slug)
    else:
        form = ArticleForm()
    return render(request, 'articles/form.html', {'form': form})

作者权限校验

def article_update(request, slug):
    article = get_object_or_404(Article, slug=slug)
    # 只有作者或管理员能改
    if request.user != article.author and not request.user.is_staff:
        raise PermissionDenied
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            form.save()
            return redirect('articles:detail', slug=article.slug)
    else:
        form = ArticleForm(instance=article)
    return render(request, 'articles/form.html', {'form': form})

模板用户菜单

{% if user.is_authenticated %}
  <a href="{% url 'users:profile' %}">{{ user.username }}</a>
  <a href="{% url 'users:logout' %}">退出</a>
{% else %}
  <a href="{% url 'users:login' %}">登录</a>
  <a href="{% url 'users:register' %}">注册</a>
{% endif %}
💡 提示

权限校验三件套:@login_required(登录)、request.user.is_staff(管理员)、request.user == obj.author(本人)。

表单与收藏功能

博客项目的表单与收藏功能:发布/编辑文章表单、收藏/取消收藏。

文章表单(ModelForm)

# articles/forms.py
from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'slug', 'summary', 'content', 'category', 'cover']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'summary': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 12}),
        }

    def clean_title(self):
        title = self.cleaned_data['title']
        if len(title) < 5:
            raise forms.ValidationError('标题至少 5 个字')
        return title

收藏模型

class Favorite(models.Model):
    user = models.ForeignKey(User, related_name='favorites')
    article = models.ForeignKey(Article, related_name='favorite_set')

    class Meta:
        unique_together = ('user', 'article')    # 防重复收藏
        ordering = ['-id']

收藏切换视图

from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

@login_required
def toggle_favorite(request, slug):
    article = get_object_or_404(Article, slug=slug)
    fav, created = Favorite.objects.get_or_create(
        user=request.user, article=article)

    if not created:
        fav.delete()           # 已收藏则取消

    return JsonResponse({
        'favorited': created,
        'count': article.favorite_set.count(),
    })

模板收藏按钮

<button id="favBtn" data-url="{% url 'articles:favorite' article.slug %}">
  {% if is_favorited %}取消收藏{% else %}收藏{% endif %}
</button>

<script>
document.getElementById('favBtn').addEventListener('click', async function() {
  const res = await fetch(this.dataset.url, {method: 'POST'});
  const data = await res.json();
  this.textContent = data.favorited ? '取消收藏' : '收藏';
});
</script>

文章列表展示收藏数

from django.db.models import Count

articles = Article.objects.annotate(fav_count=Count('favorite_set'))

我的收藏页

@login_required
def my_favorites(request):
    articles = Article.objects.filter(
        favorite_set__user=request.user
    ).select_related('category')
    return render(request, 'articles/favorites.html', {'articles': articles})
💡 提示

收藏切换用 get_or_create 一行搞定"存在则删、不存在则建";unique_together 保证数据层不重复。

类视图(CBV)

深入类视图(CBV):通用视图的覆盖点、混入(Mixin)、以及复杂场景实现。

ListViews 常用覆盖点

class ArticleList(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'
    paginate_by = 10

    # 1. 覆盖查询集
    def get_queryset(self):
        return Article.objects.filter(status='published')

    # 2. 覆盖上下文(传额外变量)
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        context['hot_articles'] = Article.objects.order_by('-views')[:5]
        return context

    # 3. 控制分页数量
    def get_paginate_by(self, queryset):
        return self.request.GET.get('size', 10)

CreateView 完整流程

from django.urls import reverse_lazy

class ArticleCreate(LoginRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'articles/form.html'
    success_url = reverse_lazy('articles:list')

    def form_valid(self, form):
        form.instance.author = self.request.user   # 自动填作者
        return super().form_valid(form)

UpdateView 权限控制

class ArticleUpdate(LoginRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm
    template_name = 'articles/form.html'

    def dispatch(self, request, *args, **kwargs):
        # 非作者/管理员禁止修改
        if request.user != self.get_object().author and not request.user.is_staff:
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

DeleteView 确认删除

class ArticleDelete(LoginRequiredMixin, DeleteView):
    model = Article
    success_url = reverse_lazy('articles:list')
    template_name = 'articles/confirm_delete.html'
<!-- confirm_delete.html -->
<form method="post">
  {% csrf_token %}
  <p>确定删除 "{{ object.title }}" 吗?</p>
  <button type="submit">确认删除</button>
  <a href="{% url 'articles:detail' object.slug %}">取消</a>
</form>

混入(Mixin)复用

class AuthorRequiredMixin:
    # 只有作者或管理员可操作

    def dispatch(self, request, *args, **kwargs):
        if request.user != self.get_object().author and not request.user.is_staff:
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

class ArticleUpdate(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm
    template_name = 'articles/form.html'
💡 提示

Mixin 顺序有讲究:LoginRequiredMixin 要放最左(最先执行),确保认证先于权限判断。

打包上线

博客项目打包上线:从开发到生产的完整流程。

上线检查清单

  • 关闭 DEBUG:DEBUG = False
  • 配置 ALLOWED_HOSTS
  • 收集静态文件:collectstatic
  • 执行数据库迁移:migrate
  • 配置 HTTPS + 安全头
  • 设置日志

生产 settings 分离

myblog/
├── settings/
│   ├── __init__.py      # from .prod import *
│   ├── base.py          # 公共配置
│   ├── dev.py           # 开发配置
│   └── prod.py          # 生产配置
# prod.py(节选)
from .base import *

DEBUG = False
ALLOWED_HOSTS = ['www.laowang.click']

import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']   # 从环境变量读取

STATIC_ROOT = '/www/wwwroot/myblog/staticfiles'
MEDIA_ROOT = '/www/wwwroot/myblog/media'

# 安全
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000

# 日志
LOGGING = {
    'version': 1,
    'handlers': {
        'file': {
            'class': 'logging.FileHandler',
            'filename': '/www/wwwroot/myblog/logs/django.log',
        },
    },
    'root': {'handlers': ['file'], 'level': 'INFO'},
}

部署步骤(宝塔)

# 1. 上传代码到服务器
# 2. 创建 venv 并安装依赖
cd /www/wwwroot/myblog
python3 -m venv venv
venv/bin/pip install -r requirements.txt

# 3. 迁移与收集静态
venv/bin/python manage.py migrate
venv/bin/python manage.py collectstatic --noinput

# 4. 启动 gunicorn(通过面板 Python 项目管理器)
gunicorn myblog.wsgi:application -w 4 -b 127.0.0.1:8001

Nginx 配置

server {
    listen 443 ssl http2;
    server_name www.laowang.click;

    ssl_certificate     /www/server/panel/vhost/cert/laowang/fullchain.pem;
    ssl_certificate_key /www/server/panel/vhost/cert/laowang/privkey.pem;

    location /static/ {
        alias /www/wwwroot/myblog/staticfiles/;
        expires 7d;
    }

    location /media/ {
        alias /www/wwwroot/myblog/media/;
    }

    location / {
        proxy_pass http://127.0.0.1:8001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

上线后验证

curl -I https://www.laowang.click/          # 200
python manage.py check --deploy             # 部署安全检查
⚠️ 注意

上线后执行 python manage.py check --deploy 做安全自查,重点确认 DEBUG=False、CSRF/HTTPS 配置。

第 1 / 31 节