feat: bot-bridge数据通道 — POST /kpi-result回填KPI+触发预警+MPM自动回填脚本
- 新增 POST /api/cma/bot-bridge/kpi-result: (entity_id,kpi_code)查KPI, 写入KPIValue(source_type=bot, data_status=verified), 复用run_alert_check 触发预警(自动联动行动计划), 返回new_alerts; KPI不存在/缺字段返回错误 - 修复verify引擎_get_kpi_current_value排序: 按calculated_at取最新值, bot回填值可被auto-verify读到 (修复2026H1字符串排序遮蔽月值问题) - 新增scripts/bot_bridge_push.py: 财务Bot MPM结果自动回填(MPM字段→KPI 映射), 支持--mpm/--mpm-file/单KPI直推/--ping
This commit is contained in:
@@ -60,7 +60,7 @@ def _get_kpi_current_value(db: Session, kpi_id: int, entity_id: int = None) -> O
|
||||
latest = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi_id,
|
||||
KPIValue.data_status == "verified",
|
||||
).order_by(KPIValue.period.desc()).first()
|
||||
).order_by(KPIValue.calculated_at.desc(), KPIValue.period.desc()).first()
|
||||
return {
|
||||
"kpi_id": kpi.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
@@ -403,6 +403,123 @@ def receive_mpm_result(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/kpi-result")
|
||||
def push_kpi_result(
|
||||
data: dict,
|
||||
bridge_bot: str = Depends(verify_bridge_token),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Bot分析结果回填KPI值 + 触发预警(PRD第七部分 bot-bridge数据通道)
|
||||
|
||||
入参:
|
||||
entity_id (int, 默认1): 企业实体ID
|
||||
kpi_code (str, 必填): KPI编码
|
||||
period (str, 必填): 期间 YYYY-MM
|
||||
value (num, 必填): 实际值
|
||||
source (str, 默认finance-bot): 来源Bot标识
|
||||
remark (str, 可选): 备注
|
||||
|
||||
行为:
|
||||
1. 按(entity_id, kpi_code)查KPI → 不存在返回错误
|
||||
2. 写入KPIValue (source_type=bot, data_status=verified) — 与auto-verify引擎兼容
|
||||
3. 调用 run_alert_check 触发阈值预警 → 返回 new_alerts
|
||||
"""
|
||||
entity_id = data.get("entity_id", 1)
|
||||
kpi_code = data.get("kpi_code")
|
||||
period = data.get("period")
|
||||
value = data.get("value")
|
||||
source = data.get("source", "finance-bot")
|
||||
remark = data.get("remark")
|
||||
|
||||
if not kpi_code:
|
||||
raise HTTPException(400, "缺少必填字段: kpi_code")
|
||||
if not period:
|
||||
raise HTTPException(400, "缺少必填字段: period")
|
||||
if value is None or value == "":
|
||||
raise HTTPException(400, "缺少必填字段: value")
|
||||
try:
|
||||
actual_value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(400, f"value不是有效数值: {value!r}")
|
||||
|
||||
# 1. 按(entity_id, kpi_code)查KPI
|
||||
kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.entity_id == entity_id,
|
||||
KPIDefinition.kpi_code == kpi_code,
|
||||
).first()
|
||||
if not kpi:
|
||||
raise HTTPException(404, f"KPI {kpi_code} 不存在 (entity_id={entity_id})")
|
||||
|
||||
# 2. 写入KPIValue(同period已存在则更新,幂等upsert)
|
||||
existing = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi.id,
|
||||
KPIValue.period == period,
|
||||
).first()
|
||||
if existing:
|
||||
existing.actual_value = actual_value
|
||||
existing.source_type = "bot"
|
||||
existing.source_batch = source
|
||||
existing.data_status = "verified"
|
||||
existing.calculated_at = datetime.now()
|
||||
if remark:
|
||||
existing.remark = remark
|
||||
val = existing
|
||||
else:
|
||||
val = KPIValue(
|
||||
kpi_id=kpi.id,
|
||||
period=period,
|
||||
actual_value=actual_value,
|
||||
source_type="bot",
|
||||
source_batch=source,
|
||||
data_status="verified",
|
||||
calculated_at=datetime.now(),
|
||||
remark=remark,
|
||||
)
|
||||
db.add(val)
|
||||
db.commit()
|
||||
db.refresh(val)
|
||||
|
||||
# 3. 触发预警检查(复用alert_generator引擎,yellow/red自动联动行动计划)
|
||||
from scripts.alert_generator import run_alert_check
|
||||
new_count = run_alert_check(db, period)
|
||||
|
||||
# 收集本次写入值直接触发的预警(kpi_value_id关联)
|
||||
new_alerts = []
|
||||
if new_count:
|
||||
triggered = db.query(KPIAlert).filter(
|
||||
KPIAlert.kpi_value_id == val.id,
|
||||
).order_by(KPIAlert.created_at.desc()).all()
|
||||
new_alerts = [
|
||||
{
|
||||
"id": a.id,
|
||||
"kpi_id": a.kpi_id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"level": a.alert_level,
|
||||
"message": a.alert_message,
|
||||
"action_plan_id": a.action_plan_linked_id,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
} for a in triggered
|
||||
]
|
||||
|
||||
logger.info(
|
||||
f"[bot-bridge] KPI回填: {kpi.kpi_code}@{period}={actual_value} "
|
||||
f"source={source} entity={entity_id} alerts={new_count}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"kpi_value_id": val.id,
|
||||
"kpi_code": kpi.kpi_code,
|
||||
"kpi_name": kpi.kpi_name,
|
||||
"period": period,
|
||||
"value": actual_value,
|
||||
"source": source,
|
||||
"new_alerts_count": len(new_alerts),
|
||||
"new_alerts": new_alerts,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/verify/{action_plan_id}")
|
||||
def verify_action_plan(
|
||||
action_plan_id: int,
|
||||
|
||||
Reference in New Issue
Block a user