ForcePilot/backend/server/routers/external_systems/notification_channel_router.py
Kris 6dd16ad3f8 refactor(external-systems-routers): 统一分页参数格式并完善各路由文档与校验
本次提交对多个外部系统路由进行了多维度优化:
1.  统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式
2.  完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释
3.  增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验
4.  优化代码复用:提取重复逻辑为辅助函数,减少样板代码
5.  修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式
6.  补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
2026-07-11 06:57:24 +08:00

243 lines
9.7 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.

"""NotificationChannel 子域 Router。
外部系统限界上下文的通知渠道管理 API覆盖通知渠道列表 / 详情 / 创建 /
更新 / 删除 / 统计 / 测试。所有端点通过 ``create_use_cases_from_db`` 装配 use_cases
经 ``notification_channel_service`` 端口调用用例。
路径顺序约束:静态路径(``/stats``)必须在 ``/{channel_id}`` 前声明,
避免被路径参数捕获。``POST /{channel_id}/test`` 与 ``POST ""`` 路径模式不重叠,
无需额外排序。
认证策略:查询类端点使用 ``get_required_user``,写操作端点(含测试)使用 ``get_admin_user``。
校验层次:
- **边界校验Pydantic**``channel_type`` 使用 ``ChannelTypeLiteral`` 类型,
Pydantic 在请求反序列化时即拒绝非法类型,并在 OpenAPI schema 中生成枚举。
- **领域校验framework**``validate_channel_config`` 按 ``channel_type``
校验 ``config`` 必填字段与 URL 协议,抛 ``DomainValidationError``(映射 400
创建时直接校验;更新时先读取现有渠道的 ``channel_type`` 再校验 ``config``。
Request Schema 与 Input DTO 不共享类Router 内显式构造 DTO创建 / 更新 / 删除
操作的 ``created_by`` / ``updated_by`` 字段由 ``current_user.uid`` 填充。
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.framework.adapters.notifications import (
ChannelTypeLiteral,
validate_channel_config,
)
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
from yuxi.external_systems.use_cases.dto.notification_channel import (
CreateNotificationChannelInput,
DeleteChannelInput,
GetChannelInput,
ListNotificationChannelsInput,
NotificationChannelStatsInput,
TestChannelInput,
UpdateChannelInput,
)
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
notification_channel_router = APIRouter(
prefix="/notification-channels",
tags=["external-systems-notification-channel"],
)
# ---------------- Request Schemas ----------------
class CreateNotificationChannelRequest(BaseModel):
"""创建通知渠道请求体。字段对齐 ``CreateNotificationChannelInput``(不含 created_by
字段长度约束对齐 ``ExternalNotificationChannel`` ORM 列定义,在边界层拦截非法输入:
``channel_type`` 对齐 String(64) 且必须是支持的类型枚举,
``name`` 对齐 String(128)``description`` 对齐 Text。
"""
model_config = ConfigDict(frozen=True)
channel_type: ChannelTypeLiteral
name: str = Field(..., min_length=1, max_length=128)
config: dict[str, Any] = Field(default_factory=dict)
enabled: bool = True
description: str | None = Field(default=None, max_length=2000)
class UpdateNotificationChannelRequest(BaseModel):
"""更新通知渠道请求体。字段对齐 ``UpdateChannelInput``(不含 id / updated_by
字段长度约束对齐 ``ExternalNotificationChannel`` ORM 列定义,仅透传客户端显式设置的字段。
``channel_type`` 不可更新(渠道类型一经确定不可变更),如需变更应删除重建。
"""
model_config = ConfigDict(frozen=True)
name: str | None = Field(default=None, min_length=1, max_length=128)
config: dict[str, Any] | None = None
enabled: bool | None = None
description: str | None = Field(default=None, max_length=2000)
class TestNotificationChannelRequest(BaseModel):
"""测试通知渠道请求体。``message`` 可选,未传时由 service 使用默认测试消息。"""
model_config = ConfigDict(frozen=True)
message: str | None = Field(default=None, max_length=2000)
# ---------------- Endpoints ----------------
@notification_channel_router.get("", response_model=dict)
async def list_notification_channels(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
channel_type: ChannelTypeLiteral | None = Query(None),
enabled: bool | None = Query(None),
keyword: str | None = Query(None, max_length=128),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""分页查询通知渠道列表(支持 channel_type / enabled / keyword 过滤)。"""
use_cases = create_use_cases_from_db(db)
input_dto = ListNotificationChannelsInput(
limit=limit,
offset=offset,
channel_type=channel_type,
enabled=enabled,
keyword=keyword,
)
output = await use_cases.notification_channel_service.list_channels(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.get("/stats", response_model=dict)
async def get_notification_channel_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询通知渠道统计(按 channel_type 分组 + 按启用状态分组)。"""
use_cases = create_use_cases_from_db(db)
input_dto = NotificationChannelStatsInput()
output = await use_cases.notification_channel_service.get_channel_stats(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.get("/{channel_id}", response_model=dict)
async def get_notification_channel(
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询通知渠道详情。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetChannelInput(id=channel_id)
output = await use_cases.notification_channel_service.get_channel(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.post("", response_model=dict)
async def create_notification_channel(
payload: CreateNotificationChannelRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建通知渠道。
``created_by`` 由当前管理员填充。
``channel_type`` 通过 ``ChannelTypeLiteral`` 在 Pydantic 层完成枚举校验,
``config`` 通过 ``validate_channel_config`` 按 ``channel_type`` 校验必填字段与 URL 协议。
"""
validate_channel_config(payload.channel_type, payload.config)
use_cases = create_use_cases_from_db(db)
input_dto = CreateNotificationChannelInput(
channel_type=payload.channel_type,
name=payload.name,
config=payload.config,
enabled=payload.enabled,
description=payload.description,
created_by=current_user.uid,
)
output = await use_cases.notification_channel_service.create_channel(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.put("/{channel_id}", response_model=dict)
async def update_notification_channel(
payload: UpdateNotificationChannelRequest,
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新通知渠道。
仅透传客户端显式设置的字段。``updated_by`` 由当前管理员填充。
若更新包含 ``config``,先读取现有渠道的 ``channel_type``,再按该类型校验
``config`` 结构(``channel_type`` 不可变更,校验基于已持久化的类型)。
"""
use_cases = create_use_cases_from_db(db)
if payload.config is not None:
existing = await use_cases.notification_channel_service.get_channel(
GetChannelInput(id=channel_id),
)
validate_channel_config(existing.channel_type, payload.config)
input_dto = UpdateChannelInput(
id=channel_id,
updated_by=current_user.uid,
**payload.model_dump(exclude_unset=True),
)
output = await use_cases.notification_channel_service.update_channel(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.delete("/{channel_id}", response_model=dict)
async def delete_notification_channel(
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""删除通知渠道(软删除)。
``updated_by`` 由当前管理员填充,写入 ORM 记录确保软删除可追溯操作人。
若渠道被活跃告警引用service 抛 ``ReferencedError``(映射 409
"""
use_cases = create_use_cases_from_db(db)
input_dto = DeleteChannelInput(
id=channel_id,
updated_by=current_user.uid,
)
output = await use_cases.notification_channel_service.delete_channel(input_dto)
return {"success": True, "data": output.model_dump()}
@notification_channel_router.post("/{channel_id}/test", response_model=dict)
async def test_notification_channel(
payload: TestNotificationChannelRequest,
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""测试通知渠道连通性。
构造测试 ``NotificationMessage``,通过 ``NotificationDispatcher`` 派发到渠道,
返回派发结果(成功/失败 + 错误信息 + HTTP 状态码 + 耗时)。
不持久化任何状态,仅用于验证渠道配置是否正确。
"""
use_cases = create_use_cases_from_db(db)
input_dto = TestChannelInput(
id=channel_id,
message=payload.message,
)
output = await use_cases.notification_channel_service.test_channel(input_dto)
return {"success": True, "data": output.model_dump()}