From 0496c23a2681f8e8cdafd5170ae9106082cd7725 Mon Sep 17 00:00:00 2001 From: n0ctal <4c866w5fn9@privaterelay.appleid.com> Date: Fri, 14 Aug 2026 22:50:06 +0500 Subject: [PATCH] fix(groups): report changed bulk moves without restarting xray (#6199) --- internal/web/controller/group.go | 1 - .../web/service/client_group_bulk_test.go | 42 +++++++++++++++++++ internal/web/service/client_groups.go | 16 ++++--- 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 internal/web/service/client_group_bulk_test.go diff --git a/internal/web/controller/group.go b/internal/web/controller/group.go index d7312e3bc..ede252294 100644 --- a/internal/web/controller/group.go +++ b/internal/web/controller/group.go @@ -148,7 +148,6 @@ func (a *GroupController) bulkAdd(c *gin.Context) { return } jsonObj(c, gin.H{"affected": affected}, nil) - a.xrayService.SetToNeedRestart() notifyClientsChanged() } diff --git a/internal/web/service/client_group_bulk_test.go b/internal/web/service/client_group_bulk_test.go new file mode 100644 index 000000000..6b8899a9b --- /dev/null +++ b/internal/web/service/client_group_bulk_test.go @@ -0,0 +1,42 @@ +package service + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +func TestAddToGroupReportsOnlyChangedRecordsIncludingNull(t *testing.T) { + setupConflictDB(t) + db := database.GetDB() + if err := db.Create(&model.ClientGroup{Name: "paid"}).Error; err != nil { + t.Fatalf("create group: %v", err) + } + rows := []model.ClientRecord{ + {Email: "same@example", UUID: "same", Group: "paid"}, + {Email: "other@example", UUID: "other", Group: "free"}, + {Email: "null@example", UUID: "null"}, + } + if err := db.Create(&rows).Error; err != nil { + t.Fatalf("create clients: %v", err) + } + if err := db.Model(&model.ClientRecord{}).Where("email = ?", "null@example").UpdateColumn("group_name", nil).Error; err != nil { + t.Fatalf("set NULL group: %v", err) + } + + got, err := (&ClientService{}).AddToGroup([]string{"same@example", "other@example", "null@example", "missing@example"}, "paid") + if err != nil { + t.Fatalf("AddToGroup: %v", err) + } + if got != 2 { + t.Fatalf("affected = %d, want 2 changed records", got) + } + got, err = (&ClientService{}).AddToGroup([]string{"same@example", "other@example", "null@example"}, "paid") + if err != nil { + t.Fatalf("second AddToGroup: %v", err) + } + if got != 0 { + t.Fatalf("second affected = %d, want 0", got) + } +} diff --git a/internal/web/service/client_groups.go b/internal/web/service/client_groups.go index fa3f2c159..2f3b70e20 100644 --- a/internal/web/service/client_groups.go +++ b/internal/web/service/client_groups.go @@ -234,7 +234,9 @@ func (s *ClientService) AddToGroup(emails []string, group string) (int, error) { var records []model.ClientRecord for _, batch := range chunkStrings(emails, sqlInChunk) { var rows []model.ClientRecord - if err := db.Where("email IN ?", batch).Find(&rows).Error; err != nil { + if err := db.Where("email IN ?", batch). + Where("group_name IS NULL OR group_name <> ?", group). + Find(&rows).Error; err != nil { return 0, err } records = append(records, rows...) @@ -248,13 +250,17 @@ func (s *ClientService) AddToGroup(emails []string, group string) (int, error) { } tx := db.Begin() + var affected int64 for _, batch := range chunkStrings(affectedEmails, sqlInChunk) { - if err := tx.Model(&model.ClientRecord{}). + result := tx.Model(&model.ClientRecord{}). Where("email IN ?", batch). - UpdateColumn("group_name", group).Error; err != nil { + Where("group_name IS NULL OR group_name <> ?", group). + UpdateColumn("group_name", group) + if result.Error != nil { tx.Rollback() - return 0, err + return 0, result.Error } + affected += result.RowsAffected } var inboundIDs []int @@ -331,7 +337,7 @@ func (s *ClientService) AddToGroup(emails []string, group string) (int, error) { if err := tx.Commit().Error; err != nil { return 0, err } - return len(records), nil + return int(affected), nil } func (s *ClientService) replaceGroupValue(oldName, newName string) (int, error) {