fix(sub): {{INBOUND}} = inbound remark, fix {{TRAFFIC_LEFT}} across inbounds (#5443)

Issue 1: the host endpoint remark no longer substitutes the inbound remark
as the config name. {{INBOUND}} always resolves to the inbound's own remark
and {{HOST}} to the host remark, so both can be shown side by side instead
of the host name appearing twice. configName() drops hostRemark entirely;
token help text updated in all locales.

Issue 2: client_traffics.email is globally unique, so a client shared across
several inbounds of one subscription has a single traffic row owned by one
inbound. statsForClient only searched the current inbound's preloaded
ClientStats, missing on every other inbound's link and falling back to
Up=Down=0 -- so {{TRAFFIC_LEFT}} printed the full quota. Build a per-request
email->stats map from all the subscription's inbounds (no extra queries) and
fall back to it.
This commit is contained in:
MHSanaei
2026-06-20 10:54:26 +02:00
parent 6a032bcb2a
commit 6d9fd4b41b
17 changed files with 95 additions and 46 deletions
+22
View File
@@ -47,6 +47,12 @@ type SubService struct {
// inbound whose NodeID is set. Keeps the per-link host derivation
// O(1) instead of O(N) DB hits.
nodesByID map[int]*model.Node
// statsByEmail maps a client email to its traffic row across ALL inbounds
// loaded for the request. client_traffics.email is globally unique, so this
// lets statsForClient resolve usage for a client even on an inbound that
// doesn't own its row (multi-inbound subscriptions). Filled in
// getInboundsBySubId; reset per request in PrepareForRequest.
statsByEmail map[string]xray.ClientTraffic
}
// NewSubService creates a new subscription service with the given configuration.
@@ -78,6 +84,7 @@ func (s *SubService) PrepareForRequest(host string) {
}
s.address = host
s.usageShown = map[string]bool{}
s.statsByEmail = map[string]xray.ClientTraffic{}
s.loadNodes()
s.loadRemarkSettings()
}
@@ -335,9 +342,24 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
if err != nil {
return nil, err
}
s.indexStatsByEmail(inbounds)
return inbounds, nil
}
// indexStatsByEmail records every loaded inbound's client traffic rows keyed by
// email so statsForClient can resolve a client's usage even on an inbound that
// doesn't own its (globally unique) client_traffics row. See statsByEmail.
func (s *SubService) indexStatsByEmail(inbounds []*model.Inbound) {
if s.statsByEmail == nil {
s.statsByEmail = map[string]xray.ClientTraffic{}
}
for _, inbound := range inbounds {
for _, st := range inbound.ClientStats {
s.statsByEmail[st.Email] = st
}
}
}
// projectThroughFallbackMaster mutates the inbound in place so its
// Listen/Port/StreamSettings reflect the externally reachable master
// when applicable. Covers both fallback mechanisms: