mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-30 06:57:14 +00:00
fix: refresh stale client_traffics row when an inbound-deleted client's email is reused (#6003)
* fix: refresh stale client_traffics row when an inbound-deleted client's email is reused AddClientStat's OnConflict was DoNothing on email, so once an inbound is deleted (DelInbound only removes the client_inbounds link, matching ClientService.Detach's intentional Detach-then-later-Attach behavior) the orphaned client_traffics row for that email survives untouched. Re-creating a client under the same email on a new inbound silently kept the old enable/expiry_time/reset/total/inbound_id instead of adopting the new client's config. Switch the conflict path to DoUpdates on inbound_id/total/expiry_time/ enable/reset. up/down stay excluded on purpose: every call for an already-attached identity carries the same config values (one call per inbound), so the refresh is a no-op for that legitimate multi-inbound share, while zeroing usage counters on each additional attach would erase real traffic. Fixes #5958 * fix: don't let AddClientStat clobber import's forced-enabled ClientStats rows github-actions[bot] review on #6003 found that AddInbound writes client_traffics twice for the same import payload: first inserting each ClientStats row (DoNothing, with Enable forced true by controller.importInbound), then calling AddClientStat once per Settings-derived client. With AddClientStat's OnConflict now DoUpdates, that second call was unconditionally overwriting enable (and total/expiry_time/reset/inbound_id) with the Settings.clients[].enable value — which still holds whatever the client had at export time, silently undoing the controller's "always import as enabled" behavior for any client disabled at export. Fix: track which emails were already seeded by the ClientStats loop and skip the AddClientStat call for those emails, leaving the import path's forced values as authoritative. Plain (non-import) creates are unaffected since ClientStats is empty there, so every client still goes through AddClientStat's refresh as before. Also updated a stale comment in addClientTraffic that still described AddClientStat as DoNothing. Added TestAddInbound_ImportForcedEnableSurvivesDisabledSettingsClient, which reproduces the exact regression (verified it fails without this fix) and passes with it.
This commit is contained in:
@@ -116,8 +116,9 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
||||
// attached to a local inbound. The old `inbound_id NOT IN (node inbounds)`
|
||||
// filter dropped the local traffic of a client attached to both a node and the
|
||||
// mother inbound whenever the node inbound happened to be attached first — its
|
||||
// shared row then carried the node inbound's id (AddClientStat uses OnConflict
|
||||
// DoNothing and never refreshes it), so the local poll skipped it entirely.
|
||||
// shared row then carried the node inbound's id (AddClientStat used to use
|
||||
// OnConflict DoNothing and never refreshed it; it now refreshes inbound_id on
|
||||
// conflict, but this filter was removed rather than relying on that ordering).
|
||||
err = tx.Model(xray.ClientTraffic{}).
|
||||
Where("email IN (?)", emails).
|
||||
Find(&dbClientTraffics).Error
|
||||
@@ -446,9 +447,28 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
|
||||
return needRestart, int64(len(traffics)), nil
|
||||
}
|
||||
|
||||
// AddClientStat inserts a per-client accounting row, no-op on email
|
||||
// conflict. Xray reports traffic per email, so the surviving row acts as
|
||||
// the shared accumulator for inbounds that re-use the same identity.
|
||||
// AddClientStat inserts a per-client accounting row, or refreshes the
|
||||
// config-derived columns on an email conflict. Xray reports traffic per
|
||||
// email, so the surviving row also acts as the shared accumulator for
|
||||
// inbounds that re-use the same identity — every call for that identity
|
||||
// (one per attached inbound) carries the same enable/expiry/reset/total,
|
||||
// so re-asserting them here is idempotent for that legitimate case.
|
||||
//
|
||||
// The conflict path matters on its own for a second reason: an inbound
|
||||
// delete detaches its clients (InboundService.DelInbound) without deleting
|
||||
// their client_traffics row, by design — mirroring ClientService.Detach,
|
||||
// which intentionally leaves a fully-detached client's row in place so a
|
||||
// later Attach can resume it with its accumulated traffic intact. If that
|
||||
// same email is instead reused for a freshly (re)created client, the new
|
||||
// config's enable/expiry/reset/total must win over whatever the orphaned
|
||||
// row still holds; DoNothing left them stale indefinitely (#5958).
|
||||
//
|
||||
// up/down are deliberately excluded from the refresh: they are the
|
||||
// accumulated traffic totals, and zeroing them here would erase real usage
|
||||
// every time an existing, actively-used client is attached to one more
|
||||
// inbound. One tradeoff this does not resolve: a genuinely new client that
|
||||
// happens to reuse an orphaned email still inherits that row's leftover
|
||||
// up/down, since nothing at this call site can tell the two cases apart.
|
||||
func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {
|
||||
clientTraffic := xray.ClientTraffic{
|
||||
InboundId: inboundId,
|
||||
@@ -458,8 +478,10 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
|
||||
Enable: client.Enable,
|
||||
Reset: client.Reset,
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
|
||||
Create(&clientTraffic).Error
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "email"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"inbound_id", "total", "expiry_time", "enable", "reset"}),
|
||||
}).Create(&clientTraffic).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
|
||||
|
||||
Reference in New Issue
Block a user