from django.db import models from apps.common.models import TimeStampedModel class CreditAccount(TimeStampedModel): team = models.OneToOneField("accounts.Team", on_delete=models.CASCADE, related_name="credit_account") balance = models.DecimalField(max_digits=14, decimal_places=4, default=0) reserved_balance = models.DecimalField(max_digits=14, decimal_places=4, default=0) currency = models.CharField(max_length=16, default="CNY") def __str__(self) -> str: return f"{self.team} / {self.balance}" class CreditLedger(TimeStampedModel): class Type(models.TextChoices): RECHARGE = "recharge", "Recharge" RESERVE = "reserve", "Reserve" RELEASE = "release", "Release" CHARGE = "charge", "Charge" ADJUSTMENT = "adjustment", "Adjustment" REFUND = "refund", "Refund" team = models.ForeignKey("accounts.Team", on_delete=models.CASCADE, related_name="credit_ledgers") user = models.ForeignKey("accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="credit_ledgers") project = models.ForeignKey( "projects.Project", on_delete=models.SET_NULL, null=True, blank=True, related_name="credit_ledgers", ) task = models.ForeignKey("ai.AITask", on_delete=models.SET_NULL, null=True, blank=True, related_name="credit_ledgers") ledger_type = models.CharField(max_length=32, choices=Type.choices) amount = models.DecimalField(max_digits=14, decimal_places=4) balance_after = models.DecimalField(max_digits=14, decimal_places=4) reason = models.CharField(max_length=255, blank=True) metadata = models.JSONField(default=dict, blank=True) class Meta: indexes = [ # 团队流水倒序翻页 + 按类型过滤(账户消费页 / 消息中心 charge 通知) models.Index(fields=["team", "-created_at"]), models.Index(fields=["team", "ledger_type", "created_at"]), models.Index(fields=["project", "task"]), ] class CreditReservation(TimeStampedModel): class Status(models.TextChoices): ACTIVE = "active", "Active" RELEASED = "released", "Released" CHARGED = "charged", "Charged" CANCELLED = "cancelled", "Cancelled" team = models.ForeignKey("accounts.Team", on_delete=models.CASCADE, related_name="credit_reservations") user = models.ForeignKey("accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="credit_reservations") project = models.ForeignKey( "projects.Project", on_delete=models.SET_NULL, null=True, blank=True, related_name="credit_reservations", ) task = models.OneToOneField("ai.AITask", on_delete=models.CASCADE, related_name="credit_reservation") amount = models.DecimalField(max_digits=14, decimal_places=4) status = models.CharField(max_length=32, choices=Status.choices, default=Status.ACTIVE) expires_at = models.DateTimeField(null=True, blank=True) class BillingConfig(TimeStampedModel): """平台计费全局配置 · 单行单例(pk 恒定,经 pricing.get_billing_config() 缓存读取)。 积分制核心参数:1 积分 = 1/points_per_yuan 元(默认 ¥0.1);用户侧余额/价格/流水全用积分, 平台成本(base_cost)仍以 ¥ 记账。视频类用户价 = 火山成本价 × video_margin_multiplier 换积分 (首发 ×1.5,商业决策;adminpanel 可调,改动即刻生效于下一次估价/结算)。 """ points_per_yuan = models.DecimalField(max_digits=8, decimal_places=2, default=10) video_margin_multiplier = models.DecimalField(max_digits=6, decimal_places=2, default=1.50) # 视频预留 buffer(收编 free_video.RESERVE_BUFFER):预留=预估积分×buffer,应对真实 tokens 略超预估 video_reserve_buffer = models.DecimalField(max_digits=4, decimal_places=2, default=1.10) def __str__(self) -> str: return f"1¥={self.points_per_yuan}积分 · 视频毛利×{self.video_margin_multiplier}" class QuotaPolicy(TimeStampedModel): team = models.ForeignKey("accounts.Team", on_delete=models.CASCADE, related_name="quota_policies") user = models.ForeignKey("accounts.User", on_delete=models.CASCADE, null=True, blank=True, related_name="quota_policies") project = models.ForeignKey( "projects.Project", on_delete=models.CASCADE, null=True, blank=True, related_name="quota_policies", ) monthly_limit = models.DecimalField(max_digits=14, decimal_places=4, null=True, blank=True) project_limit = models.DecimalField(max_digits=14, decimal_places=4, null=True, blank=True) per_task_limit = models.DecimalField(max_digits=14, decimal_places=4, null=True, blank=True) is_active = models.BooleanField(default=True)