fix: 接通服务器V2排序链路
- 将排序参数从V2 Handler传递至Service与Repository - 增加仓储分页排序与参数白名单测试
This commit is contained in:
1 parent
09e6271bb4
commit
83662487cb
7 files changed
+98
-41
No files matched your search
@@ -74,9 +74,8 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
|||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
regionID := c.Query("region_id")
|
regionID := c.Query("region_id")
|
||||||
|
|
||||||
// 构建缓存键(按查询参数区分) // Logan.Li
|
// 构建缓存键(按查询参数区分)
|
||||||
cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID +
|
cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID +
|
||||||
":" + query.Sort + ":" + query.Order +
|
|
||||||
":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize)
|
":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize)
|
||||||
const cacheTTL = 10 * time.Second
|
const cacheTTL = 10 * time.Second
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
|||||||
var servers []model.Server
|
var servers []model.Server
|
||||||
db.Preload("Region").Preload("Cloud").
|
db.Preload("Region").Preload("Cloud").
|
||||||
Offset(query.GetOffset()).Limit(query.GetLimit()).
|
Offset(query.GetOffset()).Limit(query.GetLimit()).
|
||||||
Order(serverOrderClause(query.Sort, query.Order)).
|
Order("created_at DESC").
|
||||||
Find(&servers)
|
Find(&servers)
|
||||||
|
|
||||||
// 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li
|
// 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li
|
||||||
@@ -143,17 +142,6 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, resp)
|
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 获取服务器详情
|
// GetServer 获取服务器详情
|
||||||
// @Summary 获取服务器详情
|
// @Summary 获取服务器详情
|
||||||
// @Description 获取指定服务器的详细信息
|
// @Description 获取指定服务器的详细信息
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
// TestServerOrderClause 验证服务器排序字段仅使用后端白名单表达式。 // Logan.Li
|
"ezops/model"
|
||||||
func TestServerOrderClause(t *testing.T) {
|
|
||||||
tests := []struct {
|
"github.com/gin-gonic/gin"
|
||||||
name string
|
)
|
||||||
sortField string
|
|
||||||
order string
|
// TestServerPageQueryBinding 验证服务器列表能绑定排序查询参数。 // Logan.Li
|
||||||
want string
|
func TestServerPageQueryBinding(t *testing.T) {
|
||||||
}{
|
gin.SetMode(gin.TestMode)
|
||||||
{name: "默认排序", want: "created_at DESC"},
|
request := httptest.NewRequest("GET", "/servers?page=1&page_size=10&sort=hostname&order=asc", nil)
|
||||||
{name: "主机名升序", sortField: "hostname", order: "asc", want: "LOWER(hostname) ASC, id ASC"},
|
context, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
{name: "主机名降序", sortField: "hostname", order: "desc", want: "LOWER(hostname) DESC, id DESC"},
|
context.Request = request
|
||||||
{name: "非法方向回退升序", sortField: "hostname", order: "invalid", want: "LOWER(hostname) ASC, id ASC"},
|
|
||||||
{name: "非法字段回退默认", sortField: "hostname; DROP TABLE sys_servers", order: "asc", want: "created_at DESC"},
|
var query model.PageQuery
|
||||||
|
if err := context.ShouldBindQuery(&query); err != nil {
|
||||||
|
t.Fatalf("ShouldBindQuery() error = %v", err)
|
||||||
}
|
}
|
||||||
|
if query.Sort != "hostname" || query.Order != "asc" {
|
||||||
for _, tt := range tests {
|
t.Fatalf("排序参数绑定结果为 sort=%q order=%q", query.Sort, query.Order)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,9 @@ func (h *ServerHandlerV2) ListServers(c *gin.Context) {
|
|||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
regionID := c.Query("region_id")
|
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 {
|
if err != nil {
|
||||||
utils.InternalError(c, "查询失败")
|
utils.InternalError(c, "查询失败")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (r *ServerRepository) DB() *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List 分页查询服务器列表
|
// 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{})
|
q := r.db.Model(&model.Server{})
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
q = q.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?",
|
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
|
var list []model.Server
|
||||||
err := q.Preload("Region").Preload("Cloud").
|
err := q.Preload("Region").Preload("Cloud").
|
||||||
Offset((page - 1) * pageSize).Limit(pageSize).
|
Offset((page - 1) * pageSize).Limit(pageSize).
|
||||||
Order("created_at DESC").Find(&list).Error
|
Order(serverOrderClause(sortField, sortOrder)).Find(&list).Error
|
||||||
|
|
||||||
return list, total, err
|
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 获取服务器
|
// GetByID 根据 ID 获取服务器
|
||||||
func (r *ServerRepository) GetByID(id uint) (*model.Server, error) {
|
func (r *ServerRepository) GetByID(id uint) (*model.Server, error) {
|
||||||
var s model.Server
|
var s model.Server
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,8 @@ func NewServerService(repo *repository.ServerRepository, cache ezredis.Cache, cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListServers 获取服务器列表
|
// ListServers 获取服务器列表
|
||||||
func (s *ServerService) ListServers(keyword, status, regionID string, page, pageSize int) ([]model.Server, int64, error) {
|
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, page, pageSize)
|
list, total, err := s.repo.List(keyword, status, regionID, sortField, sortOrder, page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { http, type ListResponse } from '@/utils/request';
|
import { http, type ListResponse } from '@/utils/request';
|
||||||
|
|
||||||
export const serverApi = {
|
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<ListResponse>('/servers', params);
|
return http.get<ListResponse>('/servers', params);
|
||||||
},
|
},
|
||||||
getServerDetail: async (id: number | string) => {
|
getServerDetail: async (id: number | string) => {
|
||||||
|
|||||||
Reference in new issue
Block a user