Ver Fonte

feat(groups): reset group traffic without touching client counters

The group page shows traffic counting per group, but the only reset
available zeroed every member client's up/down counters (and their
quotas) via bulkResetTraffic. Group traffic is a derived sum of client
traffic, so zeroing the group display previously required mutating the
clients themselves.

Add a display-only baseline: ClientGroup gains reset_up/reset_down
columns (additive, handled by AutoMigrate). ResetGroupTraffic snapshots
the group's current up/down sum into the baseline, and ListGroups now
reports max(0, sum - baseline). Client counters are left untouched and
no Xray restart is triggered. A new POST /panel/api/clients/groups/
resetTraffic endpoint drives it, creating the client_groups row when the
group exists only as a derived label.

The groups page action now calls the new endpoint; confirm/success
strings updated across all 13 locales to reflect group-only semantics.
MHSanaei há 8 horas atrás
pai
commit
9b8a0c9b17

+ 49 - 0
frontend/public/openapi.json

@@ -6619,6 +6619,55 @@
         }
       }
     },
+    "/panel/api/clients/groups/resetTraffic": {
+      "post": {
+        "tags": [
+          "Clients"
+        ],
+        "summary": "Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group's members as a baseline so the group total reads zero, while leaving each client's own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.",
+        "operationId": "post_panel_api_clients_groups_resetTraffic",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object"
+              },
+              "example": {
+                "name": "customer-a"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": {
+                    "name": "customer-a"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/clients/resetTraffic/{email}": {
       "post": {
         "tags": [

+ 7 - 0
frontend/src/pages/api-docs/endpoints.ts

@@ -784,6 +784,13 @@ export const sections: readonly Section[] = [
         body: '{\n  "name": "customer-a"\n}',
         response: '{\n  "success": true,\n  "obj": {\n    "affected": 5\n  }\n}',
       },
+      {
+        method: 'POST',
+        path: '/panel/api/clients/groups/resetTraffic',
+        summary: 'Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group\'s members as a baseline so the group total reads zero, while leaving each client\'s own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.',
+        body: '{\n  "name": "customer-a"\n}',
+        response: '{\n  "success": true,\n  "obj": {\n    "name": "customer-a"\n  }\n}',
+      },
       {
         method: 'POST',
         path: '/panel/api/clients/resetTraffic/:email',

+ 6 - 9
frontend/src/pages/groups/GroupsPage.tsx

@@ -126,9 +126,9 @@ export default function GroupsPage() {
     onSuccess: (msg) => { if (msg?.success) invalidate(); },
   });
 
-  const bulkResetMut = useMutation({
-    mutationFn: (body: { emails: string[] }) =>
-      HttpUtil.post('/panel/api/clients/bulkResetTraffic', body, JSON_HEADERS),
+  const groupResetMut = useMutation({
+    mutationFn: (body: { name: string }) =>
+      HttpUtil.post('/panel/api/clients/groups/resetTraffic', body, JSON_HEADERS),
     onSuccess: (msg) => { if (msg?.success) invalidate(); },
   });
 
@@ -321,17 +321,14 @@ export default function GroupsPage() {
     }
     modal.confirm({
       title: t('pages.groups.resetConfirmTitle', { name: g.name }),
-      content: t('pages.groups.resetConfirmContent', { count: g.clientCount }),
+      content: t('pages.groups.resetConfirmContent'),
       okText: t('reset'),
       okType: 'danger',
       cancelText: t('cancel'),
       onOk: async () => {
-        const emails = await fetchEmailsForGroup(g.name);
-        if (emails.length === 0) return;
-        const msg = await bulkResetMut.mutateAsync({ emails });
+        const msg = await groupResetMut.mutateAsync({ name: g.name });
         if (msg?.success) {
-          const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? emails.length;
-          messageApi.success(t('pages.groups.resetSuccess', { count: affected }));
+          messageApi.success(t('pages.groups.resetSuccess', { name: g.name }));
         }
       },
     });

+ 2 - 0
internal/database/model/model.go

@@ -639,6 +639,8 @@ func (ClientRecord) TableName() string { return "clients" }
 type ClientGroup struct {
 	Id        int    `json:"id" gorm:"primaryKey;autoIncrement"`
 	Name      string `json:"name" gorm:"uniqueIndex;not null"`
+	ResetUp   int64  `json:"resetUp" gorm:"column:reset_up;default:0"`
+	ResetDown int64  `json:"resetDown" gorm:"column:reset_down;default:0"`
 	CreatedAt int64  `json:"createdAt" gorm:"autoCreateTime:milli"`
 	UpdatedAt int64  `json:"updatedAt" gorm:"autoUpdateTime:milli"`
 }

+ 19 - 0
internal/web/controller/group.go

@@ -26,6 +26,7 @@ func (a *GroupController) initRouter(g *gin.RouterGroup) {
 	g.POST("/groups/create", a.create)
 	g.POST("/groups/rename", a.rename)
 	g.POST("/groups/delete", a.delete)
+	g.POST("/groups/resetTraffic", a.resetTraffic)
 	g.POST("/groups/bulkAdd", a.bulkAdd)
 	g.POST("/groups/bulkRemove", a.bulkRemove)
 }
@@ -108,6 +109,24 @@ func (a *GroupController) delete(c *gin.Context) {
 	notifyClientsChanged()
 }
 
+type groupResetTrafficBody struct {
+	Name string `json:"name"`
+}
+
+func (a *GroupController) resetTraffic(c *gin.Context) {
+	var body groupResetTrafficBody
+	if err := c.ShouldBindJSON(&body); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	if err := a.clientService.ResetGroupTraffic(body.Name); err != nil {
+		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
+		return
+	}
+	jsonObj(c, gin.H{"name": body.Name}, nil)
+	notifyClientsChanged()
+}
+
 type bulkAddToGroupRequest struct {
 	Emails []string `json:"emails"`
 	Group  string   `json:"group"`

+ 127 - 0
internal/web/service/client_group_reset_test.go

@@ -0,0 +1,127 @@
+package service
+
+import (
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
+)
+
+func groupByName(t *testing.T, svc *ClientService, name string) GroupSummary {
+	t.Helper()
+	rows, err := svc.ListGroups()
+	if err != nil {
+		t.Fatalf("ListGroups: %v", err)
+	}
+	for _, g := range rows {
+		if g.Name == name {
+			return g
+		}
+	}
+	t.Fatalf("group %q not found in %v", name, rows)
+	return GroupSummary{}
+}
+
+func seedGroupedClient(t *testing.T, email, group string, up, down int64) {
+	t.Helper()
+	if err := database.GetDB().Create(&model.ClientRecord{Email: email, Enable: true, Group: group}).Error; err != nil {
+		t.Fatalf("seed client record %q: %v", email, err)
+	}
+	seedClientRow(t, email, 1, up, down, 0)
+}
+
+func TestResetGroupTraffic_ZeroesGroupButKeepsClients(t *testing.T) {
+	initTrafficTestDB(t)
+	svc := &ClientService{}
+
+	seedGroupedClient(t, "alice", "vip", 100, 200)
+	seedGroupedClient(t, "bob", "vip", 50, 50)
+
+	before := groupByName(t, svc, "vip")
+	if before.Up != 150 || before.Down != 250 || before.TrafficUsed != 400 || before.ClientCount != 2 {
+		t.Fatalf("before reset: got %+v, want up=150 down=250 used=400 count=2", before)
+	}
+
+	if err := svc.ResetGroupTraffic("vip"); err != nil {
+		t.Fatalf("ResetGroupTraffic: %v", err)
+	}
+
+	after := groupByName(t, svc, "vip")
+	if after.Up != 0 || after.Down != 0 || after.TrafficUsed != 0 {
+		t.Fatalf("after reset: got %+v, want up=0 down=0 used=0", after)
+	}
+	if after.ClientCount != 2 {
+		t.Fatalf("after reset: client count changed to %d, want 2", after.ClientCount)
+	}
+
+	var alice xray.ClientTraffic
+	if err := database.GetDB().Where("email = ?", "alice").First(&alice).Error; err != nil {
+		t.Fatalf("load alice traffic: %v", err)
+	}
+	if alice.Up != 100 || alice.Down != 200 {
+		t.Fatalf("client counter modified by group reset: alice up=%d down=%d, want 100/200", alice.Up, alice.Down)
+	}
+}
+
+func TestResetGroupTraffic_NewTrafficAccumulatesAboveBaseline(t *testing.T) {
+	initTrafficTestDB(t)
+	svc := &ClientService{}
+
+	seedGroupedClient(t, "carol", "team", 100, 100)
+	if err := svc.ResetGroupTraffic("team"); err != nil {
+		t.Fatalf("ResetGroupTraffic: %v", err)
+	}
+	if g := groupByName(t, svc, "team"); g.Up != 0 || g.Down != 0 {
+		t.Fatalf("after reset: got %+v, want up=0 down=0", g)
+	}
+
+	if err := database.GetDB().Table("client_traffics").
+		Where("email = ?", "carol").
+		Updates(map[string]any{"up": 130, "down": 100}).Error; err != nil {
+		t.Fatalf("bump carol traffic: %v", err)
+	}
+
+	g := groupByName(t, svc, "team")
+	if g.Up != 30 || g.Down != 0 || g.TrafficUsed != 30 {
+		t.Fatalf("post-bump: got %+v, want up=30 down=0 used=30", g)
+	}
+}
+
+func TestResetGroupTraffic_CreatesRowForDerivedGroup(t *testing.T) {
+	initTrafficTestDB(t)
+	svc := &ClientService{}
+
+	seedGroupedClient(t, "dave", "adhoc", 70, 30)
+
+	var rows int64
+	if err := database.GetDB().Model(&model.ClientGroup{}).Where("name = ?", "adhoc").Count(&rows).Error; err != nil {
+		t.Fatalf("count client_groups: %v", err)
+	}
+	if rows != 0 {
+		t.Fatalf("precondition: derived group should have no client_groups row, got %d", rows)
+	}
+
+	if err := svc.ResetGroupTraffic("adhoc"); err != nil {
+		t.Fatalf("ResetGroupTraffic: %v", err)
+	}
+
+	var stored model.ClientGroup
+	if err := database.GetDB().Where("name = ?", "adhoc").First(&stored).Error; err != nil {
+		t.Fatalf("client_groups row not created: %v", err)
+	}
+	if stored.ResetUp != 70 || stored.ResetDown != 30 {
+		t.Fatalf("baseline not snapshotted: got up=%d down=%d, want 70/30", stored.ResetUp, stored.ResetDown)
+	}
+	if g := groupByName(t, svc, "adhoc"); g.Up != 0 || g.Down != 0 {
+		t.Fatalf("after reset: got %+v, want up=0 down=0", g)
+	}
+}
+
+func TestResetGroupTraffic_EmptyNameRejected(t *testing.T) {
+	initTrafficTestDB(t)
+	svc := &ClientService{}
+	if err := svc.ResetGroupTraffic("   "); err == nil {
+		t.Fatal("ResetGroupTraffic(blank) = nil, want error")
+	}
+}

+ 45 - 6
internal/web/service/client_groups.go

@@ -36,21 +36,32 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
 		return nil, err
 	}
 	type groupAgg struct {
-		count   int
-		traffic int64
-		up      int64
-		down    int64
+		count int
+		up    int64
+		down  int64
 	}
+	baseUp := make(map[string]int64, len(stored))
+	baseDown := make(map[string]int64, len(stored))
 	merged := make(map[string]groupAgg, len(derived)+len(stored))
 	for _, g := range stored {
 		merged[g.Name] = groupAgg{}
+		baseUp[g.Name] = g.ResetUp
+		baseDown[g.Name] = g.ResetDown
 	}
 	for _, g := range derived {
-		merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed, up: g.Up, down: g.Down}
+		merged[g.Name] = groupAgg{count: g.ClientCount, up: g.Up, down: g.Down}
 	}
 	out := make([]GroupSummary, 0, len(merged))
 	for name, agg := range merged {
-		out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic, Up: agg.up, Down: agg.down})
+		up := agg.up - baseUp[name]
+		if up < 0 {
+			up = 0
+		}
+		down := agg.down - baseDown[name]
+		if down < 0 {
+			down = 0
+		}
+		out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: up + down, Up: up, Down: down})
 	}
 	sort.Slice(out, func(i, j int) bool {
 		return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
@@ -77,6 +88,34 @@ func (s *ClientService) EmailsByGroup(name string) ([]string, error) {
 	return emails, nil
 }
 
+func (s *ClientService) ResetGroupTraffic(name string) error {
+	name = strings.TrimSpace(name)
+	if name == "" {
+		return common.NewError("group name is required")
+	}
+	db := database.GetDB()
+	var agg struct {
+		Up   int64
+		Down int64
+	}
+	if err := db.Table("clients AS c").
+		Select("COALESCE(SUM(ct.up), 0) AS up, COALESCE(SUM(ct.down), 0) AS down").
+		Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
+		Where("c.group_name = ?", name).
+		Scan(&agg).Error; err != nil {
+		return err
+	}
+	var count int64
+	if err := db.Model(&model.ClientGroup{}).Where("name = ?", name).Count(&count).Error; err != nil {
+		return err
+	}
+	if count == 0 {
+		return db.Create(&model.ClientGroup{Name: name, ResetUp: agg.Up, ResetDown: agg.Down}).Error
+	}
+	return db.Model(&model.ClientGroup{}).Where("name = ?", name).
+		Updates(map[string]any{"reset_up": agg.Up, "reset_down": agg.Down}).Error
+}
+
 func (s *ClientService) CreateGroup(name string) error {
 	name = strings.TrimSpace(name)
 	if name == "" {

+ 2 - 2
internal/web/translation/ar-EG.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "تم مسح المجموعة من {count} عميل.",
       "resetTraffic": "إعادة تعيين حركة المرور",
       "resetConfirmTitle": "إعادة تعيين حركة المرور للمجموعة {name}؟",
-      "resetConfirmContent": "يصفر up/down لجميع {count} عميل في هذه المجموعة.",
-      "resetSuccess": "تمت إعادة تعيين حركة المرور لـ {count} عميل.",
+      "resetConfirmContent": "يعيد تعيين عداد حركة مرور المجموعة فقط؛ ولا تتأثر عدادات العملاء الفرديين.",
+      "resetSuccess": "تمت إعادة تعيين حركة مرور المجموعة {name}.",
       "adjustSuccess": "تم ضبط {count} عميل في {name}.",
       "emptyForAction": "هذه المجموعة فارغة.",
       "deleteGroupOnly": "حذف المجموعة (مع الاحتفاظ بالعملاء)",

+ 2 - 2
internal/web/translation/en-US.json

@@ -937,8 +937,8 @@
       "deleteSuccess": "Cleared group from {count} client(s).",
       "resetTraffic": "Reset traffic",
       "resetConfirmTitle": "Reset traffic for group {name}?",
-      "resetConfirmContent": "This zeros up/down for all {count} client(s) in this group.",
-      "resetSuccess": "Reset traffic for {count} client(s).",
+      "resetConfirmContent": "This resets only the group's traffic counter. Individual client counters are not affected.",
+      "resetSuccess": "Group {name} traffic reset.",
       "adjustSuccess": "Adjusted {count} client(s) in {name}.",
       "emptyForAction": "This group has no clients yet.",
       "deleteGroupOnly": "Delete group (keep clients)",

+ 2 - 2
internal/web/translation/es-ES.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Grupo limpiado de {count} cliente(s).",
       "resetTraffic": "Restablecer tráfico",
       "resetConfirmTitle": "¿Restablecer tráfico del grupo {name}?",
-      "resetConfirmContent": "Esto pone a cero up/down para los {count} cliente(s) de este grupo.",
-      "resetSuccess": "Tráfico restablecido en {count} cliente(s).",
+      "resetConfirmContent": "Esto restablece solo el contador de tráfico del grupo. Los contadores de cada cliente no se ven afectados.",
+      "resetSuccess": "Tráfico del grupo {name} restablecido.",
       "adjustSuccess": "Ajustados {count} cliente(s) en {name}.",
       "emptyForAction": "Este grupo aún no tiene clientes.",
       "deleteGroupOnly": "Eliminar grupo (conservar clientes)",

+ 2 - 2
internal/web/translation/fa-IR.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "گروه از {count} کاربر پاک شد.",
       "resetTraffic": "بازنشانی ترافیک",
       "resetConfirmTitle": "بازنشانی ترافیک گروه {name}؟",
-      "resetConfirmContent": "این عمل آپلود/دانلود تمام {count} کاربر این گروه را صفر می‌کند.",
-      "resetSuccess": "ترافیک {count} کاربر بازنشانی شد.",
+      "resetConfirmContent": "این فقط شمارنده‌ی ترافیک گروه را صفر می‌کند؛ شمارنده‌ی تک‌تک کاربران دست‌نخورده می‌ماند.",
+      "resetSuccess": "ترافیک گروه {name} صفر شد.",
       "adjustSuccess": "{count} کاربر در {name} تنظیم شد.",
       "emptyForAction": "این گروه هنوز کاربری ندارد.",
       "deleteGroupOnly": "حذف گروه (نگه داشتن کاربران)",

+ 2 - 2
internal/web/translation/id-ID.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Grup dihapus dari {count} klien.",
       "resetTraffic": "Reset trafik",
       "resetConfirmTitle": "Reset trafik grup {name}?",
-      "resetConfirmContent": "Ini mengatur ulang up/down ke 0 untuk semua {count} klien di grup ini.",
-      "resetSuccess": "Trafik direset untuk {count} klien.",
+      "resetConfirmContent": "Ini hanya mengatur ulang penghitung trafik grup. Penghitung tiap klien tidak terpengaruh.",
+      "resetSuccess": "Trafik grup {name} direset.",
       "adjustSuccess": "{count} klien di {name} disesuaikan.",
       "emptyForAction": "Grup ini belum memiliki klien.",
       "deleteGroupOnly": "Hapus grup (pertahankan klien)",

+ 2 - 2
internal/web/translation/ja-JP.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "{count} クライアントのグループをクリアしました。",
       "resetTraffic": "トラフィックをリセット",
       "resetConfirmTitle": "グループ {name} のトラフィックをリセット?",
-      "resetConfirmContent": "このグループ内のすべての {count} クライアントの up/down をゼロにします。",
-      "resetSuccess": "{count} クライアントのトラフィックをリセットしました。",
+      "resetConfirmContent": "グループのトラフィックカウンターのみをリセットします。個々のクライアントのカウンターには影響しません。",
+      "resetSuccess": "グループ {name} のトラフィックをリセットしました。",
       "adjustSuccess": "{name} 内の {count} クライアントを調整しました。",
       "emptyForAction": "このグループにはまだクライアントがありません。",
       "deleteGroupOnly": "グループ削除 (クライアントは保持)",

+ 2 - 2
internal/web/translation/pt-BR.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Grupo limpo de {count} cliente(s).",
       "resetTraffic": "Redefinir tráfego",
       "resetConfirmTitle": "Redefinir tráfego do grupo {name}?",
-      "resetConfirmContent": "Isso zera up/down para todos os {count} cliente(s) deste grupo.",
-      "resetSuccess": "Tráfego redefinido para {count} cliente(s).",
+      "resetConfirmContent": "Isso redefine apenas o contador de tráfego do grupo. Os contadores de cada cliente não são afetados.",
+      "resetSuccess": "Tráfego do grupo {name} redefinido.",
       "adjustSuccess": "Ajustados {count} cliente(s) em {name}.",
       "emptyForAction": "Este grupo ainda não tem clientes.",
       "deleteGroupOnly": "Excluir grupo (manter clientes)",

+ 2 - 2
internal/web/translation/ru-RU.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Группа очищена у {count} клиент(ов).",
       "resetTraffic": "Сбросить трафик",
       "resetConfirmTitle": "Сбросить трафик группы {name}?",
-      "resetConfirmContent": "Это обнулит up/down для всех {count} клиент(ов) в этой группе.",
-      "resetSuccess": "Сброшен трафик у {count} клиент(ов).",
+      "resetConfirmContent": "Это сбросит только счётчик трафика группы. Счётчики отдельных клиентов не затрагиваются.",
+      "resetSuccess": "Трафик группы {name} сброшен.",
       "adjustSuccess": "Скорректировано {count} клиент(ов) в {name}.",
       "emptyForAction": "В этой группе пока нет клиентов.",
       "deleteGroupOnly": "Удалить группу (сохранить клиентов)",

+ 2 - 2
internal/web/translation/tr-TR.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "{count} kullanıcının grubu temizlendi.",
       "resetTraffic": "Trafiği Sıfırla",
       "resetConfirmTitle": "{name} Grubunun Trafiğini Sıfırla?",
-      "resetConfirmContent": "Bu, bu gruptaki tüm {count} kullanıcının yukarı/aşağı trafiğini sıfırlar.",
-      "resetSuccess": "{count} kullanıcının trafiği sıfırlandı.",
+      "resetConfirmContent": "Bu yalnızca grubun trafik sayacını sıfırlar. Tek tek kullanıcı sayaçları etkilenmez.",
+      "resetSuccess": "{name} grubunun trafiği sıfırlandı.",
       "adjustSuccess": "{name} içinde {count} kullanıcı ayarlandı.",
       "emptyForAction": "Bu grupta henüz kullanıcı yok.",
       "deleteGroupOnly": "Grubu Sil (Kullanıcıları Tut)",

+ 2 - 2
internal/web/translation/uk-UA.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Групу очищено у {count} клієнт(ів).",
       "resetTraffic": "Скинути трафік",
       "resetConfirmTitle": "Скинути трафік групи {name}?",
-      "resetConfirmContent": "Це обнулить up/down для всіх {count} клієнт(ів) у цій групі.",
-      "resetSuccess": "Скинуто трафік у {count} клієнт(ів).",
+      "resetConfirmContent": "Це скине лише лічильник трафіку групи. Лічильники окремих клієнтів не змінюються.",
+      "resetSuccess": "Трафік групи {name} скинуто.",
       "adjustSuccess": "Скориговано {count} клієнт(ів) у {name}.",
       "emptyForAction": "У цій групі ще немає клієнтів.",
       "deleteGroupOnly": "Видалити групу (зберегти клієнтів)",

+ 2 - 2
internal/web/translation/vi-VN.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "Đã xóa nhóm khỏi {count} client.",
       "resetTraffic": "Đặt lại lưu lượng",
       "resetConfirmTitle": "Đặt lại lưu lượng nhóm {name}?",
-      "resetConfirmContent": "Việc này đưa up/down về 0 cho tất cả {count} client trong nhóm.",
-      "resetSuccess": "Đã đặt lại lưu lượng cho {count} client.",
+      "resetConfirmContent": "Việc này chỉ đặt lại bộ đếm lưu lượng của nhóm. Bộ đếm của từng client không bị ảnh hưởng.",
+      "resetSuccess": "Đã đặt lại lưu lượng nhóm {name}.",
       "adjustSuccess": "Đã điều chỉnh {count} client trong {name}.",
       "emptyForAction": "Nhóm này chưa có client.",
       "deleteGroupOnly": "Xóa nhóm (giữ client)",

+ 2 - 2
internal/web/translation/zh-CN.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "已清除 {count} 个客户端的分组。",
       "resetTraffic": "重置流量",
       "resetConfirmTitle": "重置分组 {name} 的流量?",
-      "resetConfirmContent": "这将清零此分组中所有 {count} 个客户端的上行/下行流量。",
-      "resetSuccess": "已重置 {count} 个客户端的流量。",
+      "resetConfirmContent": "这只会清零该分组的流量计数器,不影响各个客户端的计数器。",
+      "resetSuccess": "已重置分组 {name} 的流量。",
       "adjustSuccess": "已调整 {name} 中的 {count} 个客户端。",
       "emptyForAction": "此分组尚无客户端。",
       "deleteGroupOnly": "删除分组(保留客户端)",

+ 2 - 2
internal/web/translation/zh-TW.json

@@ -934,8 +934,8 @@
       "deleteSuccess": "已清除 {count} 個客戶端的群組。",
       "resetTraffic": "重置流量",
       "resetConfirmTitle": "重置群組 {name} 的流量?",
-      "resetConfirmContent": "這將將此群組中所有 {count} 個客戶端的上行/下行流量歸零。",
-      "resetSuccess": "已重置 {count} 個客戶端的流量。",
+      "resetConfirmContent": "這只會將此群組的流量計數器歸零,不影響各個客戶端的計數器。",
+      "resetSuccess": "已重置群組 {name} 的流量。",
       "adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。",
       "emptyForAction": "此群組尚無客戶端。",
       "deleteGroupOnly": "刪除群組(保留客戶端)",