feat(api): ADR-027 Phase 2 - 簽核/拒絕後自動同步 Incident 狀態

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 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 19:44:59 +08:00
parent 42bf6a8729
commit fb03430469
2 changed files with 68 additions and 42 deletions

View File

@@ -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: 發布事件

View File

@@ -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