161 lines
5.8 KiB
Go
161 lines
5.8 KiB
Go
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
|
|
}
|