diff --git a/database/model/model.go b/database/model/model.go index 01654d22..c5e47895 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -143,6 +143,7 @@ type Client struct { Enable bool `json:"enable" form:"enable"` // Whether the client is enabled TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications SubID string `json:"subId" form:"subId"` // Subscription identifier + SubTotalGB int64 `json:"subTotalGB" form:"subTotalGB"` // Shared total traffic limit in GB Comment string `json:"comment" form:"comment"` // Client comment Reset int `json:"reset" form:"reset"` // Reset period in days CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index af663923..7b1b2c84 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -2394,6 +2394,7 @@ Inbound.ClientBase = class extends XrayCommonClass { reset = 0, created_at = undefined, updated_at = undefined, + subTotalGB = 0, ) { super(); this.email = email; @@ -2407,6 +2408,7 @@ Inbound.ClientBase = class extends XrayCommonClass { this.reset = reset; this.created_at = created_at; this.updated_at = updated_at; + this.subTotalGB = subTotalGB; } static commonArgsFromJson(json = {}) { @@ -2422,6 +2424,7 @@ Inbound.ClientBase = class extends XrayCommonClass { json.reset, json.created_at, json.updated_at, + json.subTotalGB, ]; } @@ -2438,6 +2441,7 @@ Inbound.ClientBase = class extends XrayCommonClass { reset: this.reset, created_at: this.created_at, updated_at: this.updated_at, + subTotalGB: this.subTotalGB, }; } @@ -2466,6 +2470,14 @@ Inbound.ClientBase = class extends XrayCommonClass { set _totalGB(gb) { this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0); } + + get _subTotalGB() { + return NumberFormatter.toFixed(this.subTotalGB / SizeFormatter.ONE_GB, 2); + } + + set _subTotalGB(gb) { + this.subTotalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0); + } }; Inbound.VmessSettings = class extends Inbound.Settings { @@ -2511,9 +2523,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, subTotalGB, ) { - 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, subTotalGB); this.id = id; this.security = security; } @@ -2616,9 +2628,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, subTotalGB, ) { - 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, subTotalGB); this.id = id; this.flow = flow; } @@ -2714,9 +2726,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, subTotalGB, ) { - 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, subTotalGB); this.password = password; } @@ -2816,9 +2828,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, subTotalGB, ) { - 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, subTotalGB); this.method = method; this.password = password; } @@ -2866,9 +2878,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, subTotalGB, ) { - 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, subTotalGB); this.auth = auth; } diff --git a/web/controller/inbound.go b/web/controller/inbound.go index ee024cc6..a7925a9e 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -53,6 +53,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/lastOnline", a.lastOnline) g.POST("/updateClientTraffic/:email", a.updateClientTraffic) g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail) + g.POST("/subTraffic/:subId", a.getSubTraffic) } type CopyInboundClientsRequest struct { @@ -109,6 +110,16 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) { jsonObj(c, clientTraffics, nil) } +func (a *InboundController) getSubTraffic(c *gin.Context) { + subId := c.Param("subId") + up, down, err := a.inboundService.GetSubTraffic(subId) + if err != nil { + jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) + return + } + jsonObj(c, map[string]int64{"up": up, "down": down}, nil) +} + // addInbound creates a new inbound configuration. func (a *InboundController) addInbound(c *gin.Context) { inbound := &model.Inbound{} diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html index 6e525396..47d92608 100644 --- a/web/html/component/aClientTable.html +++ b/web/html/component/aClientTable.html @@ -91,6 +91,10 @@ {{ i18n "remained" }} [[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]] + + {{ i18n "pages.inbounds.subTotalFlow" }} + [[ SizeFormatter.sizeFormat(client.subTotalGB) ]] + @@ -110,7 +114,11 @@
- + +
diff --git a/web/html/form/client.html b/web/html/form/client.html index 989bb471..f1b7c93e 100644 --- a/web/html/form/client.html +++ b/web/html/form/client.html @@ -141,6 +141,18 @@ + + + + [[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[ diff --git a/web/html/modals/client_bulk_modal.html b/web/html/modals/client_bulk_modal.html index 81f711e7..24ce156b 100644 --- a/web/html/modals/client_bulk_modal.html +++ b/web/html/modals/client_bulk_modal.html @@ -91,6 +91,18 @@ + + + + @@ -140,6 +152,7 @@ inbound: new Inbound(), quantity: 1, totalGB: 0, + subTotalGB: 0, limitIp: 0, expiryTime: '', emailMethod: 0, @@ -175,6 +188,7 @@ newClient.security = clientsBulkModal.security; newClient.limitIp = clientsBulkModal.limitIp; newClient._totalGB = clientsBulkModal.totalGB; + newClient._subTotalGB = clientsBulkModal.subTotalGB; newClient._expiryTime = clientsBulkModal.expiryTime; if (clientsBulkModal.inbound.canEnableTlsFlow()) { newClient.flow = clientsBulkModal.flow; @@ -196,6 +210,7 @@ this.confirm = confirm; this.quantity = 1; this.totalGB = 0; + this.subTotalGB = 0; this.expiryTime = 0; this.emailMethod = 0; this.limitIp = 0; diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html index 8ecb59bf..270bb076 100644 --- a/web/html/modals/inbound_info_modal.html +++ b/web/html/modals/inbound_info_modal.html @@ -272,6 +272,22 @@ + + + + + + + + + + + +
{{ i18n "remained" }} (Sub.){{ i18n "pages.inbounds.subTotalFlow" }}
+ [[ getSubRemStats() ]] + + [[ SizeFormatter.sizeFormat(infoModal.clientSettings.subTotalGB) ]] +
@@ -639,6 +655,7 @@ subJsonLink: '', clientIps: '', clientIpsArray: [], + subTraffic: { up: 0, down: 0 }, show(dbInbound, index) { this.index = index; this.inbound = dbInbound.toInbound(); @@ -674,6 +691,11 @@ if (this.clientSettings.subId) { this.subLink = this.genSubLink(this.clientSettings.subId); this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : ''; + HttpUtil.post(`/panel/api/inbounds/subTraffic/${this.clientSettings.subId}`).then(res => { + if (res.success) this.subTraffic = res.obj; + }); + } else { + this.subTraffic = { up: 0, down: 0 }; } } this.visible = true; @@ -776,6 +798,13 @@ .down; return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-'; }, + getSubRemStats() { + const stats = this.infoModal.subTraffic; + const settings = this.infoModal.clientSettings; + if (!stats || !settings || settings.subTotalGB <= 0) return "-"; + let rem = settings.subTotalGB - (stats.up + stats.down); + return SizeFormatter.sizeFormat(rem < 0 ? 0 : rem); + }, refreshIPs() { this.refreshing = true; refreshIPs(this.infoModal.clientStats.email) diff --git a/web/service/inbound.go b/web/service/inbound.go index 74b44b99..243e8047 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1552,6 +1552,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) Email string } + // Check individual limits err := tx.Table("inbounds"). Select("inbounds.tag, client_traffics.email"). Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id"). @@ -1560,6 +1561,22 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) if err != nil { return false, 0, err } + + // Check shared quota (SubTotal) + var sharedResults []struct { + Tag string + Email string + } + err = tx.Table("inbounds"). + Select("inbounds.tag, client_traffics.email"). + Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id"). + Where("client_traffics.sub_id IN (SELECT sub_id FROM client_traffics WHERE sub_id != '' AND sub_total > 0 AND enable = ? GROUP BY sub_id, sub_total HAVING SUM(up + down) >= sub_total)", true). + Where("client_traffics.enable = ?", true). + Scan(&sharedResults).Error + if err == nil { + results = append(results, sharedResults...) + } + s.xrayApi.Init(p.GetAPIPort()) for _, result := range results { err1 := s.xrayApi.RemoveUser(result.Tag, result.Email) @@ -1569,22 +1586,27 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { logger.Debug("User is already disabled. Nothing to do more...") } else { - if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { - logger.Debug("User is already disabled. Nothing to do more...") - } else { - logger.Debug("Error in disabling client by api:", err1) - needRestart = true - } + logger.Debug("Error in disabling client by api:", err1) + needRestart = true } } } s.xrayApi.Close() } + + // Update DB for individual limits result := tx.Model(xray.ClientTraffic{}). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Update("enable", false) err := result.Error count := result.RowsAffected + + // Update DB for shared quota + resultShared := tx.Exec("UPDATE client_traffics SET enable = ? WHERE sub_id IN (SELECT sub_id FROM client_traffics WHERE sub_id != '' AND sub_total > 0 AND enable = ? GROUP BY sub_id, sub_total HAVING SUM(up + down) >= sub_total) AND enable = ?", false, true, true) + if resultShared.Error == nil { + count += resultShared.RowsAffected + } + return needRestart, count, err } @@ -1611,19 +1633,71 @@ func (s *InboundService) MigrationRemoveOrphanedTraffics() { `) } -func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { - clientTraffic := xray.ClientTraffic{} - clientTraffic.InboundId = inboundId - clientTraffic.Email = client.Email - clientTraffic.Total = client.TotalGB - clientTraffic.ExpiryTime = client.ExpiryTime - clientTraffic.Enable = client.Enable - clientTraffic.Up = 0 - clientTraffic.Down = 0 - clientTraffic.Reset = client.Reset - result := tx.Create(&clientTraffic) - err := result.Error - return err +func (s *InboundService) GetSubTraffic(subId string) (int64, int64, error) { + db := database.GetDB() + var result struct { + Up int64 + Down int64 + } + err := db.Model(xray.ClientTraffic{}). + Select("SUM(up) as up, SUM(down) as down"). + Where("sub_id = ?", subId). + Scan(&result).Error + return result.Up, result.Down, err +} + +func (s *InboundService) SyncSubTotal(tx *gorm.DB, subId string, subTotal int64) error { + if subId == "" { + return nil + } + + // 1. Update client_traffics table + err := tx.Model(xray.ClientTraffic{}). + Where("sub_id = ?", subId). + Update("sub_total", subTotal).Error + if err != nil { + return err + } + + // 2. Update inbounds table (JSON settings) + var inbounds []*model.Inbound + // Use a partial match to find relevant inbounds efficiently + err = tx.Where("settings LIKE ?", "%"+subId+"%").Find(&inbounds).Error + if err != nil { + return err + } + + for _, inbound := range inbounds { + var settings map[string]any + err := json.Unmarshal([]byte(inbound.Settings), &settings) + if err != nil { + continue + } + clients, ok := settings["clients"].([]any) + if !ok { + continue + } + modified := false + for _, c := range clients { + clientMap, ok := c.(map[string]any) + if !ok { + continue + } + if sid, ok := clientMap["subId"].(string); ok && sid == subId { + // Update subTotalGB in the JSON + clientMap["subTotalGB"] = subTotal + modified = true + } + } + if modified { + newSettings, _ := json.MarshalIndent(settings, "", " ") + err = tx.Model(model.Inbound{}).Where("id = ?", inbound.Id).Update("settings", string(newSettings)).Error + if err != nil { + logger.Warning("Failed to update inbound settings for sub_total sync:", err) + } + } + } + return nil } func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error { @@ -1635,8 +1709,33 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod "total": client.TotalGB, "expiry_time": client.ExpiryTime, "reset": client.Reset, + "sub_id": client.SubID, + "sub_total": client.SubTotalGB, }) err := result.Error + if err == nil { + s.SyncSubTotal(tx, client.SubID, client.SubTotalGB) + } + return err +} + +func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { + clientTraffic := xray.ClientTraffic{} + clientTraffic.InboundId = inboundId + clientTraffic.Email = client.Email + clientTraffic.Total = client.TotalGB + clientTraffic.ExpiryTime = client.ExpiryTime + clientTraffic.Enable = client.Enable + clientTraffic.Up = 0 + clientTraffic.Down = 0 + clientTraffic.Reset = client.Reset + clientTraffic.SubId = client.SubID + clientTraffic.SubTotal = client.SubTotalGB + result := tx.Create(&clientTraffic) + err := result.Error + if err == nil { + s.SyncSubTotal(tx, client.SubID, client.SubTotalGB) + } return err } diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index 2e76cf94..4cb7a32b 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -257,6 +257,8 @@ "monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs" "meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)" "totalFlow" = "إجمالي التدفق" +"subTotalFlow" = "إجمالي ترافيك الاشتراك" +"subTotalFlowDesc" = "حد الترافيك الإجمالي لكل العملاء اللي بيستخدموا نفس الـ SubID ده" "leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش" "noRecommendKeepDefault" = "ننصح باستخدام الافتراضي" "certificatePath" = "مسار الملف" diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 49c9f952..29333c64 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Leave blank to listen on all IPs" "meansNoLimit" = "= Unlimited. (unit: GB)" "totalFlow" = "Total Flow" +"subTotalFlow" = "Sub. Total Flow" +"subTotalFlowDesc" = "Aggregated traffic limit for all clients sharing this SubID" "leaveBlankToNeverExpire" = "Leave blank to never expire" "noRecommendKeepDefault" = "It is recommended to keep the default" "certificatePath" = "File Path" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index cce4018f..310482ef 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Dejar en blanco por defecto" "meansNoLimit" = " = illimitata. (unidad: GB)" "totalFlow" = "Flujo Total" +"subTotalFlow" = "Tráfico total de suscripción" +"subTotalFlowDesc" = "Límite de tráfico agregado para todos los clientes que comparten este SubID" "leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar" "noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada" "certificatePath" = "Ruta Cert" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index e1f49b80..d69bf9aa 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -257,6 +257,8 @@ "monitorDesc" = "به‌طور پیش‌فرض خالی‌بگذارید" "meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)" "totalFlow" = "ترافیک کل" +"subTotalFlow" = "ترافیک کل اشتراک" +"subTotalFlowDesc" = "مجموع حجم مجاز برای تمام کاربران با این شناسه اشتراک" "leaveBlankToNeverExpire" = "برای منقضی‌نشدن خالی‌بگذارید" "noRecommendKeepDefault" = "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود" "certificatePath" = "مسیر فایل" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml index f2ac71fc..04206aa9 100644 --- a/web/translation/translate.id_ID.toml +++ b/web/translation/translate.id_ID.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP" "meansNoLimit" = "= Unlimited. (unit: GB)" "totalFlow" = "Total Aliran" +"subTotalFlow" = "Total Lalu Lintas Langganan" +"subTotalFlowDesc" = "Batas lalu lintas agregat untuk semua klien yang berbagi SubID ini" "leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa" "noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default" "certificatePath" = "Path Berkas" diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml index da67e758..155d8da8 100644 --- a/web/translation/translate.ja_JP.toml +++ b/web/translation/translate.ja_JP.toml @@ -257,6 +257,8 @@ "monitorDesc" = "空白にするとすべてのIPを監視" "meansNoLimit" = "= 無制限(単位:GB)" "totalFlow" = "総トラフィック" +"subTotalFlow" = "サブスクリプション総流量" +"subTotalFlowDesc" = "このSubIDを共有するすべてのクライアントの集計トラフィック制限" "leaveBlankToNeverExpire" = "空白にすると期限なし" "noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします" "certificatePath" = "ファイルパス" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml index 10a2b156..dbb9660a 100644 --- a/web/translation/translate.pt_BR.toml +++ b/web/translation/translate.pt_BR.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Deixe em branco para ouvir todos os IPs" "meansNoLimit" = "= Ilimitado. (unidade: GB)" "totalFlow" = "Fluxo Total" +"subTotalFlow" = "Fluxo Total de Subscrição" +"subTotalFlowDesc" = "Limite de tráfego agregado para todos os clientes que compartilham este SubID" "leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar" "noRecommendKeepDefault" = "Recomenda-se manter o padrão" "certificatePath" = "Caminho" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index b3ec617d..6b6cc8b1 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов" "meansNoLimit" = "= Без ограничений (значение: ГБ)" "totalFlow" = "Общий расход" +"subTotalFlow" = "Общий трафик подписки" +"subTotalFlowDesc" = "Общий лимит трафика для всех клиентов с этим SubID" "leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным" "noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию" "certificatePath" = "Путь к сертификату" diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml index 5aaa1b03..534e511f 100644 --- a/web/translation/translate.tr_TR.toml +++ b/web/translation/translate.tr_TR.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın" "meansNoLimit" = "= Sınırsız. (birim: GB)" "totalFlow" = "Toplam Akış" +"subTotalFlow" = "Abonelik Toplam Trafiği" +"subTotalFlowDesc" = "Bu SubID'yi paylaşan tüm istemciler için toplam trafik sınırı" "leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın" "noRecommendKeepDefault" = "Varsayılanı korumanız önerilir" "certificatePath" = "Dosya Yolu" diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml index b83122c9..a812ea33 100644 --- a/web/translation/translate.uk_UA.toml +++ b/web/translation/translate.uk_UA.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси" "meansNoLimit" = "= Необмежено. (одиниця: ГБ)" "totalFlow" = "Загальна витрата" +"subTotalFlow" = "Загальний трафік підписки" +"subTotalFlowDesc" = "Сумарне обмеження трафіку для всіх клієнтів із цим SubID" "leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався" "noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням" "certificatePath" = "Шлях до файлу" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 3d836b33..0acae63b 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -257,6 +257,8 @@ "monitorDesc" = "Mặc định để trống" "meansNoLimit" = "= Không giới hạn (đơn vị: GB)" "totalFlow" = "Tổng lưu lượng" +"subTotalFlow" = "Tổng lưu lượng gói đăng ký" +"subTotalFlowDesc" = "Giới hạn lưu lượng tổng hợp cho tất cả người dùng dùng chung SubID này" "leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn" "noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định" "certificatePath" = "Đường dẫn tập" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index 57c23eac..dd93fbfe 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -257,6 +257,8 @@ "monitorDesc" = "留空表示监听所有 IP" "meansNoLimit" = "= 无限制(单位:GB)" "totalFlow" = "总流量" +"subTotalFlow" = "订阅总流量" +"subTotalFlowDesc" = "此订阅 ID 下所有客户端的累计流量限制" "leaveBlankToNeverExpire" = "留空表示永不过期" "noRecommendKeepDefault" = "建议保留默认值" "certificatePath" = "文件路径" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml index 69e5164c..afd0bb7e 100644 --- a/web/translation/translate.zh_TW.toml +++ b/web/translation/translate.zh_TW.toml @@ -257,6 +257,8 @@ "monitorDesc" = "留空表示監聽所有 IP" "meansNoLimit" = "= 無限制(單位:GB)" "totalFlow" = "總流量" +"subTotalFlow" = "訂閱總流量" +"subTotalFlowDesc" = "此 SubID 共享的所有客戶端的流量限制總額" "leaveBlankToNeverExpire" = "留空表示永不過期" "noRecommendKeepDefault" = "建議保留預設值" "certificatePath" = "檔案路徑" diff --git a/xray/client_traffic.go b/xray/client_traffic.go index fcb2585e..76213a84 100644 --- a/xray/client_traffic.go +++ b/xray/client_traffic.go @@ -8,12 +8,13 @@ type ClientTraffic struct { Enable bool `json:"enable" form:"enable"` Email string `json:"email" form:"email" gorm:"unique"` UUID string `json:"uuid" form:"uuid" gorm:"-"` - SubId string `json:"subId" form:"subId" gorm:"-"` + SubId string `json:"subId" form:"subId" gorm:"index"` Up int64 `json:"up" form:"up"` Down int64 `json:"down" form:"down"` AllTime int64 `json:"allTime" form:"allTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` Total int64 `json:"total" form:"total"` + SubTotal int64 `json:"subTotal" form:"subTotal"` Reset int `json:"reset" form:"reset" gorm:"default:0"` LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"` }
{{ i18n "remained" }}