Django 实战项目案例
2026/3/20大约 7 分钟
Django 实战项目案例
一、博客系统
1.1 项目结构
blog_project/
├── manage.py
├── requirements.txt
├── .env
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── blog/
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── forms.py
│ │ ├── admin.py
│ │ └── templatetags/
│ ├── users/
│ │ ├── models.py
│ │ ├── views.py
│ │ └── forms.py
│ └── comments/
├── templates/
├── static/
└── media/
1.2 核心模型
# apps/blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.text import slugify
User = get_user_model()
class Category(models.Model):
name = models.CharField('分类名', max_length=100)
slug = models.SlugField('URL别名', unique=True)
description = models.TextField('描述', blank=True)
parent = models.ForeignKey(
'self', on_delete=models.CASCADE,
null=True, blank=True, related_name='children'
)
class Meta:
verbose_name = '分类'
verbose_name_plural = verbose_name
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', args=[self.slug])
class Tag(models.Model):
name = models.CharField('标签名', max_length=50)
slug = models.SlugField('URL别名', unique=True)
class Meta:
verbose_name = '标签'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Article(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
title = models.CharField('标题', max_length=200)
slug = models.SlugField('URL别名', unique=True, blank=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, related_name='articles', verbose_name='分类'
)
tags = models.ManyToManyField(Tag, blank=True, related_name='articles')
cover = models.ImageField('封面图', upload_to='covers/', blank=True)
excerpt = models.TextField('摘要', max_length=500, blank=True)
content = models.TextField('内容')
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='draft')
views = models.PositiveIntegerField('浏览量', default=0)
is_featured = models.BooleanField('是否推荐', default=False)
allow_comments = models.BooleanField('允许评论', default=True)
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 = verbose_name
ordering = ['-published_at', '-created_at']
indexes = [
models.Index(fields=['status', '-published_at']),
models.Index(fields=['author', 'status']),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title, allow_unicode=True)
if not self.excerpt and self.content:
self.excerpt = self.content[:200]
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('blog:article_detail', args=[self.slug])
def increment_views(self):
self.views += 1
self.save(update_fields=['views'])
@property
def reading_time(self):
"""估算阅读时间(分钟)"""
word_count = len(self.content.split())
return max(1, word_count // 200)
1.3 视图实现
# apps/blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q, Count
from django.shortcuts import get_object_or_404
from .models import Article, Category, Tag
class ArticleListView(ListView):
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
queryset = Article.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related('tags')
# 搜索
query = self.request.GET.get('q')
if query:
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['categories'] = Category.objects.annotate(
article_count=Count('articles')
).filter(article_count__gt=0)
context['popular_tags'] = Tag.objects.annotate(
article_count=Count('articles')
).order_by('-article_count')[:20]
return context
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
slug_field = 'slug'
slug_url_kwarg = 'slug'
def get_queryset(self):
return Article.objects.filter(
status='published'
).select_related('author', 'category').prefetch_related('tags')
def get_object(self, queryset=None):
obj = super().get_object(queryset)
obj.increment_views()
return obj
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['related_articles'] = Article.objects.filter(
category=self.object.category,
status='published'
).exclude(pk=self.object.pk)[:5]
return context
class ArticleCreateView(LoginRequiredMixin, CreateView):
model = Article
template_name = 'blog/article_form.html'
fields = ['title', 'category', 'tags', 'cover', 'content', 'status']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class CategoryDetailView(ListView):
template_name = 'blog/category_detail.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
self.category = get_object_or_404(Category, slug=self.kwargs['slug'])
return Article.objects.filter(
category=self.category,
status='published'
).select_related('author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
return context
1.4 URL 配置
# apps/blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.ArticleListView.as_view(), name='article_list'),
path('article/<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
path('article/create/', views.ArticleCreateView.as_view(), name='article_create'),
path('category/<slug:slug>/', views.CategoryDetailView.as_view(), name='category_detail'),
path('tag/<slug:slug>/', views.TagDetailView.as_view(), name='tag_detail'),
path('archive/<int:year>/<int:month>/', views.ArchiveView.as_view(), name='archive'),
]
二、RESTful API 服务
2.1 序列化器
# api/serializers.py
from rest_framework import serializers
from apps.blog.models import Article, Category, Tag
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
class CategorySerializer(serializers.ModelSerializer):
article_count = serializers.IntegerField(read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'article_count']
class ArticleListSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
category = serializers.StringRelatedField()
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Article
fields = [
'id', 'title', 'slug', 'author', 'category', 'tags',
'excerpt', 'cover', 'views', 'reading_time', 'published_at'
]
class ArticleDetailSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Article
fields = '__all__'
class ArticleCreateSerializer(serializers.ModelSerializer):
tags = serializers.PrimaryKeyRelatedField(
many=True, queryset=Tag.objects.all(), required=False
)
class Meta:
model = Article
fields = ['title', 'content', 'category', 'tags', 'cover', 'status']
def create(self, validated_data):
tags = validated_data.pop('tags', [])
article = Article.objects.create(**validated_data)
article.tags.set(tags)
return article
2.2 视图集
# api/views.py
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count
from .serializers import *
from .permissions import IsOwnerOrReadOnly
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'category', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'views', 'published_at']
lookup_field = 'slug'
def get_queryset(self):
queryset = Article.objects.select_related('author', 'category').prefetch_related('tags')
if self.action == 'list' and not self.request.user.is_staff:
queryset = queryset.filter(status='published')
return queryset
def get_serializer_class(self):
if self.action == 'list':
return ArticleListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return ArticleCreateSerializer
return ArticleDetailSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
article = self.get_object()
if article.author != request.user and not request.user.is_staff:
return Response({'detail': '无权限'}, status=status.HTTP_403_FORBIDDEN)
article.status = 'published'
article.published_at = timezone.now()
article.save()
return Response({'status': 'published'})
@action(detail=False, methods=['get'])
def featured(self, request):
articles = self.get_queryset().filter(is_featured=True, status='published')[:10]
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def statistics(self, request):
stats = {
'total': Article.objects.count(),
'published': Article.objects.filter(status='published').count(),
'by_category': list(Category.objects.annotate(
count=Count('articles')
).values('name', 'count')),
}
return Response(stats)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.annotate(article_count=Count('articles'))
serializer_class = CategorySerializer
lookup_field = 'slug'
三、用户系统
3.1 自定义用户模型
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
email = models.EmailField('邮箱', unique=True)
avatar = models.ImageField('头像', upload_to='avatars/', blank=True)
bio = models.TextField('个人简介', max_length=500, blank=True)
website = models.URLField('个人网站', blank=True)
location = models.CharField('位置', max_length=100, blank=True)
# 社交账号
github = models.CharField('GitHub', max_length=100, blank=True)
twitter = models.CharField('Twitter', max_length=100, blank=True)
# 设置
email_notifications = models.BooleanField('邮件通知', default=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
verbose_name = '用户'
verbose_name_plural = verbose_name
def get_full_name(self):
return f'{self.first_name} {self.last_name}'.strip() or self.username
class UserFollowing(models.Model):
"""用户关注关系"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following')
following_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='followers')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'following_user']
verbose_name = '关注'
3.2 用户视图
# apps/users/views.py
from django.contrib.auth import login, logout
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import CreateView, UpdateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .forms import RegistrationForm, ProfileForm
class UserLoginView(LoginView):
template_name = 'users/login.html'
redirect_authenticated_user = True
class UserRegisterView(CreateView):
form_class = RegistrationForm
template_name = 'users/register.html'
success_url = reverse_lazy('users:login')
def form_valid(self, form):
response = super().form_valid(form)
# 发送验证邮件
send_verification_email(self.object)
return response
class ProfileView(DetailView):
model = User
template_name = 'users/profile.html'
context_object_name = 'profile_user'
slug_field = 'username'
slug_url_kwarg = 'username'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.object
context['articles'] = user.articles.filter(status='published')[:10]
context['followers_count'] = user.followers.count()
context['following_count'] = user.following.count()
if self.request.user.is_authenticated:
context['is_following'] = UserFollowing.objects.filter(
user=self.request.user, following_user=user
).exists()
return context
class ProfileEditView(LoginRequiredMixin, UpdateView):
model = User
form_class = ProfileForm
template_name = 'users/profile_edit.html'
def get_object(self):
return self.request.user
def get_success_url(self):
return reverse_lazy('users:profile', args=[self.object.username])
四、评论系统
4.1 评论模型
# apps/comments/models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class Comment(models.Model):
# 通用外键,支持多种内容类型
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
author = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='comments'
)
parent = models.ForeignKey(
'self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies'
)
content = models.TextField('内容')
is_approved = models.BooleanField('已审核', default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '评论'
ordering = ['-created_at']
indexes = [
models.Index(fields=['content_type', 'object_id']),
]
def __str__(self):
return f'{self.author.username}: {self.content[:50]}'
@property
def is_reply(self):
return self.parent is not None
4.2 评论视图
# apps/comments/views.py
from django.views.generic import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.http import JsonResponse
from .models import Comment
from .forms import CommentForm
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
comment = form.save(commit=False)
comment.author = self.request.user
# 设置关联对象
content_type = ContentType.objects.get(
app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
comment.content_type = content_type
comment.object_id = self.kwargs['object_id']
# 回复
parent_id = self.request.POST.get('parent_id')
if parent_id:
comment.parent_id = parent_id
comment.save()
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'status': 'success',
'comment': {
'id': comment.id,
'content': comment.content,
'author': comment.author.username,
'created_at': comment.created_at.isoformat(),
}
})
return super().form_valid(form)
五、通知系统
5.1 通知模型
# apps/notifications/models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class Notification(models.Model):
NOTIFICATION_TYPES = [
('comment', '评论'),
('like', '点赞'),
('follow', '关注'),
('mention', '提及'),
]
recipient = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='notifications'
)
actor = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='actions'
)
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey()
message = models.CharField(max_length=255)
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
@classmethod
def create_notification(cls, recipient, actor, notification_type, target=None, message=''):
if recipient == actor:
return None
notification = cls.objects.create(
recipient=recipient,
actor=actor,
notification_type=notification_type,
content_object=target,
message=message,
)
# 发送实时通知
send_realtime_notification(notification)
return notification
5.2 信号处理
# apps/notifications/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.comments.models import Comment
from apps.blog.models import Article
from .models import Notification
@receiver(post_save, sender=Comment)
def notify_on_comment(sender, instance, created, **kwargs):
if created and instance.content_object:
target = instance.content_object
if hasattr(target, 'author') and target.author != instance.author:
Notification.create_notification(
recipient=target.author,
actor=instance.author,
notification_type='comment',
target=instance,
message=f'{instance.author.username} 评论了你的文章'
)
# 回复通知
if instance.parent and instance.parent.author != instance.author:
Notification.create_notification(
recipient=instance.parent.author,
actor=instance.author,
notification_type='comment',
target=instance,
message=f'{instance.author.username} 回复了你的评论'
)
六、项目配置最佳实践
6.1 环境配置
# config/settings/base.py
import os
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env()
environ.Env.read_env(BASE_DIR / '.env')
SECRET_KEY = env('SECRET_KEY')
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 第三方应用
'rest_framework',
'corsheaders',
'django_filters',
# 本地应用
'apps.users',
'apps.blog',
'apps.comments',
'apps.notifications',
]
AUTH_USER_MODEL = 'users.User'
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# 缓存
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': env('REDIS_URL', default='redis://localhost:6379'),
}
}
# Celery
CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='redis://localhost:6379/0')
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default='redis://localhost:6379/0')
6.2 依赖管理
# requirements/base.txt
Django>=4.2,<5.0
djangorestframework>=3.14
django-environ>=0.10
django-cors-headers>=4.0
django-filter>=23.0
Pillow>=10.0
celery>=5.3
redis>=4.5
# requirements/development.txt
-r base.txt
django-debug-toolbar>=4.0
pytest-django>=4.5
factory-boy>=3.2
coverage>=7.0
# requirements/production.txt
-r base.txt
gunicorn>=21.0
psycopg2-binary>=2.9
whitenoise>=6.5
sentry-sdk>=1.25
七、项目优化建议
7.1 性能优化
# 查询优化
articles = Article.objects.select_related(
'author', 'category'
).prefetch_related(
'tags',
Prefetch(
'comments',
queryset=Comment.objects.filter(is_approved=True).select_related('author')[:5]
)
).filter(status='published')
# 缓存策略
from django.core.cache import cache
def get_popular_articles():
cache_key = 'popular_articles'
articles = cache.get(cache_key)
if articles is None:
articles = list(Article.objects.filter(
status='published'
).order_by('-views')[:10])
cache.set(cache_key, articles, 60 * 15)
return articles
# 异步任务
@shared_task
def process_article_after_publish(article_id):
article = Article.objects.get(pk=article_id)
# 生成摘要、更新搜索索引、发送通知等
generate_excerpt(article)
update_search_index(article)
notify_subscribers(article)
7.2 安全加固
# 输入验证
from django.core.validators import MinLengthValidator
from bleach import clean
class Article(models.Model):
title = models.CharField(
max_length=200,
validators=[MinLengthValidator(5)]
)
def save(self, *args, **kwargs):
# 清理 HTML
self.content = clean(
self.content,
tags=['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li', 'code', 'pre'],
attributes={'a': ['href', 'title']}
)
super().save(*args, **kwargs)
# API 权限
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user