网站正能量晚上不用下载进入免费请人做网站收费多少钱
网站正能量晚上不用下载进入免费,请人做网站收费多少钱,徐州软件外包,网站开发公司谁家好实战#xff01;分组拖动排序功能全流程实现#xff08;前端Sortable.js 后端Java批量更新#xff09;
在后台管理系统开发中#xff0c;“分组拖动排序”是高频交互需求——比如用户分组、权限分组、菜单分组等场景#xff0c;产品往往要求支持通过拖拽调整分组顺序分组拖动排序功能全流程实现前端Sortable.js 后端Java批量更新在后台管理系统开发中“分组拖动排序”是高频交互需求——比如用户分组、权限分组、菜单分组等场景产品往往要求支持通过拖拽调整分组顺序且排序结果实时持久化到数据库。本文从业务场景出发完整拆解“前端拖拽交互 后端高效持久化”的实现方案全程使用脱敏表名/类名兼顾实用性与可落地性。一、需求背景与技术选型1. 核心需求前端展示用户分组列表支持鼠标拖拽调整分组顺序后端接收前端传入的分组ID顺序自动分配连续的排序序号1、2、3…批量更新到数据库性能要求避免循环单条更新数据库尽可能减少数据库交互次数数据安全确保排序更新原子性要么全成功要么全回滚避免部分分组排序失效。2. 技术选型技术栈选型理由前端Sortable.js轻量无依赖仅20KB支持拖拽动画、自定义拖拽手柄后端Java Spring Boot业务逻辑 MyBatis批量SQL更新数据库MySQL新增sort_num字段存储排序序号二、数据库设计脱敏版新建用户分组表t_user_group核心字段聚焦排序相关其他业务字段按需扩展CREATETABLEt_user_group(idBIGINTPRIMARYKEYAUTO_INCREMENTCOMMENT分组ID,group_nameVARCHAR(50)NOTNULLCOMMENT分组名称,sort_numINTNOTNULLDEFAULT0COMMENT排序序号1、2、3...越小越靠前,parent_idBIGINTDEFAULT-1COMMENT父分组ID-1代表顶级分组,create_timeDATETIMEDEFAULTCURRENT_TIMESTAMPCOMMENT创建时间)ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT用户分组表;核心字段说明sort_num是排序核心字段存储连续的整数序号查询时通过ORDER BY sort_num ASC即可按拖拽顺序展示。三、前端实现Sortable.js 拖拽交互1. 引入依赖可通过CDN或npm引入Sortable.js这里使用CDN简化示例!-- 引入Sortable.js --scriptsrchttps://cdn.jsdelivr.net/npm/sortablejs1.15.0/Sortable.min.js/script!-- 引入Axios用于请求后端接口 --scriptsrchttps://cdn.jsdelivr.net/npm/axios/dist/axios.min.js/script2. 渲染分组列表前端页面展示分组列表为每个分组行绑定data-group-id存储分组ID核心后端仅需ID顺序无需传排序号divclassgroup-list-containertabletheadtrth分组名称/thth操作/th/tr/theadtbodyidgroup-list-tbody!-- 后端渲染示例也可前端异步加载 --trdata-group-id1td普通用户组/tdtdiclasssort-handle☰/i/td/trtrdata-group-id2tdVIP用户组/tdtdiclasssort-handle☰/i/td/trtrdata-group-id3td管理员组/tdtdiclasssort-handle☰/i/td/tr/tbody/table/div3. 初始化拖拽并提交排序核心逻辑拖拽结束后收集分组ID顺序调用后端接口提交前端无需关心排序号由后端自动分配1、2、3…// 获取分组列表DOMconstgroupTbodydocument.querySelector(#group-list-tbody);// 初始化SortableconstsortablenewSortable(groupTbody,{animation:150,// 拖拽动画时长毫秒handle:.sort-handle,// 仅拖拽手柄可触发排序提升交互体验onEnd:function(){// 拖拽结束后收集分组ID顺序核心仅传ID列表constgroupIdListArray.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封装复杂字段直接用ListLong接收即可若需扩展可定义简单DTOimportlombok.Data;importjava.util.List;/** * 分组排序入参DTO可选也可直接用ListLong接收 */DatapublicclassUserGroupSortDTO{privateListLonggroupIdList;// 拖拽后的分组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;RestControllerRequestMapping(/api/user-group)publicclassUserGroupController{ResourceprivateUserGroupServiceuserGroupService;/** * 分组批量排序接口 * param groupIdList 前端传入的分组ID顺序列表 */PostMapping(/batch-sort)publicResultResponseBooleanbatchSort(RequestBodyListLonggroupIdList){userGroupService.redefineSort(groupIdList);returnResultResponse.getSuccessResponse(true,排序成功);}}3. Service层核心逻辑校验 事务 自动分配排序号Service层是核心需做好参数校验避免脏数据、事务保障原子性、自动分配sort_numimportlombok.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;Slf4jServicepublicclassUserGroupService{ResourceprivateUserGroupMapperuserGroupMapper;/** * 重新定义分组排序按前端ID顺序自动分配sort_num1、2、3... */Transactional(rollbackForException.class)// 事务批量更新原子性publicvoidredefineSort(ListLonggroupIdList){// 步骤1参数校验避免脏数据 // 1.1 校验列表非空if(CollectionUtils.isEmpty(groupIdList)){log.warn(分组排序失败传入的ID列表为空);thrownewBusinessException(分组ID列表不能为空);}// 1.2 校验列表无重复IDSetLongidSetnewHashSet(groupIdList);if(idSet.size()!groupIdList.size()){log.warn(分组排序失败ID列表包含重复值列表{},groupIdList);thrownewBusinessException(分组ID不能重复);}// 1.3 校验所有ID都存在避免更新无效IDintexistCountuserGroupMapper.countExistGroupIds(groupIdList);if(existCount!groupIdList.size()){log.warn(分组排序失败存在无效ID传入数量{}有效数量{},groupIdList.size(),existCount);thrownewBusinessException(存在无效的分组ID请检查);}// 步骤2构造批量更新数据自动分配sort_num ListUserGroupsortListnewArrayList();intsortNum1;// 排序序号从1开始for(LonggroupId:groupIdList){UserGroupuserGroupnewUserGroup();userGroup.setId(groupId);userGroup.setSortNum(sortNum);sortList.add(userGroup);sortNum;}// 步骤3批量更新排序核心操作 try{intupdateCountuserGroupMapper.batchUpdateSort(sortList);log.info(分组排序成功更新{}个分组的sort_numID列表{},updateCount,groupIdList);}catch(Exceptione){log.error(分组排序批量更新失败,e);thrownewBusinessException(排序失败e.getMessage());}}}4. Mapper层MyBatis 批量更新SQL4.1 Mapper接口importorg.apache.ibatis.annotations.Param;importjava.util.List;publicinterfaceUserGroupMapper{/** * 统计有效分组ID数量校验ID是否存在 */intcountExistGroupIds(Param(groupIdList)ListLonggroupIdList);/** * 批量更新分组排序核心一条SQL完成所有更新 */intbatchUpdateSort(Param(sortList)ListUserGroupsortList);}4.2 Mapper.xml关键CASE WHEN 批量更新避坑重点CASE WHEN的条件必须是分组IDid而非排序号sort_num否则更新逻辑完全失效?xml version1.0 encodingUTF-8?!DOCTYPEmapperPUBLIC-//mybatis.org//DTD Mapper 3.0//ENhttp://mybatis.org/dtd/mybatis-3-mapper.dtdmappernamespacecn.demo.user.mapper.UserGroupMapper!-- 统计有效分组ID数量 --selectidcountExistGroupIdsresultTypeintSELECT COUNT(1) FROM t_user_group WHERE id INforeachcollectiongroupIdListitemidopen(close)separator,#{id}/foreach/select!-- 批量更新排序核心一条SQL替代循环单更 --updateidbatchUpdateSortUPDATE t_user_group SET sort_num CASE idforeachcollectionsortListitemitemindexindexWHEN #{item.id} THEN #{item.sortNum}!-- 按ID匹配赋值新排序号 --/foreachEND WHERE id INforeachcollectionsortListitemitemopen(close)separator,#{item.id}/foreach/update/mapper5. 配套工具类异常 统一返回5.1 业务异常类/** * 自定义业务异常 */publicclassBusinessExceptionextendsRuntimeException{publicBusinessException(Stringmessage){super(message);}}5.2 统一返回类importlombok.Data;/** * 接口统一返回结果 */DatapublicclassResultResponseT{privateintcode;// 200成功500失败privateStringmsg;// 提示信息privateTdata;// 返回数据// 成功响应publicstaticTResultResponseTgetSuccessResponse(Tdata,Stringmsg){ResultResponseTresponsenewResultResponse();response.setCode(200);response.setMsg(msg);response.setData(data);returnresponse;}// 失败响应publicstaticTResultResponseTgetFailResponse(Stringmsg){ResultResponseTresponsenewResultResponse();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或UserGroup的sortNum字段为null排序号不连续后端未从1开始分配序号或序号递增逻辑错误如sortNum 2。六、扩展场景部分分组排序非全量若业务需求是“仅拖拽调整单个分组其余分组自动顺延”比如把3号分组拖到2号位置原2、4号分组顺延可调整逻辑前端传“被拖拽分组ID 目标位置序号”后端先查询所有分组的sort_num调整目标位置后分组的序号如sort_num 1仍用批量SQL更新避免循环单更。七、总结分组拖动排序功能的核心是“前端轻量交互 后端高效持久化”前端用Sortable.js实现拖拽仅需传递ID顺序无需关心排序号计算后端通过CASE WHEN批量SQL更新配合事务和参数校验确保排序高效且安全避坑关键批量SQL的正确性、事务的原子性、参数的全量校验。该方案兼顾性能与可维护性可直接适配到用户分组、菜单、角色等各类需要拖拽排序的场景中。