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 "" }