"""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