DRF统一数据模型的视图集实现(Quasar前端上传图片文件、PDF文件、Office文档的DRF后端实现)
# 统一数据模型的 DRF 视图集实现
下面是将 DRF 视图集修改为使用统一的 `UploadedFile` 数据模型和 `UploadedFileSerializer` 序列化器的完整实现。
## 统一数据模型 (models.py)
```python
# models.py
from django.db import models
from django.utils import timezone
import os
import hashlib
from typing import Optional
class UploadedFile(models.Model):
# 文件分类选项
CATEGORY_CHOICES = [
('general', '通用文件'),
('documents', '文档资料'),
('presentations', '演示文稿'),
('spreadsheets', '数据表格'),
('images', '图片文件'),
('pdfs', 'PDF文档'),
('reports', '报告材料'),
('contracts', '合同协议'),
]
# 文件类型选项
FILE_TYPE_CHOICES = [
('image/jpeg', 'JPEG图像'),
('image/png', 'PNG图像'),
('image/gif', 'GIF图像'),
('image/webp', 'WebP图像'),
('application/pdf', 'PDF文档'),
('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'Word文档'),
('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'PowerPoint演示文稿'),
('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Excel电子表格'),
('application/octet-stream', '其他文件'),
]
# 核心字段
file = models.FileField(upload_to='uploads/%Y/%m/%d/')
original_name = models.CharField(max_length=255, verbose_name="原始文件名")
display_name = models.CharField(max_length=255, blank=True, verbose_name="显示名称")
size = models.BigIntegerField(verbose_name="文件大小")
file_type = models.CharField(max_length=100, choices=FILE_TYPE_CHOICES, verbose_name="文件类型")
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='general', verbose_name="文件分类")
# 元数据字段
checksum = models.CharField(max_length=64, blank=True, verbose_name="文件校验和")
uploaded_by = models.ForeignKey(
'auth.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="上传者"
)
uploaded_at = models.DateTimeField(default=timezone.now, verbose_name="上传时间")
description = models.TextField(blank=True, verbose_name="文件描述")
# 状态字段
is_public = models.BooleanField(default=False, verbose_name="是否公开")
is_deleted = models.BooleanField(default=False, verbose_name="是否已删除")
class Meta:
ordering = ['-uploaded_at']
verbose_name = '上传文件'
verbose_name_plural = '上传文件'
indexes = [
models.Index(fields=['file_type']),
models.Index(fields=['category']),
models.Index(fields=['uploaded_at']),
models.Index(fields=['is_public']),
]
def __str__(self):
return self.display_name or self.original_name
def save(self, *args, **kwargs):
# 自动填充文件信息
if self.file:
# 设置原始文件名
if not self.original_name:
self.original_name = self.file.name
# 设置显示名称(如果没有提供)
if not self.display_name:
self.display_name = os.path.splitext(self.original_name)[0]
# 设置文件大小
self.size = self.file.size
# 设置文件类型
if not self.file_type:
self.file_type = self._get_file_content_type()
# 计算校验和
self.checksum = self._calculate_checksum()
super().save(*args, **kwargs)
def _get_file_content_type(self) -> str:
"""获取文件的MIME类型"""
# 通过文件扩展名判断
ext = os.path.splitext(self.original_name)[1].lower()
mime_types = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
return mime_types.get(ext, 'application/octet-stream')
def _calculate_checksum(self) -> str:
"""计算文件校验和用于去重"""
if not hasattr(self.file, 'file'):
return ''
try:
hash_sha256 = hashlib.sha256()
# 保存当前位置
current_position = self.file.tell()
# 回到文件开头
self.file.seek(0)
for chunk in self.file.chunks(chunk_size=4096):
hash_sha256.update(chunk)
# 恢复文件指针位置
self.file.seek(current_position)
return hash_sha256.hexdigest()
except (IOError, AttributeError):
return ''
def get_absolute_url(self):
"""获取文件的绝对URL"""
from django.urls import reverse
return reverse('uploadedfile-detail', kwargs={'pk': self.pk})
def soft_delete(self):
"""软删除文件"""
self.is_deleted = True
self.save()
def restore(self):
"""恢复已删除的文件"""
self.is_deleted = False
self.save()
@property
def extension(self):
"""获取文件扩展名"""
return os.path.splitext(self.original_name)[1].lower()
@property
def is_image(self):
"""检查是否为图片文件"""
return self.file_type.startswith('image/')
@property
def is_pdf(self):
"""检查是否为PDF文件"""
return self.file_type == 'application/pdf'
@property
def is_office_document(self):
"""检查是否为Office文档"""
office_types = [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
]
return self.file_type in office_types
```
## 统一序列化器 (serializers.py)
```python
# serializers.py
from rest_framework import serializers
from .models import UploadedFile
import magic
from django.contrib.auth import get_user_model
User = get_user_model()
class UploadedFileSerializer(serializers.ModelSerializer):
# 只读字段
uploaded_by_username = serializers.CharField(source='uploaded_by.username', read_only=True)
uploaded_by_email = serializers.CharField(source='uploaded_by.email', read_only=True)
file_url = serializers.SerializerMethodField()
file_extension = serializers.CharField(source='extension', read_only=True)
is_image = serializers.BooleanField(read_only=True)
is_pdf = serializers.BooleanField(read_only=True)
is_office_document = serializers.BooleanField(read_only=True)
class Meta:
model = UploadedFile
fields = [
'id', 'file', 'original_name', 'display_name', 'size', 'file_type',
'category', 'checksum', 'uploaded_by', 'uploaded_by_username',
'uploaded_by_email', 'uploaded_at', 'description', 'is_public',
'is_deleted', 'file_url', 'file_extension', 'is_image', 'is_pdf',
'is_office_document'
]
read_only_fields = [
'id', 'original_name', 'size', 'file_type', 'checksum',
'uploaded_by', 'uploaded_at', 'file_url', 'file_extension',
'is_image', 'is_pdf', 'is_office_document'
]
def get_file_url(self, obj):
"""获取文件的URL"""
request = self.context.get('request')
if request and obj.file:
return request.build_absolute_uri(obj.file.url)
return None
def validate_file(self, value):
"""验证上传的文件"""
# 验证文件大小 (20MB限制)
max_size = 20 * 1024 * 1024 # 20MB
if value.size > max_size:
raise serializers.ValidationError(f"文件大小不能超过 {max_size//1024//1024}MB")
# 验证文件类型
valid_types = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
]
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.docx', '.pptx', '.xlsx']
# 使用python-magic库进行文件类型检测
try:
file_type = magic.from_buffer(value.read(2048), mime=True)
value.seek(0) # 重置文件指针
# 验证MIME类型
if file_type not in valid_types:
raise serializers.ValidationError("不支持的文件类型")
except ImportError:
# 如果没有安装python-magic,使用文件名扩展判断
name = value.name.lower()
if not any(name.endswith(ext) for ext in valid_extensions):
raise serializers.ValidationError("不支持的文件类型")
return value
def validate(self, data):
"""全局验证"""
# 检查是否已存在相同文件 (基于校验和)
if 'file' in data and data['file']:
# 创建临时实例来计算校验和
temp_instance = UploadedFile(file=data['file'])
temp_instance.original_name = data['file'].name
checksum = temp_instance._calculate_checksum()
# 检查是否已存在相同文件
if UploadedFile.objects.filter(checksum=checksum, is_deleted=False).exists():
raise serializers.ValidationError("该文件已存在")
return data
def create(self, validated_data):
"""创建文件记录"""
# 设置上传用户
request = self.context.get('request')
if request and request.user.is_authenticated:
validated_data['uploaded_by'] = request.user
return super().create(validated_data)
class UploadedFileListSerializer(serializers.ModelSerializer):
"""用于列表视图的简化序列化器"""
file_url = serializers.SerializerMethodField()
uploaded_by_username = serializers.CharField(source='uploaded_by.username', read_only=True)
class Meta:
model = UploadedFile
fields = [
'id', 'display_name', 'original_name', 'size', 'file_type',
'category', 'uploaded_at', 'file_url', 'uploaded_by_username',
'is_public'
]
def get_file_url(self, obj):
request = self.context.get('request')
if request and obj.file:
return request.build_absolute_uri(obj.file.url)
return None
```
## 统一视图集 (views.py)
```python
# views.py
from rest_framework import status, viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend
from .models import UploadedFile
from .serializers import UploadedFileSerializer, UploadedFileListSerializer
from .filters import UploadedFileFilter
import magic
class UploadedFileViewSet(viewsets.ModelViewSet):
"""
统一文件上传API端点,支持所有类型的文件上传
"""
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 perform_create(self, serializer):
"""创建文件时自动设置上传者"""
if self.request.user.is_authenticated:
serializer.save(uploaded_by=self.request.user)
else:
serializer.save()
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(
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:
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=['post'])
def bulk_upload(self, request):
"""批量上传文件"""
files = request.FILES.getlist('files')
category = request.data.get('category', 'general')
results = []
for file in files:
file_data = {
'file': file,
'category': category,
'display_name': request.data.get('display_name') or os.path.splitext(file.name)[0]
}
serializer = self.get_serializer(data=file_data)
try:
serializer.is_valid(raise_exception=True)
instance = serializer.save()
results.append({
'success': True,
'file': UploadedFileListSerializer(instance, context={'request': request}).data,
'message': '上传成功'
})
except Exception as e:
results.append({
'success': False,
'name': file.name,
'message': str(e)
})
return Response({'results': results}, status=status.HTTP_207_MULTI_STATUS)
@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)
```
## 过滤器 (filters.py)
```python
# filters.py
import django_filters
from .models import UploadedFile
class UploadedFileFilter(django_filters.FilterSet):
"""文件过滤器"""
min_size = django_filters.NumberFilter(field_name="size", lookup_expr='gte')
max_size = django_filters.NumberFilter(field_name="size", lookup_expr='lte')
uploaded_after = django_filters.DateTimeFilter(field_name="uploaded_at", lookup_expr='gte')
uploaded_before = django_filters.DateTimeFilter(field_name="uploaded_at", lookup_expr='lte')
class Meta:
model = UploadedFile
fields = {
'file_type': ['exact'],
'category': ['exact'],
'is_public': ['exact'],
'uploaded_by': ['exact'],
}
```
## URL配置 (urls.py)
```python
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UploadedFileViewSet
router = DefaultRouter()
router.register(r'files', UploadedFileViewSet, basename='uploadedfile')
urlpatterns = [
path('api/', include(router.urls)),
]
```
## 管理员配置 (admin.py)
```python
# admin.py
from django.contrib import admin
from .models import UploadedFile
@admin.register(UploadedFile)
class UploadedFileAdmin(admin.ModelAdmin):
list_display = ['display_name', 'file_type', 'category', 'size', 'uploaded_by', 'uploaded_at', 'is_public']
list_filter = ['file_type', 'category', 'is_public', 'is_deleted', 'uploaded_at']
search_fields = ['original_name', 'display_name', 'description']
readonly_fields = ['size', 'file_type', 'checksum', 'uploaded_by', 'uploaded_at']
fieldsets = (
('文件信息', {
'fields': ('file', 'original_name', 'display_name', 'size', 'file_type', 'checksum')
}),
('分类信息', {
'fields': ('category', 'description')
}),
('权限设置', {
'fields': ('is_public', 'is_deleted')
}),
('元数据', {
'fields': ('uploaded_by', 'uploaded_at'),
'classes': ('collapse',)
}),
)
def get_queryset(self, request):
# 显示所有文件(包括已删除的)
return super().get_queryset(request)
def save_model(self, request, obj, form, change):
if not change: # 新建对象时设置上传者
obj.uploaded_by = request.user
super().save_model(request, obj, form, change)
```
## 安装依赖
```bash
pip install django-filter python-magic
# Windows 用户还需要安装
pip install python-magic-bin
```
## 配置设置 (settings.py)
```python
# settings.py 相关配置
INSTALLED_APPS = [
# ...
'django_filters',
'rest_framework',
# ...
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# 文件上传设置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# 增加最大文件上传大小
DATA_UPLOAD_MAX_MEMORY_SIZE = 20 * 1024 * 1024 # 20MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 20 * 1024 * 1024 # 20MB
```
## 使用示例
### 前端组件使用
前端组件可以继续使用之前提供的 `UniversalFileUploader.vue`,只需将上传端点修改为统一的API端点:
```javascript
// 上传单个文件
const uploadSingleFile = async (fileItem: FileUploadItem): Promise<void> => {
const formData = new FormData();
formData.append('file', fileItem.nativeFile);
formData.append('category', fileItem.category);
formData.append('display_name', fileItem.name); // 使用统一字段
try {
await axios.post('/api/files/', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
// ...其他配置
});
// ...处理响应
} catch (error) {
// ...错误处理
}
};
```
### API端点示例
1. **获取所有文件**:
```
GET /api/files/
```
2. **按类型过滤文件**:
```
GET /api/files/?file_type=application/pdf
```
3. **按分类过滤文件**:
```
GET /api/files/?category=documents
```
4. **搜索文件**:
```
GET /api/files/?search=report
```
5. **获取统计信息**:
```
GET /api/files/stats/
```
6. **获取当前用户的文件**:
```
GET /api/files/my_files/
```
7. **批量上传**:
```
POST /api/files/bulk_upload/
```
## 功能特点
### 1. 统一数据模型
- 使用单个 `UploadedFile` 模型管理所有类型的文件
- 支持图片、PDF、Office文档等多种文件类型
- 统一的元数据管理和分类系统
### 2. 完整的权限控制
- 用户只能访问自己的文件或公开文件
- 管理员可以访问所有文件
- 支持文件公开/私有状态切换
### 3. 丰富的API功能
- 文件列表、创建、检索、更新、删除
- 高级过滤、搜索和排序
- 文件统计和批量操作
- 软删除和恢复功能
### 4. 企业级特性
- 文件去重(基于SHA256校验和)
- 完整的文件验证(类型、大小)
- 详细的错误处理和状态码
- 文件元数据自动提取
### 5. 管理界面支持
- Django管理员界面集成
- 完整的文件管理功能
- 搜索和过滤支持
这个统一的数据模型和API设计提供了一个完整的企业级文件上传解决方案,支持多种文件类型,具有优秀的可扩展性和维护性。