fix: 数据治理P0 规则4前端level过滤+数据清洗

This commit is contained in:
Hermes CI Fix
2026-08-15 21:26:27 +08:00
parent 266277d121
commit 5a196dc105
2 changed files with 143 additions and 2 deletions
@@ -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_071id=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()
+2 -2
View File
@@ -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) {