Browse Source

完成自动取消订单任务功能

main
科技小王子 6 days ago
parent
commit
4fc30e53cf
  1. 29
      src/main/java/com/gxwebsoft/common/core/annotation/IgnoreTenant.java
  2. 63
      src/main/java/com/gxwebsoft/common/core/aspect/IgnoreTenantAspect.java
  3. 9
      src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java
  4. 67
      src/main/java/com/gxwebsoft/common/core/context/TenantContext.java
  5. 7
      src/main/java/com/gxwebsoft/shop/service/impl/OrderCancelServiceImpl.java
  6. 19
      src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java
  7. 8
      src/main/resources/application.yml
  8. 182
      src/test/java/com/gxwebsoft/shop/OrderQueryTest.java

29
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;
}

63
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);
}
}
}

9
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);

67
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<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);
}
}
}

7
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<ShopOrder> 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<ShopOrder> list = shopOrderService.list(queryWrapper);
System.out.println("list = " + list.size());
return shopOrderService.list(queryWrapper);
}
@Override
@IgnoreTenant("定时任务需要查询特定租户的超时订单")
public List<ShopOrder> findExpiredUnpaidOrdersByTenant(Integer tenantId, Integer timeoutMinutes, Integer batchSize) {
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(timeoutMinutes);

19
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<ShopOrder> expiredOrders = orderCancelService.findExpiredUnpaidOrders(defaultTimeout, batchSize);
if (expiredOrders.isEmpty()) {
log.debug("没有找到使用默认配置的超时订单");
return 0;
@ -93,14 +95,14 @@ public class OrderAutoCancelTask {
// 过滤掉有特殊租户配置的订单
List<ShopOrder> 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<ShopOrder> 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();

8
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: "其他租户"

182
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<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…
Cancel
Save