Procházet zdrojové kódy

fix(client): clean node_client_traffics rows when deleting a client

Delete and DeleteByEmail removed client_traffics, global-traffic, and
inbound_client_ips rows but never the per-node baseline rows in
node_client_traffics, so every deleted client left orphaned baselines
behind for each registered node. The shared DelClientStat and
delClientStatsByEmails helpers already clean that table; mirror the
same cleanup in both row-cleanup paths so the record-only and
record-less delete flows stop leaking baselines.

Closes #5841
MHSanaei před 11 hodinami
rodič
revize
7cb2adf429

+ 6 - 0
internal/web/service/client_crud.go

@@ -523,6 +523,9 @@ func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic b
 			if err := tx.Where("client_email = ?", existing.Email).Delete(&model.InboundClientIps{}).Error; err != nil {
 				return err
 			}
+			if err := tx.Where("email = ?", existing.Email).Delete(&model.NodeClientTraffic{}).Error; err != nil {
+				return err
+			}
 		}
 		return tx.Delete(&model.ClientRecord{}, id).Error
 	}); err != nil {
@@ -666,6 +669,9 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string,
 		if err := db.Where("client_email = ?", email).Delete(&model.InboundClientIps{}).Error; err != nil {
 			return needRestart, err
 		}
+		if err := db.Where("email = ?", email).Delete(&model.NodeClientTraffic{}).Error; err != nil {
+			return needRestart, err
+		}
 	}
 	return needRestart, nil
 }

+ 95 - 0
internal/web/service/client_delete_node_baseline_test.go

@@ -0,0 +1,95 @@
+package service
+
+import (
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+func TestClientDelete_CleansNodeBaselines(t *testing.T) {
+	db := initTrafficTestDB(t)
+
+	const email = "[email protected]"
+	rec := &model.ClientRecord{Email: email, SubID: "sub-orphan", Enable: true}
+	if err := db.Create(rec).Error; err != nil {
+		t.Fatalf("seed client record: %v", err)
+	}
+	if err := db.Create(&model.NodeClientTraffic{NodeId: 1, Email: email, Up: 10, Down: 20}).Error; err != nil {
+		t.Fatalf("seed node baseline 1: %v", err)
+	}
+	if err := db.Create(&model.NodeClientTraffic{NodeId: 2, Email: email, Up: 30, Down: 40}).Error; err != nil {
+		t.Fatalf("seed node baseline 2: %v", err)
+	}
+
+	svc := &ClientService{}
+	if _, err := svc.Delete(&InboundService{}, rec.Id, false); err != nil {
+		t.Fatalf("Delete: %v", err)
+	}
+
+	var cnt int64
+	if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&cnt).Error; err != nil {
+		t.Fatalf("count baselines: %v", err)
+	}
+	if cnt != 0 {
+		t.Errorf("expected node baselines cleaned on client delete, found %d", cnt)
+	}
+}
+
+func TestClientDelete_KeepTrafficPreservesNodeBaselines(t *testing.T) {
+	db := initTrafficTestDB(t)
+
+	const email = "[email protected]"
+	rec := &model.ClientRecord{Email: email, SubID: "sub-kept", Enable: true}
+	if err := db.Create(rec).Error; err != nil {
+		t.Fatalf("seed client record: %v", err)
+	}
+	if err := db.Create(&model.NodeClientTraffic{NodeId: 1, Email: email, Up: 10, Down: 20}).Error; err != nil {
+		t.Fatalf("seed node baseline: %v", err)
+	}
+
+	svc := &ClientService{}
+	if _, err := svc.Delete(&InboundService{}, rec.Id, true); err != nil {
+		t.Fatalf("Delete(keepTraffic): %v", err)
+	}
+
+	var cnt int64
+	if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&cnt).Error; err != nil {
+		t.Fatalf("count baselines: %v", err)
+	}
+	if cnt != 1 {
+		t.Errorf("keepTraffic delete must preserve node baselines, found %d", cnt)
+	}
+}
+
+func TestDeleteByEmail_FallbackCleansNodeBaselines(t *testing.T) {
+	db := initTrafficTestDB(t)
+
+	const email = "[email protected]"
+	ib := &model.Inbound{
+		UserId:   1,
+		Tag:      "local-fallback",
+		Enable:   true,
+		Port:     41001,
+		Protocol: model.VLESS,
+		Settings: `{"clients": [{"id": "5f2eb9d6-3a2f-4a55-9812-6ea1e2f7a001", "email": "[email protected]", "enable": true}]}`,
+	}
+	if err := db.Create(ib).Error; err != nil {
+		t.Fatalf("seed inbound: %v", err)
+	}
+	if err := db.Create(&model.NodeClientTraffic{NodeId: 3, Email: email, Up: 5, Down: 6}).Error; err != nil {
+		t.Fatalf("seed node baseline: %v", err)
+	}
+
+	svc := &ClientService{}
+	if _, err := svc.DeleteByEmail(&InboundService{}, email, false); err != nil {
+		t.Fatalf("DeleteByEmail: %v", err)
+	}
+
+	var cnt int64
+	if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&cnt).Error; err != nil {
+		t.Fatalf("count baselines: %v", err)
+	}
+	if cnt != 0 {
+		t.Errorf("expected node baselines cleaned on record-less delete, found %d", cnt)
+	}
+}