"""Alerts API endpoints - 告警通知接口""" 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.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 router = APIRouter() # ============================================================ # 告警接口 # ============================================================ @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: # 验证品牌属于当前用户 from app.models.brand import Brand brand_stmt = select(Brand).where( and_( Brand.id == item.brand_id, Brand.user_id == current_user.id, ) ) brand_result = await db.execute(brand_stmt) brand = brand_result.scalar_one_or_none() if not brand: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"品牌 {item.brand_id} 不存在或不属于当前用户", ) # 查找现有设置 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) 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