All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m31s
- GenerationRecord 新增 completed_at 字段,任务完成/失败时记录时间 - 超管/团管/个人消费记录 API 返回 completed_at - RecordsPage、TeamRecordsPage 表格新增"耗时"列 - CSV 导出包含耗时字段 - 历史记录 completed_at 为空显示"-" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
158 lines
8.3 KiB
Python
158 lines
8.3 KiB
Python
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')
|
||
error_message = 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='已收藏')
|
||
seed = models.BigIntegerField(default=-1, verbose_name='种子值')
|
||
created_at = models.DateTimeField(auto_now_add=True, db_index=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)')
|
||
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', '失败'),
|
||
]
|
||
|
||
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')
|
||
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}'
|