feat(alerts): 新增客户集中度预警规则 customer_concentration + 预测偏差规则启用/联动每日预警生成

- alert_rules: 新增 rule_type=customer_concentration(单一/前N客户收入占比超阈值触发,黄=超阈值/红=阈值x倍数)
  数据源 zxt_masterbill 客户(往来单位)收入聚合,unit_id<=0 占位剔除;无客户数据可回退 KPI 口径
  复用现有 KPIAlert 生成链路:check-all 内联分支 + 专用端点 POST /run-customer-concentration
- alert_generator(每日9:00): 新增规则引擎步骤,复用上述检查函数跑 forecast_deviation/customer_concentration
- AlertList.vue: 规则类型筛选/编辑项 + 参数表单 + 客户集中度检查按钮
- 测试: tests/test_customer_concentration_rule.py 17 条(规则注册/聚合/双数据源/黄红灯/去重/check-all)
- 验证脚本: scripts/verify_forecast_deviation_rules_e2e.py / verify_customer_concentration_rule_e2e.py
This commit is contained in:
Hermes CI Fix
2026-09-10 09:56:50 +08:00
parent f8688713db
commit 9822fbd9ba
7 changed files with 866 additions and 8 deletions
+42 -2
View File
@@ -190,6 +190,41 @@ def run_alert_check(db_session, period: str = None) -> int:
return new_count
def run_rule_engine_checks(db_session) -> dict:
"""预警规则引擎(rule_type 驱动)— 预测偏差 + 客户集中度(P2 2026-09-10)
复用 alert_rules 的检查端点实现(FastAPI 路由函数=带 Depends 默认值的普通函数,
显式传参即可复用,零逻辑复制),按存在规则的账套逐个执行。
任何单点失败只记录日志,不影响静态阈值主流程。
"""
from app.api.alert_rules import (
AlertRule, run_forecast_deviation_check, run_customer_concentration_check,
)
stats = {"forecast_deviation": 0, "customer_concentration": 0}
try:
rows = db_session.query(AlertRule.entity_id).filter(
AlertRule.enabled == 1,
AlertRule.rule_type.in_(("forecast_deviation", "customer_concentration")),
).distinct().all()
except Exception as e:
logger.error(f"规则引擎检查跳过(规则查询失败): {e}")
return stats
for (eid,) in rows:
for key, fn in (("forecast_deviation", run_forecast_deviation_check),
("customer_concentration", run_customer_concentration_check)):
try:
r = fn(db=db_session, entity_id=eid)
n = int(r.get("generated", 0) or 0)
stats[key] += n
if n:
logger.info(f" 规则预警 entity={eid} {key}: {n}条")
except Exception as e:
logger.error(f"规则检查失败 entity={eid} {key}: {e}")
return stats
def generate_and_push(db_session) -> dict:
"""生成预警并推送,返回统计"""
period = datetime.now().strftime("%Y-%m")
@@ -202,6 +237,9 @@ def generate_and_push(db_session) -> dict:
new_count = run_alert_check(db_session, period)
logger.info(f"预警检查完成: 新增 {new_count} 条")
# 1b. 规则引擎检查(预测偏差 / 客户集中度,P2 2026-09-10)
rule_stats = run_rule_engine_checks(db_session)
# 2. 推送
pushed = 0
if new_count > 0:
@@ -212,7 +250,8 @@ def generate_and_push(db_session) -> dict:
except Exception as e:
logger.error(f"推送失败: {e}")
return {"period": period, "new_alerts": new_count, "pushed": pushed, "cleaned_green": cleaned}
return {"period": period, "new_alerts": new_count, "pushed": pushed,
"cleaned_green": cleaned, "rule_engine": rule_stats}
if __name__ == "__main__":
@@ -224,6 +263,7 @@ if __name__ == "__main__":
db = get_session_local()()
try:
result = generate_and_push(db)
print(f"预警检查: {result['new_alerts']}条新预警 / {result['pushed']}条已推送")
print(f"预警检查: {result['new_alerts']}条新预警 / {result['pushed']}条已推送"
f" / 规则引擎{result['rule_engine']}")
finally:
db.close()