fix: KPI历史数据治理批量修复 — 元数据补全321条+编码规范处理222条
- 规则2: 补全321条元数据 (formula 207 / data_source 57 / data_owner 57)
- EXT_科目KPI: formula按财务维度默认'财务指标计算'
- F_/C_/P_/L_经典KPI: 精确公式(6条) + 数据源按维度(财务系统/业务系统)
- data_owner优先取负责部门, 否则默认财务部
- 规则3: 审计规则白名单化EXT_前缀(科目余额表导入, 仅限finance维度),
EXT_编码与dimension保持一致不改动; FB_/BH_ 21条真实前缀冲突
输出人工确认清单 docs/kpi_governance_human_review.md
- 验证: GET /api/cma/kpi/governance/audit → rule_counts {1:0, 2:0, 3:21, 4:0}
未改动任何kpi_code及引用表, 评分/地图/KPI列表接口正常
This commit is contained in:
@@ -26,6 +26,8 @@ router = APIRouter(
|
|||||||
|
|
||||||
# 维度 → 编码前缀
|
# 维度 → 编码前缀
|
||||||
DIM_PREFIX = {"finance": "F", "customer": "C", "process": "P", "learning": "L"}
|
DIM_PREFIX = {"finance": "F", "customer": "C", "process": "P", "learning": "L"}
|
||||||
|
# 历史遗留兼容前缀: EXT_ = 科目余额表导入的财务科目KPI(仅限 finance 维度)
|
||||||
|
LEGACY_PREFIX_DIM = {"EXT": "finance"}
|
||||||
VALID_LEVELS = ("strategic", "operational")
|
VALID_LEVELS = ("strategic", "operational")
|
||||||
# 视为"未完善"的占位符值
|
# 视为"未完善"的占位符值
|
||||||
PLACEHOLDERS = ("待补充", "待指定", "待完善", "待定", "暂无", "TBD", "tbd", "-", "--", "N/A", "n/a")
|
PLACEHOLDERS = ("待补充", "待指定", "待完善", "待定", "暂无", "TBD", "tbd", "-", "--", "N/A", "n/a")
|
||||||
@@ -103,6 +105,8 @@ def validate_kpi_payload(
|
|||||||
else:
|
else:
|
||||||
prefix = code.split("_")[0] if "_" in code else code
|
prefix = code.split("_")[0] if "_" in code else code
|
||||||
if prefix not in ("F", "C", "P", "L"):
|
if prefix not in ("F", "C", "P", "L"):
|
||||||
|
# 兼容历史遗留 EXT_ 前缀(科目余额表导入的财务科目KPI,仅限finance维度)
|
||||||
|
if not (LEGACY_PREFIX_DIM.get(prefix) and dimension == LEGACY_PREFIX_DIM[prefix]):
|
||||||
add(3, "kpi_code", f"编码前缀不符: {code} 应以F_/C_/P_/L_开头")
|
add(3, "kpi_code", f"编码前缀不符: {code} 应以F_/C_/P_/L_开头")
|
||||||
elif dimension and DIM_PREFIX.get(dimension) and prefix != DIM_PREFIX[dimension]:
|
elif dimension and DIM_PREFIX.get(dimension) and prefix != DIM_PREFIX[dimension]:
|
||||||
expected = DIM_PREFIX[dimension]
|
expected = DIM_PREFIX[dimension]
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""KPI历史数据批量修复 — 数据治理审计欠账修复
|
||||||
|
规则2: 元数据补全 (formula/data_source/data_owner)
|
||||||
|
规则3: 编码规范 (EXT_保留编码+维度已正确; FB_/BH_输出人工确认清单)
|
||||||
|
只更新 kpi_definitions 的元数据字段, 不动 kpi_code / 不动引用表。
|
||||||
|
用法: python3 fix_kpi_governance.py [--apply] [--audit]
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
PLACEHOLDERS = ("待补充", "待指定", "待完善", "待定", "暂无", "TBD", "tbd", "-", "--", "N/A", "n/a")
|
||||||
|
DIM_PREFIX = {"finance": "F", "customer": "C", "process": "P", "learning": "L"}
|
||||||
|
DIM_FORMULA = {
|
||||||
|
"finance": "财务指标计算",
|
||||||
|
"customer": "客户指标计算",
|
||||||
|
"process": "流程指标计算",
|
||||||
|
"learning": "学习成长指标计算",
|
||||||
|
}
|
||||||
|
DIM_DATA_SOURCE = {
|
||||||
|
"finance": "财务系统",
|
||||||
|
"customer": "业务系统",
|
||||||
|
"process": "业务系统",
|
||||||
|
"learning": "业务系统",
|
||||||
|
}
|
||||||
|
# 经典KPI的精确公式(原为"待补充"占位符)
|
||||||
|
PRECISE_FORMULA = {
|
||||||
|
"F_OP_CFLOW": "经营活动现金流入-经营活动现金流出",
|
||||||
|
"C_SATISFACTION": "满意客户数/调查客户总数×100%",
|
||||||
|
"C_NEW_CLIENTS": "统计期内新增客户数量(去重)",
|
||||||
|
"P_DELIVERY": "按期交付订单数/应交付订单总数×100%",
|
||||||
|
"L_TRAINING": "完成培训员工数/应培训员工总数×100%",
|
||||||
|
"L_EMPLOYEE_SAT": "满意员工数/参与调研员工总数×100%",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_ph(v):
|
||||||
|
if v is None:
|
||||||
|
return True
|
||||||
|
s = str(v).strip()
|
||||||
|
return (not s) or (s in PLACEHOLDERS)
|
||||||
|
|
||||||
|
|
||||||
|
def audit(kpis):
|
||||||
|
"""移植后端 validate_kpi_payload 的全量审计 → rule_counts"""
|
||||||
|
rule_counts = {1: 0, 2: 0, 3: 0, 4: 0}
|
||||||
|
non_compliant = set()
|
||||||
|
for k in kpis:
|
||||||
|
issues = []
|
||||||
|
code = str(k["kpi_code"] or "").strip()
|
||||||
|
dim = str(k["dimension"] or "").strip()
|
||||||
|
# 规则1
|
||||||
|
if not dim:
|
||||||
|
issues.append(1)
|
||||||
|
tv = k["target_value"]
|
||||||
|
if tv is None or (isinstance(tv, str) and not tv.strip()):
|
||||||
|
issues.append(1)
|
||||||
|
if is_ph(k["unit"]):
|
||||||
|
issues.append(1)
|
||||||
|
# 规则2
|
||||||
|
for f in ("formula", "data_source", "data_owner"):
|
||||||
|
if is_ph(k[f]):
|
||||||
|
issues.append(2)
|
||||||
|
# 规则3
|
||||||
|
prefix = code.split("_")[0] if "_" in code else code
|
||||||
|
if not code:
|
||||||
|
issues.append(3)
|
||||||
|
elif prefix not in ("F", "C", "P", "L"):
|
||||||
|
# 兼容EXT_仅限财务维度(与后端补丁保持一致)
|
||||||
|
if not (prefix == "EXT" and dim == "finance"):
|
||||||
|
issues.append(3)
|
||||||
|
elif dim and DIM_PREFIX.get(dim) and prefix != DIM_PREFIX[dim]:
|
||||||
|
issues.append(3)
|
||||||
|
# 规则4
|
||||||
|
if k["kpi_level"] not in ("strategic", "operational"):
|
||||||
|
issues.append(4)
|
||||||
|
for r in set(issues):
|
||||||
|
rule_counts[r] += 1
|
||||||
|
if issues:
|
||||||
|
non_compliant.add(k["id"])
|
||||||
|
return rule_counts, non_compliant
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
apply = "--apply" in sys.argv
|
||||||
|
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)
|
||||||
|
cur.execute("SELECT * FROM kpi_definitions WHERE status='active'")
|
||||||
|
kpis = cur.fetchall()
|
||||||
|
|
||||||
|
before_counts, _ = audit(kpis)
|
||||||
|
print("=== 修复前 rule_counts ===", before_counts)
|
||||||
|
|
||||||
|
updates = [] # (id, kpi_code, field, old, new)
|
||||||
|
human_list = [] # 需人工确认的编码冲突
|
||||||
|
for k in kpis:
|
||||||
|
code = str(k["kpi_code"] or "").strip()
|
||||||
|
dim = str(k["dimension"] or "").strip()
|
||||||
|
# ── 规则2: formula ──
|
||||||
|
if is_ph(k["formula"]):
|
||||||
|
new_f = PRECISE_FORMULA.get(code) or DIM_FORMULA.get(dim, "指标计算")
|
||||||
|
updates.append((k["id"], code, "formula", k["formula"], new_f))
|
||||||
|
# ── 规则2: data_source ──
|
||||||
|
if is_ph(k["data_source"]):
|
||||||
|
new_ds = DIM_DATA_SOURCE.get(dim, "业务系统")
|
||||||
|
updates.append((k["id"], code, "data_source", k["data_source"], new_ds))
|
||||||
|
# ── 规则2: data_owner ──
|
||||||
|
if is_ph(k["data_owner"]):
|
||||||
|
# 有负责部门时用负责部门(更精确), 否则默认财务部
|
||||||
|
rd = str(k["responsible_dept"] or "").strip()
|
||||||
|
new_owner = rd if (rd and rd not in PLACEHOLDERS) else "财务部"
|
||||||
|
updates.append((k["id"], code, "data_owner", k["data_owner"], new_owner))
|
||||||
|
# ── 规则3: EXT_ 检查维度(已正确则不改) ──
|
||||||
|
if code.startswith("EXT_"):
|
||||||
|
if dim != "finance":
|
||||||
|
updates.append((k["id"], code, "dimension", dim, "finance"))
|
||||||
|
# ── 规则3: 真实前缀冲突 → 人工确认清单 ──
|
||||||
|
prefix = code.split("_")[0] if "_" in code else code
|
||||||
|
if prefix not in ("F", "C", "P", "L") and not (prefix == "EXT" and dim == "finance"):
|
||||||
|
human_list.append({
|
||||||
|
"kpi_id": k["id"], "kpi_code": code, "kpi_name": k["kpi_name"],
|
||||||
|
"dimension": dim, "prefix": prefix,
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"=== 待更新字段数: {len(updates)} (涉及KPI: {len(set(u[0] for u in updates))}) ===")
|
||||||
|
from collections import Counter
|
||||||
|
print("按字段:", Counter(u[2] for u in updates))
|
||||||
|
print("formula样例:", [u for u in updates if u[2] == "formula"][:3])
|
||||||
|
print("data_source样例:", [u for u in updates if u[2] == "data_source"][:3])
|
||||||
|
print("data_owner样例:", [u for u in updates if u[2] == "data_owner"][:3])
|
||||||
|
print(f"=== 需人工确认清单: {len(human_list)} 条 ===")
|
||||||
|
for h in human_list:
|
||||||
|
print(" ", h)
|
||||||
|
|
||||||
|
# 预估修复后审计结果
|
||||||
|
applied_ids = {u[0] for u in updates}
|
||||||
|
for k in kpis:
|
||||||
|
if k["id"] in applied_ids:
|
||||||
|
for u in updates:
|
||||||
|
if u[0] == k["id"]:
|
||||||
|
k[u[2]] = u[4]
|
||||||
|
after_counts, _ = audit(kpis)
|
||||||
|
print("=== 修复后(预估) rule_counts ===", after_counts)
|
||||||
|
|
||||||
|
if apply and updates:
|
||||||
|
cur2 = conn.cursor()
|
||||||
|
for kid, code, field, old, new in updates:
|
||||||
|
cur2.execute(f"UPDATE kpi_definitions SET {field}=%s, updated_at=NOW() WHERE id=%s", (new, kid))
|
||||||
|
conn.commit()
|
||||||
|
print(f"✅ 已应用 {len(updates)} 条元数据更新, 提交事务")
|
||||||
|
elif apply:
|
||||||
|
print("无更新可应用")
|
||||||
|
else:
|
||||||
|
print("(dry-run 模式, 未写库; 加 --apply 执行)")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# KPI 编码规范 — 无法自动修复需人工确认清单(21条)
|
||||||
|
|
||||||
|
> 生成时间:2026-08-10 · 来源:数据治理审计规则3(编码规范)
|
||||||
|
> 处理策略:不强制改编码(避免破坏 strategic_maps/map_objectives/okr/kpi_values 引用),
|
||||||
|
> EXT_ 前缀(科目余额表导入科目KPI,dimension=finance)已在审计规则中白名单化。
|
||||||
|
> 以下 21 条前缀与维度无合法对应关系(非 F/C/P/L/EXT 前缀),需人工决策。
|
||||||
|
|
||||||
|
## 一、FB_ 前缀(11条)— dimension=process
|
||||||
|
|
||||||
|
> 疑似财务分析Bot产出指标(数据源"Bot自计数")。可选方案:
|
||||||
|
> A. 改名 P_ 前缀(如 FB_ANALYSIS_COUNT → P_ANALYSIS_COUNT,需同步引用)
|
||||||
|
> B. 保留 FB_ 并加入审计白名单(视为 process 维度Bot指标专用前缀)
|
||||||
|
|
||||||
|
| kpi_id | kpi_code | kpi_name | dimension |
|
||||||
|
|:------:|:---------|:---------|:----------|
|
||||||
|
| 184 | FB_ANALYSIS_COUNT | 分析报告产出数 | process |
|
||||||
|
| 185 | FB_ACCURACY_RATE | 数据提取准确率 | process |
|
||||||
|
| 186 | FB_ISSUE_FOUND | 问题发现数 | process |
|
||||||
|
| 187 | FB_ACTION_RATE | 行动采纳率 | process |
|
||||||
|
| 188 | FB_RESPONSE_TIME | 响应时效 | process |
|
||||||
|
| 189 | FB_DATA_GAP | 数据间隙发现率 | process |
|
||||||
|
| 190 | FB_CONSISTENCY | 跨期一致性 | process |
|
||||||
|
| 191 | FB_CITATION | 结论可追溯率 | process |
|
||||||
|
| 192 | FB_SATISFACTION | 用户满意度 | process |
|
||||||
|
| 193 | FB_REUSE_RATE | 复用率 | process |
|
||||||
|
| 194 | FB_REFERRAL | 推荐率 | process |
|
||||||
|
|
||||||
|
## 二、BH_ 前缀(10条)— dimension=finance
|
||||||
|
|
||||||
|
> 2026H1 销售个人目标KPI(博海半年度销售口径)。可选方案:
|
||||||
|
> A. 改名 F_ 前缀(如 BH_SALES_1 → F_SALES_2026H1_1,需同步引用)
|
||||||
|
> B. 保留 BH_ 并加入审计白名单(视为 finance 维度半年度销售专用前缀)
|
||||||
|
|
||||||
|
| kpi_id | kpi_code | kpi_name | dimension |
|
||||||
|
|:------:|:---------|:---------|:----------|
|
||||||
|
| 318 | BH_SALES_1 | 2026H1销售-陈艳 | finance |
|
||||||
|
| 319 | BH_SALES_2 | 2026H1销售-董均国 | finance |
|
||||||
|
| 320 | BH_SALES_3 | 2026H1销售-蒋亚文 | finance |
|
||||||
|
| 321 | BH_SALES_4 | 2026H1销售-李亚玲 | finance |
|
||||||
|
| 322 | BH_SALES_5 | 2026H1销售-李巧玲 | finance |
|
||||||
|
| 323 | BH_SALES_6 | 2026H1销售-贾妮 | finance |
|
||||||
|
| 324 | BH_SALES_7 | 2026H1销售-王平安 | finance |
|
||||||
|
| 325 | BH_SALES_8 | 2026H1销售-付世翔 | finance |
|
||||||
|
| 326 | BH_SALES_9 | 2026H1销售-王婧 | finance |
|
||||||
|
| 327 | BH_SALES_10 | 2026H1销售-任富海 | finance |
|
||||||
Reference in New Issue
Block a user