mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: implement shared subscription traffic quota across multiple inbounds
- Added SubTotal synchronization across all clients sharing a SubID - Implemented real-time traffic aggregation for subscription groups - Updated UI to display shared remaining quota in info modals - Added full i18n support for 13 languages
This commit is contained in:
parent
15ebf3df10
commit
32c7ceec55
22 changed files with 245 additions and 31 deletions
|
|
@ -143,6 +143,7 @@ type Client struct {
|
||||||
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||||
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||||
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
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
|
Comment string `json:"comment" form:"comment"` // Client comment
|
||||||
Reset int `json:"reset" form:"reset"` // Reset period in days
|
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||||
|
|
|
||||||
|
|
@ -2394,6 +2394,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
||||||
reset = 0,
|
reset = 0,
|
||||||
created_at = undefined,
|
created_at = undefined,
|
||||||
updated_at = undefined,
|
updated_at = undefined,
|
||||||
|
subTotalGB = 0,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
|
@ -2407,6 +2408,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
||||||
this.reset = reset;
|
this.reset = reset;
|
||||||
this.created_at = created_at;
|
this.created_at = created_at;
|
||||||
this.updated_at = updated_at;
|
this.updated_at = updated_at;
|
||||||
|
this.subTotalGB = subTotalGB;
|
||||||
}
|
}
|
||||||
|
|
||||||
static commonArgsFromJson(json = {}) {
|
static commonArgsFromJson(json = {}) {
|
||||||
|
|
@ -2422,6 +2424,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
||||||
json.reset,
|
json.reset,
|
||||||
json.created_at,
|
json.created_at,
|
||||||
json.updated_at,
|
json.updated_at,
|
||||||
|
json.subTotalGB,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2438,6 +2441,7 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
||||||
reset: this.reset,
|
reset: this.reset,
|
||||||
created_at: this.created_at,
|
created_at: this.created_at,
|
||||||
updated_at: this.updated_at,
|
updated_at: this.updated_at,
|
||||||
|
subTotalGB: this.subTotalGB,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2466,6 +2470,14 @@ Inbound.ClientBase = class extends XrayCommonClass {
|
||||||
set _totalGB(gb) {
|
set _totalGB(gb) {
|
||||||
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
|
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 {
|
Inbound.VmessSettings = class extends Inbound.Settings {
|
||||||
|
|
@ -2511,9 +2523,9 @@ Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
|
||||||
constructor(
|
constructor(
|
||||||
id = RandomUtil.randomUUID(),
|
id = RandomUtil.randomUUID(),
|
||||||
security = USERS_SECURITY.AUTO,
|
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.id = id;
|
||||||
this.security = security;
|
this.security = security;
|
||||||
}
|
}
|
||||||
|
|
@ -2616,9 +2628,9 @@ Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase {
|
||||||
constructor(
|
constructor(
|
||||||
id = RandomUtil.randomUUID(),
|
id = RandomUtil.randomUUID(),
|
||||||
flow = '',
|
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.id = id;
|
||||||
this.flow = flow;
|
this.flow = flow;
|
||||||
}
|
}
|
||||||
|
|
@ -2714,9 +2726,9 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||||
Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
|
Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
|
||||||
constructor(
|
constructor(
|
||||||
password = RandomUtil.randomSeq(10),
|
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;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2816,9 +2828,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
|
||||||
constructor(
|
constructor(
|
||||||
method = '',
|
method = '',
|
||||||
password = RandomUtil.randomShadowsocksPassword(),
|
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.method = method;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
@ -2866,9 +2878,9 @@ Inbound.HysteriaSettings = class extends Inbound.Settings {
|
||||||
Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
|
Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
|
||||||
constructor(
|
constructor(
|
||||||
auth = RandomUtil.randomSeq(10),
|
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;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/lastOnline", a.lastOnline)
|
g.POST("/lastOnline", a.lastOnline)
|
||||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||||
|
g.POST("/subTraffic/:subId", a.getSubTraffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CopyInboundClientsRequest struct {
|
type CopyInboundClientsRequest struct {
|
||||||
|
|
@ -109,6 +110,16 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||||
jsonObj(c, clientTraffics, nil)
|
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.
|
// addInbound creates a new inbound configuration.
|
||||||
func (a *InboundController) addInbound(c *gin.Context) {
|
func (a *InboundController) addInbound(c *gin.Context) {
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@
|
||||||
<td>{{ i18n "remained" }}</td>
|
<td>{{ i18n "remained" }}</td>
|
||||||
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
|
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr v-if="client.subTotalGB > 0">
|
||||||
|
<td>{{ i18n "pages.inbounds.subTotalFlow" }}</td>
|
||||||
|
<td>[[ SizeFormatter.sizeFormat(client.subTotalGB) ]]</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -110,7 +114,11 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="tr-table-lt">
|
<td class="tr-table-lt">
|
||||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||||
<span v-else class="tr-infinity-ch">∞</span>
|
<template v-if="client.subTotalGB > 0">
|
||||||
|
<br v-if="client.totalGB > 0" />
|
||||||
|
<span :style="{ fontSize: '10px', color: '#1890ff' }">[[ client._subTotalGB + "GB (Sub)" ]]</span>
|
||||||
|
</template>
|
||||||
|
<span v-if="client.totalGB <= 0 && client.subTotalGB <= 0" class="tr-infinity-ch">∞</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,18 @@
|
||||||
</template>
|
</template>
|
||||||
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
{{ i18n "pages.inbounds.subTotalFlow" }}
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input-number v-model.number="client._subTotalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
|
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
|
||||||
<a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
|
<a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
|
||||||
[[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[
|
[[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,18 @@
|
||||||
</template>
|
</template>
|
||||||
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item v-if="app.subSettings?.enable">
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
{{ i18n "pages.inbounds.subTotalFlow" }}
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input-number v-model.number="clientsBulkModal.subTotalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
||||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -140,6 +152,7 @@
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
totalGB: 0,
|
totalGB: 0,
|
||||||
|
subTotalGB: 0,
|
||||||
limitIp: 0,
|
limitIp: 0,
|
||||||
expiryTime: '',
|
expiryTime: '',
|
||||||
emailMethod: 0,
|
emailMethod: 0,
|
||||||
|
|
@ -175,6 +188,7 @@
|
||||||
newClient.security = clientsBulkModal.security;
|
newClient.security = clientsBulkModal.security;
|
||||||
newClient.limitIp = clientsBulkModal.limitIp;
|
newClient.limitIp = clientsBulkModal.limitIp;
|
||||||
newClient._totalGB = clientsBulkModal.totalGB;
|
newClient._totalGB = clientsBulkModal.totalGB;
|
||||||
|
newClient._subTotalGB = clientsBulkModal.subTotalGB;
|
||||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||||
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
|
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
|
||||||
newClient.flow = clientsBulkModal.flow;
|
newClient.flow = clientsBulkModal.flow;
|
||||||
|
|
@ -196,6 +210,7 @@
|
||||||
this.confirm = confirm;
|
this.confirm = confirm;
|
||||||
this.quantity = 1;
|
this.quantity = 1;
|
||||||
this.totalGB = 0;
|
this.totalGB = 0;
|
||||||
|
this.subTotalGB = 0;
|
||||||
this.expiryTime = 0;
|
this.expiryTime = 0;
|
||||||
this.emailMethod = 0;
|
this.emailMethod = 0;
|
||||||
this.limitIp = 0;
|
this.limitIp = 0;
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,22 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<table v-if="infoModal.clientSettings.subId && infoModal.clientSettings.subTotalGB > 0" :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "remained" }} (Sub.)</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.subTotalFlow" }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a-tag color="blue"> [[ getSubRemStats() ]] </a-tag>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-tag color="blue"> [[ SizeFormatter.sizeFormat(infoModal.clientSettings.subTotalGB) ]] </a-tag>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
<table :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
|
<table :style="{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ i18n "remained" }}</th>
|
<th>{{ i18n "remained" }}</th>
|
||||||
|
|
@ -639,6 +655,7 @@
|
||||||
subJsonLink: '',
|
subJsonLink: '',
|
||||||
clientIps: '',
|
clientIps: '',
|
||||||
clientIpsArray: [],
|
clientIpsArray: [],
|
||||||
|
subTraffic: { up: 0, down: 0 },
|
||||||
show(dbInbound, index) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
|
|
@ -674,6 +691,11 @@
|
||||||
if (this.clientSettings.subId) {
|
if (this.clientSettings.subId) {
|
||||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||||
this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(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;
|
this.visible = true;
|
||||||
|
|
@ -776,6 +798,13 @@
|
||||||
.down;
|
.down;
|
||||||
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
|
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() {
|
refreshIPs() {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
refreshIPs(this.infoModal.clientStats.email)
|
refreshIPs(this.infoModal.clientStats.email)
|
||||||
|
|
|
||||||
|
|
@ -1552,6 +1552,7 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
||||||
Email string
|
Email string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check individual limits
|
||||||
err := tx.Table("inbounds").
|
err := tx.Table("inbounds").
|
||||||
Select("inbounds.tag, client_traffics.email").
|
Select("inbounds.tag, client_traffics.email").
|
||||||
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
|
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
|
||||||
|
|
@ -1560,14 +1561,27 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0, err
|
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())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
|
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
|
||||||
if err1 == nil {
|
if err1 == nil {
|
||||||
logger.Debug("Client disabled by api:", result.Email)
|
logger.Debug("Client disabled by api:", result.Email)
|
||||||
} 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 {
|
} else {
|
||||||
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
|
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
|
||||||
logger.Debug("User is already disabled. Nothing to do more...")
|
logger.Debug("User is already disabled. Nothing to do more...")
|
||||||
|
|
@ -1577,14 +1591,22 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update DB for individual limits
|
||||||
result := tx.Model(xray.ClientTraffic{}).
|
result := tx.Model(xray.ClientTraffic{}).
|
||||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||||
Update("enable", false)
|
Update("enable", false)
|
||||||
err := result.Error
|
err := result.Error
|
||||||
count := result.RowsAffected
|
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
|
return needRestart, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1611,21 +1633,73 @@ func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
|
func (s *InboundService) GetSubTraffic(subId string) (int64, int64, error) {
|
||||||
clientTraffic := xray.ClientTraffic{}
|
db := database.GetDB()
|
||||||
clientTraffic.InboundId = inboundId
|
var result struct {
|
||||||
clientTraffic.Email = client.Email
|
Up int64
|
||||||
clientTraffic.Total = client.TotalGB
|
Down int64
|
||||||
clientTraffic.ExpiryTime = client.ExpiryTime
|
}
|
||||||
clientTraffic.Enable = client.Enable
|
err := db.Model(xray.ClientTraffic{}).
|
||||||
clientTraffic.Up = 0
|
Select("SUM(up) as up, SUM(down) as down").
|
||||||
clientTraffic.Down = 0
|
Where("sub_id = ?", subId).
|
||||||
clientTraffic.Reset = client.Reset
|
Scan(&result).Error
|
||||||
result := tx.Create(&clientTraffic)
|
return result.Up, result.Down, err
|
||||||
err := result.Error
|
}
|
||||||
|
|
||||||
|
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
|
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 {
|
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
|
||||||
result := tx.Model(xray.ClientTraffic{}).
|
result := tx.Model(xray.ClientTraffic{}).
|
||||||
Where("email = ?", email).
|
Where("email = ?", email).
|
||||||
|
|
@ -1635,8 +1709,33 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod
|
||||||
"total": client.TotalGB,
|
"total": client.TotalGB,
|
||||||
"expiry_time": client.ExpiryTime,
|
"expiry_time": client.ExpiryTime,
|
||||||
"reset": client.Reset,
|
"reset": client.Reset,
|
||||||
|
"sub_id": client.SubID,
|
||||||
|
"sub_total": client.SubTotalGB,
|
||||||
})
|
})
|
||||||
err := result.Error
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs"
|
"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs"
|
||||||
"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)"
|
"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)"
|
||||||
"totalFlow" = "إجمالي التدفق"
|
"totalFlow" = "إجمالي التدفق"
|
||||||
|
"subTotalFlow" = "إجمالي ترافيك الاشتراك"
|
||||||
|
"subTotalFlowDesc" = "حد الترافيك الإجمالي لكل العملاء اللي بيستخدموا نفس الـ SubID ده"
|
||||||
"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش"
|
"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش"
|
||||||
"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي"
|
"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي"
|
||||||
"certificatePath" = "مسار الملف"
|
"certificatePath" = "مسار الملف"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Leave blank to listen on all IPs"
|
"monitorDesc" = "Leave blank to listen on all IPs"
|
||||||
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
||||||
"totalFlow" = "Total Flow"
|
"totalFlow" = "Total Flow"
|
||||||
|
"subTotalFlow" = "Sub. Total Flow"
|
||||||
|
"subTotalFlowDesc" = "Aggregated traffic limit for all clients sharing this SubID"
|
||||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||||
"noRecommendKeepDefault" = "It is recommended to keep the default"
|
"noRecommendKeepDefault" = "It is recommended to keep the default"
|
||||||
"certificatePath" = "File Path"
|
"certificatePath" = "File Path"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Dejar en blanco por defecto"
|
"monitorDesc" = "Dejar en blanco por defecto"
|
||||||
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
||||||
"totalFlow" = "Flujo Total"
|
"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"
|
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
||||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||||
"certificatePath" = "Ruta Cert"
|
"certificatePath" = "Ruta Cert"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
|
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
|
||||||
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
|
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
|
||||||
"totalFlow" = "ترافیک کل"
|
"totalFlow" = "ترافیک کل"
|
||||||
|
"subTotalFlow" = "ترافیک کل اشتراک"
|
||||||
|
"subTotalFlowDesc" = "مجموع حجم مجاز برای تمام کاربران با این شناسه اشتراک"
|
||||||
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
|
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
|
||||||
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
|
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
|
||||||
"certificatePath" = "مسیر فایل"
|
"certificatePath" = "مسیر فایل"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
|
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
|
||||||
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
||||||
"totalFlow" = "Total Aliran"
|
"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"
|
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
|
||||||
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
|
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
|
||||||
"certificatePath" = "Path Berkas"
|
"certificatePath" = "Path Berkas"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "空白にするとすべてのIPを監視"
|
"monitorDesc" = "空白にするとすべてのIPを監視"
|
||||||
"meansNoLimit" = "= 無制限(単位:GB)"
|
"meansNoLimit" = "= 無制限(単位:GB)"
|
||||||
"totalFlow" = "総トラフィック"
|
"totalFlow" = "総トラフィック"
|
||||||
|
"subTotalFlow" = "サブスクリプション総流量"
|
||||||
|
"subTotalFlowDesc" = "このSubIDを共有するすべてのクライアントの集計トラフィック制限"
|
||||||
"leaveBlankToNeverExpire" = "空白にすると期限なし"
|
"leaveBlankToNeverExpire" = "空白にすると期限なし"
|
||||||
"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします"
|
"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします"
|
||||||
"certificatePath" = "ファイルパス"
|
"certificatePath" = "ファイルパス"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Deixe em branco para ouvir todos os IPs"
|
"monitorDesc" = "Deixe em branco para ouvir todos os IPs"
|
||||||
"meansNoLimit" = "= Ilimitado. (unidade: GB)"
|
"meansNoLimit" = "= Ilimitado. (unidade: GB)"
|
||||||
"totalFlow" = "Fluxo Total"
|
"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"
|
"leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar"
|
||||||
"noRecommendKeepDefault" = "Recomenda-se manter o padrão"
|
"noRecommendKeepDefault" = "Recomenda-se manter o padrão"
|
||||||
"certificatePath" = "Caminho"
|
"certificatePath" = "Caminho"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов"
|
"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов"
|
||||||
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
|
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
|
||||||
"totalFlow" = "Общий расход"
|
"totalFlow" = "Общий расход"
|
||||||
|
"subTotalFlow" = "Общий трафик подписки"
|
||||||
|
"subTotalFlowDesc" = "Общий лимит трафика для всех клиентов с этим SubID"
|
||||||
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным"
|
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным"
|
||||||
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
|
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
|
||||||
"certificatePath" = "Путь к сертификату"
|
"certificatePath" = "Путь к сертификату"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın"
|
"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın"
|
||||||
"meansNoLimit" = "= Sınırsız. (birim: GB)"
|
"meansNoLimit" = "= Sınırsız. (birim: GB)"
|
||||||
"totalFlow" = "Toplam Akış"
|
"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"
|
"leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın"
|
||||||
"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir"
|
"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir"
|
||||||
"certificatePath" = "Dosya Yolu"
|
"certificatePath" = "Dosya Yolu"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
|
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
|
||||||
"meansNoLimit" = "= Необмежено. (одиниця: ГБ)"
|
"meansNoLimit" = "= Необмежено. (одиниця: ГБ)"
|
||||||
"totalFlow" = "Загальна витрата"
|
"totalFlow" = "Загальна витрата"
|
||||||
|
"subTotalFlow" = "Загальний трафік підписки"
|
||||||
|
"subTotalFlowDesc" = "Сумарне обмеження трафіку для всіх клієнтів із цим SubID"
|
||||||
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
|
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
|
||||||
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
|
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
|
||||||
"certificatePath" = "Шлях до файлу"
|
"certificatePath" = "Шлях до файлу"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "Mặc định để trống"
|
"monitorDesc" = "Mặc định để trống"
|
||||||
"meansNoLimit" = "= Không giới hạn (đơn vị: GB)"
|
"meansNoLimit" = "= Không giới hạn (đơn vị: GB)"
|
||||||
"totalFlow" = "Tổng lưu lượng"
|
"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"
|
"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"
|
"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"
|
"certificatePath" = "Đường dẫn tập"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "留空表示监听所有 IP"
|
"monitorDesc" = "留空表示监听所有 IP"
|
||||||
"meansNoLimit" = "= 无限制(单位:GB)"
|
"meansNoLimit" = "= 无限制(单位:GB)"
|
||||||
"totalFlow" = "总流量"
|
"totalFlow" = "总流量"
|
||||||
|
"subTotalFlow" = "订阅总流量"
|
||||||
|
"subTotalFlowDesc" = "此订阅 ID 下所有客户端的累计流量限制"
|
||||||
"leaveBlankToNeverExpire" = "留空表示永不过期"
|
"leaveBlankToNeverExpire" = "留空表示永不过期"
|
||||||
"noRecommendKeepDefault" = "建议保留默认值"
|
"noRecommendKeepDefault" = "建议保留默认值"
|
||||||
"certificatePath" = "文件路径"
|
"certificatePath" = "文件路径"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@
|
||||||
"monitorDesc" = "留空表示監聽所有 IP"
|
"monitorDesc" = "留空表示監聽所有 IP"
|
||||||
"meansNoLimit" = "= 無限制(單位:GB)"
|
"meansNoLimit" = "= 無限制(單位:GB)"
|
||||||
"totalFlow" = "總流量"
|
"totalFlow" = "總流量"
|
||||||
|
"subTotalFlow" = "訂閱總流量"
|
||||||
|
"subTotalFlowDesc" = "此 SubID 共享的所有客戶端的流量限制總額"
|
||||||
"leaveBlankToNeverExpire" = "留空表示永不過期"
|
"leaveBlankToNeverExpire" = "留空表示永不過期"
|
||||||
"noRecommendKeepDefault" = "建議保留預設值"
|
"noRecommendKeepDefault" = "建議保留預設值"
|
||||||
"certificatePath" = "檔案路徑"
|
"certificatePath" = "檔案路徑"
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@ type ClientTraffic struct {
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"`
|
||||||
Email string `json:"email" form:"email" gorm:"unique"`
|
Email string `json:"email" form:"email" gorm:"unique"`
|
||||||
UUID string `json:"uuid" form:"uuid" gorm:"-"`
|
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"`
|
Up int64 `json:"up" form:"up"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"`
|
||||||
AllTime int64 `json:"allTime" form:"allTime"`
|
AllTime int64 `json:"allTime" form:"allTime"`
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total"`
|
||||||
|
SubTotal int64 `json:"subTotal" form:"subTotal"`
|
||||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
||||||
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue