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;
|
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 {
|
getClientStats(email: string): ClientStats | undefined {
|
||||||
if (!this._clientStatsMap) {
|
if (!this._clientStatsMap) {
|
||||||
this._clientStatsMap = new Map();
|
this._clientStatsMap = new Map();
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ func (a *InboundController) getInbound(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "get"), err)
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inbound, err := a.inboundService.GetInbound(id)
|
inbound, err := a.inboundService.GetInboundDetail(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||||
return
|
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 {
|
type InboundOption struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
|
@ -252,10 +247,6 @@ type InboundOption struct {
|
||||||
TlsFlowCapable bool `json:"tlsFlowCapable"`
|
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) {
|
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
|
|
@ -619,40 +610,9 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
logger.Debug("DelInbound: inbound not found, id:", id)
|
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 {
|
if err := s.clientService.DetachInbound(db, id); err != nil {
|
||||||
return false, err
|
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
|
return needRestart, db.Delete(model.Inbound{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
@ -667,15 +627,17 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
|
||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetInboundEnable toggles only the enable flag of an inbound, without
|
func (s *InboundService) GetInboundDetail(id int) (*model.Inbound, error) {
|
||||||
// rewriting the (potentially multi-MB) settings JSON. Used by the UI's
|
db := database.GetDB()
|
||||||
// per-row enable switch — for inbounds with thousands of clients the full
|
inbound := &model.Inbound{}
|
||||||
// UpdateInbound path is an order of magnitude too slow for an interactive
|
err := db.Model(model.Inbound{}).Preload("ClientStats").First(inbound, id).Error
|
||||||
// toggle (parses + reserialises every client, runs O(N) traffic diff).
|
if err != nil {
|
||||||
//
|
return nil, err
|
||||||
// Returns (needRestart, error). needRestart is true when the xray runtime
|
}
|
||||||
// could not be re-synced from the cached config and a full restart is
|
s.enrichClientStats(db, []*model.Inbound{inbound})
|
||||||
// required to pick up the change.
|
return inbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
||||||
inbound, err := s.GetInbound(id)
|
inbound, err := s.GetInbound(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue