Ruoyi框架 | 扩展部门数据权限实现

一、背景与目标

在若依框架原有 DataScope 的基础上,实现一套独立的、基于部门层级的数据权限过滤机制,用于按组织结构灵活控制数据可见范围。

设计目标

  • 不依赖角色、不判断是否管理员

  • 通过注解参数动态控制数据范围

  • 支持:

    • 是否包含本部门
    • 向上查询 N 级部门
    • 向下查询 N 级部门 / 所有子部门
  • 与若依原有 BaseEntity + params + MyBatis XML 机制完全兼容


二、核心设计思路

1. 技术方案

  • 使用 AOP + 自定义注解 拦截查询方法

  • 在方法执行前:

    • 根据当前用户部门 ID
    • 动态拼接部门过滤 SQL
    • 注入到 BaseEntity.params.dataScope
  • Mapper XML 中通过 ${params.dataScope} 拼接 WHERE 条件

2. 依赖表结构(sys_dept)

/* by 01130.hk - online tools website : 01130.hk/zh/endecodejs.html */
dept_id     部门ID
parent_id   父部门ID
ancestors   祖先路径,如:0,1,3,10

三、自定义注解:ExtendedDataScope

/* by 01130.hk - online tools website : 01130.hk/zh/endecodejs.html */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExtendedDataScope {

    // 部门表别名(必填)
    String deptAlias() default "";

    // 用户表别名(预留扩展)
    String userAlias() default "";

    // 权限类型(当前主要使用 dept)
    String type() default "dept";

    // 向上级部门层数(0 = 不包含)
    int upLevel() default 0;

    // 向下级部门层数(0 = 不包含,999 = 所有子级)
    int downLevel() default 0;

    // 是否包含本部门
    boolean includeSelf() default true;
}

四、AOP 实现要点

1. 切面职责

  • 拦截所有标注 @ExtendedDataScope 的方法
  • 清空历史 dataScope,防止 SQL 注入
  • 生成部门层级 SQL
  • 写入 BaseEntity.params.dataScope

2. 核心处理流程

@Before
  ├─ clearDataScope()
  ├─ 获取当前用户
  ├─ 读取注解参数
  ├─ 构建部门范围 SQL
  └─ 写入 params.dataScope
点击查看代码
@Aspect
@Component
public class ExtendedDataScopeAspect {

    /**
     * 参数 key(和若依一致)
     */
    public static final String DATA_SCOPE = "dataScope";

    @Before("@annotation(dataScope)")
    public void doBefore(JoinPoint joinPoint, ExtendedDataScope dataScope) {
        clearDataScope(joinPoint);
        handleDataScope(joinPoint, dataScope);
    }

    private void handleDataScope(JoinPoint joinPoint, ExtendedDataScope dataScope) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (loginUser == null) {
            return;
        }

        String deptAlias = dataScope.deptAlias();
        if (StringUtils.isBlank(deptAlias)) {
            return;
        }

        String sql = buildDeptScopeSql(loginUser.getDeptId(), deptAlias, dataScope);
        if (StringUtils.isBlank(sql)) {
            return;
        }

        Object params = joinPoint.getArgs()[0];
        if (params instanceof BaseEntity) {
            BaseEntity baseEntity = (BaseEntity) params;
            baseEntity.getParams().put(DATA_SCOPE, " AND (" + sql + ")");
        }
    }

    /**
     * 构建部门层级 SQL
     */
    private String buildDeptScopeSql(Long deptId, String deptAlias, ExtendedDataScope scope) {
        List<String> conditions = new ArrayList<>();

        /* ========== 本部门 ========== */
        if (scope.includeSelf()) {
            conditions.add(deptAlias + ".dept_id = " + deptId);
        }

        /* ========== 向上 ========== */
        if (scope.upLevel() > 0) {
            // ancestors 形如:0,1,3,10
            // 向上 N 级:取 ancestors 中倒数 N 位
            conditions.add(buildUpDeptSql(deptAlias, deptId, scope.upLevel()));
        }

        /* ========== 向下 ========== */
        if (scope.downLevel() > 0) {
            if (scope.downLevel() >= 999) {
                // 所有子级
                conditions.add(
                        deptAlias + ".dept_id IN (" +
                                "SELECT dept_id FROM sys_dept " +
                                "WHERE find_in_set(" + deptId + ", ancestors)" +
                                ")"
                );
            } else {
                conditions.add(buildDownDeptSql(deptAlias, deptId, scope.downLevel()));
            }
        }

        return String.join(" OR ", conditions);
    }

    /**
     * 向上 N 级部门
     */
    private String buildUpDeptSql(String deptAlias, Long deptId, int upLevel) {
        // 使用子查询,取 ancestors 中的上级
        return deptAlias + ".dept_id IN (" +
                " SELECT t.dept_id FROM sys_dept t " +
                " WHERE t.dept_id IN ( " +
                "   SELECT SUBSTRING_INDEX(" +
                "     SUBSTRING_INDEX(d.ancestors, ',', -( " + upLevel + " + 1)), ',', 1" +
                "   ) FROM sys_dept d WHERE d.dept_id = " + deptId +
                " )" +
                ")";
    }

    /**
     * 向下 N 级部门
     */
    private String buildDownDeptSql(String deptAlias, Long deptId, int downLevel) {
        // ancestors 深度控制(当前 depth + N)
        return deptAlias + ".dept_id IN (" +
                " SELECT d.dept_id FROM sys_dept d " +
                " WHERE find_in_set(" + deptId + ", d.ancestors) " +
                " AND (LENGTH(d.ancestors) - LENGTH(REPLACE(d.ancestors, ',', ''))) <= " +
                "     ( " +
                "       SELECT (LENGTH(ancestors) - LENGTH(REPLACE(ancestors, ',', ''))) + " + downLevel +
                "       FROM sys_dept WHERE dept_id = " + deptId +
                "     )" +
                ")";
    }

    /**
     * 清空 dataScope,防止 SQL 注入
     */
    private void clearDataScope(JoinPoint joinPoint) {
        Object params = joinPoint.getArgs()[0];
        if (params instanceof BaseEntity) {
            ((BaseEntity) params).getParams().put(DATA_SCOPE, "");
        }
    }
}


五、部门层级 SQL 构建规则

1. 本部门

d.dept_id = {currentDeptId}

includeSelf = true 控制


2. 向上 N 级部门(upLevel)

原理:

  • 利用 ancestors 字段
  • 从 ancestors 中向前截取 N 个父级

示意 SQL:

d.dept_id IN (
  SELECT SUBSTRING_INDEX(
    SUBSTRING_INDEX(ancestors, ',', -(N + 1)), ',', 1
  )
  FROM sys_dept
  WHERE dept_id = 当前部门ID
)

3. 向下 N 级部门(downLevel)

3.1 所有子级(downLevel = 999)
d.dept_id IN (
  SELECT dept_id FROM sys_dept
  WHERE find_in_set(当前部门ID, ancestors)
)
3.2 限定层级子部门

思路:

  • 计算 ancestors 的层级深度(逗号个数)
  • 控制最大深度 = 当前深度 + N
d.dept_id IN (
  SELECT d.dept_id
  FROM sys_dept d
  WHERE find_in_set(当前部门ID, d.ancestors)
    AND 层级深度(d) <= 当前层级 + N
)

六、Mapper XML 使用方式

<select id="selectList" resultType="xxx">
  SELECT *
  FROM biz_table t
  LEFT JOIN sys_dept d ON t.dept_id = d.dept_id
  <where>
    1 = 1
    ${params.dataScope}
  </where>
</select>

说明:

  • ${params.dataScope} 必须保留
  • AOP 动态注入 AND ( ... )

七、使用示例

@ExtendedDataScope(
    deptAlias = "d",
    includeSelf = true,
    upLevel = 1,
    downLevel = 2
)
public List<SysDept> selectDeptList(SysDept dept)
{
    return deptMapper.selectDeptList(dept);
}

含义说明

参数含义
includeSelf包含本部门
upLevel=1包含上一级部门
downLevel=2包含下两级部门
downLevel=999包含所有子部门

八、方案特点总结

  • ✅ 与若依原生 DataScope 解耦
  • ✅ 仅依赖部门层级,不依赖角色权限
  • ✅ 控制粒度细,适合复杂组织结构
  • ✅ 非侵入式,Mapper 无需改动
  • ✅ 特别适合安全监管 / GIS / 组织树场景

九、可扩展方向(后续优化)

  • 使用 MySQL 8 / PostgreSQL 的 WITH RECURSIVE 优化层级查询
  • 部门层级缓存(Redis)减少子查询
  • 扩展到:部门 + 用户混合数据权限
  • 支持多部门归属(兼职部门)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值