树形结构后端构建
一、页面结构 overview
该页面主要由三部分组成:顶部操作栏、左侧树形区域选择器、右侧标签页式穿梭框组件,用于管理不同区域的水表设备关联关系。
<el-tree:data="areaOptions":props="{ label: 'label', children: 'children' }":expand-on-click-node="false"node-key="id"highlight-currentdefault-expand-all@node-click="handleNodeClick"
/>
/** 查询区域下拉树结构 */
function getAreaTree() {areaTreeSelect().then(response => {areaOptions.value = response.data})
}// 查询区域下拉树结构
export function areaTreeSelect() {return request({url: '/dma/area/areaTree',method: 'get'})
}
/*** 获取区域树列表*/@PreAuthorize("@ss.hasPermi('system:user:list')")@GetMapping("/areaTree")public AjaxResult areaTree(DmaArea area){return success(dmaAreaService.selectDeptTreeList(area));}
// 入口方法:获取树形选择器列表
public List<TreeSelect> selectDeptTreeList(DmaArea area) {// 1. 获取AOP代理对象并查询区域列表(解决自调用AOP失效问题)List<DmaArea> areaList = SpringUtils.getAopProxy(this).selectDmaAreaList(area);// 2. 构建树形结构并转换为TreeSelectreturn buildDeptTreeSelect(areaList);
}
SpringUtils.getAopProxy(this):获取当前对象的AOP代理,确保selectDmaAreaList方法的事务等AOP增强生效
SpringUtils.java
selectDmaAreaList(area):查询满足条件的区域列表(具体实现依赖MyBatis映射器)
buildDeptTreeSelect(areaList):核心转换方法,将平面列表转为树形结构
public List<TreeSelect> buildDeptTreeSelect(List<DmaArea> areaList) {// 1. 构建部门树形结构List<DmaArea> deptTrees = buildDeptTree(areaList);// 2. 转换为前端需要的TreeSelect对象return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
}
TreeSelect构造函数 :将 DmaArea 对象转换为前端组件所需的树形结构模型,包含 id / label / children 等标准树形属性`TreeSelect`
package com.ruoyi.project.dma.domain;import com.ruoyi.project.system.domain.SysDept;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import com.ruoyi.framework.web.domain.BaseEntity;import java.util.ArrayList;
import java.util.List;/*** 区域管理对象 dma_area* * @author smart* @date 2025-08-28*/
public class DmaArea extends BaseEntity
{private static final long serialVersionUID = 1L;/** ID */private Long id;/** pid */@Excel(name = "pid")private Long pid;/** 名称 */@Excel(name = "名称")private String name;/** 分区类型 */@Excel(name = "分区类型")private String type;/** 区域数据 */private DmaData dmaData;/** 子区域 */private List<DmaArea> children = new ArrayList<DmaArea>();public void setId(Long id) {this.id = id;}public List<DmaArea> getChildren() {return children;}public void setChildren(List<DmaArea> children) {this.children = children;}public Long getId(){return id;}public void setPid(Long pid) {this.pid = pid;}public Long getPid() {return pid;}public void setName(String name) {this.name = name;}public String getName() {return name;}public void setType(String type) {this.type = type;}public String getType() {return type;}public DmaData getDmaData() {return dmaData;}public void setDmaData(DmaData dmaData) {this.dmaData = dmaData;}@Overridepublic String toString() {return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE).append("id", getId()).append("pid", getPid()).append("name", getName()).append("type", getType()).toString();}
}
public List<DmaArea> buildDeptTree(List<DmaArea> depts) {if (depts == null || depts.isEmpty()) return Collections.emptyList();// 步骤1:构建父ID到子节点的映射表(O(n)时间复杂度)Map<Long, List<DmaArea>> pidMap = depts.stream().filter(d -> d.getPid() != null).collect(Collectors.groupingBy(DmaArea::getPid));// 步骤2:获取所有区域ID集合,用于判断顶级节点Set<Long> idSet = depts.stream().map(DmaArea::getId).collect(Collectors.toSet());// 步骤3:筛选顶级节点(PID不存在于ID集合中的节点)List<DmaArea> topNodes = depts.stream().filter(d -> d.getPid() == null || !idSet.contains(d.getPid())).collect(Collectors.toList());// 步骤4:递归构建整棵树for (DmaArea top : topNodes) {buildTreeRecursive(top, pidMap);}return topNodes.isEmpty() ? depts : topNodes;
}
- pidMap : Map<父ID, 子节点列表> ,实现O(1)时间复杂度查找子节点
- idSet :快速判断某个PID是否为有效区域ID(排除外部引用的无效PID)
- topNodes :树形结构的根节点集合(可能存在多个根节点)
private DmaData buildTreeRecursive(DmaArea node, Map<Long, List<DmaArea>> pidMap) {// 1. 获取当前节点的子节点列表List<DmaArea> children = pidMap.getOrDefault(node.getId(), Collections.emptyList());node.setChildren(children);// 2. 递归处理所有子节点for (DmaArea child : children) {buildTreeRecursive(child, pidMap);}// 3. 数据聚合逻辑(核心业务计算)// ... 子节点数据汇总 ...
}
- 递归终止条件 :当节点ID在pidMap中无对应子节点时,children为空列表
- 树结构存储 :通过 node.setChildren(children) 将子节点挂载到当前节点,形成层级关系
// 统计子节点数据
boolean hasChildData = false;
double supplyTotal = 0.0;
double salesTotal = 0.0;for (DmaArea child : children) {DmaData childData = child.getDmaData();if (childData != null &&(childData.getSupply() != null || childData.getSales() != null|| childData.getLoss() != null || childData.getNrw() != null)) {hasChildData = true;supplyTotal += childData.getSupply() == null ? 0.0 : childData.getSupply();salesTotal += childData.getSales() == null ? 0.0 : childData.getSales();}
}// 计算聚合数据
DmaData dmaData = new DmaData();
dmaData.setName(node.getName());
if (hasChildData) {dmaData.setSupply(supplyTotal);dmaData.setSales(salesTotal);dmaData.setLoss(supplyTotal - salesTotal);// 计算产销差率(NRW)if (dmaData.getLoss() != null && dmaData.getSupply() != null && dmaData.getSupply() != 0.0) {dmaData.setNrw(dmaData.getLoss() / dmaData.getSupply());}
}
node.setDmaData(dmaData);
return dmaData;
聚合规则 :
- 仅当子节点存在有效数据时才进行聚合
- 供应总量=所有子节点供应量之和
- 损失量=供应量-销售量
- 产销差率(NRW)=损失量/供应量(避免除以零)
package com.ruoyi.system.service.impl;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.TreeSelect;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.system.mapper.SysRoleMapper;
import com.ruoyi.system.service.ISysDeptService;/*** 部门管理 服务实现* * @author ruoyi*/
@Service
public class SysDeptServiceImpl implements ISysDeptService
{@Autowiredprivate SysDeptMapper deptMapper;@Autowiredprivate SysRoleMapper roleMapper;/*** 查询部门管理数据* * @param dept 部门信息* @return 部门信息集合*/@Override@DataScope(deptAlias = "d")public List<SysDept> selectDeptList(SysDept dept){return deptMapper.selectDeptList(dept);}/*** 查询部门树结构信息* * @param dept 部门信息* @return 部门树信息集合*/@Overridepublic List<TreeSelect> selectDeptTreeList(SysDept dept){List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);return buildDeptTreeSelect(depts);}/*** 构建前端所需要树结构* * @param depts 部门列表* @return 树结构列表*/@Overridepublic List<SysDept> buildDeptTree(List<SysDept> depts){List<SysDept> returnList = new ArrayList<SysDept>();List<Long> tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList());for (SysDept dept : depts){// 如果是顶级节点, 遍历该父节点的所有子节点if (!tempList.contains(dept.getParentId())){recursionFn(depts, dept);returnList.add(dept);}}if (returnList.isEmpty()){returnList = depts;}return returnList;}/*** 构建前端所需要下拉树结构* * @param depts 部门列表* @return 下拉树结构列表*/@Overridepublic List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts){List<SysDept> deptTrees = buildDeptTree(depts);return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());}/*** 根据角色ID查询部门树信息* * @param roleId 角色ID* @return 选中部门列表*/@Overridepublic List<Long> selectDeptListByRoleId(Long roleId){SysRole role = roleMapper.selectRoleById(roleId);return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly());}/*** 根据部门ID查询信息* * @param deptId 部门ID* @return 部门信息*/@Overridepublic SysDept selectDeptById(Long deptId){return deptMapper.selectDeptById(deptId);}/*** 根据ID查询所有子部门(正常状态)* * @param deptId 部门ID* @return 子部门数*/@Overridepublic int selectNormalChildrenDeptById(Long deptId){return deptMapper.selectNormalChildrenDeptById(deptId);}/*** 是否存在子节点* * @param deptId 部门ID* @return 结果*/@Overridepublic boolean hasChildByDeptId(Long deptId){int result = deptMapper.hasChildByDeptId(deptId);return result > 0;}/*** 查询部门是否存在用户* * @param deptId 部门ID* @return 结果 true 存在 false 不存在*/@Overridepublic boolean checkDeptExistUser(Long deptId){int result = deptMapper.checkDeptExistUser(deptId);return result > 0;}/*** 校验部门名称是否唯一* * @param dept 部门信息* @return 结果*/@Overridepublic boolean checkDeptNameUnique(SysDept dept){Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId();SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId());if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()){return UserConstants.NOT_UNIQUE;}return UserConstants.UNIQUE;}/*** 校验部门是否有数据权限* * @param deptId 部门id*/@Overridepublic void checkDeptDataScope(Long deptId){if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId)){SysDept dept = new SysDept();dept.setDeptId(deptId);List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);if (StringUtils.isEmpty(depts)){throw new ServiceException("没有权限访问部门数据!");}}}/*** 新增保存部门信息* * @param dept 部门信息* @return 结果*/@Overridepublic int insertDept(SysDept dept){SysDept info = deptMapper.selectDeptById(dept.getParentId());// 如果父节点不为正常状态,则不允许新增子节点if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())){throw new ServiceException("部门停用,不允许新增");}dept.setAncestors(info.getAncestors() + "," + dept.getParentId());return deptMapper.insertDept(dept);}/*** 修改保存部门信息* * @param dept 部门信息* @return 结果*/@Overridepublic int updateDept(SysDept dept){SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId());SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId());if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)){String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId();String oldAncestors = oldDept.getAncestors();dept.setAncestors(newAncestors);updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);}int result = deptMapper.updateDept(dept);if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors())&& !StringUtils.equals("0", dept.getAncestors())){// 如果该部门是启用状态,则启用该部门的所有上级部门updateParentDeptStatusNormal(dept);}return result;}/*** 修改该部门的父级部门状态* * @param dept 当前部门*/private void updateParentDeptStatusNormal(SysDept dept){String ancestors = dept.getAncestors();Long[] deptIds = Convert.toLongArray(ancestors);deptMapper.updateDeptStatusNormal(deptIds);}/*** 修改子元素关系* * @param deptId 被修改的部门ID* @param newAncestors 新的父ID集合* @param oldAncestors 旧的父ID集合*/public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors){List<SysDept> children = deptMapper.selectChildrenDeptById(deptId);for (SysDept child : children){child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));}if (children.size() > 0){deptMapper.updateDeptChildren(children);}}/*** 删除部门管理信息* * @param deptId 部门ID* @return 结果*/@Overridepublic int deleteDeptById(Long deptId){return deptMapper.deleteDeptById(deptId);}/*** 递归列表*/private void recursionFn(List<SysDept> list, SysDept t){// 得到子节点列表List<SysDept> childList = getChildList(list, t);t.setChildren(childList);for (SysDept tChild : childList){if (hasChild(list, tChild)){recursionFn(list, tChild);}}}/*** 得到子节点列表*/private List<SysDept> getChildList(List<SysDept> list, SysDept t){List<SysDept> tlist = new ArrayList<SysDept>();Iterator<SysDept> it = list.iterator();while (it.hasNext()){SysDept n = (SysDept) it.next();if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()){tlist.add(n);}}return tlist;}/*** 判断是否有子节点*/private boolean hasChild(List<SysDept> list, SysDept t){return getChildList(list, t).size() > 0;}
}