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

+175 -99
View File
@@ -1,10 +1,10 @@
package controller
import (
"fmt"
"math"
"net/http"
"sort"
"strings"
"time"
"ezops/model"
@@ -12,133 +12,208 @@ import (
"github.com/gin-gonic/gin"
)
// billingMonthCost 表示月度费用汇总。 // Logan.Li
type billingMonthCost struct {
Month string `json:"month"`
Cost float64 `json:"cost"`
// billingAnalysisItem 表示单月一个费用分类的汇总值。 // Logan.Li
type billingAnalysisItem struct {
Name string `json:"name"`
Cost float64 `json:"cost"`
Percent float64 `json:"percent"`
}
// billingAttribution 表示可复核的费用变化归因。 // Logan.Li
// billingAnalysisMonth 表示堆叠图和明细矩阵共用的月度数据。 // Logan.Li
type billingAnalysisMonth struct {
Month string `json:"month"`
Total float64 `json:"total"`
Items []billingAnalysisItem `json:"items"`
}
// billingAttribution 表示最近两个月可复核的费用变化归因。 // Logan.Li
type billingAttribution struct {
Dimension string `json:"dimension"`
Name string `json:"name"`
BaselineCost float64 `json:"baseline_cost"`
PreviousCost float64 `json:"previous_cost"`
CurrentCost float64 `json:"current_cost"`
Delta float64 `json:"delta"`
Contribution float64 `json:"contribution"`
}
// GetBillingCostAnalysis 返回月度趋势和同期费用归因,AI 只负责解释确定性结果。 // Logan.Li
// GetBillingCostAnalysis 返回多云费用堆叠趋势及同期归因。 // Logan.Li
func (h *BillingHandler) GetBillingCostAnalysis(c *gin.Context) {
now := time.Now()
currentStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
baselineStart := currentStart.AddDate(0, -1, 0)
baselineEndDay := now.Day()
baselineLastDay := baselineStart.AddDate(0, 1, -1).Day()
if baselineEndDay > baselineLastDay {
baselineEndDay = baselineLastDay
endMonth := c.DefaultQuery("end_month", now.Format("2006-01"))
startMonth := c.DefaultQuery("start_month", now.AddDate(0, -5, 0).Format("2006-01"))
currency := strings.ToUpper(c.DefaultQuery("currency", h.billing.BaseCurrency))
costExpression, costArgs, currencyOK := h.currencyExpression(currency)
if !currencyOK {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "不支持的报告币种"})
return
}
dimension := c.DefaultQuery("dimension", "service")
dimensionColumns := map[string]string{
"service": "service", "provider": "provider", "account": "account_name", "region": "region",
}
column, ok := dimensionColumns[dimension]
if !ok {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "不支持的分析维度"})
return
}
start, startErr := time.ParseInLocation("2006-01", startMonth, now.Location())
end, endErr := time.ParseInLocation("2006-01", endMonth, now.Location())
if startErr != nil || endErr != nil || start.After(end) || end.After(start.AddDate(0, 23, 0)) {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "月份范围无效,最多查询 24 个月"})
return
}
baselineEnd := time.Date(baselineStart.Year(), baselineStart.Month(), baselineEndDay, 23, 59, 59, 0, now.Location())
analysisStart := currentStart.AddDate(0, -5, 0)
currency := c.DefaultQuery("currency", "CNY")
query := h.db.Model(&model.CloudBilling{}).Where("billing_date >= ? AND currency = ?", analysisStart.Format("2006-01-02"), currency)
type monthRow struct {
type aggregateRow struct {
Month string
Name string
Cost float64
}
var monthRows []monthRow
query.Select("SUBSTR(billing_date, 1, 7) AS month, COALESCE(SUM(cost), 0) AS cost").
Group("SUBSTR(billing_date, 1, 7)").Order("month ASC").Scan(&monthRows)
months := make([]billingMonthCost, 0, len(monthRows))
for _, row := range monthRows {
months = append(months, billingMonthCost{Month: row.Month, Cost: roundBilling(row.Cost)})
var rows []aggregateRow
queryEnd := end.AddDate(0, 1, 0)
query := h.db.Model(&model.CloudBilling{}).
Select("SUBSTR(billing_date, 1, 7) AS month, "+column+" AS name, COALESCE(SUM("+costExpression+"), 0) AS cost", costArgs...).
Where("billing_date >= ? AND billing_date < ?", start.Format("2006-01-02"), queryEnd.Format("2006-01-02"))
if provider := c.Query("provider"); provider != "" {
query = query.Where("provider = ?", provider)
}
if accountID := c.Query("cloud_account_id"); accountID != "" {
query = query.Where("cloud_account_id = ?", accountID)
}
if err := query.Group("SUBSTR(billing_date, 1, 7), " + column).Scan(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "费用分析查询失败"})
return
}
baselineStartDate := baselineStart.Format("2006-01-02")
baselineEndDate := baselineEnd.Format("2006-01-02")
currentStartDate := currentStart.Format("2006-01-02")
currentEndDate := now.Format("2006-01-02")
baselineCost := h.sumBillingPeriod(baselineStartDate, baselineEndDate, currency)
currentCost := h.sumBillingPeriod(currentStartDate, currentEndDate, currency)
totalDelta := currentCost - baselineCost
monthCosts := map[string]map[string]float64{}
categoryTotals := map[string]float64{}
for _, row := range rows {
name := row.Name
if name == "" {
name = "未分类"
}
if monthCosts[row.Month] == nil {
monthCosts[row.Month] = map[string]float64{}
}
monthCosts[row.Month][name] += row.Cost
categoryTotals[name] += row.Cost
}
categories := topBillingCategories(categoryTotals, 8)
months := buildBillingAnalysisMonths(start, end, monthCosts, categories)
attributions := make([]billingAttribution, 0, 24)
for _, dimension := range []string{"provider", "account_name", "service", "region"} {
attributions = append(attributions, h.billingDimensionAttribution(
dimension, baselineStartDate, baselineEndDate, currentStartDate, currentEndDate, currency, totalDelta,
)...)
totalCost := 0.0
peakCost := 0.0
peakMonth := ""
for _, month := range months {
totalCost += month.Total
if month.Total > peakCost {
peakCost = month.Total
peakMonth = month.Month
}
}
sort.Slice(attributions, func(i, j int) bool {
return math.Abs(attributions[i].Delta) > math.Abs(attributions[j].Delta)
})
if len(attributions) > 20 {
attributions = attributions[:20]
averageCost := 0.0
if len(months) > 0 {
averageCost = totalCost / float64(len(months))
}
previousCost, currentCost := 0.0, 0.0
if len(months) >= 2 {
previousCost = months[len(months)-2].Total
currentCost = months[len(months)-1].Total
} else if len(months) == 1 {
currentCost = months[0].Total
}
momPercent := 0.0
if previousCost != 0 {
momPercent = (currentCost - previousCost) / previousCost * 100
}
attributions := buildBillingAttributions(monthCosts[end.AddDate(0, -1, 0).Format("2006-01")], monthCosts[endMonth])
var dataThrough string
h.db.Model(&model.CloudBilling{}).Where("currency = ?", currency).Select("COALESCE(MAX(billing_date), '')").Scan(&dataThrough)
dataThroughQuery := h.db.Model(&model.CloudBilling{})
if provider := c.Query("provider"); provider != "" {
dataThroughQuery = dataThroughQuery.Where("provider = ?", provider)
}
if accountID := c.Query("cloud_account_id"); accountID != "" {
dataThroughQuery = dataThroughQuery.Where("cloud_account_id = ?", accountID)
}
dataThroughQuery.Select("COALESCE(MAX(billing_date), '')").Scan(&dataThrough)
incompletePeriod := endMonth == now.Format("2006-01") && dataThrough < end.AddDate(0, 1, -1).Format("2006-01-02")
metadata := h.reportCurrencyMetadata(currency)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: gin.H{
"analysis_start": analysisStart.Format("2006-01-02"),
"analysis_end": currentEndDate,
"data_through": dataThrough,
"currency": currency,
"incomplete_period": true,
"months": months,
"baseline_period": fmt.Sprintf("%s - %s", baselineStartDate, baselineEndDate),
"comparison_period": fmt.Sprintf("%s - %s", currentStartDate, currentEndDate),
"baseline_cost": roundBilling(baselineCost),
"comparison_cost": roundBilling(currentCost),
"total_delta": roundBilling(totalDelta),
"attributions": attributions,
"contribution_formula": "(当前期费用 - 基准期费用) / (当前期总费用 - 基准期总费用)",
"start_month": startMonth, "end_month": endMonth, "currency": currency, "dimension": dimension,
"exchange_rates": metadata["exchange_rates"], "rate_source": metadata["rate_source"],
"data_through": dataThrough, "incomplete_period": incompletePeriod, "categories": categories, "months": months,
"summary": gin.H{"total_cost": roundBilling(totalCost), "average_cost": roundBilling(averageCost), "peak_cost": roundBilling(peakCost), "peak_month": peakMonth, "mom_percent": roundBilling(momPercent)},
"attributions": attributions,
}})
}
// sumBillingPeriod 汇总指定币种和日期范围费用。 // Logan.Li
func (h *BillingHandler) sumBillingPeriod(startDate, endDate, currency string) float64 {
var total float64
h.db.Model(&model.CloudBilling{}).
Where("billing_date BETWEEN ? AND ? AND currency = ?", startDate, endDate, currency).
Select("COALESCE(SUM(cost), 0)").Scan(&total)
return total
// topBillingCategories 按总费用选择 Top N 分类,其余合并为“其他”。 // Logan.Li
func topBillingCategories(totals map[string]float64, limit int) []string {
names := make([]string, 0, len(totals))
for name := range totals {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool { return totals[names[i]] > totals[names[j]] })
if len(names) > limit {
return append(names[:limit], "其他")
}
return names
}
// billingDimensionAttribution 计算单一维度的同期费用变化贡献。 // Logan.Li
func (h *BillingHandler) billingDimensionAttribution(dimension, baselineStart, baselineEnd, currentStart, currentEnd, currency string, totalDelta float64) []billingAttribution {
type dimensionRow struct {
Name string
Cost float64
}
load := func(startDate, endDate string) []dimensionRow {
var rows []dimensionRow
h.db.Model(&model.CloudBilling{}).
Select(dimension+" AS name, COALESCE(SUM(cost), 0) AS cost").
Where("billing_date BETWEEN ? AND ? AND currency = ?", startDate, endDate, currency).
Group(dimension).Scan(&rows)
return rows
}
baseline := map[string]float64{}
current := map[string]float64{}
for _, row := range load(baselineStart, baselineEnd) {
baseline[row.Name] = row.Cost
}
for _, row := range load(currentStart, currentEnd) {
current[row.Name] = row.Cost
}
for name := range baseline {
if _, ok := current[name]; !ok {
current[name] = 0
// buildBillingAnalysisMonths 补齐空月份并将非 Top 分类合并。 // Logan.Li
func buildBillingAnalysisMonths(start, end time.Time, costs map[string]map[string]float64, categories []string) []billingAnalysisMonth {
top := map[string]bool{}
for _, name := range categories {
if name != "其他" {
top[name] = true
}
}
result := make([]billingAttribution, 0, len(current))
for rawName, currentCost := range current {
months := make([]billingAnalysisMonth, 0)
for cursor := start; !cursor.After(end); cursor = cursor.AddDate(0, 1, 0) {
month := cursor.Format("2006-01")
grouped := map[string]float64{}
total := 0.0
for name, cost := range costs[month] {
key := name
if !top[name] {
key = "其他"
}
grouped[key] += cost
total += cost
}
items := make([]billingAnalysisItem, 0, len(categories))
for _, name := range categories {
cost := grouped[name]
percent := 0.0
if total != 0 {
percent = cost / total * 100
}
items = append(items, billingAnalysisItem{Name: name, Cost: roundBilling(cost), Percent: roundBilling(percent)})
}
months = append(months, billingAnalysisMonth{Month: month, Total: roundBilling(total), Items: items})
}
return months
}
// buildBillingAttributions 计算最近两个月各分类对总变化的贡献度。 // Logan.Li
func buildBillingAttributions(previous, current map[string]float64) []billingAttribution {
names := map[string]bool{}
previousTotal, currentTotal := 0.0, 0.0
for name, cost := range previous {
names[name] = true
previousTotal += cost
}
for name, cost := range current {
names[name] = true
currentTotal += cost
}
totalDelta := currentTotal - previousTotal
items := make([]billingAttribution, 0, len(names))
for rawName := range names {
name := rawName
if rawName == "" {
if name == "" {
name = "未分类"
}
baselineCost := baseline[rawName]
delta := currentCost - baselineCost
delta := current[rawName] - previous[rawName]
if math.Abs(delta) < 0.01 {
continue
}
@@ -146,12 +221,13 @@ func (h *BillingHandler) billingDimensionAttribution(dimension, baselineStart, b
if math.Abs(totalDelta) >= 0.01 {
contribution = delta / totalDelta * 100
}
result = append(result, billingAttribution{
Dimension: dimension, Name: name, BaselineCost: roundBilling(baselineCost),
CurrentCost: roundBilling(currentCost), Delta: roundBilling(delta), Contribution: roundBilling(contribution),
})
items = append(items, billingAttribution{Name: name, PreviousCost: roundBilling(previous[rawName]), CurrentCost: roundBilling(current[rawName]), Delta: roundBilling(delta), Contribution: roundBilling(contribution)})
}
return result
sort.Slice(items, func(i, j int) bool { return math.Abs(items[i].Delta) > math.Abs(items[j].Delta) })
if len(items) > 10 {
items = items[:10]
}
return items
}
// roundBilling 统一费用和百分比精度。 // Logan.Li