fix search by id (#1047)

* fix search by id

* make SearchByID Public
This commit is contained in:
UUBulb
2025-03-27 19:17:00 +08:00
committed by GitHub
parent 314eadafc6
commit ec44dbbd9b
5 changed files with 119 additions and 36 deletions

View File

@@ -40,7 +40,7 @@ var (
frontendDist embed.FS frontendDist embed.FS
) )
func initSystem() error { func initSystem(bus chan<- *model.Service) error {
// 初始化管理员账户 // 初始化管理员账户
var usersCount int64 var usersCount int64
if err := singleton.DB.Model(&model.User{}).Count(&usersCount).Error; err != nil { if err := singleton.DB.Model(&model.User{}).Count(&usersCount).Error; err != nil {
@@ -61,7 +61,9 @@ func initSystem() error {
} }
// 启动 singleton 包下的所有服务 // 启动 singleton 包下的所有服务
singleton.LoadSingleton() if err := singleton.LoadSingleton(bus); err != nil {
return err
}
// 每天的3:30 对 监控记录 和 流量记录 进行清理 // 每天的3:30 对 监控记录 和 流量记录 进行清理
if _, err := singleton.CronShared.AddFunc("0 30 3 * * *", singleton.CleanServiceHistory); err != nil { if _, err := singleton.CronShared.AddFunc("0 30 3 * * *", singleton.CleanServiceHistory); err != nil {
@@ -107,12 +109,13 @@ func main() {
os.Exit(0) os.Exit(0)
} }
serviceSentinelDispatchBus := make(chan *model.Service) // 用于传递服务监控任务信息的channel
// 初始化 dao 包 // 初始化 dao 包
if err := utils.FirstError(singleton.InitFrontendTemplates, if err := utils.FirstError(singleton.InitFrontendTemplates,
func() error { return singleton.InitConfigFromPath(dashboardCliParam.ConfigFile) }, func() error { return singleton.InitConfigFromPath(dashboardCliParam.ConfigFile) },
singleton.InitTimezoneAndCache, singleton.InitTimezoneAndCache,
func() error { return singleton.InitDBFromPath(dashboardCliParam.DatabaseLocation) }, func() error { return singleton.InitDBFromPath(dashboardCliParam.DatabaseLocation) },
initSystem); err != nil { func() error { return initSystem(serviceSentinelDispatchBus) }); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -122,15 +125,9 @@ func main() {
} }
singleton.CleanServiceHistory() singleton.CleanServiceHistory()
serviceSentinelDispatchBus := make(chan *model.Service) // 用于传递服务监控任务信息的channel
rpc.DispatchKeepalive() rpc.DispatchKeepalive()
go rpc.DispatchTask(serviceSentinelDispatchBus) go rpc.DispatchTask(serviceSentinelDispatchBus)
go singleton.AlertSentinelStart() go singleton.AlertSentinelStart()
singleton.ServiceSentinelShared, err = singleton.NewServiceSentinel(
serviceSentinelDispatchBus, singleton.ServerShared, singleton.NotificationShared, singleton.CronShared)
if err != nil {
log.Fatal(err)
}
grpcHandler := rpc.ServeRPC() grpcHandler := rpc.ServeRPC()
httpHandler := controller.ServeWeb(frontendDist) httpHandler := controller.ServeWeb(frontendDist)

View File

@@ -2,6 +2,7 @@ package model
import ( import (
"cmp" "cmp"
"iter"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -71,13 +72,16 @@ func FindByUserID[S ~[]E, E CommonInterface](s S, uid uint64) []uint64 {
} }
func SearchByIDCtx[S ~[]E, E CommonInterface](c *gin.Context, x S) S { func SearchByIDCtx[S ~[]E, E CommonInterface](c *gin.Context, x S) S {
switch any(x).(type) { return SearchByID(strings.SplitSeq(c.Query("id"), ","), x)
case []*Server: }
l := searchByIDCtxServer(c, any(x).([]*Server))
return any(l).(S) func SearchByID[S ~[]E, E CommonInterface](seq iter.Seq[string], x S) S {
default: if hasPriorityList[E]() {
return searchByIDPri(seq, x)
}
var s S var s S
for idStr := range strings.SplitSeq(c.Query("id"), ",") { for idStr := range seq {
id, err := strconv.ParseUint(idStr, 10, 64) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
continue continue
@@ -87,19 +91,44 @@ func SearchByIDCtx[S ~[]E, E CommonInterface](c *gin.Context, x S) S {
} }
return utils.IfOr(len(s) > 0, s, x) return utils.IfOr(len(s) > 0, s, x)
} }
func hasPriorityList[T CommonInterface]() bool {
var class T
switch any(class).(type) {
case *Server:
return true
default:
return false
}
} }
func searchByIDCtxServer(c *gin.Context, x []*Server) []*Server { type splitter[S ~[]E, E CommonInterface] interface {
list1, list2 := SplitList(x) // SplitList should split a sorted list into two separate lists:
// The first list contains elements with a priority set (DisplayIndex != 0).
// The second list contains elements without a priority set (DisplayIndex == 0).
// The original slice is not modified. If no element without a priority is found, it returns nil.
// Should be safe to use with a nil pointer.
SplitList(x S) (S, S)
}
var clist1, clist2 []*Server func searchByIDPri[S ~[]E, E CommonInterface](seq iter.Seq[string], x S) S {
for idStr := range strings.SplitSeq(c.Query("id"), ",") { var class E
split, ok := any(class).(splitter[S, E])
if !ok {
return nil
}
plist, list2 := split.SplitList(x)
var clist1, clist2 S
for idStr := range seq {
id, err := strconv.ParseUint(idStr, 10, 64) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
continue continue
} }
clist1 = appendBinarySearch(clist1, list1, id) clist1 = appendSearch(clist1, plist, id)
clist2 = appendBinarySearch(clist2, list2, id) clist2 = appendBinarySearch(clist2, list2, id)
} }
@@ -115,3 +144,13 @@ func appendBinarySearch[S ~[]E, E CommonInterface](x, y S, target uint64) S {
} }
return x return x
} }
func appendSearch[S ~[]E, E CommonInterface](x, y S, target uint64) S {
if i := slices.IndexFunc(y, func(e E) bool {
return e.GetID() == target
}); i != -1 {
x = append(x, y[i])
}
return x
}

49
model/common_test.go Normal file
View File

@@ -0,0 +1,49 @@
package model
import (
"reflect"
"slices"
"testing"
)
func TestSearchByID(t *testing.T) {
t.Run("WithoutPriorityList", func(t *testing.T) {
list, exp := []*DDNSProfile{
{Common: Common{ID: 1}},
{Common: Common{ID: 2}},
{Common: Common{ID: 3}},
{Common: Common{ID: 4}},
{Common: Common{ID: 5}},
}, []*DDNSProfile{
{Common: Common{ID: 4}},
{Common: Common{ID: 1}},
{Common: Common{ID: 3}},
}
searchList := slices.Values([]string{"4", "1", "3"})
filtered := SearchByID(searchList, list)
if !reflect.DeepEqual(filtered, exp) {
t.Fatalf("expected %v, but got %v", exp, filtered)
}
})
t.Run("WithPriorityTest", func(t *testing.T) {
list, exp := []*Server{
{Common: Common{ID: 5}, DisplayIndex: 2},
{Common: Common{ID: 4}, DisplayIndex: 1},
{Common: Common{ID: 1}},
{Common: Common{ID: 2}},
{Common: Common{ID: 3}},
}, []*Server{
{Common: Common{ID: 4}, DisplayIndex: 1},
{Common: Common{ID: 5}, DisplayIndex: 2},
{Common: Common{ID: 3}},
}
searchList := slices.Values([]string{"3", "4", "5"})
filtered := SearchByID(searchList, list)
if !reflect.DeepEqual(filtered, exp) {
t.Fatalf("expected %v, but got %v", exp, filtered)
}
})
}

View File

@@ -73,11 +73,7 @@ func (s *Server) AfterFind(tx *gorm.DB) error {
return nil return nil
} }
// Split a sorted server list into two separate lists: func (s *Server) SplitList(x []*Server) ([]*Server, []*Server) {
// The first list contains servers with a priority set (DisplayIndex != 0).
// The second list contains servers without a priority set (DisplayIndex == 0).
// The original slice is not modified. If no server without a priority is found, it returns nil.
func SplitList(x []*Server) ([]*Server, []*Server) {
pri := func(s *Server) bool { pri := func(s *Server) bool {
return s.DisplayIndex == 0 return s.DisplayIndex == 0
} }

View File

@@ -51,7 +51,7 @@ func InitTimezoneAndCache() error {
} }
// LoadSingleton 加载子服务并执行 // LoadSingleton 加载子服务并执行
func LoadSingleton() { func LoadSingleton(bus chan<- *model.Service) (err error) {
initUser() // 加载用户ID绑定表 initUser() // 加载用户ID绑定表
initI18n() // 加载本地化服务 initI18n() // 加载本地化服务
NotificationShared = NewNotificationClass() // 加载通知服务 NotificationShared = NewNotificationClass() // 加载通知服务
@@ -59,6 +59,8 @@ func LoadSingleton() {
CronShared = NewCronClass() // 加载定时任务 CronShared = NewCronClass() // 加载定时任务
NATShared = NewNATClass() NATShared = NewNATClass()
DDNSShared = NewDDNSClass() DDNSShared = NewDDNSClass()
ServiceSentinelShared, err = NewServiceSentinel(bus, ServerShared, NotificationShared, CronShared)
return
} }
// InitFrontendTemplates 从内置文件中加载FrontendTemplates // InitFrontendTemplates 从内置文件中加载FrontendTemplates