package cloud import ( "bytes" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) // FetchDailyBill 通过 AWS Cost Explorer 按服务和区域同步指定日的未摊销成本。 // Logan.Li func (p *AWSProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) { day, err := time.Parse("2006-01-02", billingDate) if err != nil { return nil, fmt.Errorf("账单日期格式错误: %w", err) } payload, err := json.Marshal(map[string]interface{}{ "TimePeriod": map[string]string{ "Start": day.Format("2006-01-02"), "End": day.AddDate(0, 0, 1).Format("2006-01-02"), }, "Granularity": "DAILY", "Metrics": []string{"UnblendedCost"}, "GroupBy": []map[string]string{ {"Type": "DIMENSION", "Key": "SERVICE"}, {"Type": "DIMENSION", "Key": "REGION"}, }, }) if err != nil { return nil, fmt.Errorf("构造 AWS Cost Explorer 请求失败: %w", err) } body, err := p.callCostExplorer(payload) if err != nil { return nil, err } return parseAWSCostExplorerBill(billingDate, body) } // callCostExplorer 使用 SigV4 调用 Cost Explorer;该服务固定使用 us-east-1。 // Logan.Li func (p *AWSProvider) callCostExplorer(payload []byte) ([]byte, error) { const host = "ce.us-east-1.amazonaws.com" now := time.Now().UTC() date := now.Format("20060102") amzDate := now.Format("20060102T150405Z") payloadHash := sha256Hex(string(payload)) canonicalHeaders := "content-type:application/x-amz-json-1.1\n" + "host:" + host + "\n" + "x-amz-date:" + amzDate + "\n" + "x-amz-target:AWSInsightsIndexService.GetCostAndUsage\n" signedHeaders := "content-type;host;x-amz-date;x-amz-target" canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + payloadHash credentialScope := date + "/us-east-1/ce/aws4_request" stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest) kDate := hmacSHA256([]byte("AWS4"+p.secretKey), date) kRegion := hmacSHA256(kDate, "us-east-1") kService := hmacSHA256(kRegion, "ce") signingKey := hmacSHA256(kService, "aws4_request") signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("创建 AWS Cost Explorer 请求失败: %w", err) } req.Header.Set("Content-Type", "application/x-amz-json-1.1") req.Header.Set("X-Amz-Date", amzDate) req.Header.Set("X-Amz-Target", "AWSInsightsIndexService.GetCostAndUsage") req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+p.accessKey+"/"+credentialScope+", SignedHeaders="+signedHeaders+", Signature="+signature) resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { return nil, fmt.Errorf("请求 AWS Cost Explorer 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 AWS Cost Explorer 响应失败: %w", err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return nil, fmt.Errorf("AWS Cost Explorer 返回 HTTP %d: %s", resp.StatusCode, string(body)) } return body, nil } func parseAWSCostExplorerBill(billingDate string, body []byte) ([]CloudBillingRecord, error) { var response struct { ResultsByTime []struct { Groups []struct { Keys []string `json:"Keys"` Metrics map[string]struct { Amount string `json:"Amount"` Unit string `json:"Unit"` } `json:"Metrics"` } `json:"Groups"` } `json:"ResultsByTime"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("解析 AWS Cost Explorer 响应失败: %w", err) } records := make([]CloudBillingRecord, 0) for _, result := range response.ResultsByTime { for _, group := range result.Groups { metric, ok := group.Metrics["UnblendedCost"] if !ok { continue } amount, err := strconv.ParseFloat(metric.Amount, 64) if err != nil { return nil, fmt.Errorf("解析 AWS 成本金额失败: %w", err) } service, region := "AWS", "global" if len(group.Keys) > 0 && group.Keys[0] != "" { service = group.Keys[0] } if len(group.Keys) > 1 && group.Keys[1] != "" { region = group.Keys[1] } records = append(records, CloudBillingRecord{BillingDate: billingDate, Provider: "aws", Cost: amount, Currency: metric.Unit, Service: service, Region: region, Description: "AWS Cost Explorer UnblendedCost"}) } } return records, nil }