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

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

## 目录

- [功能概述](#功能概述)
- [接口详情](#接口详情)
  - [获取血缘可视化数据](#获取血缘可视化数据)
- [数据结构说明](#数据结构说明)
- [错误码说明](#错误码说明)
- [Vue 页面接入示例](#vue-页面接入示例)

---

## 功能概述

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

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

### 血缘关系模型

```
[DataResource] --INPUT--> [DataFlow] --OUTPUT--> [BusinessDomain] --INPUT--> [DataFlow] --OUTPUT--> [目标节点]
```

- **INPUT 关系**: BusinessDomain 作为 DataFlow 的输入源
- **OUTPUT 关系**: DataFlow 输出到 BusinessDomain
- **终止条件**: 节点同时具有 `BusinessDomain` 和 `DataResource` 两个标签

---

## 接口详情

### 获取血缘可视化数据

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

**请求 URL：** `POST /api/dataservice/products/{product_id}/lineage-visualization`

**请求方式：** `POST`

**Content-Type：** `application/json`

#### 请求参数

**路径参数：**

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

**请求体参数：**

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

**请求示例：**

```json
{
    "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 | 节点类型，如 `BusinessDomain`、`DataFlow`、`DataResource` |
| 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 | 关系类型，`INPUT` 或 `OUTPUT` |

#### 响应示例

**成功响应：**

```json
{
    "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
    }
}
```

**错误响应：**

```json
{
    "code": 400,
    "message": "sample_data 必须是非空的 JSON 对象",
    "data": null
}
```

```json
{
    "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 服务封装

```javascript
// 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 组件示例

```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. 父组件调用示例

```vue
<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 渲染图谱（可选方案）

```javascript
// 使用 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` 数组中的 `from` 和 `to` 表示关系的起点和终点，需按箭头方向渲染
4. **空结果处理**：如果数据产品未关联 BusinessDomain 节点，返回空数组
5. **性能考虑**：血缘追溯最大深度为 10 层，避免无限循环

---

## 更新日志

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

