|
|
@@ -0,0 +1,1120 @@
|
|
|
+# 数据服务 API 前端开发指南
|
|
|
+
|
|
|
+> **模块说明**: 数据服务 API 提供数据产品的列表查询、详情获取、数据预览、Excel 导出、状态管理等功能。
|
|
|
+>
|
|
|
+> **基础路径**: `/api/dataservice`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+- [通用说明](#通用说明)
|
|
|
+ - [响应格式](#响应格式)
|
|
|
+ - [错误码说明](#错误码说明)
|
|
|
+ - [Axios 配置](#axios-配置)
|
|
|
+- [接口列表](#接口列表)
|
|
|
+ 1. [获取数据产品列表](#1-获取数据产品列表)
|
|
|
+ 2. [获取数据产品详情](#2-获取数据产品详情)
|
|
|
+ 3. [获取数据预览](#3-获取数据预览)
|
|
|
+ 4. [下载 Excel 文件](#4-下载-excel-文件)
|
|
|
+ 5. [标记已查看](#5-标记已查看)
|
|
|
+ 6. [刷新统计信息](#6-刷新统计信息)
|
|
|
+ 7. [删除数据产品](#7-删除数据产品)
|
|
|
+ 8. [注册数据产品](#8-注册数据产品)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 通用说明
|
|
|
+
|
|
|
+### 响应格式
|
|
|
+
|
|
|
+所有接口返回统一的 JSON 格式:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "操作成功",
|
|
|
+ "data": { ... }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `code` | number | 状态码,200 表示成功,其他表示失败 |
|
|
|
+| `message` | string | 操作结果描述信息 |
|
|
|
+| `data` | object \| array \| null | 返回的数据内容 |
|
|
|
+
|
|
|
+### 错误码说明
|
|
|
+
|
|
|
+| 状态码 | 说明 | 常见场景 |
|
|
|
+|--------|------|----------|
|
|
|
+| 200 | 成功 | 操作成功完成 |
|
|
|
+| 400 | 请求参数错误 | 缺少必填字段、参数格式错误 |
|
|
|
+| 404 | 资源不存在 | 数据产品 ID 不存在 |
|
|
|
+| 500 | 服务器内部错误 | 数据库连接失败、SQL 执行异常 |
|
|
|
+
|
|
|
+### Axios 配置
|
|
|
+
|
|
|
+建议的 Axios 全局配置:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 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 | 否 | - | 状态过滤:`active`、`inactive`、`error` |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 | 是否有新数据未查看(用于更新提示) |
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```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 |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 404,
|
|
|
+ "message": "数据产品不存在",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 在 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 |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 | 本次预览返回的记录数 |
|
|
|
+
|
|
|
+#### 错误响应
|
|
|
+
|
|
|
+**目标表不存在时:**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取数据预览成功",
|
|
|
+ "data": {
|
|
|
+ "product": { ... },
|
|
|
+ "columns": [],
|
|
|
+ "data": [],
|
|
|
+ "total_count": 0,
|
|
|
+ "preview_count": 0,
|
|
|
+ "error": "目标表 public.dwd_talent_info 不存在"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```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 格式):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 404,
|
|
|
+ "message": "数据产品不存在: ID=999",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 方式 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('下载失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```vue
|
|
|
+<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 |
|
|
|
+
|
|
|
+#### 请求体
|
|
|
+
|
|
|
+无需请求体。
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "标记已查看成功",
|
|
|
+ "data": {
|
|
|
+ "id": 1,
|
|
|
+ "product_name": "人才数据产品",
|
|
|
+ "has_new_data": false,
|
|
|
+ "last_viewed_at": "2024-12-26T14:30:00"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+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 |
|
|
|
+
|
|
|
+#### 请求体
|
|
|
+
|
|
|
+无需请求体。
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 接入示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+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 |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "删除数据产品成功",
|
|
|
+ "data": {}
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 错误响应
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 404,
|
|
|
+ "message": "数据产品不存在",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+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" | 创建人标识 |
|
|
|
+
|
|
|
+#### 请求示例
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "product_name": "人才简历数据",
|
|
|
+ "product_name_en": "talent_resume_data",
|
|
|
+ "target_table": "dwd_talent_resume",
|
|
|
+ "target_schema": "public",
|
|
|
+ "description": "经过清洗处理的人才简历数据",
|
|
|
+ "source_dataflow_id": 15,
|
|
|
+ "source_dataflow_name": "简历数据清洗流程"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 400,
|
|
|
+ "message": "缺少必填字段: product_name",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**请求体为空 (400):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 400,
|
|
|
+ "message": "请求数据不能为空",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 接入示例
|
|
|
+
|
|
|
+```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 封装到独立模块:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 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 获取错误信息:
|
|
|
+
|
|
|
+```javascript
|
|
|
+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 文档 |
|
|
|
+
|