mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-23 06:42:41 +00:00
196 lines
4.9 KiB
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
|
|
}
|