Files
yingqing/core/backend/apps/billing/models.py
T
zycandClaude Sonnet 5 bf20de956c feat(billing): 计费商业化重构(积分制)+ 团队差异化调价
统一计价引擎 apps/billing/pricing.py:1积分=¥0.1,平台成本(¥)与用户价(积分)双记账,
视频按火山真实 usage.total_tokens 结算(true-up,终结¥1/段倒贴)+ 首发×1.5毛利系数。
Team.price_multiplier 差异化调价(jimeng同款):挂牌价×系数两步HALF_UP取整,视频类
按下单时的价格/汇率快照结算(中途改配不影响在途任务)。

BillingConfig 单例(汇率/毛利系数/预留buffer,admin可调即刻生效)。存量数据 ×10 rescale
迁移(RunPython+atomic,MySQL 迁移不可中断重跑)。开户赠送归零(DEFAULT_TRIAL_CREDITS=0,
商业决策)。audit_billing 加 I9(卖亏审计)。271 条测试 + tsc/build 全绿。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 10:04:00 +08:00

104 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)