refactor: finish transactional runtime migration

This commit is contained in:
jxxghp
2026-08-22 15:18:16 +08:00
parent fe2e6809f7
commit be18cace1f
59 changed files with 1006 additions and 568 deletions
+16 -1
View File
@@ -1,6 +1,6 @@
"""从 FastAPI AppState 读取类型化宿主能力。"""
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, Generator
from typing import cast
from fastapi import Depends, Request
@@ -39,6 +39,21 @@ def get_api_runtime_config(
return runtime.configuration.api()
def get_sync_session(
runtime: HostRuntime = Depends(get_host_runtime),
) -> Generator[object, None, None]:
"""从 HostRuntime 生成请求独占的同步数据库会话。"""
yield from runtime.persistence.sync_session()
async def get_async_session(
runtime: HostRuntime = Depends(get_host_runtime),
) -> AsyncGenerator[object, None]:
"""从 HostRuntime 生成请求独占的异步数据库会话。"""
async for session in runtime.persistence.async_session():
yield session
def resolve_api_runtime_config(value: object) -> ApiRuntimeConfig:
"""兼容直接调用 endpoint 的旧入口,并统一返回真实配置快照。"""
if isinstance(value, ApiRuntimeConfig):
+10 -5
View File
@@ -3,15 +3,19 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.context import get_agent_chat_repository, get_agent_chat_transaction
from app.api.data import get_async_db
from app.api.dependencies.data import repository
from app.api.context import (
get_agent_chat_repository,
get_agent_chat_transaction,
get_async_session,
get_host_runtime,
)
from app.application.messaging.chat import (
AgentChatService,
AsyncAgentChatRepository,
AsyncUnitOfWork,
)
from app.application.messaging.message import MessageQueryService
from app.startup.context import HostRuntime
def get_agent_chat_service(
@@ -23,7 +27,8 @@ def get_agent_chat_service(
def get_message_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> MessageQueryService:
"""组装消息历史异步查询服务。"""
return MessageQueryService(repository=repository("message", db))
return MessageQueryService(repository=runtime.messaging.repository(db))
+53 -22
View File
@@ -1,58 +1,89 @@
"""用户身份、授权与认证服务依赖。"""
from typing import Any
from typing import Any, cast
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.web.security.access import verify_token
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, standalone_repository
from app.application.security.auth import AuthService
from app.application.security.passkeys import PasskeyService
from app.application.security.user import UserService
from app.api.context import get_async_session, get_host_runtime, get_sync_session
from app.application.security.auth import (
AuthConfigRepository,
AuthPasskeyRepository,
AuthService,
AuthUserRepository,
)
from app.application.security.passkeys import PasskeyRepository, PasskeyService
from app.application.security.user import (
AsyncUnitOfWork,
UserRepository,
UserService,
)
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.startup.context import HostRuntime
def get_user_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> UserService:
"""组装用户管理应用服务。"""
return UserService(repository=repository("user", db))
def get_auth_service() -> AuthService:
"""组装同步认证应用服务。"""
return AuthService(
users=standalone_repository("user"),
config=standalone_repository("system_config"),
passkeys=standalone_repository("passkey"),
return UserService(
repository=cast(
UserRepository, runtime.authentication.user_repository(db)
),
unit_of_work=cast(
AsyncUnitOfWork, runtime.persistence.async_transaction(db)
),
)
def get_passkey_service() -> PasskeyService:
def get_auth_service(
runtime: HostRuntime = Depends(get_host_runtime),
) -> AuthService:
"""组装同步认证应用服务。"""
return AuthService(
users=cast(AuthUserRepository, runtime.authentication.standalone_user()),
config=cast(AuthConfigRepository, runtime.authentication.system_config()),
passkeys=cast(AuthPasskeyRepository, runtime.authentication.passkey()),
)
def get_passkey_service(
runtime: HostRuntime = Depends(get_host_runtime),
) -> PasskeyService:
"""组装 PassKey 应用服务。"""
return PasskeyService(repository=standalone_repository("passkey"))
return PasskeyService(repository=cast(
PasskeyRepository, runtime.authentication.passkey()
))
def get_current_user(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
token_data: _SchemaTokenPayload = Depends(verify_token),
runtime: HostRuntime = Depends(get_host_runtime),
) -> Any:
"""读取令牌对应用户,不存在时返回 403。"""
user = repository("user", db).get_by_id(token_data.sub)
user_repository = cast(
AuthUserRepository, runtime.authentication.user_repository(db)
)
user = user_repository.get_by_id(token_data.sub)
if not user:
raise HTTPException(status_code=403, detail="用户不存在")
return user
async def get_current_user_async(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
token_data: _SchemaTokenPayload = Depends(verify_token),
runtime: HostRuntime = Depends(get_host_runtime),
) -> Any:
"""异步读取令牌对应用户,不存在时返回 403。"""
user = await repository("user", db).async_get_by_id(token_data.sub)
user_repository = cast(
UserRepository, runtime.authentication.user_repository(db)
)
user = await user_repository.async_get_by_id(token_data.sub)
if not user:
raise HTTPException(status_code=403, detail="用户不存在")
return user
+28 -18
View File
@@ -4,8 +4,7 @@ from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.api.context import get_async_session, get_host_runtime, get_sync_session
from app.application.dashboard import DashboardQueryService
from app.application.history import (
DownloadHistoryMutationCommand,
@@ -19,63 +18,74 @@ from app.chain.storage import StorageChain
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.startup.context import HostRuntime
def get_mediaserver_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> MediaServerQueryService:
"""组装媒体服务器本地条目异步查询服务。"""
return MediaServerQueryService(repository=repository("media_server", db))
return MediaServerQueryService(
repository=runtime.history.media_server_repository(db)
)
def get_dashboard_query_service(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> DashboardQueryService:
"""组装 Dashboard 媒体与整理历史统计查询服务。"""
from app.chain.dashboard import DashboardChain
return DashboardQueryService(
repository=repository("transfer_history", db),
repository=runtime.history.transfer_repository(db),
media_statistics=DashboardChain().media_statistic,
)
def get_download_history_mutation_command(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> DownloadHistoryMutationCommand:
"""组装下载历史删除用例及其请求级事务。"""
return DownloadHistoryMutationCommand(
repository=repository("download_history", db),
unit_of_work=transaction("sync", db),
repository=runtime.history.download_repository(db),
unit_of_work=runtime.persistence.sync_transaction(db),
)
def get_history_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> HistoryQueryService:
"""组装历史列表和详情异步查询服务。"""
return HistoryQueryService(
download_repository=repository("download_history", db),
transfer_repository=repository("transfer_history", db),
download_repository=runtime.history.download_repository(db),
transfer_repository=runtime.history.transfer_repository(db),
)
def get_transfer_history_lookup_service(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> TransferHistoryLookupService:
"""组装手动整理使用的同步历史投影服务。"""
return TransferHistoryLookupService(repository("transfer_history", db))
return TransferHistoryLookupService(
runtime.history.transfer_repository(db)
)
def get_transfer_history_mutation_command(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> TransferHistoryMutationCommand:
"""组装整理历史删除、文件处理和事件发布用例。"""
storage_chain = StorageChain()
return TransferHistoryMutationCommand(
repository=repository("transfer_history", db),
download_repository=repository("download_history", db),
unit_of_work=transaction("sync", db),
repository=runtime.history.transfer_repository(db),
download_repository=runtime.history.download_repository(db),
unit_of_work=runtime.persistence.sync_transaction(db),
file_item_factory=lambda payload: _SchemaFileItem(**payload),
delete_media_file=storage_chain.delete_media_file,
publish_download_file_deleted=lambda payload: eventmanager.send_event(
+16 -11
View File
@@ -1,11 +1,12 @@
"""站点领域的请求级 command/query 依赖。"""
from typing import Any
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.api.context import get_async_session, get_host_runtime, get_sync_session
from app.application.site.mutation import SiteMutationCommand
from app.application.site.query import SiteQueryService
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
@@ -13,20 +14,22 @@ from app.domain import site as site_rules
from app.foundation import url as url_tools
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.startup.context import HostRuntime
async def _publish_site_updated(payload: dict) -> None:
async def _publish_site_updated(payload: dict[str, Any]) -> None:
"""发布已提交的站点更新事件。"""
await eventmanager.async_send_event(EventType.SiteUpdated, payload)
async def _publish_site_deleted(payload: dict) -> None:
async def _publish_site_deleted(payload: dict[str, Any]) -> None:
"""发布已提交的站点删除事件。"""
await eventmanager.async_send_event(EventType.SiteDeleted, payload)
def get_site_mutation_command(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SiteMutationCommand:
"""组装请求级站点写用例及其事务和外部目录依赖。"""
sites_helper = SitesHelper()
@@ -37,8 +40,8 @@ def get_site_mutation_command(
return f"{scheme}://{netloc}/"
return SiteMutationCommand(
repository=repository("site", db),
unit_of_work=transaction("async", db),
repository=runtime.site.repository(db),
unit_of_work=runtime.persistence.async_transaction(db),
auth_level_provider=lambda: sites_helper.auth_level,
indexer_loader=sites_helper.async_get_indexer,
domain_extractor=site_rules.extract_domain,
@@ -49,14 +52,16 @@ def get_site_mutation_command(
def get_site_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SiteQueryService:
"""组装站点异步查询服务。"""
return SiteQueryService(repository=repository("site", db))
return SiteQueryService(repository=runtime.site.repository(db))
def get_site_sync_query_service(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SiteQueryService:
"""组装站点同步查询服务,用于同步 Chain 路由。"""
return SiteQueryService(repository=repository("site", db))
return SiteQueryService(repository=runtime.site.repository(db))
+25 -14
View File
@@ -8,13 +8,14 @@ from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper
from app.api.context import (
get_async_session,
get_host_runtime,
get_subscription_history_repository,
get_subscription_outbox,
get_subscription_repository,
get_subscription_transaction,
get_sync_session,
)
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository
from app.application.outbox import AsyncOutboxTransaction
from app.application.scheduling import start_scheduler_job
from app.application.servarr import ServarrSubscriptionService
@@ -36,6 +37,7 @@ from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager
from app.runtime.log import logger
from app.schemas.types import EventType
from app.startup.context import HostRuntime
async def _publish_subscribe_deleted(
@@ -93,7 +95,8 @@ def get_delete_subscriptions_by_identity_command(
def get_search_subscriptions_command(
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SearchSubscriptionsCommand:
"""组装手工订阅搜索用例,并把调度延迟到响应后的后台任务。"""
def schedule_search(subscribe_id: int | None, state: str | None) -> None:
@@ -107,19 +110,20 @@ def get_search_subscriptions_command(
)
return SearchSubscriptionsCommand(
repository=repository("subscribe", db),
repository=runtime.subscription.repository(db),
schedule_search=schedule_search,
)
def get_subscription_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SubscriptionQueryService:
"""组装订阅和订阅历史异步查询服务。"""
return SubscriptionQueryService(
repository=repository("subscribe", db),
async_repository=repository("subscribe", db),
history_repository=repository("subscribe_history", db),
repository=runtime.subscription.repository(db),
async_repository=runtime.subscription.repository(db),
history_repository=runtime.subscription.history_repository(db),
)
@@ -142,18 +146,25 @@ def get_subscription_mutation_service(
def get_subscription_sync_mutation_service(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> SubscriptionMutationService:
"""组装同步订阅查询服务,供文件信息接口使用。"""
return SubscriptionMutationService(repository=repository("subscribe", db))
return SubscriptionMutationService(
repository=cast(
SubscriptionMutationRepository,
runtime.subscription.repository(db),
)
)
def get_servarr_subscription_service(
async_db: AsyncSession = Depends(get_async_db),
db: Session = Depends(get_db),
async_db: AsyncSession = Depends(get_async_session),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> ServarrSubscriptionService:
"""组装 Servarr 兼容路由的请求级订阅数据用例。"""
return ServarrSubscriptionService(
async_repository=repository("subscribe", async_db),
sync_repository=repository("subscribe", db),
async_repository=runtime.subscription.repository(async_db),
sync_repository=runtime.subscription.repository(db),
)
+19 -14
View File
@@ -1,12 +1,13 @@
"""工作流领域的请求级 command/query 依赖。"""
from typing import Any, cast
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, standalone_repository, transaction
from app.api.context import get_async_session, get_host_runtime, get_sync_session
from app.application.scheduling import Scheduler
from app.application.workflow import (
WorkflowDefinitionCommand,
@@ -15,46 +16,50 @@ from app.application.workflow import (
)
from app.runtime.config import global_vars
from app.workflow import WorkFlowManager
from app.startup.context import HostRuntime
def get_workflow_mutation_command(
db: Session = Depends(get_db),
db: Session = Depends(get_sync_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> WorkflowMutationCommand:
"""组装请求级工作流写用例和提交后的调度副作用。"""
scheduler = Scheduler()
workflow_manager = WorkFlowManager()
return WorkflowMutationCommand(
repository=repository("workflow", db),
unit_of_work=transaction("sync", db),
repository=runtime.workflow.repository(db),
unit_of_work=runtime.persistence.sync_transaction(db),
add_timer=scheduler.update_workflow_job,
remove_timer=scheduler.remove_workflow_job,
load_event=workflow_manager.load_workflow_events,
remove_event=workflow_manager.remove_workflow_event,
refresh_event=workflow_manager.update_workflow_event,
stop_running=global_vars.stop_workflow,
delete_cache=lambda workflow_id: standalone_repository(
"system_config"
delete_cache=lambda workflow_id: cast(
Any, runtime.workflow.system_config()
).delete(f"WorkflowCache-{workflow_id}"),
)
def get_workflow_definition_command(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> WorkflowDefinitionCommand:
"""组装工作流创建、复用和重置的异步写用例。"""
return WorkflowDefinitionCommand(
repository=repository("workflow", db),
unit_of_work=transaction("async", db),
repository=runtime.workflow.repository(db),
unit_of_work=runtime.persistence.async_transaction(db),
stop_running=global_vars.stop_workflow,
delete_cache=lambda workflow_id: standalone_repository(
"system_config"
delete_cache=lambda workflow_id: cast(
Any, runtime.workflow.system_config()
).delete(f"WorkflowCache-{workflow_id}"),
report_fork=MoviePilotServerHelper.async_workflow_fork_by_id,
)
def get_workflow_query_service(
db: AsyncSession = Depends(get_async_db),
db: AsyncSession = Depends(get_async_session),
runtime: HostRuntime = Depends(get_host_runtime),
) -> WorkflowQueryService:
"""组装工作流只读查询用例,避免端点直接持有数据库操作器。"""
return WorkflowQueryService(repository=repository("workflow", db))
return WorkflowQueryService(repository=runtime.workflow.repository(db))