chore: 统一本地服务器与远程仓库源码

- 合并 main 与 master 历史并以服务器结构为基线
- 保留迁移、数据保留、AWS 账单与部署安全能力
- 建立 SDD 规格和 CI 验证闭环
This commit is contained in:
Logan.Li committed 2026-08-05 20:03:25 +08:00
commit fd60361d93
460 files changed
+37492 -23871

No files matched your search

+81 -105
View File
@@ -1,147 +1,123 @@
package cloud
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// FetchDailyBill 通过 Cost Explorer 拉取 AWS 单日服务和区域费用。 // Logan.Li
// FetchDailyBill 通过 AWS Cost Explorer 按服务和区域同步指定日的未摊销成本。 // Logan.Li
func (p *AWSProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) {
start, err := time.Parse("2006-01-02", billingDate)
day, err := time.Parse("2006-01-02", billingDate)
if err != nil {
return nil, fmt.Errorf("账单日期格式应为 YYYY-MM-DD")
return nil, fmt.Errorf("账单日期格式错误: %w", err)
}
type aggregationKey struct {
service string
region string
}
aggregation := map[aggregationKey]float64{}
nextToken := ""
currency := "USD"
for {
payload := map[string]interface{}{
"TimePeriod": map[string]string{"Start": billingDate, "End": start.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 nextToken != "" {
payload["NextPageToken"] = nextToken
}
body, callErr := p.callCostExplorer(payload)
if callErr != nil {
return nil, callErr
}
var response struct {
NextPageToken string `json:"NextPageToken"`
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)
}
for _, period := range response.ResultsByTime {
for _, group := range period.Groups {
metric := group.Metrics["UnblendedCost"]
cost, _ := strconv.ParseFloat(metric.Amount, 64)
if cost == 0 {
continue
}
if metric.Unit != "" {
currency = metric.Unit
}
service, region := "其他", "全局"
if len(group.Keys) > 0 && group.Keys[0] != "" {
service = group.Keys[0]
}
if len(group.Keys) > 1 && group.Keys[1] != "" && group.Keys[1] != "NoRegion" {
region = group.Keys[1]
}
aggregation[aggregationKey{service: service, region: region}] += cost
}
}
nextToken = response.NextPageToken
if nextToken == "" {
break
}
}
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
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"},
},
})
records := make([]CloudBillingRecord, 0, len(keys))
for _, key := range keys {
records = append(records, CloudBillingRecord{
BillingDate: billingDate, Provider: "aws", Cost: math.Round(aggregation[key]*1000000) / 1000000,
Currency: currency, Service: key.service, Region: key.region,
Description: "云 API 自动同步(AWS Cost Explorer)",
})
if err != nil {
return nil, fmt.Errorf("构造 AWS Cost Explorer 请求失败: %w", err)
}
return records, nil
body, err := p.callCostExplorer(payload)
if err != nil {
return nil, err
}
return parseAWSCostExplorerBill(billingDate, body)
}
// callCostExplorer 使用 Signature V4 调用 AWS Cost Explorer JSON API。 // Logan.Li
func (p *AWSProvider) callCostExplorer(payload interface{}) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("编码 AWS 账单请求失败: %w", err)
}
// 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"
const target = "AWSInsightsIndexService.GetCostAndUsage"
now := time.Now().UTC()
amzDate, date := now.Format("20060102T150405Z"), now.Format("20060102")
canonicalHeaders := "content-type:application/x-amz-json-1.1\nhost:" + host + "\nx-amz-date:" + amzDate + "\nx-amz-target:" + target + "\n"
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" + sha256Hex(string(body))
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")
signature := hex.EncodeToString(hmacSHA256(hmacSHA256(kService, "aws4_request"), stringToSign))
req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", strings.NewReader(string(body)))
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 账单请求失败: %w", err)
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", target)
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()
data, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取 AWS Cost Explorer 响应失败: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("AWS Cost Explorer 返回 HTTP %d: %s", resp.StatusCode, string(data))
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("AWS Cost Explorer 返回 HTTP %d: %s", resp.StatusCode, string(body))
}
return data, nil
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
}