AirGate/backend/utils/scheduler.py
seaislee1209 555c86ce76 feat: initialize AirGate - Volcengine IAM sub-account management platform
Backend (Django 4.2 + DRF):
- Admin auth with SimpleJWT
- Volcengine API client with HMAC-SHA256 signing
- IAM user management (create/sync/import/disable/enable)
- Billing query with pagination
- Feishu webhook notifications (async)
- APScheduler for periodic spending checks
- AES-256 encrypted credential storage
- API key auth for external system integration

Frontend (Vue 3 + Element Plus):
- Login page
- Dashboard with stats overview
- IAM user list with per-user threshold config
- Billing view with spending progress bars
- Alert history with type filtering
- Settings page for global config and Volcengine account management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:03:30 +08:00

145 lines
6.1 KiB
Python

"""定时消费监控任务"""
import logging
from datetime import datetime
from decimal import Decimal
logger = logging.getLogger(__name__)
_scheduler_started = False
def check_spending():
"""定时检查所有子账号消费"""
from apps.monitor.models import VolcAccount, IAMUser, GlobalConfig, AlertRecord
from utils.crypto import decrypt
from utils.billing_service import BillingService
from utils.iam_service import IAMService
from utils.feishu import send_feishu_alert
bill_period = datetime.now().strftime("%Y-%m")
config = GlobalConfig.get_solo()
for volc_account in VolcAccount.objects.filter(is_active=True):
ak = decrypt(volc_account.access_key_enc)
sk = decrypt(volc_account.secret_key_enc)
if not ak or not sk:
logger.warning(f"主账号 {volc_account.name} 密钥为空,跳过")
continue
billing = BillingService(ak, sk)
iam_svc = IAMService(ak, sk)
users = IAMUser.objects.filter(
volc_account=volc_account,
monitor_enabled=True,
).exclude(status=IAMUser.Status.DISABLED)
for user in users:
try:
spending = billing.get_spending_by_project(
bill_period, user.project_name or None
)
user.current_month_spending = spending
user.spending_updated_at = datetime.now()
user.save(update_fields=['current_month_spending', 'spending_updated_at'])
disable_threshold = user.get_disable_threshold()
alert_threshold = user.get_alert_threshold()
# Check disable threshold
if (user.auto_disable_enabled
and disable_threshold
and spending >= disable_threshold):
already_disabled = AlertRecord.objects.filter(
iam_user=user,
alert_type=AlertRecord.AlertType.DISABLE,
created_at__month=datetime.now().month,
created_at__year=datetime.now().year,
).exists()
if not already_disabled:
try:
iam_svc.disable_user(user.username)
user.status = IAMUser.Status.DISABLED
user.save(update_fields=['status'])
except Exception as e:
logger.error(f"停用用户 {user.username} 失败: {e}")
alert = AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.DISABLE,
title=f"子账号 {user.username} 已自动停用",
content=f"本月消费 ¥{spending:.2f},达到停用阈值 ¥{disable_threshold:.2f}",
spending_amount=spending,
threshold_amount=disable_threshold,
)
webhook = config.feishu_webhook_url
send_feishu_alert(
webhook,
"🚨 子账号已自动停用",
f"**用户**: {user.username}\n"
f"**消费**: ¥{spending:.2f}\n"
f"**阈值**: ¥{disable_threshold:.2f}\n"
f"如需恢复,请在 AirGate 管理后台操作。",
template="red",
)
alert.notified = True
alert.save(update_fields=['notified'])
# Check alert threshold
elif alert_threshold and spending >= alert_threshold:
already_alerted = AlertRecord.objects.filter(
iam_user=user,
alert_type=AlertRecord.AlertType.WARNING,
created_at__month=datetime.now().month,
created_at__year=datetime.now().year,
).exists()
if not already_alerted:
alert = AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.WARNING,
title=f"子账号 {user.username} 消费告警",
content=f"本月消费 ¥{spending:.2f},达到告警阈值 ¥{alert_threshold:.2f}",
spending_amount=spending,
threshold_amount=alert_threshold,
)
webhook = config.feishu_webhook_url
send_feishu_alert(
webhook,
"⚠️ 子账号消费告警",
f"**用户**: {user.username}\n"
f"**消费**: ¥{spending:.2f}\n"
f"**告警阈值**: ¥{alert_threshold:.2f}\n"
f"**停用阈值**: ¥{disable_threshold:.2f}",
template="orange",
)
alert.notified = True
alert.save(update_fields=['notified'])
except Exception as e:
logger.error(f"检查用户 {user.username} 消费失败: {e}")
def start_scheduler():
"""启动定时任务"""
global _scheduler_started
if _scheduler_started:
return
_scheduler_started = True
try:
from apscheduler.schedulers.background import BackgroundScheduler
from django.conf import settings
scheduler = BackgroundScheduler()
interval = getattr(settings, 'MONITOR_INTERVAL', 3600)
scheduler.add_job(check_spending, 'interval', seconds=interval,
id='check_spending', replace_existing=True)
scheduler.start()
logger.info(f"消费监控定时任务已启动,间隔 {interval}")
except Exception as e:
logger.error(f"启动定时任务失败: {e}")