diff --git a/src/main/java/com/gxwebsoft/common/core/annotation/IgnoreTenant.java b/src/main/java/com/gxwebsoft/common/core/annotation/IgnoreTenant.java new file mode 100644 index 0000000..ace48fa --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/annotation/IgnoreTenant.java @@ -0,0 +1,29 @@ +package com.gxwebsoft.common.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 忽略租户隔离注解 + * + * 用于标记需要跨租户操作的方法,如定时任务、系统管理等场景 + * + * @author WebSoft + * @since 2025-01-26 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreTenant { + + /** + * 说明信息,用于记录为什么需要忽略租户隔离 + */ + String value() default ""; + + /** + * 是否记录日志 + */ + boolean logAccess() default true; +} diff --git a/src/main/java/com/gxwebsoft/common/core/aspect/IgnoreTenantAspect.java b/src/main/java/com/gxwebsoft/common/core/aspect/IgnoreTenantAspect.java new file mode 100644 index 0000000..da58a3b --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/aspect/IgnoreTenantAspect.java @@ -0,0 +1,63 @@ +package com.gxwebsoft.common.core.aspect; + +import com.gxwebsoft.common.core.annotation.IgnoreTenant; +import com.gxwebsoft.common.core.context.TenantContext; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * 忽略租户隔离切面 + * + * 自动处理 @IgnoreTenant 注解标记的方法,临时禁用租户隔离 + * + * @author WebSoft + * @since 2025-01-26 + */ +@Slf4j +@Aspect +@Component +@Order(1) // 确保在其他切面之前执行 +public class IgnoreTenantAspect { + + @Around("@annotation(com.gxwebsoft.common.core.annotation.IgnoreTenant)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + IgnoreTenant ignoreTenant = method.getAnnotation(IgnoreTenant.class); + + // 记录原始状态 + boolean originalIgnore = TenantContext.isIgnoreTenant(); + + try { + // 设置忽略租户隔离 + TenantContext.setIgnoreTenant(true); + + // 记录日志 + if (ignoreTenant.logAccess()) { + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = method.getName(); + String reason = ignoreTenant.value(); + + if (reason.isEmpty()) { + log.debug("执行跨租户操作: {}.{}", className, methodName); + } else { + log.debug("执行跨租户操作: {}.{} - {}", className, methodName, reason); + } + } + + // 执行目标方法 + return joinPoint.proceed(); + + } finally { + // 恢复原始状态 + TenantContext.setIgnoreTenant(originalIgnore); + } + } +} diff --git a/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java b/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java index 0176982..a579cc5 100644 --- a/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.gxwebsoft.common.core.utils.RedisUtil; +import com.gxwebsoft.common.core.context.TenantContext; import com.gxwebsoft.common.system.entity.User; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; @@ -70,6 +71,12 @@ public class MybatisPlusConfig { @Override public boolean ignoreTable(String tableName) { + // 如果当前上下文设置了忽略租户隔离,则忽略所有表的租户隔离 + if (TenantContext.isIgnoreTenant()) { + return true; + } + + // 系统级别的表始终忽略租户隔离 return Arrays.asList( "sys_tenant", "sys_dictionary", @@ -84,7 +91,7 @@ public class MybatisPlusConfig { // "shop_order_goods", // "shop_goods" // "shop_users", -// "shop_order", +// "shop_order" // 移除shop_order,改为通过注解控制 // "shop_order_info", // "booking_user_invoice" ).contains(tableName); diff --git a/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java b/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java new file mode 100644 index 0000000..42adb46 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java @@ -0,0 +1,67 @@ +package com.gxwebsoft.common.core.context; + +/** + * 租户上下文管理器 + * + * 用于在特定场景下临时禁用租户隔离 + * + * @author WebSoft + * @since 2025-01-26 + */ +public class TenantContext { + + private static final ThreadLocal IGNORE_TENANT = new ThreadLocal<>(); + + /** + * 设置忽略租户隔离 + */ + public static void setIgnoreTenant(boolean ignore) { + IGNORE_TENANT.set(ignore); + } + + /** + * 是否忽略租户隔离 + */ + public static boolean isIgnoreTenant() { + Boolean ignore = IGNORE_TENANT.get(); + return ignore != null && ignore; + } + + /** + * 清除租户上下文 + */ + public static void clear() { + IGNORE_TENANT.remove(); + } + + /** + * 在忽略租户隔离的上下文中执行操作 + * + * @param runnable 要执行的操作 + */ + public static void runIgnoreTenant(Runnable runnable) { + boolean originalIgnore = isIgnoreTenant(); + try { + setIgnoreTenant(true); + runnable.run(); + } finally { + setIgnoreTenant(originalIgnore); + } + } + + /** + * 在忽略租户隔离的上下文中执行操作并返回结果 + * + * @param supplier 要执行的操作 + * @return 操作结果 + */ + public static T callIgnoreTenant(java.util.function.Supplier supplier) { + boolean originalIgnore = isIgnoreTenant(); + try { + setIgnoreTenant(true); + return supplier.get(); + } finally { + setIgnoreTenant(originalIgnore); + } + } +} diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/OrderCancelServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/OrderCancelServiceImpl.java index dd29b9b..603e267 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/OrderCancelServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/OrderCancelServiceImpl.java @@ -1,6 +1,7 @@ package com.gxwebsoft.shop.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.gxwebsoft.common.core.annotation.IgnoreTenant; import com.gxwebsoft.shop.entity.*; import com.gxwebsoft.shop.service.*; import lombok.extern.slf4j.Slf4j; @@ -113,6 +114,7 @@ public class OrderCancelServiceImpl implements OrderCancelService { } @Override + @IgnoreTenant("定时任务需要查询所有租户的超时订单") public List findExpiredUnpaidOrders(Integer timeoutMinutes, Integer batchSize) { LocalDateTime expireTime = LocalDateTime.now().minusMinutes(timeoutMinutes); @@ -123,10 +125,13 @@ public class OrderCancelServiceImpl implements OrderCancelService { .orderByAsc(ShopOrder::getCreateTime) .last("LIMIT " + batchSize); - return shopOrderService.list(queryWrapper); + final List list = shopOrderService.list(queryWrapper); + System.out.println("list = " + list.size()); + return shopOrderService.list(queryWrapper); } @Override + @IgnoreTenant("定时任务需要查询特定租户的超时订单") public List findExpiredUnpaidOrdersByTenant(Integer tenantId, Integer timeoutMinutes, Integer batchSize) { LocalDateTime expireTime = LocalDateTime.now().minusMinutes(timeoutMinutes); diff --git a/src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java b/src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java index 55c1f2b..d1b71ff 100644 --- a/src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java +++ b/src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java @@ -1,5 +1,6 @@ package com.gxwebsoft.shop.task; +import com.gxwebsoft.common.core.annotation.IgnoreTenant; import com.gxwebsoft.shop.config.OrderConfigProperties; import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.service.OrderCancelService; @@ -38,6 +39,7 @@ public class OrderAutoCancelTask { * 开发环境:每1分钟执行一次(便于测试) */ @Scheduled(cron = "${shop.order.auto-cancel.cron:0 */5 * * * ?}") + @IgnoreTenant("定时任务需要处理所有租户的超时订单") public void cancelExpiredOrders() { if (!orderConfig.getAutoCancel().isEnabled()) { log.debug("订单自动取消功能已禁用"); @@ -45,7 +47,7 @@ public class OrderAutoCancelTask { } log.info("开始执行订单自动取消任务..."); - + try { long startTime = System.currentTimeMillis(); int totalCancelledCount = 0; @@ -61,7 +63,7 @@ public class OrderAutoCancelTask { long endTime = System.currentTimeMillis(); long duration = endTime - startTime; - log.info("订单自动取消任务完成,总取消数量: {},默认配置: {},租户配置: {},耗时: {}ms", + log.info("订单自动取消任务完成,总取消数量: {},默认配置: {},租户配置: {},耗时: {}ms", totalCancelledCount, defaultCancelledCount, tenantCancelledCount, duration); // 开发环境输出更详细的日志 @@ -85,7 +87,7 @@ public class OrderAutoCancelTask { log.debug("处理默认超时订单,超时时间: {}分钟,批量大小: {}", defaultTimeout, batchSize); List expiredOrders = orderCancelService.findExpiredUnpaidOrders(defaultTimeout, batchSize); - + if (expiredOrders.isEmpty()) { log.debug("没有找到使用默认配置的超时订单"); return 0; @@ -93,14 +95,14 @@ public class OrderAutoCancelTask { // 过滤掉有特殊租户配置的订单 List ordersToCancel = filterOrdersWithoutTenantConfig(expiredOrders); - + if (ordersToCancel.isEmpty()) { log.debug("过滤后没有需要使用默认配置取消的订单"); return 0; } int cancelledCount = orderCancelService.batchCancelOrders(ordersToCancel); - log.info("默认配置取消订单完成,找到: {}个,过滤后: {}个,成功取消: {}个", + log.info("默认配置取消订单完成,找到: {}个,过滤后: {}个,成功取消: {}个", expiredOrders.size(), ordersToCancel.size(), cancelledCount); return cancelledCount; @@ -131,7 +133,7 @@ public class OrderAutoCancelTask { continue; } - log.debug("处理租户{}的超时订单,超时时间: {}分钟", + log.debug("处理租户{}的超时订单,超时时间: {}分钟", tenantConfig.getTenantId(), tenantConfig.getTimeoutMinutes()); List tenantExpiredOrders = orderCancelService.findExpiredUnpaidOrdersByTenant( @@ -140,8 +142,8 @@ public class OrderAutoCancelTask { if (!tenantExpiredOrders.isEmpty()) { int cancelledCount = orderCancelService.batchCancelOrders(tenantExpiredOrders); totalCancelledCount += cancelledCount; - - log.info("租户{}取消订单完成,找到: {}个,成功取消: {}个", + + log.info("租户{}取消订单完成,找到: {}个,成功取消: {}个", tenantConfig.getTenantId(), tenantExpiredOrders.size(), cancelledCount); } } @@ -175,6 +177,7 @@ public class OrderAutoCancelTask { /** * 手动触发订单自动取消任务(用于测试) */ + @IgnoreTenant("手动触发的定时任务需要处理所有租户的超时订单") public void manualCancelExpiredOrders() { log.info("手动触发订单自动取消任务..."); cancelExpiredOrders(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 665b992..677f0d1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -146,21 +146,21 @@ shop: # 默认超时时间(分钟) default-timeout-minutes: 30 # 定时任务检查间隔(分钟) - check-interval-minutes: 5 + check-interval-minutes: 1 # 批量处理大小 batch-size: 100 # 定时任务执行时间(cron表达式) # 生产环境:每5分钟执行一次 # 开发环境:每1分钟执行一次(便于测试) - cron: "0 */5 * * * ?" + cron: "0 */1 * * * ?" # 开发环境可以设置为: "0 */1 * * * ?" # 租户特殊配置 tenant-configs: - tenant-id: 10324 tenant-name: "百色中学" - timeout-minutes: 60 # 捐款订单给更长的支付时间 - enabled: true + timeout-minutes: 1 # 测试环境:1分钟超时,便于测试 + enabled: true # 使用注解方案,重新启用 # 可以添加更多租户配置 # - tenant-id: 10550 # tenant-name: "其他租户" diff --git a/src/test/java/com/gxwebsoft/shop/OrderQueryTest.java b/src/test/java/com/gxwebsoft/shop/OrderQueryTest.java new file mode 100644 index 0000000..a82a008 --- /dev/null +++ b/src/test/java/com/gxwebsoft/shop/OrderQueryTest.java @@ -0,0 +1,182 @@ +package com.gxwebsoft.shop; + +import com.gxwebsoft.shop.entity.ShopOrder; +import com.gxwebsoft.shop.service.ShopOrderService; +import com.gxwebsoft.shop.service.OrderCancelService; +import com.gxwebsoft.shop.config.OrderConfigProperties; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 订单查询测试 + */ +@Slf4j +@SpringBootTest +public class OrderQueryTest { + + @Autowired + private ShopOrderService shopOrderService; + + @Autowired + private OrderCancelService orderCancelService; + + @Autowired + private OrderConfigProperties orderConfig; + + @Test + public void testQuerySpecificOrder() { + String orderNo = "1957754623870595072"; + log.info("查询订单号: {}", orderNo); + + ShopOrder order = shopOrderService.getByOutTradeNo(orderNo); + if (order != null) { + log.info("订单信息:"); + log.info(" 订单ID: {}", order.getOrderId()); + log.info(" 订单号: {}", order.getOrderNo()); + log.info(" 订单状态: {} (0=待支付, 1=待发货, 2=已取消, 3=已完成)", order.getOrderStatus()); + log.info(" 支付状态: {} (false=未支付, true=已支付)", order.getPayStatus()); + log.info(" 创建时间: {}", order.getCreateTime()); + log.info(" 支付时间: {}", order.getPayTime()); + log.info(" 取消时间: {}", order.getCancelTime()); + log.info(" 租户ID: {}", order.getTenantId()); + log.info(" 订单金额: {}", order.getTotalPrice()); + log.info(" 取消原因: {}", order.getCancelReason()); + + // 检查是否符合自动取消条件 + checkAutoCancelConditions(order); + + // 计算什么时候会符合自动取消条件 + calculateCancelTime(order); + } else { + log.warn("未找到订单号为 {} 的订单", orderNo); + } + } + + private void checkAutoCancelConditions(ShopOrder order) { + log.info("\n=== 检查自动取消条件 ==="); + + // 1. 检查订单状态 + boolean statusOk = (order.getOrderStatus() != null && order.getOrderStatus() == 0); + log.info("1. 订单状态检查: {} (需要为0-待支付)", statusOk ? "✓通过" : "✗不通过"); + + // 2. 检查支付状态 + boolean payStatusOk = (order.getPayStatus() != null && !order.getPayStatus()); + log.info("2. 支付状态检查: {} (需要为false-未支付)", payStatusOk ? "✓通过" : "✗不通过"); + + // 3. 检查创建时间是否超时 + if (order.getCreateTime() != null) { + LocalDateTime createTime = order.getCreateTime(); + LocalDateTime now = LocalDateTime.now(); + + // 获取超时配置 + Integer timeoutMinutes = getTimeoutMinutes(order.getTenantId()); + LocalDateTime expireTime = createTime.plusMinutes(timeoutMinutes); + + boolean timeoutOk = now.isAfter(expireTime); + long minutesElapsed = java.time.Duration.between(createTime, now).toMinutes(); + + log.info("3. 超时检查:"); + log.info(" 创建时间: {}", createTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info(" 当前时间: {}", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info(" 超时配置: {}分钟", timeoutMinutes); + log.info(" 过期时间: {}", expireTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info(" 已过时间: {}分钟", minutesElapsed); + log.info(" 是否超时: {} (需要超过{}分钟)", timeoutOk ? "✓是" : "✗否", timeoutMinutes); + + // 4. 综合判断 + boolean shouldCancel = statusOk && payStatusOk && timeoutOk; + log.info("\n=== 综合判断 ==="); + log.info("是否符合自动取消条件: {}", shouldCancel ? "✓是" : "✗否"); + + if (shouldCancel) { + log.info("该订单符合自动取消任务的处理条件"); + } else { + log.info("该订单不符合自动取消任务的处理条件"); + if (!statusOk) log.info(" - 订单状态不是待支付状态"); + if (!payStatusOk) log.info(" - 订单已支付"); + if (!timeoutOk) log.info(" - 订单未超时"); + } + } else { + log.warn("订单创建时间为空,无法判断是否超时"); + } + } + + private void calculateCancelTime(ShopOrder order) { + log.info("\n=== 计算自动取消时间点 ==="); + + if (order.getCreateTime() == null) { + log.warn("订单创建时间为空,无法计算取消时间"); + return; + } + + // 获取超时配置 + Integer timeoutMinutes = getTimeoutMinutes(order.getTenantId()); + LocalDateTime createTime = order.getCreateTime(); + LocalDateTime cancelTime = createTime.plusMinutes(timeoutMinutes); + LocalDateTime now = LocalDateTime.now(); + + log.info("订单创建时间: {}", createTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info("当前时间: {}", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info("超时配置: {}分钟", timeoutMinutes); + log.info("预计取消时间: {}", cancelTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + + if (now.isBefore(cancelTime)) { + long minutesLeft = java.time.Duration.between(now, cancelTime).toMinutes(); + long secondsLeft = java.time.Duration.between(now, cancelTime).getSeconds() % 60; + log.info("距离自动取消还有: {}分{}秒", minutesLeft, secondsLeft); + log.info("状态: ⏰ 等待中"); + } else { + long minutesOverdue = java.time.Duration.between(cancelTime, now).toMinutes(); + log.info("已超时: {}分钟", minutesOverdue); + log.info("状态: ⚠️ 应该被取消"); + + // 检查为什么没有被取消 + if (order.getPayStatus() != null && order.getPayStatus()) { + log.info("原因: 订单已支付,不会被自动取消"); + } else if (order.getOrderStatus() != null && order.getOrderStatus() != 0) { + log.info("原因: 订单状态不是待支付({}), 不会被自动取消", order.getOrderStatus()); + } else { + log.info("原因: 订单符合取消条件,可能定时任务尚未执行或执行失败"); + } + } + } + + private Integer getTimeoutMinutes(Integer tenantId) { + // 检查是否有租户特殊配置 + List tenantConfigs = orderConfig.getAutoCancel().getTenantConfigs(); + if (tenantConfigs != null) { + for (OrderConfigProperties.TenantCancelConfig config : tenantConfigs) { + if (config.isEnabled() && config.getTenantId().equals(tenantId)) { + return config.getTimeoutMinutes(); + } + } + } + + // 使用默认配置 + return orderConfig.getAutoCancel().getDefaultTimeoutMinutes(); + } + + @Test + public void testFindExpiredOrders() { + log.info("=== 测试查找超时订单 ==="); + + Integer defaultTimeout = orderConfig.getAutoCancel().getDefaultTimeoutMinutes(); + Integer batchSize = orderConfig.getAutoCancel().getBatchSize(); + + log.info("默认超时时间: {}分钟", defaultTimeout); + log.info("批量大小: {}", batchSize); + + List expiredOrders = orderCancelService.findExpiredUnpaidOrders(defaultTimeout, batchSize); + log.info("找到{}个超时订单", expiredOrders.size()); + + for (ShopOrder order : expiredOrders) { + log.info("超时订单: {} - 创建时间: {}", order.getOrderNo(), order.getCreateTime()); + } + } +}