فهرست منبع

fix(client): match clients by email for delete/update, not credentials

Delete/update located the client in an inbound's settings JSON by the
record's credential (uuid/password/auth). When that credential drifted
from the inbound JSON -- e.g. a rotated UUID left behind, or duplicated
by a past partial-update bug -- the lookup failed with "Client Not Found
In Inbound For ID: <uuid>" and aborted the whole operation, making the
client impossible to remove from the panel.

Key every delete/update/detach path on email, the client's stable
identity. This survives credential drift and heals duplicate-email
entries by removing all of them.

- Delete/DeleteByEmail/Detach/DetachByEmailMany -> DelInboundClientByEmail
- delInboundClients / bulkDelInboundClients: match settings by email
- UpdateInboundClient: locate the entry to replace by email
  (param clientId -> oldEmail); update all callers to pass the email
- bulkAdjustInboundClients: match by email
- writeBackClientSubID: pass email; drop unused sourceProtocol param
- make per-inbound deletion idempotent via ErrClientNotInInbound
- remove now-orphaned DelInboundClient, clientKeyForProtocol and
  getClientPrimaryKey; scale test deletes by email
MHSanaei 9 ساعت پیش
والد
کامیت
26c549a95a
3فایلهای تغییر یافته به همراه103 افزوده شده و 359 حذف شده
  1. 95 337
      web/service/client.go
  2. 5 19
      web/service/inbound.go
  3. 3 3
      web/service/sync_scale_postgres_test.go

+ 95 - 337
web/service/client.go

@@ -63,24 +63,14 @@ func (c ClientWithAttachments) MarshalJSON() ([]byte, error) {
 	return out, nil
 }
 
-func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string {
-	if rec == nil {
-		return ""
-	}
-	switch p {
-	case model.Trojan:
-		return rec.Password
-	case model.Shadowsocks:
-		return rec.Email
-	case model.Hysteria:
-		return rec.Auth
-	default:
-		return rec.UUID
-	}
-}
-
 type ClientService struct{}
 
+// ErrClientNotInInbound is returned (wrapped) when a client cannot be located
+// in an inbound's settings during deletion. Deletion treats it as non-fatal so
+// the operation stays idempotent and tolerant of pre-existing data drift
+// between the clients table and the inbound's settings JSON.
+var ErrClientNotInInbound = errors.New("client not found in inbound")
+
 // Short-lived tombstone of just-deleted client emails so that a node snapshot
 // arriving between delete and node-side processing doesn't resurrect them.
 var (
@@ -836,8 +826,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
 			}
 			return needRestart, getErr
 		}
-		oldKey := clientKeyForProtocol(inbound.Protocol, existing)
-		if oldKey == "" {
+		if existing.Email == "" {
 			continue
 		}
 		if err := s.fillProtocolDefaults(&updated, inbound); err != nil {
@@ -850,7 +839,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
 		nr, upErr := s.UpdateInboundClient(inboundSvc, &model.Inbound{
 			Id:       ibId,
 			Settings: string(settingsPayload),
-		}, oldKey)
+		}, existing.Email)
 		if upErr != nil {
 			return needRestart, upErr
 		}
@@ -893,19 +882,27 @@ func (s *ClientService) Delete(inboundSvc *InboundService, id int, keepTraffic b
 
 	needRestart := false
 	for _, ibId := range inboundIds {
-		inbound, getErr := inboundSvc.GetInbound(ibId)
-		if getErr != nil {
+		if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
 			if errors.Is(getErr, gorm.ErrRecordNotFound) {
 				continue
 			}
 			return needRestart, getErr
 		}
-		key := clientKeyForProtocol(inbound.Protocol, existing)
-		if key == "" {
+
+		// Always delete by email — the client's stable identity. This removes
+		// every matching entry from the inbound's settings even when the stored
+		// credential (UUID/password/auth) drifted from the inbound JSON, or a
+		// duplicate entry with the same email exists.
+		if existing.Email == "" {
 			continue
 		}
-		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, false)
+		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, false)
 		if delErr != nil {
+			// The client is already absent from this inbound (data drift or a
+			// retried delete). Skip it — deletion stays idempotent.
+			if errors.Is(delErr, ErrClientNotInInbound) {
+				continue
+			}
 			return needRestart, delErr
 		}
 		if nr {
@@ -1220,8 +1217,8 @@ func (s *ClientService) BulkDetach(inboundSvc *InboundService, emails []string,
 // delInboundClients removes several clients from a single inbound in one pass:
 // one settings rewrite, one runtime sweep, one Save and one SyncInbound for the
 // whole batch, instead of repeating the full per-client cycle. It mirrors the
-// semantics of DelInboundClient for each removed client. needRestart is the OR
-// across all removals.
+// semantics of DelInboundClientByEmail for each removed client. needRestart is
+// the OR across all removals.
 func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId int, recs []*model.ClientRecord, keepTraffic bool) (bool, error) {
 	if len(recs) == 0 {
 		return false, nil
@@ -1239,20 +1236,12 @@ func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId
 		return false, err
 	}
 
-	clientKey := "id"
-	switch oldInbound.Protocol {
-	case "trojan":
-		clientKey = "password"
-	case "shadowsocks":
-		clientKey = "email"
-	case "hysteria":
-		clientKey = "auth"
-	}
-
+	// Match by email — the client's stable identity (see Delete). Removes every
+	// entry carrying a wanted email, independent of credential drift.
 	wanted := make(map[string]struct{}, len(recs))
 	for _, rec := range recs {
-		if k := clientKeyForProtocol(oldInbound.Protocol, rec); k != "" {
-			wanted[k] = struct{}{}
+		if rec.Email != "" {
+			wanted[rec.Email] = struct{}{}
 		}
 	}
 
@@ -1273,9 +1262,8 @@ func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId
 			newClients = append(newClients, client)
 			continue
 		}
-		cid, _ := c[clientKey].(string)
-		if _, hit := wanted[cid]; hit && cid != "" {
-			email, _ := c["email"].(string)
+		email, _ := c["email"].(string)
+		if _, hit := wanted[email]; hit && email != "" {
 			enable, _ := c["enable"].(bool)
 			removed = append(removed, removedClient{email: email, needApiDel: enable})
 			continue
@@ -1417,6 +1405,9 @@ func (s *ClientService) DeleteByEmail(inboundSvc *InboundService, email string,
 	for _, ibId := range inboundIds {
 		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, email, false)
 		if delErr != nil {
+			if errors.Is(delErr, ErrClientNotInInbound) {
+				continue
+			}
 			return needRestart, delErr
 		}
 		if nr {
@@ -2720,29 +2711,15 @@ func (s *ClientService) bulkAdjustInboundClients(
 		return res
 	}
 
-	clientKey := "id"
-	switch oldInbound.Protocol {
-	case model.Trojan:
-		clientKey = "password"
-	case model.Shadowsocks:
-		clientKey = "email"
-	case model.Hysteria:
-		clientKey = "auth"
-	}
-
-	keyToEmail := make(map[string]string, len(emails))
+	// Match by email — the client's stable identity (see Delete). Credentials
+	// can drift from the inbound JSON, so they are never used for matching.
+	wantedEmails := make(map[string]struct{}, len(emails))
 	for _, email := range emails {
-		entry := plan[email]
-		if entry == nil {
+		if plan[email] == nil {
 			res.perEmailSkipped[email] = "client not found"
 			continue
 		}
-		key := clientKeyForProtocol(oldInbound.Protocol, entry.record)
-		if key == "" {
-			res.perEmailSkipped[email] = "missing client key for protocol"
-			continue
-		}
-		keyToEmail[key] = email
+		wantedEmails[email] = struct{}{}
 	}
 
 	interfaceClients, _ := settings["clients"].([]any)
@@ -2753,9 +2730,8 @@ func (s *ClientService) bulkAdjustInboundClients(
 		if !ok {
 			continue
 		}
-		cKey, _ := c[clientKey].(string)
-		targetEmail, found := keyToEmail[cKey]
-		if !found {
+		targetEmail, _ := c["email"].(string)
+		if _, want := wantedEmails[targetEmail]; !want || targetEmail == "" {
 			continue
 		}
 		entry := plan[targetEmail]
@@ -2770,7 +2746,7 @@ func (s *ClientService) bulkAdjustInboundClients(
 		foundEmails[targetEmail] = true
 	}
 
-	for _, email := range keyToEmail {
+	for email := range wantedEmails {
 		if !foundEmails[email] {
 			res.perEmailSkipped[email] = "Client Not Found In Inbound"
 		}
@@ -3031,29 +3007,15 @@ func (s *ClientService) bulkDelInboundClients(
 		return res
 	}
 
-	clientKey := "id"
-	switch oldInbound.Protocol {
-	case model.Trojan:
-		clientKey = "password"
-	case model.Shadowsocks:
-		clientKey = "email"
-	case model.Hysteria:
-		clientKey = "auth"
-	}
-
-	keyToEmail := make(map[string]string, len(emails))
+	// Match by email — the client's stable identity (see Delete). Removes every
+	// entry carrying a wanted email, independent of credential drift.
+	wantedEmails := make(map[string]struct{}, len(emails))
 	for _, email := range emails {
-		rec := records[email]
-		if rec == nil {
+		if records[email] == nil {
 			res.perEmailSkipped[email] = "client not found"
 			continue
 		}
-		key := clientKeyForProtocol(oldInbound.Protocol, rec)
-		if key == "" {
-			res.perEmailSkipped[email] = "missing client key for protocol"
-			continue
-		}
-		keyToEmail[key] = email
+		wantedEmails[email] = struct{}{}
 	}
 
 	interfaceClients, _ := settings["clients"].([]any)
@@ -3066,19 +3028,17 @@ func (s *ClientService) bulkDelInboundClients(
 			newClients = append(newClients, client)
 			continue
 		}
-		cKey, _ := c[clientKey].(string)
-		if targetEmail, found := keyToEmail[cKey]; found {
-			foundEmails[targetEmail] = true
-			if em, _ := c["email"].(string); em != "" {
-				en, _ := c["enable"].(bool)
-				enableByEmail[em] = en
-			}
+		em, _ := c["email"].(string)
+		if _, found := wantedEmails[em]; found && em != "" {
+			foundEmails[em] = true
+			en, _ := c["enable"].(bool)
+			enableByEmail[em] = en
 			continue
 		}
 		newClients = append(newClients, client)
 	}
 
-	for _, email := range keyToEmail {
+	for email := range wantedEmails {
 		if !foundEmails[email] {
 			res.perEmailSkipped[email] = "Client Not Found In Inbound"
 		}
@@ -3547,16 +3507,18 @@ func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []
 		if _, attached := have[ibId]; !attached {
 			continue
 		}
-		inbound, getErr := inboundSvc.GetInbound(ibId)
-		if getErr != nil {
+		if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
 			return needRestart, getErr
 		}
-		key := clientKeyForProtocol(inbound.Protocol, existing)
-		if key == "" {
+		// Detach by email — the client's stable identity (see Delete).
+		if existing.Email == "" {
 			continue
 		}
-		nr, delErr := s.DelInboundClient(inboundSvc, ibId, key, true)
+		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, true)
 		if delErr != nil {
+			if errors.Is(delErr, ErrClientNotInInbound) {
+				continue
+			}
 			return needRestart, delErr
 		}
 		if nr {
@@ -3782,7 +3744,7 @@ func (s *ClientService) addInboundClient(inboundSvc *InboundService, data *model
 	return needRestart, nil
 }
 
-func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, clientId string) (bool, error) {
+func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, oldEmail string) (bool, error) {
 	defer lockInbound(data.Id).Unlock()
 
 	clients, err := inboundSvc.GetClients(data)
@@ -3808,56 +3770,30 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
 		return false, err
 	}
 
-	oldEmail := ""
 	newClientId := ""
+	switch oldInbound.Protocol {
+	case "trojan":
+		newClientId = clients[0].Password
+	case "shadowsocks":
+		newClientId = clients[0].Email
+	case "hysteria":
+		newClientId = clients[0].Auth
+	default:
+		newClientId = clients[0].ID
+	}
+
+	// Locate the client to replace by email — the client's stable identity.
+	// Credentials (uuid/password/auth) can drift from the inbound JSON, so they
+	// are never used for matching.
 	clientIndex := -1
 	for index, oldClient := range oldClients {
-		oldClientId := ""
-		switch oldInbound.Protocol {
-		case "trojan":
-			oldClientId = oldClient.Password
-			newClientId = clients[0].Password
-		case "shadowsocks":
-			oldClientId = oldClient.Email
-			newClientId = clients[0].Email
-		case "hysteria":
-			oldClientId = oldClient.Auth
-			newClientId = clients[0].Auth
-		default:
-			oldClientId = oldClient.ID
-			newClientId = clients[0].ID
-		}
-		if clientId == oldClientId {
+		if strings.EqualFold(oldClient.Email, oldEmail) {
 			oldEmail = oldClient.Email
 			clientIndex = index
 			break
 		}
 	}
 
-	if clientIndex == -1 {
-		var rec model.ClientRecord
-		var lookupErr error
-		switch oldInbound.Protocol {
-		case "trojan":
-			lookupErr = database.GetDB().Where("password = ?", clientId).First(&rec).Error
-		case "shadowsocks":
-			lookupErr = database.GetDB().Where("email = ?", clientId).First(&rec).Error
-		case "hysteria":
-			lookupErr = database.GetDB().Where("auth = ?", clientId).First(&rec).Error
-		default:
-			lookupErr = database.GetDB().Where("uuid = ?", clientId).First(&rec).Error
-		}
-		if lookupErr == nil && rec.Email != "" {
-			for index, oldClient := range oldClients {
-				if oldClient.Email == rec.Email {
-					oldEmail = oldClient.Email
-					clientIndex = index
-					break
-				}
-			}
-		}
-	}
-
 	if newClientId == "" || clientIndex == -1 {
 		return false, common.NewError("empty client ID")
 	}
@@ -4080,145 +4016,6 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
 	return needRestart, nil
 }
 
-func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId int, clientId string, keepTraffic bool) (bool, error) {
-	defer lockInbound(inboundId).Unlock()
-
-	oldInbound, err := inboundSvc.GetInbound(inboundId)
-	if err != nil {
-		logger.Error("Load Old Data Error")
-		return false, err
-	}
-	var settings map[string]any
-	err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
-	if err != nil {
-		return false, err
-	}
-
-	email := ""
-	client_key := "id"
-	switch oldInbound.Protocol {
-	case "trojan":
-		client_key = "password"
-	case "shadowsocks":
-		client_key = "email"
-	case "hysteria":
-		client_key = "auth"
-	}
-
-	interfaceClients := settings["clients"].([]any)
-	var newClients []any
-	needApiDel := false
-	clientFound := false
-	for _, client := range interfaceClients {
-		c := client.(map[string]any)
-		c_id := c[client_key].(string)
-		if c_id == clientId {
-			clientFound = true
-			email, _ = c["email"].(string)
-			needApiDel, _ = c["enable"].(bool)
-		} else {
-			newClients = append(newClients, client)
-		}
-	}
-
-	if !clientFound {
-		return false, common.NewError("Client Not Found In Inbound For ID:", clientId)
-	}
-
-	db := database.GetDB()
-	newClients = compactOrphans(db, newClients)
-	if newClients == nil {
-		newClients = []any{}
-	}
-	settings["clients"] = newClients
-	newSettings, err := json.MarshalIndent(settings, "", "  ")
-	if err != nil {
-		return false, err
-	}
-
-	oldInbound.Settings = string(newSettings)
-
-	emailShared, err := inboundSvc.emailUsedByOtherInbounds(email, inboundId)
-	if err != nil {
-		return false, err
-	}
-
-	if !emailShared && !keepTraffic {
-		err = inboundSvc.DelClientIPs(db, email)
-		if err != nil {
-			logger.Error("Error in delete client IPs")
-			return false, err
-		}
-	}
-	needRestart := false
-	markDirty := false
-
-	if len(email) > 0 {
-		var enables []bool
-		err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Limit(1).Pluck("enable", &enables).Error
-		if err != nil {
-			logger.Error("Get stats error")
-			return false, err
-		}
-		notDepleted := len(enables) > 0 && enables[0]
-		if !emailShared && !keepTraffic {
-			err = inboundSvc.DelClientStat(db, email)
-			if err != nil {
-				logger.Error("Delete stats Data Error")
-				return false, err
-			}
-		}
-		if needApiDel && notDepleted && oldInbound.NodeID == nil {
-			rt, rterr := inboundSvc.runtimeFor(oldInbound)
-			if rterr != nil {
-				needRestart = true
-			} else {
-				err1 := rt.RemoveUser(context.Background(), oldInbound, email)
-				if err1 == nil {
-					logger.Debug("Client deleted on", rt.Name(), ":", email)
-					needRestart = false
-				} else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
-					logger.Debug("User is already deleted. Nothing to do more...")
-				} else {
-					logger.Debug("Error in deleting client on", rt.Name(), ":", err1)
-					needRestart = true
-				}
-			}
-		}
-	}
-	if oldInbound.NodeID != nil && len(email) > 0 {
-		rt, push, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
-		if perr != nil {
-			return false, perr
-		}
-		if dirty {
-			markDirty = true
-		}
-		if push {
-			if err1 := rt.DeleteUser(context.Background(), oldInbound, email); err1 != nil {
-				logger.Warning("Error in deleting client on", rt.Name(), ":", err1)
-				markDirty = true
-			}
-		}
-	}
-	if err := db.Save(oldInbound).Error; err != nil {
-		return false, err
-	}
-	finalClients, gcErr := inboundSvc.GetClients(oldInbound)
-	if gcErr != nil {
-		return false, gcErr
-	}
-	if err := s.SyncInbound(db, inboundId, finalClients); err != nil {
-		return false, err
-	}
-	if markDirty && oldInbound.NodeID != nil {
-		if dErr := (&NodeService{}).MarkNodeDirty(*oldInbound.NodeID); dErr != nil {
-			logger.Warning("mark node dirty failed:", dErr)
-		}
-	}
-	return needRestart, nil
-}
-
 func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string, keepTraffic bool) (bool, error) {
 	defer lockInbound(inboundId).Unlock()
 
@@ -4256,7 +4053,7 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
 	}
 
 	if !found {
-		return false, common.NewError(fmt.Sprintf("client with email %s not found", email))
+		return false, fmt.Errorf("%w for email: %s", ErrClientNotInInbound, email)
 	}
 	db := database.GetDB()
 	newClients = compactOrphans(db, newClients)
@@ -4363,23 +4160,15 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf
 		return false, err
 	}
 
-	clientId := ""
-
+	found := false
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			switch inbound.Protocol {
-			case "trojan":
-				clientId = oldClient.Password
-			case "shadowsocks":
-				clientId = oldClient.Email
-			default:
-				clientId = oldClient.ID
-			}
+			found = true
 			break
 		}
 	}
 
-	if len(clientId) == 0 {
+	if !found {
 		return false, common.NewError("Client Not Found For Email:", clientEmail)
 	}
 
@@ -4404,7 +4193,7 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf
 		return false, err
 	}
 	inbound.Settings = string(modifiedSettings)
-	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
+	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
 	return needRestart, err
 }
 
@@ -4448,25 +4237,18 @@ func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, cl
 		return false, false, err
 	}
 
-	clientId := ""
+	found := false
 	clientOldEnabled := false
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			switch inbound.Protocol {
-			case "trojan":
-				clientId = oldClient.Password
-			case "shadowsocks":
-				clientId = oldClient.Email
-			default:
-				clientId = oldClient.ID
-			}
+			found = true
 			clientOldEnabled = oldClient.Enable
 			break
 		}
 	}
 
-	if len(clientId) == 0 {
+	if !found {
 		return false, false, common.NewError("Client Not Found For Email:", clientEmail)
 	}
 
@@ -4492,7 +4274,7 @@ func (s *ClientService) ToggleClientEnableByEmail(inboundSvc *InboundService, cl
 	}
 	inbound.Settings = string(modifiedSettings)
 
-	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
+	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
 	if err != nil {
 		return false, needRestart, err
 	}
@@ -4529,23 +4311,15 @@ func (s *ClientService) ResetClientIpLimitByEmail(inboundSvc *InboundService, cl
 		return false, err
 	}
 
-	clientId := ""
-
+	found := false
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			switch inbound.Protocol {
-			case "trojan":
-				clientId = oldClient.Password
-			case "shadowsocks":
-				clientId = oldClient.Email
-			default:
-				clientId = oldClient.ID
-			}
+			found = true
 			break
 		}
 	}
 
-	if len(clientId) == 0 {
+	if !found {
 		return false, common.NewError("Client Not Found For Email:", clientEmail)
 	}
 
@@ -4570,7 +4344,7 @@ func (s *ClientService) ResetClientIpLimitByEmail(inboundSvc *InboundService, cl
 		return false, err
 	}
 	inbound.Settings = string(modifiedSettings)
-	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
+	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
 	return needRestart, err
 }
 
@@ -4588,23 +4362,15 @@ func (s *ClientService) ResetClientExpiryTimeByEmail(inboundSvc *InboundService,
 		return false, err
 	}
 
-	clientId := ""
-
+	found := false
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			switch inbound.Protocol {
-			case "trojan":
-				clientId = oldClient.Password
-			case "shadowsocks":
-				clientId = oldClient.Email
-			default:
-				clientId = oldClient.ID
-			}
+			found = true
 			break
 		}
 	}
 
-	if len(clientId) == 0 {
+	if !found {
 		return false, common.NewError("Client Not Found For Email:", clientEmail)
 	}
 
@@ -4629,7 +4395,7 @@ func (s *ClientService) ResetClientExpiryTimeByEmail(inboundSvc *InboundService,
 		return false, err
 	}
 	inbound.Settings = string(modifiedSettings)
-	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
+	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
 	return needRestart, err
 }
 
@@ -4650,23 +4416,15 @@ func (s *ClientService) ResetClientTrafficLimitByEmail(inboundSvc *InboundServic
 		return false, err
 	}
 
-	clientId := ""
-
+	found := false
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			switch inbound.Protocol {
-			case "trojan":
-				clientId = oldClient.Password
-			case "shadowsocks":
-				clientId = oldClient.Email
-			default:
-				clientId = oldClient.ID
-			}
+			found = true
 			break
 		}
 	}
 
-	if len(clientId) == 0 {
+	if !found {
 		return false, common.NewError("Client Not Found For Email:", clientEmail)
 	}
 
@@ -4691,6 +4449,6 @@ func (s *ClientService) ResetClientTrafficLimitByEmail(inboundSvc *InboundServic
 		return false, err
 	}
 	inbound.Settings = string(modifiedSettings)
-	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientId)
+	needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
 	return needRestart, err
 }

+ 5 - 19
web/service/inbound.go

@@ -1402,25 +1402,11 @@ func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inb
 	return nil
 }
 
-func (s *InboundService) getClientPrimaryKey(protocol model.Protocol, client model.Client) string {
-	switch protocol {
-	case model.Trojan:
-		return client.Password
-	case model.Shadowsocks:
-		return client.Email
-	case model.Hysteria:
-		return client.Auth
-	default:
-		return client.ID
-	}
-}
-
-func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtocol model.Protocol, client model.Client, subID string) (bool, error) {
+func (s *InboundService) writeBackClientSubID(sourceInboundID int, client model.Client, subID string) (bool, error) {
 	client.SubID = subID
 	client.UpdatedAt = time.Now().UnixMilli()
-	clientID := s.getClientPrimaryKey(sourceProtocol, client)
-	if clientID == "" {
-		return false, common.NewError("empty client ID")
+	if client.Email == "" {
+		return false, common.NewError("empty client email")
 	}
 
 	settingsBytes, err := json.Marshal(map[string][]model.Client{
@@ -1434,7 +1420,7 @@ func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtoco
 		Id:       sourceInboundID,
 		Settings: string(settingsBytes),
 	}
-	return s.clientService.UpdateInboundClient(s, updatePayload, clientID)
+	return s.clientService.UpdateInboundClient(s, updatePayload, client.Email)
 }
 
 func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
@@ -1554,7 +1540,7 @@ func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID
 
 		if sourceClient.SubID == "" {
 			newSubID := uuid.NewString()
-			subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceInbound.Protocol, sourceClient, newSubID)
+			subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceClient, newSubID)
 			if subErr != nil {
 				result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
 				continue

+ 3 - 3
web/service/sync_scale_postgres_test.go

@@ -215,10 +215,10 @@ func TestAddDelClientPostgresScale(t *testing.T) {
 			}
 			addDur := time.Since(start)
 
-			delId := clients[n/2].ID
+			delEmail := clients[n/2].Email
 			start = time.Now()
-			if _, err := svc.DelInboundClient(inboundSvc, ib.Id, delId, false); err != nil {
-				t.Fatalf("DelInboundClient: %v", err)
+			if _, err := svc.DelInboundClientByEmail(inboundSvc, ib.Id, delEmail, false); err != nil {
+				t.Fatalf("DelInboundClientByEmail: %v", err)
 			}
 			delDur := time.Since(start)