目的:减少数据库压力
传统解决方案
获取拖拽后元素的前一个元素的位置,重新排序后面的所有元素,如果没有前一个元素,则统一全排。
例如:将item1拖拽到item6之后,按顺序排列可以不处理item6及之前的数据,但是必须重新处理item1及之后所有的数据。
位置间隔法
获取拖拽后前一个元素和后一个元素,设置当前顺序为中间数值,不用修改其他元素,如果顺序过于紧密则出发重平衡机制。
这里需要几个参数:初始值(BASE_SCORE),间隔(INITIAL_INTERVAL),平衡参数(MIN_GAP )。
1.第一个元素作为初始值
2.当新添加一个元素时,取最大值+间隔
3.当修改顺序时,计算相邻项目的中间值作为新元素顺序值。
4.当新元素值与前后元素间隔小于平衡参数时,触发重平衡。
初始数据
@Entity
public class SortableItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 使用Double类型存储分数(也可用BigDecimal提高精度)
private Double sortScore;
// 其他业务字段
private String name;
private boolean visible;
// 初始分数分配
@PrePersist
public void initializeSortScore() {
if (this.sortScore == null) {
// 获取当前最大分数
Double maxScore = repository.findMaxSortScore();
this.sortScore = (maxScore != null) ? maxScore + INITIAL_INTERVAL : BASE_SCORE;
}
}
// 常量定义
private static final Double BASE_SCORE = 1000000.0;
private static final Double INITIAL_INTERVAL = 10000.0;
}
插入移动实现
@Service
public class FractionalSortService {
// 最小允许间隔(避免浮点精度问题)
private static final Double MIN_GAP = 0.0001;
@Transactional
public void moveItem(Long itemId, Long prevId, Long nextId) {
SortableItem movingItem = itemRepository.findById(itemId).orElseThrow();
SortableItem prevItem = prevId != null ? itemRepository.findById(prevId).orElse(null) : null;
SortableItem nextItem = nextId != null ? itemRepository.findById(nextId).orElse(null) : null;
// 计算新分数
Double newScore = calculateNewScore(prevItem, nextItem);
// 检查是否需要重平衡
if (requiresRebalance(prevItem, newScore, nextItem)) {
rebalanceItems(prevItem, nextItem);
// 重新计算新位置
newScore = calculateNewScore(prevItem, nextItem);
}
// 更新分数
movingItem.setSortScore(newScore);
itemRepository.save(movingItem);
}
private Double calculateNewScore(SortableItem prev, SortableItem next) {
Double prevScore = (prev != null) ? prev.getSortScore() : 0.0;
Double nextScore = (next != null) ? next.getSortScore() : prevScore + 2 * INITIAL_INTERVAL;
// 简单情况:有足够间隔
if (nextScore - prevScore > MIN_GAP) {
return (prevScore + nextScore) / 2.0;
}
// 复杂情况:使用扩展范围算法
return extendedRangeCalculation(prevScore, nextScore);
}
private Double extendedRangeCalculation(Double prevScore, Double nextScore) {
// 使用对数刻度扩展范围
double range = nextScore - prevScore;
double logRange = Math.log(range);
// 在指数空间计算中点
double midLog = (Math.log(prevScore) + Math.log(nextScore)) / 2;
return Math.exp(midLog);
}
private boolean requiresRebalance(SortableItem prev, Double newScore, SortableItem next) {
if (prev != null && (newScore - prev.getSortScore()) < MIN_GAP) {
return true;
}
if (next != null && (next.getSortScore() - newScore) < MIN_GAP) {
return true;
}
return false;
}
}
重平衡实现
private void rebalanceItems(SortableItem startItem, SortableItem endItem) {
// 获取需要重平衡的范围
List<SortableItem> items = findItemsBetween(startItem, endItem);
if (items.size() < 2) return;
// 计算总范围和步长
double startScore = items.get(0).getSortScore();
double endScore = items.get(items.size()-1).getSortScore();
double totalRange = endScore - startScore;
double step = totalRange / (items.size() + 1);
// 分配新分数
for (int i = 0; i < items.size(); i++) {
SortableItem item = items.get(i);
item.setSortScore(startScore + step * (i + 1));
}
// 批量保存
itemRepository.saveAll(items);
}
private List<SortableItem> findItemsBetween(SortableItem start, SortableItem end) {
// 根据业务需求确定范围大小
int neighborCount = 50; // 前后各取50个项目
List<SortableItem> before = itemRepository.findPreviousItems(
start != null ? start.getId() : null,
neighborCount
);
List<SortableItem> after = itemRepository.findNextItems(
end != null ? end.getId() : null,
neighborCount
);
// 合并结果
List<SortableItem> result = new ArrayList<>();
result.addAll(before);
if (start != null) result.add(start);
if (end != null) result.add(end);
result.addAll(after);
// 按分数排序
result.sort(Comparator.comparingDouble(SortableItem::getSortScore));
return result;
}
重平衡策略选择:
局部重平衡:仅重平衡受影响区域(推荐)
全局重平衡:定期重排整个列表(维护时使用)
局部更新可以选择传入位置前后50条重新等间隔排序。
此文助力马上行计划管理WEB端四象限拖拽处理。
欢迎体验微信小程序:马上行计划管理
WEB端在紧锣密鼓的开发中敬请期待