AirGate/backend/utils/billing_service.py
seaislee1209 610058ae5f feat: switch billing to ListSplitBillDetail for accurate project spending
- BillingService now uses ListSplitBillDetail (split bill) instead of
  ListBillDetail (bill detail) - the latter shows Project='-' for
  Seedance pay-as-you-go products
- Added get_spending_all_projects() for batch query (avoids N+1 API calls)
- Scheduler optimized: single API call fetches all project spending
- Verified: amounts match Volcengine console split bill page exactly
- Updated research report with billing API findings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:08 +08:00

92 lines
3.0 KiB
Python
Raw Permalink 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.

"""消费查询服务"""
import logging
from decimal import Decimal
from .volcengine_client import get_billing_client
logger = logging.getLogger(__name__)
class BillingService:
"""封装火山引擎 Billing API
使用 ListSplitBillDetail分账账单而非 ListBillDetail账单明细
因为后者的 Project 字段对 Seedance 等按量付费产品显示为 '-',不准确。
分账账单能正确按项目归属消费,与火山控制台分账账单页面一致。
"""
def __init__(self, ak: str, sk: str):
self.client = get_billing_client(ak, sk)
def get_spending_by_project(self, bill_period: str, project_name: str = None) -> Decimal:
"""查询指定项目的消费总额(使用分账账单,带分页)"""
total = Decimal("0")
offset = 0
page_size = 100
while True:
params = {
"BillPeriod": bill_period,
"Limit": str(page_size),
"Offset": str(offset),
"GroupTerm": "0",
"GroupPeriod": "2",
}
result = self.client.call("ListSplitBillDetail", params)
items = result.get("Result", {}).get("List", [])
for item in items:
item_project = item.get("Project", "-")
if project_name and item_project != project_name:
continue
amount = item.get("PayableAmount", "0")
total += Decimal(str(amount))
if len(items) < page_size:
break
offset += page_size
return total
def get_spending_all_projects(self, bill_period: str) -> dict:
"""查询所有项目的消费汇总(返回 {project_name: Decimal}"""
by_project = {}
offset = 0
page_size = 100
while True:
params = {
"BillPeriod": bill_period,
"Limit": str(page_size),
"Offset": str(offset),
"GroupTerm": "0",
"GroupPeriod": "2",
}
result = self.client.call("ListSplitBillDetail", params)
items = result.get("Result", {}).get("List", [])
for item in items:
project = item.get("Project", "-")
amount = Decimal(str(item.get("PayableAmount", "0")))
by_project[project] = by_project.get(project, Decimal("0")) + amount
if len(items) < page_size:
break
offset += page_size
return by_project
def get_bill_overview(self, bill_period: str) -> dict:
"""获取账单总览(按产品维度)"""
result = self.client.call("ListBillOverviewByProd", {
"BillPeriod": bill_period,
"Limit": "100",
"NeedRecordNum": "1",
})
return result.get("Result", {})
def get_balance(self) -> dict:
"""查询主账号余额"""
result = self.client.call("QueryBalanceAcct")
return result.get("Result", {})