You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

26 KiB

执法码数据权限过滤使用说明

1. 虚拟单位树(efcode_virtual_unit)数据模型,将一期通过程序生成的执法单位树,改变成为实际存储在数据库中的关系,形成实际上的上下级关系

执法单位设置.png架构图

数据权限过滤的核心依赖是 虚拟单位树(表 efcode_virtual_unit),它构建了一棵与实际部门关联的逻辑组织树,用于控制数据可见范围。

  • 所有的业务相关的表中都加上3个字段
    • bureau_dept_id:执法单位ID(当前用户所在的执法单位的id)=DataFilterUtil.getUserBureauDeptId
    • dept_id:部门ID(当前用户所在的部门id)=DataFilterUtil.getUserDeptId
    • organ_id:组织单位ID (当前用户所在的组织单位id)=DataFilterUtil.getUserOrganId
    • 对应的名称作为可选项,自行决定加不加

1.1 表结构(EfcodeVirtualUnit / EfcodeVirtualUnitVo)

字段 类型 说明
vir_id String 虚拟单位ID(主键,UUID)
vir_is_virtual String 是否是虚拟节点("是"/"否")
vir_parent_id String 父节点ID
vir_name String 节点名称
vir_zone Long 节点类型/层级
vir_ancestors String 祖级ID列表(逗号分隔,包含自身)
vir_display_path String 节点的单位路径(包含自身)
vir_include_statistics String 是否为统计节点("是"/"否")
vir_include_search String 是否为查询节点("是"/"否")
vir_show_at_business String 是否在业务中显示("是"/"否")
vir_domain String
vir_status String 状态
vir_remark String 备注
sys_dept_id String 关联的系统部门ID(sys_user.dept_id)
srz_organization_id String 市认证的组织ID(srz_dept.organizationId)
order_num Long 显示顺序
cust_order_num Long 自定义显示顺序

1.2 关键字段说明

  • vir_ancestors:存储从根到当前节点的完整路径(逗号分隔的 vir_id 列表),用于 FIND_IN_SET 子查询实现快速子孙查找
  • sys_dept_id:将虚拟单位节点与系统用户的 dept_id 关联,是数据过滤的核心匹配字段
  • vir_include_statistics:标记该节点是否参与统计类查询的过滤
  • vir_include_search:标记该节点是否参与搜索类查询的过滤
  • vir_show_at_business:标记该节点是否在业务端可见,用于 getUserBureauDeptId 方法确定用户所属执法单位
  • vir_is_virtual:虚拟节点(如"深圳各区")仅用于组织结构,不关联真实部门

1.3 树结构示例

深圳市(根)
├── ShenZhen_Areas(区)      ← 虚拟节点
│   ├── Area_FuTianQu(福田区)
│   │   ├── 福田区XX执法队      ← sys_dept_id 关联真实部门
│   │   └── ...
│   ├── Area_NanShanQu(南山区)
│   └── ...
└── ShenZhen_ShiZhi(市直)    ← 虚拟节点
    ├── 市XX局                 ← sys_dept_id 关联真实部门
    └── ...

2. 功能概述

执法码数据权限过滤(@EfcodeDataFilter)是一套基于 AOP 切面 + MyBatis-Plus InnerInterceptor 的数据行级过滤方案。它根据用户角色和所在部门,在 SQL 查询的 WHERE 子句中自动追加 bureau_dept_id 过滤条件,实现不同用户看到不同范围的数据。

2.1 过滤规则

用户角色 行为 生成的SQL条件
超级管理员 全部放行 不追加任何条件
ViewAllDataRole 全部放行 不追加任何条件
ViewFilterDataRole 按虚拟单位树过滤 bureau_dept_id IN (子查询) OR bureau_dept_id = '用户部门ID'
其他用户 禁止访问 1 = 0

2.2 核心组件清单

文件 职责
EfcodeDataFilter.java 注解定义
EfcodeDataFilterAdvice.java AOP 方法拦截器(设置/清除 ThreadLocal)
EfcodeDataFilterPointcut.java 切入点匹配器
EfcodeDataFilterPointcutAdvisor.java Advisor 注册器
EfcodeDataFilterHelper.java ThreadLocal 管理器
EfcodeDataFilterInterceptor.java MyBatis InnerInterceptor(修改SQL)
EfcodeDataFilterConfig.java Spring 配置类(注册拦截器+切面)
DataFilterUtil.java 过滤 SQL 构建工具类

所有组件位于:zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/


3. 架构设计

3.1 整体流程

用户请求
    │
    ▼
┌──────────────────┐
│ Service 方法调用  │  ← @EfcodeDataFilter(可选,覆盖整个方法内的所有Mapper调用)
└──────────────────┘
    │
    ▼
┌──────────────────┐
│ AOP 切面拦截      │  EfcodeDataFilterAdvice → setFilter(注解) 到 ThreadLocal
└──────────────────┘
    │
    ▼
┌──────────────────┐
│ MyBatis 执行SQL   │
└──────────────────┘
    │
    ▼
┌────────────────────────────────────────────────────────────────┐
│ MyBatis-Plus 拦截器链(按顺序执行 beforeQuery):               │
│                                                                │
│  1. TenantLineInnerInterceptor     (多租户)                  │
│  2. PlusDataPermissionInterceptor   (通用数据权限)            │
│  3. EfcodeDataFilterInterceptor     (执法码数据过滤)  ← 重点  │
│  4. PaginationInnerInterceptor      (分页:COUNT + LIMIT)    │
│  5. OptimisticLockerInnerInterceptor(乐观锁)                 │
└────────────────────────────────────────────────────────────────┘
    │
    ▼
┌──────────────────┐
│ AOP finally 清除  │  EfcodeDataFilterAdvice → removeFilter()
└──────────────────┘

3.2 拦截器链顺序(关键)

EfcodeDataFilterInterceptor 必须在 PaginationInnerInterceptor 之前

原因:PaginationInnerInterceptorbeforeQuery 中会内部执行 COUNT 查询。如果数据过滤在分页之后,COUNT 查询不会被过滤,导致 total 不正确(如全表10000条但过滤后仅3000条)。

配置由 EfcodeDataFilterConfig.configureEfcodeDataFilter() 自动完成。


4. 使用方式

4.1 方式一:@EfcodeDataFilter 注解(推荐)

4.1.1 标注在 Service 方法上

适用于 Service 方法内调用多个 Mapper 方法的场景(如手动分页需要先 COUNT 再查数据):

@EfcodeDataFilter
@Override
public TableDataInfo<TestEnfDemoVo> queryPageList(TestEnfDemoBo bo, PageQuery pageQuery) {
    LambdaQueryWrapper<TestEnfDemo> lqw = buildQueryWrapper(bo);
    Page<TestEnfDemoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
    return TableDataInfo.build(result);
}

4.1.2 标注在 Mapper 方法上

适用于自定义 XML SQL 的 Mapper 方法:

@EfcodeDataFilter(column = "bureau_dept_id")
Page<TestEnfDemoVo> customPageList(@Param("page") Page<TestEnfDemo> page, 
                                    @Param("ew") Wrapper<TestEnfDemo> wrapper);

4.1.3 注解属性说明

属性 默认值 说明
column "bureau_dept_id" 业务表中用于匹配的列名
tableAlias "" 表别名(SQL 中有别名时使用,如 "t"t.bureau_dept_id
includeSearch true 是否包含虚拟单位树中标记为"查询节点"的数据
includeStatistics true 是否包含虚拟单位树中标记为"统计节点"的数据

4.2 方式二:直接调用 DataFilterUtil(不依赖AOP)

适用于不想使用注解的场景,手动在代码中拼接过滤条件:

@Override
public TableDataInfo<TestEnfDemoVo> directFilterPageList(TestEnfDemoBo bo, PageQuery pageQuery) {
    QueryWrapper<TestEnfDemo> qw = Wrappers.query();
    qw.orderByAsc("id");
    
    // 直接调用工具类生成过滤SQL
    String filterSql = DataFilterUtil.buildFilterSql(true, true);
    if (filterSql != null) {
        qw.apply(filterSql);  // null表示全放行,不追加条件
    }
    
    Page<TestEnfDemoVo> result = baseMapper.selectVoPage(pageQuery.build(), qw);
    return TableDataInfo.build(result);
}

注意DataFilterUtil.buildFilterSql() 返回值:

  • null → 全部放行(超管/ViewAllDataRole),不要追加条件
  • "1 = 0" → 无权访问
  • 其他字符串 → 过滤条件SQL

5. DataFilterUtil 方法列表

DataFilterUtil 是数据过滤的核心工具类,位于 com.zdxt.enforcementcode.common.util 包下,提供权限判断、SQL 构建、子孙节点查询、用户执法单位查询等能力。

5.1 权限判断方法

方法签名 返回值 说明
isFullAccess() boolean 判断当前用户是否全部放行(超管或 ViewAllDataRole)
needFilter() boolean 判断当前用户是否需要数据过滤(拥有 ViewFilterDataRole)
hasNoAccess() boolean 判断当前用户是否无权访问(既非放行也非过滤)
getUserAccessibleDeptIds List<String> 获取当前用户可访问的 sys_dept_id 列表(null=全放行,空=无权)
getFilteredDeptIdsForCurrentUser List<String> 获取 ViewFilterDataRole 用户在虚拟单位树中对应节点及子孙的 sys_dept_id

5.2 子孙节点查询方法

方法签名 返回值 说明
getDescendantDeptIds List<String> 获取指定 virId 下所有子孙节点的 sys_dept_id(已去重)
getDescendantVirIds List<String> 获取指定 virId 下所有子孙节点的 vir_id(已去重)

参数说明:

  • virId:起始节点的虚拟单位ID(如 DataFilterUtil.SHENZHEN_AREAS
  • includeSearch:是否包含 vir_include_search='是' 的节点
  • includeStatistics:是否包含 vir_include_statistics='是' 的节点
  • includeBusiness:是否包含 vir_show_at_business='是' 的节点

5.3 SQL 过滤条件构建方法

方法签名 返回值 说明
buildFilterSql(tableAlias, column, includeSearch, includeStatistics) String 完整版:指定表别名 + 列名
buildFilterSql(tableAlias, includeSearch, includeStatistics) String 默认列名 bureau_dept_id
buildFilterSql(includeSearch, includeStatistics) String 最简版:无别名 + 默认列名

返回值含义:

  • null → 全部放行(超管/ViewAllDataRole),调用方不要追加条件
  • "1 = 0" → 无权访问
  • 其他字符串 → 过滤条件SQL片段,通过 QueryWrapper.apply(filterSql) 追加

5.4 用户执法单位查询方法

方法签名 返回值 说明
getUserDeptId(userId) String 获取用户的 dept_id(来源 sys_user 表)
getUserOrganId(userId) String 查询用户所属执法单位的 organizationId(来源 srz_dept 表,向上递归)
getUserBureauDeptId(userId) String 查询用户所在执法单位的 sys_dept_id(沿 sys_dept 父链向上递归查找 efcode_virtual_unit)

5.4.1 getUserDeptId 获取用户所在的部门ID

通过 UserService.selectListByIds() 查询用户的 dept_id 并返回字符串。

5.4.2 getUserOrganId 获取用户所在的单位ID,有可能和部门ID相同

userId → getUserDeptId → deptId
deptId → srz_dept.organizationId 匹配
    ├─ unitTypeCode=99 → 抛出异常(不合法的机构类型)
    ├─ unitTypeCode=10 → 返回 organizationId(已是执法单位)
    └─ unitTypeCode=11 → 递归查找 parentid,直到 unitTypeCode=10
                          (最大递归深度:4层)

5.4.3 getUserBureauDeptId 获取用户所在的执法单位ID有可能和部门ID相同,单位ID相同

userId → getUserDeptId → deptId
循环(自身 + 最多4层父节点,共5层):
    currentDeptId → efcode_virtual_unit.sys_dept_id 匹配
    ├─ 存在匹配记录
    │   ├─ 筛选 vir_show_at_business='是' 的记录
    │   │   ├─ 恰好1条 → 返回该记录的 sys_dept_id
    │   │   └─ 多条 → 抛出异常
    │   └─ 无 vir_show_at_business='是' 的记录
    │       ├─ 总匹配只有1条 → 返回该记录的 sys_dept_id
    │       └─ 多条 → 抛出异常
    └─ 无匹配记录
        └─ 通过 sys_dept 查找 parentId → currentDeptId = parentId → 继续下一层
到达根节点或超过4层仍未找到 → 返回 null

5.5 常量定义

常量 说明
ROLE_VIEW_ALL_DATA "ViewAllDataRole" 全部放行角色
ROLE_VIEW_FILTER_DATA "ViewFilterDataRole" 需过滤角色
SHENZHEN_AREAS "ShenZhen_Areas" 区(二级节点)
ShenZhen_ShiZhi "ShenZhen_ShiZhi" 深圳市直
AREA_GUANG_MING_QU "Area_GuangMingQu" 光明区
AREA_NAN_SHAN_QU "Area_NanShanQu" 南山区
AREA_PING_SHAN_QU "Area_PingsHanQu" 坪山区
AREA_DA_PENG_XIN_QU "Area_DapengXinQu" 大鹏新区
AREA_BAO_AN_QU "Area_BaoAnQu" 宝安区
AREA_SHEN_SHAN_TE_BIE_HE_ZUO_QU "Area_ShenShanTeBieHeZuoQu" 深汕特别合作区
AREA_YAN_TIAN_QU "Area_YanTianQu" 盐田区
AREA_FU_TIAN_QU "Area_FuTianQu" 福田区
AREA_LUO_HU_QU "Area_LuoHuQu" 罗湖区
AREA_LONG_HUA_QU "Area_LongHuaQu" 龙华区
AREA_LONG_GANG_QU "Area_LongGangQu" 龙岗区

6. ITestEnfDemoService 接口说明

ITestEnfDemoService 是数据过滤功能的完整演示接口,展示了多种分页和过滤方式:

6.1 方法列表

方法 数据过滤方式 分页方式 说明
queryPageList Service 注解 MyBatis-Plus自动分页 最常用方式,注解覆盖 selectVoPage
queryList Service 注解 无分页 列表查询
customPageList Mapper 注解 MyBatis-Plus自动分页 自定义XML SQL + 自动分页拦截器
manualPageList Service 注解 全手动分页 绕过分页拦截器,自己 COUNT + LIMIT
directFilterPageList DataFilterUtil 直接调用 MyBatis-Plus自动分页 不依赖AOP注解
directFilterList DataFilterUtil 直接调用 无分页 不依赖AOP注解

6.2 三种分页方式对比

6.2.1 方式A:MyBatis-Plus 自动分页(queryPageList / customPageList)

Page<TestEnfDemoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
  • 传入 Page 对象 → PaginationInnerInterceptor 自动拦截
  • 自动执行 COUNT 查询获取 total
  • 自动添加 LIMIT 子句

6.2.2 方式B:自定义 XML SQL + 自动分页(customPageList)

Mapper XML:

<select id="customPageList" resultType="...">
    SELECT ... FROM test_enf_demo
    WHERE del_flag = 0
    <if test="ew != null and ew.sqlSegment != null and ew.sqlSegment != ''">
        <if test="ew.nonEmptyOfNormal"> AND </if>
        ${ew.sqlSegment}
    </if>
</select>

注意:自定义 XML SQL 必须手动添加 del_flag = 0,因为 @TableLogic 逻辑删除不会对自定义 SQL 生效。

6.2.3 方式C:全手动分页(manualPageList)

Long total = baseMapper.manualCount(lqw);      // 手动 COUNT
List<TestEnfDemoVo> list = baseMapper.manualSelectList(lqw, offset, pageSize); // 手动 LIMIT
return new TableDataInfo<>(list, total);        // 手动组装
  • 完全绕过 PaginationInnerInterceptor
  • 适合需要精确控制分页行为的场景

7. 自定义 XML SQL 编写规范

当使用 Mapper XML 编写自定义 SQL 时,必须注意以下要点:

7.1 必须手动添加 del_flag 条件

WHERE del_flag = 0

因为 @TableLogic 只对 MyBatis-Plus 内置的 CRUD 方法生效。

7.2 正确处理 Wrapper 条件 + ORDER BY

推荐模式(兼容纯 ORDER BY 无 WHERE 条件的情况):

WHERE del_flag = 0
<if test="ew != null and ew.sqlSegment != null and ew.sqlSegment != ''">
    <if test="ew.nonEmptyOfNormal"> AND </if>
    ${ew.sqlSegment}
</if>

工作原理

  • 当有 WHERE 条件 + ORDER BY 时:nonEmptyOfNormal=true → 输出 AND condition ORDER BY ...
  • 当只有 ORDER BY 时:nonEmptyOfNormal=false → 直接输出 ORDER BY ...(不加 AND)
  • 当都没有时:外层 if 不满足 → 不输出

错误示例(会丢失 ORDER BY):

<!-- ❌ 错误:当只有 ORDER BY 时条件不满足,ORDER BY 被丢失 -->
<if test="ew != null and ew.nonEmptyOfNormal">
    AND ${ew.sqlSegment}
</if>

8. 单元测试说明

8.1 TestEnfDemoServiceTest(数据过滤功能测试)

8.1.1 测试文件

zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java

8.1.2 测试架构

@TestInstance(TestInstance.Lifecycle.PER_CLASS)  // 允许非静态 @BeforeAll
@SpringBootTest(classes = ZdxtAdminApplication.class)
@ActiveProfiles({"dev", "dev-cust-zrb"})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestEnfDemoServiceTest {

8.1.3 数据初始化策略

@BeforeAll 中:

  1. 物理删除 test_enf_demo 表全部数据(通过 JdbcTemplate 绕过 @TableLogic
  2. 批量插入 10000 条已知分布的测试数据:
    • 前 3000 条:bureau_dept_id = 过滤用户部门ID(可见)
    • 后 7000 条:bureau_dept_id = 虚假部门ID(不可见)
  3. 动态查询 过滤用户实际可见数量(考虑虚拟单位树子孙节点匹配)

8.1.4 测试用户角色

用户 userId 角色 预期行为
超级管理员 1 admin 看到全部 10000 条
ViewAllDataRole 2 ViewAllDataRole 看到全部 10000 条
ViewFilterDataRole 3 ViewFilterDataRole 看到约 3000 条(按虚拟单位树过滤)
无权用户 4 common 看到 0 条

8.1.5 测试覆盖(37个测试方法)

序号 测试范围 验证内容
1-4 queryPageList 四种角色的 total 和 records 数量
5-7 queryList 列表查询的数据过滤
8-10 manualPageList 手动分页的数据过滤
11-13 customPageList 自定义XML + Mapper注解的数据过滤
14-17 directFilterPageList DataFilterUtil直接调用方式
18-19 directFilterList 直接调用列表查询
20-21 数据一致性 注解方式 vs 直接调用方式结果一致
22-32 分页功能 翻页、total一致性、过滤用户分页
33-35 综合验证 所有方法在相同角色下结果一致
36-37 尾页/超出页码 最后一页记录数、overflow=true 行为

8.1.6 运行测试

# 运行全部测试
mvn test -pl zdxt-web-server/zdxt-admin-web-server -Dtest="TestEnfDemoServiceTest" -DfailIfNoTests=false

# 运行单个测试方法
mvn test -pl zdxt-web-server/zdxt-admin-web-server -Dtest="TestEnfDemoServiceTest#testQueryPageList_SuperAdmin" -DfailIfNoTests=false

8.1.7 SaToken Mock 上下文

单元测试中需要 Mock SaToken 的上下文环境:

@BeforeEach
public void setUp() {
    SaTokenContextMockUtil.setMockContext();
    MockHttpServletRequest mockRequest = new MockHttpServletRequest();
    mockRequest.addHeader("User-Agent", "...");
    RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest));
}

@AfterEach
public void tearDown() {
    StpUtil.logout();
    RequestContextHolder.resetRequestAttributes();
    SaTokenContextMockUtil.clearContext();
}

8.2 DataFilterUtilTest(工具类方法测试)

8.2.1 测试文件

zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/DataFilterUtilTest.java

8.2.2 测试架构

@SpringBootTest(classes = ZdxtAdminApplication.class)
@ActiveProfiles({"dev", "dev-cust-zrb"})
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DataFilterUtilTest {
  • 每个测试方法前通过 @BeforeEach 模拟 SaToken 登录上下文
  • 测试方法通过 @Order 注解控制执行顺序
  • 连接真实数据库验证,非 Mock 测试

8.2.3 测试覆盖(24个测试方法)

序号 测试方法 验证内容
1 testConstants 所有区域常量值正确性
2 testGetDescendantDeptIds_blankVirId 空/null/空白 virId 返回空列表
3 testGetDescendantDeptIds_shenzhenAreas 深圳各区子孙部门ID查询正确性
4 testGetDescendantDeptIds_guangMingQu 光明区子孙部门ID查询
5 testGetDescendantDeptIds_shiZhi 市直子孙部门ID查询
6 testGetDescendantDeptIds_nonExistentVirId 不存在的virId返回空列表
7 testGetDescendantDeptIds_filterCombinations 不同过滤条件组合对比
8 testGetDescendantVirIds_blankVirId 空/null/空白 virId 返回空列表
9 testGetDescendantVirIds_shenzhenAreas 深圳各区子孙虚拟单位ID查询
10 testGetDescendantVirIds_shiZhi 市直子孙虚拟单位ID查询
11 testGetDescendantVirIds_nonExistentVirId 不存在的virId返回空列表
12 testGetDescendantDeptIds_allAreaConstants 遍历所有区域常量查询子孙部门
13 testGetDescendantVirIds_filterCombinations virId过滤条件组合对比
14 testGetUserBureauDeptId_nullUserId null userId 返回 null
15 testGetUserBureauDeptId_nonExistentUser 不存在的用户返回 null
16 testGetUserBureauDeptId_validUser 有效用户返回执法单位 virId(含异常场景)
17 testGetUserBureauDeptId_multipleUsers 遍历多个用户验证 getUserBureauDeptId
18 testGetUserDeptId_nullUserId null userId 返回 null
19 testGetUserDeptId_nonExistentUser 不存在的用户返回 null
20 testGetUserDeptId_validUser 有效用户返回正确的 deptId
21 testGetUserOrganId_nullUserId null userId 返回 null
22 testGetUserOrganId_nonExistentUser 不存在的用户返回 null
23 testGetUserOrganId_validUser 有效用户返回执法单位 organizationId(含异常场景)
24 testGetUserOrganId_multipleUsers 遍历多个用户验证 getUserOrganId

8.2.4 测试设计要点

  1. 边界值测试:null 入参、不存在的记录、空白字符串
  2. 异常场景覆盖getUserBureauDeptId 多条匹配抛 IllegalStateExceptiongetUserOrganId 中 unitTypeCode=99 或递归超深度
  3. 批量验证:遍历多个真实用户ID,验证方法在不同数据条件下的鲁棒性
  4. 结果断言:返回值不为空白、不为 null(有效用户场景)

8.2.5 运行测试

# 运行 DataFilterUtilTest 全部测试
mvn test -pl zdxt-web-server/zdxt-admin-web-server -Dtest="DataFilterUtilTest" -DfailIfNoTests=false

# 运行单个测试方法
mvn test -pl zdxt-web-server/zdxt-admin-web-server -Dtest="DataFilterUtilTest#testGetUserOrganId_validUser" -DfailIfNoTests=false

9. 注意事项

9.1 分页合理化(overflow=true)

MybatisPlusConfig 中配置了 paginationInnerInterceptor.setOverflow(true)

  • pageNum > totalPages 时,自动回退到第1页
  • 不会返回空结果,而是返回第1页数据

9.2 Service + Mapper 双重注解的注意事项

当 Service 和 Mapper 方法都标注了 @EfcodeDataFilter 时:

  • Mapper 的注解会覆盖 Service 的注解设置
  • 每个 Mapper 方法执行完毕后会清除 ThreadLocal
  • 重要:如果 Service 方法中间调用了没有 @EfcodeDataFilter 的 Mapper 方法,该调用不会被过滤

推荐做法

  • 如果 Service 方法内所有 Mapper 调用都需要过滤:每个 Mapper 方法都加注解
  • 或者只在 Service 层加注解,Mapper 方法不加(前提是 Service 内只有一次 Mapper 调用)

9.3 不同方式的选择建议

场景 推荐方式
常规 CRUD 查询 Service 加 @EfcodeDataFilter
自定义 XML SQL Mapper 方法加 @EfcodeDataFilter
需要精确控制过滤参数 DataFilterUtil.buildFilterSql() 直接调用
Service 内多次 Mapper 调用 Service + 每个 Mapper 都加注解

10. 文件目录结构

zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/
├── common/
│   ├── annotation/
│   │   ├── EfcodeDataFilter.java              # 注解定义
│   │   ├── EfcodeDataFilterAdvice.java        # AOP 方法拦截器
│   │   ├── EfcodeDataFilterPointcut.java      # 切入点匹配器
│   │   ├── EfcodeDataFilterPointcutAdvisor.java # Advisor 注册器
│   │   ├── EfcodeDataFilterHelper.java        # ThreadLocal 管理
│   │   ├── EfcodeDataFilterInterceptor.java   # MyBatis 拦截器
│   │   └── EfcodeDataFilterConfig.java        # Spring 配置
│   └── util/
│       └── DataFilterUtil.java                # 过滤SQL构建工具
├── mapper/
│   └── TestEnfDemoMapper.java                 # Mapper接口(Mapper注解示例)
├── service/
│   ├── ITestEnfDemoService.java               # Service接口
│   └── impl/
│       └── TestEnfDemoServiceImpl.java        # Service实现(Service注解示例)
└── resources/mapper/enforcementcode/
    └── TestEnfDemoMapper.xml                  # 自定义SQL(XML编写规范示例)

zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/
├── TestEnfDemoServiceTest.java                # 数据过滤功能完整测试(37个测试方法)
└── DataFilterUtilTest.java                    # DataFilterUtil 工具类测试(24个测试方法)