fix(sub): deduplicate settings.clients entries per inbound in subscription output (#5134)

Multi-node sync/import drift can leave the same client twice inside an
inbound's legacy settings.clients JSON while the normalized
client_inbounds table stays clean (SyncInbound dedupes the rows it
writes but never rewrites the JSON). All three subscription builders
iterated that JSON verbatim, so every duplicate entry became a
duplicate profile in the raw, Clash, and JSON output.

Filter and dedupe by email in one shared helper (link generation keys
purely on inbound + email, so same-email entries are pure duplicates
and dropping them is lossless). The clash/json services' own
inboundService copies became unused and are removed.
This commit is contained in:
MHSanaei
2026-06-11 22:19:14 +02:00
parent 09a887f95c
commit 1b0dbf8e6d
4 changed files with 114 additions and 40 deletions
+7 -15
View File
@@ -9,15 +9,12 @@ import (
yaml "github.com/goccy/go-yaml"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
)
type SubClashService struct {
inboundService service.InboundService
enableRouting bool
clashRules string
SubService *SubService
enableRouting bool
clashRules string
SubService *SubService
}
func NewSubClashService(enableRouting bool, clashRules string, subService *SubService) *SubClashService {
@@ -36,19 +33,14 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
seenEmails := make(map[string]struct{})
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubClashService - GetClients: Unable to get clients from inbound")
}
if clients == nil {
clients := s.SubService.matchingClients(inbound, subId)
if len(clients) == 0 {
continue
}
s.SubService.projectThroughFallbackMaster(inbound)
for _, client := range clients {
if client.SubID == subId {
seenEmails[client.Email] = struct{}{}
proxies = append(proxies, s.getProxies(inbound, client, host)...)
}
seenEmails[client.Email] = struct{}{}
proxies = append(proxies, s.getProxies(inbound, client, host)...)
}
}