最近在实现拖拽排序、置顶和置底逻辑时遇到了比较恶心的问题。以下为记录
背景
我们系统的排序是基于数据库中的 sort_num 字段进行的,sort_num 值越大,数据排位越靠前。新增数据时,其 sort_num 默认为其 id 值。假设当前有 10 条数据,最大 id 为 10,新增数据 id=11,sort_num 也为 11,因此新增的数据会排在最前面。
拖拽排序
最简单的拖拽排序方法是对当前页面的数据重新排序。假设当前页面有 5 条数据,排序顺序为:
1
2
3
4
5
当用户将 id=3 的数据拖动到第一位时,前端会传递新的排序顺序:
3
1
2
4
5
服务端会获取这些数据中最大
ID 排序号
3 5
1 4
2 3
4 2
5 1
这种方式可以实现拖拽排序。
置顶和置底逻辑
置顶:获取sort_num最大的一条记录,把当前需要置顶的id的sort_num改为和最大记录一致。
置底:获取sort_num最大的一条记录,把当前需要指于地步
为解决此问题,我们引入了多字段排序。即在 sort_num 相同的情况下,使用 update_time 作为第二排序字段。每次更新数据时,update_time 会更新为当前时间,因此可以通过 ORDER BY sort_num DESC, update_time DESC 实现正确排序。
置底逻辑
置底逻辑与置顶类似,不同之处在于我们会找到当前最小的排序值,并将其减 1 作为新的排序值。然而,由于 sort_num 字段是无符号类型,当最小排序值为 0 时,再减 1 仍然是 0。例如:
【id=4,sort_num=0,update_time=2024-07-09 17:00:00】
【id=5,sort_num=0,update_time=2024-07-09 17:01:00】
此时 id=4 在最底部,id=5 倒数第二,显然不符合预期。
解决方案
将 sort_num 字段改为有符号类型,使其支持负数。这样在置底逻辑中,当最小排序值为 0 时,我们可以继续向负数方向延伸,避免排序冲突。
func (s *adminRewardService) AdminRewardActivitySetSortNum(ctx context.Context, req *api.AdminRewardActivitySetSortNumReq) (resp *api.AdminRewardActivitySetSortNumResp, err error) {
ids := req.GetId()
idsCopy := make([]int64, len(ids))
copy(idsCopy, ids)
resp = new(api.AdminRewardActivitySetSortNumResp)
var wg sync.WaitGroup
var mu sync.Mutex
// 置顶:找到最顶部的数据的排序,然后把当前需要置顶的id改为最顶部的数据的id一致,两者一致通过updated_time排序
if req.GetTopId() > 0 {
topRecord, err := userIntegralRedeemActivityDao.GetTopRecord(ctx, s.Dao.DB)
if err != nil {
return nil, ecode.UnknownError
}
if err = userIntegralRedeemActivityDao.UpdateSortNum(ctx, req.GetTopId(), topRecord.SortNum, s.Dao.DB); err != nil {
zlog.WithContext(ctx).Sugar().Errorf("SetTopSortNumError:%v", err)
}
return resp, ecode.SuccessCode
}
// 置底排序:找到最底部数据排序,然后把当前数据排序改为最底部排序-1
if req.GetBottomId() > 0 {
bottomRecord, err := userIntegralRedeemActivityDao.GetBottomRecord(ctx, s.Dao.DB)
if err != nil {
return nil, ecode.UnknownError
}
if err = userIntegralRedeemActivityDao.UpdateSortNum(ctx, req.GetBottomId(), bottomRecord.SortNum-1, s.Dao.DB); err != nil {
zlog.WithContext(ctx).Sugar().Errorf("SetBottomSortNumError:%v", err)
}
return resp, ecode.SuccessCode
}
// 如果就一个元素,不排序
if len(req.GetId()) <= 1 {
return resp, ecode.SuccessCode
}
// 拖拽排序逻辑
maxSortId := req.GetId()[0]
//minSortId := maxSortId - int64(len(ids))
maxSort, _ := userIntegralRedeemActivityDao.GetUserIntegralRedeemActivitySortNumById(ctx, maxSortId, s.Dao.DB)
minSort := maxSort - int64(len(req.GetId()))
mark := 0
// 这是准备排序的index
for index := maxSort; index > minSort; index-- {
wg.Add(1)
currentId := ids[mark]
go func(goCtx context.Context, id int64, index int64) {
defer wg.Done()
if updateError := userIntegralRedeemActivityDao.UpdateSortNum(ctx, id, index, s.Dao.DB); updateError != nil {
mu.Lock()
err = updateError
mu.Unlock()
}
}(utils.GetTraceCtx(ctx), currentId, index)
mark++
}
wg.Wait()
if err != nil {
zlog.WithContext(ctx).Sugar().Errorf("拖拽排序失败, err = [%s]", err)
return resp, ecode.UnknownError
}
return resp, ecode.SuccessCode
}