381 lines
11 KiB
Python
381 lines
11 KiB
Python
"""Alerts API endpoints - 告警通知接口"""
|
||
import logging
|
||
import uuid
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||
from sqlalchemy import select, func, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.api.deps import get_current_user
|
||
from app.database import get_db
|
||
from app.models.alert import Alert
|
||
from app.models.alert_setting import AlertSetting
|
||
from app.models.brand import Brand
|
||
from app.models.user import User
|
||
from app.schemas.alert import (
|
||
AlertResponse,
|
||
AlertListResponse,
|
||
AlertUnreadCountResponse,
|
||
AlertReadResponse,
|
||
AlertReadAllResponse,
|
||
AlertSettingResponse,
|
||
AlertSettingUpdate,
|
||
AlertSettingCreate,
|
||
AlertSettingListResponse,
|
||
AlertSettingBulkUpdate,
|
||
)
|
||
from app.services.alert_engine import AlertEngine
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
async def verify_brand_ownership(
|
||
brand_id: uuid.UUID,
|
||
current_user: User,
|
||
db: AsyncSession,
|
||
) -> Brand:
|
||
"""验证品牌属于当前用户"""
|
||
stmt = select(Brand).where(
|
||
and_(
|
||
Brand.id == brand_id,
|
||
Brand.user_id == current_user.id,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
brand = result.scalar_one_or_none()
|
||
|
||
if not brand:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"品牌 {brand_id} 不存在或不属于当前用户",
|
||
)
|
||
|
||
return brand
|
||
|
||
|
||
# ============================================================
|
||
# 告警接口
|
||
# ============================================================
|
||
|
||
@router.get("/", response_model=AlertListResponse)
|
||
async def get_alerts(
|
||
skip: int = Query(0, ge=0, description="跳过条数"),
|
||
limit: int = Query(20, ge=1, le=100, description="每页条数"),
|
||
alert_type: str | None = Query(None, description="按告警类型筛选"),
|
||
severity: str | None = Query(None, description="按严重程度筛选"),
|
||
is_read: bool | None = Query(None, description="按已读状态筛选"),
|
||
brand_id: uuid.UUID | None = Query(None, description="按品牌筛选"),
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
获取当前用户的告警列表
|
||
|
||
支持按类型、严重程度、已读状态和品牌筛选,按创建时间倒序排列。
|
||
"""
|
||
# 构建查询条件
|
||
conditions = [Alert.user_id == current_user.id]
|
||
if alert_type:
|
||
conditions.append(Alert.alert_type == alert_type)
|
||
if severity:
|
||
conditions.append(Alert.severity == severity)
|
||
if is_read is not None:
|
||
conditions.append(Alert.is_read == is_read)
|
||
if brand_id:
|
||
conditions.append(Alert.brand_id == brand_id)
|
||
|
||
# 查询总数
|
||
count_stmt = select(func.count()).select_from(Alert).where(and_(*conditions))
|
||
count_result = await db.execute(count_stmt)
|
||
total = count_result.scalar_one()
|
||
|
||
# 查询列表
|
||
stmt = (
|
||
select(Alert)
|
||
.where(and_(*conditions))
|
||
.order_by(Alert.created_at.desc())
|
||
.offset(skip)
|
||
.limit(limit)
|
||
)
|
||
result = await db.execute(stmt)
|
||
alerts = list(result.scalars().all())
|
||
|
||
return {"items": alerts, "total": total}
|
||
|
||
|
||
@router.get("/unread-count", response_model=AlertUnreadCountResponse)
|
||
async def get_unread_count(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取当前用户的未读告警数量"""
|
||
stmt = select(func.count()).select_from(Alert).where(
|
||
and_(
|
||
Alert.user_id == current_user.id,
|
||
Alert.is_read == False,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
count = result.scalar_one()
|
||
|
||
return {"unread_count": count}
|
||
|
||
|
||
@router.patch("/read-all", response_model=AlertReadAllResponse)
|
||
async def mark_all_read(
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""将当前用户的所有告警标记为已读"""
|
||
stmt = select(Alert).where(
|
||
and_(
|
||
Alert.user_id == current_user.id,
|
||
Alert.is_read == False,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
unread_alerts = list(result.scalars().all())
|
||
|
||
updated_count = 0
|
||
for alert in unread_alerts:
|
||
alert.is_read = True
|
||
updated_count += 1
|
||
|
||
await db.commit()
|
||
|
||
return {"updated_count": updated_count}
|
||
|
||
|
||
@router.patch("/{alert_id}/read", response_model=AlertReadResponse)
|
||
async def mark_read(
|
||
alert_id: uuid.UUID,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""标记指定告警为已读"""
|
||
stmt = select(Alert).where(
|
||
and_(
|
||
Alert.id == alert_id,
|
||
Alert.user_id == current_user.id,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
alert = result.scalar_one_or_none()
|
||
|
||
if not alert:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="告警不存在",
|
||
)
|
||
|
||
alert.is_read = True
|
||
await db.commit()
|
||
await db.refresh(alert)
|
||
|
||
return alert
|
||
|
||
|
||
# ============================================================
|
||
# 告警设置接口
|
||
# ============================================================
|
||
|
||
@router.get("/settings", response_model=AlertSettingListResponse)
|
||
async def get_alert_settings(
|
||
brand_id: uuid.UUID | None = Query(None, description="按品牌筛选"),
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
获取当前用户的告警设置
|
||
|
||
如果指定 brand_id,则返回该品牌的告警设置;
|
||
否则返回所有品牌的告警设置。
|
||
"""
|
||
conditions = [AlertSetting.user_id == current_user.id]
|
||
if brand_id:
|
||
conditions.append(AlertSetting.brand_id == brand_id)
|
||
|
||
# 如果指定了品牌但该品牌没有设置,则自动初始化默认设置
|
||
if brand_id:
|
||
engine = AlertEngine(db)
|
||
await engine.ensure_default_settings(brand_id, current_user.id)
|
||
|
||
count_stmt = select(func.count()).select_from(AlertSetting).where(and_(*conditions))
|
||
count_result = await db.execute(count_stmt)
|
||
total = count_result.scalar_one()
|
||
|
||
stmt = (
|
||
select(AlertSetting)
|
||
.where(and_(*conditions))
|
||
.order_by(AlertSetting.brand_id, AlertSetting.alert_type)
|
||
)
|
||
result = await db.execute(stmt)
|
||
settings = list(result.scalars().all())
|
||
|
||
return {"items": settings, "total": total}
|
||
|
||
|
||
@router.put("/settings", response_model=AlertSettingListResponse)
|
||
async def update_alert_settings(
|
||
data: AlertSettingBulkUpdate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""
|
||
批量更新告警设置
|
||
|
||
接收一组告警设置,对每条记录进行 upsert 操作:
|
||
- 如果品牌+类型组合已存在,则更新
|
||
- 如果不存在,则创建
|
||
"""
|
||
engine = AlertEngine(db)
|
||
updated_settings: list[AlertSetting] = []
|
||
|
||
for item in data.settings:
|
||
# 验证品牌属于当前用户
|
||
await verify_brand_ownership(item.brand_id, current_user, db)
|
||
|
||
# 查找现有设置
|
||
existing_stmt = select(AlertSetting).where(
|
||
and_(
|
||
AlertSetting.brand_id == item.brand_id,
|
||
AlertSetting.alert_type == item.alert_type,
|
||
)
|
||
)
|
||
existing_result = await db.execute(existing_stmt)
|
||
existing = existing_result.scalar_one_or_none()
|
||
|
||
if existing:
|
||
# 更新
|
||
existing.enabled = item.enabled
|
||
if item.threshold is not None:
|
||
existing.threshold = item.threshold
|
||
updated_settings.append(existing)
|
||
else:
|
||
# 创建
|
||
setting = AlertSetting(
|
||
brand_id=item.brand_id,
|
||
user_id=current_user.id,
|
||
alert_type=item.alert_type,
|
||
enabled=item.enabled,
|
||
threshold=item.threshold,
|
||
)
|
||
db.add(setting)
|
||
updated_settings.append(setting)
|
||
|
||
await db.commit()
|
||
|
||
# 刷新所有设置以获取最新数据
|
||
for setting in updated_settings:
|
||
await db.refresh(setting)
|
||
|
||
logger.info(f"批量更新告警设置: user={current_user.id}, count={len(updated_settings)}")
|
||
return {"items": updated_settings, "total": len(updated_settings)}
|
||
|
||
|
||
@router.put("/settings/{setting_id}", response_model=AlertSettingResponse)
|
||
async def update_single_setting(
|
||
setting_id: uuid.UUID,
|
||
data: AlertSettingUpdate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""更新单条告警设置"""
|
||
stmt = select(AlertSetting).where(
|
||
and_(
|
||
AlertSetting.id == setting_id,
|
||
AlertSetting.user_id == current_user.id,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
setting = result.scalar_one_or_none()
|
||
|
||
if not setting:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="告警设置不存在",
|
||
)
|
||
|
||
# 更新提供的字段
|
||
update_data = data.model_dump(exclude_unset=True)
|
||
for field, value in update_data.items():
|
||
setattr(setting, field, value)
|
||
|
||
await db.commit()
|
||
await db.refresh(setting)
|
||
|
||
return setting
|
||
|
||
|
||
@router.post("/settings", response_model=AlertSettingResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_alert_setting(
|
||
data: AlertSettingCreate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""创建告警设置"""
|
||
# 验证品牌属于当前用户
|
||
await verify_brand_ownership(data.brand_id, current_user, db)
|
||
|
||
# 检查是否已存在相同品牌+类型的设置
|
||
existing_stmt = select(AlertSetting).where(
|
||
and_(
|
||
AlertSetting.brand_id == data.brand_id,
|
||
AlertSetting.alert_type == data.alert_type,
|
||
)
|
||
)
|
||
existing_result = await db.execute(existing_stmt)
|
||
existing = existing_result.scalar_one_or_none()
|
||
|
||
if existing:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail=f"品牌 {data.brand_id} 的告警类型 {data.alert_type} 已存在",
|
||
)
|
||
|
||
# 创建新设置
|
||
setting = AlertSetting(
|
||
brand_id=data.brand_id,
|
||
user_id=current_user.id,
|
||
alert_type=data.alert_type,
|
||
enabled=data.enabled,
|
||
threshold=data.threshold,
|
||
)
|
||
db.add(setting)
|
||
await db.commit()
|
||
await db.refresh(setting)
|
||
|
||
logger.info(f"创建告警设置: user={current_user.id}, brand={data.brand_id}, type={data.alert_type}")
|
||
return setting
|
||
|
||
|
||
@router.delete("/settings/{setting_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
async def delete_alert_setting(
|
||
setting_id: uuid.UUID,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""删除告警设置"""
|
||
stmt = select(AlertSetting).where(
|
||
and_(
|
||
AlertSetting.id == setting_id,
|
||
AlertSetting.user_id == current_user.id,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
setting = result.scalar_one_or_none()
|
||
|
||
if not setting:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="告警设置不存在",
|
||
)
|
||
|
||
await db.delete(setting)
|
||
await db.commit()
|
||
|
||
logger.info(f"删除告警设置: user={current_user.id}, setting={setting_id}")
|
||
return None
|