package controller import ( "fmt" "math" "net/http" "sort" "time" "ezops/model" "github.com/gin-gonic/gin" ) // billingMonthCost 表示月度费用汇总。 // Logan.Li type billingMonthCost struct { Month string `json:"month"` Cost float64 `json:"cost"` } // billingAttribution 表示可复核的费用变化归因。 // Logan.Li type billingAttribution struct { Dimension string `json:"dimension"` Name string `json:"name"` BaselineCost float64 `json:"baseline_cost"` CurrentCost float64 `json:"current_cost"` Delta float64 `json:"delta"` Contribution float64 `json:"contribution"` } // GetBillingCostAnalysis 返回月度趋势和同期费用归因,AI 只负责解释确定性结果。 // 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 } 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 { Month 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)}) } 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 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, )...) } 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] } var dataThrough string h.db.Model(&model.CloudBilling{}).Where("currency = ?", currency).Select("COALESCE(MAX(billing_date), '')").Scan(&dataThrough) 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": "(当前期费用 - 基准期费用) / (当前期总费用 - 基准期总费用)", }}) } // 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 } // 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 } } result := make([]billingAttribution, 0, len(current)) for rawName, currentCost := range current { name := rawName if rawName == "" { name = "未分类" } baselineCost := baseline[rawName] delta := currentCost - baselineCost if math.Abs(delta) < 0.01 { continue } contribution := 0.0 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), }) } return result } // roundBilling 统一费用和百分比精度。 // Logan.Li func roundBilling(value float64) float64 { return math.Round(value*100) / 100 }