From 83662487cb444ae4fcc0b2e5e01bbc13e4f07231 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Sun, 26 Jul 2026 23:38:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8E=A5=E9=80=9A=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8V2=E6=8E=92=E5=BA=8F=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将排序参数从V2 Handler传递至Service与Repository - 增加仓储分页排序与参数白名单测试 --- backend/controller/server.go | 16 +------- backend/controller/server_sort_test.go | 41 +++++++++--------- backend/controller/server_v2.go | 4 +- backend/repository/repo.go | 15 ++++++- backend/repository/server_sort_test.go | 57 ++++++++++++++++++++++++++ backend/service/service.go | 4 +- frontend/src/api/server/server.ts | 2 +- 7 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 backend/repository/server_sort_test.go diff --git a/backend/controller/server.go b/backend/controller/server.go index ea30a5a..a3167e0 100644 --- a/backend/controller/server.go +++ b/backend/controller/server.go @@ -74,9 +74,8 @@ func (h *ServerHandler) ListServers(c *gin.Context) { status := c.Query("status") regionID := c.Query("region_id") - // 构建缓存键(按查询参数区分) // Logan.Li + // 构建缓存键(按查询参数区分) cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID + - ":" + query.Sort + ":" + query.Order + ":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize) const cacheTTL = 10 * time.Second @@ -115,7 +114,7 @@ func (h *ServerHandler) ListServers(c *gin.Context) { var servers []model.Server db.Preload("Region").Preload("Cloud"). Offset(query.GetOffset()).Limit(query.GetLimit()). - Order(serverOrderClause(query.Sort, query.Order)). + Order("created_at DESC"). Find(&servers) // 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li @@ -143,17 +142,6 @@ func (h *ServerHandler) ListServers(c *gin.Context) { c.JSON(http.StatusOK, resp) } -// serverOrderClause 返回服务器列表允许使用的排序表达式,避免直接拼接请求参数。 // Logan.Li -func serverOrderClause(sortField, order string) string { - if sortField != "hostname" { - return "created_at DESC" - } - if order == "desc" { - return "LOWER(hostname) DESC, id DESC" - } - return "LOWER(hostname) ASC, id ASC" -} - // GetServer 获取服务器详情 // @Summary 获取服务器详情 // @Description 获取指定服务器的详细信息 diff --git a/backend/controller/server_sort_test.go b/backend/controller/server_sort_test.go index 3095bcd..2522640 100644 --- a/backend/controller/server_sort_test.go +++ b/backend/controller/server_sort_test.go @@ -1,27 +1,26 @@ package controller -import "testing" +import ( + "net/http/httptest" + "testing" -// TestServerOrderClause 验证服务器排序字段仅使用后端白名单表达式。 // Logan.Li -func TestServerOrderClause(t *testing.T) { - tests := []struct { - name string - sortField string - order string - want string - }{ - {name: "默认排序", want: "created_at DESC"}, - {name: "主机名升序", sortField: "hostname", order: "asc", want: "LOWER(hostname) ASC, id ASC"}, - {name: "主机名降序", sortField: "hostname", order: "desc", want: "LOWER(hostname) DESC, id DESC"}, - {name: "非法方向回退升序", sortField: "hostname", order: "invalid", want: "LOWER(hostname) ASC, id ASC"}, - {name: "非法字段回退默认", sortField: "hostname; DROP TABLE sys_servers", order: "asc", want: "created_at DESC"}, + "ezops/model" + + "github.com/gin-gonic/gin" +) + +// TestServerPageQueryBinding 验证服务器列表能绑定排序查询参数。 // Logan.Li +func TestServerPageQueryBinding(t *testing.T) { + gin.SetMode(gin.TestMode) + request := httptest.NewRequest("GET", "/servers?page=1&page_size=10&sort=hostname&order=asc", nil) + context, _ := gin.CreateTestContext(httptest.NewRecorder()) + context.Request = request + + var query model.PageQuery + if err := context.ShouldBindQuery(&query); err != nil { + t.Fatalf("ShouldBindQuery() error = %v", err) } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := serverOrderClause(tt.sortField, tt.order); got != tt.want { - t.Fatalf("serverOrderClause(%q, %q) = %q, want %q", tt.sortField, tt.order, got, tt.want) - } - }) + if query.Sort != "hostname" || query.Order != "asc" { + t.Fatalf("排序参数绑定结果为 sort=%q order=%q", query.Sort, query.Order) } } diff --git a/backend/controller/server_v2.go b/backend/controller/server_v2.go index af8e883..93a2379 100644 --- a/backend/controller/server_v2.go +++ b/backend/controller/server_v2.go @@ -57,7 +57,9 @@ func (h *ServerHandlerV2) ListServers(c *gin.Context) { status := c.Query("status") regionID := c.Query("region_id") - list, total, err := h.svc.ListServers(query.Keyword, status, regionID, query.Page, query.PageSize) + list, total, err := h.svc.ListServers( + query.Keyword, status, regionID, query.Sort, query.Order, query.Page, query.PageSize, + ) if err != nil { utils.InternalError(c, "查询失败") return diff --git a/backend/repository/repo.go b/backend/repository/repo.go index 995e4d2..4fbbb9e 100644 --- a/backend/repository/repo.go +++ b/backend/repository/repo.go @@ -23,7 +23,7 @@ func (r *ServerRepository) DB() *gorm.DB { } // List 分页查询服务器列表 -func (r *ServerRepository) List(keyword, status, regionID string, page, pageSize int) ([]model.Server, int64, error) { +func (r *ServerRepository) List(keyword, status, regionID, sortField, sortOrder string, page, pageSize int) ([]model.Server, int64, error) { q := r.db.Model(&model.Server{}) if keyword != "" { q = q.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?", @@ -42,11 +42,22 @@ func (r *ServerRepository) List(keyword, status, regionID string, page, pageSize var list []model.Server err := q.Preload("Region").Preload("Cloud"). Offset((page - 1) * pageSize).Limit(pageSize). - Order("created_at DESC").Find(&list).Error + Order(serverOrderClause(sortField, sortOrder)).Find(&list).Error return list, total, err } +// serverOrderClause 返回服务器列表允许使用的排序表达式,避免直接拼接请求参数。 // Logan.Li +func serverOrderClause(sortField, sortOrder string) string { + if sortField != "hostname" { + return "created_at DESC" + } + if sortOrder == "desc" { + return "LOWER(hostname) DESC, id DESC" + } + return "LOWER(hostname) ASC, id ASC" +} + // GetByID 根据 ID 获取服务器 func (r *ServerRepository) GetByID(id uint) (*model.Server, error) { var s model.Server diff --git a/backend/repository/server_sort_test.go b/backend/repository/server_sort_test.go new file mode 100644 index 0000000..4fb57fb --- /dev/null +++ b/backend/repository/server_sort_test.go @@ -0,0 +1,57 @@ +package repository + +import ( + "testing" + + "ezops/model" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// TestServerRepositoryListHostnameOrder 验证实际服务器仓储按主机名分页排序。 // Logan.Li +func TestServerRepositoryListHostnameOrder(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("打开测试数据库失败: %v", err) + } + if err := db.AutoMigrate(&model.Server{}, &model.Region{}, &model.Cloud{}); err != nil { + t.Fatalf("迁移测试表失败: %v", err) + } + for _, hostname := range []string{"test-win", "c-windows", "alpha-host"} { + if err := db.Create(&model.Server{Hostname: hostname, IP: "127.0.0.1"}).Error; err != nil { + t.Fatalf("创建测试服务器失败: %v", err) + } + } + + repo := NewServerRepository(db) + servers, total, err := repo.List("", "", "", "hostname", "asc", 1, 10) + if err != nil { + t.Fatalf("查询测试服务器失败: %v", err) + } + if total != 3 || len(servers) != 3 { + t.Fatalf("服务器数量异常 total=%d len=%d", total, len(servers)) + } + if servers[0].Hostname != "alpha-host" || servers[2].Hostname != "test-win" { + t.Fatalf("主机名升序结果异常: %q, %q, %q", servers[0].Hostname, servers[1].Hostname, servers[2].Hostname) + } +} + +// TestServerOrderClause 验证非法排序参数无法进入 SQL 表达式。 // Logan.Li +func TestServerOrderClause(t *testing.T) { + tests := []struct { + sortField string + sortOrder string + want string + }{ + {want: "created_at DESC"}, + {sortField: "hostname", sortOrder: "asc", want: "LOWER(hostname) ASC, id ASC"}, + {sortField: "hostname", sortOrder: "desc", want: "LOWER(hostname) DESC, id DESC"}, + {sortField: "hostname; DROP TABLE asset_servers", sortOrder: "asc", want: "created_at DESC"}, + } + for _, tt := range tests { + if got := serverOrderClause(tt.sortField, tt.sortOrder); got != tt.want { + t.Fatalf("serverOrderClause(%q, %q) = %q, want %q", tt.sortField, tt.sortOrder, got, tt.want) + } + } +} diff --git a/backend/service/service.go b/backend/service/service.go index f749eea..857ec5a 100644 --- a/backend/service/service.go +++ b/backend/service/service.go @@ -29,8 +29,8 @@ func NewServerService(repo *repository.ServerRepository, cache ezredis.Cache, cr } // ListServers 获取服务器列表 -func (s *ServerService) ListServers(keyword, status, regionID string, page, pageSize int) ([]model.Server, int64, error) { - list, total, err := s.repo.List(keyword, status, regionID, page, pageSize) +func (s *ServerService) ListServers(keyword, status, regionID, sortField, sortOrder string, page, pageSize int) ([]model.Server, int64, error) { + list, total, err := s.repo.List(keyword, status, regionID, sortField, sortOrder, page, pageSize) if err != nil { return nil, 0, err } diff --git a/frontend/src/api/server/server.ts b/frontend/src/api/server/server.ts index 5d3c9fe..c8d22b1 100644 --- a/frontend/src/api/server/server.ts +++ b/frontend/src/api/server/server.ts @@ -3,7 +3,7 @@ import { http, type ListResponse } from '@/utils/request'; export const serverApi = { - getServers: async (params?: { page?: number; page_size?: number; keyword?: string; status?: string }) => { + getServers: async (params?: { page?: number; page_size?: number; keyword?: string; status?: string; sort?: 'hostname'; order?: 'asc' | 'desc' }) => { return http.get('/servers', params); }, getServerDetail: async (id: number | string) => {