mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
fix: reliable SubTotal synchronization across identical inbounds
- Removed artificial `SubTotalGB == 0` inheritance blocker. Admins can now explicitly reset a group's shared quota to 0 (unlimited) without the system automatically reverting it to the old value. - Fixed a JSON overwrite bug in `UpdateInboundClient` and `AddInboundClient` where `tx.Save()` would revert SubTotal updates for other clients located on the exact same inbound. The backend now proactively syncs the new SubTotal to all sibling clients within the JSON payload before marshaling and saving.
This commit is contained in:
parent
32c7ceec55
commit
a52c3fd768
2 changed files with 115 additions and 14 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
|
|
@ -108,6 +109,49 @@ func runSeeders(isUsersEmpty bool) error {
|
|||
}
|
||||
return db.Create(hashSeeder).Error
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "SubTotalMigration") {
|
||||
// Explicitly add sub_total column if it was somehow skipped, though AutoMigrate should handle it
|
||||
if !db.Migrator().HasColumn(&xray.ClientTraffic{}, "sub_total") {
|
||||
err := db.Exec("ALTER TABLE client_traffics ADD COLUMN sub_total integer DEFAULT 0").Error
|
||||
if err != nil {
|
||||
log.Printf("Error adding sub_total column to client_traffics: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill subTotalGB into inbounds.settings JSON
|
||||
var inbounds []model.Inbound
|
||||
db.Find(&inbounds)
|
||||
|
||||
for i := range inbounds {
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(inbounds[i].Settings), &settings); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if clients, ok := settings["clients"].([]any); ok {
|
||||
modified := false
|
||||
for j := range clients {
|
||||
if clientMap, ok := clients[j].(map[string]any); ok {
|
||||
if _, exists := clientMap["subTotalGB"]; !exists {
|
||||
clientMap["subTotalGB"] = 0
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if modified {
|
||||
newSettings, _ := json.MarshalIndent(settings, "", " ")
|
||||
db.Model(&inbounds[i]).Update("settings", string(newSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subTotalSeeder := &model.HistoryOfSeeders{
|
||||
SeederName: "SubTotalMigration",
|
||||
}
|
||||
db.Create(subTotalSeeder)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -24,7 +25,8 @@ import (
|
|||
// It handles CRUD operations for inbounds, client management, traffic monitoring,
|
||||
// and integration with the Xray API for real-time updates.
|
||||
type InboundService struct {
|
||||
xrayApi xray.XrayAPI
|
||||
xrayApi xray.XrayAPI
|
||||
jsonMutex sync.Mutex
|
||||
}
|
||||
|
||||
type CopyClientsResult struct {
|
||||
|
|
@ -700,8 +702,22 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
}
|
||||
|
||||
oldClients := oldSettings["clients"].([]any)
|
||||
oldClients = append(oldClients, interfaceClients...)
|
||||
|
||||
// Sync subTotalGB to old clients on this inbound with the same SubID
|
||||
for _, newClient := range clients {
|
||||
if newClient.SubID != "" {
|
||||
for i, cAny := range oldClients {
|
||||
if cMap, ok := cAny.(map[string]any); ok {
|
||||
if sid, ok := cMap["subId"].(string); ok && sid == newClient.SubID {
|
||||
cMap["subTotalGB"] = newClient.SubTotalGB
|
||||
oldClients[i] = cMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldClients = append(oldClients, interfaceClients...)
|
||||
oldSettings["clients"] = oldClients
|
||||
|
||||
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||
|
|
@ -1141,6 +1157,19 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
}
|
||||
}
|
||||
settingsClients[clientIndex] = interfaceClients[0]
|
||||
|
||||
// Sync subTotalGB to all clients on this inbound with the same SubID
|
||||
if clients[0].SubID != "" {
|
||||
for i, cAny := range settingsClients {
|
||||
if cMap, ok := cAny.(map[string]any); ok {
|
||||
if sid, ok := cMap["subId"].(string); ok && sid == clients[0].SubID {
|
||||
cMap["subTotalGB"] = clients[0].SubTotalGB
|
||||
settingsClients[i] = cMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldSettings["clients"] = settingsClients
|
||||
|
||||
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||
|
|
@ -1329,14 +1358,40 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
for dbTraffic_index := range dbClientTraffics {
|
||||
for traffic_index := range traffics {
|
||||
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
|
||||
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
|
||||
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
|
||||
dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)
|
||||
upToAdd := traffics[traffic_index].Up
|
||||
downToAdd := traffics[traffic_index].Down
|
||||
|
||||
// Add user in onlineUsers array on traffic
|
||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||
if upToAdd+downToAdd > 0 || dbClientTraffics[dbTraffic_index].ExpiryTime != traffics[traffic_index].ExpiryTime {
|
||||
updates := map[string]any{
|
||||
"up": gorm.Expr("up + ?", upToAdd),
|
||||
"down": gorm.Expr("down + ?", downToAdd),
|
||||
"all_time": gorm.Expr("all_time + ?", upToAdd+downToAdd),
|
||||
}
|
||||
|
||||
// Update ExpiryTime if adjustTraffics modified it
|
||||
if dbClientTraffics[dbTraffic_index].ExpiryTime > 0 {
|
||||
updates["expiry_time"] = dbClientTraffics[dbTraffic_index].ExpiryTime
|
||||
}
|
||||
|
||||
if upToAdd+downToAdd > 0 {
|
||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||
updates["last_online"] = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
err = tx.Model(&xray.ClientTraffic{}).
|
||||
Where("id = ?", dbClientTraffics[dbTraffic_index].Id).
|
||||
Updates(updates).Error
|
||||
|
||||
if err != nil {
|
||||
logger.Warning("AddClientTraffic update data ", err)
|
||||
}
|
||||
|
||||
dbClientTraffics[dbTraffic_index].Up += upToAdd
|
||||
dbClientTraffics[dbTraffic_index].Down += downToAdd
|
||||
dbClientTraffics[dbTraffic_index].AllTime += (upToAdd + downToAdd)
|
||||
if upToAdd+downToAdd > 0 {
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -1346,11 +1401,6 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
// Set onlineUsers
|
||||
p.SetOnlineClients(onlineClients)
|
||||
|
||||
err = tx.Save(dbClientTraffics).Error
|
||||
if err != nil {
|
||||
logger.Warning("AddClientTraffic update data ", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1368,6 +1418,10 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.jsonMutex.Lock()
|
||||
defer s.jsonMutex.Unlock()
|
||||
|
||||
for inbound_index := range inbounds {
|
||||
settings := map[string]any{}
|
||||
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
|
||||
|
|
@ -1651,6 +1705,9 @@ func (s *InboundService) SyncSubTotal(tx *gorm.DB, subId string, subTotal int64)
|
|||
return nil
|
||||
}
|
||||
|
||||
s.jsonMutex.Lock()
|
||||
defer s.jsonMutex.Unlock()
|
||||
|
||||
// 1. Update client_traffics table
|
||||
err := tx.Model(xray.ClientTraffic{}).
|
||||
Where("sub_id = ?", subId).
|
||||
|
|
|
|||
Loading…
Reference in a new issue