api_data_lineage_visualization.md 18 KB

数据加工可视化 API 接口文档

本文档为前端开发人员提供数据加工可视化(血缘追溯)功能的 API 接口说明。

目录


功能概述

数据加工可视化功能用于展示数据产品的完整血缘关系图谱。当用户查看某个数据产品的数据样例时,前端发送一条样例数据,后端会:

  1. 通过该数据产品关联的 BusinessDomain 节点
  2. 沿着 INPUT/OUTPUT 关系向上追溯血缘
  3. 直到到达数据源节点(具有 DataResource 标签)
  4. 将样例数据的字段值映射到各层级节点的对应字段
  5. 返回完整的节点图谱和关系数据

血缘关系模型

[DataResource] --INPUT--> [DataFlow] --OUTPUT--> [BusinessDomain] --INPUT--> [DataFlow] --OUTPUT--> [目标节点]
  • INPUT 关系: BusinessDomain 作为 DataFlow 的输入源
  • OUTPUT 关系: DataFlow 输出到 BusinessDomain
  • 终止条件: 节点同时具有 BusinessDomainDataResource 两个标签

接口详情

获取血缘可视化数据

获取指定数据产品的血缘追溯图谱,并将样例数据映射到各节点字段。

请求 URL: POST /api/dataservice/products/{product_id}/lineage-visualization

请求方式: POST

Content-Type: application/json

请求参数

路径参数:

参数名 类型 必填 说明
product_id integer 数据产品 ID

请求体参数:

参数名 类型 必填 说明
sample_data object 样例数据,key 为中文字段名,value 为对应的值

请求示例:

{
    "sample_data": {
        "用户ID": 12345,
        "姓名": "张三",
        "年龄": 28,
        "用户标签": "高价值用户"
    }
}

响应参数

成功响应:

参数名 类型 说明
code integer 状态码,200 表示成功
message string 响应消息
data object 响应数据
data.nodes array 节点列表
data.lines array 关系列表
data.lineage_depth integer 血缘追溯深度

节点对象 (node) 结构:

字段名 类型 说明
id integer 节点 ID(Neo4j 内部 ID)
name_zh string 节点中文名称
name_en string 节点英文名称
node_type string 节点类型,如 BusinessDomainDataFlowDataResource
labels array 节点标签列表
is_target boolean 是否为目标节点(起始查询节点)
is_source boolean 是否为源节点(数据资源,血缘追溯终点)
matched_fields array 匹配到的字段列表(仅 BusinessDomain 节点有此字段)

匹配字段对象 (matched_field) 结构:

字段名 类型 说明
field_name string 字段中文名称
field_name_en string 字段英文名称
data_type string 字段数据类型
value any 样例数据中对应的值
meta_id integer DataMeta 节点 ID

关系对象 (line) 结构:

字段名 类型 说明
from integer 起始节点 ID
to integer 目标节点 ID
type string 关系类型,INPUTOUTPUT

响应示例

成功响应:

{
    "code": 200,
    "message": "获取血缘可视化数据成功",
    "data": {
        "nodes": [
            {
                "id": 212,
                "name_zh": "用户标签库",
                "name_en": "user_tag_library",
                "node_type": "BusinessDomain",
                "labels": ["BusinessDomain"],
                "is_target": true,
                "is_source": false,
                "matched_fields": [
                    {
                        "field_name": "用户ID",
                        "field_name_en": "user_id",
                        "data_type": "integer",
                        "value": 12345,
                        "meta_id": 234
                    },
                    {
                        "field_name": "姓名",
                        "field_name_en": "name",
                        "data_type": "string",
                        "value": "张三",
                        "meta_id": 235
                    }
                ]
            },
            {
                "id": 183,
                "name_zh": "用户标签生成",
                "name_en": "user_tag_generate",
                "node_type": "DataFlow",
                "labels": ["DataFlow"],
                "is_target": false,
                "is_source": false
            },
            {
                "id": 159,
                "name_zh": "用户画像",
                "name_en": "user_profile",
                "node_type": "BusinessDomain",
                "labels": ["BusinessDomain"],
                "is_target": false,
                "is_source": false,
                "matched_fields": [
                    {
                        "field_name": "用户ID",
                        "field_name_en": "user_id",
                        "data_type": "integer",
                        "value": 12345,
                        "meta_id": 234
                    }
                ]
            },
            {
                "id": 156,
                "name_zh": "用户数据清洗",
                "name_en": "user_data_clean",
                "node_type": "DataFlow",
                "labels": ["DataFlow"],
                "is_target": false,
                "is_source": false
            },
            {
                "id": 154,
                "name_zh": "用户基础数据",
                "name_en": "user_base_info",
                "node_type": "DataResource",
                "labels": ["DataResource", "BusinessDomain"],
                "is_target": false,
                "is_source": true,
                "matched_fields": [
                    {
                        "field_name": "用户ID",
                        "field_name_en": "user_id",
                        "data_type": "integer",
                        "value": 12345,
                        "meta_id": 234
                    }
                ]
            }
        ],
        "lines": [
            {"from": 183, "to": 212, "type": "OUTPUT"},
            {"from": 159, "to": 183, "type": "INPUT"},
            {"from": 156, "to": 159, "type": "OUTPUT"},
            {"from": 154, "to": 156, "type": "INPUT"}
        ],
        "lineage_depth": 2
    }
}

错误响应:

{
    "code": 400,
    "message": "sample_data 必须是非空的 JSON 对象",
    "data": null
}
{
    "code": 404,
    "message": "数据产品不存在: ID=999",
    "data": null
}

数据结构说明

节点类型说明

节点类型 说明 图标建议
BusinessDomain 业务领域节点,表示一个数据表或业务实体 表格图标
DataFlow 数据流节点,表示一个 ETL 加工过程 流程图标
DataResource 数据资源节点,表示原始数据源 数据库图标

关系类型说明

关系类型 方向 说明
INPUT BusinessDomain → DataFlow 业务域作为数据流的输入
OUTPUT DataFlow → BusinessDomain 数据流输出到业务域

特殊标识说明

标识 说明 可视化建议
is_target = true 目标节点(用户查询的数据产品对应的节点) 高亮显示或置于图谱中心
is_source = true 源节点(数据资源,血缘追溯的终点) 使用不同颜色标识

错误码说明

HTTP 状态码 code message 说明
200 200 获取血缘可视化数据成功 请求成功
400 400 请求数据不能为空 未提供请求体
400 400 sample_data 必须是非空的 JSON 对象 sample_data 格式错误
404 404 数据产品不存在: ID=xxx 指定的数据产品不存在
500 500 获取血缘可视化数据失败: xxx 服务器内部错误

Vue 页面接入示例

1. API 服务封装

// api/dataService.js
import request from '@/utils/request'

/**
 * 获取数据产品血缘可视化数据
 * @param {number} productId - 数据产品ID
 * @param {object} sampleData - 样例数据
 * @returns {Promise}
 */
export function getLineageVisualization(productId, sampleData) {
  return request({
    url: `/api/dataservice/products/${productId}/lineage-visualization`,
    method: 'post',
    data: {
      sample_data: sampleData
    }
  })
}

2. Vue 组件示例

<template>
  <div class="lineage-visualization">
    <!-- 标题栏 -->
    <div class="header">
      <h3>数据加工可视化</h3>
      <el-button type="primary" @click="loadLineage" :loading="loading">
        刷新血缘图谱
      </el-button>
    </div>

    <!-- 图谱容器 -->
    <div class="graph-container" ref="graphContainer">
      <div v-if="loading" class="loading-mask">
        <el-icon class="is-loading"><Loading /></el-icon>
        <span>加载中...</span>
      </div>
      
      <div v-else-if="error" class="error-mask">
        <el-icon><WarningFilled /></el-icon>
        <span>{{ error }}</span>
      </div>
      
      <!-- 图谱将在这里渲染 -->
      <div id="lineage-graph" ref="graphRef"></div>
    </div>

    <!-- 节点详情面板 -->
    <el-drawer
      v-model="showNodeDetail"
      title="节点详情"
      :size="400"
    >
      <div v-if="selectedNode" class="node-detail">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="节点名称">
            {{ selectedNode.name_zh }}
          </el-descriptions-item>
          <el-descriptions-item label="英文名称">
            {{ selectedNode.name_en }}
          </el-descriptions-item>
          <el-descriptions-item label="节点类型">
            <el-tag :type="getNodeTypeTag(selectedNode.node_type)">
              {{ selectedNode.node_type }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="节点标签">
            <el-tag v-for="label in selectedNode.labels" :key="label" class="mr-2">
              {{ label }}
            </el-tag>
          </el-descriptions-item>
        </el-descriptions>

        <!-- 匹配字段 -->
        <div v-if="selectedNode.matched_fields?.length" class="matched-fields">
          <h4>匹配字段</h4>
          <el-table :data="selectedNode.matched_fields" stripe size="small">
            <el-table-column prop="field_name" label="字段名" />
            <el-table-column prop="data_type" label="类型" width="80" />
            <el-table-column prop="value" label="样例值" />
          </el-table>
        </div>
      </div>
    </el-drawer>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { Loading, WarningFilled } from '@element-plus/icons-vue'
import { getLineageVisualization } from '@/api/dataService'
// 可选:使用 G6 或 ECharts 等图形库渲染图谱
// import G6 from '@antv/g6'

const props = defineProps({
  productId: {
    type: Number,
    required: true
  },
  sampleData: {
    type: Object,
    default: () => ({})
  }
})

const loading = ref(false)
const error = ref('')
const graphRef = ref(null)
const nodes = ref([])
const lines = ref([])
const lineageDepth = ref(0)
const showNodeDetail = ref(false)
const selectedNode = ref(null)

// 加载血缘数据
const loadLineage = async () => {
  if (!props.productId) {
    ElMessage.warning('请先选择数据产品')
    return
  }

  if (!props.sampleData || Object.keys(props.sampleData).length === 0) {
    ElMessage.warning('请先选择一条样例数据')
    return
  }

  loading.value = true
  error.value = ''

  try {
    const res = await getLineageVisualization(props.productId, props.sampleData)
    
    if (res.code === 200) {
      nodes.value = res.data.nodes
      lines.value = res.data.lines
      lineageDepth.value = res.data.lineage_depth

      ElMessage.success(`成功加载血缘图谱,共 ${nodes.value.length} 个节点`)
      
      // 渲染图谱
      await nextTick()
      renderGraph()
    } else {
      error.value = res.message || '获取血缘数据失败'
      ElMessage.error(error.value)
    }
  } catch (err) {
    console.error('获取血缘数据失败:', err)
    error.value = err.message || '网络请求失败'
    ElMessage.error(error.value)
  } finally {
    loading.value = false
  }
}

// 渲染图谱(使用 G6 示例)
const renderGraph = () => {
  // 这里以 G6 为例,也可以使用 ECharts、D3.js 等
  // 需要先安装:npm install @antv/g6
  
  if (!graphRef.value) return

  // 转换数据格式为 G6 所需格式
  const graphData = {
    nodes: nodes.value.map(node => ({
      id: String(node.id),
      label: node.name_zh,
      nodeType: node.node_type,
      isSource: node.is_source,
      isTarget: node.is_target,
      // 原始数据
      _data: node
    })),
    edges: lines.value.map((line, index) => ({
      id: `edge-${index}`,
      source: String(line.from),
      target: String(line.to),
      label: line.type
    }))
  }

  // G6 图谱配置
  // const graph = new G6.Graph({
  //   container: graphRef.value,
  //   width: graphRef.value.offsetWidth,
  //   height: 500,
  //   layout: {
  //     type: 'dagre',
  //     rankdir: 'LR',
  //     nodesep: 50,
  //     ranksep: 100
  //   },
  //   defaultNode: {
  //     type: 'rect',
  //     size: [120, 40]
  //   },
  //   defaultEdge: {
  //     type: 'polyline',
  //     style: {
  //       endArrow: true
  //     }
  //   }
  // })
  //
  // graph.data(graphData)
  // graph.render()
  //
  // // 节点点击事件
  // graph.on('node:click', (evt) => {
  //   selectedNode.value = evt.item.getModel()._data
  //   showNodeDetail.value = true
  // })

  console.log('Graph data ready:', graphData)
}

// 获取节点类型对应的标签样式
const getNodeTypeTag = (nodeType) => {
  const typeMap = {
    'BusinessDomain': 'primary',
    'DataFlow': 'success',
    'DataResource': 'warning'
  }
  return typeMap[nodeType] || 'info'
}

// 组件挂载时自动加载
onMounted(() => {
  if (props.productId && Object.keys(props.sampleData).length > 0) {
    loadLineage()
  }
})

// 暴露方法供父组件调用
defineExpose({
  loadLineage
})
</script>

<style scoped>
.lineage-visualization {
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.graph-container {
  position: relative;
  min-height: 500px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  background: #fafafa;
}

.loading-mask,
.error-mask {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: rgba(255, 255, 255, 0.9);
  z-index: 10;
}

.loading-mask .el-icon {
  font-size: 32px;
  margin-bottom: 10px;
}

.error-mask {
  color: #f56c6c;
}

.error-mask .el-icon {
  font-size: 48px;
  margin-bottom: 10px;
}

#lineage-graph {
  width: 100%;
  height: 500px;
}

.node-detail {
  padding: 10px;
}

.matched-fields {
  margin-top: 20px;
}

.matched-fields h4 {
  margin-bottom: 10px;
  color: #606266;
}

.mr-2 {
  margin-right: 8px;
}
</style>

3. 父组件调用示例

<template>
  <div class="data-product-detail">
    <!-- 数据预览表格 -->
    <el-table
      :data="previewData"
      @row-click="handleRowClick"
      highlight-current-row
    >
      <el-table-column
        v-for="col in columns"
        :key="col.name"
        :prop="col.name"
        :label="col.name"
      />
    </el-table>

    <!-- 血缘可视化组件 -->
    <LineageVisualization
      ref="lineageRef"
      :product-id="productId"
      :sample-data="selectedRowData"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import LineageVisualization from './LineageVisualization.vue'

const productId = ref(123)
const previewData = ref([])
const columns = ref([])
const selectedRowData = ref({})
const lineageRef = ref(null)

// 表格行点击事件
const handleRowClick = (row) => {
  selectedRowData.value = row
  // 触发血缘图谱加载
  lineageRef.value?.loadLineage()
}
</script>

4. 使用 ECharts 渲染图谱(可选方案)

// 使用 ECharts 关系图渲染
import * as echarts from 'echarts'

const renderWithECharts = (container, nodes, lines) => {
  const chart = echarts.init(container)
  
  const option = {
    tooltip: {},
    series: [{
      type: 'graph',
      layout: 'force',
      symbolSize: 50,
      roam: true,
      label: {
        show: true
      },
      edgeSymbol: ['circle', 'arrow'],
      edgeSymbolSize: [4, 10],
      data: nodes.map(node => ({
        id: String(node.id),
        name: node.name_zh,
        category: node.node_type === 'DataFlow' ? 1 : 0,
        itemStyle: {
          color: node.is_source ? '#67C23A' : 
                 node.is_target ? '#409EFF' : '#909399'
        }
      })),
      links: lines.map(line => ({
        source: String(line.from),
        target: String(line.to),
        label: {
          show: true,
          formatter: line.type
        }
      })),
      categories: [
        { name: 'BusinessDomain' },
        { name: 'DataFlow' }
      ],
      force: {
        repulsion: 500
      }
    }]
  }
  
  chart.setOption(option)
  return chart
}

注意事项

  1. 样例数据格式sample_data 的 key 必须使用中文字段名,与 DataMeta 节点的 name_zh 匹配
  2. 节点 ID:返回的节点 ID 是 Neo4j 内部 ID,在构建图谱时需转为字符串
  3. 关系方向lines 数组中的 fromto 表示关系的起点和终点,需按箭头方向渲染
  4. 空结果处理:如果数据产品未关联 BusinessDomain 节点,返回空数组
  5. 性能考虑:血缘追溯最大深度为 10 层,避免无限循环

更新日志

版本 日期 更新内容
1.0.0 2025-12-30 初始版本,支持血缘追溯和字段匹配