diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/runtime/PaymentContextFilter.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/runtime/PaymentContextFilter.java index 4f63602a6..c10784e4f 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/runtime/PaymentContextFilter.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/common/runtime/PaymentContextFilter.java @@ -1,10 +1,12 @@ package cn.daxpay.open.payment.common.runtime; +import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; @@ -16,6 +18,8 @@ import java.io.IOException; /// /// 在 HTTP 请求开始时开启线程级身份作用域,请求结束时关闭。 /// 仅管理 [PaymentContext] 的 open/close,不负责身份填充(由切面 / 装载器完成)。 +/// 请求结束时额外强制清理 `@IgnoreTenant` 引用计数,防止 depth 泄漏污染线程池。 +@Slf4j @Component @RequiredArgsConstructor @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @@ -29,6 +33,12 @@ public class PaymentContextFilter extends OncePerRequestFilter implements Ordere filterChain.doFilter(request, response); } finally { paymentContext.close(); + // 防 @IgnoreTenant depth 泄漏污染线程池 + int ignoreDepth = MpUtil.getIgnoreTenantDepth(); + if (ignoreDepth > 0) { + log.error("IgnoreTenant depth leaked at request end: {}, force clearing", ignoreDepth); + } + MpUtil.forceClearIgnoreTenant(); } } diff --git a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/aspect/IgnoreTenantAspectHandler.java b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/aspect/IgnoreTenantAspectHandler.java index e9ac4b404..5ab4cfded 100644 --- a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/aspect/IgnoreTenantAspectHandler.java +++ b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/aspect/IgnoreTenantAspectHandler.java @@ -11,6 +11,9 @@ import org.springframework.stereotype.Component; /// # 忽略租户(商户)数据权限切面 /// +/// 与 [MpUtil.ignoreTenant]/[MpUtil.clearIgnoreTenant] 的线程内引用计数配合, +/// 支持 `@IgnoreTenant` 嵌套:内层返回后不会清掉外层 ignore,直到最外层 finally 才恢复过滤。 +/// @Slf4j @Aspect @Component @@ -29,15 +32,15 @@ public class IgnoreTenantAspectHandler { return doHandle(pjp); } - /// 忽略租户处理逻辑 + /// 可重入忽略租户:内层 @IgnoreTenant 不会清掉外层 ignore(见 MpUtil 引用计数) private Object doHandle(ProceedingJoinPoint pjp) throws Throwable { - // 设置忽略租户插件 + // 进入忽略租户作用域(可重入) MpUtil.ignoreTenant(); try { // 执行逻辑 return pjp.proceed(); } finally { - // 关闭忽略策略 + // 退出忽略租户作用域(depth 归零时才真正 clear) MpUtil.clearIgnoreTenant(); } } diff --git a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/util/MpUtil.java b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/util/MpUtil.java index 48de32bb1..bc9ac939b 100644 --- a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/util/MpUtil.java +++ b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/util/MpUtil.java @@ -23,6 +23,8 @@ import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapp import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.experimental.UtilityClass; import org.apache.ibatis.reflection.property.PropertyNamer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.lang.reflect.Method; @@ -40,6 +42,15 @@ import cn.daxpay.open.platform.core.code.CommonCode; @UtilityClass public class MpUtil { + private static final Logger log = LoggerFactory.getLogger(MpUtil.class); + + /// 当前线程 @IgnoreTenant 嵌套深度(null/0 = 未忽略) + private static final ThreadLocal IGNORE_TENANT_DEPTH = new ThreadLocal<>(); + + /// 忽略租户行拦截策略(tenantLine = true) + private static final IgnoreStrategy IGNORE_TENANT_STRATEGY = + IgnoreStrategy.builder().tenantLine(true).build(); + /// 获取分页对象 MyBatis-Plus public Page getMpPage(PageParam page) { return Page.of(page.getCurrent(), page.getSize()); @@ -172,13 +183,54 @@ public class MpUtil { return tableInfo.getPropertyValue(object, keyProperty); } - /// 忽略租户拦截 + /// 进入忽略租户作用域(可重入,与 [clearIgnoreTenant] 成对使用) + /// + /// 基于线程内引用计数:仅 depth 从 0→1 时真正打开 MP 忽略策略; + /// 嵌套调用不会互相 clear,直到最外层 exit 才恢复租户过滤。 public void ignoreTenant() { - InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build()); + int depth = Optional.ofNullable(IGNORE_TENANT_DEPTH.get()).orElse(0) + 1; + IGNORE_TENANT_DEPTH.set(depth); + if (depth == 1) { + InterceptorIgnoreHelper.handle(IGNORE_TENANT_STRATEGY); + } } - /// 清除忽略租户拦截 + /// 退出忽略租户作用域(可重入;depth 归零时才真正 clear) + /// + /// 不配对的 exit(depth 已为 0)会强制清空并打错误日志,避免脏状态。 public void clearIgnoreTenant() { + Integer cur = IGNORE_TENANT_DEPTH.get(); + if (cur == null || cur <= 0) { + // 不配对 exit:强制清干净,避免脏状态 + IGNORE_TENANT_DEPTH.remove(); + InterceptorIgnoreHelper.clearIgnoreStrategy(); + log.error("clearIgnoreTenant without matching ignoreTenant (depth underflow), force cleared"); + return; + } + int depth = cur - 1; + if (depth == 0) { + IGNORE_TENANT_DEPTH.remove(); + InterceptorIgnoreHelper.clearIgnoreStrategy(); + } else { + IGNORE_TENANT_DEPTH.set(depth); + // 防御:depth>0 但 MP 标志被硬 clear 时补回 + if (!InterceptorIgnoreHelper.hasIgnoreStrategy()) { + InterceptorIgnoreHelper.handle(IGNORE_TENANT_STRATEGY); + log.warn("IgnoreStrategy missing while ignoreTenant depth={}, re-applied", depth); + } + } + } + + /// 当前线程忽略租户嵌套深度(0 表示未忽略;测试 / 巡检用) + public int getIgnoreTenantDepth() { + return Optional.ofNullable(IGNORE_TENANT_DEPTH.get()).orElse(0); + } + + /// 强制清空 depth 与 MP 忽略标志 + /// + /// 仅用于请求结束兜底(如 Filter finally),业务中间勿调用,以免打断外层 ignore 作用域。 + public void forceClearIgnoreTenant() { + IGNORE_TENANT_DEPTH.remove(); InterceptorIgnoreHelper.clearIgnoreStrategy(); } } diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/IgnoreTenant.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/IgnoreTenant.java index 447fb7335..7d73dedcc 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/IgnoreTenant.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/annotation/IgnoreTenant.java @@ -5,7 +5,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/// # 忽略租户数据隔离注解, 根据是实际情况发现添加注解后, 有多条语句只会第一条生效, 这个所以需要最好放在Manager层使用 +/// # 忽略租户(商户)数据隔离 +/// +/// 在标注的方法/类执行期间关闭 TenantLine 租户行拦截(不拼 mch_no)。 +/// 基于线程内引用计数,支持嵌套:外层 + 内层同时标注时,内层返回后外层仍保持忽略, +/// 直到最外层退出才恢复过滤。 +/// +/// 默认态为开启租户过滤;仅需局部忽略时在对应方法上标注本注解即可。 +/// 不跨线程:异步任务需在子线程入口再次标注或手动 MpUtil.ignoreTenant/clearIgnoreTenant。 /// @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME)