chore: 统一本地服务器与远程仓库源码
- 合并 main 与 master 历史并以服务器结构为基线 - 保留迁移、数据保留、AWS 账单与部署安全能力 - 建立 SDD 规格和 CI 验证闭环
This commit is contained in:
commit
fd60361d93
460 files changed
+37492
-23871
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user