api_data_service_guide.md 27 KB

数据服务 API 前端开发指南

模块说明: 数据服务 API 提供数据产品的列表查询、详情获取、数据预览、Excel 导出、状态管理等功能。

基础路径: /api/dataservice


目录


通用说明

响应格式

所有接口返回统一的 JSON 格式:

{
  "code": 200,
  "message": "操作成功",
  "data": { ... }
}
字段 类型 说明
code number 状态码,200 表示成功,其他表示失败
message string 操作结果描述信息
data object | array | null 返回的数据内容

错误码说明

状态码 说明 常见场景
200 成功 操作成功完成
400 请求参数错误 缺少必填字段、参数格式错误
404 资源不存在 数据产品 ID 不存在
500 服务器内部错误 数据库连接失败、SQL 执行异常

Axios 配置

建议的 Axios 全局配置:

// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'

const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5050',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 响应拦截器
request.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res
  },
  error => {
    ElMessage.error(error.message || '网络错误')
    return Promise.reject(error)
  }
)

export default request

接口列表


1. 获取数据产品列表

分页获取数据产品列表,支持搜索和状态过滤。

请求信息

项目 说明
URL GET /api/dataservice/products
Method GET
Content-Type -

请求参数 (Query String)

参数名 类型 必填 默认值 说明
page integer 1 页码
page_size integer 20 每页数量
search string "" 搜索关键词(匹配名称、英文名、描述、表名)
status string - 状态过滤:activeinactiveerror

响应数据

{
  "code": 200,
  "message": "获取数据产品列表成功",
  "data": {
    "list": [
      {
        "id": 1,
        "product_name": "人才数据产品",
        "product_name_en": "talent_data",
        "description": "包含所有人才的基本信息",
        "source_dataflow_id": 10,
        "source_dataflow_name": "人才数据清洗流程",
        "target_table": "dwd_talent_info",
        "target_schema": "public",
        "record_count": 15890,
        "column_count": 25,
        "last_updated_at": "2024-12-26T10:30:00",
        "last_viewed_at": "2024-12-26T09:00:00",
        "status": "active",
        "created_at": "2024-12-01T08:00:00",
        "created_by": "system",
        "updated_at": "2024-12-26T10:30:00",
        "has_new_data": true
      }
    ],
    "pagination": {
      "page": 1,
      "page_size": 20,
      "total": 45,
      "total_pages": 3
    }
  }
}

数据产品字段说明

字段 类型 说明
id integer 数据产品唯一 ID
product_name string 产品中文名称
product_name_en string 产品英文名称
description string | null 产品描述
source_dataflow_id integer | null 关联的数据流 ID
source_dataflow_name string | null 关联的数据流名称
target_table string 目标数据表名
target_schema string 目标 schema,默认 "public"
record_count integer 数据记录数
column_count integer 数据列数
last_updated_at string | null 数据最后更新时间(ISO 8601 格式)
last_viewed_at string | null 最后查看时间
status string 状态:activeinactiveerror
created_at string 创建时间
created_by string 创建人
updated_at string 更新时间
has_new_data boolean 是否有新数据未查看(用于更新提示)

Vue 接入示例

<template>
  <div class="data-product-list">
    <!-- 搜索栏 -->
    <div class="search-bar">
      <el-input
        v-model="searchParams.search"
        placeholder="搜索产品名称..."
        @keyup.enter="fetchProducts"
        clearable
      />
      <el-select v-model="searchParams.status" placeholder="状态" clearable>
        <el-option label="活跃" value="active" />
        <el-option label="非活跃" value="inactive" />
        <el-option label="错误" value="error" />
      </el-select>
      <el-button type="primary" @click="fetchProducts">查询</el-button>
    </div>

    <!-- 数据表格 -->
    <el-table :data="productList" v-loading="loading">
      <el-table-column prop="product_name" label="产品名称">
        <template #default="{ row }">
          <span>{{ row.product_name }}</span>
          <el-tag v-if="row.has_new_data" type="danger" size="small">新</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="target_table" label="目标表" />
      <el-table-column prop="record_count" label="记录数" />
      <el-table-column prop="status" label="状态">
        <template #default="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200">
        <template #default="{ row }">
          <el-button size="small" @click="handlePreview(row.id)">预览</el-button>
          <el-button size="small" type="success" @click="handleDownload(row.id)">
            下载
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <el-pagination
      v-model:current-page="searchParams.page"
      v-model:page-size="searchParams.page_size"
      :total="pagination.total"
      @current-change="fetchProducts"
    />
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import request from '@/utils/request'

const loading = ref(false)
const productList = ref([])
const pagination = ref({})
const searchParams = reactive({
  page: 1,
  page_size: 20,
  search: '',
  status: ''
})

const fetchProducts = async () => {
  loading.value = true
  try {
    const res = await request.get('/api/dataservice/products', {
      params: searchParams
    })
    productList.value = res.data.list
    pagination.value = res.data.pagination
  } finally {
    loading.value = false
  }
}

const getStatusType = (status) => {
  const types = { active: 'success', inactive: 'info', error: 'danger' }
  return types[status] || 'info'
}

onMounted(() => {
  fetchProducts()
})
</script>

2. 获取数据产品详情

根据 ID 获取单个数据产品的详细信息。

请求信息

项目 说明
URL GET /api/dataservice/products/{product_id}
Method GET
Content-Type -

路径参数

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

响应数据

{
  "code": 200,
  "message": "获取数据产品详情成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "product_name_en": "talent_data",
    "description": "包含所有人才的基本信息",
    "source_dataflow_id": 10,
    "source_dataflow_name": "人才数据清洗流程",
    "target_table": "dwd_talent_info",
    "target_schema": "public",
    "record_count": 15890,
    "column_count": 25,
    "last_updated_at": "2024-12-26T10:30:00",
    "last_viewed_at": "2024-12-26T09:00:00",
    "status": "active",
    "created_at": "2024-12-01T08:00:00",
    "created_by": "system",
    "updated_at": "2024-12-26T10:30:00",
    "has_new_data": true
  }
}

错误响应

产品不存在 (404):

{
  "code": 404,
  "message": "数据产品不存在",
  "data": null
}

Vue 接入示例

// 在 API 模块中定义
export const dataProductApi = {
  // 获取产品详情
  getDetail(productId) {
    return request.get(`/api/dataservice/products/${productId}`)
  }
}

// 在组件中使用
const fetchProductDetail = async (productId) => {
  try {
    const res = await dataProductApi.getDetail(productId)
    productDetail.value = res.data
  } catch (error) {
    console.error('获取产品详情失败:', error)
  }
}

3. 获取数据预览

获取数据产品的数据预览,支持自定义预览条数(默认 200 条,最大 1000 条)。

注意: 调用此接口会自动将产品标记为已查看。

请求信息

项目 说明
URL GET /api/dataservice/products/{product_id}/preview
Method GET
Content-Type -

路径参数

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

请求参数 (Query String)

参数名 类型 必填 默认值 说明
limit integer 200 预览数据条数,最大 1000

响应数据

{
  "code": 200,
  "message": "获取数据预览成功",
  "data": {
    "product": {
      "id": 1,
      "product_name": "人才数据产品",
      "product_name_en": "talent_data",
      "target_table": "dwd_talent_info",
      "target_schema": "public",
      "record_count": 15890,
      "column_count": 25,
      "status": "active",
      "has_new_data": false
    },
    "columns": [
      { "name": "id", "type": "integer", "nullable": false },
      { "name": "name", "type": "character varying", "nullable": false },
      { "name": "email", "type": "character varying", "nullable": true },
      { "name": "created_at", "type": "timestamp without time zone", "nullable": false }
    ],
    "data": [
      { "id": 1, "name": "张三", "email": "zhangsan@example.com", "created_at": "2024-01-15T08:30:00" },
      { "id": 2, "name": "李四", "email": "lisi@example.com", "created_at": "2024-01-16T09:45:00" }
    ],
    "total_count": 15890,
    "preview_count": 200
  }
}

响应字段说明

字段 类型 说明
product object 数据产品基本信息
columns array 列定义数组
columns[].name string 列名
columns[].type string 数据类型
columns[].nullable boolean 是否允许为空
data array 数据行数组
total_count integer 目标表总记录数
preview_count integer 本次预览返回的记录数

错误响应

目标表不存在时:

{
  "code": 200,
  "message": "获取数据预览成功",
  "data": {
    "product": { ... },
    "columns": [],
    "data": [],
    "total_count": 0,
    "preview_count": 0,
    "error": "目标表 public.dwd_talent_info 不存在"
  }
}

Vue 接入示例

<template>
  <div class="data-preview">
    <!-- 产品信息卡片 -->
    <el-card class="product-info">
      <template #header>
        <span>{{ previewData.product?.product_name }}</span>
        <el-tag>{{ previewData.total_count }} 条记录</el-tag>
      </template>
      <p>目标表: {{ previewData.product?.target_schema }}.{{ previewData.product?.target_table }}</p>
    </el-card>

    <!-- 数据预览表格 -->
    <el-table :data="previewData.data" v-loading="loading" border>
      <el-table-column
        v-for="col in previewData.columns"
        :key="col.name"
        :prop="col.name"
        :label="col.name"
      >
        <template #header>
          <div>
            <span>{{ col.name }}</span>
            <br />
            <small style="color: #909399">{{ col.type }}</small>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <!-- 预览条数控制 -->
    <div class="preview-controls">
      <span>已加载 {{ previewData.preview_count }} / {{ previewData.total_count }} 条</span>
      <el-button
        v-if="previewData.preview_count < previewData.total_count"
        @click="loadMore"
      >
        加载更多
      </el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import request from '@/utils/request'

const props = defineProps({
  productId: { type: Number, required: true }
})

const loading = ref(false)
const previewData = ref({
  product: null,
  columns: [],
  data: [],
  total_count: 0,
  preview_count: 0
})
const limit = ref(200)

const fetchPreview = async () => {
  loading.value = true
  try {
    const res = await request.get(
      `/api/dataservice/products/${props.productId}/preview`,
      { params: { limit: limit.value } }
    )
    previewData.value = res.data
  } finally {
    loading.value = false
  }
}

const loadMore = () => {
  limit.value = Math.min(limit.value + 200, 1000)
  fetchPreview()
}

onMounted(() => {
  fetchPreview()
})
</script>

4. 下载 Excel 文件

下载数据产品数据为 Excel 文件。

注意: 调用此接口会自动将产品标记为已查看。

请求信息

项目 说明
URL GET /api/dataservice/products/{product_id}/download
Method GET
Content-Type -

路径参数

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

请求参数 (Query String)

参数名 类型 必填 默认值 说明
limit integer 200 导出数据条数,最大 10000

响应数据

成功: 返回 Excel 文件(MIME 类型: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

文件名格式: {product_name_en}_{YYYYMMDD_HHMMSS}.xlsx

例如: talent_data_20241226_103000.xlsx

错误响应

产品或表不存在 (JSON 格式):

{
  "code": 404,
  "message": "数据产品不存在: ID=999",
  "data": null
}

Vue 接入示例

// 方式 1: 使用 a 标签下载(推荐简单场景)
const downloadExcel = (productId, limit = 200) => {
  const baseUrl = process.env.VUE_APP_API_BASE_URL || 'http://localhost:5050'
  const url = `${baseUrl}/api/dataservice/products/${productId}/download?limit=${limit}`
  window.open(url, '_blank')
}

// 方式 2: 使用 axios 下载(支持进度、Token 验证)
const downloadExcelWithAxios = async (productId, limit = 200) => {
  try {
    const response = await request.get(
      `/api/dataservice/products/${productId}/download`,
      {
        params: { limit },
        responseType: 'blob'
      }
    )

    // 从响应头获取文件名
    const contentDisposition = response.headers['content-disposition']
    let filename = 'download.xlsx'
    if (contentDisposition) {
      const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
      if (filenameMatch) {
        filename = filenameMatch[1].replace(/['"]/g, '')
      }
    }

    // 创建下载链接
    const blob = new Blob([response.data], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    })
    const url = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    link.click()
    window.URL.revokeObjectURL(url)
  } catch (error) {
    ElMessage.error('下载失败')
  }
}
<template>
  <div class="download-section">
    <el-input-number v-model="downloadLimit" :min="1" :max="10000" />
    <el-button type="primary" @click="handleDownload" :loading="downloading">
      下载 Excel
    </el-button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  productId: { type: Number, required: true }
})

const downloadLimit = ref(200)
const downloading = ref(false)

const handleDownload = async () => {
  downloading.value = true
  try {
    await downloadExcelWithAxios(props.productId, downloadLimit.value)
  } finally {
    downloading.value = false
  }
}
</script>

5. 标记已查看

手动标记数据产品为已查看,消除更新提示(红点)。

请求信息

项目 说明
URL POST /api/dataservice/products/{product_id}/viewed
Method POST
Content-Type application/json

路径参数

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

请求体

无需请求体。

响应数据

{
  "code": 200,
  "message": "标记已查看成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "has_new_data": false,
    "last_viewed_at": "2024-12-26T14:30:00"
  }
}

Vue 接入示例

const markAsViewed = async (productId) => {
  try {
    await request.post(`/api/dataservice/products/${productId}/viewed`)
    // 更新本地数据状态
    const product = productList.value.find(p => p.id === productId)
    if (product) {
      product.has_new_data = false
    }
  } catch (error) {
    console.error('标记失败:', error)
  }
}

6. 刷新统计信息

刷新数据产品的统计信息(从目标表重新统计记录数和列数)。

请求信息

项目 说明
URL POST /api/dataservice/products/{product_id}/refresh
Method POST
Content-Type application/json

路径参数

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

请求体

无需请求体。

响应数据

{
  "code": 200,
  "message": "刷新统计信息成功",
  "data": {
    "id": 1,
    "product_name": "人才数据产品",
    "record_count": 16500,
    "column_count": 25,
    "status": "active",
    "last_updated_at": "2024-12-26T14:35:00"
  }
}

说明: 如果目标表不存在,status 会更新为 error

Vue 接入示例

const refreshStats = async (productId) => {
  try {
    const res = await request.post(`/api/dataservice/products/${productId}/refresh`)
    ElMessage.success(`刷新成功,共 ${res.data.record_count} 条记录`)
    // 更新本地数据
    const index = productList.value.findIndex(p => p.id === productId)
    if (index !== -1) {
      productList.value[index] = res.data
    }
  } catch (error) {
    ElMessage.error('刷新失败')
  }
}

7. 删除数据产品

删除数据产品记录(仅删除注册信息,不删除实际数据表)。

请求信息

项目 说明
URL DELETE /api/dataservice/products/{product_id}
Method DELETE
Content-Type -

路径参数

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

响应数据

{
  "code": 200,
  "message": "删除数据产品成功",
  "data": {}
}

错误响应

{
  "code": 404,
  "message": "数据产品不存在",
  "data": null
}

Vue 接入示例

const deleteProduct = async (productId) => {
  try {
    await ElMessageBox.confirm('确定要删除该数据产品吗?此操作不可恢复。', '删除确认', {
      type: 'warning'
    })

    await request.delete(`/api/dataservice/products/${productId}`)
    ElMessage.success('删除成功')

    // 从本地列表移除
    productList.value = productList.value.filter(p => p.id !== productId)
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}

8. 注册数据产品

手动注册新的数据产品。如果目标表已存在对应产品,则更新现有记录。

请求信息

项目 说明
URL POST /api/dataservice/products
Method POST
Content-Type application/json

请求体参数

参数名 类型 必填 默认值 说明
product_name string - 产品中文名称
product_name_en string - 产品英文名称
target_table string - 目标数据表名
target_schema string "public" 目标 schema
description string null 产品描述
source_dataflow_id integer null 关联的数据流 ID
source_dataflow_name string null 关联的数据流名称
created_by string "manual" 创建人标识

请求示例

{
  "product_name": "人才简历数据",
  "product_name_en": "talent_resume_data",
  "target_table": "dwd_talent_resume",
  "target_schema": "public",
  "description": "经过清洗处理的人才简历数据",
  "source_dataflow_id": 15,
  "source_dataflow_name": "简历数据清洗流程"
}

响应数据

{
  "code": 200,
  "message": "注册数据产品成功",
  "data": {
    "id": 5,
    "product_name": "人才简历数据",
    "product_name_en": "talent_resume_data",
    "target_table": "dwd_talent_resume",
    "target_schema": "public",
    "description": "经过清洗处理的人才简历数据",
    "source_dataflow_id": 15,
    "source_dataflow_name": "简历数据清洗流程",
    "record_count": 0,
    "column_count": 0,
    "status": "active",
    "created_at": "2024-12-26T15:00:00",
    "created_by": "manual",
    "has_new_data": false
  }
}

错误响应

缺少必填字段 (400):

{
  "code": 400,
  "message": "缺少必填字段: product_name",
  "data": null
}

请求体为空 (400):

{
  "code": 400,
  "message": "请求数据不能为空",
  "data": null
}

Vue 接入示例

<template>
  <el-dialog v-model="dialogVisible" title="注册数据产品">
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <el-form-item label="产品名称" prop="product_name">
        <el-input v-model="form.product_name" placeholder="请输入中文名称" />
      </el-form-item>
      <el-form-item label="英文名称" prop="product_name_en">
        <el-input v-model="form.product_name_en" placeholder="请输入英文名称" />
      </el-form-item>
      <el-form-item label="目标表名" prop="target_table">
        <el-input v-model="form.target_table" placeholder="例如: dwd_talent_info" />
      </el-form-item>
      <el-form-item label="Schema">
        <el-input v-model="form.target_schema" placeholder="默认 public" />
      </el-form-item>
      <el-form-item label="描述">
        <el-input v-model="form.description" type="textarea" :rows="3" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handleSubmit" :loading="submitting">
        确定
      </el-button>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref, reactive } from 'vue'
import request from '@/utils/request'

const dialogVisible = ref(false)
const formRef = ref(null)
const submitting = ref(false)

const form = reactive({
  product_name: '',
  product_name_en: '',
  target_table: '',
  target_schema: 'public',
  description: ''
})

const rules = {
  product_name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
  product_name_en: [{ required: true, message: '请输入英文名称', trigger: 'blur' }],
  target_table: [{ required: true, message: '请输入目标表名', trigger: 'blur' }]
}

const handleSubmit = async () => {
  const valid = await formRef.value.validate()
  if (!valid) return

  submitting.value = true
  try {
    const res = await request.post('/api/dataservice/products', form)
    ElMessage.success('注册成功')
    dialogVisible.value = false
    emit('success', res.data)
  } catch (error) {
    ElMessage.error(error.message || '注册失败')
  } finally {
    submitting.value = false
  }
}

const emit = defineEmits(['success'])
</script>

API 模块封装示例

建议将所有数据服务 API 封装到独立模块:

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

const BASE_URL = '/api/dataservice'

export const dataServiceApi = {
  /**
   * 获取数据产品列表
   */
  getProducts(params) {
    return request.get(`${BASE_URL}/products`, { params })
  },

  /**
   * 获取数据产品详情
   */
  getProductDetail(productId) {
    return request.get(`${BASE_URL}/products/${productId}`)
  },

  /**
   * 获取数据预览
   */
  getProductPreview(productId, limit = 200) {
    return request.get(`${BASE_URL}/products/${productId}/preview`, {
      params: { limit }
    })
  },

  /**
   * 下载 Excel (返回 Blob)
   */
  downloadExcel(productId, limit = 200) {
    return request.get(`${BASE_URL}/products/${productId}/download`, {
      params: { limit },
      responseType: 'blob'
    })
  },

  /**
   * 获取下载链接
   */
  getDownloadUrl(productId, limit = 200) {
    const baseUrl = process.env.VUE_APP_API_BASE_URL || ''
    return `${baseUrl}${BASE_URL}/products/${productId}/download?limit=${limit}`
  },

  /**
   * 标记为已查看
   */
  markAsViewed(productId) {
    return request.post(`${BASE_URL}/products/${productId}/viewed`)
  },

  /**
   * 刷新统计信息
   */
  refreshStats(productId) {
    return request.post(`${BASE_URL}/products/${productId}/refresh`)
  },

  /**
   * 删除数据产品
   */
  deleteProduct(productId) {
    return request.delete(`${BASE_URL}/products/${productId}`)
  },

  /**
   * 注册数据产品
   */
  registerProduct(data) {
    return request.post(`${BASE_URL}/products`, data)
  }
}

export default dataServiceApi

常见问题

Q1: 数据预览加载很慢怎么办?

A: 减少 limit 参数值,默认 200 条已足够预览。对于大表,避免使用 1000 条限制。

Q2: Excel 下载失败,返回 JSON 错误信息?

A: 检查响应的 Content-Type,如果是 application/json,说明发生了错误。需要解析 JSON 获取错误信息:

const response = await request.get(url, { responseType: 'blob' })
if (response.data.type === 'application/json') {
  const text = await response.data.text()
  const error = JSON.parse(text)
  ElMessage.error(error.message)
}

Q3: has_new_data 什么时候会变为 true?

A: 当数据产品的 last_updated_at 大于 last_viewed_at 时,表示有新数据更新但用户尚未查看。调用预览、下载或标记已查看接口后会自动重置。

Q4: 如何处理跨域问题?

A: 后端已配置 CORS,支持任意 Origin。如遇问题,检查请求头是否正确设置 Content-Type


更新日志

版本 日期 说明
1.0.0 2024-12-26 初始版本,包含完整的 API 文档