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

149 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.db import models
class VolcAccount(models.Model):
"""火山引擎主账号配置(加密存储)"""
name = models.CharField('账号名称', max_length=100, default='默认主账号')
access_key_enc = models.TextField('AccessKey加密', blank=True)
secret_key_enc = models.TextField('SecretKey加密', blank=True)
access_key_hint = models.CharField('AK 提示前4后4', max_length=20, blank=True)
is_active = models.BooleanField('启用', default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '火山主账号'
verbose_name_plural = '火山主账号'
db_table = 'airgate_volc_account'
def __str__(self):
return f"{self.name} ({self.access_key_hint})"
class IAMUser(models.Model):
"""受管理的 IAM 子账号"""
class Status(models.TextChoices):
ACTIVE = 'active', '正常'
DISABLED = 'disabled', '已停用'
UNKNOWN = 'unknown', '未知'
volc_account = models.ForeignKey(VolcAccount, on_delete=models.CASCADE, related_name='iam_users')
username = models.CharField('IAM 用户名', max_length=200, db_index=True)
display_name = models.CharField('显示名', max_length=200, blank=True)
user_id = models.CharField('火山 UserID', max_length=100, blank=True)
email = models.EmailField('邮箱', blank=True)
phone = models.CharField('手机号', max_length=20, blank=True)
project_name = models.CharField('关联项目名', max_length=200, blank=True,
help_text='用于按项目维度追踪消费')
status = models.CharField('状态', max_length=20, choices=Status.choices, default=Status.UNKNOWN)
# Access keys (stored as JSON list of AK IDs, not secrets)
access_key_ids = models.JSONField('AccessKey ID 列表', default=list, blank=True)
# Monitoring config
monitor_enabled = models.BooleanField('启用消费监控', default=True)
auto_disable_enabled = models.BooleanField('启用自动停用', default=True)
alert_threshold = models.DecimalField('告警阈值(元)', max_digits=12, decimal_places=2, null=True, blank=True,
help_text='为空则使用全局默认值')
disable_threshold = models.DecimalField('停用阈值(元)', max_digits=12, decimal_places=2, null=True, blank=True,
help_text='为空则使用全局默认值')
# Spending cache
current_month_spending = models.DecimalField('本月消费(元)', max_digits=12, decimal_places=2, default=0)
spending_updated_at = models.DateTimeField('消费更新时间', null=True, blank=True)
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'IAM 子账号'
verbose_name_plural = 'IAM 子账号'
db_table = 'airgate_iam_user'
unique_together = [('volc_account', 'username')]
def __str__(self):
return f"{self.display_name or self.username} ({self.status})"
def get_alert_threshold(self):
if self.alert_threshold is not None:
return self.alert_threshold
config = GlobalConfig.get_solo()
return config.default_alert_threshold
def get_disable_threshold(self):
if self.disable_threshold is not None:
return self.disable_threshold
config = GlobalConfig.get_solo()
return config.default_disable_threshold
class GlobalConfig(models.Model):
"""全局配置(单例)"""
default_alert_threshold = models.DecimalField('默认告警阈值(元)', max_digits=12, decimal_places=2, default=1000)
default_disable_threshold = models.DecimalField('默认停用阈值(元)', max_digits=12, decimal_places=2, default=5000)
monitor_interval_seconds = models.IntegerField('监控间隔(秒)', default=3600)
feishu_webhook_url = models.URLField('飞书 Webhook URL', max_length=500, blank=True)
feishu_alert_mobiles = models.CharField('飞书通知手机号(逗号分隔)', max_length=500, blank=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '全局配置'
verbose_name_plural = '全局配置'
db_table = 'airgate_global_config'
@classmethod
def get_solo(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj
def __str__(self):
return '全局配置'
class AlertRecord(models.Model):
"""告警记录"""
class AlertType(models.TextChoices):
WARNING = 'warning', '告警'
DISABLE = 'disable', '自动停用'
ERROR = 'error', '错误'
MANUAL = 'manual', '手动操作'
iam_user = models.ForeignKey(IAMUser, on_delete=models.CASCADE, related_name='alerts', null=True, blank=True)
alert_type = models.CharField('告警类型', max_length=20, choices=AlertType.choices)
title = models.CharField('标题', max_length=200)
content = models.TextField('详情')
spending_amount = models.DecimalField('触发时消费金额', max_digits=12, decimal_places=2, null=True, blank=True)
threshold_amount = models.DecimalField('触发阈值', max_digits=12, decimal_places=2, null=True, blank=True)
notified = models.BooleanField('已通知', default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '告警记录'
verbose_name_plural = '告警记录'
db_table = 'airgate_alert_record'
ordering = ['-created_at']
def __str__(self):
return f"[{self.alert_type}] {self.title}"
class SpendingRecord(models.Model):
"""月度消费快照"""
iam_user = models.ForeignKey(IAMUser, on_delete=models.CASCADE, related_name='spending_records')
bill_period = models.CharField('账期 (YYYY-MM)', max_length=7, db_index=True)
amount = models.DecimalField('消费金额(元)', max_digits=12, decimal_places=2, default=0)
detail = models.JSONField('消费明细', default=dict, blank=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '消费记录'
verbose_name_plural = '消费记录'
db_table = 'airgate_spending_record'
unique_together = [('iam_user', 'bill_period')]
def __str__(self):
return f"{self.iam_user.username} {self.bill_period}: ¥{self.amount}"