""" OKR目标管理 API — 季度目标 + 关键结果 + KPI联动 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func from datetime import datetime, timedelta 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( data: dict, db: Session = Depends(get_db), ): """创建OKR目标(支持JSON Body和Query参数两种方式)""" # 兼容旧版Query参数 title = data.get("title") or "" quarter = data.get("quarter") or "" description = data.get("description") dimension = data.get("dimension") owner = data.get("owner") if not title or not quarter: raise HTTPException(422, "缺少必填字段: title, quarter") 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.put("/{okr_id}/decomposition/milestones/{kr_id}") def update_kr_milestones(okr_id: int, kr_id: int, data: dict, db: Session = Depends(get_db)): """保存KR的月度里程碑(时间分解视图编辑) — 同步更新KR进度""" obj = db.query(Objective).filter(Objective.id == okr_id).first() if not obj: raise HTTPException(404, "OKR不存在") kr = db.query(ActionPlan).filter( ActionPlan.id == kr_id, ActionPlan.objective_id == okr_id ).first() if not kr: raise HTTPException(404, "KR不存在或不属于该OKR") milestones = data.get("milestones") if not isinstance(milestones, list): raise HTTPException(400, "milestones 必须是数组") # 规范化:只保留 month/label/status/target 字段 normalized = [] for ms in milestones: normalized.append({ "month": ms.get("month", ""), "label": ms.get("label", ""), "status": ms.get("status", "pending"), "target": ms.get("target"), }) kr.monthly_milestones = normalized # 里程碑完成 → KR进度同步更新(完成数/总数) if normalized: done = sum(1 for ms in normalized if ms.get("status") == "completed") kr.progress = round(done / len(normalized) * 100) if kr.progress >= 100: kr.status = "completed" elif kr.status == "completed" and kr.progress < 100: kr.status = "in_progress" db.commit() return {"ok": True, "kr_id": kr.id, "progress": kr.progress, "milestones": kr.monthly_milestones} @router.post("/{okr_id}/decomposition/milestones/generate") def auto_generate_milestones(okr_id: int, data: dict, db: Session = Depends(get_db)): """为KR自动生成月度里程碑(按季度剩余月份均分)""" obj = db.query(Objective).filter(Objective.id == okr_id).first() if not obj: raise HTTPException(404, "OKR不存在") kr_id = data.get("kr_id") if not kr_id: raise HTTPException(400, "缺少 kr_id") kr = db.query(ActionPlan).filter( ActionPlan.id == kr_id, ActionPlan.objective_id == okr_id ).first() if not kr: raise HTTPException(404, "KR不存在或不属于该OKR") # 重新生成:覆盖已有里程碑(幂等) milestones = _build_auto_milestones(kr) kr.monthly_milestones = milestones db.commit() return {"ok": True, "kr_id": kr.id, "milestones": kr.monthly_milestones} def _build_auto_milestones(kr: ActionPlan) -> list: """按截止日期向前均分3个月度里程碑""" base = kr.due_date or datetime.now() + timedelta(days=90) base = base.replace(day=1) # 计算前3个月份(从最早的月份开始) month_keys = [] for i in range(3, 0, -1): y, m = base.year, base.month - i while m <= 0: m += 12 y -= 1 month_keys.append(f"{y:04d}-{m:02d}") return [ { "month": mk, "label": f"里程碑{i + 1}", "status": "pending", "target": None, } for i, mk in enumerate(month_keys) ] @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} @router.get("/{okr_id}/decomposition") def get_okr_decomposition(okr_id: int, db: Session = Depends(get_db)): """获取OKR的时间分解视图数据""" okr = db.query(Objective).filter(Objective.id == okr_id).first() if not okr: raise HTTPException(404, "OKR不存在") # 1. 关联的BSC战略O(年度 — 相同维度且没有季度标识) bsc_o = db.query(Objective).filter( Objective.dimension == okr.dimension, Objective.quarter.is_(None) ).first() # 2. 本OKR的所有KR(关联到该Objective的ActionPlan) krs = db.query(ActionPlan).filter(ActionPlan.objective_id == okr_id).all() # 3. 当前周的ActionPlan(本周行动计划) now = datetime.now() week_start = now - timedelta(days=now.weekday()) week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) week_end = week_start + timedelta(days=7) action_plans = db.query(ActionPlan).filter( ActionPlan.objective_id == okr_id, ActionPlan.due_date.between(week_start, week_end) ).all() return { "annual_o": bsc_o.title if bsc_o else None, "quarterly_o": okr.title, "krs": [ { "kr_id": kr.id, "title": kr.title, "progress": kr.progress, "milestones": kr.monthly_milestones or [] } for kr in krs ], "weekly_actions": [ { "id": ap.id, "title": ap.title, "status": ap.status, "deadline": ap.due_date.isoformat() if ap.due_date else None } for ap in action_plans ] }