""" 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"}, "cma-bot-shop-2026": {"role": "business", "name": "店研学BOT"}, "cma-bot-admin-2026": {"role": "ceo", "name": "管理BOT"}, } 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, "target_value": p.target_value, "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)