mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-27 07:29:36 +00:00
Replace the panel-side fan-out (Promise.all of single /add and /del
calls) that raced on the shared inbound config and capped throughput at
roughly one round-trip per client. New endpoints batch the work on the
server:
- POST /panel/api/clients/bulkDel { emails, keepTraffic }
- POST /panel/api/clients/bulkCreate [ {client, inboundIds}, ... ]
BulkDelete groups emails by inbound and performs a single
read-modify-write per inbound (one JSON parse, one marshal, one Save)
instead of N. Per-row DB cleanups (ClientInbound, ClientTraffic,
InboundClientIps, ClientRecord) are batched with WHERE...IN queries.
Per-email failures are reported via Skipped[] and processing continues.
BulkCreate iterates payloads sequentially through the same Create path
single-add uses, so heterogeneous batches (different inboundIds, plans)
remain valid in one round-trip.
Frontend bulkDelete/bulkCreate hooks parse the new response shape
({ deleted|created, skipped[] }) and the bulk-add modal now posts a
single request instead of fanning out emails.
396 lines
11 KiB
Go
396 lines
11 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
|
"github.com/mhsanaei/3x-ui/v3/web/websocket"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func notifyClientsChanged() {
|
|
websocket.BroadcastInvalidate(websocket.MessageTypeClients)
|
|
}
|
|
|
|
type ClientController struct {
|
|
clientService service.ClientService
|
|
inboundService service.InboundService
|
|
xrayService service.XrayService
|
|
settingService service.SettingService
|
|
}
|
|
|
|
func NewClientController(g *gin.RouterGroup) *ClientController {
|
|
a := &ClientController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
|
g.GET("/list", a.list)
|
|
g.GET("/list/paged", a.listPaged)
|
|
g.GET("/get/:email", a.get)
|
|
g.GET("/traffic/:email", a.getTrafficByEmail)
|
|
g.GET("/subLinks/:subId", a.getSubLinks)
|
|
g.GET("/links/:email", a.getClientLinks)
|
|
|
|
g.POST("/add", a.create)
|
|
g.POST("/update/:email", a.update)
|
|
g.POST("/del/:email", a.delete)
|
|
g.POST("/:email/attach", a.attach)
|
|
g.POST("/:email/detach", a.detach)
|
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
|
g.POST("/delDepleted", a.delDepleted)
|
|
g.POST("/bulkAdjust", a.bulkAdjust)
|
|
g.POST("/bulkDel", a.bulkDelete)
|
|
g.POST("/bulkCreate", a.bulkCreate)
|
|
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
|
|
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
|
|
g.POST("/ips/:email", a.getIps)
|
|
g.POST("/clearIps/:email", a.clearIps)
|
|
g.POST("/onlines", a.onlines)
|
|
g.POST("/lastOnline", a.lastOnline)
|
|
}
|
|
|
|
func (a *ClientController) list(c *gin.Context) {
|
|
rows, err := a.clientService.List()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
return
|
|
}
|
|
jsonObj(c, rows, nil)
|
|
}
|
|
|
|
func (a *ClientController) listPaged(c *gin.Context) {
|
|
var params service.ClientPageParams
|
|
if err := c.ShouldBindQuery(¶ms); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
return
|
|
}
|
|
resp, err := a.clientService.ListPaged(&a.inboundService, &a.settingService, params)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
return
|
|
}
|
|
jsonObj(c, resp, nil)
|
|
}
|
|
|
|
func (a *ClientController) get(c *gin.Context) {
|
|
email := c.Param("email")
|
|
rec, err := a.clientService.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
inboundIds, err := a.clientService.GetInboundIdsForRecord(rec.Id)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"client": rec, "inboundIds": inboundIds}, nil)
|
|
}
|
|
|
|
func (a *ClientController) create(c *gin.Context) {
|
|
var payload service.ClientCreatePayload
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
needRestart, err := a.clientService.Create(&a.inboundService, &payload)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) update(c *gin.Context) {
|
|
email := c.Param("email")
|
|
var updated model.Client
|
|
if err := c.ShouldBindJSON(&updated); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
needRestart, err := a.clientService.UpdateByEmail(&a.inboundService, email, updated)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) delete(c *gin.Context) {
|
|
email := c.Param("email")
|
|
keepTraffic := c.Query("keepTraffic") == "1"
|
|
needRestart, err := a.clientService.DeleteByEmail(&a.inboundService, email, keepTraffic)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type attachDetachBody struct {
|
|
InboundIds []int `json:"inboundIds"`
|
|
}
|
|
|
|
func (a *ClientController) attach(c *gin.Context) {
|
|
email := c.Param("email")
|
|
var body attachDetachBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
needRestart, err := a.clientService.AttachByEmail(&a.inboundService, email, body.InboundIds)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) resetAllTraffics(c *gin.Context) {
|
|
needRestart, err := a.clientService.ResetAllTraffics()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkAdjustRequest struct {
|
|
Emails []string `json:"emails"`
|
|
AddDays int `json:"addDays"`
|
|
AddBytes int64 `json:"addBytes"`
|
|
}
|
|
|
|
func (a *ClientController) bulkAdjust(c *gin.Context) {
|
|
var req bulkAdjustRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
result, needRestart, err := a.clientService.BulkAdjust(&a.inboundService, req.Emails, req.AddDays, req.AddBytes)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, result, nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkDeleteRequest struct {
|
|
Emails []string `json:"emails"`
|
|
KeepTraffic bool `json:"keepTraffic"`
|
|
}
|
|
|
|
func (a *ClientController) bulkDelete(c *gin.Context) {
|
|
var req bulkDeleteRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
result, needRestart, err := a.clientService.BulkDelete(&a.inboundService, req.Emails, req.KeepTraffic)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, result, nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) bulkCreate(c *gin.Context) {
|
|
var payloads []service.ClientCreatePayload
|
|
if err := c.ShouldBindJSON(&payloads); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
result, needRestart, err := a.clientService.BulkCreate(&a.inboundService, payloads)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, result, nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) delDepleted(c *gin.Context) {
|
|
deleted, needRestart, err := a.clientService.DelDepleted(&a.inboundService)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"deleted": deleted}, nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) resetTrafficByEmail(c *gin.Context) {
|
|
email := c.Param("email")
|
|
needRestart, err := a.clientService.ResetTrafficByEmail(&a.inboundService, email)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundClientTrafficSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type trafficUpdateRequest struct {
|
|
Upload int64 `json:"upload"`
|
|
Download int64 `json:"download"`
|
|
}
|
|
|
|
func (a *ClientController) updateTrafficByEmail(c *gin.Context) {
|
|
email := c.Param("email")
|
|
var req trafficUpdateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if err := a.inboundService.UpdateClientTrafficByEmail(email, req.Upload, req.Download); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
func (a *ClientController) getIps(c *gin.Context) {
|
|
email := c.Param("email")
|
|
ips, err := a.inboundService.GetInboundClientIps(email)
|
|
if err != nil || ips == "" {
|
|
jsonObj(c, "No IP Record", nil)
|
|
return
|
|
}
|
|
type ipWithTimestamp struct {
|
|
IP string `json:"ip"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
var ipsWithTime []ipWithTimestamp
|
|
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
|
|
formatted := make([]string, 0, len(ipsWithTime))
|
|
for _, item := range ipsWithTime {
|
|
if item.IP == "" {
|
|
continue
|
|
}
|
|
if item.Timestamp > 0 {
|
|
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
|
|
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
|
|
continue
|
|
}
|
|
formatted = append(formatted, item.IP)
|
|
}
|
|
jsonObj(c, formatted, nil)
|
|
return
|
|
}
|
|
var oldIps []string
|
|
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
|
|
jsonObj(c, oldIps, nil)
|
|
return
|
|
}
|
|
jsonObj(c, ips, nil)
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
|
}
|
|
|
|
func (a *ClientController) onlines(c *gin.Context) {
|
|
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
|
}
|
|
|
|
func (a *ClientController) lastOnline(c *gin.Context) {
|
|
data, err := a.inboundService.GetClientsLastOnline()
|
|
jsonObj(c, data, err)
|
|
}
|
|
|
|
func (a *ClientController) getTrafficByEmail(c *gin.Context) {
|
|
email := c.Param("email")
|
|
traffic, err := a.inboundService.GetClientTrafficByEmail(email)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
|
return
|
|
}
|
|
jsonObj(c, traffic, nil)
|
|
}
|
|
|
|
func (a *ClientController) getSubLinks(c *gin.Context) {
|
|
links, err := a.inboundService.GetSubLinks(resolveHost(c), c.Param("subId"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
return
|
|
}
|
|
jsonObj(c, links, nil)
|
|
}
|
|
|
|
func (a *ClientController) getClientLinks(c *gin.Context) {
|
|
links, err := a.inboundService.GetAllClientLinks(resolveHost(c), c.Param("email"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
return
|
|
}
|
|
jsonObj(c, links, nil)
|
|
}
|
|
|
|
func (a *ClientController) detach(c *gin.Context) {
|
|
email := c.Param("email")
|
|
var body attachDetachBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
needRestart, err := a.clientService.DetachByEmailMany(&a.inboundService, email, body.InboundIds)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
|
if needRestart {
|
|
a.xrayService.SetToNeedRestart()
|
|
}
|
|
notifyClientsChanged()
|
|
}
|