"""资金核算审计:针对真实 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()