Files
yingqing/core/qa/audit_billing.py
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

179 lines
9.2 KiB
Python

"""资金核算审计:针对真实 DB,逐条验证账目不变量,任何不平即报。
跑法:cd backend && DJANGO_SETTINGS_MODULE=airshelf.settings.development .venv/bin/python ../qa/audit_billing.py
不变量:
I1 余额自洽 balance == Σ(recharge+refund) - Σ(charge) + Σ(adjustment 带符号)
I2 冻结自洽 reserved_balance == Σ(amount of ACTIVE reservations)
I3 非负 & 可用 balance>=0, reserved>=0, balance>=reserved(可用余额>=0)
I4 预留闭环 每个 reservation 状态与其 ledger 一致;CHARGED 恰好 1 条 charge 且 amount<=预留
I5 失败不扣费 失败 task 的 reservation 不得为 CHARGED
I6 无重复扣费 同一 task 不得有 >1 条 charge ledger
I7 流水连续 按时间重放,recharge/charge 的 balance_after 与重放值一致
"""
import os, sys
from decimal import Decimal
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "airshelf.settings.development")
import django # noqa: E402
django.setup()
from django.db.models import Sum # noqa: E402
from apps.billing.models import CreditAccount, CreditLedger, CreditReservation # noqa: E402
from apps.ai.models import AITask # noqa: E402
Z = Decimal("0")
problems = []
def flag(account, code, msg):
problems.append((str(account.team), code, msg))
print(f" ✗ [{code}] {msg}")
def audit_account(acct):
team = acct.team
led = CreditLedger.objects.filter(team=team)
print(f"\n== team={team} | balance={acct.balance} reserved={acct.reserved_balance} ==")
def s(t):
return led.filter(ledger_type=t).aggregate(x=Sum("amount"))["x"] or Z
recharge, charge, refund, adjustment = s("recharge"), s("charge"), s("refund"), s("adjustment")
# I1 余额=流水终点:account.balance 应等于最后一条动-balance 流水的 balance_after(不依赖开户 genesis)
last_bal_led = led.filter(ledger_type__in=["recharge", "refund", "charge", "adjustment"]).order_by("created_at", "id").last()
if last_bal_led is not None:
if acct.balance != last_bal_led.balance_after:
flag(acct, "I1", f"账户余额与流水终点脱节:account.balance={acct.balance} 但末条动账流水 balance_after={last_bal_led.balance_after}")
else:
print(f" ✓ I1 余额=流水终点 {acct.balance}")
else:
print(f" · I1 无动账流水(纯开户额度 {acct.balance})")
# I2 冻结自洽
active_sum = CreditReservation.objects.filter(team=team, status="active").aggregate(x=Sum("amount"))["x"] or Z
if acct.reserved_balance != active_sum:
flag(acct, "I2", f"冻结不平:account.reserved_balance={acct.reserved_balance} 但 ACTIVE 预留之和={active_sum}(差 {acct.reserved_balance - active_sum})")
else:
print(f" ✓ I2 冻结自洽 ACTIVE 预留之和={active_sum}")
# I3 非负 & 可用
if acct.balance < Z:
flag(acct, "I3", f"余额为负 {acct.balance}")
if acct.reserved_balance < Z:
flag(acct, "I3", f"冻结为负 {acct.reserved_balance}")
if acct.balance < acct.reserved_balance:
flag(acct, "I3", f"可用余额为负:balance {acct.balance} < reserved {acct.reserved_balance}")
if acct.balance >= Z and acct.reserved_balance >= Z and acct.balance >= acct.reserved_balance:
print(f" ✓ I3 非负&可用余额>=0(可用 {acct.balance - acct.reserved_balance})")
# I4/I5/I6 预留闭环 + 失败不扣 + 无重复扣
i4 = i5 = i6 = True
for r in CreditReservation.objects.filter(team=team).select_related("task"):
charges = led.filter(task=r.task, ledger_type="charge")
charge_sum = charges.aggregate(x=Sum("amount"))["x"] or Z
adj = led.filter(task=r.task, ledger_type="adjustment").aggregate(x=Sum("amount"))["x"] or Z
net_charge = charge_sum - adj # 扣费扣除回冲后的净额
dup_reconciled = charges.count() > 1 and net_charge == r.amount
if charges.count() > 1:
if dup_reconciled:
print(f" · I6 task={str(r.task_id)[:8]} 历史双扣 {charges.count()} 笔,已被 adjustment 回冲(净扣 {net_charge} = 预留 {r.amount},账平)")
else:
i6 = False
flag(acct, "I6", f"task={str(r.task_id)[:8]} {charges.count()} 条扣费,净扣 {net_charge} != 预留 {r.amount}(未完全回冲,真多扣)")
if r.status == "charged":
if charges.count() != 1 and not dup_reconciled:
i4 = False
flag(acct, "I4", f"reservation(task={str(r.task_id)[:8]}) 状态 CHARGED 却有 {charges.count()} 条扣费")
elif net_charge > r.amount:
i4 = False
flag(acct, "I4", f"净扣费 {net_charge} > 预留 {r.amount}(task={str(r.task_id)[:8]})")
elif r.status == "active":
if charges.exists():
i4 = False
flag(acct, "I4", f"reservation ACTIVE 却已有扣费(task={str(r.task_id)[:8]})")
# I5 失败不扣费
task = r.task
if task and task.status in ("failed", "cancelled") and r.status == "charged":
i5 = False
flag(acct, "I5", f"失败/取消 task={str(r.task_id)[:8]}(status={task.status})却被扣费")
if i4:
print(" ✓ I4 预留状态与扣费/释放流水一致")
if i5:
print(" ✓ I5 失败/取消任务均未扣费")
if i6:
print(" ✓ I6 无重复扣费")
# I7 逐笔差分自洽(不依赖开户起点):每条动账流水的 balance_after 必须 = 前一条动账流水的 balance_after ± 本笔金额。
# reserve/release 不动 balance,其 balance_after 应 = 当时余额快照(上一条动账流水的 balance_after)。
i7 = True
prev_bal = None # 上一条动账后的余额
for L in led.order_by("created_at", "id"):
if L.ledger_type in ("recharge", "refund", "adjustment"):
expected = (prev_bal if prev_bal is not None else Z) + L.amount
if prev_bal is not None and L.balance_after != expected:
i7 = False
flag(acct, "I7", f"差分不符 {L.ledger_type}@{str(L.created_at)[:19]} amount={L.amount} 记录={L.balance_after} 应为前值{prev_bal}+{L.amount}={expected}")
prev_bal = L.balance_after
elif L.ledger_type == "charge":
if prev_bal is not None:
expected = prev_bal - L.amount
if L.balance_after != expected:
i7 = False
flag(acct, "I7", f"差分不符 charge@{str(L.created_at)[:19]} amount={L.amount} 记录={L.balance_after} 应为前值{prev_bal}-{L.amount}={expected}")
prev_bal = L.balance_after
else: # reserve / release:不动 balance,快照应等于上一条动账后的余额
if prev_bal is not None and L.balance_after != prev_bal:
i7 = False
flag(acct, "I7", f"reserve/release 余额快照漂移@{str(L.created_at)[:19]} 记录={L.balance_after} 应={prev_bal}")
if i7:
print(" ✓ I7 逐笔差分自洽(每条 balance_after = 前一条 ± 本笔金额)")
# I8 流水完整性(可审计):首条流水之前的隐含余额应为 0(开户额度也应有 genesis 流水)
first = led.order_by("created_at", "id").first()
if first is not None:
delta = first.amount if first.ledger_type in ("recharge", "refund", "adjustment") else (-first.amount if first.ledger_type == "charge" else Z)
opening = first.balance_after - delta
if opening != Z:
flag(acct, "I8", f"开户额度无流水凭证:首条流水前隐含余额={opening}(应为 0,缺 genesis 赠送流水)")
else:
print(" ✓ I8 流水完整(开户额度有凭证)")
# I9 毛利审计(积分制新增,先 warning 不计入失败):succeeded 且 base_cost>0(成本已知)的任务,
# 用户实收 actual_cost/points_per_yuan(¥)不应低于平台成本 base_cost —— 低于即「卖亏」。
# 注:充值赠送积分会稀释实际实现汇率(paid/credited < 名义 0.1),此处按名义汇率近似。
from apps.billing.pricing import get_billing_config
rate = get_billing_config().points_per_yuan
loss_tasks = []
for t in AITask.objects.filter(team=team, status="succeeded", base_cost__gt=0).only("id", "task_type", "actual_cost", "base_cost"):
revenue_yuan = (t.actual_cost or Z) / rate
if revenue_yuan < t.base_cost:
loss_tasks.append((t, revenue_yuan))
if loss_tasks:
for t, revenue_yuan in loss_tasks[:5]:
print(f" ⚠ I9 卖亏任务(warning) {t.task_type}:{t.id} 实收¥{revenue_yuan:.2f} < 成本¥{t.base_cost}")
if len(loss_tasks) > 5:
print(f" ⚠ I9 …另有 {len(loss_tasks) - 5} 条卖亏任务")
else:
print(" ✓ I9 无卖亏任务(base_cost>0 口径)")
def main():
accts = CreditAccount.objects.select_related("team").all()
print(f"审计 {accts.count()} 个团队账户…")
for a in accts:
audit_account(a)
print("\n" + "=" * 60)
if problems:
print(f"发现 {len(problems)} 处账目问题:")
for team, code, msg in problems:
print(f" [{code}] {team}: {msg}")
sys.exit(1)
print("✓ 全部账户账目自洽,无金额核算错误")
if __name__ == "__main__":
main()