Files
cma-management/backend/app/api/ontology.py
T
Hermes CI Fix 27b5b0da47 feat: 多租户隔离P2批1 — OKR域+kpi_values加entity_id列
- 模型: Objective/KR/ObjectiveKPI/KPIValue 加 entity_id
- DB: 4表加列; kpi_values 1610条按kpi_id回填(1269酣客/341博海)
- OKR域: 测试O删除重建; okr list/get/create + ontology trace/objectives 按token企业隔离(跨企业404)
- KPIValue写入: data/bot_bridge/bot_kpis 创建时带entity_id
- 验证: 酣客创建OKR博海不可见; 跨企业读404; pytest 451通过(expenses单跑37通过为既有排序flaky)
2026-08-23 17:53:37 +08:00

161 lines
5.8 KiB
Python

"""
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.deps import get_entity_id
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),
entity_id: int = Depends(get_entity_id),
):
"""本体追溯链: O(目标) → KPI(指标) → 科目(数据) — 账套隔离 (2026-08-23 P2)"""
obj = db.query(Objective).filter(Objective.id == objective_id, Objective.entity_id == entity_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),
entity_id: int = Depends(get_entity_id),
):
"""所有OKR目标的三层链路概览(前端OKR页用) — 账套隔离 (2026-08-23 P2)"""
q = db.query(Objective).filter(Objective.entity_id == entity_id)
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}