mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 05:04:22 +00:00
fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ef98a932d7
commit
ad56b1bbd7
3 changed files with 71 additions and 21 deletions
|
|
@ -72,11 +72,17 @@ const delayedExpireDays = computed({
|
||||||
set: (days) => { form.expiryTime = -86400000 * (days || 0); },
|
set: (days) => { form.expiryTime = -86400000 * (days || 0); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MULTI_CLIENT_PROTOCOLS = new Set([
|
||||||
|
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2', 'portfallback',
|
||||||
|
]);
|
||||||
|
|
||||||
const inboundOptions = computed(() =>
|
const inboundOptions = computed(() =>
|
||||||
(props.inbounds || []).map((ib) => ({
|
(props.inbounds || [])
|
||||||
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
|
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol))
|
||||||
value: ib.id,
|
.map((ib) => ({
|
||||||
})),
|
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
|
||||||
|
value: ib.id,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(() => props.open, (next) => {
|
watch(() => props.open, (next) => {
|
||||||
|
|
|
||||||
|
|
@ -85,12 +85,18 @@ function gbToBytes(gb) {
|
||||||
return Math.round(gb * 1024 * 1024 * 1024);
|
return Math.round(gb * 1024 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MULTI_CLIENT_PROTOCOLS = new Set([
|
||||||
|
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2', 'portfallback',
|
||||||
|
]);
|
||||||
|
|
||||||
const inboundOptions = computed(() =>
|
const inboundOptions = computed(() =>
|
||||||
(props.inbounds || []).map((ib) => ({
|
(props.inbounds || [])
|
||||||
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
|
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol))
|
||||||
value: ib.id,
|
.map((ib) => ({
|
||||||
title: `${ib.remark || ''} (${ib.protocol}:${ib.port})`,
|
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
|
||||||
})),
|
value: ib.id,
|
||||||
|
title: `${ib.remark || ''} (${ib.protocol}:${ib.port})`,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const flowCapableIds = computed(() => {
|
const flowCapableIds = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -1277,6 +1277,11 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type oldSet struct {
|
||||||
|
inboundID int
|
||||||
|
emails map[string]struct{}
|
||||||
|
}
|
||||||
|
var perInboundOld []oldSet
|
||||||
for _, snapIb := range snap.Inbounds {
|
for _, snapIb := range snap.Inbounds {
|
||||||
if snapIb == nil {
|
if snapIb == nil {
|
||||||
continue
|
continue
|
||||||
|
|
@ -1285,6 +1290,19 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var oldEmailsRows []string
|
||||||
|
if err := tx.Table("clients").
|
||||||
|
Joins("JOIN client_inbounds ON client_inbounds.client_id = clients.id").
|
||||||
|
Where("client_inbounds.inbound_id = ?", c.Id).
|
||||||
|
Pluck("email", &oldEmailsRows).Error; err == nil {
|
||||||
|
oldEmails := make(map[string]struct{}, len(oldEmailsRows))
|
||||||
|
for _, e := range oldEmailsRows {
|
||||||
|
if e != "" {
|
||||||
|
oldEmails[e] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perInboundOld = append(perInboundOld, oldSet{inboundID: c.Id, emails: oldEmails})
|
||||||
|
}
|
||||||
|
|
||||||
clients, gcErr := s.GetClients(snapIb)
|
clients, gcErr := s.GetClients(snapIb)
|
||||||
if gcErr != nil {
|
if gcErr != nil {
|
||||||
|
|
@ -1310,20 +1328,40 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var orphanEmails []string
|
for _, old := range perInboundOld {
|
||||||
if err := tx.Table("clients").
|
var stillAttached []string
|
||||||
Joins("LEFT JOIN client_inbounds ON client_inbounds.client_id = clients.id").
|
if err := tx.Table("clients").
|
||||||
Where("client_inbounds.client_id IS NULL").
|
Joins("JOIN client_inbounds ON client_inbounds.client_id = clients.id").
|
||||||
Pluck("clients.email", &orphanEmails).Error; err != nil {
|
Where("client_inbounds.inbound_id = ?", old.inboundID).
|
||||||
logger.Warning("setRemoteTraffic: orphan sweep query failed:", err)
|
Pluck("email", &stillAttached).Error; err != nil {
|
||||||
} else if len(orphanEmails) > 0 {
|
continue
|
||||||
if err := tx.Where("email IN ?", orphanEmails).Delete(&model.ClientRecord{}).Error; err != nil {
|
|
||||||
logger.Warning("setRemoteTraffic: orphan sweep delete ClientRecord failed:", err)
|
|
||||||
}
|
}
|
||||||
if err := tx.Where("email IN ?", orphanEmails).Delete(&xray.ClientTraffic{}).Error; err != nil {
|
stillSet := make(map[string]struct{}, len(stillAttached))
|
||||||
logger.Warning("setRemoteTraffic: orphan sweep delete ClientTraffic failed:", err)
|
for _, e := range stillAttached {
|
||||||
|
stillSet[e] = struct{}{}
|
||||||
|
}
|
||||||
|
for email := range old.emails {
|
||||||
|
if _, kept := stillSet[email]; kept {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var attachmentCount int64
|
||||||
|
if err := tx.Table("client_inbounds").
|
||||||
|
Joins("JOIN clients ON clients.id = client_inbounds.client_id").
|
||||||
|
Where("clients.email = ?", email).
|
||||||
|
Count(&attachmentCount).Error; err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if attachmentCount > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := tx.Where("email = ?", email).Delete(&model.ClientRecord{}).Error; err != nil {
|
||||||
|
logger.Warning("setRemoteTraffic: delete ClientRecord", email, "failed:", err)
|
||||||
|
}
|
||||||
|
if err := tx.Where("email = ?", email).Delete(&xray.ClientTraffic{}).Error; err != nil {
|
||||||
|
logger.Warning("setRemoteTraffic: delete ClientTraffic", email, "failed:", err)
|
||||||
|
}
|
||||||
|
structuralChange = true
|
||||||
}
|
}
|
||||||
structuralChange = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue