"""配置热更新 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-50,key 不可重复)。 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, ...]