mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 16:20:59 +00:00
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.
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user