diff --git a/backend/scripts/clean_kpi_data_governance.py b/backend/scripts/clean_kpi_data_governance.py new file mode 100644 index 00000000..5f5bb5e9 --- /dev/null +++ b/backend/scripts/clean_kpi_data_governance.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CMA 数据治理 P0-2 数据清洗脚本(2026-08-15) +================================================ +背景: CMA 系统数据治理 P0 任务, 财务Bot已拍板执行 P0-2 数据清洗。 + +操作清单: + 1. 备份 kpi_values -> kpi_values_bak_20260815(先备份再动数据) + 2. 删除异常周期数据: source_batch='986360a37b99'(26条, id 1323-1348, + period 列是金额乱码: 2459.2/1021.21/1016.12/71981.55/103833.02/nan 等, 非有效期间) + 3. 176条 active 且 target_value=0 的 KPI: formula_desc 追加【目标值待补充】(不删除)。 + ⚠️ 任务书 SQL 写 SET remark=..., 但 kpi_definitions 表无 remark 列(已 SHOW COLUMNS 核实); + 实际落点为 formula_desc —— 经查该 176 条 formula_desc 全部非空且为说明文字 + (如 EXT_071"待财务提供旧科目表核对归属"),是事实上的备注/说明字段。 + 4. EXT_071 (id=196, kpi_name=未命名科目(历史导入数据), entity_id=1): + 历史Excel导入残留(source_batch=0720ca516636) -> 归档 status='archived', + 其 kpi_values 数据保留不动 + 5. FB_ ×11(process维度) / BH_SALES_ ×10(finance维度) 编码保留不改(只加注释): + - FB_ = 财务Bot自身工作KPI, 涉及前端引用, 改码会破坏财务Bot链路 + - BH_SALES_ = 博海销售真实数据, 涉及前端引用 + - 本脚本仅输出只读清单确认未改动 + +约束(铁律): + - 不删除 kpi_definitions 任何行; 不删除 EXT_071 的 kpi_values + - 可重复执行(幂等): 备份表先 DROP IF EXISTS 再重建; 其余操作以 WHERE 条件天然幂等 + +用法: python3 clean_kpi_data_governance.py +""" +import pymysql + + +def main(): + conn = pymysql.connect(host="127.0.0.1", port=3306, user="cma_user", + password="cma_pass_2026", database="cma", charset="utf8mb4") + cur = conn.cursor(pymysql.cursors.DictCursor) + + def q(sql, args=None): + cur.execute(sql, args) + return cur.fetchall() + + def c(sql, args=None): + return q(sql, args)[0]["c"] + + print("=" * 64) + print("CMA 数据治理 P0-2 数据清洗 开始") + print("=" * 64) + + # ---- 步骤1: 备份 kpi_values(先备份再动数据) ---- + print("\n[步骤1] 备份 kpi_values -> kpi_values_bak_20260815") + before = c("SELECT COUNT(*) AS c FROM kpi_values") + print(f" 备份前 kpi_values 总行数: {before}") + cur.execute("DROP TABLE IF EXISTS kpi_values_bak_20260815") + cur.execute("CREATE TABLE kpi_values_bak_20260815 AS SELECT * FROM kpi_values") + bak = c("SELECT COUNT(*) AS c FROM kpi_values_bak_20260815") + print(f" 备份表 kpi_values_bak_20260815 行数: {bak}(应等于 {before})") + assert bak == before, "备份行数不一致, 中止!" + conn.commit() + + # ---- 步骤2: 删除异常周期数据 ---- + print("\n[步骤2] 删除异常周期数据 source_batch='986360a37b99'") + rows = q("SELECT id, period FROM kpi_values WHERE source_batch='986360a37b99' ORDER BY id") + print(f" 删除前命中 {len(rows)} 条", end="") + if rows: + print(f"(id {rows[0]['id']} ~ {rows[-1]['id']})") + for r in rows: + print(f" id={r['id']} period={r['period']!r}") + else: + print(" —— 该批次已无数据(可能上次执行已删除), 跳过打印") + cur.execute("DELETE FROM kpi_values WHERE source_batch='986360a37b99'") + remain = c("SELECT COUNT(*) AS c FROM kpi_values WHERE source_batch='986360a37b99'") + total_after = c("SELECT COUNT(*) AS c FROM kpi_values") + print(f" 删除后该批次剩余 {remain} 条(应为 0)") + print(f" kpi_values 总行数: {before} -> {total_after}(应减少 {len(rows)})") + conn.commit() + + # ---- 步骤3: active 且 target_value=0 标注【目标值待补充】 ---- + print("\n[步骤3] active 且 target_value=0 的 KPI 标注『目标值待补充』(落点 formula_desc)") + n = c("SELECT COUNT(*) AS c FROM kpi_definitions WHERE status='active' AND target_value=0") + print(f" 命中 {n} 条(任务书预期 176)") + assert n == 176, f"与任务书预期 176 不符(实际 {n}), 中止!" + already = c("SELECT COUNT(*) AS c FROM kpi_definitions WHERE status='active' AND target_value=0 AND formula_desc LIKE '%【目标值待补充】%'") + print(f" 已带标记 {already} 条(防重复追加)") + if already == 0: + cur.execute(""" + UPDATE kpi_definitions + SET formula_desc = CONCAT(IFNULL(NULLIF(formula_desc,''),''), '【目标值待补充】') + WHERE status='active' AND target_value=0 + """) + print(f" UPDATE 影响行数: {cur.rowcount}") + else: + print(f" 已存在标记 {already} 条, 跳过追加(幂等)") + marked = c("SELECT COUNT(*) AS c FROM kpi_definitions WHERE status='active' AND target_value=0 AND formula_desc LIKE '%【目标值待补充】%'") + n_after = c("SELECT COUNT(*) AS c FROM kpi_definitions WHERE status='active' AND target_value=0") + print(f" 标注后带标记 {marked} 条; active 且 target_value=0 仍为 {n_after} 条(只标注, 不删除)") + conn.commit() + + # ---- 步骤4: EXT_071 归档 ---- + print("\n[步骤4] EXT_071(id=196)归档 status='archived'") + ext_before = q("SELECT id,kpi_code,kpi_name,entity_id,status FROM kpi_definitions WHERE id=196 AND kpi_code='EXT_071'") + print(f" 归档前: {ext_before}") + cur.execute("UPDATE kpi_definitions SET status='archived' WHERE id=196 AND kpi_code='EXT_071'") + print(f" UPDATE 影响行数: {cur.rowcount}") + ext_after = q("SELECT id,kpi_code,kpi_name,entity_id,status FROM kpi_definitions WHERE id=196 AND kpi_code='EXT_071'") + print(f" 归档后: {ext_after}") + kv = c("SELECT COUNT(*) AS c FROM kpi_values WHERE kpi_id=196") + print(f" EXT_071 关联 kpi_values 行数(保留不动): {kv}") + # 只读引用检查(不修改; 引用表不存在则跳过) + for label, sql in ( + ("kpi_causality", "SELECT COUNT(*) AS c FROM kpi_causality WHERE source_kpi_id=196 OR target_kpi_id=196"), + ("kpi_alerts", "SELECT COUNT(*) AS c FROM kpi_alerts WHERE kpi_id=196"), + ("alert_rules", "SELECT COUNT(*) AS c FROM alert_rules WHERE kpi_id=196"), + ("kpi_hierarchy", "SELECT COUNT(*) AS c FROM kpi_hierarchy WHERE parent_kpi_id=196 OR child_kpi_id=196"), + ): + try: + print(f" 引用表 {label}: {c(sql)} 条(只读检查)") + except pymysql.err.ProgrammingError as e: + print(f" 引用表 {label}: 表不存在, 跳过({e})") + conn.commit() + + # ---- 步骤5: FB_/BH_SALES_ 编码保留(不改) ---- + print("\n[步骤5] FB_/BH_SALES_ 编码保留(不改)— 只读确认") + print(" 保留原因: FB_=财务Bot自身工作KPI; BH_SALES_=博海销售真实数据; 均涉及前端引用") + fb = q("SELECT kpi_code,kpi_name,entity_id,dimension,status FROM kpi_definitions WHERE kpi_code LIKE 'FB\\_%' ORDER BY kpi_code") + bh = q("SELECT kpi_code,kpi_name,entity_id,dimension,status FROM kpi_definitions WHERE kpi_code LIKE 'BH\\_%' ORDER BY kpi_code") + print(f" FB_ 共 {len(fb)} 条:") + for r in fb: + print(f" {r['kpi_code']} | {r['kpi_name']} | entity={r['entity_id']} | {r['dimension']} | {r['status']}") + print(f" BH_SALES_ 共 {len(bh)} 条:") + for r in bh: + print(f" {r['kpi_code']} | {r['kpi_name']} | entity={r['entity_id']} | {r['dimension']} | {r['status']}") + print(" 本轮未对 FB_/BH_SALES_ 做任何修改") + + print("\n" + "=" * 64) + print("P0-2 数据清洗完成(步骤1-4 已提交; 步骤5 仅只读)") + print("=" * 64) + conn.close() + + +if __name__ == "__main__": + main() diff --git a/frontend/src/views/MapCanvas.vue b/frontend/src/views/MapCanvas.vue index 7ac37178..08c3ee7c 100644 --- a/frontend/src/views/MapCanvas.vue +++ b/frontend/src/views/MapCanvas.vue @@ -781,7 +781,7 @@ async function rollback(verId: number) { async function loadObjectiveLevels() { if (!selectedMap.value) return try { - const r: any = await api.get(`/maps/${selectedMap.value}/review`) + const r: any = await api.get(`/maps/${selectedMap.value}/review?level=strategic`) for (const key of Object.keys(objectiveLevels)) delete objectiveLevels[key] for (const key of Object.keys(kpiProgressMap)) delete kpiProgressMap[key] const dims = r.dimensions || [] @@ -851,7 +851,7 @@ async function showPlanList(dimKey: string, oi: number, obj: any) { planPopupObjIdx.value = oi planPopupList.value = [] try { - const r: any = await api.get(`/maps/${selectedMap.value}/review`) + const r: any = await api.get(`/maps/${selectedMap.value}/review?level=strategic`) const dims = r.dimensions || [] const allPlans = r.action_plans || [] for (const dim of dims) {