"""订阅完成应用命令及其同步事务端口。""" from __future__ import annotations from collections.abc import Callable, Mapping from contextlib import AbstractContextManager from datetime import datetime, timezone from typing import Any, Protocol from app.application.outbox import OutboxIntent, SyncOutboxTransaction, SyncUnitOfWork class SubscriptionCompletionRepository(Protocol): """订阅完成命令需要的最小同步持久化端口。""" def add_history(self, **payload: Any) -> None: """在当前事务中暂存订阅历史。""" ... def delete(self, subscribe_id: int) -> None: """在当前事务中暂存订阅删除。""" ... CompletionEffect = Callable[[], None] CompletionReporter = Callable[[Mapping[str, Any]], object] class CompleteSubscriptionCommand: """原子完成订阅,并按通知、事件、统计顺序执行提交后副作用。""" def __init__( self, repository: SubscriptionCompletionRepository, unit_of_work: SyncUnitOfWork, outbox: SyncOutboxTransaction | None, publish: Callable[[dict[str, Any]], None], ) -> None: """注入共享同步会话、事件发布端口和可选 durable outbox。""" self._repository = repository self._unit_of_work = unit_of_work self._outbox = outbox self._publish = publish def execute( self, subscribe_id: int, subscribe_info: Mapping[str, Any], mediainfo: Mapping[str, Any], notify: CompletionEffect, report: CompletionReporter, notification: Mapping[str, Any] | None = None, ) -> None: """在同一事务中写历史、删订阅并暂存完成事件、通知与统计意图。""" info = dict(subscribe_info) event_payload = { "subscribe_id": subscribe_id, "subscribe_info": info, "mediainfo": dict(mediainfo), "idempotency_key": completion_event_key(subscribe_id, info), } event_key = event_payload["idempotency_key"] report_key = completion_report_key(subscribe_id, info) notification_key = completion_notification_key(subscribe_id, info) report_payload = {"subscribe_info": _completion_report_payload(info, report_key)} try: self._repository.add_history(**info) self._repository.delete(subscribe_id) if self._outbox: now = datetime.now(timezone.utc) self._outbox.stage( OutboxIntent( event_key=event_key, topic="subscribe.complete", payload=event_payload, ), now, ) if notification: self._outbox.stage( OutboxIntent( event_key=notification_key, topic="subscribe.complete.notification", payload={ "idempotency_key": notification_key, "message": dict(notification), }, ), now, ) self._outbox.stage( OutboxIntent( event_key=report_key, topic="subscribe.complete.report", payload=report_payload, ), now, ) self._unit_of_work.commit() except Exception: self._unit_of_work.rollback() raise notify() if self._outbox and notification: self._outbox.complete_by_event_key( notification_key, datetime.now(timezone.utc), ) self._publish(event_payload) if self._outbox: self._outbox.complete_by_event_key(event_key, datetime.now(timezone.utc)) if report(report_payload["subscribe_info"]) is False: raise RuntimeError("订阅完成统计上报未确认") if self._outbox: self._outbox.complete_by_event_key(report_key, datetime.now(timezone.utc)) def completion_event_key(subscribe_id: int, subscribe_info: Mapping[str, Any]) -> str: """构造跨重试稳定的订阅完成事件幂等键。""" return ( f"subscribe.complete:{subscribe_id}:" f"{subscribe_info.get('media_source') or 'unknown'}:" f"{subscribe_info.get('media_id') or 'unknown'}:v1" ) def completion_report_key(subscribe_id: int, subscribe_info: Mapping[str, Any]) -> str: """构造可独立重试的订阅完成统计幂等键。""" return f"{completion_event_key(subscribe_id, subscribe_info)}:report" def completion_notification_key( subscribe_id: int, subscribe_info: Mapping[str, Any], ) -> str: """构造订阅完成通知的稳定幂等键,避免恢复时重复生成不同消息。""" return f"{completion_event_key(subscribe_id, subscribe_info)}:notification" def _completion_report_payload( subscribe_info: Mapping[str, Any], report_key: str, ) -> dict[str, Any]: """保留旧统计接口字段,同时为恢复 handler 固化幂等键。""" return { "media_source": subscribe_info.get("media_source"), "media_id": subscribe_info.get("media_id"), "season": subscribe_info.get("season"), "idempotency_key": report_key, } CompletionScope = Callable[[], AbstractContextManager[CompleteSubscriptionCommand]] _configured_completion_scope: CompletionScope | None = None def configure_subscription_completion_scope(provider: CompletionScope) -> None: """由启动组合根登记订阅完成独占事务作用域。""" global _configured_completion_scope _configured_completion_scope = provider def get_subscription_completion_scope() -> AbstractContextManager[CompleteSubscriptionCommand]: """返回一次独占同步订阅完成事务作用域。""" if _configured_completion_scope is None: raise RuntimeError("订阅完成事务作用域尚未配置") return _configured_completion_scope()