"""Alert service: budget monitoring and system alerts.""" from sqlalchemy.orm import Session from typing import List, Optional from app.models.alert import Alert from app.models.project import Project from app.models.worker import AIWorker from app.services.cost_service import CostService class AlertService: @staticmethod def check_and_alert(db: Session, tenant_id: int, project_id: int) -> Optional[Alert]: """Check project budget and create alert if needed.""" project = db.query(Project).filter( Project.id == project_id, Project.tenant_id == tenant_id ).first() if not project or not project.budget_limit_cents: return None spent = CostService.get_project_cost(db, tenant_id, project_id) limit = project.budget_limit_cents pct = (spent / limit) if limit > 0 else 0 if pct >= 1.0: # Pause project project.status = "paused" db.commit() return AlertService._create( db, tenant_id, "budget_exceeded", "critical", f"项目「{project.name}」预算已超限!已花费 ¥{spent/100:.2f} / ¥{limit/100:.2f},项目已自动暂停。", project_id=project_id, ) elif pct >= 0.8: return AlertService._create( db, tenant_id, "budget_warning", "warning", f"项目「{project.name}」预算使用达 {pct*100:.0f}%(¥{spent/100:.2f} / ¥{limit/100:.2f})", project_id=project_id, ) return None @staticmethod def check_worker_budget(db: Session, tenant_id: int, worker_id: int) -> Optional[Alert]: worker = db.query(AIWorker).filter( AIWorker.id == worker_id, AIWorker.tenant_id == tenant_id ).first() if not worker or not worker.max_cost_per_month_cents: return None spent = CostService.get_worker_monthly_cost(db, tenant_id, worker_id) limit = worker.max_cost_per_month_cents pct = (spent / limit) if limit > 0 else 0 if pct >= 1.0: worker.is_active = False db.commit() return AlertService._create( db, tenant_id, "budget_exceeded", "critical", f"Worker「{worker.name}」月度预算已超限!已花费 ¥{spent/100:.2f} / ¥{limit/100:.2f},Worker已停用。", worker_id=worker_id, ) elif pct >= 0.8: return AlertService._create( db, tenant_id, "budget_warning", "warning", f"Worker「{worker.name}」月度预算使用达 {pct*100:.0f}%", worker_id=worker_id, ) return None @staticmethod def _create(db, tenant_id, alert_type, severity, message, project_id=None, worker_id=None): alert = Alert( tenant_id=tenant_id, alert_type=alert_type, severity=severity, message=message, project_id=project_id, worker_id=worker_id, ) db.add(alert) db.commit() db.refresh(alert) return alert @staticmethod def list_alerts(db, tenant_id, is_read=None, skip=0, limit=50): q = db.query(Alert).filter(Alert.tenant_id == tenant_id) if is_read is not None: q = q.filter(Alert.is_read == is_read) return q.order_by(Alert.created_at.desc()).offset(skip).limit(limit).all() @staticmethod def unread_count(db, tenant_id): return db.query(Alert).filter( Alert.tenant_id == tenant_id, Alert.is_read == False ).count() @staticmethod def mark_read(db, tenant_id, alert_id): alert = db.query(Alert).filter( Alert.id == alert_id, Alert.tenant_id == tenant_id ).first() if alert: alert.is_read = True db.commit() return alert