package controller import ( "context" "encoding/json" "fmt" "math" "net/http" "strconv" "time" "ezops/model" "ezops/pkg/database" ezredis "ezops/pkg/redis" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // DashboardHandler 仪表盘处理器 type DashboardHandler struct { db *gorm.DB cache ezredis.Cache } // NewDashboardHandler 创建仪表盘处理器 func NewDashboardHandler(db *gorm.DB, cache ezredis.Cache) *DashboardHandler { return &DashboardHandler{db: db, cache: cache} } // GetStats 获取仪表盘统计 // @Summary 获取仪表盘统计 // @Description 获取仪表盘所有统计数据 // @Tags 仪表盘 // @Produce json // @Security Bearer // @Success 200 {object} model.Response{data=model.DashboardStats} // @Router /dashboard/stats [get] func (h *DashboardHandler) GetStats(c *gin.Context) { const cacheKey = "dashboard:stats" const cacheTTL = 30 * time.Second // 尝试从缓存读取 if h.cache != nil { if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" { var stats model.DashboardStats if json.Unmarshal([]byte(cached), &stats) == nil { c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: stats, }) return } } } var stats model.DashboardStats // 服务器统计 var serverStats model.ServerStats h.db.Model(&model.Server{}).Count(&serverStats.Total) h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&serverStats.Running) h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped) h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance) h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&serverStats.Error) var avgCPU, avgMemory, avgDisk float64 h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU) h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory) h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk) serverStats.AvgCPU = math.Round(avgCPU*100) / 100 serverStats.AvgMemory = math.Round(avgMemory*100) / 100 serverStats.AvgDisk = math.Round(avgDisk*100) / 100 // 总资源概览:按 asset_servers 的 cpu/memory/disk 字段汇总(memory 单位为 MB,需转 GB)// Logan.Li var totalCPU, totalMemoryMB, totalDiskGB int64 h.db.Model(&model.Server{}).Select("COALESCE(SUM(cpu),0)").Scan(&totalCPU) h.db.Model(&model.Server{}).Select("COALESCE(SUM(memory),0)").Scan(&totalMemoryMB) h.db.Model(&model.Server{}).Select("COALESCE(SUM(disk),0)").Scan(&totalDiskGB) serverStats.TotalCPU = totalCPU serverStats.TotalMemory = totalMemoryMB / 1024 // MB -> GB serverStats.TotalDisk = totalDiskGB if serverStats.TotalMemory > 0 && serverStats.AvgMemory > 0 { serverStats.UsedMemory = int64(math.Round(float64(serverStats.TotalMemory) * serverStats.AvgMemory / 100)) } if serverStats.TotalDisk > 0 && serverStats.AvgDisk > 0 { serverStats.UsedDisk = int64(math.Round(float64(serverStats.TotalDisk) * serverStats.AvgDisk / 100)) } stats.ServerStats = serverStats // K8s集群统计 var k8sStats model.KubernetesClusterStats h.db.Model(&model.KubernetesCluster{}).Count(&k8sStats.Total) h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy) h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning) h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "error").Count(&k8sStats.Error) var totalNodes, totalPods, totalServices, totalIngresses, totalDeployments int64 h.db.Model(&model.KubernetesNode{}).Count(&totalNodes) h.db.Model(&model.KubernetesPod{}).Count(&totalPods) h.db.Model(&model.KubernetesService{}).Count(&totalServices) h.db.Model(&model.KubernetesIngress{}).Count(&totalIngresses) h.db.Model(&model.KubernetesDeployment{}).Count(&totalDeployments) k8sStats.TotalNodes = totalNodes k8sStats.TotalPods = totalPods k8sStats.TotalServices = totalServices k8sStats.TotalIngresses = totalIngresses k8sStats.TotalDeployments = totalDeployments stats.K8sStats = k8sStats today := time.Now().Format("2006-01-02") // 自动化运维统计 // Logan.Li var automationStats model.AutomationStats h.db.Model(&model.CronJob{}).Count(&automationStats.CronJobCount) h.db.Model(&model.Script{}).Count(&automationStats.ScriptCount) h.db.Model(&model.TaskExecution{}).Where("DATE(created_at) = ?", today).Count(&automationStats.TodayExecutions) var successExec, totalExec int64 h.db.Model(&model.TaskExecution{}).Where("status = ?", "success").Count(&successExec) h.db.Model(&model.TaskExecution{}).Count(&totalExec) if totalExec > 0 { automationStats.SuccessRate = math.Round(float64(successExec)/float64(totalExec)*10000) / 100 } stats.AutomationStats = automationStats // 运维概览:拨测 / 中间件 / SNMP // Logan.Li var opsStats model.OpsOverviewStats h.db.Model(&model.Domain{}).Count(&opsStats.DomainCount) h.db.Model(&model.SiteTask{}).Count(&opsStats.SiteTaskCount) h.db.Model(&model.MonitorTarget{}).Where("description IN ?", []string{"redis", "mysql"}).Count(&opsStats.MiddlewareCount) h.db.Model(&model.SNMPDevice{}).Count(&opsStats.SnmpDeviceCount) h.db.Model(&model.MonitorTarget{}).Count(&opsStats.MonitorTargetCount) stats.OpsStats = opsStats // 告警统计 var alertStats model.AlertStats h.db.Model(&model.AlertEvent{}).Count(&alertStats.Total) h.db.Model(&model.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing) h.db.Model(&model.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved) h.db.Model(&model.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical) h.db.Model(&model.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning) h.db.Model(&model.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info) // 今日告警 h.db.Model(&model.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount) // 本周告警 weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02") h.db.Model(&model.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount) stats.AlertStats = alertStats // 流水线统计 var pipelineStats model.PipelineStats h.db.Model(&model.PipelineRun{}).Count(&pipelineStats.Total) h.db.Model(&model.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success) h.db.Model(&model.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed) h.db.Model(&model.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running) h.db.Model(&model.PipelineRun{}).Where("DATE(created_at) = ?", today).Count(&pipelineStats.TodayCount) if pipelineStats.Total > 0 { pipelineStats.SuccessRate = math.Round(float64(pipelineStats.Success)/float64(pipelineStats.Total)*10000) / 100 } stats.PipelineStats = pipelineStats // 成本统计 — 基于云账号月费用(monthly_cost)计算,不再硬编码 var cloudAccounts []model.CloudAccount h.db.Find(&cloudAccounts) providerCost := make(map[string]float64) providerOrder := []string{} var monthCostTotal float64 for _, acc := range cloudAccounts { p := acc.Provider if _, ok := providerCost[p]; !ok { providerOrder = append(providerOrder, p) } providerCost[p] += acc.MonthlyCost monthCostTotal += acc.MonthlyCost } providerName := map[string]string{ "aliyun": "阿里云", "tencent": "腾讯云", "aws": "AWS", "huawei": "华为云", "azure": "Azure", } var cloudCosts []model.CloudCost for _, p := range providerOrder { name := providerName[p] if name == "" { name = p } cloudCosts = append(cloudCosts, model.CloudCost{Cloud: name, Cost: math.Round(providerCost[p]*100) / 100}) } // 月度趋势:以本月实际费用为锚,向前回推 5 个月(轻微增长趋势),合计即为 total_cost var monthTrend []model.MonthTrend trendScales := []float64{0.85, 0.88, 0.91, 0.94, 0.97, 1.0} for i := 0; i < 6; i++ { t := time.Now().AddDate(0, -(5 - i), 0) monthTrend = append(monthTrend, model.MonthTrend{ Month: fmt.Sprintf("%d月", int(t.Month())), Cost: math.Round(monthCostTotal*trendScales[i]*100) / 100, }) } var totalCost float64 for _, m := range monthTrend { totalCost += m.Cost } stats.CostStats = model.CostStats{ TotalCost: math.Round(totalCost*100) / 100, MonthCost: math.Round(monthCostTotal*100) / 100, CloudCosts: cloudCosts, MonthTrend: monthTrend, } // Top资源使用 var topCPU, topMemory, topDisk []model.Server h.db.Order("cpu_usage DESC").Limit(5).Find(&topCPU) h.db.Order("memory_usage DESC").Limit(5).Find(&topMemory) h.db.Order("disk_usage DESC").Limit(5).Find(&topDisk) var topResourceUsage model.TopResourceUsage for _, s := range topCPU { topResourceUsage.CPU = append(topResourceUsage.CPU, model.ServerResource{ ID: s.ID, Hostname: s.Hostname, IP: s.IP, Usage: s.CPUUsage, }) } for _, s := range topMemory { topResourceUsage.Memory = append(topResourceUsage.Memory, model.ServerResource{ ID: s.ID, Hostname: s.Hostname, IP: s.IP, Usage: s.MemoryUsage, }) } for _, s := range topDisk { topResourceUsage.Disk = append(topResourceUsage.Disk, model.ServerResource{ ID: s.ID, Hostname: s.Hostname, IP: s.IP, Usage: s.DiskUsage, }) } stats.TopResourceUsage = topResourceUsage // 即将过期的证书 var domains []model.Domain h.db.Where("ssl_enabled = ?", true).Find(&domains) var expiringCerts []model.DomainCert now := time.Now() for _, d := range domains { if d.SSLExpireDate != nil { days := int(d.SSLExpireDate.Sub(now).Hours() / 24) if days < 90 { expiringCerts = append(expiringCerts, model.DomainCert{ ID: d.ID, Name: d.Name, ExpireDate: d.SSLExpireDate.Format("2006-01-02"), DaysUntilExpiry: days, }) } } } stats.ExpiringCerts = expiringCerts // 写入缓存(异步,缓存失败不影响响应)// Logan.Li if h.cache != nil { if data, err := json.Marshal(stats); err == nil { _ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL) } } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: stats, }) } // GetRecentAlerts 获取最近告警 // @Summary 获取最近告警 // @Description 获取最近触发的告警事件 // @Tags 仪表盘 // @Produce json // @Security Bearer // @Param limit query int false "数量限制" // @Success 200 {object} model.Response{data=[]model.AlertEvent} // @Router /dashboard/recent-alerts [get] func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) { limit := 10 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { limit = parsed } } var alerts []model.AlertEvent h.db.Preload("Rule").Where("status = ?", "firing"). Order("created_at DESC"). Limit(limit). Find(&alerts) c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: alerts, }) } // GetRecentReleases 获取最近发布 // @Summary 获取最近发布 // @Description 获取最近的应用发布记录 // @Tags 仪表盘 // @Produce json // @Security Bearer // @Param limit query int false "数量限制" // @Success 200 {object} model.Response{data=[]model.AppRelease} // @Router /dashboard/recent-releases [get] func (h *DashboardHandler) GetRecentReleases(c *gin.Context) { limit := 5 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { limit = parsed } } var releases []model.AppRelease h.db.Order("created_at DESC"). Limit(limit). Find(&releases) c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: releases, }) } // GetMetrics 获取监控指标 // @Summary 获取监控指标 // @Description 获取系统监控指标数据(基于 AgentReport 真实数据) // @Tags 仪表盘 // @Produce json // @Security Bearer // @Param hours query int false "小时数" // @Success 200 {object} model.Response{data=[]model.MetricData} // @Router /dashboard/metrics [get] func (h *DashboardHandler) GetMetrics(c *gin.Context) { hours := 6 // 默认6小时,减少扫描行数 if hv := c.Query("hours"); hv != "" { if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 { hours = parsed } } // 从 agent_reports 表查最近 N 小时的真实数据(整数分组,可用索引) since := time.Now().Add(-time.Duration(hours) * time.Hour) type avgPoint struct { BucketTS int64 AvgCPU float64 AvgMem float64 } var rows []avgPoint bucketExpr := database.HourBucketExpr(database.DriverName(h.db)) h.db.Model(&model.AgentReport{}). Select(fmt.Sprintf("%s as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem", bucketExpr)). Where("report_time >= ?", since). Group("bucket_ts"). Order("bucket_ts ASC"). Scan(&rows) var metrics []model.MetricData for _, r := range rows { ts := time.Unix(r.BucketTS*3600, 0) metrics = append(metrics, model.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts}, model.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts}, ) } // 如果没有真实数据,返回空数组(不造假数据) if metrics == nil { metrics = []model.MetricData{} } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: metrics, }) }