mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
fix(inbounds): allow negative subSortIndex for subscription order (#6465)
* fix(inbounds): allow negative subSortIndex for subscription order Preserve explicitly set negative indices so primary inbounds can sort ahead of the default without renumbering peers; keep 0/omitted → 1. * fix(inbounds): gofumpt model.go and trim subSortIndex comments --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
@@ -1031,11 +1031,10 @@ func migrateTgIDIndex() error {
|
||||
return db.Migrator().CreateIndex(&model.ClientRecord{}, "TgID")
|
||||
}
|
||||
|
||||
// normalizeInboundSubSortIndex lifts sub_sort_index values below the 1-based
|
||||
// minimum (rows written by builds that defaulted the column to 0, or by nodes
|
||||
// predating the field) so they cannot sort ahead of explicitly ranked inbounds.
|
||||
// normalizeInboundSubSortIndex lifts legacy zero defaults to 1.
|
||||
// Explicit negatives are left alone so primary inbounds can sort first.
|
||||
func normalizeInboundSubSortIndex() error {
|
||||
res := db.Exec("UPDATE inbounds SET sub_sort_index = 1 WHERE sub_sort_index < 1")
|
||||
res := db.Exec("UPDATE inbounds SET sub_sort_index = 1 WHERE sub_sort_index = 0")
|
||||
if res.Error != nil {
|
||||
log.Printf("Error normalizing inbound sub_sort_index: %v", res.Error)
|
||||
return res.Error
|
||||
|
||||
@@ -52,7 +52,7 @@ type Inbound struct {
|
||||
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||
Remark string `json:"remark" form:"remark" example:"VLESS-443"` // Human-readable remark
|
||||
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" gorm:"default:1" validate:"omitempty,gte=1" example:"1"` // 1-based sort order of this inbound's links in subscription output only (lower first; ties by id)
|
||||
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" gorm:"default:1" validate:"omitempty" example:"1"` // Sort order of this inbound's links in subscription output only (lower first; negatives allowed; 0/omitted → 1; ties by id)
|
||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
|
||||
|
||||
@@ -274,7 +274,7 @@ func (a *InboundController) setInboundSubSortIndex(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
type form struct {
|
||||
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" binding:"required,min=1"`
|
||||
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" binding:"required"`
|
||||
}
|
||||
var f form
|
||||
if err := c.ShouldBind(&f); err != nil {
|
||||
|
||||
@@ -52,10 +52,9 @@ func TestUpdateInbound_PersistsSubSortIndex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateInbound_SubSortIndexClampedToMinimum verifies that values below
|
||||
// the 1-based minimum (0 from clients that predate the field, or negatives)
|
||||
// are clamped to 1 instead of being stored.
|
||||
func TestUpdateInbound_SubSortIndexClampedToMinimum(t *testing.T) {
|
||||
// TestUpdateInbound_SubSortIndexZeroMapsToDefault verifies that 0 from clients
|
||||
// that predate the field is normalized to 1 instead of being stored.
|
||||
func TestUpdateInbound_SubSortIndexZeroMapsToDefault(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
ib := makeInboundWithSubSortIndex("in-7002-tcp", 7002, 5)
|
||||
@@ -64,25 +63,54 @@ func TestUpdateInbound_SubSortIndexClampedToMinimum(t *testing.T) {
|
||||
}
|
||||
|
||||
svc := &InboundService{}
|
||||
for _, below := range []int{0, -3} {
|
||||
update := *ib
|
||||
update.SubSortIndex = below
|
||||
update := *ib
|
||||
update.SubSortIndex = 0
|
||||
|
||||
got, _, err := svc.UpdateInbound(&update)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInbound(%d): %v", below, err)
|
||||
}
|
||||
if got.SubSortIndex != 1 {
|
||||
t.Fatalf("returned SubSortIndex = %d for input %d, want 1", got.SubSortIndex, below)
|
||||
}
|
||||
got, _, err := svc.UpdateInbound(&update)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInbound: %v", err)
|
||||
}
|
||||
if got.SubSortIndex != 1 {
|
||||
t.Fatalf("returned SubSortIndex = %d, want 1", got.SubSortIndex)
|
||||
}
|
||||
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != 1 {
|
||||
t.Fatalf("persisted SubSortIndex = %d for input %d, want 1", reloaded.SubSortIndex, below)
|
||||
}
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != 1 {
|
||||
t.Fatalf("persisted SubSortIndex = %d, want 1", reloaded.SubSortIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateInbound_PreservesNegativeSubSortIndex verifies that an explicitly
|
||||
// set negative index is stored (so it can sort ahead of the default of 1).
|
||||
func TestUpdateInbound_PreservesNegativeSubSortIndex(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
ib := makeInboundWithSubSortIndex("in-7004-tcp", 7004, 5)
|
||||
if err := database.GetDB().Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
svc := &InboundService{}
|
||||
update := *ib
|
||||
update.SubSortIndex = -1
|
||||
|
||||
got, _, err := svc.UpdateInbound(&update)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInbound: %v", err)
|
||||
}
|
||||
if got.SubSortIndex != -1 {
|
||||
t.Fatalf("returned SubSortIndex = %d, want -1", got.SubSortIndex)
|
||||
}
|
||||
|
||||
var reloaded model.Inbound
|
||||
if err := database.GetDB().First(&reloaded, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if reloaded.SubSortIndex != -1 {
|
||||
t.Fatalf("persisted SubSortIndex = %d, want -1", reloaded.SubSortIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,3 +55,27 @@ func TestSetInboundSubSortIndexUsesNarrowNodeUpdate(t *testing.T) {
|
||||
t.Fatalf("full snapshot node updates = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetInboundSubSortIndexPreservesNegative(t *testing.T) {
|
||||
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
||||
t.Fatalf("init db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
|
||||
ib := &model.Inbound{UserId: 1, Remark: "r", Port: 21003, Protocol: model.VLESS, Settings: `{"clients":[]}`, SubSortIndex: 1, Enable: true}
|
||||
if err := database.GetDB().Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
if err := (&InboundService{}).SetInboundSubSortIndex(ib.Id, -2); err != nil {
|
||||
t.Fatalf("set: %v", err)
|
||||
}
|
||||
|
||||
var got model.Inbound
|
||||
if err := database.GetDB().First(&got, ib.Id).Error; err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if got.SubSortIndex != -2 {
|
||||
t.Fatalf("subSortIndex = %d, want -2", got.SubSortIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ import "strings"
|
||||
// installs (>32k clients) where even modern SQLite would refuse a single IN.
|
||||
const sqliteMaxVars = 900
|
||||
|
||||
// normalizeSubSortIndex clamps the 1-based subscription sort order. Values
|
||||
// below 1 arrive from clients that predate the field (omitted form key binds
|
||||
// to 0) and must not sort ahead of explicitly ranked inbounds.
|
||||
// normalizeSubSortIndex maps omitted/zero to 1; explicit negatives are kept
|
||||
// so a primary inbound can sort ahead of the default.
|
||||
func normalizeSubSortIndex(v int) int {
|
||||
if v < 1 {
|
||||
if v == 0 {
|
||||
return 1
|
||||
}
|
||||
return v
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeSubSortIndex(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want int
|
||||
}{
|
||||
{0, 1},
|
||||
{1, 1},
|
||||
{7, 7},
|
||||
{-1, -1},
|
||||
{-10, -10},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := normalizeSubSortIndex(tc.in); got != tc.want {
|
||||
t.Fatalf("normalizeSubSortIndex(%d) = %d, want %d", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user