3x-ui/web/service/analytics.go

196 lines
4.9 KiB
Go

package service
import (
"fmt"
"strconv"
"time"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
redisutil "github.com/mhsanaei/3x-ui/v2/util/redis"
)
// AnalyticsService handles traffic analytics
type AnalyticsService struct {
inboundService InboundService
}
// TrafficStats represents traffic statistics
type TrafficStats struct {
Time time.Time `json:"time"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Total int64 `json:"total"`
ClientCount int `json:"client_count"`
}
// HourlyStats represents hourly traffic statistics
type HourlyStats struct {
Hour int `json:"hour"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Total int64 `json:"total"`
}
// DailyStats represents daily traffic statistics
type DailyStats struct {
Date string `json:"date"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Total int64 `json:"total"`
}
// GetHourlyStats gets hourly traffic statistics for the last 24 hours
func (s *AnalyticsService) GetHourlyStats(inboundID int) ([]HourlyStats, error) {
now := time.Now()
stats := make([]HourlyStats, 24)
for i := 0; i < 24; i++ {
hour := now.Add(-time.Duration(23-i) * time.Hour)
var up, down int64
// Query traffic from database or Redis
// This is simplified - in production, aggregate from Xray logs or API
key := fmt.Sprintf("traffic:hourly:%d:%d", inboundID, hour.Hour())
data, _ := redisutil.HGetAll(key)
if upStr, ok := data["up"]; ok && upStr != "" {
if parsed, err := strconv.ParseInt(upStr, 10, 64); err == nil {
up = parsed
}
}
if downStr, ok := data["down"]; ok && downStr != "" {
if parsed, err := strconv.ParseInt(downStr, 10, 64); err == nil {
down = parsed
}
}
stats[i] = HourlyStats{
Hour: hour.Hour(),
Up: up,
Down: down,
Total: up + down,
}
}
return stats, nil
}
// GetDailyStats gets daily traffic statistics for the last 30 days
func (s *AnalyticsService) GetDailyStats(inboundID int) ([]DailyStats, error) {
stats := make([]DailyStats, 30)
now := time.Now()
for i := 0; i < 30; i++ {
date := now.AddDate(0, 0, -29+i)
dateStr := date.Format("2006-01-02")
// Query from database or Redis
key := fmt.Sprintf("traffic:daily:%d:%s", inboundID, dateStr)
data, _ := redisutil.HGetAll(key)
var up, down int64
if upStr, ok := data["up"]; ok && upStr != "" {
if parsed, err := strconv.ParseInt(upStr, 10, 64); err == nil {
up = parsed
}
}
if downStr, ok := data["down"]; ok && downStr != "" {
if parsed, err := strconv.ParseInt(downStr, 10, 64); err == nil {
down = parsed
}
}
stats[i] = DailyStats{
Date: dateStr,
Up: up,
Down: down,
Total: up + down,
}
}
return stats, nil
}
// GetTopClients gets top clients by traffic
func (s *AnalyticsService) GetTopClients(inboundID int, limit int) ([]model.Client, error) {
db := database.GetDB()
var inbound model.Inbound
if err := db.First(&inbound, inboundID).Error; err != nil {
return nil, err
}
clients, err := s.inboundService.GetClients(&inbound)
if err != nil {
return nil, err
}
// Sort by traffic (simplified)
// In production, get from Xray API or aggregate from logs
return clients[:min(limit, len(clients))], nil
}
// RecordTraffic records traffic for analytics
func (s *AnalyticsService) RecordTraffic(inboundID int, email string, up, down int64) error {
if inboundID <= 0 {
return fmt.Errorf("invalid inbound ID")
}
if email == "" {
return fmt.Errorf("email is required")
}
if up < 0 || down < 0 {
return fmt.Errorf("traffic values cannot be negative")
}
now := time.Now()
// Record hourly (aggregate)
hourKey := fmt.Sprintf("traffic:hourly:%d:%d", inboundID, now.Hour())
currentUpStr, _ := redisutil.HGet(hourKey, "up")
currentDownStr, _ := redisutil.HGet(hourKey, "down")
var currentUp, currentDown int64
if currentUpStr != "" {
if parsed, err := strconv.ParseInt(currentUpStr, 10, 64); err == nil {
currentUp = parsed
}
}
if currentDownStr != "" {
if parsed, err := strconv.ParseInt(currentDownStr, 10, 64); err == nil {
currentDown = parsed
}
}
redisutil.HSet(hourKey, "up", currentUp+up)
redisutil.HSet(hourKey, "down", currentDown+down)
redisutil.Expire(hourKey, 25*time.Hour)
// Record daily (aggregate)
dateKey := fmt.Sprintf("traffic:daily:%d:%s", inboundID, now.Format("2006-01-02"))
dailyUpStr, _ := redisutil.HGet(dateKey, "up")
dailyDownStr, _ := redisutil.HGet(dateKey, "down")
var dailyUp, dailyDown int64
if dailyUpStr != "" {
if parsed, err := strconv.ParseInt(dailyUpStr, 10, 64); err == nil {
dailyUp = parsed
}
}
if dailyDownStr != "" {
if parsed, err := strconv.ParseInt(dailyDownStr, 10, 64); err == nil {
dailyDown = parsed
}
}
redisutil.HSet(dateKey, "up", dailyUp+up)
redisutil.HSet(dateKey, "down", dailyDown+down)
redisutil.Expire(dateKey, 32*24*time.Hour)
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}