ForcePilot/backend/server/routers/channels/config_router.py
Kris bba1775220 refactor(channel-router): 清理冗余空行并优化代码结构
本次提交包含多类优化:
1.  移除多个路由文件中多余的空导入行,统一代码格式
2.  重构Query参数定义,将长参数拆分为多行提升可读性
3.  新增多个业务端点:
    - 渠道能力画像矩阵查询CAP-03
    - 配对审批计数接口用于待办角标
    - 批量查询对端目录资料接口
    - 向导扫码登录相关端点
    - 会话实时事件SSE推送端点
    - 工作台待办统计接口
4.  完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5.  调整路由导入顺序,修复动态路由路径冲突隐患
6.  更新文档注释与接口清单,修正部分接口描述细节
2026-07-06 20:50:03 +08:00

337 lines
14 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.

"""配置管理域 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)}