目录
- 一、背景
- 二、动机
- 三、实现思路
- 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)
- 提供自身统一的用户及数据权限对象抽象,使得不同框架的用户、权限模型都可以进行适配
- 支持数据权限类型限制(ADMIN、USER、USER_CUSTOM、DEPT、DEPT_AND_CHILD、DEPT_CUSTOM)
- 支持数据操作(ALL、SELECT、INSERT、UPDATE、DELETE)限制(可限制仅支持特定类型的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 | 表示数据权限类型( ADMIN、USER、USER_CUSTOM、DEPT、DEPT_AND_CHILD、DEPT_CUSTOM) | 
| BaseDpDto.getOperations | 表示当前数据权限类型支持的操作集合( ALL、SELECT、INSERT、UPDATE、DELETE),若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);
    }
}
之后凡是需集成该数据权限插件,都需要将各自的用户、权限模型统一转换为当前定义的统一的用户及数据权限集合模型,例如在用户完成鉴权后,可以获取用户及数据权限信息并转换为BaseUserDto和BaseDpDto,然后设置到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语句后
 
- 若原始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
 













![[洛谷]B3601 [图论与代数结构 201] 最短路问题_1(负权)(spfa)](https://img-blog.csdnimg.cn/bb17d50e011141a5a12a23a34d98bb3c.png)





