mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-22 20:46:08 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user