feat(app): 添加流量统计功能和服务器配置管理
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m48s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m53s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m40s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m36s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m18s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m48s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m53s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m40s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m36s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m18s
- 在WebServer中添加TrafficStore存储接口 - 将Web配置从根级别移动到Server.Web子结构下 - 移除Web配置中的BindAddr字段并调整默认值逻辑 - 在前端HomeView中替换模拟流量数据显示真实统计数据 - 添加流量统计API接口(/traffic/stats和/traffic/hourly) - 实现SQLite数据库流量统计表创建和CRUD操作 - 在Relay包中添加带流量统计的数据转发功能 - 在设置页面添加服务器配置编辑和保存功能 - 创建流量统计处理器和相关数据模型定义
This commit is contained in:
@@ -21,16 +21,18 @@ type WebServer struct {
|
||||
Config *config.ServerConfig
|
||||
ConfigPath string
|
||||
JSPluginStore db.JSPluginStore
|
||||
TrafficStore db.TrafficStore
|
||||
}
|
||||
|
||||
// NewWebServer 创建Web服务
|
||||
func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string, jsStore db.JSPluginStore) *WebServer {
|
||||
func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string, store db.Store) *WebServer {
|
||||
return &WebServer{
|
||||
ClientStore: cs,
|
||||
Server: srv,
|
||||
Config: cfg,
|
||||
ConfigPath: cfgPath,
|
||||
JSPluginStore: jsStore,
|
||||
JSPluginStore: store,
|
||||
TrafficStore: store,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,3 +111,8 @@ func (w *WebServer) SaveConfig() error {
|
||||
func (w *WebServer) GetJSPluginStore() db.JSPluginStore {
|
||||
return w.JSPluginStore
|
||||
}
|
||||
|
||||
// GetTrafficStore 获取流量存储
|
||||
func (w *WebServer) GetTrafficStore() db.TrafficStore {
|
||||
return w.TrafficStore
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
// ServerConfig 服务端配置
|
||||
type ServerConfig struct {
|
||||
Server ServerSettings `yaml:"server"`
|
||||
Web WebSettings `yaml:"web"`
|
||||
PluginStore PluginStoreSettings `yaml:"plugin_store"`
|
||||
JSPlugins []JSPluginConfig `yaml:"js_plugins,omitempty"`
|
||||
}
|
||||
@@ -44,19 +43,19 @@ func (s *PluginStoreSettings) GetPluginStoreURL() string {
|
||||
|
||||
// ServerSettings 服务端设置
|
||||
type ServerSettings struct {
|
||||
BindAddr string `yaml:"bind_addr"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
Token string `yaml:"token"`
|
||||
HeartbeatSec int `yaml:"heartbeat_sec"`
|
||||
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
|
||||
DBPath string `yaml:"db_path"`
|
||||
TLSDisabled bool `yaml:"tls_disabled"` // 默认启用 TLS,设置为 true 禁用
|
||||
BindAddr string `yaml:"bind_addr"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
Token string `yaml:"token"`
|
||||
HeartbeatSec int `yaml:"heartbeat_sec"`
|
||||
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
|
||||
DBPath string `yaml:"db_path"`
|
||||
TLSDisabled bool `yaml:"tls_disabled"`
|
||||
Web WebSettings `yaml:"web"`
|
||||
}
|
||||
|
||||
// WebSettings Web控制台设置
|
||||
type WebSettings struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
BindAddr string `yaml:"bind_addr"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
@@ -99,12 +98,9 @@ func setDefaults(cfg *ServerConfig) {
|
||||
}
|
||||
|
||||
// Web 默认启用
|
||||
if cfg.Web.BindAddr == "" {
|
||||
cfg.Web.BindAddr = "0.0.0.0"
|
||||
}
|
||||
if cfg.Web.BindPort == 0 {
|
||||
cfg.Web.BindPort = 7500
|
||||
cfg.Web.Enabled = true
|
||||
if cfg.Server.Web.BindPort == 0 {
|
||||
cfg.Server.Web.BindPort = 7500
|
||||
cfg.Server.Web.Enabled = true
|
||||
}
|
||||
|
||||
// Token 未配置时自动生成 32 位
|
||||
@@ -126,11 +122,11 @@ func generateToken(length int) string {
|
||||
|
||||
// GenerateWebCredentials 生成 Web 控制台凭据
|
||||
func GenerateWebCredentials(cfg *ServerConfig) bool {
|
||||
if cfg.Web.Username == "" {
|
||||
cfg.Web.Username = "admin"
|
||||
if cfg.Server.Web.Username == "" {
|
||||
cfg.Server.Web.Username = "admin"
|
||||
}
|
||||
if cfg.Web.Password == "" {
|
||||
cfg.Web.Password = generateToken(16)
|
||||
if cfg.Server.Web.Password == "" {
|
||||
cfg.Server.Web.Password = generateToken(16)
|
||||
return true // 表示生成了新密码
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -76,5 +76,21 @@ type JSPluginStore interface {
|
||||
type Store interface {
|
||||
ClientStore
|
||||
JSPluginStore
|
||||
TrafficStore
|
||||
Close() error
|
||||
}
|
||||
|
||||
// TrafficRecord 流量记录
|
||||
type TrafficRecord struct {
|
||||
Timestamp int64 `json:"timestamp"` // Unix 时间戳(小时级别)
|
||||
Inbound int64 `json:"inbound"` // 入站流量(字节)
|
||||
Outbound int64 `json:"outbound"` // 出站流量(字节)
|
||||
}
|
||||
|
||||
// TrafficStore 流量存储接口
|
||||
type TrafficStore interface {
|
||||
AddTraffic(inbound, outbound int64) error
|
||||
GetTotalTraffic() (inbound, outbound int64, err error)
|
||||
Get24HourTraffic() (inbound, outbound int64, err error)
|
||||
GetHourlyTraffic(hours int) ([]TrafficRecord, error)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
@@ -80,6 +81,33 @@ func (s *SQLiteStore) init() error {
|
||||
// 迁移:添加 updated_at 列
|
||||
s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP`)
|
||||
|
||||
// 创建流量统计表
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS traffic_stats (
|
||||
hour_ts INTEGER PRIMARY KEY,
|
||||
inbound INTEGER NOT NULL DEFAULT 0,
|
||||
outbound INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建总流量表
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS traffic_total (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
inbound INTEGER NOT NULL DEFAULT 0,
|
||||
outbound INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化总流量记录
|
||||
s.db.Exec(`INSERT OR IGNORE INTO traffic_total (id, inbound, outbound) VALUES (1, 0, 0)`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,3 +343,82 @@ func (s *SQLiteStore) UpdateJSPluginConfig(name string, config map[string]string
|
||||
_, err := s.db.Exec(`UPDATE js_plugins SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?`, string(configJSON), name)
|
||||
return err
|
||||
}
|
||||
|
||||
// ========== 流量统计方法 ==========
|
||||
|
||||
// getHourTimestamp 获取当前小时的时间戳
|
||||
func getHourTimestamp() int64 {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()).Unix()
|
||||
}
|
||||
|
||||
// AddTraffic 添加流量记录
|
||||
func (s *SQLiteStore) AddTraffic(inbound, outbound int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
hourTs := getHourTimestamp()
|
||||
|
||||
// 更新小时统计
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO traffic_stats (hour_ts, inbound, outbound) VALUES (?, ?, ?)
|
||||
ON CONFLICT(hour_ts) DO UPDATE SET inbound = inbound + ?, outbound = outbound + ?
|
||||
`, hourTs, inbound, outbound, inbound, outbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新总流量
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE traffic_total SET inbound = inbound + ?, outbound = outbound + ? WHERE id = 1
|
||||
`, inbound, outbound)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTotalTraffic 获取总流量
|
||||
func (s *SQLiteStore) GetTotalTraffic() (inbound, outbound int64, err error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
err = s.db.QueryRow(`SELECT inbound, outbound FROM traffic_total WHERE id = 1`).Scan(&inbound, &outbound)
|
||||
return
|
||||
}
|
||||
|
||||
// Get24HourTraffic 获取24小时流量
|
||||
func (s *SQLiteStore) Get24HourTraffic() (inbound, outbound int64, err error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
cutoff := time.Now().Add(-24 * time.Hour).Unix()
|
||||
err = s.db.QueryRow(`
|
||||
SELECT COALESCE(SUM(inbound), 0), COALESCE(SUM(outbound), 0)
|
||||
FROM traffic_stats WHERE hour_ts >= ?
|
||||
`, cutoff).Scan(&inbound, &outbound)
|
||||
return
|
||||
}
|
||||
|
||||
// GetHourlyTraffic 获取每小时流量记录
|
||||
func (s *SQLiteStore) GetHourlyTraffic(hours int) ([]TrafficRecord, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
cutoff := time.Now().Add(-time.Duration(hours) * time.Hour).Unix()
|
||||
rows, err := s.db.Query(`
|
||||
SELECT hour_ts, inbound, outbound FROM traffic_stats
|
||||
WHERE hour_ts >= ? ORDER BY hour_ts ASC
|
||||
`, cutoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []TrafficRecord
|
||||
for rows.Next() {
|
||||
var r TrafficRecord
|
||||
if err := rows.Scan(&r.Timestamp, &r.Inbound, &r.Outbound); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, r)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ type ServerConfigPart struct {
|
||||
// @Description Web 控制台配置
|
||||
type WebConfigPart struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BindAddr string `json:"bind_addr" binding:"omitempty"`
|
||||
BindPort int `json:"bind_port" binding:"omitempty,min=1,max=65535"`
|
||||
Username string `json:"username" binding:"omitempty,min=3,max=32"`
|
||||
Password string `json:"password" binding:"omitempty,min=6,max=64"`
|
||||
@@ -46,7 +45,6 @@ type ServerConfigInfo struct {
|
||||
// WebConfigInfo Web 配置信息
|
||||
type WebConfigInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BindAddr string `json:"bind_addr"`
|
||||
BindPort int `json:"bind_port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"` // 显示为 ****
|
||||
|
||||
@@ -42,10 +42,9 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
HeartbeatTimeout: cfg.Server.HeartbeatTimeout,
|
||||
},
|
||||
Web: dto.WebConfigInfo{
|
||||
Enabled: cfg.Web.Enabled,
|
||||
BindAddr: cfg.Web.BindAddr,
|
||||
BindPort: cfg.Web.BindPort,
|
||||
Username: cfg.Web.Username,
|
||||
Enabled: cfg.Server.Web.Enabled,
|
||||
BindPort: cfg.Server.Web.BindPort,
|
||||
Username: cfg.Server.Web.Username,
|
||||
Password: "****",
|
||||
},
|
||||
}
|
||||
@@ -93,15 +92,12 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
|
||||
// 更新 Web 配置
|
||||
if req.Web != nil {
|
||||
cfg.Web.Enabled = req.Web.Enabled
|
||||
if req.Web.BindAddr != "" {
|
||||
cfg.Web.BindAddr = req.Web.BindAddr
|
||||
}
|
||||
cfg.Server.Web.Enabled = req.Web.Enabled
|
||||
if req.Web.BindPort > 0 {
|
||||
cfg.Web.BindPort = req.Web.BindPort
|
||||
cfg.Server.Web.BindPort = req.Web.BindPort
|
||||
}
|
||||
cfg.Web.Username = req.Web.Username
|
||||
cfg.Web.Password = req.Web.Password
|
||||
cfg.Server.Web.Username = req.Web.Username
|
||||
cfg.Server.Web.Password = req.Web.Password
|
||||
}
|
||||
|
||||
if err := h.app.SaveConfig(); err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ type AppInterface interface {
|
||||
GetConfigPath() string
|
||||
SaveConfig() error
|
||||
GetJSPluginStore() db.JSPluginStore
|
||||
GetTrafficStore() db.TrafficStore
|
||||
}
|
||||
|
||||
// ServerInterface 服务端接口
|
||||
|
||||
76
internal/server/router/handler/traffic.go
Normal file
76
internal/server/router/handler/traffic.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TrafficHandler 流量统计处理器
|
||||
type TrafficHandler struct {
|
||||
app AppInterface
|
||||
}
|
||||
|
||||
// NewTrafficHandler 创建流量统计处理器
|
||||
func NewTrafficHandler(app AppInterface) *TrafficHandler {
|
||||
return &TrafficHandler{app: app}
|
||||
}
|
||||
|
||||
// GetStats 获取流量统计
|
||||
// @Summary 获取流量统计
|
||||
// @Description 获取24小时和总流量统计
|
||||
// @Tags 流量
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} Response
|
||||
// @Router /api/traffic/stats [get]
|
||||
func (h *TrafficHandler) GetStats(c *gin.Context) {
|
||||
store := h.app.GetTrafficStore()
|
||||
|
||||
// 获取24小时流量
|
||||
in24h, out24h, err := store.Get24HourTraffic()
|
||||
if err != nil {
|
||||
InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取总流量
|
||||
inTotal, outTotal, err := store.GetTotalTraffic()
|
||||
if err != nil {
|
||||
InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"traffic_24h": gin.H{
|
||||
"inbound": in24h,
|
||||
"outbound": out24h,
|
||||
},
|
||||
"traffic_total": gin.H{
|
||||
"inbound": inTotal,
|
||||
"outbound": outTotal,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetHourly 获取每小时流量
|
||||
// @Summary 获取每小时流量
|
||||
// @Description 获取最近N小时的流量记录
|
||||
// @Tags 流量
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param hours query int false "小时数" default(24)
|
||||
// @Success 200 {object} Response
|
||||
// @Router /api/traffic/hourly [get]
|
||||
func (h *TrafficHandler) GetHourly(c *gin.Context) {
|
||||
hours := 24
|
||||
|
||||
store := h.app.GetTrafficStore()
|
||||
records, err := store.GetHourlyTraffic(hours)
|
||||
if err != nil {
|
||||
InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"records": records,
|
||||
})
|
||||
}
|
||||
@@ -110,6 +110,11 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
|
||||
logHandler := handler.NewLogHandler(app)
|
||||
api.GET("/client/:id/logs", logHandler.StreamLogs)
|
||||
|
||||
// 流量统计
|
||||
trafficHandler := handler.NewTrafficHandler(app)
|
||||
api.GET("/traffic/stats", trafficHandler.GetStats)
|
||||
api.GET("/traffic/hourly", trafficHandler.GetHourly)
|
||||
|
||||
// 插件 API 代理 (通过 Web API 访问客户端插件)
|
||||
pluginAPIHandler := handler.NewPluginAPIHandler(app)
|
||||
api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest)
|
||||
|
||||
Reference in New Issue
Block a user