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

340 lines
11 KiB
Python

"""
订阅删除应用用例及其依赖端口。
"""
import inspect
from contextlib import AbstractAsyncContextManager, AbstractContextManager
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Awaitable, Callable, Literal, Mapping, Optional, Protocol, cast
from uuid import uuid4
from app.application.outbox import (
SUBSCRIBE_DELETED_TOPIC,
AsyncOutboxDispatchStore,
AsyncOutboxStager,
OutboxDispatchStore,
OutboxIntent,
OutboxStager,
SyncUnitOfWork,
deliver_async_outbox_effect,
deliver_outbox_effect,
)
from app.application.subscription.contract import (
SubscribeDeletionCandidate,
SubscriptionStagingPort,
)
from app.runtime.log import logger
from app.schemas.common import JsonData
from app.schemas.event import SubscribeDeletedEventData
@dataclass(frozen=True, slots=True)
class SubscribeDeletionActor:
"""
执行订阅删除的用户身份。
:param username: 发起操作的已认证本地用户名
:param is_superuser: 操作人是否具有超级管理员权限
"""
username: str
is_superuser: bool
SubscribeDeletionStatus = Literal["deleted", "not_found", "forbidden"]
class AsyncUnitOfWork(Protocol):
"""
订阅写用例使用的异步事务端口。
"""
async def commit(self) -> None:
"""
提交当前事务。
"""
...
async def rollback(self) -> None:
"""
回滚当前事务。
"""
...
SubscribeDeletedPublisher = Callable[[dict[str, JsonData]], Awaitable[None]]
SubscribeDeletedReporter = Callable[[Mapping[str, JsonData]], object | Awaitable[object]]
SyncSubscribeDeletedPublisher = Callable[[dict[str, JsonData]], None]
SyncSubscribeDeletedReporter = Callable[[Mapping[str, JsonData]], object]
@dataclass(frozen=True, slots=True)
class _SubscribeDeletionEffects:
"""
同步和异步入口共用的删除事件、统计与 outbox 意图。
:param event_payload: 用于发布订阅业务事件的字段快照
:param report_payload: 用于订阅统计上报的字段快照
:param event_intent: 与删除事务一同暂存的事件投递意图
:param report_intent: 与删除事务一同暂存的统计上报意图
"""
event_payload: dict[str, JsonData]
report_payload: dict[str, JsonData]
event_intent: OutboxIntent
report_intent: OutboxIntent
class DeleteSubscribeCommand:
"""
按权限删除订阅,并在提交成功后依次发送事件和统计上报。
"""
def __init__(
self,
repository: SubscriptionStagingPort,
unit_of_work: AsyncUnitOfWork,
publish_deleted: SubscribeDeletedPublisher,
report_deleted: SubscribeDeletedReporter,
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._report_deleted = report_deleted
self._outbox = outbox
self._dispatch_store = dispatch_store
async def execute(
self,
subscribe_id: int,
actor: SubscribeDeletionActor,
) -> bool:
"""
删除当前用户可访问的订阅。
返回 False 表示订阅未删除;需要区分具体原因时使用 ``execute_with_status``。
提交后的事件与上报保持原有顺序,任一副作用失败都会继续向调用方抛出。
"""
return (await self.execute_with_status(subscribe_id, actor)) == "deleted"
async def execute_with_status(
self,
subscribe_id: int,
actor: SubscribeDeletionActor,
) -> SubscribeDeletionStatus:
"""
按权限删除订阅并返回可供 HTTP 层判断的结果状态。
"""
candidate = await self._repository.get_candidate(subscribe_id)
if candidate is None:
return "not_found"
if not can_delete_subscribe(candidate, actor):
return "forbidden"
assert candidate is not None
effects = _build_deletion_effects(
subscribe_id,
candidate.event_payload,
)
try:
await self._repository.stage_delete(subscribe_id)
if self._outbox:
await self._outbox.stage(
effects.event_intent,
datetime.now(timezone.utc),
)
await self._outbox.stage(
effects.report_intent,
datetime.now(timezone.utc),
)
await self._unit_of_work.commit()
except Exception:
await self._unit_of_work.rollback()
raise
if self._dispatch_store:
await deliver_async_outbox_effect(
self._dispatch_store,
effects.event_intent.event_key,
lambda: self._publish_deleted(effects.event_payload),
)
else:
await self._publish_deleted(effects.event_payload)
# 上报适配器会自行白名单过滤公开字段;传完整删除前快照可保留音乐实体维度,
# 避免 Agent 与 API 入口收敛后丢失 music_type / total_tracks。
try:
async def report() -> object:
"""
统一等待同步或异步统计上报回调 的确认结果。
"""
result = self._report_deleted(effects.report_payload)
return await result if inspect.isawaitable(result) else result
report_result: object
if self._dispatch_store:
report_result = await deliver_async_outbox_effect(
self._dispatch_store,
effects.report_intent.event_key,
report,
)
else:
report_result = await report()
except Exception as error:
logger.warning(f"订阅删除统计上报失败,将由后台重试:{error}")
else:
if report_result is False:
logger.warning("订阅删除统计上报未确认,将由后台重试")
return "deleted"
class SyncDeleteSubscribeCommand:
"""
为同步消息入口执行同一订阅删除事务与 durable 副作用协议。
"""
def __init__(
self,
repository: SubscriptionStagingPort,
unit_of_work: SyncUnitOfWork,
publish_deleted: SyncSubscribeDeletedPublisher,
report_deleted: SyncSubscribeDeletedReporter,
outbox: Optional[OutboxStager] = None,
dispatch_store: Optional[OutboxDispatchStore] = None,
) -> None:
"""
注入同步数据访问、事务与提交后副作用端口。
"""
self._repository = repository
self._unit_of_work = unit_of_work
self._publish_deleted = publish_deleted
self._report_deleted = report_deleted
self._outbox = outbox
self._dispatch_store = dispatch_store
def execute(
self,
subscribe_id: int,
actor: SubscribeDeletionActor,
) -> bool:
"""
同步删除当前用户可访问的订阅,并保持事件和统计的可靠投递顺序。
"""
candidate = self._repository.get_candidate_sync(subscribe_id)
if not can_delete_subscribe(candidate, actor):
return False
assert candidate is not None
effects = _build_deletion_effects(
subscribe_id,
candidate.event_payload,
)
try:
self._repository.stage_delete_sync(subscribe_id)
if self._outbox:
now = datetime.now(timezone.utc)
self._outbox.stage(effects.event_intent, now)
self._outbox.stage(effects.report_intent, now)
self._unit_of_work.commit()
except Exception:
self._unit_of_work.rollback()
raise
if self._dispatch_store:
deliver_outbox_effect(
self._dispatch_store,
effects.event_intent.event_key,
lambda: self._publish_deleted(effects.event_payload),
)
else:
self._publish_deleted(effects.event_payload)
try:
report_result: object
if self._dispatch_store:
report_result = deliver_outbox_effect(
self._dispatch_store,
effects.report_intent.event_key,
lambda: self._report_deleted(effects.report_payload),
)
else:
report_result = self._report_deleted(effects.report_payload)
except Exception as error:
logger.warning(f"订阅删除统计上报失败,将由后台重试:{error}")
else:
if report_result is False:
logger.warning("订阅删除统计上报未确认,将由后台重试")
return True
def can_delete_subscribe(
candidate: SubscribeDeletionCandidate | None,
actor: SubscribeDeletionActor,
) -> bool:
"""
判断用户是否拥有目标订阅的删除权限。
"""
if candidate is None:
return False
if actor.is_superuser:
return True
return bool(candidate.username) and candidate.username == actor.username
def _build_deletion_effects(
subscribe_id: int,
subscribe_info: Mapping[str, JsonData],
) -> _SubscribeDeletionEffects:
"""
一次性构造两种执行风格共用的事件、上报和持久投递意图。
"""
event_payload = build_subscribe_deleted_payload(subscribe_id, subscribe_info)
event_key = cast(str, event_payload["idempotency_key"])
report_key = f"{event_key}:report"
report_payload = dict(subscribe_info)
report_payload["idempotency_key"] = report_key
return _SubscribeDeletionEffects(
event_payload=event_payload,
report_payload=report_payload,
event_intent=OutboxIntent(
event_key=event_key,
topic=SUBSCRIBE_DELETED_TOPIC,
payload=event_payload,
),
report_intent=OutboxIntent(
event_key=report_key,
topic="subscribe.deleted.report",
payload={
"idempotency_key": report_key,
"subscribe_info": report_payload,
},
),
)
def build_subscribe_deleted_payload(
subscribe_id: int,
subscribe_info: Mapping[str, JsonData],
) -> dict[str, JsonData]:
"""
构造兼容旧字段并携带幂等键的订阅删除事件快照。
"""
event_key = f"subscribe.deleted:{subscribe_id}:{uuid4().hex}:v1"
return cast(
dict[str, JsonData],
SubscribeDeletedEventData(
subscribe_id=subscribe_id,
subscribe_info=dict(subscribe_info),
idempotency_key=event_key,
).model_dump(mode="json"),
)
DeleteSubscribeScope = Callable[[], AbstractAsyncContextManager[DeleteSubscribeCommand]]
SyncDeleteSubscribeScope = Callable[[], AbstractContextManager[SyncDeleteSubscribeCommand]]