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>
This commit is contained in:
zyc
2026-07-07 10:04:00 +08:00
co-authored by Claude Sonnet 5
parent c1420316c2
commit bf20de956c
46 changed files with 1490 additions and 150 deletions
+31 -6
View File
@@ -34,20 +34,44 @@ def _enforce_member_monthly_limit(*, team, user, amount: Decimal) -> None:
day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
charged = _charged_since(day_start)
if charged + reserved + amount > day_limit:
raise ValueError(f"成员今日额度不足:每日限额 ¥{day_limit},今日已用 ¥{charged}(另有在途 ¥{reserved})")
raise ValueError(f"成员今日额度不足:每日限额 {day_limit} 积分,今日已用 {charged}(另有在途 {reserved})")
month_limit = member.monthly_credit_limit or Decimal("0")
if month_limit > 0:
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
charged = _charged_since(month_start)
if charged + reserved + amount > month_limit:
raise ValueError(f"成员本月额度不足:限额 ¥{month_limit},本月已用 ¥{charged}(另有在途 ¥{reserved})")
raise ValueError(f"成员本月额度不足:限额 {month_limit} 积分,本月已用 {charged}(另有在途 {reserved})")
total_limit = member.total_credit_limit or Decimal("0")
if total_limit > 0:
charged = _charged_since(None)
if charged + reserved + amount > total_limit:
raise ValueError(f"成员累计额度不足:总额度 ¥{total_limit},累计已用 ¥{charged}(另有在途 ¥{reserved})")
raise ValueError(f"成员累计额度不足:总额度 {total_limit} 积分,累计已用 {charged}(另有在途 {reserved})")
def _enforce_team_monthly_limit(*, team, amount: Decimal) -> None:
"""Team.monthly_credit_limit 真管控(此前是装饰性字段:团队页可配,reserve 从不读 → 形同虚设)。
语义:None/-1/0 均不管控,仅正数为硬上限 —— 0 必须视为「未设置/不限」而非「冻结」:
前端把 null 映射成 0 展示、成员限额也是 0=不限,存量数据里存在 0 行,若 0=冻结会在
部署当天把这些团队全体静默冻结(review 确认的语义冲突)。冻结消费请用 QuotaPolicy(monthly=0)。
口径与成员/QuotaPolicy 月度一致:自然月团队 CHARGE 合计 + 团队全部 ACTIVE 预留 + 本次。
调用方(reserve_credit)已持 account 行锁,团队内 reserve 串行化,无竞态。"""
limit = team.monthly_credit_limit
if limit is None or limit <= 0:
return
month_start = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
charged = (
CreditLedger.objects.filter(team=team, ledger_type=CreditLedger.Type.CHARGE, created_at__gte=month_start)
.aggregate(s=Sum("amount"))["s"] or Decimal("0")
)
reserved = (
CreditReservation.objects.filter(team=team, status=CreditReservation.Status.ACTIVE)
.aggregate(s=Sum("amount"))["s"] or Decimal("0")
)
if charged + reserved + amount > limit:
raise ValueError(f"团队本月额度不足:限额 {limit} 积分,本月已用 {charged}(另有在途 {reserved})")
def _enforce_quota_policy(*, team, project, amount: Decimal) -> None:
@@ -61,7 +85,7 @@ def _enforce_quota_policy(*, team, project, amount: Decimal) -> None:
if policy is None:
return
if policy.per_task_limit is not None and amount > policy.per_task_limit:
raise ValueError(f"单任务额度超限:上限 ¥{policy.per_task_limit}")
raise ValueError(f"单任务额度超限:上限 {policy.per_task_limit} 积分")
now = timezone.now()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if policy.monthly_limit is not None:
@@ -74,7 +98,7 @@ def _enforce_quota_policy(*, team, project, amount: Decimal) -> None:
.aggregate(s=Sum("amount"))["s"] or Decimal("0")
)
if charged + reserved + amount > policy.monthly_limit:
raise ValueError(f"团队本月额度超限:上限 ¥{policy.monthly_limit}")
raise ValueError(f"团队本月额度超限:上限 {policy.monthly_limit} 积分")
if policy.project_limit is not None and project is not None:
p_charged = (
CreditLedger.objects.filter(team=team, project=project, ledger_type=CreditLedger.Type.CHARGE)
@@ -85,7 +109,7 @@ def _enforce_quota_policy(*, team, project, amount: Decimal) -> None:
.aggregate(s=Sum("amount"))["s"] or Decimal("0")
)
if p_charged + p_reserved + amount > policy.project_limit:
raise ValueError(f"项目额度超限:上限 ¥{policy.project_limit}")
raise ValueError(f"项目额度超限:上限 {policy.project_limit} 积分")
@transaction.atomic
@@ -116,6 +140,7 @@ def reserve_credit(*, team, user, task, amount: Decimal) -> CreditReservation:
if available < amount:
raise ValueError("insufficient credit")
_enforce_member_monthly_limit(team=team, user=user, amount=amount)
_enforce_team_monthly_limit(team=team, amount=amount)
_enforce_quota_policy(team=team, project=task.project, amount=amount)
account.reserved_balance += amount