139 lines
4.3 KiB
Go
139 lines
4.3 KiB
Go
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 {
|
||
// DescribeBillDetail 不支持 Day 参数,使用账期和起止时间限定单日范围。 // Logan.Li
|
||
body, err := p.callService("billing", "billing.tencentcloudapi.com", "2018-07-09", "DescribeBillDetail", "", map[string]interface{}{
|
||
"Month": billingDate[:7],
|
||
"BeginTime": billingDate + " 00:00:00",
|
||
"EndTime": billingDate + " 23:59:59",
|
||
"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 ""
|
||
}
|