Files
cma-management/backend/scripts/verify_bep_threshold_fix.py
T

116 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")