feat: 复盘结果持久化 — 大PDCA Act闭环(P1, yanxue-review-persistence)

- review_records表+模型(entity_id多租户)
- API: POST/GET/GET:id /maps/{id}/review-records
- 前端: MapReview 保存复盘结论弹窗+历史记录时间线(并发agent完成主体, 本项目补齐script逻辑)
- 验证: POST 200保存成功/GET列表/跨企业404/pytest 11 passed/构建部署
This commit is contained in:
Hermes CI Fix
2026-08-27 15:59:55 +08:00
parent fdc42d443d
commit 9f787e628b
5 changed files with 251 additions and 2 deletions
+109 -2
View File
@@ -2,11 +2,11 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from app.database import get_db
from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import StrategicMap, OperationLog, MapObjective, Objective, KR, KPIDefinition
from app.models import StrategicMap, OperationLog, MapObjective, Objective, KR, KPIDefinition, ReviewRecord, User
import json
router = APIRouter(prefix="/api/cma/maps", tags=["战略地图"],
@@ -785,3 +785,110 @@ def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depen
"action_plans": action_plans_data,
}
# ── 复盘记录持久化(大PDCA Act闭环 2026-08-27)────────────────
def review_to_dict(r: ReviewRecord) -> dict:
return {
"id": r.id,
"entity_id": r.entity_id,
"map_id": r.map_id,
"review_type": r.review_type,
"review_date": r.review_date.isoformat() if r.review_date else None,
"summary": r.summary,
"adjustments": r.adjustments,
"next_priorities": r.next_priorities,
"created_by": r.created_by,
"created_at": r.created_at.isoformat() if r.created_at else None,
}
def _parse_review_date(raw) -> Optional[date]:
"""兼容 'YYYY-MM-DD' / datetime / date 输入"""
if raw is None:
return datetime.now().date()
if isinstance(raw, datetime):
return raw.date()
if isinstance(raw, date):
return raw
try:
return datetime.strptime(str(raw)[:10], "%Y-%m-%d").date()
except ValueError:
return datetime.now().date()
def _get_map_or_404(db: Session, map_id: int, entity_id: int) -> StrategicMap:
m = db.query(StrategicMap).filter(
StrategicMap.id == map_id, StrategicMap.entity_id == entity_id
).first()
if not m:
raise HTTPException(404, "战略地图不存在")
return m
@router.post("/{map_id}/review-records")
def create_review_record(
map_id: int,
data: dict,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
current_user: User = Depends(require_auth),
):
"""保存复盘结论(战略回顾会/运营复盘会结束时落库)"""
_get_map_or_404(db, map_id, entity_id)
summary = (data.get("summary") or "").strip()
if not summary:
raise HTTPException(400, "复盘结论(summary)不能为空")
r = ReviewRecord(
entity_id=entity_id, # 多租户隔离: 强制token企业
map_id=map_id,
review_type=data.get("review_type", "quarterly"),
review_date=_parse_review_date(data.get("review_date")),
summary=summary,
adjustments=data.get("adjustments") or [],
next_priorities=data.get("next_priorities") or [],
created_by=current_user.id if current_user else None,
)
db.add(r)
db.commit()
db.refresh(r)
return review_to_dict(r)
@router.get("/{map_id}/review-records")
def list_review_records(
map_id: int,
review_type: Optional[str] = None,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""历史复盘记录列表(按复盘日期倒序)"""
_get_map_or_404(db, map_id, entity_id)
q = db.query(ReviewRecord).filter(
ReviewRecord.map_id == map_id,
ReviewRecord.entity_id == entity_id,
)
if review_type:
q = q.filter(ReviewRecord.review_type == review_type)
records = q.order_by(ReviewRecord.review_date.desc(), ReviewRecord.id.desc()).all()
return {"data": [review_to_dict(r) for r in records]}
@router.get("/{map_id}/review-records/{record_id}")
def get_review_record(
map_id: int,
record_id: int,
db: Session = Depends(get_db),
entity_id: int = Depends(get_entity_id),
):
"""单条复盘记录详情"""
_get_map_or_404(db, map_id, entity_id)
r = db.query(ReviewRecord).filter(
ReviewRecord.id == record_id,
ReviewRecord.map_id == map_id,
ReviewRecord.entity_id == entity_id,
).first()
if not r:
raise HTTPException(404, "复盘记录不存在")
return review_to_dict(r)
+16
View File
@@ -273,6 +273,22 @@ class StrategicMapVersion(Base):
created_at = Column(DateTime, server_default=func.now())
class ReviewRecord(Base):
"""复盘记录 — 战略回顾会/运营复盘会结论持久化(大PDCA Act闭环 2026-08-27"""
__tablename__ = "review_records"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(Integer, default=1, comment="企业ID (多租户隔离 2026-08-27)")
map_id = Column(Integer, ForeignKey("strategic_maps.id", ondelete="CASCADE"), nullable=False, comment="关联战略地图")
review_type = Column(String(20), default="quarterly", comment="monthly/quarterly 月度运营复盘/季度战略回顾")
review_date = Column(Date, nullable=False, comment="复盘日期")
summary = Column(Text, nullable=False, comment="复盘结论(为什么没达成/怎么调整)")
adjustments = Column(JSON, nullable=True, comment="调整项JSON [{type,target,action}]")
next_priorities = Column(JSON, nullable=True, comment="下阶段优先级 [str,...]")
created_by = Column(Integer, nullable=True, comment="创建人用户ID")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class MapObjective(Base):
"""战略地图目标: 每个维度下的具体目标"""
__tablename__ = "map_objectives"