package handler import ( "net" "net/http" "os" "path/filepath" "runtime" "strconv" "time" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/model" sqlite "git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/mem" gopsutilNet "github.com/shirou/gopsutil/v4/net" "go.uber.org/zap" ) // DashboardHandler Dashboard Handler type DashboardHandler struct { logger *zap.Logger store *sqlite.Store } // NewDashboardHandler 创建 Dashboard Handler func NewDashboardHandler(store *sqlite.Store, logger *zap.Logger) *DashboardHandler { return &DashboardHandler{ logger: logger, store: store, } } // GetStats 获取统计数据 func (h *DashboardHandler) GetStats(c *gin.Context) { var deviceCount, networkCount, onlineCount int64 // 统计设备数量 h.store.DB().Model(&model.Device{}).Count(&deviceCount) // 统计网络数量 h.store.DB().Model(&model.Network{}).Count(&networkCount) // 统计在线设备数量 h.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount) c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "total_devices": deviceCount, "total_networks": networkCount, "online_devices": onlineCount, }, }) } // GetRecentLogs 获取最近日志 func (h *DashboardHandler) GetRecentLogs(c *gin.Context) { limitStr := c.DefaultQuery("limit", "10") limit := 10 if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } // 从数据库查询最近的 AuditLog var logs []model.AuditLog if err := h.store.DB(). Order("created_at DESC"). Limit(limit). Find(&logs).Error; err != nil { h.logger.Error("查询日志失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "查询日志失败", }) return } // 转换为前端格式 type LogEntry struct { ID uint64 `json:"id"` Timestamp string `json:"timestamp"` Message string `json:"message"` Action string `json:"action"` OperatorIP string `json:"operator_ip"` Type string `json:"type"` // info, success, warning, error } logEntries := make([]LogEntry, 0, len(logs)) for _, log := range logs { // 从 Detail 中提取信息构建 Message message := log.Action if log.Detail != "" { message = log.Detail } logEntries = append(logEntries, LogEntry{ ID: uint64(log.ID), Timestamp: log.CreatedAt.Format(time.RFC3339), Message: message, Action: log.Action, OperatorIP: log.OperatorIP, Type: "info", // AuditLog 默认类型 }) } c.JSON(http.StatusOK, gin.H{ "data": logEntries, }) } // GetSystemInfo 获取系统信息 func (h *DashboardHandler) GetSystemInfo(c *gin.Context) { // 获取主机信息 hostInfo, err := host.Info() if err != nil { h.logger.Warn("获取主机信息失败", zap.Error(err)) hostInfo = &host.InfoStat{} // 使用空对象避免 nil 指针 } // 获取 CPU 信息 cpuPercent, _ := cpu.Percent(0, false) cpuUsage := float64(0) if len(cpuPercent) > 0 { cpuUsage = cpuPercent[0] } // 获取内存信息 memInfo, _ := mem.VirtualMemory() // 获取磁盘信息 diskInfo, _ := disk.Usage("/") // Windows 下使用 C:\ if runtime.GOOS == "windows" { diskInfo, _ = disk.Usage("C:/") } // 获取网络 IO netIO, _ := gopsutilNet.IOCounters(false) // 获取本机 IP(IPv4 + IPv6) ipv4, ipv6 := getLocalIPs() ip := ipv4 // 默认返回 IPv4 if ipv4 == "unknown" && ipv6 != "unknown" { ip = ipv6 // 如果只有 IPv6,则返回 IPv6 } // 计算运行时长(小时) uptimeHours := float64(hostInfo.Uptime) / 3600.0 c.JSON(http.StatusOK, gin.H{ "data": gin.H{ // 基础信息 "hostname": hostInfo.Hostname, "os": hostInfo.OS, "platform": hostInfo.Platform, "os_version": hostInfo.PlatformVersion, "kernel": hostInfo.KernelVersion, "arch": runtime.GOARCH, // CPU "cpu_count": runtime.NumCPU(), "cpu_usage": cpuUsage, "cpu_model": getCPUModel(), // 从 cpu.Info() 获取 // 内存 "memory_total": int(memInfo.Total / 1024 / 1024), // MB "memory_used": int(memInfo.Used / 1024 / 1024), // MB "memory_percent": memInfo.UsedPercent, "memory_alloc": int(runtime.MemStats{}.Alloc / 1024 / 1024), // Go 内存 // 磁盘 "disk_total": int(diskInfo.Total / 1024 / 1024 / 1024), // GB "disk_used": int(diskInfo.Used / 1024 / 1024 / 1024), // GB "disk_percent": diskInfo.UsedPercent, // 网络 "net_sent": netIO[0].BytesSent, "net_recv": netIO[0].BytesRecv, // 运行时长 "boot_time": formatBootTime(hostInfo), "uptime": uptimeHours, // IP 地址(同时返回 IPv4 和 IPv6) "ip": ip, // 保持向后兼容 "ipv4": ipv4, // IPv4 地址 "ipv6": ipv6, // IPv6 地址 }, }) } // getLocalIPs 获取本机 IPv4 和 IPv6 地址 func getLocalIPs() (ipv4, ipv6 string) { addrs, err := net.InterfaceAddrs() if err != nil { return "unknown", "unknown" } for _, addr := range addrs { if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { // 检查是否为公网地址(可选,目前保留私有地址) if ipNet.IP.To4() != nil { // 优先选择非链路本地地址 if ipv4 == "" || !ipNet.IP.IsLinkLocalUnicast() { ipv4 = ipNet.IP.String() } } else if ipNet.IP.To16() != nil { // IPv6 if ipv6 == "" || !ipNet.IP.IsLinkLocalUnicast() { ipv6 = ipNet.IP.String() } } } } if ipv4 == "" { ipv4 = "unknown" } if ipv6 == "" { ipv6 = "unknown" } return ipv4, ipv6 } // formatBootTime 格式化启动时间 func formatBootTime(hostInfo *host.InfoStat) string { if hostInfo == nil || hostInfo.BootTime == 0 { return "unknown" } return time.Unix(int64(hostInfo.BootTime), 0).Format("2006-01-02 15:04:05") } // GetLinkDistribution 获取链路分布 func (h *DashboardHandler) GetLinkDistribution(c *gin.Context) { // 查询所有网络 var networks []model.Network if err := h.store.DB().Find(&networks).Error; err != nil { h.logger.Error("查询网络失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "查询网络失败", }) return } // 构建链路分布数据 distribution := make([]gin.H, 0, len(networks)) totalP2P := 0 totalRelay := 0 for _, network := range networks { // 查询该网络下的所有设备 var devices []model.Device if err := h.store.DB().Where("network_id = ?", network.ID).Find(&devices).Error; err != nil { h.logger.Warn("查询设备失败", zap.Uint64("network_id", network.ID), zap.Error(err)) continue } // 统计 P2P 和 Relay 连接 p2pCount := 0 relayCount := 0 for _, device := range devices { // 根据 Endpoint 判断是 P2P 还是 Relay // 如果 Endpoint 为空或为内网地址,认为是 P2P // 如果 Endpoint 包含中继端口(如 53493),认为是 Relay if device.Endpoint == "" { p2pCount++ } else if isRelayEndpoint(device.Endpoint) { relayCount++ } else { p2pCount++ } } totalP2P += p2pCount totalRelay += relayCount distribution = append(distribution, gin.H{ "network_id": network.ID, "network_name": network.Name, "p2p_count": p2pCount, "relay_count": relayCount, "total_peers": len(devices), "mode": network.Mode, // native / userspace }) } // 返回总体统计和详细分布 c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "summary": gin.H{ "total_p2p": totalP2P, "total_relay": totalRelay, "total": totalP2P + totalRelay, "p2p_percent": calculatePercent(totalP2P, totalP2P+totalRelay), }, "by_network": distribution, }, }) } // isRelayEndpoint 判断是否为中继端点 func isRelayEndpoint(endpoint string) bool { // 简单的启发式判断:如果端口在常见 TURN 端口范围内 _, port, err := net.SplitHostPort(endpoint) if err != nil { return false } // 常见 TURN/Relay 端口 relayPorts := []string{"53493", "53494", "53495", "3478", "5349"} for _, p := range relayPorts { if port == p { return true } } return false } // calculatePercent 计算百分比 func calculatePercent(part, total int) float64 { if total == 0 { return 0 } return float64(part) / float64(total) * 100 } // ClearLogs 清理日志文件 func (h *DashboardHandler) ClearLogs(c *gin.Context) { logPath := "./logs" // 检查日志目录是否存在 if _, err := os.Stat(logPath); os.IsNotExist(err) { c.JSON(http.StatusOK, gin.H{ "message": "日志目录不存在", }) return } // 读取所有日志文件 files, err := filepath.Glob(filepath.Join(logPath, "*.log")) if err != nil { h.logger.Error("读取日志文件失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "读取日志文件失败", }) return } // 删除所有日志文件 deletedCount := 0 for _, file := range files { if err := os.Remove(file); err != nil { h.logger.Warn("删除日志文件失败", zap.String("file", file), zap.Error(err)) continue } deletedCount++ } h.logger.Info("清理日志完成", zap.Int("deleted", deletedCount)) c.JSON(http.StatusOK, gin.H{ "message": "日志清理完成", "deleted": deletedCount, }) } // getCPUModel 获取 CPU 型号 func getCPUModel() string { cpuInfos, err := cpu.Info() if err != nil || len(cpuInfos) == 0 { return "Unknown" } return cpuInfos[0].ModelName }