ソースを参照

fix(tgbot): find clients by tgId regardless of settings JSON formatting

The Telegram-bot usage lookup prefiltered inbounds with
settings LIKE '%"tgId": N%', which requires the exact space the panel's
MarshalIndent happens to emit. Inbounds whose settings were serialized
compactly (node sync, imports, external edits) never matched, so the
bot reported no configuration even though the client and traffic rows
exist. Replace the string match with the driver-portable JSON helpers
already used by GetAllEmails, which read the actual clients array on
SQLite and Postgres alike.

Closes #5805
MHSanaei 1 日 前
コミット
b6183271da

+ 67 - 0
internal/web/service/inbound_tgbot_lookup_test.go

@@ -0,0 +1,67 @@
+package service
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
+)
+
+// TestGetClientTrafficTgBot_SettingsSerializationStyles guards against the
+// prefilter regressing into a formatting-sensitive string match (#5805): the
+// lookup must find clients whether inbounds.settings stores compact JSON
+// ("tgId":N, as written by node sync/import) or indented JSON ("tgId": N, as
+// written by the panel's MarshalIndent).
+func TestGetClientTrafficTgBot_SettingsSerializationStyles(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 tgId int64 = 123456789
+	cases := []struct {
+		name     string
+		settings string
+		email    string
+		port     int
+	}{
+		{"compact", `{"clients":[{"id":"u1","email":"compact-user","tgId":123456789}]}`, "compact-user", 41001},
+		{"spaced", `{"clients": [{"id": "u2", "email": "spaced-user", "tgId": 123456789}]}`, "spaced-user", 41002},
+	}
+	for _, c := range cases {
+		inbound := &model.Inbound{UserId: 1, Tag: "tg-" + c.name, Enable: true, Port: c.port, Protocol: model.VLESS, Settings: c.settings}
+		if err := db.Create(inbound).Error; err != nil {
+			t.Fatalf("create %s inbound: %v", c.name, err)
+		}
+		if err := db.Create(&xray.ClientTraffic{InboundId: inbound.Id, Email: c.email, Enable: true, Up: 10, Down: 20}).Error; err != nil {
+			t.Fatalf("create %s client_traffics: %v", c.name, err)
+		}
+	}
+
+	svc := InboundService{}
+	traffics, err := svc.GetClientTrafficTgBot(tgId)
+	if err != nil {
+		t.Fatalf("GetClientTrafficTgBot: %v", err)
+	}
+	got := make(map[string]bool, len(traffics))
+	for _, tr := range traffics {
+		got[tr.Email] = true
+	}
+	if len(traffics) != 2 || !got["compact-user"] || !got["spaced-user"] {
+		t.Fatalf("expected traffic for compact-user and spaced-user, got %v", got)
+	}
+
+	other, err := svc.GetClientTrafficTgBot(42)
+	if err != nil {
+		t.Fatalf("GetClientTrafficTgBot(42): %v", err)
+	}
+	if len(other) != 0 {
+		t.Fatalf("expected no traffic for unknown tgId, got %d rows", len(other))
+	}
+}

+ 18 - 5
internal/web/service/inbound_traffic.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"maps"
 	"slices"
+	"strconv"
 	"strings"
 	"time"
 
@@ -837,15 +838,27 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
 
 func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {
 	db := database.GetDB()
-	var inbounds []*model.Inbound
 
-	// Retrieve inbounds where settings contain the given tgId
-	err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error
-	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+	idQuery := fmt.Sprintf(
+		"SELECT DISTINCT inbounds.id %s WHERE %s = ?",
+		database.JSONClientsFromInbound(),
+		database.JSONFieldText("client.value", "tgId"),
+	)
+	var inboundIds []int
+	if err := db.Raw(idQuery, strconv.FormatInt(tgId, 10)).Scan(&inboundIds).Error; err != nil {
 		logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err)
 		return nil, err
 	}
 
+	var inbounds []*model.Inbound
+	if len(inboundIds) > 0 {
+		err := db.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error
+		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+			logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err)
+			return nil, err
+		}
+	}
+
 	var emails []string
 	for _, inbound := range inbounds {
 		clients, err := s.GetClients(inbound)
@@ -866,7 +879,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
 	traffics := make([]*xray.ClientTraffic, 0, len(uniqEmails))
 	for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) {
 		var page []*xray.ClientTraffic
-		if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
+		if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
 			if errors.Is(err, gorm.ErrRecordNotFound) {
 				continue
 			}