PMFlow/backend/app/api/notifications.py

172 lines
5.4 KiB
Python

"""站内通知 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 ..database import get_db
from ..dependencies import get_current_user, get_tenant_id
from ..models import Notification, User
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 = datetime.now(timezone.utc)
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 = datetime.now(timezone.utc)
for row in rows:
row.is_read = True
row.read_at = now
if rows:
await db.commit()
return success_response(data={"updated": len(rows)})