前言

Django是一个高级的Python Web框架,由Lawrence Journal-World的开发团队于2005年创建并开源。它以”快速开发、干净设计”为核心理念,遵循DRY(Don’t Repeat Yourself)原则,为开发者提供了一个功能完整、开箱即用的Web开发解决方案。

为什么选择Django?

  • 完整的框架:内置ORM、模板引擎、表单处理、用户认证、管理后台等功能
  • 安全性优先:内置多种安全防护机制,如CSRF保护、SQL注入防护、XSS防护等
  • 可扩展性强:支持大规模应用开发,Instagram、Pinterest等知名网站都在使用
  • 丰富的生态:拥有庞大的第三方包生态系统和活跃的社区支持
  • 文档完善:官方文档详细全面,学习曲线相对平缓

本文内容概览

本文将全面介绍Django框架的核心概念和实践应用,包括:

  • MTV架构模式:理解Django的设计哲学和架构思想
  • ORM系统详解:掌握数据库操作和模型设计的最佳实践
  • 模板引擎系统:学习Django模板语言和前端集成
  • 表单处理机制:实现数据验证和用户交互
  • 用户认证授权:构建安全的用户管理系统
  • REST API开发:使用Django REST Framework构建现代API
  • 高级特性应用:中间件、缓存、信号等进阶功能
  • 性能优化策略:提升应用性能和用户体验
  • 部署与运维:生产环境的配置和最佳实践

一、Django核心概念

(一)MTV架构模式

1. Model(模型)

  • 数据层:定义数据结构和业务逻辑
  • ORM映射:对象关系映射,简化数据库操作
  • 数据验证:内置数据验证机制

2. Template(模板)

  • 表现层:负责数据的展示
  • 模板语言:Django模板语言(DTL)
  • 模板继承:支持模板继承和包含

3. View(视图)

  • 控制层:处理用户请求和业务逻辑
  • 函数视图:基于函数的视图
  • 类视图:基于类的视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# models.py - 模型示例
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator

class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name_plural = "Categories"
ordering = ['name']

def __str__(self):
return self.name

class Product(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]

title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)]
)
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='products'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='products'
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft'
)
featured = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', 'created_at']),
models.Index(fields=['category', 'status']),
]

def __str__(self):
return self.title

def get_absolute_url(self):
from django.urls import reverse
return reverse('product_detail', kwargs={'slug': self.slug})

class Review(models.Model):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='reviews'
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
rating = models.IntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ['product', 'user']
ordering = ['-created_at']

def __str__(self):
return f'{self.user.username} - {self.product.title}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# views.py - 视图示例
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Q, Avg, Count
from django.contrib import messages
from .models import Product, Category, Review
from .forms import ProductForm, ReviewForm

# 函数视图示例
def product_list(request):
products = Product.objects.filter(status='published')

# 搜索功能
query = request.GET.get('q')
if query:
products = products.filter(
Q(title__icontains=query) |
Q(description__icontains=query)
)

# 分类筛选
category_id = request.GET.get('category')
if category_id:
products = products.filter(category_id=category_id)

# 排序
sort_by = request.GET.get('sort', '-created_at')
if sort_by in ['title', '-title', 'price', '-price', 'created_at', '-created_at']:
products = products.order_by(sort_by)

# 分页
paginator = Paginator(products, 12)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)

# 获取分类列表
categories = Category.objects.annotate(
product_count=Count('products', filter=Q(products__status='published'))
).filter(product_count__gt=0)

context = {
'page_obj': page_obj,
'categories': categories,
'current_category': category_id,
'query': query,
'sort_by': sort_by,
}
return render(request, 'products/list.html', context)

def product_detail(request, slug):
product = get_object_or_404(
Product.objects.select_related('category', 'author'),
slug=slug,
status='published'
)

# 获取评论
reviews = product.reviews.select_related('user').all()

# 计算平均评分
avg_rating = reviews.aggregate(Avg('rating'))['rating__avg']

# 相关产品
related_products = Product.objects.filter(
category=product.category,
status='published'
).exclude(id=product.id)[:4]

# 处理评论表单
review_form = None
if request.user.is_authenticated:
if request.method == 'POST':
review_form = ReviewForm(request.POST)
if review_form.is_valid():
review = review_form.save(commit=False)
review.product = product
review.user = request.user
review.save()
messages.success(request, '评论提交成功!')
return redirect('product_detail', slug=slug)
else:
# 检查用户是否已经评论过
existing_review = reviews.filter(user=request.user).first()
if not existing_review:
review_form = ReviewForm()

context = {
'product': product,
'reviews': reviews,
'avg_rating': avg_rating,
'related_products': related_products,
'review_form': review_form,
}
return render(request, 'products/detail.html', context)

# 类视图示例
class ProductListView(ListView):
model = Product
template_name = 'products/list.html'
context_object_name = 'products'
paginate_by = 12

def get_queryset(self):
queryset = Product.objects.filter(status='published')

# 搜索
query = self.request.GET.get('q')
if query:
queryset = queryset.filter(
Q(title__icontains=query) |
Q(description__icontains=query)
)

# 分类筛选
category_id = self.request.GET.get('category')
if category_id:
queryset = queryset.filter(category_id=category_id)

return queryset.select_related('category', 'author')

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.annotate(
product_count=Count('products', filter=Q(products__status='published'))
).filter(product_count__gt=0)
return context

class ProductDetailView(DetailView):
model = Product
template_name = 'products/detail.html'
context_object_name = 'product'
slug_field = 'slug'

def get_queryset(self):
return Product.objects.filter(status='published').select_related(
'category', 'author'
)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
product = self.object

# 获取评论和平均评分
reviews = product.reviews.select_related('user').all()
context['reviews'] = reviews
context['avg_rating'] = reviews.aggregate(Avg('rating'))['rating__avg']

# 相关产品
context['related_products'] = Product.objects.filter(
category=product.category,
status='published'
).exclude(id=product.id)[:4]

return context

class ProductCreateView(LoginRequiredMixin, CreateView):
model = Product
form_class = ProductForm
template_name = 'products/create.html'

def form_valid(self, form):
form.instance.author = self.request.user
messages.success(self.request, '产品创建成功!')
return super().form_valid(form)

@login_required
def ajax_add_review(request, product_id):
if request.method == 'POST':
product = get_object_or_404(Product, id=product_id, status='published')

# 检查用户是否已经评论过
existing_review = Review.objects.filter(
product=product, user=request.user
).first()

if existing_review:
return JsonResponse({
'success': False,
'error': '您已经评论过这个产品了'
})

form = ReviewForm(request.POST)
if form.is_valid():
review = form.save(commit=False)
review.product = product
review.user = request.user
review.save()

return JsonResponse({
'success': True,
'message': '评论提交成功!',
'review': {
'user': review.user.username,
'rating': review.rating,
'comment': review.comment,
'created_at': review.created_at.strftime('%Y-%m-%d %H:%M')
}
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})

return JsonResponse({'success': False, 'error': '无效的请求方法'})

(二)URL配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# urls.py - URL配置
from django.urls import path, include
from django.contrib import admin
from . import views

# 项目主URL配置
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.home, name='home'),
path('products/', include('products.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('api/', include('api.urls')),
]

# 应用URL配置 (products/urls.py)
app_name = 'products'

urlpatterns = [
path('', views.ProductListView.as_view(), name='list'),
path('create/', views.ProductCreateView.as_view(), name='create'),
path('<slug:slug>/', views.ProductDetailView.as_view(), name='detail'),
path('<slug:slug>/edit/', views.ProductUpdateView.as_view(), name='edit'),
path('<int:product_id>/review/', views.ajax_add_review, name='add_review'),
path('category/<int:category_id>/', views.category_products, name='category'),
]

二、Django ORM系统

(一)模型定义与关系

1. 字段类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 常用字段类型示例
from django.db import models
from django.contrib.auth.models import User

class ExampleModel(models.Model):
# 文本字段
title = models.CharField(max_length=200) # 短文本
content = models.TextField() # 长文本
slug = models.SlugField(unique=True) # URL友好的字符串

# 数字字段
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.IntegerField(default=0)
rating = models.FloatField()

# 日期时间字段
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
publish_date = models.DateField()

# 布尔字段
is_active = models.BooleanField(default=True)
is_featured = models.BooleanField(default=False)

# 选择字段
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES)

# 文件字段
image = models.ImageField(upload_to='images/%Y/%m/%d/')
document = models.FileField(upload_to='documents/')

# 关系字段
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)

2. 模型关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 一对一关系
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

# 一对多关系
class Category(models.Model):
name = models.CharField(max_length=100)

class Article(models.Model):
title = models.CharField(max_length=200)
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='articles'
)

# 多对多关系
class Tag(models.Model):
name = models.CharField(max_length=50)

class Post(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, through='PostTag')

# 自定义中间表
class PostTag(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_by = models.ForeignKey(User, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ['post', 'tag']

(二)查询操作

1. 基本查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 基本查询示例
from django.db.models import Q, F, Count, Avg, Sum, Max, Min
from .models import Product, Category, Review

# 获取所有对象
all_products = Product.objects.all()

# 过滤查询
published_products = Product.objects.filter(status='published')
expensive_products = Product.objects.filter(price__gt=100)
recent_products = Product.objects.filter(created_at__gte='2024-01-01')

# 排除查询
non_draft_products = Product.objects.exclude(status='draft')

# 获取单个对象
product = Product.objects.get(id=1)
product = Product.objects.get(slug='example-product')

# 安全获取对象
from django.shortcuts import get_object_or_404
product = get_object_or_404(Product, slug='example-product')

# 检查存在性
exists = Product.objects.filter(slug='example').exists()

# 计数
count = Product.objects.filter(status='published').count()

# 字段查找
products = Product.objects.filter(
title__icontains='django', # 包含(忽略大小写)
price__range=(10, 100), # 范围
created_at__year=2024, # 年份
category__name='Technology' # 关联查询
)

2. 复杂查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# Q对象复杂查询
from django.db.models import Q

# OR查询
products = Product.objects.filter(
Q(title__icontains='python') | Q(title__icontains='django')
)

# AND查询
products = Product.objects.filter(
Q(status='published') & Q(price__lt=50)
)

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

# 复杂组合
products = Product.objects.filter(
Q(status='published') &
(Q(title__icontains='python') | Q(description__icontains='python'))
)

# F对象字段比较
from django.db.models import F

# 比较字段值
products = Product.objects.filter(updated_at__gt=F('created_at'))

# 字段运算
Product.objects.update(price=F('price') * 1.1) # 价格上涨10%

# 聚合查询
from django.db.models import Count, Avg, Sum, Max, Min

# 聚合函数
stats = Product.objects.aggregate(
total_count=Count('id'),
avg_price=Avg('price'),
total_value=Sum('price'),
max_price=Max('price'),
min_price=Min('price')
)

# 分组聚合
category_stats = Category.objects.annotate(
product_count=Count('products'),
avg_price=Avg('products__price')
).filter(product_count__gt=0)

# 子查询
from django.db.models import OuterRef, Subquery

# 获取每个分类的最新产品
latest_products = Product.objects.filter(
category=OuterRef('pk')
).order_by('-created_at')

categories_with_latest = Category.objects.annotate(
latest_product_title=Subquery(
latest_products.values('title')[:1]
)
)

3. 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# select_related - 一对一和外键关系
products = Product.objects.select_related(
'category', 'author'
).all()

# prefetch_related - 多对多和反向外键关系
products = Product.objects.prefetch_related(
'reviews', 'tags'
).all()

# 组合使用
products = Product.objects.select_related(
'category', 'author'
).prefetch_related(
'reviews__user', 'tags'
).all()

# 自定义预取
from django.db.models import Prefetch

products = Product.objects.prefetch_related(
Prefetch(
'reviews',
queryset=Review.objects.select_related('user').filter(rating__gte=4)
)
).all()

# 只获取需要的字段
products = Product.objects.values('id', 'title', 'price')
products = Product.objects.only('title', 'price') # 延迟加载其他字段
products = Product.objects.defer('description') # 延迟加载指定字段

# 批量操作
# 批量创建
Product.objects.bulk_create([
Product(title='Product 1', price=10),
Product(title='Product 2', price=20),
])

# 批量更新
Product.objects.filter(category_id=1).update(status='published')

# 批量删除
Product.objects.filter(status='draft').delete()

三、模板系统

(一)模板语法

1. 基础模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!-- 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>

<!-- CSS -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'home' %}">我的网站</a>

<div class="navbar-nav ms-auto">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'profile' %}">{{ user.username }}</a>
<a class="nav-link" href="{% url 'logout' %}">退出</a>
{% else %}
<a class="nav-link" href="{% url 'login' %}">登录</a>
<a class="nav-link" href="{% url 'register' %}">注册</a>
{% endif %}
</div>
</div>
</nav>

<!-- 消息提示 -->
{% if messages %}
<div class="container mt-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}

<!-- 主要内容 -->
<main class="container my-4">
{% block content %}
{% endblock %}
</main>

<!-- 页脚 -->
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<p>&copy; 2024 我的网站. 保留所有权利.</p>
</div>
<div class="col-md-6 text-end">
<a href="{% url 'about' %}" class="text-light">关于我们</a> |
<a href="{% url 'contact' %}" class="text-light">联系我们</a>
</div>
</div>
</div>
</footer>

<!-- JavaScript -->
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

2. 产品列表模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<!-- products/list.html -->
{% extends 'base.html' %}
{% load static %}

{% block title %}产品列表{% endblock %}

{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3">
<div class="card">
<div class="card-header">
<h5>分类筛选</h5>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li><a href="{% url 'products:list' %}"
class="{% if not current_category %}fw-bold{% endif %}">全部</a></li>
{% for category in categories %}
<li>
<a href="{% url 'products:list' %}?category={{ category.id }}"
class="{% if current_category == category.id|stringformat:'s' %}fw-bold{% endif %}">
{{ category.name }} ({{ category.product_count }})
</a>
</li>
{% endfor %}
</ul>
</div>
</div>

<!-- 搜索框 -->
<div class="card mt-3">
<div class="card-header">
<h5>搜索</h5>
</div>
<div class="card-body">
<form method="get">
<div class="mb-3">
<input type="text" name="q" class="form-control"
placeholder="搜索产品..." value="{{ query }}">
</div>
<button type="submit" class="btn btn-primary">搜索</button>
</form>
</div>
</div>
</div>

<!-- 主要内容 -->
<div class="col-md-9">
<!-- 排序和结果统计 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<p class="mb-0">找到 {{ page_obj.paginator.count }} 个产品</p>
</div>
<div>
<select class="form-select" onchange="location.href=this.value">
<option value="{% url 'products:list' %}?sort=-created_at"
{% if sort_by == '-created_at' %}selected{% endif %}>最新</option>
<option value="{% url 'products:list' %}?sort=price"
{% if sort_by == 'price' %}selected{% endif %}>价格从低到高</option>
<option value="{% url 'products:list' %}?sort=-price"
{% if sort_by == '-price' %}selected{% endif %}>价格从高到低</option>
<option value="{% url 'products:list' %}?sort=title"
{% if sort_by == 'title' %}selected{% endif %}>名称A-Z</option>
</select>
</div>
</div>

<!-- 产品网格 -->
<div class="row">
{% for product in page_obj %}
<div class="col-md-4 mb-4">
<div class="card h-100">
{% if product.image %}
<img src="{{ product.image.url }}" class="card-img-top"
alt="{{ product.title }}" style="height: 200px; object-fit: cover;">
{% endif %}

<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ product.title }}</h5>
<p class="card-text">{{ product.description|truncatewords:20 }}</p>
<p class="card-text">
<small class="text-muted">{{ product.category.name }}</small>
</p>

<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center">
<span class="h5 mb-0 text-primary">¥{{ product.price }}</span>
<a href="{{ product.get_absolute_url }}" class="btn btn-primary">查看详情</a>
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info text-center">
<h4>没有找到产品</h4>
<p>请尝试调整搜索条件或浏览其他分类。</p>
</div>
</div>
{% endfor %}
</div>

<!-- 分页 -->
{% if page_obj.has_other_pages %}
<nav aria-label="产品分页">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if query %}&q={{ query }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}">
首页
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if query %}&q={{ query }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}">
上一页
</a>
</li>
{% endif %}

{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if query %}&q={{ query }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}">
{{ num }}
</a>
</li>
{% endif %}
{% endfor %}

{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if query %}&q={{ query }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}">
下一页
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if query %}&q={{ query }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}">
末页
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% endblock %}

(二)模板标签和过滤器

1. 内置标签和过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- 常用模板标签 -->
{% load static %}
{% load humanize %}

<!-- 条件判断 -->
{% if user.is_authenticated %}
<p>欢迎,{{ user.username }}!</p>
{% elif user.is_anonymous %}
<p>请登录</p>
{% else %}
<p>未知状态</p>
{% endif %}

<!-- 循环 -->
{% for product in products %}
<div class="product-item">
<h3>{{ product.title }}</h3>
<p>{{ product.description|truncatewords:30 }}</p>

<!-- 循环变量 -->
<small>第 {{ forloop.counter }} 个产品</small>
{% if forloop.first %}<span class="badge">最新</span>{% endif %}
</div>
{% empty %}
<p>没有产品</p>
{% endfor %}

<!-- 常用过滤器 -->
<p>价格:{{ product.price|floatformat:2 }}</p>
<p>创建时间:{{ product.created_at|date:"Y-m-d H:i" }}</p>
<p>相对时间:{{ product.created_at|timesince }}前</p>
<p>描述:{{ product.description|linebreaks }}</p>
<p>标题:{{ product.title|title }}</p>
<p>长度:{{ product.title|length }}</p>
<p>默认值:{{ product.subtitle|default:"无副标题" }}</p>

<!-- 人性化显示 -->
<p>文件大小:{{ file_size|filesizeformat }}</p>
<p>数字:{{ large_number|intcomma }}</p>
<p>序数:{{ number|ordinal }}</p>

2. 自定义模板标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# templatetags/product_tags.py
from django import template
from django.db.models import Count, Avg
from ..models import Product, Category

register = template.Library()

@register.simple_tag
def get_featured_products(count=5):
"""获取推荐产品"""
return Product.objects.filter(
status='published',
featured=True
)[:count]

@register.simple_tag
def get_popular_categories(count=10):
"""获取热门分类"""
return Category.objects.annotate(
product_count=Count('products')
).filter(product_count__gt=0).order_by('-product_count')[:count]

@register.inclusion_tag('products/tags/product_card.html')
def product_card(product, show_category=True):
"""产品卡片组件"""
return {
'product': product,
'show_category': show_category,
}

@register.filter
def multiply(value, arg):
"""乘法过滤器"""
try:
return float(value) * float(arg)
except (ValueError, TypeError):
return 0

@register.filter
def rating_stars(rating):
"""评分星星显示"""
if not rating:
return ''

full_stars = int(rating)
half_star = 1 if rating - full_stars >= 0.5 else 0
empty_stars = 5 - full_stars - half_star

stars = '★' * full_stars + '☆' * half_star + '☆' * empty_stars
return stars

register.filter('multiply', multiply)
register.filter('rating_stars', rating_stars)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 使用自定义标签 -->
{% load product_tags %}

<!-- 简单标签 -->
{% get_featured_products 3 as featured %}
<div class="featured-products">
{% for product in featured %}
{% product_card product %}
{% endfor %}
</div>

<!-- 包含标签 -->
{% product_card product show_category=False %}

<!-- 自定义过滤器 -->
<p>总价:{{ product.price|multiply:quantity }}</p>
<p>评分:{{ product.avg_rating|rating_stars }}</p>

四、表单处理

(一)Django表单

1. 表单定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from .models import Product, Review, Category

class ProductForm(forms.ModelForm):
"""产品表单"""

class Meta:
model = Product
fields = ['title', 'slug', 'description', 'price', 'category', 'image', 'featured']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入产品标题'
}),
'slug': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '产品URL标识符'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': '请输入产品描述'
}),
'price': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'category': forms.Select(attrs={
'class': 'form-select'
}),
'image': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*'
}),
'featured': forms.CheckboxInput(attrs={
'class': 'form-check-input'
})
}

def clean_slug(self):
"""验证slug唯一性"""
slug = self.cleaned_data['slug']
if Product.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise ValidationError('该URL标识符已存在')
return slug

def clean_price(self):
"""验证价格"""
price = self.cleaned_data['price']
if price <= 0:
raise ValidationError('价格必须大于0')
return price

class ReviewForm(forms.ModelForm):
"""评论表单"""

class Meta:
model = Review
fields = ['rating', 'comment']
widgets = {
'rating': forms.Select(
choices=[(i, f'{i}星') for i in range(1, 6)],
attrs={'class': 'form-select'}
),
'comment': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': '请分享您的使用体验...'
})
}

def clean_comment(self):
"""验证评论内容"""
comment = self.cleaned_data['comment']
if len(comment) < 10:
raise ValidationError('评论内容至少需要10个字符')
return comment

class ContactForm(forms.Form):
"""联系表单"""

SUBJECT_CHOICES = [
('general', '一般咨询'),
('support', '技术支持'),
('business', '商务合作'),
('complaint', '投诉建议'),
]

name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '您的姓名'
})
)

email = forms.EmailField(
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '您的邮箱'
})
)

subject = forms.ChoiceField(
choices=SUBJECT_CHOICES,
widget=forms.Select(attrs={
'class': 'form-select'
})
)

message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': '请详细描述您的问题或建议...'
})
)

def clean_message(self):
"""验证消息内容"""
message = self.cleaned_data['message']
if len(message) < 20:
raise ValidationError('消息内容至少需要20个字符')
return message

class CustomUserCreationForm(UserCreationForm):
"""自定义用户注册表单"""

email = forms.EmailField(
required=True,
widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': '邮箱地址'
})
)

first_name = forms.CharField(
max_length=30,
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '名字'
})
)

last_name = forms.CharField(
max_length=30,
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '姓氏'
})
)

class Meta:
model = User
fields = ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')
widgets = {
'username': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '用户名'
}),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['password1'].widget.attrs.update({
'class': 'form-control',
'placeholder': '密码'
})
self.fields['password2'].widget.attrs.update({
'class': 'form-control',
'placeholder': '确认密码'
})

def clean_email(self):
"""验证邮箱唯一性"""
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise ValidationError('该邮箱已被注册')
return email

def save(self, commit=True):
"""保存用户"""
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user

2. 表单视图处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# views.py - 表单处理视图
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth import login
from django.core.mail import send_mail
from django.conf import settings
from .forms import ProductForm, ContactForm, CustomUserCreationForm

def create_product(request):
"""创建产品"""
if request.method == 'POST':
form = ProductForm(request.POST, request.FILES)
if form.is_valid():
product = form.save(commit=False)
product.author = request.user
product.save()
messages.success(request, '产品创建成功!')
return redirect('products:detail', slug=product.slug)
else:
form = ProductForm()

return render(request, 'products/create.html', {'form': form})

def edit_product(request, slug):
"""编辑产品"""
product = get_object_or_404(Product, slug=slug, author=request.user)

if request.method == 'POST':
form = ProductForm(request.POST, request.FILES, instance=product)
if form.is_valid():
form.save()
messages.success(request, '产品更新成功!')
return redirect('products:detail', slug=product.slug)
else:
form = ProductForm(instance=product)

return render(request, 'products/edit.html', {
'form': form,
'product': product
})

def contact_view(request):
"""联系表单"""
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# 发送邮件
subject = f"联系表单:{form.cleaned_data['subject']}"
message = f"""
姓名:{form.cleaned_data['name']}
邮箱:{form.cleaned_data['email']}
主题:{form.cleaned_data['subject']}

消息内容:
{form.cleaned_data['message']}
"""

try:
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[settings.CONTACT_EMAIL],
fail_silently=False,
)
messages.success(request, '您的消息已发送,我们会尽快回复!')
return redirect('contact')
except Exception as e:
messages.error(request, '发送失败,请稍后重试。')
else:
form = ContactForm()

return render(request, 'contact.html', {'form': form})

def register_view(request):
"""用户注册"""
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, f'欢迎,{user.username}!注册成功。')
return redirect('home')
else:
form = CustomUserCreationForm()

return render(request, 'registration/register.html', {'form': form})

(二)表单模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!-- products/create.html -->
{% extends 'base.html' %}

{% block title %}创建产品{% endblock %}

{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>创建新产品</h3>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}

<!-- 显示表单错误 -->
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}

<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.title.id_for_label }}" class="form-label">产品标题 *</label>
{{ form.title }}
{% if form.title.errors %}
<div class="text-danger small">
{{ form.title.errors }}
</div>
{% endif %}
</div>
</div>

<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.slug.id_for_label }}" class="form-label">URL标识符 *</label>
{{ form.slug }}
{% if form.slug.errors %}
<div class="text-danger small">
{{ form.slug.errors }}
</div>
{% endif %}
<div class="form-text">用于生成产品页面URL</div>
</div>
</div>
</div>

<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">产品描述 *</label>
{{ form.description }}
{% if form.description.errors %}
<div class="text-danger small">
{{ form.description.errors }}
</div>
{% endif %}
</div>

<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.price.id_for_label }}" class="form-label">价格 *</label>
<div class="input-group">
<span class="input-group-text">¥</span>
{{ form.price }}
</div>
{% if form.price.errors %}
<div class="text-danger small">
{{ form.price.errors }}
</div>
{% endif %}
</div>
</div>

<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.category.id_for_label }}" class="form-label">分类 *</label>
{{ form.category }}
{% if form.category.errors %}
<div class="text-danger small">
{{ form.category.errors }}
</div>
{% endif %}
</div>
</div>
</div>

<div class="mb-3">
<label for="{{ form.image.id_for_label }}" class="form-label">产品图片</label>
{{ form.image }}
{% if form.image.errors %}
<div class="text-danger small">
{{ form.image.errors }}
</div>
{% endif %}
<div class="form-text">支持JPG、PNG格式,建议尺寸800x600像素</div>
</div>

<div class="mb-3 form-check">
{{ form.featured }}
<label class="form-check-label" for="{{ form.featured.id_for_label }}">
设为推荐产品
</label>
</div>

<div class="d-flex justify-content-between">
<a href="{% url 'products:list' %}" class="btn btn-secondary">取消</a>
<button type="submit" class="btn btn-primary">创建产品</button>
</div>
</form>
</div>
</div>
</div>
</div>

<script>
// 自动生成slug
document.getElementById('id_title').addEventListener('input', function() {
const title = this.value;
const slug = title.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
.replace(/^-+|-+$/g, '');
document.getElementById('id_slug').value = slug;
});
</script>
{% endblock %}

五、用户认证与权限

(一)用户认证系统

1. 认证配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# settings.py
AUTH_USER_MODEL = 'accounts.User' # 自定义用户模型

# 认证后端
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'accounts.backends.EmailBackend', # 邮箱登录
]

# 登录/登出重定向
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

# 密码验证
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 8,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

2. 自定义用户模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
location = models.CharField(max_length=100, blank=True)
website = models.URLField(blank=True)

# 邮箱验证
email_verified = models.BooleanField(default=False)
email_verification_token = models.CharField(max_length=100, blank=True)

# 隐私设置
is_profile_public = models.BooleanField(default=True)

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']

def get_full_name(self):
return f'{self.first_name} {self.last_name}'.strip() or self.username

def get_short_name(self):
return self.first_name or self.username

class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)

# 社交媒体链接
github_url = models.URLField(blank=True)
linkedin_url = models.URLField(blank=True)
twitter_url = models.URLField(blank=True)

# 通知设置
email_notifications = models.BooleanField(default=True)
sms_notifications = models.BooleanField(default=False)

# 统计信息
profile_views = models.PositiveIntegerField(default=0)
last_login_ip = models.GenericIPAddressField(null=True, blank=True)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

3. 认证视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core.mail import send_mail
from django.utils.crypto import get_random_string
from django.urls import reverse
from .forms import CustomUserCreationForm, UserProfileForm
from .models import User

def register_view(request):
"""用户注册"""
if request.user.is_authenticated:
return redirect('home')

if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
# 发送验证邮件
token = get_random_string(50)
user.email_verification_token = token
user.save()

verification_url = request.build_absolute_uri(
reverse('verify_email', kwargs={'token': token})
)

send_mail(
'验证您的邮箱',
f'请点击链接验证邮箱:{verification_url}',
'noreply@example.com',
[user.email],
fail_silently=False,
)

messages.success(request, '注册成功!请检查邮箱并验证。')
return redirect('login')
else:
form = CustomUserCreationForm()

return render(request, 'registration/register.html', {'form': form})

@login_required
def profile_view(request):
"""用户资料"""
if request.method == 'POST':
form = UserProfileForm(request.POST, request.FILES, instance=request.user)
if form.is_valid():
form.save()
messages.success(request, '资料更新成功!')
return redirect('profile')
else:
form = UserProfileForm(instance=request.user)

return render(request, 'accounts/profile.html', {'form': form})

def verify_email(request, token):
"""邮箱验证"""
try:
user = User.objects.get(email_verification_token=token)
user.email_verified = True
user.email_verification_token = ''
user.save()
messages.success(request, '邮箱验证成功!')
return redirect('login')
except User.DoesNotExist:
messages.error(request, '验证链接无效或已过期。')
return redirect('home')

(二)权限系统

1. 权限装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# decorators.py
from functools import wraps
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404

def owner_required(model):
"""要求用户是对象的所有者"""
def decorator(view_func):
@wraps(view_func)
@login_required
def _wrapped_view(request, *args, **kwargs):
obj_id = kwargs.get('pk') or kwargs.get('id')
obj = get_object_or_404(model, pk=obj_id)

if hasattr(obj, 'author') and obj.author != request.user:
raise PermissionDenied
elif hasattr(obj, 'user') and obj.user != request.user:
raise PermissionDenied
elif hasattr(obj, 'owner') and obj.owner != request.user:
raise PermissionDenied

return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator

def staff_required(view_func):
"""要求用户是员工"""
@wraps(view_func)
@login_required
def _wrapped_view(request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied
return view_func(request, *args, **kwargs)
return _wrapped_view

2. 基于类的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# mixins.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied

class OwnerRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
"""要求用户是对象所有者的混入类"""

def test_func(self):
obj = self.get_object()
return (
hasattr(obj, 'author') and obj.author == self.request.user or
hasattr(obj, 'user') and obj.user == self.request.user or
hasattr(obj, 'owner') and obj.owner == self.request.user
)

class StaffRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
"""要求用户是员工的混入类"""

def test_func(self):
return self.request.user.is_staff

# 使用示例
class ProductUpdateView(OwnerRequiredMixin, UpdateView):
model = Product
form_class = ProductForm
template_name = 'products/edit.html'

def form_valid(self, form):
messages.success(self.request, '产品更新成功!')
return super().form_valid(form)

六、Django REST Framework

(一)API配置

1. 基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# settings.py
INSTALLED_APPS = [
# ... 其他应用
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
]

MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
# ... 其他中间件
]

# DRF配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}

# CORS配置
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]

2. 序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Product, Category, Review

class CategorySerializer(serializers.ModelSerializer):
product_count = serializers.SerializerMethodField()

class Meta:
model = Category
fields = ['id', 'name', 'description', 'product_count']

def get_product_count(self, obj):
return obj.products.filter(status='published').count()

class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']

class ReviewSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)

class Meta:
model = Review
fields = ['id', 'user', 'rating', 'comment', 'created_at']
read_only_fields = ['user', 'created_at']

class ProductListSerializer(serializers.ModelSerializer):
category = CategorySerializer(read_only=True)
author = UserSerializer(read_only=True)
avg_rating = serializers.SerializerMethodField()
review_count = serializers.SerializerMethodField()

class Meta:
model = Product
fields = [
'id', 'title', 'slug', 'description', 'price',
'category', 'author', 'image', 'featured',
'avg_rating', 'review_count', 'created_at'
]

def get_avg_rating(self, obj):
return obj.reviews.aggregate(models.Avg('rating'))['rating__avg']

def get_review_count(self, obj):
return obj.reviews.count()

class ProductDetailSerializer(ProductListSerializer):
reviews = ReviewSerializer(many=True, read_only=True)

class Meta(ProductListSerializer.Meta):
fields = ProductListSerializer.Meta.fields + ['reviews']

class ProductCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = [
'title', 'slug', 'description', 'price',
'category', 'image', 'featured'
]

def validate_slug(self, value):
if Product.objects.filter(slug=value).exclude(pk=self.instance.pk if self.instance else None).exists():
raise serializers.ValidationError('该URL标识符已存在')
return value

def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError('价格必须大于0')
return value

3. API视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# api/views.py
from rest_framework import generics, status, filters
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from .serializers import *
from .models import Product, Category, Review

class CategoryListView(generics.ListAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

class ProductListCreateView(generics.ListCreateAPIView):
serializer_class = ProductListSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'featured', 'status']
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'price', 'title']
ordering = ['-created_at']

def get_queryset(self):
return Product.objects.filter(status='published').select_related(
'category', 'author'
).prefetch_related('reviews')

def get_serializer_class(self):
if self.request.method == 'POST':
return ProductCreateUpdateSerializer
return ProductListSerializer

def perform_create(self, serializer):
serializer.save(author=self.request.user)

class ProductDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Product.objects.filter(status='published')
lookup_field = 'slug'
permission_classes = [IsAuthenticatedOrReadOnly]

def get_serializer_class(self):
if self.request.method in ['PUT', 'PATCH']:
return ProductCreateUpdateSerializer
return ProductDetailSerializer

def get_queryset(self):
return Product.objects.filter(status='published').select_related(
'category', 'author'
).prefetch_related('reviews__user')

def perform_update(self, serializer):
# 只允许作者更新
if self.get_object().author != self.request.user:
raise PermissionDenied('您只能编辑自己的产品')
serializer.save()

def perform_destroy(self, instance):
# 只允许作者删除
if instance.author != self.request.user:
raise PermissionDenied('您只能删除自己的产品')
instance.delete()

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def add_review(request, product_slug):
"""添加产品评论"""
try:
product = Product.objects.get(slug=product_slug, status='published')
except Product.DoesNotExist:
return Response(
{'error': '产品不存在'},
status=status.HTTP_404_NOT_FOUND
)

# 检查用户是否已经评论过
if Review.objects.filter(product=product, user=request.user).exists():
return Response(
{'error': '您已经评论过这个产品了'},
status=status.HTTP_400_BAD_REQUEST
)

serializer = ReviewSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, product=product)
return Response(serializer.data, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(['GET'])
def search_products(request):
"""产品搜索"""
query = request.GET.get('q', '')
if not query:
return Response({'results': []})

products = Product.objects.filter(
Q(title__icontains=query) | Q(description__icontains=query),
status='published'
).select_related('category', 'author')[:10]

serializer = ProductListSerializer(products, many=True)
return Response({'results': serializer.data})

七、缓存与性能优化

(一)缓存配置

1. Redis缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'myapp',
'TIMEOUT': 300,
}
}

# 会话存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

2. 缓存使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# utils/cache.py
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from functools import wraps
import hashlib

def cache_result(timeout=300, key_prefix=''):
"""缓存函数结果装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
cache_key = f"{key_prefix}:{func.__name__}:{hashlib.md5(str(args + tuple(kwargs.items())).encode()).hexdigest()}"

# 尝试从缓存获取
result = cache.get(cache_key)
if result is not None:
return result

# 执行函数并缓存结果
result = func(*args, **kwargs)
cache.set(cache_key, result, timeout)
return result
return wrapper
return decorator

# 使用示例
@cache_result(timeout=600, key_prefix='products')
def get_featured_products():
return Product.objects.filter(
status='published',
featured=True
).select_related('category')[:5]

@cache_result(timeout=3600, key_prefix='stats')
def get_site_statistics():
return {
'total_products': Product.objects.filter(status='published').count(),
'total_categories': Category.objects.count(),
'total_reviews': Review.objects.count(),
}

(二)数据库优化

1. 查询优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 优化查询示例
class OptimizedProductViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# 使用select_related和prefetch_related
return Product.objects.select_related(
'category', 'author'
).prefetch_related(
'reviews__user', 'tags'
).filter(status='published')

def list(self, request, *args, **kwargs):
# 使用缓存
cache_key = f"products_list_{request.GET.urlencode()}"
cached_data = cache.get(cache_key)

if cached_data:
return Response(cached_data)

response = super().list(request, *args, **kwargs)
cache.set(cache_key, response.data, 300)
return response

# 数据库索引
class Product(models.Model):
# ... 字段定义

class Meta:
indexes = [
models.Index(fields=['status', 'created_at']),
models.Index(fields=['category', 'status']),
models.Index(fields=['featured', 'status']),
]

八、部署与配置

(一)生产环境配置

1. 设置文件分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# settings/base.py
import os
from pathlib import Path

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

# 基础配置
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []

# 应用配置
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

THIRD_PARTY_APPS = [
'rest_framework',
'corsheaders',
'django_filters',
]

LOCAL_APPS = [
'accounts',
'products',
'api',
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

# 数据库
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'),
}
}

# settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# 安全设置
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'

# 静态文件
STATIC_ROOT = '/var/www/static/'
MEDIA_ROOT = '/var/www/media/'

# 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}

2. Docker部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制项目文件
COPY . .

# 收集静态文件
RUN python manage.py collectstatic --noinput

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# docker-compose.yml
version: '3.8'

services:
web:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=False
- SECRET_KEY=your-secret-key
- DB_NAME=myapp
- DB_USER=postgres
- DB_PASSWORD=password
- DB_HOST=db
depends_on:
- db
- redis
volumes:
- static_volume:/app/static
- media_volume:/app/media

db:
image: postgres:13
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data

redis:
image: redis:6-alpine

nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/static
- media_volume:/media
depends_on:
- web

volumes:
postgres_data:
static_volume:
media_volume:

九、最佳实践与总结

(一)开发最佳实践

1. 项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
myproject/
├── manage.py
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── myproject/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── accounts/
│ ├── products/
│ └── api/
├── static/
├── media/
├── templates/
└── tests/

2. 代码质量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 使用类型提示
from typing import List, Optional
from django.http import HttpRequest, HttpResponse

def get_products(request: HttpRequest, category_id: Optional[int] = None) -> HttpResponse:
products: List[Product] = Product.objects.filter(status='published')
if category_id:
products = products.filter(category_id=category_id)
return render(request, 'products/list.html', {'products': products})

# 使用常量
class ProductStatus:
DRAFT = 'draft'
PUBLISHED = 'published'
ARCHIVED = 'archived'

CHOICES = [
(DRAFT, 'Draft'),
(PUBLISHED, 'Published'),
(ARCHIVED, 'Archived'),
]

# 模型方法
class Product(models.Model):
# ... 字段定义

def is_published(self) -> bool:
return self.status == ProductStatus.PUBLISHED

def can_be_edited_by(self, user) -> bool:
return self.author == user or user.is_staff

(二)安全最佳实践

1. 输入验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 表单验证
class ProductForm(forms.ModelForm):
def clean_title(self):
title = self.cleaned_data['title']
# 防止XSS
title = bleach.clean(title, tags=[], strip=True)
if len(title) < 3:
raise ValidationError('标题至少需要3个字符')
return title

# API验证
class ProductSerializer(serializers.ModelSerializer):
def validate_price(self, value):
if value < 0:
raise serializers.ValidationError('价格不能为负数')
if value > 999999:
raise serializers.ValidationError('价格过高')
return value

2. 权限控制

1
2
3
4
5
6
7
8
9
# 基于对象的权限
class ProductPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# 读取权限:所有人
if request.method in permissions.SAFE_METHODS:
return True

# 写入权限:仅作者
return obj.author == request.user

(三)总结

Django是一个功能强大且成熟的Web框架,具有以下优势:

1. 核心优势

  • 快速开发:内置管理后台、ORM、模板引擎
  • 安全性:内置CSRF、XSS、SQL注入防护
  • 可扩展性:丰富的第三方包生态
  • 文档完善:官方文档详细且更新及时

2. 适用场景

  • 内容管理系统:博客、新闻网站
  • 电商平台:在线商店、市场平台
  • 企业应用:CRM、ERP系统
  • API服务:RESTful API、微服务

3. 学习建议

  • 掌握基础:Python基础、Web开发概念
  • 实践项目:从简单项目开始,逐步增加复杂度
  • 阅读源码:理解Django内部机制
  • 关注社区:参与开源项目,学习最佳实践

Django为Python Web开发提供了一个完整的解决方案,无论是快速原型开发还是大型企业应用,都能找到合适的解决方案。通过合理的架构设计和最佳实践,可以构建出高性能、可维护的Web应用。