预计阅读时间:55 分钟
Django 框架深度解析
一、Django 的核心哲学
Django 是一个"大而全"的 Web 框架,它遵循 "Batteries Included" 哲学,提供了开箱即用的完整解决方案。理解 Django,首先要理解它的设计理念:
- MTV 架构:Model(数据)、Template(展示)、View(逻辑)的清晰分离
- DRY 原则:Don't Repeat Yourself,强调代码复用
- 约定优于配置:合理的默认配置,减少决策负担
- 快速开发:从概念到上线的最短路径
1.1 Django 项目结构
"""
Django 项目结构解析:
myproject/ # 项目根目录
├── manage.py # 项目管理脚本
├── myproject/ # 项目配置包
│ ├── __init__.py
│ ├── settings.py # 项目配置(核心)
│ ├── urls.py # URL 路由配置
│ ├── asgi.py # ASGI 配置(异步)
│ └── wsgi.py # WSGI 配置(部署)
│
├── app1/ # 应用1
│ ├── migrations/ # 数据库迁移文件
│ ├── __init__.py
│ ├── admin.py # 管理后台配置
│ ├── apps.py # 应用配置
│ ├── models.py # 数据模型(核心)
│ ├── views.py # 视图函数/类
│ ├── urls.py # 应用路由
│ ├── serializers.py # DRF 序列化器
│ └── tests.py # 测试
│
├── templates/ # 全局模板目录
├── static/ # 静态文件
└── media/ # 用户上传文件
"""
# settings.py 核心配置示例
"""
# 关键配置项
DEBUG = True # 开发模式
ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # 允许访问的主机
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',
]
DATABASES = { # 数据库配置
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'postgres',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
}
}
TEMPLATES = [ # 模板配置
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
],
},
},
]
"""
二、模型层(Models)
2.1 定义数据模型
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator, EmailValidator
class BaseModel(models.Model):
"""抽象基类,提供通用字段"""
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
is_active = models.BooleanField(default=True, verbose_name="是否激活")
class Meta:
abstract = True # 抽象类,不会创建数据表
class Category(BaseModel):
"""文章分类"""
name = models.CharField(max_length=100, unique=True, verbose_name="分类名称")
slug = models.SlugField(max_length=100, unique=True, verbose_name="URL 标识")
description = models.TextField(blank=True, verbose_name="描述")
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name="父分类"
)
class Meta:
verbose_name = "分类"
verbose_name_plural = "分类"
ordering = ['name']
indexes = [
models.Index(fields=['name']),
models.Index(fields=['slug']),
]
def __str__(self):
return self.name
def get_absolute_url(self):
from django.urls import reverse
return reverse('category-detail', kwargs={'slug': self.slug})
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(max_length=50, unique=True, verbose_name="标签名称")
slug = models.SlugField(max_length=50, unique=True, verbose_name="URL 标识")
class Meta:
verbose_name = "标签"
verbose_name_plural = "标签"
def __str__(self):
return self.name
class Author(BaseModel):
"""作者信息(扩展 User 模型)"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='author')
bio = models.TextField(blank=True, verbose_name="个人简介")
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name="头像")
website = models.URLField(blank=True, verbose_name="个人网站")
# 统计字段(通过信号或定时任务更新)
total_posts = models.PositiveIntegerField(default=0, verbose_name="文章总数")
total_views = models.PositiveIntegerField(default=0, verbose_name="总浏览量")
class Meta:
verbose_name = "作者"
verbose_name_plural = "作者"
def __str__(self):
return self.user.username
@property
def full_name(self):
return f"{self.user.first_name} {self.user.last_name}".strip() or self.user.username
class Post(BaseModel):
"""博客文章"""
# 状态选项
STATUS_DRAFT = 'draft'
STATUS_PUBLISHED = 'published'
STATUS_ARCHIVED = 'archived'
STATUS_CHOICES = [
(STATUS_DRAFT, '草稿'),
(STATUS_PUBLISHED, '已发布'),
(STATUS_ARCHIVED, '已归档'),
]
# 基本字段
title = models.CharField(max_length=200, verbose_name="标题")
slug = models.SlugField(max_length=200, unique_for_date='published_at', verbose_name="URL 标识")
content = models.TextField(verbose_name="内容")
excerpt = models.TextField(max_length=500, blank=True, verbose_name="摘要")
# 关系字段
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='posts',
verbose_name="作者"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='posts',
verbose_name="分类"
)
tags = models.ManyToManyField(Tag, blank=True, related_name='posts', verbose_name="标签")
# 状态和时间
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default=STATUS_DRAFT,
verbose_name="状态"
)
published_at = models.DateTimeField(default=timezone.now, verbose_name="发布时间")
# 统计字段
views_count = models.PositiveIntegerField(default=0, verbose_name="浏览次数")
likes_count = models.PositiveIntegerField(default=0, verbose_name="点赞数")
comments_count = models.PositiveIntegerField(default=0, verbose_name="评论数")
# 高级字段
meta_description = models.CharField(max_length=160, blank=True, verbose_name="SEO 描述")
meta_keywords = models.CharField(max_length=255, blank=True, verbose_name="SEO 关键词")
is_featured = models.BooleanField(default=False, verbose_name="是否精选")
class Meta:
verbose_name = "文章"
verbose_name_plural = "文章"
ordering = ['-published_at']
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['slug', 'published_at']),
models.Index(fields=['author', 'status']),
]
constraints = [
models.UniqueConstraint(
fields=['author', 'title'],
name='unique_author_title'
)
]
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse('post-detail', kwargs={
'year': self.published_at.year,
'month': self.published_at.month,
'day': self.published_at.day,
'slug': self.slug
})
def save(self, *args, **kwargs):
"""重写保存方法"""
# 自动生成摘要
if not self.excerpt:
self.excerpt = self.content[:200]
# 自动生成 slug
if not self.slug:
from django.utils.text import slugify
self.slug = slugify(self.title)
super().save(*args, **kwargs)
@property
def reading_time(self):
"""估算阅读时间(分钟)"""
words_per_minute = 200
word_count = len(self.content.split())
return max(1, round(word_count / words_per_minute))
class Comment(BaseModel):
"""评论"""
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(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(verbose_name="评论内容")
class Meta:
verbose_name = "评论"
verbose_name_plural = "评论"
ordering = ['-created_at']
def __str__(self):
return f"{self.author.username} 评论了 {self.post.title}"
@property
def is_reply(self):
return self.parent is not None
2.2 模型管理器与查询集
# managers.py
from django.db import models
from django.utils import timezone
class PostQuerySet(models.QuerySet):
"""自定义查询集"""
def published(self):
"""已发布的文章"""
return self.filter(status='published', published_at__lte=timezone.now())
def draft(self):
"""草稿"""
return self.filter(status='draft')
def featured(self):
"""精选文章"""
return self.published().filter(is_featured=True)
def by_author(self, author_id):
"""按作者筛选"""
return self.filter(author_id=author_id)
def by_category(self, category_slug):
"""按分类筛选"""
return self.filter(category__slug=category_slug)
def by_tag(self, tag_slug):
"""按标签筛选"""
return self.filter(tags__slug=tag_slug)
def popular(self, days=30):
"""热门文章(最近 N 天)"""
from django.utils import timezone
from datetime import timedelta
start_date = timezone.now() - timedelta(days=days)
return self.published().filter(
published_at__gte=start_date
).order_by('-views_count', '-likes_count')
def with_related(self):
"""预加载关联数据"""
return self.select_related('author', 'category').prefetch_related('tags')
class PostManager(models.Manager):
"""文章管理器"""
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def published(self):
return self.get_queryset().published()
def featured(self):
return self.get_queryset().featured()
def popular(self, days=30):
return self.get_queryset().popular(days)
# 在 Post 模型中添加
# objects = PostManager()
# 使用示例
"""
# 获取所有已发布文章
posts = Post.objects.published()
# 获取精选文章,并按浏览量排序
featured_posts = Post.objects.featured().order_by('-views_count')[:5]
# 获取最近30天热门文章
popular_posts = Post.objects.popular(days=30)[:10]
# 复杂查询
from django.db.models import Q, Count, Avg, F
# Q 对象实现复杂条件
posts = Post.objects.filter(
Q(status='published') &
(Q(category__name='Python') | Q(tags__name='Django'))
)
# 聚合查询
from django.db.models import Count, Avg, Sum
# 每个作者的文章数
author_stats = Author.objects.annotate(
post_count=Count('posts'),
avg_views=Avg('posts__views_count')
).filter(post_count__gt=0)
# F 表达式(引用字段值)
Post.objects.update(views_count=F('views_count') + 1)
# 子查询
from django.db.models import Subquery, OuterRef
# 获取每篇文章的最新评论
latest_comments = Comment.objects.filter(
post=OuterRef('pk')
).order_by('-created_at')
posts_with_latest_comment = Post.objects.annotate(
latest_comment_content=Subquery(latest_comments.values('content')[:1])
)
"""
2.3 数据库迁移
"""
Django 迁移命令:
# 创建迁移文件
python manage.py makemigrations
# 查看迁移 SQL
python manage.py sqlmigrate app_name 0001
# 执行迁移
python manage.py migrate
# 回滚迁移
python manage.py migrate app_name 0001
# 查看迁移状态
python manage.py showmigrations
"""
# 自定义迁移示例
# migrations/0002_add_indexes.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
# 添加索引
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['status', 'published_at'], name='status_published_idx'),
),
# 运行自定义 SQL
migrations.RunSQL(
sql="""
CREATE FULLTEXT INDEX post_content_ft_idx
ON blog_post(content);
""",
reverse_sql="""
DROP INDEX post_content_ft_idx ON blog_post;
"""
),
# 使用 Python 函数迁移数据
migrations.RunPython(
code=migrate_slugs_forward,
reverse_code=migrate_slugs_backward,
),
]
def migrate_slugs_forward(apps, schema_editor):
"""为没有 slug 的文章生成 slug"""
Post = apps.get_model('blog', 'Post')
from django.utils.text import slugify
for post in Post.objects.filter(slug=''):
post.slug = slugify(post.title)
post.save(update_fields=['slug'])
def migrate_slugs_backward(apps, schema_editor):
"""回滚操作"""
pass
三、视图层(Views)
3.1 函数视图与类视图
# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse, Http404
from django.views import View
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView,
TemplateView, RedirectView, ArchiveIndexView, YearArchiveView,
MonthArchiveView, DayArchiveView, DateDetailView
)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.urls import reverse_lazy
from django.utils import timezone
from django.contrib import messages
from django.core.cache import cache
from .models import Post, Category, Tag, Comment
from .forms import PostForm, CommentForm, SearchForm
# ============== 函数视图 ==============
def post_list(request):
"""文章列表视图(函数式)"""
queryset = Post.objects.published().with_related()
# 搜索
search_form = SearchForm(request.GET)
if search_form.is_valid():
query = search_form.cleaned_data.get('query')
if query:
queryset = queryset.filter(
Q(title__icontains=query) |
Q(content__icontains=query)
)
# 分类筛选
category_slug = request.GET.get('category')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
# 标签筛选
tag_slug = request.GET.get('tag')
if tag_slug:
queryset = queryset.filter(tags__slug=tag_slug)
# 分页
paginator = Paginator(queryset, 10)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'search_form': search_form,
'categories': Category.objects.annotate(post_count=Count('posts')),
'popular_tags': Tag.objects.annotate(post_count=Count('posts')).order_by('-post_count')[:10],
}
return render(request, 'blog/post_list.html', context)
def post_detail(request, year, month, day, slug):
"""文章详情视图(函数式)"""
# 使用缓存减少数据库查询
cache_key = f'post_detail_{year}_{month}_{day}_{slug}'
post = cache.get(cache_key)
if not post:
post = get_object_or_404(
Post.objects.with_related()
.filter(
published_at__year=year,
published_at__month=month,
published_at__day=day,
slug=slug,
status='published'
)
)
cache.set(cache_key, post, 3600) # 缓存1小时
# 增加浏览量(使用 F 表达式避免竞态条件)
Post.objects.filter(pk=post.pk).update(views_count=F('views_count') + 1)
# 获取相关文章
related_posts = Post.objects.published().filter(
Q(category=post.category) | Q(tags__in=post.tags.all())
).exclude(pk=post.pk).distinct()[:5]
# 获取评论
comments = post.comments.filter(is_active=True, parent__isnull=True)
# 评论表单
if request.method == 'POST':
comment_form = CommentForm(request.POST)
if comment_form.is_valid() and request.user.is_authenticated:
comment = comment_form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, '评论已提交!')
return redirect(post.get_absolute_url())
else:
comment_form = CommentForm()
context = {
'post': post,
'related_posts': related_posts,
'comments': comments,
'comment_form': comment_form,
}
return render(request, 'blog/post_detail.html', context)
# ============== 类视图 ==============
class PostListView(ListView):
"""文章列表视图(类视图)"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
queryset = Post.objects.published().with_related()
# 搜索
query = self.request.GET.get('q')
if query:
queryset = queryset.filter(
Q(title__icontains=query) |
Q(content__icontains=query)
)
# 分类筛选
self.category = None
category_slug = self.kwargs.get('category_slug')
if category_slug:
self.category = get_object_or_404(Category, slug=category_slug)
queryset = queryset.filter(category=self.category)
# 标签筛选
self.tag = None
tag_slug = self.kwargs.get('tag_slug')
if tag_slug:
self.tag = get_object_or_404(Tag, slug=tag_slug)
queryset = queryset.filter(tags=self.tag)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = getattr(self, 'category', None)
context['tag'] = getattr(self, 'tag', None)
context['categories'] = Category.objects.annotate(
post_count=Count('posts')
)
context['popular_tags'] = Tag.objects.annotate(
post_count=Count('posts')
).order_by('-post_count')[:10]
return context
class PostDetailView(DetailView):
"""文章详情视图(类视图)"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.published().with_related()
def get_object(self, queryset=None):
obj = super().get_object(queryset)
# 增加浏览量
Post.objects.filter(pk=obj.pk).update(views_count=F('views_count') + 1)
return obj
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
post = self.object
# 相关文章
context['related_posts'] = Post.objects.published().filter(
Q(category=post.category) | Q(tags__in=post.tags.all())
).exclude(pk=post.pk).distinct()[:5]
# 评论
context['comments'] = post.comments.filter(
is_active=True,
parent__isnull=True
)
context['comment_form'] = CommentForm()
return context
class PostCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user.author
messages.success(self.request, '文章创建成功!')
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('post-detail', kwargs={'pk': self.object.pk})
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
"""更新文章视图"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self):
"""检查用户是否有权限编辑"""
post = self.get_object()
return self.request.user == post.author.user or self.request.user.is_staff
def form_valid(self, form):
messages.success(self.request, '文章更新成功!')
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('post-detail', kwargs={'pk': self.object.pk})
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""删除文章视图"""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('post-list')
def test_func(self):
post = self.get_object()
return self.request.user == post.author.user or self.request.user.is_staff
def delete(self, request, *args, **kwargs):
messages.success(request, '文章已删除!')
return super().delete(request, *args, **kwargs)
# ============== API 视图 ==============
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
import json
@require_http_methods(["GET", "POST"])
def api_post_list(request):
"""文章列表 API"""
if request.method == 'GET':
posts = Post.objects.published().values(
'id', 'title', 'slug', 'excerpt', 'published_at',
'author__user__username', 'category__name'
)[:20]
return JsonResponse({'posts': list(posts)})
elif request.method == 'POST':
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
# 处理 POST 请求
form = PostForm(data)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user.author
post.save()
return JsonResponse({
'id': post.id,
'title': post.title,
'slug': post.slug
}, status=201)
else:
return JsonResponse({'errors': form.errors}, status=400)
@csrf_exempt
@require_http_methods(["POST"])
def api_toggle_like(request, post_id):
"""点赞/取消点赞 API"""
if not request.user.is_authenticated:
return JsonResponse({'error': 'Authentication required'}, status=401)
post = get_object_or_404(Post, pk=post_id)
# 检查是否已点赞
liked = request.user.liked_posts.filter(pk=post.pk).exists()
if liked:
request.user.liked_posts.remove(post)
post.likes_count = F('likes_count') - 1
action = 'unliked'
else:
request.user.liked_posts.add(post)
post.likes_count = F('likes_count') + 1
action = 'liked'
post.save(update_fields=['likes_count'])
post.refresh_from_db()
return JsonResponse({
'action': action,
'likes_count': post.likes_count
})
3.2 中间件
# middleware.py
import time
import logging
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponseForbidden
from django.core.cache import cache
from django.conf import settings
logger = logging.getLogger(__name__)
class RequestLoggingMiddleware(MiddlewareMixin):
"""记录请求日志"""
def process_request(self, request):
request.start_time = time.time()
# 记录请求信息
logger.info(
f"Request: {request.method} {request.path} "
f"from {request.META.get('REMOTE_ADDR')}"
)
def process_response(self, request, response):
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.info(
f"Response: {response.status_code} "
f"Duration: {duration:.3f}s"
)
return response
class RateLimitMiddleware(MiddlewareMixin):
"""简单的请求限流中间件"""
def process_request(self, request):
# 只对 API 路径限流
if not request.path.startswith('/api/'):
return None
# 获取客户端 IP
ip = request.META.get('REMOTE_ADDR')
key = f'rate_limit:{ip}'
# 获取请求计数
count = cache.get(key, 0)
if count >= 100: # 每分钟最多100个请求
return HttpResponseForbidden('Rate limit exceeded')
# 增加计数
cache.set(key, count + 1, timeout=60)
return None
class MaintenanceModeMiddleware(MiddlewareMixin):
"""维护模式中间件"""
def process_request(self, request):
# 检查是否处于维护模式
if cache.get('maintenance_mode') and not request.user.is_staff:
from django.shortcuts import render
return render(request, 'maintenance.html', status=503)
return None
四、模板层(Templates)
4.1 模板基础
<!-- 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/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<a href="{% url 'home' %}" class="logo">Blog</a>
<ul class="nav-links">
<li><a href="{% url 'post-list' %}">文章</a></li>
<li><a href="{% url 'category-list' %}">分类</a></li>
{% if user.is_authenticated %}
<li><a href="{% url 'profile' %}">{{ user.username }}</a></li>
<li>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">退出</button>
</form>
</li>
{% else %}
<li><a href="{% url 'login' %}">登录</a></li>
<li><a href="{% url 'register' %}">注册</a></li>
{% endif %}
</ul>
</div>
</nav>
<!-- 消息提示 -->
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<!-- 主要内容 -->
<main class="container">
{% block content %}{% endblock %}
</main>
<!-- 侧边栏 -->
<aside class="sidebar">
{% block sidebar %}
{% include "blog/includes/sidebar.html" %}
{% endblock %}
</aside>
<!-- 页脚 -->
<footer>
<p>© {% now "Y" %} 我的博客. All rights reserved.</p>
</footer>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
<!-- post_list.html - 文章列表模板 -->
{% extends "base.html" %}
{% load static %}
{% load blog_tags %}
{% block title %}
{% if category %}{{ category.name }}{% elif tag %}{{ tag.name }}{% else %}所有文章{% endif %} - 我的博客
{% endblock %}
{% block content %}
<div class="post-list-container">
<h1>
{% if category %}
分类:{{ category.name }}
{% elif tag %}
标签:{{ tag.name }}
{% else %}
最新文章
{% endif %}
</h1>
<!-- 搜索框 -->
<form method="get" class="search-form">
<input type="text" name="q" placeholder="搜索文章..." value="{{ request.GET.q }}">
<button type="submit">搜索</button>
</form>
<!-- 文章列表 -->
{% if posts %}
{% for post in posts %}
<article class="post-preview">
<h2>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</h2>
<div class="post-meta">
<span class="author">
<i class="fas fa-user"></i> {{ post.author.user.username }}
</span>
<span class="date">
<i class="fas fa-calendar"></i> {{ post.published_at|date:"Y-m-d" }}
</span>
<span class="category">
<i class="fas fa-folder"></i>
<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
</span>
<span class="views">
<i class="fas fa-eye"></i> {{ post.views_count }}
</span>
<span class="comments">
<i class="fas fa-comments"></i> {{ post.comments_count }}
</span>
</div>
<div class="post-excerpt">
{{ post.excerpt|default:post.content|truncatewords:50 }}
</div>
{% if post.tags.all %}
<div class="post-tags">
<i class="fas fa-tags"></i>
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
<a href="{{ post.get_absolute_url }}" class="read-more">
阅读全文 →
</a>
</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 class="current">
第 {{ 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 %}
{% else %}
<p>暂无文章。</p>
{% endif %}
</div>
{% endblock %}
{% block sidebar %}
{% include "blog/includes/categories.html" %}
{% include "blog/includes/popular_tags.html" %}
{% include "blog/includes/recent_posts.html" %}
{% endblock %}
<!-- post_detail.html - 文章详情模板 -->
{% extends "base.html" %}
{% load static %}
{% load blog_tags %}
{% block title %}{{ post.title }} - 我的博客{% endblock %}
{% block meta %}
<meta name="description" content="{{ post.meta_description|default:post.excerpt }}">
<meta name="keywords" content="{{ post.meta_keywords }}">
<meta property="og:title" content="{{ post.title }}">
<meta property="og:description" content="{{ post.excerpt }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
{% endblock %}
{% block content %}
<article class="post-detail">
<header class="post-header">
<h1>{{ post.title }}</h1>
<div class="post-meta">
<span class="author">
<img src="{{ post.author.avatar.url }}" alt="{{ post.author }}" class="avatar">
{{ post.author.user.username }}
</span>
<span class="date">{{ post.published_at|date:"Y-m-d H:i" }}</span>
<span class="reading-time">{{ post.reading_time }} 分钟阅读</span>
</div>
</header>
<div class="post-content">
{{ post.content|safe }}
</div>
<footer class="post-footer">
<!-- 标签 -->
{% if post.tags.all %}
<div class="post-tags">
<strong>标签:</strong>
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
<!-- 点赞按钮 -->
<div class="post-actions">
<button class="btn-like" data-post-id="{{ post.id }}">
<i class="fas fa-heart"></i>
<span class="likes-count">{{ post.likes_count }}</span> 点赞
</button>
<button class="btn-share">
<i class="fas fa-share"></i> 分享
</button>
</div>
</footer>
</article>
<!-- 相关文章 -->
{% if related_posts %}
<section class="related-posts">
<h3>相关文章</h3>
<div class="post-grid">
{% for related in related_posts %}
<div class="post-card">
<h4><a href="{{ related.get_absolute_url }}">{{ related.title }}</a></h4>
<p>{{ related.excerpt|truncatewords:20 }}</p>
</div>
{% endfor %}
</div>
</section>
{% endif %}
<!-- 评论区域 -->
<section class="comments-section">
<h3>评论 ({{ comments.count }})</h3>
<!-- 评论列表 -->
{% if comments %}
<div class="comments-list">
{% for comment in comments %}
<div class="comment" id="comment-{{ comment.id }}">
<div class="comment-avatar">
<img src="{{ comment.author.profile.avatar.url }}" alt="{{ comment.author.username }}">
</div>
<div class="comment-content">
<div class="comment-meta">
<strong>{{ comment.author.username }}</strong>
<span class="comment-time">{{ comment.created_at|timesince }}前</span>
</div>
<p>{{ comment.content }}</p>
<button class="btn-reply" data-comment-id="{{ comment.id }}">回复</button>
<!-- 回复列表 -->
{% if comment.replies.all %}
<div class="replies">
{% for reply in comment.replies.all %}
<div class="reply">
<strong>{{ reply.author.username }}</strong>
<p>{{ reply.content }}</p>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 评论表单 -->
{% if user.is_authenticated %}
<div class="comment-form">
<h4>发表评论</h4>
<form method="post">
{% csrf_token %}
{{ comment_form.as_p }}
<button type="submit" class="btn-primary">提交评论</button>
</form>
</div>
{% else %}
<p class="login-prompt">
请 <a href="{% url 'login' %}?next={{ request.path }}">登录</a> 后发表评论。
</p>
{% endif %}
</section>
{% endblock %}
{% block extra_js %}
<script>
// 点赞功能
document.querySelector('.btn-like')?.addEventListener('click', function() {
const postId = this.dataset.postId;
const likesSpan = this.querySelector('.likes-count');
fetch(`/api/posts/${postId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
},
})
.then(response => response.json())
.then(data => {
likesSpan.textContent = data.likes_count;
this.classList.toggle('liked', data.action === 'liked');
});
});
// 获取 CSRF Token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
{% endblock %}
4.2 自定义模板标签
# templatetags/blog_tags.py
from django import template
from django.db.models import Count, Q
from django.utils.safestring import mark_safe
import markdown
from ..models import Post, Category, Tag
register = template.Library()
@register.simple_tag
def get_recent_posts(num=5):
"""获取最近文章"""
return Post.objects.published()[:num]
@register.simple_tag
def get_popular_posts(num=5, days=30):
"""获取热门文章"""
return Post.objects.popular(days=days)[:num]
@register.simple_tag
def get_categories():
"""获取所有分类及文章数"""
return Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
)
@register.simple_tag
def get_tags():
"""获取所有标签及文章数"""
return Tag.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
).order_by('-post_count')
@register.inclusion_tag('blog/includes/recent_posts.html')
def show_recent_posts(num=5):
"""显示最近文章(包含模板)"""
return {'posts': get_recent_posts(num)}
@register.filter(name='markdown')
def markdown_filter(text):
"""将 Markdown 转换为 HTML"""
return mark_safe(markdown.markdown(
text,
extensions=['extra', 'codehilite', 'toc']
))
@register.filter(name='truncate_chars')
def truncate_chars(value, max_length):
"""按字符数截断"""
if len(value) > max_length:
return value[:max_length] + '...'
return value
@register.simple_tag(takes_context=True)
def is_active(context, url_name):
"""检查当前 URL 是否匹配,用于导航高亮"""
request = context['request']
return 'active' if request.resolver_match.url_name == url_name else ''
@register.assignment_tag
def get_archives():
"""获取文章归档"""
from django.db.models.functions import TruncMonth
return Post.objects.published().annotate(
month=TruncMonth('published_at')
).values('month').annotate(
count=Count('id')
).order_by('-month')
五、表单处理
5.1 表单定义与验证
# forms.py
from django import forms
from django.core.validators import EmailValidator, RegexValidator
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
from .models import Post, Comment, Author
class PostForm(forms.ModelForm):
"""文章表单"""
# 自定义字段
tags_input = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': '用逗号分隔多个标签'}),
help_text='用逗号分隔多个标签'
)
class Meta:
model = Post
fields = ['title', 'content', 'category', 'excerpt',
'meta_description', 'meta_keywords', 'status']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 20}),
'excerpt': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'category': forms.Select(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
}
labels = {
'title': '文章标题',
'content': '文章内容',
}
help_texts = {
'excerpt': '留空则自动从内容中提取前200字',
}
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')
if status == 'published':
# 发布前检查内容长度
content = cleaned_data.get('content', '')
if len(content) < 100:
raise forms.ValidationError(
'发布文章内容至少需要100个字符'
)
return cleaned_data
def save(self, commit=True):
"""保存表单,处理标签"""
post = super().save(commit=False)
if commit:
post.save()
self.save_m2m() # 保存多对多关系
# 处理标签
tags_input = self.cleaned_data.get('tags_input', '')
if tags_input:
tag_names = [name.strip() for name in tags_input.split(',') if name.strip()]
tags = []
for name in tag_names:
tag, _ = Tag.objects.get_or_create(
name=name,
defaults={'slug': slugify(name)}
)
tags.append(tag)
post.tags.set(tags)
return post
class CommentForm(forms.ModelForm):
"""评论表单"""
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': '写下你的评论...'
})
}
labels = {
'content': ''
}
def clean_content(self):
content = self.cleaned_data.get('content')
# 防止垃圾评论
if 'http://' in content or 'https://' in content:
raise forms.ValidationError('评论中不能包含链接')
return content
class SearchForm(forms.Form):
"""搜索表单"""
query = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '搜索文章...'
})
)
class UserRegisterForm(UserCreationForm):
"""用户注册表单"""
email = forms.EmailField(
required=True,
validators=[EmailValidator()],
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
}
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError('该邮箱已被注册')
return email
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
# 创建关联的 Author 对象
Author.objects.create(user=user)
return user
class UserLoginForm(AuthenticationForm):
"""用户登录表单"""
username = forms.CharField(
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '用户名'})
)
password = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '密码'})
)
六、Admin 管理后台
6.1 自定义 Admin
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone
from django.db.models import Count, Sum
from .models import Post, Category, Tag, Comment, Author
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'parent', 'post_count', 'created_at']
list_filter = ['created_at', 'parent']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
ordering = ['name']
def get_queryset(self, request):
return super().get_queryset(request).annotate(
post_count=Count('posts')
)
def post_count(self, obj):
return obj.post_count
post_count.short_description = '文章数'
post_count.admin_order_field = 'post_count'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count', 'created_at']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def get_queryset(self, request):
return super().get_queryset(request).annotate(
post_count=Count('posts')
)
def post_count(self, obj):
return obj.post_count
post_count.short_description = '文章数'
class CommentInline(admin.TabularInline):
"""内联评论"""
model = Comment
extra = 0
fields = ['author', 'content', 'is_active', 'created_at']
readonly_fields = ['created_at']
raw_id_fields = ['author']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = [
'title', 'author', 'category', 'status',
'views_count', 'likes_count', 'published_at'
]
list_filter = ['status', 'category', 'tags', 'is_featured', 'published_at']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ['author']
date_hierarchy = 'published_at'
ordering = ['-published_at']
fieldsets = (
('基本信息', {
'fields': ('title', 'slug', 'content', 'excerpt')
}),
('分类与标签', {
'fields': ('category', 'tags')
}),
('发布信息', {
'fields': ('author', 'status', 'published_at', 'is_featured')
}),
('统计数据', {
'fields': ('views_count', 'likes_count', 'comments_count'),
'classes': ('collapse',)
}),
('SEO', {
'fields': ('meta_description', 'meta_keywords'),
'classes': ('collapse',)
}),
)
readonly_fields = ['views_count', 'likes_count', 'comments_count']
inlines = [CommentInline]
actions = ['make_published', 'make_draft', 'make_featured', 'unmake_featured']
def get_queryset(self, request):
return super().get_queryset(request).select_related('author', 'category')
def make_published(self, request, queryset):
"""批量发布"""
updated = queryset.update(status='published', published_at=timezone.now())
self.message_user(request, f'{updated} 篇文章已发布')
make_published.short_description = '发布选中文章'
def make_draft(self, request, queryset):
"""批量设为草稿"""
updated = queryset.update(status='draft')
self.message_user(request, f'{updated} 篇文章已设为草稿')
make_draft.short_description = '设为草稿'
def make_featured(self, request, queryset):
"""批量设为精选"""
updated = queryset.update(is_featured=True)
self.message_user(request, f'{updated} 篇文章已设为精选')
make_featured.short_description = '设为精选'
def unmake_featured(self, request, queryset):
"""取消精选"""
updated = queryset.update(is_featured=False)
self.message_user(request, f'{updated} 篇文章已取消精选')
unmake_featured.short_description = '取消精选'
def view_on_site(self, obj):
"""查看网站上的文章"""
return obj.get_absolute_url()
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['author', 'post_link', 'content_preview', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['content', 'author__username', 'author__email']
raw_id_fields = ['author', 'post', 'parent']
actions = ['approve_comments', 'disapprove_comments']
def post_link(self, obj):
"""文章链接"""
url = reverse('admin:blog_post_change', args=[obj.post.id])
return format_html('<a href="{}">{}</a>', url, obj.post.title)
post_link.short_description = '文章'
def content_preview(self, obj):
"""内容预览"""
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = '内容'
def approve_comments(self, request, queryset):
"""批量批准评论"""
updated = queryset.update(is_active=True)
self.message_user(request, f'{updated} 条评论已批准')
approve_comments.short_description = '批准选中评论'
def disapprove_comments(self, request, queryset):
"""批量取消批准"""
updated = queryset.update(is_active=False)
self.message_user(request, f'{updated} 条评论已取消批准')
disapprove_comments.short_description = '取消批准'
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
list_display = ['user', 'total_posts', 'total_views', 'created_at']
search_fields = ['user__username', 'user__email', 'bio']
raw_id_fields = ['user']
fieldsets = (
('基本信息', {
'fields': ('user', 'bio', 'avatar', 'website')
}),
('统计数据', {
'fields': ('total_posts', 'total_views'),
}),
)
readonly_fields = ['total_posts', 'total_views']
# 自定义 Admin 站点
class BlogAdminSite(admin.AdminSite):
site_header = '博客管理系统'
site_title = '博客管理'
index_title = '管理面板'
def each_context(self, request):
context = super().each_context(request)
# 添加自定义上下文
context['total_posts'] = Post.objects.count()
context['total_comments'] = Comment.objects.filter(is_active=True).count()
context['recent_comments'] = Comment.objects.order_by('-created_at')[:5]
return context
# 使用自定义 Admin 站点
# admin_site = BlogAdminSite(name='blog_admin')
# admin_site.register(Post, PostAdmin)
七、URL 路由与视图
7.1 URL 配置
# 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
from django.views.generic import TemplateView
from django.contrib.sitemaps.views import sitemap
from blog.sitemaps import PostSitemap, CategorySitemap
sitemaps = {
'posts': PostSitemap,
'categories': CategorySitemap,
}
urlpatterns = [
# 管理后台
path('admin/', admin.site.urls),
# 博客应用
path('', include('blog.urls')),
# 用户认证
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('accounts.urls')),
# API
path('api/', include('blog.api.urls')),
# SEO
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='sitemap'),
path('robots.txt', TemplateView.as_view(
template_name='robots.txt',
content_type='text/plain'
)),
]
# 开发环境下提供媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Django Debug Toolbar
import debug_toolbar
urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
# blog/urls.py (应用级)
from django.urls import path, register_converter
from . import views
from . import feeds
# 自定义 URL 转换器
class YearConverter:
regex = r'[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:04d}'
class MonthConverter:
regex = r'0?[1-9]|1[0-2]'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:02d}'
register_converter(YearConverter, 'year')
register_converter(MonthConverter, 'month')
app_name = 'blog'
urlpatterns = [
# 文章
path('', views.PostListView.as_view(), name='post-list'),
path(
'post/<year:year>/<month:month>/<int:day>/<slug:slug>/',
views.PostDetailView.as_view(),
name='post-detail'
),
path('post/create/', views.PostCreateView.as_view(), name='post-create'),
path('post/<int:pk>/update/', views.PostUpdateView.as_view(), name='post-update'),
path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post-delete'),
# 分类
path('category/', views.CategoryListView.as_view(), name='category-list'),
path('category/<slug:slug>/', views.PostListView.as_view(), name='category-detail'),
# 标签
path('tag/', views.TagListView.as_view(), name='tag-list'),
path('tag/<slug:slug>/', views.PostListView.as_view(), name='tag-detail'),
# 归档
path('archive/', views.ArchiveView.as_view(), name='archive'),
path('archive/<year:year>/', views.YearArchiveView.as_view(), name='year-archive'),
path(
'archive/<year:year>/<month:month>/',
views.MonthArchiveView.as_view(),
name='month-archive'
),
# 搜索
path('search/', views.SearchView.as_view(), name='search'),
# Feed
path('feed/', feeds.LatestPostsFeed(), name='feed'),
path('feed/<slug:category_slug>/', feeds.CategoryPostsFeed(), name='category-feed'),
]
# blog/sitemaps.py
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import Post, Category
class PostSitemap(Sitemap):
changefreq = 'weekly'
priority = 0.8
def items(self):
return Post.objects.published()
def lastmod(self, obj):
return obj.updated_at
def location(self, obj):
return obj.get_absolute_url()
class CategorySitemap(Sitemap):
changefreq = 'monthly'
priority = 0.5
def items(self):
return Category.objects.all()
def location(self, obj):
return obj.get_absolute_url()
八、Django REST Framework
8.1 序列化器与视图集
# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from ..models import Post, Category, Tag, Comment, Author
class UserSerializer(serializers.ModelSerializer):
"""用户序列化器"""
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'full_name']
read_only_fields = ['id', 'username']
def get_full_name(self, obj):
return obj.get_full_name() or obj.username
class AuthorSerializer(serializers.ModelSerializer):
"""作者序列化器"""
user = UserSerializer(read_only=True)
post_count = serializers.IntegerField(source='total_posts', read_only=True)
class Meta:
model = Author
fields = ['id', 'user', 'bio', 'avatar', 'website', 'post_count', 'total_views']
class CategorySerializer(serializers.ModelSerializer):
"""分类序列化器"""
post_count = serializers.IntegerField(read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'post_count']
read_only_fields = ['slug']
class TagSerializer(serializers.ModelSerializer):
"""标签序列化器"""
post_count = serializers.IntegerField(read_only=True)
class Meta:
model = Tag
fields = ['id', 'name', 'slug', 'post_count']
read_only_fields = ['slug']
class CommentSerializer(serializers.ModelSerializer):
"""评论序列化器"""
author = UserSerializer(read_only=True)
author_id = serializers.IntegerField(write_only=True, required=False)
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ['id', 'post', 'author', 'author_id', 'parent', 'content',
'replies', 'created_at']
read_only_fields = ['author', 'created_at']
def get_replies(self, obj):
if obj.replies.exists():
return CommentSerializer(obj.replies.all(), many=True).data
return []
def create(self, validated_data):
request = self.context.get('request')
if request and request.user.is_authenticated:
validated_data['author'] = request.user
return super().create(validated_data)
class PostListSerializer(serializers.ModelSerializer):
"""文章列表序列化器(简化版)"""
author = AuthorSerializer(read_only=True)
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'excerpt', 'author', 'category',
'tags', 'views_count', 'likes_count', 'comments_count',
'published_at', 'reading_time']
read_only_fields = ['views_count', 'likes_count', 'comments_count']
class PostDetailSerializer(serializers.ModelSerializer):
"""文章详情序列化器(完整版)"""
author = AuthorSerializer(read_only=True)
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
comments = CommentSerializer(many=True, read_only=True)
related_posts = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'excerpt', 'author',
'category', 'tags', 'comments', 'related_posts',
'views_count', 'likes_count', 'comments_count',
'published_at', 'reading_time', 'meta_description',
'meta_keywords']
read_only_fields = ['views_count', 'likes_count', 'comments_count', 'slug']
def get_related_posts(self, obj):
related = Post.objects.published().filter(
Q(category=obj.category) | Q(tags__in=obj.tags.all())
).exclude(pk=obj.pk).distinct()[:5]
return PostListSerializer(related, many=True).data
class PostCreateUpdateSerializer(serializers.ModelSerializer):
"""文章创建/更新序列化器"""
tags_input = serializers.CharField(write_only=True, required=False)
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'tags_input',
'status', 'meta_description', 'meta_keywords']
def create(self, validated_data):
tags_input = validated_data.pop('tags_input', '')
request = self.context.get('request')
# 设置作者
validated_data['author'] = request.user.author
post = super().create(validated_data)
# 处理标签
self._process_tags(post, tags_input)
return post
def update(self, instance, validated_data):
tags_input = validated_data.pop('tags_input', '')
instance = super().update(instance, validated_data)
# 处理标签
if tags_input:
self._process_tags(instance, tags_input)
return instance
def _process_tags(self, post, tags_input):
"""处理标签"""
if not tags_input:
return
from django.utils.text import slugify
tag_names = [name.strip() for name in tags_input.split(',') if name.strip()]
tags = []
for name in tag_names:
tag, _ = Tag.objects.get_or_create(
name=name,
defaults={'slug': slugify(name)}
)
tags.append(tag)
post.tags.set(tags)
# api/views.py
from rest_framework import viewsets, status, generics, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from ..models import Post, Category, Tag, Comment
from ..serializers import (
PostListSerializer, PostDetailSerializer, PostCreateUpdateSerializer,
CategorySerializer, TagSerializer, CommentSerializer
)
class StandardResultsSetPagination(PageNumberPagination):
"""标准分页"""
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class IsAuthorOrReadOnly(permissions.BasePermission):
"""只有作者可以编辑"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author.user == request.user
class PostViewSet(viewsets.ModelViewSet):
"""文章视图集"""
queryset = Post.objects.published().select_related('author', 'category')
pagination_class = StandardResultsSetPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['category__slug', 'tags__slug', 'status']
search_fields = ['title', 'content']
ordering_fields = ['published_at', 'views_count', 'likes_count']
ordering = ['-published_at']
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return PostCreateUpdateSerializer
return PostDetailSerializer
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [permissions.IsAuthenticated(), IsAuthorOrReadOnly()]
return [permissions.AllowAny()]
def get_queryset(self):
queryset = super().get_queryset()
# 按标签筛选
tag_slug = self.request.query_params.get('tag')
if tag_slug:
queryset = queryset.filter(tags__slug=tag_slug)
return queryset
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""点赞/取消点赞"""
post = self.get_object()
user = request.user
if not user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
if user.liked_posts.filter(pk=post.pk).exists():
user.liked_posts.remove(post)
action = 'unliked'
else:
user.liked_posts.add(post)
action = 'liked'
post.refresh_from_db()
return Response({
'action': action,
'likes_count': post.likes_count
})
@action(detail=False, methods=['get'])
def popular(self, request):
"""热门文章"""
days = int(request.query_params.get('days', 30))
posts = Post.objects.popular(days=days)[:10]
serializer = PostListSerializer(posts, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def featured(self, request):
"""精选文章"""
posts = Post.objects.featured()[:10]
serializer = PostListSerializer(posts, many=True)
return Response(serializer.data)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""分类视图集(只读)"""
queryset = Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
)
serializer_class = CategorySerializer
lookup_field = 'slug'
@action(detail=True, methods=['get'])
def posts(self, request, slug=None):
"""获取分类下的文章"""
category = self.get_object()
posts = Post.objects.published().filter(category=category)
page = self.paginate_queryset(posts)
if page is not None:
serializer = PostListSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = PostListSerializer(posts, many=True)
return Response(serializer.data)
class CommentViewSet(viewsets.ModelViewSet):
"""评论视图集"""
serializer_class = CommentSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
return Comment.objects.filter(is_active=True).select_related('author')
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# api/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path, include
from . import views
router = DefaultRouter()
router.register(r'posts', views.PostViewSet, basename='post')
router.register(r'categories', views.CategoryViewSet, basename='category')
router.register(r'comments', views.CommentViewSet, basename='comment')
urlpatterns = [
path('', include(router.urls)),
path('auth/', include('rest_framework.urls')),
]
九、最佳实践与总结
9.1 项目结构最佳实践
"""
Django 项目最佳实践清单:
1. 项目结构
✓ 使用多个小应用而非单个大应用
✓ 将配置分离为 development.py, production.py, testing.py
✓ 使用环境变量管理敏感信息
✓ 遵循 "Fat Models, Thin Views" 原则
2. 性能优化
✓ 使用 select_related 和 prefetch_related 减少查询
✓ 实现缓存策略(视图缓存、模板片段缓存、底层缓存)
✓ 使用数据库索引优化查询
✓ 使用 Django Debug Toolbar 分析性能瓶颈
3. 安全性
✓ 始终使用 HTTPS(SECURE_SSL_REDIRECT = True)
✓ 启用 CSRF 保护
✓ 使用 Django 的密码哈希系统
✓ 避免在模板中使用 |safe 除非必要
✓ 定期更新 Django 和安全补丁
4. 测试
✓ 编写单元测试和集成测试
✓ 使用 pytest-django 增强测试体验
✓ 测试覆盖率至少 80%
✓ 使用工厂(factory_boy)生成测试数据
5. 部署
✓ 使用 Gunicorn/uWSGI + Nginx
✓ 使用环境变量配置(python-decouple, django-environ)
✓ 静态文件使用 CDN 或 WhiteNoise
✓ 使用 Celery 处理异步任务
✓ 配置日志和监控(Sentry, Prometheus)
6. API 设计
✓ 使用 Django REST Framework
✓ 实现版本控制(/api/v1/)
✓ 使用分页、过滤、搜索
✓ 编写 API 文档(drf-yasg, drf-spectacular)
✓ 实现限流和认证
"""
# settings 分离示例
# settings/base.py
from pathlib import Path
import os
from decouple import config
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = config('SECRET_KEY')
DEBUG = False
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
# ...
# Third party apps
'rest_framework',
'corsheaders',
'debug_toolbar',
# Local apps
'blog',
'accounts',
'api',
]
# settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
INSTALLED_APPS += ['django_extensions']
# 使用 SQLite 进行开发
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# 启用 Debug Toolbar
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']
# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
# 使用 PostgreSQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT', default='5432'),
}
}
# 安全配置
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': config('REDIS_URL'),
}
}
# 静态文件
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': BASE_DIR / 'logs/django.log',
'maxBytes': 1024 * 1024 * 10, # 10 MB
'backupCount': 5,
'formatter': 'verbose',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
}
9.2 总结
Django 核心组件回顾: - Models:ORM 数据层,定义数据结构 - Views:业务逻辑层,处理请求和响应 - Templates:展示层,渲染 HTML - Forms:表单处理和数据验证 - Admin:自动生成的管理后台 - URLs:路由配置,映射 URL 到视图
Django 的优势: - 完整的生态系统(认证、管理、ORM、表单) - 出色的文档和社区支持 - 内置安全保护(XSS、CSRF、SQL 注入) - 适合快速开发和原型验证
Django 的适用场景: - 内容管理系统(CMS) - 电子商务网站 - 社交网络平台 - 企业内部系统 - RESTful API 服务
学习路径建议: 1. 掌握 Django 基础:MTV 架构、ORM、模板 2. 深入理解:类视图、中间件、信号、自定义管理 3. 扩展学习:Django REST Framework、Celery、Channels 4. 最佳实践:测试、部署、性能优化、安全配置
记住:Django 的设计哲学是"让 Web 开发更快、更简单"。它不是银弹,但在合适的场景下,Django 能够让你用最少的代码构建出功能完善的 Web 应用。
本文由 尚先生 原创,转载请注明出处。
评论
0