From 54e39b54933c0088746b3a8d7d074b0f57f37e68 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Tue, 8 Sep 2026 15:59:41 +0800 Subject: [PATCH] =?UTF-8?q?chore(kpi):=20BEP=E9=98=88=E5=80=BC+=E7=8E=B0?= =?UTF-8?q?=E9=87=91=E5=8F=A3=E5=BE=84=E4=BF=AE=E5=A4=8D=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=20verify=5Fbep=5Fthreshold=5Ffix.py=20(fin-f?= =?UTF-8?q?ix-bep-threshold-20260908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/verify_bep_threshold_fix.py | 115 ++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 backend/scripts/verify_bep_threshold_fix.py diff --git a/backend/scripts/verify_bep_threshold_fix.py b/backend/scripts/verify_bep_threshold_fix.py new file mode 100644 index 00000000..54c3c380 --- /dev/null +++ b/backend/scripts/verify_bep_threshold_fix.py @@ -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")