eagleye

企业级实现 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设计原则,并且与您现有的代码架构完美集成。

posted on 2025-08-26 11:22  GoGrid  阅读(13)  评论(0)    收藏  举报

导航