diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index c5bb40a..3b44ff0 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -2,6 +2,7 @@ Printing API 序列化器 """ import json +from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers from rest_framework.fields import empty @@ -149,8 +150,8 @@ class PlateImageInputSerializer(serializers.Serializer): class PrintingOrderListSerializer(serializers.ModelSerializer): """印染订单列表序列化器""" - customer_name = serializers.CharField(source='customer.name', read_only=True) - customer_phone = serializers.CharField(source='customer.mobile', read_only=True) + customer_name = serializers.SerializerMethodField() + customer_phone = serializers.SerializerMethodField() process_name = serializers.CharField(source='process.name', read_only=True) progress = serializers.IntegerField(read_only=True) jobs_status_summary = serializers.SerializerMethodField() @@ -177,6 +178,24 @@ class PrintingOrderListSerializer(serializers.ModelSerializer): ] read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id'] + def get_customer_name(self, obj): + if hasattr(obj, 'customer_name'): + return obj.customer_name + try: + customer = getattr(obj, 'customer', None) + except ObjectDoesNotExist: + return None + return getattr(customer, 'name', None) + + def get_customer_phone(self, obj): + if hasattr(obj, 'customer_phone'): + return obj.customer_phone + try: + customer = getattr(obj, 'customer', None) + except ObjectDoesNotExist: + return None + return getattr(customer, 'mobile', None) + def get_jobs_status_summary(self, obj): """ 返回订单下所有 PrintingJob 按当前状态分组的数量汇总 diff --git a/api_v1/views/printing/test_api.py b/api_v1/views/printing/test_api.py index ce31832..f527d55 100644 --- a/api_v1/views/printing/test_api.py +++ b/api_v1/views/printing/test_api.py @@ -3,6 +3,7 @@ PrintingOrder API 测试 """ from datetime import datetime from django.test import TestCase +from django.db import connection from django.conf import settings from django.utils import timezone from rest_framework.test import APIClient @@ -170,6 +171,57 @@ class PrintingOrderAPITestCase(TestCase): # 分页响应格式 self.assertEqual(response.data['count'], 2) + def test_list_pagination_keeps_rows_with_dangling_customer_reference(self): + """历史脏数据 customer_id 悬空时,列表分页不应被 INNER JOIN 推空。""" + base = timezone.now() + dangling = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric='悬空客户订单', + width='150cm', + ) + middle = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric='中间订单', + width='150cm', + ) + oldest = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric='最旧订单', + width='150cm', + ) + printing_models.PrintingOrder.objects.filter(id=dangling.id).update( + created_at=base + timezone.timedelta(minutes=2), + ) + printing_models.PrintingOrder.objects.filter(id=middle.id).update( + created_at=base + timezone.timedelta(minutes=1), + ) + printing_models.PrintingOrder.objects.filter(id=oldest.id).update(created_at=base) + + missing_customer_id = 99999999 + with connection.cursor() as cursor: + cursor.execute( + 'UPDATE printing_printingorder SET customer_id = %s WHERE id = %s', + [missing_customer_id, dangling.id], + ) + + try: + response = self.client.get( + '/api/v1/printing-orders/?limit=1&offset=2&ordering=-created_at', + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 3) + self.assertEqual(len(response.data['results']), 1) + self.assertEqual(response.data['results'][0]['id'], oldest.id) + finally: + with connection.cursor() as cursor: + cursor.execute( + 'UPDATE printing_printingorder SET customer_id = %s WHERE id = %s', + [self.customer.id, dangling.id], + ) + def test_list_printing_orders_default_excludes_invalid(self): """默认不返回作废订单;显式传 is_invalid=true 时可查询作废订单""" printing_models.PrintingOrder.objects.create( @@ -443,6 +495,68 @@ class PrintingOrderAPITestCase(TestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('已经标记', response.data['detail']) + def _create_product_for_job(self, name='测试产品'): + category = basic_models.ProductCategory.objects.create( + merchant=self.merchant, + name=f'{name}类别', + product_prefix='JOB', + ) + return basic_models.Product.objects.create( + merchant=self.merchant, + category=category, + name=name, + unit=basic_models.ProductUnitEnum.METER, + ) + + def test_printing_order_ids_returns_job_ids(self): + """测试获取订单下所有 PrintingJob ID。""" + order = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric='测试布料', + width='150cm', + ) + product = self._create_product_for_job() + job1 = printing_models.PrintingJob.objects.create( + merchant=self.merchant, + printing_order=order, + product=product, + quantity=10, + unit='米', + ) + job2 = printing_models.PrintingJob.objects.create( + merchant=self.merchant, + printing_order=order, + product=product, + quantity=20, + unit='米', + ) + + response = self.client.get(f'/api/v1/printing-orders/{order.id}/ids/') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'ids': [job1.id, job2.id]}) + + def test_printing_order_ids_returns_empty_list_when_no_jobs(self): + """订单存在但没有任务时返回空数组。""" + order = printing_models.PrintingOrder.objects.create( + merchant=self.merchant, + customer=self.customer, + fabric='测试布料', + width='150cm', + ) + + response = self.client.get(f'/api/v1/printing-orders/{order.id}/ids/') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'ids': []}) + + def test_printing_order_ids_returns_404_when_order_missing(self): + """订单不存在时返回 404。""" + response = self.client.get('/api/v1/printing-orders/99999999/ids/') + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_filter_by_customer(self): """测试按客户过滤""" customer2 = basic_models.Customer.objects.create( diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index dd99075..2e86b86 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -24,6 +24,7 @@ from datetime import datetime, time, timedelta from flower.viewsets import LimitedModelViewSet from printing import models +from basic_info import models as basic_models from basic_info.models import MerchantTypeEnum from .serializers import ( @@ -301,8 +302,16 @@ class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): ): queryset = queryset.filter(is_invalid=False) - if self.action in ["list", "retrieve", "by_external_order_id"]: + if self.action == "list": + customer_qs = basic_models.Customer.objects.filter(id=OuterRef("customer_id")) + queryset = queryset.annotate( + customer_name=Subquery(customer_qs.values("name")[:1]), + customer_phone=Subquery(customer_qs.values("mobile")[:1]), + ) + elif self.action in ["retrieve", "by_external_order_id"]: queryset = queryset.select_related("customer") + + if self.action in ["list", "retrieve", "by_external_order_id"]: # 预取 printing_jobs 及其 business_object,用于状态汇总统计 # 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询 queryset = queryset.prefetch_related( @@ -350,6 +359,15 @@ class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): serializer = self.get_serializer(printing_order) return Response(serializer.data) + @action(detail=True, methods=["get"], url_path="ids") + def ids(self, request, pk=None): + """返回该印染订单下所有 PrintingJob ID。""" + printing_order = self.get_object() + ids = list( + printing_order.printing_jobs.order_by("id").values_list("id", flat=True) + ) + return Response({"ids": ids}) + @action(detail=True, methods=["post"]) def invalidate(self, request, pk=None): """ diff --git a/flower-app_latest.tar.gz b/flower-app_latest.tar.gz index 7330098..c741ee8 100644 Binary files a/flower-app_latest.tar.gz and b/flower-app_latest.tar.gz differ