feat: AI事前预警 — 现金流预测+预警扩展+准确率+情景建议

This commit is contained in:
Hermes CI Fix
2026-07-21 18:22:04 +08:00
parent 270d7758f8
commit c4e91f28ef
9 changed files with 1101 additions and 6 deletions
+140 -1
View File
@@ -25,6 +25,7 @@ class AlertRule(Base):
id = Column(Integer, primary_key=True, index=True)
kpi_id = Column(Integer, nullable=False, comment="关联KPI ID")
rule_type = Column(String(30), nullable=False, comment="static/dynamic/trend_up/trend_down")
trigger_on = Column(String(20), default="actual", comment="actual/forecast/both — 实际值/预测值/两者触发")
enabled = Column(Integer, default=1, comment="1启用 0禁用")
params = Column(JSON, nullable=True, comment="规则参数")
# static: {"green": ">=90", "yellow": ">=80", "red": "<80"}
@@ -98,6 +99,7 @@ def create_alert_rule(data: dict, db: Session = Depends(get_db), user=Depends(re
"""创建预警规则"""
kpi_id = data.get("kpi_id")
rule_type = data.get("rule_type", "static")
trigger_on = data.get("trigger_on", "actual")
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi:
@@ -108,6 +110,7 @@ def create_alert_rule(data: dict, db: Session = Depends(get_db), user=Depends(re
rule = AlertRule(
kpi_id=kpi_id,
rule_type=rule_type,
trigger_on=trigger_on,
enabled=data.get("enabled", 1),
params=data.get("params"),
)
@@ -132,7 +135,7 @@ def update_alert_rule(rule_id: int, data: dict, db: Session = Depends(get_db)):
if not rule:
raise HTTPException(404, "预警规则不存在")
for field in ("rule_type", "enabled", "params"):
for field in ("rule_type", "trigger_on", "enabled", "params"):
if field in data:
setattr(rule, field, data[field])
db.commit()
@@ -477,3 +480,139 @@ def _eval_threshold(value: float, threshold_str: str, invert: bool = False) -> b
return False
except (ValueError, TypeError):
return False
# ============================================================
# 预测值检查 + 情景建议
# ============================================================
def _check_forecast_alerts(db: Session) -> int:
"""检查未来7天预测值是否超限 — 针对trigger_on='forecast''both'的规则"""
from app.utils.cash_forecast_engine import forecast_cash_flow, generate_scenario_suggestion
from app.models import CashForecast
rules = db.query(AlertRule).filter(
AlertRule.enabled == 1,
AlertRule.trigger_on.in_(["forecast", "both"]),
).all()
if not rules:
return 0
alerts_generated = 0
rule_kpi_cache = {}
for rule in rules:
try:
kpi = rule_kpi_cache.get(rule.kpi_id)
if kpi is None:
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == rule.kpi_id).first()
if kpi:
rule_kpi_cache[rule.kpi_id] = kpi
if not kpi:
continue
entity_id = kpi.entity_id or 1
# 获取最新的预测
latest_forecasts = db.query(CashForecast).filter(
CashForecast.entity_id == entity_id,
).order_by(CashForecast.forecast_date.asc()).limit(7).all()
if not latest_forecasts:
# 没有已有预测,执行一次实时预测
from app.utils.cash_forecast_engine import save_forecast_to_db
result = forecast_cash_flow(entity_id, db)
try:
save_forecast_to_db(entity_id, result, db)
except:
pass
latest_forecasts = db.query(CashForecast).filter(
CashForecast.entity_id == entity_id,
).order_by(CashForecast.forecast_date.asc()).limit(7).all()
if not latest_forecasts:
continue
# 检查预测值是否超限
params = rule.params or {}
params["kpi"] = kpi
for forecast in latest_forecasts:
value = forecast.predicted_cash
if value is None:
continue
alert_level, alert_message = _check_static(value, params, kpi)
if alert_level and alert_level != "green":
# 生成情景建议
sug_type = "cash_critical" if alert_level == "red" else "cash_low"
sug = generate_scenario_suggestion(
sug_type, kpi.kpi_name,
{"expected_receivables": 20, "forecast_date": forecast.forecast_date.isoformat()}
)
suggestion_text = f"{sug['title']}{sug['description']}\\n建议行动:{''.join(sug['actions'])}"
existing = db.query(KPIAlert).filter(
KPIAlert.kpi_id == rule.kpi_id,
KPIAlert.alert_type == "forecast",
KPIAlert.alert_level == alert_level,
KPIAlert.alert_message == alert_message,
KPIAlert.status == "pending",
).first()
if not existing:
alert = KPIAlert(
kpi_id=rule.kpi_id,
alert_level=alert_level,
alert_message=alert_message,
alert_type="forecast",
suggestion=suggestion_text,
status="pending",
)
db.add(alert)
alerts_generated += 1
except Exception as e:
logger.error(f"预测值预警检查失败: rule_id={rule.id}, error={e}")
continue
db.commit()
return alerts_generated
@router.post("/check-forecast")
def run_forecast_alert_check(db: Session = Depends(get_db)):
"""执行预测值预警检查 — 检查未来7天预测值是否超限"""
generated = _check_forecast_alerts(db)
return {"message": f"预测值预警检查完成: 生成{generated}", "generated": generated}
@router.post("/generate-suggestions")
def generate_alert_suggestions(db: Session = Depends(get_db)):
"""为所有未处理的预警生成情景建议"""
from app.utils.cash_forecast_engine import generate_scenario_suggestion
pending = db.query(KPIAlert).filter(
KPIAlert.status == "pending",
KPIAlert.suggestion.is_(None),
).all()
updated = 0
for alert in pending:
try:
sug_type = "cash_critical" if alert.alert_level == "red" else "cash_low"
if alert.alert_type == "forecast":
sug_type = "cash_critical" if alert.alert_level == "red" else "cash_low"
else:
sug_type = "cash_low"
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == alert.kpi_id).first()
kpi_name = kpi.kpi_name if kpi else "未知KPI"
sug = generate_scenario_suggestion(sug_type, kpi_name, {
"alert_level": alert.alert_level,
"alert_message": alert.alert_message,
})
alert.suggestion = f"{sug['title']}{sug['description']}\\n建议行动:{''.join(sug['actions'])}"
updated += 1
except Exception as e:
logger.error(f"生成建议失败: alert_id={alert.id}, error={e}")
db.commit()
return {"message": f"已为{updated}条预警生成情景建议", "updated": updated}