## 计费体系 - 团队额度从秒数改为金额(余额/冻结/月消费上限) - 用户限额从秒数改为次数(每日50次/每月1500次) - 新增 billing.py 工具模块(分辨率→像素映射 + token/费用计算) - 扣费流程:预扣制→冻结制(提交冻结预估金额,完成按实际tokens扣费,失败释放) - 允许小额透支(实际费用超预估时余额可变负) - 团队加价比例(markup_percentage),创建团队时必填 ## Token 追踪 - GenerationRecord 新增 tokens_consumed/cost_amount/base_cost_amount - 任务完成时从 Seedance API usage.total_tokens 获取精确值 - 生成页显示预估消耗(tokens + 金额),按团队售价计算 ## 管理后台 - 仪表盘新增利润分析板块(总收入/成本/利润/利润率 + 团队利润排行) - 消费记录新增 Tokens/售价/成本/利润列 - 团队管理:充值改为充金额,新增加价比例设置 - 系统设置:默认限额改为次数,新增基础token单价配置 ## Bug 修复 - 登录弹窗:拖选输入框内容不再误关闭(onClick→mousedown+mouseup) - 视频详情弹窗:遮罩层覆盖全视口(left:76px→0),admin/团管侧栏不再露出 ## UI 增强 - 图片大图预览:上传区和视频详情弹窗的图片支持点击查看大图(ImageLightbox) - 移除 adaptive 比例和智能时长选项,确保 token 预估可精确计算 - 视频详情弹窗显示实际消耗 tokens 和费用 ## 前端全量更新 - 所有页面秒数显示替换为金额(元)和次数(次) - TypeScript 类型全量更新 - API 调用参数同步更新 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
5.9 KiB
Python
102 lines
5.9 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='参考素材信息')
|
||
created_at = models.DateTimeField(auto_now_add=True, db_index=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/月'
|