From e4ef8a54d482dd78abac07f9894219addaea9a33 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 14 Jul 2026 23:39:51 +0200 Subject: [PATCH] fix(client): persist a bulk adjustment's applied field even when the sibling field is skipped In a mixed BulkAdjust (both a days delta and a bytes delta), a per-field planning skip such as "unlimited expiry" or "unlimited traffic" was recorded in the same map that gated the client_traffics write. The applied field was already written to the inbound JSON and the clients table, but the enforcement row was left untouched, so the depletion job cut the client on the old limit while the panel showed the new one. Gate the traffic-row write on an actual inbound-processing failure rather than on any planning-phase skip note. --- internal/web/service/client_bulk.go | 4 +++- .../web/service/client_bulk_reenable_test.go | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/internal/web/service/client_bulk.go b/internal/web/service/client_bulk.go index 6a18dbf19..c0cc32eec 100644 --- a/internal/web/service/client_bulk.go +++ b/internal/web/service/client_bulk.go @@ -409,6 +409,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, needRestart := false flowHonored := map[string]bool{} flowIneligible := map[string]bool{} + execFailed := map[string]bool{} for inboundId, ibEmails := range emailsByInbound { ibRes := s.bulkAdjustInboundClients(inboundSvc, inboundId, ibEmails, plan, flow) if ibRes.needRestart { @@ -421,6 +422,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, flowIneligible[email] = true } for email, reason := range ibRes.perEmailSkipped { + execFailed[email] = true if _, already := skippedReasons[email]; !already { skippedReasons[email] = reason } @@ -450,7 +452,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, adjusted := map[string]struct{}{} for email, entry := range plan { - if _, skipped := skippedReasons[email]; skipped { + if execFailed[email] { continue } updates := map[string]any{} diff --git a/internal/web/service/client_bulk_reenable_test.go b/internal/web/service/client_bulk_reenable_test.go index 693bc0f4f..e753616dd 100644 --- a/internal/web/service/client_bulk_reenable_test.go +++ b/internal/web/service/client_bulk_reenable_test.go @@ -189,6 +189,27 @@ func TestBulkAdjust_QuotaReductionBelowZeroSkipsInsteadOfUnlimited(t *testing.T) } } +func TestBulkAdjust_AppliedFieldReachesTrafficRowDespiteOtherFieldSkip(t *testing.T) { + setupBulkDB(t) + svc := &ClientService{} + inboundSvc := &InboundService{} + + email := "mix2@x" + c := model.Client{Email: email, ID: "11111111-1111-1111-1111-111111111111", SubID: email, Enable: true, TotalGB: 100, ExpiryTime: 0} + ib := mkInbound(t, 52021, model.VLESS, clientsSettings(t, []model.Client{c})) + if err := svc.SyncInbound(nil, ib.Id, []model.Client{c}); err != nil { + t.Fatalf("seed linkage: %v", err) + } + mkTraffic(t, ib.Id, email, 0, 0, 100, 0, true) + + if _, _, err := svc.BulkAdjust(inboundSvc, []string{email}, 30, 50, ""); err != nil { + t.Fatalf("BulkAdjust: %v", err) + } + if got := trafficOf(t, email).Total; got != 150 { + t.Fatalf("client_traffics.total = %d, want 150: the applied quota adjustment must reach the enforcement row even though the unlimited-expiry field was skipped", got) + } +} + func TestBulkAdjust_OverQuota_DaysOnly_StaysDisabled(t *testing.T) { setupBulkDB(t) svc := &ClientService{}