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:
+109
-2
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user