Przeglądaj źródła

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.
MHSanaei 1 dzień temu
rodzic
commit
e4ef8a54d4

+ 3 - 1
internal/web/service/client_bulk.go

@@ -409,6 +409,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
 	needRestart := false
 	needRestart := false
 	flowHonored := map[string]bool{}
 	flowHonored := map[string]bool{}
 	flowIneligible := map[string]bool{}
 	flowIneligible := map[string]bool{}
+	execFailed := map[string]bool{}
 	for inboundId, ibEmails := range emailsByInbound {
 	for inboundId, ibEmails := range emailsByInbound {
 		ibRes := s.bulkAdjustInboundClients(inboundSvc, inboundId, ibEmails, plan, flow)
 		ibRes := s.bulkAdjustInboundClients(inboundSvc, inboundId, ibEmails, plan, flow)
 		if ibRes.needRestart {
 		if ibRes.needRestart {
@@ -421,6 +422,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
 			flowIneligible[email] = true
 			flowIneligible[email] = true
 		}
 		}
 		for email, reason := range ibRes.perEmailSkipped {
 		for email, reason := range ibRes.perEmailSkipped {
+			execFailed[email] = true
 			if _, already := skippedReasons[email]; !already {
 			if _, already := skippedReasons[email]; !already {
 				skippedReasons[email] = reason
 				skippedReasons[email] = reason
 			}
 			}
@@ -450,7 +452,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
 
 
 	adjusted := map[string]struct{}{}
 	adjusted := map[string]struct{}{}
 	for email, entry := range plan {
 	for email, entry := range plan {
-		if _, skipped := skippedReasons[email]; skipped {
+		if execFailed[email] {
 			continue
 			continue
 		}
 		}
 		updates := map[string]any{}
 		updates := map[string]any{}

+ 21 - 0
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) {
 func TestBulkAdjust_OverQuota_DaysOnly_StaysDisabled(t *testing.T) {
 	setupBulkDB(t)
 	setupBulkDB(t)
 	svc := &ClientService{}
 	svc := &ClientService{}