From 112c0dd3bcac2e20e1ee0e535b6c13403930f049 Mon Sep 17 00:00:00 2001 From: XIE7654 <765462425@qq.com> Date: Wed, 8 Oct 2025 14:46:43 +0800 Subject: [PATCH] feat: add export --- backend/backend/urls.py | 1 + backend/requirements.txt | 3 +- backend/system/views/dict_data.py | 3 +- backend/system/views/user.py | 17 +++++ backend/utils/custom_model_viewSet.py | 4 +- backend/utils/export_mixin.py | 101 ++++++++++++++++++++++++++ 6 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 backend/utils/export_mixin.py diff --git a/backend/backend/urls.py b/backend/backend/urls.py index a0710d4..eef37ae 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -21,6 +21,7 @@ from django.conf import settings urlpatterns = [ path('api/admin/system/', include('system.urls')), path('api/admin/ai/', include('ai.urls')), + path('api-auth/', include('rest_framework.urls')) ] # 演示环境下禁用 admin 路由 diff --git a/backend/requirements.txt b/backend/requirements.txt index b683396..724e69f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -15,4 +15,5 @@ flower==2.0.1 gunicorn==23.0.0 django_redis==6.0.0 django-ninja==1.4.3 -ip2geotools==0.1.6 \ No newline at end of file +ip2geotools==0.1.6 +pandas==2.2.3 \ No newline at end of file diff --git a/backend/system/views/dict_data.py b/backend/system/views/dict_data.py index 1f36614..8698cb4 100644 --- a/backend/system/views/dict_data.py +++ b/backend/system/views/dict_data.py @@ -30,7 +30,8 @@ class DictDataLabelValueSerializer(serializers.ModelSerializer): class DictDataViewSet(CustomModelViewSet): - queryset = DictData.objects.filter(is_deleted=False) + queryset = (DictData.objects.filter(is_deleted=False).select_related('dict_type') + .order_by('dict_type__id', 'sort')) serializer_class = DictDataSerializer filterset_class = DictDataFilter diff --git a/backend/system/views/user.py b/backend/system/views/user.py index d3906f8..cec84a1 100644 --- a/backend/system/views/user.py +++ b/backend/system/views/user.py @@ -182,6 +182,23 @@ class UserViewSet(CustomModelViewSet): search_fields = ['username', 'nickname', 'mobile'] # 支持模糊搜索 ordering_fields = ['create_time', 'id'] ordering = ['-create_time'] + export_fields = { + 'id': 'ID', + 'username': '用户名', + 'nickname': '昵称', + 'mobile': '手机号', + 'email': '邮箱', + 'status': { + 'name': '状态', + 'processor': lambda value, item: '启用' if value == 1 else '禁用' + }, + 'login_ip': '登录IP', + 'last_login': '最后登录时间', + 'create_time': '创建时间', + 'update_time': '更新时间', + } + + export_filename = '用户数据' class Logout(APIView): diff --git a/backend/utils/custom_model_viewSet.py b/backend/utils/custom_model_viewSet.py index 6a72872..024b0de 100644 --- a/backend/utils/custom_model_viewSet.py +++ b/backend/utils/custom_model_viewSet.py @@ -1,8 +1,10 @@ from rest_framework import viewsets, status from rest_framework.response import Response +from utils.export_mixin import ExportMixin -class CustomModelViewSet(viewsets.ModelViewSet): + +class CustomModelViewSet(viewsets.ModelViewSet, ExportMixin): """ 自定义ModelViewSet,提供以下增强功能: - 基于动作的序列化器选择 diff --git a/backend/utils/export_mixin.py b/backend/utils/export_mixin.py new file mode 100644 index 0000000..956ef7f --- /dev/null +++ b/backend/utils/export_mixin.py @@ -0,0 +1,101 @@ +import pandas as pd + +from rest_framework.decorators import action +from django.http import HttpResponse + +class ExportMixin: + """ + 导出功能 Mixin,提供数据导出为 Excel 或 CSV 的功能 + """ + # 导出配置 + export_fields = {} # 字段映射配置 + export_filename = None # 导出文件名 + + @action(detail=False, methods=['get'], url_path='export') + def export_data(self, request): + """ + 导出数据功能 + 支持通过参数控制导出字段: + - fields: 指定要导出的字段,多个字段用逗号分隔 + - format: 导出格式 (excel/csv),默认excel + """ + # 获取查询集 + queryset = self.filter_queryset(self.get_queryset()) + + # 获取导出字段配置 + export_config = self.get_export_fields(request) + + # 序列化数据 + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + + # 处理数据 + processed_data = self.process_export_data(data, export_config) + + # 生成文件名 + filename = self.export_filename or f"{self.__class__.__name__}_export" + + # 根据格式返回响应 + export_format = request.query_params.get('format', 'excel').lower() + + if export_format == 'csv': + return self.generate_csv_response(processed_data, filename) + else: + return self.generate_excel_response(processed_data, filename) + + def get_export_fields(self, request): + """ + 获取导出字段配置 + 支持通过URL参数指定字段: ?fields=id,name,email + """ + # 优先使用请求参数中的字段 + fields_param = request.query_params.get('fields') + if fields_param: + field_names = [f.strip() for f in fields_param.split(',')] + # 只返回指定字段的配置 + return {field: self.export_fields.get(field, field) for field in field_names} + # 默认返回所有配置字段 + return self.export_fields or {} + + def process_export_data(self, data, export_config): + """ + 处理导出数据,根据配置映射字段名和处理数据 + """ + if not export_config: + return data + + processed_data = [] + for item in data: + processed_item = {} + for field_key, field_config in export_config.items(): + # field_config 可以是字符串(列名)或字典(包含列名和处理函数) + if isinstance(field_config, dict): + column_name = field_config.get('name', field_key) + processor = field_config.get('processor') + value = item.get(field_key) + if processor and callable(processor): + value = processor(value, item) + processed_item[column_name] = value + else: + # 简单映射 + processed_item[field_config] = item.get(field_key, '') + processed_data.append(processed_item) + return processed_data + + def generate_excel_response(self, data, filename): + """生成Excel格式响应""" + df = pd.DataFrame(data) + response = HttpResponse( + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = f'attachment; filename="{filename}.xlsx"' + df.to_excel(response, index=False) + return response + + def generate_csv_response(self, data, filename): + """生成CSV格式响应""" + df = pd.DataFrame(data) + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename="{filename}.csv"' + df.to_csv(response, index=False) + return response