feat: add FastAPI routers for channels, notify, and history
This commit is contained in:
parent
53bc48fee6
commit
7f581e3fbb
BIN
backend/__pycache__/main.cpython-310.pyc
Normal file
BIN
backend/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/schemas.cpython-310.pyc
Normal file
BIN
backend/__pycache__/schemas.cpython-310.pyc
Normal file
Binary file not shown.
49
backend/main.py
Normal file
49
backend/main.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import os
|
||||||
|
|
||||||
|
from backend.database import init_db
|
||||||
|
from backend.routers import channels, notify, history
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Startup
|
||||||
|
await init_db()
|
||||||
|
yield
|
||||||
|
# Shutdown
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Apprise Notify Center",
|
||||||
|
description="多通道通知中心 - 支持 80+ 种通知服务",
|
||||||
|
version="1.0.0",
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册路由
|
||||||
|
app.include_router(channels.router)
|
||||||
|
app.include_router(notify.router)
|
||||||
|
app.include_router(history.router)
|
||||||
|
|
||||||
|
# 静态文件(前端)
|
||||||
|
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend")
|
||||||
|
if os.path.exists(frontend_path):
|
||||||
|
app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend")
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
1
backend/routers/__init__.py
Normal file
1
backend/routers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# routers package
|
||||||
BIN
backend/routers/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/routers/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/routers/__pycache__/channels.cpython-310.pyc
Normal file
BIN
backend/routers/__pycache__/channels.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/routers/__pycache__/history.cpython-310.pyc
Normal file
BIN
backend/routers/__pycache__/history.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/routers/__pycache__/notify.cpython-310.pyc
Normal file
BIN
backend/routers/__pycache__/notify.cpython-310.pyc
Normal file
Binary file not shown.
82
backend/routers/channels.py
Normal file
82
backend/routers/channels.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from backend.database import get_db
|
||||||
|
from backend import crud
|
||||||
|
from backend.schemas import Channel, ChannelCreate, ChannelUpdate, ChannelList
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/channels", tags=["channels"])
|
||||||
|
|
||||||
|
@router.get("", response_model=ChannelList)
|
||||||
|
async def list_channels(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取所有通道列表"""
|
||||||
|
channels = await crud.get_channels(db, skip=skip, limit=limit)
|
||||||
|
total = len(channels)
|
||||||
|
return {"channels": channels, "total": total}
|
||||||
|
|
||||||
|
@router.get("/{channel_id}", response_model=Channel)
|
||||||
|
async def get_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""获取单个通道详情"""
|
||||||
|
channel = await crud.get_channel(db, channel_id)
|
||||||
|
if not channel:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@router.post("", response_model=Channel, status_code=201)
|
||||||
|
async def create_channel(
|
||||||
|
channel: ChannelCreate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""创建新通道"""
|
||||||
|
existing = await crud.get_channel_by_name(db, channel.name)
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail="Channel name already exists")
|
||||||
|
|
||||||
|
return await crud.create_channel(db, channel.model_dump())
|
||||||
|
|
||||||
|
@router.put("/{channel_id}", response_model=Channel)
|
||||||
|
async def update_channel(
|
||||||
|
channel_id: int,
|
||||||
|
channel: ChannelUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""更新通道"""
|
||||||
|
if channel.name:
|
||||||
|
existing = await crud.get_channel_by_name(db, channel.name)
|
||||||
|
if existing and existing.id != channel_id:
|
||||||
|
raise HTTPException(status_code=400, detail="Channel name already exists")
|
||||||
|
|
||||||
|
updated = await crud.update_channel(db, channel_id, channel.model_dump(exclude_unset=True))
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
return updated
|
||||||
|
|
||||||
|
@router.delete("/{channel_id}")
|
||||||
|
async def delete_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""删除通道"""
|
||||||
|
success = await crud.delete_channel(db, channel_id)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
return {"message": "Channel deleted successfully"}
|
||||||
|
|
||||||
|
@router.post("/{channel_id}/test")
|
||||||
|
async def test_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""测试通道配置"""
|
||||||
|
from backend.notify_service import NotifyService
|
||||||
|
|
||||||
|
service = NotifyService()
|
||||||
|
result = await service.send_notification(
|
||||||
|
db, channel_id, "测试通知", "这是一条测试消息", "normal"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["status"] == "sent":
|
||||||
|
return {"success": True, "message": "Test notification sent successfully"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Test failed: {result.get('error_msg', 'Unknown error')}"
|
||||||
|
)
|
||||||
41
backend/routers/history.py
Normal file
41
backend/routers/history.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from backend.database import get_db
|
||||||
|
from backend import crud
|
||||||
|
from backend.schemas import Notification
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/notifications", tags=["history"])
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_notifications(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
|
channel_id: Optional[int] = None,
|
||||||
|
status: Optional[str] = Query(None, enum=["pending", "sent", "failed"]),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取通知历史记录"""
|
||||||
|
notifications = await crud.get_notifications(db, skip, limit, channel_id, status)
|
||||||
|
return {
|
||||||
|
"notifications": notifications,
|
||||||
|
"skip": skip,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/{notification_id}")
|
||||||
|
async def get_notification(notification_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""获取单条通知详情"""
|
||||||
|
from sqlalchemy import select
|
||||||
|
from backend.models import Notification
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(Notification).where(Notification.id == notification_id)
|
||||||
|
)
|
||||||
|
notification = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not notification:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
raise HTTPException(status_code=404, detail="Notification not found")
|
||||||
|
|
||||||
|
return notification
|
||||||
82
backend/routers/notify.py
Normal file
82
backend/routers/notify.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from backend.database import get_db
|
||||||
|
from backend.schemas import NotifyRequest, NotifyResponse
|
||||||
|
from backend.notify_service import NotifyService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api", tags=["notify"])
|
||||||
|
|
||||||
|
@router.post("/notify", response_model=NotifyResponse)
|
||||||
|
async def send_notification(
|
||||||
|
request: NotifyRequest,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""发送通知到指定通道或按标签发送"""
|
||||||
|
if not request.channels and not request.tags:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Either 'channels' or 'tags' must be provided"
|
||||||
|
)
|
||||||
|
|
||||||
|
service = NotifyService()
|
||||||
|
results = await service.send_to_channels(
|
||||||
|
db,
|
||||||
|
request.channels,
|
||||||
|
request.tags,
|
||||||
|
request.title,
|
||||||
|
request.body,
|
||||||
|
request.priority
|
||||||
|
)
|
||||||
|
|
||||||
|
total = len(results)
|
||||||
|
sent = sum(1 for r in results if r["status"] == "sent")
|
||||||
|
failed = sum(1 for r in results if r["status"] == "failed")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": sent == total,
|
||||||
|
"results": results,
|
||||||
|
"total": total,
|
||||||
|
"sent": sent,
|
||||||
|
"failed": failed
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/notify")
|
||||||
|
async def send_notification_get(
|
||||||
|
channels: Optional[str] = Query(None, description="通道名称,多个用逗号分隔"),
|
||||||
|
tags: Optional[str] = Query(None, description="标签,多个用逗号分隔"),
|
||||||
|
title: Optional[str] = Query(None),
|
||||||
|
body: str = Query(..., description="消息内容"),
|
||||||
|
priority: str = Query("normal"),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""通过 GET 请求发送通知(方便脚本调用)"""
|
||||||
|
channel_list = channels.split(",") if channels else None
|
||||||
|
tag_list = tags.split(",") if tags else None
|
||||||
|
|
||||||
|
if not channel_list and not tag_list:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Either 'channels' or 'tags' must be provided"
|
||||||
|
)
|
||||||
|
|
||||||
|
service = NotifyService()
|
||||||
|
results = await service.send_to_channels(
|
||||||
|
db,
|
||||||
|
channel_list,
|
||||||
|
tag_list,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
priority
|
||||||
|
)
|
||||||
|
|
||||||
|
total = len(results)
|
||||||
|
sent = sum(1 for r in results if r["status"] == "sent")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": sent == total,
|
||||||
|
"results": results,
|
||||||
|
"total": total,
|
||||||
|
"sent": sent,
|
||||||
|
"failed": total - sent
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user