package cloud import ( "encoding/json" "fmt" "math" "strconv" "strings" ) // FetchDailyBill 拉取指定日期的阿里云日账单(按产品+地域聚合) // Logan.Li func (a *AliyunProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) { if len(billingDate) != 10 { return nil, fmt.Errorf("账单日期格式应为 YYYY-MM-DD") } billingCycle := billingDate[:7] type aggKey struct { service string region string } agg := map[aggKey]float64{} currency := "CNY" pageNum := 1 pageSize := 300 totalCount := -1 for { body, err := a.callBSS("QueryInstanceBill", map[string]string{ "BillingCycle": billingCycle, "BillingDate": billingDate, "Granularity": "DAILY", "PageNum": strconv.Itoa(pageNum), "PageSize": strconv.Itoa(pageSize), "IsHideZeroCharge": "true", }) if err != nil { return nil, err } var resp aliyunInstanceBillResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, fmt.Errorf("解析账单响应失败: %w", err) } if !resp.Success && resp.Code != "Success" { msg := resp.Message if msg == "" { msg = string(body) } return nil, fmt.Errorf("阿里云账单 API 错误: %s", msg) } if totalCount < 0 { totalCount = resp.Data.TotalCount } items := normalizeBillItems(resp.Data.Items.Item) for _, it := range items { amount := it.PretaxAmount if amount <= 0 { amount = it.PaymentAmount } if amount <= 0 { continue } service := strings.TrimSpace(it.ProductName) if service == "" { service = strings.TrimSpace(it.ProductCode) } if service == "" { service = "其他" } region := strings.TrimSpace(it.Region) if region == "" { region = "全局" } key := aggKey{service: service, region: region} agg[key] += amount if it.Currency != "" { currency = it.Currency } } if len(items) == 0 || pageNum*pageSize >= totalCount { break } pageNum++ } records := make([]CloudBillingRecord, 0, len(agg)) for key, cost := range agg { records = append(records, CloudBillingRecord{ BillingDate: billingDate, Provider: "aliyun", Cost: math.Round(cost*100) / 100, Currency: currency, Service: key.service, Region: key.region, Description: "云API自动同步(阿里云 QueryInstanceBill)", }) } return records, nil } type aliyunInstanceBillResponse struct { Code string `json:"Code"` Message string `json:"Message"` Success bool `json:"Success"` Data struct { TotalCount int `json:"TotalCount"` Items struct { Item json.RawMessage `json:"Item"` } `json:"Items"` } `json:"Data"` } type aliyunBillItem struct { ProductCode string `json:"ProductCode"` ProductName string `json:"ProductName"` Region string `json:"Region"` PretaxAmount float64 `json:"PretaxAmount"` PaymentAmount float64 `json:"PaymentAmount"` Currency string `json:"Currency"` } // normalizeBillItems 兼容 Item 为对象或数组两种返回格式 // Logan.Li func normalizeBillItems(raw json.RawMessage) []aliyunBillItem { if len(raw) == 0 || string(raw) == "null" { return nil } var items []aliyunBillItem if err := json.Unmarshal(raw, &items); err == nil { return items } var one aliyunBillItem if err := json.Unmarshal(raw, &one); err == nil { return []aliyunBillItem{one} } return nil }