2026-04-10 18:50:08 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
|
|
2026-04-13 14:52:22 +08:00
|
|
|
from core.logging import get_logger
|
2026-04-10 18:50:08 +08:00
|
|
|
from core.auth.models import CurrentUser
|
|
|
|
|
from v1.notifications.dependencies import get_notification_service
|
|
|
|
|
from v1.notifications.schemas import (
|
|
|
|
|
MarkAllReadResponse,
|
|
|
|
|
NotificationItemResponse,
|
|
|
|
|
NotificationListResponse,
|
|
|
|
|
UnreadCountResponse,
|
|
|
|
|
)
|
|
|
|
|
from v1.notifications.service import NotificationService
|
|
|
|
|
from v1.users.dependencies import get_current_user
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
2026-04-13 14:52:22 +08:00
|
|
|
logger = get_logger("v1.notifications.router")
|
2026-04-10 18:50:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=NotificationListResponse)
|
|
|
|
|
async def list_notifications(
|
|
|
|
|
service: Annotated[NotificationService, Depends(get_notification_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
limit: int = Query(default=20, ge=1, le=50),
|
|
|
|
|
cursor: str | None = Query(default=None),
|
|
|
|
|
) -> NotificationListResponse:
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
parsed_cursor = None
|
|
|
|
|
if cursor is not None:
|
|
|
|
|
try:
|
|
|
|
|
parsed_cursor = datetime.fromisoformat(cursor.replace("Z", "+00:00"))
|
|
|
|
|
except (ValueError, AttributeError):
|
|
|
|
|
parsed_cursor = None
|
|
|
|
|
|
|
|
|
|
result = await service.list_notifications(
|
|
|
|
|
user_id=current_user.id,
|
|
|
|
|
limit=limit,
|
|
|
|
|
cursor=parsed_cursor,
|
|
|
|
|
)
|
2026-04-13 14:52:22 +08:00
|
|
|
logger.info(
|
|
|
|
|
"Notification list fetched",
|
|
|
|
|
user_id=str(current_user.id),
|
|
|
|
|
limit=limit,
|
|
|
|
|
item_count=len(result.items),
|
|
|
|
|
has_more=result.has_more,
|
|
|
|
|
)
|
2026-04-10 18:50:08 +08:00
|
|
|
items = []
|
|
|
|
|
for item in result.items:
|
|
|
|
|
items.append(
|
|
|
|
|
NotificationItemResponse(
|
|
|
|
|
id=str(item.id),
|
|
|
|
|
notificationId=str(item.notification_id),
|
|
|
|
|
type=item.type,
|
|
|
|
|
title=item.title,
|
|
|
|
|
body=item.body,
|
|
|
|
|
payload=item.payload,
|
|
|
|
|
isRead=item.is_read,
|
|
|
|
|
readAt=item.read_at,
|
|
|
|
|
createdAt=item.created_at,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return NotificationListResponse(
|
|
|
|
|
items=items,
|
|
|
|
|
nextCursor=result.next_cursor.isoformat() if result.next_cursor else None,
|
|
|
|
|
hasMore=result.has_more,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/unread-count", response_model=UnreadCountResponse)
|
|
|
|
|
async def get_unread_count(
|
|
|
|
|
service: Annotated[NotificationService, Depends(get_notification_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> UnreadCountResponse:
|
|
|
|
|
count = await service.get_unread_count(user_id=current_user.id)
|
2026-04-13 14:52:22 +08:00
|
|
|
logger.info(
|
|
|
|
|
"Notification unread count fetched",
|
|
|
|
|
user_id=str(current_user.id),
|
|
|
|
|
count=count,
|
|
|
|
|
)
|
2026-04-10 18:50:08 +08:00
|
|
|
return UnreadCountResponse(count=count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{notification_id}/read", response_model=NotificationItemResponse)
|
|
|
|
|
async def mark_notification_read(
|
|
|
|
|
notification_id: str,
|
|
|
|
|
service: Annotated[NotificationService, Depends(get_notification_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> NotificationItemResponse:
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
uid = UUID(notification_id)
|
|
|
|
|
except ValueError:
|
|
|
|
|
from core.http.errors import ApiProblemError, problem_payload
|
|
|
|
|
|
|
|
|
|
raise ApiProblemError(
|
|
|
|
|
status_code=404,
|
|
|
|
|
detail=problem_payload(
|
|
|
|
|
code="NOTIFICATION_NOT_FOUND",
|
|
|
|
|
detail="Notification not found or not owned by current user",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
item = await service.mark_read(
|
|
|
|
|
user_notification_id=uid,
|
|
|
|
|
user_id=current_user.id,
|
|
|
|
|
)
|
2026-04-13 14:52:22 +08:00
|
|
|
logger.info(
|
|
|
|
|
"Notification marked as read",
|
|
|
|
|
user_id=str(current_user.id),
|
|
|
|
|
user_notification_id=str(uid),
|
|
|
|
|
)
|
2026-04-10 18:50:08 +08:00
|
|
|
return NotificationItemResponse(
|
|
|
|
|
id=str(item.id),
|
|
|
|
|
notificationId=str(item.notification_id),
|
|
|
|
|
type=item.type,
|
|
|
|
|
title=item.title,
|
|
|
|
|
body=item.body,
|
|
|
|
|
payload=item.payload,
|
|
|
|
|
isRead=item.is_read,
|
|
|
|
|
readAt=item.read_at,
|
|
|
|
|
createdAt=item.created_at,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/mark-all-read", response_model=MarkAllReadResponse)
|
|
|
|
|
async def mark_all_read(
|
|
|
|
|
service: Annotated[NotificationService, Depends(get_notification_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> MarkAllReadResponse:
|
|
|
|
|
updated_count = await service.mark_all_read(user_id=current_user.id)
|
2026-04-13 14:52:22 +08:00
|
|
|
logger.info(
|
|
|
|
|
"All notifications marked as read",
|
|
|
|
|
user_id=str(current_user.id),
|
|
|
|
|
updated_count=updated_count,
|
|
|
|
|
)
|
2026-04-10 18:50:08 +08:00
|
|
|
return MarkAllReadResponse(updatedCount=updated_count)
|