diff --git a/internal/app.go b/internal/app.go
index 269bf57..7e1073f 100644
--- a/internal/app.go
+++ b/internal/app.go
@@ -445,7 +445,7 @@ func startTrafficResetCheck(ctx context.Context, components *AppComponents, logg
logger.Info("流量重置检查任务已停止")
return
case <-ticker.C:
- if err := components.AgentService.CheckAndResetTraffic(ctx); err != nil {
+ if err := components.TrafficService.CheckAndResetTraffic(ctx); err != nil {
logger.Error("流量重置检查失败", zap.Error(err))
}
}
diff --git a/internal/handler/agent_admin_handler.go b/internal/handler/agent_admin_handler.go
index 2ff597a..5479de6 100644
--- a/internal/handler/agent_admin_handler.go
+++ b/internal/handler/agent_admin_handler.go
@@ -268,7 +268,9 @@ func (h *AgentHandler) UpdateTrafficConfig(c echo.Context) error {
var req struct {
Enabled bool `json:"enabled"`
+ Type string `json:"type"` // 流量类型: recv/send/both
Limit uint64 `json:"limit"`
+ Used uint64 `json:"used"` // 手动设置已使用流量(可选)
ResetDay int `json:"resetDay"`
}
@@ -277,7 +279,7 @@ func (h *AgentHandler) UpdateTrafficConfig(c echo.Context) error {
}
ctx := c.Request().Context()
- if err := h.agentService.UpdateTrafficConfig(ctx, agentID, req.Enabled, req.Limit, req.ResetDay); err != nil {
+ if err := h.trafficService.UpdateTrafficConfig(ctx, agentID, req.Enabled, req.Type, req.Limit, req.ResetDay, req.Used); err != nil {
return err
}
@@ -289,7 +291,7 @@ func (h *AgentHandler) GetTrafficStats(c echo.Context) error {
agentID := c.Param("id")
ctx := c.Request().Context()
- stats, err := h.agentService.GetTrafficStats(ctx, agentID)
+ stats, err := h.trafficService.GetTrafficStats(ctx, agentID)
if err != nil {
return err
}
@@ -302,7 +304,7 @@ func (h *AgentHandler) ResetAgentTraffic(c echo.Context) error {
agentID := c.Param("id")
ctx := c.Request().Context()
- if err := h.agentService.ResetAgentTraffic(ctx, agentID); err != nil {
+ if err := h.trafficService.ResetAgentTraffic(ctx, agentID); err != nil {
return err
}
diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go
index 6c8781a..6595021 100644
--- a/internal/handler/agent_handler.go
+++ b/internal/handler/agent_handler.go
@@ -10,6 +10,7 @@ import (
type AgentHandler struct {
logger *zap.Logger
agentService *service.AgentService
+ trafficService *service.TrafficService
metricService *service.MetricService
monitorSvc *service.MonitorService
tamperService *service.TamperService
@@ -19,13 +20,14 @@ type AgentHandler struct {
upgrader websocket.Upgrader
}
-func NewAgentHandler(logger *zap.Logger, agentService *service.AgentService, metricService *service.MetricService,
- monitorService *service.MonitorService, tamperService *service.TamperService, ddnsService *service.DDNSService,
- sshLoginService *service.SSHLoginService, wsManager *ws.Manager) *AgentHandler {
+func NewAgentHandler(logger *zap.Logger, agentService *service.AgentService, trafficService *service.TrafficService,
+ metricService *service.MetricService, monitorService *service.MonitorService, tamperService *service.TamperService,
+ ddnsService *service.DDNSService, sshLoginService *service.SSHLoginService, wsManager *ws.Manager) *AgentHandler {
h := &AgentHandler{
logger: logger,
agentService: agentService,
+ trafficService: trafficService,
metricService: metricService,
monitorSvc: monitorService,
tamperService: tamperService,
diff --git a/internal/models/agent.go b/internal/models/agent.go
index 7d25419..f8001ca 100644
--- a/internal/models/agent.go
+++ b/internal/models/agent.go
@@ -34,11 +34,13 @@ type Agent struct {
// TrafficStatsData 流量统计数据
type TrafficStatsData struct {
Enabled bool `json:"enabled"` // 是否启用
+ Type string `json:"type"` // 统计类型: "recv"进站, "send"出站, "both"全部
Limit uint64 `json:"limit"` // 流量限额(字节), 0表示不限制
Used uint64 `json:"used"` // 当前周期已使用流量(字节)
ResetDay int `json:"resetDay"` // 流量重置日期(1-31), 0表示不自动重置
PeriodStart int64 `json:"periodStart"` // 当前周期开始时间(时间戳毫秒)
BaselineRecv uint64 `json:"baselineRecv"` // 当前周期流量基线(BytesRecvTotal)
+ BaselineSend uint64 `json:"baselineSend"` // 当前周期流量基线(BytesSentTotal)
AlertSent80 bool `json:"alertSent80"` // 是否已发送80%告警
AlertSent90 bool `json:"alertSent90"` // 是否已发送90%告警
AlertSent100 bool `json:"alertSent100"` // 是否已发送100%告警
diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go
index 8a96376..993b611 100644
--- a/internal/service/agent_service.go
+++ b/internal/service/agent_service.go
@@ -13,7 +13,6 @@ import (
"github.com/dushixiang/pika/internal/repo"
"github.com/go-orz/orz"
"go.uber.org/zap"
- "gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -478,222 +477,3 @@ func (s *AgentService) InitStatus(ctx context.Context) error {
}
return nil
}
-
-// UpdateTrafficConfig 更新流量配置
-func (s *AgentService) UpdateTrafficConfig(ctx context.Context, agentID string, enabled bool, limit uint64, resetDay int) error {
- if resetDay < 0 || resetDay > 31 {
- return fmt.Errorf("重置日期必须在0-31之间")
- }
-
- agent, err := s.AgentRepo.FindById(ctx, agentID)
- if err != nil {
- return err
- }
-
- now := time.Now().UnixMilli()
- stats := agent.TrafficStats.Data()
- oldResetDay := stats.ResetDay
- oldEnabled := stats.Enabled
-
- // 设置配置
- stats.Enabled = enabled
- stats.Limit = limit
- stats.ResetDay = resetDay
-
- // 如果是首次启用、禁用后重新启用,或修改重置日期,重置流量统计
- if (!oldEnabled && stats.Enabled) || (stats.Enabled && resetDay != oldResetDay) || (stats.Enabled && stats.PeriodStart == 0) {
- stats.Used = 0
- stats.PeriodStart = now
- stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
- stats.AlertSent80 = false
- stats.AlertSent90 = false
- stats.AlertSent100 = false
- }
-
- // 如果禁用流量统计,清空相关数据
- if !stats.Enabled {
- stats.Used = 0
- stats.PeriodStart = 0
- stats.BaselineRecv = 0
- stats.AlertSent80 = false
- stats.AlertSent90 = false
- stats.AlertSent100 = false
- }
-
- agent.TrafficStats = datatypes.NewJSONType(stats)
- agent.UpdatedAt = now
- return s.AgentRepo.UpdateById(ctx, &agent)
-}
-
-// GetTrafficStats 获取流量统计信息
-func (s *AgentService) GetTrafficStats(ctx context.Context, agentID string) (*TrafficStats, error) {
- agent, err := s.AgentRepo.FindById(ctx, agentID)
- if err != nil {
- return nil, err
- }
-
- trafficData := agent.TrafficStats.Data()
-
- stats := &TrafficStats{
- Enabled: trafficData.Enabled,
- Limit: trafficData.Limit,
- Used: trafficData.Used,
- ResetDay: trafficData.ResetDay,
- PeriodStart: trafficData.PeriodStart,
- Alerts: TrafficAlerts{
- Sent80: trafficData.AlertSent80,
- Sent90: trafficData.AlertSent90,
- Sent100: trafficData.AlertSent100,
- },
- }
-
- // 计算使用百分比
- if trafficData.Limit > 0 {
- stats.UsedPercent = float64(trafficData.Used) / float64(trafficData.Limit) * 100
- if trafficData.Used < trafficData.Limit {
- stats.Remaining = trafficData.Limit - trafficData.Used
- } else {
- stats.Remaining = 0
- }
- }
-
- // 计算下次重置日期和剩余天数
- if trafficData.ResetDay > 0 && trafficData.PeriodStart > 0 {
- periodStart := time.UnixMilli(trafficData.PeriodStart)
- nextReset := calculateNextResetDate(periodStart, trafficData.ResetDay)
- stats.PeriodEnd = nextReset.UnixMilli()
- stats.DaysUntilReset = int(time.Until(nextReset).Hours() / 24)
- if stats.DaysUntilReset < 0 {
- stats.DaysUntilReset = 0
- }
- }
-
- return stats, nil
-}
-
-// ResetAgentTraffic 重置探针流量
-func (s *AgentService) ResetAgentTraffic(ctx context.Context, agentID string) error {
- agent, err := s.AgentRepo.FindById(ctx, agentID)
- if err != nil {
- return err
- }
-
- now := time.Now().UnixMilli()
- stats := agent.TrafficStats.Data()
-
- stats.Used = 0
- stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
- stats.PeriodStart = now
- stats.AlertSent80 = false
- stats.AlertSent90 = false
- stats.AlertSent100 = false
-
- agent.TrafficStats = datatypes.NewJSONType(stats)
- agent.UpdatedAt = now
-
- s.logger.Info("探针流量已重置",
- zap.String("agentId", agentID),
- zap.String("agentName", agent.Name))
-
- return s.AgentRepo.UpdateById(ctx, &agent)
-}
-
-// CheckAndResetTraffic 检查并重置所有到期的探针流量(定时任务调用)
-func (s *AgentService) CheckAndResetTraffic(ctx context.Context) error {
- agents, err := s.AgentRepo.FindAgentsWithTrafficReset(ctx)
- if err != nil {
- return err
- }
-
- now := time.Now()
- resetCount := 0
-
- for _, agent := range agents {
- if s.shouldResetTraffic(&agent, now) {
- if err := s.ResetAgentTraffic(ctx, agent.ID); err != nil {
- s.logger.Error("重置探针流量失败",
- zap.String("agentId", agent.ID),
- zap.Error(err))
- continue
- }
- resetCount++
- }
- }
-
- if resetCount > 0 {
- s.logger.Info("流量重置检查完成", zap.Int("重置数量", resetCount))
- }
-
- return nil
-}
-
-// shouldResetTraffic 判断是否需要重置流量
-func (s *AgentService) shouldResetTraffic(agent *models.Agent, now time.Time) bool {
- stats := agent.TrafficStats.Data()
- // 流量统计未启用、未配置重置日期或周期未开始时不重置
- if !stats.Enabled || stats.ResetDay == 0 || stats.PeriodStart == 0 {
- return false
- }
-
- periodStart := time.UnixMilli(stats.PeriodStart)
- nextReset := calculateNextResetDate(periodStart, stats.ResetDay)
-
- return now.After(nextReset) || now.Equal(nextReset)
-}
-
-// calculateNextResetDate 计算下次重置日期
-func calculateNextResetDate(periodStart time.Time, resetDay int) time.Time {
- year, month, day := periodStart.Date()
- location := periodStart.Location()
-
- // 先计算当月的重置日期
- currentMonthLastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, location).Day()
- currentMonthResetDay := resetDay
- if resetDay > currentMonthLastDay {
- currentMonthResetDay = currentMonthLastDay
- }
- currentMonthReset := time.Date(year, month, currentMonthResetDay, 0, 0, 0, 0, location)
-
- // 如果当月的重置日期还没到(在 periodStart 之后或同一天),返回当月重置日期
- if day < currentMonthResetDay || (day == currentMonthResetDay && periodStart.Before(currentMonthReset)) {
- return currentMonthReset
- }
-
- // 当月重置日期已过,计算下个月的重置日期
- nextMonth := month + 1
- nextYear := year
- if nextMonth > 12 {
- nextMonth = 1
- nextYear++
- }
-
- // 处理月末日期(如2月没有31号)
- nextMonthLastDay := time.Date(nextYear, nextMonth+1, 0, 0, 0, 0, 0, location).Day()
- nextMonthResetDay := resetDay
- if resetDay > nextMonthLastDay {
- nextMonthResetDay = nextMonthLastDay
- }
-
- return time.Date(nextYear, nextMonth, nextMonthResetDay, 0, 0, 0, 0, location)
-}
-
-// TrafficStats 流量统计信息
-type TrafficStats struct {
- Enabled bool `json:"enabled"`
- Limit uint64 `json:"limit"`
- Used uint64 `json:"used"`
- UsedPercent float64 `json:"usedPercent"`
- Remaining uint64 `json:"remaining"`
- ResetDay int `json:"resetDay"`
- PeriodStart int64 `json:"periodStart"`
- PeriodEnd int64 `json:"periodEnd"`
- DaysUntilReset int `json:"daysUntilReset"`
- Alerts TrafficAlerts `json:"alerts"`
-}
-
-// TrafficAlerts 流量告警状态
-type TrafficAlerts struct {
- Sent80 bool `json:"sent80"`
- Sent90 bool `json:"sent90"`
- Sent100 bool `json:"sent100"`
-}
diff --git a/internal/service/metric_service.go b/internal/service/metric_service.go
index 4162786..c9c52bb 100644
--- a/internal/service/metric_service.go
+++ b/internal/service/metric_service.go
@@ -128,7 +128,7 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
TotalInterfaces: len(networkDataList),
}
// 更新流量统计
- if err := s.trafficService.UpdateAgentTraffic(ctx, agentID, totalRecvTotal); err != nil {
+ if err := s.trafficService.UpdateAgentTraffic(ctx, agentID, totalRecvTotal, totalSentTotal); err != nil {
s.logger.Error("更新探针流量统计失败",
zap.String("agentId", agentID),
zap.Error(err))
diff --git a/internal/service/traffic_service.go b/internal/service/traffic_service.go
index b221aa0..f97e22d 100644
--- a/internal/service/traffic_service.go
+++ b/internal/service/traffic_service.go
@@ -27,7 +27,7 @@ func NewTrafficService(logger *zap.Logger, db *gorm.DB) *TrafficService {
}
// UpdateAgentTraffic 更新探针流量统计(每次上报网络指标时调用)
-func (s *TrafficService) UpdateAgentTraffic(ctx context.Context, agentID string, currentRecvTotal uint64) error {
+func (s *TrafficService) UpdateAgentTraffic(ctx context.Context, agentID string, currentRecvTotal, currentSentTotal uint64) error {
agent, err := s.agentRepo.FindById(ctx, agentID)
if err != nil {
return err
@@ -41,9 +41,15 @@ func (s *TrafficService) UpdateAgentTraffic(ctx context.Context, agentID string,
return nil
}
+ // 设置默认类型
+ if stats.Type == "" {
+ stats.Type = "recv" // 默认只统计进站流量,向后兼容
+ }
+
// 初始化基线(首次统计)
- if stats.BaselineRecv == 0 {
+ if stats.BaselineRecv == 0 && stats.BaselineSend == 0 {
stats.BaselineRecv = currentRecvTotal
+ stats.BaselineSend = currentSentTotal
stats.Used = 0
if stats.PeriodStart == 0 {
stats.PeriodStart = time.Now().UnixMilli()
@@ -53,16 +59,38 @@ func (s *TrafficService) UpdateAgentTraffic(ctx context.Context, agentID string,
}
// 检测计数器重置(探针重启)
- if currentRecvTotal < stats.BaselineRecv {
+ recvReset := currentRecvTotal < stats.BaselineRecv
+ sendReset := currentSentTotal < stats.BaselineSend
+
+ if recvReset || sendReset {
s.logger.Warn("检测到流量计数器重置",
zap.String("agentId", agentID),
- zap.Uint64("baseline", stats.BaselineRecv),
- zap.Uint64("current", currentRecvTotal))
- stats.BaselineRecv = currentRecvTotal
+ zap.Uint64("baselineRecv", stats.BaselineRecv),
+ zap.Uint64("currentRecv", currentRecvTotal),
+ zap.Uint64("baselineSend", stats.BaselineSend),
+ zap.Uint64("currentSend", currentSentTotal))
+
+ if recvReset {
+ stats.BaselineRecv = currentRecvTotal
+ }
+ if sendReset {
+ stats.BaselineSend = currentSentTotal
+ }
// 保持 Used 不变,避免丢失已统计的流量
} else {
- // 计算使用量
- stats.Used = currentRecvTotal - stats.BaselineRecv
+ // 根据配置的类型计算使用量
+ switch stats.Type {
+ case "recv": // 只统计进站流量
+ stats.Used = currentRecvTotal - stats.BaselineRecv
+ case "send": // 只统计出站流量
+ stats.Used = currentSentTotal - stats.BaselineSend
+ case "both": // 统计全部流量
+ recvUsed := currentRecvTotal - stats.BaselineRecv
+ sendUsed := currentSentTotal - stats.BaselineSend
+ stats.Used = recvUsed + sendUsed
+ default: // 默认只统计进站流量
+ stats.Used = currentRecvTotal - stats.BaselineRecv
+ }
}
// 检查告警(如果配置了限额)
@@ -153,3 +181,252 @@ func formatBytes(bytes uint64) string {
}
return fmt.Sprintf("%.2f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
+
+// UpdateTrafficConfig 更新流量配置
+// used: 如果大于0,则使用该值作为已使用流量;如果为0,则保持当前值或重置
+func (s *TrafficService) UpdateTrafficConfig(ctx context.Context, agentID string, enabled bool, trafficType string, limit uint64, resetDay int, used uint64) error {
+ if resetDay < 0 || resetDay > 31 {
+ return fmt.Errorf("重置日期必须在0-31之间")
+ }
+
+ // 验证流量类型
+ if trafficType != "" && trafficType != "recv" && trafficType != "send" && trafficType != "both" {
+ return fmt.Errorf("流量类型必须是 recv、send 或 both")
+ }
+
+ agent, err := s.agentRepo.FindById(ctx, agentID)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now().UnixMilli()
+ stats := agent.TrafficStats.Data()
+ oldResetDay := stats.ResetDay
+ oldEnabled := stats.Enabled
+ oldType := stats.Type
+
+ // 设置配置
+ stats.Enabled = enabled
+ stats.Limit = limit
+ stats.ResetDay = resetDay
+
+ // 设置流量类型(默认为 recv)
+ if trafficType != "" {
+ stats.Type = trafficType
+ } else if stats.Type == "" {
+ stats.Type = "recv" // 默认只统计进站流量
+ }
+
+ // 如果是首次启用、禁用后重新启用、修改重置日期或修改流量类型,重置流量统计
+ if (!oldEnabled && stats.Enabled) || (stats.Enabled && resetDay != oldResetDay) || (stats.Enabled && stats.PeriodStart == 0) || (stats.Enabled && oldType != stats.Type && oldType != "") {
+ stats.Used = 0
+ stats.PeriodStart = now
+ stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
+ stats.BaselineSend = 0
+ stats.AlertSent80 = false
+ stats.AlertSent90 = false
+ stats.AlertSent100 = false
+ }
+
+ // 如果提供了 used 参数且大于 0,则使用该值
+ if used > 0 {
+ stats.Used = used
+ }
+
+ // 如果禁用流量统计,清空相关数据
+ if !stats.Enabled {
+ stats.Used = 0
+ stats.PeriodStart = 0
+ stats.BaselineRecv = 0
+ stats.BaselineSend = 0
+ stats.AlertSent80 = false
+ stats.AlertSent90 = false
+ stats.AlertSent100 = false
+ }
+
+ agent.TrafficStats = datatypes.NewJSONType(stats)
+ agent.UpdatedAt = now
+ return s.agentRepo.UpdateById(ctx, &agent)
+}
+
+// GetTrafficStats 获取流量统计信息
+func (s *TrafficService) GetTrafficStats(ctx context.Context, agentID string) (*TrafficStats, error) {
+ agent, err := s.agentRepo.FindById(ctx, agentID)
+ if err != nil {
+ return nil, err
+ }
+
+ trafficData := agent.TrafficStats.Data()
+
+ // 设置默认类型
+ trafficType := trafficData.Type
+ if trafficType == "" {
+ trafficType = "recv" // 默认只统计进站流量
+ }
+
+ stats := &TrafficStats{
+ Enabled: trafficData.Enabled,
+ Type: trafficType,
+ Limit: trafficData.Limit,
+ Used: trafficData.Used,
+ ResetDay: trafficData.ResetDay,
+ PeriodStart: trafficData.PeriodStart,
+ Alerts: TrafficAlerts{
+ Sent80: trafficData.AlertSent80,
+ Sent90: trafficData.AlertSent90,
+ Sent100: trafficData.AlertSent100,
+ },
+ }
+
+ // 计算使用百分比
+ if trafficData.Limit > 0 {
+ stats.UsedPercent = float64(trafficData.Used) / float64(trafficData.Limit) * 100
+ if trafficData.Used < trafficData.Limit {
+ stats.Remaining = trafficData.Limit - trafficData.Used
+ } else {
+ stats.Remaining = 0
+ }
+ }
+
+ // 计算下次重置日期和剩余天数
+ if trafficData.ResetDay > 0 && trafficData.PeriodStart > 0 {
+ periodStart := time.UnixMilli(trafficData.PeriodStart)
+ nextReset := calculateNextResetDate(periodStart, trafficData.ResetDay)
+ stats.PeriodEnd = nextReset.UnixMilli()
+ stats.DaysUntilReset = int(time.Until(nextReset).Hours() / 24)
+ if stats.DaysUntilReset < 0 {
+ stats.DaysUntilReset = 0
+ }
+ }
+
+ return stats, nil
+}
+
+// ResetAgentTraffic 重置探针流量
+func (s *TrafficService) ResetAgentTraffic(ctx context.Context, agentID string) error {
+ agent, err := s.agentRepo.FindById(ctx, agentID)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now().UnixMilli()
+ stats := agent.TrafficStats.Data()
+
+ stats.Used = 0
+ stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
+ stats.BaselineSend = 0
+ stats.PeriodStart = now
+ stats.AlertSent80 = false
+ stats.AlertSent90 = false
+ stats.AlertSent100 = false
+
+ agent.TrafficStats = datatypes.NewJSONType(stats)
+ agent.UpdatedAt = now
+
+ s.logger.Info("探针流量已重置",
+ zap.String("agentId", agentID),
+ zap.String("agentName", agent.Name))
+
+ return s.agentRepo.UpdateById(ctx, &agent)
+}
+
+// CheckAndResetTraffic 检查并重置所有到期的探针流量(定时任务调用)
+func (s *TrafficService) CheckAndResetTraffic(ctx context.Context) error {
+ agents, err := s.agentRepo.FindAgentsWithTrafficReset(ctx)
+ if err != nil {
+ return err
+ }
+
+ now := time.Now()
+ resetCount := 0
+
+ for _, agent := range agents {
+ if s.shouldResetTraffic(&agent, now) {
+ if err := s.ResetAgentTraffic(ctx, agent.ID); err != nil {
+ s.logger.Error("重置探针流量失败",
+ zap.String("agentId", agent.ID),
+ zap.Error(err))
+ continue
+ }
+ resetCount++
+ }
+ }
+
+ if resetCount > 0 {
+ s.logger.Info("流量重置检查完成", zap.Int("重置数量", resetCount))
+ }
+
+ return nil
+}
+
+// shouldResetTraffic 判断是否需要重置流量
+func (s *TrafficService) shouldResetTraffic(agent *models.Agent, now time.Time) bool {
+ stats := agent.TrafficStats.Data()
+ // 流量统计未启用、未配置重置日期或周期未开始时不重置
+ if !stats.Enabled || stats.ResetDay == 0 || stats.PeriodStart == 0 {
+ return false
+ }
+
+ periodStart := time.UnixMilli(stats.PeriodStart)
+ nextReset := calculateNextResetDate(periodStart, stats.ResetDay)
+
+ return now.After(nextReset) || now.Equal(nextReset)
+}
+
+// calculateNextResetDate 计算下次重置日期
+func calculateNextResetDate(periodStart time.Time, resetDay int) time.Time {
+ year, month, day := periodStart.Date()
+ location := periodStart.Location()
+
+ // 先计算当月的重置日期
+ currentMonthLastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, location).Day()
+ currentMonthResetDay := resetDay
+ if resetDay > currentMonthLastDay {
+ currentMonthResetDay = currentMonthLastDay
+ }
+ currentMonthReset := time.Date(year, month, currentMonthResetDay, 0, 0, 0, 0, location)
+
+ // 如果当月的重置日期还没到(在 periodStart 之后或同一天),返回当月重置日期
+ if day < currentMonthResetDay || (day == currentMonthResetDay && periodStart.Before(currentMonthReset)) {
+ return currentMonthReset
+ }
+
+ // 当月重置日期已过,计算下个月的重置日期
+ nextMonth := month + 1
+ nextYear := year
+ if nextMonth > 12 {
+ nextMonth = 1
+ nextYear++
+ }
+
+ // 处理月末日期(如2月没有31号)
+ nextMonthLastDay := time.Date(nextYear, nextMonth+1, 0, 0, 0, 0, 0, location).Day()
+ nextMonthResetDay := resetDay
+ if resetDay > nextMonthLastDay {
+ nextMonthResetDay = nextMonthLastDay
+ }
+
+ return time.Date(nextYear, nextMonth, nextMonthResetDay, 0, 0, 0, 0, location)
+}
+
+// TrafficStats 流量统计信息
+type TrafficStats struct {
+ Enabled bool `json:"enabled"`
+ Type string `json:"type"` // 统计类型: recv/send/both
+ Limit uint64 `json:"limit"`
+ Used uint64 `json:"used"`
+ UsedPercent float64 `json:"usedPercent"`
+ Remaining uint64 `json:"remaining"`
+ ResetDay int `json:"resetDay"`
+ PeriodStart int64 `json:"periodStart"`
+ PeriodEnd int64 `json:"periodEnd"`
+ DaysUntilReset int `json:"daysUntilReset"`
+ Alerts TrafficAlerts `json:"alerts"`
+}
+
+// TrafficAlerts 流量告警状态
+type TrafficAlerts struct {
+ Sent80 bool `json:"sent80"`
+ Sent90 bool `json:"sent90"`
+ Sent100 bool `json:"sent100"`
+}
diff --git a/internal/wire.go b/internal/wire.go
index c8438e5..a34b8b2 100644
--- a/internal/wire.go
+++ b/internal/wire.go
@@ -74,6 +74,7 @@ type AppComponents struct {
SSHLoginHandler *handler.SSHLoginHandler
AgentService *service.AgentService
+ TrafficService *service.TrafficService
MetricService *service.MetricService
AlertService *service.AlertService
PropertyService *service.PropertyService
diff --git a/internal/wire_gen.go b/internal/wire_gen.go
index 1de7d3c..2caea58 100644
--- a/internal/wire_gen.go
+++ b/internal/wire_gen.go
@@ -41,7 +41,7 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App
tamperService := service.NewTamperService(logger, db, manager)
ddnsService := service.NewDDNSService(logger, db, propertyService, manager)
sshLoginService := service.NewSSHLoginService(logger, db, manager, geoIPService)
- agentHandler := handler.NewAgentHandler(logger, agentService, metricService, monitorService, tamperService, ddnsService, sshLoginService, manager)
+ agentHandler := handler.NewAgentHandler(logger, agentService, trafficService, metricService, monitorService, tamperService, ddnsService, sshLoginService, manager)
apiKeyHandler := handler.NewApiKeyHandler(logger, apiKeyService)
notifier := service.NewNotifier(logger)
alertService := service.NewAlertService(logger, db, propertyService, monitorService, notifier)
@@ -64,6 +64,7 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App
DDNSHandler: ddnsHandler,
SSHLoginHandler: sshLoginHandler,
AgentService: agentService,
+ TrafficService: trafficService,
MetricService: metricService,
AlertService: alertService,
PropertyService: propertyService,
@@ -94,6 +95,7 @@ type AppComponents struct {
SSHLoginHandler *handler.SSHLoginHandler
AgentService *service.AgentService
+ TrafficService *service.TrafficService
MetricService *service.MetricService
AlertService *service.AlertService
PropertyService *service.PropertyService
diff --git a/web/src/admin/pages/Agents/AgentDetail.tsx b/web/src/admin/pages/Agents/AgentDetail.tsx
index 6f615f8..f93c108 100644
--- a/web/src/admin/pages/Agents/AgentDetail.tsx
+++ b/web/src/admin/pages/Agents/AgentDetail.tsx
@@ -119,7 +119,7 @@ const AgentDetail = () => {
{agent?.name}
{agent?.id}
{agent?.hostname}
- {agent?.ip}
+ {agent?.ip}
{agent?.os}
diff --git a/web/src/admin/pages/Agents/TrafficStats.tsx b/web/src/admin/pages/Agents/TrafficStats.tsx
index 63306f9..aad6e34 100644
--- a/web/src/admin/pages/Agents/TrafficStats.tsx
+++ b/web/src/admin/pages/Agents/TrafficStats.tsx
@@ -16,6 +16,7 @@ const TrafficStats: React.FC = ({agentId}) => {
const [form] = Form.useForm();
const queryClient = useQueryClient();
const [enabled, setEnabled] = useState(false);
+ const [editingUsed, setEditingUsed] = useState(false);
// 获取流量统计
const {data: stats, isLoading} = useQuery({
@@ -32,14 +33,18 @@ const TrafficStats: React.FC = ({agentId}) => {
mutationFn: async () => {
const values = form.getFieldsValue();
const limitBytes = enabled ? (values.trafficLimit || 0) * 1024 * 1024 * 1024 : 0;
+ const usedBytes = editingUsed && values.trafficUsed ? values.trafficUsed * 1024 * 1024 * 1024 : 0;
return updateTrafficConfig(agentId, {
enabled: enabled,
+ type: values.trafficType || 'recv',
limit: limitBytes,
+ used: usedBytes,
resetDay: enabled ? (values.trafficResetDay || 0) : 0,
});
},
onSuccess: () => {
message.success('配置已保存');
+ setEditingUsed(false);
queryClient.invalidateQueries({queryKey: ['trafficStats', agentId]});
},
onError: (error: unknown) => {
@@ -80,13 +85,17 @@ const TrafficStats: React.FC = ({agentId}) => {
// 从服务器返回的 enabled 字段读取启用状态
setEnabled(stats.enabled);
form.setFieldsValue({
+ trafficType: stats.type || 'recv',
trafficLimit: stats.limit > 0 ? stats.limit / (1024 * 1024 * 1024) : 0,
+ trafficUsed: stats.used > 0 ? stats.used / (1024 * 1024 * 1024) : 0,
trafficResetDay: stats.resetDay || 0,
});
} else {
setEnabled(false);
form.setFieldsValue({
+ trafficType: 'recv',
trafficLimit: 0,
+ trafficUsed: 0,
trafficResetDay: 0,
});
}
@@ -217,7 +226,9 @@ const TrafficStats: React.FC = ({agentId}) => {
form={form}
layout="vertical"
initialValues={{
+ trafficType: 'recv',
trafficLimit: 0,
+ trafficUsed: 0,
trafficResetDay: 0,
}}
>
@@ -235,6 +246,22 @@ const TrafficStats: React.FC = ({agentId}) => {
{enabled && (
<>
+
+
+
+
= ({agentId}) => {
/>
+
+ 已使用流量
+
+
+ }
+ name="trafficUsed"
+ extra={editingUsed ? "手动设置已使用的流量大小(GB)" : "当前已使用的流量(只读)"}
+ >
+
+
+