Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+376
View File
@@ -0,0 +1,376 @@
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)
// 获取本机 IPIPv4 + 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
}