import logging from django.conf import settings from django.db.models import Case, IntegerField, Q, Value, When from django.template.loader import render_to_string from notifier.models import Notifier, NotifierRoute from notifier.registry import get_notifier_backend logger = logging.getLogger(__name__) def render_notification_content(*, notifier: Notifier, payload: dict) -> str: context = { **(payload or {}), "notifier_config": notifier.config or {}, "notifier_config_url": notifier.get_config_value("url", "") or "", "notifier_config_image_url": notifier.get_config_value("image_url", "") or "", "default_news_image_url": getattr(settings, "MESSAGE_API_DEFAULT_NEWS_IMAGE_URL", "") or "", "notifier": notifier, } content = render_to_string(notifier.get_template_name(), context).strip() if not content: raise ValueError("通知模板渲染结果不能为空") return content def send_notification_with_notifier(*, notifier: Notifier, payload: dict) -> dict: content = render_notification_content(notifier=notifier, payload=payload) backend = get_notifier_backend(notifier.channel) backend_result = backend.notify(notifier=notifier, content=content, context=payload) result = { "notifier_id": notifier.id, "notifier_name": notifier.name, "channel": notifier.channel, "template_key": notifier.template_key, "status": "sent", **backend_result, } logger.info("[notifier.services] notification sent: %s", result) return result def _match_notifier_routes(*, event_key: str, merchant_id: int, payload: dict | None = None) -> list[NotifierRoute]: payload = payload or {} category_id = payload.get("category_id") queryset = NotifierRoute.objects.filter( merchant_id=merchant_id, event_key=event_key, is_enabled=True, notifier__is_enabled=True, ).select_related("notifier", "mission_category") if category_id is not None: queryset = queryset.filter(Q(mission_category_id=category_id) | Q(mission_category__isnull=True)).annotate( route_priority=Case( When(mission_category_id=category_id, then=Value(0)), default=Value(1), output_field=IntegerField(), ) ) else: queryset = queryset.filter(mission_category__isnull=True).annotate( route_priority=Value(0, output_field=IntegerField()) ) queryset = queryset.order_by("notifier_id", "route_priority", "id") matched_by_notifier_id = {} for route in queryset: matched_by_notifier_id.setdefault(route.notifier_id, route) return list(matched_by_notifier_id.values()) def dispatch_notification_event(*, event_key: str, merchant_id: int, payload: dict | None = None) -> list[dict]: payload = payload or {} routes = _match_notifier_routes( event_key=event_key, merchant_id=merchant_id, payload=payload, ) if not routes: logger.info( "[notifier.services] no enabled notifier route matched: event_key=%s merchant_id=%s category_id=%s", event_key, merchant_id, payload.get("category_id"), ) return [] results = [] for route in routes: notifier = route.notifier try: item = send_notification_with_notifier(notifier=notifier, payload=payload) item.update( { "event_key": event_key, "route_id": route.id, "route_event_key": route.event_key, "route_mission_category_id": route.mission_category_id, } ) results.append(item) logger.info( "[notifier.services] notification routed: route_id=%s notifier_id=%s event_key=%s merchant_id=%s route_category_id=%s", route.id, notifier.id, event_key, merchant_id, route.mission_category_id, ) except Exception as exc: logger.exception( "[notifier.services] notification failed: route_id=%s notifier_id=%s event_key=%s merchant_id=%s", route.id, notifier.id, event_key, merchant_id, ) results.append( { "notifier_id": notifier.id, "notifier_name": notifier.name, "event_key": event_key, "channel": notifier.channel, "template_key": notifier.template_key, "route_id": route.id, "route_event_key": route.event_key, "route_mission_category_id": route.mission_category_id, "status": "failed", "error": str(exc), } ) return results def enqueue_notification_event(*, event_key: str, merchant_id: int, payload: dict | None = None) -> str | None: from notifier.tasks import dispatch_notification_event_task try: async_result = dispatch_notification_event_task.delay( event_key=event_key, merchant_id=merchant_id, payload=payload or {}, ) except Exception: logger.exception( "[notifier.services] queue notification task failed: event_key=%s merchant_id=%s", event_key, merchant_id, ) return None logger.info( "[notifier.services] queued notification task: event_key=%s merchant_id=%s task_id=%s", event_key, merchant_id, async_result.id, ) return async_result.id