mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
refactor: reorganize startup persistence boundaries
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""宿主运行时对象、配置快照与跨层依赖的组合构建。"""
|
||||
@@ -0,0 +1,174 @@
|
||||
"""把可变部署设置转换成宿主各领域使用的类型化配置快照。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.application.configuration import (
|
||||
ApiRuntimeConfig,
|
||||
ChainRuntimeConfig,
|
||||
SchedulerRuntimeConfig,
|
||||
TokenRuntimeConfig,
|
||||
)
|
||||
from app.runtime.config import Settings
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
def normalize_subscribe_rss_interval(value: object) -> int:
|
||||
"""把无效或过小的 RSS 间隔收敛为兼容的安全值。"""
|
||||
try:
|
||||
if not isinstance(value, (str, bytes, bytearray, int, float)):
|
||||
return 30
|
||||
return max(int(value), 5)
|
||||
except (TypeError, ValueError):
|
||||
return 30
|
||||
|
||||
|
||||
def build_api_runtime_config(settings: Settings) -> ApiRuntimeConfig:
|
||||
"""从可热更新的部署设置构建一次 API 请求配置快照。"""
|
||||
return ApiRuntimeConfig(
|
||||
advanced_mode=settings.ADVANCED_MODE,
|
||||
access_token_expire_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
btrfs_fsid_dedup=settings.BTRFS_FSID_DEDUP,
|
||||
ai_agent_enable=settings.AI_AGENT_ENABLE,
|
||||
api_token=settings.API_TOKEN,
|
||||
temp_path=settings.TEMP_PATH,
|
||||
media_recognize_share=settings.MEDIA_RECOGNIZE_SHARE,
|
||||
subscribe_mode=settings.SUBSCRIBE_MODE,
|
||||
search_source=settings.SEARCH_SOURCE,
|
||||
media_extensions=tuple(settings.RMT_MEDIAEXT),
|
||||
subtitle_extensions=tuple(settings.RMT_SUBEXT),
|
||||
audio_extensions=tuple(settings.RMT_AUDIOEXT),
|
||||
movie_rename_format=settings.RENAME_FORMAT(MediaType.MOVIE),
|
||||
television_rename_format=settings.RENAME_FORMAT(MediaType.TV),
|
||||
music_rename_format=settings.RENAME_FORMAT(MediaType.MUSIC),
|
||||
vapid_private_key=settings.VAPID.get("privateKey", ""),
|
||||
vapid_subject=settings.VAPID.get("subject", ""),
|
||||
cookiecloud_enable_local=bool(settings.COOKIECLOUD_ENABLE_LOCAL),
|
||||
cookiecloud_auth_header=settings.COOKIECLOUD_AUTH_HEADER,
|
||||
cookie_path=settings.COOKIE_PATH,
|
||||
root_path=settings.ROOT_PATH,
|
||||
version_flag=settings.VERSION_FLAG,
|
||||
app_domain=settings.APP_DOMAIN,
|
||||
nginx_port=settings.NGINX_PORT,
|
||||
passkey_require_uv=settings.PASSKEY_REQUIRE_UV,
|
||||
)
|
||||
|
||||
|
||||
def build_token_runtime_config(settings: Settings) -> TokenRuntimeConfig:
|
||||
"""从部署设置构建令牌编解码使用的安全配置快照。"""
|
||||
return TokenRuntimeConfig(
|
||||
secret_key=settings.SECRET_KEY,
|
||||
resource_secret_key=settings.RESOURCE_SECRET_KEY,
|
||||
access_token_expire_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
resource_access_token_expire_seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
def build_scheduler_runtime_config(settings: Settings) -> SchedulerRuntimeConfig:
|
||||
"""从可热更新的部署设置构建一次 Scheduler 配置快照。"""
|
||||
return SchedulerRuntimeConfig(
|
||||
dev=settings.DEV,
|
||||
timezone=settings.TZ,
|
||||
scheduler_workers=settings.CONF.scheduler,
|
||||
db_backup_enable=settings.DB_BACKUP_ENABLE,
|
||||
db_backup_cron=settings.DB_BACKUP_CRON,
|
||||
cookiecloud_interval=settings.COOKIECLOUD_INTERVAL,
|
||||
mediaserver_sync_interval=settings.MEDIASERVER_SYNC_INTERVAL,
|
||||
subscribe_search=settings.SUBSCRIBE_SEARCH,
|
||||
subscribe_search_interval=settings.SUBSCRIBE_SEARCH_INTERVAL,
|
||||
subscribe_mode=settings.SUBSCRIBE_MODE,
|
||||
subscribe_rss_interval=normalize_subscribe_rss_interval(
|
||||
settings.SUBSCRIBE_RSS_INTERVAL
|
||||
),
|
||||
data_cleanup_enable=settings.DATA_CLEANUP_ENABLE,
|
||||
sitedata_refresh_interval=settings.SITEDATA_REFRESH_INTERVAL,
|
||||
memory_gc_interval=settings.MEMORY_GC_INTERVAL,
|
||||
ai_agent_enable=settings.AI_AGENT_ENABLE,
|
||||
ai_agent_job_interval=settings.AI_AGENT_JOB_INTERVAL,
|
||||
usage_statistic_share=settings.USAGE_STATISTIC_SHARE,
|
||||
site_link=settings.MP_DOMAIN("#/site"),
|
||||
)
|
||||
|
||||
|
||||
def build_chain_runtime_config(settings: Settings) -> ChainRuntimeConfig:
|
||||
"""从部署设置构建 Chain 在本次实例生命周期使用的配置快照。"""
|
||||
return ChainRuntimeConfig(
|
||||
media_extensions=tuple(
|
||||
settings.RMT_MEDIAEXT
|
||||
+ settings.DOWNLOAD_TMPEXT
|
||||
+ settings.RMT_SUBEXT
|
||||
+ settings.RMT_AUDIOEXT
|
||||
),
|
||||
api_port=settings.PORT,
|
||||
api_token=settings.API_TOKEN,
|
||||
video_extensions=tuple(settings.RMT_MEDIAEXT),
|
||||
subtitle_extensions=tuple(settings.RMT_SUBEXT),
|
||||
audio_extensions=tuple(settings.RMT_AUDIOEXT),
|
||||
temporary_path=settings.TEMP_PATH,
|
||||
root_path=settings.ROOT_PATH,
|
||||
config_path=settings.CONFIG_PATH,
|
||||
frontend_path=Path(settings.FRONTEND_PATH),
|
||||
superuser=settings.SUPERUSER,
|
||||
media_recognize_share=settings.MEDIA_RECOGNIZE_SHARE,
|
||||
auxiliary_auth_enable=settings.AUXILIARY_AUTH_ENABLE,
|
||||
global_image_cache=settings.GLOBAL_IMAGE_CACHE,
|
||||
encoding_detection_performance_mode=settings.ENCODING_DETECTION_PERFORMANCE_MODE,
|
||||
encoding_detection_min_confidence=settings.ENCODING_DETECTION_MIN_CONFIDENCE,
|
||||
data_cleanup_enable=settings.DATA_CLEANUP_ENABLE,
|
||||
data_cleanup_message_days=settings.DATA_CLEANUP_MESSAGE_DAYS,
|
||||
data_cleanup_download_history_days=settings.DATA_CLEANUP_DOWNLOAD_HISTORY_DAYS,
|
||||
data_cleanup_site_userdata_days=settings.DATA_CLEANUP_SITE_USERDATA_DAYS,
|
||||
data_cleanup_transfer_history_days=settings.DATA_CLEANUP_TRANSFER_HISTORY_DAYS,
|
||||
data_cleanup_download_failure_days=settings.DATA_CLEANUP_DOWNLOAD_FAILURE_DAYS,
|
||||
download_subtitle=settings.DOWNLOAD_SUBTITLE,
|
||||
music_metadata_to_simplified=settings.MUSIC_METADATA_TO_SIMPLIFIED,
|
||||
recognize_plugin_first=settings.RECOGNIZE_PLUGIN_FIRST,
|
||||
ai_agent_enable=settings.AI_AGENT_ENABLE,
|
||||
ai_agent_global=settings.AI_AGENT_GLOBAL,
|
||||
ai_agent_retry_transfer=settings.AI_AGENT_RETRY_TRANSFER,
|
||||
llm_provider=settings.LLM_PROVIDER,
|
||||
llm_model=settings.LLM_MODEL,
|
||||
search_resource_pages=settings.SEARCH_RESOURCE_PAGES,
|
||||
ai_recommend_enabled=settings.AI_RECOMMEND_ENABLED,
|
||||
ai_recommend_max_items=settings.AI_RECOMMEND_MAX_ITEMS,
|
||||
ai_recommend_user_preference=settings.AI_RECOMMEND_USER_PREFERENCE,
|
||||
max_search_name_limit=settings.MAX_SEARCH_NAME_LIMIT,
|
||||
search_multiple_name=settings.SEARCH_MULTIPLE_NAME,
|
||||
search_threadpool_size=settings.CONF.threadpool,
|
||||
transfer_threads=settings.TRANSFER_THREADS,
|
||||
transfer_failure_notification_aggregation=(
|
||||
settings.TRANSFER_FAILURE_NOTIFICATION_AGGREGATION
|
||||
),
|
||||
transfer_task_timeout=settings.TRANSFER_TASK_TIMEOUT,
|
||||
scrape_follow_tmdb=settings.SCRAP_FOLLOW_TMDB,
|
||||
metadata_cache_ttl=settings.CONF.meta,
|
||||
auto_download_user=settings.AUTO_DOWNLOAD_USER,
|
||||
resource_url=settings.MP_DOMAIN("#/resource"),
|
||||
history_url=settings.MP_DOMAIN("#/history"),
|
||||
downloading_url=settings.MP_DOMAIN("#/downloading"),
|
||||
movie_subscribe_url=settings.MP_DOMAIN("#/subscribe/movie?tab=mysub"),
|
||||
television_subscribe_url=settings.MP_DOMAIN("#/subscribe/tv?tab=mysub"),
|
||||
music_subscribe_url=settings.MP_DOMAIN("#/subscribe/music?tab=mysub"),
|
||||
user_agent=settings.USER_AGENT,
|
||||
normal_user_agent=settings.NORMAL_USER_AGENT,
|
||||
proxy=settings.PROXY,
|
||||
proxy_server=settings.PROXY_SERVER,
|
||||
proxy_host=settings.PROXY_HOST,
|
||||
github_headers=settings.GITHUB_HEADERS,
|
||||
cookiecloud_blacklist=settings.COOKIECLOUD_BLACKLIST,
|
||||
subscribe_mode=settings.SUBSCRIBE_MODE,
|
||||
no_cache_site_key=settings.NO_CACHE_SITE_KEY,
|
||||
refresh_batch_size=settings.CONF.refresh,
|
||||
torrent_cache_size=settings.CONF.torrents,
|
||||
site_url=settings.MP_DOMAIN("#/site"),
|
||||
workflow_url=settings.MP_DOMAIN("#/workflow"),
|
||||
season_zero_names=tuple(settings.RENAME_FORMAT_S0_NAMES),
|
||||
movie_rename_format=settings.RENAME_FORMAT(MediaType.MOVIE),
|
||||
television_rename_format=settings.RENAME_FORMAT(MediaType.TV),
|
||||
music_rename_format=settings.RENAME_FORMAT(MediaType.MUSIC),
|
||||
tmdb_image_domain=settings.TMDB_IMAGE_DOMAIN,
|
||||
wallpaper=settings.WALLPAPER,
|
||||
customize_wallpaper_api_url=settings.CUSTOMIZE_WALLPAPER_API_URL,
|
||||
security_image_suffixes=tuple(settings.SECURITY_IMAGE_SUFFIXES),
|
||||
cache_path=settings.CACHE_PATH,
|
||||
global_image_cache_days=settings.GLOBAL_IMAGE_CACHE_DAYS,
|
||||
)
|
||||
@@ -0,0 +1,198 @@
|
||||
"""宿主启动阶段构建的类型化运行时上下文。"""
|
||||
|
||||
from collections.abc import AsyncGenerator, Callable, Generator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Protocol
|
||||
|
||||
from app.runtime.tasks import TaskRegistry
|
||||
|
||||
from app.application.messaging.chat import (
|
||||
AsyncAgentChatRepository,
|
||||
AgentChatPersistenceService,
|
||||
AsyncUnitOfWork,
|
||||
)
|
||||
from app.application.outbox import AsyncOutboxTransaction
|
||||
from app.application.configuration import RuntimeConfiguration, RuntimeSettingsService
|
||||
from app.application.subscription.delete import SubscribeDeletionRepository
|
||||
from app.application.subscription.identity import SubscribeIdentityDeletionRepository
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionHistoryMutationRepository,
|
||||
SubscriptionMutationRepository,
|
||||
)
|
||||
from app.application.workflow import WorkflowCachePort
|
||||
|
||||
|
||||
class AgentChatRepositoryFactory(Protocol):
|
||||
"""由请求会话构造 Agent 会话仓储的工厂端口。"""
|
||||
|
||||
def __call__(self, session: object) -> AsyncAgentChatRepository:
|
||||
"""绑定请求会话并返回 Agent 会话仓储。"""
|
||||
...
|
||||
|
||||
|
||||
class AsyncUnitOfWorkFactory(Protocol):
|
||||
"""由请求会话构造异步事务端口的工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> AsyncUnitOfWork:
|
||||
"""绑定请求会话并返回异步事务端口。"""
|
||||
...
|
||||
|
||||
|
||||
class AsyncOutboxFactory(Protocol):
|
||||
"""由请求会话构造异步 outbox 事务端口的工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> AsyncOutboxTransaction:
|
||||
"""绑定请求会话并返回 outbox 暂存与收口端口。"""
|
||||
...
|
||||
|
||||
|
||||
class SubscriptionRepositoryFactory(Protocol):
|
||||
"""由请求会话构造订阅写仓储的工厂。"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
session: object,
|
||||
) -> (
|
||||
SubscriptionMutationRepository
|
||||
| SubscribeDeletionRepository
|
||||
| SubscribeIdentityDeletionRepository
|
||||
):
|
||||
"""绑定请求会话并返回订阅领域仓储。"""
|
||||
...
|
||||
|
||||
|
||||
class SubscriptionHistoryRepositoryFactory(Protocol):
|
||||
"""由请求会话构造订阅历史写仓储的工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> SubscriptionHistoryMutationRepository:
|
||||
"""绑定请求会话并返回订阅历史仓储。"""
|
||||
...
|
||||
|
||||
|
||||
class AsyncSessionProvider(Protocol):
|
||||
"""FastAPI 请求级异步会话提供器。"""
|
||||
|
||||
def __call__(self) -> AsyncGenerator[object, None]:
|
||||
"""生成一个请求独占的异步数据库会话。"""
|
||||
...
|
||||
|
||||
|
||||
class SyncSessionProvider(Protocol):
|
||||
"""兼容 API Facade 使用的同步会话提供器。"""
|
||||
|
||||
def __call__(self) -> Generator[object, None, None]:
|
||||
"""生成一个请求独占的同步数据库会话。"""
|
||||
...
|
||||
|
||||
|
||||
class RepositoryFactory(Protocol):
|
||||
"""由请求 Session 构造某一明确领域仓储的通用工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> object:
|
||||
"""绑定请求会话并返回领域仓储。"""
|
||||
...
|
||||
|
||||
|
||||
class StandaloneRepositoryFactory(Protocol):
|
||||
"""构造自持有兼容事务边界的领域仓储。"""
|
||||
|
||||
def __call__(self) -> object:
|
||||
"""返回无需请求 Session 的领域仓储。"""
|
||||
...
|
||||
|
||||
|
||||
class SyncUnitOfWorkFactory(Protocol):
|
||||
"""由同步请求 Session 构造事务端口的工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> object:
|
||||
"""绑定请求会话并返回同步事务端口。"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AgentChatRuntime:
|
||||
"""Agent 会话 API 可见的最小数据运行时。"""
|
||||
|
||||
async_session: AsyncSessionProvider
|
||||
repository: AgentChatRepositoryFactory
|
||||
transaction: AsyncUnitOfWorkFactory
|
||||
persistence: AgentChatPersistenceService
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PersistenceRuntime:
|
||||
"""全部 HTTP 业务领域共享的请求会话与事务工厂。"""
|
||||
|
||||
sync_session: SyncSessionProvider
|
||||
async_session: AsyncSessionProvider
|
||||
sync_transaction: SyncUnitOfWorkFactory
|
||||
async_transaction: AsyncUnitOfWorkFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AuthenticationRuntime:
|
||||
"""认证、用户管理与 PassKey API 的显式数据工厂。"""
|
||||
|
||||
user_repository: RepositoryFactory
|
||||
standalone_user: StandaloneRepositoryFactory
|
||||
system_config: StandaloneRepositoryFactory
|
||||
passkey: StandaloneRepositoryFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MessagingRuntime:
|
||||
"""消息历史 API 的显式仓储工厂。"""
|
||||
|
||||
repository: RepositoryFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HistoryRuntime:
|
||||
"""下载、整理、媒体服务器与 Dashboard 领域的数据工厂。"""
|
||||
|
||||
download_repository: RepositoryFactory
|
||||
transfer_repository: RepositoryFactory
|
||||
media_server_repository: RepositoryFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SiteRuntime:
|
||||
"""站点读写领域的显式仓储工厂。"""
|
||||
|
||||
repository: RepositoryFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkflowRuntime:
|
||||
"""工作流定义、状态与缓存操作所需的数据工厂。"""
|
||||
|
||||
repository: RepositoryFactory
|
||||
system_config: Callable[[], WorkflowCachePort]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SubscriptionRuntime:
|
||||
"""订阅 API 可见的请求级写事务运行时。"""
|
||||
|
||||
async_session: AsyncSessionProvider
|
||||
repository: SubscriptionRepositoryFactory
|
||||
history_repository: SubscriptionHistoryRepositoryFactory
|
||||
transaction: AsyncUnitOfWorkFactory
|
||||
outbox: AsyncOutboxFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HostRuntime:
|
||||
"""宿主组合根构建且在一个 FastAPI lifespan 内共享的运行时对象。"""
|
||||
|
||||
agent_chat: AgentChatRuntime
|
||||
persistence: PersistenceRuntime
|
||||
authentication: AuthenticationRuntime
|
||||
messaging: MessagingRuntime
|
||||
history: HistoryRuntime
|
||||
site: SiteRuntime
|
||||
subscription: SubscriptionRuntime
|
||||
workflow: WorkflowRuntime
|
||||
configuration: RuntimeConfiguration
|
||||
settings: RuntimeSettingsService
|
||||
tasks: TaskRegistry = field(default_factory=TaskRegistry)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""数据库治理能力的宿主组合根。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.adapters.system.backup.database import (
|
||||
PostgreSQLBackupBackend,
|
||||
SQLiteBackupBackend,
|
||||
)
|
||||
from app.application.backup import BackupPolicy, DatabaseBackupService
|
||||
from app.application.database import (
|
||||
DatabaseGovernance,
|
||||
DatabaseHealthService,
|
||||
configure_database_governance,
|
||||
)
|
||||
from app.application.maintenance import (
|
||||
DataCleanupService,
|
||||
read_cleanup_policy,
|
||||
)
|
||||
from app.db.engine import get_engine
|
||||
from app.db.health import probe_database
|
||||
from app.db.maintenance import DatabaseCleanupRepository
|
||||
from app.db.session import SessionFactory
|
||||
from app.runtime.settings import RuntimeSettingsCompat
|
||||
|
||||
settings = RuntimeSettingsCompat()
|
||||
|
||||
|
||||
def build_database_governance() -> DatabaseGovernance:
|
||||
"""以缓存同步引擎为事实源构造一个完整数据库治理门面。"""
|
||||
engine = get_engine()
|
||||
dialect = engine.dialect.name
|
||||
if dialect == "sqlite":
|
||||
backup_backend = SQLiteBackupBackend(engine)
|
||||
elif dialect == "postgresql":
|
||||
backup_backend = PostgreSQLBackupBackend(engine)
|
||||
else:
|
||||
raise RuntimeError(f"不支持的数据库类型:{dialect}")
|
||||
|
||||
return DatabaseGovernance(
|
||||
health=DatabaseHealthService(probe_database),
|
||||
cleanup=DataCleanupService(
|
||||
repository=DatabaseCleanupRepository(session_factory=SessionFactory),
|
||||
policy_reader=read_cleanup_policy,
|
||||
),
|
||||
backup=DatabaseBackupService(
|
||||
backend=backup_backend,
|
||||
policy_reader=read_backup_policy,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def configure_database() -> None:
|
||||
"""构造并登记宿主进程唯一的数据库治理门面。"""
|
||||
configure_database_governance(build_database_governance())
|
||||
|
||||
|
||||
def read_backup_policy() -> BackupPolicy:
|
||||
"""读取一次可热更新的数据库备份目录与保留策略。"""
|
||||
return BackupPolicy(
|
||||
root=settings.DATABASE_BACKUP_PATH,
|
||||
retention_days=settings.DB_BACKUP_RETENTION_DAYS,
|
||||
max_count=settings.DB_BACKUP_MAX_COUNT,
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
"""订阅事务作用域及提交后回调的组合装配。"""
|
||||
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import Any
|
||||
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.application.subscription.complete import (
|
||||
CompleteSubscriptionCommand,
|
||||
configure_subscription_completion_scope,
|
||||
)
|
||||
from app.application.subscription.delete import (
|
||||
DeleteSubscribeCommand,
|
||||
configure_delete_subscribe_scope,
|
||||
)
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionMutationService,
|
||||
configure_subscription_mutation_scope,
|
||||
)
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
SqlAlchemyOutboxRepository,
|
||||
)
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.db.session import SessionFactory, async_session_scope
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.runtime.events import EventManager
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
async def _publish_modified(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅修改事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeModified, payload)
|
||||
|
||||
|
||||
async def _publish_deleted(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅删除事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
|
||||
|
||||
|
||||
def _publish_completed(payload: dict[str, Any]) -> None:
|
||||
"""发布已提交的订阅完成事件。"""
|
||||
EventManager().send_event(EventType.SubscribeComplete, payload)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def subscription_completion_scope():
|
||||
"""为同步完成链创建独占 Session、UoW 与 durable outbox。"""
|
||||
session = SessionFactory()
|
||||
try:
|
||||
yield CompleteSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=SqlAlchemyOutboxRepository(session),
|
||||
publish=_publish_completed,
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def subscription_mutation_scope():
|
||||
"""为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield SubscriptionMutationService(
|
||||
repository=SubscribeOper(session),
|
||||
history_repository=SubscribeHistoryOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
publish_modified=_publish_modified,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def delete_subscribe_scope():
|
||||
"""为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield DeleteSubscribeCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
publish_deleted=_publish_deleted,
|
||||
report_deleted=MoviePilotServerHelper.async_sub_done_durable,
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
)
|
||||
|
||||
|
||||
def configure_transactional_subscription_scopes() -> None:
|
||||
"""登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
|
||||
configure_subscription_mutation_scope(subscription_mutation_scope)
|
||||
configure_delete_subscribe_scope(delete_subscribe_scope)
|
||||
configure_subscription_completion_scope(subscription_completion_scope)
|
||||
Reference in New Issue
Block a user