Files
000-platform/backend/app/routers/notification_channels.py
Admin 2fbba63884
All checks were successful
continuous-integration/drone/push Build is passing
feat: 实现通知渠道管理功能
- 新增 platform_notification_channels 表管理通知渠道(钉钉/企微机器人)
- 新增通知渠道管理页面,支持创建、编辑、测试、删除
- 定时任务增加通知渠道选择和企微应用选择
- 脚本执行支持返回值(result变量),自动发送到配置的渠道
- 调度器执行脚本后根据配置自动发送通知

使用方式:
1. 在「通知渠道」页面为租户配置钉钉/企微机器人
2. 创建定时任务时选择通知渠道
3. 脚本中设置 result = {'content': '内容', 'title': '标题'}
4. 任务执行后自动发送到配置的渠道
2026-01-28 17:02:20 +08:00

182 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""通知渠道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
}