import json import copy from typing import List from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import require_role from app.models.user import User from app.models.notification import NotificationChannel from app.schemas.notification import ( NotificationChannelCreate, NotificationChannelUpdate, NotificationChannelResponse, ) router = APIRouter(prefix="/notifications", tags=["消息推送配置"]) def _mask_config(config: dict) -> dict: """Mask sensitive fields in config for response.""" masked = copy.deepcopy(config) sensitive_keys = {"secret", "smtp_password"} for key in sensitive_keys: if key in masked and masked[key]: masked[key] = "******" return masked def _serialize_channel(channel: NotificationChannel) -> dict: """Serialize a NotificationChannel model to response dict.""" config = json.loads(channel.config) if channel.config else {} return { "id": channel.id, "name": channel.name, "channel_type": channel.channel_type, "config": _mask_config(config), "is_enabled": channel.is_enabled, "created_at": channel.created_at, "updated_at": channel.updated_at, } @router.get("", response_model=List[NotificationChannelResponse]) async def list_notifications( db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """管理员获取通知渠道列表""" result = await db.execute(select(NotificationChannel).order_by(NotificationChannel.id)) channels = result.scalars().all() return [_serialize_channel(c) for c in channels] @router.post("", response_model=NotificationChannelResponse, status_code=201) async def create_notification( data: NotificationChannelCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """管理员创建通知渠道""" channel = NotificationChannel( name=data.name, channel_type=data.channel_type, config=json.dumps(data.config, ensure_ascii=False), is_enabled=data.is_enabled, ) db.add(channel) await db.commit() await db.refresh(channel) return _serialize_channel(channel) @router.put("/{channel_id}", response_model=NotificationChannelResponse) async def update_notification( channel_id: int, data: NotificationChannelUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """管理员更新通知渠道""" result = await db.execute( select(NotificationChannel).where(NotificationChannel.id == channel_id) ) channel = result.scalar_one_or_none() if not channel: raise HTTPException(status_code=404, detail="通知渠道不存在") update_data = data.model_dump(exclude_unset=True) if "config" in update_data: update_data["config"] = json.dumps(update_data["config"], ensure_ascii=False) for key, value in update_data.items(): setattr(channel, key, value) await db.commit() await db.refresh(channel) return _serialize_channel(channel) @router.delete("/{channel_id}") async def delete_notification( channel_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """管理员删除通知渠道""" result = await db.execute( select(NotificationChannel).where(NotificationChannel.id == channel_id) ) channel = result.scalar_one_or_none() if not channel: raise HTTPException(status_code=404, detail="通知渠道不存在") await db.delete(channel) await db.commit() return {"message": "通知渠道已删除"} @router.post("/{channel_id}/test") async def test_notification( channel_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """测试发送通知 — 实际发送测试消息到配置的渠道。""" from app.services.notification_sender import NotificationSender result = await db.execute( select(NotificationChannel).where(NotificationChannel.id == channel_id) ) channel = result.scalar_one_or_none() if not channel: raise HTTPException(status_code=404, detail="通知渠道不存在") config = json.loads(channel.config) if channel.config else {} sender = NotificationSender() test_data = { "title": "测试通知", "applicant": current_user.display_name or current_user.username, "db_type": "mysql", "target_database": "test_db", "sql_preview": "SELECT 1", "priority": "normal", "approver": "系统", "ticket_id": "TEST", "result": "成功", "duration": "10", "affected_rows": "0", } result = await sender.send(channel.channel_type, config, "ticket_created", test_data) return result