mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-23 14:52:43 +00:00
148 lines
3.7 KiB
Go
148 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
redisutil "github.com/mhsanaei/3x-ui/v2/util/redis"
|
|
)
|
|
|
|
// QuotaService handles bandwidth quota management
|
|
type QuotaService struct {
|
|
inboundService InboundService
|
|
}
|
|
|
|
// QuotaInfo represents quota information for a client
|
|
type QuotaInfo struct {
|
|
Email string `json:"email"`
|
|
UsedBytes int64 `json:"used_bytes"`
|
|
TotalBytes int64 `json:"total_bytes"`
|
|
UsagePercent float64 `json:"usage_percent"`
|
|
ResetTime int64 `json:"reset_time"`
|
|
Status string `json:"status"` // normal, warning, exceeded
|
|
}
|
|
|
|
// CheckQuota checks if client has exceeded quota
|
|
func (s *QuotaService) CheckQuota(email string, inbound *model.Inbound) (bool, *QuotaInfo, error) {
|
|
clients, err := s.inboundService.GetClients(inbound)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
var client *model.Client
|
|
for i := range clients {
|
|
if clients[i].Email == email {
|
|
client = &clients[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if client == nil {
|
|
return false, nil, nil
|
|
}
|
|
|
|
// Get traffic from Xray API or database
|
|
trafficKey := "traffic:" + email
|
|
usedBytesStr, err := redisutil.Get(trafficKey)
|
|
var usedBytes int64
|
|
if err == nil && usedBytesStr != "" {
|
|
if parsed, parseErr := strconv.ParseInt(usedBytesStr, 10, 64); parseErr == nil {
|
|
usedBytes = parsed
|
|
}
|
|
}
|
|
|
|
totalBytes := client.TotalGB * 1024 * 1024 * 1024
|
|
var usagePercent float64
|
|
if totalBytes > 0 {
|
|
usagePercent = float64(usedBytes) / float64(totalBytes) * 100
|
|
} else {
|
|
// Unlimited quota
|
|
usagePercent = 0
|
|
}
|
|
|
|
quotaInfo := &QuotaInfo{
|
|
Email: email,
|
|
UsedBytes: usedBytes,
|
|
TotalBytes: totalBytes,
|
|
UsagePercent: usagePercent,
|
|
ResetTime: client.ExpiryTime,
|
|
}
|
|
|
|
// Determine status
|
|
if totalBytes > 0 {
|
|
if usagePercent >= 100 {
|
|
quotaInfo.Status = "exceeded"
|
|
return false, quotaInfo, nil
|
|
} else if usagePercent >= 80 {
|
|
quotaInfo.Status = "warning"
|
|
return true, quotaInfo, nil
|
|
}
|
|
}
|
|
|
|
quotaInfo.Status = "normal"
|
|
return true, quotaInfo, nil
|
|
}
|
|
|
|
// ThrottleClient throttles client speed when quota exceeded
|
|
func (s *QuotaService) ThrottleClient(email string, inbound *model.Inbound, throttle bool) error {
|
|
// This would integrate with Xray API to throttle speed
|
|
// For now, we'll just log it
|
|
if throttle {
|
|
logger.Infof("Throttling client %s due to quota", email)
|
|
} else {
|
|
logger.Infof("Removing throttle for client %s", email)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetQuotaInfo gets quota information for all clients
|
|
func (s *QuotaService) GetQuotaInfo(inbound *model.Inbound) ([]QuotaInfo, error) {
|
|
clients, err := s.inboundService.GetClients(inbound)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
quotaInfos := make([]QuotaInfo, 0, len(clients))
|
|
for _, client := range clients {
|
|
_, quotaInfo, err := s.CheckQuota(client.Email, inbound)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if quotaInfo != nil {
|
|
quotaInfos = append(quotaInfos, *quotaInfo)
|
|
}
|
|
}
|
|
|
|
return quotaInfos, nil
|
|
}
|
|
|
|
// ResetQuota resets quota for a client
|
|
func (s *QuotaService) ResetQuota(email string) error {
|
|
trafficKey := "traffic:" + email
|
|
return redisutil.Del(trafficKey)
|
|
}
|
|
|
|
// UpdateQuotaUsage updates quota usage from Xray traffic
|
|
func (s *QuotaService) UpdateQuotaUsage(email string, up, down int64) error {
|
|
if email == "" {
|
|
return fmt.Errorf("email is required")
|
|
}
|
|
if up < 0 || down < 0 {
|
|
return fmt.Errorf("traffic values cannot be negative")
|
|
}
|
|
|
|
trafficKey := "traffic:" + email
|
|
currentStr, err := redisutil.Get(trafficKey)
|
|
var current int64
|
|
if err == nil && currentStr != "" {
|
|
if parsed, parseErr := strconv.ParseInt(currentStr, 10, 64); parseErr == nil {
|
|
current = parsed
|
|
}
|
|
}
|
|
|
|
newTotal := current + up + down
|
|
return redisutil.Set(trafficKey, newTotal, 30*24*time.Hour)
|
|
}
|