package handlers import ( "fmt" "net/http" "sort" "strings" "time" "ezops/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // BillingHandler 云账单处理器 type BillingHandler struct { db *gorm.DB } func NewBillingHandler(db *gorm.DB) *BillingHandler { return &BillingHandler{db: db} } // ListBillings 获取账单列表 func (h *BillingHandler) ListBillings(c *gin.Context) { var billings []models.CloudBilling query := h.db.Model(&models.CloudBilling{}) // 筛选 if provider := c.Query("provider"); provider != "" { query = query.Where("provider = ?", provider) } if accountName := c.Query("account_name"); accountName != "" { query = query.Where("account_name LIKE ?", "%"+accountName+"%") } if startDate := c.Query("start_date"); startDate != "" { query = query.Where("billing_date >= ?", startDate) } if endDate := c.Query("end_date"); endDate != "" { query = query.Where("billing_date <= ?", endDate) } // 排序 query = query.Order("billing_date DESC, id DESC") // 分页 page := 1 pageSize := 50 if c.Query("page") != "" { fmt.Sscanf(c.Query("page"), "%d", &page) } if c.Query("page_size") != "" { fmt.Sscanf(c.Query("page_size"), "%d", &pageSize) } if page < 1 { page = 1 } if pageSize < 1 || pageSize > 500 { pageSize = 50 } var total int64 query.Count(&total) query.Offset((page - 1) * pageSize).Limit(pageSize).Find(&billings) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: billings, Total: total, }, }) } // CreateBilling 创建账单记录 func (h *BillingHandler) CreateBilling(c *gin.Context) { var req models.CreateBillingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } billing := models.CloudBilling{ CloudAccountID: req.CloudAccountID, AccountName: req.AccountName, Provider: req.Provider, BillingDate: req.BillingDate, Cost: req.Cost, Currency: req.Currency, Service: req.Service, Region: req.Region, Description: req.Description, } if billing.Currency == "" { billing.Currency = "CNY" } if err := h.db.Create(&billing).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()}) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "创建成功", Data: billing, }) } // BatchCreateBillings 批量创建账单 func (h *BillingHandler) BatchCreateBillings(c *gin.Context) { var req struct { Records []models.CreateBillingRequest `json:"records" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } created := 0 for _, r := range req.Records { billing := models.CloudBilling{ CloudAccountID: r.CloudAccountID, AccountName: r.AccountName, Provider: r.Provider, BillingDate: r.BillingDate, Cost: r.Cost, Currency: r.Currency, Service: r.Service, Region: r.Region, Description: r.Description, } if billing.Currency == "" { billing.Currency = "CNY" } if err := h.db.Create(&billing).Error; err == nil { created++ } } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: fmt.Sprintf("成功导入 %d 条记录", created), Data: gin.H{"created": created, "total": len(req.Records)}, }) } // DeleteBilling 删除账单记录 func (h *BillingHandler) DeleteBilling(c *gin.Context) { id := c.Param("id") if err := h.db.Delete(&models.CloudBilling{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) } // GetBillingStats 获取账单统计 func (h *BillingHandler) GetBillingStats(c *gin.Context) { now := time.Now() today := now.Format("2006-01-02") yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") monthStart := now.Format("2006-01") + "-01" yearStart := fmt.Sprintf("%d-01-01", now.Year()) stats := models.BillingStats{ MonthTrend: []models.DailyCost{}, ProviderDist: []models.ProviderCost{}, Alerts: []models.BillingAlert{}, } // 今日成本 h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", today). Select("COALESCE(SUM(cost), 0)").Scan(&stats.TodayCost) // 昨日成本 h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", yesterday). Select("COALESCE(SUM(cost), 0)").Scan(&stats.YesterdayCost) // 本月成本 h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", monthStart). Select("COALESCE(SUM(cost), 0)").Scan(&stats.MonthCost) // 本年成本 h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", yearStart). Select("COALESCE(SUM(cost), 0)").Scan(&stats.YearCost) // 近30天趋势 type dayCost struct { BillingDate string Total float64 } var trends []dayCost h.db.Model(&models.CloudBilling{}). Select("billing_date, SUM(cost) as total"). Where("billing_date >= ?", now.AddDate(0, 0, -29).Format("2006-01-02")). Group("billing_date"). Order("billing_date ASC"). Scan(&trends) for _, t := range trends { stats.MonthTrend = append(stats.MonthTrend, models.DailyCost{Date: t.BillingDate, Cost: t.Total}) } // 本月厂商成本分布 type provCost struct { Provider string Total float64 } var provCosts []provCost h.db.Model(&models.CloudBilling{}). Select("provider, SUM(cost) as total"). Where("billing_date >= ?", monthStart). Group("provider"). Scan(&provCosts) var totalProviderCost float64 for _, p := range provCosts { totalProviderCost += p.Total } for _, p := range provCosts { percent := 0.0 if totalProviderCost > 0 { percent = p.Total / totalProviderCost * 100 } stats.ProviderDist = append(stats.ProviderDist, models.ProviderCost{ Provider: p.Provider, Cost: p.Total, Percent: percent, }) } sort.Slice(stats.ProviderDist, func(i, j int) bool { return stats.ProviderDist[i].Cost > stats.ProviderDist[j].Cost }) // 检查预警规则 h.checkBillingAlerts(&stats, now) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: stats, }) } // checkBillingAlerts 检查预警规则是否触发 func (h *BillingHandler) checkBillingAlerts(stats *models.BillingStats, now time.Time) { var rules []models.BillingAlertRule h.db.Where("enabled = ?", true).Find(&rules) for _, rule := range rules { var current float64 if rule.Metric == "daily" { // 查今日该厂商/账号的成本 q := h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", now.Format("2006-01-02")) if rule.Provider != "" { q = q.Where("provider = ?", rule.Provider) } if rule.CloudAccountID != nil { q = q.Where("cloud_account_id = ?", *rule.CloudAccountID) } q.Select("COALESCE(SUM(cost), 0)").Scan(¤t) } else { // monthly - 本月累计 q := h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", now.Format("2006-01")+"-01") if rule.Provider != "" { q = q.Where("provider = ?", rule.Provider) } if rule.CloudAccountID != nil { q = q.Where("cloud_account_id = ?", *rule.CloudAccountID) } q.Select("COALESCE(SUM(cost), 0)").Scan(¤t) } // 检查阈值 triggered := false switch rule.Operator { case ">": triggered = current > rule.Threshold case ">=": triggered = current >= rule.Threshold case "<": triggered = current < rule.Threshold case "<=": triggered = current <= rule.Threshold } if triggered { providerLabel := rule.Provider if providerLabel == "" { providerLabel = "全部" } stats.Alerts = append(stats.Alerts, models.BillingAlert{ RuleID: rule.ID, RuleName: rule.Name, Provider: providerLabel, Current: current, Threshold: rule.Threshold, Metric: rule.Metric, Operator: rule.Operator, }) } } } // ListAlertRules 获取预警规则列表 func (h *BillingHandler) ListAlertRules(c *gin.Context) { var rules []models.BillingAlertRule h.db.Order("created_at DESC").Find(&rules) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: rules, Total: int64(len(rules)), }, }) } // CreateAlertRule 创建预警规则 func (h *BillingHandler) CreateAlertRule(c *gin.Context) { var req models.CreateBillingAlertRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } rule := models.BillingAlertRule{ Name: req.Name, Provider: req.Provider, CloudAccountID: req.CloudAccountID, Metric: req.Metric, Threshold: req.Threshold, Operator: req.Operator, Enabled: req.Enabled, } if rule.Metric == "" { rule.Metric = "daily" } if rule.Operator == "" { rule.Operator = ">" } if err := h.db.Create(&rule).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"}) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "创建成功", Data: rule, }) } // UpdateAlertRule 更新预警规则 func (h *BillingHandler) UpdateAlertRule(c *gin.Context) { id := c.Param("id") var rule models.BillingAlertRule if err := h.db.First(&rule, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"}) return } var req models.CreateBillingAlertRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } rule.Name = req.Name rule.Provider = req.Provider rule.CloudAccountID = req.CloudAccountID rule.Metric = req.Metric rule.Threshold = req.Threshold rule.Operator = req.Operator rule.Enabled = req.Enabled if rule.Metric == "" { rule.Metric = "daily" } if rule.Operator == "" { rule.Operator = ">" } h.db.Save(&rule) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "更新成功", Data: rule, }) } // DeleteAlertRule 删除预警规则 func (h *BillingHandler) DeleteAlertRule(c *gin.Context) { id := c.Param("id") if err := h.db.Delete(&models.BillingAlertRule{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) } // ToggleAlertRule 启用/禁用预警规则 func (h *BillingHandler) ToggleAlertRule(c *gin.Context) { id := c.Param("id") var rule models.BillingAlertRule if err := h.db.First(&rule, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"}) return } rule.Enabled = !rule.Enabled h.db.Save(&rule) status := "已启用" if !rule.Enabled { status = "已禁用" } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: status, Data: rule, }) } // GetProviderOptions 获取厂商选项(用于筛选下拉) func (h *BillingHandler) GetProviderOptions(c *gin.Context) { var providers []string h.db.Model(&models.CloudBilling{}).Distinct("provider").Where("provider != ''").Pluck("provider", &providers) result := make([]gin.H, 0, len(providers)) for _, p := range providers { label := p switch p { case "aliyun": label = "阿里云" case "tencent": label = "腾讯云" case "aws": label = "AWS" case "huawei": label = "华为云" case "azure": label = "Azure" } result = append(result, gin.H{"value": p, "label": label}) } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: result, }) } // formatProvider 格式化厂商名称 func formatProvider(provider string) string { names := map[string]string{ "aliyun": "阿里云", "tencent": "腾讯云", "aws": "AWS", "huawei": "华为云", "azure": "Azure", } if name, ok := names[strings.ToLower(provider)]; ok { return name } return provider }