feat(KR): 行动挂KR完成 — action_plans.kr_id字段+API+前端目标/KR选择+KR中文名自动填+地图krs端点

- action_plans表加kr_id字段(关联krs.id),create/update/list支持kr_id读写
- 创建行动选KR时自动继承objective_id;列表附kr_title
- 前端行动方案库加'所属目标/KR'两级联动选择,选中KR自动带出KPI
- OKR详情页KR卡片下显示达成行动列表(O→KR→行动三层)
- MapCanvasDialogs onKpiChange选中KPI自动填kr.name=kpi.kpi_name(中文)
- 编辑回填英文code KR名转中文显示兜底
- maps.py 新增GET /{map_id}/krs端点(MapReview战略回顾会KR进度)
- 数据修复: krs表9条英文code title→中文名(按kpi_definitions映射)
- 数据迁移: 高置信行动挂KR(费用率→kr10,应收→kr14); 地图32空KR清理
- okr list的kr_summary补metric_kpi_id(前端联动用)
- 新增6个kr_id测试(test_action_plans),全量547通过
This commit is contained in:
Hermes CI Fix
2026-08-27 12:50:30 +08:00
parent 61936302b2
commit 6b479bfe7d
10 changed files with 402 additions and 14 deletions
+65
View File
@@ -533,6 +533,71 @@ def _parse_kr_date(v):
# ── 战略回顾会 聚合接口 ──────────────────────
@router.get("/{map_id}/krs")
def get_map_krs(map_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
"""战略回顾会 KR进度: 从krs表读取地图各目标的KR (KR修复 2026-08-27)
地图dimensions JSON中每个带krs的目标节点已同步到objectives+krs表,
此处按 节点._objective_id(或 entity+name 回查)取KR。
"""
from app.models import KPIDefinition, KR, Objective
m = db.query(StrategicMap).filter(StrategicMap.id == map_id, StrategicMap.entity_id == entity_id).first()
if not m:
raise HTTPException(404, "战略地图不存在")
dims = m.dimensions
if isinstance(dims, str):
try:
dims = json.loads(dims)
except Exception:
dims = []
dims = dims or []
obj_ids = []
for dim in dims:
for obj in dim.get("objectives", []):
oid = obj.get("_objective_id")
if oid:
obj_ids.append(int(oid))
continue
# 兼容无回写字段的旧节点: 按 entity+name 回查
name = (obj.get("name") or "").strip()
if name:
o = db.query(Objective).filter(
Objective.entity_id == entity_id,
Objective.title == name,
).first()
if o:
obj_ids.append(o.id)
result = []
if obj_ids:
krs = db.query(KR).filter(
KR.entity_id == entity_id,
KR.objective_id.in_(obj_ids),
).order_by(KR.objective_id, KR.sort_order, KR.id).all()
for kr in krs:
mkpi = None
if kr.metric_kpi_id:
mkpi = db.query(KPIDefinition).filter(KPIDefinition.id == kr.metric_kpi_id).first()
result.append({
"id": kr.id,
"objective_id": kr.objective_id,
"title": kr.title,
"metric_kpi_id": kr.metric_kpi_id,
"metric_kpi_code": mkpi.kpi_code if mkpi else None,
"metric_kpi_name": mkpi.kpi_name if mkpi else None,
"operator": _parse_kr_operator_target(kr.operator)[0] if hasattr(kr, "operator") and kr.operator else ">=",
"weight": float(kr.weight) if kr.weight is not None else None,
"target_value": float(kr.target_value) if kr.target_value is not None else None,
"current_value": float(kr.current_value) if kr.current_value is not None else None,
"progress": kr.progress or 0,
"status": kr.status,
"due_date": kr.due_date.isoformat() if kr.due_date else None,
})
return {"data": result}
@router.get("/{map_id}/review")
def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
"""战略回顾会:返回目标状态、KPI值、改善行动