## Context collaboration spec 要求即時通知透過 WebSocket 推播,但現行 NotificationService 僅寫入資料庫,未實作即時推送。 ## Goals / Non-Goals **Goals:** - 建立 WebSocket 連線管理基礎設施 - 通知建立時透過 Redis Pub/Sub 廣播 - 使用者連線時補送未讀通知 - 前端即時接收並更新通知 **Non-Goals:** - 不實作通知偏好設定(靜音/訂閱) - 不實作 Push Notification (PWA/Mobile) - 不實作通知分組或摺疊 ## Decisions ### 1. WebSocket 連線管理 **Decision:** 使用 in-memory dict + Redis Pub/Sub **Rationale:** - 單 process 內使用 dict 維護 WebSocket 連線 - 跨 process 透過 Redis Pub/Sub 廣播 - 簡單且符合現有架構 ```python class ConnectionManager: def __init__(self): self.active_connections: Dict[str, List[WebSocket]] = {} async def connect(self, user_id: str, websocket: WebSocket): await websocket.accept() if user_id not in self.active_connections: self.active_connections[user_id] = [] self.active_connections[user_id].append(websocket) async def disconnect(self, user_id: str, websocket: WebSocket): self.active_connections[user_id].remove(websocket) if not self.active_connections[user_id]: del self.active_connections[user_id] async def send_to_user(self, user_id: str, message: dict): if user_id in self.active_connections: for connection in self.active_connections[user_id]: await connection.send_json(message) ``` ### 2. Redis Pub/Sub 架構 **Decision:** 使用 user-specific channel **Rationale:** - Channel 命名: `notifications:{user_id}` - 避免 broadcast 給不相關的 worker - 減少訊息處理量 ```python async def publish_notification(user_id: str, notification: dict): channel = f"notifications:{user_id}" await redis.publish(channel, json.dumps(notification)) async def subscribe_notifications(user_id: str): pubsub = redis.pubsub() await pubsub.subscribe(f"notifications:{user_id}") return pubsub ``` ### 3. 連線時補送未讀 **Decision:** 連線建立後立即查詢並推送 **Rationale:** - 確保使用者不漏接通知 - 簡化前端狀態同步邏輯 ```python @router.websocket("/ws/notifications") async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)): user = await verify_ws_token(token) await manager.connect(user.id, websocket) # 連線時補送未讀 unread = get_unread_notifications(db, user.id) await websocket.send_json({ "type": "unread_sync", "notifications": [n.dict() for n in unread] }) # 開始監聽 await listen_for_notifications(user.id, websocket) ``` ### 4. 訊息格式 **Decision:** 統一 JSON 格式 ```json { "type": "notification", "data": { "id": "uuid", "type": "mention|assignment|blocker|...", "title": "...", "message": "...", "reference_type": "task|comment", "reference_id": "uuid", "created_at": "ISO8601" } } ``` ## API Changes ``` # 新增 WebSocket endpoint WS /ws/notifications?token={jwt_token} # 訊息類型 -> {"type": "unread_sync", "notifications": [...]} # 連線時 -> {"type": "notification", "data": {...}} # 新通知 -> {"type": "mark_read", "notification_id": "..."} # 已讀確認 <- {"type": "ping"} # 心跳 ``` ## Risks / Trade-offs | Risk | Mitigation | |------|------------| | WebSocket 連線數量大 | 使用心跳偵測清理斷線 | | Redis Pub/Sub 可靠性 | Pub/Sub 為 fire-and-forget,已有 DB 紀錄作為 fallback | | Token 驗證 in query | WebSocket 標準限制,token 有過期機制 |