"""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()}