feat(okr): Phase2 - 行业扩展包12个+OKR模板库页面

- 插入12个行业包模板:贸易经销(7)+IT服务(5)
- 后端: name搜索+apply端点
- 前端: OKRTemplates.vue页面(搜索/筛选/三分区/应用弹窗)
- 路由+菜单+权限配置
This commit is contained in:
Hermes CI Fix
2026-07-21 17:30:50 +08:00
parent 9c6c1614fb
commit 4bc4c32e4f
6 changed files with 710 additions and 5 deletions
+90 -2
View File
@@ -6,22 +6,44 @@ 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
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 模板,支持按维度筛选"""
"""列出 OKR 模板,支持按维度/来源/行业/名称搜索"""
q = db.query(OKRTemplate)
if dimension:
q = q.filter(OKRTemplate.dimension == dimension)
@@ -29,6 +51,8 @@ def list_okr_templates(
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()
@@ -113,3 +137,67 @@ def increment_use_count(template_id: int, db: Session = Depends(get_db)):
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,
}
+217
View File
@@ -0,0 +1,217 @@
"""OKR模板库 Phase 2 — 行业扩展包(12个模板)
source='industry_pack'
"""
import sys, os, logging
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.database import get_session_local
from app.models import OKRTemplate
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("seed_industry_packs")
# ── 贸易经销包(7个,industry_tag='trading')─
# PRD参考:优化渠补结构 / 加速应收周转 / 建立渠道分级体系 / 提升核心渠道忠诚度 / 建立渠补谈判SOP / 进销存系统全覆盖 / 渠道赋能培训
TRADING_TEMPLATES = [
{
"name": "优化渠补结构",
"description": "优化渠道补贴结构,提升渠道投入产出比",
"dimension": "finance",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 101,
"preset_krs": [
{"name": "渠补率≤75%", "target_value": "≤75%", "weight": 40, "sort_order": 1},
{"name": "渠补成本下降≥20%", "target_value": "≥20%", "weight": 30, "sort_order": 2},
{"name": "渠道综合利润率提升≥5%", "target_value": "≥5%", "weight": 30, "sort_order": 3},
],
},
{
"name": "加速应收周转",
"description": "加速应收账款周转,改善现金流健康度",
"dimension": "finance",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 102,
"preset_krs": [
{"name": "应收账款周转天数≤30天", "target_value": "≤30天", "weight": 40, "sort_order": 1},
{"name": "逾期账款占比≤5%", "target_value": "≤5%", "weight": 30, "sort_order": 2},
{"name": "回款及时率≥95%", "target_value": "≥95%", "weight": 30, "sort_order": 3},
],
},
{
"name": "建立渠道分级体系",
"description": "建立科学渠道分级管理体系,差异化赋能",
"dimension": "customer",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 103,
"preset_krs": [
{"name": "渠道分级覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1},
{"name": "A级渠道占比≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2},
{"name": "分级规则满意度≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 3},
],
},
{
"name": "提升核心渠道忠诚度",
"description": "提升核心渠道伙伴忠诚度和合作粘性",
"dimension": "customer",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 104,
"preset_krs": [
{"name": "核心渠道流失率≤3%", "target_value": "≤3%", "weight": 34, "sort_order": 1},
{"name": "核心渠道续约率≥90%", "target_value": "≥90%", "weight": 33, "sort_order": 2},
{"name": "渠道满意度评分≥4.5分", "target_value": "≥4.5分", "weight": 33, "sort_order": 3},
],
},
{
"name": "建立渠补谈判SOP",
"description": "建立标准化渠补谈判流程与SOP,减少灰色地带",
"dimension": "process",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 105,
"preset_krs": [
{"name": "渠补谈判SOP覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1},
{"name": "谈判周期缩短≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2},
{"name": "渠补争议事件≤3起/季", "target_value": "≤3起/季", "weight": 33, "sort_order": 3},
],
},
{
"name": "进销存系统全覆盖",
"description": "实现进销存系统全渠道覆盖,数据实时可视",
"dimension": "process",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 106,
"preset_krs": [
{"name": "进销存系统覆盖率≥90%", "target_value": "≥90%", "weight": 34, "sort_order": 1},
{"name": "库存数据实时更新率100%", "target_value": "100%", "weight": 33, "sort_order": 2},
{"name": "进销存报表自动生成率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3},
],
},
{
"name": "渠道赋能培训",
"description": "系统化渠道伙伴赋能培训,提升渠道综合能力",
"dimension": "learning",
"layer": "level2",
"industry_tag": "trading",
"source": "industry_pack",
"sort_order": 107,
"preset_krs": [
{"name": "渠道培训覆盖率≥80%", "target_value": "≥80%", "weight": 34, "sort_order": 1},
{"name": "培训考核通过率≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 2},
{"name": "培训后渠道业绩提升≥10%", "target_value": "≥10%", "weight": 33, "sort_order": 3},
],
},
]
# ── IT服务包(5个,industry_tag='it_service')─
# PRD参考:提升人均产值 / 提高NPS净推荐值 / 知识管理体系化 / 交付标准化 / 工程师认证体系
ITSERVICE_TEMPLATES = [
{
"name": "提升人均产值",
"description": "提升技术团队人均产值和项目利润率",
"dimension": "finance",
"layer": "level2",
"industry_tag": "it_service",
"source": "industry_pack",
"sort_order": 201,
"preset_krs": [
{"name": "人均产值≥30万/季", "target_value": "≥30万/季", "weight": 40, "sort_order": 1},
{"name": "项目毛利率≥25%", "target_value": "≥25%", "weight": 30, "sort_order": 2},
{"name": "人效同比增长≥15%", "target_value": "≥15%", "weight": 30, "sort_order": 3},
],
},
{
"name": "提高NPS净推荐值",
"description": "提高客户NPS净推荐值,建立良好口碑",
"dimension": "customer",
"layer": "level2",
"industry_tag": "it_service",
"source": "industry_pack",
"sort_order": 202,
"preset_krs": [
{"name": "NPS得分≥70", "target_value": "≥70", "weight": 34, "sort_order": 1},
{"name": "客户满意度评分≥4.5分", "target_value": "≥4.5分", "weight": 33, "sort_order": 2},
{"name": "客户推荐率≥40%", "target_value": "≥40%", "weight": 33, "sort_order": 3},
],
},
{
"name": "交付标准化",
"description": "实现项目交付流程标准化和可复制",
"dimension": "process",
"layer": "level2",
"industry_tag": "it_service",
"source": "industry_pack",
"sort_order": 203,
"preset_krs": [
{"name": "交付SOP覆盖率100%", "target_value": "100%", "weight": 34, "sort_order": 1},
{"name": "项目按时交付率≥90%", "target_value": "≥90%", "weight": 33, "sort_order": 2},
{"name": "项目验收一次通过率≥85%", "target_value": "≥85%", "weight": 33, "sort_order": 3},
],
},
{
"name": "知识管理体系化",
"description": "建设体系化知识管理平台,沉淀项目经验",
"dimension": "learning",
"layer": "level2",
"industry_tag": "it_service",
"source": "industry_pack",
"sort_order": 204,
"preset_krs": [
{"name": "知识库文章数量≥500篇", "target_value": "≥500篇", "weight": 34, "sort_order": 1},
{"name": "知识复用率≥40%", "target_value": "≥40%", "weight": 33, "sort_order": 2},
{"name": "知识贡献覆盖率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3},
],
},
{
"name": "工程师认证体系",
"description": "建立工程师技术认证与职业成长体系",
"dimension": "learning",
"layer": "level2",
"industry_tag": "it_service",
"source": "industry_pack",
"sort_order": 205,
"preset_krs": [
{"name": "认证覆盖率≥60%", "target_value": "≥60%", "weight": 34, "sort_order": 1},
{"name": "高级工程师占比≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 2},
{"name": "认证通过率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3},
],
},
]
ALL_INDUSTRY_TEMPLATES = TRADING_TEMPLATES + ITSERVICE_TEMPLATES
def seed_industry_packs():
"""插入行业包模板(如果不存在)"""
db = get_session_local()()
try:
existing = db.query(OKRTemplate).filter(OKRTemplate.source == "industry_pack").count()
if existing > 0:
logger.info(f"已有 {existing} 条行业包模板,跳过")
return
for item in ALL_INDUSTRY_TEMPLATES:
t = OKRTemplate(**item)
db.add(t)
db.commit()
logger.info(f"✔ 已插入 {len(ALL_INDUSTRY_TEMPLATES)} 条行业包模板(贸易经销7 + IT服务5)")
except Exception as e:
db.rollback()
logger.error(f"行业包种子数据插入失败: {e}")
raise
finally:
db.close()
if __name__ == "__main__":
seed_industry_packs()
logger.info("OKR行业扩展包初始化完成")