本文档为前端开发人员提供数据加工可视化(血缘追溯)功能的 API 接口说明。
数据加工可视化功能用于展示数据产品的完整血缘关系图谱。当用户查看某个数据产品的数据样例时,前端发送一条样例数据,后端会:
[DataResource] --INPUT--> [DataFlow] --OUTPUT--> [BusinessDomain] --INPUT--> [DataFlow] --OUTPUT--> [目标节点]
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 为对应的值 |
请求示例:
{
"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 |
成功响应:
{
"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 | 服务器内部错误 |
// 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
}
})
}
<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>
<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>
// 使用 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
}
sample_data 的 key 必须使用中文字段名,与 DataMeta 节点的 name_zh 匹配lines 数组中的 from 和 to 表示关系的起点和终点,需按箭头方向渲染| 版本 | 日期 | 更新内容 |
|---|---|---|
| 1.0.0 | 2025-12-30 | 初始版本,支持血缘追溯和字段匹配 |