Files
aiops/backend/cloud/azure_billing.go
T
Logan.Li 132fe9ba97 feat: 补全多云资产与账单同步
- 接入 AWS、华为云和 Azure 资产及账单 API

- 按原始币种隔离统计并修正账号服务器数

- 修复侧栏菜单展开后的滚动行为
2026-07-29 06:15:00 +08:00

106 lines
3.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 通过 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 != "<nil>" {
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
}