diff --git a/internal/web/service/bulk_traffic_test.go b/internal/web/service/bulk_traffic_test.go index 2fbb2a878..b3fc9fdfa 100644 --- a/internal/web/service/bulk_traffic_test.go +++ b/internal/web/service/bulk_traffic_test.go @@ -204,3 +204,30 @@ func TestGetClientTrafficByEmailReadsClientsTable(t *testing.T) { t.Fatalf("SubId not enriched from clients table, got %q", tr.SubId) } } + +// A depleted usage row keyed by a padded email belongs to no client; trimming it on the +// way to BulkDelete deleted the healthy client it resembles. +func TestDelDepletedIgnoresPaddedUsageRow(t *testing.T) { + setupBulkDB(t) + svc := &ClientService{} + inboundSvc := &InboundService{} + + source := []model.Client{{Email: "alice@x", ID: "11111111-1111-1111-1111-111111111111", SubID: "sa", Enable: true}} + ib := mkInbound(t, 21003, model.VLESS, clientsSettings(t, source)) + if err := svc.SyncInbound(nil, ib.Id, source); err != nil { + t.Fatalf("seed linkage: %v", err) + } + mkTraffic(t, ib.Id, "alice@x", 0, 0, 0, 0, true) + mkTraffic(t, ib.Id, "alice@x ", 0, 0, 0, 1, false) + + deleted, _, err := svc.DelDepleted(inboundSvc) + if err != nil { + t.Fatalf("DelDepleted: %v", err) + } + if deleted != 0 { + t.Fatalf("deleted %d clients, want 0", deleted) + } + if _, err := svc.GetRecordByEmail(nil, "alice@x"); err != nil { + t.Fatalf("alice was deleted through a look-alike usage row: %v", err) + } +} diff --git a/internal/web/service/client_bulk.go b/internal/web/service/client_bulk.go index a7d5733fe..1f500c8ac 100644 --- a/internal/web/service/client_bulk.go +++ b/internal/web/service/client_bulk.go @@ -1533,8 +1533,9 @@ func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, erro seen := make(map[string]struct{}, len(rows)) emails := make([]string, 0, len(rows)) + // BulkDelete trims each email, so a padded usage row would resolve to the client it resembles. for _, r := range rows { - if r.Email == "" { + if r.Email == "" || r.Email != strings.TrimSpace(r.Email) { continue } if _, ok := seen[r.Email]; ok {