From 4cab996e5d86314fa31f948934397156c9fda07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=94=90=E6=B3=A2?= <10549902+zhuangruibo@user.noreply.gitee.com> Date: Thu, 14 May 2026 22:30:57 +0800 Subject: [PATCH] =?UTF-8?q?=E8=99=9A=E6=8B=9F=E5=8D=95=E4=BD=8D=E9=87=8D?= =?UTF-8?q?=E7=BB=84=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1001-修改用于测试的账号密码.sql | 89 ----- .../1001-生成测试数据.sql | 107 ++++++ .../common/annotation/EfcodeDataFilter.java | 49 +++ .../annotation/EfcodeDataFilterAdvice.java | 48 +++ .../annotation/EfcodeDataFilterConfig.java | 37 ++ .../annotation/EfcodeDataFilterHelper.java | 38 +++ .../EfcodeDataFilterInterceptor.java | 161 +++++++++ .../annotation/EfcodeDataFilterPointcut.java | 31 ++ .../EfcodeDataFilterPointcutAdvisor.java | 33 ++ .../common/util/DataFilterUtil.java | 124 +++++++ .../enforcementcode/domain/TestEnfDemo.java | 5 + .../mapper/TestEnfDemoMapper.java | 4 + .../service/IEfcodeVirtualUnitService.java | 8 + .../impl/EfcodeVirtualUnitServiceImpl.java | 16 + .../service/impl/TestEnfDemoServiceImpl.java | 9 +- .../service/test/TestEnfDemoServiceTest.java | 316 ++++++++++++++++++ 16 files changed, 984 insertions(+), 91 deletions(-) delete mode 100644 script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql create mode 100644 script/sql/zfjd-refactoring-v1/1001-生成测试数据.sql create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilter.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterAdvice.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterConfig.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterHelper.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterInterceptor.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcut.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcutAdvisor.java create mode 100644 zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java diff --git a/script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql b/script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql deleted file mode 100644 index b440bfef..00000000 --- a/script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql +++ /dev/null @@ -1,89 +0,0 @@ ---测试表 -create table test_enf_demo -( - id bigint not null comment '主键' - primary key, - enf_text varchar(255) null comment '文本', - enf_content longtext null comment '长文本', - tenant_id varchar(20) default '000000' null comment '租户编号', - dept_id bigint null comment '部门id', - user_id bigint null comment '用户id', - order_num int default 0 null comment '排序号', - test_key varchar(255) null comment 'key键', - value varchar(255) null comment '值', - version int default 0 null comment '版本', - create_dept varchar(100) null comment '创建部门', - create_time datetime null comment '创建时间', - create_by bigint null comment '创建人', - update_time datetime null comment '更新时间', - update_by bigint null comment '更新人', - del_flag int default 0 null comment '删除标志' -) - comment 'enf测试单表'; - - --- 为 test_enf_demo 表生成 10000 条测试数据 --- 使用存储过程批量插入 - -DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; - -DELIMITER $$ - -CREATE PROCEDURE generate_test_enf_demo_data() -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE batch_size INT DEFAULT 500; - DECLARE current_batch INT DEFAULT 0; - - -- 关闭自动提交,提升插入性能 - SET autocommit = 0; - - WHILE i <= 10000 DO - INSERT INTO test_enf_demo ( - id, enf_text, enf_content, tenant_id, dept_id, user_id, - order_num, test_key, value, version, - create_dept, create_time, create_by, update_time, update_by, del_flag - ) VALUES ( - i, - CONCAT('文本_', i), - CONCAT('这是第', i, '条测试长文本内容,用于测试数据填充。'), - '000000', - 100 + FLOOR(RAND() * 50), - 1000 + FLOOR(RAND() * 500), - i, - CONCAT('KEY_', LPAD(i, 5, '0')), - CONCAT('VAL_', LPAD(i, 5, '0')), - 0, - CONCAT('部门_', FLOOR(RAND() * 20)), - DATE_ADD('2024-01-01', INTERVAL FLOOR(RAND() * 730) DAY), - 1, - NOW(), - 1, - 0 - ); - - -- 每 500 条提交一次 - SET current_batch = current_batch + 1; - IF current_batch >= batch_size THEN - COMMIT; - SET current_batch = 0; -END IF; - - SET i = i + 1; -END WHILE; - - -- 提交剩余数据 -COMMIT; -SET autocommit = 1; -END$$ - -DELIMITER ; - --- 执行存储过程 -CALL generate_test_enf_demo_data(); - --- 清理存储过程 -DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; - --- 验证数据量 -SELECT COUNT(*) AS total_count FROM test_enf_demo; diff --git a/script/sql/zfjd-refactoring-v1/1001-生成测试数据.sql b/script/sql/zfjd-refactoring-v1/1001-生成测试数据.sql new file mode 100644 index 00000000..05755e80 --- /dev/null +++ b/script/sql/zfjd-refactoring-v1/1001-生成测试数据.sql @@ -0,0 +1,107 @@ +--测试表 +create table test_enf_demo +( + id bigint not null comment '主键' + primary key, + enf_text varchar(255) null comment '文本', + enf_content longtext null comment '长文本', + tenant_id varchar(20) default '000000' null comment '租户编号', + dept_id bigint null comment '部门id', + user_id bigint null comment '用户id', + order_num int default 0 null comment '排序号', + test_key varchar(255) null comment 'key键', + value varchar(255) null comment '值', + version int default 0 null comment '版本', + create_dept varchar(100) null comment '创建部门', + bureau_dept_id varchar(100) null comment '执法部门ID', + create_time datetime null comment '创建时间', + create_by bigint null comment '创建人', + update_time datetime null comment '更新时间', + update_by bigint null comment '更新人', + del_flag int default 0 null comment '删除标志' +) comment 'enf测试单表'; + + +-- 为 test_enf_demo 表生成 10000 条测试数据 +-- 使用存储过程批量插入 +-- 为 test_enf_demo 表生成 100000 条测试数据 +-- bureau_dept_id 从 efcode_virtual_unit 表的 sys_dept_id 中随机选取 + +DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; + +DELIMITER $$ + +CREATE PROCEDURE generate_test_enf_demo_data() +BEGIN + DECLARE i INT DEFAULT 1; + DECLARE batch_size INT DEFAULT 1000; + DECLARE current_batch INT DEFAULT 0; + DECLARE v_dept_count INT DEFAULT 0; + + -- 创建临时表缓存 sys_dept_id 列表,避免每次子查询 + DROP TEMPORARY TABLE IF EXISTS tmp_dept_ids; + CREATE TEMPORARY TABLE tmp_dept_ids ( + row_num INT AUTO_INCREMENT PRIMARY KEY, + sys_dept_id VARCHAR(100) + ); +INSERT INTO tmp_dept_ids (sys_dept_id) +SELECT sys_dept_id FROM efcode_virtual_unit WHERE sys_dept_id IS NOT NULL; + +SELECT COUNT(*) INTO v_dept_count FROM tmp_dept_ids; + +-- 关闭自动提交,提升插入性能 +SET autocommit = 0; + + WHILE i <= 100000 DO + INSERT INTO test_enf_demo ( + id, enf_text, enf_content, tenant_id, dept_id, user_id, + order_num, test_key, value, version, + create_dept, bureau_dept_id, create_time, create_by, update_time, update_by, del_flag + ) VALUES ( + i, + CONCAT('文本_', i), + CONCAT('这是第', i, '条测试长文本内容,用于测试数据填充。'), + '000000', + 100 + FLOOR(RAND() * 50), + 1000 + FLOOR(RAND() * 500), + i, + CONCAT('KEY_', LPAD(i, 5, '0')), + CONCAT('VAL_', LPAD(i, 5, '0')), + 0, + CONCAT('部门_', FLOOR(RAND() * 20)), + (SELECT sys_dept_id FROM tmp_dept_ids WHERE row_num = 1 + FLOOR(RAND() * v_dept_count) LIMIT 1), + DATE_ADD('2024-01-01', INTERVAL FLOOR(RAND() * 730) DAY), + 1, + NOW(), + 1, + 0 + ); + + -- 每 1000 条提交一次 + SET current_batch = current_batch + 1; + IF current_batch >= batch_size THEN + COMMIT; + SET current_batch = 0; +END IF; + + SET i = i + 1; +END WHILE; + + -- 提交剩余数据 +COMMIT; +SET autocommit = 1; + + -- 清理临时表 + DROP TEMPORARY TABLE IF EXISTS tmp_dept_ids; +END$$ + +DELIMITER ; + +-- 执行存储过程 +CALL generate_test_enf_demo_data(); + +-- 清理存储过程 +DROP PROCEDURE IF EXISTS generate_test_enf_demo_data; + +-- 验证数据量 +SELECT COUNT(*) AS total_count FROM test_enf_demo; diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilter.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilter.java new file mode 100644 index 00000000..0f63bdde --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilter.java @@ -0,0 +1,49 @@ +package com.zdxt.enforcementcode.common.annotation; + +import java.lang.annotation.*; + +/** + * 执法码数据权限过滤注解。 + *

+ * 标注在 Mapper 方法或 Mapper 类上,声明该查询需要按虚拟单位树进行数据过滤。 + *

+ * 过滤规则: + *

+ * + * @author zrb + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EfcodeDataFilter { + + /** + * 是否包含统计节点(efcode_virtual_unit.vir_include_statistics = '是') + */ + boolean includeStatistics() default true; + + /** + * 是否包含查询节点(efcode_virtual_unit.vir_include_search = '是') + */ + boolean includeSearch() default true; + + /** + * 业务表的别名(如 SQL 中使用了别名,如 "t"、"e" 等)。 + *

+ * 为空表示不使用别名,直接使用列名。 + */ + String tableAlias() default ""; + + /** + * 业务表中用于匹配 sys_dept_id 的列名。 + *

+ * 默认为 "bureau_dept_id"。 + */ + String column() default "bureau_dept_id"; +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterAdvice.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterAdvice.java new file mode 100644 index 00000000..7e6a65d7 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterAdvice.java @@ -0,0 +1,48 @@ +package com.zdxt.enforcementcode.common.annotation; + +import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * EfcodeDataFilter 注解方法拦截器。 + *

+ * 在 Mapper 方法执行前将注解信息存入 ThreadLocal,执行后清除。 + * + * @author zrb + */ +@Slf4j +public class EfcodeDataFilterAdvice implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Object target = invocation.getThis(); + Method method = invocation.getMethod(); + // 设置过滤注解 + EfcodeDataFilterHelper.setFilter(getAnnotation(target, method)); + try { + return invocation.proceed(); + } finally { + EfcodeDataFilterHelper.removeFilter(); + } + } + + /** + * 获取 EfcodeDataFilter 注解:优先方法级别,其次类级别 + */ + private EfcodeDataFilter getAnnotation(Object target, Method method) { + EfcodeDataFilter annotation = method.getAnnotation(EfcodeDataFilter.class); + if (annotation != null) { + return annotation; + } + Class targetClass = target.getClass(); + // JDK 动态代理(Mapper)需要获取接口上的注解 + if (Proxy.isProxyClass(targetClass)) { + targetClass = targetClass.getInterfaces()[0]; + } + return targetClass.getAnnotation(EfcodeDataFilter.class); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterConfig.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterConfig.java new file mode 100644 index 00000000..b125d3e5 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterConfig.java @@ -0,0 +1,37 @@ +package com.zdxt.enforcementcode.common.annotation; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * 执法码数据权限过滤配置。 + *

+ * 注册 {@link EfcodeDataFilterInterceptor} 到 MyBatis-Plus 拦截器链, + * 并注册 {@link EfcodeDataFilterPointcutAdvisor} 用于 AOP 拦截。 + * + * @author zrb + */ +@Configuration +public class EfcodeDataFilterConfig { + + /** + * 将执法码数据过滤拦截器追加到 MyBatis-Plus 拦截器链中 + */ + @Autowired + public void configureEfcodeDataFilter(MybatisPlusInterceptor interceptor) { + interceptor.addInnerInterceptor(new EfcodeDataFilterInterceptor()); + } + + /** + * 执法码数据过滤切面注册器 + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public EfcodeDataFilterPointcutAdvisor efcodeDataFilterPointcutAdvisor() { + return new EfcodeDataFilterPointcutAdvisor(); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterHelper.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterHelper.java new file mode 100644 index 00000000..652d283c --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterHelper.java @@ -0,0 +1,38 @@ +package com.zdxt.enforcementcode.common.annotation; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * EfcodeDataFilter 注解上下文管理器。 + *

+ * 通过 ThreadLocal 在 AOP 切面设置注解信息,在 MyBatis 拦截器中读取并使用。 + * + * @author zrb + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EfcodeDataFilterHelper { + + private static final ThreadLocal FILTER_CACHE = new ThreadLocal<>(); + + /** + * 设置当前线程的数据过滤注解 + */ + public static void setFilter(EfcodeDataFilter filter) { + FILTER_CACHE.set(filter); + } + + /** + * 获取当前线程的数据过滤注解 + */ + public static EfcodeDataFilter getFilter() { + return FILTER_CACHE.get(); + } + + /** + * 清除当前线程的数据过滤注解 + */ + public static void removeFilter() { + FILTER_CACHE.remove(); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterInterceptor.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterInterceptor.java new file mode 100644 index 00000000..9a8c758e --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterInterceptor.java @@ -0,0 +1,161 @@ +package com.zdxt.enforcementcode.common.annotation; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.zdxt.enforcementcode.common.util.DataFilterUtil; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.satoken.utils.LoginHelper; + +import java.sql.SQLException; + +/** + * 执法码数据权限 MyBatis 拦截器。 + *

+ * 在 SQL 执行前,根据 {@link EfcodeDataFilter} 注解配置和用户角色, + * 自动向 SELECT 语句的 WHERE 子句追加基于 bureau_dept_id 的数据过滤条件。 + * + * @author zrb + */ +@Slf4j +public class EfcodeDataFilterInterceptor implements InnerInterceptor { + + @Override + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, + RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + // 从 ThreadLocal 获取注解 + EfcodeDataFilter filter = EfcodeDataFilterHelper.getFilter(); + if (filter == null) { + return; + } + + // 构建过滤 SQL 片段 + String filterSql = buildFilterSql(filter); + if (filterSql == null) { + // null 表示全部放行,不追加条件 + return; + } + + // 解析并修改 SQL + try { + PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); + String originalSql = mpBs.sql(); + String newSql = appendWhere(originalSql, filterSql); + mpBs.sql(newSql); + } catch (Exception e) { + log.error("EfcodeDataFilter SQL注入异常: {}", e.getMessage(), e); + } + } + + /** + * 根据注解配置和用户角色,构建过滤 SQL 片段。 + *

+ * 使用 SQL 子查询替代 Java 端拼接 IN 列表,避免部门数量过多导致 SQL 超长。 + * + * @param filter 注解 + * @return SQL 片段;null 表示全部放行;非 null 字符串追加到 WHERE + */ + private String buildFilterSql(EfcodeDataFilter filter) { + // 超管或 ViewAllDataRole → 不过滤 + if (DataFilterUtil.isFullAccess()) { + return null; + } + + // ViewFilterDataRole → 构建子查询条件 + if (DataFilterUtil.needFilter()) { + String userDeptId = LoginHelper.getDeptId(); + if (StrUtil.isBlank(userDeptId)) { + return "1 = 0"; + } + + // 构建列名(支持别名) + String columnRef = buildColumnRef(filter.tableAlias(), filter.column()); + String escapedDeptId = escapeSql(userDeptId); + + // 使用子查询:通过 userDeptId 找到 virId,再通过 FIND_IN_SET 查子孙节点的 sys_dept_id + StringBuilder sb = new StringBuilder(); + sb.append(columnRef).append(" IN ("); + sb.append("SELECT eu.sys_dept_id FROM efcode_virtual_unit eu"); + sb.append(" WHERE FIND_IN_SET("); + sb.append("(SELECT eu2.vir_id FROM efcode_virtual_unit eu2 WHERE eu2.sys_dept_id = '").append(escapedDeptId).append("' LIMIT 1)"); + sb.append(", eu.vir_ancestors)"); + sb.append(" AND eu.sys_dept_id IS NOT NULL AND eu.sys_dept_id != ''"); + + // includeSearch / includeStatistics 过滤条件 + if (filter.includeSearch() || filter.includeStatistics()) { + sb.append(" AND (1=0"); + if (filter.includeSearch()) { + sb.append(" OR eu.vir_include_search = '是'"); + } + if (filter.includeStatistics()) { + sb.append(" OR eu.vir_include_statistics = '是'"); + } + sb.append(")"); + } + sb.append(")"); + + // 补充自身部门(子查询通过 FIND_IN_SET 只返回子孙,不含自身节点) + sb.append(" OR ").append(columnRef).append(" = '").append(escapedDeptId).append("'"); + + return sb.toString(); + } + + // 无权访问 + return "1 = 0"; + } + + /** + * 构建列引用(支持表别名) + */ + private String buildColumnRef(String tableAlias, String column) { + if (StringUtils.isNotBlank(tableAlias)) { + return tableAlias + "." + column; + } + return column; + } + + /** + * 将过滤条件追加到 SQL 的 WHERE 子句中 + */ + private String appendWhere(String originalSql, String filterSql) throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse(originalSql); + if (select instanceof PlainSelect plainSelect) { + Expression filterExpression = CCJSqlParserUtil.parseCondExpression(filterSql); + // 用括号包裹过滤条件 + ParenthesedExpressionList parenthesis = new ParenthesedExpressionList<>(filterExpression); + Expression where = plainSelect.getWhere(); + if (where != null) { + plainSelect.setWhere(new AndExpression(where, parenthesis)); + } else { + plainSelect.setWhere(parenthesis); + } + return plainSelect.toString(); + } + // 非简单 SELECT(如 UNION),尝试在末尾追加 + log.warn("EfcodeDataFilter: 无法解析为PlainSelect,跳过SQL过滤: {}", originalSql); + return originalSql; + } + + /** + * 简单的 SQL 注入防护(转义单引号) + */ + private String escapeSql(String value) { + if (value == null) { + return ""; + } + return value.replace("'", "''"); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcut.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcut.java new file mode 100644 index 00000000..e9d2cbfe --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcut.java @@ -0,0 +1,31 @@ +package com.zdxt.enforcementcode.common.annotation; + +import org.springframework.aop.support.StaticMethodMatcherPointcut; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * EfcodeDataFilter 注解切入点匹配器。 + *

+ * 匹配方法或类上标注了 {@link EfcodeDataFilter} 注解的目标。 + * + * @author zrb + */ +@SuppressWarnings("all") +public class EfcodeDataFilterPointcut extends StaticMethodMatcherPointcut { + + @Override + public boolean matches(Method method, Class targetClass) { + // 优先匹配方法上的注解 + if (method.isAnnotationPresent(EfcodeDataFilter.class)) { + return true; + } + // MyBatis 的 Mapper 通过 JDK 动态代理实现,需要检查接口 + Class targetClassRef = targetClass; + if (Proxy.isProxyClass(targetClassRef)) { + targetClassRef = targetClass.getInterfaces()[0]; + } + return targetClassRef.isAnnotationPresent(EfcodeDataFilter.class); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcutAdvisor.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcutAdvisor.java new file mode 100644 index 00000000..fe3e9e37 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcutAdvisor.java @@ -0,0 +1,33 @@ +package com.zdxt.enforcementcode.common.annotation; + +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; + +/** + * EfcodeDataFilter 切面注册器。 + *

+ * 将 {@link EfcodeDataFilterPointcut} 和 {@link EfcodeDataFilterAdvice} 组合注册为 Spring AOP Advisor。 + * + * @author zrb + */ +public class EfcodeDataFilterPointcutAdvisor extends AbstractPointcutAdvisor { + + private final Advice advice; + private final Pointcut pointcut; + + public EfcodeDataFilterPointcutAdvisor() { + this.advice = new EfcodeDataFilterAdvice(); + this.pointcut = new EfcodeDataFilterPointcut(); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this.advice; + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java index 5ea6723f..3c2e6e6b 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java @@ -1,17 +1,31 @@ package com.zdxt.enforcementcode.common.util; + +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import com.zdxt.enforcementcode.domain.vo.EfcodeVirtualUnitVo; import com.zdxt.enforcementcode.service.IEfcodeVirtualUnitService; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.satoken.utils.LoginHelper; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class DataFilterUtil { + // ======================== 角色常量 ======================== + + /** 拥有此角色的用户可查看全部数据(不做过滤) */ + public static final String ROLE_VIEW_ALL_DATA = "ViewAllDataRole"; + + /** 拥有此角色的用户需要按虚拟单位树做数据过滤 */ + public static final String ROLE_VIEW_FILTER_DATA = "ViewFilterDataRole"; + // ======================== 区(ShenZhen_Areas)(2) 的子节点常量 ======================== /** 区 */ @@ -53,6 +67,116 @@ public class DataFilterUtil { /** 深圳市直 */ public static final String ShenZhen_ShiZhi = "ShenZhen_ShiZhi"; + // ======================== 权限判断方法 ======================== + + /** + * 判断当前用户是否拥有全部数据访问权限(无需过滤)。 + *

+ * 满足以下任一条件即为全部放行: + *

+ * + * @return true=全部放行, false=需要进一步判断 + */ + public static boolean isFullAccess() { + if (LoginHelper.isSuperAdmin()) { + return true; + } + LoginUser user = LoginHelper.getLoginUser(); + if (user == null) { + return false; + } + Set rolePermission = user.getRolePermission(); + return CollUtil.isNotEmpty(rolePermission) && rolePermission.contains(ROLE_VIEW_ALL_DATA); + } + + /** + * 判断当前用户是否需要进行数据过滤(拥有 ViewFilterDataRole 角色) + * + * @return true=需要过滤, false=不需要 + */ + public static boolean needFilter() { + LoginUser user = LoginHelper.getLoginUser(); + if (user == null) { + return false; + } + Set rolePermission = user.getRolePermission(); + return CollUtil.isNotEmpty(rolePermission) && rolePermission.contains(ROLE_VIEW_FILTER_DATA); + } + + /** + * 判断当前用户是否无权访问数据。 + *

+ * 既不是超管、也没有 ViewAllDataRole、也没有 ViewFilterDataRole + * + * @return true=无权访问, false=有权限 + */ + public static boolean hasNoAccess() { + return !isFullAccess() && !needFilter(); + } + + /** + * 获取当前用户可统计的全部单位的 sys_dept_id 列表。 + *

+ * 逻辑: + *

+ * + * @param includeStatistics 是否包含统计节点 + * @param includeSearch 是否包含查询节点 + * @return sys_dept_id 列表;null 表示全部放行;空列表表示无权 + */ + public static List getUserAccessibleDeptIds(boolean includeStatistics, boolean includeSearch) { + // 全部放行 + if (isFullAccess()) { + return null; + } + // 需要过滤 + if (needFilter()) { + return getFilteredDeptIdsForCurrentUser(includeStatistics, includeSearch); + } + // 无权访问 + return Collections.emptyList(); + } + + /** + * 针对 ViewFilterDataRole 用户,根据其部门ID查找虚拟单位树中对应节点及其子孙的 sys_dept_id + * + * @param includeStatistics 是否包含统计节点 + * @param includeSearch 是否包含查询节点 + * @return 用户可访问的 sys_dept_id 列表(含自身节点) + */ + public static List getFilteredDeptIdsForCurrentUser(boolean includeStatistics, boolean includeSearch) { + String userDeptId = LoginHelper.getDeptId(); + if (StrUtil.isBlank(userDeptId)) { + return Collections.emptyList(); + } + IEfcodeVirtualUnitService service = SpringUtils.getBean(IEfcodeVirtualUnitService.class); + // 先通过 sys_dept_id 找到用户在虚拟单位树中的节点 vir_id + String virId = service.findVirIdBySysDeptId(userDeptId); + if (StrUtil.isBlank(virId)) { + return Collections.emptyList(); + } + // 查询该节点及其子孙(包含自身)的 sys_dept_id + List descendantDeptIds = service.queryDescendants(virId, includeSearch, includeStatistics, false) + .stream() + .map(EfcodeVirtualUnitVo::getSysDeptId) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + // 补充自身节点的 sys_dept_id + if (!descendantDeptIds.contains(userDeptId)) { + descendantDeptIds.add(userDeptId); + } + return descendantDeptIds; + } + // ======================== 公开方法 ======================== /** diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/TestEnfDemo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/TestEnfDemo.java index 52ebfeba..a0236849 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/TestEnfDemo.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/TestEnfDemo.java @@ -42,6 +42,11 @@ public class TestEnfDemo extends TenantEntity { */ private Long deptId; + /** + * 执法部门ID + */ + private String bureauDeptId; + /** * 用户id */ diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/TestEnfDemoMapper.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/TestEnfDemoMapper.java index 63190856..7bcada36 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/TestEnfDemoMapper.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/TestEnfDemoMapper.java @@ -2,6 +2,7 @@ package com.zdxt.enforcementcode.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.zdxt.enforcementcode.common.annotation.EfcodeDataFilter; import com.zdxt.enforcementcode.domain.TestEnfDemo; import com.zdxt.enforcementcode.domain.vo.TestEnfDemoVo; import org.apache.ibatis.annotations.Mapper; @@ -22,16 +23,19 @@ public interface TestEnfDemoMapper extends BaseMapperPlus customPageList(@Param("page") Page page, @Param("ew") Wrapper wrapper); /** * 手动分页 - 查询总数(不经过拦截器) */ + @EfcodeDataFilter(column = "bureau_dept_id") Long manualCount(@Param("ew") Wrapper wrapper); /** * 手动分页 - 查询当前页数据(SQL中自带LIMIT,不经过拦截器) */ + @EfcodeDataFilter(column = "bureau_dept_id") List manualSelectList(@Param("ew") Wrapper wrapper, @Param("offset") int offset, @Param("pageSize") int pageSize); diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeVirtualUnitService.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeVirtualUnitService.java index 3f13fc64..4e180438 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeVirtualUnitService.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeVirtualUnitService.java @@ -126,6 +126,14 @@ public interface IEfcodeVirtualUnitService { */ void updateUser(UnitUserBo bo); + /** + * 根据 sys_dept_id 查找对应的虚拟单位 vir_id + * + * @param sysDeptId 部门ID + * @return 对应的 vir_id,不存在则返回 null + */ + String findVirIdBySysDeptId(String sysDeptId); + /** * 删除执法人员 * diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeVirtualUnitServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeVirtualUnitServiceImpl.java index 77adcb97..6957ec8b 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeVirtualUnitServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeVirtualUnitServiceImpl.java @@ -343,6 +343,22 @@ public class EfcodeVirtualUnitServiceImpl implements IEfcodeVirtualUnitService { sysUserMapper.update(null, uw); } + /** + * 根据 sys_dept_id 查找对应的虚拟单位 vir_id + */ + @Override + public String findVirIdBySysDeptId(String sysDeptId) { + if (StringUtils.isBlank(sysDeptId)) { + return null; + } + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(EfcodeVirtualUnit::getSysDeptId, sysDeptId); + lqw.select(EfcodeVirtualUnit::getVirId); + lqw.last("LIMIT 1"); + EfcodeVirtualUnitVo vo = baseMapper.selectVoOne(lqw); + return vo != null ? vo.getVirId() : null; + } + /** * 删除执法人员(逻辑删除) */ diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/TestEnfDemoServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/TestEnfDemoServiceImpl.java index 90313f11..3f4889b1 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/TestEnfDemoServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/TestEnfDemoServiceImpl.java @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import com.zdxt.enforcementcode.common.annotation.EfcodeDataFilter; import com.zdxt.enforcementcode.domain.bo.TestEnfDemoBo; import com.zdxt.enforcementcode.domain.vo.TestEnfDemoVo; import com.zdxt.enforcementcode.domain.TestEnfDemo; @@ -45,12 +46,13 @@ public class TestEnfDemoServiceImpl implements ITestEnfDemoService { } /** - * 分页查询enf测试单列表 + * 分页查询enf测试单列表(Service层注解示例:覆盖继承方法selectVoPage) * * @param bo 查询条件 * @param pageQuery 分页参数 * @return enf测试单分页列表 */ + @EfcodeDataFilter @Override public TableDataInfo queryPageList(TestEnfDemoBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); @@ -71,7 +73,9 @@ public class TestEnfDemoServiceImpl implements ITestEnfDemoService { /** * 全手动分页(方案A demo) * 完全绕过PaginationInnerInterceptor拦截器,自己控制COUNT和LIMIT + * Service层注解示例:一个注解覆盖方法内多次Mapper调用(manualCount + manualSelectList) */ + @EfcodeDataFilter @Override public TableDataInfo manualPageList(TestEnfDemoBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); @@ -91,11 +95,12 @@ public class TestEnfDemoServiceImpl implements ITestEnfDemoService { } /** - * 查询符合条件的enf测试单列表 + * 查询符合条件的enf测试单列表(Service层注解示例:覆盖继承方法selectVoList) * * @param bo 查询条件 * @return enf测试单列表 */ + @EfcodeDataFilter @Override public List queryList(TestEnfDemoBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); diff --git a/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java new file mode 100644 index 00000000..8d3011e2 --- /dev/null +++ b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java @@ -0,0 +1,316 @@ +package com.zdxt.service.test; + +import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import com.zdxt.enforcementcode.domain.bo.TestEnfDemoBo; +import com.zdxt.enforcementcode.domain.vo.TestEnfDemoVo; +import com.zdxt.enforcementcode.service.ITestEnfDemoService; +import org.dromara.ZdxtAdminApplication; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * TestEnfDemoServiceImpl 单元测试 + *

+ * 验证 @EfcodeDataFilter 注解在 Service 层和 Mapper 层的数据过滤效果。 + *

+ * + * @author zrb + */ +@SpringBootTest(classes = ZdxtAdminApplication.class) +@ActiveProfiles({"dev", "dev-cust-zrb"}) +@AutoConfigureMockMvc +@DisplayName("TestEnfDemoService 数据过滤注解测试") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestEnfDemoServiceTest { + + @Autowired + private ITestEnfDemoService testEnfDemoService; + + private static final String TEST_CLIENT_ID = "e5cd7e4891bf95d1d19206ce24a7b32e"; + private static final String TENANT_ID = "000000"; + + @BeforeEach + public void setUp() { + SaTokenContextMockUtil.setMockContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest)); + } + + @AfterEach + public void tearDown() { + try { + StpUtil.logout(); + } catch (Exception ignored) { + } + RequestContextHolder.resetRequestAttributes(); + SaTokenContextMockUtil.clearContext(); + } + + /** + * 模拟超级管理员登录(全部放行) + */ + private void loginAsSuperAdmin() { + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(1L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId(TENANT_ID); + loginUser.setUsername("admin"); + loginUser.setNickname("超级管理员"); + loginUser.setDeptId("103"); + loginUser.setDeptName("研发部门"); + loginUser.setRolePermission(new HashSet<>(Set.of("admin"))); + loginUser.setMenuPermission(new HashSet<>(Set.of("*:*:*"))); + + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + } + + /** + * 模拟 ViewAllDataRole 用户登录(全部放行) + */ + private void loginAsViewAllDataUser() { + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(2L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId(TENANT_ID); + loginUser.setUsername("view_all_user"); + loginUser.setNickname("全数据查看用户"); + loginUser.setDeptId("103"); + loginUser.setDeptName("研发部门"); + loginUser.setRolePermission(new HashSet<>(Set.of("ViewAllDataRole"))); + loginUser.setMenuPermission(new HashSet<>()); + + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + } + + /** + * 模拟 ViewFilterDataRole 用户登录(需要数据过滤) + */ + private void loginAsFilterDataUser(String deptId) { + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(3L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId(TENANT_ID); + loginUser.setUsername("filter_user"); + loginUser.setNickname("过滤数据用户"); + loginUser.setDeptId(deptId); + loginUser.setDeptName("过滤部门"); + loginUser.setRolePermission(new HashSet<>(Set.of("ViewFilterDataRole"))); + loginUser.setMenuPermission(new HashSet<>()); + + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + } + + /** + * 模拟无任何数据权限角色的用户登录(无权访问) + */ + private void loginAsNoAccessUser() { + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(4L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId(TENANT_ID); + loginUser.setUsername("no_access_user"); + loginUser.setNickname("无权用户"); + loginUser.setDeptId("103"); + loginUser.setDeptName("研发部门"); + loginUser.setRolePermission(new HashSet<>(Set.of("common"))); + loginUser.setMenuPermission(new HashSet<>()); + + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + } + + // ======================== queryPageList 测试(Service层注解,覆盖selectVoPage) ======================== + + @Test + @Order(1) + @DisplayName("超级管理员 - queryPageList - 全部放行,可查询到数据") + public void testQueryPageList_SuperAdmin() { + loginAsSuperAdmin(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.queryPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[超管] queryPageList 总数: " + result.getTotal()); + assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); + } + + @Test + @Order(2) + @DisplayName("ViewAllDataRole用户 - queryPageList - 全部放行") + public void testQueryPageList_ViewAllData() { + loginAsViewAllDataUser(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.queryPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[ViewAllData] queryPageList 总数: " + result.getTotal()); + assertTrue(result.getTotal() > 0, "ViewAllDataRole用户应能查询到数据"); + } + + @Test + @Order(3) + @DisplayName("ViewFilterDataRole用户 - queryPageList - 数据过滤生效") + public void testQueryPageList_FilterData() { + // 使用一个在虚拟单位树中存在的部门ID + loginAsFilterDataUser("20210922211852148-9BC0-86CB268FF"); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.queryPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[ViewFilterData] queryPageList 总数: " + result.getTotal()); + // 过滤后数据量应小于等于全量 + } + + @Test + @Order(4) + @DisplayName("无权用户 - queryPageList - 无法查询到任何数据") + public void testQueryPageList_NoAccess() { + loginAsNoAccessUser(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.queryPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[无权用户] queryPageList 总数: " + result.getTotal()); + assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); + } + + // ======================== queryList 测试(Service层注解,覆盖selectVoList) ======================== + + @Test + @Order(5) + @DisplayName("超级管理员 - queryList - 全部放行") + public void testQueryList_SuperAdmin() { + loginAsSuperAdmin(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + + List result = testEnfDemoService.queryList(bo); + assertNotNull(result); + System.out.println("[超管] queryList 数量: " + result.size()); + assertFalse(result.isEmpty(), "超级管理员应能查询到数据"); + } + + @Test + @Order(6) + @DisplayName("无权用户 - queryList - 查询结果为空") + public void testQueryList_NoAccess() { + loginAsNoAccessUser(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + + List result = testEnfDemoService.queryList(bo); + assertNotNull(result); + System.out.println("[无权用户] queryList 数量: " + result.size()); + assertTrue(result.isEmpty(), "无权用户不应查询到任何数据"); + } + + // ======================== manualPageList 测试(Service层注解,覆盖多次Mapper调用) ======================== + + @Test + @Order(7) + @DisplayName("超级管理员 - manualPageList - 全部放行") + public void testManualPageList_SuperAdmin() { + loginAsSuperAdmin(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.manualPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[超管] manualPageList 总数: " + result.getTotal()); + assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); + } + + @Test + @Order(8) + @DisplayName("无权用户 - manualPageList - 无法查询到任何数据") + public void testManualPageList_NoAccess() { + loginAsNoAccessUser(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.manualPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[无权用户] manualPageList 总数: " + result.getTotal()); + assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); + } + + // ======================== customPageList 测试(Mapper层注解) ======================== + + @Test + @Order(9) + @DisplayName("超级管理员 - customPageList(Mapper注解) - 全部放行") + public void testCustomPageList_SuperAdmin() { + loginAsSuperAdmin(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.customPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[超管] customPageList 总数: " + result.getTotal()); + assertTrue(result.getTotal() > 0, "超级管理员应能查询到数据"); + } + + @Test + @Order(10) + @DisplayName("无权用户 - customPageList(Mapper注解) - 无法查询到任何数据") + public void testCustomPageList_NoAccess() { + loginAsNoAccessUser(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + PageQuery pageQuery = new PageQuery(10, 1); + + TableDataInfo result = testEnfDemoService.customPageList(bo, pageQuery); + assertNotNull(result); + System.out.println("[无权用户] customPageList 总数: " + result.getTotal()); + assertEquals(0, result.getTotal(), "无权用户不应查询到任何数据"); + } + + // ======================== 对比验证:超管 vs 过滤用户数据量 ======================== + + @Test + @Order(11) + @DisplayName("数据量对比 - 超管全量 vs ViewFilterDataRole过滤量") + public void testDataComparison() { + // 先以超管身份查全量 + loginAsSuperAdmin(); + TestEnfDemoBo bo = new TestEnfDemoBo(); + List allData = testEnfDemoService.queryList(bo); + long allCount = allData.size(); + StpUtil.logout(); + + // 再以过滤用户身份查 + loginAsFilterDataUser("20210922211852148-9BC0-86CB268FF"); + List filteredData = testEnfDemoService.queryList(bo); + long filteredCount = filteredData.size(); + + System.out.println("[对比] 超管全量: " + allCount + ", 过滤用户: " + filteredCount); + assertTrue(filteredCount <= allCount, "过滤后数据量应小于等于全量数据"); + } +}