背景
最近在做課程后臺開發(fā)時,遇到一個很有意思的問題。問題是這樣的,有多節(jié)課程,運營人員可以根據(jù)需要調(diào)整課程的先后順序,從而更改課程的展示順序。
接到這個需求,立刻就想到了以前做首頁banner輪播圖的管理后臺。輪播圖中每一張圖有先后順序之分,為了進(jìn)行圖片排序,在設(shè)計banner實體時,設(shè)計了一個 weight
(權(quán)重)字段。然后,讓運營人員給每張圖填寫1~500值,值越大,則排序越靠前。由于輪播圖展示的圖片有限(不超過5張),即使通過人工填值的方式管理順序,也不是很麻煩。但是,這種填權(quán)重管理順序的方式不適用于數(shù)據(jù)量較多的課程管理。那么,如何解決這種排序問題呢?
為了解決元素自定義排序問題,我們想了很多解決辦法。例如,
- 增加上移/下移功能,更改當(dāng)前元素排序
- 填權(quán)重,或增減權(quán)重值
- 置頂排序
- 拖拽排序
最后,綜合考慮了一下,認(rèn)為第四種最符合日常操作的習(xí)慣。針對拖拽排序進(jìn)行了后端方案設(shè)計。
需求描述
- 允許更改元素的排序;
- 允許新增數(shù)據(jù),并更新現(xiàn)有排序;
- 允許刪除數(shù)據(jù),并更新現(xiàn)有排序。
解決辦法
方法一 全量更新元素位置法
適用場景:排序元素數(shù)量較少
原理:每個元素?fù)碛幸粋€字段,表示元素當(dāng)前排序的位置。通過前端排序,將排好的元素位置,一次性發(fā)送到后端。然后,后端統(tǒng)一更新所有元素的位置。
具體實現(xiàn)
實體設(shè)計:增加排序字段 sort
,表示元素當(dāng)前的位置。例如,sort = 1
,則表示元素處于第一位。
接口設(shè)計:
// 排序接口
POST /courses/sorting
// 格式:JSON
// 數(shù)組中為元素的ID
// 元素在數(shù)組中的序號,表示了元素的位置
// 例如,ID為3的元素,排序為 1,ID為2的元素,排序為2
[3, 2, 4, 1]
前端邏輯:當(dāng)前端排序后,或刪除元素后,將剩余元素ID,以數(shù)組的形式發(fā)送給后端。數(shù)組的索引序號,則表示元素當(dāng)前的排序。
后端實現(xiàn)邏輯:
- 刪除不存在前端數(shù)組中的ID。將前端
ids
,與服務(wù)器端的ids
進(jìn)行對比。刪除服務(wù)器端存在但前端不存在的ids
。 - 按照數(shù)組的排序,更新所有元素排序。
總結(jié),此方法僅適用于排序元素較少(例如,總元素為5~15個)的場景。對于大量數(shù)據(jù)排序并不適用,適合首頁輪播圖管理、任務(wù)卡片管理。leangoo 的看板卡片管理就是采用這種方式。
方法二 取中值法(推薦)
原理與實現(xiàn)步驟:
- 創(chuàng)建元素時給元素賦默認(rèn)位置(
pos
字段記錄該值)。賦值規(guī)則為,當(dāng)創(chuàng)建第一個元素時,默認(rèn)位置賦值為65536,第二個元素為2 * 65536 = 13172
,增加第N個元素時,位置賦值為N*65536。 - 當(dāng)拖拽改變元素位置時,更新
pos
。更新規(guī)則如下:
- 調(diào)整一個元素到兩個元素中間時,
(pre_item.pos + after_item.pos)/ 2 = pos
- 調(diào)整一個元素到第一個元素時,
old_first_item.pos / 2 = pos
- 調(diào)整一個元素到最后一個元素時,
old_last_item.post + 65536 = pos
- 當(dāng)前后兩個元素的數(shù)值,不滿足整數(shù)時,更新所有元素的排序。依次給每個元素的
pos
賦新值。例如,第一位賦值65536,第二位為2 * 65536
,第N位賦值N*65536。
通過取中值的方法,改變元素的位置。當(dāng)需要按序獲取時,只需要對 pos
進(jìn)行排序,就可以獲取元素的位置。
關(guān)于中值重排的問題,解決方法有多種。例如,我們可以使用浮點數(shù)儲存 pos
,但是需要考慮數(shù)據(jù)庫存儲的精度問題。而且,數(shù)值過小,會在前端丟失精度,元素排序會出現(xiàn)問題。當(dāng)然,如果在接口層,當(dāng)檢測到中值過小,則對所有元素進(jìn)行重排,接口相應(yīng)速度會存在問題(如果是后端管理系統(tǒng)就不用考慮性能問題了)。
有人提出,利用定時任務(wù)每天對所有元素定時重排,來解決單次接口的性能問題。個人覺得這個方法,還是存在問題。若定時任務(wù)不及時,那么排序由于精度問題,發(fā)生了排序錯亂的問題。那么,定時重排已經(jīng)無意義。
方法三 單表單列
每個元素,都有一個字段index
,表示元素的排序信息。
規(guī)定元素從0開始遞增。
基本操作如下:
- 增加數(shù)據(jù)。 新增元素時,序號為當(dāng)前元素數(shù)據(jù)總量值。
- 刪除元素。刪除元素時,將大于該元素的序號,都減1。
- 修改元素排序。當(dāng)元素從 x 移動到 y 時,
- 若 x < y 時,則將(x, y)范圍內(nèi)的元素都減1
- 若 x > y 時,則將(y, x)范圍內(nèi)的元素都加1
- 查詢元素。展示列表時,按照
index
字段進(jìn)行排序即可。若需要查第n位元素時,元素位置為index = n - 1
。
這種方式優(yōu)點是,查詢快,修改慢。而且,修改接口的邏輯較重,處理起來比較麻煩。我們很多項目都是采用這種模式。在接口設(shè)計方面,我們讓前端傳給后端是一個偏移值(offset
),offset = y - x。當(dāng)元素向排序大的方向移動時,
offset的為正值;若往排序小的方向移動時,
offset`為負(fù)值。
總結(jié)
在選擇具體方案時,還需要根據(jù)具體的業(yè)務(wù)場景選擇。方案的選擇規(guī)則總結(jié)如下:
- 若排序元素較少,采用方案一;
- 若排序移動次數(shù)不是非常頻繁且對接口性能要求高,建議采用方案二;
- 若排序移動非常頻繁且接口性能要求不高,可以采用方案三。