From fb0343046962e93564c88a67848640ddc94f94b2 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 26 Mar 2026 19:44:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20ADR-027=20Phase=202=20-=20?= =?UTF-8?q?=E7=B0=BD=E6=A0=B8/=E6=8B=92=E7=B5=95=E5=BE=8C=E8=87=AA?= =?UTF-8?q?=E5=8B=95=E5=90=8C=E6=AD=A5=20Incident=20=E7=8B=80=E6=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Router 整合點: - POST /approvals/{id}/sign → on_approval_status_change("approved") - POST /approvals/{id}/reject → on_approval_status_change("rejected") - POST /approvals/bulk-approve → 批次同步 變更: - 移除舊的 resolve_incident_after_approval() 調用 - 改用 IncidentApprovalService.on_approval_status_change() - 同步失敗不阻斷主流程 (容錯設計) ADR-027 進度: Phase 1-2 ✅ 完成 Co-Authored-By: Claude Opus 4.5 --- apps/api/src/api/v1/approvals.py | 80 ++++++++++++---------- docs/adr/ADR-027-incident-approval-sync.md | 30 ++++++-- 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/apps/api/src/api/v1/approvals.py b/apps/api/src/api/v1/approvals.py index 96390dc7..1b561bb3 100644 --- a/apps/api/src/api/v1/approvals.py +++ b/apps/api/src/api/v1/approvals.py @@ -52,6 +52,7 @@ from src.models.approval import ( from src.services.approval_db import get_approval_service, get_timeline_service from src.services.approval_execution import get_execution_service from src.services.executor import get_executor +from src.services.incident_approval_service import get_incident_approval_service from src.services.proposal_service import get_proposal_service router = APIRouter(prefix="/approvals", tags=["HITL Approvals"]) @@ -357,41 +358,26 @@ async def sign_approval( execution_svc = get_execution_service() background_tasks.add_task(execution_svc.execute_approved_action, approval) - # Phase 6.5: 更新關聯的 Incident 狀態為 RESOLVED - # 🔴 關鍵: 這是審核後 Incident 狀態更新的核心邏輯 - incident_id = approval.metadata.get("incident_id") if approval.metadata else None - logger.info( - "sign_approval_checking_incident", - approval_id=str(approval_id), - has_metadata=approval.metadata is not None, - incident_id=incident_id, - metadata_keys=list(approval.metadata.keys()) if approval.metadata else [], - ) - - if incident_id: - proposal_svc = get_proposal_service() - resolve_success = await proposal_svc.resolve_incident_after_approval( - incident_id=incident_id, + # Phase 6.5 + ADR-027: 更新關聯的 Incident 狀態為 RESOLVED + # 🔴 2026-03-26: 使用 IncidentApprovalService 確保原子同步 + try: + sync_service = get_incident_approval_service() + await sync_service.on_approval_status_change( approval_id=str(approval_id), + new_status="approved", ) logger.info( - "incident_resolved_after_sign", - incident_id=incident_id, + "incident_approval_synced", approval_id=str(approval_id), - success=resolve_success, + new_status="approved", ) - if not resolve_success: - logger.error( - "incident_resolve_failed", - incident_id=incident_id, - approval_id=str(approval_id), - note="Incident status may not be updated correctly", - ) - else: - logger.warning( - "sign_approval_no_incident_id", + except Exception as e: + # 同步失敗不阻斷主流程,但記錄錯誤 + logger.error( + "incident_approval_sync_failed", approval_id=str(approval_id), - note="Approval has no incident_id in metadata, cannot update Incident status", + error=str(e), + note="Incident status may not be updated correctly", ) # SSE: 發布簽核/核准事件 @@ -483,6 +469,25 @@ async def reject_approval( reason=request.reason, ) + # ADR-027: 同步更新 Incident 狀態為 rejected + try: + sync_service = get_incident_approval_service() + await sync_service.on_approval_status_change( + approval_id=str(approval_id), + new_status="rejected", + ) + logger.info( + "incident_approval_synced", + approval_id=str(approval_id), + new_status="rejected", + ) + except Exception as e: + logger.error( + "incident_approval_sync_failed", + approval_id=str(approval_id), + error=str(e), + ) + # SSE: 發布拒絕事件 asyncio.create_task(_publish_approval_event("rejected", approval)) @@ -614,13 +619,18 @@ async def bulk_approve( bulk_execution_svc = get_execution_service() background_tasks.add_task(bulk_execution_svc.execute_approved_action, signed_approval) - # 更新關聯的 Incident 狀態 - incident_id = signed_approval.metadata.get("incident_id") if signed_approval.metadata else None - if incident_id: - proposal_svc = get_proposal_service() - await proposal_svc.resolve_incident_after_approval( - incident_id=incident_id, + # ADR-027: 同步更新 Incident 狀態 + try: + sync_service = get_incident_approval_service() + await sync_service.on_approval_status_change( approval_id=approval_id_str, + new_status="approved", + ) + except Exception as sync_err: + logger.error( + "bulk_incident_sync_failed", + approval_id=approval_id_str, + error=str(sync_err), ) # SSE: 發布事件 diff --git a/docs/adr/ADR-027-incident-approval-sync.md b/docs/adr/ADR-027-incident-approval-sync.md index 0e01aaca..c0a03e79 100644 --- a/docs/adr/ADR-027-incident-approval-sync.md +++ b/docs/adr/ADR-027-incident-approval-sync.md @@ -1,6 +1,6 @@ # ADR-027: Incident-Approval 同步架構 -**狀態**: 批准 +**狀態**: 實作中 (Phase 1-2 ✅ 完成) **日期**: 2026-03-26 (台北時區) **決策者**: 統帥 **觸發**: 活躍事件顯示 0 + Telegram 告警鏈異常 (2026-03-26 首席架構師審查) @@ -191,12 +191,28 @@ APPROVAL_TO_INCIDENT_STATUS = { ## 四階段實作計畫 -| Phase | 內容 | 估時 | 優先級 | -|-------|------|------|--------| -| 1 | UnitOfWork + IncidentApprovalService | 3-4h | P0 | -| 2 | 狀態同步 Hook + TTL 延長 | 2-3h | P0 | -| 3 | 分散式鎖 + TTL 同步 | 2-3h | P1 | -| 4 | 整合測試 | 2h | P1 | +| Phase | 內容 | 估時 | 狀態 | +|-------|------|------|------| +| 1 | UnitOfWork + IncidentApprovalService | 3-4h | ✅ 完成 | +| 2 | 狀態同步 Hook + TTL 延長 | 2-3h | ✅ 完成 | +| 3 | 分散式鎖 + TTL 同步 | 2-3h | 🔲 待做 | +| 4 | 整合測試 | 2h | 🔲 待做 | + +### 實作完成檔案 (2026-03-26) + +| 檔案 | 功能 | 行數 | +|------|------|------| +| `apps/api/src/core/constants.py` | TTL + Redis Key 常量 | 38 | +| `apps/api/src/core/unit_of_work.py` | PostgreSQL 事務管理 | 138 | +| `apps/api/src/services/incident_approval_service.py` | 原子同步服務 | 470 | + +### Router 整合點 + +| 端點 | 整合方式 | +|------|----------| +| `POST /approvals/{id}/sign` | `on_approval_status_change("approved")` | +| `POST /approvals/{id}/reject` | `on_approval_status_change("rejected")` | +| `POST /approvals/bulk-approve` | `on_approval_status_change("approved")` | **總估時**: 9-12h