fix(alerts): enforce caliber rulings R1-R4 for alert generation

R1: exclude status=demo entities from alert traversal (alert_generator / deviation_engine / kpi_bep_deriver / alert_rules / cash engine)

R2: only generate alerts within the last 6 calendar months (incl. current)

R3: only regular month periods (YYYY-MM) participate

R4: decouple generation from push - empty notification_channels never blocks

Single source of truth: app/utils/alert_entity_scope.py
This commit is contained in:
Hermes CI Fix
2026-09-19 10:05:11 +08:00
parent d785a22efd
commit 3beec14d96
7 changed files with 641 additions and 21 deletions
+22 -1
View File
@@ -14,6 +14,10 @@ from typing import Optional
from app.database import get_session_local
from app.models import KPIDefinition, KPIValue, KPIAlert, BudgetPlan, SystemConfig
# 裁定 R1/R2/R3(2026-09-19 财务口径):demo 账套排除 / 期间窗口 / 规整月期间
from app.utils.alert_entity_scope import (
excluded_entity_ids, kpi_excluded, period_in_window, window_lower_period,
)
logger = logging.getLogger("cma.deviation")
@@ -311,16 +315,31 @@ def build_deviation_alert(db, kpi, period: str, entity_id: int = 1, min_rate: fl
def run_deviation_check(db_session, period: str = None) -> int:
"""运行差异预警检查,返回新增预警数(统一走 build_deviation_alert,P2-⑤)"""
"""运行差异预警检查,返回新增预警数(统一走 build_deviation_alert,P2-⑤)
2026-09-19 财务口径裁定 R1/R2/R3(`alert_entity_scope`):
· R1 —— `status='demo'` 的账套不参与遍历(demo 仅演示,不产生业务预警);
· R2/R3 —— 期间须为规整月 `YYYY-MM` 且落在窗口(当月往前 6 个自然月,含当月)内。
"""
if period is None:
period = datetime.now().strftime("%Y-%m")
if not period_in_window(period):
logger.warning(" 差异预警跳过(R2/R3): 期间 %s 非规整月或早于窗口下限 %s",
period, window_lower_period())
return 0
excluded = excluded_entity_ids(db_session)
kpis = db_session.query(KPIDefinition).filter(
KPIDefinition.status == "active"
).all()
new_count = 0
skipped_demo = 0
for kpi in kpis:
# R1(裁定 2026-09-19):demo 演示账套不参与预警遍历
if kpi_excluded(db_session, kpi, excluded):
skipped_demo += 1
continue
# 1. 差异预警:实际 vs 预算(统一逻辑)
result = build_deviation_alert(db_session, kpi, period)
if result["triggered"]:
@@ -375,6 +394,8 @@ def run_deviation_check(db_session, period: str = None) -> int:
new_count += 1
logger.info(f" 新增趋势预警 [{trend['level']}] {trend_alert_msg}")
if skipped_demo:
logger.info(" 差异预警跳过 demo 账套 KPI %s 项(R1,裁定 2026-09-19)", skipped_demo)
db_session.commit()
return new_count