BREAKING CHANGES: - Удалена старая система (6 моделей → 2 модели) - models_new.py → models.py (Notification, UserChannelPreferences) - signals_new.py → signals.py (notify.send API) - admin_new.py → admin.py Новая архитектура: - Все отправки через Celery (полностью асинхронно) - Модульная структура tasks/ (base, email, push, websocket) - Каждый канал - отдельный класс задачи наследующий BaseNotificationTask - Rate limiting, retry, приоритеты из коробки Обратная совместимость: - Создан services.py facade для старого API - NotificationService.create_notification() работает через notify.send() - Старый код продолжает работать без изменений Удалено: - Старые models.py, signals.py, admin.py, tasks.py (→ .old) - telegram_bot.py, telegram_models.py (Telegram отключен) - test_views.py, views.py, urls.py (устаревшие) Создано: - tasks/ модуль с классовой архитектурой (как senders/) - tasks/base.py - BaseNotificationTask - tasks/email.py - EmailNotificationTask + DigestEmailTask - tasks/push.py - PushNotificationTask - tasks/websocket.py - WebSocketNotificationTask Обновлено: - apps.py - загружает signals, channels, tasks - channels.py - все отправки через Celery задачи - Все импорты models_new → models
139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
"""
|
||
Базовый класс для Celery задач отправки уведомлений
|
||
"""
|
||
from abc import ABC, abstractmethod
|
||
from celery import shared_task
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class BaseNotificationTask(ABC):
|
||
"""
|
||
Абстрактный базовый класс для Celery задач по отправке уведомлений.
|
||
|
||
Инкапсулирует общую логику:
|
||
- Получение notification из БД
|
||
- Обработка ошибок
|
||
- Логирование
|
||
- Retry логика
|
||
|
||
Наследники реализуют только send_notification() с конкретным sender'ом.
|
||
"""
|
||
|
||
# Настройки Celery для конкретного канала (переопределяются в наследниках)
|
||
task_name = None
|
||
max_retries = 3
|
||
retry_delay = 60 # секунд
|
||
rate_limit = None # например '10/m' для email
|
||
|
||
def __init__(self):
|
||
self.logger = logging.getLogger(self.__class__.__name__)
|
||
|
||
@classmethod
|
||
def register_task(cls):
|
||
"""
|
||
Регистрирует Celery задачу для этого класса.
|
||
Должен вызываться при импорте модуля.
|
||
"""
|
||
instance = cls()
|
||
|
||
@shared_task(
|
||
name=cls.task_name,
|
||
bind=True,
|
||
max_retries=cls.max_retries,
|
||
default_retry_delay=cls.retry_delay,
|
||
autoretry_for=(Exception,),
|
||
rate_limit=cls.rate_limit,
|
||
)
|
||
def task_wrapper(self_task, notification_id: int, **kwargs):
|
||
"""Wrapper для вызова метода класса из Celery"""
|
||
return instance.execute(self_task, notification_id, **kwargs)
|
||
|
||
return task_wrapper
|
||
|
||
def execute(self, celery_task, notification_id: int, **kwargs):
|
||
"""
|
||
Выполняет задачу отправки уведомления.
|
||
|
||
Args:
|
||
celery_task: Объект Celery task (для retry)
|
||
notification_id: ID уведомления
|
||
**kwargs: Дополнительные параметры для sender
|
||
|
||
Returns:
|
||
True если успешно, False иначе
|
||
"""
|
||
try:
|
||
# Получаем уведомление из БД
|
||
notification = self.get_notification(notification_id)
|
||
|
||
if not notification:
|
||
self.logger.error(f"❌ Notification {notification_id} not found")
|
||
return False
|
||
|
||
# Отправляем через конкретный sender
|
||
success = self.send_notification(notification, **kwargs)
|
||
|
||
if success:
|
||
self.log_success(notification)
|
||
else:
|
||
self.log_failure(notification)
|
||
|
||
return success
|
||
|
||
except Exception as exc:
|
||
self.log_error(notification_id, exc)
|
||
raise celery_task.retry(exc=exc)
|
||
|
||
def get_notification(self, notification_id: int):
|
||
"""Получает Notification из БД"""
|
||
try:
|
||
from notifications.models import Notification
|
||
return Notification.objects.get(id=notification_id)
|
||
except Exception:
|
||
return None
|
||
|
||
@abstractmethod
|
||
def send_notification(self, notification, **kwargs) -> bool:
|
||
"""
|
||
Отправляет уведомление через конкретный канал.
|
||
Должен быть реализован в наследниках.
|
||
|
||
Args:
|
||
notification: Объект Notification
|
||
**kwargs: Дополнительные параметры
|
||
|
||
Returns:
|
||
True если успешно, False иначе
|
||
"""
|
||
pass
|
||
|
||
def log_success(self, notification):
|
||
"""Логирует успешную отправку"""
|
||
self.logger.info(
|
||
f"✅ Notification sent: "
|
||
f"id={notification.id}, "
|
||
f"recipient={notification.recipient.id}, "
|
||
f"channel={self.__class__.__name__}"
|
||
)
|
||
|
||
def log_failure(self, notification):
|
||
"""Логирует неуспешную отправку"""
|
||
self.logger.warning(
|
||
f"⚠️ Notification failed: "
|
||
f"id={notification.id}, "
|
||
f"recipient={notification.recipient.id}, "
|
||
f"channel={self.__class__.__name__}"
|
||
)
|
||
|
||
def log_error(self, notification_id: int, error: Exception):
|
||
"""Логирует ошибку"""
|
||
self.logger.error(
|
||
f"❌ Task error: "
|
||
f"notification_id={notification_id}, "
|
||
f"channel={self.__class__.__name__}, "
|
||
f"error={type(error).__name__}: {error}",
|
||
exc_info=True
|
||
)
|