Skip to content

第14章:API开发

14.2 RESTful API设计

API版本控制

实现API版本控制以确保向后兼容性:

python
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

# 版本1的路由
v1_router = DefaultRouter()
v1_router.register(r'articles', views.v1.ArticleViewSet, basename='v1-article')
v1_router.register(r'categories', views.v1.CategoryViewSet, basename='v1-category')

# 版本2的路由
v2_router = DefaultRouter()
v2_router.register(r'articles', views.v2.ArticleViewSet, basename='v2-article')
v2_router.register(r'categories', views.v2.CategoryViewSet, basename='v2-category')
v2_router.register(r'tags', views.v2.TagViewSet, basename='v2-tag')

urlpatterns = [
    # 版本1 API
    path('api/v1/', include(v1_router.urls)),
    path('api/v1/', include('myapp.api.v1.urls')),
    
    # 版本2 API
    path('api/v2/', include(v2_router.urls)),
    path('api/v2/', include('myapp.api.v2.urls')),
    
    # 默认版本(指向最新版本)
    path('api/', include(v2_router.urls)),
]

# 版本控制通过Accept头实现
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
    'DEFAULT_VERSION': 'v2',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
}

# 或通过URL路径实现版本控制
# urls.py
from rest_framework.versioning import URLPathVersioning

urlpatterns = [
    path('api/<str:version>/', include(router.urls)),
]

# 在视图中处理版本
class ArticleViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        if self.request.version == 'v1':
            return ArticleSerializerV1
        return ArticleSerializerV2
    
    def get_queryset(self):
        queryset = Article.objects.all()
        if self.request.version == 'v1':
            # v1版本的特殊处理
            queryset = queryset.filter(status='published')
        return queryset

认证和权限

实现多种认证和权限控制机制:

python
# authentication.py
from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from rest_framework.permissions import BasePermission, IsAuthenticated, IsAdminUser
from rest_framework.authtoken.models import Token

class CustomTokenAuthentication(TokenAuthentication):
    """自定义Token认证"""
    def authenticate_credentials(self, key):
        try:
            token = Token.objects.select_related('user').get(key=key)
        except Token.DoesNotExist:
            raise exceptions.AuthenticationFailed('无效的Token')
        
        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('用户账户已被禁用')
        
        # 检查Token是否过期
        from django.utils import timezone
        from datetime import timedelta
        if token.created < timezone.now() - timedelta(days=30):
            raise exceptions.AuthenticationFailed('Token已过期')
        
        return (token.user, token)

# permissions.py
from rest_framework.permissions import BasePermission

class IsOwnerOrReadOnly(BasePermission):
    """只允许所有者编辑对象"""
    
    def has_object_permission(self, request, view, obj):
        # 读取权限允许任何请求
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # 写入权限只允许对象的所有者
        return obj.author == request.user

class IsAuthorOrAdmin(BasePermission):
    """只允许作者或管理员"""
    
    def has_permission(self, request, view):
        # 匿名用户只能查看
        if request.method in permissions.SAFE_METHODS:
            return True
        # 认证用户可以创建
        return request.user and request.user.is_authenticated
    
    def has_object_permission(self, request, view, obj):
        # 读取权限允许任何请求
        if request.method in permissions.SAFE_METHODS:
            return True
        # 修改权限只允许作者或管理员
        return obj.author == request.user or request.user.is_staff

class IsVerifiedUser(BasePermission):
    """只允许已验证的用户"""
    
    def has_permission(self, request, view):
        return request.user and request.user.is_authenticated and request.user.profile.is_verified

# views.py
from rest_framework.decorators import action
from rest_framework.response import Response

class ArticleViewSet(viewsets.ModelViewSet):
    authentication_classes = [CustomTokenAuthentication, SessionAuthentication]
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
    
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def like(self, request, pk=None):
        """点赞文章"""
        article = self.get_object()
        # 实现点赞逻辑
        article.likes.add(request.user)
        return Response({'status': 'liked'})
    
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def unlike(self, request, pk=None):
        """取消点赞"""
        article = self.get_object()
        article.likes.remove(request.user)
        return Response({'status': 'unliked'})
    
    @action(detail=False, methods=['get'], permission_classes=[IsAdminUser])
    def draft_articles(self, request):
        """获取草稿文章(仅管理员)"""
        drafts = Article.objects.filter(status='draft')
        serializer = self.get_serializer(drafts, many=True)
        return Response(serializer.data)

分页和过滤

实现分页和过滤功能:

python
# pagination.py
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
from rest_framework.response import Response

class CustomPageNumberPagination(PageNumberPagination):
    """自定义页码分页"""
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100
    
    def get_paginated_response(self, data):
        return Response({
            'links': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link()
            },
            'count': self.page.paginator.count,
            'total_pages': self.page.paginator.num_pages,
            'current_page': self.page.number,
            'results': data
        })

class CursorPagination(CursorPagination):
    """游标分页"""
    page_size = 20
    ordering = '-created_at'

# filters.py
from django_filters import rest_framework as filters
from .models import Article, Category

class ArticleFilter(filters.FilterSet):
    """文章过滤器"""
    title = filters.CharFilter(lookup_expr='icontains')
    category = filters.ModelChoiceFilter(queryset=Category.objects.all())
    tags = filters.ModelMultipleChoiceFilter(
        queryset=Tag.objects.all(),
        field_name='tags',
        lookup_expr='in'
    )
    author = filters.CharFilter(field_name='author__username', lookup_expr='icontains')
    created_after = filters.DateFilter(field_name='created_at', lookup_expr='gte')
    created_before = filters.DateFilter(field_name='created_at', lookup_expr='lte')
    is_published = filters.BooleanFilter(field_name='status', method='filter_is_published')
    
    class Meta:
        model = Article
        fields = {
            'title': ['icontains'],
            'category': ['exact'],
            'author': ['exact'],
            'created_at': ['gte', 'lte'],
        }
    
    def filter_is_published(self, queryset, name, value):
        status = 'published' if value else 'draft'
        return queryset.filter(status=status)

# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter

class ArticleViewSet(viewsets.ModelViewSet):
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_class = ArticleFilter
    search_fields = ['title', 'content', 'excerpt', 'author__username']
    ordering_fields = ['created_at', 'updated_at', 'view_count', 'published_at']
    ordering = ['-created_at']
    pagination_class = CustomPageNumberPagination
    
    @action(detail=False, methods=['get'])
    def popular(self, request):
        """获取热门文章"""
        queryset = self.filter_queryset(
            self.get_queryset().filter(view_count__gt=1000)
        )
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

API文档生成

生成API文档:

python
# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'rest_framework.authtoken',
    'drf_yasg',  # Swagger/OpenAPI文档
]

# urls.py
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi

schema_view = get_schema_view(
   openapi.Info(
      title="博客API",
      default_version='v2',
      description="博客系统的RESTful API文档",
      terms_of_service="https://www.google.com/policies/terms/",
      contact=openapi.Contact(email="contact@blog.local"),
      license=openapi.License(name="BSD License"),
   ),
   public=True,
   permission_classes=[permissions.AllowAny],
)

urlpatterns = [
    # API路由
    path('api/v2/', include(router.urls)),
    
    # API文档
    path('swagger/', schema_view.with_ui(
        'swagger', 
        cache_timeout=0
    ), name='schema-swagger-ui'),
    
    path('redoc/', schema_view.with_ui(
        'redoc', 
        cache_timeout=0
    ), name='schema-redoc'),
    
    path('swagger.json', schema_view.without_ui(
        cache_timeout=0
    ), name='schema-json'),
]

# serializers.py - 添加文档注释
class ArticleSerializer(serializers.ModelSerializer):
    """
    文章序列化器
    
    用于序列化和反序列化Article模型实例
    """
    
    class Meta:
        model = Article
        fields = '__all__'
        read_only_fields = ['author', 'view_count', 'created_at', 'updated_at']
    
    def create(self, validated_data):
        """
        创建文章
        
        Args:
            validated_data (dict): 验证后的数据
            
        Returns:
            Article: 创建的文章实例
        """
        return Article.objects.create(**validated_data)

# views.py - 添加详细文档
class ArticleViewSet(viewsets.ModelViewSet):
    """
    文章视图集
    
    提供文章的完整CRUD操作
    """
    
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    
    @action(detail=True, methods=['post'])
    def like(self, request, pk=None):
        """
        点赞文章
        
        为指定的文章添加点赞
        
        Args:
            request: HTTP请求对象
            pk (int): 文章ID
            
        Returns:
            Response: 点赞结果
        """
        article = self.get_object()
        article.likes.add(request.user)
        return Response({'status': 'liked'})

通过这些RESTful API设计实践,你可以创建出功能完善、易于使用和维护的API接口。

小结

RESTful API设计的核心要点:

  1. ✅ 实施API版本控制确保向后兼容性
  2. ✅ 设计灵活的认证和权限控制机制
  3. ✅ 实现高效的分页和过滤功能
  4. ✅ 生成完整的API文档便于开发者使用
  5. ✅ 遵循REST架构约束和最佳实践

良好的API设计能够提升开发效率和用户体验。

下一篇

我们将学习Django测试系统。

15.1 单元测试 →

目录

返回课程目录

Released under the Apache 2.0 License.