mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
refactor: close transactional boundary debt batch
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
@@ -43,13 +44,13 @@ class AuthTicketStore(metaclass=Singleton):
|
||||
ticket = secrets.token_urlsafe(32)
|
||||
now = time.time()
|
||||
with self._lock:
|
||||
self._cleanup(now)
|
||||
self._tickets[ticket] = {
|
||||
"user_id": int(user_id),
|
||||
"provider_id": provider_id,
|
||||
"metadata": metadata or {},
|
||||
"metadata": copy.deepcopy(metadata) if metadata is not None else {},
|
||||
"created_at": now,
|
||||
}
|
||||
self._cleanup(now)
|
||||
return ticket
|
||||
|
||||
def consume(self, ticket: str) -> Optional[dict[str, Any]]:
|
||||
@@ -69,7 +70,7 @@ class AuthTicketStore(metaclass=Singleton):
|
||||
return None
|
||||
if now - float(data.get("created_at") or 0) > self._ttl_seconds:
|
||||
return None
|
||||
return data
|
||||
return copy.deepcopy(data)
|
||||
|
||||
def _cleanup(self, now: Optional[float] = None) -> None:
|
||||
"""
|
||||
@@ -77,7 +78,7 @@ class AuthTicketStore(metaclass=Singleton):
|
||||
|
||||
:param now: 当前时间戳,未传入时自动读取
|
||||
"""
|
||||
current = now or time.time()
|
||||
current = time.time() if now is None else now
|
||||
expired = [
|
||||
key
|
||||
for key, value in self._tickets.items()
|
||||
|
||||
@@ -5,7 +5,6 @@ import base64
|
||||
import binascii
|
||||
import json
|
||||
import secrets
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Literal, Optional, Protocol, Tuple
|
||||
from urllib.parse import urlparse
|
||||
@@ -28,9 +27,7 @@ from webauthn.helpers.structs import (
|
||||
UserVerificationRequirement,
|
||||
)
|
||||
|
||||
from app.adapters.cache.redis import RedisHelper
|
||||
from app.application.configuration import get_api_runtime_config_snapshot
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.runtime.log import logger
|
||||
|
||||
PASSKEY_CHALLENGE_TTL_SECONDS = 5 * 60
|
||||
@@ -46,15 +43,27 @@ class PasskeyChallenge:
|
||||
user_id: Optional[int]
|
||||
|
||||
|
||||
class PasskeyChallengeCache(Protocol):
|
||||
"""PassKey 一次性 challenge 使用的严格原子缓存端口。"""
|
||||
|
||||
def store(self, key: str, value: Any) -> None:
|
||||
"""持久化 challenge,失败时抛出后端异常。"""
|
||||
|
||||
def consume(self, key: str) -> Any:
|
||||
"""原子领取 challenge,不存在时返回 None。"""
|
||||
|
||||
|
||||
class PasskeyChallengeStore:
|
||||
"""使用当前缓存后端签发并原子消费短时 Passkey challenge。"""
|
||||
|
||||
_cache = TTLCache(
|
||||
region="passkey_challenge",
|
||||
maxsize=4096,
|
||||
ttl=PASSKEY_CHALLENGE_TTL_SECONDS,
|
||||
)
|
||||
_memory_consume_lock = threading.Lock()
|
||||
_cache: Optional[PasskeyChallengeCache] = None
|
||||
|
||||
@classmethod
|
||||
def _get_cache(cls) -> PasskeyChallengeCache:
|
||||
"""返回已装配缓存,缺失时拒绝签发认证状态。"""
|
||||
if cls._cache is None:
|
||||
raise RuntimeError("PassKey challenge 缓存尚未配置")
|
||||
return cls._cache
|
||||
|
||||
@classmethod
|
||||
def issue(
|
||||
@@ -66,7 +75,7 @@ class PasskeyChallengeStore:
|
||||
) -> str:
|
||||
"""保存 challenge 并返回不携带认证事实的随机事务 token。"""
|
||||
transaction_token = secrets.token_urlsafe(32)
|
||||
cls._cache.set(
|
||||
cls._get_cache().store(
|
||||
transaction_token,
|
||||
PasskeyChallenge(
|
||||
challenge=challenge,
|
||||
@@ -87,17 +96,7 @@ class PasskeyChallengeStore:
|
||||
if not transaction_token:
|
||||
return None
|
||||
|
||||
if cls._cache.is_redis():
|
||||
challenge = RedisHelper().pop(
|
||||
transaction_token,
|
||||
region="passkey_challenge",
|
||||
)
|
||||
else:
|
||||
with cls._memory_consume_lock:
|
||||
try:
|
||||
challenge = cls._cache.pop(transaction_token)
|
||||
except KeyError:
|
||||
challenge = None
|
||||
challenge = cls._get_cache().consume(transaction_token)
|
||||
|
||||
if not isinstance(challenge, PasskeyChallenge):
|
||||
return None
|
||||
@@ -106,6 +105,11 @@ class PasskeyChallengeStore:
|
||||
return challenge
|
||||
|
||||
|
||||
def configure_passkey_challenge_cache(cache: PasskeyChallengeCache) -> None:
|
||||
"""由启动组合根注入 PassKey challenge 的原子缓存。"""
|
||||
PasskeyChallengeStore._cache = cache
|
||||
|
||||
|
||||
class PassKeyRegistrationVerificationError(Exception):
|
||||
"""Passkey 注册响应未通过 WebAuthn 安全校验。"""
|
||||
|
||||
@@ -465,8 +469,13 @@ class PasskeyRepository(Protocol):
|
||||
def create(self, payload: dict[str, Any]) -> Any:
|
||||
"""创建凭证。"""
|
||||
|
||||
def update_last_used(self, passkey: Any, sign_count: int) -> bool:
|
||||
"""更新凭证使用计数。"""
|
||||
def compare_and_update_sign_count(
|
||||
self,
|
||||
passkey_id: int,
|
||||
expected_sign_count: int,
|
||||
sign_count: int,
|
||||
) -> bool:
|
||||
"""仅在签名计数未被并发修改时记录本次认证。"""
|
||||
|
||||
def delete_by_id(self, passkey_id: int, user_id: int) -> bool:
|
||||
"""删除用户凭证。"""
|
||||
@@ -495,9 +504,18 @@ class PasskeyService:
|
||||
"""创建凭证。"""
|
||||
return self._repository.create(payload)
|
||||
|
||||
def update_last_used(self, passkey: Any, sign_count: int) -> bool:
|
||||
"""更新凭证使用计数。"""
|
||||
return self._repository.update_last_used(passkey, sign_count)
|
||||
def compare_and_update_sign_count(
|
||||
self,
|
||||
passkey_id: int,
|
||||
expected_sign_count: int,
|
||||
sign_count: int,
|
||||
) -> bool:
|
||||
"""以验证时观察到的旧计数提交本次认证。"""
|
||||
return self._repository.compare_and_update_sign_count(
|
||||
passkey_id=passkey_id,
|
||||
expected_sign_count=expected_sign_count,
|
||||
sign_count=sign_count,
|
||||
)
|
||||
|
||||
def delete_by_id(self, passkey_id: int, user_id: int) -> bool:
|
||||
"""删除用户凭证。"""
|
||||
|
||||
@@ -7,11 +7,17 @@
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Protocol, TypeAlias, TypeVar, cast
|
||||
from typing import Any, Optional, Protocol, TypeAlias, TypeVar, Union, cast
|
||||
|
||||
FrozenJson: TypeAlias = (
|
||||
str | int | float | bool | None | tuple["FrozenJson", ...] | Mapping[str, "FrozenJson"]
|
||||
)
|
||||
FrozenJson: TypeAlias = Union[
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
None,
|
||||
tuple["FrozenJson", ...],
|
||||
Mapping[str, "FrozenJson"],
|
||||
]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -24,7 +30,9 @@ def _freeze_json(value: Any) -> FrozenJson:
|
||||
return cast(FrozenJson, value)
|
||||
|
||||
|
||||
def _freeze_mapping(value: Mapping[str, Any] | None) -> Mapping[str, FrozenJson]:
|
||||
def _freeze_mapping(
|
||||
value: Optional[Mapping[str, Any]],
|
||||
) -> Mapping[str, FrozenJson]:
|
||||
"""把可空 JSON 对象复制为只读映射。"""
|
||||
frozen = _freeze_json(value or {})
|
||||
return cast(Mapping[str, FrozenJson], frozen)
|
||||
@@ -36,10 +44,10 @@ class UserSnapshot:
|
||||
|
||||
id: int
|
||||
name: str
|
||||
email: str | None
|
||||
email: Optional[str]
|
||||
is_active: bool
|
||||
is_superuser: bool
|
||||
avatar: str | None
|
||||
avatar: Optional[str]
|
||||
is_otp: bool
|
||||
permissions: Mapping[str, FrozenJson]
|
||||
settings: Mapping[str, FrozenJson]
|
||||
@@ -50,13 +58,13 @@ class UserSnapshot:
|
||||
*,
|
||||
user_id: int,
|
||||
name: str,
|
||||
email: str | None,
|
||||
is_active: bool | None,
|
||||
is_superuser: bool | None,
|
||||
avatar: str | None,
|
||||
is_otp: bool | None,
|
||||
permissions: Mapping[str, Any] | None,
|
||||
settings: Mapping[str, Any] | None,
|
||||
email: Optional[str],
|
||||
is_active: Optional[bool],
|
||||
is_superuser: Optional[bool],
|
||||
avatar: Optional[str],
|
||||
is_otp: Optional[bool],
|
||||
permissions: Optional[Mapping[str, Any]],
|
||||
settings: Optional[Mapping[str, Any]],
|
||||
) -> "UserSnapshot":
|
||||
"""复制持久化字段并构造不可变的公开用户快照。"""
|
||||
return cls(
|
||||
@@ -77,8 +85,8 @@ class UserAuthSnapshot:
|
||||
"""仅供认证链使用的只读用户凭据快照。"""
|
||||
|
||||
user: UserSnapshot
|
||||
hashed_password: str | None
|
||||
otp_secret: str | None
|
||||
hashed_password: Optional[str]
|
||||
otp_secret: Optional[str]
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
@@ -101,7 +109,7 @@ class UserAuthSnapshot:
|
||||
return self.user.is_superuser
|
||||
|
||||
@property
|
||||
def avatar(self) -> str | None:
|
||||
def avatar(self) -> Optional[str]:
|
||||
"""返回用户头像。"""
|
||||
return self.user.avatar
|
||||
|
||||
@@ -126,13 +134,21 @@ class AuxiliaryUserCreate:
|
||||
is_superuser: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UserUpdateResult:
|
||||
"""用户更新事务产出的新快照与原用户名。"""
|
||||
|
||||
user: UserSnapshot
|
||||
previous_name: str
|
||||
|
||||
|
||||
class ChainUserRepository(Protocol):
|
||||
"""用户 Chain 和 Agent 共享的类型化查询与创建端口。"""
|
||||
|
||||
def get_auth_by_name(self, name: str) -> UserAuthSnapshot | None:
|
||||
def get_auth_by_name(self, name: str) -> Optional[UserAuthSnapshot]:
|
||||
"""按用户名读取认证快照。"""
|
||||
|
||||
async def async_get_by_name(self, name: str) -> UserSnapshot | None:
|
||||
async def async_get_by_name(self, name: str) -> Optional[UserSnapshot]:
|
||||
"""异步按用户名读取公开用户快照。"""
|
||||
|
||||
def create_auxiliary(self, command: AuxiliaryUserCreate) -> UserAuthSnapshot:
|
||||
@@ -141,16 +157,19 @@ class ChainUserRepository(Protocol):
|
||||
def get_notification_settings(
|
||||
self,
|
||||
name: str,
|
||||
) -> Mapping[str, FrozenJson] | None:
|
||||
) -> Optional[Mapping[str, FrozenJson]]:
|
||||
"""读取通知路由设置;用户不存在时返回空值。"""
|
||||
|
||||
async def async_get_notification_settings(
|
||||
self,
|
||||
name: str,
|
||||
) -> Mapping[str, FrozenJson] | None:
|
||||
) -> Optional[Mapping[str, FrozenJson]]:
|
||||
"""异步读取通知路由设置;用户不存在时返回空值。"""
|
||||
|
||||
def find_name_by_bindings(self, bindings: Mapping[str, object]) -> str | None:
|
||||
def find_name_by_bindings(
|
||||
self,
|
||||
bindings: Mapping[str, object],
|
||||
) -> Optional[str]:
|
||||
"""解析唯一启用用户的渠道绑定,歧义时拒绝归属。"""
|
||||
|
||||
|
||||
@@ -160,24 +179,27 @@ class UserRepository(Protocol):
|
||||
async def async_list(self) -> list[UserSnapshot]:
|
||||
"""返回全部用户。"""
|
||||
|
||||
async def async_get_by_name(self, name: str) -> UserSnapshot | None:
|
||||
async def async_get_by_name(self, name: str) -> Optional[UserSnapshot]:
|
||||
"""按用户名返回用户。"""
|
||||
|
||||
async def async_get_by_id(self, user_id: int) -> UserSnapshot | None:
|
||||
async def async_get_by_id(self, user_id: int) -> Optional[UserSnapshot]:
|
||||
"""按用户 ID 返回用户。"""
|
||||
|
||||
async def async_create(self, payload: dict[str, Any]) -> UserSnapshot | None:
|
||||
async def async_create(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
) -> Optional[UserSnapshot]:
|
||||
"""创建用户并返回持久化对象。"""
|
||||
|
||||
async def async_update(
|
||||
self,
|
||||
user_id: int,
|
||||
payload: dict[str, Any],
|
||||
) -> UserSnapshot | None:
|
||||
"""更新用户并返回原用户对象。"""
|
||||
) -> Optional[UserUpdateResult]:
|
||||
"""更新用户并返回提交后快照发布所需的变更结果。"""
|
||||
|
||||
async def async_delete(self, user_id: int) -> None:
|
||||
"""删除用户。"""
|
||||
async def async_delete(self, user_id: int) -> Optional[str]:
|
||||
"""删除用户并返回被删除用户名。"""
|
||||
|
||||
async def async_update_otp_by_name(self, name: str, otp: bool, secret: str) -> None:
|
||||
"""更新用户 OTP 状态。"""
|
||||
@@ -193,31 +215,51 @@ class AsyncUnitOfWork(Protocol):
|
||||
"""回滚失败的用户写入。"""
|
||||
|
||||
|
||||
class UserConfigurationPublisher(Protocol):
|
||||
"""用户聚合提交后同步进程级配置快照的应用端口。"""
|
||||
|
||||
async def rename(self, previous_name: str, current_name: str) -> None:
|
||||
"""数据库改名提交后迁移对应用户名配置快照。"""
|
||||
|
||||
async def delete(self, username: str) -> None:
|
||||
"""数据库删除提交后移除对应用户名配置快照。"""
|
||||
|
||||
|
||||
class UserNameConflictError(Exception):
|
||||
"""用户名在数据库唯一约束下发生冲突。"""
|
||||
|
||||
|
||||
class LastActiveSuperuserError(Exception):
|
||||
"""用户变更会导致系统不再存在启用的超级管理员。"""
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户管理应用服务。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repository: UserRepository,
|
||||
unit_of_work: AsyncUnitOfWork | None = None,
|
||||
unit_of_work: AsyncUnitOfWork,
|
||||
configuration: UserConfigurationPublisher,
|
||||
) -> None:
|
||||
"""创建用户服务;旧独立仓储可暂不提供请求级 UoW。"""
|
||||
"""创建用户服务并注入事务边界与提交后配置发布端口。"""
|
||||
self._repository = repository
|
||||
self._unit_of_work = unit_of_work
|
||||
self._configuration = configuration
|
||||
|
||||
async def list(self) -> list[UserSnapshot]:
|
||||
"""返回用户列表。"""
|
||||
return await self._repository.async_list()
|
||||
|
||||
async def get_by_name(self, name: str) -> UserSnapshot | None:
|
||||
async def get_by_name(self, name: str) -> Optional[UserSnapshot]:
|
||||
"""按用户名查询用户。"""
|
||||
return await self._repository.async_get_by_name(name)
|
||||
|
||||
async def get_by_id(self, user_id: int) -> UserSnapshot | None:
|
||||
async def get_by_id(self, user_id: int) -> Optional[UserSnapshot]:
|
||||
"""按用户 ID 查询用户。"""
|
||||
return await self._repository.async_get_by_id(user_id)
|
||||
|
||||
async def create(self, payload: dict[str, Any]) -> UserSnapshot | None:
|
||||
async def create(self, payload: dict[str, Any]) -> Optional[UserSnapshot]:
|
||||
"""创建用户。"""
|
||||
return await self._write(lambda: self._repository.async_create(payload))
|
||||
|
||||
@@ -225,44 +267,45 @@ class UserService:
|
||||
self,
|
||||
user_id: int,
|
||||
payload: dict[str, Any],
|
||||
) -> UserSnapshot | None:
|
||||
) -> Optional[UserSnapshot]:
|
||||
"""更新用户。"""
|
||||
return await self._write(
|
||||
lambda: self._repository.async_update(user_id, payload)
|
||||
)
|
||||
result = await self._write(lambda: self._repository.async_update(user_id, payload))
|
||||
if result is None:
|
||||
return None
|
||||
if result.previous_name != result.user.name:
|
||||
await self._configuration.rename(result.previous_name, result.user.name)
|
||||
return result.user
|
||||
|
||||
async def delete(self, user_id: int) -> None:
|
||||
"""删除用户。"""
|
||||
await self._write(lambda: self._repository.async_delete(user_id))
|
||||
username = await self._write(lambda: self._repository.async_delete(user_id))
|
||||
if username is not None:
|
||||
await self._configuration.delete(username)
|
||||
|
||||
async def update_otp(self, name: str, otp: bool, secret: str) -> None:
|
||||
"""更新用户 OTP 状态。"""
|
||||
await self._write(
|
||||
lambda: 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[T]]) -> T:
|
||||
"""执行用户写入,并在正式请求路径统一提交或回滚。"""
|
||||
try:
|
||||
result = await operation()
|
||||
if self._unit_of_work is not None:
|
||||
await self._unit_of_work.commit()
|
||||
await self._unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
if self._unit_of_work is not None:
|
||||
await self._unit_of_work.rollback()
|
||||
await self._unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
|
||||
_configured_user_id_lookup: Callable[[int], UserSnapshot | None] | None = None
|
||||
_configured_user_name_lookup: Callable[[str], UserSnapshot | None] | None = None
|
||||
_configured_user_channel_lookup: Callable[..., str | None] | None = None
|
||||
_configured_user_id_lookup: Optional[Callable[[int], Optional[UserSnapshot]]] = None
|
||||
_configured_user_name_lookup: Optional[Callable[[str], Optional[UserSnapshot]]] = None
|
||||
_configured_user_channel_lookup: Optional[Callable[..., Optional[str]]] = None
|
||||
|
||||
|
||||
def configure_user_lookups(
|
||||
by_id: Callable[[int], UserSnapshot | None],
|
||||
by_name: Callable[[str], UserSnapshot | None],
|
||||
by_channel: Callable[..., str | None],
|
||||
by_id: Callable[[int], Optional[UserSnapshot]],
|
||||
by_name: Callable[[str], Optional[UserSnapshot]],
|
||||
by_channel: Callable[..., Optional[str]],
|
||||
) -> None:
|
||||
"""由启动组合根登记 ID、用户名和渠道身份查询能力。"""
|
||||
global _configured_user_id_lookup, _configured_user_name_lookup
|
||||
@@ -272,21 +315,21 @@ def configure_user_lookups(
|
||||
_configured_user_channel_lookup = by_channel
|
||||
|
||||
|
||||
def get_configured_user_id_lookup() -> Callable[[int], UserSnapshot | None]:
|
||||
def get_configured_user_id_lookup() -> Callable[[int], Optional[UserSnapshot]]:
|
||||
"""返回启动阶段登记的按 ID 用户查询函数。"""
|
||||
if _configured_user_id_lookup is None:
|
||||
raise RuntimeError("按 ID 的用户查询能力尚未配置")
|
||||
return _configured_user_id_lookup
|
||||
|
||||
|
||||
def get_configured_user_name_lookup() -> Callable[[str], UserSnapshot | None]:
|
||||
def get_configured_user_name_lookup() -> Callable[[str], Optional[UserSnapshot]]:
|
||||
"""返回启动阶段登记的按用户名查询函数。"""
|
||||
if _configured_user_name_lookup is None:
|
||||
raise RuntimeError("按用户名的用户查询能力尚未配置")
|
||||
return _configured_user_name_lookup
|
||||
|
||||
|
||||
def get_configured_user_channel_lookup() -> Callable[..., str | None]:
|
||||
def get_configured_user_channel_lookup() -> Callable[..., Optional[str]]:
|
||||
"""返回启动阶段登记的渠道身份到用户名查询函数。"""
|
||||
if _configured_user_channel_lookup is None:
|
||||
raise RuntimeError("渠道用户查询能力尚未配置")
|
||||
|
||||
@@ -3,20 +3,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import Any, Protocol
|
||||
from typing import Optional, Protocol, Union
|
||||
|
||||
from app.application.database import AsyncDatabaseExecutor
|
||||
from app.schemas.common import JsonData
|
||||
from app.schemas.types import UserConfigKey
|
||||
|
||||
|
||||
class UserConfigurationRepository(Protocol):
|
||||
"""用户配置数据端口。"""
|
||||
|
||||
def get(self, username: str, key: str) -> Any:
|
||||
def get(
|
||||
self,
|
||||
username: str,
|
||||
key: Union[str, UserConfigKey],
|
||||
) -> JsonData:
|
||||
"""读取用户配置。"""
|
||||
|
||||
def set(self, username: str, key: str, value: Any) -> Any:
|
||||
def set(
|
||||
self,
|
||||
username: str,
|
||||
key: Union[str, UserConfigKey],
|
||||
value: JsonData,
|
||||
) -> None:
|
||||
"""写入用户配置。"""
|
||||
|
||||
def publish_rename(self, previous_name: str, current_name: str) -> None:
|
||||
"""在用户改名提交后迁移进程级配置快照。"""
|
||||
|
||||
def publish_delete(self, username: str) -> None:
|
||||
"""在用户删除提交后移除进程级配置快照。"""
|
||||
|
||||
|
||||
class UserConfigurationService:
|
||||
"""编排用户个性化配置读写。"""
|
||||
@@ -25,30 +42,54 @@ class UserConfigurationService:
|
||||
self,
|
||||
repository: UserConfigurationRepository,
|
||||
*,
|
||||
async_executor: AsyncDatabaseExecutor | None = None,
|
||||
async_executor: Optional[AsyncDatabaseExecutor] = None,
|
||||
) -> None:
|
||||
"""注入用户配置数据端口及可选的异步事务执行能力。"""
|
||||
self._repository = repository
|
||||
self._async_executor = async_executor
|
||||
|
||||
def get(self, username: str, key: str) -> Any:
|
||||
def get(
|
||||
self,
|
||||
username: str,
|
||||
key: Union[str, UserConfigKey],
|
||||
) -> JsonData:
|
||||
"""读取用户配置。"""
|
||||
return self._repository.get(username=username, key=key)
|
||||
|
||||
def set(self, username: str, key: str, value: Any) -> Any:
|
||||
def set(
|
||||
self,
|
||||
username: str,
|
||||
key: Union[str, UserConfigKey],
|
||||
value: JsonData,
|
||||
) -> None:
|
||||
"""写入用户配置。"""
|
||||
return self._repository.set(username=username, key=key, value=value)
|
||||
self._repository.set(username=username, key=key, value=value)
|
||||
|
||||
async def async_set(self, username: str, key: str, value: Any) -> Any:
|
||||
async def async_set(
|
||||
self,
|
||||
username: str,
|
||||
key: Union[str, UserConfigKey],
|
||||
value: JsonData,
|
||||
) -> None:
|
||||
"""异步写入用户配置,并等待数据库提交或回滚完成。"""
|
||||
if self._async_executor is None:
|
||||
raise RuntimeError("用户配置异步数据库执行端口尚未配置")
|
||||
return await self._async_executor.run(
|
||||
partial(self._repository.set, username=username, key=key, value=value)
|
||||
)
|
||||
await self._async_executor.run(partial(self._repository.set, username=username, key=key, value=value))
|
||||
|
||||
async def rename(self, previous_name: str, current_name: str) -> None:
|
||||
"""异步发布已提交的用户名配置迁移。"""
|
||||
if self._async_executor is None:
|
||||
raise RuntimeError("用户配置异步数据库执行端口尚未配置")
|
||||
await self._async_executor.run(partial(self._repository.publish_rename, previous_name, current_name))
|
||||
|
||||
async def delete(self, username: str) -> None:
|
||||
"""异步发布已提交的用户名配置删除。"""
|
||||
if self._async_executor is None:
|
||||
raise RuntimeError("用户配置异步数据库执行端口尚未配置")
|
||||
await self._async_executor.run(partial(self._repository.publish_delete, username))
|
||||
|
||||
|
||||
_configured_user_configuration: UserConfigurationService | None = None
|
||||
_configured_user_configuration: Optional[UserConfigurationService] = None
|
||||
|
||||
|
||||
def configure_user_configuration(service: UserConfigurationService) -> None:
|
||||
|
||||
Reference in New Issue
Block a user