377 lines
9.3 KiB
Go
377 lines
9.3 KiB
Go
package handler
|
||
|
||
import (
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strconv"
|
||
"time"
|
||
|
||
"git.zkcoi.com/zkcoi/meshray/internal/model"
|
||
sqlite "git.zkcoi.com/zkcoi/meshray/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
|
||
}
|