feat: 实现站内通知系统
- 后端: 新增 notifications/user_notifications 表迁移及 ORM 模型
- 后端: 实现 schema/repository/service/router 全套通知 API
- GET /api/v1/notifications (列表+游标分页)
- GET /api/v1/notifications/unread-count
- PATCH /api/v1/notifications/{id}/read (幂等)
- PATCH /api/v1/notifications/mark-all-read (幂等)
- 后端: payload 使用 Pydantic discriminated union (none/open_route/open_url)
- 后端: 19 个单元测试全部通过
- Flutter: 通知 feature 完整实现 (models/apis/repositories/bloc/UI)
- Flutter: Home 页通知按钮接入真实页面,显示未读 badge
- Flutter: 14 个测试全部通过
- 协议文档: notification-inbox-protocol.md 及错误码注册
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.notification import Notification
|
||||
from models.user_notification import UserNotification
|
||||
|
||||
|
||||
class NotificationRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def list_notifications(
|
||||
self,
|
||||
*,
|
||||
user_id: UUID,
|
||||
limit: int = 20,
|
||||
cursor: datetime | None = None,
|
||||
) -> list[tuple[UserNotification, Notification]]:
|
||||
stmt = (
|
||||
select(UserNotification, Notification)
|
||||
.join(Notification, UserNotification.notification_id == Notification.id)
|
||||
.where(
|
||||
UserNotification.user_id == user_id,
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(UserNotification.created_at.desc())
|
||||
.limit(limit + 1)
|
||||
)
|
||||
if cursor is not None:
|
||||
stmt = stmt.where(UserNotification.created_at < cursor)
|
||||
|
||||
rows = (await self._session.execute(stmt)).all()
|
||||
return [(row[0], row[1]) for row in rows]
|
||||
|
||||
async def get_unread_count(self, *, user_id: UUID) -> int:
|
||||
stmt = (
|
||||
select(func.count())
|
||||
.select_from(UserNotification)
|
||||
.join(Notification, UserNotification.notification_id == Notification.id)
|
||||
.where(
|
||||
UserNotification.user_id == user_id,
|
||||
UserNotification.is_read.is_(False),
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
result = (await self._session.execute(stmt)).scalar_one()
|
||||
return result
|
||||
|
||||
async def get_user_notification(
|
||||
self,
|
||||
*,
|
||||
user_notification_id: UUID,
|
||||
user_id: UUID,
|
||||
) -> tuple[UserNotification, Notification] | None:
|
||||
stmt = (
|
||||
select(UserNotification, Notification)
|
||||
.join(Notification, UserNotification.notification_id == Notification.id)
|
||||
.where(
|
||||
UserNotification.id == user_notification_id,
|
||||
UserNotification.user_id == user_id,
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
row = (await self._session.execute(stmt)).first()
|
||||
if row is None:
|
||||
return None
|
||||
return (row[0], row[1])
|
||||
|
||||
async def mark_read(self, *, user_notification_id: UUID, user_id: UUID) -> bool:
|
||||
stmt = select(UserNotification).where(
|
||||
UserNotification.id == user_notification_id,
|
||||
UserNotification.user_id == user_id,
|
||||
UserNotification.is_read.is_(False),
|
||||
)
|
||||
un = (await self._session.execute(stmt)).scalar_one_or_none()
|
||||
if un is None:
|
||||
return False
|
||||
un.is_read = True
|
||||
un.read_at = datetime.now()
|
||||
await self._session.flush()
|
||||
return True
|
||||
|
||||
async def mark_all_read(self, *, user_id: UUID) -> int:
|
||||
un_ids_stmt = (
|
||||
select(UserNotification.id)
|
||||
.join(Notification, UserNotification.notification_id == Notification.id)
|
||||
.where(
|
||||
UserNotification.user_id == user_id,
|
||||
UserNotification.is_read.is_(False),
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
un_ids = list((await self._session.execute(un_ids_stmt)).scalars().all())
|
||||
if not un_ids:
|
||||
return 0
|
||||
count = len(un_ids)
|
||||
stmt = (
|
||||
update(UserNotification)
|
||||
.where(UserNotification.id.in_(un_ids))
|
||||
.values(is_read=True, read_at=func.now())
|
||||
)
|
||||
await self._session.execute(stmt)
|
||||
await self._session.flush()
|
||||
return count
|
||||
Reference in New Issue
Block a user