Преглед изворни кода

fix(inbounds): preserve client data on delete and show traffic in detail

Deleting an inbound now only detaches its clients (removes the
client_inbounds rows). It no longer deletes client_traffics or client IP
logs: those are keyed centrally by email (one row per client) and must
survive, since a client may stay attached to other inbounds and is
managed from the Clients page.

Separately, /get/:id now uses a new GetInboundDetail that preloads and
enriches ClientStats, so hydrated records (info / QR / export) carry
per-client traffic instead of null. DBInbound.toJSON drops the internal
_clientStatsMap cache so it no longer leaks into the exported JSON.
MHSanaei пре 18 часа
родитељ
комит
6bb5a3b56b
3 измењених фајлова са 18 додато и 50 уклоњено
  1. 6 0
      frontend/src/models/dbinbound.ts
  2. 1 1
      web/controller/inbound.go
  3. 11 49
      web/service/inbound.go

+ 6 - 0
frontend/src/models/dbinbound.ts

@@ -190,6 +190,12 @@ export class DBInbound {
         this._clientStatsMap = null;
     }
 
+    toJSON(): Record<string, unknown> {
+        const out: Record<string, unknown> = { ...(this as unknown as Record<string, unknown>) };
+        delete out._clientStatsMap;
+        return out;
+    }
+
     getClientStats(email: string): ClientStats | undefined {
         if (!this._clientStatsMap) {
             this._clientStatsMap = new Map();

+ 1 - 1
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

+ 11 - 49
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 {