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