refactor: own subscription create transactions

This commit is contained in:
jxxghp
2026-08-21 20:37:31 +08:00
parent de2957b9de
commit 7de72e0e83
11 changed files with 840 additions and 120 deletions
+169 -5
View File
@@ -15,7 +15,7 @@ app/application/history.py 里整理历史的写入路径同构。
下方 _translate 单点承担,两条链路只在「怎么查、怎么写」上分叉。
"""
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from typing import Optional, Protocol, Tuple
from app.domain.context import MediaInfo, MusicInfo
@@ -26,6 +26,9 @@ from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType
# 而后续按身份去重也会失效,所以必须在查询与建模之前短路
INCOMPLETE_IDENTITY = (0, "媒体身份不完整")
AfterCommitEffect = Callable[[int], None]
AsyncAfterCommitEffect = Callable[[int], Awaitable[None]]
class SubscribeWriter(Protocol):
"""订阅写入应用服务使用的数据端口。"""
@@ -35,16 +38,151 @@ class SubscribeWriter(Protocol):
identity: dict,
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AfterCommitEffect] = None,
) -> Tuple[int, str]:
"""同步新增订阅。"""
"""同步新增订阅,并在事务成功后执行外部副作用"""
async def async_add(
self,
identity: dict,
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
) -> Tuple[int, str]:
"""异步新增订阅。"""
"""异步新增订阅,并在事务成功后执行外部副作用"""
class StagedSubscription(Protocol):
"""订阅仓储暂存结果的结构化端口,避免 Application 反向约束适配器类型。"""
@property
def subscribe_id(self) -> int:
"""返回已创建或已存在的订阅 ID。"""
...
@property
def message(self) -> str:
"""返回兼容旧入口的结果说明。"""
...
@property
def created(self) -> bool:
"""标识本次是否暂存了一条新记录。"""
...
class SubscriptionStagingRepository(Protocol):
"""新增订阅命令需要的无提交仓储端口。"""
def stage_add(
self,
identity: dict,
payload: dict,
username: Optional[str] = None,
) -> StagedSubscription:
"""暂存同步新增,命中重复订阅时不写入。"""
...
async def async_stage_add(
self,
identity: dict,
payload: dict,
username: Optional[str] = None,
) -> StagedSubscription:
"""暂存异步新增,命中重复订阅时不写入。"""
...
class UnitOfWork(Protocol):
"""同步订阅新增命令使用的最小事务端口。"""
def commit(self) -> None:
"""提交当前逻辑操作。"""
...
def rollback(self) -> None:
"""回滚当前逻辑操作。"""
...
class AsyncUnitOfWork(Protocol):
"""异步订阅新增命令使用的最小事务端口。"""
async def commit(self) -> None:
"""提交当前逻辑操作。"""
...
async def rollback(self) -> None:
"""回滚当前逻辑操作。"""
...
class CreateSubscriptionCommand:
"""暂存并提交一条同步订阅,重复请求保持历史返回且不产生提交。"""
def __init__(
self,
repository: SubscriptionStagingRepository,
unit_of_work: UnitOfWork,
) -> None:
"""注入无提交仓储和事务所有者。"""
self._repository = repository
self._unit_of_work = unit_of_work
def execute(
self,
identity: dict,
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AfterCommitEffect] = None,
) -> Tuple[int, str]:
"""执行同步新增;事务失败回滚,提交后副作用失败不反向回滚。"""
try:
staged = self._repository.stage_add(identity, payload, username)
if staged.created:
self._unit_of_work.commit()
except Exception:
self._unit_of_work.rollback()
raise
if staged.subscribe_id and after_commit:
after_commit(staged.subscribe_id)
return staged.subscribe_id, staged.message
class AsyncCreateSubscriptionCommand:
"""暂存并提交一条异步订阅,事务成功后才把结果交给副作用调用方。"""
def __init__(
self,
repository: SubscriptionStagingRepository,
unit_of_work: AsyncUnitOfWork,
) -> None:
"""注入无提交异步仓储和事务所有者。"""
self._repository = repository
self._unit_of_work = unit_of_work
async def execute(
self,
identity: dict,
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
) -> Tuple[int, str]:
"""执行异步新增;事务失败回滚,提交后副作用失败不反向回滚。"""
try:
staged = await self._repository.async_stage_add(
identity,
payload,
username,
)
if staged.created:
await self._unit_of_work.commit()
except Exception:
await self._unit_of_work.rollback()
raise
if staged.subscribe_id and after_commit:
await after_commit(staged.subscribe_id)
return staged.subscribe_id, staged.message
_configured_subscribe_writer: Callable[[], SubscribeWriter] | None = None
@@ -130,6 +268,7 @@ def _translate(
def add_subscribe(
mediainfo: MediaInfo | MusicInfo,
subscribe_oper: Optional[SubscribeWriter] = None,
after_commit: Optional[AfterCommitEffect] = None,
**kwargs,
) -> Tuple[int, str]:
"""
@@ -137,6 +276,7 @@ def add_subscribe(
:param mediainfo: 识别结果
:param subscribe_oper: 复用的订阅操作对象,未传时由启动组合根提供
:param after_commit: 数据提交后执行的消息、事件或上报编排
:param kwargs: 订阅设置;owner_scope 为真时按用户名限定查重范围
:return: (订阅 ID, 结果说明)ID 为 0 表示未新增
"""
@@ -145,12 +285,20 @@ def add_subscribe(
return INCOMPLETE_IDENTITY
identity, payload, username = translated
oper = _get_subscribe_writer(subscribe_oper)
return oper.add(identity=identity, payload=payload, username=username)
if after_commit is None:
return oper.add(identity=identity, payload=payload, username=username)
return oper.add(
identity=identity,
payload=payload,
username=username,
after_commit=after_commit,
)
async def async_add_subscribe(
mediainfo: MediaInfo | MusicInfo,
subscribe_oper: Optional[SubscribeWriter] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
**kwargs,
) -> Tuple[int, str]:
"""
@@ -158,6 +306,7 @@ async def async_add_subscribe(
:param mediainfo: 识别结果
:param subscribe_oper: 复用的订阅操作对象,未传时由启动组合根提供
:param after_commit: 数据提交后执行的异步消息、事件或上报编排
:param kwargs: 订阅设置;owner_scope 为真时按用户名限定查重范围
:return: (订阅 ID, 结果说明)ID 为 0 表示未新增
"""
@@ -166,12 +315,27 @@ async def async_add_subscribe(
return INCOMPLETE_IDENTITY
identity, payload, username = translated
oper = _get_subscribe_writer(subscribe_oper)
return await oper.async_add(identity=identity, payload=payload, username=username)
if after_commit is None:
return await oper.async_add(identity=identity, payload=payload, username=username)
return await oper.async_add(
identity=identity,
payload=payload,
username=username,
after_commit=after_commit,
)
__all__ = [
"AfterCommitEffect",
"AsyncCreateSubscriptionCommand",
"AsyncAfterCommitEffect",
"AsyncUnitOfWork",
"CreateSubscriptionCommand",
"INCOMPLETE_IDENTITY",
"StagedSubscription",
"SubscriptionStagingRepository",
"SubscribeWriter",
"UnitOfWork",
"add_subscribe",
"async_add_subscribe",
"configure_subscribe_writer",