1
0
forked from erp-dev/erp

test: before all tests

This commit is contained in:
2025-12-25 21:36:50 +08:00
parent 147b5804d2
commit c0277a8218
18 changed files with 310 additions and 89 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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()

View File

@@ -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

View File

@@ -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):
"""测试任务完成状态判断""" """测试任务完成状态判断"""

View File

@@ -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 兜底,便于搜索数字编号

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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"""

View File

@@ -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):

View File

@@ -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):
""" """

View File

@@ -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)
# 金额类字段统一要求 > 0amount ≤ 0 视为非法 # 金额类字段要求非零(允许负数用于处理退款场景
if amount <= 0: # 付款单负金额 = 供应商退款,收款单负金额 = 退款给客户
# 与 API 文档/测试约定保持一致 if amount == 0:
raise ValueError(f'{field_name} 必须大于 0') raise ValueError(f'{field_name} 不能为 0')
return amount return amount

View 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 # 修改这里
```

View File

@@ -205,6 +205,21 @@ 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
# 检测是否在测试环境中
import sys
TESTING = 'test' in sys.argv
if TESTING:
# 测试环境使用内存缓存,避免 Redis 连接问题
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-testing-cache',
}
}
else:
# 生产/开发环境使用 Redis
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'BACKEND': 'django.core.cache.backends.redis.RedisCache',

49
flower/viewsets.py Normal file
View 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