系统支持切换单位来实时过滤业务数据的查询条件

This commit is contained in:
陈博文
2025-06-23 16:06:42 +08:00
parent 40863d00d2
commit 2e9c4f73de
13 changed files with 296 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.tenant.config;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties;
@@ -16,11 +17,11 @@ import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
import cn.iocoder.yudao.framework.tenant.core.web.DeptVisitContextInterceptor;
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import jakarta.annotation.Resource;
@@ -133,6 +134,10 @@ public class YudaoTenantAutoConfiguration {
SecurityFrameworkService securityFrameworkService) {
return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService);
}
@Bean
public DeptVisitContextInterceptor deptVisitContextInterceptor(SecurityFrameworkService securityFrameworkService) {
return new DeptVisitContextInterceptor(securityFrameworkService);
}
@Bean
public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties,
@@ -147,6 +152,17 @@ public class YudaoTenantAutoConfiguration {
};
}
@Bean
public WebMvcConfigurer deptWebMvcConfigurer(TenantProperties tenantProperties, DeptVisitContextInterceptor deptVisitContextInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(deptVisitContextInterceptor)
.excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0]));
}
};
}
// ========== Security ==========
@Bean

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import java.lang.annotation.*;
/**
* 忽略单位切换,标记指定方法不进行租户切换的覆盖
* @author chenbowen
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DeptVisitIgnore {
/**
* 是否开启忽略租户,默认为 true 开启
*
* 支持 Spring EL 表达式,如果返回 true 则满足条件,进行租户的忽略
*/
String enable() default "true";
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.tenant.core.aop;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
* 忽略单位切换,标记指定方法不进行租户切换的覆盖,基于 {@link DeptVisitIgnore} 注解实现,用于一些全局的逻辑。
* 例如说,一个定时任务,读取所有数据,进行处理。
* 又例如说,读取所有数据,进行缓存。
* @author 芋道源码
*/
@Aspect
@Slf4j
public class DeptVisitIgnoreAspect {
@Around("@annotation(deptVisitIgnore)")
public Object around(ProceedingJoinPoint joinPoint, DeptVisitIgnore deptVisitIgnore) throws Throwable {
Boolean oldIgnore = TenantContextHolder.isIgnore();
try {
// 计算条件,满足的情况下,才进行忽略
Object enable = SpringExpressionUtils.parseExpression(deptVisitIgnore.enable());
if (Boolean.TRUE.equals(enable)) {
TenantContextHolder.setIgnore(true);
}
// 执行逻辑
return joinPoint.proceed();
} finally {
TenantContextHolder.setIgnore(oldIgnore);
}
}
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.framework.tenant.core.context;
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.Set;
/**
* 多部门上下文 Holder
*
* @author 芋道源码
*/
public class DeptContextHolder {
/**
* 当前部门编号列表
*/
private static final ThreadLocal<Set<Long>> DEPT_ID_LIST = new TransmittableThreadLocal<>();
/**
* 是否忽略部门
*/
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
/**
* 获得部门编号列表
*
* @return 部门编号列表
*/
public static Set<Long> getDeptIdList() {
return DEPT_ID_LIST.get();
}
/**
* 获得部门编号列表。如果不存在,则抛出 NullPointerException 异常
*
* @return 部门编号列表
*/
public static Set<Long> getRequiredDeptIdList() {
Set<Long> deptIdList = getDeptIdList();
if (deptIdList == null) {
throw new NullPointerException("DeptContextHolder 不存在部门编号列表!可参考文档:"
+ DocumentEnum.TENANT.getUrl());
}
return deptIdList;
}
public static void setDeptIdList(Set<Long> deptIdList) {
DEPT_ID_LIST.set(deptIdList);
}
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
/**
* 当前是否忽略部门
*
* @return 是否忽略
*/
public static boolean isIgnore() {
return Boolean.TRUE.equals(IGNORE.get());
}
public static void clear() {
DEPT_ID_LIST.remove();
IGNORE.remove();
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.framework.tenant.core.web;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.DeptContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Set;
/**
* @author chenbowen
*/
@RequiredArgsConstructor
@Slf4j
public class DeptVisitContextInterceptor implements HandlerInterceptor {
private static final String PERMISSION = "system:dept:visit";
private final SecurityFrameworkService securityFrameworkService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 解析 header 并设置 visitDeptIds
Set<Long> deptIds = WebFrameworkUtils.getVisitDeptIds(request);
if (deptIds == null) {
return true;
}
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return true;
}
// if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
// throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换部门");
// }
loginUser.setVisitDeptIds(deptIds);
DeptContextHolder.setDeptIdList(deptIds);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 清理 visitDeptIds
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null) {
loginUser.setVisitDeptIds(null);
}
}
}

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.tenant.core.web;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
@@ -13,6 +14,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
/**
* @author chenbowen
*/
@RequiredArgsConstructor
@Slf4j
public class TenantVisitContextInterceptor implements HandlerInterceptor {
@@ -39,10 +45,10 @@ public class TenantVisitContextInterceptor implements HandlerInterceptor {
return true;
}
// // 校验用户是否可切换租户
// if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
// throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
// }
// 校验用户是否可切换租户
if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) {
throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户");
}
// 【重点】切换租户编号
loginUser.setVisitTenantId(visitTenantId);