chore: 统一本地服务器与远程仓库源码
- 合并 main 与 master 历史并以服务器结构为基线 - 保留迁移、数据保留、AWS 账单与部署安全能力 - 建立 SDD 规格和 CI 验证闭环
This commit is contained in:
commit
fd60361d93
460 files changed
+37492
-23871
No files matched your search
@@ -1,45 +1,102 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ezops/config"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestBillingDimensionAttribution 验证费用变化额和贡献度使用数据库完整数据计算。 // Logan.Li
|
||||
func TestBillingDimensionAttribution(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败: %v", err)
|
||||
// TestCurrencyExpression 验证 CNY 与 USD 报告币种使用同一组可逆汇率。 // Logan.Li
|
||||
func TestCurrencyExpression(t *testing.T) {
|
||||
handler := NewBillingHandler(nil, &config.BillingConfig{
|
||||
BaseCurrency: "CNY",
|
||||
ExchangeRates: map[string]float64{"cny": 1, "usd": 7.2},
|
||||
})
|
||||
_, cnyArgs, ok := handler.currencyExpression("CNY")
|
||||
if !ok || len(cnyArgs) != 2 {
|
||||
t.Fatalf("CNY 汇率表达式无效: ok=%v args=%v", ok, cnyArgs)
|
||||
}
|
||||
if err = db.AutoMigrate(&model.CloudBilling{}); err != nil {
|
||||
t.Fatalf("迁移账单表失败: %v", err)
|
||||
metadata := handler.reportCurrencyMetadata("USD")
|
||||
rates := metadata["exchange_rates"].(map[string]float64)
|
||||
if rates["USD"] != 1 || rates["CNY"] != 0.138889 {
|
||||
t.Fatalf("USD 折算汇率错误: %+v", rates)
|
||||
}
|
||||
if _, _, supported := handler.currencyExpression("EUR"); supported {
|
||||
t.Fatal("未配置币种不应被接受")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBillingCurrencyConversionAPI 验证统计接口不会直接混加不同原币金额。 // Logan.Li
|
||||
func TestBillingCurrencyConversionAPI(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.CloudBilling{}, &model.BillingAlertRule{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
today := time.Now().Format("2006-01-02")
|
||||
records := []model.CloudBilling{
|
||||
{BillingDate: "2026-06-01", Provider: "tencent", Service: "CVM", Region: "广州", AccountName: "预发", Currency: "CNY", Cost: 100},
|
||||
{BillingDate: "2026-07-01", Provider: "tencent", Service: "CVM", Region: "广州", AccountName: "预发", Currency: "CNY", Cost: 180},
|
||||
{BillingDate: "2026-07-01", Provider: "tencent", Service: "COS", Region: "广州", AccountName: "预发", Currency: "CNY", Cost: 20},
|
||||
{Provider: "tencent", BillingDate: today, Cost: 100, Currency: "CNY", Service: "CVM"},
|
||||
{Provider: "aws", BillingDate: today, Cost: 10, Currency: "USD", Service: "EC2"},
|
||||
}
|
||||
if err = db.Create(&records).Error; err != nil {
|
||||
t.Fatalf("写入测试账单失败: %v", err)
|
||||
if err := db.Create(&records).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler := NewBillingHandler(db, &config.BillingConfig{BaseCurrency: "CNY", ExchangeRates: map[string]float64{"CNY": 1, "USD": 7.2}})
|
||||
|
||||
handler := NewBillingHandler(db)
|
||||
items := handler.billingDimensionAttribution("service", "2026-06-01", "2026-06-30", "2026-07-01", "2026-07-31", "CNY", 100)
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("归因项数量错误: got=%d want=2", len(items))
|
||||
assertMonthCost := func(currency string, expected float64) {
|
||||
t.Helper()
|
||||
recorder := httptest.NewRecorder()
|
||||
context, _ := gin.CreateTestContext(recorder)
|
||||
context.Request = httptest.NewRequest("GET", "/?currency="+currency, nil)
|
||||
handler.GetBillingStats(context)
|
||||
var response struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
MonthCost float64 `json:"month_cost"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Code != 200 || mathRound(response.Data.MonthCost, 2) != expected {
|
||||
t.Fatalf("%s 月费用错误: code=%d cost=%f", currency, response.Code, response.Data.MonthCost)
|
||||
}
|
||||
}
|
||||
result := map[string]billingAttribution{}
|
||||
for _, item := range items {
|
||||
result[item.Name] = item
|
||||
assertMonthCost("CNY", 172)
|
||||
assertMonthCost("USD", 23.89)
|
||||
}
|
||||
|
||||
// TestBuildBillingAnalysisMonths 验证空月份补齐和非 Top 分类合并。 // Logan.Li
|
||||
func TestBuildBillingAnalysisMonths(t *testing.T) {
|
||||
start, _ := time.Parse("2006-01", "2026-03")
|
||||
end, _ := time.Parse("2006-01", "2026-04")
|
||||
costs := map[string]map[string]float64{
|
||||
"2026-03": {"CVM": 100, "TCR": 20},
|
||||
}
|
||||
months := buildBillingAnalysisMonths(start, end, costs, []string{"CVM", "其他"})
|
||||
if len(months) != 2 || months[0].Total != 120 || months[0].Items[1].Cost != 20 {
|
||||
t.Fatalf("月度聚合错误: %+v", months)
|
||||
}
|
||||
if result["CVM"].Delta != 80 || result["CVM"].Contribution != 80 {
|
||||
t.Fatalf("CVM 归因错误: %+v", result["CVM"])
|
||||
if months[1].Month != "2026-04" || months[1].Total != 0 {
|
||||
t.Fatalf("空月份未正确补齐: %+v", months[1])
|
||||
}
|
||||
if result["COS"].Delta != 20 || result["COS"].Contribution != 20 {
|
||||
t.Fatalf("COS 归因错误: %+v", result["COS"])
|
||||
}
|
||||
|
||||
// TestBuildBillingAttributions 验证归因变化额与贡献度。 // Logan.Li
|
||||
func TestBuildBillingAttributions(t *testing.T) {
|
||||
items := buildBillingAttributions(map[string]float64{"CVM": 100}, map[string]float64{"CVM": 180, "TCR": 20})
|
||||
if len(items) != 2 || items[0].Name != "CVM" || items[0].Delta != 80 || items[0].Contribution != 80 {
|
||||
t.Fatalf("费用归因错误: %+v", items)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user