feat: 管理后台仪表盘统计卡片接入真实数据
新增 /api/admin/stats 聚合接口,填充运行时长、活跃设备、今日流量、在线节点四项卡片: - VpnService 记录启动时间,提供 StartedAt/TotalLiveTraffic 方法 - 新增 TrafficStat 模型,客户端断开时 upsert 当日 rx/tx 流量(持久化) - 前端 AdminView 调用 stats 接口并每 30 秒自动刷新
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@ func Init(cfg *config.DatabaseConfig) error {
|
||||
return fmt.Errorf("数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
if err := DB.AutoMigrate(&model.User{}, &model.Session{}, &model.VpnSetting{}, &model.VpnReservation{}); err != nil {
|
||||
if err := DB.AutoMigrate(&model.User{}, &model.Session{}, &model.VpnSetting{}, &model.VpnReservation{}, &model.TrafficStat{}); err != nil {
|
||||
return fmt.Errorf("数据库迁移失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"lmvpn/internal/db"
|
||||
"lmvpn/internal/model"
|
||||
"lmvpn/internal/vpn"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type adminStatsResponse struct {
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
ActiveDevices int `json:"active_devices"`
|
||||
TodayTrafficBytes int64 `json:"today_traffic_bytes"`
|
||||
OnlineNodes int `json:"online_nodes"`
|
||||
}
|
||||
|
||||
func GetAdminStats(c *gin.Context) {
|
||||
var uptime int64
|
||||
var onlineNodes int
|
||||
if vpn.VPN.Running() {
|
||||
uptime = int64(time.Since(vpn.VPN.StartedAt()).Seconds())
|
||||
onlineNodes = 1
|
||||
}
|
||||
|
||||
activeDevices := len(vpn.VPN.ClientList())
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var stat model.TrafficStat
|
||||
db.DB.Where("date = ?", today).First(&stat)
|
||||
liveRx, liveTx := vpn.VPN.TotalLiveTraffic()
|
||||
todayTraffic := stat.RxBytes + stat.TxBytes + liveRx + liveTx
|
||||
|
||||
c.JSON(http.StatusOK, adminStatsResponse{
|
||||
UptimeSeconds: uptime,
|
||||
ActiveDevices: activeDevices,
|
||||
TodayTrafficBytes: todayTraffic,
|
||||
OnlineNodes: onlineNodes,
|
||||
})
|
||||
}
|
||||
@@ -32,3 +32,15 @@ type VpnReservation struct {
|
||||
func (VpnReservation) TableName() string {
|
||||
return "vpn_reservations"
|
||||
}
|
||||
|
||||
type TrafficStat struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
Date string `gorm:"uniqueIndex;size:10;not null"`
|
||||
RxBytes int64 `gorm:"default:0"`
|
||||
TxBytes int64 `gorm:"default:0"`
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (TrafficStat) TableName() string {
|
||||
return "traffic_stats"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func Setup(r *gin.Engine) {
|
||||
admin := r.Group("/api/admin")
|
||||
admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware())
|
||||
{
|
||||
admin.GET("/stats", handler.GetAdminStats)
|
||||
admin.GET("/users/count", handler.GetUserCount)
|
||||
admin.GET("/users", handler.ListUsers)
|
||||
admin.POST("/users", handler.CreateUser)
|
||||
|
||||
+32
-13
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lmvpn/internal/model"
|
||||
|
||||
@@ -13,21 +14,22 @@ import (
|
||||
)
|
||||
|
||||
type VpnService struct {
|
||||
mu sync.RWMutex
|
||||
settings model.VpnSetting
|
||||
net *net.IPNet
|
||||
serverIP net.IP
|
||||
prefix int
|
||||
alloc *AllocationManager
|
||||
net6 *net.IPNet
|
||||
mu sync.RWMutex
|
||||
settings model.VpnSetting
|
||||
net *net.IPNet
|
||||
serverIP net.IP
|
||||
prefix int
|
||||
alloc *AllocationManager
|
||||
net6 *net.IPNet
|
||||
serverIP6 net.IP
|
||||
prefix6 int
|
||||
alloc6 *AllocationManager
|
||||
switchx *PacketSwitch
|
||||
tun *TUNInterface
|
||||
tunDone chan struct{}
|
||||
running bool
|
||||
clients map[*tunnelConn]struct{}
|
||||
alloc6 *AllocationManager
|
||||
switchx *PacketSwitch
|
||||
tun *TUNInterface
|
||||
tunDone chan struct{}
|
||||
running bool
|
||||
startedAt time.Time
|
||||
clients map[*tunnelConn]struct{}
|
||||
}
|
||||
|
||||
func NewVpnService() *VpnService {
|
||||
@@ -42,6 +44,22 @@ func (s *VpnService) Running() bool {
|
||||
return s.running
|
||||
}
|
||||
|
||||
func (s *VpnService) StartedAt() time.Time {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.startedAt
|
||||
}
|
||||
|
||||
func (s *VpnService) TotalLiveTraffic() (rx, tx int64) {
|
||||
s.mu.RLock()
|
||||
for c := range s.clients {
|
||||
rx += c.rxBytes.Load()
|
||||
tx += c.txBytes.Load()
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *VpnService) Settings() model.VpnSetting {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -133,6 +151,7 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations4, res
|
||||
s.tun = tun
|
||||
s.tunDone = make(chan struct{})
|
||||
s.running = true
|
||||
s.startedAt = time.Now()
|
||||
s.mu.Unlock()
|
||||
|
||||
go s.serveTUN()
|
||||
|
||||
+35
-12
@@ -8,9 +8,12 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"lmvpn/internal/db"
|
||||
"lmvpn/internal/model"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,19 +31,19 @@ var (
|
||||
)
|
||||
|
||||
type tunnelConn struct {
|
||||
conn *websocket.Conn
|
||||
user *model.User
|
||||
svc *VpnService
|
||||
assignedIP net.IP
|
||||
assignedIP6 net.IP
|
||||
connectedAt time.Time
|
||||
writeMu sync.Mutex
|
||||
ready atomic.Bool
|
||||
rxBytes atomic.Int64
|
||||
txBytes atomic.Int64
|
||||
conn *websocket.Conn
|
||||
user *model.User
|
||||
svc *VpnService
|
||||
assignedIP net.IP
|
||||
assignedIP6 net.IP
|
||||
connectedAt time.Time
|
||||
writeMu sync.Mutex
|
||||
ready atomic.Bool
|
||||
rxBytes atomic.Int64
|
||||
txBytes atomic.Int64
|
||||
}
|
||||
|
||||
func (c *tunnelConn) AssignedIP() net.IP { return c.assignedIP }
|
||||
func (c *tunnelConn) AssignedIP() net.IP { return c.assignedIP }
|
||||
func (c *tunnelConn) AssignedIP6() net.IP { return c.assignedIP6 }
|
||||
|
||||
func (c *tunnelConn) WritePacket(data []byte) error {
|
||||
@@ -128,7 +131,10 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
|
||||
}
|
||||
|
||||
VPN.registerClient(tc)
|
||||
defer VPN.unregisterClient(tc)
|
||||
defer func() {
|
||||
recordTraffic(tc.rxBytes.Load(), tc.txBytes.Load())
|
||||
VPN.unregisterClient(tc)
|
||||
}()
|
||||
|
||||
settings := VPN.Settings()
|
||||
initMsg := initMessage{
|
||||
@@ -222,3 +228,20 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func recordTraffic(rx, tx int64) {
|
||||
if rx == 0 && tx == 0 {
|
||||
return
|
||||
}
|
||||
today := time.Now().Format("2006-01-02")
|
||||
stat := model.TrafficStat{Date: today, RxBytes: rx, TxBytes: tx}
|
||||
if err := db.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "date"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"rx_bytes": gorm.Expr("rx_bytes + ?", rx),
|
||||
"tx_bytes": gorm.Expr("tx_bytes + ?", tx),
|
||||
}),
|
||||
}).Create(&stat).Error; err != nil {
|
||||
log.Printf("记录流量失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user