import uuid from django.db import models from django.conf import settings class GenerationRecord(models.Model): """Video generation call record.""" MODE_CHOICES = [ ('universal', '全能参考'), ('keyframe', '首尾帧'), ] MODEL_CHOICES = [ ('seedance_2.0', 'AirDrama'), ('seedance_2.0_fast', 'AirDrama Fast'), ] STATUS_CHOICES = [ ('queued', '排队中'), ('processing', '生成中'), ('completed', '已完成'), ('failed', '失败'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='generation_records', verbose_name='用户', ) task_id = models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='任务ID') ark_task_id = models.CharField(max_length=100, blank=True, default='', verbose_name='火山ARK任务ID') prompt = models.TextField(blank=True, verbose_name='提示词') mode = models.CharField(max_length=20, choices=MODE_CHOICES, verbose_name='创作模式') model = models.CharField(max_length=30, choices=MODEL_CHOICES, verbose_name='模型') aspect_ratio = models.CharField(max_length=10, verbose_name='宽高比') duration = models.IntegerField(verbose_name='视频时长(秒)') seconds_consumed = models.FloatField(default=0, verbose_name='消费秒数') # ── 金额计费字段(v0.10.0 新增) ── tokens_consumed = models.IntegerField(default=0, verbose_name='消耗tokens') cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='用户费用(元)') base_cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='平台成本(元)') frozen_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='冻结金额(元)') resolution = models.CharField(max_length=10, blank=True, default='', verbose_name='分辨率') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued', verbose_name='状态') result_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='生成结果URL') thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='视频缩略图URL') error_message = models.TextField(blank=True, default='', verbose_name='错误信息') raw_error = models.TextField(blank=True, default='', verbose_name='原始错误信息') reference_urls = models.JSONField(default=list, blank=True, verbose_name='参考素材信息') is_favorited = models.BooleanField(default=False, verbose_name='已收藏') is_deleted = models.BooleanField(default=False, verbose_name='用户已删除') seed = models.BigIntegerField(default=-1, verbose_name='种子值') created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') completed_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') class Meta: verbose_name = '生成记录' verbose_name_plural = '生成记录' ordering = ['-created_at'] indexes = [ models.Index(fields=['user', 'created_at']), ] def __str__(self): return f'{self.user.username} - {self.task_id}' class QuotaConfig(models.Model): """Global quota configuration (singleton) — Phase 3: seconds + announcement + anomaly detection.""" default_daily_seconds_limit = models.IntegerField(default=600, verbose_name='默认每日秒数上限') default_monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='默认每月秒数上限') announcement = models.TextField(blank=True, default='', verbose_name='系统公告') announcement_enabled = models.BooleanField(default=False, verbose_name='启用公告') max_desktop_sessions = models.IntegerField(default=1, verbose_name='每用户最大桌面端会话数') max_mobile_sessions = models.IntegerField(default=0, verbose_name='每用户最大移动端会话数') # ── 异常检测全局默认配置 ── anomaly_detection_enabled = models.BooleanField(default=False, verbose_name='异常检测总开关') r1_enabled_default = models.BooleanField(default=True, verbose_name='R1 默认开关') r2_enabled_default = models.BooleanField(default=True, verbose_name='R2 默认开关') r2_window_seconds = models.IntegerField(default=3600, verbose_name='R2 默认时间窗口(秒)') r3_enabled_default = models.BooleanField(default=True, verbose_name='R3 默认开关') r3_window_seconds = models.IntegerField(default=3600, verbose_name='R3 默认时间窗口(秒)') r3_max_count = models.IntegerField(default=10, verbose_name='R3 默认最大登录次数') r4_enabled_default = models.BooleanField(default=True, verbose_name='R4 默认开关') r4_window_seconds = models.IntegerField(default=3600, verbose_name='R4 默认时间窗口(秒)') r4_city_count = models.IntegerField(default=5, verbose_name='R4 默认预期外城市数') r5_enabled_default = models.BooleanField(default=True, verbose_name='R5 默认开关') r5_days = models.IntegerField(default=7, verbose_name='R5 默认统计天数') r5_country_count = models.IntegerField(default=10, verbose_name='R5 默认海外国家数') feishu_alert_mobiles = models.CharField(max_length=500, blank=True, default='', verbose_name='飞书告警接收人手机号') sms_alert_mobiles = models.CharField(max_length=500, blank=True, default='', verbose_name='短信告警手机号(预留)') alert_cooldown_seconds = models.IntegerField(default=1800, verbose_name='告警冷却时间(秒)') # ── 计费全局配置(v0.10.0 新增) ── default_daily_generation_limit = models.IntegerField(default=50, verbose_name='默认每日生成次数') default_monthly_generation_limit = models.IntegerField(default=1500, verbose_name='默认每月生成次数') base_token_price = models.DecimalField(max_digits=10, decimal_places=2, default=46, verbose_name='基础token单价-不含视频(元/百万tokens)') base_token_price_video = models.DecimalField(max_digits=10, decimal_places=2, default=28, verbose_name='基础token单价-含视频(元/百万tokens)') base_token_price_fast = models.DecimalField(max_digits=10, decimal_places=2, default=37, verbose_name='Fast单价-不含视频(元/百万tokens)') base_token_price_fast_video = models.DecimalField(max_digits=10, decimal_places=2, default=22, verbose_name='Fast单价-含视频(元/百万tokens)') updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = '系统配置' verbose_name_plural = '系统配置' def save(self, *args, **kwargs): self.pk = 1 super().save(*args, **kwargs) def __str__(self): return f'全局配额: {self.default_daily_seconds_limit}s/日, {self.default_monthly_seconds_limit}s/月' class AssetGroup(models.Model): """虚拟人像素材组 — 一个角色对应一个组。""" team = models.ForeignKey( 'accounts.Team', on_delete=models.CASCADE, related_name='asset_groups', verbose_name='所属团队', ) remote_group_id = models.CharField(max_length=100, default='', verbose_name='火山Group ID') name = models.CharField(max_length=100, default='', verbose_name='角色名') description = models.CharField(max_length=300, blank=True, default='', verbose_name='描述') thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='缩略图URL') created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_asset_groups', verbose_name='创建人', ) created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '素材组' verbose_name_plural = '素材组' ordering = ['-created_at'] def __str__(self): return f'{self.team.name} - {self.name}' class Asset(models.Model): """虚拟人像素材 — 图片/视频/音频。""" STATUS_CHOICES = [ ('processing', '处理中'), ('active', '可用'), ('failed', '失败'), ] ASSET_TYPE_CHOICES = [ ('Image', '图像'), ('Video', '视频'), ('Audio', '音频'), ] group = models.ForeignKey( AssetGroup, on_delete=models.CASCADE, related_name='assets', verbose_name='所属素材组', ) remote_asset_id = models.CharField(max_length=100, default='', verbose_name='火山Asset ID') name = models.CharField(max_length=100, default='', verbose_name='素材名称') url = models.CharField(max_length=1000, blank=True, default='', verbose_name='素材URL') asset_type = models.CharField(max_length=10, choices=ASSET_TYPE_CHOICES, default='Image', verbose_name='素材类型') thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='缩略图URL') duration = models.FloatField(default=0, verbose_name='时长(秒)') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='processing', verbose_name='状态') error_message = models.CharField(max_length=500, blank=True, default='', verbose_name='错误信息') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '素材' verbose_name_plural = '素材' ordering = ['-created_at'] def __str__(self): return f'{self.group.name} - {self.name}'