From 462cf857e8abbfa3890f80b292346122488bf6fd Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 23 Apr 2026 22:15:53 +0000 Subject: [PATCH] add share quote between different clients from different inbounds with same subId --- config/version | 2 +- database/model/model.go | 1 + sub/subService.go | 96 +++++- web/assets/js/model/inbound.js | 24 +- web/html/form/client.html | 12 + web/html/inbounds.html | 82 +++++ web/service/inbound.go | 451 +++++++++++++++++++++++++-- web/translation/translate.ar_EG.toml | 2 + web/translation/translate.en_US.toml | 2 + web/translation/translate.es_ES.toml | 2 + web/translation/translate.fa_IR.toml | 2 + web/translation/translate.id_ID.toml | 2 + web/translation/translate.ja_JP.toml | 2 + web/translation/translate.pt_BR.toml | 2 + web/translation/translate.ru_RU.toml | 2 + web/translation/translate.tr_TR.toml | 2 + web/translation/translate.uk_UA.toml | 2 + web/translation/translate.vi_VN.toml | 2 + web/translation/translate.zh_CN.toml | 2 + web/translation/translate.zh_TW.toml | 2 + 20 files changed, 649 insertions(+), 45 deletions(-) diff --git a/config/version b/config/version index 391e9856..6799bc92 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -2.9.2 \ No newline at end of file +2.9.2-sharequota1 diff --git a/database/model/model.go b/database/model/model.go index 01654d22..7d27d717 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -145,6 +145,7 @@ type Client struct { SubID string `json:"subId" form:"subId"` // Subscription identifier Comment string `json:"comment" form:"comment"` // Client comment Reset int `json:"reset" form:"reset"` // Reset period in days + ShareQuota bool `json:"shareQuota" form:"shareQuota"` // Pool totalGB/expiryTime with siblings sharing subId CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp } diff --git a/sub/subService.go b/sub/subService.go index 272bf9d5..f281cef3 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -45,6 +45,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C var traffic xray.ClientTraffic var lastOnline int64 var clientTraffics []xray.ClientTraffic + var clientShared []bool inbounds, err := s.getInboundsBySubId(subId) if err != nil { return nil, 0, traffic, err @@ -80,6 +81,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C result = append(result, link) ct := s.getClientTraffics(inbound.ClientStats, client.Email) clientTraffics = append(clientTraffics, ct) + clientShared = append(clientShared, client.ShareQuota) if ct.LastOnline > lastOnline { lastOnline = ct.LastOnline } @@ -87,28 +89,90 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C } } - // Prepare statistics - for index, clientTraffic := range clientTraffics { - if index == 0 { - traffic.Up = clientTraffic.Up - traffic.Down = clientTraffic.Down - traffic.Total = clientTraffic.Total - if clientTraffic.ExpiryTime > 0 { - traffic.ExpiryTime = clientTraffic.ExpiryTime + // Detect whether any member of this subId opted into shared quota. + hasShared := false + for _, shared := range clientShared { + if shared { + hasShared = true + break + } + } + + if !hasShared { + // No shareQuota in this subId: preserve the original aggregation (Total + // sums across members; ExpiryTime collapses to 0 when members disagree). + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + return result, lastOnline, traffic, nil + } + + // Mixed-or-all-shared case: Up/Down sum across everyone (actual consumption). + // Total combines max-of-shared-totals + sum-of-unshared-totals so shared + // members aren't double-counted while unshared ones still add their own cap. + // ExpiryTime = earliest non-zero across all; 0 only if every member is unset. + var sharedMax int64 + sharedMaxSeen := false + sharedUnlimited := false + var unsharedSum int64 + unsharedUnlimited := false + for i, ct := range clientTraffics { + traffic.Up += ct.Up + traffic.Down += ct.Down + if clientShared[i] { + if ct.Total == 0 { + sharedUnlimited = true + } else if !sharedMaxSeen || ct.Total > sharedMax { + sharedMax = ct.Total + sharedMaxSeen = true } } else { - traffic.Up += clientTraffic.Up - traffic.Down += clientTraffic.Down - if traffic.Total == 0 || clientTraffic.Total == 0 { - traffic.Total = 0 + if ct.Total == 0 { + unsharedUnlimited = true } else { - traffic.Total += clientTraffic.Total - } - if clientTraffic.ExpiryTime != traffic.ExpiryTime { - traffic.ExpiryTime = 0 + unsharedSum += ct.Total } } } + if sharedUnlimited || unsharedUnlimited { + traffic.Total = 0 + } else { + traffic.Total = 0 + if sharedMaxSeen { + traffic.Total += sharedMax + } + traffic.Total += unsharedSum + } + + traffic.ExpiryTime = 0 + expirySet := false + for _, ct := range clientTraffics { + if ct.ExpiryTime == 0 { + continue + } + if !expirySet || ct.ExpiryTime < traffic.ExpiryTime { + traffic.ExpiryTime = ct.ExpiryTime + expirySet = true + } + } return result, lastOnline, traffic, nil } diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 26da6cb3..e7880ab9 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -2087,6 +2087,7 @@ Inbound.ClientBase = class extends XrayCommonClass { reset = 0, created_at = undefined, updated_at = undefined, + shareQuota = false, ) { super(); this.email = email; @@ -2100,6 +2101,7 @@ Inbound.ClientBase = class extends XrayCommonClass { this.reset = reset; this.created_at = created_at; this.updated_at = updated_at; + this.shareQuota = !!shareQuota; } static commonArgsFromJson(json = {}) { @@ -2115,6 +2117,7 @@ Inbound.ClientBase = class extends XrayCommonClass { json.reset, json.created_at, json.updated_at, + json.shareQuota, ]; } @@ -2131,6 +2134,7 @@ Inbound.ClientBase = class extends XrayCommonClass { reset: this.reset, created_at: this.created_at, updated_at: this.updated_at, + shareQuota: !!this.shareQuota, }; } @@ -2204,9 +2208,9 @@ Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase { constructor( id = RandomUtil.randomUUID(), security = USERS_SECURITY.AUTO, - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota, ) { - super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); + super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota); this.id = id; this.security = security; } @@ -2309,9 +2313,9 @@ Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase { constructor( id = RandomUtil.randomUUID(), flow = '', - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota, ) { - super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); + super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota); this.id = id; this.flow = flow; } @@ -2407,9 +2411,9 @@ Inbound.TrojanSettings = class extends Inbound.Settings { Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase { constructor( password = RandomUtil.randomSeq(10), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota, ) { - super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); + super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota); this.password = password; } @@ -2509,9 +2513,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase { constructor( method = '', password = RandomUtil.randomShadowsocksPassword(), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota, ) { - super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); + super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota); this.method = method; this.password = password; } @@ -2559,9 +2563,9 @@ Inbound.HysteriaSettings = class extends Inbound.Settings { Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase { constructor( auth = RandomUtil.randomSeq(10), - email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, + email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota, ) { - super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at); + super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at, shareQuota); this.auth = auth; } diff --git a/web/html/form/client.html b/web/html/form/client.html index 9c6a7f8a..44866be4 100644 --- a/web/html/form/client.html +++ b/web/html/form/client.html @@ -108,6 +108,18 @@ + + + +