mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
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.
This commit is contained in:
parent
a08bb91f58
commit
6bb5a3b56b
3 changed files with 18 additions and 50 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue