geo/backend/app/api/alerts.py

385 lines
12 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.

"""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.alert_engine import AlertEngine
logger = logging.getLogger(__name__)
router = APIRouter()
def _to_uuid(value: str | uuid.UUID) -> uuid.UUID:
if isinstance(value, uuid.UUID):
return value
return uuid.UUID(str(value))
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 == _to_uuid(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 == _to_uuid(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 == _to_uuid(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 == _to_uuid(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 == _to_uuid(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 == _to_uuid(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, _to_uuid(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=_to_uuid(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 == _to_uuid(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=_to_uuid(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 == _to_uuid(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