本次提交包含多类优化:
1. 移除多个路由文件中多余的空导入行,统一代码格式
2. 重构Query参数定义,将长参数拆分为多行提升可读性
3. 新增多个业务端点:
- 渠道能力画像矩阵查询CAP-03
- 配对审批计数接口用于待办角标
- 批量查询对端目录资料接口
- 向导扫码登录相关端点
- 会话实时事件SSE推送端点
- 工作台待办统计接口
4. 完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5. 调整路由导入顺序,修复动态路由路径冲突隐患
6. 更新文档注释与接口清单,修正部分接口描述细节
337 lines
14 KiB
Python
337 lines
14 KiB
Python
"""配置管理域 Router(CFG-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-50(dispatch 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 / succeeded(key + 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)}
|