From 970bbcc07051b4d4278da13a4e1ce4096d33c8e5 Mon Sep 17 00:00:00 2001 From: colaftc Date: Mon, 5 Jan 2026 19:35:45 +0800 Subject: [PATCH] feat: added jobs_status_summary to printing orders api --- api_v1/views/printing/serializers.py | 29 ++++++++ api_v1/views/printing/test_api.py | 99 ++++++++++++++++++++++++++++ api_v1/views/printing/views.py | 19 ++++++ 3 files changed, 147 insertions(+) diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index 92cf473..cd98bcf 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -137,6 +137,7 @@ class PrintingOrderListSerializer(serializers.ModelSerializer): customer_phone = serializers.CharField(source='customer.mobile', read_only=True) process_name = serializers.CharField(source='process.name', read_only=True) progress = serializers.IntegerField(read_only=True) + jobs_status_summary = serializers.SerializerMethodField() class Meta: model = models.PrintingOrder @@ -145,10 +146,38 @@ class PrintingOrderListSerializer(serializers.ModelSerializer): 'fabric', 'width', 'is_urgent', 'area', 'address', 'curve', 'is_fabric_received', 'outgoing_date', 'is_invalid', 'new_curve', 'process', 'process_name', 'progress', 'position', 'print_count', + 'jobs_status_summary', 'created_at', 'updated_at', ] read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count'] + def get_jobs_status_summary(self, obj): + """ + 返回订单下所有 PrintingJob 按当前状态分组的数量汇总 + + 返回格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...] + - state_name: 状态名称(已完成的返回"已完成") + - state_id: 状态ID(已完成的返回 None) + - count: 该状态下的 job 数量 + """ + # 使用预取的 printing_jobs(避免 N+1) + jobs = getattr(obj, '_prefetched_objects_cache', {}).get('printing_jobs') or obj.printing_jobs.all() + + from collections import Counter + status_counter = Counter() + + for job in jobs: + # 获取 job 的当前状态显示名称 + status_name = job.status # 调用 @property,返回状态名或"已完成" + status_id = job.status_id # 状态ID,已完成时为 None + status_counter[(status_name, status_id)] += 1 + + # 转换为列表格式 + return [ + {'state_name': name, 'state_id': sid, 'count': count} + for (name, sid), count in status_counter.items() + ] + class PrintingOrderDetailSerializer(serializers.ModelSerializer): """印染订单详情序列化器""" diff --git a/api_v1/views/printing/test_api.py b/api_v1/views/printing/test_api.py index e55f301..8b943e2 100644 --- a/api_v1/views/printing/test_api.py +++ b/api_v1/views/printing/test_api.py @@ -18,6 +18,10 @@ class PrintingOrderAPITestCase(TestCase): """测试 PrintingOrder API""" def setUp(self): + # 清除缓存,避免 cache_page 装饰器导致的测试干扰 + from django.core.cache import cache + cache.clear() + self.client = APIClient() # 创建商户 @@ -539,3 +543,98 @@ class PrintingOrderAPITestCase(TestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('已开始', str(response.data)) + def test_jobs_status_summary_in_list(self): + """测试订单列表包含 jobs_status_summary 字段(PrintingJob 状态汇总)""" + # 创建订单 + order = printing_models.PrintingOrder.objects.create( + customer=self.customer, + fabric='测试布料', + width='150cm', + process=self.process, + ) + + # 创建产品类别和产品 + category = basic_models.ProductCategory.objects.create( + name='测试类别', + merchant=self.merchant, + ) + product = basic_models.Product.objects.create( + name='测试产品', + category=category, + merchant=self.merchant, + ) + + # 创建 3 个任务 + jobs = [] + for i in range(3): + business_object = stateflow_models.BusinessObject.objects.create( + name=f'PrintingJob-test-{i}', + process=self.process, + ) + job = printing_models.PrintingJob.objects.create( + printing_order=order, + product=product, + quantity=10, + unit='件', + business_object=business_object, + ) + jobs.append(job) + + # 推进第一个任务的状态(完成第一个节点) + from stateflow.services import advance_to_next_state + advance_to_next_state(jobs[0].business_object, self.user) + + # 推进第二个任务的状态两次(完成前两个节点) + advance_to_next_state(jobs[1].business_object, self.user) + advance_to_next_state(jobs[1].business_object, self.user) + + # 第三个任务保持初始状态(待印染) + + # 获取订单列表 + response = self.client.get('/api/v1/printing-orders/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # 检查 jobs_status_summary 字段存在 + self.assertIn('jobs_status_summary', response.data['results'][0]) + summary = response.data['results'][0]['jobs_status_summary'] + + # 检查返回格式(列表) + self.assertIsInstance(summary, list) + + # 汇总结果应该包含不同状态的计数 + # - job0: 完成了第一个节点,下一个待执行是 "印染中" + # - job1: 完成了前两个节点,下一个待执行是 "已完成" + # - job2: 未开始,下一个待执行是 "待印染" + state_names = {item['state_name']: item['count'] for item in summary} + + # 验证有3个任务被正确统计 + total_count = sum(item['count'] for item in summary) + self.assertEqual(total_count, 3) + + # 验证返回的每个项都包含必要字段 + for item in summary: + self.assertIn('state_name', item) + self.assertIn('state_id', item) + self.assertIn('count', item) + + def test_jobs_status_summary_empty_when_no_jobs(self): + """测试没有任务时 jobs_status_summary 为空列表""" + order = printing_models.PrintingOrder.objects.create( + customer=self.customer, + fabric='无任务订单', + width='150cm', + process=self.process, + ) + + # 获取特定订单详情(避免被其他测试数据影响) + response = self.client.get(f'/api/v1/printing-orders/{order.id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # 详情接口不包含 jobs_status_summary(这是列表专用字段) + # 改用列表接口并按特定条件过滤 + response = self.client.get('/api/v1/printing-orders/', {'fabric': '无任务订单'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + summary = response.data['results'][0]['jobs_status_summary'] + self.assertEqual(summary, []) diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index 5f67e21..e08f510 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -123,6 +123,10 @@ class PrintingOrderViewSet(LimitedModelViewSet): - created_date_to: 创建日期结束 - search: 全文搜索(客户名称、面料、地区、工艺) - ordering: 排序字段 + + 返回字段说明: + - jobs_status_summary: 订单下所有印染任务按当前状态分组的数量汇总 + 格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...] """ queryset = models.PrintingOrder.objects.all() permission_classes = [DjangoModelPermissions] @@ -157,6 +161,21 @@ class PrintingOrderViewSet(LimitedModelViewSet): queryset = super().get_queryset() if self.action in ['list', 'retrieve']: queryset = queryset.select_related('customer') + # 预取 printing_jobs 及其 business_object,用于状态汇总统计 + # 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询 + queryset = queryset.prefetch_related( + Prefetch( + 'printing_jobs', + queryset=models.PrintingJob.objects.select_related( + 'business_object', + 'business_object__process', + ).prefetch_related( + 'business_object__state_logs', + 'business_object__process__process_nodes', + 'business_object__process__process_nodes__state', + ) + ) + ) return queryset def destroy(self, request, *args, **kwargs):