fix: 预算数据修正 — 删除v1.0错误分解记录, 修正v2.0 F_REVENUE为真实H2预测(75-120万/月,原367万错误)
- 删除v1.0 49条(今天'执行分解'基于错误v2.0聚合产生的重复数据) - 修正v2.0 F_REVENUE: 367万/月→90/75/80/85/95/120万(真实H2预测455万+7月90万) - 解决预算录入'部分指标重复'问题(同KPI+期间双版本记录)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,176 @@
|
||||
# CMA验证数据需求规范(双实体验证矩阵)
|
||||
|
||||
> 建立:2026-08-19 | 提出:任总(两套数据验证决策 + "每次需要什么样的数据")
|
||||
> 核心原则:酣客=验收场(真实客户数据,最后碰),博海=试验田(随便折腾,先试错)
|
||||
> 验证策略:"先博海试验,再酣客验收"
|
||||
|
||||
## 一、双实体角色分工
|
||||
|
||||
| 实体 | 角色 | 用途 | 数据量 |
|
||||
|:----|:----|:----|:------|
|
||||
| 陕西博海(id=2) | **试验田** | 新功能试错/MCP测试/破坏性测试/快速迭代 | 16 KPI / 316 数据点 |
|
||||
| 陕西酣客(id=1) | **验收场** | 客户验收/真实场景/交付演示 | 70 KPI / 1269 数据点 |
|
||||
| 测试企业(id=3等) | 自动化测试 | CI跑用例(不人工碰) | 空 |
|
||||
|
||||
```
|
||||
铁律:新功能先在博海跑通,再碰酣客数据
|
||||
破坏性操作(删除/清空/批量改)只在博海或测试实体做
|
||||
```
|
||||
|
||||
## 二、各验证场景所需数据(核心矩阵)
|
||||
|
||||
### 场景1:功能开发验证(新功能/改代码)
|
||||
| 数据需求 | 博海(试验田) | 说明 |
|
||||
|:--------|:------------|:----|
|
||||
| KPI定义 | ≥5个(覆盖4维度:财务/客户/流程/学习) | 测维度筛选 |
|
||||
| KPI数据点 | ≥50个(≥3个周期,含红黄绿各态) | 测红黄绿判定 |
|
||||
| 预警 | ≥3条(红/黄各至少1条) | 测预警列表 |
|
||||
| 预算 | ≥3个 | 测预算对比 |
|
||||
| 行动计划 | ≥2个(含1个逾期) | 测逾期逻辑 |
|
||||
| 组织 | ≥5个节点 | 测部门归属 |
|
||||
|
||||
### 场景2:MCP封装验证(本次重点)
|
||||
| 数据需求 | 博海 | 说明 |
|
||||
|:--------|:----|:----|
|
||||
| KPI(含历史) | ≥5个KPI × ≥3周期 | 测cma_kpi_history |
|
||||
| 预警 | ≥2条 | 测cma_query_alerts |
|
||||
| 预算 | ≥2个 | 测cma_budget_plans |
|
||||
| 概览数据 | 完整(KPI+预警+预算都有) | 测cma_overview |
|
||||
| **空值KPI** | ≥1个(无数据) | 测容错(AI问空数据不能崩) |
|
||||
| **异常KPI** | ≥1个(目标为0/负值) | 测边界 |
|
||||
|
||||
### 场景3:客户验收/演示(酣客数据)
|
||||
| 数据需求 | 酣客 | 说明 |
|
||||
|:--------|:----|:----|
|
||||
| 全量KPI | 70个(保持现状) | 真实场景 |
|
||||
| 关键KPI数据 | 财务核心(营收/净利/成本率)必须有最近3期 | 老板最关注 |
|
||||
| 预警 | 真实预警(有红黄) | 展示预警价值 |
|
||||
| 战略地图 | ≥1张 | 展示战略层 |
|
||||
| 报表 | ≥1份BI报表 | 展示汇报能力 |
|
||||
| **演示剧本** | 3个核心场景(看KPI/查预警/看趋势) | 演示有故事线 |
|
||||
|
||||
### 场景4:回归验证(改完不破坏)
|
||||
| 数据需求 | 两套都跑 | 说明 |
|
||||
|:--------|:--------|:----|
|
||||
| 博海 | 16 KPI / 316 数据点 | 快跑(功能不坏) |
|
||||
| 酣客 | 70 KPI / 1269 数据点 | 全跑(数据不坏) |
|
||||
| 关键对比 | 修复前后数据一致性 | 确认无副作用 |
|
||||
|
||||
### 场景5:破坏性/边界测试
|
||||
| 数据需求 | 只准博海/测试实体 | 说明 |
|
||||
|:--------|:--------------|:----|
|
||||
| 清空KPI值 | 博海子集(备份后) | 测空态UI |
|
||||
| 大量数据导入 | 造1000+行 | 测性能 |
|
||||
| 异常格式 | 特殊字符/超长字段 | 测容错 |
|
||||
| 权限越界 | 用测试账号 | 测权限 |
|
||||
|
||||
## 三、数据准备清单(最小可用数据集)
|
||||
|
||||
### 博海试验田(最小16个KPI覆盖全场景)
|
||||
```
|
||||
4维度各≥1个:
|
||||
财务:F_REVENUE(营收)/ F_NET_PROFIT(净利)/ F_COST_RATIO(费用率,反向)
|
||||
客户:C_SATISFACTION(满意度)/ C_REBATE_RATE(渠补率,反向)
|
||||
流程:P_DELIVERY(交付及时率)
|
||||
学习:L_TRAINING(培训完成率)
|
||||
↓
|
||||
状态覆盖:
|
||||
绿色≥2(达标)、黄色≥1(预警)、红色≥1(危险)、灰色≥1(无数据)
|
||||
↓
|
||||
周期覆盖:
|
||||
最近3期(2026-06/07/08)每期都有值
|
||||
↓
|
||||
边界覆盖:
|
||||
1个KPI目标=0(测除零)、1个KPI无数据(测空态)
|
||||
```
|
||||
|
||||
### 酣客验收场(保持真实,不造假)
|
||||
```
|
||||
❌ 禁止:为测试给酣客造假数据
|
||||
✅ 做法:用真实数据,缺的周期标注"待补充"
|
||||
↓
|
||||
验证时用"真实数据 + 演示剧本":
|
||||
① 看我的KPI(营收/净利红黄绿)
|
||||
② 查预警(哪些指标危险)
|
||||
③ 看趋势(最近3期变化)
|
||||
```
|
||||
|
||||
## 四、验证执行检查清单(每次验证前过一遍)
|
||||
|
||||
```
|
||||
[ ] 1. 确认验证类型(功能/MCP/验收/回归/破坏)
|
||||
[ ] 2. 选数据源:功能/MCP/破坏→博海;验收→酣客;回归→两套
|
||||
[ ] 3. 数据准备:对照上面的"最小可用数据集"检查是否齐全
|
||||
[ ] 4. 空值/异常KPI是否在(测容错)
|
||||
[ ] 5. 备份:改动前备份目标实体数据(mysqldump)
|
||||
[ ] 6. 执行验证 + 记录结果
|
||||
[ ] 7. 验证后检查:consistency-check 无新错误
|
||||
```
|
||||
|
||||
## 五、验证数据速查表(一句话版)
|
||||
|
||||
| 验证类型 | 用哪套 | 最少需要 |
|
||||
|:--------|:------|:--------|
|
||||
| 新功能 | 博海 | 5 KPI × 3期 + 红黄绿各1 |
|
||||
| MCP封装 | 博海 | 5 KPI×3期 + 空值KPI + 异常KPI |
|
||||
| 客户验收 | 酣客 | 70 KPI + 3期 + 演示剧本 |
|
||||
| 回归 | 两套 | 全量对比 |
|
||||
| 破坏性 | 博海/测试 | 备份后随意 |
|
||||
|
||||
## 六、数据录入归属规范(实体自动判断)
|
||||
|
||||
> 2026-08-19 任总提问"输入数据时明确博海还是酣客?系统能否自动判断"——已确认系统现状与增强方案
|
||||
|
||||
### 系统现状(账套模式已存在)
|
||||
```
|
||||
① 登录必须选企业(entity_id):
|
||||
auth.py:"账套模式:请选择登录企业"
|
||||
→ 用户登录时选酣客 or 博海
|
||||
↓
|
||||
② token绑定实体(安全):
|
||||
token.entity_id = 唯一可信来源
|
||||
用户只能访问被授权的企业(user_entities表)
|
||||
↓
|
||||
③ Bot通道(X-Entity-Id header/query):
|
||||
白名单Bot可指定实体,但token存在时忽略query(防越权)
|
||||
↓
|
||||
④ 兜底默认:1(酣客)
|
||||
```
|
||||
|
||||
### 结论:输入时无需每次明确
|
||||
```
|
||||
人工录入(网页):
|
||||
登录时已选 → 数据自动归到所选企业(token绑定)
|
||||
→ 不需要每次输入时再明确
|
||||
↓
|
||||
Bot/API导入:
|
||||
可以指定X-Entity-Id → 需要明确(API调用时传)
|
||||
↓
|
||||
唯一边界:一个Excel混了两家公司数据 → 需增强
|
||||
```
|
||||
|
||||
### 自动判断三级方案(待实现,0.5天)
|
||||
```
|
||||
① 登录实体(token.entity_id)→ 默认归属
|
||||
② Excel内容检测:表头/首列是否含"酣客/博海"字样
|
||||
→ 有:覆盖归属(跨公司导入场景)
|
||||
→ 无:用登录实体
|
||||
③ 文件名检测:文件名含"酣客/博海"→ 辅助判断
|
||||
↓
|
||||
④ 冲突时(登录=博海 但 内容=酣客):
|
||||
→ 弹确认:"检测到酣客数据,确认归属?"
|
||||
→ 人工点一下(不自动改,防误)
|
||||
```
|
||||
|
||||
### 实现位置
|
||||
```
|
||||
后端:data.py import_excel_smart 加 _detect_entity()(~30行)
|
||||
前端:import弹窗加实体确认(~20行)
|
||||
状态:待排期(记入待办,非紧急)
|
||||
```
|
||||
|
||||
## 七、关联
|
||||
- kpi-account-governance-rule.md(科目≠KPI规范)
|
||||
- 双实体验证决策(2026-08-19 任总)
|
||||
- 铁律七(验证不信任自述)
|
||||
- MCP封装(cma-mcp-strategy.md:先博海试,后酣客验)
|
||||
@@ -322,88 +322,19 @@
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 6: KPI趋势预测(预测性成本智能 MVP) -->
|
||||
<el-tab-pane label="KPI预测" name="kpiForecast">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
|
||||
<span>📈 财务KPI趋势预测(预测性成本智能 MVP)</span>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<span style="font-size:12px;color:#909399;">模型</span>
|
||||
<el-select v-model="kfModel" style="width:120px;" size="small">
|
||||
<el-option label="线性回归" value="linear" />
|
||||
<el-option label="移动平均" value="moving_average" />
|
||||
</el-select>
|
||||
<span style="font-size:12px;color:#909399;">期数</span>
|
||||
<el-select v-model="kfPeriods" style="width:80px;" size="small">
|
||||
<el-option label="3期" :value="3" />
|
||||
<el-option label="6期" :value="6" />
|
||||
<el-option label="12期" :value="12" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" @click="loadKpiForecast" :loading="kfLoading">🔄 重新预测</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="font-size:12px;color:#909399;margin-bottom:8px;">{{ kfSummary }}</div>
|
||||
<el-table :data="kfResult" border stripe size="small" style="width:100%;" @expand-change="onKpiExpand">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div style="padding:12px 24px;">
|
||||
<div style="font-size:12px;color:#606266;margin-bottom:8px;">💡 {{ row.summary }}</div>
|
||||
<div :ref="(el: any) => setKpiChartEl(el, row)" style="height:300px;width:100%;"></div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="KPI名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.kpi.name }}</div>
|
||||
<div style="font-size:11px;color:#909399;">{{ row.kpi.code }}({{ row.kpi.unit || '无量纲' }})</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="趋势方向" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="kfTrendMap[row.trend]?.tag || 'info'" size="small" effect="light">
|
||||
{{ row.trend === 'up' ? '↑ 上升' : row.trend === 'down' ? '↓ 下降' : '→ 平稳' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下一期预测" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<strong :style="{ color: row.trend === 'up' ? '#f56c6c' : row.trend === 'down' ? '#67c23a' : '#303133' }">
|
||||
{{ fmtKfValue(row.next_target) }}
|
||||
</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置信区间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.forecast?.length" style="font-size:12px;color:#606266;">
|
||||
{{ fmtKfValue(row.forecast[0].lower) }} ~ {{ fmtKfValue(row.forecast[0].upper) }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置信度" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="kfConfMap[row.confidence]?.tag || 'info'" size="small">
|
||||
{{ kfConfMap[row.confidence]?.label || row.confidence }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="history_count" label="历史期数" width="80" align="center" />
|
||||
<el-table-column label="模型" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.model === 'moving_average' ? '移动平均' : '线性回归' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<!-- Tab 6: KPI智能预测(预测性成本智能) -->
|
||||
<el-tab-pane label="KPI智能预测" name="kpiForecast">
|
||||
<CostIntelligence />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { predictApi } from '../api/index'
|
||||
import CostIntelligence from './CostIntelligence.vue'
|
||||
|
||||
const activeTab = ref('cvp')
|
||||
|
||||
@@ -680,134 +611,6 @@ function renderCfChart() {
|
||||
})
|
||||
}
|
||||
|
||||
// ── KPI趋势预测(预测性成本智能 MVP) ──
|
||||
const kfLoading = ref(false)
|
||||
const kfModel = ref('linear')
|
||||
const kfPeriods = ref(3)
|
||||
const kfResult = ref<any[]>([])
|
||||
const kfSummary = ref('')
|
||||
const kfEntityId = ref(Number(localStorage.getItem('cma_entity_id') || 1))
|
||||
const kfChartEls: Record<string, HTMLElement | null> = {}
|
||||
const kfCharts: Record<string, any> = {}
|
||||
|
||||
const kfTrendMap: Record<string, { label: string; tag: string }> = {
|
||||
up: { label: '上升', tag: 'danger' },
|
||||
down: { label: '下降', tag: 'success' },
|
||||
flat: { label: '平稳', tag: 'info' },
|
||||
}
|
||||
const kfConfMap: Record<string, { label: string; tag: string }> = {
|
||||
high: { label: '高', tag: 'success' },
|
||||
medium: { label: '中', tag: 'warning' },
|
||||
low: { label: '低', tag: 'danger' },
|
||||
}
|
||||
|
||||
function fmtKfValue(v: any): string {
|
||||
if (v === null || v === undefined) return '--'
|
||||
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function loadKpiForecast() {
|
||||
kfLoading.value = true
|
||||
try {
|
||||
const res = await predictApi.kpiForecastFinance({
|
||||
entity_id: kfEntityId.value,
|
||||
periods: kfPeriods.value,
|
||||
model: kfModel.value,
|
||||
}) as any
|
||||
kfResult.value = res?.data || []
|
||||
kfSummary.value = `共 ${res?.total ?? 0} 个财务KPI可预测(历史数据≥3期),按可预测性排序;点击行展开查看历史趋势与预测区间`
|
||||
} catch (e: any) {
|
||||
ElMessage.error('KPI预测加载失败: ' + (e?.message || ''))
|
||||
kfResult.value = []
|
||||
kfSummary.value = ''
|
||||
}
|
||||
kfLoading.value = false
|
||||
}
|
||||
|
||||
// 切换到「KPI预测」Tab 时自动加载
|
||||
watch(activeTab, (t) => {
|
||||
if (t === 'kpiForecast') loadKpiForecast()
|
||||
})
|
||||
|
||||
function setKpiChartEl(el: any, row: any) {
|
||||
if (!el) return
|
||||
kfChartEls[row.kpi.code] = el
|
||||
}
|
||||
|
||||
function onKpiExpand(row: any, expandedRows: any[]) {
|
||||
if (!expandedRows.includes(row)) return
|
||||
nextTick(() => renderKpiChart(row))
|
||||
}
|
||||
|
||||
function renderKpiChart(row: any) {
|
||||
const el = kfChartEls[row.kpi.code]
|
||||
if (!el || !row.forecast?.length) return
|
||||
import('echarts').then(echarts => {
|
||||
if (kfCharts[row.kpi.code]) kfCharts[row.kpi.code].dispose()
|
||||
const chart = echarts.init(el)
|
||||
kfCharts[row.kpi.code] = chart
|
||||
|
||||
const hist = row.history || []
|
||||
const fc = row.forecast || []
|
||||
const histLen = hist.length
|
||||
const periods = [...hist.map((h: any) => h.period), ...fc.map((f: any) => f.period)]
|
||||
const histVals = hist.map((h: any) => h.value)
|
||||
const fcVals = fc.map((f: any) => f.predicted)
|
||||
const lower = fc.map((f: any) => f.lower)
|
||||
const upper = fc.map((f: any) => f.upper)
|
||||
|
||||
// 历史线:仅历史区间;预测线:衔接历史末值后延伸
|
||||
const histSeries = [...histVals, ...Array(fc.length).fill(null)]
|
||||
const fcSeries = [...Array(Math.max(0, histLen - 1)).fill(null), histVals[histLen - 1], ...fcVals]
|
||||
// 置信区间带(stack 双线夹层)
|
||||
const bandLower = [...Array(histLen).fill(null), ...lower]
|
||||
const bandWidth = [...Array(histLen).fill(null), ...upper.map((u: number, i: number) => u - lower[i])]
|
||||
|
||||
chart.setOption({
|
||||
grid: { left: 70, right: 30, top: 30, bottom: 40 },
|
||||
xAxis: { type: 'category', data: periods, axisLabel: { rotate: 45, fontSize: 10 } },
|
||||
yAxis: { type: 'value', name: row.kpi.unit || '' },
|
||||
series: [
|
||||
{
|
||||
name: '历史值', type: 'line', data: histSeries,
|
||||
smooth: true, symbol: 'circle', symbolSize: 5,
|
||||
lineStyle: { width: 2, color: '#409eff' }, itemStyle: { color: '#409eff' },
|
||||
},
|
||||
{
|
||||
name: '预测值', type: 'line', data: fcSeries,
|
||||
smooth: true, symbol: 'circle', symbolSize: 5,
|
||||
lineStyle: { width: 2, color: '#e6a23c', type: 'dashed' }, itemStyle: { color: '#e6a23c' },
|
||||
},
|
||||
{
|
||||
name: '区间下界', type: 'line', data: bandLower,
|
||||
stack: 'ci', lineStyle: { opacity: 0 }, symbol: 'none',
|
||||
areaStyle: { color: 'rgba(230,162,60,0.15)' },
|
||||
},
|
||||
{
|
||||
name: '区间宽', type: 'line', data: bandWidth,
|
||||
stack: 'ci', lineStyle: { opacity: 0 }, symbol: 'none',
|
||||
},
|
||||
{
|
||||
name: '预测点', type: 'scatter',
|
||||
data: fcVals.map((v: number, i: number) => [histLen + i, v]),
|
||||
symbolSize: 9, itemStyle: { color: '#e6a23c', borderColor: '#fff', borderWidth: 1 },
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params: any) {
|
||||
const idx = params[0]?.dataIndex
|
||||
if (idx === undefined) return ''
|
||||
const p = periods[idx]
|
||||
if (idx < histLen) return `<strong>${p}</strong><br/>实际值: ${fmtKfValue(histVals[idx])}`
|
||||
const f = fc[idx - histLen]
|
||||
return `<strong>${p}</strong><br/>预测值: <strong>${fmtKfValue(f.predicted)}</strong><br/>置信区间: ${fmtKfValue(f.lower)} ~ ${fmtKfValue(f.upper)}`
|
||||
},
|
||||
},
|
||||
legend: { bottom: 0, icon: 'circle', itemWidth: 8, itemHeight: 8 },
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user