ForcePilot/backend/package/yuxi/channels/contract/dtos/config.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

395 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""配置热更新 DTO。
定义配置热更新端口的命令与值对象,包括配置热更新模式、配置作用域、
配置版本、配置字段、更新 / 回滚配置命令与配置值。所有 DTO 均为
``dataclass(frozen=True)``,仅依赖标准库与契约层内部类型,用于配置
热更新FR-37
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
from yuxi.channels.contract.errors import ValidationError
class ConfigHotReloadMode(StrEnum):
"""配置热更新模式。
标识配置变更的热更新策略,用于控制配置变更的生效方式。继承
``str, Enum`` 以支持 JSON 序列化与字符串比较。
取值:
OFF: 不热更新。
HOT: 所有可热更新的配置项均热更新。
RESTART: 所有变更触发重启。
HYBRID: 混合模式(默认)。
"""
OFF = "off"
HOT = "hot"
RESTART = "restart"
HYBRID = "hybrid"
class ConfigScope(StrEnum):
"""配置作用域。
标识配置项的作用域层级,用于配置管理与作用域过滤。继承
``str, Enum`` 以支持 JSON 序列化与字符串比较。
取值:
GLOBAL: 全局配置。
CHANNEL: 渠道级配置。
ACCOUNT: 账户级配置。
PLUGIN: 插件级配置target 为 plugin_id
"""
GLOBAL = "global"
CHANNEL = "channel"
ACCOUNT = "account"
PLUGIN = "plugin"
@dataclass(frozen=True)
class ConfigVersion:
"""配置版本。
描述配置项的版本信息,包括键、版本号与更新时间,用于乐观并发控制
与版本回溯。
字段:
key: 配置键。
version: 版本号。
updated_at: 更新时间配置不存在version=0时为 None
避免误报"刚刚更新"
"""
key: str
version: int
updated_at: datetime | None
@dataclass(frozen=True)
class ConfigField:
"""配置字段。
描述配置项的字段元信息,包括键、类型、是否必填、默认值、是否可热
更新与约束,用于配置校验与热更新决策。
字段:
key: 配置键。
type: 字段类型str | int | bool | json
required: 是否必填(默认 False
default: 默认值(默认 None
hot_reloadable: 是否可热更新(默认 True
constraints: 约束(可选)。
"""
key: str
type: Literal["str", "int", "bool", "json"]
required: bool = False
default: Any = None
hot_reloadable: bool = True
constraints: dict[str, Any] | None = None
@dataclass(frozen=True)
class UpdateConfigCmd:
"""更新配置命令FR-37
由配置端口方法引用,描述一次配置更新请求,携带键、值、作用域、目标、
期望版本与操作人,用于配置更新与乐观并发控制。
字段:
key: 配置键。
value: 配置值。
operator: 操作人(审计用)。
scope: 配置作用域(默认 GLOBAL
target: 目标channel_type / account_id / plugin_id可选
expected_version: 期望版本(可选,用于乐观并发控制)。
"""
key: str
value: Any
operator: Operator
scope: ConfigScope = ConfigScope.GLOBAL
target: str | None = None
expected_version: int | None = None
def __post_init__(self) -> None:
"""校验必填字段非空与版本号合理性FR-37
``key`` 必须非空,``expected_version`` 提供时必须为正整数,
``scope`` 为 ``channel`` / ``account`` / ``plugin`` 时 ``target``
必须非空。在构造时即抛出 ``ValidationError``adapter 不再做该校验INV-8
"""
if not self.key:
raise ValidationError("key", "must not be empty")
if self.expected_version is not None and self.expected_version <= 0:
raise ValidationError("expected_version", "must be positive")
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
raise ValidationError(
"target",
"must not be empty when scope is channel, account or plugin",
)
@dataclass(frozen=True)
class RollbackConfigCmd:
"""回滚配置命令FR-37
由配置端口方法引用,描述一次配置回滚请求,携带键、目标版本、作用域、
目标与操作人,用于配置版本回滚。
字段:
key: 配置键。
target_version: 目标版本。
operator: 操作人(审计用)。
scope: 配置作用域(默认 GLOBAL
target: 目标channel_type / account_id / plugin_id可选
"""
key: str
target_version: int
operator: Operator
scope: ConfigScope = ConfigScope.GLOBAL
target: str | None = None
def __post_init__(self) -> None:
"""校验必填字段非空与目标版本号合理性FR-37
``key`` 必须非空,``target_version`` 必须为正整数,
``scope`` 为 ``channel`` / ``account`` / ``plugin`` 时 ``target``
必须非空。在构造时即抛出 ``ValidationError``adapter 不再做该校验INV-8
"""
if not self.key:
raise ValidationError("key", "must not be empty")
if self.target_version <= 0:
raise ValidationError("target_version", "must be positive")
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
raise ValidationError(
"target",
"must not be empty when scope is channel, account or plugin",
)
@dataclass(frozen=True)
class ConfigValue:
"""配置值。
描述配置项的当前值,包括键、值、版本与作用域,用于配置查询与通知。
字段:
key: 配置键。
value: 配置值。
version: 版本号。
scope: 配置作用域。
"""
key: str
value: Any
version: int
scope: ConfigScope
@dataclass(frozen=True)
class ConfigHistoryEntry:
"""配置历史版本条目。
描述配置项的单个历史版本信息,包括版本号、值与更新时间,用于配置
版本历史查询FR-37。``updated_at`` 在历史条目未持久化时间戳时
为 ``None``(兼容历史数据)。
字段:
version: 版本号。
value: 配置值(已反序列化的 Python 对象)。
updated_at: 更新时间;历史数据未存储时间戳时为 ``None``。
"""
version: int
value: Any
updated_at: datetime | None
@dataclass(frozen=True)
class ConfigExportCmd:
"""配置导出命令CFG-EXPORT
由 ``ConfigManagementPort.exportConfig`` 引用,导出指定作用域的配置,
需记录操作人以满足审计要求(超级管理员)。
字段:
operator: 操作人(审计用)。
scope: 配置作用域(默认 GLOBAL
target: 目标channel_type 或 account_id可选
"""
operator: Operator
scope: ConfigScope = ConfigScope.GLOBAL
target: str | None = None
@dataclass(frozen=True)
class ConfigExportResult:
"""配置导出结果CFG-EXPORT
字段:
scope: 配置作用域。
config_data: 导出的配置数据。
version: 导出格式版本。
exported_at: 导出时间戳。
target: 目标(可选)。
"""
scope: ConfigScope
config_data: dict[str, Any]
version: int
exported_at: datetime
target: str | None = None
@dataclass(frozen=True)
class ImportConfigCmd:
"""配置导入命令CFG-IMPORT
由 ``ConfigManagementPort.importConfig`` 引用,逐条校验 schema 并写入,
需记录操作人以满足审计要求(超级管理员)。
字段:
operator: 操作人(审计用)。
config_data: 导入的配置数据。
scope: 配置作用域(默认 GLOBAL
target: 目标(可选)。
overwrite: 是否覆盖已存在 key默认 False
dry_run: 是否仅预校验不写入(默认 False
"""
operator: Operator
config_data: dict[str, Any]
scope: ConfigScope = ConfigScope.GLOBAL
target: str | None = None
overwrite: bool = False
dry_run: bool = False
@dataclass(frozen=True)
class ImportConfigResult:
"""配置导入结果CFG-IMPORT
字段:
imported_count: 成功导入数。
skipped_count: 跳过数overwrite=false 时已存在)。
failed_count: 失败数。
failed_keys: 失败的配置 key 元组。
imported_at: 导入时间戳。
"""
imported_count: int
skipped_count: int
failed_count: int
failed_keys: tuple[str, ...]
imported_at: datetime
@dataclass(frozen=True)
class ConfigUpdateItem:
"""单条配置更新项CFG-BATCH-UPDATE
描述批量更新中的单条配置项,含 key、value 与期望版本(乐观并发控制)。
字段:
key: 配置键。
value: 配置值。
expected_version: 期望版本(可选,用于乐观并发控制)。
"""
key: str
value: Any
expected_version: int | None = None
@dataclass(frozen=True)
class BatchConfigUpdateSuccessItem:
"""批量配置更新成功条目CFG-BATCH-UPDATE
字段:
key: 配置键。
new_version: 更新后的新版本号。
"""
key: str
new_version: int
@dataclass(frozen=True)
class BatchUpdateConfigCmd:
"""批量更新配置命令CFG-BATCH-UPDATE
由 ``ConfigManagementPort.batchUpdateConfig`` 引用,逐条复用
config/update 逻辑版本校验、hot-reload 检查),需记录操作人。
字段:
operator: 操作人(审计用)。
updates: 配置更新项元组必填1-50key 不可重复)。
scope: 配置作用域(默认 GLOBAL
target: 目标(可选)。
stop_on_error: 首条失败即终止(默认 False逐条独立事务
"""
operator: Operator
updates: tuple[ConfigUpdateItem, ...]
scope: ConfigScope = ConfigScope.GLOBAL
target: str | None = None
stop_on_error: bool = False
def __post_init__(self) -> None:
"""校验 updates 非空、长度上限与 key 唯一性FR-37
``updates`` 必须非空且长度不超过 50同一批次内 ``key`` 不可重复,
否则 ``BatchExecutor`` 按 key 反查 update item 时会重复命中第一条,
导致后续同 key 条目的 value 被静默丢弃INV-8
"""
if not self.updates:
raise ValidationError("updates", "must not be empty")
if len(self.updates) > 50:
raise ValidationError("updates", "must not exceed 50 items")
keys = [item.key for item in self.updates]
if not all(keys):
raise ValidationError("key", "must not be empty")
seen: set[str] = set()
for key in keys:
if key in seen:
raise ValidationError(
"updates",
f"duplicate key in batch update: {key}",
)
seen.add(key)
if self.scope in (ConfigScope.CHANNEL, ConfigScope.ACCOUNT, ConfigScope.PLUGIN) and not self.target:
raise ValidationError(
"target",
"must not be empty when scope is channel, account or plugin",
)
@dataclass(frozen=True)
class BatchUpdateConfigResult:
"""批量更新配置结果CFG-BATCH-UPDATE
描述批量更新配置的执行结果,``failed`` 使用通用
``BatchOperationFailure````id`` 字段承载 key
字段:
total: 待更新配置总数。
succeeded: 成功条目元组。
failed: 失败条目元组。
"""
total: int
succeeded: tuple[BatchConfigUpdateSuccessItem, ...]
failed: tuple[BatchOperationFailure, ...]