chore(kpi): BEP阈值+现金口径修复验收脚本 verify_bep_threshold_fix.py (fin-fix-bep-threshold-20260908)

This commit is contained in:
Hermes CI Fix
2026-09-08 15:59:41 +08:00
parent 5bcc65b47f
commit 54e39b5493
+115
View File
@@ -0,0 +1,115 @@
"""BEP保本KPI阈值+现金口径修复验收脚本 — fin-fix-bep-threshold-20260908
验收:
1. kpi_definitions 457/458 阈值与财务口径一致(COVER: 100/85/85;GAP: 0/15/15),456 保持 NULL
2. 现金口径2条定义 F_BEP_REVENUE_CASH/F_BEP_COVER_CASH 存在(无阈值/100/85/85)
3. 2026-07 派生5条落库:账面 169.9/66.9/33.1 + 现金 121.0/93.9(±1%财务权威 121.4/93.5)
4. 告警闭环:575/576 = red pending(66.9<85, 33.1>15);577 = yellow pending(93.9>=85)
旧黄色 573/574 已 resolved
5. bep-card 端点:book.cover_level=red, cash.cover_level=yellow, derived 5 条
用法: venv/bin/python3 scripts/verify_bep_threshold_fix.py (需后端8010在线)
"""
import json
import os
import sys
import urllib.request
import pymysql
BASE = "http://127.0.0.1:8010"
passed = []
def check(name, cond, detail=""):
if cond:
passed.append(name)
print(f" PASS {name}")
else:
print(f" FAIL {name} {detail}")
sys.exit(1)
def _db_cfg():
"""从 backend/.env 读生产 DB 凭据(root@localhost 走 auth_socket,pymysql 直连须用应用账号 cma_user)"""
env = {}
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
if os.path.exists(env_path):
for line in open(env_path):
line = line.strip()
if line and "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
env[k] = v.strip().strip('"').strip("'")
return dict(
host=env.get("CMA_DB_HOST", "127.0.0.1"),
port=int(env.get("CMA_DB_PORT", "3306")),
user=env.get("CMA_DB_USER", "cma_user"),
password=env.get("CMA_DB_PASS", ""),
database=env.get("CMA_DB_NAME", "cma"),
charset="utf8mb4",
)
def db_q(sql):
conn = pymysql.connect(**_db_cfg())
try:
with conn.cursor() as cur:
cur.execute(sql)
return cur.fetchall()
finally:
conn.close()
def http_json(path, method="GET", payload=None, token=None):
headers = {}
if token:
headers["Authorization"] = "Bearer " + token
data = json.dumps(payload).encode() if payload is not None else None
if data:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
with urllib.request.urlopen(req) as r:
return json.loads(r.read().decode())
print("== 1. 库阈值 ==")
rows = db_q("SELECT id, kpi_code, threshold_green, threshold_yellow, threshold_red FROM kpi_definitions WHERE id IN (456,457,458)")
m = {r[0]: r for r in rows}
check("457 COVER 100/85/85", m[457][2:5] == (">=100", ">=85", "<85"), str(m[457]))
check("458 GAP 0/15/15", m[458][2:5] == ("<=0", "<=15", ">15"), str(m[458]))
check("456 REVENUE 无阈值", m[456][2] is None and m[456][3] is None and m[456][4] is None, str(m[456]))
print("== 2. 现金口径定义 ==")
rows = db_q("SELECT kpi_code, unit, threshold_green, threshold_yellow, threshold_red FROM kpi_definitions WHERE kpi_code IN ('F_BEP_REVENUE_CASH','F_BEP_COVER_CASH')")
cash = {r[0]: r for r in rows}
check("REVENUE_CASH 存在且无阈值", "F_BEP_REVENUE_CASH" in cash and cash["F_BEP_REVENUE_CASH"][1] == "万元" and cash["F_BEP_REVENUE_CASH"][2] is None, str(cash.get("F_BEP_REVENUE_CASH")))
check("COVER_CASH 存在 100/85/85", "F_BEP_COVER_CASH" in cash and cash["F_BEP_COVER_CASH"][1] == "%" and cash["F_BEP_COVER_CASH"][2:5] == (">=100", ">=85", "<85"), str(cash.get("F_BEP_COVER_CASH")))
print("== 3. 2026-07 派生5条 ==")
rows = db_q("SELECT k.kpi_code, v.actual_value, v.source_type FROM kpi_values v JOIN kpi_definitions k ON v.kpi_id=k.id WHERE v.period='2026-07' AND k.kpi_code LIKE 'F_BEP%' AND v.source_type='derived'")
vals = {r[0]: r[1] for r in rows}
exp_book = {"F_BEP_REVENUE": 169.9, "F_BEP_COVER": 66.9, "F_BEP_GAP": 33.1}
for code, ev in exp_book.items():
check(f"{code} = {ev}", vals.get(code) == ev, f"got {vals.get(code)}")
check("REVENUE_CASH ~121.4±1%", abs(vals.get("F_BEP_REVENUE_CASH", 0) - 121.4) / 121.4 < 0.01, f"got {vals.get('F_BEP_REVENUE_CASH')}")
check("COVER_CASH ~93.5±1%", abs(vals.get("F_BEP_COVER_CASH", 0) - 93.5) / 93.5 < 0.01, f"got {vals.get('F_BEP_COVER_CASH')}")
print("== 4. 告警闭环 ==")
rows = db_q("SELECT id, alert_level, status FROM kpi_alerts WHERE kpi_id IN (457,458,463) ORDER BY id")
alerts = {r[0]: (r[1], r[2]) for r in rows}
check("573 旧黄已resolved", alerts.get(573) == ("yellow", "resolved"), str(alerts.get(573)))
check("574 旧黄已resolved", alerts.get(574) == ("yellow", "resolved"), str(alerts.get(574)))
check("575 COVER red pending", alerts.get(575) == ("red", "pending"), str(alerts.get(575)))
check("576 GAP red pending", alerts.get(576) == ("red", "pending"), str(alerts.get(576)))
check("577 COVER_CASH yellow pending", alerts.get(577) == ("yellow", "pending"), str(alerts.get(577)))
print("== 5. bep-card 端点 ==")
login = http_json("/api/cma/auth/login", "POST", {"username": "admin", "password": "admin123", "entity_id": 1})
card = http_json("/api/cma/dashboard/bep-card", token=login["token"])
check("available", card.get("available") is True)
check("book red", card["book"]["cover_level"] == "red", str(card["book"]))
check("cash yellow", card["cash"]["cover_level"] == "yellow", str(card["cash"]))
check("derived 5条", len(card.get("derived", {})) == 5, str(list(card.get("derived", {}).keys())))
print(f"\nALL {len(passed)} CHECKS PASSED")