feat(clients): make clients+client_inbounds the runtime source of truth

Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.

GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
This commit is contained in:
MHSanaei
2026-05-17 07:15:16 +02:00
parent c251482f26
commit ba3c581372
4 changed files with 297 additions and 54 deletions
+102 -6
View File
@@ -24,7 +24,8 @@ import (
)
type InboundService struct {
xrayApi xray.XrayAPI
xrayApi xray.XrayAPI
clientService ClientService
}
func (s *InboundService) runtimeFor(ib *model.Inbound) (runtime.Runtime, error) {
@@ -395,6 +396,10 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, false, err
}
if err = s.clientService.SyncInbound(tx, inbound.Id, clients); err != nil {
return inbound, false, err
}
needRestart := false
if inbound.Enable {
rt, rterr := s.runtimeFor(inbound)
@@ -447,6 +452,9 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
if err != nil {
return false, err
}
if err := s.clientService.DetachInbound(db, id); err != nil {
return false, err
}
inbound, err := s.GetInbound(id)
if err != nil {
return false, err
@@ -705,7 +713,18 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
}
}
return inbound, needRestart, tx.Save(oldInbound).Error
if err = tx.Save(oldInbound).Error; err != nil {
return inbound, false, err
}
newClients, gcErr := s.GetClients(oldInbound)
if gcErr != nil {
err = gcErr
return inbound, false, err
}
if err = s.clientService.SyncInbound(tx, oldInbound.Id, newClients); err != nil {
return inbound, false, err
}
return inbound, needRestart, nil
}
func (s *InboundService) buildRuntimeInboundForAPI(tx *gorm.DB, inbound *model.Inbound) (*model.Inbound, error) {
@@ -980,7 +999,18 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
}
}
return needRestart, tx.Save(oldInbound).Error
if err = tx.Save(oldInbound).Error; err != nil {
return false, err
}
finalClients, gcErr := s.GetClients(oldInbound)
if gcErr != nil {
err = gcErr
return false, err
}
if err = s.clientService.SyncInbound(tx, oldInbound.Id, finalClients); err != nil {
return false, err
}
return needRestart, nil
}
func (s *InboundService) getClientPrimaryKey(protocol model.Protocol, client model.Client) string {
@@ -1291,7 +1321,17 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
}
}
}
return needRestart, db.Save(oldInbound).Error
if err := db.Save(oldInbound).Error; err != nil {
return false, err
}
finalClients, gcErr := s.GetClients(oldInbound)
if gcErr != nil {
return false, gcErr
}
if err := s.clientService.SyncInbound(db, inboundId, finalClients); err != nil {
return false, err
}
return needRestart, nil
}
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) {
@@ -1540,7 +1580,18 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
logger.Debug("Client old email not found")
needRestart = true
}
return needRestart, tx.Save(oldInbound).Error
if err = tx.Save(oldInbound).Error; err != nil {
return false, err
}
finalClients, gcErr := s.GetClients(oldInbound)
if gcErr != nil {
err = gcErr
return false, err
}
if err = s.clientService.SyncInbound(tx, oldInbound.Id, finalClients); err != nil {
return false, err
}
return needRestart, nil
}
const resetGracePeriodMs int64 = 30000
@@ -2002,6 +2053,20 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
if err != nil {
logger.Warning("AddClientTraffic update inbounds ", err)
logger.Error(inbounds)
} else {
for _, ib := range inbounds {
if ib == nil {
continue
}
cs, gcErr := s.GetClients(ib)
if gcErr != nil {
logger.Warning("AddClientTraffic sync clients: GetClients failed", gcErr)
continue
}
if syncErr := s.clientService.SyncInbound(tx, ib.Id, cs); syncErr != nil {
logger.Warning("AddClientTraffic sync clients: SyncInbound failed", syncErr)
}
}
}
}
@@ -2096,6 +2161,19 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
if err != nil {
return false, 0, err
}
for _, ib := range inbounds {
if ib == nil {
continue
}
cs, gcErr := s.GetClients(ib)
if gcErr != nil {
logger.Warning("autoRenewClients sync clients: GetClients failed", gcErr)
continue
}
if syncErr := s.clientService.SyncInbound(tx, ib.Id, cs); syncErr != nil {
logger.Warning("autoRenewClients sync clients: SyncInbound failed", syncErr)
}
}
err = tx.Save(traffics).Error
if err != nil {
return false, 0, err
@@ -3156,6 +3234,14 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
if err = tx.Save(inbound).Error; err != nil {
return err
}
survivingClients, gcErr := s.GetClients(inbound)
if gcErr != nil {
err = gcErr
return err
}
if err = s.clientService.SyncInbound(tx, inbound.Id, survivingClients); err != nil {
return err
}
}
// Drop now-orphaned rows. With id >= 0, a row is safe to drop only when
@@ -3924,7 +4010,17 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
}
}
return needRestart, db.Save(oldInbound).Error
if err := db.Save(oldInbound).Error; err != nil {
return false, err
}
finalClients, gcErr := s.GetClients(oldInbound)
if gcErr != nil {
return false, gcErr
}
if err := s.clientService.SyncInbound(db, inboundId, finalClients); err != nil {
return false, err
}
return needRestart, nil
}
type SubLinkProvider interface {