ForcePilot/backend/server/routers/channels/config_router.py

337 lines
14 KiB
Python
Raw Normal View History

"""配置管理域 RouterCFG-02 / CFG-READ-01 / CFG-UPDATE-01 / CFG-ROLL-01 / CFG-HIST-01 / CFG-EXPORT / CFG-IMPORT / CFG-BATCH-UPDATE
router 实现渠道配置管理域的全部 HTTP 端点覆盖 schema 查询配置查询
更新回滚历史导出导入与批量更新共 8 个操作所有端点统一采用模板 A
控制面端口路由通过 ``get_channel_use_cases`` 装配 ``ChannelUseCases``
``config_management`` 端口调用类型化方法 ``ChannelControlService``
内部走控制面管道auth permission rate_limit dispatch audit
鉴权策略全部端点使用 ``get_admin_user`` 依赖要求管理员或超级管理员角色
角色校验由依赖函数完成router 内不做角色判断规范 §4
模板选型模板 A控制面端口路由端点函数体仅做
``operator = build_operator`` ``use_cases.config_management.<method>``
``raiseOnControlFailure``
``return {"success": True, "data": serialize_control_data(result.data)}``
不包含 try/except 吞异常直接 Redis 访问ORM 模型返回或
``_executeControl`` 直接调用模板选型决策依据详见设计方案 §2.5
路径设计静态路径 ``/config/schema`` / ``/config/export`` / ``/config/import``
/ ``/config/batch`` 先于动态路径 ``/config/{key}`` 声明避免被动态路径捕获
规范 §6.5 router 不自行设置 prefix根前缀 ``/channels``
``channels_router`` 聚合 router 统一追加完整 HTTP 路径为
``/channels/config/*``不含 ``{channel_type}`` 配置管理为跨渠道
全局能力
tags 命名``config_router = APIRouter(tags=["channels-config"])``与现有
``channels-login``/``channels-account`` 等子 router 命名规范一致
端点清单对应配置管理域设计方案§2.1
- GET /config/schema CFG-02 get_config_schema
- GET /config/export CFG-EXPORT export_config
- POST /config/import CFG-IMPORT import_config
- PUT /config/batch CFG-BATCH-UPDATE batch_update_config
- GET /config/{key} CFG-READ-01 get_config
- PUT /config/{key} CFG-UPDATE-01 update_config
- POST /config/{key}/rollback CFG-ROLL-01 rollback_config
- GET /config/{key}/history CFG-HIST-01 get_config_history
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.config import (
BatchUpdateConfigCmd,
ConfigExportCmd,
ConfigScope,
ConfigUpdateItem,
ImportConfigCmd,
RollbackConfigCmd,
UpdateConfigCmd,
)
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
config_router = APIRouter(tags=["channels-config"])
class UpdateConfigRequest(BaseModel):
"""更新配置请求体CFG-UPDATE-01
``value`` ``Any`` 类型类型校验由 ``ConfigManager._validateValueType``
基于 ``CONFIG_SCHEMA`` 声明的 ``type`` 字段执行``str`` / ``int`` /
``bool`` / ``json``
"""
model_config = ConfigDict(frozen=True)
value: Any = Field(..., description="配置值(类型需与 schema 声明一致)")
scope: ConfigScope = Field(default=ConfigScope.GLOBAL, description="配置作用域")
target: str | None = Field(default=None, description="作用域目标")
expected_version: int | None = Field(default=None, ge=1, description="乐观锁版本号")
class RollbackConfigRequest(BaseModel):
"""回滚配置请求体CFG-ROLL-01"""
model_config = ConfigDict(frozen=True)
target_version: int = Field(..., ge=1, description="回滚目标版本号")
scope: ConfigScope = Field(default=ConfigScope.GLOBAL, description="配置作用域")
target: str | None = Field(default=None, description="作用域目标")
class ImportConfigRequest(BaseModel):
"""导入配置请求体CFG-IMPORT
``config_data`` key 必须在 ``CONFIG_SCHEMA`` 中声明否则 dispatch
handler 记入 failed``dry_run=true`` 时仅校验不写入
"""
model_config = ConfigDict(frozen=True)
config_data: dict[str, Any] = Field(..., description="待导入的配置字典")
scope: ConfigScope = Field(default=ConfigScope.GLOBAL, description="配置作用域")
target: str | None = Field(default=None, description="作用域目标")
overwrite: bool = Field(default=False, description="是否覆盖已存在 key")
dry_run: bool = Field(default=False, description="是否仅预校验不写入")
class ConfigUpdateItemRequest(BaseModel):
"""单条配置更新项请求体CFG-BATCH-UPDATE 子项)。"""
model_config = ConfigDict(frozen=True)
key: str = Field(..., description="配置键")
value: Any = Field(..., description="配置值")
expected_version: int | None = Field(default=None, ge=1, description="乐观锁版本号")
class BatchUpdateConfigRequest(BaseModel):
"""批量更新配置请求体CFG-BATCH-UPDATE
``updates`` 长度限制 1-50dispatch handler 校验``stop_on_error=false``
时逐条独立事务模式 D 部分成功``stop_on_error=true`` 时首条失败即终止
"""
model_config = ConfigDict(frozen=True)
updates: list[ConfigUpdateItemRequest] = Field(..., min_length=1, max_length=50, description="配置更新项列表")
scope: ConfigScope = Field(default=ConfigScope.GLOBAL, description="配置作用域")
target: str | None = Field(default=None, description="作用域目标")
stop_on_error: bool = Field(default=False, description="首条失败即终止")
@config_router.get("/config/schema", response_model=dict)
async def get_config_schema(
request: Request,
scope: ConfigScope = Query(default=ConfigScope.GLOBAL, description="配置作用域"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询配置 schema 元数据CFG-02。对应控制面操作 config/schema。
返回 ``CONFIG_SCHEMA`` 的字段元数据类型是否必填默认值是否可
热更新约束供管理后台动态表单渲染所有作用域共享同一 schema
``scope`` 参数仅为审计上下文保留
"""
operator = build_operator(current_user, request)
result = await use_cases.config_management.getConfigSchema(
operator=operator,
scope=scope,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.get("/config/export", response_model=dict)
async def export_config(
request: Request,
scope: ConfigScope = Query(default=ConfigScope.GLOBAL, description="配置作用域"),
target: str | None = Query(default=None, description="作用域目标channel/account 时必填)"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""导出配置CFG-EXPORT。对应控制面操作 config/export。
遍历指定 ``scope`` / ``target`` 的所有配置 key 序列化返回
``ConfigExportResult`` scope / config_data / version / exported_at /
target需超级管理员权限
"""
operator = build_operator(current_user, request)
cmd = ConfigExportCmd(
operator=operator,
scope=scope,
target=target,
)
result = await use_cases.config_management.exportConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.post("/config/import", response_model=dict)
async def import_config(
payload: ImportConfigRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""导入配置CFG-IMPORT。对应控制面操作 config/import。
逐条校验 schema ``dry_run`` 时仅统计不写入 ``overwrite=false``
已存在 key 记入 skipped 逐条写入返回 ``ImportConfigResult``
imported_count / skipped_count / failed_count / failed_keys / imported_at
需超级管理员权限
"""
operator = build_operator(current_user, request)
cmd = ImportConfigCmd(
operator=operator,
config_data=payload.config_data,
scope=payload.scope,
target=payload.target,
overwrite=payload.overwrite,
dry_run=payload.dry_run,
)
result = await use_cases.config_management.importConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.put("/config/batch", response_model=dict)
async def batch_update_config(
payload: BatchUpdateConfigRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量更新配置CFG-BATCH-UPDATE。对应控制面操作 config/batch_update。
``stop_on_error=false`` 时逐条独立事务模式 D 部分成功
``stop_on_error=true`` 时首条失败即终止每条复用 ``config/update`` 逻辑
版本校验hot-reload 检查返回 ``BatchUpdateConfigResult``
total / succeededkey + new_version / failed 列表
"""
operator = build_operator(current_user, request)
cmd = BatchUpdateConfigCmd(
operator=operator,
updates=tuple(
ConfigUpdateItem(
key=item.key,
value=item.value,
expected_version=item.expected_version,
)
for item in payload.updates
),
scope=payload.scope,
target=payload.target,
stop_on_error=payload.stop_on_error,
)
result = await use_cases.config_management.batchUpdateConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.get("/config/{key}", response_model=dict)
async def get_config(
key: str,
request: Request,
scope: ConfigScope = Query(default=ConfigScope.GLOBAL, description="配置作用域"),
target: str | None = Query(default=None, description="作用域目标channel/account 时必填)"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询配置值CFG-READ-01。对应控制面操作 config/get。"""
operator = build_operator(current_user, request)
result = await use_cases.config_management.getConfig(
key=key,
operator=operator,
scope=scope,
target=target,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.put("/config/{key}", response_model=dict)
async def update_config(
key: str,
payload: UpdateConfigRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新配置值CFG-UPDATE-01。对应控制面操作 config/update。
不可热更新的配置项抛 ``ConfigRestartRequiredError``值类型校验失败抛
``ConfigValidationError``版本冲突抛 ``ConfigVersionConflictError``
"""
operator = build_operator(current_user, request)
cmd = UpdateConfigCmd(
key=key,
value=payload.value,
operator=operator,
scope=payload.scope,
target=payload.target,
expected_version=payload.expected_version,
)
result = await use_cases.config_management.updateConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.post("/config/{key}/rollback", response_model=dict)
async def rollback_config(
key: str,
payload: RollbackConfigRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""回滚配置到指定版本CFG-ROLL-01。对应控制面操作 config/rollback。
目标版本不存在抛 ``NotFoundError``版本冲突抛
``ConfigVersionConflictError``回滚失败抛 ``ConfigRollbackError``
"""
operator = build_operator(current_user, request)
cmd = RollbackConfigCmd(
key=key,
target_version=payload.target_version,
operator=operator,
scope=payload.scope,
target=payload.target,
)
result = await use_cases.config_management.rollbackConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.get("/config/{key}/history", response_model=dict)
async def get_config_history(
key: str,
request: Request,
scope: ConfigScope = Query(default=ConfigScope.GLOBAL, description="配置作用域"),
target: str | None = Query(default=None, description="作用域目标"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询配置版本历史CFG-HIST-01。对应控制面操作 config/history。"""
operator = build_operator(current_user, request)
result = await use_cases.config_management.getConfigHistory(
key=key,
operator=operator,
scope=scope,
target=target,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}