/
systemsstrategyy
/
Treker
Обзор
Документация
Войти
/
systemsstrategyy
/
Treker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
api/modules/notifications/models.py
115 строк
4 KB
SystemsStrategy
Синхронизация с актуальной линией разработки (август 2026)
06 авг 2026, 12:52
06 авг 2026, 12:52
19af6aa
Код
Авторство
О чём код?
"""ORM-модели notifications-схемы. - Notification — единичная in-app запись. user_id ссылается на auth.users(uid) (UUID-FK для cross-module изоляции, см. ADR-0001 §10 Defense in depth). partial-индекс ix_notifications_user_unread ускоряет частый запрос «непрочитанные уведомления юзера в порядке свежести». - NotificationSetting — per-(user, kind) предпочтения каналов. Если row нет — действует дефолт {"in_app": True, "email": True}. - EmailLog — audit-trail отправок email (отладка «почему юзеру не пришло»). """ from __future__ import annotations from datetime import datetime from typing import Any from uuid import UUID, uuid4 from sqlalchemy import ( BigInteger, DateTime, ForeignKey, Index, String, Text, func, text, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.orm import Mapped, mapped_column from api.shared.db.base import Base class Notification(Base): """In-app уведомление. Иммутабельно кроме read_at. `TimestampMixin` намеренно НЕ используется: у уведомления нет осмысленного `updated_at` (изменяется только `read_at`, который имеет отдельную семантику). Только `created_at` объявлен явно.""" __tablename__ = "notifications" __table_args__ = ( Index( "ix_notifications_user_unread", "user_id", text("created_at DESC"), postgresql_where=text("read_at IS NULL"), ), {"schema": "notifications"}, ) id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) user_id: Mapped[UUID] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("auth.users.uid", ondelete="CASCADE"), nullable=False, ) kind: Mapped[str] = mapped_column(String(64), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False) link: Mapped[str | None] = mapped_column(String(512), nullable=True) read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class NotificationSetting(Base): """Per-(user, kind) настройки каналов. Composite PK.""" __tablename__ = "notification_settings" __table_args__ = {"schema": "notifications"} user_id: Mapped[UUID] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("auth.users.uid", ondelete="CASCADE"), primary_key=True, ) kind: Mapped[str] = mapped_column(String(64), primary_key=True) channels: Mapped[dict[str, Any]] = mapped_column( JSONB, nullable=False, server_default=text('\'{"in_app": true, "email": true}\'::jsonb'), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, ) class EmailLog(Base): """Audit отправок email. Пишется из Procrastinate-task'а после SMTP-вызова.""" __tablename__ = "email_log" __table_args__ = {"schema": "notifications"} id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) # nullable + SET NULL: email_log — audit-trail, он должен пережить удаление # пользователя. Письмо могло быть в очереди, когда аккаунт удалили — запись # аудита делается с user_id=NULL, а не падает FK-violation (аудит #40). user_id: Mapped[UUID | None] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("auth.users.uid", ondelete="SET NULL"), nullable=True, index=True, ) template: Mapped[str] = mapped_column(String(64), nullable=False) sent_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) status: Mapped[str] = mapped_column(String(16), nullable=False) error: Mapped[str | None] = mapped_column(Text, nullable=True)