- 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>
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""消费查询服务"""
|
||
|
||
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", {})
|