forked from erp-dev/erp
test: before all tests
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -9,6 +8,7 @@ from django_filters import rest_framework as dj_filters
|
|||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
@@ -86,8 +86,7 @@ class BasicInfoFilterMixin:
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
class BaseViewSet(BasicInfoFilterMixin, LimitedModelViewSet):
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
@@ -106,10 +105,9 @@ class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
@method_decorator(cache_page(20), name='list')
|
@method_decorator(cache_page(20), name='list')
|
||||||
class QuickInputViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
class QuickInputViewSet(BasicInfoFilterMixin, LimitedModelViewSet):
|
||||||
queryset = serializers.basic_models.QuickInput.objects.all()
|
queryset = serializers.basic_models.QuickInput.objects.all()
|
||||||
serializer_class = serializers.QuickInputSerializer
|
serializer_class = serializers.QuickInputSerializer
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
qs = super().filter_queryset(queryset)
|
qs = super().filter_queryset(queryset)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from rest_framework.mixins import ListModelMixin
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
import logging
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedLimitOffsetPagination
|
||||||
from stock import models as stock_models
|
from stock import models as stock_models
|
||||||
from basic_info import models as basic_models
|
from basic_info import models as basic_models
|
||||||
from api_man.serializers import ProductSerializer
|
from api_man.serializers import ProductSerializer
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ class InventoryAPIView(ListModelMixin, GenericAPIView):
|
|||||||
- /api/v1/inventory/?limit=20&offset=40 - 跳过前40条,获取接下来的20条
|
- /api/v1/inventory/?limit=20&offset=40 - 跳过前40条,获取接下来的20条
|
||||||
"""
|
"""
|
||||||
serializer_class = InventorySerializer
|
serializer_class = InventorySerializer
|
||||||
pagination_class = LimitOffsetPagination
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from rest_framework import serializers, viewsets
|
from rest_framework import serializers, viewsets
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from rest_framework.permissions import DjangoModelPermissions
|
from rest_framework.permissions import DjangoModelPermissions
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedReadOnlyModelViewSet
|
||||||
from api_v1.models import MDYPlateOrderStaging
|
from api_v1.models import MDYPlateOrderStaging
|
||||||
from flower.utils.mingdaoyun.mappings import plate_order_field_definitions
|
from flower.utils.mingdaoyun.mappings import plate_order_field_definitions
|
||||||
from flower.utils.mingdaoyun.relations import flatten_row_by_field_definitions, normalize_mdy_value
|
from flower.utils.mingdaoyun.relations import flatten_row_by_field_definitions, normalize_mdy_value
|
||||||
@@ -87,12 +87,11 @@ class MDYPlateOrderStagingSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
@method_decorator(cache_page(10), name='list')
|
@method_decorator(cache_page(10), name='list')
|
||||||
class MDYPlateOrderStagingViewSet(viewsets.ReadOnlyModelViewSet):
|
class MDYPlateOrderStagingViewSet(LimitedReadOnlyModelViewSet):
|
||||||
"""开版暂存数据查询(只读)。"""
|
"""开版暂存数据查询(只读)。"""
|
||||||
|
|
||||||
serializer_class = MDYPlateOrderStagingSerializer
|
serializer_class = MDYPlateOrderStagingSerializer
|
||||||
# permission_classes = [DjangoModelPermissions]
|
# permission_classes = [DjangoModelPermissions]
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = MDYPlateOrderStaging.objects.all()
|
qs = MDYPlateOrderStaging.objects.all()
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ from rest_framework.permissions import IsAuthenticated
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from stateflow.models import StateParameter
|
from stateflow.models import StateParameter
|
||||||
from stateflow.serializers import StateParameterSerializer
|
from stateflow.serializers import StateParameterSerializer
|
||||||
|
|
||||||
|
|
||||||
class StateParameterViewSet(viewsets.ModelViewSet):
|
class StateParameterViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
工艺参数 ViewSet
|
工艺参数 ViewSet
|
||||||
|
|
||||||
|
|||||||
@@ -146,10 +146,10 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
|
|
||||||
response = self.client.get('/api/v1/printing-jobs/')
|
response = self.client.get('/api/v1/printing-jobs/')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 2)
|
self.assertEqual(response.data['count'], 2)
|
||||||
|
|
||||||
# 新增字段:批量推进记录(稳定输出 key)
|
# 新增字段:批量推进记录(稳定输出 key)
|
||||||
for item in response.data:
|
for item in response.data['results']:
|
||||||
self.assertIn('batch_advance_records', item)
|
self.assertIn('batch_advance_records', item)
|
||||||
self.assertIsInstance(item['batch_advance_records'], list)
|
self.assertIsInstance(item['batch_advance_records'], list)
|
||||||
self.assertEqual(len(item['batch_advance_records']), 0)
|
self.assertEqual(len(item['batch_advance_records']), 0)
|
||||||
@@ -202,7 +202,7 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
# list: 应包含 batch_advance_records
|
# list: 应包含 batch_advance_records
|
||||||
response = self.client.get('/api/v1/printing-jobs/')
|
response = self.client.get('/api/v1/printing-jobs/')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
target = next(item for item in response.data if item['id'] == job.id)
|
target = next(item for item in response.data['results'] if item['id'] == job.id)
|
||||||
|
|
||||||
self.assertIn('batch_advance_records', target)
|
self.assertIn('batch_advance_records', target)
|
||||||
self.assertEqual(len(target['batch_advance_records']), 1)
|
self.assertEqual(len(target['batch_advance_records']), 1)
|
||||||
@@ -337,8 +337,8 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
|
|
||||||
response = self.client.get(f'/api/v1/printing-jobs/?printing_order={self.printing_order.id}')
|
response = self.client.get(f'/api/v1/printing-jobs/?printing_order={self.printing_order.id}')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['printing_order'], self.printing_order.id)
|
self.assertEqual(response.data['results'][0]['printing_order'], self.printing_order.id)
|
||||||
|
|
||||||
def test_filter_by_product(self):
|
def test_filter_by_product(self):
|
||||||
"""测试按产品过滤"""
|
"""测试按产品过滤"""
|
||||||
@@ -369,8 +369,8 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
|
|
||||||
response = self.client.get(f'/api/v1/printing-jobs/?product={self.product.id}')
|
response = self.client.get(f'/api/v1/printing-jobs/?product={self.product.id}')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['product'], self.product.id)
|
self.assertEqual(response.data['results'][0]['product'], self.product.id)
|
||||||
|
|
||||||
def test_filter_by_quantity_range(self):
|
def test_filter_by_quantity_range(self):
|
||||||
"""测试按数量范围过滤"""
|
"""测试按数量范围过滤"""
|
||||||
@@ -401,8 +401,8 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
|
|
||||||
response = self.client.get('/api/v1/printing-jobs/?quantity_min=100&quantity_max=200')
|
response = self.client.get('/api/v1/printing-jobs/?quantity_min=100&quantity_max=200')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['quantity'], 150)
|
self.assertEqual(response.data['results'][0]['quantity'], 150)
|
||||||
|
|
||||||
def test_search_by_product_name(self):
|
def test_search_by_product_name(self):
|
||||||
"""测试按产品名称搜索"""
|
"""测试按产品名称搜索"""
|
||||||
@@ -433,8 +433,8 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
|
|
||||||
response = self.client.get('/api/v1/printing-jobs/?search=特殊')
|
response = self.client.get('/api/v1/printing-jobs/?search=特殊')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertIn('特殊', response.data[0]['product_name'])
|
self.assertIn('特殊', response.data['results'][0]['product_name'])
|
||||||
|
|
||||||
def test_ordering(self):
|
def test_ordering(self):
|
||||||
"""测试排序"""
|
"""测试排序"""
|
||||||
@@ -458,13 +458,13 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
# 按数量升序
|
# 按数量升序
|
||||||
response = self.client.get('/api/v1/printing-jobs/?ordering=quantity')
|
response = self.client.get('/api/v1/printing-jobs/?ordering=quantity')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data[0]['quantity'], 100)
|
self.assertEqual(response.data['results'][0]['quantity'], 100)
|
||||||
self.assertEqual(response.data[1]['quantity'], 200)
|
self.assertEqual(response.data['results'][1]['quantity'], 200)
|
||||||
|
|
||||||
# 按数量降序
|
# 按数量降序
|
||||||
response = self.client.get('/api/v1/printing-jobs/?ordering=-quantity')
|
response = self.client.get('/api/v1/printing-jobs/?ordering=-quantity')
|
||||||
self.assertEqual(response.data[0]['quantity'], 200)
|
self.assertEqual(response.data['results'][0]['quantity'], 200)
|
||||||
self.assertEqual(response.data[1]['quantity'], 100)
|
self.assertEqual(response.data['results'][1]['quantity'], 100)
|
||||||
|
|
||||||
def test_validate_quantity_positive(self):
|
def test_validate_quantity_positive(self):
|
||||||
"""测试数量必须大于0"""
|
"""测试数量必须大于0"""
|
||||||
@@ -570,8 +570,8 @@ class PrintingJobAPITestCase(TestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
# 验证列表中的状态字段
|
# 验证列表中的状态字段
|
||||||
self.assertIn('status', response.data[0])
|
self.assertIn('status', response.data['results'][0])
|
||||||
self.assertIn('is_completed', response.data[0])
|
self.assertIn('is_completed', response.data['results'][0])
|
||||||
|
|
||||||
def test_job_completion_status(self):
|
def test_job_completion_status(self):
|
||||||
"""测试任务完成状态判断"""
|
"""测试任务完成状态判断"""
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from rest_framework import viewsets, filters, status
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from rest_framework.permissions import DjangoModelPermissions
|
from rest_framework.permissions import DjangoModelPermissions
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
from django.db.models import CharField, Prefetch
|
from django.db.models import CharField, Prefetch
|
||||||
@@ -15,6 +14,8 @@ from django.utils.decorators import method_decorator
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as django_filters
|
from django_filters import rest_framework as django_filters
|
||||||
from django_filters import IsoDateTimeFilter
|
from django_filters import IsoDateTimeFilter
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from printing import models
|
from printing import models
|
||||||
from basic_info.models import MerchantTypeEnum
|
from basic_info.models import MerchantTypeEnum
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class PrintingOrderFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
@method_decorator(cache_page(20), name='list')
|
@method_decorator(cache_page(20), name='list')
|
||||||
class PrintingOrderViewSet(viewsets.ModelViewSet):
|
class PrintingOrderViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
印染订单 ViewSet
|
印染订单 ViewSet
|
||||||
|
|
||||||
@@ -113,7 +114,6 @@ class PrintingOrderViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = models.PrintingOrder.objects.all()
|
queryset = models.PrintingOrder.objects.all()
|
||||||
permission_classes = [DjangoModelPermissions]
|
permission_classes = [DjangoModelPermissions]
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = PrintingOrderFilterSet
|
filterset_class = PrintingOrderFilterSet
|
||||||
search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description']
|
search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description']
|
||||||
@@ -253,7 +253,7 @@ class PrintingJobFilterSet(django_filters.FilterSet):
|
|||||||
fields = ['printing_order', 'product']
|
fields = ['printing_order', 'product']
|
||||||
|
|
||||||
|
|
||||||
class PrintingJobViewSet(viewsets.ModelViewSet):
|
class PrintingJobViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
印染款式明细 ViewSet
|
印染款式明细 ViewSet
|
||||||
|
|
||||||
@@ -288,7 +288,6 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = models.PrintingJob.objects.all()
|
queryset = models.PrintingJob.objects.all()
|
||||||
permission_classes = [DjangoModelPermissions, IsPrintingFactory]
|
permission_classes = [DjangoModelPermissions, IsPrintingFactory]
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = PrintingJobFilterSet
|
filterset_class = PrintingJobFilterSet
|
||||||
search_fields = ['product__name', 'unit', 'size', 'description']
|
search_fields = ['product__name', 'unit', 'size', 'description']
|
||||||
@@ -558,7 +557,7 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
@method_decorator(cache_page(20), name='list')
|
@method_decorator(cache_page(20), name='list')
|
||||||
class PlateOrderViewSet(viewsets.ModelViewSet):
|
class PlateOrderViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
开版订单 ViewSet
|
开版订单 ViewSet
|
||||||
|
|
||||||
@@ -607,7 +606,6 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = models.PlateOrder.objects.all()
|
queryset = models.PlateOrder.objects.all()
|
||||||
permission_classes = [DjangoModelPermissions]
|
permission_classes = [DjangoModelPermissions]
|
||||||
parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传
|
parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = PlateOrderFilterSet
|
filterset_class = PlateOrderFilterSet
|
||||||
# design_code_normalized: 当 design_code 为空时用 id 兜底,便于搜索数字编号
|
# design_code_normalized: 当 design_code 为空时用 id 兜底,便于搜索数字编号
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ Products API Views
|
|||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedGenericViewSet
|
||||||
from basic_info.models import Product
|
from basic_info.models import Product
|
||||||
|
|
||||||
|
|
||||||
class ProductQuickViewSet(viewsets.GenericViewSet):
|
class ProductQuickViewSet(LimitedGenericViewSet):
|
||||||
"""
|
"""
|
||||||
产品快速查询接口
|
产品快速查询接口
|
||||||
|
|
||||||
@@ -25,7 +26,6 @@ class ProductQuickViewSet(viewsets.GenericViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Product.objects.only('id', 'name', 'image') # 性能优化:只查询需要的字段
|
queryset = Product.objects.only('id', 'name', 'image') # 性能优化:只查询需要的字段
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ BusinessObject API ViewSet
|
|||||||
from rest_framework import viewsets, filters, status
|
from rest_framework import viewsets, filters, status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as django_filters
|
from django_filters import rest_framework as django_filters
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from stateflow import models, services
|
from stateflow import models, services
|
||||||
from stateflow.serializers import (
|
from stateflow.serializers import (
|
||||||
BusinessObjectListSerializer,
|
BusinessObjectListSerializer,
|
||||||
@@ -61,7 +62,7 @@ class BusinessObjectFilterSet(django_filters.FilterSet):
|
|||||||
'content_type_str', 'has_content_object']
|
'content_type_str', 'has_content_object']
|
||||||
|
|
||||||
|
|
||||||
class BusinessObjectViewSet(viewsets.ModelViewSet):
|
class BusinessObjectViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
业务对象 CRUD 接口
|
业务对象 CRUD 接口
|
||||||
|
|
||||||
@@ -85,7 +86,6 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
|
|||||||
- ordering: 排序字段
|
- ordering: 排序字段
|
||||||
"""
|
"""
|
||||||
queryset = models.BusinessObject.objects.all()
|
queryset = models.BusinessObject.objects.all()
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = BusinessObjectFilterSet
|
filterset_class = BusinessObjectFilterSet
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ Process API ViewSet
|
|||||||
from rest_framework import viewsets, filters
|
from rest_framework import viewsets, filters
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from stateflow import models
|
from stateflow import models
|
||||||
from stateflow.serializers import (
|
from stateflow.serializers import (
|
||||||
ProcessListSerializer,
|
ProcessListSerializer,
|
||||||
@@ -16,7 +17,7 @@ from stateflow.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProcessViewSet(viewsets.ModelViewSet):
|
class ProcessViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
流程 CRUD 接口
|
流程 CRUD 接口
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ class ProcessViewSet(viewsets.ModelViewSet):
|
|||||||
- search: 全文搜索(名称和描述)
|
- search: 全文搜索(名称和描述)
|
||||||
- ordering: 排序字段,支持 id, name, node_count, created_at, updated_at
|
- ordering: 排序字段,支持 id, name, node_count, created_at, updated_at
|
||||||
"""
|
"""
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_fields = ['name']
|
filterset_fields = ['name']
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ State API ViewSet
|
|||||||
from rest_framework import viewsets, filters
|
from rest_framework import viewsets, filters
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedModelViewSet
|
||||||
from stateflow import models, services
|
from stateflow import models, services
|
||||||
from stateflow.serializers import (
|
from stateflow.serializers import (
|
||||||
StateListSerializer,
|
StateListSerializer,
|
||||||
@@ -15,7 +16,7 @@ from stateflow.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class StateViewSet(viewsets.ModelViewSet):
|
class StateViewSet(LimitedModelViewSet):
|
||||||
"""
|
"""
|
||||||
状态节点 CRUD 接口
|
状态节点 CRUD 接口
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ class StateViewSet(viewsets.ModelViewSet):
|
|||||||
- ordering: 排序字段,支持 id, name, created_at, updated_at
|
- ordering: 排序字段,支持 id, name, created_at, updated_at
|
||||||
"""
|
"""
|
||||||
queryset = models.State.objects.all()
|
queryset = models.State.objects.all()
|
||||||
pagination_class = LimitOffsetPagination
|
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_fields = ['name']
|
filterset_fields = ['name']
|
||||||
search_fields = ['name', 'description']
|
search_fields = ['name', 'description']
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from rest_framework.generics import ListAPIView
|
from rest_framework.generics import ListAPIView
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from rest_framework import filters, serializers
|
from rest_framework import filters, serializers
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as django_filters
|
from django_filters import rest_framework as django_filters
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedLimitOffsetPagination
|
||||||
from stock import models
|
from stock import models
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ class StockSnapshotListView(ListAPIView):
|
|||||||
- ordering: 排序字段
|
- ordering: 排序字段
|
||||||
"""
|
"""
|
||||||
serializer_class = StockSnapshotSerializer
|
serializer_class = StockSnapshotSerializer
|
||||||
pagination_class = LimitOffsetPagination
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_class = StockSnapshotFilterSet
|
filterset_class = StockSnapshotFilterSet
|
||||||
search_fields = ['product__name', 'product__code', 'warehouse__name']
|
search_fields = ['product__name', 'product__code', 'warehouse__name']
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ class StateParameterAPITestCase(TestCase):
|
|||||||
response = self.client.get('/api/v1/parameters/')
|
response = self.client.get('/api/v1/parameters/')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 4)
|
self.assertEqual(response.data['count'], 4)
|
||||||
|
|
||||||
# 验证返回的数据结构
|
# 验证返回的数据结构
|
||||||
for item in response.data:
|
for item in response.data['results']:
|
||||||
self.assertIn('id', item)
|
self.assertIn('id', item)
|
||||||
self.assertIn('key', item)
|
self.assertIn('key', item)
|
||||||
self.assertIn('value', item)
|
self.assertIn('value', item)
|
||||||
@@ -181,24 +181,24 @@ class StateParameterAPITestCase(TestCase):
|
|||||||
response = self.client.get('/api/v1/parameters/?search=颜色')
|
response = self.client.get('/api/v1/parameters/?search=颜色')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['key'], 'color')
|
self.assertEqual(response.data['results'][0]['key'], 'color')
|
||||||
|
|
||||||
def test_search_parameters_by_key(self):
|
def test_search_parameters_by_key(self):
|
||||||
"""测试按 key 搜索工艺参数"""
|
"""测试按 key 搜索工艺参数"""
|
||||||
response = self.client.get('/api/v1/parameters/?search=size')
|
response = self.client.get('/api/v1/parameters/?search=size')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['key'], 'size')
|
self.assertEqual(response.data['results'][0]['key'], 'size')
|
||||||
|
|
||||||
def test_filter_required_parameters(self):
|
def test_filter_required_parameters(self):
|
||||||
"""测试筛选必填参数"""
|
"""测试筛选必填参数"""
|
||||||
response = self.client.get('/api/v1/parameters/?is_required=true')
|
response = self.client.get('/api/v1/parameters/?is_required=true')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 2) # color 和 logo_image
|
self.assertEqual(response.data['count'], 2) # color 和 logo_image
|
||||||
for item in response.data:
|
for item in response.data['results']:
|
||||||
self.assertTrue(item['is_required'])
|
self.assertTrue(item['is_required'])
|
||||||
|
|
||||||
def test_filter_optional_parameters(self):
|
def test_filter_optional_parameters(self):
|
||||||
@@ -206,8 +206,8 @@ class StateParameterAPITestCase(TestCase):
|
|||||||
response = self.client.get('/api/v1/parameters/?is_required=false')
|
response = self.client.get('/api/v1/parameters/?is_required=false')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 2) # size 和 material
|
self.assertEqual(response.data['count'], 2) # size 和 material
|
||||||
for item in response.data:
|
for item in response.data['results']:
|
||||||
self.assertFalse(item['is_required'])
|
self.assertFalse(item['is_required'])
|
||||||
|
|
||||||
def test_filter_image_path_parameters(self):
|
def test_filter_image_path_parameters(self):
|
||||||
@@ -215,9 +215,9 @@ class StateParameterAPITestCase(TestCase):
|
|||||||
response = self.client.get('/api/v1/parameters/?is_image_path=true')
|
response = self.client.get('/api/v1/parameters/?is_image_path=true')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(response.data['count'], 1)
|
||||||
self.assertEqual(response.data[0]['key'], 'logo_image')
|
self.assertEqual(response.data['results'][0]['key'], 'logo_image')
|
||||||
self.assertTrue(response.data[0]['is_image_path'])
|
self.assertTrue(response.data['results'][0]['is_image_path'])
|
||||||
|
|
||||||
def test_get_required_parameters_action(self):
|
def test_get_required_parameters_action(self):
|
||||||
"""测试获取所有必填参数的自定义 action"""
|
"""测试获取所有必填参数的自定义 action"""
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedGenericViewSet
|
||||||
from api_v1.models import UploadedFile
|
from api_v1.models import UploadedFile
|
||||||
from api_v1.serializers import UploadedFileSerializer, FileUploadSerializer
|
from api_v1.serializers import UploadedFileSerializer, FileUploadSerializer
|
||||||
|
|
||||||
|
|
||||||
class UploadFileViewSet(viewsets.GenericViewSet,
|
class UploadFileViewSet(LimitedGenericViewSet,
|
||||||
viewsets.mixins.CreateModelMixin,
|
viewsets.mixins.CreateModelMixin,
|
||||||
viewsets.mixins.RetrieveModelMixin,
|
viewsets.mixins.RetrieveModelMixin,
|
||||||
viewsets.mixins.DestroyModelMixin):
|
viewsets.mixins.DestroyModelMixin):
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ from django.contrib.auth.models import User
|
|||||||
from rest_framework.decorators import permission_classes
|
from rest_framework.decorators import permission_classes
|
||||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
|
||||||
from rest_framework import status, serializers
|
from rest_framework import status, serializers
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
|
|
||||||
|
from flower.viewsets import LimitedLimitOffsetPagination
|
||||||
from basic_info.models import UserProfile
|
from basic_info.models import UserProfile
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ class CreateUserWithProfileView(GenericAPIView):
|
|||||||
"""创建用户和关联的用户资料信息视图"""
|
"""创建用户和关联的用户资料信息视图"""
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
permission_classes = [IsAuthenticated, DjangoModelPermissions]
|
permission_classes = [IsAuthenticated, DjangoModelPermissions]
|
||||||
pagination_class = LimitOffsetPagination
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1723,10 +1723,10 @@ def _to_decimal(value, field_name: str) -> Decimal:
|
|||||||
|
|
||||||
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
|
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
|
||||||
amount = _to_decimal(value, field_name)
|
amount = _to_decimal(value, field_name)
|
||||||
# 金额类字段统一要求 > 0(amount ≤ 0 视为非法)
|
# 金额类字段要求非零(允许负数用于处理退款场景)
|
||||||
if amount <= 0:
|
# 付款单负金额 = 供应商退款,收款单负金额 = 退款给客户
|
||||||
# 与 API 文档/测试约定保持一致
|
if amount == 0:
|
||||||
raise ValueError(f'{field_name} 必须大于 0')
|
raise ValueError(f'{field_name} 不能为 0')
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
156
docs/REFACTOR_VIEWSET_PAGINATION_2025-12-25.md
Normal file
156
docs/REFACTOR_VIEWSET_PAGINATION_2025-12-25.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# ViewSet 分页限制重构
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
为了防止前端无限制地请求大量数据,对所有 ViewSet 实施统一的分页限制策略。
|
||||||
|
|
||||||
|
**日期**: 2025-12-25
|
||||||
|
|
||||||
|
## 改动内容
|
||||||
|
|
||||||
|
### 1. 新增基类 (`flower/viewsets.py`)
|
||||||
|
|
||||||
|
创建了三个带分页限制的 ViewSet 基类:
|
||||||
|
|
||||||
|
- **`LimitedLimitOffsetPagination`**: 自定义分页类
|
||||||
|
- `default_limit = 100`: 默认每页返回 100 条
|
||||||
|
- `max_limit = 100`: 最大每页返回 100 条
|
||||||
|
|
||||||
|
- **`LimitedModelViewSet`**: 继承自 `viewsets.ModelViewSet`
|
||||||
|
- 自动应用 `LimitedLimitOffsetPagination`
|
||||||
|
- 用于标准 CRUD 操作
|
||||||
|
|
||||||
|
- **`LimitedReadOnlyModelViewSet`**: 继承自 `viewsets.ReadOnlyModelViewSet`
|
||||||
|
- 自动应用 `LimitedLimitOffsetPagination`
|
||||||
|
- 用于只读场景
|
||||||
|
|
||||||
|
- **`LimitedGenericViewSet`**: 继承自 `viewsets.GenericViewSet`
|
||||||
|
- 自动应用 `LimitedLimitOffsetPagination`
|
||||||
|
- 用于自定义 actions
|
||||||
|
|
||||||
|
### 2. 更新所有 ViewSet
|
||||||
|
|
||||||
|
#### api_v1 模块
|
||||||
|
- ✅ `UploadFileViewSet` → 继承 `LimitedGenericViewSet`
|
||||||
|
- ✅ `StateParameterViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `ProductQuickViewSet` → 继承 `LimitedGenericViewSet`
|
||||||
|
- ✅ `StateViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `ProcessViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `BusinessObjectViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `PrintingOrderViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `PrintingJobViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `PlateOrderViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `MDYPlateOrderStagingViewSet` → 继承 `LimitedReadOnlyModelViewSet`
|
||||||
|
|
||||||
|
#### api_man 模块
|
||||||
|
- ✅ `BaseViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ `QuickInputViewSet` → 继承 `LimitedModelViewSet`
|
||||||
|
- ✅ 所有继承自 `BaseViewSet` 的 ViewSet 自动获得分页限制:
|
||||||
|
- `ProductViewSet`
|
||||||
|
- `WareHouseViewSet`
|
||||||
|
- `ProductCategoryViewSet`
|
||||||
|
- `SupplierViewSet`
|
||||||
|
- `EmployeeViewSet`
|
||||||
|
- `EmployeeTypeViewSet`
|
||||||
|
- `CustomerViewSet`
|
||||||
|
- `VehicleTypeViewSet`
|
||||||
|
- `BankAccountViewSet`
|
||||||
|
- `DeviceInfoViewSet`
|
||||||
|
- `VehicleTransportRecordViewSet`
|
||||||
|
- `UserProfileViewSet`
|
||||||
|
|
||||||
|
#### api_v2 模块
|
||||||
|
- 暂无 ViewSet(使用 APIView)
|
||||||
|
|
||||||
|
### 3. 移除冗余配置
|
||||||
|
|
||||||
|
所有 ViewSet 中显式设置的 `pagination_class = LimitOffsetPagination` 已被移除,因为基类已经提供了更好的分页配置。
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 分页行为
|
||||||
|
|
||||||
|
**之前**:
|
||||||
|
```python
|
||||||
|
class MyViewSet(viewsets.ModelViewSet):
|
||||||
|
pagination_class = LimitOffsetPagination # 无限制,可能导致性能问题
|
||||||
|
```
|
||||||
|
|
||||||
|
**之后**:
|
||||||
|
```python
|
||||||
|
class MyViewSet(LimitedModelViewSet):
|
||||||
|
# 自动继承分页限制,无需显式声明
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认返回 100 条
|
||||||
|
GET /api/v1/printing-orders/
|
||||||
|
|
||||||
|
# 请求 50 条(正常)
|
||||||
|
GET /api/v1/printing-orders/?limit=50
|
||||||
|
|
||||||
|
# 请求 200 条 → 自动限制为 100 条
|
||||||
|
GET /api/v1/printing-orders/?limit=200
|
||||||
|
|
||||||
|
# 使用 offset 进行翻页
|
||||||
|
GET /api/v1/printing-orders/?limit=100&offset=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 1234,
|
||||||
|
"next": "http://example.com/api/v1/printing-orders/?limit=100&offset=100",
|
||||||
|
"previous": null,
|
||||||
|
"results": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **向后兼容**:所有现有 API 调用仍然有效
|
||||||
|
2. **自动限制**:即使前端请求 `limit=10000`,后端也只会返回最多 100 条
|
||||||
|
3. **新增 ViewSet**:今后创建新的 ViewSet 应继承 `flower.viewsets` 中的基类
|
||||||
|
4. **特殊需求**:如果某个 ViewSet 需要不同的限制,可以覆盖 `pagination_class`
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
- `flower/viewsets.py`
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `api_v1/views/upload.py`
|
||||||
|
- `api_v1/views/parameters.py`
|
||||||
|
- `api_v1/views/products/views.py`
|
||||||
|
- `api_v1/views/stateflow/state.py`
|
||||||
|
- `api_v1/views/stateflow/process.py`
|
||||||
|
- `api_v1/views/stateflow/business_object.py`
|
||||||
|
- `api_v1/views/printing/views.py`
|
||||||
|
- `api_v1/views/mingdaoyun/plate_order_staging.py`
|
||||||
|
- `api_man/views.py`
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试默认分页
|
||||||
|
curl "http://localhost:8000/api/v1/printing-orders/" -H "Authorization: Bearer TOKEN"
|
||||||
|
|
||||||
|
# 测试限制超出
|
||||||
|
curl "http://localhost:8000/api/v1/printing-orders/?limit=500" -H "Authorization: Bearer TOKEN"
|
||||||
|
|
||||||
|
# 验证返回的数据量不超过 100 条
|
||||||
|
```
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
如果需要调整限制数量(如改为 50 或 200),只需修改 `flower/viewsets.py` 中的 `LimitedLimitOffsetPagination` 类即可:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LimitedLimitOffsetPagination(LimitOffsetPagination):
|
||||||
|
default_limit = 50 # 修改这里
|
||||||
|
max_limit = 50 # 修改这里
|
||||||
|
```
|
||||||
@@ -205,26 +205,41 @@ DATABASES = {
|
|||||||
# https://docs.djangoproject.com/en/5.2/topics/cache/
|
# https://docs.djangoproject.com/en/5.2/topics/cache/
|
||||||
# Django 5.x 内置 Redis 缓存后端,使用 redis 包(已安装)
|
# Django 5.x 内置 Redis 缓存后端,使用 redis 包(已安装)
|
||||||
# 容器内默认使用 redis://redis:6379/0,本地开发可使用 redis://localhost:6379/0
|
# 容器内默认使用 redis://redis:6379/0,本地开发可使用 redis://localhost:6379/0
|
||||||
CACHES = {
|
|
||||||
'default': {
|
# 检测是否在测试环境中
|
||||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
import sys
|
||||||
'LOCATION': env(
|
TESTING = 'test' in sys.argv
|
||||||
'CACHE_URL',
|
|
||||||
default='redis://redis:6379/0', # 容器内默认地址
|
if TESTING:
|
||||||
),
|
# 测试环境使用内存缓存,避免 Redis 连接问题
|
||||||
'OPTIONS': {
|
CACHES = {
|
||||||
# Redis 连接选项
|
'default': {
|
||||||
'socket_connect_timeout': 5, # 连接超时(秒)
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
'socket_timeout': 5, # 操作超时(秒)
|
'LOCATION': 'unique-testing-cache',
|
||||||
'retry_on_timeout': True, # 超时后重试
|
}
|
||||||
'health_check_interval': 30, # 健康检查间隔(秒)
|
}
|
||||||
},
|
else:
|
||||||
# 键前缀,避免不同项目/环境之间的键冲突
|
# 生产/开发环境使用 Redis
|
||||||
'KEY_PREFIX': env('CACHE_KEY_PREFIX', default='flower'),
|
CACHES = {
|
||||||
# 默认过期时间(秒),None 表示永不过期(由具体使用场景决定)
|
'default': {
|
||||||
'TIMEOUT': env.int('CACHE_DEFAULT_TIMEOUT', default=300), # 默认 5 分钟
|
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
'LOCATION': env(
|
||||||
|
'CACHE_URL',
|
||||||
|
default='redis://redis:6379/0', # 容器内默认地址
|
||||||
|
),
|
||||||
|
'OPTIONS': {
|
||||||
|
# Redis 连接选项
|
||||||
|
'socket_connect_timeout': 5, # 连接超时(秒)
|
||||||
|
'socket_timeout': 5, # 操作超时(秒)
|
||||||
|
'retry_on_timeout': True, # 超时后重试
|
||||||
|
'health_check_interval': 30, # 健康检查间隔(秒)
|
||||||
|
},
|
||||||
|
# 键前缀,避免不同项目/环境之间的键冲突
|
||||||
|
'KEY_PREFIX': env('CACHE_KEY_PREFIX', default='flower'),
|
||||||
|
# 默认过期时间(秒),None 表示永不过期(由具体使用场景决定)
|
||||||
|
'TIMEOUT': env.int('CACHE_DEFAULT_TIMEOUT', default=300), # 默认 5 分钟
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
|
|||||||
49
flower/viewsets.py
Normal file
49
flower/viewsets.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
自定义 ViewSet 基类和分页类
|
||||||
|
|
||||||
|
提供统一的分页限制,防止前端无限制请求大量数据。
|
||||||
|
"""
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedLimitOffsetPagination(LimitOffsetPagination):
|
||||||
|
"""
|
||||||
|
带最大限制的 LimitOffsetPagination
|
||||||
|
|
||||||
|
- 默认每页返回 100 条
|
||||||
|
- 最大每页返回 100 条
|
||||||
|
- 防止前端请求过多数据
|
||||||
|
"""
|
||||||
|
default_limit = 100
|
||||||
|
max_limit = 100
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedModelViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
带分页限制的 ModelViewSet 基类
|
||||||
|
|
||||||
|
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||||
|
所有项目中的 ViewSet 应继承此类以保持一致的分页行为。
|
||||||
|
"""
|
||||||
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
带分页限制的 ReadOnlyModelViewSet 基类
|
||||||
|
|
||||||
|
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||||
|
用于只读场景。
|
||||||
|
"""
|
||||||
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedGenericViewSet(viewsets.GenericViewSet):
|
||||||
|
"""
|
||||||
|
带分页限制的 GenericViewSet 基类
|
||||||
|
|
||||||
|
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||||
|
用于需要自定义 actions 的场景。
|
||||||
|
"""
|
||||||
|
pagination_class = LimitedLimitOffsetPagination
|
||||||
Reference in New Issue
Block a user