from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from datetime import datetime, timezone from pydantic import BaseModel from app.core.database import get_db from app.core.deps import get_current_user, require_roles from app.models.alarm import AlarmRule, AlarmEvent from app.models.user import User from app.services.audit import log_audit router = APIRouter(prefix="/alarms", tags=["告警管理"]) class AlarmRuleCreate(BaseModel): name: str device_id: int | None = None device_type: str | None = None data_type: str condition: str threshold: float | None = None threshold_high: float | None = None threshold_low: float | None = None duration: int = 0 severity: str = "warning" notify_channels: list[str] | None = None notify_targets: list[str] | None = None silence_start: str | None = None silence_end: str | None = None @router.get("/rules") async def list_rules(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): result = await db.execute(select(AlarmRule).order_by(AlarmRule.id.desc())) return [_rule_to_dict(r) for r in result.scalars().all()] @router.post("/rules") async def create_rule(data: AlarmRuleCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))): rule = AlarmRule(**data.model_dump(), created_by=user.id) db.add(rule) await db.flush() return _rule_to_dict(rule) @router.put("/rules/{rule_id}") async def update_rule(rule_id: int, data: AlarmRuleCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))): result = await db.execute(select(AlarmRule).where(AlarmRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="规则不存在") for k, v in data.model_dump(exclude_unset=True).items(): setattr(rule, k, v) return _rule_to_dict(rule) @router.delete("/rules/{rule_id}") async def delete_rule(rule_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager"))): result = await db.execute(select(AlarmRule).where(AlarmRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="规则不存在") rule.is_active = False return {"message": "已删除"} @router.get("/events") async def list_events( status: str | None = None, severity: str | None = None, device_id: int | None = None, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): query = select(AlarmEvent) if status: query = query.where(AlarmEvent.status == status) if severity: query = query.where(AlarmEvent.severity == severity) if device_id: query = query.where(AlarmEvent.device_id == device_id) count_q = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_q)).scalar() query = query.order_by(AlarmEvent.triggered_at.desc()).offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) return { "total": total, "items": [{ "id": e.id, "rule_id": e.rule_id, "device_id": e.device_id, "severity": e.severity, "title": e.title, "description": e.description, "value": e.value, "threshold": e.threshold, "status": e.status, "triggered_at": str(e.triggered_at), "acknowledged_at": str(e.acknowledged_at) if e.acknowledged_at else None, "resolved_at": str(e.resolved_at) if e.resolved_at else None, } for e in result.scalars().all()] } @router.post("/events/{event_id}/acknowledge") async def acknowledge_event(event_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): result = await db.execute(select(AlarmEvent).where(AlarmEvent.id == event_id)) event = result.scalar_one_or_none() if not event: raise HTTPException(status_code=404, detail="告警不存在") event.status = "acknowledged" event.acknowledged_by = user.id event.acknowledged_at = datetime.now(timezone.utc) await log_audit(db, user.id, "acknowledge", "alarm", detail=f"确认告警 #{event_id}") return {"message": "已确认"} @router.post("/events/{event_id}/resolve") async def resolve_event(event_id: int, note: str = "", db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): result = await db.execute(select(AlarmEvent).where(AlarmEvent.id == event_id)) event = result.scalar_one_or_none() if not event: raise HTTPException(status_code=404, detail="告警不存在") event.status = "resolved" event.resolved_at = datetime.now(timezone.utc) event.resolve_note = note await log_audit(db, user.id, "resolve", "alarm", detail=f"解决告警 #{event_id}") return {"message": "已解决"} @router.get("/stats") async def alarm_stats(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): result = await db.execute( select(AlarmEvent.severity, AlarmEvent.status, func.count(AlarmEvent.id)) .group_by(AlarmEvent.severity, AlarmEvent.status) ) stats = {} for severity, status, count in result.all(): if severity not in stats: stats[severity] = {} stats[severity][status] = count return stats def _rule_to_dict(r: AlarmRule) -> dict: return { "id": r.id, "name": r.name, "device_id": r.device_id, "device_type": r.device_type, "data_type": r.data_type, "condition": r.condition, "threshold": r.threshold, "threshold_high": r.threshold_high, "threshold_low": r.threshold_low, "duration": r.duration, "severity": r.severity, "is_active": r.is_active, "notify_channels": r.notify_channels, "silence_start": r.silence_start, "silence_end": r.silence_end, }