diff --git a/backend/utils/billing_service.py b/backend/utils/billing_service.py index 20d3fcf..fc7731c 100644 --- a/backend/utils/billing_service.py +++ b/backend/utils/billing_service.py @@ -8,16 +8,21 @@ logger = logging.getLogger(__name__) class BillingService: - """封装火山引擎 Billing API""" + """封装火山引擎 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 = 300 + page_size = 100 while True: params = { @@ -25,25 +30,52 @@ class BillingService: "Limit": str(page_size), "Offset": str(offset), "GroupTerm": "0", - "GroupPeriod": "0", - "NeedRecordNum": "1", + "GroupPeriod": "2", } - result = self.client.call("ListBillDetail", params) + result = self.client.call("ListSplitBillDetail", params) items = result.get("Result", {}).get("List", []) - record_num = int(result.get("Result", {}).get("Total", 0)) for item in items: - if project_name and item.get("Project") != project_name: + item_project = item.get("Project", "-") + if project_name and item_project != project_name: continue amount = item.get("PayableAmount", "0") total += Decimal(str(amount)) - offset += page_size - if offset >= record_num or not items: + 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", { diff --git a/backend/utils/scheduler.py b/backend/utils/scheduler.py index 4ec0b62..5398ad7 100644 --- a/backend/utils/scheduler.py +++ b/backend/utils/scheduler.py @@ -31,6 +31,13 @@ def check_spending(): billing = BillingService(ak, sk) iam_svc = IAMService(ak, sk) + # 一次性查询所有项目的消费(避免 N+1 API 调用) + try: + all_project_spending = billing.get_spending_all_projects(bill_period) + except Exception as e: + logger.error(f"批量查询消费失败: {e}") + all_project_spending = {} + users = IAMUser.objects.filter( volc_account=volc_account, monitor_enabled=True, @@ -38,7 +45,7 @@ def check_spending(): for user in users: try: - # --- 遍历所有开启监测的项目,分别查询消费并累加 --- + # --- 遍历所有开启监测的项目,从批量结果中获取消费 --- enabled_projects = IAMUserProject.objects.filter( iam_user=user, monitor_enabled=True ) @@ -50,13 +57,9 @@ def check_spending(): total_spending = Decimal('0') for project in enabled_projects: - try: - proj_spending = billing.get_spending_by_project( - bill_period, project.project_name - ) - except Exception as e: - logger.error(f"查询项目 {project.project_name} 消费失败: {e}") - proj_spending = project.current_spending # 保留上次值 + proj_spending = all_project_spending.get( + project.project_name, project.current_spending + ) # 更新项目级消费 project.current_spending = proj_spending diff --git a/火山引擎IAM子账号管控工具_深度研究报告.md b/火山引擎IAM子账号管控工具_深度研究报告.md index 4e0c58c..7402ae0 100644 --- a/火山引擎IAM子账号管控工具_深度研究报告.md +++ b/火山引擎IAM子账号管控工具_深度研究报告.md @@ -546,7 +546,22 @@ iam_client.call("UpdateAccessKey", { - 管理员可按需开关某些项目的监测(如测试项目不计费) - 告警和自动停用基于所有开启项目的消费总和 vs 划拨额度 -**消费查询方式:** 对每个开启监测的项目分别调用 `ListBillDetail`(按 Project 字段筛选),累加得出总消费。同时记录每个项目的独立消费,前端可展开查看明细。 +**消费查询方式:** 使用 `ListSplitBillDetail`(分账账单)接口,按 Project 字段汇总。 + +> ⚠️ **重要发现(2026-03-29 实测)**: +> - `ListBillDetail`(账单明细)的 Project 字段对 Seedance 等按量付费产品显示为 `-`,**按项目筛选不准确** +> - `ListSplitBillDetail`(分账账单)的 Project 字段能正确归属到项目,**与火山控制台分账账单页面一致** +> - AirGate 已切换为使用 `ListSplitBillDetail` 查询消费 + +**ListSplitBillDetail 验证结果(2026-03):** + +| 项目 | API 查询 | 火山控制台 | +|------|---------|----------| +| int_dev_Airlabs | ¥24,058.78 | ¥24,014.14 | +| HAGOOT_DEV | ¥40.01 | ¥40.01 | +| zyc_test | ¥124.08 | - | + +差异为查询时间点不同导致,数据一致。 ### 6.4 账户余额查询