Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

138 lines
4.6 KiB
Python

"""
按媒体身份批量删除订阅的应用用例。
"""
from datetime import datetime, timezone
from typing import Callable, Optional, cast
from app.application.outbox import (
SUBSCRIBE_DELETED_TOPIC,
AsyncOutboxDispatchStore,
AsyncOutboxStager,
OutboxIntent,
deliver_async_outbox_effect,
)
from app.application.subscription.contract import (
SubscribeDeletionCandidate,
SubscriptionIdentity,
SubscriptionStagingPort,
)
from app.application.subscription.delete import (
AsyncUnitOfWork,
SubscribeDeletedPublisher,
SubscribeDeletionActor,
build_subscribe_deleted_payload,
)
from app.schemas.common import JsonData
from app.schemas.types import MediaSource
SubscribeDeletionEventErrorHandler = Callable[[int, Exception], None]
class DeleteSubscriptionsByIdentityCommand:
"""
按媒体身份删除当前用户可访问的全部订阅。
"""
def __init__(
self,
repository: SubscriptionStagingPort,
unit_of_work: AsyncUnitOfWork,
publish_deleted: SubscribeDeletedPublisher,
handle_event_error: SubscribeDeletionEventErrorHandler,
outbox: Optional[AsyncOutboxStager] = None,
dispatch_store: Optional[AsyncOutboxDispatchStore] = None,
) -> None:
"""
注入数据访问、事务、事件和事件错误处理端口。
"""
self._repository = repository
self._unit_of_work = unit_of_work
self._publish_deleted = publish_deleted
self._handle_event_error = handle_event_error
self._outbox = outbox
self._dispatch_store = dispatch_store
async def execute(
self,
media_source: MediaSource,
media_id: str,
season: int | None,
music_type: str | None,
actor: SubscribeDeletionActor,
) -> int:
"""
删除匹配订阅,并在提交后逐条发送兼容事件。
"""
candidates = await self._repository.list_candidates_by_identity(
SubscriptionIdentity(
media_source=media_source,
media_id=media_id,
season=season,
music_type=music_type,
)
)
deletions = [candidate for candidate in candidates if self._can_delete(candidate, actor)]
events: list[tuple[SubscribeDeletionCandidate, dict[str, JsonData]]] = []
for candidate in deletions:
await self._repository.stage_delete(candidate.subscribe_id)
event_payload = build_subscribe_deleted_payload(
candidate.subscribe_id,
candidate.event_payload,
)
events.append((candidate, event_payload))
try:
if self._outbox:
now = datetime.now(timezone.utc)
for _, event_payload in events:
event_key = cast(str, event_payload["idempotency_key"])
await self._outbox.stage(
OutboxIntent(
event_key=event_key,
topic=SUBSCRIBE_DELETED_TOPIC,
payload=event_payload,
),
now,
)
await self._unit_of_work.commit()
except Exception:
await self._unit_of_work.rollback()
raise
for candidate, event_payload in events:
try:
if self._dispatch_store:
event_key = cast(str, event_payload["idempotency_key"])
async def publish_event(
payload: dict[str, JsonData] = event_payload,
) -> None:
"""
发布当前删除候选对应的稳定事件快照。
"""
await self._publish_deleted(payload)
await deliver_async_outbox_effect(
self._dispatch_store,
event_key,
publish_event,
)
else:
await self._publish_deleted(event_payload)
except Exception as error:
self._handle_event_error(candidate.subscribe_id, error)
return len(deletions)
@staticmethod
def _can_delete(
candidate: SubscribeDeletionCandidate,
actor: SubscribeDeletionActor,
) -> bool:
"""
判断用户是否拥有候选订阅的删除权限。
"""
if actor.is_superuser:
return True
return bool(candidate.username) and candidate.username == actor.username