From 4bf5f1e58513e51eb6aac6505db6748aa643264b Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 5 Aug 2026 15:01:44 +0800 Subject: [PATCH] feat(bi): expose dataset query workflow --- apps/bi/serializers.py | 39 ++++++++++++++++++++++-- apps/bi/tests.py | 26 ++++++++++++++-- apps/bi/views.py | 69 ++++++++++++++++++++++++++++++++++++++++-- apps/utils/mixins.py | 28 +++++++++++++---- apps/wpm/views.py | 1 + 5 files changed, 150 insertions(+), 13 deletions(-) diff --git a/apps/bi/serializers.py b/apps/bi/serializers.py index 35d8fe1b..5a666dc5 100644 --- a/apps/bi/serializers.py +++ b/apps/bi/serializers.py @@ -18,10 +18,35 @@ class DatasetCreateUpdateSerializer(CustomModelSerializer): class DatasetSerializer(CustomModelSerializer): + description = serializers.CharField( + label="适用场景与统计口径", + help_text="说明该数据集适合回答的问题、指标口径、参数格式和返回字段含义", + required=False, + allow_blank=True, + ) + default_param = serializers.JSONField( + label="默认查询参数", + help_text="执行时可覆盖的参数及默认值;内部 SQL 片段参数应保留默认值", + required=False, + ) + test_param = serializers.JSONField( + label="测试查询参数", + help_text="数据集维护时使用的示例参数,普通查询优先参考 description", + required=False, + ) + class Meta: model = Dataset fields = '__all__' + +class DatasetListResponseSerializer(serializers.Serializer): + count = serializers.IntegerField(label="数据集总数") + next = serializers.URLField(required=False, allow_null=True) + previous = serializers.URLField(required=False, allow_null=True) + results = DatasetSerializer(many=True) + + class DatasetRecordSerializer(CustomModelSerializer): class Meta: model = DatasetRecord @@ -36,6 +61,14 @@ class DatasetRecordSerializer(CustomModelSerializer): class DataExecSerializer(serializers.Serializer): query = serializers.JSONField( - label="查询字典参数", required=False, allow_null=True) - is_test = serializers.BooleanField(label='是否测试', default=False) - raise_exception = serializers.BooleanField(label='是否直接报错', default=False) + label="查询字典参数", + help_text="按所选数据集 description/default_param 声明的业务参数填写", + required=False, + allow_null=True, + ) + is_test = serializers.BooleanField( + label='是否测试', help_text="普通业务查询固定为 false", default=False + ) + raise_exception = serializers.BooleanField( + label='是否直接报错', help_text="建议为 true,便于修正缺失或非法参数", default=True + ) diff --git a/apps/bi/tests.py b/apps/bi/tests.py index 7ce503c2..30d14569 100644 --- a/apps/bi/tests.py +++ b/apps/bi/tests.py @@ -1,3 +1,25 @@ -from django.test import TestCase +from django.test import SimpleTestCase -# Create your tests here. +from apps.bi.serializers import ( + DataExecSerializer, + DatasetListResponseSerializer, + DatasetSerializer, +) +from apps.bi.views import DatasetViewSet + + +class DatasetAgentDiscoveryTests(SimpleTestCase): + def test_dataset_catalog_searches_description(self): + self.assertEqual( + DatasetViewSet.search_fields, + ["name", "code", "description"], + ) + + def test_dataset_schema_explains_discovery_and_exec_parameters(self): + dataset = DatasetSerializer() + execute = DataExecSerializer() + + self.assertIn("适合回答的问题", dataset.fields["description"].help_text) + self.assertIn("业务参数", execute.fields["query"].help_text) + self.assertTrue(execute.fields["raise_exception"].default) + self.assertIn("results", DatasetListResponseSerializer().fields) diff --git a/apps/bi/views.py b/apps/bi/views.py index 6912ae74..7c5ac1d3 100644 --- a/apps/bi/views.py +++ b/apps/bi/views.py @@ -3,7 +3,13 @@ from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet from rest_framework.decorators import action from rest_framework.response import Response from apps.bi.models import Dataset, DatasetRecord -from apps.bi.serializers import DatasetSerializer, DatasetCreateUpdateSerializer, DataExecSerializer, DatasetRecordSerializer +from apps.bi.serializers import ( + DataExecSerializer, + DatasetCreateUpdateSerializer, + DatasetListResponseSerializer, + DatasetRecordSerializer, + DatasetSerializer, +) from django.apps import apps import concurrent.futures from django.core.cache import cache @@ -13,6 +19,8 @@ from rest_framework.exceptions import ParseError from rest_framework.generics import get_object_or_404 from apps.utils.mixins import ListModelMixin import logging +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema myLogger = logging.getLogger('log') # Create your views here. @@ -22,9 +30,54 @@ class DatasetViewSet(CustomModelViewSet): serializer_class = DatasetSerializer create_serializer_class = DatasetCreateUpdateSerializer update_serializer_class = DatasetCreateUpdateSerializer - search_fields = ['name', 'code'] + search_fields = ['name', 'code', 'description'] ordering = ['name', 'code', 'id'] + @swagger_auto_schema( + operation_id="bi_dataset_list", + operation_summary="查询复杂统计报表的数据集目录", + operation_description=( + "产量、良率、缺陷、库存、绩效、趋势和按日/月汇总等统计聚合查询的统一入口。" + "先调用本接口,根据 name、description、default_param 和 test_param 选择数据集," + "再调用 bi_dataset_exec。建议使用 query={id,name,code,description,default_param," + "test_param,enabled} 裁剪字段,并设置 page_size=100 查看完整目录;" + "search 可按名称、code 或 description 检索。" + ), + manual_parameters=[ + openapi.Parameter( + "search", + openapi.IN_QUERY, + description="按数据集名称、code 或适用场景关键词检索", + type=openapi.TYPE_STRING, + ), + openapi.Parameter( + "page", + openapi.IN_QUERY, + description="页码,从 1 开始", + type=openapi.TYPE_INTEGER, + ), + openapi.Parameter( + "page_size", + openapi.IN_QUERY, + description="每页数量;当前目录建议传 100", + type=openapi.TYPE_INTEGER, + ), + openapi.Parameter( + "query", + openapi.IN_QUERY, + description=( + "django-restql 字段裁剪表达式,例如 " + "{id,name,code,description,default_param,test_param,enabled}" + ), + type=openapi.TYPE_STRING, + ), + ], + responses={200: DatasetListResponseSerializer}, + tags=["BI 数据集与报表"], + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + def get_object(self): """ Returns the object the view is displaying. @@ -57,6 +110,18 @@ class DatasetViewSet(CustomModelViewSet): return obj + @swagger_auto_schema( + operation_id="bi_dataset_exec", + operation_summary="执行已配置的只读统计数据集", + operation_description=( + "使用 dataset list 返回的 id 或 code 执行数据集。body.query 只填写该数据集" + "description/default_param 声明的业务参数;正常查询设置 is_test=false。" + "统计聚合使用本接口,日志和业务明细列表用于逐条追溯。" + ), + request_body=DataExecSerializer, + responses={200: DatasetSerializer}, + tags=["BI 数据集与报表"], + ) @action(methods=['post'], detail=True, perms_map={'post': 'dataset.exec'}, serializer_class=DataExecSerializer, cache_seconds=0, logging_methods=[]) def exec(self, request, pk=None): """执行sql查询 diff --git a/apps/utils/mixins.py b/apps/utils/mixins.py index 990d5926..61382ded 100755 --- a/apps/utils/mixins.py +++ b/apps/utils/mixins.py @@ -207,12 +207,28 @@ class CustomRetrieveModelMixin(RetrieveModelMixin): class CustomListModelMixin(ListModelMixin): - @swagger_auto_schema(manual_parameters=[ - openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据", - type=openapi.TYPE_STRING, required=False), - openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)", - type=openapi.TYPE_STRING, required=False), - ]) + @swagger_auto_schema( + operation_description=( + "通用列表接口用于记录或目录浏览以及逐条追溯。跨时间范围的产量、良率、缺陷、" + "库存、绩效和趋势等统计聚合,优先查询 BI dataset 目录并执行匹配的数据集。" + ), + manual_parameters=[ + openapi.Parameter( + name="query", + in_=openapi.IN_QUERY, + description="django-restql 返回字段裁剪表达式", + type=openapi.TYPE_STRING, + required=False, + ), + openapi.Parameter( + name="with_children", + in_=openapi.IN_QUERY, + description="带有children(yes/no/count)", + type=openapi.TYPE_STRING, + required=False, + ), + ], + ) def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) diff --git a/apps/wpm/views.py b/apps/wpm/views.py index 60e747ed..1084145f 100644 --- a/apps/wpm/views.py +++ b/apps/wpm/views.py @@ -354,6 +354,7 @@ class MlogViewSet(CustomModelViewSet): return super().get_serializer_class() @swagger_auto_schema( + operation_summary="查询生产日志明细(逐条追溯)", manual_parameters=[ openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据", type=openapi.TYPE_STRING, required=False), openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)", type=openapi.TYPE_STRING, required=False),