package cloud import ( "encoding/json" "fmt" "math" "sort" "strconv" "strings" "time" ) // FetchDailyBill 通过 Azure Cost Management Query 拉取订阅单日成本。 // Logan.Li func (p *AzureProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) { start, err := time.Parse("2006-01-02", billingDate) if err != nil { return nil, fmt.Errorf("账单日期格式应为 YYYY-MM-DD") } if p.subscriptionID == "" { return nil, fmt.Errorf("Azure AccessKey 应为 租户ID:订阅ID:客户端ID") } payload := map[string]interface{}{ "type": "ActualCost", "timeframe": "Custom", "timePeriod": map[string]string{ "from": start.Format("2006-01-02T00:00:00Z"), "to": start.AddDate(0, 0, 1).Format("2006-01-02T00:00:00Z"), }, "dataset": map[string]interface{}{ "granularity": "Daily", "aggregation": map[string]interface{}{ "totalCost": map[string]string{"name": "PreTaxCost", "function": "Sum"}, }, "grouping": []map[string]string{ {"type": "Dimension", "name": "ServiceName"}, {"type": "Dimension", "name": "ResourceLocation"}, }, }, } path := "/subscriptions/" + p.subscriptionID + "/providers/Microsoft.CostManagement/query?api-version=2023-11-01" body, err := p.post(path, payload) if err != nil { return nil, err } var response struct { Properties struct { Columns []struct { Name string `json:"name"` } `json:"columns"` Rows [][]interface{} `json:"rows"` } `json:"properties"` } if err = json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("解析 Azure Cost Management 响应失败: %w", err) } columnIndex := map[string]int{} for index, column := range response.Properties.Columns { columnIndex[strings.ToLower(column.Name)] = index } valueAt := func(row []interface{}, names ...string) interface{} { for _, name := range names { if index, ok := columnIndex[strings.ToLower(name)]; ok && index < len(row) { return row[index] } } return nil } type aggregationKey struct { service string region string } aggregation := map[aggregationKey]float64{} currency := "USD" for _, row := range response.Properties.Rows { cost, _ := strconv.ParseFloat(fmt.Sprint(valueAt(row, "PreTaxCost", "Cost", "totalCost")), 64) if cost == 0 { continue } service := firstNonEmpty(fmt.Sprint(valueAt(row, "ServiceName")), "其他") region := firstNonEmpty(fmt.Sprint(valueAt(row, "ResourceLocation")), "全局") if value := fmt.Sprint(valueAt(row, "Currency")); value != "" && value != "" { currency = value } aggregation[aggregationKey{service: service, region: region}] += cost } 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: "azure", Cost: math.Round(aggregation[key]*1000000) / 1000000, Currency: currency, Service: key.service, Region: key.region, Description: "云 API 自动同步(Azure Cost Management)", }) } return records, nil }