Django 完整知识总结与使用教程
目录
- [Django 简介](#Django 简介)
- 环境安装与项目创建
- 项目结构详解
- [MTV 架构模式](#MTV 架构模式)
- [模型(Models)与 ORM](#模型(Models)与 ORM)
- 数据库迁移(Migrations)
- 视图(Views)
- [URL 路由配置](#URL 路由配置)
- 模板系统(Templates)
- 表单(Forms)
- 用户认证系统
- [Admin 后台管理](#Admin 后台管理)
- 静态文件与媒体文件
- 中间件(Middleware)
- 缓存系统
- 信号(Signals)
- [Django REST Framework(DRF)](#Django REST Framework(DRF))
- 安全性最佳实践
- 生产环境部署
- 性能优化技巧
- 测试
- 常用第三方包推荐
- 项目结构最佳实践
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
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()
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>© 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.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')) # 注解
参考资源