mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: finish transactional runtime migration
This commit is contained in:
+16
-1
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -45,6 +45,10 @@ class CleanupRepository(Protocol):
|
||||
"""返回一次维护运行共用的数据库会话上下文。"""
|
||||
...
|
||||
|
||||
def unit_of_work(self, db: Any) -> "CleanupUnitOfWork":
|
||||
"""返回绑定到当前维护会话的事务边界。"""
|
||||
...
|
||||
|
||||
def delete_messages(self, db: Any, cutoff: str, limit: int) -> int:
|
||||
"""删除早于截止时间的消息。"""
|
||||
...
|
||||
@@ -70,6 +74,18 @@ class CleanupRepository(Protocol):
|
||||
...
|
||||
|
||||
|
||||
class CleanupUnitOfWork(Protocol):
|
||||
"""数据维护每一批删除所需的最小事务能力。"""
|
||||
|
||||
def commit(self) -> None:
|
||||
"""提交当前批次。"""
|
||||
...
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""回滚失败批次并恢复会话可用状态。"""
|
||||
...
|
||||
|
||||
|
||||
class DataCleanupService:
|
||||
"""按配置执行分批数据清理并生成兼容报告。"""
|
||||
|
||||
@@ -175,16 +191,19 @@ class DataCleanupService:
|
||||
value=plan_index / total_plans * 100,
|
||||
text=f"正在清理数据表 {plan.name} ...",
|
||||
)
|
||||
unit_of_work = self._repository.unit_of_work(db)
|
||||
table_report = self._cleanup_in_batches(
|
||||
db=db,
|
||||
table_name=plan.name,
|
||||
delete_batch=plan.delete_batch,
|
||||
unit_of_work=unit_of_work,
|
||||
)
|
||||
table_report["cutoff"] = plan.cutoff
|
||||
table_report["retention_days"] = plan.retention_days
|
||||
report["tables"][plan.name] = table_report
|
||||
report["total_deleted"] += table_report["deleted"]
|
||||
except Exception as err:
|
||||
self._repository.unit_of_work(db).rollback()
|
||||
errors.append(f"{plan.name}: {str(err)}")
|
||||
logger.error(f"数据表 {plan.name} 清理失败:{str(err)}")
|
||||
report["tables"][plan.name] = {
|
||||
@@ -279,12 +298,13 @@ class DataCleanupService:
|
||||
),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _cleanup_in_batches(
|
||||
self,
|
||||
*,
|
||||
db: Any,
|
||||
table_name: str,
|
||||
delete_batch: Callable[[Any], int],
|
||||
unit_of_work: CleanupUnitOfWork,
|
||||
) -> Dict[str, int]:
|
||||
"""循环执行单表分批删除,直到持久化端口返回零。"""
|
||||
total_deleted = 0
|
||||
@@ -293,6 +313,7 @@ class DataCleanupService:
|
||||
deleted = delete_batch(db) or 0
|
||||
if deleted <= 0:
|
||||
break
|
||||
unit_of_work.commit()
|
||||
batches += 1
|
||||
total_deleted += deleted
|
||||
logger.info(
|
||||
|
||||
@@ -146,14 +146,16 @@ class OutboxDispatcher:
|
||||
lease_seconds: int = 60,
|
||||
clock: Callable[[], datetime] | None = None,
|
||||
close: Callable[[], None] | None = None,
|
||||
failure_observer: Callable[[bool], None] | None = None,
|
||||
) -> None:
|
||||
"""注入持久端口、topic handler 和有界重试策略。"""
|
||||
"""注入持久端口、topic handler、有界重试策略与失败观测端口。"""
|
||||
self._repository = repository
|
||||
self._handlers = handlers
|
||||
self._max_attempts = max_attempts
|
||||
self._lease_seconds = lease_seconds
|
||||
self._clock = clock or (lambda: datetime.now(timezone.utc))
|
||||
self._close = close or (lambda: None)
|
||||
self._failure_observer = failure_observer or (lambda _dead: None)
|
||||
|
||||
def dispatch_one(self) -> bool:
|
||||
"""处理一条到期消息;无消息返回 False,handler 失败留待重试。"""
|
||||
@@ -176,6 +178,7 @@ class OutboxDispatcher:
|
||||
last_error=str(error)[:4000],
|
||||
dead=dead,
|
||||
)
|
||||
self._failure_observer(dead)
|
||||
return True
|
||||
self._repository.complete(message.message_id, now)
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
避免 API 层同时承担 HTTP 编排和 ORM 适配职责。
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
@@ -33,12 +33,27 @@ class UserRepository(Protocol):
|
||||
"""更新用户 OTP 状态。"""
|
||||
|
||||
|
||||
class AsyncUnitOfWork(Protocol):
|
||||
"""用户写用例所需的异步事务边界。"""
|
||||
|
||||
async def commit(self) -> None:
|
||||
"""提交用户写入。"""
|
||||
|
||||
async def rollback(self) -> None:
|
||||
"""回滚失败的用户写入。"""
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户管理应用服务。"""
|
||||
|
||||
def __init__(self, repository: UserRepository) -> None:
|
||||
"""创建用户服务。"""
|
||||
def __init__(
|
||||
self,
|
||||
repository: UserRepository,
|
||||
unit_of_work: AsyncUnitOfWork | None = None,
|
||||
) -> None:
|
||||
"""创建用户服务;旧独立仓储可暂不提供请求级 UoW。"""
|
||||
self._repository = repository
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
async def list(self) -> list[Any]:
|
||||
"""返回用户列表。"""
|
||||
@@ -54,19 +69,35 @@ class UserService:
|
||||
|
||||
async def create(self, payload: dict[str, Any]) -> Any | None:
|
||||
"""创建用户。"""
|
||||
return await self._repository.async_create(payload)
|
||||
return await self._write(lambda: self._repository.async_create(payload))
|
||||
|
||||
async def update(self, user_id: int, payload: dict[str, Any]) -> Any | None:
|
||||
"""更新用户。"""
|
||||
return await self._repository.async_update(user_id, payload)
|
||||
return await self._write(
|
||||
lambda: self._repository.async_update(user_id, payload)
|
||||
)
|
||||
|
||||
async def delete(self, user_id: int) -> None:
|
||||
"""删除用户。"""
|
||||
await self._repository.async_delete(user_id)
|
||||
await self._write(lambda: self._repository.async_delete(user_id))
|
||||
|
||||
async def update_otp(self, name: str, otp: bool, secret: str) -> None:
|
||||
"""更新用户 OTP 状态。"""
|
||||
await self._repository.async_update_otp_by_name(name, otp, secret)
|
||||
await self._write(
|
||||
lambda: self._repository.async_update_otp_by_name(name, otp, secret)
|
||||
)
|
||||
|
||||
async def _write(self, operation: Callable[[], Awaitable[Any]]) -> Any:
|
||||
"""执行用户写入,并在正式请求路径统一提交或回滚。"""
|
||||
try:
|
||||
result = await operation()
|
||||
if self._unit_of_work is not None:
|
||||
await self._unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
if self._unit_of_work is not None:
|
||||
await self._unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
|
||||
_configured_user_id_lookup: Callable[[int], Any | None] | None = None
|
||||
|
||||
+26
-1
@@ -4,7 +4,8 @@ ORM 基类与数据访问基类。
|
||||
Base 提供声明式基类与通用的行为(字典转换、增删改查便利方法);
|
||||
DbOper 是各业务 Oper 的基类,持有一个可注入的会话。
|
||||
"""
|
||||
from typing import Any, List, Optional, Self, Union, cast
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, List, Optional, Self, TypeVar, Union, cast
|
||||
|
||||
from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence,
|
||||
and_, delete, inspect, select)
|
||||
@@ -13,6 +14,10 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapp
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.db.decorators import async_db_query, async_db_update, db_query, db_update
|
||||
from app.db.uow import run_async_transaction, run_sync_transaction
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def execute_dml(db: Session, statement: Executable,
|
||||
@@ -147,4 +152,24 @@ class DbOper:
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
|
||||
"""保存调用方会话;无会话写入由组合根兼容事务执行器承接。"""
|
||||
self._db = db
|
||||
|
||||
def _execute_sync_write(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在当前同步会话暂存,或委托组合根创建兼容事务。"""
|
||||
if self._db is None:
|
||||
return run_sync_transaction(operation)
|
||||
if not isinstance(self._db, Session):
|
||||
raise TypeError("同步写操作不能使用 AsyncSession")
|
||||
return operation(self._db)
|
||||
|
||||
async def _execute_async_write(
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在当前异步会话暂存,或委托组合根创建兼容事务。"""
|
||||
if self._db is None:
|
||||
return await run_async_transaction(operation)
|
||||
if not isinstance(self._db, AsyncSession):
|
||||
raise TypeError("异步写操作不能使用同步 Session")
|
||||
return await operation(self._db)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.db.models.downloadhistory import DownloadFiles, DownloadHistory
|
||||
from app.db.models.message import Message
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
class DatabaseCleanupRepository:
|
||||
@@ -20,6 +21,11 @@ class DatabaseCleanupRepository:
|
||||
"""创建一次维护运行共用的数据库会话。"""
|
||||
return self._session_factory()
|
||||
|
||||
@staticmethod
|
||||
def unit_of_work(db: Any) -> SqlAlchemyUnitOfWork:
|
||||
"""把当前维护 Session 适配成显式批次事务边界。"""
|
||||
return SqlAlchemyUnitOfWork(db)
|
||||
|
||||
@staticmethod
|
||||
def delete_messages(db: Any, cutoff: str, limit: int) -> int:
|
||||
"""删除早于截止时间的消息。"""
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import db_query, db_update
|
||||
from app.db.decorators import db_query
|
||||
|
||||
|
||||
class AgentTask(Base):
|
||||
@@ -49,7 +49,6 @@ class AgentTask(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def add_task(cls, db: Session, **kwargs: object) -> int:
|
||||
"""
|
||||
新增 Agent 定时任务并返回任务 ID。
|
||||
@@ -96,7 +95,6 @@ class AgentTask(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def update_task(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy import Index, Integer, String, Text, delete, select, update
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import db_query, db_update
|
||||
from app.db.decorators import db_query
|
||||
from app.db.models.agenttask import AgentTask
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ class AgentTaskRun(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def begin_run(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -107,7 +106,6 @@ class AgentTaskRun(Base):
|
||||
return run_id
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def finish_run(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -174,7 +172,6 @@ class AgentTaskRun(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def interrupt_task(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -226,7 +223,6 @@ class AgentTaskRun(Base):
|
||||
))
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_task_and_runs(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -4,7 +4,6 @@ from sqlalchemy import Float, Index, Integer, String, delete, select
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import db_update
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
|
||||
|
||||
@@ -115,7 +114,6 @@ class DownloadFailure(Base):
|
||||
return failure
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_expired(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import async_db_query, db_query, db_update
|
||||
from app.db.decorators import async_db_query, db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
@@ -295,7 +295,6 @@ class DownloadHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_before(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -367,14 +366,12 @@ class DownloadFiles(Base):
|
||||
return list(db.execute(select(cls).where(cls.savepath == savepath)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_by_fullpath(cls, db: Session, fullpath: str):
|
||||
db.execute(
|
||||
update(cls).where(cls.fullpath == fullpath, cls.state == 1).values(state=0)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_orphans(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import async_db_query, db_query, db_update
|
||||
from app.db.decorators import async_db_query, db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
@@ -65,16 +65,16 @@ class MediaServerItem(Base):
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def empty(cls, db: Session, server: Optional[str] = None):
|
||||
"""在调用方事务中暂存媒体服务器条目清空操作。"""
|
||||
statement = delete(cls)
|
||||
if server is not None:
|
||||
statement = statement.where(cls.server == server)
|
||||
db.execute(statement, execution_options={"synchronize_session": False})
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_stale(cls, db: Session, server: str, sync_time: str):
|
||||
"""在调用方事务中删除本轮同步未更新的条目。"""
|
||||
return execute_dml(
|
||||
db,
|
||||
delete(cls).where(
|
||||
@@ -85,8 +85,8 @@ class MediaServerItem(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_excluded_servers(cls, db: Session, servers: List[str]):
|
||||
"""在调用方事务中删除不属于启用服务器的条目。"""
|
||||
statement = delete(cls)
|
||||
if servers:
|
||||
statement = statement.where(
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import async_db_query, db_query, db_update
|
||||
from app.db.decorators import async_db_query, db_query
|
||||
|
||||
|
||||
class Message(Base):
|
||||
@@ -40,7 +40,6 @@ class Message(Base):
|
||||
Index('ix_message_reg_time_id', 'reg_time', 'id'),
|
||||
)
|
||||
|
||||
@db_update
|
||||
def create_and_to_dict(self, db: Session) -> dict:
|
||||
"""
|
||||
创建消息记录并返回写入后的字段字典。
|
||||
@@ -134,7 +133,6 @@ class Message(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_before(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
+12
-16
@@ -1,11 +1,11 @@
|
||||
from typing import Optional
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime, Text, select, ForeignKey
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime, Text, select, ForeignKey, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
from datetime import datetime
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, db_update, async_db_query, async_db_update
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class PassKey(Base):
|
||||
@@ -85,19 +85,17 @@ class PassKey(Base):
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_by_id(cls, db: Session, passkey_id: int, user_id: int):
|
||||
"""删除指定用户的PassKey"""
|
||||
passkey = db.execute(
|
||||
select(cls).where(cls.id == passkey_id, cls.user_id == user_id)
|
||||
).scalars().first()
|
||||
if passkey:
|
||||
passkey.delete(db, passkey.id)
|
||||
db.delete(passkey)
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_delete_by_id(cls, db: AsyncSession, passkey_id: int, user_id: int):
|
||||
"""异步删除指定用户的PassKey"""
|
||||
result = await db.execute(
|
||||
@@ -108,24 +106,22 @@ class PassKey(Base):
|
||||
)
|
||||
passkey = result.scalars().first()
|
||||
if passkey:
|
||||
await passkey.async_delete(db, passkey.id)
|
||||
await db.delete(passkey)
|
||||
return True
|
||||
return False
|
||||
|
||||
@db_update
|
||||
def update_last_used(self, db: Session, sign_count: int):
|
||||
"""更新最后使用时间和签名计数"""
|
||||
self.update(db, {
|
||||
'last_used_at': datetime.now(),
|
||||
'sign_count': sign_count
|
||||
})
|
||||
db.execute(update(type(self)).where(type(self).id == self.id).values(
|
||||
last_used_at=datetime.now(),
|
||||
sign_count=sign_count,
|
||||
))
|
||||
return True
|
||||
|
||||
@async_db_update
|
||||
async def async_update_last_used(self, db: AsyncSession, sign_count: int):
|
||||
"""异步更新最后使用时间和签名计数"""
|
||||
await self.async_update(db, {
|
||||
'last_used_at': datetime.now(),
|
||||
'sign_count': sign_count
|
||||
})
|
||||
await db.execute(update(type(self)).where(type(self).id == self.id).values(
|
||||
last_used_at=datetime.now(),
|
||||
sign_count=sign_count,
|
||||
))
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import db_query, db_update, async_db_query
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class PluginData(Base):
|
||||
@@ -49,13 +49,13 @@ class PluginData(Base):
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def del_plugin_data_by_key(cls, db: Session, plugin_id: str, key: str):
|
||||
"""在调用方事务中暂存单个插件键删除。"""
|
||||
db.execute(delete(cls).where(cls.plugin_id == plugin_id, cls.key == key))
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def del_plugin_data(cls, db: Session, plugin_id: str):
|
||||
"""在调用方事务中暂存插件全部数据删除。"""
|
||||
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, db_update, async_db_query, async_db_update
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class Site(Base):
|
||||
@@ -102,11 +102,11 @@ class Site(Base):
|
||||
return list(db.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def reset(cls, db: Session):
|
||||
"""在调用方持有的同步事务中暂存清空操作。"""
|
||||
db.execute(delete(cls))
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_reset(cls, db: AsyncSession):
|
||||
"""在调用方持有的异步事务中暂存清空操作。"""
|
||||
await db.execute(delete(cls))
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import db_query, db_update, async_db_query
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class SiteStatistic(Base):
|
||||
@@ -41,6 +41,6 @@ class SiteStatistic(Base):
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def reset(cls, db: Session):
|
||||
"""在调用方持有的事务中暂存统计表清空操作。"""
|
||||
db.execute(delete(cls))
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import async_db_query, db_query, db_update
|
||||
from app.db.decorators import async_db_query, db_query
|
||||
|
||||
|
||||
class SiteUserData(Base):
|
||||
@@ -138,7 +138,6 @@ class SiteUserData(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_before(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, db_update, async_db_query
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
@@ -28,9 +28,9 @@ class SystemConfig(Base):
|
||||
result = await db.execute(select(cls).where(cls.key == key))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@db_update
|
||||
def delete_by_key(self, db: Session, key: str):
|
||||
"""在调用方持有的事务中暂存指定配置删除。"""
|
||||
systemconfig = self.get_by_key(db, key)
|
||||
if systemconfig:
|
||||
systemconfig.delete(db, systemconfig.id)
|
||||
db.delete(systemconfig)
|
||||
return True
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import async_db_query, db_query, db_update
|
||||
from app.db.decorators import async_db_query, db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
|
||||
|
||||
@@ -555,14 +555,13 @@ class TransferHistory(Base):
|
||||
)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def update_download_hash(cls, db: Session, historyid: Optional[int] = None, download_hash: Optional[str] = None):
|
||||
"""在调用方事务中暂存下载任务哈希更新。"""
|
||||
db.execute(
|
||||
update(cls).where(cls.id == historyid).values(download_hash=download_hash)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def replace_by_src(cls, db: Session, **kwargs) -> "TransferHistory":
|
||||
"""
|
||||
用同源存储的新记录原子替换旧整理历史。
|
||||
@@ -600,7 +599,6 @@ class TransferHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete_before(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy import Index, String, delete, select
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import db_query, db_update
|
||||
from app.db.decorators import db_query
|
||||
|
||||
|
||||
class TransferPending(Base):
|
||||
@@ -35,7 +35,6 @@ class TransferPending(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def register(cls, db: Session, storage: str, src_path: str,
|
||||
now_time: str) -> Optional["TransferPending"]:
|
||||
"""
|
||||
@@ -58,7 +57,6 @@ class TransferPending(Base):
|
||||
return pending
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def discard(cls, db: Session, storage: str, src_path: str) -> int:
|
||||
"""
|
||||
注销一个待整理文件登记,整理到达终态(成功或失败)时调用。
|
||||
@@ -93,7 +91,6 @@ class TransferPending(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def clear(cls, db: Session) -> int:
|
||||
"""
|
||||
清空全部待整理登记。
|
||||
|
||||
+9
-19
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, db_update, async_db_query, async_db_update
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class User(Base):
|
||||
@@ -60,56 +60,46 @@ class User(Base):
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
@db_update
|
||||
def delete_by_name(self, db: Session, name: str):
|
||||
user = self.get_by_name(db, name)
|
||||
if user:
|
||||
user.delete(db, user.id)
|
||||
db.delete(user)
|
||||
return True
|
||||
|
||||
@async_db_update
|
||||
async def async_delete_by_name(self, db: AsyncSession, name: str):
|
||||
user = await self.async_get_by_name(db, name)
|
||||
if user:
|
||||
await user.async_delete(db, user.id)
|
||||
await db.delete(user)
|
||||
return True
|
||||
|
||||
@db_update
|
||||
def delete_by_id(self, db: Session, user_id: int):
|
||||
user = self.get_by_id(db, user_id)
|
||||
if user:
|
||||
user.delete(db, user.id)
|
||||
db.delete(user)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_delete_by_id(cls, db: AsyncSession, user_id: int):
|
||||
"""异步按用户 ID 删除用户,供 UserOper 通过类方法调用。"""
|
||||
user = await cls.async_get_by_id(db, user_id)
|
||||
if user:
|
||||
await user.async_delete(db, user.id)
|
||||
await db.delete(user)
|
||||
return True
|
||||
|
||||
@db_update
|
||||
def update_otp_by_name(self, db: Session, name: str, otp: bool, secret: str):
|
||||
user = self.get_by_name(db, name)
|
||||
if user:
|
||||
user.update(db, {
|
||||
'is_otp': otp,
|
||||
'otp_secret': secret
|
||||
})
|
||||
user.is_otp = otp
|
||||
user.otp_secret = secret
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_update_otp_by_name(cls, db: AsyncSession, name: str, otp: bool, secret: str):
|
||||
"""异步按用户名更新 OTP 状态,供 UserOper 通过类方法调用。"""
|
||||
user = await cls.async_get_by_name(db, name)
|
||||
if user:
|
||||
await user.async_update(db, {
|
||||
'is_otp': otp,
|
||||
'otp_secret': secret
|
||||
})
|
||||
user.is_otp = otp
|
||||
user.otp_secret = secret
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -3,7 +3,7 @@ from sqlalchemy import String, UniqueConstraint, JSON, select
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import db_query, db_update
|
||||
from app.db.decorators import db_query
|
||||
|
||||
|
||||
class UserConfig(Base):
|
||||
@@ -30,9 +30,9 @@ class UserConfig(Base):
|
||||
select(cls).where(cls.username == username, cls.key == key)
|
||||
).scalars().first()
|
||||
|
||||
@db_update
|
||||
def delete_by_key(self, db: Session, username: str, key: str):
|
||||
"""在调用方持有的事务中暂存指定用户配置删除。"""
|
||||
userconfig = self.get_by_key(db=db, username=username, key=key)
|
||||
if userconfig:
|
||||
userconfig.delete(db=db, rid=userconfig.id)
|
||||
db.delete(userconfig)
|
||||
return True
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, async_db_query, async_db_update
|
||||
from app.db.decorators import db_query, async_db_query
|
||||
|
||||
|
||||
class Workflow(Base):
|
||||
@@ -135,8 +135,8 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_update_state(cls, db: AsyncSession, wid: int, state: str):
|
||||
"""在调用方持有的异步事务中暂存工作流状态。"""
|
||||
await db.execute(update(cls).where(cls.id == wid).values(state=state))
|
||||
return True
|
||||
|
||||
@@ -146,8 +146,8 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_start(cls, db: AsyncSession, wid: int):
|
||||
"""在调用方持有的异步事务中暂存运行中状态。"""
|
||||
await db.execute(update(cls).where(cls.id == wid).values(state='R'))
|
||||
return True
|
||||
|
||||
@@ -163,8 +163,8 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_fail(cls, db: AsyncSession, wid: int, result: str):
|
||||
"""在调用方持有的异步事务中暂存失败结果。"""
|
||||
await db.execute(update(cls).where(
|
||||
and_(cls.id == wid, cls.state != "P")
|
||||
).values(
|
||||
@@ -187,8 +187,8 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_success(cls, db: AsyncSession, wid: int, result: Optional[str] = None):
|
||||
"""在调用方持有的异步事务中暂存成功结果。"""
|
||||
await db.execute(update(cls).where(
|
||||
and_(cls.id == wid, cls.state != "P")
|
||||
).values(
|
||||
@@ -212,8 +212,8 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_reset(cls, db: AsyncSession, wid: int, reset_count: Optional[bool] = False):
|
||||
"""在调用方持有的异步事务中暂存执行状态重置。"""
|
||||
await db.execute(update(cls).where(cls.id == wid).values(
|
||||
state='W',
|
||||
result=None,
|
||||
@@ -243,9 +243,9 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@async_db_update
|
||||
async def async_update_current_action(cls, db: AsyncSession, wid: int, action_id: str, context: dict,
|
||||
execution_state: Optional[dict] = None):
|
||||
"""在调用方持有的异步事务中暂存动作进度。"""
|
||||
# 先获取当前current_action
|
||||
result = await db.execute(select(cls.current_action).where(cls.id == wid))
|
||||
current_action = result.scalar()
|
||||
|
||||
+58
-36
@@ -24,14 +24,16 @@ class AgentTaskOper(DbOper):
|
||||
新增 Agent 定时任务。
|
||||
"""
|
||||
now = self._now()
|
||||
task_id = AgentTask.add_task(
|
||||
self._db,
|
||||
**kwargs,
|
||||
enabled=True,
|
||||
last_status="waiting",
|
||||
run_count=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
task_id = self._execute_sync_write(
|
||||
lambda session: AgentTask.add_task(
|
||||
session,
|
||||
**kwargs,
|
||||
enabled=True,
|
||||
last_status="waiting",
|
||||
run_count=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
return self.get(task_id)
|
||||
|
||||
@@ -81,38 +83,50 @@ class AgentTaskOper(DbOper):
|
||||
if not normalized_payload:
|
||||
return False
|
||||
normalized_payload["updated_at"] = self._now()
|
||||
return AgentTask.update_task(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
payload=normalized_payload,
|
||||
user_id=user_id,
|
||||
return self._execute_sync_write(
|
||||
lambda session: AgentTask.update_task(
|
||||
session,
|
||||
task_id=task_id,
|
||||
payload=normalized_payload,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
def delete(self, task_id: int, user_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除非运行中的 Agent 定时任务及其运行历史。
|
||||
"""
|
||||
return AgentTaskRun.delete_task_and_runs(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
return self._execute_sync_write(
|
||||
lambda session: AgentTaskRun.delete_task_and_runs(
|
||||
session,
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
def begin_run(
|
||||
self,
|
||||
task_id: int,
|
||||
trigger_source: str = "scheduled",
|
||||
*,
|
||||
run_id: Optional[str] = None,
|
||||
started_at: Optional[str] = None,
|
||||
) -> Optional[AgentTaskRun]:
|
||||
"""
|
||||
原子创建一次运行并返回其任务快照。
|
||||
|
||||
可选运行 ID 和开始时间用于恢复/幂等验证;正常调度入口由本方法生成。
|
||||
"""
|
||||
run_id = uuid4().hex
|
||||
created_run_id = AgentTaskRun.begin_run(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
run_id=run_id,
|
||||
trigger_source=trigger_source,
|
||||
started_at=self._now(),
|
||||
resolved_run_id = run_id or uuid4().hex
|
||||
resolved_started_at = started_at or self._now()
|
||||
created_run_id = self._execute_sync_write(
|
||||
lambda session: AgentTaskRun.begin_run(
|
||||
session,
|
||||
task_id=task_id,
|
||||
run_id=resolved_run_id,
|
||||
trigger_source=trigger_source,
|
||||
started_at=resolved_started_at,
|
||||
)
|
||||
)
|
||||
return self.get_run(created_run_id) if created_run_id else None
|
||||
|
||||
@@ -124,11 +138,15 @@ class AgentTaskOper(DbOper):
|
||||
"""
|
||||
将遗留的运行中任务标记为中断且结果未知。
|
||||
"""
|
||||
return AgentTaskRun.interrupt_task(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
result=(result or "")[:20000],
|
||||
finished_at=self._now(),
|
||||
finished_at = self._now()
|
||||
normalized_result = (result or "")[:20000]
|
||||
return self._execute_sync_write(
|
||||
lambda session: AgentTaskRun.interrupt_task(
|
||||
session,
|
||||
task_id=task_id,
|
||||
result=normalized_result,
|
||||
finished_at=finished_at,
|
||||
)
|
||||
)
|
||||
|
||||
def get_run(self, run_id: str) -> Optional[AgentTaskRun]:
|
||||
@@ -157,13 +175,17 @@ class AgentTaskOper(DbOper):
|
||||
disable_date_task: bool = False,
|
||||
) -> bool:
|
||||
"""收口精确运行并更新仍匹配的任务投影。"""
|
||||
return AgentTaskRun.finish_run(
|
||||
self._db,
|
||||
run_id=run_id,
|
||||
success=success,
|
||||
result=(result or "")[:20000],
|
||||
finished_at=self._now(),
|
||||
disable_date_task=disable_date_task,
|
||||
finished_at = self._now()
|
||||
normalized_result = (result or "")[:20000]
|
||||
return self._execute_sync_write(
|
||||
lambda session: AgentTaskRun.finish_run(
|
||||
session,
|
||||
run_id=run_id,
|
||||
success=success,
|
||||
result=normalized_result,
|
||||
finished_at=finished_at,
|
||||
disable_date_task=disable_date_task,
|
||||
)
|
||||
)
|
||||
|
||||
def finish(
|
||||
|
||||
@@ -54,8 +54,10 @@ class DownloadFailureOper(DbOper):
|
||||
"""
|
||||
删除已过期较久的失败记录。
|
||||
"""
|
||||
return DownloadFailure.delete_expired(
|
||||
self._db,
|
||||
before_time=before_time,
|
||||
limit=limit,
|
||||
return self._execute_sync_write(
|
||||
lambda session: DownloadFailure.delete_expired(
|
||||
session,
|
||||
before_time=before_time,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -127,7 +127,9 @@ class DownloadHistoryOper(DbOper):
|
||||
按fullpath删除下载文件记录
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
DownloadFiles.delete_by_fullpath(self._db, fullpath)
|
||||
self._execute_sync_write(
|
||||
lambda session: DownloadFiles.delete_by_fullpath(session, fullpath)
|
||||
)
|
||||
|
||||
def stage_delete_file_by_fullpath(self, fullpath: str) -> None:
|
||||
"""暂存指定完整路径的下载文件记录删除。"""
|
||||
|
||||
@@ -61,19 +61,32 @@ class MediaServerOper(DbOper):
|
||||
"""
|
||||
清空媒体服务器数据
|
||||
"""
|
||||
MediaServerItem.empty(self._db, server)
|
||||
self._execute_sync_write(
|
||||
lambda session: MediaServerItem.empty(session, server)
|
||||
)
|
||||
|
||||
def delete_stale(self, server: str, sync_time: str) -> int:
|
||||
"""
|
||||
删除本轮同步未更新的旧数据
|
||||
"""
|
||||
return MediaServerItem.delete_stale(self._db, server, sync_time)
|
||||
return self._execute_sync_write(
|
||||
lambda session: MediaServerItem.delete_stale(
|
||||
session,
|
||||
server,
|
||||
sync_time,
|
||||
)
|
||||
)
|
||||
|
||||
def delete_excluded_servers(self, servers: list[str]) -> int:
|
||||
"""
|
||||
删除未启用或已移除媒体服务器的数据
|
||||
"""
|
||||
return MediaServerItem.delete_excluded_servers(self._db, servers)
|
||||
return self._execute_sync_write(
|
||||
lambda session: MediaServerItem.delete_excluded_servers(
|
||||
session,
|
||||
servers,
|
||||
)
|
||||
)
|
||||
|
||||
def exists(self, **kwargs) -> Optional[MediaServerItem]:
|
||||
"""
|
||||
|
||||
@@ -62,7 +62,8 @@ class MessageOper(DbOper):
|
||||
if k not in Message.__table__.columns.keys(): # noqa
|
||||
kwargs.pop(k)
|
||||
|
||||
return Message(**kwargs).create_and_to_dict(self._db)
|
||||
message = Message(**kwargs)
|
||||
return self._execute_sync_write(message.create_and_to_dict)
|
||||
|
||||
async def async_add(self,
|
||||
channel: Optional[NotificationChannel] = None,
|
||||
|
||||
+21
-3
@@ -24,13 +24,31 @@ class PassKeyOper(DbOper):
|
||||
def create(self, payload: dict[str, Any]) -> PassKey:
|
||||
"""创建 PassKey 凭证。"""
|
||||
passkey = PassKey(**payload)
|
||||
passkey.create(self._db)
|
||||
self._execute_sync_write(lambda session: self._stage_create(session, passkey))
|
||||
return passkey
|
||||
|
||||
@staticmethod
|
||||
def _stage_create(session: Any, passkey: PassKey) -> None:
|
||||
"""在调用方事务中暂存凭证并分配主键。"""
|
||||
session.add(passkey)
|
||||
session.flush()
|
||||
|
||||
def update_last_used(self, passkey: PassKey, sign_count: int) -> bool:
|
||||
"""更新凭证最后使用时间和签名计数。"""
|
||||
return bool(passkey.update_last_used(self._db, sign_count))
|
||||
return bool(self._execute_sync_write(
|
||||
lambda session: passkey.update_last_used(session, sign_count)
|
||||
))
|
||||
|
||||
def delete_by_id(self, passkey_id: int, user_id: int) -> bool:
|
||||
"""删除指定用户的凭证。"""
|
||||
return bool(PassKey.delete_by_id(self._db, passkey_id, user_id))
|
||||
return bool(self._execute_sync_write(
|
||||
lambda session: PassKey.delete_by_id(session, passkey_id, user_id)
|
||||
))
|
||||
|
||||
async def async_delete_by_id(self, passkey_id: int, user_id: int) -> bool:
|
||||
"""在独立异步事务中删除指定用户的凭证。"""
|
||||
return bool(await self._execute_async_write(
|
||||
lambda session: PassKey.async_delete_by_id(
|
||||
session, passkey_id, user_id
|
||||
)
|
||||
))
|
||||
|
||||
@@ -80,10 +80,14 @@ class PluginDataOper(DbOper):
|
||||
:param plugin_id: 插件id
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
PluginData.del_plugin_data_by_key(self._db, plugin_id, key)
|
||||
else:
|
||||
PluginData.del_plugin_data(self._db, plugin_id)
|
||||
def stage(session: Session) -> None:
|
||||
"""把兼容删除入口映射到调用方或组合根持有的事务。"""
|
||||
if key:
|
||||
PluginData.del_plugin_data_by_key(session, plugin_id, key)
|
||||
else:
|
||||
PluginData.del_plugin_data(session, plugin_id)
|
||||
|
||||
self._execute_sync_write(stage)
|
||||
|
||||
def stage_delete(self, plugin_id: str) -> None:
|
||||
"""暂存目标插件全部数据删除并 flush,不提交调用方事务。"""
|
||||
|
||||
+2
-2
@@ -116,8 +116,8 @@ class SiteOper(DbOper):
|
||||
Site.delete(self._db, sid)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""清空站点表,保留站点模型细节在数据库适配层。"""
|
||||
Site.reset(self._db)
|
||||
"""清空站点表;兼容入口的事务由组合根统一持有。"""
|
||||
self._execute_sync_write(Site.reset)
|
||||
|
||||
async def stage_reset(self) -> None:
|
||||
"""暂存清空站点表,由应用事务统一提交。"""
|
||||
|
||||
@@ -264,14 +264,18 @@ class TransferHistoryOper(DbOper):
|
||||
kwargs.update({
|
||||
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
})
|
||||
TransferHistory.replace_by_src(self._db, **kwargs)
|
||||
def stage(session: Session) -> Optional[TransferHistory]:
|
||||
"""在同一事务替换记录并返回兼容查询投影。"""
|
||||
TransferHistory.replace_by_src(session, **kwargs)
|
||||
return TransferHistory.get_by_src(
|
||||
session,
|
||||
kwargs.get("src"),
|
||||
kwargs["src_storage"],
|
||||
)
|
||||
|
||||
# 保持 add_force 的既有返回契约:返回可被调用方安全读取字段的查询结果,
|
||||
# 而非事务提交后可能已脱离会话的新建实例。
|
||||
return TransferHistory.get_by_src(
|
||||
self._db,
|
||||
kwargs.get("src"),
|
||||
kwargs["src_storage"],
|
||||
)
|
||||
return self._execute_sync_write(stage)
|
||||
|
||||
def stage_replace_by_src(self, **kwargs) -> TransferHistory:
|
||||
"""在调用方事务内按源路径替换整理历史并返回已分配 ID 的新记录。"""
|
||||
@@ -295,7 +299,13 @@ class TransferHistoryOper(DbOper):
|
||||
"""
|
||||
补充转移记录download_hash
|
||||
"""
|
||||
TransferHistory.update_download_hash(self._db, historyid, download_hash)
|
||||
self._execute_sync_write(
|
||||
lambda session: TransferHistory.update_download_hash(
|
||||
session,
|
||||
historyid,
|
||||
download_hash,
|
||||
)
|
||||
)
|
||||
|
||||
def list_by_date(self, date: str) -> List[TransferHistory]:
|
||||
"""
|
||||
|
||||
@@ -20,11 +20,14 @@ class TransferPendingOper(DbOper):
|
||||
:param src_path: 源文件路径
|
||||
:return: 登记记录
|
||||
"""
|
||||
return TransferPending.register(
|
||||
self._db,
|
||||
storage=storage,
|
||||
src_path=src_path,
|
||||
now_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
now_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
return self._execute_sync_write(
|
||||
lambda session: TransferPending.register(
|
||||
session,
|
||||
storage=storage,
|
||||
src_path=src_path,
|
||||
now_time=now_time,
|
||||
)
|
||||
)
|
||||
|
||||
def discard(self, storage: str, src_path: str) -> int:
|
||||
@@ -34,7 +37,13 @@ class TransferPendingOper(DbOper):
|
||||
:param src_path: 源文件路径
|
||||
:return: 删除的记录数
|
||||
"""
|
||||
return TransferPending.discard(self._db, storage=storage, src_path=src_path)
|
||||
return self._execute_sync_write(
|
||||
lambda session: TransferPending.discard(
|
||||
session,
|
||||
storage=storage,
|
||||
src_path=src_path,
|
||||
)
|
||||
)
|
||||
|
||||
def list_all(self, limit: Optional[int] = 5000) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
@@ -56,4 +65,4 @@ class TransferPendingOper(DbOper):
|
||||
清空全部待整理登记。
|
||||
:return: 删除的记录数
|
||||
"""
|
||||
return TransferPending.clear(self._db)
|
||||
return self._execute_sync_write(TransferPending.clear)
|
||||
|
||||
+34
-6
@@ -11,6 +11,8 @@ runtime 兼容映射指向 SDK 薄门面;canonical 数据访问模块仍只依
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.user import User
|
||||
|
||||
@@ -49,27 +51,53 @@ class UserOper(DbOper):
|
||||
|
||||
async def async_create(self, payload: dict) -> Optional[User]:
|
||||
"""异步创建用户。"""
|
||||
return await User(**payload).async_create(self._db)
|
||||
user = User(**payload)
|
||||
|
||||
async def stage(session: AsyncSession) -> User:
|
||||
"""在当前异步事务中暂存用户并分配主键。"""
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
return user
|
||||
|
||||
return await self._execute_async_write(stage)
|
||||
|
||||
async def async_update(self, user_id: int, payload: dict) -> Optional[User]:
|
||||
"""异步更新用户。"""
|
||||
user = await self.async_get_by_id(user_id)
|
||||
if user:
|
||||
await user.async_update(self._db, payload)
|
||||
async def stage(session: AsyncSession) -> User:
|
||||
"""在当前事务中更新用户字段,必要时重新附加游离对象。"""
|
||||
for key, value in payload.items():
|
||||
setattr(user, key, value)
|
||||
return await session.merge(user)
|
||||
|
||||
await self._execute_async_write(stage)
|
||||
return user
|
||||
|
||||
async def async_delete(self, user_id: int) -> None:
|
||||
async def async_delete(self, user_id: int) -> bool:
|
||||
"""异步删除用户。"""
|
||||
await User.async_delete_by_id(self._db, user_id)
|
||||
return bool(await self._execute_async_write(
|
||||
lambda session: User.async_delete_by_id(session, user_id)
|
||||
))
|
||||
|
||||
async def async_delete_by_name(self, name: str) -> bool:
|
||||
"""在独立异步事务中按用户名删除用户。"""
|
||||
return bool(await self._execute_async_write(
|
||||
lambda session: User().async_delete_by_name(session, name)
|
||||
))
|
||||
|
||||
async def async_update_otp_by_name(
|
||||
self,
|
||||
name: str,
|
||||
otp: bool,
|
||||
secret: str,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""异步更新用户 OTP 状态。"""
|
||||
await User.async_update_otp_by_name(self._db, name, otp, secret)
|
||||
return bool(await self._execute_async_write(
|
||||
lambda session: User.async_update_otp_by_name(
|
||||
session, name, otp, secret
|
||||
)
|
||||
))
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Optional[User]:
|
||||
"""
|
||||
|
||||
+57
-1
@@ -1,9 +1,65 @@
|
||||
"""SQLAlchemy 请求级事务适配器。"""
|
||||
"""SQLAlchemy 请求级事务适配器与旧 Oper 事务执行端口。"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class SyncTransactionRunner(Protocol):
|
||||
"""为无显式 Session 的兼容写入口提供独占同步事务。"""
|
||||
|
||||
def __call__(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在一个独占会话中执行并提交操作。"""
|
||||
...
|
||||
|
||||
|
||||
class AsyncTransactionRunner(Protocol):
|
||||
"""为无显式 Session 的兼容写入口提供独占异步事务。"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> Awaitable[T]:
|
||||
"""在一个独占异步会话中执行并提交操作。"""
|
||||
...
|
||||
|
||||
|
||||
_sync_transaction_runner: SyncTransactionRunner | None = None
|
||||
_async_transaction_runner: AsyncTransactionRunner | None = None
|
||||
|
||||
|
||||
def configure_transaction_runners(
|
||||
*,
|
||||
sync: SyncTransactionRunner,
|
||||
async_: AsyncTransactionRunner,
|
||||
) -> None:
|
||||
"""由组合根登记旧 Oper 兼容入口使用的显式事务执行器。"""
|
||||
global _sync_transaction_runner, _async_transaction_runner
|
||||
_sync_transaction_runner = sync
|
||||
_async_transaction_runner = async_
|
||||
|
||||
|
||||
def run_sync_transaction(operation: Callable[[Session], T]) -> T:
|
||||
"""委托组合根在独占同步事务中执行兼容写操作。"""
|
||||
if _sync_transaction_runner is None:
|
||||
raise RuntimeError("同步事务执行器尚未配置")
|
||||
return _sync_transaction_runner(operation)
|
||||
|
||||
|
||||
async def run_async_transaction(
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""委托组合根在独占异步事务中执行兼容写操作。"""
|
||||
if _async_transaction_runner is None:
|
||||
raise RuntimeError("异步事务执行器尚未配置")
|
||||
return await _async_transaction_runner(operation)
|
||||
|
||||
|
||||
class SqlAlchemyUnitOfWork:
|
||||
"""把同步 Session 的提交与回滚能力适配为应用层事务端口。"""
|
||||
|
||||
|
||||
+73
-12
@@ -81,22 +81,27 @@ class SyncSessionProvider(Protocol):
|
||||
...
|
||||
|
||||
|
||||
class CompatibilityApiData(Protocol):
|
||||
"""未迁移 API 领域继续使用的结构化兼容 Facade。"""
|
||||
class RepositoryFactory(Protocol):
|
||||
"""由请求 Session 构造某一明确领域仓储的通用工厂。"""
|
||||
|
||||
sync_session: SyncSessionProvider
|
||||
async_session: AsyncSessionProvider
|
||||
|
||||
def repository(self, name: str, session: object) -> object:
|
||||
"""按旧能力名构造请求级仓储。"""
|
||||
def __call__(self, session: object) -> object:
|
||||
"""绑定请求会话并返回领域仓储。"""
|
||||
...
|
||||
|
||||
def standalone_repository(self, name: str) -> object:
|
||||
"""按旧能力名构造独立仓储。"""
|
||||
|
||||
class StandaloneRepositoryFactory(Protocol):
|
||||
"""构造自持有兼容事务边界的领域仓储。"""
|
||||
|
||||
def __call__(self) -> object:
|
||||
"""返回无需请求 Session 的领域仓储。"""
|
||||
...
|
||||
|
||||
def transaction(self, name: str, session: object) -> object:
|
||||
"""按旧能力名构造事务端口。"""
|
||||
|
||||
class SyncUnitOfWorkFactory(Protocol):
|
||||
"""由同步请求 Session 构造事务端口的工厂。"""
|
||||
|
||||
def __call__(self, session: object) -> object:
|
||||
"""绑定请求会话并返回同步事务端口。"""
|
||||
...
|
||||
|
||||
|
||||
@@ -109,6 +114,57 @@ class AgentChatRuntime:
|
||||
transaction: AsyncUnitOfWorkFactory
|
||||
|
||||
|
||||
@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: StandaloneRepositoryFactory
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SubscriptionRuntime:
|
||||
"""订阅 API 可见的请求级写事务运行时。"""
|
||||
@@ -125,6 +181,11 @@ class HostRuntime:
|
||||
"""宿主组合根构建且在一个 FastAPI lifespan 内共享的运行时对象。"""
|
||||
|
||||
agent_chat: AgentChatRuntime
|
||||
persistence: PersistenceRuntime
|
||||
authentication: AuthenticationRuntime
|
||||
messaging: MessagingRuntime
|
||||
history: HistoryRuntime
|
||||
site: SiteRuntime
|
||||
subscription: SubscriptionRuntime
|
||||
workflow: WorkflowRuntime
|
||||
configuration: RuntimeConfiguration
|
||||
compatibility_api_data: CompatibilityApiData
|
||||
|
||||
@@ -23,6 +23,7 @@ from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.events import EventHandlerBinding, EventManager
|
||||
from app.runtime.observability import record_metric
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.thread import ThreadHelper
|
||||
from app.adapters.network.doh import DohHelper
|
||||
@@ -80,7 +81,11 @@ from app.db.session import (
|
||||
get_async_db,
|
||||
get_db,
|
||||
)
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.db.uow import (
|
||||
SqlAlchemyAsyncUnitOfWork,
|
||||
SqlAlchemyUnitOfWork,
|
||||
configure_transaction_runners,
|
||||
)
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
@@ -114,7 +119,18 @@ from app.startup.subscription import (
|
||||
from app.startup.chain_events import TransactionalChainDurableEventWriter
|
||||
from app.startup.download_failure import TransactionalDownloadFailureRepository
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.context import AgentChatRuntime, HostRuntime, SubscriptionRuntime
|
||||
from app.startup.transaction import TransactionalWriteRunner
|
||||
from app.startup.context import (
|
||||
AgentChatRuntime,
|
||||
AuthenticationRuntime,
|
||||
HistoryRuntime,
|
||||
HostRuntime,
|
||||
MessagingRuntime,
|
||||
PersistenceRuntime,
|
||||
SiteRuntime,
|
||||
SubscriptionRuntime,
|
||||
WorkflowRuntime,
|
||||
)
|
||||
from app.adapters.web.security.access import set_superuser_token_payload_provider
|
||||
from app.application.security.auth import build_superuser_token_payload
|
||||
from app.application.image import configure_wallpaper_providers
|
||||
@@ -320,6 +336,10 @@ def _build_outbox_dispatcher() -> OutboxDispatcher:
|
||||
),
|
||||
},
|
||||
close=session.close,
|
||||
failure_observer=lambda dead: record_metric(
|
||||
"scheduler.job.dead_letter" if dead else "scheduler.job.retry",
|
||||
owner="outbox",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -535,6 +555,15 @@ async def init_modules() -> HostRuntime:
|
||||
"""
|
||||
启动模块并返回本次 lifespan 唯一的类型化 HostRuntime。
|
||||
"""
|
||||
# 兼容 Oper 的无 Session 写入口仍由组合根持有事务,避免模型恢复自动提交。
|
||||
transaction_runner = TransactionalWriteRunner(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
configure_transaction_runners(
|
||||
sync=transaction_runner.sync,
|
||||
async_=transaction_runner.async_,
|
||||
)
|
||||
# 数据访问能力统一在启动组合根注入,Runtime 和 Adapter 不再直接依赖 Oper。
|
||||
api_data = ApiDataPorts(
|
||||
sync_session=get_db,
|
||||
@@ -572,6 +601,25 @@ async def init_modules() -> HostRuntime:
|
||||
repository=AgentChatOper,
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
),
|
||||
persistence=PersistenceRuntime(
|
||||
sync_session=get_db,
|
||||
async_session=get_async_db,
|
||||
sync_transaction=SqlAlchemyUnitOfWork,
|
||||
async_transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
),
|
||||
authentication=AuthenticationRuntime(
|
||||
user_repository=UserOper,
|
||||
standalone_user=UserOper,
|
||||
system_config=SystemConfigOper,
|
||||
passkey=PassKeyOper,
|
||||
),
|
||||
messaging=MessagingRuntime(repository=MessageOper),
|
||||
history=HistoryRuntime(
|
||||
download_repository=DownloadHistoryOper,
|
||||
transfer_repository=TransferHistoryOper,
|
||||
media_server_repository=MediaServerOper,
|
||||
),
|
||||
site=SiteRuntime(repository=SiteOper),
|
||||
subscription=SubscriptionRuntime(
|
||||
async_session=get_async_db,
|
||||
repository=SubscribeOper,
|
||||
@@ -579,11 +627,15 @@ async def init_modules() -> HostRuntime:
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
outbox=SqlAlchemyAsyncOutboxStager,
|
||||
),
|
||||
workflow=WorkflowRuntime(
|
||||
repository=WorkflowOper,
|
||||
system_config=SystemConfigOper,
|
||||
),
|
||||
configuration=runtime_configuration,
|
||||
compatibility_api_data=api_data,
|
||||
)
|
||||
configure_runtime_configuration(host_runtime.configuration)
|
||||
configure_api_data_runtime(host_runtime.compatibility_api_data)
|
||||
# 旧 app.api.data 导入只保留 ABI 转发,正式 API 依赖全部读取 HostRuntime。
|
||||
configure_api_data_runtime(api_data)
|
||||
configure_runtime_data_providers()
|
||||
workflow_execution = TransactionalWorkflowExecutionService(SessionFactory)
|
||||
configure_workflow_legacy_writer(workflow_execution)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""旧 Oper 写入口的 SQLAlchemy 事务执行适配器。"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalWriteRunner:
|
||||
"""为兼容写入口创建独占会话,并用 UoW 明确提交或回滚。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def sync(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在独占同步 Session 中执行操作并统一收口事务。"""
|
||||
session = self._sync_session()
|
||||
# 兼容 Oper 历史上会返回刚写入的 ORM 对象;提交后若过期,Session 关闭后连主键
|
||||
# 都无法读取。独占短会话没有后续一致性读取需求,因此保留已 flush 的字段快照。
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(session)
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
async def async_(
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独占 AsyncSession 中执行操作并统一收口事务。"""
|
||||
async with self._async_session() as session:
|
||||
# 与同步兼容入口保持相同的返回对象生命周期。
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(session)
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
Reference in New Issue
Block a user