""" OKR模板库 API — 按维度筛选 + 用户自定义模板 """ 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 OKRTemplate, StrategicMap router = APIRouter(prefix="/api/cma/okr-templates", tags=["OKR模板库"], dependencies=[Depends(require_role("ceo", "finance", "it"))], ) DIMENSION_LAYER_NAMES = { "finance": "财务层", "customer": "客户层", "process": "内部流程层", "learning": "学习成长层", } DIMENSION_LAYER_ICONS = { "finance": "💰", "customer": "👥", "process": "⚙️", "learning": "📚", } DIMENSION_LAYER_COLORS = { "finance": "#F56C6C", "customer": "#409EFF", "process": "#67C23A", "learning": "#E6A23C", } @router.get("") def list_okr_templates( dimension: Optional[str] = Query(None, description="按维度筛选: finance/customer/process/learning"), source: Optional[str] = Query(None, description="按来源筛选: system/user/industry_pack"), industry_tag: Optional[str] = Query(None, description="按行业标签筛选"), search: Optional[str] = Query(None, description="按O名称关键词搜索"), active_only: bool = Query(True, description="仅返回启用模板"), db: Session = Depends(get_db), ): """列出 OKR 模板,支持按维度/来源/行业/名称搜索""" q = db.query(OKRTemplate) if dimension: q = q.filter(OKRTemplate.dimension == dimension) if source: q = q.filter(OKRTemplate.source == source) if industry_tag: q = q.filter(OKRTemplate.industry_tag == industry_tag) if search: q = q.filter(OKRTemplate.name.like(f"%{search}%")) if active_only: q = q.filter(OKRTemplate.is_active == 1) templates = q.order_by(OKRTemplate.sort_order, OKRTemplate.id).all() return { "total": len(templates), "items": [ { "id": t.id, "name": t.name, "description": t.description, "dimension": t.dimension, "layer": t.layer, "industry_tag": t.industry_tag, "preset_krs": t.preset_krs, "source": t.source, "use_count": t.use_count, "sort_order": t.sort_order, "is_active": t.is_active, "created_at": t.created_at.isoformat() if t.created_at else None, } for t in templates ], } @router.get("/{template_id}") def get_okr_template(template_id: int, db: Session = Depends(get_db)): """获取单个 OKR 模板详情""" t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first() if not t: raise HTTPException(404, "模板不存在") return { "id": t.id, "name": t.name, "description": t.description, "dimension": t.dimension, "layer": t.layer, "industry_tag": t.industry_tag, "preset_krs": t.preset_krs, "source": t.source, "use_count": t.use_count, "sort_order": t.sort_order, "is_active": t.is_active, } @router.post("") def create_okr_template(data: dict, db: Session = Depends(get_db)): """用户自定义 OKR 模板""" name = data.get("name", "").strip() if not name: raise HTTPException(400, "模板名称不能为空") dimension = data.get("dimension", "") if dimension not in ("finance", "customer", "process", "learning"): raise HTTPException(400, "维度无效,必须是 finance/customer/process/learning") preset_krs = data.get("preset_krs", []) if not isinstance(preset_krs, list) or len(preset_krs) == 0: raise HTTPException(400, "至少需要一个预设KR") t = OKRTemplate( name=name, description=data.get("description", ""), dimension=dimension, layer=data.get("layer", "level3"), industry_tag=data.get("industry_tag", "general"), preset_krs=preset_krs, source="user", sort_order=data.get("sort_order", 0), ) db.add(t) db.commit() db.refresh(t) return {"ok": True, "id": t.id, "name": t.name} @router.post("/{template_id}/use") def increment_use_count(template_id: int, db: Session = Depends(get_db)): """增加模板使用次数""" t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first() if not t: raise HTTPException(404, "模板不存在") t.use_count = (t.use_count or 0) + 1 db.commit() return {"ok": True, "use_count": t.use_count} @router.post("/{template_id}/apply") def apply_okr_template(template_id: int, data: dict, db: Session = Depends(get_db)): """应用 OKR 模板 — 创建战略地图并填入 O+KR""" t = db.query(OKRTemplate).filter(OKRTemplate.id == template_id).first() if not t: raise HTTPException(404, "模板不存在") map_title = data.get("title", t.name) dim = t.dimension preset_krs = t.preset_krs or [] # 构建 dimensions: 仅包含模板所在的维度层 dimensions = [] for dk in ("finance", "customer", "process", "learning"): objectives = [] if dk == dim: objectives.append({ "name": t.name, "description": t.description or "", "kpis": [], "krs": [ { "name": kr.get("name", ""), "target_value": kr.get("target_value", ""), "weight": kr.get("weight", 33), } for kr in preset_krs ], }) dimensions.append({ "key": dk, "name": DIMENSION_LAYER_NAMES.get(dk, dk), "icon": DIMENSION_LAYER_ICONS.get(dk, "📌"), "color": DIMENSION_LAYER_COLORS.get(dk, "#909399"), "objectives": objectives, }) m = StrategicMap( title=map_title, version=data.get("version", "v1.0"), status="draft", dimensions=dimensions, canvas_data={"connections": []}, ) db.add(m) db.commit() db.refresh(m) # 使用 maps API 的 _sync_map_objectives 同步到 map_objectives 表 from app.api.maps import _sync_map_objectives _sync_map_objectives(m, db) # 增加模板使用次数 t.use_count = (t.use_count or 0) + 1 db.commit() return { "ok": True, "map_id": m.id, "title": m.title, "template_id": template_id, }