fix(alerts): enforce caliber ruling R5

- alert_entity_scope: add is_attributable_period / guard_alert_period (R5) and entity_alert_guard (R1, any write path)
- alert_rules.run_all_alert_checks: skip non-YYYY-MM period alert rows (R5)
- bot_bridge / bot_bridge_v2: R1 demo-entity guard + R5 period guard on write paths
- tests: new R5 suite (10 cases)

Not pushed / not deployed (release gate held by 任总).
This commit is contained in:
Hermes CI Fix
2026-09-19 10:42:09 +08:00
parent 3beec14d96
commit 5a99ed87e0
5 changed files with 347 additions and 6 deletions
+19 -2
View File
@@ -15,7 +15,8 @@ from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import KPIDefinition, KPIValue, KPIAlert, OperationLog
# 裁定 R1(2026-09-19 财务口径):demo 账套不参与预警生成/遍历(单一事实源)
from app.utils.alert_entity_scope import is_excluded_entity
# 裁定 R5(2026-09-19 财务口径):期间非规整月 YYYY-MM → 该 kpi_alerts 行不入库(生成侧守卫)
from app.utils.alert_entity_scope import guard_alert_period, is_excluded_entity
logger = logging.getLogger("alert_rules")
@@ -273,6 +274,9 @@ def run_all_alert_checks(db: Session = Depends(get_db), entity_id: int = Depends
value_cache = {}
alerts_generated = 0
# R5(裁定 2026-09-19):期间非规整月(含伪期间 2026-H1/H2)→ 该行不入库,跳过并记数
skipped_non_month_period = 0
skipped_non_month_period_kpis = []
for rule in rules:
try:
@@ -341,6 +345,14 @@ def run_all_alert_checks(db: Session = Depends(get_db), entity_id: int = Depends
alert_level, alert_message = _check_trend(kpi.id, value, "down", params, db)
if alert_level and alert_level != "green":
# R5 生成侧守卫(裁定 2026-09-19):期间非规整月 YYYY-MM → 该 kpi_alerts 行不入库。
# 注:`latest_value` 按 period 字符串倒序取,`2026-H1` 会排在 `2026-09` 之前 →
# 伪期间确实可能被取为「最新值」,此处是伪期间入库唯一的拦截点。
if not guard_alert_period(period, source="alert_rules.run_all_alert_checks",
detail="kpi=%s rule_id=%s" % (kpi.kpi_code, rule.id)):
skipped_non_month_period += 1
skipped_non_month_period_kpis.append("%s[%s]" % (kpi.kpi_code, period))
continue
# 检查是否已有相同预警
existing_alert = db.query(KPIAlert).filter(
KPIAlert.kpi_id == rule.kpi_id,
@@ -366,7 +378,12 @@ def run_all_alert_checks(db: Session = Depends(get_db), entity_id: int = Depends
continue
db.commit()
return {"message": f"预警检查完成: 生成{alerts_generated}条", "generated": alerts_generated}
if skipped_non_month_period:
logger.info("R5 期间守卫:本次跳过 %s 条不可归期预警(%s)",
skipped_non_month_period, skipped_non_month_period_kpis)
return {"message": f"预警检查完成: 生成{alerts_generated}条", "generated": alerts_generated,
"skipped_non_month_period": skipped_non_month_period,
"skipped_non_month_period_kpis": skipped_non_month_period_kpis}
@router.get("/dynamic-thresholds")