Browse Source

虚拟单位重组功能开发

dev-V1.0.1
庄锐波 6 months ago
parent
commit
4cab996e5d
  1. 89
      script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql
  2. 107
      script/sql/zfjd-refactoring-v1/1001-生成测试数据.sql
  3. 49
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilter.java
  4. 48
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterAdvice.java
  5. 37
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterConfig.java
  6. 38
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterHelper.java
  7. 161
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterInterceptor.java
  8. 31
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcut.java
  9. 33
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/annotation/EfcodeDataFilterPointcutAdvisor.java
  10. 124
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java
  11. 5
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/TestEnfDemo.java
  12. 4
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/TestEnfDemoMapper.java
  13. 8
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeVirtualUnitService.java
  14. 16
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeVirtualUnitServiceImpl.java
  15. 9
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/TestEnfDemoServiceImpl.java
  16. 316
      zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/service/test/TestEnfDemoServiceTest.java

89
script/sql/zfjd-refactoring-v1/1001-修改用于测试的账号密码.sql

@ -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;

107
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;

49
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.*;
/**
* 执法码数据权限过滤注解。
* <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";
}

48
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 注解方法拦截器。
* <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);
}
}

37
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;
/**
* 执法码数据权限过滤配置。
* <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();
}
}

38
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 注解上下文管理器。
* <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();
}
}

161
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 拦截器。
* <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("'", "''");
}
}

31
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 注解切入点匹配器。
* <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);
}
}

33
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 切面注册器。
* <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;
}
}

124
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";
// ======================== 权限判断方法 ========================
/**
* 判断当前用户是否拥有全部数据访问权限(无需过滤)。
* <p>
* 满足以下任一条件即为全部放行:
* <ul>
* <li>超级管理员</li>
* <li>拥有 ViewAllDataRole 角色</li>
* </ul>
*
* @return true=全部放行, false=需要进一步判断
*/
public static boolean isFullAccess() {
if (LoginHelper.isSuperAdmin()) {
return true;
}
LoginUser user = LoginHelper.getLoginUser();
if (user == null) {
return false;
}
Set<String> 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<String> rolePermission = user.getRolePermission();
return CollUtil.isNotEmpty(rolePermission) && rolePermission.contains(ROLE_VIEW_FILTER_DATA);
}
/**
* 判断当前用户是否无权访问数据。
* <p>
* 既不是超管、也没有 ViewAllDataRole、也没有 ViewFilterDataRole
*
* @return true=无权访问, false=有权限
*/
public static boolean hasNoAccess() {
return !isFullAccess() && !needFilter();
}
/**
* 获取当前用户可统计的全部单位的 sys_dept_id 列表。
* <p>
* 逻辑:
* <ul>
* <li>超管或 ViewAllDataRole → 返回 null,表示不限制(调用方应不追加过滤条件)</li>
* <li>ViewFilterDataRole → 根据用户所在部门在虚拟单位树中的位置,
* 查询该节点下所有子孙 sys_dept_id(包含自身)</li>
* <li>其他 → 返回空列表(无权访问任何数据)</li>
* </ul>
*
* @param includeStatistics 是否包含统计节点
* @param includeSearch 是否包含查询节点
* @return sys_dept_id 列表;null 表示全部放行;空列表表示无权
*/
public static List<String> 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<String> 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<String> 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;
}
// ======================== 公开方法 ========================
/**

5
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
*/

4
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<TestEnfDemo, TestEnfDe
/**
* 自定义SQL分页查询
*/
@EfcodeDataFilter(column = "bureau_dept_id")
Page<TestEnfDemoVo> customPageList(@Param("page") Page<TestEnfDemo> page, @Param("ew") Wrapper<TestEnfDemo> wrapper);
/**
* 手动分页 - 查询总数(不经过拦截器)
*/
@EfcodeDataFilter(column = "bureau_dept_id")
Long manualCount(@Param("ew") Wrapper<TestEnfDemo> wrapper);
/**
* 手动分页 - 查询当前页数据(SQL中自带LIMIT,不经过拦截器)
*/
@EfcodeDataFilter(column = "bureau_dept_id")
List<TestEnfDemoVo> manualSelectList(@Param("ew") Wrapper<TestEnfDemo> wrapper,
@Param("offset") int offset,
@Param("pageSize") int pageSize);

8
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);
/**
* 删除执法人员
*

16
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<EfcodeVirtualUnit> 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;
}
/**
* 删除执法人员(逻辑删除)
*/

9
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<TestEnfDemoVo> queryPageList(TestEnfDemoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestEnfDemo> 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<TestEnfDemoVo> manualPageList(TestEnfDemoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestEnfDemo> lqw = buildQueryWrapper(bo);
@ -91,11 +95,12 @@ public class TestEnfDemoServiceImpl implements ITestEnfDemoService {
}
/**
* 查询符合条件的enf测试单列表
* 查询符合条件的enf测试单列表(Service层注解示例:覆盖继承方法selectVoList)
*
* @param bo 查询条件
* @return enf测试单列表
*/
@EfcodeDataFilter
@Override
public List<TestEnfDemoVo> queryList(TestEnfDemoBo bo) {
LambdaQueryWrapper<TestEnfDemo> lqw = buildQueryWrapper(bo);

316
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 单元测试
* <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…
Cancel
Save