完善流量使用统计
This commit is contained in:
1 parent
bca13fab7b
commit
f4d6a3fc3e
12 files changed
+364
-239
No files matched your search
+1
-1
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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%告警
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -119,7 +119,7 @@ const AgentDetail = () => {
|
||||
<Descriptions.Item label="探针名称">{agent?.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="探针ID">{agent?.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="主机名">{agent?.hostname}</Descriptions.Item>
|
||||
<Descriptions.Item label="IP地址">{agent?.ip}</Descriptions.Item>
|
||||
<Descriptions.Item label="通信IP">{agent?.ip}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作系统">
|
||||
<Tag color="blue">{agent?.os}</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -16,6 +16,7 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
const [form] = Form.useForm();
|
||||
const queryClient = useQueryClient();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [editingUsed, setEditingUsed] = useState(false);
|
||||
|
||||
// 获取流量统计
|
||||
const {data: stats, isLoading} = useQuery<TrafficStatsType>({
|
||||
@@ -32,14 +33,18 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({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<TrafficStatsProps> = ({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<TrafficStatsProps> = ({agentId}) => {
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
trafficType: 'recv',
|
||||
trafficLimit: 0,
|
||||
trafficUsed: 0,
|
||||
trafficResetDay: 0,
|
||||
}}
|
||||
>
|
||||
@@ -235,6 +246,22 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="统计类型"
|
||||
name="trafficType"
|
||||
rules={[{required: true, message: '请选择统计类型'}]}
|
||||
extra="选择要统计的流量类型"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择统计类型"
|
||||
options={[
|
||||
{label: '进站流量 (下载)', value: 'recv'},
|
||||
{label: '出站流量 (上传)', value: 'send'},
|
||||
{label: '全部流量 (上传+下载)', value: 'both'},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="流量限额"
|
||||
name="trafficLimit"
|
||||
@@ -251,6 +278,33 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>已使用流量</span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={editingUsed}
|
||||
onChange={setEditingUsed}
|
||||
checkedChildren="编辑中"
|
||||
unCheckedChildren="锁定"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
name="trafficUsed"
|
||||
extra={editingUsed ? "手动设置已使用的流量大小(GB)" : "当前已使用的流量(只读)"}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.1}
|
||||
precision={2}
|
||||
placeholder="已使用流量(GB)"
|
||||
style={{width: '100%'}}
|
||||
addonAfter="GB"
|
||||
disabled={!editingUsed}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="流量重置日期"
|
||||
name="trafficResetDay"
|
||||
|
||||
@@ -46,11 +46,13 @@ export interface TrafficData {
|
||||
|
||||
export interface TrafficStatsData {
|
||||
enabled: boolean;
|
||||
type: string; // 统计类型: recv进站/send出站/both全部
|
||||
limit: number; // 流量限额(字节), 0表示不限制
|
||||
used: number; // 当前周期已使用流量(字节)
|
||||
resetDay: number; // 流量重置日期(1-31), 0表示不自动重置
|
||||
periodStart: number; // 当前周期开始时间(时间戳毫秒)
|
||||
baselineRecv: number; // 当前周期流量基线
|
||||
baselineRecv: number; // 当前周期流量基线(接收)
|
||||
baselineSend: number; // 当前周期流量基线(发送)
|
||||
alertSent80: boolean;
|
||||
alertSent90: boolean;
|
||||
alertSent100: boolean;
|
||||
@@ -494,6 +496,7 @@ export interface TrafficAlerts {
|
||||
|
||||
export interface TrafficStats {
|
||||
enabled: boolean;
|
||||
type: string; // 统计类型: recv进站/send出站/both全部
|
||||
limit: number;
|
||||
used: number;
|
||||
usedPercent: number;
|
||||
@@ -507,7 +510,9 @@ export interface TrafficStats {
|
||||
|
||||
export interface UpdateTrafficConfigRequest {
|
||||
enabled: boolean; // 是否启用
|
||||
type: string; // 统计类型: recv进站/send出站/both全部
|
||||
limit: number; // 流量限额(字节), 0表示不限制
|
||||
used?: number; // 手动设置已使用流量(可选)
|
||||
resetDay: number; // 流量重置日期(1-31), 0表示不自动重置
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user