fix: add server-side sorting to user list, prevent client-side sort on paged data (#6194)

* fix: add server-side sorting to user list, prevent client-side sort on paged data

The user management table applied client-side sorting to the current
page slice while pagination was handled server-side, causing ID-asc
views to show pages out of order (e.g. 23-42, 3-22, 1-2).

Backend: add sort_by/sort_order query params to GetAllUsers and
SearchUsers with a column whitelist for safe ORDER BY generation.
Frontend: pass sorting state to the API and reset to page 1 on sort
change. Generic useDataTable hook now disables client-side sorted row
model and sort UI for tables with manual pagination but no server-side
sort handler.

* fix: add id tie-breaker to non-unique sort columns, remove side effect from state updater

Append secondary ORDER BY id DESC when sorting by non-unique columns
(quota, created_at, etc.) to prevent row duplication/skipping across
OFFSET pages.

Move onPaginationChange out of setSorting updater to avoid side effects
inside a pure function (React Strict Mode double-invocation safety).
This commit is contained in:
DawnMoon1542
2026-07-17 19:36:44 +08:00
committed by GitHub
parent a63364d156
commit 506e41c116
8 changed files with 207 additions and 11 deletions
+62 -4
View File
@@ -14,10 +14,66 @@ import (
"github.com/bytedance/gopkg/util/gopool"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const UserNameMaxLength = 20
var userSortColumns = map[string]string{
"id": "id",
"username": "username",
"quota": "quota",
"group": "group",
"created_at": "created_at",
"last_login_at": "last_login_at",
}
type UserSortOptions struct {
SortBy string
SortOrder string
}
func NewUserSortOptions(sortBy string, sortOrder string) UserSortOptions {
normalizedSortBy := strings.ToLower(strings.TrimSpace(sortBy))
normalizedSortOrder := strings.ToLower(strings.TrimSpace(sortOrder))
if _, ok := userSortColumns[normalizedSortBy]; !ok {
normalizedSortBy = "id"
normalizedSortOrder = "desc"
} else if normalizedSortOrder != "asc" {
normalizedSortOrder = "desc"
}
return UserSortOptions{
SortBy: normalizedSortBy,
SortOrder: normalizedSortOrder,
}
}
func (options UserSortOptions) Apply(query *gorm.DB) *gorm.DB {
columnName, ok := userSortColumns[options.SortBy]
if !ok {
columnName = "id"
}
q := query.Order(clause.OrderByColumn{
Column: clause.Column{Name: columnName},
Desc: options.SortOrder != "asc",
})
if columnName != "id" {
q = q.Order(clause.OrderByColumn{
Column: clause.Column{Name: "id"},
Desc: true,
})
}
return q
}
func resolveUserSortOptions(sortOptions []UserSortOptions) UserSortOptions {
if len(sortOptions) == 0 {
return NewUserSortOptions("", "")
}
return sortOptions[0]
}
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
// Otherwise, the sensitive information will be saved on local storage in plain text!
type User struct {
@@ -286,7 +342,7 @@ func GetMaxUserId() int {
return user.Id
}
func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err error) {
func GetAllUsers(pageInfo *common.PageInfo, sortOptions ...UserSortOptions) (users []*User, total int64, err error) {
// Start transaction
tx := DB.Begin()
if tx.Error != nil {
@@ -306,7 +362,8 @@ func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err err
}
// Get paginated users within same transaction
err = tx.Unscoped().Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("password", "access_token").Find(&users).Error
order := resolveUserSortOptions(sortOptions)
err = order.Apply(tx.Unscoped()).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("password", "access_token").Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
@@ -320,7 +377,7 @@ func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err err
return users, total, nil
}
func SearchUsers(keyword string, group string, role *int, status *int, startIdx int, num int) ([]*User, int64, error) {
func SearchUsers(keyword string, group string, role *int, status *int, startIdx int, num int, sortOptions ...UserSortOptions) ([]*User, int64, error) {
var users []*User
var total int64
var err error
@@ -374,7 +431,8 @@ func SearchUsers(keyword string, group string, role *int, status *int, startIdx
}
// 获取分页数据
err = query.Omit("password", "access_token").Order("id desc").Limit(num).Offset(startIdx).Find(&users).Error
order := resolveUserSortOptions(sortOptions)
err = order.Apply(query.Omit("password", "access_token")).Limit(num).Offset(startIdx).Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err