feat: 补全多云资产与账单同步
- 接入 AWS、华为云和 Azure 资产及账单 API - 按原始币种隔离统计并修正账号服务器数 - 修复侧栏菜单展开后的滚动行为
This commit is contained in:
1 parent
65e2f09b97
commit
132fe9ba97
25 files changed
+1094
-141
No files matched your search
@@ -0,0 +1,105 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user