Files
cma-management/backend/app/api/bot_bridge.py
T

489 lines
16 KiB
Python

"""
CMA BOT API桥接层 — 供财务BOT/店研学BOT调用
无需用户登录,使用 BOT API Key 认证
"""
import os, json, logging
from fastapi import APIRouter, Depends, HTTPException, Query, Header
from sqlalchemy.orm import Session
from sqlalchemy import func, desc
from datetime import datetime
from typing import Optional
from app.database import get_db
from app.models import (
User, StrategicMap, KPIDefinition, KPITemplate, KPIValue,
DataSourceConfig, KPIAlert, OperationLog, NotificationChannel,
NotificationLog, RolePermission, ActionPlan, OrgNode,
StrategicMapVersion, MapObjective,
)
from app.models.budget_plan import BudgetPlan
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
logger = logging.getLogger("cma.bot_bridge")
router = APIRouter(prefix="/api/cma/bot", tags=["BOT桥接"])
# ── BOT API Key 配置 ──
_BOT_API_KEYS = {}
def _load_bot_keys():
global _BOT_API_KEYS
raw = os.getenv("CMA_BOT_API_KEYS", "")
if not raw:
_BOT_API_KEYS = {
"cma-bot-finance-2026": {"role": "finance", "name": "财务分析Bot", "dimension": "finance"},
"cma-bot-customer-2026": {"role": "business", "name": "客户分析Bot", "dimension": "customer"},
"cma-bot-process-2026": {"role": "it", "name": "流程分析Bot", "dimension": "process"},
"cma-bot-learning-2026": {"role": "ceo", "name": "战略分析Bot", "dimension": "learning"},
}
else:
try:
_BOT_API_KEYS = json.loads(raw)
except:
_BOT_API_KEYS = {}
_load_bot_keys()
def verify_bot_key(x_bot_key: str = Header(None, alias="X-BOT-KEY")):
if not x_bot_key or x_bot_key not in _BOT_API_KEYS:
raise HTTPException(401, "无效的BOT API Key")
bot_info = _BOT_API_KEYS[x_bot_key]
logger.info(f"BOT访问: {bot_info['name']} ({bot_info['role']})")
return bot_info
# ═══════════════ 通用工具 ═══════════════
def _float(v):
if v is None: return None
try: return float(v)
except: return None
def _safe_iso(dt):
if dt is None: return None
try: return dt.isoformat() if hasattr(dt, 'isoformat') else str(dt)
except: return None
def _model_dict(obj, fields: dict):
"""安全地将模型字段转为dict"""
result = {}
for key, attr in fields.items():
v = getattr(obj, attr, None)
if isinstance(v, float):
result[key] = _float(v)
else:
result[key] = v
return result
# ═══════════════ 端点 ═══════════════
@router.get("/ping")
def ping():
return {"status": "ok", "version": "1.0", "timestamp": datetime.now().isoformat()}
# ── 总览 ──
@router.get("/overview")
def bot_overview(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
"""系统总览 — BOT首选入口"""
return {
"bot": bot,
"timestamp": datetime.now().isoformat(),
"stats": {
"kpis_total": db.query(func.count(KPIDefinition.id)).filter(KPIDefinition.status == "active").scalar() or 0,
"alerts_open": db.query(func.count(KPIAlert.id)).filter(KPIAlert.status == "pending").scalar() or 0,
"maps_total": db.query(func.count(StrategicMap.id)).scalar() or 0,
"budget_plans": db.query(func.count(BudgetPlan.id)).scalar() or 0,
"action_plans_pending": db.query(func.count(ActionPlan.id)).filter(ActionPlan.status.in_(["pending", "in_progress"])).scalar() or 0,
"data_sources": db.query(func.count(DataSourceConfig.id)).scalar() or 0,
"users": db.query(func.count(User.id)).scalar() or 0,
"org_nodes": db.query(func.count(OrgNode.id)).scalar() or 0,
}
}
# ── KPI ──
@router.get("/kpis")
def bot_kpis(
dimension: Optional[str] = Query(None),
status: str = Query("active"),
limit: int = Query(200, le=1000),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
query = db.query(KPIDefinition).filter(KPIDefinition.status == status)
if dimension:
query = query.filter(KPIDefinition.dimension == dimension)
kpis = query.order_by(KPIDefinition.dimension, KPIDefinition.kpi_code).limit(limit).all()
results = []
for k in kpis:
latest = db.query(KPIValue).filter(KPIValue.kpi_id == k.id)\
.order_by(KPIValue.period.desc()).first()
results.append({
"id": k.id, "name": k.kpi_name, "code": k.kpi_code,
"dimension": k.dimension, "category": k.category,
"unit": k.unit, "formula": k.formula,
"frequency": k.frequency, "data_source_type": k.data_source_type,
"target_value": _float(k.target_value),
"threshold_green": k.threshold_green,
"threshold_yellow": k.threshold_yellow,
"threshold_red": k.threshold_red,
"responsible_dept": k.responsible_dept, "owner": k.responsible_user,
"objective": k.objective, "description": k.description,
"latest_value": _float(latest.actual_value) if latest else None,
"latest_period": latest.period if latest else None,
"status": k.status,
})
return {"total": len(results), "items": results}
@router.get("/kpis/{kpi_id}/history")
def bot_kpi_history(
kpi_id: int, limit: int = Query(12, le=60),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi:
raise HTTPException(404, "KPI不存在")
values = db.query(KPIValue).filter(KPIValue.kpi_id == kpi_id)\
.order_by(KPIValue.period.desc()).limit(limit).all()
return {
"kpi": {"id": kpi.id, "name": kpi.kpi_name, "code": kpi.kpi_code, "unit": kpi.unit},
"values": [
{
"period": v.period,
"actual": _float(v.actual_value),
"source_type": v.source_type,
"data_status": v.data_status,
} for v in values
],
}
# ── 战略地图 ──
@router.get("/strategic-maps")
def bot_maps(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
maps = db.query(StrategicMap).order_by(StrategicMap.id.desc()).all()
result = []
for m in maps:
objectives = db.query(MapObjective).filter(MapObjective.map_id == m.id).all()
dims = {}
for obj in objectives:
dk = obj.dimension_key
if dk not in dims:
dims[dk] = []
dims[dk].append({"id": obj.id, "name": obj.name, "description": obj.description})
result.append({
"id": m.id, "title": m.title, "version": m.version,
"status": m.status, "dimensions": m.dimensions,
"objectives": dims,
"created_at": _safe_iso(m.created_at),
"updated_at": _safe_iso(m.updated_at),
})
return {"total": len(result), "items": result}
# ── 预警 ──
@router.get("/alerts")
def bot_alerts(
status: str = Query("pending"),
level: Optional[str] = Query(None),
limit: int = Query(50, le=200),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
query = db.query(KPIAlert)
query = query.filter(KPIAlert.status == status)
if level:
query = query.filter(KPIAlert.alert_level == level)
alerts = query.order_by(KPIAlert.created_at.desc()).limit(limit).all()
return {
"total": len(alerts),
"items": [
{
"id": a.id, "kpi_id": a.kpi_id,
"level": a.alert_level, "message": a.alert_message,
"status": a.status, "assignee": a.assignee,
"resolution": a.resolution,
"created_at": _safe_iso(a.created_at),
"resolved_at": _safe_iso(a.resolved_at),
} for a in alerts
],
}
# ── 预算 ──
@router.get("/budget/plans")
def bot_budget_plans(
year: Optional[int] = Query(None),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
query = db.query(BudgetPlan)
if year:
query = query.filter(BudgetPlan.budget_year == year)
plans = query.order_by(BudgetPlan.period.desc()).limit(200).all()
return {
"total": len(plans),
"items": [
{
"id": p.id, "kpi_id": p.kpi_id,
"period": p.period,
"budget_value": _float(p.budget_value),
"year": p.budget_year, "month": p.budget_month,
"version": p.version, "status": p.status,
"remark": p.remark,
} for p in plans
],
}
# ── 成本 ──
@router.get("/cost/standard")
def bot_standard_costs(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
costs = db.query(StandardCost).filter(StandardCost.status == "active").limit(200).all()
return {
"total": len(costs),
"items": [
{
"id": c.id, "product_code": c.product_code,
"product_name": c.product_name, "cost_type": c.cost_type,
"item_name": c.item_name,
"standard_quantity": _float(c.standard_quantity),
"unit": c.unit,
"standard_price": _float(c.standard_price),
"standard_cost": _float(c.standard_cost),
"version": c.version, "remark": c.remark,
} for c in costs
],
}
@router.get("/cost/actual")
def bot_actual_costs(
period: Optional[str] = Query(None),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
query = db.query(ActualCost)
if period:
query = query.filter(ActualCost.period == period)
costs = query.order_by(ActualCost.period.desc()).limit(200).all()
return {
"total": len(costs),
"items": [
{
"id": c.id, "period": c.period,
"product_code": c.product_code,
"product_name": c.product_name,
"cost_type": c.cost_type, "item_name": c.item_name,
"actual_quantity": _float(c.actual_quantity),
"actual_price": _float(c.actual_price),
"actual_cost": _float(c.actual_cost),
} for c in costs
],
}
# ── 行动方案 ──
@router.get("/actions")
def bot_actions(
status: Optional[str] = Query(None),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
query = db.query(ActionPlan)
if status:
query = query.filter(ActionPlan.status == status)
plans = query.order_by(ActionPlan.priority, ActionPlan.id.desc()).limit(100).all()
return {
"total": len(plans),
"items": [
{
"id": p.id, "title": p.title,
"description": p.description, "kpi_id": p.kpi_id,
"assignee": p.assignee, "priority": p.priority,
"status": p.status, "progress": p.progress,
"due_date": _safe_iso(p.due_date),
"created_at": _safe_iso(p.created_at),
} for p in plans
],
}
# ── 组织 ──
@router.get("/organization")
def bot_org(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
nodes = db.query(OrgNode).order_by(OrgNode.level, OrgNode.sort_order).all()
return {
"total": len(nodes),
"items": [
{
"id": n.id, "name": n.name,
"parent_id": n.parent_id, "level": n.level,
"code": n.code, "sort_order": n.sort_order,
"enabled": n.enabled,
} for n in nodes
],
}
# ── 数据源 ──
@router.get("/data-sources")
def bot_data_sources(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
sources = db.query(DataSourceConfig).all()
return {
"total": len(sources),
"items": [
{
"id": s.id, "name": s.name,
"source_type": s.source_type,
"api_endpoint": s.api_endpoint,
"sync_type": s.sync_type,
"status": s.status,
"last_sync_at": _safe_iso(s.last_sync_at),
} for s in sources
],
}
# ── 用户 ──
@router.get("/users")
def bot_users(
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
users = db.query(User).all()
return {
"total": len(users),
"items": [
{"id": u.id, "username": u.username, "name": u.name,
"role": u.role, "phone": u.phone}
for u in users
],
}
# ── 统一查询(BOT首选) ──
@router.get("/query")
def bot_query(
q: str = Query("overview", description="overview/kpis/alerts/maps/budget/cost/actions/all"),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
"""统一查询入口 — BOT用这个一次拿完需要的数据"""
result = {"bot": bot["name"], "role": bot["role"], "timestamp": datetime.now().isoformat()}
if q in ("overview", "all"):
result["overview"] = {
"kpis": db.query(func.count(KPIDefinition.id)).filter(KPIDefinition.status == "active").scalar() or 0,
"alerts_open": db.query(func.count(KPIAlert.id)).filter(KPIAlert.status == "pending").scalar() or 0,
"maps": db.query(func.count(StrategicMap.id)).scalar() or 0,
"budget_plans": db.query(func.count(BudgetPlan.id)).scalar() or 0,
}
if q in ("kpis", "all"):
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").limit(100).all()
result["kpis"] = [
{"id": k.id, "name": k.kpi_name, "code": k.kpi_code,
"dimension": k.dimension, "target": _float(k.target_value), "unit": k.unit}
for k in kpis
]
if q in ("alerts", "all"):
alerts = db.query(KPIAlert).filter(KPIAlert.status == "pending")\
.order_by(KPIAlert.created_at.desc()).limit(20).all()
result["alerts"] = [
{"id": a.id, "level": a.alert_level, "message": a.alert_message,
"kpi_id": a.kpi_id, "created_at": _safe_iso(a.created_at)}
for a in alerts
]
if q in ("maps", "all"):
maps = db.query(StrategicMap).limit(10).all()
result["maps"] = [
{"id": m.id, "title": m.title, "status": m.status,
"version": m.version, "created_at": _safe_iso(m.created_at)}
for m in maps
]
if q in ("budget", "all"):
plans = db.query(BudgetPlan).limit(50).all()
result["budget"] = [
{"id": p.id, "period": p.period, "budget_value": _float(p.budget_value),
"year": p.budget_year, "month": p.budget_month, "status": p.status,
"kpi_id": p.kpi_id}
for p in plans
]
if q in ("cost", "all"):
sc = db.query(StandardCost).limit(50).all()
result["costs"] = [
{"id": c.id, "product": c.product_name, "type": c.cost_type,
"standard": _float(c.standard_cost), "unit": c.unit}
for c in sc
]
if q in ("actions", "all"):
acts = db.query(ActionPlan).limit(30).all()
result["actions"] = [
{"id": a.id, "title": a.title, "status": a.status,
"progress": a.progress, "assignee": a.assignee}
for a in acts
]
return result
# ── 自然语言查询 ──
@router.get("/nlp")
def bot_nlp(
intent: str = Query("overview"),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
"""
自然语言意图映射:
overview/总览/finance/财务/alerts/预警/budget/预算/cost/成本/maps/战略/actions/行动
"""
m = {
"总览": "overview", "驾驶舱": "overview",
"财务": "finance", "财务状况": "finance",
"预警": "alerts", "风险": "alerts",
"预算": "budget", "预算执行": "budget",
"成本": "cost", "成本分析": "cost",
"战略": "maps", "战略地图": "maps",
"行动": "actions", "改善": "actions",
}
resolved = m.get(intent, intent)
return bot_query(q=resolved, bot=bot, db=db)