Files
opsxsql/backend/app/api/v1/endpoints/notification.py
T
logan bab55d3927 feat: 审核规则模版+设置页面整合+去掉角色切换demo
后端:
- AuditRule 新增 value 字段, rule_type 扩展 DDL/DML
- MySQL/PG/Redis 预设规则种子接口 POST /audit-rules/seed/{db_type}
- 公告管理 CRUD (announcements)
- 消息推送 CRUD+测试 (notifications: 钉钉/飞书/邮件)
- LDAP 配置 CRUD+测试连接 (ldap-config)
- 164 tests passing (+31 新增)

前端:
- AuditRulesPage 重写: DB类型Tab+DDL/DML分组+参数编辑
- SettingsPage: 系统设置主页(审核规则/公告/推送/LDAP)
- NotificationPage: 钉钉/飞书/SMTP配置
- LdapConfigPage: LDAP设置+测试连接
- AnnouncementPage: 公告Markdown编辑
- 去掉左下角角色切换demo
- 侧边栏合并为: 系统设置(含审核规则+公告+推送+LDAP)
2026-05-22 22:26:10 +08:00

157 lines
5.4 KiB
Python

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