16 changed files with 984 additions and 91 deletions
@ -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; |
|||
@ -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; |
|||
@ -0,0 +1,49 @@ |
|||
package com.zdxt.enforcementcode.common.annotation; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 执法码数据权限过滤注解。 |
|||
* <p> |
|||
* 标注在 Mapper 方法或 Mapper 类上,声明该查询需要按虚拟单位树进行数据过滤。 |
|||
* <p> |
|||
* 过滤规则: |
|||
* <ul> |
|||
* <li>超级管理员 → 全部放行,不追加过滤条件</li> |
|||
* <li>拥有 ViewAllDataRole 角色 → 全部放行</li> |
|||
* <li>拥有 ViewFilterDataRole 角色 → 根据用户所在部门在 efcode_virtual_unit 中的位置, |
|||
* 查找该节点及其子孙的 sys_dept_id,生成 IN 条件注入 SQL</li> |
|||
* <li>其他用户 → 追加 1=0 条件,无权访问任何数据</li> |
|||
* </ul> |
|||
* |
|||
* @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" 等)。 |
|||
* <p> |
|||
* 为空表示不使用别名,直接使用列名。 |
|||
*/ |
|||
String tableAlias() default ""; |
|||
|
|||
/** |
|||
* 业务表中用于匹配 sys_dept_id 的列名。 |
|||
* <p> |
|||
* 默认为 "bureau_dept_id"。 |
|||
*/ |
|||
String column() default "bureau_dept_id"; |
|||
} |
|||
@ -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 注解方法拦截器。 |
|||
* <p> |
|||
* 在 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); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
/** |
|||
* 执法码数据权限过滤配置。 |
|||
* <p> |
|||
* 注册 {@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(); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
package com.zdxt.enforcementcode.common.annotation; |
|||
|
|||
import lombok.AccessLevel; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* EfcodeDataFilter 注解上下文管理器。 |
|||
* <p> |
|||
* 通过 ThreadLocal 在 AOP 切面设置注解信息,在 MyBatis 拦截器中读取并使用。 |
|||
* |
|||
* @author zrb |
|||
*/ |
|||
@NoArgsConstructor(access = AccessLevel.PRIVATE) |
|||
public class EfcodeDataFilterHelper { |
|||
|
|||
private static final ThreadLocal<EfcodeDataFilter> 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(); |
|||
} |
|||
} |
|||
@ -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 拦截器。 |
|||
* <p> |
|||
* 在 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 片段。 |
|||
* <p> |
|||
* 使用 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<Expression> 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("'", "''"); |
|||
} |
|||
} |
|||
@ -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 注解切入点匹配器。 |
|||
* <p> |
|||
* 匹配方法或类上标注了 {@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); |
|||
} |
|||
} |
|||
@ -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 切面注册器。 |
|||
* <p> |
|||
* 将 {@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; |
|||
} |
|||
} |
|||
@ -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 单元测试 |
|||
* <p> |
|||
* 验证 @EfcodeDataFilter 注解在 Service 层和 Mapper 层的数据过滤效果。 |
|||
* </p> |
|||
* |
|||
* @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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> 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<TestEnfDemoVo> allData = testEnfDemoService.queryList(bo); |
|||
long allCount = allData.size(); |
|||
StpUtil.logout(); |
|||
|
|||
// 再以过滤用户身份查
|
|||
loginAsFilterDataUser("20210922211852148-9BC0-86CB268FF"); |
|||
List<TestEnfDemoVo> filteredData = testEnfDemoService.queryList(bo); |
|||
long filteredCount = filteredData.size(); |
|||
|
|||
System.out.println("[对比] 超管全量: " + allCount + ", 过滤用户: " + filteredCount); |
|||
assertTrue(filteredCount <= allCount, "过滤后数据量应小于等于全量数据"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue