Files
aiops/backend/cloud/tencent_billing.go
T
Logan.Li 0b785da8ae feat: 完善多云资产与腾讯云账单同步
- 接入 AWS、腾讯云、华为云和 Azure 区域及主机发现
- 接入腾讯云账单分页、限流与组件费用汇总
- 批量账单仅同步已支持厂商并优化云账号操作
2026-07-25 14:45:19 +08:00

140 lines
4.3 KiB
Go
Raw 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"
"time"
)
// 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
}
// 腾讯云账单接口频率上限为每秒 5 次,分页请求限速为每秒 4 次。 // Logan.Li
time.Sleep(250 * time.Millisecond)
}
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 ""
}