fix(clients): re-enable depleted clients on API renewal (#5619)

Renewing a subscription via POST /panel/api/clients/bulkAdjust extended a client's expiry/quota but left it disabled. The enforcement loop disables a depleted client across client_traffics, client_records and the inbound settings JSON (and pushes that to the node), while BulkAdjust only updated expiry/total and never cleared enable. On a node its UpdateUser push was built from the stale ClientRecord (Enable=false), which the next traffic poll merged back onto the master, so the client never recovered.

BulkAdjust now re-enables a client only when it was disabled because it was depleted and the adjustment lifts it back within limits, computed as a set-difference of the production depletedCond predicate and applied through the canonical BulkSetEnable (run after the per-inbound loop, since lockInbound is non-reentrant). Manually-disabled or still-depleted clients stay disabled.

Update now writes the clients.enable column explicitly so re-enabling sticks for inbound-less clients and stops feeding a stale record into node pushes.
This commit is contained in:
MHSanaei
2026-06-29 13:39:03 +02:00
parent 7a5d6da28c
commit 789e92cddc
6 changed files with 504 additions and 2 deletions
+56
View File
@@ -421,6 +421,27 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
}
}
now := time.Now().Unix() * 1000
cond := depletedCond(db)
candidateEmails := make([]string, 0, len(plan))
for email, entry := range plan {
if entry.applyExpiry || entry.applyTotal {
candidateEmails = append(candidateEmails, email)
}
}
wasDisabledDepleted := map[string]struct{}{}
for _, batch := range chunkStrings(candidateEmails, sqlInChunk) {
var rows []string
if err := db.Model(xray.ClientTraffic{}).
Where(cond+" AND enable = ? AND email IN ?", now, false, batch).
Pluck("email", &rows).Error; err != nil {
return result, needRestart, err
}
for _, e := range rows {
wasDisabledDepleted[e] = struct{}{}
}
}
adjusted := map[string]struct{}{}
for email, entry := range plan {
if _, skipped := skippedReasons[email]; skipped {
@@ -464,6 +485,41 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
}
result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "flow not supported on inbound"})
}
if len(wasDisabledDepleted) > 0 {
stillDepleted := map[string]struct{}{}
wasList := make([]string, 0, len(wasDisabledDepleted))
for e := range wasDisabledDepleted {
wasList = append(wasList, e)
}
for _, batch := range chunkStrings(wasList, sqlInChunk) {
var rows []string
if err := db.Model(xray.ClientTraffic{}).
Where(cond+" AND email IN ?", now, batch).
Pluck("email", &rows).Error; err != nil {
return result, needRestart, err
}
for _, e := range rows {
stillDepleted[e] = struct{}{}
}
}
reEnable := make([]string, 0, len(wasDisabledDepleted))
for e := range wasDisabledDepleted {
if _, still := stillDepleted[e]; !still {
reEnable = append(reEnable, e)
}
}
if len(reEnable) > 0 {
_, nr, err := s.BulkSetEnable(inboundSvc, reEnable, true)
if err != nil {
return result, needRestart, err
}
if nr {
needRestart = true
}
}
}
return result, needRestart, nil
}