Django全栈开发终极指南

Django 完整知识总结与使用教程

目录

  1. [Django 简介](#Django 简介)
  2. 环境安装与项目创建
  3. 项目结构详解
  4. [MTV 架构模式](#MTV 架构模式)
  5. [模型(Models)与 ORM](#模型(Models)与 ORM)
  6. 数据库迁移(Migrations)
  7. 视图(Views)
  8. [URL 路由配置](#URL 路由配置)
  9. 模板系统(Templates)
  10. 表单(Forms)
  11. 用户认证系统
  12. [Admin 后台管理](#Admin 后台管理)
  13. 静态文件与媒体文件
  14. 中间件(Middleware)
  15. 缓存系统
  16. 信号(Signals)
  17. [Django REST Framework(DRF)](#Django REST Framework(DRF))
  18. 安全性最佳实践
  19. 生产环境部署
  20. 性能优化技巧
  21. 测试
  22. 常用第三方包推荐
  23. 项目结构最佳实践

1. Django 简介

1.1 什么是 Django

Django 是一个基于 Python 的高级开源 Web 框架,遵循"为完美主义者准备的快速开发框架"的理念。由 Lawrence Journal-World 报社的 Adrian Holovaty 和 Simon Willison 于 2003 年创建,2005 年以 BSD 许可证开源发布,框架名字来自爵士吉他手 Django Reinhardt。

Django 采用 MTV(Model-Template-View)架构,其核心目标是简化复杂的、数据库驱动的 Web 应用开发,强调代码复用性、模块化与快速开发。

1.2 Django 的核心特点

特性 说明
极速开发 内置大量开箱即用的功能,让应用从概念到上线更快
安全可靠 内置防 SQL 注入、XSS、CSRF、点击劫持等安全机制
高度可扩展 Instagram、Mozilla、Disqus 等大型网站均基于 Django
完整文档 拥有业界最完善的框架文档之一
DRY 原则 Don't Repeat Yourself,减少冗余代码
电池包含 ORM、Admin、认证、模板等全套工具内置其中

1.3 Django 与其他框架对比

框架 语言 类型 适用场景
Django Python 全功能框架 中大型 Web 应用、API
Flask Python 微框架 小型应用、原型验证
FastAPI Python 现代异步框架 高性能 API、异步场景
Express JavaScript 微框架 Node.js 生态 API
Rails Ruby 全功能框架 快速开发

1.4 支持的数据库

Django 官方支持以下五种数据库后端:

  • PostgreSQL(推荐生产环境使用)
  • MySQL / MariaDB
  • SQLite(默认,适合开发环境)
  • Oracle
  • Microsoft SQL Server(通过 mssql-django 第三方包)

2. 环境安装与项目创建

2.1 安装 Django

bash 复制代码
# 创建虚拟环境(推荐)
python -m venv venv

# 激活虚拟环境
# macOS / Linux
source venv/bin/activate
# Windows
venv\Scripts\activate

# 安装 Django
pip install django

# 查看版本
django-admin --version

2.2 创建 Django 项目

bash 复制代码
# 创建新项目
django-admin startproject myproject

# 进入项目目录
cd myproject

# 创建应用
python manage.py startapp myapp

# 启动开发服务器
python manage.py runserver
# 默认地址: http://127.0.0.1:8000/

# 指定端口
python manage.py runserver 8080

2.3 常用 manage.py 命令

bash 复制代码
# 数据库迁移
python manage.py makemigrations     # 生成迁移文件
python manage.py migrate            # 执行迁移

# 创建超级管理员
python manage.py createsuperuser

# 打开 Django Shell
python manage.py shell

# 收集静态文件(生产环境)
python manage.py collectstatic

# 检查项目配置问题
python manage.py check
python manage.py check --deploy     # 检查生产环境配置

# 查看 SQL 语句(不执行)
python manage.py sqlmigrate myapp 0001

# 列出所有可用命令
python manage.py help

3. 项目结构详解

复制代码
myproject/
├── manage.py                  # 项目管理入口脚本
├── myproject/                 # 项目配置目录
│   ├── __init__.py
│   ├── settings.py            # 项目配置文件
│   ├── urls.py                # 根 URL 路由
│   ├── asgi.py                # ASGI 服务器入口(异步)
│   └── wsgi.py                # WSGI 服务器入口(同步)
├── myapp/                     # 应用目录
│   ├── __init__.py
│   ├── admin.py               # Admin 后台注册
│   ├── apps.py                # 应用配置类
│   ├── models.py              # 数据模型
│   ├── views.py               # 视图逻辑
│   ├── urls.py                # 应用 URL 路由(需手动创建)
│   ├── serializers.py         # 序列化器(DRF)
│   ├── forms.py               # 表单(需手动创建)
│   ├── tests.py               # 测试文件
│   └── migrations/            # 迁移文件目录
│       └── __init__.py
├── templates/                 # 模板目录
├── static/                    # 静态文件目录
└── requirements.txt           # 依赖包列表

3.1 多环境 settings 配置(最佳实践)

复制代码
myproject/
└── settings/
    ├── __init__.py
    ├── base.py        # 公共配置
    ├── development.py # 开发环境
    └── production.py  # 生产环境
python 复制代码
# base.py - 公共配置
from pathlib import Path
import os

BASE_DIR = Path(__file__).resolve().parent.parent.parent

SECRET_KEY = os.environ.get('SECRET_KEY')

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myproject.urls'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
python 复制代码
# development.py - 开发配置
from .base import *

DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
python 复制代码
# production.py - 生产配置
from .base import *
import os

DEBUG = False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

4. MTV 架构模式

Django 遵循 MTV(Model-Template-View)架构,本质上是 MVC 的变体:

复制代码
HTTP 请求
    │
    ▼
┌─────────────┐
│   URL 路由   │  urls.py --- 将 URL 映射到对应视图
└──────┬──────┘
       │
       ▼
┌─────────────┐
│    View     │  views.py --- 处理业务逻辑,调用 Model 和 Template
└──────┬──────┘
       │
    ┌──┴──┐
    ▼     ▼
┌───────┐ ┌──────────┐
│ Model │ │ Template │
└───────┘ └──────────┘
数据层      展示层
    │          │
    └────┬─────┘
         ▼
      HTTP 响应
组件 对应 MVC 职责
Model Model 数据定义与数据库交互
Template View 数据展示(HTML 渲染)
View Controller 业务逻辑处理

5. 模型(Models)与 ORM

5.1 定义模型

python 复制代码
# myapp/models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100, verbose_name='分类名称')
    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = '分类'
        verbose_name_plural = '分类列表'
        ordering = ['name']
    
    def __str__(self):
        return self.name


class Article(models.Model):
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]
    
    title = models.CharField(max_length=200, verbose_name='标题')
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='articles',
        verbose_name='作者'
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='articles'
    )
    tags = models.ManyToManyField('Tag', blank=True, related_name='articles')
    content = models.TextField(verbose_name='内容')
    summary = models.CharField(max_length=500, blank=True)
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default='draft'
    )
    views_count = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        verbose_name = '文章'
        verbose_name_plural = '文章列表'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['status', '-created_at']),
            models.Index(fields=['author', 'status']),
        ]
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        from django.urls import reverse
        return reverse('article-detail', kwargs={'slug': self.slug})


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    
    def __str__(self):
        return self.name

5.2 常用字段类型

python 复制代码
# 字符串类型
models.CharField(max_length=200)           # 短字符串
models.TextField()                          # 长文本
models.SlugField()                          # URL 友好的短标识
models.EmailField()                         # 邮箱
models.URLField()                           # URL
models.UUIDField(default=uuid.uuid4)        # UUID

# 数值类型
models.IntegerField()
models.PositiveIntegerField()
models.BigIntegerField()
models.FloatField()
models.DecimalField(max_digits=10, decimal_places=2)

# 布尔类型
models.BooleanField(default=True)

# 日期时间类型
models.DateField(auto_now=False, auto_now_add=False)
models.TimeField()
models.DateTimeField(auto_now_add=True)     # 创建时自动填充
models.DateTimeField(auto_now=True)         # 每次保存时更新

# 文件类型
models.FileField(upload_to='uploads/')
models.ImageField(upload_to='images/')

# 关系字段
models.ForeignKey(Model, on_delete=models.CASCADE)          # 多对一
models.OneToOneField(Model, on_delete=models.CASCADE)       # 一对一
models.ManyToManyField(Model, blank=True)                   # 多对多

# on_delete 选项
# CASCADE    --- 级联删除(删除父记录时同步删除子记录)
# SET_NULL   --- 设为 NULL(需要 null=True)
# SET_DEFAULT--- 设为默认值
# PROTECT    --- 阻止删除(抛出异常)
# DO_NOTHING --- 不做任何操作

5.3 QuerySet 查询 API

python 复制代码
from myapp.models import Article

# ======= 基础查询 =======
Article.objects.all()                           # 所有记录
Article.objects.filter(status='published')      # 过滤
Article.objects.exclude(status='draft')         # 排除
Article.objects.get(id=1)                       # 获取单条(不存在时抛异常)
Article.objects.first()                         # 第一条
Article.objects.last()                          # 最后一条
Article.objects.count()                         # 统计数量
Article.objects.exists()                        # 是否存在

# ======= 字段查找(Field Lookups)=======
Article.objects.filter(title__exact='Django')   # 精确匹配
Article.objects.filter(title__iexact='django')  # 不区分大小写
Article.objects.filter(title__contains='Django')# 包含
Article.objects.filter(title__icontains='django')
Article.objects.filter(title__startswith='Dj')
Article.objects.filter(title__endswith='go')
Article.objects.filter(views_count__gt=100)     # 大于
Article.objects.filter(views_count__gte=100)    # 大于等于
Article.objects.filter(views_count__lt=10)      # 小于
Article.objects.filter(views_count__lte=10)     # 小于等于
Article.objects.filter(views_count__in=[1,2,3]) # IN 查询
Article.objects.filter(published_at__isnull=True)# IS NULL
Article.objects.filter(
    created_at__year=2025,
    created_at__month=1
)

# ======= 排序 =======
Article.objects.order_by('title')               # 升序
Article.objects.order_by('-created_at')         # 降序
Article.objects.order_by('author', '-created_at')# 多字段排序

# ======= 分片/分页 =======
Article.objects.all()[0:10]                     # 前10条(不支持负索引)

# ======= 创建 / 更新 / 删除 =======
# 创建
article = Article.objects.create(title='新文章', ...)
article = Article(title='新文章', ...)
article.save()

# 获取或创建
article, created = Article.objects.get_or_create(
    slug='my-slug',
    defaults={'title': '我的文章', 'content': '...'}
)

# 更新
Article.objects.filter(status='draft').update(status='published')
article.title = '更新标题'
article.save()

# 删除
Article.objects.filter(status='archived').delete()
article.delete()

# ======= Q 对象(复杂查询)=======
from django.db.models import Q

# OR 查询
Article.objects.filter(Q(status='published') | Q(views_count__gt=1000))

# AND 查询
Article.objects.filter(Q(status='published') & Q(author__username='admin'))

# NOT 查询
Article.objects.filter(~Q(status='draft'))

# 复合条件
Article.objects.filter(
    Q(title__icontains='django') | Q(content__icontains='django'),
    status='published'
)

# ======= F 对象(字段间计算)=======
from django.db.models import F

# 字段间比较(无需 Python 层介入,直接在数据库层执行)
Article.objects.filter(views_count__gt=F('comments_count'))

# 原子更新(避免竞争条件)
Article.objects.filter(pk=1).update(views_count=F('views_count') + 1)

# ======= 聚合与注解 =======
from django.db.models import Count, Avg, Sum, Max, Min

# 聚合(返回单个值)
Article.objects.aggregate(total=Count('id'))
Article.objects.aggregate(avg_views=Avg('views_count'))

# 注解(为 QuerySet 每条记录增加计算字段)
from django.db.models import Count
authors = User.objects.annotate(article_count=Count('articles'))
for author in authors:
    print(author.username, author.article_count)

# ======= 关联查询优化 =======
# select_related:预加载 ForeignKey / OneToOneField(JOIN 查询,适合一对一/多对一)
articles = Article.objects.select_related('author', 'category')

# prefetch_related:预加载 ManyToMany / 反向关系(独立查询后 Python 层合并)
articles = Article.objects.prefetch_related('tags')

# 组合使用
articles = Article.objects.select_related('author').prefetch_related('tags', 'comments')

# ======= 原生 SQL =======
from django.db import connection

# 方式一:raw()
articles = Article.objects.raw('SELECT * FROM myapp_article WHERE status = %s', ['published'])

# 方式二:直接执行 SQL
with connection.cursor() as cursor:
    cursor.execute("UPDATE myapp_article SET views_count = 0")
    cursor.fetchall()

5.4 模型的 Meta 选项

python 复制代码
class Article(models.Model):
    class Meta:
        # 数据库表名
        db_table = 'blog_article'
        
        # 显示名称(Admin 使用)
        verbose_name = '文章'
        verbose_name_plural = '文章列表'
        
        # 默认排序
        ordering = ['-created_at']
        
        # 数据库索引(提升查询性能)
        indexes = [
            models.Index(fields=['status', '-created_at'], name='article_status_date_idx'),
        ]
        
        # 联合唯一约束
        unique_together = [['author', 'slug']]
        
        # 新版唯一约束写法(Django 4.0+)
        constraints = [
            models.UniqueConstraint(fields=['author', 'slug'], name='unique_author_slug'),
            models.CheckConstraint(check=models.Q(views_count__gte=0), name='views_count_positive'),
        ]
        
        # 权限
        permissions = [('can_publish', '可以发布文章')]

6. 数据库迁移(Migrations)

bash 复制代码
# 生成迁移文件(检测 models.py 变化)
python manage.py makemigrations
python manage.py makemigrations myapp          # 针对特定 app
python manage.py makemigrations --name add_slug_field  # 自定义名称

# 执行迁移
python manage.py migrate
python manage.py migrate myapp                 # 针对特定 app
python manage.py migrate myapp 0002            # 回滚/前进到指定版本

# 查看迁移状态
python manage.py showmigrations

# 查看迁移 SQL(不执行)
python manage.py sqlmigrate myapp 0001

# 伪迁移(标记为已完成,不实际执行)
python manage.py migrate --fake
python manage.py migrate --fake-initial        # 初始迁移已存在时使用

6.1 数据迁移(Data Migration)

python 复制代码
# 在迁移文件中编写数据迁移
from django.db import migrations

def populate_slugs(apps, schema_editor):
    """为已有文章生成 slug"""
    Article = apps.get_model('myapp', 'Article')
    from django.utils.text import slugify
    for article in Article.objects.all():
        article.slug = slugify(article.title)
        article.save()

def reverse_populate_slugs(apps, schema_editor):
    """回滚操作"""
    Article = apps.get_model('myapp', 'Article')
    Article.objects.update(slug='')

class Migration(migrations.Migration):
    dependencies = [
        ('myapp', '0001_initial'),
    ]
    
    operations = [
        migrations.RunPython(populate_slugs, reverse_populate_slugs),
    ]

7. 视图(Views)

7.1 函数视图(Function-Based Views)

python 复制代码
# myapp/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from .models import Article

def article_list(request):
    """文章列表视图"""
    articles = Article.objects.filter(status='published').select_related('author')
    
    # 搜索
    query = request.GET.get('q', '')
    if query:
        from django.db.models import Q
        articles = articles.filter(
            Q(title__icontains=query) | Q(content__icontains=query)
        )
    
    context = {
        'articles': articles,
        'query': query,
    }
    return render(request, 'myapp/article_list.html', context)


def article_detail(request, slug):
    """文章详情视图"""
    article = get_object_or_404(Article, slug=slug, status='published')
    
    # 更新阅读量(使用 F 表达式避免竞争条件)
    from django.db.models import F
    Article.objects.filter(pk=article.pk).update(views_count=F('views_count') + 1)
    
    return render(request, 'myapp/article_detail.html', {'article': article})


@login_required
@require_http_methods(["GET", "POST"])
def article_create(request):
    """创建文章视图"""
    from .forms import ArticleForm
    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('article-detail', slug=article.slug)
    else:
        form = ArticleForm()
    return render(request, 'myapp/article_form.html', {'form': form})

7.2 类视图(Class-Based Views)

python 复制代码
from django.views import View
from django.views.generic import (
    ListView, DetailView, CreateView, UpdateView, DeleteView,
    TemplateView, RedirectView
)
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy

class ArticleListView(ListView):
    model = Article
    template_name = 'myapp/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10                           # 每页显示条数
    
    def get_queryset(self):
        queryset = Article.objects.filter(
            status='published'
        ).select_related('author', 'category')
        
        query = self.request.GET.get('q')
        if query:
            from django.db.models import Q
            queryset = queryset.filter(
                Q(title__icontains=query) | Q(content__icontains=query)
            )
        return queryset
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['query'] = self.request.GET.get('q', '')
        return context


class ArticleDetailView(DetailView):
    model = Article
    template_name = 'myapp/article_detail.html'
    context_object_name = 'article'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'
    
    def get_queryset(self):
        return Article.objects.filter(status='published')


class ArticleCreateView(LoginRequiredMixin, CreateView):
    model = Article
    fields = ['title', 'content', 'category', 'tags', 'status']
    template_name = 'myapp/article_form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def get_success_url(self):
        return reverse_lazy('article-detail', kwargs={'slug': self.object.slug})


class ArticleUpdateView(LoginRequiredMixin, UpdateView):
    model = Article
    fields = ['title', 'content', 'category', 'tags', 'status']
    template_name = 'myapp/article_form.html'
    
    def get_queryset(self):
        # 只允许编辑自己的文章
        return Article.objects.filter(author=self.request.user)


class ArticleDeleteView(LoginRequiredMixin, DeleteView):
    model = Article
    template_name = 'myapp/article_confirm_delete.html'
    success_url = reverse_lazy('article-list')
    
    def get_queryset(self):
        return Article.objects.filter(author=self.request.user)

8. URL 路由配置

8.1 根 URL 配置

python 复制代码
# myproject/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('myapp.urls')),
    path('api/', include('myapp.api_urls')),
    path('api/auth/', include('rest_framework.urls')),  # DRF 登录
]

# 开发环境下提供媒体文件
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

8.2 应用 URL 配置

python 复制代码
# myapp/urls.py
from django.urls import path, re_path
from . import views

app_name = 'myapp'  # URL 命名空间

urlpatterns = [
    # 文章列表
    path('', views.ArticleListView.as_view(), name='article-list'),
    
    # 文章详情(使用 slug)
    path('articles/<slug:slug>/', views.ArticleDetailView.as_view(), name='article-detail'),
    
    # 文章创建/编辑/删除
    path('articles/create/', views.ArticleCreateView.as_view(), name='article-create'),
    path('articles/<slug:slug>/edit/', views.ArticleUpdateView.as_view(), name='article-update'),
    path('articles/<slug:slug>/delete/', views.ArticleDeleteView.as_view(), name='article-delete'),
    
    # 带参数的 URL
    path('articles/<int:pk>/', views.article_detail_by_pk, name='article-detail-pk'),
    
    # 正则表达式 URL
    re_path(r'^archive/(?P<year>[0-9]{4})/$', views.archive, name='archive'),
]

8.3 URL 参数类型转换器

转换器 说明 示例
str 任意非空字符串(默认) <str:name>
int 正整数 <int:pk>
slug ASCII 字母、数字、连字符、下划线 <slug:slug>
uuid UUID 格式 <uuid:id>
path 包含斜杠的路径字符串 <path:file_path>

8.4 URL 反向解析

python 复制代码
# 在视图中
from django.urls import reverse
url = reverse('myapp:article-detail', kwargs={'slug': 'my-article'})

# 在模板中
{% url 'myapp:article-detail' slug=article.slug %}

# 在模型中(get_absolute_url)
def get_absolute_url(self):
    return reverse('myapp:article-detail', kwargs={'slug': self.slug})

9. 模板系统(Templates)

9.1 配置模板目录

python 复制代码
# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # 全局模板目录
        'APP_DIRS': True,                   # 自动查找各 app 的 templates 目录
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

9.2 基础模板(base.html)

html 复制代码
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的网站{% endblock %}</title>
    {% load static %}
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <nav>
        <a href="{% url 'myapp:article-list' %}">首页</a>
        {% if user.is_authenticated %}
            <span>欢迎,{{ user.username }}</span>
            <a href="{% url 'logout' %}">退出</a>
        {% else %}
            <a href="{% url 'login' %}">登录</a>
        {% endif %}
    </nav>
    
    <!-- 消息提示 -->
    {% if messages %}
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2025 我的网站</p>
    </footer>
    
    <script src="{% static 'js/main.js' %}"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

9.3 子模板继承

html 复制代码
<!-- templates/myapp/article_list.html -->
{% extends 'base.html' %}
{% load static %}

{% block title %}文章列表 - 我的网站{% endblock %}

{% block content %}
<div class="container">
    <h1>文章列表</h1>
    
    <!-- 搜索框 -->
    <form method="get">
        <input type="text" name="q" value="{{ query }}" placeholder="搜索文章...">
        <button type="submit">搜索</button>
    </form>
    
    <!-- 文章列表 -->
    {% for article in articles %}
    <article>
        <h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
        <p>作者:{{ article.author.get_full_name|default:article.author.username }}</p>
        <p>{{ article.created_at|date:"Y年m月d日" }}</p>
        <p>{{ article.summary|truncatechars:150 }}</p>
    </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 %}
</div>
{% endblock %}

9.4 常用模板标签与过滤器

django 复制代码
{# 标签 #}
{% if condition %}...{% elif condition %}...{% else %}...{% endif %}
{% for item in list %}...{% empty %}...{% endfor %}
{% with total=article.views_count %}{{ total }}{% endwith %}
{% include 'partial/card.html' with article=article %}
{% block name %}...{% endblock %}
{% extends 'base.html' %}
{% load static %}{% static 'css/main.css' %}
{% url 'myapp:article-detail' slug=article.slug %}
{% csrf_token %}

{# 常用过滤器 #}
{{ value|default:"无" }}              {# 默认值 #}
{{ value|length }}                     {# 长度 #}
{{ value|truncatechars:50 }}           {# 截断字符数 #}
{{ value|truncatewords:20 }}           {# 截断单词数 #}
{{ value|upper }}                      {# 大写 #}
{{ value|lower }}                      {# 小写 #}
{{ value|title }}                      {# 首字母大写 #}
{{ value|date:"Y-m-d H:i" }}           {# 日期格式化 #}
{{ value|timesince }}                  {# 距今多久 #}
{{ value|linebreaks }}                 {# 换行转 <br> #}
{{ value|safe }}                       {# 标记为安全 HTML(谨慎使用)#}
{{ value|escape }}                     {# 转义 HTML #}
{{ value|floatformat:2 }}              {# 浮点数格式化 #}
{{ value|add:10 }}                     {# 加法 #}
{{ list|join:", " }}                   {# 列表连接 #}
{{ value|yesno:"是,否,未知" }}         {# 布尔值转换 #}

9.5 自定义模板标签和过滤器

python 复制代码
# myapp/templatetags/myapp_tags.py
from django import template
from django.utils.html import format_html

register = template.Library()

# 自定义过滤器
@register.filter(name='reading_time')
def reading_time(content):
    """估算阅读时间"""
    word_count = len(content.split())
    minutes = max(1, word_count // 200)
    return f'{minutes} 分钟'

# 简单标签
@register.simple_tag
def get_recent_articles(count=5):
    from myapp.models import Article
    return Article.objects.filter(status='published')[:count]

# 包含标签
@register.inclusion_tag('myapp/tags/recent_articles.html')
def show_recent_articles(count=5):
    from myapp.models import Article
    articles = Article.objects.filter(status='published')[:count]
    return {'articles': articles}
django 复制代码
{# 模板中使用 #}
{% load myapp_tags %}

{{ article.content|reading_time }}

{% get_recent_articles as recent_articles %}
{% for article in recent_articles %}
    <a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
{% endfor %}

{% show_recent_articles 5 %}

10. 表单(Forms)

10.1 基础表单

python 复制代码
# myapp/forms.py
from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    """文章表单"""
    class Meta:
        model = Article
        fields = ['title', 'slug', 'category', 'content', 'summary', 'status']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '文章标题'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
            'summary': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
            'status': forms.Select(attrs={'class': 'form-select'}),
        }
        labels = {
            'title': '文章标题',
            'content': '内容',
        }
        help_texts = {
            'slug': '用于 URL,只能包含字母、数字和连字符',
        }
    
    def clean_title(self):
        """自定义标题验证"""
        title = self.cleaned_data.get('title')
        if len(title) < 5:
            raise forms.ValidationError('标题至少需要5个字符')
        return title
    
    def clean(self):
        """跨字段验证"""
        cleaned_data = super().clean()
        status = cleaned_data.get('status')
        content = cleaned_data.get('content')
        if status == 'published' and not content:
            raise forms.ValidationError('发布的文章必须有内容')
        return cleaned_data


class SearchForm(forms.Form):
    """搜索表单"""
    q = forms.CharField(
        max_length=100,
        required=False,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': '搜索...'
        })
    )
    category = forms.ChoiceField(
        choices=[('', '全部分类')] + [('tech', '技术'), ('life', '生活')],
        required=False
    )

10.2 表单模板渲染

html 复制代码
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    {# 渲染所有字段(快速方式)#}
    {{ form.as_p }}
    
    {# 或逐字段渲染(灵活方式)#}
    {% for field in form %}
        <div class="form-group">
            {{ field.label_tag }}
            {{ field }}
            {% if field.help_text %}
                <small class="form-text">{{ field.help_text }}</small>
            {% endif %}
            {% if field.errors %}
                <ul class="errorlist">
                    {% for error in field.errors %}
                        <li>{{ error }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        </div>
    {% endfor %}
    
    {# 非字段错误 #}
    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            {{ form.non_field_errors }}
        </div>
    {% endif %}
    
    <button type="submit" class="btn btn-primary">提交</button>
</form>

11. 用户认证系统

11.1 配置认证 URL

python 复制代码
# urls.py
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(template_name='auth/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('password-change/', auth_views.PasswordChangeView.as_view(), name='password-change'),
    path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password-change-done'),
    path('password-reset/', auth_views.PasswordResetView.as_view(), name='password-reset'),
    path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password-reset-done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password-reset-confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password-reset-complete'),
]

11.2 自定义用户模型(强烈推荐在项目初始时配置)

python 复制代码
# myapp/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    """自定义用户模型"""
    bio = models.TextField(blank=True, verbose_name='个人简介')
    avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
    website = models.URLField(blank=True)
    
    class Meta:
        verbose_name = '用户'
        verbose_name_plural = '用户列表'
    
    def __str__(self):
        return self.username

# settings.py
AUTH_USER_MODEL = 'myapp.CustomUser'  # 必须在首次迁移前设置

11.3 视图中使用认证

python 复制代码
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth import authenticate, login, logout
from django.contrib import messages

# 函数视图装饰器
@login_required(login_url='/login/')
def dashboard(request):
    return render(request, 'dashboard.html')

@permission_required('myapp.can_publish', raise_exception=True)
def publish_article(request, pk):
    ...

# 自定义登录视图
def custom_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)
            messages.success(request, '登录成功!')
            return redirect('dashboard')
        else:
            messages.error(request, '用户名或密码错误')
    return render(request, 'auth/login.html')

# 模板中判断权限
# {{ user.is_authenticated }}
# {{ user.is_staff }}
# {% if perms.myapp.can_publish %}...{% endif %}

12. Admin 后台管理

12.1 基础注册

python 复制代码
# myapp/admin.py
from django.contrib import admin
from .models import Article, Category, Tag

# 简单注册
admin.site.register(Tag)

# 自定义注册
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug', 'created_at']
    search_fields = ['name']
    prepopulated_fields = {'slug': ('name',)}


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    # 列表页设置
    list_display = ['title', 'author', 'category', 'status', 'views_count', 'created_at']
    list_filter = ['status', 'category', 'created_at']
    search_fields = ['title', 'content', 'author__username']
    list_per_page = 20
    list_editable = ['status']
    date_hierarchy = 'created_at'
    
    # 详情页设置
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ['author']                      # 大量用户时使用 ID 输入
    filter_horizontal = ['tags']                    # 多对多字段的水平选择器
    readonly_fields = ['views_count', 'created_at', 'updated_at']
    
    fieldsets = (
        ('基本信息', {
            'fields': ('title', 'slug', 'author', 'category', 'tags')
        }),
        ('内容', {
            'fields': ('content', 'summary'),
        }),
        ('发布设置', {
            'fields': ('status', 'published_at'),
        }),
        ('统计信息', {
            'fields': ('views_count', 'created_at', 'updated_at'),
            'classes': ('collapse',),  # 默认折叠
        }),
    )
    
    # 自定义操作
    actions = ['publish_articles', 'archive_articles']
    
    @admin.action(description='发布选中的文章')
    def publish_articles(self, request, queryset):
        count = queryset.update(status='published')
        self.message_user(request, f'已发布 {count} 篇文章')
    
    @admin.action(description='归档选中的文章')
    def archive_articles(self, request, queryset):
        count = queryset.update(status='archived')
        self.message_user(request, f'已归档 {count} 篇文章')


# 自定义 Admin 站点信息
admin.site.site_header = '我的博客管理后台'
admin.site.site_title = '博客管理'
admin.site.index_title = '欢迎使用博客管理系统'

13. 静态文件与媒体文件

13.1 静态文件配置

python 复制代码
# settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']   # 开发环境:额外静态文件目录
STATIC_ROOT = BASE_DIR / 'staticfiles'     # 生产环境:collectstatic 目标目录

# 媒体文件(用户上传)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
python 复制代码
# 开发环境 URL 配置(urls.py)
from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
django 复制代码
{# 模板中使用静态文件 #}
{% load static %}
<img src="{% static 'images/logo.png' %}" alt="Logo">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<script src="{% static 'js/app.js' %}"></script>

{# 模板中使用媒体文件 #}
{% if user.avatar %}
    <img src="{{ user.avatar.url }}" alt="{{ user.username }}">
{% endif %}

14. 中间件(Middleware)

14.1 内置中间件说明

python 复制代码
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',          # 安全增强(HTTPS重定向、安全头)
    'django.contrib.sessions.middleware.SessionMiddleware',   # Session 支持
    'django.middleware.common.CommonMiddleware',               # URL 规范化(尾部斜杠等)
    'django.middleware.csrf.CsrfViewMiddleware',               # CSRF 防护
    'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证(request.user)
    'django.contrib.messages.middleware.MessageMiddleware',    # 消息框架
    'django.middleware.clickjacking.XFrameOptionsMiddleware',  # 防点击劫持
]

14.2 自定义中间件

python 复制代码
# myapp/middleware.py
import time
import logging

logger = logging.getLogger(__name__)

class RequestTimingMiddleware:
    """记录请求处理时间"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        start_time = time.time()
        response = self.get_response(request)
        duration = time.time() - start_time
        
        logger.info(
            f'{request.method} {request.path} '
            f'[{response.status_code}] {duration:.3f}s'
        )
        
        # 在响应头中添加处理时间
        response['X-Request-Duration'] = f'{duration:.3f}'
        return response


class MaintenanceModeMiddleware:
    """维护模式中间件"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        from django.conf import settings
        from django.shortcuts import render
        
        if getattr(settings, 'MAINTENANCE_MODE', False):
            if not request.path.startswith('/admin/'):
                return render(request, 'maintenance.html', status=503)
        
        return self.get_response(request)
python 复制代码
# settings.py 注册中间件
MIDDLEWARE = [
    ...
    'myapp.middleware.RequestTimingMiddleware',
    'myapp.middleware.MaintenanceModeMiddleware',
]

15. 缓存系统

15.1 缓存配置

python 复制代码
# settings.py

# 内存缓存(开发环境)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    }
}

# Redis 缓存(生产环境推荐)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        },
        'TIMEOUT': 300,  # 默认超时时间(秒)
    }
}

15.2 缓存使用

python 复制代码
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie

# ===== 低级缓存 API =====
# 设置缓存
cache.set('my_key', 'hello', timeout=60)   # 60秒超时
cache.set_many({'key1': 'v1', 'key2': 'v2'}, timeout=300)

# 获取缓存
value = cache.get('my_key')
value = cache.get('my_key', default='default_value')
values = cache.get_many(['key1', 'key2'])

# 删除缓存
cache.delete('my_key')
cache.delete_many(['key1', 'key2'])
cache.clear()                              # 清空所有缓存(谨慎使用)

# ===== 视图缓存 =====
@cache_page(60 * 15)                       # 缓存15分钟
def article_list(request):
    ...

# ===== 模板片段缓存 =====
# {% load cache %}
# {% cache 300 sidebar request.user.id %}
#     ...侧边栏内容...
# {% endcache %}

16. 信号(Signals)

Django 信号允许特定的 sender 在特定操作发生时通知一组 receivers。

python 复制代码
# myapp/signals.py
from django.db.models.signals import post_save, pre_delete, m2m_changed
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Article

User = get_user_model()

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    """用户创建时自动创建 Profile"""
    if created:
        from .models import UserProfile
        UserProfile.objects.create(user=instance)

@receiver(post_save, sender=Article)
def article_published_notification(sender, instance, created, **kwargs):
    """文章发布时发送通知"""
    if not created and instance.status == 'published':
        # 发送邮件通知订阅者等
        pass

@receiver(pre_delete, sender=Article)
def article_pre_delete(sender, instance, **kwargs):
    """文章删除前的操作"""
    # 清理相关缓存
    from django.core.cache import cache
    cache.delete(f'article_{instance.pk}')
python 复制代码
# myapp/apps.py
class MyappConfig(AppConfig):
    name = 'myapp'
    
    def ready(self):
        import myapp.signals  # 注册信号

17. Django REST Framework(DRF)

17.1 安装与配置

bash 复制代码
pip install djangorestframework
pip install django-filter
pip install markdown
python 复制代码
# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'django_filters',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
        # JWT 认证(需安装 djangorestframework-simplejwt)
        # 'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.UserRateThrottle',
        'rest_framework.throttling.AnonRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'user': '100/hour',
        'anon': '20/hour',
    },
}

17.2 序列化器(Serializers)

python 复制代码
# myapp/serializers.py
from rest_framework import serializers
from .models import Article, Category, Tag
from django.contrib.auth import get_user_model

User = get_user_model()

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'name']


class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']


class CategorySerializer(serializers.ModelSerializer):
    article_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Category
        fields = ['id', 'name', 'slug', 'article_count']
    
    def get_article_count(self, obj):
        return obj.articles.filter(status='published').count()


class ArticleListSerializer(serializers.ModelSerializer):
    """列表序列化器(轻量级)"""
    author = AuthorSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    
    class Meta:
        model = Article
        fields = ['id', 'title', 'slug', 'author', 'category', 'tags',
                  'summary', 'status', 'views_count', 'created_at']


class ArticleDetailSerializer(serializers.ModelSerializer):
    """详情序列化器(完整字段)"""
    author = AuthorSerializer(read_only=True)
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(), source='category', write_only=True, required=False
    )
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    tag_ids = serializers.PrimaryKeyRelatedField(
        queryset=Tag.objects.all(), source='tags', many=True, write_only=True, required=False
    )
    
    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'author', 'category', 'category_id',
            'tags', 'tag_ids', 'content', 'summary', 'status',
            'views_count', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'author', 'views_count', 'created_at', 'updated_at']
    
    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError('标题至少5个字符')
        return value
    
    def create(self, validated_data):
        tags = validated_data.pop('tags', [])
        article = Article.objects.create(**validated_data)
        article.tags.set(tags)
        return article
    
    def update(self, instance, validated_data):
        tags = validated_data.pop('tags', None)
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        if tags is not None:
            instance.tags.set(tags)
        return instance

17.3 API 视图

python 复制代码
# myapp/views.py (API 视图)
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Article
from .serializers import ArticleListSerializer, ArticleDetailSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    """文章视图集"""
    queryset = Article.objects.select_related('author', 'category').prefetch_related('tags')
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['status', 'category', 'author']
    search_fields = ['title', 'content', 'author__username']
    ordering_fields = ['created_at', 'views_count']
    ordering = ['-created_at']
    lookup_field = 'slug'
    
    def get_serializer_class(self):
        if self.action == 'list':
            return ArticleListSerializer
        return ArticleDetailSerializer
    
    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action == 'list' and not self.request.user.is_staff:
            queryset = queryset.filter(status='published')
        return queryset
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def publish(self, request, slug=None):
        """发布文章"""
        article = self.get_object()
        if article.author != request.user:
            return Response({'detail': '无权操作'}, status=status.HTTP_403_FORBIDDEN)
        article.status = 'published'
        article.save()
        return Response({'detail': '已发布'})
    
    @action(detail=False, methods=['get'])
    def trending(self, request):
        """热门文章"""
        articles = self.get_queryset().filter(
            status='published'
        ).order_by('-views_count')[:10]
        serializer = ArticleListSerializer(articles, many=True, context={'request': request})
        return Response(serializer.data)

17.4 DRF URL 配置(Router)

python 复制代码
# myapp/api_urls.py
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet, CategoryViewSet, TagViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'tags', TagViewSet, basename='tag')

urlpatterns = router.urls

# 自动生成的 URL:
# GET  /api/articles/           - 列表
# POST /api/articles/           - 创建
# GET  /api/articles/{slug}/    - 详情
# PUT  /api/articles/{slug}/    - 全量更新
# PATCH /api/articles/{slug}/   - 部分更新
# DELETE /api/articles/{slug}/  - 删除
# POST /api/articles/{slug}/publish/   - 自定义 action
# GET  /api/articles/trending/         - 自定义 action

17.5 JWT 认证

bash 复制代码
pip install djangorestframework-simplejwt
python 复制代码
# settings.py
from datetime import timedelta

INSTALLED_APPS += ['rest_framework_simplejwt']

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
}

# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns += [
    path('api/token/', TokenObtainPairView.as_view(), name='token-obtain-pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token-refresh'),
]

18. 安全性最佳实践

18.1 生产环境安全配置

python 复制代码
# production settings.py

# 关闭调试模式(最重要)
DEBUG = False

# 必须设置允许的主机
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# 使用环境变量存储 SECRET_KEY
import os
SECRET_KEY = os.environ.get('SECRET_KEY')

# HTTPS 相关
SECURE_SSL_REDIRECT = True                    # HTTP 重定向到 HTTPS
SESSION_COOKIE_SECURE = True                  # Session Cookie 仅通过 HTTPS 传输
CSRF_COOKIE_SECURE = True                     # CSRF Cookie 仅通过 HTTPS 传输
SECURE_HSTS_SECONDS = 31536000               # HSTS:1年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# 其他安全头
SECURE_BROWSER_XSS_FILTER = True             # XSS 过滤
SECURE_CONTENT_TYPE_NOSNIFF = True           # 防 MIME 类型嗅探
X_FRAME_OPTIONS = 'DENY'                     # 防点击劫持
SECURE_REFERRER_POLICY = 'same-origin'

18.2 Django 内置安全机制

安全威胁 Django 防护机制
SQL 注入 ORM 参数化查询(默认启用)
XSS 跨站脚本 模板自动 HTML 转义(默认启用)
CSRF 跨站请求伪造 CsrfViewMiddleware + {% csrf_token %}(默认启用)
点击劫持 XFrameOptionsMiddleware(默认启用)
敏感数据泄露 DEBUG=False 关闭错误详情
密码安全 PBKDF2、bcrypt 等哈希算法(默认启用)
文件上传漏洞 不执行上传文件(需 Web 服务器配合)

18.3 Admin 安全加固

python 复制代码
# 修改默认 admin 路径
urlpatterns = [
    path('secret-admin-path/', admin.site.urls),  # 不使用默认 /admin/
]

# 添加 IP 白名单限制(中间件实现)
ADMIN_IP_WHITELIST = ['192.168.1.0/24']

18.4 敏感配置通过环境变量管理

bash 复制代码
# .env 文件(不提交到版本控制)
SECRET_KEY=your-random-secret-key
DEBUG=False
DB_NAME=mydb
DB_USER=myuser
DB_PASSWORD=securepassword
ALLOWED_HOSTS=yourdomain.com
python 复制代码
# 使用 python-decouple 读取
from decouple import config

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS').split(',')

19. 生产环境部署

19.1 部署架构

复制代码
Internet
   │
   ▼
Nginx(反向代理 + 静态文件服务)
   │
   ▼
Gunicorn / uWSGI(WSGI 服务器)
   │
   ▼
Django 应用
   │
   ▼
PostgreSQL + Redis(数据库 + 缓存)

19.2 Gunicorn 配置

bash 复制代码
# 安装
pip install gunicorn

# 启动(基础)
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000

# 生产配置
gunicorn myproject.wsgi:application \
    --bind 0.0.0.0:8000 \
    --workers 4 \          # CPU核心数 * 2 + 1
    --worker-class sync \  # 或 gevent(异步)
    --timeout 30 \
    --access-logfile /var/log/gunicorn/access.log \
    --error-logfile /var/log/gunicorn/error.log \
    --log-level info
ini 复制代码
# gunicorn.conf.py
bind = '0.0.0.0:8000'
workers = 4
worker_class = 'sync'
timeout = 30
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'
preload_app = True

19.3 Nginx 配置

nginx 复制代码
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
    
    ssl_certificate /etc/ssl/certs/yourdomain.pem;
    ssl_certificate_key /etc/ssl/private/yourdomain.key;
    
    # 静态文件
    location /static/ {
        alias /var/www/myproject/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
    
    # 媒体文件
    location /media/ {
        alias /var/www/myproject/media/;
        expires 7d;
    }
    
    # Django 应用
    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 X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 30s;
        proxy_read_timeout 30s;
    }
}

19.4 Systemd 服务配置

ini 复制代码
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for Django project
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn \
          --config /var/www/myproject/gunicorn.conf.py \
          myproject.wsgi:application
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
bash 复制代码
# 启动服务
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl status gunicorn

# 部署脚本
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate --settings=myproject.settings.production
python manage.py collectstatic --noinput --settings=myproject.settings.production
sudo systemctl reload gunicorn

19.5 部署前检查清单

bash 复制代码
# Django 内置部署检查
python manage.py check --deploy --settings=myproject.settings.production
检查项 状态
DEBUG = False
SECRET_KEY 使用环境变量
ALLOWED_HOSTS 已配置
使用 PostgreSQL
HTTPS 相关配置已启用
静态文件已收集(collectstatic)
数据库已迁移
媒体文件目录权限正确
日志系统已配置
错误监控(Sentry 等)已接入

20. 性能优化技巧

20.1 数据库查询优化

python 复制代码
# ❌ N+1 问题(每篇文章都会额外查询作者)
articles = Article.objects.filter(status='published')
for article in articles:
    print(article.author.username)  # 每次都发起查询

# ✅ 使用 select_related 预加载(ForeignKey/OneToOne)
articles = Article.objects.filter(status='published').select_related('author', 'category')

# ✅ 使用 prefetch_related 预加载(ManyToMany/反向关系)
articles = Article.objects.filter(status='published').prefetch_related('tags', 'comments')

# ✅ 只获取需要的字段
Article.objects.values('id', 'title', 'created_at')     # 返回字典
Article.objects.values_list('id', 'title', flat=False)   # 返回元组列表
Article.objects.only('id', 'title')                      # 延迟加载其他字段
Article.objects.defer('content')                          # 延迟加载大字段

# ✅ 使用 iterator() 处理大量数据(避免内存溢出)
for article in Article.objects.all().iterator(chunk_size=1000):
    process(article)

# ✅ 使用数据库索引
class Article(models.Model):
    class Meta:
        indexes = [
            models.Index(fields=['status', '-created_at']),
        ]

20.2 缓存策略

python 复制代码
# 低级缓存:手动缓存复杂查询
def get_trending_articles():
    cache_key = 'trending_articles'
    result = cache.get(cache_key)
    if result is None:
        result = list(
            Article.objects.filter(status='published')
                   .order_by('-views_count')[:10]
                   .values('id', 'title', 'slug', 'views_count')
        )
        cache.set(cache_key, result, timeout=300)  # 5分钟
    return result

# 视图缓存
@cache_page(60 * 15)
def article_list(request):
    ...

# 模板片段缓存
# {% cache 300 article_sidebar %}
# {% endcache %}

20.3 异步视图(Django 4.1+)

python 复制代码
# 异步视图(ASGI 部署时性能更优)
import asyncio
from django.http import HttpResponse

async def async_view(request):
    # 并发执行多个异步操作
    result1, result2 = await asyncio.gather(
        fetch_data_1(),
        fetch_data_2(),
    )
    return HttpResponse(f'{result1} {result2}')

# 异步 ORM 查询
async def async_article_list(request):
    articles = await Article.objects.filter(status='published').acount()
    # 注意:完整的异步 ORM 支持在 Django 4.2+ 持续改善
    return HttpResponse(f'共 {articles} 篇文章')

21. 测试

21.1 单元测试

python 复制代码
# myapp/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from .models import Article, Category

User = get_user_model()

class ArticleModelTest(TestCase):
    """模型测试"""
    
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            password='testpassword123'
        )
        self.category = Category.objects.create(name='技术', slug='tech')
    
    def test_create_article(self):
        """测试创建文章"""
        article = Article.objects.create(
            title='测试文章',
            slug='test-article',
            author=self.user,
            category=self.category,
            content='这是测试内容',
            status='published'
        )
        self.assertEqual(article.title, '测试文章')
        self.assertEqual(str(article), '测试文章')
    
    def test_article_ordering(self):
        """测试文章排序"""
        article1 = Article.objects.create(
            title='文章1', slug='article-1', author=self.user, content='...'
        )
        article2 = Article.objects.create(
            title='文章2', slug='article-2', author=self.user, content='...'
        )
        articles = Article.objects.all()
        self.assertEqual(articles[0], article2)  # 最新文章在前


class ArticleViewTest(TestCase):
    """视图测试"""
    
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser', password='testpassword123'
        )
        self.article = Article.objects.create(
            title='测试文章', slug='test-article',
            author=self.user, content='内容', status='published'
        )
    
    def test_article_list_view(self):
        """测试文章列表页"""
        response = self.client.get(reverse('myapp:article-list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
        self.assertTemplateUsed(response, 'myapp/article_list.html')
    
    def test_article_detail_view(self):
        """测试文章详情页"""
        response = self.client.get(
            reverse('myapp:article-detail', kwargs={'slug': 'test-article'})
        )
        self.assertEqual(response.status_code, 200)
    
    def test_article_create_requires_login(self):
        """测试创建文章需要登录"""
        response = self.client.get(reverse('myapp:article-create'))
        self.assertRedirects(response, '/login/?next=/articles/create/')
    
    def test_article_create_logged_in(self):
        """测试登录后创建文章"""
        self.client.login(username='testuser', password='testpassword123')
        response = self.client.post(reverse('myapp:article-create'), {
            'title': '新文章标题',
            'slug': 'new-article',
            'content': '文章内容',
            'status': 'draft',
        })
        self.assertEqual(Article.objects.count(), 2)

21.2 API 测试(DRF)

python 复制代码
from rest_framework.test import APITestCase, APIClient
from rest_framework import status

class ArticleAPITest(APITestCase):
    
    def setUp(self):
        self.user = User.objects.create_user(
            username='apiuser', password='apipassword123'
        )
        self.client = APIClient()
    
    def test_list_articles_unauthenticated(self):
        """未认证用户可以查看文章列表"""
        response = self.client.get('/api/articles/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_create_article_authenticated(self):
        """认证用户可以创建文章"""
        self.client.force_authenticate(user=self.user)
        data = {
            'title': 'API 测试文章',
            'slug': 'api-test-article',
            'content': '内容',
            'status': 'draft',
        }
        response = self.client.post('/api/articles/', data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    
    def test_create_article_unauthenticated(self):
        """未认证用户不能创建文章"""
        data = {'title': '文章', 'slug': 'article', 'content': '内容'}
        response = self.client.post('/api/articles/', data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

21.3 运行测试

bash 复制代码
# 运行所有测试
python manage.py test

# 运行特定 app 的测试
python manage.py test myapp

# 运行特定测试类或方法
python manage.py test myapp.tests.ArticleModelTest
python manage.py test myapp.tests.ArticleModelTest.test_create_article

# 显示详细输出
python manage.py test -v 2

# 代码覆盖率报告
pip install coverage
coverage run manage.py test
coverage report
coverage html  # 生成 HTML 报告

22. 常用第三方包推荐

22.1 认证与权限

包名 功能
django-allauth 社交登录(GitHub、Google 等)+ 邮箱认证
djangorestframework-simplejwt JWT 认证
django-guardian 对象级权限控制
dj-rest-auth DRF 认证 REST API

22.2 数据库与 ORM

包名 功能
psycopg2-binary PostgreSQL 数据库驱动
django-redis Redis 缓存集成
django-model-utils 模型工具(时间戳 Mixin、软删除等)
django-mptt 树形结构数据(分类、评论嵌套)

22.3 API 与序列化

包名 功能
djangorestframework RESTful API 框架
django-filter 查询过滤
drf-spectacular OpenAPI/Swagger 文档自动生成
django-cors-headers 跨域请求处理(CORS)

22.4 任务队列与异步

包名 功能
celery 分布式任务队列
django-celery-beat 定时任务调度
channels WebSocket 与实时通信(ASGI)

22.5 部署与运维

包名 功能
gunicorn WSGI 生产服务器
whitenoise 静态文件服务(无需 Nginx)
python-decouple 环境变量管理
sentry-sdk 错误监控
django-storages 云存储(AWS S3、阿里云 OSS)

22.6 开发工具

包名 功能
django-debug-toolbar 开发调试工具栏(SQL 查询分析)
django-extensions 扩展命令(shell_plus、reset_db 等)
factory_boy 测试数据工厂
pytest-django pytest 与 Django 集成
black + flake8 代码格式化与风格检查

23. 项目结构最佳实践

23.1 推荐的项目结构

复制代码
myproject/
├── .env                        # 环境变量(不提交 Git)
├── .env.example                # 环境变量示例
├── .gitignore
├── requirements/
│   ├── base.txt                # 公共依赖
│   ├── development.txt         # 开发依赖
│   └── production.txt          # 生产依赖
├── manage.py
├── myproject/
│   ├── settings/
│   │   ├── base.py
│   │   ├── development.py
│   │   └── production.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
├── apps/
│   ├── accounts/               # 用户认证 App
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   ├── admin.py
│   │   └── tests/
│   │       ├── test_models.py
│   │       └── test_views.py
│   └── blog/                   # 博客 App
│       ├── models.py
│       ├── views.py
│       ├── serializers.py
│       ├── services.py         # 业务逻辑层
│       ├── utils.py
│       ├── urls.py
│       ├── admin.py
│       └── tests/
├── templates/
│   ├── base.html
│   ├── accounts/
│   └── blog/
├── static/
│   ├── css/
│   ├── js/
│   └── images/
└── docs/                       # 项目文档

23.2 代码组织原则

python 复制代码
# ✅ 将复杂业务逻辑放在 services.py 中,保持视图简洁
# apps/blog/services.py
class ArticleService:
    @staticmethod
    def publish_article(article, user):
        """发布文章的完整业务流程"""
        if article.author != user:
            raise PermissionError('无权发布他人文章')
        if not article.content:
            raise ValueError('文章内容不能为空')
        article.status = 'published'
        from django.utils import timezone
        article.published_at = timezone.now()
        article.save()
        # 触发通知、更新索引等操作
        return article

# apps/blog/views.py(视图保持简洁)
from .services import ArticleService

class ArticlePublishView(LoginRequiredMixin, View):
    def post(self, request, pk):
        article = get_object_or_404(Article, pk=pk)
        try:
            ArticleService.publish_article(article, request.user)
            messages.success(request, '文章已发布')
        except PermissionError as e:
            messages.error(request, str(e))
        return redirect(article.get_absolute_url())

23.3 Git 忽略文件

gitignore 复制代码
# .gitignore
*.pyc
__pycache__/
*.py[cod]
.env
*.env
db.sqlite3
media/
staticfiles/
.venv/
venv/
node_modules/
*.log
.DS_Store

快速参考速查表

Django 核心命令

bash 复制代码
django-admin startproject <project_name>
python manage.py startapp <app_name>
python manage.py runserver [port]
python manage.py makemigrations [app]
python manage.py migrate [app] [migration]
python manage.py createsuperuser
python manage.py collectstatic
python manage.py shell
python manage.py test [app]
python manage.py check --deploy

常用 ORM 操作

python 复制代码
Model.objects.all()                         # 全部
Model.objects.filter(**kwargs)              # 过滤
Model.objects.get(**kwargs)                 # 单条(不存在报错)
Model.objects.get_or_create(**kwargs)       # 获取或创建
Model.objects.create(**kwargs)              # 创建
Model.objects.update(**kwargs)              # 更新
Model.objects.delete()                      # 删除
Model.objects.count()                       # 计数
Model.objects.exists()                      # 是否存在
Model.objects.order_by('-field')            # 排序
Model.objects.select_related('fk_field')    # 预加载 FK
Model.objects.prefetch_related('m2m_field') # 预加载 M2M
Model.objects.values('field1', 'field2')    # 返回字典
Model.objects.aggregate(total=Count('id'))  # 聚合
Model.objects.annotate(count=Count('rel'))  # 注解

参考资源

相关推荐
Java成神之路-2 小时前
Spring 注解开发进阶实战:Bean 生命周期、 依赖注入及Properties配置(Spring系列4)
java·后端·spring
牛奔2 小时前
升级Go 版本,导致兼容性依赖编译错误排查并解决
开发语言·后端·golang
zzwq.2 小时前
Socket网络编程详解
python
小邓的技术笔记2 小时前
聊聊 ASP.NET Core 中间件和过滤器的区别
后端·中间件·asp.net
运维行者_2 小时前
网络监控告警设置指南:如何配置智能告警规避“告警风暴”?
linux·运维·服务器·网络·后端
FreeBuf_2 小时前
谷歌将Axios npm供应链攻击归因于朝鲜APT组织UNC1069
前端·npm·node.js
知识汲取者2 小时前
初识 RuoYi-Vue
java·spring boot·后端·开源软件
码界筑梦坊2 小时前
324-基于Python的中国传染病数据可视化分析系统
开发语言·python·信息可视化
ZC跨境爬虫2 小时前
Playwright基础操作:元素坐标获取与坐标截图实战
python·microsoft·前端框架