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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user