树形结构后端构建

一、页面结构 overview

该页面主要由三部分组成:顶部操作栏、左侧树形区域选择器、右侧标签页式穿梭框组件,用于管理不同区域的水表设备关联关系。

java 复制代码
<el-tree
  :data="areaOptions"
  :props="{ label: 'label', children: 'children' }"
  :expand-on-click-node="false"
  node-key="id"
  highlight-current
  default-expand-all
  @node-click="handleNodeClick"
/>
java 复制代码
/** 查询区域下拉树结构 */
function getAreaTree() {
  areaTreeSelect().then(response => {
    areaOptions.value = response.data
  })
}

// 查询区域下拉树结构
export function areaTreeSelect() {
  return request({
    url: '/dma/area/areaTree',
    method: 'get'
  })
}
java 复制代码
 /**
     * 获取区域树列表
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/areaTree")
    public AjaxResult areaTree(DmaArea area)
    {
        return success(dmaAreaService.selectDeptTreeList(area));
    }
java 复制代码
// 入口方法:获取树形选择器列表
public List<TreeSelect> selectDeptTreeList(DmaArea area) {
    // 1. 获取AOP代理对象并查询区域列表(解决自调用AOP失效问题)
    List<DmaArea> areaList = SpringUtils.getAopProxy(this).selectDmaAreaList(area);
    // 2. 构建树形结构并转换为TreeSelect
    return buildDeptTreeSelect(areaList);
}
  • SpringUtils.getAopProxy(this):获取当前对象的AOP代理,确保selectDmaAreaList方法的事务等AOP增强生效 SpringUtils.java

  • selectDmaAreaList(area):查询满足条件的区域列表(具体实现依赖MyBatis映射器)

  • buildDeptTreeSelect(areaList):核心转换方法,将平面列表转为树形结构

java 复制代码
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`

java 复制代码
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;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("id", getId())
            .append("pid", getPid())
            .append("name", getName())
            .append("type", getType())
            .toString();
    }
}
java 复制代码
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 :树形结构的根节点集合(可能存在多个根节点)**
java 复制代码
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) 将子节点挂载到当前节点,形成层级关系**
java 复制代码
// 统计子节点数据
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)=损失量/供应量(避免除以零)

java 复制代码
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
{
    @Autowired
    private SysDeptMapper deptMapper;

    @Autowired
    private SysRoleMapper roleMapper;

    /**
     * 查询部门管理数据
     * 
     * @param dept 部门信息
     * @return 部门信息集合
     */
    @Override
    @DataScope(deptAlias = "d")
    public List<SysDept> selectDeptList(SysDept dept)
    {
        return deptMapper.selectDeptList(dept);
    }

    /**
     * 查询部门树结构信息
     * 
     * @param dept 部门信息
     * @return 部门树信息集合
     */
    @Override
    public List<TreeSelect> selectDeptTreeList(SysDept dept)
    {
        List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);
        return buildDeptTreeSelect(depts);
    }

    /**
     * 构建前端所需要树结构
     * 
     * @param depts 部门列表
     * @return 树结构列表
     */
    @Override
    public 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 下拉树结构列表
     */
    @Override
    public 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 选中部门列表
     */
    @Override
    public List<Long> selectDeptListByRoleId(Long roleId)
    {
        SysRole role = roleMapper.selectRoleById(roleId);
        return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly());
    }

    /**
     * 根据部门ID查询信息
     * 
     * @param deptId 部门ID
     * @return 部门信息
     */
    @Override
    public SysDept selectDeptById(Long deptId)
    {
        return deptMapper.selectDeptById(deptId);
    }

    /**
     * 根据ID查询所有子部门(正常状态)
     * 
     * @param deptId 部门ID
     * @return 子部门数
     */
    @Override
    public int selectNormalChildrenDeptById(Long deptId)
    {
        return deptMapper.selectNormalChildrenDeptById(deptId);
    }

    /**
     * 是否存在子节点
     * 
     * @param deptId 部门ID
     * @return 结果
     */
    @Override
    public boolean hasChildByDeptId(Long deptId)
    {
        int result = deptMapper.hasChildByDeptId(deptId);
        return result > 0;
    }

    /**
     * 查询部门是否存在用户
     * 
     * @param deptId 部门ID
     * @return 结果 true 存在 false 不存在
     */
    @Override
    public boolean checkDeptExistUser(Long deptId)
    {
        int result = deptMapper.checkDeptExistUser(deptId);
        return result > 0;
    }

    /**
     * 校验部门名称是否唯一
     * 
     * @param dept 部门信息
     * @return 结果
     */
    @Override
    public 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
     */
    @Override
    public 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 结果
     */
    @Override
    public 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 结果
     */
    @Override
    public 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 结果
     */
    @Override
    public 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;
    }
}
相关推荐
漂流瓶jz20 小时前
Webpack如何实现万物皆可import?loader的使用/配置/手写实践
前端·javascript·webpack
ZC跨境爬虫20 小时前
跟着 MDN 学CSS day_41:显式轨道、隐式网格与区域命名放置
前端·javascript·css·ui·交互
石山代码20 小时前
ArrayList / HashMap / ConcurrentHashMap
java·开发语言
修己xj21 小时前
告别手动存图!这款叫 Fatkun 的浏览器插件,简直是素材收集神器
前端
袋鼠云数栈21 小时前
从前端到基础设施,ACOS 如何打通企业全链路可观测
运维·前端·人工智能·数据治理·数据智能
AskHarries21 小时前
系统提示词、开发者指令和用户输入的优先级
java·前端·数据库
Moment1 天前
长上下文会最终杀死 Rag 吗?
前端·javascript·后端
daidaidaiyu1 天前
ThingsBoard 规则链系统源码分析和自定义定时器
java
qcx231 天前
【系统学AI】25 论文导读 ①:两篇改变 AI 的开山之作——Attention Is All You Need & ReAct
前端·人工智能·react.js·transformer
小毛驴8501 天前
spring-boot-maven-plugin,maven-compiler-plugin 功能对比
java·python·maven