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 注册新路由
This commit is contained in:
Hermes CI Fix
2026-07-21 17:22:43 +08:00
parent a9cc7c3f1d
commit 9c6c1614fb
8 changed files with 1251 additions and 41 deletions
+55 -1
View File
@@ -13,7 +13,7 @@ from app.models import (
User, StrategicMap, KPIDefinition, KPITemplate, KPIValue,
DataSourceConfig, KPIAlert, OperationLog, NotificationChannel,
NotificationLog, RolePermission, ActionPlan, OrgNode,
StrategicMapVersion, MapObjective,
StrategicMapVersion, MapObjective, Objective,
)
from app.models.budget_plan import BudgetPlan
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
@@ -451,6 +451,21 @@ def bot_query(
for c in sc
]
if q in ("okr", "all"):
objs = db.query(Objective).filter(Objective.status == "active").all()
result["okr"] = []
for o in objs:
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == o.id).all()
result["okr"].append({
"id": o.id, "title": o.title, "quarter": o.quarter,
"dimension": o.dimension, "progress": o.progress,
"confidence": o.confidence,
"key_results": [
{"title": kr.title, "status": kr.status, "progress": kr.progress}
for kr in krs
]
})
if q in ("actions", "all"):
acts = db.query(ActionPlan).limit(30).all()
result["actions"] = [
@@ -526,6 +541,44 @@ def bot_import_excel(
# ── 自然语言查询 ──
@router.post("/okr/create")
def bot_okr_create(
title: str = Query(...),
quarter: str = Query(...),
dimension: Optional[str] = Query(None),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
"""Bot创建OKR目标"""
from app.models import Objective
obj = Objective(title=title, quarter=quarter, dimension=dimension, owner=bot["name"])
db.add(obj)
db.commit()
db.refresh(obj)
return {"ok": True, "id": obj.id, "title": obj.title, "confidence": obj.confidence}
@router.get("/okr/list")
def bot_okr_list(
quarter: Optional[str] = Query(None),
bot: dict = Depends(verify_bot_key),
db: Session = Depends(get_db),
):
"""Bot列出OKR(含KR进度)"""
from app.models import Objective
q = db.query(Objective)
if quarter:
q = q.filter(Objective.quarter == quarter)
objs = q.order_by(Objective.quarter.desc()).all()
return {"total": len(objs), "items": [
{"id": o.id, "title": o.title, "quarter": o.quarter,
"dimension": o.dimension, "progress": o.progress,
"confidence": o.confidence, "status": o.status,
"kr_count": db.query(func.count(ActionPlan.id)).filter(ActionPlan.objective_id == o.id).scalar() or 0}
for o in objs
]}
@router.get("/nlp")
def bot_nlp(
intent: str = Query("overview"),
@@ -544,6 +597,7 @@ def bot_nlp(
"成本": "cost", "成本分析": "cost",
"战略": "maps", "战略地图": "maps",
"行动": "actions", "改善": "actions",
"okr": "okr", "目标": "okr", "季度目标": "okr",
}
resolved = m.get(intent, intent)
return bot_query(q=resolved, bot=bot, db=db)
+106
View File
@@ -0,0 +1,106 @@
"""
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}
+115
View File
@@ -0,0 +1,115 @@
"""
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
router = APIRouter(prefix="/api/cma/okr-templates", tags=["OKR模板库"],
dependencies=[Depends(require_role("ceo", "finance", "it"))],
)
@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="按行业标签筛选"),
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 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}
+3 -1
View File
@@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from dotenv import load_dotenv
from app.database import init_db
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
from scripts.erp_sync import run_sync as run_erp_sync
from app.auth_middleware import require_auth
@@ -64,6 +64,8 @@ app.include_router(data_quality.router)
app.include_router(bi_reports.router)
app.include_router(entities.router)
app.include_router(bsc_layers.router)
app.include_router(okr.router)
app.include_router(okr_templates.router)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
+36
View File
@@ -166,12 +166,30 @@ class RolePermission(Base):
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class Objective(Base):
"""OKR目标"""
__tablename__ = "objectives"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False, comment="目标标题")
description = Column(Text, nullable=True, comment="目标描述")
dimension = Column(String(50), nullable=True, comment="关联维度: finance/customer/process/learning")
strategic_map_id = Column(Integer, ForeignKey("strategic_maps.id"), nullable=True, comment="关联战略地图")
quarter = Column(String(20), nullable=False, comment="季度: 2026Q3")
owner = Column(String(100), nullable=True, comment="负责人")
status = Column(String(20), default="active", comment="active/completed/cancelled")
progress = Column(Integer, default=0, comment="整体进度 0-100")
confidence = Column(Integer, default=5, comment="信心指数 1-10")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class ActionPlan(Base):
"""改善行动计划"""
__tablename__ = "action_plans"
id = Column(Integer, primary_key=True, index=True)
alert_id = Column(Integer, ForeignKey("kpi_alerts.id"), nullable=True, comment="关联预警")
kpi_id = Column(Integer, ForeignKey("kpi_definitions.id"), nullable=False, comment="关联KPI")
objective_id = Column(Integer, ForeignKey("objectives.id"), nullable=True, comment="关联OKR目标")
title = Column(String(200), nullable=False, comment="计划标题")
description = Column(Text, nullable=True, comment="详细描述")
assignee = Column(String(100), nullable=True, comment="负责人")
@@ -326,3 +344,21 @@ class KPITemplate(Base):
created_by = Column(Integer)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class OKRTemplate(Base):
"""OKR模板库"""
__tablename__ = "okr_templates"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, comment="O名称")
description = Column(Text, comment="O描述")
dimension = Column(String(20), nullable=False, comment="finance/customer/process/learning")
layer = Column(String(20), default="level1", comment="level1/level2/level3")
industry_tag = Column(String(50), default="general", comment="行业标签")
preset_krs = Column(JSON, nullable=False, comment="预设关键结果列表")
source = Column(String(20), default="system", comment="system/user/industry_pack")
use_count = Column(Integer, default=0, comment="使用次数")
sort_order = Column(Integer, default=0)
is_active = Column(Integer, default=1)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+252
View File
@@ -0,0 +1,252 @@
"""
OKR模板库 建表 + 插入14个CMA标准模板种子数据
"""
import sys
import os
import logging
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.database import get_engine, get_session_local, Base
from app.models import OKRTemplate
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("seed_okr_templates")
# ── 14个CMA标准模板 ──
SEED_TEMPLATES = [
# 💰 财务层(4个)
{
"name": "扩大收入规模",
"description": "通过新市场开拓、新产品线、新客户群实现收入增长",
"dimension": "finance",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 1,
"preset_krs": [
{"name": "营收增长率≥20%", "target_value": "≥20%", "weight": 40, "sort_order": 1},
{"name": "新客户收入占比≥15%", "target_value": "≥15%", "weight": 30, "sort_order": 2},
{"name": "客单价提升≥10%", "target_value": "≥10%", "weight": 30, "sort_order": 3},
],
},
{
"name": "优化成本结构",
"description": "通过成本降低和费用管控提升盈利能力",
"dimension": "finance",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 2,
"preset_krs": [
{"name": "毛利率提升≥3%", "target_value": "≥3%", "weight": 40, "sort_order": 1},
{"name": "管理费用率≤15%", "target_value": "≤15%", "weight": 30, "sort_order": 2},
{"name": "销售费用率≤10%", "target_value": "≤10%", "weight": 30, "sort_order": 3},
],
},
{
"name": "提升资产效率",
"description": "提高资产周转速度,释放沉淀资金",
"dimension": "finance",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 3,
"preset_krs": [
{"name": "库存周转天数≤45天", "target_value": "≤45天", "weight": 34, "sort_order": 1},
{"name": "应收账款周转天数≤45天", "target_value": "≤45天", "weight": 33, "sort_order": 2},
{"name": "总资产周转率≥1.0", "target_value": "≥1.0", "weight": 33, "sort_order": 3},
],
},
{
"name": "保障现金流安全",
"description": "确保经营现金流为正,防范流动性风险",
"dimension": "finance",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 4,
"preset_krs": [
{"name": "现金比率≥20%", "target_value": "≥20%", "weight": 34, "sort_order": 1},
{"name": "经营现金流为正", "target_value": "为正", "weight": 33, "sort_order": 2},
{"name": "自由现金流≥0", "target_value": "≥0", "weight": 33, "sort_order": 3},
],
},
# 👥 客户层(3个)
{
"name": "产品领先",
"description": "提供行业最优的产品质量和功能",
"dimension": "customer",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 5,
"preset_krs": [
{"name": "产品合格率≥99%", "target_value": "≥99%", "weight": 34, "sort_order": 1},
{"name": "NPS净推荐值≥60", "target_value": "≥60", "weight": 33, "sort_order": 2},
{"name": "市场份额提升≥2%", "target_value": "≥2%", "weight": 33, "sort_order": 3},
],
},
{
"name": "客户亲密",
"description": "建立深度客户关系,提升客户粘性",
"dimension": "customer",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 6,
"preset_krs": [
{"name": "客户满意度≥90%", "target_value": "≥90%", "weight": 34, "sort_order": 1},
{"name": "大客户留存率≥95%", "target_value": "≥95%", "weight": 33, "sort_order": 2},
{"name": "服务响应时间≤2小时", "target_value": "≤2小时", "weight": 33, "sort_order": 3},
],
},
{
"name": "卓越运营",
"description": "以最优的价格和最高的便利性服务客户",
"dimension": "customer",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 7,
"preset_krs": [
{"name": "价格竞争力行业TOP3", "target_value": "TOP3", "weight": 34, "sort_order": 1},
{"name": "按时交付率≥98%", "target_value": "≥98%", "weight": 33, "sort_order": 2},
{"name": "客户投诉率≤1%", "target_value": "≤1%", "weight": 33, "sort_order": 3},
],
},
# ⚙️ 流程层(4个)
{
"name": "运营高效顺畅",
"description": "优化供应链和交付流程,提升运营效率",
"dimension": "process",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 8,
"preset_krs": [
{"name": "交付及时率≥95%", "target_value": "≥95%", "weight": 34, "sort_order": 1},
{"name": "库存周转≤45天", "target_value": "≤45天", "weight": 33, "sort_order": 2},
{"name": "流程自动化率≥30%", "target_value": "≥30%", "weight": 33, "sort_order": 3},
],
},
{
"name": "客户管理有序",
"description": "建立标准化客户管理流程",
"dimension": "process",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 9,
"preset_krs": [
{"name": "新客获取成本下降≥10%", "target_value": "≥10%", "weight": 34, "sort_order": 1},
{"name": "客户流失率≤5%", "target_value": "≤5%", "weight": 33, "sort_order": 2},
{"name": "客户线索转化率≥20%", "target_value": "≥20%", "weight": 33, "sort_order": 3},
],
},
{
"name": "创新驱动增长",
"description": "通过产品和服务创新驱动业务增长",
"dimension": "process",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 10,
"preset_krs": [
{"name": "新产品收入占比≥15%", "target_value": "≥15%", "weight": 34, "sort_order": 1},
{"name": "研发投入占比≥5%", "target_value": "≥5%", "weight": 33, "sort_order": 2},
{"name": "年度创新提案≥12项", "target_value": "≥12项", "weight": 33, "sort_order": 3},
],
},
{
"name": "合规稳健运营",
"description": "确保合规运营,防范法律和税务风险",
"dimension": "process",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 11,
"preset_krs": [
{"name": "合规事件0起", "target_value": "0起", "weight": 34, "sort_order": 1},
{"name": "审计通过率100%", "target_value": "100%", "weight": 33, "sort_order": 2},
{"name": "税务申报及时率100%", "target_value": "100%", "weight": 33, "sort_order": 3},
],
},
# 📚 学习层(3个)
{
"name": "团队能力升级",
"description": "提升员工核心技能和关键岗位胜任度",
"dimension": "learning",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 12,
"preset_krs": [
{"name": "关键岗位培训完成率100%", "target_value": "100%", "weight": 34, "sort_order": 1},
{"name": "认证持有率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 2},
{"name": "人均培训时长≥40小时/年", "target_value": "≥40小时/年", "weight": 33, "sort_order": 3},
],
},
{
"name": "数字系统赋能",
"description": "通过数字化系统提升工作效率",
"dimension": "learning",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 13,
"preset_krs": [
{"name": "系统覆盖率≥90%", "target_value": "≥90%", "weight": 34, "sort_order": 1},
{"name": "数据自动化率≥70%", "target_value": "≥70%", "weight": 33, "sort_order": 2},
{"name": "报表自动生成率≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 3},
],
},
{
"name": "组织协同对齐",
"description": "建立高绩效文化和战略共识",
"dimension": "learning",
"layer": "level1",
"industry_tag": "general",
"source": "system",
"sort_order": 14,
"preset_krs": [
{"name": "员工满意度≥85%", "target_value": "≥85%", "weight": 34, "sort_order": 1},
{"name": "战略认知度≥80%", "target_value": "≥80%", "weight": 33, "sort_order": 2},
{"name": "跨部门协作评分≥4分", "target_value": "≥4分", "weight": 33, "sort_order": 3},
],
},
]
def ensure_table():
"""确保 okr_templates 表存在"""
Base.metadata.create_all(bind=get_engine())
logger.info("表 okr_templates 已就绪")
def seed_templates():
"""插入种子数据(如果表为空)"""
session = get_session_local()()
try:
existing = session.query(OKRTemplate).filter(OKRTemplate.source == "system").count()
if existing > 0:
logger.info(f"已有 {existing} 条系统模板,跳过种子数据")
return
for item in SEED_TEMPLATES:
t = OKRTemplate(**item)
session.add(t)
session.commit()
logger.info(f"✔ 已插入 {len(SEED_TEMPLATES)} 条 CMA 标准模板")
except Exception as e:
session.rollback()
logger.error(f"种子数据插入失败: {e}")
raise
finally:
session.close()
if __name__ == "__main__":
ensure_table()
seed_templates()
logger.info("OKR模板库初始化完成")