Browse Source

Add all-time traffic for inbounds and clients (#3387)

* feat(db): add allTime field to Inbound and ClientTraffic models

* feat(inbound): increment all_time for inbounds and clients on traffic updates

calculate correct all_time traffic on migrate command

* feat(ui): show all-time traffic column for inbounds and its clients

* i18n: add pages.inbounds.allTimeTraffic label across locales

* Add All Time Traffic Usage in inbounds page top banner
Ali Golzar 1 week ago
parent
commit
3087c1b123

+ 1 - 0
database/model/model.go

@@ -32,6 +32,7 @@ type Inbound struct {
 	Up          int64                `json:"up" form:"up"`
 	Up          int64                `json:"up" form:"up"`
 	Down        int64                `json:"down" form:"down"`
 	Down        int64                `json:"down" form:"down"`
 	Total       int64                `json:"total" form:"total"`
 	Total       int64                `json:"total" form:"total"`
+	AllTime     int64                `json:"allTime" form:"allTime" gorm:"default:0"`
 	Remark      string               `json:"remark" form:"remark"`
 	Remark      string               `json:"remark" form:"remark"`
 	Enable      bool                 `json:"enable" form:"enable"`
 	Enable      bool                 `json:"enable" form:"enable"`
 	ExpiryTime  int64                `json:"expiryTime" form:"expiryTime"`
 	ExpiryTime  int64                `json:"expiryTime" form:"expiryTime"`

+ 1 - 0
web/assets/js/model/dbinbound.js

@@ -6,6 +6,7 @@ class DBInbound {
         this.up = 0;
         this.up = 0;
         this.down = 0;
         this.down = 0;
         this.total = 0;
         this.total = 0;
+        this.allTime = 0;
         this.remark = "";
         this.remark = "";
         this.enable = true;
         this.enable = true;
         this.expiryTime = 0;
         this.expiryTime = 0;

+ 4 - 0
web/html/component/aClientTable.html

@@ -98,6 +98,10 @@
     </table>
     </table>
   </a-popover>
   </a-popover>
 </template>
 </template>
+
+<template slot="allTime" slot-scope="text, client">
+  <a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
+</template>
 <template slot="expiryTime" slot-scope="text, client, index">
 <template slot="expiryTime" slot-scope="text, client, index">
   <template v-if="client.expiryTime !=0 && client.reset >0">
   <template v-if="client.expiryTime !=0 && client.reset >0">
     <a-popover :overlay-class-name="themeSwitcher.currentTheme">
     <a-popover :overlay-class-name="themeSwitcher.currentTheme">

+ 29 - 5
web/html/inbounds.html

@@ -167,28 +167,35 @@
             <a-col>
             <a-col>
               <a-card size="small" :style="{ padding: '16px' }" hoverable>
               <a-card size="small" :style="{ padding: '16px' }" hoverable>
                 <a-row>
                 <a-row>
-                  <a-col :sm="12" :md="6">
+                  <a-col :sm="12" :md="5">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="swap"></a-icon>
                         <a-icon type="swap"></a-icon>
                       </template>
                       </template>
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
-                  <a-col :sm="12" :md="6">
+                  <a-col :sm="12" :md="5">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="pie-chart"></a-icon>
                         <a-icon type="pie-chart"></a-icon>
                       </template>
                       </template>
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
-                  <a-col :sm="12" :md="6">
+                  <a-col :sm="12" :md="5">
+                    <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
+                      <template #prefix>
+                        <a-icon type="history"></a-icon>
+                      </template>
+                    </a-custom-statistic>
+                  </a-col>
+                  <a-col :sm="12" :md="5">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
                     <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
                         <a-icon type="bars"></a-icon>
                         <a-icon type="bars"></a-icon>
                       </template>
                       </template>
                     </a-custom-statistic>
                     </a-custom-statistic>
                   </a-col>
                   </a-col>
-                  <a-col :sm="12" :md="6">
+                  <a-col :sm="12" :md="4">
                     <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
                     <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                       <template #prefix>
                         <a-space direction="horizontal">
                         <a-space direction="horizontal">
@@ -484,6 +491,9 @@
                         </a-tag>
                         </a-tag>
                       </a-popover>
                       </a-popover>
                     </template>
                     </template>
+                    <template slot="allTimeInbound" slot-scope="text, dbInbound">
+                      <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
+                    </template>
                     <template slot="enable" slot-scope="text, dbInbound">
                     <template slot="enable" slot-scope="text, dbInbound">
                       <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                       <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                     </template>
                     </template>
@@ -723,6 +733,11 @@
         align: 'center',
         align: 'center',
         width: 60,
         width: 60,
         scopedSlots: { customRender: 'traffic' },
         scopedSlots: { customRender: 'traffic' },
+    }, {
+        title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
+        align: 'center',
+        width: 60,
+        scopedSlots: { customRender: 'allTimeInbound' },
     }, {
     }, {
         title: '{{ i18n "pages.inbounds.expireDate" }}',
         title: '{{ i18n "pages.inbounds.expireDate" }}',
         align: 'center',
         align: 'center',
@@ -759,6 +774,7 @@
         { title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
         { title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
         { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
         { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
         { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
         { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
+        { title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
         { title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } },
         { title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } },
         { title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } },
         { title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } },
@@ -1419,6 +1435,12 @@
                 clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                 clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                 return clientStats ? clientStats.up + clientStats.down : 0;
                 return clientStats ? clientStats.up + clientStats.down : 0;
             },
             },
+            getAllTimeClient(dbInbound, email) {
+                if (email.length == 0) return 0;
+                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+                if (!clientStats) return 0;
+                return clientStats.allTime || (clientStats.up + clientStats.down);
+            },
             getRemStats(dbInbound, email) {
             getRemStats(dbInbound, email) {
                 if (email.length == 0) return 0;
                 if (email.length == 0) return 0;
                 clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                 clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1608,11 +1630,12 @@
         },
         },
         computed: {
         computed: {
             total() {
             total() {
-                let down = 0, up = 0;
+                let down = 0, up = 0, allTime = 0;
                 let clients = 0, deactive = [], depleted = [], expiring = [];
                 let clients = 0, deactive = [], depleted = [], expiring = [];
                 this.dbInbounds.forEach(dbInbound => {
                 this.dbInbounds.forEach(dbInbound => {
                     down += dbInbound.down;
                     down += dbInbound.down;
                     up += dbInbound.up;
                     up += dbInbound.up;
+                    allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
                     if (this.clientCount[dbInbound.id]) {
                     if (this.clientCount[dbInbound.id]) {
                         clients += this.clientCount[dbInbound.id].clients;
                         clients += this.clientCount[dbInbound.id].clients;
                         deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
                         deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
@@ -1623,6 +1646,7 @@
                 return {
                 return {
                     down: down,
                     down: down,
                     up: up,
                     up: up,
+                    allTime: allTime,
                     clients: clients,
                     clients: clients,
                     deactive: deactive,
                     deactive: deactive,
                     depleted: depleted,
                     depleted: depleted,

+ 24 - 2
web/service/inbound.go

@@ -915,8 +915,9 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
 		if traffic.IsInbound {
 		if traffic.IsInbound {
 			err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
 			err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
 				Updates(map[string]any{
 				Updates(map[string]any{
-					"up":   gorm.Expr("up + ?", traffic.Up),
-					"down": gorm.Expr("down + ?", traffic.Down),
+					"up":       gorm.Expr("up + ?", traffic.Up),
+					"down":     gorm.Expr("down + ?", traffic.Down),
+					"all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down),
 				}).Error
 				}).Error
 			if err != nil {
 			if err != nil {
 				return err
 				return err
@@ -962,6 +963,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
 			if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
 			if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
 				dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
 				dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
 				dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
 				dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
+				dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)
 
 
 				// Add user in onlineUsers array on traffic
 				// Add user in onlineUsers array on traffic
 				if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
 				if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
@@ -2035,6 +2037,26 @@ func (s *InboundService) MigrationRequirements() {
 			tx.Rollback()
 			tx.Rollback()
 		}
 		}
 	}()
 	}()
+	
+
+	// Calculate and backfill all_time from up+down for inbounds and clients
+	err = tx.Exec(`
+		UPDATE inbounds
+		SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
+		WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
+	`).Error
+	if err != nil {
+		return
+	}
+	err = tx.Exec(`
+		UPDATE client_traffics
+		SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
+		WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
+	`).Error
+
+	if err != nil {
+		return
+	}
 
 
 	// Fix inbounds based problems
 	// Fix inbounds based problems
 	var inbounds []*model.Inbound
 	var inbounds []*model.Inbound

+ 2 - 0
web/translation/translate.ar_EG.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
 "getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "إجمالي حركة المرور"
+"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
 "title" = "الإدخالات"
 "title" = "الإدخالات"
 "totalDownUp" = "إجمالي المرسل/المستقبل"
 "totalDownUp" = "إجمالي المرسل/المستقبل"
 "totalUsage" = "إجمالي الاستخدام"
 "totalUsage" = "إجمالي الاستخدام"

+ 2 - 0
web/translation/translate.en_US.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "An error occurred while retrieving the config file."
 "getConfigError" = "An error occurred while retrieving the config file."
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "All-time Traffic"
+"allTimeTrafficUsage" = "All Time Total Usage"
 "title" = "Inbounds"
 "title" = "Inbounds"
 "totalDownUp" = "Total Sent/Received"
 "totalDownUp" = "Total Sent/Received"
 "totalUsage" = "Total Usage"
 "totalUsage" = "Total Usage"

+ 2 - 0
web/translation/translate.es_ES.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
 "getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Tráfico Total"
+"allTimeTrafficUsage" = "Uso total de todos los tiempos"
 "title" = "Entradas"
 "title" = "Entradas"
 "totalDownUp" = "Subidas/Descargas Totales"
 "totalDownUp" = "Subidas/Descargas Totales"
 "totalUsage" = "Uso Total"
 "totalUsage" = "Uso Total"

+ 2 - 0
web/translation/translate.fa_IR.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "خطا در دریافت فایل پیکربندی"
 "getConfigError" = "خطا در دریافت فایل پیکربندی"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "کل ترافیک"
+"allTimeTrafficUsage" = "کل استفاده در تمام مدت"
 "title" = "کاربران"
 "title" = "کاربران"
 "totalDownUp" = "دریافت/ارسال کل"
 "totalDownUp" = "دریافت/ارسال کل"
 "totalUsage" = "‌‌‌مصرف کل"
 "totalUsage" = "‌‌‌مصرف کل"

+ 2 - 0
web/translation/translate.id_ID.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
 "getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Total Lalu Lintas"
+"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu"
 "title" = "Masuk"
 "title" = "Masuk"
 "totalDownUp" = "Total Terkirim/Diterima"
 "totalDownUp" = "Total Terkirim/Diterima"
 "totalUsage" = "Penggunaan Total"
 "totalUsage" = "Penggunaan Total"

+ 2 - 0
web/translation/translate.ja_JP.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "設定ファイルの取得中にエラーが発生しました"
 "getConfigError" = "設定ファイルの取得中にエラーが発生しました"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "総トラフィック"
+"allTimeTrafficUsage" = "これまでの総使用量"
 "title" = "インバウンド一覧"
 "title" = "インバウンド一覧"
 "totalDownUp" = "総アップロード / ダウンロード"
 "totalDownUp" = "総アップロード / ダウンロード"
 "totalUsage" = "総使用量"
 "totalUsage" = "総使用量"

+ 2 - 0
web/translation/translate.pt_BR.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
 "getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Tráfego Total"
+"allTimeTrafficUsage" = "Uso total de todos os tempos"
 "title" = "Inbounds"
 "title" = "Inbounds"
 "totalDownUp" = "Total Enviado/Recebido"
 "totalDownUp" = "Total Enviado/Recebido"
 "totalUsage" = "Uso Total"
 "totalUsage" = "Uso Total"

+ 2 - 0
web/translation/translate.ru_RU.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Произошла ошибка при получении конфигурационного файла"
 "getConfigError" = "Произошла ошибка при получении конфигурационного файла"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Общий трафик"
+"allTimeTrafficUsage" = "Общее использование за все время"
 "title" = "Инбаунды"
 "title" = "Инбаунды"
 "totalDownUp" = "Объем отправленного/полученного трафика"
 "totalDownUp" = "Объем отправленного/полученного трафика"
 "totalUsage" = "Всего трафика"
 "totalUsage" = "Всего трафика"

+ 2 - 0
web/translation/translate.tr_TR.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
 "getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Toplam Trafik"
+"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı"
 "title" = "Gelenler"
 "title" = "Gelenler"
 "totalDownUp" = "Toplam Gönderilen/Alınan"
 "totalDownUp" = "Toplam Gönderilen/Alınan"
 "totalUsage" = "Toplam Kullanım"
 "totalUsage" = "Toplam Kullanım"

+ 2 - 0
web/translation/translate.uk_UA.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
 "getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Загальний трафік"
+"allTimeTrafficUsage" = "Загальне використання за весь час"
 "title" = "Вхідні"
 "title" = "Вхідні"
 "totalDownUp" = "Всього надісланих/отриманих"
 "totalDownUp" = "Всього надісланих/отриманих"
 "totalUsage" = "Всього використанно"
 "totalUsage" = "Всього використанно"

+ 2 - 0
web/translation/translate.vi_VN.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
 "getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "Tổng Lưu Lượng"
+"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc"
 "title" = "Điểm vào (Inbounds)"
 "title" = "Điểm vào (Inbounds)"
 "totalDownUp" = "Tổng tải lên/tải xuống"
 "totalDownUp" = "Tổng tải lên/tải xuống"
 "totalUsage" = "Tổng sử dụng"
 "totalUsage" = "Tổng sử dụng"

+ 2 - 0
web/translation/translate.zh_CN.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "检索配置文件时出错"
 "getConfigError" = "检索配置文件时出错"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "累计总流量"
+"allTimeTrafficUsage" = "所有时间总使用量"
 "title" = "入站列表"
 "title" = "入站列表"
 "totalDownUp" = "总上传 / 下载"
 "totalDownUp" = "总上传 / 下载"
 "totalUsage" = "总用量"
 "totalUsage" = "总用量"

+ 2 - 0
web/translation/translate.zh_TW.toml

@@ -151,6 +151,8 @@
 "getConfigError" = "檢索設定檔時發生錯誤"
 "getConfigError" = "檢索設定檔時發生錯誤"
 
 
 [pages.inbounds]
 [pages.inbounds]
+"allTimeTraffic" = "累計總流量"
+"allTimeTrafficUsage" = "所有时间总使用量"
 "title" = "入站列表"
 "title" = "入站列表"
 "totalDownUp" = "總上傳 / 下載"
 "totalDownUp" = "總上傳 / 下載"
 "totalUsage" = "總用量"
 "totalUsage" = "總用量"

+ 1 - 0
xray/client_traffic.go

@@ -7,6 +7,7 @@ type ClientTraffic struct {
 	Email      string `json:"email" form:"email" gorm:"unique"`
 	Email      string `json:"email" form:"email" gorm:"unique"`
 	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"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
 	Total      int64  `json:"total" form:"total"`
 	Total      int64  `json:"total" form:"total"`
 	Reset      int    `json:"reset" form:"reset" gorm:"default:0"`
 	Reset      int    `json:"reset" form:"reset" gorm:"default:0"`