后端改进: - 修复 dashboard/recent-alerts 的 limit 参数解析 (原代码错误地将数字解析为日期) - 为 dashboard/recent-releases 添加 limit 参数解析支持 - 新增 generateAppReleases 种子数据生成函数 (10条应用发布记录) - 修复 AlertEvent 的 rule_name/rule_id/severity 数据一致性 - 添加 go-ldap/ldap/v3 依赖并集成 LDAP 认证处理器 - 添加 seed.go 自动种子数据初始化 (admin用户+LDAP配置) - User model 添加 AuthSource/LDAPConfig 字段 前端改进: - 修复 User 类型定义以匹配后端 API (real_name/is_admin/auth_source等) - 修复 Header.tsx 的 real_name 属性引用导致 TypeScript 编译错误 - 统一 API 服务层、暗色主题样式、组件联调优化 验证: - go build ./... 编译通过 - npm run build 构建通过 - 仪表盘发布历史、告警消息等区域数据正常展示 - 健康检查 /health 正常
286 lines
8.6 KiB
Go
286 lines
8.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"ezops/internal/models"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// DashboardHandler 仪表盘处理器
|
|
type DashboardHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewDashboardHandler 创建仪表盘处理器
|
|
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
|
|
return &DashboardHandler{db: db}
|
|
}
|
|
|
|
// GetStats 获取仪表盘统计
|
|
// @Summary 获取仪表盘统计
|
|
// @Description 获取仪表盘所有统计数据
|
|
// @Tags 仪表盘
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Success 200 {object} models.Response{data=models.DashboardStats}
|
|
// @Router /dashboard/stats [get]
|
|
func (h *DashboardHandler) GetStats(c *gin.Context) {
|
|
var stats models.DashboardStats
|
|
|
|
// 服务器统计
|
|
var serverStats models.ServerStats
|
|
h.db.Model(&models.Server{}).Count(&serverStats.Total)
|
|
h.db.Model(&models.Server{}).Where("status = ?", "running").Count(&serverStats.Running)
|
|
h.db.Model(&models.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped)
|
|
h.db.Model(&models.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance)
|
|
h.db.Model(&models.Server{}).Where("status = ?", "error").Count(&serverStats.Error)
|
|
|
|
var avgCPU, avgMemory, avgDisk float64
|
|
h.db.Model(&models.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
|
|
h.db.Model(&models.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
|
|
h.db.Model(&models.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
|
|
|
|
stats.ServerStats = serverStats
|
|
|
|
// K8s集群统计
|
|
var k8sStats models.K8sClusterStats
|
|
h.db.Model(&models.K8sCluster{}).Count(&k8sStats.Total)
|
|
h.db.Model(&models.K8sCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
|
|
h.db.Model(&models.K8sCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
|
|
h.db.Model(&models.K8sCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
|
|
|
|
var totalNodes, totalPods, totalServices int64
|
|
h.db.Model(&models.K8sNode{}).Count(&totalNodes)
|
|
h.db.Model(&models.K8sPod{}).Count(&totalPods)
|
|
h.db.Model(&models.K8sService{}).Count(&totalServices)
|
|
k8sStats.TotalNodes = totalNodes
|
|
k8sStats.TotalPods = totalPods
|
|
k8sStats.TotalServices = totalServices
|
|
|
|
stats.K8sStats = k8sStats
|
|
|
|
// 告警统计
|
|
var alertStats models.AlertStats
|
|
h.db.Model(&models.AlertEvent{}).Count(&alertStats.Total)
|
|
h.db.Model(&models.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing)
|
|
h.db.Model(&models.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved)
|
|
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical)
|
|
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning)
|
|
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info)
|
|
|
|
// 今日告警
|
|
today := time.Now().Format("2006-01-02")
|
|
h.db.Model(&models.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount)
|
|
|
|
// 本周告警
|
|
weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
|
|
h.db.Model(&models.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount)
|
|
|
|
stats.AlertStats = alertStats
|
|
|
|
// 流水线统计
|
|
var pipelineStats models.PipelineStats
|
|
h.db.Model(&models.PipelineRun{}).Count(&pipelineStats.Total)
|
|
h.db.Model(&models.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success)
|
|
h.db.Model(&models.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed)
|
|
h.db.Model(&models.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running)
|
|
h.db.Model(&models.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
|
|
|
|
// 成本统计
|
|
costStats := models.CostStats{
|
|
TotalCost: 450000,
|
|
MonthCost: 107700,
|
|
CloudCosts: []models.CloudCost{
|
|
{Cloud: "阿里云", Cost: 28500},
|
|
{Cloud: "AWS", Cost: 42000},
|
|
{Cloud: "腾讯云", Cost: 15800},
|
|
{Cloud: "华为云", Cost: 8900},
|
|
{Cloud: "Azure", Cost: 12500},
|
|
},
|
|
MonthTrend: []models.MonthTrend{
|
|
{Month: "1月", Cost: 85000},
|
|
{Month: "2月", Cost: 92000},
|
|
{Month: "3月", Cost: 88000},
|
|
{Month: "4月", Cost: 95000},
|
|
{Month: "5月", Cost: 107700},
|
|
},
|
|
}
|
|
stats.CostStats = costStats
|
|
|
|
// Top资源使用
|
|
var topCPU, topMemory, topDisk []models.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 models.TopResourceUsage
|
|
for _, s := range topCPU {
|
|
topResourceUsage.CPU = append(topResourceUsage.CPU, models.ServerResource{
|
|
ID: s.ID,
|
|
Hostname: s.Hostname,
|
|
IP: s.IP,
|
|
Usage: s.CPUUsage,
|
|
})
|
|
}
|
|
for _, s := range topMemory {
|
|
topResourceUsage.Memory = append(topResourceUsage.Memory, models.ServerResource{
|
|
ID: s.ID,
|
|
Hostname: s.Hostname,
|
|
IP: s.IP,
|
|
Usage: s.MemoryUsage,
|
|
})
|
|
}
|
|
for _, s := range topDisk {
|
|
topResourceUsage.Disk = append(topResourceUsage.Disk, models.ServerResource{
|
|
ID: s.ID,
|
|
Hostname: s.Hostname,
|
|
IP: s.IP,
|
|
Usage: s.DiskUsage,
|
|
})
|
|
}
|
|
stats.TopResourceUsage = topResourceUsage
|
|
|
|
// 即将过期的证书
|
|
var domains []models.Domain
|
|
h.db.Where("ssl_enabled = ?", true).Find(&domains)
|
|
|
|
var expiringCerts []models.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, models.DomainCert{
|
|
ID: d.ID,
|
|
Name: d.Name,
|
|
ExpireDate: d.SSLExpireDate.Format("2006-01-02"),
|
|
DaysUntilExpiry: days,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
stats.ExpiringCerts = expiringCerts
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: stats,
|
|
})
|
|
}
|
|
|
|
// GetRecentAlerts 获取最近告警
|
|
// @Summary 获取最近告警
|
|
// @Description 获取最近触发的告警事件
|
|
// @Tags 仪表盘
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param limit query int false "数量限制"
|
|
// @Success 200 {object} models.Response{data=[]models.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 []models.AlertEvent
|
|
h.db.Preload("Rule").Where("status = ?", "firing").
|
|
Order("created_at DESC").
|
|
Limit(limit).
|
|
Find(&alerts)
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: alerts,
|
|
})
|
|
}
|
|
|
|
// GetRecentReleases 获取最近发布
|
|
// @Summary 获取最近发布
|
|
// @Description 获取最近的应用发布记录
|
|
// @Tags 仪表盘
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param limit query int false "数量限制"
|
|
// @Success 200 {object} models.Response{data=[]models.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 []models.AppRelease
|
|
h.db.Order("created_at DESC").
|
|
Limit(limit).
|
|
Find(&releases)
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: releases,
|
|
})
|
|
}
|
|
|
|
// GetMetrics 获取监控指标
|
|
// @Summary 获取监控指标
|
|
// @Description 获取系统监控指标数据
|
|
// @Tags 仪表盘
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param hours query int false "小时数"
|
|
// @Success 200 {object} models.Response{data=[]models.MetricData}
|
|
// @Router /dashboard/metrics [get]
|
|
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
|
hours := 24
|
|
|
|
// 生成模拟数据
|
|
var metrics []models.MetricData
|
|
now := time.Now()
|
|
|
|
for i := 0; i < hours; i++ {
|
|
timestamp := now.Add(-time.Duration(hours-i) * time.Hour)
|
|
metrics = append(metrics, models.MetricData{
|
|
Name: "cpu",
|
|
Value: 40 + float64(i%30),
|
|
Timestamp: timestamp,
|
|
})
|
|
metrics = append(metrics, models.MetricData{
|
|
Name: "memory",
|
|
Value: 50 + float64(i%20),
|
|
Timestamp: timestamp,
|
|
})
|
|
metrics = append(metrics, models.MetricData{
|
|
Name: "disk",
|
|
Value: 45 + float64(i%10),
|
|
Timestamp: timestamp,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: metrics,
|
|
})
|
|
}
|