mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 00:01:02 +00:00
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:
+37
-12
@@ -106,6 +106,36 @@ func listenIsInternalOnly(listen string) bool {
|
||||
return isLoopbackHost(listen)
|
||||
}
|
||||
|
||||
// matchingClients returns the inbound's clients whose SubID equals subId,
|
||||
// deduplicated by email. settings.clients can accumulate duplicate entries
|
||||
// for the same client (multi-node sync/import drift, old DBs): SyncInbound
|
||||
// dedupes the normalized client_inbounds rows on write but never rewrites
|
||||
// the legacy JSON, and the subscription builders iterate that JSON — so
|
||||
// without this guard every duplicate became a duplicate profile in the
|
||||
// output (#5134). Link generation keys purely on (inbound, email), so
|
||||
// same-email entries are pure duplicates and dropping them is lossless.
|
||||
func (s *SubService) matchingClients(inbound *model.Inbound, subId string) []model.Client {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetClients: Unable to get clients from inbound")
|
||||
return nil
|
||||
}
|
||||
var out []model.Client
|
||||
seen := make(map[string]struct{}, len(clients))
|
||||
for _, client := range clients {
|
||||
if client.SubID != subId {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(client.Email)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, client)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
||||
s.PrepareForRequest(host)
|
||||
@@ -134,23 +164,18 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int
|
||||
|
||||
seenEmails := make(map[string]struct{})
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetClients: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
clients := s.matchingClients(inbound, subId)
|
||||
if len(clients) == 0 {
|
||||
continue
|
||||
}
|
||||
s.projectThroughFallbackMaster(inbound)
|
||||
for _, client := range clients {
|
||||
if client.SubID == subId {
|
||||
if client.Enable {
|
||||
hasEnabledClient = true
|
||||
}
|
||||
result = append(result, s.GetLink(inbound, client.Email))
|
||||
emails = append(emails, client.Email)
|
||||
seenEmails[client.Email] = struct{}{}
|
||||
if client.Enable {
|
||||
hasEnabledClient = true
|
||||
}
|
||||
result = append(result, s.GetLink(inbound, client.Email))
|
||||
emails = append(emails, client.Email)
|
||||
seenEmails[client.Email] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user