Skip to content

章节4:DRF 与前后端分离


一、章节概述

本章聚焦 Django REST Framework(DRF),深入学习如何构建符合 RESTful 规范的 API 接口。内容涵盖 API 设计原则、序列化器、视图集、认证权限、限流、过滤分页以及接口文档生成等。前后端分离架构已成为现代 Web 开发的主流模式,掌握 DRF 是成为全栈工程师的关键技能。

二、学习目标

目标分类具体目标
知识目标理解 RESTful API 设计规范与成熟度模型;掌握 DRF 的核心组件及其作用
技能目标能使用 ModelSerializer 快速序列化数据;熟练使用 ModelViewSet 构建 CRUD API;能配置认证、权限、限流及接口文档
素养目标培养 API 设计的前瞻性思维;理解前后端分离架构的优势与挑战

三、核心知识点


3.1 RESTful API 设计规范

定义

REST(Representational State Transfer,表现层状态转换) 是一种软件架构风格,用于设计网络应用程序的 API。RESTful API 使用 HTTP 协议的原生语义(GET/POST/PUT/PATCH/DELETE)对资源进行操作,以 JSON/XML 格式传输数据。

设计原则

▶ 资源命名规范

# ✅ 好的设计:使用名词复数表示资源集合
GET    /api/articles/           # 文章列表
GET    /api/articles/1/         # 单篇文章
POST   /api/articles/           # 创建文章
PUT    /api/articles/1/         # 全量更新文章
PATCH  /api/articles/1/         # 部分更新文章
DELETE /api/articles/1/         # 删除文章

# 子资源
GET    /api/articles/1/comments/     # 文章的评论列表
GET    /api/articles/1/comments/5/   # 文章的某条评论

# 过滤、排序、分页
GET    /api/articles/?category=python&page=2&page_size=10
GET    /api/articles/?search=django&ordering=-pub_date

# ❌ 不好的设计:使用动词
GET    /api/getArticles
POST   /api/createArticle
POST   /api/deleteArticle

▶ HTTP 方法对应 CRUD

HTTP 方法行为请求体响应幂等
GET获取资源200 OK
POST创建资源201 Created
PUT全量更新200 OK
PATCH部分更新200 OK
DELETE删除资源可选204 No Content
OPTIONS查询可用操作200 OK

▶ 状态码规范

状态码含义示例场景
200 OK请求成功GET、PUT、PATCH 成功
201 Created资源创建成功POST 创建成功
204 No Content成功但无返回内容DELETE 成功
400 Bad Request请求参数错误表单验证失败
401 Unauthorized未认证未提供 Token
403 Forbidden无权限权限不足
404 Not Found资源不存在查询不存在的 ID
405 Method Not AllowedHTTP 方法不允许使用 GET 调用仅 POST 的接口
429 Too Many Requests请求频率超限超出限流阈值
500 Internal Server Error服务器内部错误代码异常

▶ API 版本管理

# URL 路径版本(推荐)
/api/v1/articles/
/api/v2/articles/

# 请求头版本
Accept: application/vnd.myapp.v1+json
Accept: application/vnd.myapp.v2+json

# 查询参数版本(不推荐)
/api/articles/?version=1

安装 DRF

bash
pip install djangorestframework
pip install django-filter            # 过滤支持
pip install drf-spectacular          # API 文档生成(替代 swagger)
pip install markdown                 # Markdown 渲染(浏览 API 用)
python
# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'django_filters',
    'drf_spectacular',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

3.2 DRF 序列化器

定义

序列化器(Serializer) 是 DRF 的核心组件,负责将 Python 对象(模型实例、查询集)转换为 JSON/XML 格式(序列化),以及将接收到的 JSON 数据验证后转为 Python 对象(反序列化)。ModelSerializer 是基于模型自动生成序列化字段的便捷类。

语法

▶ Serializer(基础序列化器)

python
from rest_framework import serializers

class ArticleSerializer(serializers.Serializer):
    """基础序列化器:需要手动定义字段"""
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    content = serializers.CharField()
    pub_date = serializers.DateTimeField(read_only=True)
    views = serializers.IntegerField(read_only=True)
    category_id = serializers.IntegerField()
    
    def validate_title(self, value):
        """单个字段验证"""
        if len(value) < 5:
            raise serializers.ValidationError('标题至少需要 5 个字符')
        return value
    
    def validate(self, attrs):
        """全局字段验证"""
        if 'category_id' not in attrs:
            raise serializers.ValidationError('必须指定分类')
        return attrs
    
    def create(self, validated_data):
        """自定义创建逻辑"""
        return Article.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        """自定义更新逻辑"""
        instance.title = validated_data.get('title', instance.title)
        instance.content = validated_data.get('content', instance.content)
        instance.save()
        return instance

▶ ModelSerializer(模型序列化器) ⭐ 推荐

python
from rest_framework import serializers
from blog.models import Article, Category, Tag, Comment


class CategorySerializer(serializers.ModelSerializer):
    """分类序列化器"""
    class Meta:
        model = Category
        fields = '__all__'  # 包含所有字段
        # fields = ['id', 'name', 'slug', 'description']  # 指定字段
        # exclude = ['created_at']  # 排除字段
        read_only_fields = ['slug']  # 只读字段


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


class CommentSerializer(serializers.ModelSerializer):
    """评论序列化器(包含嵌套和自定义字段)"""
    
    # 嵌套序列化器(外键关联)
    author_name = serializers.CharField(source='author.username', read_only=True)
    
    # 自定义方法字段
    created_time = serializers.SerializerMethodField()
    
    class Meta:
        model = Comment
        fields = [
            'id', 'article', 'author', 'author_name', 'content',
            'created_at', 'created_time', 'is_approved',
        ]
        read_only_fields = ['author', 'is_approved']
    
    def get_created_time(self, obj):
        """SerializerMethodField 对应的方法"""
        return obj.created_at.strftime('%Y-%m-%d %H:%M')


class ArticleListSerializer(serializers.ModelSerializer):
    """文章列表序列化器(精简字段)"""
    category_name = serializers.CharField(source='category.name', read_only=True)
    author_name = serializers.CharField(source='author.username', read_only=True)
    tags = serializers.StringRelatedField(many=True, read_only=True)
    comment_count = serializers.IntegerField(read_only=True)
    
    class Meta:
        model = Article
        fields = [
            'id', 'title', 'category_name', 'author_name',
            'tags', 'pub_date', 'views', 'comment_count',
        ]


class ArticleDetailSerializer(serializers.ModelSerializer):
    """文章详情序列化器(完整字段 + 嵌套)"""
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    author = serializers.CharField(source='author.username', read_only=True)
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta:
        model = Article
        fields = '__all__'
        read_only_fields = ['views', 'pub_date', 'update_date']

▶ 验证器与自定义验证

python
# 内置验证器
from rest_framework.validators import UniqueValidator, UniqueTogetherValidator

class UserSerializer(serializers.ModelSerializer):
    email = serializers.EmailField(
        validators=[UniqueValidator(queryset=User.objects.all())]
    )
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']
        extra_kwargs = {
            'password': {'write_only': True},  # 密码只写不读
        }
        validators = [
            UniqueTogetherValidator(
                queryset=User.objects.all(),
                fields=['username', 'email'],
                message='用户名和邮箱组合必须唯一'
            ),
        ]

# 自定义验证器
def validate_even_number(value):
    """验证数字是否为偶数"""
    if value % 2 != 0:
        raise serializers.ValidationError('必须是偶数')
    return value

# 在 serializer 中应用
class ExampleSerializer(serializers.Serializer):
    number = serializers.IntegerField(validators=[validate_even_number])

序列化器使用

python
# ---- 序列化(对象 → JSON) ----

article = Article.objects.get(id=1)
serializer = ArticleDetailSerializer(article)
data = serializer.data  # Python 字典
# data → {'id': 1, 'title': '...', 'content': '...', ...}

# 序列化查询集
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
data = serializer.data  # 有序字典列表


# ---- 反序列化(JSON → 对象) ----

# 创建
serializer = ArticleDetailSerializer(data=request.data)
if serializer.is_valid():
    serializer.save(author=request.user)  # 调用 create()
    return Response(serializer.data, status=201)
else:
    return Response(serializer.errors, status=400)

# 更新
serializer = ArticleDetailSerializer(article, data=request.data)
if serializer.is_valid():
    serializer.save()  # 调用 update()
    return Response(serializer.data)

# 部分更新
serializer = ArticleDetailSerializer(article, data=request.data, partial=True)

3.3 视图集、过滤器与分页

定义

视图集(ViewSet) 是 DRF 提供的将多个相关视图(list、create、retrieve、update、partial_update、destroy)组合到一个类中的方式。配合 Router 可以自动生成 URL 路由。过滤器 用于对 API 查询结果进行筛选,分页 控制返回数据的数量与翻页方式。

语法

▶ ModelViewSet(通用视图集)

python
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from .models import Article, Category, Comment
from .serializers import (
    ArticleListSerializer, ArticleDetailSerializer,
    CategorySerializer, CommentSerializer,
)


class CategoryViewSet(ModelViewSet):
    """分类 CRUD API"""
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    

class ArticleViewSet(ModelViewSet):
    """文章 CRUD API"""
    queryset = Article.objects.select_related('author', 'category').prefetch_related('tags')
    
    # 根据动作使用不同的序列化器
    def get_serializer_class(self):
        if self.action == 'list':
            return ArticleListSerializer
        return ArticleDetailSerializer
    
    # 自定义查询集(如过滤当前用户文章)
    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action == 'list':
            queryset = queryset.filter(is_published=True)
        return queryset
    
    # 自动设置作者
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    # 自定义动作(添加额外端点)
    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        """发布文章(自定义动作)"""
        article = self.get_object()
        article.is_published = True
        article.pub_date = timezone.now()
        article.save()
        return Response({'status': 'published'})
    
    @action(detail=True, methods=['get'])
    def comments(self, request, pk=None):
        """获取文章的评论(子资源)"""
        article = self.get_object()
        comments = article.comments.all()
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def stats(self, request):
        """统计信息"""
        data = Article.objects.aggregate(
            total=Count('id'),
            published=Count('id', filter=Q(is_published=True)),
            total_views=Sum('views'),
        )
        return Response(data)


class CommentViewSet(ModelViewSet):
    """评论 CRUD API"""
    queryset = Comment.objects.select_related('author', 'article').all()
    serializer_class = CommentSerializer
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

▶ 路由注册

python
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet, CategoryViewSet, CommentViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'comments', CommentViewSet, basename='comment')

urlpatterns = router.urls

# 自动生成的 URL:
# GET    /articles/          → list
# POST   /articles/          → create
# GET    /articles/{id}/     → retrieve
# PUT    /articles/{id}/     → update
# PATCH  /articles/{id}/     → partial_update
# DELETE /articles/{id}/     → destroy
# POST   /articles/{id}/publish/  → 自定义动作
# GET    /articles/{id}/comments/ → 自定义动作
# GET    /articles/stats/    → 自定义动作(无 pk)

▶ APIView 与通用视图(适用于更精细的控制)

python
from rest_framework.views import APIView
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from rest_framework.response import Response
from rest_framework import status

# APIView:完全自定义
class ArticleListAPIView(APIView):
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleListSerializer(articles, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = ArticleDetailSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# Generic 视图:更少的代码
class ArticleListCreateView(ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

class ArticleDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleDetailSerializer

▶ 过滤器(Filter)

python
# 方式一:django-filter 集成
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as filters

class ArticleFilter(filters.FilterSet):
    """自定义过滤器"""
    title = filters.CharFilter(lookup_expr='icontains')  # 标题模糊搜索
    content = filters.CharFilter(lookup_expr='icontains')
    category = filters.NumberFilter(field_name='category_id')
    pub_date_after = filters.DateFilter(field_name='pub_date', lookup_expr='gte')
    pub_date_before = filters.DateFilter(field_name='pub_date', lookup_expr='lte')
    min_views = filters.NumberFilter(field_name='views', lookup_expr='gte')
    max_views = filters.NumberFilter(field_name='views', lookup_expr='lte')
    author = filters.NumberFilter(field_name='author_id')
    
    # 多字段搜索
    search = filters.CharFilter(method='filter_search')
    
    def filter_search(self, queryset, name, value):
        return queryset.filter(
            Q(title__icontains=value) | Q(content__icontains=value)
        )
    
    class Meta:
        model = Article
        fields = ['title', 'category', 'author', 'is_published']


class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.select_related('author', 'category').all()
    
    # 应用过滤器
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
    filterset_class = ArticleFilter  # 自定义过滤器
    search_fields = ['title', 'content']  # 搜索字段
    ordering_fields = ['pub_date', 'views', 'title']  # 可排序字段
    ordering = ['-pub_date']  # 默认排序
python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
}

▶ 分页

python
# 方式一:全局配置(settings.py)
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    # 或使用 LimitOffsetPagination
    # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
}

# 方式二:自定义分页器(推荐)
from rest_framework.pagination import (
    PageNumberPagination,
    LimitOffsetPagination,
    CursorPagination,
)

class StandardResultsSetPagination(PageNumberPagination):
    """标准分页:页码 + 每页数量"""
    page_size = 10                        # 默认每页数量
    page_size_query_param = 'page_size'   # URL 参数名:?page_size=20
    max_page_size = 100                   # 最大每页数量
    page_query_param = 'page'            # 页码参数名:?page=2

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 50
    page_size_query_param = 'page_size'
    max_page_size = 1000

class CustomLimitOffsetPagination(LimitOffsetPagination):
    """偏移分页:?limit=10&offset=20"""
    default_limit = 10
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    max_limit = 100

class CursorPaginationWithoutOrdering(CursorPagination):
    """游标分页:适合实时数据(如无限滚动)"""
    page_size = 10
    ordering = '-pub_date'
    cursor_query_param = 'cursor'

# 在视图集中使用
class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer
    pagination_class = StandardResultsSetPagination  # 使用自定义分页

分页响应格式:

json
{
    "count": 100,
    "next": "http://api.example.com/articles/?page=3",
    "previous": "http://api.example.com/articles/?page=1",
    "results": [
        { "id": 1, "title": "文章1" },
        { "id": 2, "title": "文章2" }
    ]
}

3.4 认证、权限与限流

定义

认证(Authentication) 确认"你是谁"——验证用户身份;权限(Permission) 确认"你能做什么"——控制对资源的访问级别;限流(Throttling) 控制请求频率,防止滥用。

语法

▶ 认证方式配置

python
# settings.py —— 全局配置

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',   # Basic Auth
        'rest_framework.authentication.SessionAuthentication', # Session 认证
        'rest_framework.authentication.TokenAuthentication',   # Token 认证
        # 'rest_framework_simplejwt.authentication.JWTAuthentication',  # JWT(下章详述)
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',  # 允许任何人访问
        # 'rest_framework.permissions.IsAuthenticated',  # 仅登录用户
        # 'rest_framework.permissions.IsAdminUser',  # 仅管理员
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',      # 未认证用户每小时 100 次
        'user': '1000/hour',     # 认证用户每小时 1000 次
        'burst': '10/minute',    # 突发限流
        'sustained': '500/day',  # 持续限流
    },
}

▶ Token 认证配置

bash
pip install djangorestframework
# Token 认证是 DRF 内置的,不需要额外包
python
# settings.py
INSTALLED_APPS = [
    'rest_framework.authtoken',  # 添加 Token 应用(需 migrate)
]

# 执行迁移:python manage.py migrate

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

# 获取 Token 的 URL 配置
# urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
]
python
# views.py —— 在视图中使用 Token

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer
    
    # 视图级别认证与权限
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)


# 手动生成/管理 Token
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User

# 为用户创建 Token
user = User.objects.get(username='admin')
token, created = Token.objects.get_or_create(user=user)
print(token.key)  # 类似:9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

# 在请求中使用 Token
# Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

▶ 权限控制

python
from rest_framework.permissions import (
    BasePermission,        # 基础权限类(自定义用)
    IsAuthenticated,       # 仅认证用户
    IsAdminUser,           # 仅管理员
    IsAuthenticatedOrReadOnly,  # 未认证用户只能读
    AllowAny,              # 允许任何人
    DjangoModelPermissions,      # Django 模型权限
    DjangoObjectPermissions,     # Django 对象权限
)

# 自定义权限
class IsAuthorOrReadOnly(BasePermission):
    """仅作者可编辑,其他人只读"""
    
    def has_permission(self, request, view):
        # 基本权限检查(所有请求)
        return request.user and request.user.is_authenticated
    
    def has_object_permission(self, request, view, obj):
        # 对象级别权限检查
        if request.method in ('GET', 'HEAD', 'OPTIONS'):
            return True  # 安全方法允许所有人
        return obj.author == request.user  # 只有作者可修改


class IsAdminOrReadOnly(BasePermission):
    """管理员可编辑,其他人只读"""
    
    def has_permission(self, request, view):
        if request.method in ('GET', 'HEAD', 'OPTIONS'):
            return True
        return request.user and request.user.is_staff


# 在视图中使用
class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer
    
    permission_classes = [IsAuthenticatedOrReadOnly | IsAuthorOrReadOnly]
    # 注意:| 表示 OR 组合
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

# 不同动作使用不同权限
class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    
    def get_permissions(self):
        if self.action in ('list', 'retrieve'):
            permission_classes = [AllowAny]
        elif self.action == 'create':
            permission_classes = [IsAuthenticated]
        elif self.action in ('update', 'partial_update', 'destroy'):
            permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
        else:
            permission_classes = [IsAdminUser]
        return [p() for p in permission_classes]

▶ 限流(Throttling)

python
from rest_framework.throttling import (
    AnonRateThrottle,     # 未认证用户限流
    UserRateThrottle,     # 认证用户限流
    ScopedRateThrottle,   # 范围限流
)

# 自定义限流
class BurstRateThrottle(UserRateThrottle):
    """突发请求限流"""
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    """持续请求限流"""
    scope = 'sustained'

# 在视图中使用
class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleListSerializer
    
    throttle_classes = [AnonRateThrottle, UserRateThrottle]
    # 或自定义
    # throttle_classes = [BurstRateThrottle, SustainedRateThrottle]

# 不同视图使用不同限流
class SensitiveAPIView(APIView):
    """敏感接口(如登录)使用更严格的限流"""
    throttle_classes = [AnonRateThrottle]
    throttle_scope = 'login'
    
    def post(self, request):
        # ... 登录逻辑
        pass

# 或者使用装饰器
from rest_framework.decorators import throttle_classes

@throttle_classes([AnonRateThrottle])
def special_view(request):
    pass

▶ 完整示例:文章 API 权限与限流

python
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from .permissions import IsAuthorOrReadOnly
from .models import Article
from .serializers import ArticleListSerializer, ArticleDetailSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.select_related('author', 'category').all()
    
    def get_serializer_class(self):
        if self.action == 'list':
            return ArticleListSerializer
        return ArticleDetailSerializer
    
    def get_permissions(self):
        """不同动作不同权限"""
        if self.action in ('list', 'retrieve'):
            return [IsAuthenticatedOrReadOnly()]
        elif self.action == 'create':
            return [IsAuthenticated()]
        elif self.action in ('update', 'partial_update', 'destroy'):
            return [IsAuthenticated(), IsAuthorOrReadOnly()]
        return [IsAuthenticated()]
    
    def get_throttles(self):
        """不同动作不同限流"""
        if self.action == 'create':
            self.throttle_scope = 'create'
            return [UserRateThrottle()]
        return [AnonRateThrottle(), UserRateThrottle()]
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def like(self, request, pk=None):
        """点赞(仅认证用户)"""
        article = self.get_object()
        article.likes += 1
        article.save()
        return Response({'likes': article.likes})

3.5 接口文档与版本管理

定义

API 文档 是 API 的使用说明书,描述每个接口的 URL、方法、参数、请求体和响应格式。drf-spectacular 是目前最推荐的 DRF API 文档工具,它根据 DRF 代码自动生成符合 OpenAPI 3.0 规范的文档,并提供 Swagger UI 和 ReDoc 两种可视化界面。

语法

▶ drf-spectacular 安装与配置

bash
pip install drf-spectacular
python
# settings.py

INSTALLED_APPS = [
    # ...
    'drf_spectacular',
    'drf_spectacular_sidecar',  # 可选:本地加载 Swagger UI
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': '博客系统 API',
    'DESCRIPTION': '博客系统后端 RESTful API 接口文档',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
    
    # 认证
    'COMPONENT_SPLIT_REQUEST': True,
    'SECURITY': [
        {'Bearer': []},
    ],
    
    # Swagger UI 设置
    'SWAGGER_UI_SETTINGS': {
        'deepLinking': True,
        'persistAuthorization': True,
        'displayOperationId': True,
    },
    
    # 语言
    'CONTACT': {'email': 'admin@example.com'},
    'LICENSE': {'name': 'MIT License'},
    
    # 标签分组
    'TAGS': [
        {'name': '文章', 'description': '文章 CRUD 操作'},
        {'name': '分类', 'description': '分类管理'},
        {'name': '评论', 'description': '评论管理'},
        {'name': '用户', 'description': '用户认证与注册'},
    ],
}
python
# urls.py —— 添加文档路由

from django.urls import path, include
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularSwaggerView,
    SpectacularRedocView,
)

urlpatterns = [
    # API Schema(JSON/YAML 格式)
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    
    # Swagger UI(交互式测试界面)
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    
    # ReDoc(更整洁的文档阅读界面)
    path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
    
    # API 路由
    path('api/', include('blog.urls')),
]

▶ 在代码中添加文档描述

python
from drf_spectacular.utils import (
    extend_schema,           # 扩展整个视图的 schema
    extend_schema_view,      # 视图类装饰器
    OpenApiParameter,        # 参数描述
    OpenApiExample,          # 示例
    OpenApiResponse,         # 响应描述
    inline_serializer,       # 内联序列化器
)


# 方式一:装饰视图方法
class ArticleListCreateView(APIView):
    
    @extend_schema(
        summary='获取文章列表',
        description='返回所有已发布文章的列表,支持分页、过滤和排序',
        parameters=[
            OpenApiParameter(
                name='category',
                type=int,
                description='按分类 ID 过滤',
                required=False,
            ),
            OpenApiParameter(
                name='search',
                type=str,
                description='全文搜索(标题+内容)',
                required=False,
            ),
            OpenApiParameter(
                name='ordering',
                type=str,
                description='排序字段(pub_date/-pub_date/views/-views)',
                required=False,
            ),
            OpenApiParameter(
                name='page',
                type=int,
                description='页码(从 1 开始)',
                default=1,
            ),
        ],
        responses={
            200: ArticleListSerializer(many=True),
            401: OpenApiResponse(description='未认证'),
        },
        tags=['文章'],
    )
    def get(self, request):
        """获取文章列表"""
        # ...
        pass
    
    @extend_schema(
        summary='创建文章',
        request=ArticleDetailSerializer,
        responses={
            201: ArticleDetailSerializer,
            400: OpenApiResponse(description='请求参数错误'),
        },
    )
    def post(self, request):
        """创建文章"""
        # ...
        pass


# 方式二:使用装饰器批量修饰视图集
@extend_schema_view(
    list=extend_schema(
        summary='获取分类列表',
        description='返回所有文章分类',
        responses={200: CategorySerializer(many=True)},
    ),
    create=extend_schema(
        summary='创建分类',
        request=CategorySerializer,
        responses={201: CategorySerializer},
    ),
    retrieve=extend_schema(
        summary='获取分类详情',
        responses={200: CategorySerializer},
    ),
)
class CategoryViewSet(ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer

▶ API 版本管理

python
# 方式一:URL 路径版本(推荐)

# myproject/urls.py
from django.urls import path, include

urlpatterns = [
    path('api/v1/', include('blog.urls_v1')),
    path('api/v2/', include('blog.urls_v2')),
]

# blog/urls_v1.py(旧版 API)
from rest_framework.routers import DefaultRouter
from .views_v1 import ArticleViewSetV1

router = DefaultRouter()
router.register(r'articles', ArticleViewSetV1, basename='article')
urlpatterns = router.urls

# blog/urls_v2.py(新版 API,添加了新功能)
from rest_framework.routers import DefaultRouter
from .views_v2 import ArticleViewSetV2

router = DefaultRouter()
router.register(r'articles', ArticleViewSetV2, basename='article')
urlpatterns = router.urls


# 方式二:同一视图集内判断版本

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    
    def get_serializer_class(self):
        version = self.request.version
        
        if version == 'v1':
            return ArticleSerializerV1
        elif version == 'v2':
            return ArticleSerializerV2
        return ArticleListSerializer
    
    def list(self, request, *args, **kwargs):
        version = request.version
        
        if version == 'v2':
            # v2 版本返回更多字段
            return super().list(request, *args, **kwargs)
        else:
            # v1 版本行为
            return super().list(request, *args, **kwargs)

# settings.py
REST_FRAMEWORK = {
    'ALLOWED_VERSIONS': ['v1', 'v2'],
    'VERSION_PARAM': 'version',
    'DEFAULT_VERSION': 'v1',
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    # 其他选项:
    # 'rest_framework.versioning.QueryParameterVersioning'  # ?version=v1
    # 'rest_framework.versioning.AcceptHeaderVersioning'    # Accept: vnd.myapp.v1+json
    # 'rest_framework.versioning.NamespaceVersioning'       # URL 命名空间
}

四、小结

知识点核心要点
RESTful API 设计资源用名词复数表示;HTTP 方法对应 CRUD;正确使用状态码;推荐 URL 路径版本管理
序列化器ModelSerializer 自动生成字段;is_valid() 验证 + save() 持久化;支持嵌套序列化、自定义验证
视图集ModelViewSet 一键生成 CRUD;Router 自动注册路由;@action 添加自定义端点
过滤器与分页django-filter 集成实现过滤;SearchFilter/OrderingFilter 搜索排序;PageNumberPagination 分页
认证权限限流TokenAuthentication 令牌认证;自定义 BasePermission 控制权限;Throttling 限制请求频率
API 文档drf-spectacular 自动生成 OpenAPI 文档;Swagger UI 交互测试;ReDoc 阅读文档

五、练习

基础练习

  1. ModelSerializer 练习:为 CategoryTag 模型创建 ModelSerializer,包含所有字段和必要的验证
  2. ViewSet 实践:为 Category 模型创建 ModelViewSet,注册到 Router,测试所有 CRUD 接口

进阶练习

  1. 过滤与分页:为文章 API 添加以下功能:
    • 按分类、标题关键词、发布日期的范围过滤
    • 按阅读量、发布时间排序
    • 每页 10 条,支持 ?page=2&page_size=20 参数
  2. 权限控制:实现以下权限策略:
    • 未登录用户可以查看文章列表和详情
    • 登录用户可以创建文章
    • 只有文章作者可以更新和删除自己的文章
    • 管理员可以操作所有文章

挑战练习

  1. 构建完整博客 API
    • 设计文章、分类、标签、评论四个模型的 API
    • 使用 ModelViewSet 实现完整 CRUD
    • 配置 Token 认证 + 角色权限
    • 添加限流策略(创建文章每分钟 5 次)
    • 集成 drf-spectacular 生成文档
    • 实现 API 版本 v1 和 v2

参考代码(练习 5 路由配置)

python
router = DefaultRouter()
router.register(r'articles', ArticleViewSet)
router.register(r'categories', CategoryViewSet)
router.register(r'tags', TagViewSet)
router.register(r'comments', CommentViewSet)

urlpatterns = [
    path('api/v1/', include(router.urls)),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema')),
]

Python 学习资料