From a79cb9fe6d6c24645fb1c0c11d161e775459774b Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 17 May 2026 11:25:24 +0200 Subject: [PATCH] refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/api-docs/endpoints.js | 4 +- .../src/pages/inbounds/InboundInfoModal.vue | 4 +- web/controller/client.go | 8 +-- web/job/periodic_traffic_reset_job.go | 3 +- web/service/client.go | 65 ++++++++++++++++++ web/service/inbound.go | 67 ------------------- 6 files changed, 75 insertions(+), 76 deletions(-) diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js index aa239bfc..09e3cdfc 100644 --- a/frontend/src/pages/api-docs/endpoints.js +++ b/frontend/src/pages/api-docs/endpoints.js @@ -473,7 +473,7 @@ export const sections = [ }, { method: 'POST', - path: '/panel/api/clients/clientIps/:email', + path: '/panel/api/clients/ips/:email', summary: 'List source IPs that have connected with the given client’s credentials. Returns an array of "ip (timestamp)" strings.', params: [ { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, @@ -481,7 +481,7 @@ export const sections = [ }, { method: 'POST', - path: '/panel/api/clients/clearClientIps/:email', + path: '/panel/api/clients/clearIps/:email', summary: 'Reset the recorded IP list for a client.', params: [ { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, diff --git a/frontend/src/pages/inbounds/InboundInfoModal.vue b/frontend/src/pages/inbounds/InboundInfoModal.vue index b557c574..99d0855b 100644 --- a/frontend/src/pages/inbounds/InboundInfoModal.vue +++ b/frontend/src/pages/inbounds/InboundInfoModal.vue @@ -137,7 +137,7 @@ async function loadClientIps() { if (!clientStats.value?.email) return; refreshing.value = true; try { - const msg = await HttpUtil.post(`/panel/api/clients/clientIps/${clientStats.value.email}`); + const msg = await HttpUtil.post(`/panel/api/clients/ips/${clientStats.value.email}`); if (!msg?.success) { clientIpsText.value = msg?.obj || 'No IP record'; clientIpsArray.value = []; @@ -164,7 +164,7 @@ async function loadClientIps() { async function clearClientIps() { if (!clientStats.value?.email) return; - const msg = await HttpUtil.post(`/panel/api/clients/clearClientIps/${clientStats.value.email}`); + const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${clientStats.value.email}`); if (msg?.success) { clientIpsArray.value = []; clientIpsText.value = t('tgbot.noIpRecord'); diff --git a/web/controller/client.go b/web/controller/client.go index 6900a52f..04373d1b 100644 --- a/web/controller/client.go +++ b/web/controller/client.go @@ -36,8 +36,8 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) { g.POST("/delDepleted", a.delDepleted) g.POST("/resetTraffic/:email", a.resetTrafficByEmail) g.POST("/updateTraffic/:email", a.updateTrafficByEmail) - g.POST("/clientIps/:email", a.getClientIps) - g.POST("/clearClientIps/:email", a.clearClientIps) + g.POST("/ips/:email", a.getIps) + g.POST("/clearIps/:email", a.clearIps) g.POST("/onlines", a.onlines) g.POST("/lastOnline", a.lastOnline) g.GET("/traffic/:email", a.getTrafficByEmail) @@ -213,7 +213,7 @@ func (a *ClientController) updateTrafficByEmail(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) } -func (a *ClientController) getClientIps(c *gin.Context) { +func (a *ClientController) getIps(c *gin.Context) { email := c.Param("email") ips, err := a.inboundService.GetInboundClientIps(email) if err != nil || ips == "" { @@ -249,7 +249,7 @@ func (a *ClientController) getClientIps(c *gin.Context) { jsonObj(c, ips, nil) } -func (a *ClientController) clearClientIps(c *gin.Context) { +func (a *ClientController) clearIps(c *gin.Context) { email := c.Param("email") if err := a.inboundService.ClearClientIps(email); err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.updateSuccess"), err) diff --git a/web/job/periodic_traffic_reset_job.go b/web/job/periodic_traffic_reset_job.go index 50780765..acc0a354 100644 --- a/web/job/periodic_traffic_reset_job.go +++ b/web/job/periodic_traffic_reset_job.go @@ -11,6 +11,7 @@ type Period string // PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period. type PeriodicTrafficResetJob struct { inboundService service.InboundService + clientService service.ClientService period Period } @@ -42,7 +43,7 @@ func (j *PeriodicTrafficResetJob) Run() { logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr) } - resetClientErr := j.inboundService.ResetAllClientTraffics(inbound.Id) + resetClientErr := j.clientService.ResetAllClientTraffics(&j.inboundService, inbound.Id) if resetClientErr != nil { logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr) } diff --git a/web/service/client.go b/web/service/client.go index fcf3fc59..a3e7d935 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -569,6 +569,71 @@ func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, erro return deleted, needRestart, nil } +func (s *ClientService) ResetAllClientTraffics(inboundSvc *InboundService, id int) error { + return submitTrafficWrite(func() error { + return s.resetAllClientTrafficsLocked(inboundSvc, id) + }) +} + +func (s *ClientService) resetAllClientTrafficsLocked(inboundSvc *InboundService, id int) error { + db := database.GetDB() + now := time.Now().Unix() * 1000 + + if err := db.Transaction(func(tx *gorm.DB) error { + whereText := "inbound_id " + if id == -1 { + whereText += " > ?" + } else { + whereText += " = ?" + } + + result := tx.Model(xray.ClientTraffic{}). + Where(whereText, id). + Updates(map[string]any{"enable": true, "up": 0, "down": 0}) + + if result.Error != nil { + return result.Error + } + + inboundWhereText := "id " + if id == -1 { + inboundWhereText += " > ?" + } else { + inboundWhereText += " = ?" + } + + result = tx.Model(model.Inbound{}). + Where(inboundWhereText, id). + Update("last_traffic_reset_time", now) + + return result.Error + }); err != nil { + return err + } + + var inbounds []model.Inbound + q := db.Model(model.Inbound{}).Where("node_id IS NOT NULL") + if id != -1 { + q = q.Where("id = ?", id) + } + if err := q.Find(&inbounds).Error; err != nil { + logger.Warning("ResetAllClientTraffics: discover node inbounds failed:", err) + return nil + } + for i := range inbounds { + ib := &inbounds[i] + rt, rterr := inboundSvc.runtimeFor(ib) + if rterr != nil { + logger.Warning("ResetAllClientTraffics: runtime lookup for inbound", ib.Id, "failed:", rterr) + continue + } + if e := rt.ResetInboundClientTraffics(context.Background(), ib); e != nil { + logger.Warning("ResetAllClientTraffics: remote propagation to", rt.Name(), "failed:", e) + } + } + return nil +} + func (s *ClientService) ResetAllTraffics() (bool, error) { res := database.GetDB().Model(&xray.ClientTraffic{}). Where("1 = 1"). diff --git a/web/service/inbound.go b/web/service/inbound.go index 8bc6e776..08575845 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -2101,73 +2101,6 @@ func (s *InboundService) resetClientTrafficLocked(id int, clientEmail string) (b return needRestart, nil } -func (s *InboundService) ResetAllClientTraffics(id int) error { - return submitTrafficWrite(func() error { - return s.resetAllClientTrafficsLocked(id) - }) -} - -func (s *InboundService) resetAllClientTrafficsLocked(id int) error { - db := database.GetDB() - now := time.Now().Unix() * 1000 - - if err := db.Transaction(func(tx *gorm.DB) error { - whereText := "inbound_id " - if id == -1 { - whereText += " > ?" - } else { - whereText += " = ?" - } - - // Reset client traffics - result := tx.Model(xray.ClientTraffic{}). - Where(whereText, id). - Updates(map[string]any{"enable": true, "up": 0, "down": 0}) - - if result.Error != nil { - return result.Error - } - - // Update lastTrafficResetTime for the inbound(s) - inboundWhereText := "id " - if id == -1 { - inboundWhereText += " > ?" - } else { - inboundWhereText += " = ?" - } - - result = tx.Model(model.Inbound{}). - Where(inboundWhereText, id). - Update("last_traffic_reset_time", now) - - return result.Error - }); err != nil { - return err - } - - var inbounds []model.Inbound - q := db.Model(model.Inbound{}).Where("node_id IS NOT NULL") - if id != -1 { - q = q.Where("id = ?", id) - } - if err := q.Find(&inbounds).Error; err != nil { - logger.Warning("ResetAllClientTraffics: discover node inbounds failed:", err) - return nil - } - for i := range inbounds { - ib := &inbounds[i] - rt, rterr := s.runtimeFor(ib) - if rterr != nil { - logger.Warning("ResetAllClientTraffics: runtime lookup for inbound", ib.Id, "failed:", rterr) - continue - } - if e := rt.ResetInboundClientTraffics(context.Background(), ib); e != nil { - logger.Warning("ResetAllClientTraffics: remote propagation to", rt.Name(), "failed:", e) - } - } - return nil -} - func (s *InboundService) ResetAllTraffics() error { return submitTrafficWrite(func() error { return s.resetAllTrafficsLocked()