"""站内通知 API""" from __future__ import annotations from datetime import datetime, timezone import re from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import and_, desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from ..core.response import success_response from ..core.database import get_db from ..dependencies import get_current_user, get_tenant_id from ..models import Notification, User from ..utils.timezone import now_cn_naive router = APIRouter(prefix="/notifications", tags=["notifications"]) PROJECT_TARGET_RE = re.compile(r"/projects/(\d+)") def _extract_project_id(row: Notification) -> Optional[int]: payload = row.payload if isinstance(row.payload, dict) else {} payload_project_id = payload.get("project_id") if payload_project_id is not None: try: return int(payload_project_id) except (TypeError, ValueError): pass if row.entity_type == "project" and row.entity_id is not None: return int(row.entity_id) target_url = str(row.target_url or "").strip() matched = PROJECT_TARGET_RE.search(target_url) if matched: try: return int(matched.group(1)) except (TypeError, ValueError): return None return None def _extract_project_name(row: Notification) -> Optional[str]: payload = row.payload if isinstance(row.payload, dict) else {} name = payload.get("project_name") if isinstance(name, str) and name.strip(): return name.strip() return None def _serialize_notification(row: Notification) -> dict: payload: Optional[dict[str, Any]] = row.payload if isinstance(row.payload, dict) else None return { "id": int(row.id), "title": row.title, "content": row.content, "category": row.category, "source": row.source, "entity_type": row.entity_type, "entity_id": int(row.entity_id) if row.entity_id is not None else None, "target_url": row.target_url, "kind": row.kind or "info", "priority": row.priority, "status": row.status or row.kind or "info", "blocking_reason": row.blocking_reason, "recommended_action": row.recommended_action, "dedupe_key": row.dedupe_key, "occurrence_count": int(row.occurrence_count or 1), "payload": payload, "project_id": _extract_project_id(row), "project_name": _extract_project_name(row), "is_read": bool(row.is_read), "read_at": row.read_at.isoformat() if row.read_at else None, "created_at": row.created_at.isoformat() if row.created_at else None, "updated_at": row.updated_at.isoformat() if row.updated_at else None, } @router.get("") async def list_notifications( unread_only: bool = Query(default=False), limit: int = Query(default=20, ge=1, le=100), current_user: User = Depends(get_current_user), tenant_id: int = Depends(get_tenant_id), db: AsyncSession = Depends(get_db), ): query = select(Notification).where( and_( Notification.tenant_id == tenant_id, Notification.user_id == int(current_user.id), ) ) if unread_only: query = query.where(Notification.is_read.is_(False)) query = query.order_by(desc(Notification.updated_at), desc(Notification.created_at)).limit(limit) rows = (await db.execute(query)).scalars().all() unread_count = await db.scalar( select(func.count(Notification.id)).where( and_( Notification.tenant_id == tenant_id, Notification.user_id == int(current_user.id), Notification.is_read.is_(False), ) ) ) return success_response( data={ "items": [_serialize_notification(row) for row in rows], "unread_count": int(unread_count or 0), } ) @router.post("/{notification_id}/read") async def mark_notification_read( notification_id: int, current_user: User = Depends(get_current_user), tenant_id: int = Depends(get_tenant_id), db: AsyncSession = Depends(get_db), ): row = ( await db.execute( select(Notification).where( and_( Notification.id == notification_id, Notification.tenant_id == tenant_id, Notification.user_id == int(current_user.id), ) ) ) ).scalar_one_or_none() if not row: raise HTTPException(status_code=404, detail="通知不存在") if not row.is_read: row.is_read = True row.read_at = now_cn_naive() await db.commit() return success_response(data=_serialize_notification(row)) @router.post("/read-all") async def mark_all_notifications_read( current_user: User = Depends(get_current_user), tenant_id: int = Depends(get_tenant_id), db: AsyncSession = Depends(get_db), ): rows = ( await db.execute( select(Notification).where( and_( Notification.tenant_id == tenant_id, Notification.user_id == int(current_user.id), Notification.is_read.is_(False), ) ) ) ).scalars().all() now = now_cn_naive() for row in rows: row.is_read = True row.read_at = now if rows: await db.commit() return success_response(data={"updated": len(rows)})