Files
aiops/backend/cloud/tencent_billing.go
Logan.Li dd3770a66c fix: 恢复腾讯云账单同步并完善成本预测
- 接入腾讯云单日账单、全局限频和指数退避

- 修正月末费用预测并增加账期提示和费用归因

- 补充腾讯云限频及账单归因回归测试
2026-07-29 05:44:40 +08:00

137 lines
4.2 KiB
Go
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.
package cloud
import (
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"strings"
)
// FetchDailyBill 拉取指定日期的腾讯云日账单,并按产品和地域汇总。 // Logan.Li
func (p *TencentProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) {
if len(billingDate) != 10 {
return nil, fmt.Errorf("账单日期格式应为 YYYY-MM-DD")
}
type aggregationKey struct {
service string
region string
}
aggregation := map[aggregationKey]float64{}
const pageSize = 100
for offset := 0; ; offset += pageSize {
// 腾讯云账单明细仅按账期查询,不支持按日参数。 // Logan.Li
body, err := p.callService("billing", "billing.tencentcloudapi.com", "2018-07-09", "DescribeBillDetail", "", map[string]interface{}{
"Month": billingDate[:7],
"Offset": offset,
"Limit": pageSize,
"NeedRecordNum": 1,
})
if err != nil {
return nil, err
}
var response tencentBillDetailResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("解析腾讯云账单响应失败: %w", err)
}
for _, detail := range response.Response.DetailSet {
cost := tencentBillAmount(detail.RealTotalCost)
if cost == 0 {
cost = tencentBillAmount(detail.RealCost)
}
if cost == 0 {
cost = tencentBillAmount(detail.CashPayAmount)
}
if cost == 0 {
// 腾讯云明细账单将组件费用放在 ComponentSet,逐项汇总实际费用。 // Logan.Li
for _, component := range detail.ComponentSet {
componentCost := tencentBillAmount(component.RealCost)
if componentCost == 0 {
componentCost = tencentBillAmount(component.CashPayAmount)
}
cost += componentCost
}
}
if cost <= 0 {
continue
}
service := firstNonEmpty(detail.ProductCodeName, detail.BusinessCodeName, detail.ProductCode, "其他")
region := firstNonEmpty(detail.RegionName, "全局")
aggregation[aggregationKey{service: service, region: region}] += cost
}
if len(response.Response.DetailSet) < pageSize || (response.Response.Total > 0 && offset+pageSize >= response.Response.Total) {
break
}
}
keys := make([]aggregationKey, 0, len(aggregation))
for key := range aggregation {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].service == keys[j].service {
return keys[i].region < keys[j].region
}
return keys[i].service < keys[j].service
})
records := make([]CloudBillingRecord, 0, len(keys))
for _, key := range keys {
records = append(records, CloudBillingRecord{
BillingDate: billingDate,
Provider: "tencent",
Cost: math.Round(aggregation[key]*100) / 100,
Currency: "CNY",
Service: key.service,
Region: key.region,
Description: "云 API 自动同步(腾讯云 DescribeBillDetail)",
})
}
return records, nil
}
// tencentBillDetailResponse 描述腾讯云费用中心明细账单响应。 // Logan.Li
type tencentBillDetailResponse struct {
Response struct {
Total int `json:"Total"`
DetailSet []struct {
ProductCode string `json:"ProductCode"`
ProductCodeName string `json:"ProductCodeName"`
BusinessCodeName string `json:"BusinessCodeName"`
RegionName string `json:"RegionName"`
RealTotalCost json.RawMessage `json:"RealTotalCost"`
RealCost json.RawMessage `json:"RealCost"`
CashPayAmount json.RawMessage `json:"CashPayAmount"`
ComponentSet []struct {
RealCost json.RawMessage `json:"RealCost"`
CashPayAmount json.RawMessage `json:"CashPayAmount"`
} `json:"ComponentSet"`
} `json:"DetailSet"`
} `json:"Response"`
}
// tencentBillAmount 兼容腾讯云账单金额字段为字符串或数字的响应。 // Logan.Li
func tencentBillAmount(raw json.RawMessage) float64 {
value := strings.Trim(strings.TrimSpace(string(raw)), "\"")
amount, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0
}
return amount
}
// firstNonEmpty 返回首个非空文本,用于兼容不同账单产品字段。 // Logan.Li
func firstNonEmpty(values ...string) string {
for _, value := range values {
if text := strings.TrimSpace(value); text != "" {
return text
}
}
return ""
}