refactor: make subscription lifecycle events durable

This commit is contained in:
jxxghp
2026-08-22 07:43:41 +08:00
parent 8f94fd620d
commit c5de1c7b1b
27 changed files with 1106 additions and 230 deletions
+63 -1
View File
@@ -6,7 +6,18 @@ from typing import cast
from fastapi import Depends, Request
from app.application.messaging.chat import AsyncAgentChatRepository, AsyncUnitOfWork
from app.startup.context import AgentChatRuntime, HostRuntime
from app.application.outbox import AsyncOutboxTransaction
from app.application.subscription.delete import SubscribeDeletionRepository
from app.application.subscription.identity import SubscribeIdentityDeletionRepository
from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository,
)
from app.startup.context import (
AgentChatRuntime,
HostRuntime,
SubscriptionRuntime,
)
def get_host_runtime(request: Request) -> HostRuntime:
@@ -46,3 +57,54 @@ def get_agent_chat_transaction(
) -> AsyncUnitOfWork:
"""构造绑定当前请求会话的 Agent 会话事务端口。"""
return cast(AsyncUnitOfWork, runtime.transaction(session))
def get_subscription_runtime(
runtime: HostRuntime = Depends(get_host_runtime),
) -> SubscriptionRuntime:
"""从完整宿主运行时收窄到订阅写事务能力。"""
return runtime.subscription
async def get_subscription_session(
runtime: SubscriptionRuntime = Depends(get_subscription_runtime),
) -> AsyncGenerator[object, None]:
"""从订阅运行时生成请求独占的异步会话。"""
async for session in runtime.async_session():
yield session
def get_subscription_repository(
session: object = Depends(get_subscription_session),
runtime: SubscriptionRuntime = Depends(get_subscription_runtime),
) -> (
SubscriptionMutationRepository
| SubscribeDeletionRepository
| SubscribeIdentityDeletionRepository
):
"""构造绑定当前请求会话的订阅仓储。"""
return runtime.repository(session)
def get_subscription_history_repository(
session: object = Depends(get_subscription_session),
runtime: SubscriptionRuntime = Depends(get_subscription_runtime),
) -> SubscriptionHistoryMutationRepository:
"""构造绑定当前请求会话的订阅历史仓储。"""
return runtime.history_repository(session)
def get_subscription_transaction(
session: object = Depends(get_subscription_session),
runtime: SubscriptionRuntime = Depends(get_subscription_runtime),
) -> AsyncUnitOfWork:
"""构造绑定当前订阅请求会话的异步事务端口。"""
return cast(AsyncUnitOfWork, runtime.transaction(session))
def get_subscription_outbox(
session: object = Depends(get_subscription_session),
runtime: SubscriptionRuntime = Depends(get_subscription_runtime),
) -> AsyncOutboxTransaction:
"""构造与订阅写入共享请求会话的 outbox 端口。"""
return runtime.outbox(session)
+52 -18
View File
@@ -1,17 +1,36 @@
"""订阅领域的请求级 command/query 依赖。"""
from typing import Any, cast
from fastapi import BackgroundTasks, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper
from app.api.context import (
get_subscription_history_repository,
get_subscription_outbox,
get_subscription_repository,
get_subscription_transaction,
)
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.api.dependencies.data import repository
from app.application.outbox import AsyncOutboxTransaction
from app.application.scheduling import Scheduler
from app.application.servarr import ServarrSubscriptionService
from app.application.subscription.delete import DeleteSubscribeCommand
from app.application.subscription.delete import (
AsyncUnitOfWork as DeleteUnitOfWork,
DeleteSubscribeCommand,
SubscribeDeletionRepository,
)
from app.application.subscription.identity import DeleteSubscriptionsByIdentityCommand
from app.application.subscription.mutation import SubscriptionMutationService
from app.application.subscription.identity import SubscribeIdentityDeletionRepository
from app.application.subscription.mutation import (
AsyncUnitOfWork as MutationUnitOfWork,
SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository,
SubscriptionMutationService,
)
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager
@@ -20,25 +39,29 @@ from app.schemas.types import EventType
async def _publish_subscribe_deleted(
subscribe_id: int,
subscribe_info: dict,
payload: dict[str, Any],
) -> None:
"""通过宿主事件总线发布已提交的订阅删除事件。"""
await eventmanager.async_send_event(
EventType.SubscribeDeleted,
{"subscribe_id": subscribe_id, "subscribe_info": subscribe_info},
)
await eventmanager.async_send_event(EventType.SubscribeDeleted, payload)
async def _publish_subscribe_modified(payload: dict[str, Any]) -> None:
"""通过宿主事件总线发布已提交的订阅修改事件。"""
await eventmanager.async_send_event(EventType.SubscribeModified, payload)
def get_delete_subscribe_command(
db: AsyncSession = Depends(get_async_db),
repository_port: object = Depends(get_subscription_repository),
unit_of_work: object = Depends(get_subscription_transaction),
outbox: AsyncOutboxTransaction = Depends(get_subscription_outbox),
) -> DeleteSubscribeCommand:
"""组装请求级订阅删除用例及其具体适配器。"""
return DeleteSubscribeCommand(
repository=repository("subscribe", db),
unit_of_work=transaction("async", db),
repository=cast(SubscribeDeletionRepository, repository_port),
unit_of_work=cast(DeleteUnitOfWork, unit_of_work),
publish_deleted=_publish_subscribe_deleted,
report_deleted=MoviePilotServerHelper.sub_done_async,
outbox=outbox,
)
@@ -54,14 +77,17 @@ def _log_subscribe_deleted_event_error(
def get_delete_subscriptions_by_identity_command(
db: AsyncSession = Depends(get_async_db),
repository_port: object = Depends(get_subscription_repository),
unit_of_work: object = Depends(get_subscription_transaction),
outbox: AsyncOutboxTransaction = Depends(get_subscription_outbox),
) -> DeleteSubscriptionsByIdentityCommand:
"""组装请求级按媒体身份删除订阅用例。"""
return DeleteSubscriptionsByIdentityCommand(
repository=repository("subscribe", db),
unit_of_work=transaction("async", db),
repository=cast(SubscribeIdentityDeletionRepository, repository_port),
unit_of_work=cast(DeleteUnitOfWork, unit_of_work),
publish_deleted=_publish_subscribe_deleted,
handle_event_error=_log_subscribe_deleted_event_error,
outbox=outbox,
)
@@ -98,12 +124,20 @@ def get_subscription_query_service(
def get_subscription_mutation_service(
db: AsyncSession = Depends(get_async_db),
repository_port: object = Depends(get_subscription_repository),
history_repository: SubscriptionHistoryMutationRepository = Depends(
get_subscription_history_repository
),
unit_of_work: object = Depends(get_subscription_transaction),
outbox: AsyncOutboxTransaction = Depends(get_subscription_outbox),
) -> SubscriptionMutationService:
"""组装异步订阅写服务。"""
return SubscriptionMutationService(
repository=repository("subscribe", db),
history_repository=repository("subscribe_history", db),
repository=cast(SubscriptionMutationRepository, repository_port),
history_repository=history_repository,
unit_of_work=cast(MutationUnitOfWork, unit_of_work),
outbox=outbox,
publish_modified=_publish_subscribe_modified,
)
+31 -30
View File
@@ -14,8 +14,8 @@ from app.schemas.workflow import Subscribe as _SchemaSubscribe
from app.api.response import ResponseAPIRouter
from app.chain.subscribe import SubscribeChain
from app.runtime.config import settings
from app.domain.context import MediaInfo
from app.runtime.events import eventmanager
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.application.subscription.delete import (
@@ -294,16 +294,16 @@ async def update_subscribe(
)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subscribe_in.id,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="update",
).to_dict(),
)
if not change.event_published:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subscribe_in.id,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="update",
).to_dict(),
)
return _SchemaResponse(success=True)
@@ -327,16 +327,16 @@ async def update_subscribe_status(
change = await mutation.update_status(subid, state, actor)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="status",
).to_dict(),
)
if not change.event_published:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="status",
).to_dict(),
)
return _SchemaResponse(success=True)
@@ -388,15 +388,16 @@ async def reset_subscribes(
)
change = await mutation.reset(subid, actor)
if change:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="reset",
).to_dict(),
)
if not change.event_published:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="reset",
).to_dict(),
)
return _SchemaResponse(success=True)
return _SchemaResponse(success=False, message="订阅不存在")