From a52c3fd768cabc1ef6264cb26083755b0f4e4ba5 Mon Sep 17 00:00:00 2001 From: SadeghKalami Date: Mon, 4 May 2026 23:29:51 +0330 Subject: [PATCH] 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. --- database/db.go | 44 ++++++++++++++++++++++ web/service/inbound.go | 85 +++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/database/db.go b/database/db.go index 2e468587..89d601ee 100644 --- a/database/db.go +++ b/database/db.go @@ -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 diff --git a/web/service/inbound.go b/web/service/inbound.go index 243e8047..0711aa37 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -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).