ForcePilot/backend/server/routers/external_systems/notification_channel_router.py
Kris aabde54f2e refactor(routers): 统一完善所有外部系统接口的参数校验和类型约束
1. 为所有查询参数添加max_length长度限制,规范参数输入范围
2. 使用Literal类型替换普通字符串参数,限定合法取值范围
3. 为路径参数添加Path校验,确保ID参数合法有效
4. 优化请求体参数的声明,补充缺失的Body注解和校验规则
5. 统一分页参数的offset/limit使用方式,替换旧的page/page_size模式
2026-07-04 00:16:45 +08:00

175 lines
6.8 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}`` 前声明,
避免被路径参数捕获。
认证策略:查询类端点使用 ``get_required_user``,写操作端点使用 ``get_admin_user``。
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.infrastructure.container import create_use_cases_from_db
from yuxi.external_systems.use_cases.dto.notification_channel import (
CreateNotificationChannelInput,
DeleteChannelInput,
GetChannelInput,
ListNotificationChannelsInput,
NotificationChannelStatsInput,
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: str = Field(..., min_length=1, max_length=64)
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 列定义,仅透传客户端显式设置的字段。
"""
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)
# ---------------- 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: str | None = Query(None, max_length=32),
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`` 由当前管理员填充。"""
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`` 由当前管理员填充。"""
use_cases = create_use_cases_from_db(db)
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]:
"""删除通知渠道(软删除)。"""
use_cases = create_use_cases_from_db(db)
input_dto = DeleteChannelInput(id=channel_id)
output = await use_cases.notification_channel_service.delete_channel(input_dto)
return {"success": True, "data": output.model_dump()}