Skip to content

部门数据权限控制方案 - 物化路径+正则表达式

一、方案概述

本方案使用物化路径(Materialized Path)结合MySQL正则表达式实现高性能的部门数据权限控制,相比FIND_IN_SET性能提升5-10倍。

核心特点

  • ancestors包含自身ID:如部门3的路径为 0,1,2,3
  • 使用正则表达式匹配:避免FIND_IN_SET的性能瓶颈
  • 最小化改动:完全兼容RuoYi框架

二、数据结构设计

2.1 表结构

sql
-- 部门表核心字段
CREATE TABLE sys_dept (
    dept_id BIGINT PRIMARY KEY,
    parent_id BIGINT DEFAULT 0,
    ancestors VARCHAR(500) COMMENT '祖级列表(含自身),如:0,1,2,3',
    dept_name VARCHAR(50),
    -- 添加索引优化查询
    INDEX idx_ancestors (ancestors)
);

2.2 数据示例

dept_iddept_nameparent_idancestors
1总公司00,1
100深圳分公司10,1,100
200研发部1000,1,100,200
300前端组2000,1,100,200,300

三、核心实现

3.1 Service层 - 维护ancestors

java
@Service
public class SysDeptServiceImpl implements ISysDeptService {
    
    @Override
    @Transactional
    public int insertDept(SysDept dept) {
        // 先插入获取ID
        deptMapper.insertDept(dept);
        
        // 构建包含自身的祖级路径
        if (dept.getParentId() == 0) {
            dept.setAncestors("0," + dept.getDeptId());
        } else {
            SysDept parentDept = deptMapper.selectDeptById(dept.getParentId());
            dept.setAncestors(parentDept.getAncestors() + "," + dept.getDeptId());
        }
        
        // 更新ancestors
        return deptMapper.updateDept(dept);
    }
}

3.2 数据权限AOP - 核心逻辑

java
@Aspect
@Component
public class DataScopeAspect {
    
    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, 
                                     String deptAlias, String userAlias) {
        StringBuilder sqlString = new StringBuilder();
        
        for (SysRole role : user.getRoles()) {
            String dataScope = role.getDataScope();
            
            if ("4".equals(dataScope)) { // 本部门及以下
                // 核心SQL:使用正则表达式匹配
                sqlString.append(String.format(
                    " OR %s.dept_id IN (" +
                    "   SELECT dept_id FROM sys_dept " +
                    "   WHERE CONCAT(',', ancestors, ',') REGEXP ',%d,'" +
                    " )",
                    deptAlias, user.getDeptId()
                ));
            }
            // ... 其他数据权限类型
        }
        
        // 注入SQL条件
        BaseEntity baseEntity = (BaseEntity) joinPoint.getArgs()[0];
        baseEntity.getParams().put("dataScope", " AND (" + sqlString.substring(4) + ")");
    }
}

3.3 Mapper XML - 查询配置

xml
<!-- 查询部门及所有子部门 -->
<select id="selectDeptAndChildren" resultType="SysDept">
    SELECT * FROM sys_dept 
    WHERE CONCAT(',', ancestors, ',') REGEXP CONCAT(',', #{deptId}, ',')
    ORDER BY ancestors
</select>

<!-- 业务查询示例 -->
<select id="selectBusinessList" parameterType="BusinessData" resultType="BusinessData">
    SELECT b.*, d.dept_name, u.nick_name
    FROM business_data b
    LEFT JOIN sys_dept d ON b.dept_id = d.dept_id
    LEFT JOIN sys_user u ON b.create_by = u.user_id
    <where>
        <!-- 业务条件 -->
        <if test="title != null">
            AND b.title LIKE CONCAT('%', #{title}, '%')
        </if>
        <!-- 数据权限过滤 -->
        ${params.dataScope}
    </where>
</select>

四、性能优化策略

4.1 索引优化

sql
-- 1. ancestors前缀索引
ALTER TABLE sys_dept ADD INDEX idx_ancestors_prefix (ancestors(100));

-- 2. 联合索引
ALTER TABLE sys_dept ADD INDEX idx_dept_ancestors (dept_id, ancestors);

4.2 查询优化

使用EXISTS优化大数据量查询

sql
-- 原查询
AND dept_id IN (SELECT dept_id FROM sys_dept WHERE ...)

-- 优化后
AND EXISTS (
    SELECT 1 FROM sys_dept d 
    WHERE d.dept_id = t.dept_id 
    AND CONCAT(',', d.ancestors, ',') REGEXP ',100,'
)

批量部门权限查询

sql
-- 查询多个部门的所有子部门
SELECT dept_id FROM sys_dept 
WHERE CONCAT(',', ancestors, ',') REGEXP ',(100|200|300),'

4.3 缓存策略

java
@Service
public class DeptCacheService {
    
    @Cacheable(value = "dept:children", key = "#deptId")
    public Set<Long> getDeptAndChildrenIds(Long deptId) {
        String sql = "SELECT dept_id FROM sys_dept " +
                    "WHERE CONCAT(',', ancestors, ',') REGEXP CONCAT(',', ?, ',')";
        return new HashSet<>(jdbcTemplate.queryForList(sql, Long.class, deptId));
    }
    
    @CacheEvict(value = "dept:children", allEntries = true)
    public void clearCache() {
        // 部门变更时清除缓存
    }
}

五、数据迁移方案

5.1 从原RuoYi格式迁移

sql
-- 原格式:ancestors = "0,1,2" (不含自身)
-- 新格式:ancestors = "0,1,2,3" (包含自身)

UPDATE sys_dept 
SET ancestors = CASE 
    WHEN ancestors = '' OR ancestors = '0' 
        THEN CONCAT('0,', dept_id)
    ELSE 
        CONCAT(ancestors, ',', dept_id)
END
WHERE ancestors NOT LIKE CONCAT('%,', dept_id);

5.2 数据完整性检查

sql
-- 检查ancestors是否包含自身ID
SELECT dept_id, dept_name, ancestors 
FROM sys_dept 
WHERE ancestors NOT REGEXP CONCAT(',', dept_id, '$');

-- 检查ancestors路径完整性
SELECT d1.dept_id, d1.dept_name
FROM sys_dept d1
WHERE d1.parent_id > 0
  AND NOT EXISTS (
    SELECT 1 FROM sys_dept d2 
    WHERE d2.dept_id = d1.parent_id 
    AND d1.ancestors LIKE CONCAT(d2.ancestors, '%')
);