Переглянути джерело

fix(postgres): commit client traffic backfill in migration

MigrationRequirements backfills missing client_traffics rows from each inbound's settings.clients, but the later MultiDomain->ExternalProxy detection query used SQLite-only json_extract and executed via .Scan. On PostgreSQL it errored, rolling back the whole transaction including the backfill, so clients had no traffic rows: client traffic was never recorded, clients showed offline, and the inbound list showed 0 clients until each inbound was edited and saved.

Make the detection query dialect-aware (NULLIF(stream_settings,'')::jsonb #>> / #>) so the function runs to completion and commits on both dialects.
MHSanaei 13 годин тому
батько
коміт
61ba5754ca
2 змінених файлів з 101 додано та 2 видалено
  1. 10 2
      web/service/inbound.go
  2. 91 0
      web/service/inbound_migration_test.go

+ 10 - 2
web/service/inbound.go

@@ -3122,11 +3122,19 @@ func (s *InboundService) MigrationRequirements() {
 		Port           int
 		StreamSettings []byte
 	}
-	err = tx.Raw(`select id, port, stream_settings
+	externalProxyQuery := `select id, port, stream_settings
 	from inbounds
 	WHERE protocol in ('vmess','vless','trojan')
 	  AND json_extract(stream_settings, '$.security') = 'tls'
-	  AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error
+	  AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`
+	if database.IsPostgres() {
+		externalProxyQuery = `select id, port, stream_settings
+	from inbounds
+	WHERE protocol in ('vmess','vless','trojan')
+	  AND NULLIF(stream_settings, '')::jsonb #>> '{security}' = 'tls'
+	  AND NULLIF(stream_settings, '')::jsonb #> '{tlsSettings,settings,domains}' IS NOT NULL`
+	}
+	err = tx.Raw(externalProxyQuery).Scan(&externalProxy).Error
 	if err != nil || len(externalProxy) == 0 {
 		return
 	}

+ 91 - 0
web/service/inbound_migration_test.go

@@ -0,0 +1,91 @@
+package service
+
+import (
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/database"
+	"github.com/mhsanaei/3x-ui/v3/database/model"
+	"github.com/mhsanaei/3x-ui/v3/xray"
+)
+
+// TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound guards the
+// PostgreSQL fix where the externalProxy detection query (executed via .Scan) errored on
+// json_extract and rolled back the whole transaction — including the client_traffics
+// backfill at inbound.go:3093-3106, leaving clients with no traffic rows. A MultiDomain
+// inbound is present so that query returns rows and the function runs to completion; both
+// the backfill and the MultiDomain→ExternalProxy migration must then commit.
+func TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound(t *testing.T) {
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+
+	db := database.GetDB()
+
+	const backfillEmail = "[email protected]"
+	const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c010"
+
+	// Inbound A: a client present only in settings.clients, with no client_traffics row.
+	clientInbound := &model.Inbound{
+		UserId:         1,
+		Tag:            "a-tag",
+		Enable:         true,
+		Port:           30001,
+		Protocol:       model.VLESS,
+		Settings:       `{"clients":[{"email":"` + backfillEmail + `","id":"` + uid + `","enable":true}]}`,
+		StreamSettings: `{"network":"tcp","security":"none"}`,
+	}
+	if err := db.Create(clientInbound).Error; err != nil {
+		t.Fatalf("create client inbound: %v", err)
+	}
+
+	// Inbound B: a legacy MultiDomain inbound whose tag carries the 0.0.0.0: prefix.
+	// Its presence makes the externalProxy query return rows, so the function does not
+	// early-return and reaches the tag-cleanup statement.
+	multiDomainInbound := &model.Inbound{
+		UserId:         1,
+		Tag:            "inbound-0.0.0.0:30002",
+		Enable:         true,
+		Port:           30002,
+		Protocol:       model.VLESS,
+		Settings:       `{"clients":[]}`,
+		StreamSettings: `{"security":"tls","tlsSettings":{"settings":{"domains":[{"domain":"example.com"}]}}}`,
+	}
+	if err := db.Create(multiDomainInbound).Error; err != nil {
+		t.Fatalf("create multidomain inbound: %v", err)
+	}
+
+	var before int64
+	if err := db.Model(xray.ClientTraffic{}).Count(&before).Error; err != nil {
+		t.Fatalf("count client_traffics before: %v", err)
+	}
+	if before != 0 {
+		t.Fatalf("expected no client_traffics before migration, got %d", before)
+	}
+
+	svc := InboundService{}
+	svc.MigrationRequirements()
+
+	// The backfill must have committed: the settings-only client now owns a row.
+	// Before the fix this was rolled back whenever the externalProxy detection query
+	// errored (it does on Postgres via json_extract), so the MultiDomain inbound below
+	// is deliberately present to make that query return rows and run to completion.
+	var ct xray.ClientTraffic
+	if err := db.Model(xray.ClientTraffic{}).Where("email = ?", backfillEmail).First(&ct).Error; err != nil {
+		t.Fatalf("client_traffics row not backfilled for %s: %v", backfillEmail, err)
+	}
+
+	// The MultiDomain→ExternalProxy migration must have committed too: the detection
+	// query ran (.Scan executes it) and the loop rewrote the inbound's streamSettings.
+	var refreshed model.Inbound
+	if err := db.First(&refreshed, multiDomainInbound.Id).Error; err != nil {
+		t.Fatalf("reload multidomain inbound: %v", err)
+	}
+	if !strings.Contains(refreshed.StreamSettings, "externalProxy") {
+		t.Errorf("MultiDomain migration did not commit; streamSettings = %q", refreshed.StreamSettings)
+	}
+}