feat(bi): expose dataset query workflow

This commit is contained in:
caoqianming 2026-08-05 15:01:44 +08:00
parent 5fb179eb9e
commit 4bf5f1e585
5 changed files with 150 additions and 13 deletions

View File

@ -18,10 +18,35 @@ class DatasetCreateUpdateSerializer(CustomModelSerializer):
class DatasetSerializer(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: class Meta:
model = Dataset model = Dataset
fields = '__all__' 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 DatasetRecordSerializer(CustomModelSerializer):
class Meta: class Meta:
model = DatasetRecord model = DatasetRecord
@ -36,6 +61,14 @@ class DatasetRecordSerializer(CustomModelSerializer):
class DataExecSerializer(serializers.Serializer): class DataExecSerializer(serializers.Serializer):
query = serializers.JSONField( query = serializers.JSONField(
label="查询字典参数", required=False, allow_null=True) label="查询字典参数",
is_test = serializers.BooleanField(label='是否测试', default=False) help_text="按所选数据集 description/default_param 声明的业务参数填写",
raise_exception = serializers.BooleanField(label='是否直接报错', default=False) 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
)

View File

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

View File

@ -3,7 +3,13 @@ from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
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 apps.bi.models import Dataset, DatasetRecord 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 from django.apps import apps
import concurrent.futures import concurrent.futures
from django.core.cache import cache 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 rest_framework.generics import get_object_or_404
from apps.utils.mixins import ListModelMixin from apps.utils.mixins import ListModelMixin
import logging import logging
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
# Create your views here. # Create your views here.
@ -22,9 +30,54 @@ class DatasetViewSet(CustomModelViewSet):
serializer_class = DatasetSerializer serializer_class = DatasetSerializer
create_serializer_class = DatasetCreateUpdateSerializer create_serializer_class = DatasetCreateUpdateSerializer
update_serializer_class = DatasetCreateUpdateSerializer update_serializer_class = DatasetCreateUpdateSerializer
search_fields = ['name', 'code'] search_fields = ['name', 'code', 'description']
ordering = ['name', 'code', 'id'] 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): def get_object(self):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
@ -57,6 +110,18 @@ class DatasetViewSet(CustomModelViewSet):
return obj 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=[]) @action(methods=['post'], detail=True, perms_map={'post': 'dataset.exec'}, serializer_class=DataExecSerializer, cache_seconds=0, logging_methods=[])
def exec(self, request, pk=None): def exec(self, request, pk=None):
"""执行sql查询 """执行sql查询

View File

@ -207,12 +207,28 @@ class CustomRetrieveModelMixin(RetrieveModelMixin):
class CustomListModelMixin(ListModelMixin): class CustomListModelMixin(ListModelMixin):
@swagger_auto_schema(manual_parameters=[ @swagger_auto_schema(
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据", operation_description=(
type=openapi.TYPE_STRING, required=False), "通用列表接口用于记录或目录浏览以及逐条追溯。跨时间范围的产量、良率、缺陷、"
openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)", "库存、绩效和趋势等统计聚合,优先查询 BI dataset 目录并执行匹配的数据集。"
type=openapi.TYPE_STRING, required=False), ),
]) 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): def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())

View File

@ -354,6 +354,7 @@ class MlogViewSet(CustomModelViewSet):
return super().get_serializer_class() return super().get_serializer_class()
@swagger_auto_schema( @swagger_auto_schema(
operation_summary="查询生产日志明细(逐条追溯)",
manual_parameters=[ manual_parameters=[
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据", type=openapi.TYPE_STRING, required=False), 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), openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)", type=openapi.TYPE_STRING, required=False),