fix(data): 数据管理模块脱节修复 — Excel导入/sources端点 entity 隔离 + ERP直连通道诚实化(410) + 真实zxt链路展示

- import_excel/import_excel_smart KPI字典按 entity_id 过滤(防跨账套匹配串写,P0安全)
- import_excel_smart 自动建KPI编码防冲突(_next_code 循环取号)
- sources list/create/update/delete 全部按 entity_id 隔离(create 写入当前账套)
- sync-kpis + admin/erp-sync 下线返回410(erp-gateway DB凭据2026-08-31已删,真实通道=zxt智享通注入)
- 新增 GET /data/channel-status:展示真实zxt取数通道状态+遗留ERP数据源清单(诚实化)
- DataManage.vue:数据源tab顶部展示真实通道(最近注入period/时间/行数/落盘目录mtime)、ERP遗留源标注'非当前通道'、实际取数映射(kpi_value_sources)表格
- 新增 test_data_entity_isolation_20260908.py(8条:双账套导入隔离/CRUD隔离/sync-kpis 410/channel-status)
This commit is contained in:
Hermes CI Fix
2026-09-08 11:19:38 +08:00
parent bdcfa2500e
commit 193061cf4d
5 changed files with 471 additions and 79 deletions
+1
View File
@@ -144,6 +144,7 @@ export const dataApi = {
createSource: (data: any) => api.post('/data/sources', data),
updateSource: (id: number, data: any) => api.put(`/data/sources/${id}`, data),
deleteSource: (id: number) => api.delete(`/data/sources/${id}`),
channelStatus: () => api.get('/data/channel-status'),
}
export const alertApi = {
+112 -6
View File
@@ -20,6 +20,30 @@
</el-tab-pane>
<el-tab-pane label="数据源管理" name="sources">
<!-- ⚠️ 数据链路诚实化:真实取数通道状态(2026-09-08 P0) -->
<el-alert v-if="channelLoading" title="加载数据链路状态..." type="info" show-icon :closable="false" class="channel-alert" />
<template v-else>
<el-alert v-if="realChannel && realChannel.latest_inject_at" type="success" :closable="false" class="channel-alert">
<template #title>
✅ 真实数据通道(当前在用):{{ realChannel.channel }}
<span class="channel-inline">最近注入:{{ realChannel.latest_period }}({{ realChannel.latest_inject_at }},共 {{ realChannel.injected_rows }} 行)</span>
<span v-if="realChannel.landing_dir_mtime" class="channel-inline">落盘目录最新文件:{{ realChannel.landing_dir_mtime }}</span>
</template>
<div class="channel-desc">
数据链路:DAMA 从智享通网页读取科目余额表 → 落盘 {{ realChannel.landing_dir }} → 财务Bot 注入 CMA(subjects + kpi_values 双写,source_type='zxt-sync')。
该通道为陕西酣客财务原始数据的真实来源,每日/结账后由 DAMA 例行执行。
</div>
</el-alert>
<el-alert v-else type="info" :closable="false" class="channel-alert" title="当前账套暂无 zxt 智享通注入数据(可能为企业数据未接入智享通通道)" />
<el-alert v-if="erpDirect && erpDirect.legacy_sources_count > 0" type="warning" :closable="false" class="channel-alert">
<template #title>
⚠️ ERP 直连同步通道已停用(2026-08-31),以下 {{ erpDirect.legacy_sources_count }} 条 ERP 数据源为<strong>遗留配置</strong>,不属于当前数据通道
</template>
<div class="channel-desc">{{ erpDirect.reason }}</div>
</el-alert>
</template>
<el-card>
<template #header>
<div class="card-header-flex">
@@ -29,7 +53,7 @@
</template>
<el-table :data="sources" v-loading="sourcesLoading" class="full-width-table">
<el-table-column prop="name" label="名称" min-width="140" />
<el-table-column prop="source_type" label="类型" width="80">
<el-table-column prop="source_type" label="类型" width="90">
<template #default="{ row }">
<el-tag :type="sourceTypeTag(row.source_type)" size="small">{{ row.source_type }}</el-tag>
</template>
@@ -40,9 +64,11 @@
<el-tag size="small" :type="row.sync_type==='realtime'?'success':'info'">{{ row.sync_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="70">
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="row.status==='active'?'success':'danger'">{{ row.status }}</el-tag>
<!-- 诚实化:ERP 遗留数据源即使 status=active 也标注"非当前通道",避免误导 -->
<el-tag v-if="isLegacyErpSource(row)" type="warning" size="small">遗留·非当前通道</el-tag>
<el-tag v-else :type="row.status==='active'?'success':'danger'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="last_sync_at" label="上次同步" width="160" />
@@ -54,21 +80,52 @@
</el-table-column>
</el-table>
<el-empty v-if="!sourcesLoading && sources.length === 0" description="暂无数据源,请点击「新增数据源」添加" />
<div v-if="legacyErpIds.size > 0" class="legacy-note">
💡 上表中「ERP」类型数据源为遗留演示配置:其端点指向 erp-gateway 容器(2026-08-31 起 DB 凭据删除、已停用)。
新增数据源请选择非 ERP 类型;企业财务数据由上方 zxt 智享通通道真实供给,无需在此配置。
</div>
</el-card>
<!-- 真实取数映射(采集器实际使用) -->
<el-card v-if="valueSources.length > 0" class="vs-card">
<template #header>
<div class="card-header-flex">
<span>📡 实际取数映射(采集器使用,kpi_value_sources)</span>
</div>
</template>
<div class="channel-desc">06:30 定时采集(scripts/kpi_value_collector.py)按此映射写入 kpi_values(source_type='auto_collect'),与上方 ERP 遗留数据源相互独立。</div>
<el-table :data="valueSources" class="full-width-table">
<el-table-column prop="kpi_code" label="KPI编码" min-width="130" />
<el-table-column prop="kpi_name" label="KPI名称" min-width="130" />
<el-table-column prop="source_table" label="源表" min-width="140" />
<el-table-column prop="source_field" label="源字段" min-width="120" />
<el-table-column prop="aggregate" label="聚合" width="90" />
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status==='active'?'success':'danger'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
<MyDialog v-model="showSourceForm" :title="sourceForm.id ? '编辑数据源' : '新增数据源'" :width="550">
<el-alert v-if="!sourceForm.id && sourceForm.source_type==='erp'" type="warning" :closable="false" class="form-warn"
title="ERP 直连通道已停用(2026-08-31)。ERP 类型仅用于保留/查看遗留配置,企业财务数据由 zxt 智享通通道供给。" />
<el-form :model="sourceForm" label-width="120px">
<el-form-item label="名称"><el-input v-model="sourceForm.name" /></el-form-item>
<el-form-item label="类型">
<el-select v-model="sourceForm.source_type">
<el-select v-model="sourceForm.source_type" @change="onSourceTypeChange">
<el-option label="ERP" value="erp" />
<el-option label="业务系统" value="business" />
<el-option label="Excel" value="excel" />
<el-option label="手工" value="manual" />
</el-select>
</el-form-item>
<el-form-item v-if="sourceForm.source_type==='erp'" label="遗留说明">
<el-alert type="warning" :closable="false" show-icon title="该类型数据源不会执行同步(通道已停用),仅作记录" />
</el-form-item>
<el-form-item label="API地址"><el-input v-model="sourceForm.api_endpoint" placeholder="https://..." /></el-form-item>
<el-form-item label="API Key"><el-input v-model="sourceForm.api_key" type="password" show-password /></el-form-item>
<el-form-item label="SQL查询"><el-input v-model="sourceForm.query_sql" type="textarea" :rows="3" placeholder="可选,用于查询型数据源" /></el-form-item>
@@ -92,7 +149,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { dataApi } from '../api/index'
import { dataApi, budgetApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
const activeTab = ref('import')
@@ -132,6 +189,38 @@ async function doImport() {
uploading.value = false
}
// === 数据链路状态(2026-09-08 诚实化) ===
const channelLoading = ref(false)
const realChannel = ref<any>(null)
const erpDirect = ref<any>(null)
const legacyErpIds = ref<Set<number>>(new Set())
const valueSources = ref<any[]>([])
function isLegacyErpSource(row: any): boolean {
return row.source_type === 'erp' && legacyErpIds.value.has(Number(row.id))
}
async function loadChannelStatus() {
channelLoading.value = true
try {
const r: any = await dataApi.channelStatus()
const d = r.data || r
realChannel.value = d.real_channel || null
erpDirect.value = d.erp_direct || null
const ids = (d.erp_direct?.legacy_sources || []).map((s: any) => Number(s.id))
legacyErpIds.value = new Set(ids)
} catch (e) { /* 非阻塞:状态卡片加载失败不影响列表 */ }
channelLoading.value = false
}
async function loadValueSources() {
try {
const r: any = await budgetApi.valueSources({ status: 'active' })
const d = Array.isArray(r) ? r : (r.data || [])
valueSources.value = d
} catch (e) { valueSources.value = [] }
}
// === 数据源管理 ===
const sources = ref<any[]>([])
const sourcesLoading = ref(false)
@@ -143,6 +232,11 @@ function sourceTypeTag(t: string) {
return ({ erp: 'primary', business: 'warning', excel: 'success', manual: 'info' } as any)[t] || 'info'
}
function onSourceTypeChange() {
// 触发表单内 ERP 遗留说明显示
sourceForm.value = { ...sourceForm.value }
}
async function loadSources() {
sourcesLoading.value = true
try {
@@ -173,6 +267,7 @@ async function saveSource() {
}
showSourceForm.value = false
loadSources()
loadChannelStatus()
} catch (e) { ElMessage.error('保存失败') }
sourceSaving.value = false
}
@@ -183,12 +278,17 @@ async function doDeleteSource(row: any) {
await dataApi.deleteSource(row.id)
ElMessage.success('已删除')
loadSources()
loadChannelStatus()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error('删除失败')
}
}
onMounted(loadSources)
onMounted(() => {
loadSources()
loadChannelStatus()
loadValueSources()
})
</script>
<style scoped>
@@ -199,4 +299,10 @@ onMounted(loadSources)
.tag-list .file-tag { margin: 2px; }
.upload-actions { margin-top: 12px; }
.result-area { margin-top: 12px; white-space: pre-wrap; }
.channel-alert { margin-bottom: 12px; }
.channel-inline { margin-left: 12px; color: #606266; font-weight: normal; }
.channel-desc { margin-top: 4px; color: #606266; font-size: 12px; line-height: 1.7; }
.vs-card { margin-top: 12px; }
.legacy-note { margin-top: 8px; color: #b88230; font-size: 12px; line-height: 1.7; }
.form-warn { margin-bottom: 12px; }
</style>