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 @@
[[ client._totalGB + "GB" ]]
- ∞
+
+
+ [[ client._subTotalGB + "GB (Sub)" ]]
+
+ ∞
|
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 @@
+
+
+
+
+ 0 {{ i18n "pages.inbounds.meansNoLimit" }}
+
+ {{ i18n "pages.inbounds.subTotalFlow" }}
+
+
+
+
+
[[ 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 @@
+
+
+
+
+ 0 {{ i18n "pages.inbounds.meansNoLimit" }}
+
+ {{ i18n "pages.inbounds.subTotalFlow" }}
+
+
+
+
+
@@ -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) ]]
+ |
+ |
+
+
| {{ i18n "remained" }} |
@@ -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"`
}