- 新增 platform_notification_channels 表管理通知渠道(钉钉/企微机器人)
- 新增通知渠道管理页面,支持创建、编辑、测试、删除
- 定时任务增加通知渠道选择和企微应用选择
- 脚本执行支持返回值(result变量),自动发送到配置的渠道
- 调度器执行脚本后根据配置自动发送通知
使用方式:
1. 在「通知渠道」页面为租户配置钉钉/企微机器人
2. 创建定时任务时选择通知渠道
3. 脚本中设置 result = {'content': '内容', 'title': '标题'}
4. 任务执行后自动发送到配置的渠道
This commit is contained in:
181
backend/app/routers/notification_channels.py
Normal file
181
backend/app/routers/notification_channels.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""通知渠道API路由"""
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.notification_channel import NotificationChannel
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/notification-channels", tags=["notification-channels"])
|
||||
|
||||
|
||||
# ==================== Schemas ====================
|
||||
|
||||
class ChannelCreate(BaseModel):
|
||||
tenant_id: str
|
||||
channel_name: str
|
||||
channel_type: str # dingtalk_bot, wecom_bot
|
||||
webhook_url: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ChannelUpdate(BaseModel):
|
||||
channel_name: Optional[str] = None
|
||||
channel_type: Optional[str] = None
|
||||
webhook_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
is_enabled: Optional[bool] = None
|
||||
|
||||
|
||||
# ==================== CRUD ====================
|
||||
|
||||
@router.get("")
|
||||
async def list_channels(
|
||||
tenant_id: Optional[str] = None,
|
||||
channel_type: Optional[str] = None,
|
||||
is_enabled: Optional[bool] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取通知渠道列表"""
|
||||
query = db.query(NotificationChannel)
|
||||
|
||||
if tenant_id:
|
||||
query = query.filter(NotificationChannel.tenant_id == tenant_id)
|
||||
if channel_type:
|
||||
query = query.filter(NotificationChannel.channel_type == channel_type)
|
||||
if is_enabled is not None:
|
||||
query = query.filter(NotificationChannel.is_enabled == is_enabled)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(desc(NotificationChannel.created_at)).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"items": [format_channel(c) for c in items]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{channel_id}")
|
||||
async def get_channel(channel_id: int, db: Session = Depends(get_db)):
|
||||
"""获取渠道详情"""
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||||
return format_channel(channel)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_channel(data: ChannelCreate, db: Session = Depends(get_db)):
|
||||
"""创建通知渠道"""
|
||||
channel = NotificationChannel(
|
||||
tenant_id=data.tenant_id,
|
||||
channel_name=data.channel_name,
|
||||
channel_type=data.channel_type,
|
||||
webhook_url=data.webhook_url,
|
||||
description=data.description,
|
||||
is_enabled=True
|
||||
)
|
||||
|
||||
db.add(channel)
|
||||
db.commit()
|
||||
db.refresh(channel)
|
||||
|
||||
return {"success": True, "id": channel.id}
|
||||
|
||||
|
||||
@router.put("/{channel_id}")
|
||||
async def update_channel(channel_id: int, data: ChannelUpdate, db: Session = Depends(get_db)):
|
||||
"""更新通知渠道"""
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||||
|
||||
if data.channel_name is not None:
|
||||
channel.channel_name = data.channel_name
|
||||
if data.channel_type is not None:
|
||||
channel.channel_type = data.channel_type
|
||||
if data.webhook_url is not None:
|
||||
channel.webhook_url = data.webhook_url
|
||||
if data.description is not None:
|
||||
channel.description = data.description
|
||||
if data.is_enabled is not None:
|
||||
channel.is_enabled = data.is_enabled
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/{channel_id}")
|
||||
async def delete_channel(channel_id: int, db: Session = Depends(get_db)):
|
||||
"""删除通知渠道"""
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||||
|
||||
db.delete(channel)
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/{channel_id}/test")
|
||||
async def test_channel(channel_id: int, db: Session = Depends(get_db)):
|
||||
"""测试通知渠道"""
|
||||
import httpx
|
||||
|
||||
channel = db.query(NotificationChannel).filter(NotificationChannel.id == channel_id).first()
|
||||
if not channel:
|
||||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||||
|
||||
test_content = f"**测试消息**\n\n渠道名称: {channel.channel_name}\n发送时间: 测试中..."
|
||||
|
||||
try:
|
||||
if channel.channel_type == 'dingtalk_bot':
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": "渠道测试",
|
||||
"text": test_content
|
||||
}
|
||||
}
|
||||
else: # wecom_bot
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": test_content
|
||||
}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.post(channel.webhook_url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
# 钉钉返回 errcode=0,企微返回 errcode=0
|
||||
if result.get('errcode') == 0:
|
||||
return {"success": True, "message": "发送成功"}
|
||||
else:
|
||||
return {"success": False, "message": f"发送失败: {result}"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"发送失败: {str(e)}"}
|
||||
|
||||
|
||||
# ==================== Helpers ====================
|
||||
|
||||
def format_channel(channel: NotificationChannel) -> dict:
|
||||
"""格式化渠道数据"""
|
||||
return {
|
||||
"id": channel.id,
|
||||
"tenant_id": channel.tenant_id,
|
||||
"channel_name": channel.channel_name,
|
||||
"channel_type": channel.channel_type,
|
||||
"webhook_url": channel.webhook_url,
|
||||
"description": channel.description,
|
||||
"is_enabled": channel.is_enabled,
|
||||
"created_at": channel.created_at,
|
||||
"updated_at": channel.updated_at
|
||||
}
|
||||
@@ -33,6 +33,8 @@ class TaskCreate(BaseModel):
|
||||
retry_interval: Optional[int] = 60
|
||||
alert_on_failure: Optional[bool] = False
|
||||
alert_webhook: Optional[str] = None
|
||||
notify_channels: Optional[List[int]] = None # 通知渠道ID列表
|
||||
notify_wecom_app_id: Optional[int] = None # 企微应用ID
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
@@ -51,6 +53,8 @@ class TaskUpdate(BaseModel):
|
||||
alert_on_failure: Optional[bool] = None
|
||||
alert_webhook: Optional[str] = None
|
||||
is_enabled: Optional[bool] = None
|
||||
notify_channels: Optional[List[int]] = None
|
||||
notify_wecom_app_id: Optional[int] = None
|
||||
|
||||
|
||||
class SecretCreate(BaseModel):
|
||||
@@ -126,6 +130,8 @@ async def create_task(data: TaskCreate, db: Session = Depends(get_db)):
|
||||
retry_interval=data.retry_interval,
|
||||
alert_on_failure=data.alert_on_failure,
|
||||
alert_webhook=data.alert_webhook,
|
||||
notify_channels=data.notify_channels,
|
||||
notify_wecom_app_id=data.notify_wecom_app_id,
|
||||
is_enabled=True
|
||||
)
|
||||
|
||||
@@ -175,6 +181,10 @@ async def update_task(task_id: int, data: TaskUpdate, db: Session = Depends(get_
|
||||
task.alert_on_failure = data.alert_on_failure
|
||||
if data.alert_webhook is not None:
|
||||
task.alert_webhook = data.alert_webhook
|
||||
if data.notify_channels is not None:
|
||||
task.notify_channels = data.notify_channels
|
||||
if data.notify_wecom_app_id is not None:
|
||||
task.notify_wecom_app_id = data.notify_wecom_app_id
|
||||
if data.is_enabled is not None:
|
||||
task.is_enabled = data.is_enabled
|
||||
|
||||
@@ -490,6 +500,14 @@ def format_task(task: ScheduledTask, include_content: bool = False) -> dict:
|
||||
except:
|
||||
time_points = []
|
||||
|
||||
# 处理 notify_channels
|
||||
notify_channels = task.notify_channels
|
||||
if isinstance(notify_channels, str):
|
||||
try:
|
||||
notify_channels = json.loads(notify_channels)
|
||||
except:
|
||||
notify_channels = []
|
||||
|
||||
data = {
|
||||
"id": task.id,
|
||||
"tenant_id": task.tenant_id,
|
||||
@@ -505,6 +523,8 @@ def format_task(task: ScheduledTask, include_content: bool = False) -> dict:
|
||||
"retry_interval": task.retry_interval,
|
||||
"alert_on_failure": bool(task.alert_on_failure),
|
||||
"alert_webhook": task.alert_webhook,
|
||||
"notify_channels": notify_channels or [],
|
||||
"notify_wecom_app_id": task.notify_wecom_app_id,
|
||||
"created_at": task.created_at,
|
||||
"updated_at": task.updated_at
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user