diff --git a/frontend/src/models/dbinbound.ts b/frontend/src/models/dbinbound.ts index 77f7e7de..74cd95ac 100644 --- a/frontend/src/models/dbinbound.ts +++ b/frontend/src/models/dbinbound.ts @@ -190,6 +190,12 @@ export class DBInbound { this._clientStatsMap = null; } + toJSON(): Record { + const out: Record = { ...(this as unknown as Record) }; + delete out._clientStatsMap; + return out; + } + getClientStats(email: string): ClientStats | undefined { if (!this._clientStatsMap) { this._clientStatsMap = new Map(); diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 064d469c..aa5f77a6 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -121,7 +121,7 @@ func (a *InboundController) getInbound(c *gin.Context) { jsonMsg(c, I18nWeb(c, "get"), err) return } - inbound, err := a.inboundService.GetInbound(id) + inbound, err := a.inboundService.GetInboundDetail(id) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) return diff --git a/web/service/inbound.go b/web/service/inbound.go index 360ae116..039b30a8 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -238,11 +238,6 @@ func (s *InboundService) annotateFallbackParents(db *gorm.DB, inbounds []*model. } } -// InboundOption is the lightweight projection of an inbound used by client UI -// pickers — only the fields needed to render labels, filter by protocol, and -// decide whether the XTLS Vision flow selector should appear. Keeping this -// payload minimal avoids shipping per-client settings and traffic stats just -// to populate a dropdown. type InboundOption struct { Id int `json:"id"` Remark string `json:"remark"` @@ -252,10 +247,6 @@ type InboundOption struct { TlsFlowCapable bool `json:"tlsFlowCapable"` } -// GetInboundOptions returns the picker-sized projection of the user's inbounds. -// The TlsFlowCapable flag mirrors Inbound.canEnableTlsFlow() on the frontend -// (VLESS over TCP with tls or reality) so the client modal does not need -// StreamSettings to decide whether to show the Flow field. func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) { db := database.GetDB() var rows []struct { @@ -619,40 +610,9 @@ func (s *InboundService) DelInbound(id int) (bool, error) { logger.Debug("DelInbound: inbound not found, id:", id) } - // Delete client traffics of inbounds - err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).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 - } - clients, err := s.GetClients(inbound) - if err != nil { - return false, err - } - // Bulk-delete client IPs for every email in this inbound. The previous - // per-client loop fired one DELETE per row — at 7k+ clients that meant - // thousands of synchronous SQL roundtrips and a multi-second freeze. - // Chunked to stay under SQLite's bind-variable limit on huge inbounds. - if len(clients) > 0 { - emails := make([]string, 0, len(clients)) - for i := range clients { - if clients[i].Email != "" { - emails = append(emails, clients[i].Email) - } - } - for _, batch := range chunkStrings(uniqueNonEmptyStrings(emails), sqliteMaxVars) { - if err := db.Where("client_email IN ?", batch). - Delete(model.InboundClientIps{}).Error; err != nil { - return false, err - } - } - } return needRestart, db.Delete(model.Inbound{}, id).Error } @@ -667,15 +627,17 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { return inbound, nil } -// SetInboundEnable toggles only the enable flag of an inbound, without -// rewriting the (potentially multi-MB) settings JSON. Used by the UI's -// per-row enable switch — for inbounds with thousands of clients the full -// UpdateInbound path is an order of magnitude too slow for an interactive -// toggle (parses + reserialises every client, runs O(N) traffic diff). -// -// Returns (needRestart, error). needRestart is true when the xray runtime -// could not be re-synced from the cached config and a full restart is -// required to pick up the change. +func (s *InboundService) GetInboundDetail(id int) (*model.Inbound, error) { + db := database.GetDB() + inbound := &model.Inbound{} + err := db.Model(model.Inbound{}).Preload("ClientStats").First(inbound, id).Error + if err != nil { + return nil, err + } + s.enrichClientStats(db, []*model.Inbound{inbound}) + return inbound, nil +} + func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) { inbound, err := s.GetInbound(id) if err != nil {