基于Mybatis的数据权限拦截器实现

news2024/11/17 7:35:59

目录

    • 一、背景
    • 二、动机
    • 三、实现思路
      • 3.1 权限类型、操作类型
      • 3.2 统一用户及数据权限集合模型
      • 3.3 定义数据权限拦截注解
      • 3.4 提取配置属性
      • 3.5 数据权限拦截器实现
    • 四、集成方式
    • 五、关于D3S

一、背景

最近一直在做RBAC相关的架构设计与实现,传统的RBAC的权限控制只是控制到REST接口(url)、具体方法(权限码)等,而通常实际业务场景还需要对数据权限进行控制,例如:

  • 用户仅允许查询自己的用户信息,不允许查询其他人的用户信息
  • 用户仅被允许查询 同部门 或 同部门及子部门 的用户信息
  • 用户仅允许查询指定部门的数据等
  • 用户仅允许查看自己的信息,不允许修改和删除
  • . . . . . .

而对应到具体操作界面上,可以参见【若依】实现的角色数据权限设置
在这里插入图片描述
在这里插入图片描述

在若依的实现中,将数据权限类型划分为:

数据权限类型说明
全部数据权限允许查询所有数据
仅本人数据权限仅允许查询自己的数据
本部门数据权限仅允许查询同部门的数据
本部门及以下数据权限允许查询同部门及子部门下的数据
自定义数据权限允许查询指定部门下的数据

且在若依中每个角色仅对应一种数据权限,而在实际设计RBAC模型时,有可能一个角色可以对应多种数据权限,如下图:
在这里插入图片描述

二、动机

若依框架已经包含了其自身的基于Mybatis的数据权限实现:

  • 基于AOP(DataScopeAspect)实现,在需要数据权限控制的Service实现类方法中标注@DataScope(deptAlias = “d”, userAlias = “u”)注解
    • 拦截登录用户信息
    • 根据用户角色及数据权限信息动态生成数据权限过滤SQL条件
    • 设置数据权限过滤SQL条件到BaseEntity.params["dataScope"]
  • 在Mapper.xml中需要控制数据权限的SQL中添加数据权限条件占位符${params.dataScope}

但是在实际使用过程中,还是会有以下问题:

  • DataScopeAspect中的数据权限条件SQL写死在代码中,无法支持动态配置
  • 支持数据条件的Mapper接口方法参数需要与BaseEntity绑定
  • Mapper.xml中需通过数据权限条件占位符${params.dataScope}才能使用数据权限,若使用Mybatis-Plus内置BaseMapper中的方法,没有对应的xml,则无法设置数据权限条件占位符${params.dataScope}

由于若依的数据权限实现强依赖了若依框架自身(BaseEntity、Mybatis Mapper.xml、固定的数据库设计等),在脱离了若依框架则难以适用,但其实现思想还是值得参考与借鉴的,综上,促使本作者实现了更广泛适用的基于Mybatis数据权限插件实现。

三、实现思路

所谓更广泛适用,即结合本作者日常的架构设计,需:

  • 支持数据权限条件SQL的动态配置(尽可能支持不同数据库设计)
  • 支持Mybatis-Plus内置BaseMapper中的方法
  • 自动附加数据权限SQL条件,同时保留支持在Mapper.xml使用数据权限占位符
  • 数据权限注解需放置在Mapper接口方法上(支持具体到单独Sql的数据权限控制)
  • 数据权限无需依赖Mapper方法中的特定类型参数(如无需依赖BaseEntity)
  • 提供自身统一的用户及数据权限对象抽象,使得不同框架的用户、权限模型都可以进行适配
  • 支持数据权限类型限制(ADMINUSERUSER_CUSTOMDEPTDEPT_AND_CHILDDEPT_CUSTOM
  • 支持数据操作(ALLSELECTINSERTUPDATEDELETE)限制(可限制仅支持特定类型的SQL,如仅支持查询SELECT等)

由于仅需要支持Mapper接口方法上的数据权限控制,且考虑到有修改SQL的需要,故采用了Mybatis拦截器的实现方式。同时Mybatis-Plus框架也提供了数据权限拦截器实现,具体使用可参见:gitee/baomidou/mybatis-plus/issues/I37I90,但考虑到还需要支持原生Mybatis(未使用Mybatis-Plus)实现,故采用了原生的Mybatis拦截器实现。

注:
Mybatis-Plus DataPermissionHandler实现需依赖jsqlparser,
个人感觉没有直接拼接SQL来的方便😅,还有一定学习成本,
如需使用Mybatis-Plus DataPermissionHandler,关于jsqlparser的使用可参见:JSqlParser入门系列

3.1 权限类型、操作类型

首先,定义支持的 数据权限类型枚举操作类型枚举

数据权限类型 用于限制用户的可见数据范围,
操作类型 即限制了用户对可见数据的操作。

数据权限类型 / DpTypeEnum说明
ADMIN管理员数据权限,拥有全部数据的权限
USER用户自身数据权限,仅允许操作自己的数据
USER_CUSTOM自定义用户数据权限,仅允许操作指定用户的数据
DEPT部门数据权限,仅允许操作同部门的数据
DEPT_AND_CHILD部门及子部门数据权限,仅允许操作同部门及子部门的数据
DEPT_CUSTOM自定义部门数据权限,仅允许操作指定部门的数据
/**
 * 数据权限类型枚举
 *
 * @author luohq
 * @date 2023-07-02
 */
public enum DpTypeEnum {
    //管理员数据权限(全部数据)
    ADMIN("1"),
    //用户自身数据权限
    USER("2"),
    //自定义用户数据权限
    USER_CUSTOM("3"),
    //部门数据权限
    DEPT("4"),
    //部门及子部门数据权限
    DEPT_AND_CHILD("5"),
    //自定义部门数据权限
    DEPT_CUSTOM("6");
	......
}
数据操作类型 / DpOpEnum说明
ALL全部操作,允许执行全部类型的SQL语句
SELECT查询操作,仅允许执行SELECT类型的SQL语句
UPDATE修改操作,仅允许执行UPDATE类型的SQL语句
INSERT插入操作,仅允许执行INSERT类型的SQL语句
DELETE删除操作,仅允许执行DELETE类型的SQL语句
/**
 * 数据权限 - 操作 - 枚举
 *
 * @author luohq
 * @date 2023-07-02
 */
public enum DpOpEnum {
    //全部操作
    ALL("1", "ALL"),
    //查询操作
    SELECT("2", "SELECT"),
    //修改操作
    UPDATE("3", "UPDATE"),
    //插入操作
    INSERT("4", "INSERT"),
    //删除操作
    DELETE("5", "DELETE");
	......
}

3.2 统一用户及数据权限集合模型

接下来定义统一的 用户用户所拥有的数据权限及操作集合 抽象:

用户信息属性 / BaseUserDto说明
BaseUserDto.getUserId表示当前用户标识,在拥有数据权限USER时使用
BaseUserDto.getDeptId表示当前用户所在部门标识,在拥有数据权限DEPT时使用
BaseUserDto.getDataPermissions表示当前用户拥有的数据权限集合
BaseUserDto.getDeptAndChildDeptIds表示当前用户所在部门及子部门标识集合,在拥有数据权限DEPT_AND_CHILD时使用,若不需要可为空
BaseUserDto.getAdditionalParams表示附加参数集合,可根据需要自行定义,可用于后续数据权限SQL条件拼接
/**
 * 用户及数据权限集合DTO
 *
 * @author luohq
 * @date 2023-07-02
 */
public interface BaseUserDto {
    /**
     * 获取当前用户ID
     *
     * @return 当前用户ID
     */
    String getUserId();

    /**
     * 获取当前用户所属部门ID
     *
     * @return
     */
    String getDeptId();

    /**
     * 获取当前用户所属的部门及子部门ID集合
     *
     * @return 前用户所属的部门及子部门ID集合
     */
    Collection<String> getDeptAndChildDeptIds();

    /**
     * 获取当前用户数据权限集合
     *
     * @return 当前用户数据权限集合
     */
    Collection<BaseDpDto> getDataPermissions();

    /**
     * 获取其他附加的参数集合
     *
     * @return 附加参数集合
     */
    Map<String, String> getAdditionalParams();
}

数据权限属性 / BaseDpDto说明
BaseDpDto.getType表示数据权限类型(ADMINUSERUSER_CUSTOMDEPTDEPT_AND_CHILDDEPT_CUSTOM
BaseDpDto.getOperations表示当前数据权限类型支持的操作集合(ALLSELECTINSERTUPDATEDELETE),
若BaseDpDto.getOperations返回空或空集合,则表示默认支持所有操作
BaseDpDto.getManageDeptIds表示当前用户管控的部门标识集合,仅在getType为DEPT_CUSTOM时被使用
BaseDpDto.getManageUserIds表示当前用户管控的用户标识集合,仅在getType为USER_CUSTOM时被使用
/**
 * 数据权限DTO
 *
 * @author luohq
 * @date 2023-07-02
 */
public interface BaseDpDto {
    /**
     * 获取数据权限类型
     *
     * @return 数据权限类型
     */
    DpTypeEnum getType();

    /**
     * 获取数据权限支持的操作集合,若操作集合为空则表示默认支持所有操作
     *
     * @return 数据权限支持的操作集合
     */
    Collection<DpOpEnum> getOperations();

    /**
     * 获取当前用户管控部门ID集合
     *
     * @return 用户管控部门ID集合
     */
    Collection<String> getManageDeptIds();

    /**
     * 获取当前用户管控人员ID集合
     *
     * @return 用户管控人员ID集合
     */
    Collection<String> getManageUserIds();
}

数据权限用户上下文实现:

/**
 * 数据权限用户信息上线文容器
 *
 * @author luohq
 * @date 2023-07-02
 */
public class DpUserContextHolder {

    /**
     * 用户ThreadLocal
     */
    private static final ThreadLocal<BaseUserDto> contextHolder = new ThreadLocal<>();


    /**
     * 内部构造函数
     */
    private DpUserContextHolder() {
    }

    /**
     * 清空用户上下文
     */
    public static void clearContext() {
        contextHolder.remove();
    }

    /**
     * 获取当前用户上下文
     *
     * @return 数据权限用户信息
     */
    public static BaseUserDto getContext() {
        return contextHolder.get();
    }

    /**
     * 设置当前用户上下文
     *
     * @param dpUserDto 数据权限用户信息
     */
    public static void setContext(BaseUserDto dpUserDto) {
        Assert.notNull(dpUserDto, "Only non-null DpUser instances are permitted");
        contextHolder.set(dpUserDto);
    }
}

之后凡是需集成该数据权限插件,都需要将各自的用户、权限模型统一转换为当前定义的统一的用户及数据权限集合模型,例如在用户完成鉴权后,可以获取用户及数据权限信息并转换为BaseUserDtoBaseDpDto,然后设置到DpUserContextHolder中。

而如上的统一用户及数据权限集合模型,都会被转换为参数集,后续会被用于数据权限条件SQL的拼接中:

注:
可在后续不同数据权限SQL条件配置中使用 {参数名} 的形式使用对应的参数值,
{deptAlias}{deptIdColumn}等。

/**
 * SQl解析参数DTO
 *
 * @author luohq
 * @date 2023-07-02
 */
public class SqlParseParamDto {
    ......
    public SqlParseParamDto(DataPermission dpAnno, BaseUserDto user, BaseDpDto dp) {
        ......
        //初始参数Map
        this.initParamMap(user);
    }
    
    private void initParamMap(BaseUserDto user) {
        this.paramMap = new HashMap<>(8);

        //基础参数
        paramMap.put("deptAlias", this.deptAlias);
        paramMap.put("deptIdColumn", this.deptIdColumn);
        paramMap.put("userAlias", this.userAlias);
        paramMap.put("userIdColumn", this.userIdColumn);
        paramMap.put("userId", this.userId);
        paramMap.put("deptId", this.deptId);
        paramMap.put("deptAndChildDeptIds", this.idsToString(this.deptAndChildDeptIds, EMPTY));
        paramMap.put("userIdIdWithSingleQuote", this.idsToString(Collections.singleton(this.userId), SINGLE_QUOTE));
        paramMap.put("deptIdWithSingleQuote", this.idsToString(Collections.singleton(this.deptId), SINGLE_QUOTE));
        paramMap.put("deptAndChildDeptIdsWithSingleQuote", this.idsToString(this.deptAndChildDeptIds, SINGLE_QUOTE));
        paramMap.put("userIdIdWithDoubleQuote", this.idsToString(Collections.singleton(this.userId), DOUBLE_QUOTE));
        paramMap.put("deptIdWithDoubleQuote", this.idsToString(Collections.singleton(this.deptId), DOUBLE_QUOTE));
        paramMap.put("deptAndChildDeptIdsWithDoubleQuote", this.idsToString(this.deptAndChildDeptIds, DOUBLE_QUOTE));
        paramMap.put("manageDeptIds", this.idsToString(this.manageDeptIds, EMPTY));
        paramMap.put("manageUserIds", this.idsToString(this.manageDeptIds, EMPTY));
        paramMap.put("manageDeptIdsWithSingleQuote", this.idsToString(this.manageDeptIds, SINGLE_QUOTE));
        paramMap.put("manageUserIdsWithSingleQuote", this.idsToString(this.manageUserIds, SINGLE_QUOTE));
        paramMap.put("manageDeptIdsWithDoubleQuote", this.idsToString(this.manageDeptIds, DOUBLE_QUOTE));
        paramMap.put("manageUserIdsWithDoubleQuote", this.idsToString(this.manageUserIds, DOUBLE_QUOTE));

        //附加参数
        if (null != user.getAdditionalParams() && !user.getAdditionalParams().isEmpty()) {
            this.paramMap.putAll(user.getAdditionalParams());
        }
    }
    ......
}

3.3 定义数据权限拦截注解

定义数据权限拦截注解@DataPermission,即在需要支持数据权限拦截的Mapper接口方法上添加该注解,同时该注解亦可用在接口定义上,用于支持Mybatis-Plus的BaseMapper中的内建方法。

@DataPermission属性说明
@DataPermission.userAlias
@DataPermission.userIdColumn
数据权限拦截器拼接SQL时可根据userAlias、userIdColumn设置用户数据过滤条件的表别名、用户标识列名
@DataPermission.deptAlias
@DataPermission.deptIdColumn
数据权限拦截器拼接SQL时可根据deptAlias、deptIdColumn设置部门数据过滤条件的表别名、部门标识列名
@DataPermission.supportTypes若supportTypes为空,则表示支持所有数据权限类型,
若supportTypes非空,则表示仅支持指定的数据权限类型
@DataPermission.methodName仅当@DataPermission注解在 接口 定义上时需设置此方法名methodName,
此时methodName用于表示父接口如BaseMapper中的方法名selectById,
而当@DataPermission注解在方法上时无需设置此方法名
@DataPermission.defaultAllowAll当用户数据权限集合为空时,默认是否允许查看全部数据(默认false,即不允许)
/**
 * 数据权限注解
 *
 * @author luohq
 * @date 2023-07-02
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(DataPermission.DataPermissions.class)
public @interface DataPermission {
    /**
     * 部门表的别名
     */
    String deptAlias() default "";

    /**
     * 部门表的ID列名
     */
    String deptIdColumn() default "";

    /**
     * 用户表的别名
     */
    String userAlias() default "";

    /**
     * 用户表的ID列名
     */
    String userIdColumn() default "";

    /**
     * 支持的数据权限类型(默认支持所有类型)
     */
    DpTypeEnum[] supportTypes() default {};

    /**
     * 当用户数据权限集合为空时,默认是否允许查看全部数据(默认false,即不允许)
     */
    boolean defaultAllowAll() default false;

    /**
     * 方法名(仅注解在类或接口上时需设置此方法名,而注解在方法上时无需设置此方法名)
     */
    String methodName() default "";



    /**
     * 数据权限组合注解
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface DataPermissions {

        /**
         * 数据权限集合
         */
        DataPermission[] value();
    }
}

3.4 提取配置属性

将不同数据权限类型的SQL条件语句皆提取为配置属性,以便支持不同的数据库设计。

配置属性 / DataPermissionProps
前缀:d3s.data-permission
配置说明
condition-for-user用户权限USER限制条件,
默认:{userAlias}.{userIdColumn} = {userId}
condition-for-user-custom自定义用户权限USER_CUSTOM限制条件,
默认:{userAlias}.{userIdColumn} in ({manageUserIds})
condition-for-dept部门权限DEPT限制条件,
默认:{deptAlias}.{deptIdColumn} = {deptId}
condition-for-dept-and-child部门及子部门权限DEPT_AND_CHILD限制条件,
默认:{deptAlias}.{deptIdColumn} in ({deptAndChildDeptIds})
亦可支持ancestors等类似祖先ID路径的查询条件配置,例如:
{deptAlias}.{deptIdColumn} in (select dept_id from sys_dept where dept_id = {deptId} or find_in_set({deptId}, ancestors))
或者
{deptAlias}.{deptIdColumn} in (select dept_id from sys_dept where dept_id = {deptId} or ancestors like '%,{deptId},%')
condition-for-dept-custom自定义部门权限DEPT_CUSTOM限制条件,
默认:{deptAlias}.{deptIdColumn} in ({manageDeptIds})
default-invalid-id默认无效ID,默认为0
throw-exception-when-no-data-permission没有数据权限时是否抛出DataPermissionException,
true则表示没有数据权限时抛出DataPermissionException,
false则表示没有数据权限时拼接false条件,仅INSERT命令类型会抛出DataPermissionException
/**
 * 数据权限配置属性
 *
 * @author luohq
 * @date 2023-06-21 10:13
 */
@ConfigurationProperties(prefix = DataPermissionProps.PREFIX)
public class DataPermissionProps {

    public static final String PREFIX = "d3s.data-permission";

    /**
     * 用户权限限制条件
     */
    private String conditionForUser = "{userAlias}.{userIdColumn} = {userId}";
    /**
     * 自定义用户权限限制条件
     */
    private String conditionForUserCustom = "{userAlias}.{userIdColumn} in ({manageUserIds})";
    /**
     * 部门权限限制条件
     */
    private String conditionForDept = "{deptAlias}.{deptIdColumn} = {deptId}";
    /**
     * 部门及子部门权限限制条件,例如:
     * <ul>
     *     <li>{deptAlias}.{deptIdColumn} in (select dept_id from sys_dept where dept_id = {deptId} or find_in_set({deptId}, ancestors))</li>
     *     <li>{deptAlias}.{deptIdColumn} in (select dept_id from sys_dept where dept_id = {deptId} or ancestors like '%,{deptId},%')</li>
     *     <li>{deptAlias}.{deptIdColumn} in ({deptAndChildDeptIds})</li>
     * </ul>
     */
    private String conditionForDeptAndChild = "{deptAlias}.{deptIdColumn} in ({deptAndChildDeptIds})";
    /**
     * 自定义部门权限限制条件
     */
    private String conditionForDeptCustom = "{deptAlias}.{deptIdColumn} in ({manageDeptIds})";
    /**
     * 默认无效ID
     */
    private String defaultInvalidId = "0";

    /**
     * 没有数据权限时是否抛出DataPermissionException
     * <ol>
     *     <li>true则表示没有数据权限时抛出DataPermissionException</li>
     *     <li>false则表示没有数据权限时拼接false条件,仅INSERT命令类型会抛出DataPermissionException</li>
     * </ol>
     */
    private Boolean throwExceptionWhenNoDataPermission = true;
    
    ......
}

3.5 数据权限拦截器实现

实现Mybatis拦截器,对Executor接口的query、update方法进行拦截,且需保证数据权限拦截器最后被添加(则最先被执行)。
该数据权限拦截器的主要功能如下:

  • 判断当前被拦截的Mapper接口方法是否标注了@DataPermission注解(又或者Mapper接口上标注了@DataPermission.methodName为对应方法),若标注了@DataPermission注解则启用数据权限拦截
  • 获取DpUserContextHolder上下文中的用户及数据权限信息,根据不同数据权限类型动态生成数据权限SQL限制条件
    • 若数据权限为空 或 不满足操作权限时,则根据d3s.data-permission.throw-exception-when-no-data-permission配置,若配置为true则抛出DataPermissionException,若配置为false,则仅在INSERT操作抛出DataPermissionException,其他操作则拼接false条件
  • 替换原始SQL为拼接了数据权限限制的SQL语句
    • 若原始SQL中存在{DATA_PERMISSION_CONDITION}占位符,则替换此占位符为对应的数据权限SQL限制条件
    • 若原始SQL中不存在{DATA_PERMISSION_CONDITION}占位符,则将对应的数据权限SQL限制条件拼接到原始SQL语句后

数据权限拦截器具体实现代码如下:

/**
 * 数据权限 - Mybatis Interceptor
 *
 * @author luohq
 * @date 2023-06-18
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataPermissionInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(DataPermissionInterceptor.class);

    /**
     * Executor.query()带有cacheKey和boundSql参数的方法的参数个数
     */
    private static final Integer EXECUTOR_QUERY_CACHE_ARGS_COUNT = 6;

    /**
     * 数据权限条件占位符
     */
    private static final String DATA_PERMISSION_CONDITION_PLACEHOLDER = "{DATA_PERMISSION_CONDITION}";

    /**
     * 空白字符正则表达式
     */
    private static final String BLANK_CHAR_REGEX = "[\\t\\n\\r]";
    /**
     * 空格字符串
     */
    private static final String SPACE_STRING = " ";

    /**
     * 缓存Map(mapperMethodId, 对应的@DataPermission注解)
     */
    private Map<String, DataPermission> mapperMethodIdToDpAnnoMap = new ConcurrentHashMap<>();

    /**
     * 数据权限配置属性
     */
    private DataPermissionProps dpProps;

    /**
     * 数据权限类型对应的条件SQL属性
     */
    private Map<DpTypeEnum, Supplier<String>> dpTypeToConditionSqlProplMap = new HashMap<>(6);

    /**
     * 数据权限烂机器 - 构造函数
     *
     * @param dpProps 数据权限配置属性
     */
    public DataPermissionInterceptor(DataPermissionProps dpProps) {
        this.dpProps = dpProps;
        //初始化
        this.init();
    }

    /**
     * 初始数据权限类型对应的条件SQL属性
     */
    private void init() {
        dpTypeToConditionSqlProplMap.put(DpTypeEnum.USER, this.dpProps::getConditionForUser);
        dpTypeToConditionSqlProplMap.put(DpTypeEnum.USER_CUSTOM, this.dpProps::getConditionForUserCustom);
        dpTypeToConditionSqlProplMap.put(DpTypeEnum.DEPT, this.dpProps::getConditionForDept);
        dpTypeToConditionSqlProplMap.put(DpTypeEnum.DEPT_AND_CHILD, this.dpProps::getConditionForDeptAndChild);
        dpTypeToConditionSqlProplMap.put(DpTypeEnum.DEPT_CUSTOM, this.dpProps::getConditionForDeptCustom);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取执行参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        //当前SQL命令类型 - UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        //mapper方法参数
        Object paramObjOfMapperMethod = args[1];
        //获取BoundSql(区分处理2个query方法)
        BoundSql boundSql = EXECUTOR_QUERY_CACHE_ARGS_COUNT.equals(args.length)
                ? (BoundSql) args[EXECUTOR_QUERY_CACHE_ARGS_COUNT - 1]
                : ms.getSqlSource().getBoundSql(paramObjOfMapperMethod);
        //Mapper方法ID,格式: mapper接口名全路径.mapper方法名,例如com.luo.dao.BizMapper.insert
        String mapperMethodId = ms.getId();


        //解析当前执行的Mapper方法
        DataPermission dpAnno = this.parseMapperMethodDpAnno(mapperMethodId);
        //如果没有数据权限注解,则继续执行逻辑
        if (Objects.isNull(dpAnno)) {
            //继续执行逻辑
            return invocation.proceed();
        }

        //判断是否已设置当前用户数据权限上下文
        BaseUserDto dpUser = DpUserContextHolder.getContext();
        if (Objects.isNull(dpUser)) {
            throw new RuntimeException("无法获取当前用户数据权限信息 - 请先执行DpUserContextHolder.setContext(dpUser)方法");
        }

        //提取原SQL
        String oldSql = boundSql.getSql().replaceAll(BLANK_CHAR_REGEX, SPACE_STRING);
        //拼接生成新SQL - 附加数据权限条件
        String newSqlWithDataPermission = this.fillSqlWithDataPermissionCondition(mapperMethodId, oldSql, sqlCommandType, dpAnno, dpUser);
        log.debug("DataPermissionInterceptor[{}] SQL Before Refactoring: {}", mapperMethodId, oldSql);
        log.debug("DataPermissionInterceptor[{}] SQL After  Refactoring: {}", mapperMethodId, newSqlWithDataPermission);

        //替换原SQL
        this.replaceSql(newSqlWithDataPermission, ms, boundSql, invocation);

        //继续执行逻辑
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object o) {
        //获取代理权
        if (o instanceof Executor) {
            //如果是Executor(执行增删改查操作),则拦截下来
            return Plugin.wrap(o, this);
        } else {
            return o;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        //读取mybatis配置文件中属性
    }

    /**
     * 解析Mapper方法上的@DataPermission注解(或者Mapper接口上的@DataPermission注解)
     *
     * @param mapperMethodId mapper方法ID,格式: mapper接口名全路径.mapper方法名,例如com.luo.dao.BizMapper.insert
     * @return Mapper方法对应的@DataPermission注解
     * @throws ClassNotFoundException
     */
    private DataPermission parseMapperMethodDpAnno(String mapperMethodId) throws ClassNotFoundException {
        //优先从缓存中获取mapperMethodId对应的@DataPermission注解
        if (this.mapperMethodIdToDpAnnoMap.containsKey(mapperMethodId)) {
            return this.mapperMethodIdToDpAnnoMap.get(mapperMethodId);
        }


        int lastDotIndex = mapperMethodId.lastIndexOf(".");
        //Mapper接口类
        String mapperClassFromId = mapperMethodId.substring(0, lastDotIndex);
        //Mapper接口方法名
        String mapperMethodNameFromId = mapperMethodId.substring((lastDotIndex + 1));
        //反射Mapper接口类
        Class<?> mapperClass = Class.forName(mapperClassFromId);
        Method[] mapperMethods = mapperClass.getMethods();
        //获取当前执行的mapper方法
        Method mapperMethod = null;
        for (Method method : mapperMethods) {
            String methodName = method.getName();
            //匹配当前执行的mapper方法
            if (this.matchMapperMethod(methodName, mapperMethodNameFromId)) {
                mapperMethod = method;
                break;
            }
        }

        //方法不匹配,则无需拦截
        if (Objects.isNull(mapperMethod)) {
            return null;
        }


        //解析当前方法的DataPermission注解
        DataPermission dpAnnoOfMethod = AnnotatedElementUtils.getMergedAnnotation(mapperMethod, DataPermission.class);
        if (Objects.nonNull(dpAnnoOfMethod)) {
            //缓存mapperMethodId对应的@DataPermission注解
            this.mapperMethodIdToDpAnnoMap.put(mapperMethodId, dpAnnoOfMethod);
            return dpAnnoOfMethod;
        }

        //解析类上的DataPermission注解(Repeatable支持解析多注解)
        Set<DataPermission> dpAnnoSetOfClass = AnnotatedElementUtils.getMergedRepeatableAnnotations(mapperClass, DataPermission.class);
        for (DataPermission dpAnnoOfClass : dpAnnoSetOfClass) {
            //匹配当前执行的mapper方法
            if (Objects.nonNull(dpAnnoOfClass.methodName()) && this.matchMapperMethod(dpAnnoOfClass.methodName(), mapperMethodNameFromId)) {
                //缓存mapperMethodId对应的@DataPermission注解
                this.mapperMethodIdToDpAnnoMap.put(mapperMethodId, dpAnnoOfClass);
                return dpAnnoOfClass;
            }
        }

        //方法上没有@DataPermission则返回null
        return null;
    }

    /**
     * 添加数据权限SQL条件
     *
     * @param mapperMethodId mapper方法ID,格式: mapper接口名全路径.mapper方法名,例如com.luo.dao.BizMapper.insert
     * @param oldSql         原始SQL
     * @param sqlCommandType SQL命令类型
     * @param dpAnno         Mapper方法上的数据权限注解
     * @param dpUser         当前用户数据权限上下文
     * @return
     */
    private String fillSqlWithDataPermissionCondition(String mapperMethodId, String oldSql, SqlCommandType sqlCommandType, DataPermission dpAnno, BaseUserDto dpUser) {
        //若无匹配的数据权限,是否允许查询全部数据
        String defaultAllowAll = String.valueOf(dpAnno.defaultAllowAll());

        //若当前用户没有数据权限
        if (Objects.isNull(dpUser.getDataPermissions()) || dpUser.getDataPermissions().isEmpty()) {
            //根据defaultAllowAll属性,判断允许或不允许查询全部数据
            return this.fillSqlWithFinalDpCondition(mapperMethodId, oldSql, sqlCommandType, defaultAllowAll, dpUser);
        }


        //获取当前用户的数据权限(非空)
        Collection<BaseDpDto> dpCollection = dpUser.getDataPermissions();
        String dpCondition = "";
        //遍历用户拥有的数据权限集合,根据不同权限类型依次拼接数据权限Sql条件
        for (BaseDpDto curDp : dpCollection) {
            DpTypeEnum curDpType = curDp.getType();
            //转换sql填充参数
            SqlParseParamDto sqlParseParamDto = SqlParseParamDto.of(dpAnno, dpUser, curDp);

            //最高权限 - 查询全部
            if (DpTypeEnum.ADMIN.equals(curDpType)) {
                //直接在原SQL上拼接true条件
                return this.fillSqlWithFinalDpCondition(mapperMethodId, oldSql, sqlCommandType, SqlConditionUtils.ALLOW_ALL_CONDITION, dpUser);
            }

            //当前Mapper方法是否支持该数据权限类型和操作类型
            if (this.supportDpTypeAndOperation(dpAnno, curDp, sqlCommandType)) {
                //其他权限 - 拼接条件
                String conditionForCurDpType = this.dpTypeToConditionSqlProplMap.get(curDpType).get();
                conditionForCurDpType = sqlParseParamDto.fillSqlParams(conditionForCurDpType);
                dpCondition = SqlConditionUtils.appendOrCondition(dpCondition, conditionForCurDpType);
            }
        }

        //是否拥有数据权限
        Boolean hasDataPermission = !dpCondition.isEmpty();
        //doCondition = ((dp_condition1) or (dp_condition2))
        dpCondition = hasDataPermission
                //删除前置 OR
                ? dpCondition.substring(SqlConditionUtils.OR_SEPARATOR.length())
                //若无匹配的数据权限类型,默认则不允许查询全部(false)
                : defaultAllowAll;

        //TODO 特殊处理Insert,若dpCondition为空,则抛出数据权限异常
        //TODO 根据commandType限制SQL执行类型 - UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH

        //替换原Sql中的数据权限占位符{DATA_PERMISSION_CONDITION}为dpCondition
        //若不存在数据权限占位符{DATA_PERMISSION_CONDITION},则拼接dpCondition到最后
        return this.fillSqlWithFinalDpCondition(mapperMethodId, oldSql, sqlCommandType, dpCondition, dpUser);
    }

    /**
     * 填充最终的数据权限SQL条件
     * <ol>
     *     <li>替换原Sql中的数据权限占位符{DATA_PERMISSION_CONDITION}为dpCondition</li>
     *     <li>若为默认全部数据权限true,则不拼接数据权限条件</li>
     *     <li>若不存在数据权限占位符{DATA_PERMISSION_CONDITION} 且 不是默认全部数据权限,则拼接dpCondition到最后</li>
     * </ol>
     *
     * @param oldSql      原始SQL
     * @param dpCondition 拼接后的数据权限SQL条件
     * @return 填充后的SQL
     */
    private String fillSqlWithFinalDpCondition(String mapperMethodId, String oldSql, SqlCommandType sqlCommandType, String dpCondition, BaseUserDto dpUser) {
        //若无数据权限,则特殊处理INSERT 或 依照配置抛出DataPermissionException
        if (SqlConditionUtils.isDenyAllCondition(dpCondition)
                && (SqlCommandType.INSERT.equals(sqlCommandType) || this.dpProps.getThrowExceptionWhenNoDataPermission())) {
            throw new DataPermissionException(mapperMethodId, sqlCommandType.name(), dpUser);
        }

        //替换原Sql中的数据权限占位符{DATA_PERMISSION_CONDITION}为:(dp_condition)
        if (oldSql.contains(DATA_PERMISSION_CONDITION_PLACEHOLDER)) {
            return oldSql.replace(DATA_PERMISSION_CONDITION_PLACEHOLDER, SqlConditionUtils.formatBracketConditionWithParams(dpCondition));
        }
        //若为默认全部数据权限,则不拼接数据权限条件
        if (SqlConditionUtils.isAllowAllCondition(dpCondition)) {
            return oldSql;
        }

        //若不存在数据权限占位符{DATA_PERMISSION_CONDITION} 且 不是默认全部数据权限,则拼接数据权限条件
        return SqlConditionUtils.appendWhereAndCondition(oldSql, dpCondition);
    }


    /**
     * 当前Mapper方法是否支持该数据权限类型和操作类型<br/>
     * <ol>
     *     <li>若@DataPermission.supportTypes为空,则表示支持所有权限类型</li>
     *     <li>若BaseDpDto.operations为空,则表示支持所有SqlCommandType</li>
     *     <li>若@DataPermission.supportTypes非空,则需要与BaseDpTo.type进行匹配</li>
     *     <li>若BaseDpDto.operations非空,则需要与当前SqlCommandType进行匹配</li>
     *     <li>若BaseDpDto.operations包含ALL,则表示支持所有SqlCommandType</li>
     * </ol>
     *
     * @param dpAnno         Mapper方法上的数据权限注解
     * @param curDpDto       当前待处理数据权限
     * @param sqlCommandType Sql命令类型
     * @return 是否支持
     */
    private Boolean supportDpTypeAndOperation(DataPermission dpAnno, BaseDpDto curDpDto, SqlCommandType sqlCommandType) {
        //是否支持数据权限类型
        Boolean matchDpType = DpUtils.isEmptyArray(dpAnno.supportTypes())
                ? true
                : Stream.of(dpAnno.supportTypes()).anyMatch(supportDpType -> supportDpType.equals(curDpDto.getType()));
        //是否支持数据权限操作
        Boolean matchDpOperation = DpUtils.isEmptyCollection(curDpDto.getOperations())
                ? true
                : curDpDto.getOperations().stream().anyMatch(operation -> DpOpEnum.ALL.equals(operation) || operation.getSqlCommandType().equals(sqlCommandType.name()));
        return matchDpType && matchDpOperation;
    }

    /**
     * Mapper接口中的方法是否匹配当前Mybatis拦截器拦截到的方法
     *
     * @param curMethodName          mapper中的方法名
     * @param mapperMethodNameFromId 当前Mybatis拦截器拦截到的方法名
     * @return
     */
    private Boolean matchMapperMethod(String curMethodName, String mapperMethodNameFromId) {
        //"_COUNT"兼容PageHelper自定义分页sql
        return curMethodName.equals(mapperMethodNameFromId) || curMethodName.concat("_COUNT").equals(mapperMethodNameFromId);
    }

    /**
     * 替换原SQL
     *
     * @param newSql     新SQL语句
     * @param ms         原MappedStatement
     * @param boundSql   原BoundSql
     * @param invocation Invocation
     */
    private void replaceSql(String newSql, MappedStatement ms, BoundSql boundSql, Invocation invocation) {
        //创建新的BoundSql
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }

        //创建新的MappedStatement
        MappedStatement newMs = newMappedStatement(ms, parameterObject -> newBoundSql);
        Object[] queryArgs = invocation.getArgs();

        //替换参数MappedStatement
        queryArgs[0] = newMs;
        //替换参数BoundSql
        if (EXECUTOR_QUERY_CACHE_ARGS_COUNT.equals(queryArgs.length)) {
            queryArgs[EXECUTOR_QUERY_CACHE_ARGS_COUNT - 1] = newBoundSql;
        }
    }

    /**
     * 创建新的MappedStatement
     *
     * @param ms           原MappedStatement
     * @param newSqlSource 新的SqlSource
     * @return 新的MappedStatement
     */
    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
}

注:
完整的数据权限插件实现可参见:
https://gitee.com/luoex/d3s/tree/master/d3s-extend/d3s-data-permission-mybatis

四、集成方式

maven依赖:

<!-- D3S 数据权限Mybatis扩展依赖 -->
<dependency>
    <groupId>com.luo.d3s</groupId>
    <artifactId>d3s-data-permission-mybatis</artifactId>
</dependency>

在需要支持数据权限的Mapper接口或方法上标注@DataPermission注解,示例SysUserMapper.java代码如下:

/**
 * 用户Mapper
 *
 * @author luohq
 * @date 2023-06-25
 */
@DataPermission(methodName = "selectById", userAlias = "", userIdColumn = "id", supportTypes = {DpTypeEnum.USER})
@DataPermission(methodName = "selectPage", userAlias = "", userIdColumn = "id", deptAlias = "", deptIdColumn = "dept_id", supportTypes = {DpTypeEnum.USER, DpTypeEnum.DEPT_AND_CHILD})
@DataPermission(methodName = "updateById", userAlias = "", userIdColumn = "id", supportTypes = {DpTypeEnum.USER})
@DataPermission(methodName = "deleteById", userAlias = "", userIdColumn = "id", supportTypes = {DpTypeEnum.USER})
@DataPermission(methodName = "deleteBatchIds", userAlias = "", userIdColumn = "id", supportTypes = {DpTypeEnum.USER})
@DataPermission(methodName = "selectList", userAlias = "", userIdColumn = "id", deptAlias = "", deptIdColumn = "dept_id", supportTypes = {DpTypeEnum.USER, DpTypeEnum.DEPT_AND_CHILD})
@DataPermission(methodName = "insert")
public interface SysUserMapper extends BaseMapper<SysUser> {

    /**
     * 根据ID查询用户
     *
     * @param id 用户ID
     * @return 用户
     */
    @DataPermission(userAlias = "", userIdColumn = "id", supportTypes = {DpTypeEnum.USER})
    SysUser findById(Long id);

    /**
     * Mapper接口中的默认方法(default)不走Mybatis拦截器,仅默认方法中调用的具体方法才会走拦截器,
     * 如该方法中的this.selectList()方法,拦截器拦截到的也是selectList()方法,
     * 可通过在Mapper接口类上声明@DataPermission(methodName = "selectList", ...)来指定拦截的具体方法
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @return 分页结果
     */
    default PageInfo<SysUser> findPageWithPageHelper(Integer pageNum, Integer pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<SysUser> sysUsers = this.selectList(Wrappers.emptyWrapper());
        return new PageInfo(sysUsers);
    }

    /**
     * PageHelper - 动态SQL查询
     *
     * @param userName 用户名
     * @return 用户列表
     */
    @DataPermission(userAlias = "", userIdColumn = "id", deptAlias = "", deptIdColumn = "dept_id", supportTypes = {DpTypeEnum.USER, DpTypeEnum.DEPT_AND_CHILD})
    @Select("select * from sys_user where user_name like concat('%', #{userName}, '%')")
    List<SysUser> findPageWithPageHelper_dynamicSql(String userName);

    /**
     * 查询用户列表(Mapper.xml中使用了数据权限SQL条件占位符{DATA_PERMISSION_CONDITION})
     *
     * @param status 帐号状态(0正常 1停用)
     * @param sex    用户性别(0男 1女 2未知)
     * @return 用户列表
     */
    @DataPermission(userAlias = "", userIdColumn = "id", deptAlias = "", deptIdColumn = "dept_id", supportTypes = {DpTypeEnum.USER, DpTypeEnum.DEPT_AND_CHILD})
    List<SysUser> findList_dpConditionPlaceholder(@Param("status") String status, @Param("sex") String sex);

}

示例SysUserMapper对应的SysUserMapper.xml定义如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luo.d3s.ext.dp.sample.dao.SysUserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.luo.d3s.ext.dp.sample.entity.SysUser">
        <id column="id" property="id" />
        <result column="dept_id" property="deptId" />
        <result column="user_name" property="userName" />
        <result column="nick_name" property="nickName" />
        <result column="user_type" property="userType" />
        <result column="email" property="email" />
        <result column="phonenumber" property="phonenumber" />
        <result column="sex" property="sex" />
        <result column="avatar" property="avatar" />
        <result column="password" property="password" />
        <result column="status" property="status" />
        <result column="del_flag" property="delFlag" />
        <result column="login_ip" property="loginIp" />
        <result column="login_date" property="loginDate" />
        <result column="create_by" property="createBy" />
        <result column="create_time" property="createTime" />
        <result column="update_by" property="updateBy" />
        <result column="update_time" property="updateTime" />
        <result column="remark" property="remark" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id,
        dept_id,
        user_name,
        nick_name,
        user_type,
        email,
        phonenumber,
        sex,
        avatar,
        password,
        status,
        del_flag,
        login_ip,
        login_date,
        create_by,
        create_time,
        update_by,
        update_time,
        remark
    </sql>

    <select id="findById" parameterType="Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"></include>
        from sys_user
        <where>
            id = #{id}
        </where>
    </select>

    <select id="findList_dpConditionPlaceholder" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"></include>
        from sys_user
        <where>
            {DATA_PERMISSION_CONDITION}
            <if test="status != null and status != ''">
                and status = #{status}
            </if>
            <if test="sex != null and sex != ''">
                and sex = #{sex}
            </if>
        </where>
    </select>

</mapper>

集成配置如下:

spring:
  # Sql初始化配置
  sql:
    init:
      # 导入h2 table定义
      schema-locations: classpath:h2/schema.sql
      # 导入h2 数据定义
      data-locations: classpath:h2/data.sql
  # 数据库配置
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    # ============================================================
    # ============= 使用H2内存数据库 ================================
    # ============================================================
    driver-class-name: org.h2.Driver
    # 使用h2内存数据(以mysql兼容模式运行)
    url: jdbc:h2:mem:demo-db;MODE=MySQL;DATABASE_TO_LOWER=TRUE
    username: root
    password: 123456
  # 日志级别配置
logging:
  level:
    root: info
    com.luo.d3s: debug



# mybatis-plus配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.luo.d3s.ext.dp.sample.entity
  global-config:
    banner: false
    db-config:
      # 主键类型 - 自增
      id-type: AUTO
  configuration:
    # Mybatis日志实现类 - 使用SLF4J
    # 具体实现可参见包:mybatis.jar/org.apache.ibatis.logging
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
    call-setters-on-nulls: true

d3s:
  data-permission:
    # 没有数据权限时不抛异常,拼接false条件
    throw-exception-when-no-data-permission: false

具体单元测试代码如下:

/**
 * 数据权限测试
 *
 * @author luohq
 * @date 2023-06-23 19:48
 */
@Slf4j
@SpringBootTest
@ContextConfiguration(classes = {TestConfig.class})
@TestMethodOrder(MethodOrderer.MethodName.class)
public class SyUserMapperTest {

    @Resource
    SysUserMapper sysUserMapper;

    @Resource
    private DataPermissionProps dpProps;

    public static final Long USER_ID_WITH_DP_USER = 2L;
    public static final Long USER_ID_WITHOUT_DP_USER = 1L;

    @BeforeEach
    void initDpUserContext() {
        this.mockUser();
    }

    private void mockUser() {
        Collection<BaseDpDto> dpCollection = Arrays.asList(
                DpPropDto.ofType(DpTypeEnum.USER),
                DpPropDto.ofType(DpTypeEnum.DEPT_AND_CHILD),
                DpPropDto.ofTypeAndManageIds(DpTypeEnum.DEPT_CUSTOM, Arrays.asList(100L, 101L, 105L), null)
        );
        //子部门未设置,则默认为{0}
        UserPropDto user = UserPropDto.ofUserDeptAndDataPermissions(USER_ID_WITH_DP_USER, 105L, dpCollection);
        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }

    private void mockUser_withDP_ADMIN() {
        Collection<BaseDpDto> dpCollection = Arrays.asList(
                DpPropDto.ofType(DpTypeEnum.ADMIN)
        );
        //子部门未设置,则默认为{0}
        UserPropDto user = UserPropDto.ofUserDeptAndDataPermissions(USER_ID_WITH_DP_USER, 105L, dpCollection);
        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }

    private void mockUser_withoutAnyDP() {
        Collection<BaseDpDto> dpCollection = Collections.emptySet();

        //子部门未设置,则默认为{0}
        UserPropDto user = UserPropDto.ofUserDeptAndDataPermissions(USER_ID_WITH_DP_USER, 105L, dpCollection);
        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }


    private void mockUser_withDP_DEPT_AND_CHILD() {
        Collection<BaseDpDto> dpCollection = Arrays.asList(
                DpPropDto.ofType(DpTypeEnum.USER),
                DpPropDto.ofType(DpTypeEnum.DEPT_AND_CHILD),
                DpPropDto.ofTypeAndManageIds(DpTypeEnum.DEPT_CUSTOM, Arrays.asList(100L, 101L, 105L), null)
        );
        //设置用户子部门
        Collection<Long> deptAndChildDeptIds = Arrays.asList(101L, 102L, 103L, 104L, 105L, 106L, 107L);
        UserPropDto user = UserPropDto.ofAll(USER_ID_WITH_DP_USER, 105L, deptAndChildDeptIds, dpCollection);
        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }

    void mockUser_withSupplier() {
        Collection<BaseDpDto> dpCollection = Arrays.asList(
                DpPropDto.ofType(DpTypeEnum.USER),
                DpPropDto.ofType(DpTypeEnum.DEPT_AND_CHILD),
                DpPropDto.ofTypeAndManageIds(DpTypeEnum.DEPT_CUSTOM, Arrays.asList(100L, 101L, 105L), null)
        );
        UserPropDto user = UserPropDto.ofUserDeptAndDataPermissions(USER_ID_WITH_DP_USER, 105L, dpCollection);
        UserSupplierDto userSupplier = UserSupplierDto.fromProp(user);
        userSupplier.setDeptAndChildDeptIdsSupplier(() -> {
            String userId = userSupplier.getUserId();
            return Arrays.asList(101L, 102L, 103L, 104L, 105L, 106L, 107L);
        });

        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }

    private void mockUser_withOperations_SELECT() {
        Collection<BaseDpDto> dpCollection = Arrays.asList(
                DpPropDto.ofTypeAndOperations(DpTypeEnum.USER, Arrays.asList(DpOpEnum.SELECT)),
                DpPropDto.ofTypeAndOperations(DpTypeEnum.DEPT_AND_CHILD, Arrays.asList(DpOpEnum.SELECT)),
                DpPropDto.ofAll(DpTypeEnum.DEPT_CUSTOM, Arrays.asList(DpOpEnum.SELECT), Arrays.asList(100L, 101L, 105L), null)
        );
        //子部门未设置,则默认为{0}
        UserPropDto user = UserPropDto.ofUserDeptAndDataPermissions(USER_ID_WITH_DP_USER, 105L, dpCollection);
        //设置数据权限用户上下文
        DpUserContextHolder.setContext(user);
    }

    /**
     * 无数据权限时,是否抛出异常
     *
     * @return 是否抛出异常
     */
    boolean throwExceptionWhenNoDataPermission() {
        return this.dpProps.getThrowExceptionWhenNoDataPermission();
    }

    @RepeatedTest(2)
    void testSysUserMapper0100_findById_withDP_USER() {
        //仅允许查询自己的用户信息
        SysUser sysUser = this.sysUserMapper.findById(USER_ID_WITH_DP_USER);
        log.info("[WITH DP USER] sysUserMapper.findById: {}", sysUser);
        Assertions.assertNotNull(sysUser);
    }

    @RepeatedTest(2)
    void testSysUserMapper0110_findById_withoutDP_USER() {
        //仅允许查询自己的用户信息
        SysUser sysUser = this.sysUserMapper.findById(USER_ID_WITHOUT_DP_USER);
        log.info("[WITHOUT DP USER] sysUserMapper.findById: {}", sysUser);
        Assertions.assertNull(sysUser);
    }


    @RepeatedTest(2)
    void testSysUserMapper0200_selectById_withDP_USER() {
        //仅允许查询自己的用户信息
        SysUser sysUser = this.sysUserMapper.selectById(USER_ID_WITH_DP_USER);
        log.info("[WITH DP USER] sysUserMapper.selectById: {}", sysUser);
        Assertions.assertNotNull(sysUser);
    }

    @RepeatedTest(2)
    void testSysUserMapper0210_selectById_withoutDP_USER() {
        //不允许查询别人的用户信息
        SysUser sysUser = this.sysUserMapper.selectById(USER_ID_WITHOUT_DP_USER);
        log.info("[WITHOUT DP USER] sysUserMapper.selectById: {}", sysUser);
        Assertions.assertNull(sysUser);
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0220_selectById_withoutAnyDP() {
        this.mockUser_withoutAnyDP();

        //不允许查询任何的用户信息
        SysUser sysUser = this.sysUserMapper.selectById(USER_ID_WITH_DP_USER);
        log.info("[WITHOUT ANY DP] sysUserMapper.selectById: {}", sysUser);
        Assertions.assertNull(sysUser);
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0220_selectById_withoutAnyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //不允许查询任何的用户信息
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许查询任何的用户信息
                    SysUser sysUser = this.sysUserMapper.selectById(USER_ID_WITH_DP_USER);
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.selectById: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0300_updateById_withDP_USER() {
        //仅允许更新自己的用户信息
        SysUser sysUser = new SysUser();
        sysUser.setId(USER_ID_WITH_DP_USER);
        sysUser.setUserName("user_haha");
        Integer retCount = this.sysUserMapper.updateById(sysUser);
        log.info("[WITH DP USER] sysUserMapper.updateById result: {}", retCount);
        Assertions.assertEquals(1, retCount);
    }


    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0305_updateById_withOP_SELECT() {
        this.mockUser_withOperations_SELECT();

        //不允许更新操作
        SysUser sysUser = new SysUser();
        sysUser.setId(USER_ID_WITH_DP_USER);
        sysUser.setUserName("user_haha");
        Integer retCount = this.sysUserMapper.updateById(sysUser);
        log.info("[WITH OP SELECT] sysUserMapper.updateById result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0305_updateById_withOP_SELECT_throwEx() {
        this.mockUser_withOperations_SELECT();

        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许更新操作
                    SysUser sysUser = new SysUser();
                    sysUser.setId(USER_ID_WITH_DP_USER);
                    sysUser.setUserName("user_haha");
                    Integer retCount = this.sysUserMapper.updateById(sysUser);
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITH OP SELECT] sysUserMapper.updateById: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0310_updateById_withoutDP_USER() {
        //不允许更新别人的用户信息
        SysUser sysUser = new SysUser();
        sysUser.setId(USER_ID_WITHOUT_DP_USER);
        sysUser.setUserName("user_haha");
        Integer retCount = this.sysUserMapper.updateById(sysUser);
        log.info("[WITHOUT DP USER] sysUserMapper.updateById result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }

    @Disabled
    @Test
    void testSysUserMapper0400_deleteById_withDP_USER() {
        //仅允许删除自己的用户信息
        Integer retCount = this.sysUserMapper.deleteById(USER_ID_WITH_DP_USER);
        log.info("[WITH DP USER] sysUserMapper.deleteById result: {}", retCount);
        Assertions.assertEquals(1, retCount);
    }

    @Test
    void testSysUserMapper0410_deleteById_withoutDP_USER() {
        //不允许删除其他用户的用户信息
        Integer retCount = this.sysUserMapper.deleteById(USER_ID_WITHOUT_DP_USER);
        log.info("[WITHOUT DP USER] sysUserMapper.deleteById result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }
    
    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0415_deleteById_withoutOP_SELECT() {
        this.mockUser_withOperations_SELECT();
        
        //不允许删除操作
        Integer retCount = this.sysUserMapper.deleteById(USER_ID_WITHOUT_DP_USER);
        log.info("[WITH OP SELECT] sysUserMapper.deleteById result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0415_deleteById_withoutOP_SELECT_throwEx() {
        this.mockUser_withOperations_SELECT();

        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class, 
                () -> {
                    //不允许删除操作
                    Integer retCount = this.sysUserMapper.deleteById(USER_ID_WITHOUT_DP_USER);
        });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITH OP SELECT] sysUserMapper.deleteById: {}", dataPermissionException.getMessage(), dataPermissionException);
    }
    
    @Test
    void testSysUserMapper0500_deleteBatchIds_withoutDP_USER() {
        //不允许批量删除其他用户的用户信息
        Integer retCount = this.sysUserMapper.deleteBatchIds(Arrays.asList(USER_ID_WITHOUT_DP_USER));
        log.info("[WITHOUT DP USER] sysUserMapper.deleteBatchIds result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0525_deleteBatchIds_withoutAnyDP() {
        this.mockUser_withoutAnyDP();

        //不允许批量删除任何用户的用户信息
        Integer retCount = this.sysUserMapper.deleteBatchIds(Arrays.asList(USER_ID_WITHOUT_DP_USER));
        log.info("[WITHOUT ANY DP] sysUserMapper.deleteBatchIds result: {}", retCount);
        Assertions.assertEquals(0, retCount);
    }
    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0525_deleteBatchIds_withoutAnyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //没有数据权限
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许批量删除任何用户的用户信息
                    Integer retCount = this.sysUserMapper.deleteBatchIds(Arrays.asList(USER_ID_WITHOUT_DP_USER));
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.deleteBatchIds: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0600_selectPage_withDP_USER() {
        //仅允许查询自己的用户信息
        Page pageParam = Page.of(1, 10);
        OrderItem orderItem = OrderItem.desc("create_time");
        pageParam.addOrder(orderItem);
        Page pageResult = this.sysUserMapper.selectPage(pageParam, Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getStatus, 0));
        log.info("[WITH DP USER] sysUserMapper.selectPage: total={}, pages={}, records={}", pageResult.getTotal(), pageResult.getPages(), pageResult.getRecords());
        Assertions.assertEquals(1, pageResult.getTotal());
        Assertions.assertEquals(1, pageResult.getRecords().size());
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0610_selectPage_withoutAnyDP() {
        this.mockUser_withoutAnyDP();

        //不允许查询任何的用户信息
        Page pageParam = Page.of(1, 10);
        OrderItem orderItem = OrderItem.desc("create_time");
        pageParam.addOrder(orderItem);
        Page pageResult = this.sysUserMapper.selectPage(pageParam, Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getStatus, 0));
        log.info("[WITHOUT ANY DP] sysUserMapper.selectPage: total={}, pages={}, records={}", pageResult.getTotal(), pageResult.getPages(), pageResult.getRecords());
        Assertions.assertEquals(0, pageResult.getTotal());
        Assertions.assertEquals(0, pageResult.getRecords().size());
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0610_selectPage_withoutAnyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //没有数据权限
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许查询任何的用户信息
                    Page pageParam = Page.of(1, 10);
                    OrderItem orderItem = OrderItem.desc("create_time");
                    pageParam.addOrder(orderItem);
                    Page pageResult = this.sysUserMapper.selectPage(pageParam, Wrappers.<SysUser>lambdaQuery()
                            .eq(SysUser::getStatus, 0));
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.selectPage: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0620_selectPage_withDP_ADMIN() {
        //重置用户为超级管理员
        this.mockUser_withDP_ADMIN();

        //允许查询全部的用户信息
        Page pageParam = Page.of(1, 10);
        OrderItem orderItem = OrderItem.desc("create_time");
        pageParam.addOrder(orderItem);
        Page pageResult = this.sysUserMapper.selectPage(pageParam, Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getStatus, 0));
        log.info("[WITH DP ADMIN] sysUserMapper.selectPage: total={}, pages={}, records={}", pageResult.getTotal(), pageResult.getPages(), pageResult.getRecords());
        Assertions.assertEquals(13, pageResult.getTotal());
        Assertions.assertEquals(10, pageResult.getRecords().size());
    }

    @Test
    void testSysUserMapper0630_selectPage_withDP_USER__DEPT_AND_CHILD() {
        //重置用户,该用户设置了部门及子部门
        this.mockUser_withDP_DEPT_AND_CHILD();

        //仅允许查询自己及子部门的用户信息
        Page pageParam = Page.of(1, 10);
        OrderItem orderItem = OrderItem.desc("create_time");
        pageParam.addOrder(orderItem);
        Page pageResult = this.sysUserMapper.selectPage(pageParam, Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getStatus, 0));
        log.info("[WITH DP USER, DEPT_AND_CHILD] sysUserMapper.selectPage: total={}, pages={}, records={}", pageResult.getTotal(), pageResult.getPages(), pageResult.getRecords());
        Assertions.assertEquals(2, pageResult.getTotal());
        Assertions.assertEquals(2, pageResult.getRecords().size());
    }

    @Test
    void testSysUserMapper0700_findPageWithPageHelper_withDP_USER() {
        //仅允许查询自己的用户信息
        PageInfo<SysUser> pageInfo = this.sysUserMapper.findPageWithPageHelper(1, 10);
        log.info("[WITH DP USER] sysUserMapper.findPageWithPageHelper: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(1, pageInfo.getTotal());
        Assertions.assertEquals(1, pageInfo.getList().size());
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0710_findPageWithPageHelper_withoutAndyDP() {
        this.mockUser_withoutAnyDP();

        //不允许查询任何的用户信息
        PageInfo<SysUser> pageInfo = this.sysUserMapper.findPageWithPageHelper(1, 10);
        log.info("[WITHOUT ANY DP] sysUserMapper.findPageWithPageHelper: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(0, pageInfo.getTotal());
        Assertions.assertEquals(0, pageInfo.getList().size());
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0710_findPageWithPageHelper_withoutAndyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //没有数据权限
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许查询任何的用户信息
                    PageInfo<SysUser> pageInfo = this.sysUserMapper.findPageWithPageHelper(1, 10);
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.findPageWithPageHelper: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0720_findPageWithPageHelper_withDP_ADMIN() {
        //重置用户为超级管理员
        this.mockUser_withDP_ADMIN();

        //允许查询全部的用户信息
        PageInfo<SysUser> pageInfo = this.sysUserMapper.findPageWithPageHelper(1, 10);
        log.info("[WITH DP ADMIN] sysUserMapper.findPageWithPageHelper: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(13, pageInfo.getTotal());
        Assertions.assertEquals(10, pageInfo.getList().size());
    }

    @Test
    void testSysUserMapper0730_findPageWithPageHelper_withDP_USER__DEPT_AND_CHILD() {
        //重置用户,该用户设置了部门及子部门
        this.mockUser_withDP_DEPT_AND_CHILD();

        //仅允许查询自己及子部门的用户信息
        PageInfo<SysUser> pageInfo = this.sysUserMapper.findPageWithPageHelper(1, 10);
        log.info("[WITH DP USER, DEPT_AND_CHILD] sysUserMapper.findPageWithPageHelper: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(2, pageInfo.getTotal());
        Assertions.assertEquals(2, pageInfo.getList().size());
    }

    @Test
    void testSysUserMapper0800_findPageWithPageHelper_dynamicSql_withDP_USER() {
        //仅允许查询自己的用户信息
        String userName = "user";
        //开启分页
        PageHelper.startPage(1, 10);
        List<SysUser> userList = this.sysUserMapper.findPageWithPageHelper_dynamicSql(userName);
        PageInfo<SysUser> pageInfo = new PageInfo<>(userList);
        log.info("[WITH DP USER] sysUserMapper.findPageWithPageHelper_dynamicSql: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(1, pageInfo.getTotal());
        Assertions.assertEquals(1, pageInfo.getList().size());
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0810_findPageWithPageHelper_dynamicSql_withoutAnyDP() {
        this.mockUser_withoutAnyDP();

        //仅允许查询自己的用户信息
        String userName = "user";
        //开启分页
        PageHelper.startPage(1, 10);
        List<SysUser> userList = this.sysUserMapper.findPageWithPageHelper_dynamicSql(userName);
        PageInfo<SysUser> pageInfo = new PageInfo<>(userList);
        log.info("[WITHOUT ANY DP] sysUserMapper.findPageWithPageHelper_dynamicSql: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(0, pageInfo.getTotal());
        Assertions.assertEquals(0, pageInfo.getList().size());
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0810_findPageWithPageHelper_dynamicSql_withoutAnyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //没有数据权限
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //仅允许查询自己的用户信息
                    String userName = "user";
                    //开启分页
                    PageHelper.startPage(1, 10);
                    List<SysUser> userList = this.sysUserMapper.findPageWithPageHelper_dynamicSql(userName);
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.findPageWithPageHelper_dynamicSql: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @Test
    void testSysUserMapper0820_findPageWithPageHelper_dynamicSql_withDP_ADMIN() {
        //重置用户为超级管理员
        this.mockUser_withDP_ADMIN();

        //仅允许查询自己的用户信息
        String userName = "user";
        //开启分页
        PageHelper.startPage(1, 10);
        List<SysUser> userList = this.sysUserMapper.findPageWithPageHelper_dynamicSql(userName);
        PageInfo<SysUser> pageInfo = new PageInfo<>(userList);
        log.info("[WITH DP ADMIN] sysUserMapper.findPageWithPageHelper_dynamicSql: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(12, pageInfo.getTotal());
        Assertions.assertEquals(10, pageInfo.getList().size());
    }

    @Test
    void testSysUserMapper0830_findPageWithPageHelper_dynamicSql_withDP_USER__DEPT_AND_CHILD() {
        //重置用户,该用户设置了部门及子部门
        this.mockUser_withDP_DEPT_AND_CHILD();

        //仅允许查询自己的用户信息
        String userName = "user";
        //开启分页
        PageHelper.startPage(1, 10);
        List<SysUser> userList = this.sysUserMapper.findPageWithPageHelper_dynamicSql(userName);
        PageInfo<SysUser> pageInfo = new PageInfo<>(userList);
        log.info("[WITH DP USER, DEPT_AND_CHILD] sysUserMapper.findPageWithPageHelper_dynamicSql: total={}, pages={}, list={}", pageInfo.getTotal(), pageInfo.getPages(), pageInfo.getList());
        Assertions.assertEquals(1, pageInfo.getTotal());
        Assertions.assertEquals(1, pageInfo.getList().size());
    }


    @Test
    void testSysUserMapper0900_findList_dpConditionPlaceholder_withDP_USER() {
        //仅允许查询自己的用户信息
        List<SysUser> sysUsers = this.sysUserMapper.findList_dpConditionPlaceholder("0", "1");
        log.info("[WITH DP USER] sysUserMapper.findList_dpConditionPlaceholder: total={},sysUsers={}", sysUsers.size(), sysUsers);
        Assertions.assertEquals(1, sysUsers.size());
        Assertions.assertEquals(USER_ID_WITH_DP_USER, sysUsers.get(0).getId());
    }

    @Test
    void testSysUserMapper0905_findList_dpConditionPlaceholder_withoutDP_USER() {
        //当前用户和性别参数不匹配,故查询结果为空
        List<SysUser> sysUsers = this.sysUserMapper.findList_dpConditionPlaceholder("0", "0");
        log.info("[WITH DP USER] sysUserMapper.findList_dpConditionPlaceholder: total={},sysUsers={}", sysUsers.size(), sysUsers);
        Assertions.assertEquals(0, sysUsers.size());
    }

    @Test
    @DisabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0910_findList_dpConditionPlaceholder_withoutAnyDP() {
        this.mockUser_withoutAnyDP();

        //不允许查询任何的用户信息
        List<SysUser> sysUsers = this.sysUserMapper.findList_dpConditionPlaceholder("0", "1");
        log.info("[WITHOUT ANY DP] sysUserMapper.findList_dpConditionPlaceholder: total={},sysUsers={}", sysUsers.size(), sysUsers);
        Assertions.assertEquals(0, sysUsers.size());
    }

    @Test
    @EnabledIf("throwExceptionWhenNoDataPermission")
    void testSysUserMapper0910_findList_dpConditionPlaceholder_withoutAnyDP_throwEx() {
        this.mockUser_withoutAnyDP();

        //没有数据权限
        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    //不允许查询任何的用户信息
                    List<SysUser> sysUsers = this.sysUserMapper.findList_dpConditionPlaceholder("0", "1");
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT ANY DP] sysUserMapper.findList_dpConditionPlaceholder: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    @ParameterizedTest
    @CsvSource(value = {
            "1,7",
            "0,6"
    })
    void testSysUserMapper0920_findList_dpConditionPlaceholder_withDP_ADMIN(String sex, Integer userCount) {
        this.mockUser_withDP_ADMIN();

        //许查询任何的用户信息
        List<SysUser> sysUsers = this.sysUserMapper.findList_dpConditionPlaceholder("0", sex);
        log.info("[WITH DP ADMIN] sysUserMapper.findList_dpConditionPlaceholder: total={},sysUsers={}", sysUsers.size(), sysUsers);
        Assertions.assertEquals(userCount, sysUsers.size());
    }

    @Test
    void testSysUserMapper1000_insert_throwEx() {
        this.mockUser_withOperations_SELECT();

        MyBatisSystemException myBatisSystemException = Assertions.assertThrows(
                MyBatisSystemException.class,
                () -> {
                    SysUser sysUser = new SysUser();
                    sysUser.setUserName("haha");
                    sysUser.setNickName("haha");
                    sysUser.setSex("1");
                    sysUser.setPhonenumber("123456789");
                    sysUser.setEmail("luo@email.com");
                    int retCount = this.sysUserMapper.insert(sysUser);
                });
        DataPermissionException dataPermissionException = this.extractDpException(myBatisSystemException);
        log.error("[WITHOUT OP INSERT] sysUserMapper.insert: {}", dataPermissionException.getMessage(), dataPermissionException);
    }

    /**
     * 从MyBatisSystemException提取DataPermissionException
     *
     * @param myBatisSystemException Mybatis系统异常
     * @return 数据权限异常
     */
    private DataPermissionException extractDpException(MyBatisSystemException myBatisSystemException) {
        return Optional.ofNullable(myBatisSystemException)
                //org.apache.ibatis.exceptions.PersistenceException
                .map(MyBatisSystemException::getCause)
                //DataPermissionException
                .map(Throwable::getCause)
                .filter(ex -> ex instanceof DataPermissionException)
                .map(DataPermissionException.class::cast)
                .orElse(null);
    }
}

注:
完整的数据权限插件单元测试可参见:
https://gitee.com/luoex/d3s/tree/master/d3s-extend/d3s-data-permission-mybatis/src/test

五、关于D3S

D3S(DDD with SpringBoot)为本作者使用DDD过程中开发的框架,目前已可公开查看源码,目前笔者正在持续完善该框架,争取打造一个可落地的DDD框架。而本文所讲的d3s-data-permission-mybatis为D3S中的一个扩展组件,此组件亦可独立使用。

D3S源码地址:https://gitee.com/luoex/d3s
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023CCF CAT- 全国算法精英大赛

目录 A Flower B Tree C Best Travel Plans D Hearthstone E Hotpot F Mystery Sailing Challenge G Card H The diameter of a rectangle I Tourist J Mysterious Rune String A Flower B Tree C Best Travel Plans D Hearthstone E Hotpot F Mystery Sailing Challe…

Git标签管理

目录 标签介绍 创建标签 删除标签 推送标签到远程仓库 标签介绍 标签tag&#xff0c;可以理解为是对某次commit 的一个标识&#xff0c;相当于起了一个别名。 比如说有些重要的commit id 难以记住&#xff0c;这时候就可以利用tag给这个commit id 起一个有意义的名字&…

【QT】图形化页面设计

可视化的图形化界面共有三种设计方式&#xff0c;一种是通过可视化来设计界面的方式&#xff1b;一种是代码化的设计方式&#xff1b;最后是混合上面两种的混合界面设计方式。目前我们只考虑通过Designer和代码来设计图形化页面的两种方式。 目录 可视化图形界面设计 图形页…

数字信号的载波传输

从信号传输质量来看&#xff0c;数字系统优于模拟系统。 由于数字基带信号的频谱包含低频成分&#xff0c;而许多重要的通信信道是带通型的&#xff0c;比如无线信道和许多有线信道&#xff0c;这时需要调制成数字频带信号。 数字调制与模拟调制原理基本相似&#xff0c;有调幅…

ChatGLM-6B详细学习实践记录与资料分享

随着年初chatGPT产品的退出和迭代发展&#xff0c;凭借一己之力将大模型带火&#xff0c;国产很多厂商后续也陆续跟进开始投入研发属于自己的大模型产品&#xff0c;在这段时间里面陆陆续续出来了很多不同的产品&#xff0c;比如&#xff1a;文心一言、星火大模型、通义千问、商…

吴恩达AIGC《How Diffusion Models Work》笔记

1. Introduction Midjourney&#xff0c;Stable Diffusion&#xff0c;DALL-E等产品能够仅通过Prompt就能够生成图像。本课程将介绍这些应用背后算法的原理。 课程地址&#xff1a;https://learn.deeplearning.ai/diffusion-models/ 2. Intuition 本小节将介绍扩散模型的基础…

gof23设计模式之代理模型

1. 代理模式 1.1. 概述 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时&#xff0c;访问对象不适合或者不能直接引用目标对象&#xff0c;代理对象作为访问对象和目标对象之间的中介。 Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代…

Kubernetes对象深入学习之一:概览

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 关于《Kubernetes对象深入学习》系列 在client-go的学习和使用过程中&#xff0c;不可避免涉及到对象相关的代码&#xff0c;经常面临一个尴尬的现象&#x…

PCL点云处理之多角度剖面切割(一百九十五)

PCL点云处理之多角度切割点云剖面(一百九十五) 一、算法介绍二、具体实现1.沿法向量方向切割剖面2.沿竖直方向切割剖面3.沿水平方向切割剖面一、算法介绍 点云的剖面往往隐藏着很多有用信息,而且分析更加简单一些,这里自己实现一系列不同角度的点云剖面切割,包括沿着法向量…

车载软件架构 —— 闲聊几句AUTOSAR OS(七)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他人的角度来反对自己。人生在世,最怕的就是把别人的眼光当成自己生活的唯一标…

Framework - AMS

一、概念 Android10&#xff08;API29&#xff09;开始&#xff0c;ActivityTaskManagerService 接管 ActivityManagerService。 二、启动ATMS过程 三、启动APP & 跳转Activity过程 3.1 Activity → ATMS 启动一个 APP 或跳转一个 Activity 都是调用的 startActivity()&a…

数据结构--串的存储结构

数据结构–串的存储结构 串的顺序存储 静态数组实现(定长顺序存储) #define MAXLEN 255 typedef struct {char ch[MAXLEN];int length; }SString;动态数组实现(堆分配存储) typedef struct {char* ch;int length; }HString;int main() {HString S;S.ch (char*)malloc(sizeo…

问题解决:centos7异常关闭后无法开机

前言&#xff1a;主机卡死&#xff0c;直接关了电脑电源&#xff0c;虚拟机中的centos7 产生错误&#xff0c;无法开机 重点是取消挂载。很多文章都提到了xfs_repair /dev/dm-0 , 但是不适用我遇到的情况。 # ls /dev/mapper umount /dev/mapper/centos-root xfs_repair -v -…

[洛谷]B3601 [图论与代数结构 201] 最短路问题_1(负权)(spfa)

SPFA模板啦~ 直接上ACcode: #include<bits/stdc.h> using namespace std; //#define int long long #define inf 2147483647 const int N15e310,M2*N; int dis[N],head[N],cnt; bool vis[N]; int n,m; struct E {int to,w,next; } e[M]; queue<int>q; void add(in…

U-Boot移植 - 1_嵌入式Linux系统移植概述

文章目录 1. 嵌入式Linux系统移植概述2. 实验开发板简介3. U-Boot简介4. NXP uboot测试 1. 嵌入式Linux系统移植概述 Linux 的移植主要包括3部分&#xff1a; 移植「bootloader 代码」&#xff0c; Linux 系统要启动就必须需要一个 bootloader 程序&#xff0c;也就说芯片上电…

【Android Framework (十) 】- ContentProvider

文章目录 知识回顾启动第一个流程initZygote的流程system_serverServiceManagerBinderLauncher的启动AMSservicebinderService 前言源码分析1.使用方法2.ContentProvider实现类。3.使用方法4.注册Observer正文 拓展知识 总结 知识回顾 启动第一个流程init 1&#xff0c;挂载文…

基于eBPF技术的云原生可观测实践

** 基于eBPF技术的云原生可观测实践 ** eBPF技术是Linux内核3.15版本中引入的全新设计&#xff0c;自从2014年发布以来&#xff0c;一直都备受瞩目。在过去几年中&#xff0c;基于eBPF技术的实践和工程落地层出不穷&#xff0c;出现了爆发式的增长。2015年微软、Google、Face…

浏览器里的任意一个请求通过postman生成对应的代码

大多数情况下&#xff0c;我们都是不知道某个网站的get或者post请求以及其他请求&#xff08;比如说PUT请求等&#xff09;是该加哪些headers和cookie才能用代码请求成功&#xff0c;这时就需要下面的操作了。 浏览器里的任意一个请求通过postman生成对应的代码&#xff1a; …

外观模式的学习与使用

1、外观模式的学习 当你在开发软件系统时&#xff0c;系统内部的子系统可能会变得非常复杂&#xff0c;包含了许多相互关联的类和接口。在使用这些子系统时&#xff0c;你可能需要调用多个类和方法才能完成所需的功能。这样的复杂性可能导致代码难以维护、理解和使用。外观模式…

NSQ 实现逻辑探秘

1 什么是 NSQ NSQ 是一个消息队列中间件&#xff0c;用 go 实现&#xff0c;有如下特点&#xff1a; 分布式&#xff1a; 它提供了分布式的、去中心化且没有单点故障的拓扑结构&#xff0c;稳定的消息传输发布保障&#xff0c;能够具有高容错和高可用特性。 易于扩展&#xf…