模块说明: 数据服务 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 全局配置:
// 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
分页获取数据产品列表,支持搜索和状态过滤。
| 项目 | 说明 |
|---|---|
| URL | GET /api/dataservice/products |
| Method | GET |
| Content-Type | - |
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
page |
integer | 否 | 1 | 页码 |
page_size |
integer | 否 | 20 | 每页数量 |
search |
string | 否 | "" | 搜索关键词(匹配名称、英文名、描述、表名) |
status |
string | 否 | - | 状态过滤:active、inactive、error |
{
"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 | 状态:active、inactive、error |
created_at |
string | 创建时间 |
created_by |
string | 创建人 |
updated_at |
string | 更新时间 |
has_new_data |
boolean | 是否有新数据未查看(用于更新提示) |
<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>
根据 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
}
// 在 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)
}
}
获取数据产品的数据预览,支持自定义预览条数(默认 200 条,最大 1000 条)。
注意: 调用此接口会自动将产品标记为已查看。
| 项目 | 说明 |
|---|---|
| URL | GET /api/dataservice/products/{product_id}/preview |
| Method | GET |
| Content-Type | - |
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
product_id |
integer | 是 | 数据产品 ID |
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
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 不存在"
}
}
<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>
下载数据产品数据为 Excel 文件。
注意: 调用此接口会自动将产品标记为已查看。
| 项目 | 说明 |
|---|---|
| URL | GET /api/dataservice/products/{product_id}/download |
| Method | GET |
| Content-Type | - |
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
product_id |
integer | 是 | 数据产品 ID |
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
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
}
// 方式 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>
手动标记数据产品为已查看,消除更新提示(红点)。
| 项目 | 说明 |
|---|---|
| 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"
}
}
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)
}
}
刷新数据产品的统计信息(从目标表重新统计记录数和列数)。
| 项目 | 说明 |
|---|---|
| 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。
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('刷新失败')
}
}
删除数据产品记录(仅删除注册信息,不删除实际数据表)。
| 项目 | 说明 |
|---|---|
| URL | DELETE /api/dataservice/products/{product_id} |
| Method | DELETE |
| Content-Type | - |
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
product_id |
integer | 是 | 数据产品 ID |
{
"code": 200,
"message": "删除数据产品成功",
"data": {}
}
{
"code": 404,
"message": "数据产品不存在",
"data": null
}
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('删除失败')
}
}
}
手动注册新的数据产品。如果目标表已存在对应产品,则更新现有记录。
| 项目 | 说明 |
|---|---|
| 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
}
<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 封装到独立模块:
// 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
A: 减少 limit 参数值,默认 200 条已足够预览。对于大表,避免使用 1000 条限制。
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)
}
has_new_data 什么时候会变为 true?A: 当数据产品的 last_updated_at 大于 last_viewed_at 时,表示有新数据更新但用户尚未查看。调用预览、下载或标记已查看接口后会自动重置。
A: 后端已配置 CORS,支持任意 Origin。如遇问题,检查请求头是否正确设置 Content-Type。
| 版本 | 日期 | 说明 |
|---|---|---|
| 1.0.0 | 2024-12-26 | 初始版本,包含完整的 API 文档 |