from decimal import Decimal 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) 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) # --- 额度管理(划拨制) --- allocated_quota = models.DecimalField('已划拨额度(元)', max_digits=12, decimal_places=2, default=0, help_text='主账号累计划拨给此子账号的总额度') consumed_total = models.DecimalField('累计消费(元)', max_digits=12, decimal_places=2, default=0, help_text='从 Billing API 获取的累计消费总额') spending_updated_at = models.DateTimeField('消费更新时间', null=True, blank=True) # --- 监控配置 --- monitor_enabled = models.BooleanField('启用消费监控', default=True) alert_thresholds = models.JSONField('告警阈值(百分比列表)', default=list, blank=True, help_text='如 [50, 80, 90] 表示消费达到额度的 50%/80%/90% 时告警') auto_disable_enabled = models.BooleanField('额度用尽自动停用', default=True, help_text='消费达到已划拨额度 100% 时自动停用') triggered_alerts = models.JSONField('已触发的告警阈值', default=list, blank=True, help_text='记录已通知过的百分比,划拨新额度时自动重置') # --- 停用时保存的策略快照(恢复时自动加回) --- saved_policies_on_disable = models.JSONField('停用时保存的策略', default=list, blank=True, help_text='停用时自动移除的策略列表,恢复时加回') 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})" @property def remaining_quota(self): """剩余额度""" return max(Decimal('0'), self.allocated_quota - self.consumed_total) @property def usage_percent(self): """额度使用率""" if self.allocated_quota <= 0: return 0 return round(float(self.consumed_total) / float(self.allocated_quota) * 100, 1) def get_alert_thresholds(self): if self.alert_thresholds: return sorted(self.alert_thresholds) config = GlobalConfig.get_solo() return sorted(config.default_alert_thresholds or [50, 80, 90]) class IAMUserProject(models.Model): """子账号关联的火山项目(多对多)""" iam_user = models.ForeignKey(IAMUser, on_delete=models.CASCADE, related_name='projects') project_name = models.CharField('火山项目名', max_length=200) display_name = models.CharField('显示名', max_length=200, blank=True) monitor_enabled = models.BooleanField('启用监测', default=True) current_spending = models.DecimalField('当前消费(元)', max_digits=12, decimal_places=2, default=0, help_text='此项目的累计消费,由定时任务更新') attached_policies = models.JSONField('已授权的策略列表', default=list, blank=True, help_text='添加项目时自动附加的策略名列表,移除时自动回收') created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '子账号关联项目' verbose_name_plural = '子账号关联项目' db_table = 'airgate_iam_user_project' unique_together = [('iam_user', 'project_name')] ordering = ['project_name'] def __str__(self): status = '监测中' if self.monitor_enabled else '未监测' return f"{self.project_name} ({status}) ¥{self.current_spending}" class QuotaAllocation(models.Model): """额度划拨记录""" iam_user = models.ForeignKey(IAMUser, on_delete=models.CASCADE, related_name='quota_allocations') amount = models.DecimalField('变更金额(元,正=追加,负=扣减)', max_digits=12, decimal_places=2) total_after = models.DecimalField('划拨后总额度', max_digits=12, decimal_places=2) note = models.CharField('备注', max_length=500, blank=True) created_by = models.CharField('操作人', max_length=100, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '额度划拨记录' verbose_name_plural = '额度划拨记录' db_table = 'airgate_quota_allocation' ordering = ['-created_at'] def __str__(self): return f"{self.iam_user.username} +¥{self.amount} → ¥{self.total_after}" class GlobalConfig(models.Model): """全局配置(单例)""" default_alert_thresholds = models.JSONField('默认告警阈值(百分比列表)', default=list, blank=True, help_text='如 [50, 80, 90]') default_project_policies = models.JSONField('添加项目时自动授权的策略', default=list, blank=True, help_text='如 ["ArkFullAccess", "TOSFullAccess"]') 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') project_name = models.CharField('项目名', max_length=200, blank=True, help_text='空=子账号总消费') bill_period = models.CharField('账期 (YYYY-MM)', max_length=7, db_index=True) amount = models.DecimalField('消费金额(元)', max_digits=12, decimal_places=2, default=0) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = '消费记录' verbose_name_plural = '消费记录' db_table = 'airgate_spending_record' unique_together = [('iam_user', 'project_name', 'bill_period')] def __str__(self): proj = self.project_name or '总计' return f"{self.iam_user.username} [{proj}] {self.bill_period}: ¥{self.amount}"