8 changed files with 370 additions and 14 deletions
@ -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; |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,67 @@ |
|||||
|
package com.gxwebsoft.common.core.context; |
||||
|
|
||||
|
/** |
||||
|
* 租户上下文管理器 |
||||
|
* |
||||
|
* 用于在特定场景下临时禁用租户隔离 |
||||
|
* |
||||
|
* @author WebSoft |
||||
|
* @since 2025-01-26 |
||||
|
*/ |
||||
|
public class TenantContext { |
||||
|
|
||||
|
private static final ThreadLocal<Boolean> 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> T callIgnoreTenant(java.util.function.Supplier<T> supplier) { |
||||
|
boolean originalIgnore = isIgnoreTenant(); |
||||
|
try { |
||||
|
setIgnoreTenant(true); |
||||
|
return supplier.get(); |
||||
|
} finally { |
||||
|
setIgnoreTenant(originalIgnore); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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<OrderConfigProperties.TenantCancelConfig> 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<ShopOrder> expiredOrders = orderCancelService.findExpiredUnpaidOrders(defaultTimeout, batchSize); |
||||
|
log.info("找到{}个超时订单", expiredOrders.size()); |
||||
|
|
||||
|
for (ShopOrder order : expiredOrders) { |
||||
|
log.info("超时订单: {} - 创建时间: {}", order.getOrderNo(), order.getCreateTime()); |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue