news 2026/6/23 12:32:02

分组拖动排序功能全流程实现(前端Sortable.js + 后端Java批量更新)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
分组拖动排序功能全流程实现(前端Sortable.js + 后端Java批量更新)

实战!分组拖动排序功能全流程实现(前端Sortable.js + 后端Java批量更新)

在后台管理系统开发中,“分组拖动排序”是高频交互需求——比如用户分组、权限分组、菜单分组等场景,产品往往要求支持通过拖拽调整分组顺序,且排序结果实时持久化到数据库。本文从业务场景出发,完整拆解“前端拖拽交互 + 后端高效持久化”的实现方案,全程使用脱敏表名/类名,兼顾实用性与可落地性。

一、需求背景与技术选型

1. 核心需求

  • 前端:展示用户分组列表,支持鼠标拖拽调整分组顺序;
  • 后端:接收前端传入的分组ID顺序,自动分配连续的排序序号(1、2、3…),批量更新到数据库;
  • 性能要求:避免循环单条更新数据库,尽可能减少数据库交互次数;
  • 数据安全:确保排序更新原子性(要么全成功,要么全回滚),避免部分分组排序失效。

2. 技术选型

技术栈选型理由
前端Sortable.js(轻量无依赖,仅20KB,支持拖拽动画、自定义拖拽手柄)
后端Java + Spring Boot(业务逻辑) + MyBatis(批量SQL更新)
数据库MySQL(新增sort_num字段存储排序序号)

二、数据库设计(脱敏版)

新建用户分组表t_user_group,核心字段聚焦排序相关,其他业务字段按需扩展:

CREATETABLE`t_user_group`(`id`BIGINTPRIMARYKEYAUTO_INCREMENTCOMMENT'分组ID',`group_name`VARCHAR(50)NOTNULLCOMMENT'分组名称',`sort_num`INTNOTNULLDEFAULT0COMMENT'排序序号(1、2、3...,越小越靠前)',`parent_id`BIGINTDEFAULT-1COMMENT'父分组ID(-1代表顶级分组)',`create_time`DATETIMEDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='用户分组表';

核心字段说明sort_num是排序核心字段,存储连续的整数序号,查询时通过ORDER BY sort_num ASC即可按拖拽顺序展示。

三、前端实现(Sortable.js 拖拽交互)

1. 引入依赖

可通过CDN或npm引入Sortable.js,这里使用CDN简化示例:

<!-- 引入Sortable.js --><scriptsrc="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script><!-- 引入Axios(用于请求后端接口) --><scriptsrc="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

2. 渲染分组列表

前端页面展示分组列表,为每个分组行绑定data-group-id存储分组ID(核心:后端仅需ID顺序,无需传排序号):

<divclass="group-list-container"><table><thead><tr><th>分组名称</th><th>操作</th></tr></thead><tbodyid="group-list-tbody"><!-- 后端渲染示例(也可前端异步加载) --><trdata-group-id="1"><td>普通用户组</td><td><iclass="sort-handle"></i></td></tr><trdata-group-id="2"><td>VIP用户组</td><td><iclass="sort-handle"></i></td></tr><trdata-group-id="3"><td>管理员组</td><td><iclass="sort-handle"></i></td></tr></tbody></table></div>

3. 初始化拖拽并提交排序

核心逻辑:拖拽结束后收集分组ID顺序,调用后端接口提交,前端无需关心排序号(由后端自动分配1、2、3…):

// 获取分组列表DOMconstgroupTbody=document.querySelector('#group-list-tbody');// 初始化Sortableconstsortable=newSortable(groupTbody,{animation:150,// 拖拽动画时长(毫秒)handle:'.sort-handle',// 仅拖拽手柄可触发排序(提升交互体验)onEnd:function(){// 拖拽结束后,收集分组ID顺序(核心:仅传ID列表)constgroupIdList=Array.from(groupTbody.children).map(tr=>{returnNumber(tr.dataset.groupId);// 结果示例:[2,1,3]});// 调用后端排序接口axios.post('/api/user-group/batch-sort',groupIdList).then(res=>{alert('排序成功!');// 可选:刷新列表(若需实时展示排序结果)// window.location.reload();}).catch(err=>{alert('排序失败:'+err.response.data.msg);});}});

四、后端实现(Java + MyBatis 批量更新)

1. 定义DTO(接收前端参数)

前端仅传分组ID列表,无需DTO封装复杂字段,直接用List<Long>接收即可;若需扩展,可定义简单DTO:

importlombok.Data;importjava.util.List;/** * 分组排序入参DTO(可选,也可直接用List<Long>接收) */@DatapublicclassUserGroupSortDTO{privateList<Long>groupIdList;// 拖拽后的分组ID顺序列表}

2. Controller层(接收请求)

importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.annotation.Resource;importjava.util.List;@RestController@RequestMapping("/api/user-group")publicclassUserGroupController{@ResourceprivateUserGroupServiceuserGroupService;/** * 分组批量排序接口 * @param groupIdList 前端传入的分组ID顺序列表 */@PostMapping("/batch-sort")publicResultResponse<Boolean>batchSort(@RequestBodyList<Long>groupIdList){userGroupService.redefineSort(groupIdList);returnResultResponse.getSuccessResponse(true,"排序成功");}}

3. Service层(核心逻辑:校验 + 事务 + 自动分配排序号)

Service层是核心,需做好参数校验(避免脏数据)、事务保障(原子性)、自动分配sort_num

importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.util.CollectionUtils;importjavax.annotation.Resource;importjava.util.ArrayList;importjava.util.HashSet;importjava.util.List;importjava.util.Set;@Slf4j@ServicepublicclassUserGroupService{@ResourceprivateUserGroupMapperuserGroupMapper;/** * 重新定义分组排序:按前端ID顺序,自动分配sort_num=1、2、3... */@Transactional(rollbackFor=Exception.class)// 事务:批量更新原子性publicvoidredefineSort(List<Long>groupIdList){// ========== 步骤1:参数校验(避免脏数据) ==========// 1.1 校验列表非空if(CollectionUtils.isEmpty(groupIdList)){log.warn("分组排序失败:传入的ID列表为空");thrownewBusinessException("分组ID列表不能为空");}// 1.2 校验列表无重复IDSet<Long>idSet=newHashSet<>(groupIdList);if(idSet.size()!=groupIdList.size()){log.warn("分组排序失败:ID列表包含重复值,列表:{}",groupIdList);thrownewBusinessException("分组ID不能重复");}// 1.3 校验所有ID都存在(避免更新无效ID)intexistCount=userGroupMapper.countExistGroupIds(groupIdList);if(existCount!=groupIdList.size()){log.warn("分组排序失败:存在无效ID,传入数量:{},有效数量:{}",groupIdList.size(),existCount);thrownewBusinessException("存在无效的分组ID,请检查");}// ========== 步骤2:构造批量更新数据(自动分配sort_num) ==========List<UserGroup>sortList=newArrayList<>();intsortNum=1;// 排序序号从1开始for(LonggroupId:groupIdList){UserGroupuserGroup=newUserGroup();userGroup.setId(groupId);userGroup.setSortNum(sortNum);sortList.add(userGroup);sortNum++;}// ========== 步骤3:批量更新排序(核心操作) ==========try{intupdateCount=userGroupMapper.batchUpdateSort(sortList);log.info("分组排序成功:更新{}个分组的sort_num,ID列表:{}",updateCount,groupIdList);}catch(Exceptione){log.error("分组排序批量更新失败",e);thrownewBusinessException("排序失败:"+e.getMessage());}}}

4. Mapper层(MyBatis 批量更新SQL)

4.1 Mapper接口
importorg.apache.ibatis.annotations.Param;importjava.util.List;publicinterfaceUserGroupMapper{/** * 统计有效分组ID数量(校验ID是否存在) */intcountExistGroupIds(@Param("groupIdList")List<Long>groupIdList);/** * 批量更新分组排序(核心:一条SQL完成所有更新) */intbatchUpdateSort(@Param("sortList")List<UserGroup>sortList);}
4.2 Mapper.xml(关键:CASE WHEN 批量更新)

避坑重点CASE WHEN的条件必须是分组ID(id),而非排序号(sort_num),否则更新逻辑完全失效!

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="cn.demo.user.mapper.UserGroupMapper"><!-- 统计有效分组ID数量 --><selectid="countExistGroupIds"resultType="int">SELECT COUNT(1) FROM t_user_group WHERE id IN<foreachcollection="groupIdList"item="id"open="("close=")"separator=",">#{id}</foreach></select><!-- 批量更新排序(核心:一条SQL替代循环单更) --><updateid="batchUpdateSort">UPDATE t_user_group SET sort_num = CASE id<foreachcollection="sortList"item="item"index="index">WHEN #{item.id} THEN #{item.sortNum}<!-- 按ID匹配,赋值新排序号 --></foreach>END WHERE id IN<foreachcollection="sortList"item="item"open="("close=")"separator=",">#{item.id}</foreach></update></mapper>

5. 配套工具类(异常 + 统一返回)

5.1 业务异常类
/** * 自定义业务异常 */publicclassBusinessExceptionextendsRuntimeException{publicBusinessException(Stringmessage){super(message);}}
5.2 统一返回类
importlombok.Data;/** * 接口统一返回结果 */@DatapublicclassResultResponse<T>{privateintcode;// 200=成功,500=失败privateStringmsg;// 提示信息privateTdata;// 返回数据// 成功响应publicstatic<T>ResultResponse<T>getSuccessResponse(Tdata,Stringmsg){ResultResponse<T>response=newResultResponse<>();response.setCode(200);response.setMsg(msg);response.setData(data);returnresponse;}// 失败响应publicstatic<T>ResultResponse<T>getFailResponse(Stringmsg){ResultResponse<T>response=newResultResponse<>();response.setCode(500);response.setMsg(msg);response.setData(null);returnresponse;}}

五、关键优化点与避坑指南

1. 核心优化点

优化策略价值
批量SQL更新一条SQL完成所有分组的sort_num更新,替代循环单条更新,减少数据库交互
事务保障确保排序更新原子性,避免“部分分组更新成功、部分失败”
全量参数校验拦截空列表、重复ID、无效ID,避免脏数据入库
前端仅传ID列表简化前端逻辑,排序号由后端统一分配,避免前后端数据不一致

2. 常见避坑点

  • SQL语法错误CASE WHEN条件写成#{item.sortNum}而非#{item.id},导致更新无效果;
  • 无事务包裹:批量更新时数据库异常,导致部分分组排序号错误;
  • 前端传部分ID:仅传拖拽的分组ID,未传全量,导致未传的分组sort_num断层;
  • 空指针风险:未校验groupIdList为null,或UserGroupsortNum字段为null;
  • 排序号不连续:后端未从1开始分配序号,或序号递增逻辑错误(如sortNum += 2)。

六、扩展场景:部分分组排序(非全量)

若业务需求是“仅拖拽调整单个分组,其余分组自动顺延”(比如把3号分组拖到2号位置,原2、4号分组顺延),可调整逻辑:

  1. 前端传“被拖拽分组ID + 目标位置序号”;
  2. 后端先查询所有分组的sort_num,调整目标位置后分组的序号(如sort_num += 1);
  3. 仍用批量SQL更新,避免循环单更。

七、总结

分组拖动排序功能的核心是“前端轻量交互 + 后端高效持久化”:

  1. 前端用Sortable.js实现拖拽,仅需传递ID顺序,无需关心排序号计算;
  2. 后端通过CASE WHEN批量SQL更新,配合事务和参数校验,确保排序高效且安全;
  3. 避坑关键:批量SQL的正确性、事务的原子性、参数的全量校验。

该方案兼顾性能与可维护性,可直接适配到用户分组、菜单、角色等各类需要拖拽排序的场景中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 18:48:28

使用PyTorch训练微调Qwen3-14B的入门级教程

使用PyTorch训练微调Qwen3-14B的入门级教程 在企业智能化转型加速的今天&#xff0c;越来越多公司希望部署具备领域理解能力的AI助手——既能读懂行业术语&#xff0c;又能联动内部系统自动执行任务。然而&#xff0c;通用大模型往往“懂语言但不懂业务”&#xff0c;而从零训练…

作者头像 李华
网站建设 2026/6/22 22:27:26

从代码看BuildingAI:企业级智能体平台设计解析

引言 近期&#xff0c; 在企业级开源智能体平台领域引起了开发者社区的关注。作为一名长期关注 AI 工程化落地的架构师&#xff0c;我决定深入其代码仓库&#xff08;GitHub/BidingCC/BuildingAI&#xff09;&#xff0c;从工程实现的角度进行一次系统性的技术分析。本文将以专…

作者头像 李华
网站建设 2026/6/23 7:36:09

负责处理大数据量的Excel导出功能

/*** 数据导出控制器* 负责处理大数据量的Excel导出功能*/ RestController RequestMapping("/api/export") public class ExportController {Autowiredprivate DataService dataService;/*** 内部类&#xff1a;Excel数据导出服务器* 实现EasyPOI的IExcelExportServe…

作者头像 李华
网站建设 2026/6/22 22:38:44

JMeter---正则表达式提取器

JMeter的正则表达式提取器是一个用于从服务器响应中提取特定数据的监听器。它可以根据正则表达式模式匹配响应内容&#xff0c;并提取匹配到的数据供后续测试步骤使用。 在JMeter的测试计划中选择需要提取数据的HTTP请求或其他请求&#xff0c;右键点击&#xff0c;选择"…

作者头像 李华
网站建设 2026/6/23 18:33:46

如何利用diskinfo下载官网资源优化Qwen3-VL-8B存储性能

如何利用diskinfo优化Qwen3-VL-8B的存储部署性能 在AI模型日益“重型化”的今天&#xff0c;一个反向趋势正在悄然兴起&#xff1a;轻量级多模态模型正成为工业落地的香饽饽。以Qwen3-VL-8B为例&#xff0c;这款80亿参数的视觉语言模型虽不及百亿巨兽那般耀眼&#xff0c;却凭借…

作者头像 李华
网站建设 2026/6/22 22:47:33

量子电导式氢气浓度检测仪在制氢系统中的优势

​ ​氢能产业高速发展背景下&#xff0c;制氢系统的安全稳定运行与高效管控已成为行业核心诉求。氢气易燃易爆&#xff08;爆炸极限4.0%~75.6%VOL&#xff09;&#xff0c;浓度检测是保障系统安全的关键屏障。量子电导式氢气浓度检测仪凭借独特技术原理&#xff0c;在复杂工…

作者头像 李华