api_review_resolve_guide.md 21 KB

元数据审核处理接口 API 前端开发指南

本文档面向前端开发人员,用于开发"元数据审核处理"功能。

接口基本信息

属性
接口路径 POST /api/meta/review/resolve
完整URL http://{host}:{port}/api/meta/review/resolve
Content-Type application/json
请求方式 POST

统一返回格式

成功响应

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "id": 123,
    "record_type": "redundancy",
    "status": "resolved",
    "resolution_action": "alias",
    "resolution_payload": {
      "primary_meta_id": 100,
      "alias_meta_id": 200
    },
    "resolved_by": "admin",
    "resolved_at": "2025-01-09T10:30:00.000000",
    "notes": "合并重复元数据"
  }
}

失败响应

{
  "code": 500,
  "message": "错误原因",
  "data": null,
  "error": "详细错误信息(可选)"
}

请求参数

公共参数

字段 类型 必填 说明
id int 审核记录ID
action string 处理动作,可选值见下方说明
payload object 动作参数,根据action不同而变化
resolved_by string 处理人标识(建议传登录用户名/工号)
notes string 处理备注

action 可选值

action 值 用途 适用场景
alias 设置元数据别名关系 redundancy(疑似冗余)
create_new 创建新元数据 redundancy(疑似冗余)
accept_change 接受元数据变动 change(疑似变动)
reject_change 拒绝元数据变动 change(疑似变动)
ignore 忽略该记录 任意类型

action=alias(设置元数据别名关系)

功能说明

在 Neo4j 的 DataMeta 节点之间建立 ALIAS 关系,用于合并重复/相似的元数据。

核心行为

  • 创建 (alias_meta)-[:ALIAS]->(primary_meta) 关系
  • 将所有原先指向 alias_meta 的 ALIAS 关系转移到 primary_meta
  • primary_meta 已有的 ALIAS 关系保持不变
  • BusinessDomain 的 INCLUDES 关系不受影响

    操作前:
    [other_alias_1] --ALIAS--> [alias_meta]
    [other_alias_2] --ALIAS--> [alias_meta]
    [existing_alias] --ALIAS--> [primary_meta]
    
    操作后:
    [other_alias_1] --ALIAS--> [primary_meta]
    [other_alias_2] --ALIAS--> [primary_meta]
    [alias_meta] --ALIAS--> [primary_meta]
    [existing_alias] --ALIAS--> [primary_meta]
    

payload 参数

字段 类型 必填 说明
primary_meta_id int 主元数据 Neo4j ID(作为别名目标)
alias_meta_id int 别名元数据 Neo4j ID(将成为别名)

请求示例

{
  "id": 1001,
  "action": "alias",
  "payload": {
    "primary_meta_id": 100,
    "alias_meta_id": 200
  },
  "resolved_by": "admin",
  "notes": "将重复元数据合并为别名"
}

响应示例

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "id": 1001,
    "record_type": "redundancy",
    "business_domain_id": 345,
    "new_meta": {
      "name_zh": "科室名称",
      "name_en": "DEPT_NAME",
      "data_type": "varchar(50)"
    },
    "candidates": [...],
    "status": "resolved",
    "resolution_action": "alias",
    "resolution_payload": {
      "primary_meta_id": 100,
      "alias_meta_id": 200
    },
    "resolved_by": "admin",
    "resolved_at": "2025-01-09T10:30:00.000000",
    "notes": "将重复元数据合并为别名",
    "created_at": "2025-01-08T09:00:00.000000",
    "updated_at": "2025-01-09T10:30:00.000000"
  }
}

错误信息

错误信息 原因
payload.primary_meta_id 不能为空 未提供 primary_meta_id
payload.alias_meta_id 不能为空 未提供 alias_meta_id
primary_meta_id 和 alias_meta_id 不能相同 两个ID相同

action=create_new(创建新元数据)

payload 参数

字段 类型 必填 说明
new_name_zh string 新元数据中文名(需与现有区分)

请求示例

{
  "id": 1002,
  "action": "create_new",
  "payload": {
    "new_name_zh": "HIS科室名称(新)"
  },
  "resolved_by": "admin"
}

错误信息

错误信息 原因
记录缺少 business_domain_id,无法执行 create_new 审核记录无业务领域关联
payload.new_name_zh 不能为空 未提供中文名
创建新元数据失败 Neo4j 创建节点失败

action=accept_change(接受变动)

payload 参数

字段 类型 必填 说明
meta_id int 目标元数据ID,不传时使用 old_meta.meta_id

请求示例

{
  "id": 2001,
  "action": "accept_change",
  "payload": {
    "meta_id": 789
  },
  "resolved_by": "admin",
  "notes": "接受字段长度调整"
}

错误信息

错误信息 原因
无法确定需要更新的 meta_id 未提供且记录中无 old_meta.meta_id

action=reject_change(拒绝变动)

请求示例

{
  "id": 2002,
  "action": "reject_change",
  "resolved_by": "admin",
  "notes": "变动不合规,暂不更新"
}

action=ignore(忽略)

请求示例

{
  "id": 3001,
  "action": "ignore",
  "resolved_by": "admin"
}

公共错误信息

错误信息 原因
请求数据格式错误,应为 JSON 对象 请求体不是有效的 JSON 对象
id 不能为空 未提供审核记录ID
action 不能为空 未提供处理动作
记录不存在 审核记录ID不存在
记录已处理,无法重复处理 审核记录状态非 pending
不支持的action: xxx action 值不在允许列表中
处理审核记录失败 服务器内部错误

Vue 示例代码

1. API 封装

// api/metaReview.js
import axios from 'axios'

const API_BASE = '/api/meta'

/**
 * 处理审核记录
 * @param {Object} params - 请求参数
 * @param {number} params.id - 审核记录ID
 * @param {string} params.action - 处理动作
 * @param {Object} params.payload - 动作参数
 * @param {string} params.resolved_by - 处理人
 * @param {string} params.notes - 备注
 * @returns {Promise}
 */
export function resolveReviewRecord(params) {
  return axios.post(`${API_BASE}/review/resolve`, params)
}

/**
 * 设置元数据别名关系
 * @param {number} recordId - 审核记录ID
 * @param {number} primaryMetaId - 主元数据ID
 * @param {number} aliasMetaId - 别名元数据ID
 * @param {string} resolvedBy - 处理人
 * @param {string} notes - 备注
 * @returns {Promise}
 */
export function setMetaAlias(recordId, primaryMetaId, aliasMetaId, resolvedBy, notes = '') {
  return resolveReviewRecord({
    id: recordId,
    action: 'alias',
    payload: {
      primary_meta_id: primaryMetaId,
      alias_meta_id: aliasMetaId
    },
    resolved_by: resolvedBy,
    notes
  })
}

/**
 * 创建新元数据
 * @param {number} recordId - 审核记录ID
 * @param {string} newNameZh - 新元数据中文名
 * @param {string} resolvedBy - 处理人
 * @returns {Promise}
 */
export function createNewMeta(recordId, newNameZh, resolvedBy) {
  return resolveReviewRecord({
    id: recordId,
    action: 'create_new',
    payload: {
      new_name_zh: newNameZh
    },
    resolved_by: resolvedBy
  })
}

/**
 * 接受元数据变动
 * @param {number} recordId - 审核记录ID
 * @param {number} metaId - 目标元数据ID(可选)
 * @param {string} resolvedBy - 处理人
 * @param {string} notes - 备注
 * @returns {Promise}
 */
export function acceptChange(recordId, metaId, resolvedBy, notes = '') {
  const payload = metaId ? { meta_id: metaId } : {}
  return resolveReviewRecord({
    id: recordId,
    action: 'accept_change',
    payload,
    resolved_by: resolvedBy,
    notes
  })
}

/**
 * 拒绝元数据变动
 * @param {number} recordId - 审核记录ID
 * @param {string} resolvedBy - 处理人
 * @param {string} notes - 备注
 * @returns {Promise}
 */
export function rejectChange(recordId, resolvedBy, notes = '') {
  return resolveReviewRecord({
    id: recordId,
    action: 'reject_change',
    resolved_by: resolvedBy,
    notes
  })
}

/**
 * 忽略审核记录
 * @param {number} recordId - 审核记录ID
 * @param {string} resolvedBy - 处理人
 * @returns {Promise}
 */
export function ignoreRecord(recordId, resolvedBy) {
  return resolveReviewRecord({
    id: recordId,
    action: 'ignore',
    resolved_by: resolvedBy
  })
}

2. 设置别名组件

<template>
  <div class="alias-dialog">
    <el-dialog
      title="设置元数据别名"
      :visible.sync="dialogVisible"
      width="600px"
      @close="handleClose"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="120px"
      >
        <el-form-item label="主元数据" prop="primaryMetaId">
          <el-select
            v-model="form.primaryMetaId"
            placeholder="请选择主元数据"
            filterable
            style="width: 100%"
          >
            <el-option
              v-for="item in candidateList"
              :key="item.candidate_meta_id"
              :label="`${item.name_zh} (${item.name_en || '-'})`"
              :value="item.candidate_meta_id"
            />
          </el-select>
          <div class="form-tip">选择作为主元数据的节点,其他节点将成为它的别名</div>
        </el-form-item>

        <el-form-item label="别名元数据" prop="aliasMetaId">
          <el-select
            v-model="form.aliasMetaId"
            placeholder="请选择别名元数据"
            filterable
            style="width: 100%"
          >
            <el-option
              v-for="item in candidateList"
              :key="item.candidate_meta_id"
              :label="`${item.name_zh} (${item.name_en || '-'})`"
              :value="item.candidate_meta_id"
              :disabled="item.candidate_meta_id === form.primaryMetaId"
            />
          </el-select>
          <div class="form-tip">选择要降级为别名的元数据节点</div>
        </el-form-item>

        <el-form-item label="备注">
          <el-input
            v-model="form.notes"
            type="textarea"
            :rows="3"
            placeholder="请输入处理备注(可选)"
          />
        </el-form-item>
      </el-form>

      <el-alert
        v-if="form.aliasMetaId"
        type="warning"
        :closable="false"
        style="margin-top: 16px"
      >
        <template #title>
          <span>操作提示</span>
        </template>
        <div>
          <p>执行此操作后:</p>
          <ul>
            <li>别名元数据将指向主元数据</li>
            <li>原先指向别名元数据的所有 ALIAS 关系将转移到主元数据</li>
            <li>业务领域的 INCLUDES 关系不受影响</li>
          </ul>
        </div>
      </el-alert>

      <template #footer>
        <el-button @click="handleClose">取消</el-button>
        <el-button
          type="primary"
          :loading="loading"
          @click="handleSubmit"
        >
          确定
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
import { setMetaAlias } from '@/api/metaReview'

export default {
  name: 'AliasDialog',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    recordId: {
      type: Number,
      required: true
    },
    candidateList: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      loading: false,
      form: {
        primaryMetaId: null,
        aliasMetaId: null,
        notes: ''
      },
      rules: {
        primaryMetaId: [
          { required: true, message: '请选择主元数据', trigger: 'change' }
        ],
        aliasMetaId: [
          { required: true, message: '请选择别名元数据', trigger: 'change' },
          {
            validator: (rule, value, callback) => {
              if (value && value === this.form.primaryMetaId) {
                callback(new Error('主元数据和别名元数据不能相同'))
              } else {
                callback()
              }
            },
            trigger: 'change'
          }
        ]
      }
    }
  },
  computed: {
    dialogVisible: {
      get() {
        return this.visible
      },
      set(val) {
        this.$emit('update:visible', val)
      }
    }
  },
  methods: {
    handleClose() {
      this.$refs.formRef?.resetFields()
      this.form = {
        primaryMetaId: null,
        aliasMetaId: null,
        notes: ''
      }
      this.dialogVisible = false
    },

    async handleSubmit() {
      try {
        await this.$refs.formRef.validate()
      } catch {
        return
      }

      this.loading = true
      try {
        const currentUser = this.$store.getters.userInfo?.username || 'unknown'
        const res = await setMetaAlias(
          this.recordId,
          this.form.primaryMetaId,
          this.form.aliasMetaId,
          currentUser,
          this.form.notes
        )

        if (res.data.code === 200) {
          this.$message.success('别名关系设置成功')
          this.$emit('success', res.data.data)
          this.handleClose()
        } else {
          this.$message.error(res.data.message || '操作失败')
        }
      } catch (error) {
        console.error('设置别名失败:', error)
        this.$message.error(error.response?.data?.message || '网络错误,请稍后重试')
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

<style scoped>
.form-tip {
  color: #909399;
  font-size: 12px;
  line-height: 1.5;
  margin-top: 4px;
}
</style>

3. 审核详情页面示例

<template>
  <div class="review-detail">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>审核记录详情 #{{ record.id }}</span>
          <el-tag :type="statusTagType">{{ statusText }}</el-tag>
        </div>
      </template>

      <!-- 记录基本信息 -->
      <el-descriptions :column="2" border>
        <el-descriptions-item label="记录类型">
          {{ record.record_type === 'redundancy' ? '疑似冗余' : '疑似变动' }}
        </el-descriptions-item>
        <el-descriptions-item label="业务领域ID">
          {{ record.business_domain_id }}
        </el-descriptions-item>
        <el-descriptions-item label="创建时间">
          {{ record.created_at }}
        </el-descriptions-item>
        <el-descriptions-item label="更新时间">
          {{ record.updated_at }}
        </el-descriptions-item>
      </el-descriptions>

      <!-- 新元数据信息 -->
      <h4>新解析元数据</h4>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="中文名">
          {{ record.new_meta?.name_zh }}
        </el-descriptions-item>
        <el-descriptions-item label="英文名">
          {{ record.new_meta?.name_en }}
        </el-descriptions-item>
        <el-descriptions-item label="数据类型">
          {{ record.new_meta?.data_type }}
        </el-descriptions-item>
      </el-descriptions>

      <!-- 冗余场景:候选列表 -->
      <template v-if="record.record_type === 'redundancy'">
        <h4>候选元数据列表</h4>
        <el-table :data="record.candidates" border>
          <el-table-column prop="candidate_meta_id" label="ID" width="80" />
          <el-table-column prop="name_zh" label="中文名" />
          <el-table-column prop="name_en" label="英文名" />
          <el-table-column prop="data_type" label="数据类型" />
          <el-table-column label="差异字段">
            <template #default="{ row }">
              <el-tag
                v-for="field in row.diff_fields"
                :key="field"
                size="small"
                style="margin-right: 4px"
              >
                {{ field }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
      </template>

      <!-- 操作按钮 -->
      <div v-if="record.status === 'pending'" class="action-buttons">
        <template v-if="record.record_type === 'redundancy'">
          <el-button type="primary" @click="showAliasDialog = true">
            设为别名
          </el-button>
          <el-button type="success" @click="showCreateDialog = true">
            创建新元数据
          </el-button>
        </template>
        <template v-else>
          <el-button type="primary" @click="handleAcceptChange">
            接受变动
          </el-button>
          <el-button type="warning" @click="handleRejectChange">
            拒绝变动
          </el-button>
        </template>
        <el-button @click="handleIgnore">忽略</el-button>
      </div>
    </el-card>

    <!-- 别名设置弹窗 -->
    <alias-dialog
      :visible.sync="showAliasDialog"
      :record-id="record.id"
      :candidate-list="record.candidates"
      @success="handleSuccess"
    />
  </div>
</template>

<script>
import AliasDialog from './AliasDialog.vue'
import { acceptChange, rejectChange, ignoreRecord } from '@/api/metaReview'

export default {
  name: 'ReviewDetail',
  components: { AliasDialog },
  props: {
    record: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      showAliasDialog: false,
      showCreateDialog: false
    }
  },
  computed: {
    statusTagType() {
      const map = {
        pending: 'warning',
        resolved: 'success',
        ignored: 'info'
      }
      return map[this.record.status] || 'info'
    },
    statusText() {
      const map = {
        pending: '待处理',
        resolved: '已处理',
        ignored: '已忽略'
      }
      return map[this.record.status] || this.record.status
    }
  },
  methods: {
    getCurrentUser() {
      return this.$store.getters.userInfo?.username || 'unknown'
    },

    async handleAcceptChange() {
      try {
        await this.$confirm('确定接受此变动吗?', '提示', {
          type: 'warning'
        })
        const res = await acceptChange(
          this.record.id,
          this.record.old_meta?.meta_id,
          this.getCurrentUser()
        )
        if (res.data.code === 200) {
          this.$message.success('操作成功')
          this.$emit('refresh')
        } else {
          this.$message.error(res.data.message)
        }
      } catch (e) {
        if (e !== 'cancel') {
          this.$message.error('操作失败')
        }
      }
    },

    async handleRejectChange() {
      try {
        await this.$confirm('确定拒绝此变动吗?', '提示', {
          type: 'warning'
        })
        const res = await rejectChange(this.record.id, this.getCurrentUser())
        if (res.data.code === 200) {
          this.$message.success('操作成功')
          this.$emit('refresh')
        } else {
          this.$message.error(res.data.message)
        }
      } catch (e) {
        if (e !== 'cancel') {
          this.$message.error('操作失败')
        }
      }
    },

    async handleIgnore() {
      try {
        await this.$confirm('确定忽略此记录吗?', '提示', {
          type: 'warning'
        })
        const res = await ignoreRecord(this.record.id, this.getCurrentUser())
        if (res.data.code === 200) {
          this.$message.success('操作成功')
          this.$emit('refresh')
        } else {
          this.$message.error(res.data.message)
        }
      } catch (e) {
        if (e !== 'cancel') {
          this.$message.error('操作失败')
        }
      }
    },

    handleSuccess() {
      this.$emit('refresh')
    }
  }
}
</script>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

h4 {
  margin: 20px 0 10px;
  color: #303133;
}

.action-buttons {
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid #ebeef5;
}

.action-buttons .el-button {
  margin-right: 12px;
}
</style>

注意事项

  1. 状态限制:只有 status=pending 的记录才能处理,已处理记录会返回错误。

  2. 参数验证:前端应在提交前验证必填参数,避免无效请求。

  3. 别名操作不可逆:alias 操作会修改 Neo4j 中的关系结构,请确保用户确认后再执行。

  4. ID 类型:所有 ID 参数(record_id、primary_meta_id、alias_meta_id、meta_id)应为整数。

  5. 错误处理:建议统一封装 axios 拦截器处理错误响应,对 code !== 200 的情况进行统一提示。


更新日志

日期 版本 更新内容
2025-01-09 v2.0 action=alias 参数变更:使用 primary_meta_idalias_meta_id 替代原 candidate_meta_id,支持 ALIAS 关系重建