""" CMA本体三支柱追溯链 API — 科目 ↔ KPI ↔ OKR的O 三层互联 (2026-08-19) 追溯链: 目标(O) → 指标(KPI) → 科目(数据) objective_kpi 表: O 由哪些 KPI 度量 kpi_subject_map 表: KPI 由哪些科目计算 krs 表: O 的关键结果 KR (OKR完整化) """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Optional from app.database import get_db from app.auth_middleware import require_role from app.models import Objective, KPIDefinition, ObjectiveKPI, KPISubjectMap, KR, Subject router = APIRouter(prefix="/api/cma/ontology", tags=["本体三支柱"], dependencies=[Depends(require_role("ceo", "finance", "it", "business"))], ) def _num(v): """Decimal → float 便于 JSON 序列化""" return float(v) if v is not None else None @router.get("/trace") def trace_ontology( objective_id: int = Query(..., description="OKR目标ID, 从O→KPI→科目逐层追溯"), db: Session = Depends(get_db), ): """本体追溯链: O(目标) → KPI(指标) → 科目(数据)""" obj = db.query(Objective).filter(Objective.id == objective_id).first() if not obj: raise HTTPException(404, "目标不存在") # 第2层: O 支撑的 KPI (objective_kpi) links = ( db.query(ObjectiveKPI, KPIDefinition) .join(KPIDefinition, KPIDefinition.id == ObjectiveKPI.kpi_id) .filter(ObjectiveKPI.objective_id == objective_id) .order_by(ObjectiveKPI.id) .all() ) # 第3层: 每个 KPI 依赖的科目 (kpi_subject_map) subject_by_code = { s.subject_code: s.subject_name for s in db.query(Subject).filter(Subject.is_active == 1).all() } kpi_layer = [] for link, kpi in links: maps = ( db.query(KPISubjectMap) .filter(KPISubjectMap.kpi_id == kpi.id) .order_by(KPISubjectMap.id) .all() ) subjects = [ { "subject_code": m.subject_code, "subject_name": subject_by_code.get(m.subject_code, ""), "calc_type": m.calc_type, "weight": _num(m.weight), "remark": m.remark, } for m in maps ] kpi_layer.append({ "kpi_id": kpi.id, "kpi_code": kpi.kpi_code, "kpi_name": kpi.kpi_name, "dimension": kpi.dimension, "unit": kpi.unit, "weight": _num(link.weight), "formula": kpi.formula, "subjects": subjects, }) # KR 层 (OKR完整化: O→KR) krs = db.query(KR).filter(KR.objective_id == objective_id).order_by(KR.id).all() kr_list = [] for kr in krs: mkpi = db.query(KPIDefinition).filter(KPIDefinition.id == kr.metric_kpi_id).first() if kr.metric_kpi_id else None kr_list.append({ "id": kr.id, "title": kr.title, "metric_kpi_id": kr.metric_kpi_id, "metric_kpi_code": mkpi.kpi_code if mkpi else None, "metric_kpi_name": mkpi.kpi_name if mkpi else None, "target_value": _num(kr.target_value), "current_value": _num(kr.current_value), "progress": kr.progress, "status": kr.status, "due_date": kr.due_date.isoformat() if kr.due_date else None, }) subject_total = sum(len(k["subjects"]) for k in kpi_layer) return { "objective": { "id": obj.id, "title": obj.title, "description": obj.description, "dimension": obj.dimension, "quarter": obj.quarter, "owner": obj.owner, "status": obj.status, "progress": obj.progress, }, "krs": kr_list, "kpis": kpi_layer, "chain": { "objective_id": obj.id, "objective_title": obj.title, "kpi_count": len(kpi_layer), "subject_count": subject_total, "path": "O(目标) → KPI(指标) → 科目(数据)", }, } @router.get("/objectives") def list_ontology_objectives( quarter: Optional[str] = Query(None, description="筛选季度: 2026Q3"), db: Session = Depends(get_db), ): """所有OKR目标的三层链路概览(前端OKR页用)""" q = db.query(Objective) if quarter: q = q.filter(Objective.quarter == quarter) objs = q.order_by(Objective.quarter.desc(), Objective.id).all() results = [] for o in objs: kpi_links = ( db.query(ObjectiveKPI, KPIDefinition) .join(KPIDefinition, KPIDefinition.id == ObjectiveKPI.kpi_id) .filter(ObjectiveKPI.objective_id == o.id) .all() ) krs = db.query(KR).filter(KR.objective_id == o.id).all() results.append({ "id": o.id, "title": o.title, "dimension": o.dimension, "quarter": o.quarter, "owner": o.owner, "status": o.status, "progress": o.progress, "kpi_count": len(kpi_links), "kr_count": len(krs), "kpis": [{"kpi_id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, "weight": _num(l.weight)} for l, k in kpi_links], "krs": [{"id": kr.id, "title": kr.title, "progress": kr.progress, "status": kr.status, "due_date": kr.due_date.isoformat() if kr.due_date else None} for kr in krs], }) return {"total": len(results), "items": results}