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 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-17 11:25:24 +02:00
parent d4ddf702de
commit a79cb9fe6d
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
6 changed files with 75 additions and 76 deletions

View file

@ -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 clients 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.' },

View file

@ -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');

View file

@ -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)

View file

@ -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)
}

View file

@ -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").

View file

@ -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()