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