Files
cma-management/backend/app/api/okr.py
T
Hermes CI Fix 9c6c1614fb Phase 1: OKR模板 — 两步弹窗(O+KR结构) + 14个CMA模板 + okr_templates表 + API
MapCanvasDialogs.vue:
- 节点编辑弹窗改为两步交互(模板选择→O+KR编辑)
- 第一步:按维度展示对应模板(财务4/客户3/流程4/学习3)
- 第二步:编辑O名称+描述+3个KR(名称/目标值/权重)
- 14个CMA标准模板硬编码在前端
- 支持添加/删除KR、自定义目标跳过模板

MapCanvas.vue:
- 传递 editingLayerKey 到弹窗
- onSaveDialog 适配新 O+KR 数据结构
- onNodeSave 存储 krs 和 template_name 字段

Backend:
- OKRTemplate 模型 (okr_templates 表)
- okr_templates API: GET(按维度筛选) + POST(用户自定义) + increment use_count
- seed_okr_templates.py 迁移脚本(建表+14条种子数据)
- 在 main.py 注册新路由
2026-07-21 17:22:43 +08:00

107 lines
3.8 KiB
Python

"""
OKR目标管理 API — 季度目标 + 关键结果 + KPI联动
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
from typing import Optional
from app.database import get_db
from app.auth_middleware import require_role
from app.models import Objective, ActionPlan
router = APIRouter(prefix="/api/cma/okr", tags=["OKR目标管理"],
dependencies=[Depends(require_role("ceo", "finance", "it"))],
)
@router.get("")
def list_objectives(
quarter: Optional[str] = Query(None, description="筛选季度: 2026Q3"),
dimension: Optional[str] = Query(None),
status: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""列出OKR目标"""
q = db.query(Objective)
if quarter:
q = q.filter(Objective.quarter == quarter)
if dimension:
q = q.filter(Objective.dimension == dimension)
if status:
q = q.filter(Objective.status == status)
objs = q.order_by(Objective.quarter.desc(), Objective.id).all()
results = []
for o in objs:
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == o.id).all()
kr_summary = [
{"id": kr.id, "title": kr.title, "status": kr.status, "progress": kr.progress}
for kr in krs
]
results.append({
"id": o.id, "title": o.title, "description": o.description,
"dimension": o.dimension, "quarter": o.quarter,
"owner": o.owner, "status": o.status, "progress": o.progress,
"confidence": o.confidence,
"key_results": kr_summary,
"kr_count": len(krs),
"kr_completed": sum(1 for kr in krs if kr.status == "completed"),
"created_at": o.created_at.isoformat() if o.created_at else None,
})
return {"total": len(results), "items": results}
@router.post("")
def create_objective(
title: str = Query(...),
quarter: str = Query(...),
description: Optional[str] = Query(None),
dimension: Optional[str] = Query(None),
owner: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""创建OKR目标"""
obj = Objective(title=title, quarter=quarter, description=description,
dimension=dimension, owner=owner)
db.add(obj)
db.commit()
db.refresh(obj)
return {"ok": True, "id": obj.id, "title": obj.title}
@router.get("/{obj_id}")
def get_objective(obj_id: int, db: Session = Depends(get_db)):
"""获取单个OKR详情"""
obj = db.query(Objective).filter(Objective.id == obj_id).first()
if not obj:
raise HTTPException(404, "目标不存在")
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == obj_id).all()
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,
"confidence": obj.confidence,
},
"key_results": [
{"id": kr.id, "title": kr.title, "kpi_id": kr.kpi_id,
"status": kr.status, "progress": kr.progress,
"due_date": kr.due_date.isoformat() if kr.due_date else None,
"assignee": kr.assignee}
for kr in krs
]
}
@router.patch("/{obj_id}")
def update_objective(obj_id: int, db: Session = Depends(get_db)):
"""更新OKR进度(通过查询ActionPlan自动计算)"""
obj = db.query(Objective).filter(Objective.id == obj_id).first()
if not obj:
raise HTTPException(404, "目标不存在")
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == obj_id).all()
if krs:
obj.progress = sum(kr.progress for kr in krs) // len(krs)
db.commit()
return {"ok": True, "id": obj_id, "progress": obj.progress}