feat(workbench): persist topbar notification state

This commit is contained in:
caoxiaozhu
2026-06-03 21:43:35 +08:00
parent b9826a1985
commit 75d5c178e1
15 changed files with 799 additions and 59 deletions

View File

@@ -0,0 +1,88 @@
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.notification_state import NotificationState
from app.schemas.notification_state import (
NotificationStateBatchPatch,
NotificationStateListRead,
NotificationStateRead,
)
class NotificationStateService:
def __init__(self, db: Session) -> None:
self.db = db
def ensure_storage_ready(self) -> None:
Base.metadata.create_all(bind=self.db.get_bind(), tables=[NotificationState.__table__])
def list_states(self, current_user: CurrentUserContext) -> NotificationStateListRead:
self.ensure_storage_ready()
stmt = (
select(NotificationState)
.where(NotificationState.user_id == self._user_key(current_user))
.order_by(NotificationState.updated_at.desc())
)
states = list(self.db.scalars(stmt).all())
return NotificationStateListRead(
states=[NotificationStateRead.model_validate(item) for item in states]
)
def patch_states(
self,
payload: NotificationStateBatchPatch,
current_user: CurrentUserContext,
) -> NotificationStateListRead:
self.ensure_storage_ready()
user_id = self._user_key(current_user)
patches = [item for item in payload.states if item.notification_id]
if not patches:
return self.list_states(current_user)
ids = {item.notification_id for item in patches}
existing_rows = list(
self.db.scalars(
select(NotificationState).where(
NotificationState.user_id == user_id,
NotificationState.notification_id.in_(ids),
)
).all()
)
existing_by_id = {item.notification_id: item for item in existing_rows}
now = datetime.now(UTC)
for patch in patches:
row = existing_by_id.get(patch.notification_id)
if row is None:
row = NotificationState(
user_id=user_id,
notification_id=patch.notification_id,
context_json={},
)
self.db.add(row)
existing_by_id[patch.notification_id] = row
if patch.read and row.read_at is None:
row.read_at = now
if patch.hidden and row.hidden_at is None:
row.hidden_at = now
if patch.context_json:
row.context_json = self._merge_context(row.context_json, patch.context_json)
self.db.commit()
return self.list_states(current_user)
@staticmethod
def _user_key(current_user: CurrentUserContext) -> str:
return str(current_user.username or current_user.name or "anonymous").strip() or "anonymous"
@staticmethod
def _merge_context(current: dict | None, patch: dict) -> dict:
base = current if isinstance(current, dict) else {}
return {**base, **patch}