"""KPI字典 API""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func from typing import Optional, List from datetime import datetime import json from app.database import get_db from app.auth_middleware import require_auth, require_role, filter_kpis_by_role, kpi_visible_dims from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog router = APIRouter(prefix="/api/cma/kpis", tags=["KPI字典"], dependencies=[Depends(require_role("ceo", "finance", "business", "it"))], ) # 写操作只允许 ceo/finance/it WRITE_ROLES = Depends(require_role("ceo", "finance", "it")) @router.get("") def list_kpis( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), dimension: Optional[str] = None, keyword: Optional[str] = None, epic: Optional[str] = None, category: Optional[str] = None, db: Session = Depends(get_db), current_user = Depends(require_auth), ): query = db.query(KPIDefinition).filter(KPIDefinition.status == "active") # 角色权限过滤 dims = kpi_visible_dims(current_user.role, db) if dims: query = query.filter(KPIDefinition.dimension.in_(dims)) if dimension: query = query.filter(KPIDefinition.dimension == dimension) if keyword: query = query.filter(KPIDefinition.kpi_name.contains(keyword)) if epic: query = query.filter(KPIDefinition.epic == epic) if category: query = query.filter(KPIDefinition.category == category) total = query.count() kpis = query.order_by(KPIDefinition.kpi_code).offset((page-1)*page_size).limit(page_size).all() return {"total": total, "page": page, "page_size": page_size, "data": [kpi_to_dict(k) for k in kpis]} @router.get("/categories") def get_kpi_categories(current_user = Depends(require_auth), db: Session = Depends(get_db)): """获取BSC分类结构(带可见性过滤)""" from sqlalchemy import func as sa_func dims = kpi_visible_dims(current_user.role, db) query = db.query( KPIDefinition.dimension, KPIDefinition.category, sa_func.count(KPIDefinition.id) ).filter(KPIDefinition.status == "active") if dims: query = query.filter(KPIDefinition.dimension.in_(dims)) rows = query.group_by(KPIDefinition.dimension, KPIDefinition.category).all() # 构建树形结构 dim_map = {"finance": "财务", "customer": "客户", "process": "内部流程", "learning": "学习成长"} cat_map = { "revenue_growth": "收入增长", "profitability": "盈利水平", "cost_control": "成本费用", "asset_efficiency": "资产效率", "cash_risk": "现金流风控", "customer_scale": "客户规模", "customer_concentration": "客户集中度", "customer_satisfaction": "客户满意", "supply_chain": "供应链效率", "delivery_quality": "交付质量", "talent_pipeline": "人才梯队", "employee_engagement": "员工敬业", "innovation": "创新改善", } tree = [] for dim, cat, cnt in rows: # 找或创建维度节点 dim_node = next((n for n in tree if n["key"] == dim), None) if not dim_node: dim_node = {"key": dim, "label": dim_map.get(dim, dim), "children": []} tree.append(dim_node) dim_node["children"].append({ "key": cat, "label": cat_map.get(cat, cat), "count": cnt, }) dim_counts = {} for d in tree: dim_counts[d["key"]] = sum(c["count"] for c in d["children"]) d["count"] = dim_counts[d["key"]] return {"tree": tree, "total": sum(dim_counts.values())} @router.get("/{kpi_id}") def get_kpi(kpi_id: int, db: Session = Depends(get_db)): kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() if not kpi: raise HTTPException(404, "KPI不存在") return kpi_to_dict(kpi) @router.post("") def create_kpi(data: dict, db: Session = Depends(get_db), user=WRITE_ROLES): kpi = KPIDefinition(**data) db.add(kpi) db.commit() db.refresh(kpi) _log(db, 1, "create", "kpi", kpi.id, data) return kpi_to_dict(kpi) @router.put("/{kpi_id}") def update_kpi(kpi_id: int, data: dict, db: Session = Depends(get_db), user=WRITE_ROLES): kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() if not kpi: raise HTTPException(404, "KPI不存在") for k, v in data.items(): if hasattr(kpi, k) and v is not None: setattr(kpi, k, v) db.commit() _log(db, 1, "update", "kpi", kpi_id, data) return kpi_to_dict(kpi) @router.delete("/{kpi_id}") def delete_kpi(kpi_id: int, db: Session = Depends(get_db), user=WRITE_ROLES): kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() if kpi: kpi.status = "disabled" db.commit() return {"message": "已删除"} def kpi_to_dict(k): return {c.name: getattr(k, c.name) for c in k.__table__.columns} def _log(db, user_id, action, target_type, target_id, detail): log = OperationLog(user_id=user_id, action=action, target_type=target_type, target_id=target_id, detail=json.dumps(detail, ensure_ascii=False) if detail else None) db.add(log) db.commit() @router.get("/{kpi_id}/objectives") def get_kpi_objectives(kpi_id: int, db: Session = Depends(get_db)): """查看KPI所属的目标和战略地图""" kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() if not kpi: raise HTTPException(404, "KPI不存在") # 通过 kpi_definitions.objective 字段关联目标 # 也通过 map_id 关联地图 result = { "kpi": {"id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name}, "objectives": [], "map": None, } if kpi.map_id: m = db.query(StrategicMap).filter(StrategicMap.id == kpi.map_id).first() if m: result["map"] = {"id": m.id, "title": m.title, "status": m.status} if kpi.objective: objs = db.query(MapObjective).filter( MapObjective.map_id == kpi.map_id, MapObjective.name == kpi.objective, ).all() result["objectives"] = [{"id": o.id, "name": o.name, "dimension_key": o.dimension_key} for o in objs] return result