Уведомления: непрочитанные не протухают — игрок увидит их при любом заходе
Раньше уведомления удалялись через 72 часа после создания независимо от прочтения: кто не зашёл за 3 дня, не видел их вовсе. Теперь по времени чистятся только прочитанные (72ч после прочтения), а от роста у незаходящих защищает кап MAX_PER_USER=100 — при создании удаляются старейшие сверх лимита. Механика доставки не менялась: запись в БД + pull при заходе, SSE лишь ускоряет обновление открытых вкладок. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -72,12 +72,12 @@ class CSRFMiddleware:
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
|
||||
_NOTIFICATIONS_PURGE_INTERVAL = 3600 # раз в час чистим протухшие уведомления (>72ч)
|
||||
_NOTIFICATIONS_PURGE_INTERVAL = 3600 # раз в час чистим давно прочитанные уведомления
|
||||
|
||||
|
||||
async def _notifications_purge_loop() -> None:
|
||||
"""Фоновая чистка протухших уведомлений (single-worker безопасно). Чтобы удалялись
|
||||
«отовсюду» даже у неактивных пользователей (помимо очистки при чтении списка)."""
|
||||
"""Фоновая чистка давно прочитанных уведомлений (single-worker безопасно). Чтобы
|
||||
удалялись «отовсюду» даже у неактивных пользователей (помимо очистки при чтении)."""
|
||||
import asyncio
|
||||
|
||||
from app.db.session import Session, engine
|
||||
|
||||
@@ -344,7 +344,8 @@ class Notification(SQLModel, table=True):
|
||||
"""Персистентное уведомление игроку (приглашение, старт/финиш партии, титул и т.д.).
|
||||
|
||||
Текст (`title`/`body`, RU) и ссылку (`link` — относительный SPA-путь) рендерит сервер —
|
||||
фронт лишь отображает. Хранятся 72 часа; протухшие чистятся фоном и при чтении списка."""
|
||||
фронт лишь отображает. Непрочитанные хранятся до прочтения (кап на пользователя),
|
||||
прочитанные чистятся спустя 72 часа — фоном и при чтении списка."""
|
||||
|
||||
__tablename__ = "notifications"
|
||||
__table_args__ = (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Уведомления игрока: список (pull) и отметка прочитанными.
|
||||
|
||||
Появление в реальном времени обеспечивает SSE-сигнал `{type:"notifications"}` — по нему клиент
|
||||
перезапрашивает этот список. Хранение — 72 часа; протухшие чистятся при чтении (и фоном)."""
|
||||
перезапрашивает этот список. Непрочитанные хранятся до прочтения; прочитанные чистятся
|
||||
спустя 72 часа (при чтении и фоном)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
Запись в БД + живой сигнал по SSE (`notify.notifications_changed`) — клиент по сигналу
|
||||
подтягивает список (`GET /api/notifications`). Текст (RU) и ссылку рендерим здесь, на сервере.
|
||||
Хранение — 72 часа; протухшие удаляются при чтении списка и фоновой задачей.
|
||||
Хранение: непрочитанные живут до прочтения (игрок должен увидеть их, когда бы ни зашёл),
|
||||
прочитанные удаляются спустя RETENTION_HOURS после прочтения — при чтении списка и фоновой
|
||||
задачей. От роста у незаходящих защищает кап MAX_PER_USER при создании.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,7 +16,8 @@ from app.core.timeutil import iso_utc
|
||||
from app.models import Group, Match, MatchParticipant, Notification
|
||||
from app.services import notify
|
||||
|
||||
RETENTION_HOURS = 72
|
||||
RETENTION_HOURS = 72 # сколько живёт ПРОЧИТАННОЕ уведомление после прочтения
|
||||
MAX_PER_USER = 100 # кап на пользователя: старейшие сверх него удаляются при создании
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
@@ -45,6 +48,9 @@ def create_for(
|
||||
Notification(user_id=uid, type=type, title=title, body=body, link=link) for uid in ids
|
||||
]
|
||||
session.add_all(rows)
|
||||
session.flush()
|
||||
for uid in ids:
|
||||
_cap_user(session, uid)
|
||||
session.commit()
|
||||
for uid in ids:
|
||||
notify.notifications_changed(uid)
|
||||
@@ -64,10 +70,27 @@ def create(
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
def _cap_user(session: Session, user_id: int) -> None:
|
||||
"""Удалить старейшие уведомления пользователя сверх MAX_PER_USER (без commit)."""
|
||||
extra = session.exec(
|
||||
select(Notification)
|
||||
.where(Notification.user_id == user_id)
|
||||
.order_by(Notification.created_at.desc(), Notification.id.desc())
|
||||
.offset(MAX_PER_USER)
|
||||
).all()
|
||||
for row in extra:
|
||||
session.delete(row)
|
||||
|
||||
|
||||
def purge_expired(session: Session) -> int:
|
||||
"""Удалить уведомления старше RETENTION_HOURS. Возвращает число удалённых."""
|
||||
"""Удалить ПРОЧИТАННЫЕ уведомления спустя RETENTION_HOURS после прочтения.
|
||||
|
||||
Непрочитанные по времени не протухают — иначе игрок, не заходивший дольше окна
|
||||
хранения, не увидел бы их вовсе. Возвращает число удалённых."""
|
||||
rows = session.exec(
|
||||
select(Notification).where(Notification.created_at < _cutoff())
|
||||
select(Notification).where(
|
||||
Notification.read_at.is_not(None), Notification.read_at < _cutoff()
|
||||
)
|
||||
).all()
|
||||
for row in rows:
|
||||
session.delete(row)
|
||||
@@ -77,7 +100,7 @@ def purge_expired(session: Session) -> int:
|
||||
|
||||
|
||||
def list_for_user(session: Session, user_id: int) -> dict:
|
||||
"""Свежие (<72ч) уведомления пользователя + число непрочитанных. Чистит протухшие."""
|
||||
"""Уведомления пользователя + число непрочитанных. Попутно чистит давно прочитанные."""
|
||||
purge_expired(session)
|
||||
rows = session.exec(
|
||||
select(Notification)
|
||||
|
||||
Reference in New Issue
Block a user