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")), ): """测试发送通知 — 目前返回模拟结果""" 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 {} channel_type = channel.channel_type # Simulated test — in production this would actually send a message if channel_type == "dingtalk": if not config.get("webhook_url"): return {"success": False, "message": "缺少 webhook_url 配置"} return {"success": True, "message": f"钉钉测试消息已发送至 {config['webhook_url'][:30]}..."} elif channel_type == "webhook_feishu": if not config.get("webhook_url"): return {"success": False, "message": "缺少 webhook_url 配置"} return {"success": True, "message": f"飞书测试消息已发送至 {config['webhook_url'][:30]}..."} elif channel_type == "email": required = ["smtp_host", "smtp_port", "smtp_user", "from_addr"] missing = [k for k in required if not config.get(k)] if missing: return {"success": False, "message": f"缺少必要配置: {', '.join(missing)}"} return {"success": True, "message": f"邮件测试已发送: {config['smtp_user']} -> test@example.com"} return {"success": False, "message": f"不支持的通知类型: {channel_type}"}