企业级实现 DRF 后端 API 端点 `/knowledge/files/batch/`
# 企业级实现 DRF 后端 API 端点 `/knowledge/files/batch/`
基于您提供的代码,我将实现一个企业级的批量文件上传API端点。这个实现将包含完整的序列化器、视图和路由配置。
## 1. 创建批量上传序列化器
首先,创建一个专门用于批量上传的序列化器:
```python
# serializers.py
from rest_framework import serializers
from django.utils import timezone
import os
from .models import UploadedFile
class BatchUploadSerializer(serializers.Serializer):
    """
    批量文件上传序列化器
    """
    files = serializers.ListField(
        child=serializers.FileField(max_length=100000, allow_empty_file=False),
        write_only=True
    )
    categories = serializers.ListField(
        child=serializers.CharField(max_length=100),
        write_only=True,
        required=False
    )
    display_names = serializers.ListField(
        child=serializers.CharField(max_length=255),
        write_only=True,
        required=False
    )
    category = serializers.CharField(max_length=50, default='general', write_only=True)
    
    # 响应字段
    success_count = serializers.IntegerField(read_only=True)
    failed_count = serializers.IntegerField(read_only=True)
    results = serializers.ListField(child=serializers.DictField(), read_only=True)
    def validate(self, attrs):
        """
        验证批量上传数据
        """
        files = attrs.get('files', [])
        categories = attrs.get('categories', [])
        display_names = attrs.get('display_names', [])
        
        # 确保文件数量与分类/显示名称数量匹配(如果提供了这些列表)
        if categories and len(categories) != len(files):
            raise serializers.ValidationError(
                "categories列表长度必须与files列表长度相同"
            )
            
        if display_names and len(display_names) != len(files):
            raise serializers.ValidationError(
                "display_names列表长度必须与files列表长度相同"
            )
            
        # 验证每个文件
        for i, file in enumerate(files):
            # 使用现有的文件验证逻辑
            file_serializer = UploadedFileSerializer(data={'file': file})
            try:
                file_serializer.validate_file(file)
            except serializers.ValidationError as e:
                raise serializers.ValidationError({
                    f"file_{i}": e.detail
                })
                
        return attrs
    def create(self, validated_data):
        """
        批量创建文件记录
        """
        request = self.context.get('request')
        files = validated_data.get('files', [])
        categories = validated_data.get('categories', [])
        display_names = validated_data.get('display_names', [])
        default_category = validated_data.get('category', 'general')
        
        results = []
        success_count = 0
        failed_count = 0
        
        for i, file in enumerate(files):
            try:
                # 确定当前文件的分类和显示名称
                file_category = categories[i] if categories and i < len(categories) else default_category
                file_display_name = (
                    display_names[i] 
                    if display_names and i < len(display_names) 
                    else os.path.splitext(file.name)[0]
                )
                
                # 准备文件数据
                file_data = {
                    'file': file,
                    'category': file_category,
                    'display_name': file_display_name
                }
                
                # 使用现有的UploadedFileSerializer创建文件
                file_serializer = UploadedFileSerializer(
                    data=file_data,
                    context={'request': request}
                )
                
                if file_serializer.is_valid(raise_exception=True):
                    instance = file_serializer.save()
                    results.append({
                        'success': True,
                        'file_id': instance.id,
                        'file_name': file.name,
                        'message': '上传成功'
                    })
                    success_count += 1
                else:
                    raise serializers.ValidationError(file_serializer.errors)
                    
            except Exception as e:
                # 记录错误但继续处理其他文件
                error_msg = str(e)
                if hasattr(e, 'detail'):
                    error_msg = e.detail
                    
                results.append({
                    'success': False,
                    'file_name': file.name,
                    'error': error_msg
                })
                failed_count += 1
        
        # 返回批量操作结果
        return {
            'success_count': success_count,
            'failed_count': failed_count,
            'results': results
        }
```
## 2. 在视图集中添加批量上传端点
在现有的 `UploadedFileViewSet` 中添加批量上传端点:
```python
# views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from .serializers import BatchUploadSerializer
class UploadedFileViewSet(viewsets.ModelViewSet):
    # ... 现有代码保持不变 ...
    
    @action(detail=False, methods=['post'], url_path='batch')
    def batch_upload(self, request):
        """
        批量上传文件端点 - POST /api/knowledge/files/batch/
        """
        serializer = BatchUploadSerializer(
            data=request.data,
            context={'request': request}
        )
        
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )
        
        try:
            # 使用事务确保数据一致性
            with transaction.atomic():
                result = serializer.save()
            
            return Response(result, status=status.HTTP_200_OK)
            
        except Exception as e:
            # 记录错误日志
            import logging
            logger = logging.getLogger(__name__)
            logger.error(f"批量文件上传失败: {str(e)}")
            
            return Response(
                {'error': '批量上传过程中发生错误', 'details': str(e)},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )
```
## 3. 配置URL路由
在项目的URL配置中添加批量上传端点:
```python
# urls.py (项目主urls.py)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UploadedFileViewSet
router = DefaultRouter()
router.register(r'knowledge/files', UploadedFileViewSet, basename='uploadedfile')
urlpatterns = [
    path('api/', include(router.urls)),
    # 其他URL配置...
]
```
## 4. 添加API文档说明
为批量上传端点添加详细的API文档:
```python
# 在视图方法中添加文档字符串
@action(detail=False, methods=['post'], url_path='batch')
def batch_upload(self, request):
    """
    批量上传文件端点
    
    支持一次上传多个文件,最大支持20个文件同时上传,每个文件最大20MB。
    
    请求格式:
    - files: 文件列表 (必需)
    - category: 默认分类 (可选,默认为'general')
    - categories: 分类列表,与文件一一对应 (可选)
    - display_names: 显示名称列表,与文件一一对应 (可选)
    
    响应格式:
    {
        "success_count": 成功数量,
        "failed_count": 失败数量,
        "results": [
            {
                "success": true/false,
                "file_id": "文件ID(成功时)",
                "file_name": "文件名",
                "message": "成功消息/错误消息"
            },
            ...
        ]
    }
    
    状态码:
    - 200: 批量操作完成(可能有部分失败)
    - 400: 请求数据无效
    - 401: 未认证
    - 403: 权限不足
    - 500: 服务器内部错误
    """
    # ... 方法实现 ...
```
## 5. 完整的批量上传视图集
这是整合后的完整视图集:
```python
# views.py
import os
import logging
from django.db import transaction, models
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status, viewsets, filters, serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from .filters import UploadedFileFilter
from .models import UploadedFile
from .permissions import IsAdministrator
from .serializers import UploadedFileSerializer, UploadedFileListSerializer, BatchUploadSerializer
from ..users.auth.authentication import SentinelAuthentication
logger = logging.getLogger(__name__)
class UploadedFileViewSet(viewsets.ModelViewSet):
    """
    统一文件上传API端点,支持所有类型的文件上传
    """
    authentication_classes = [SentinelAuthentication]  # 企业级安全认证
    permission_classes = [IsAdministrator]  # 认证用户、管理员
    queryset = UploadedFile.objects.filter(is_deleted=False)
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_class = UploadedFileFilter
    search_fields = ['original_name', 'display_name', 'description']
    ordering_fields = ['uploaded_at', 'size', 'original_name']
    ordering = ['-uploaded_at']
    def get_serializer_class(self):
        if self.action == 'list':
            return UploadedFileListSerializer
        return UploadedFileSerializer
    def get_queryset(self):
        """根据用户权限过滤查询集"""
        queryset = super().get_queryset()
        # 非管理员用户只能看到自己的文件或公开文件
        if not self.request.user.is_staff:
            queryset = queryset.filter(
                models.Q(uploaded_by=self.request.user) |
                models.Q(is_public=True)
            )
        # 根据查询参数进一步过滤
        file_type = self.request.query_params.get('file_type')
        if file_type:
            queryset = queryset.filter(file_type=file_type)
        category = self.request.query_params.get('category')
        if category:
            queryset = queryset.filter(category=category)
return queryset
    def create(self, request, *args, **kwargs):
        """创建文件上传记录"""
        serializer = self.get_serializer(data=request.data)
        try:
            serializer.is_valid(raise_exception=True)
            with transaction.atomic():
                instance = serializer.save()
            headers = self.get_success_headers(serializer.data)
            return Response(
                data=serializer.data,
                status=status.HTTP_201_CREATED,
                headers=headers
            )
        except serializers.ValidationError as e:
            return Response(
                {'error': '文件验证失败', 'details': e.detail},
                status=status.HTTP_400_BAD_REQUEST,
            )
        except Exception as e:
            logger.error(f"文件上传失败: {str(e)}")
            return Response(
                {'error': '上传过程中发生错误', 'details': str(e)},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )
    @action(detail=False, methods=['post'], url_path='batch')
    def batch_upload(self, request):
        """
        批量上传文件端点
        
        支持一次上传多个文件,最大支持20个文件同时上传,每个文件最大20MB。
        """
        # 检查文件数量限制
        files = request.FILES.getlist('files')
        if len(files) > 20:
            return Response(
                {'error': '一次最多只能上传20个文件'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        serializer = BatchUploadSerializer(
            data=request.data,
            context={'request': request}
        )
        
        if not serializer.is_valid():
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )
        
        try:
            # 使用事务确保数据一致性
            with transaction.atomic():
                result = serializer.save()
            
            return Response(result, status=status.HTTP_200_OK)
            
        except Exception as e:
            logger.error(f"批量文件上传失败: {str(e)}")
            return Response(
                {'error': '批量上传过程中发生错误', 'details': str(e)},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )
    # 其他现有方法保持不变...
    @action(detail=True, methods=['post'])
    def toggle_public(self, request, pk=None):
        """切换文件的公开状态"""
        file_instance = self.get_object()
        file_instance.is_public = not file_instance.is_public
        file_instance.save()
        return Response({
            'status': 'success',
            'is_public': file_instance.is_public,
            'message': f'文件已{"公开" if file_instance.is_public else "私有"}'
        })
    @action(detail=True, methods=['post'])
    def soft_delete(self, request, pk=None):
        """软删除文件"""
        file_instance = self.get_object()
        file_instance.soft_delete()
        return Response({
            'status': 'success',
            'message': '文件已移至回收站'
        })
    @action(detail=False, methods=['get'])
    def stats(self, request):
        """获取文件统计信息"""
        total_count = self.get_queryset().count()
        # 按文件类型统计
        by_type = dict(self.get_queryset().values_list('file_type').annotate(
            count=models.Count('id')
        ))
        # 按分类统计
        by_category = dict(self.get_queryset().values_list('category').annotate(
            count=models.Count('id')
        ))
        # 计算总文件大小
        total_size = self.get_queryset().aggregate(total_size=models.Sum('size'))['total_size'] or 0
        return Response({
            'total_count': total_count,
            'total_size': total_size,
            'by_type': by_type,
            'by_category': by_category
        })
    @action(detail=False, methods=['get'])
    def my_files(self, request):
        """获取当前用户的文件"""
        if not request.user.is_authenticated:
            return Response(
                {'error': '需要登录'},
                status=status.HTTP_401_UNAUTHORIZED
            )
        user_files = self.get_queryset().filter(uploaded_by=request.user)
        page = self.paginate_queryset(user_files)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        serializer = self.get_serializer(user_files, many=True)
        return Response(serializer.data)
```
## 6. 使用说明
### 请求示例
```bash
curl -X POST \
  http://localhost:8000/api/knowledge/files/batch/ \
  -H 'Authorization: Bearer <your_token>' \
  -F 'files=@file1.jpg' \
  -F 'files=@file2.pdf' \
  -F 'category=documents' \
  -F 'categories=["documents", "images"]' \
  -F 'display_names=["My Document", "My Image"]'
```
### 响应示例
```json
{
    "success_count": 2,
    "failed_count": 0,
    "results": [
        {
            "success": true,
            "file_id": "550e8400-e29b-41d4-a716-446655440000",
            "file_name": "file1.jpg",
            "message": "上传成功"
        },
        {
            "success": true,
            "file_id": "550e8400-e29b-41d4-a716-446655440001",
            "file_name": "file2.pdf",
            "message": "上传成功"
        }
    ]
}
```
## 7. 企业级特性
这个实现具有以下企业级特性:
1. **安全性**:使用企业级认证和权限控制
2. **数据一致性**:使用数据库事务确保操作原子性
3. **错误处理**:详细的错误日志和用户友好的错误消息
4. **性能优化**:限制一次上传的文件数量,防止DoS攻击
5. **可扩展性**:模块化设计,易于扩展和维护
6. **文档完整**:详细的API文档和示例
7. **验证全面**:复用现有的文件验证逻辑,确保一致性
这个实现提供了一个完整的企业级批量文件上传API端点,符合RESTful设计原则,并且与您现有的代码架构完美集成。
 
                    
                 
                
            
         
 浙公网安备 33010602011771号
浙公网安备 33010602011771号