Browse Source

perf(消费服务): 记录有效性与交易流程验证线程处理优化

autumnal_wind 11 tháng trước cách đây
mục cha
commit
d3d965380d

+ 174 - 74
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidation.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.api.domain.vo.*;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.enums.TradeStatusEnum;
@@ -22,8 +23,10 @@ import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
@@ -41,6 +44,7 @@ import java.util.function.Supplier;
 @RequiredArgsConstructor
 public class CardConsumeValidation {
     private static final BigDecimal PERCENT_DIVISOR = new BigDecimal("100.0");
+    private static final long VALIDATION_TIMEOUT = 300;
     private static final Map<String, String> MEAL_TYPE_NAMES = ConsumeConstants.mealNameMap;
     private final CommonCheck commonCheck;
     private final ValidationParam validationParam;
@@ -50,12 +54,14 @@ public class CardConsumeValidation {
                                        RemoteMealTypeVo mealTypeVo,
                                        XfCardLimitedVo cardLimitedVo, Map<String, Boolean> mapCardLimited) {
 
-    // public R<ErrorInfo> cardValidation(CardConsumeValidationContext validationContext) {
+        // public R<ErrorInfo> cardValidation(CardConsumeValidationContext validationContext) {
         // 1.初始化验证上下文
         CardConsumeValidationContext validationContext = CardConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo.getTypeId());
+        CardConsumeValidationContext discountCtx = CardConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo.getTypeId());
+        CardConsumeValidationContext limitedCtx = CardConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo.getTypeId());
 
         // 2. 执行异步验证链
-        R<ErrorInfo> result = executeCardValidationChain(validationContext);
+        R<ErrorInfo> result = executeCardValidationChain(validationContext, discountCtx, limitedCtx);
         // if (R.isSuccess(result)) {
         //     dealCardQuota(validationContext);
         // }
@@ -69,33 +75,40 @@ public class CardConsumeValidation {
         return result;
     }
 
-    private R<ErrorInfo> executeCardValidationChain(CardConsumeValidationContext ctx) {
+    private R<ErrorInfo> executeCardValidationChain(CardConsumeValidationContext ctx, CardConsumeValidationContext discountCtx,
+                                                    CardConsumeValidationContext limitedCtx) {
         // 创建验证任务列表
         List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
-        validationTasks.add(() -> dealCardDiscount(ctx));
-        validationTasks.add(() -> dealCardLimited(ctx));
+
+        validationTasks.add(() -> dealCardDiscount(discountCtx));
+        validationTasks.add(() -> dealCardLimited(limitedCtx));
 
         // 用于存储第一个错误结果
         AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
-
         // 使用CountDownLatch跟踪任务完成
         CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        AtomicBoolean cancelled = new AtomicBoolean(false);
 
         // 提交所有验证任务
-        for (Supplier<R<ErrorInfo>> task : validationTasks) {
+        for (int i = 0, j = validationTasks.size(); i < j; i++) {
+            final int taskIndex = i;
+            final Supplier<R<ErrorInfo>> task = validationTasks.get(taskIndex);
             taskExecutor.execute(() -> {
+                if (cancelled.get()) {
+                    latch.countDown();
+                    return;
+                }
                 try {
                     R<ErrorInfo> result = task.get();
                     // 如果发现错误且尚未设置错误结果
-                    if (result != null && R.isError(result) &&
-                            firstError.compareAndSet(null, result)) {
-                        // 取消其他任务(通过中断)
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    if (result != null && R.isError(result) && firstError.compareAndSet(null, result)) {
+                        log.warn("{} 验证失败: {}", getTaskName(taskIndex), result);
+                        triggerCancellation(cancelled, taskIndex);
                     }
                 } catch (Exception e) {
-                    log.error("卡片消费校验:", e);
+                    log.error("任务 {} 执行异常", getTaskName(taskIndex), e);
                     if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                        triggerCancellation(cancelled, taskIndex);
                     }
                 } finally {
                     latch.countDown();
@@ -105,16 +118,23 @@ public class CardConsumeValidation {
 
         try {
             // 等待所有任务完成或超时
-            if (!latch.await(300, TimeUnit.MILLISECONDS)) {
-                return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+            if (!latch.await(VALIDATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
+                log.warn("任务超时 ({} ms)", VALIDATION_TIMEOUT);
+                triggerCancellation(cancelled, -1);
             }
 
             // 返回第一个发现的错误,如果没有错误则返回成功
-            // return firstError.get() != null ? firstError.get() : R.ok();
-            if(firstError.get() != null){
+            if (firstError.get() != null) {
                 return firstError.get();
             } else {
-                return dealCardQuota(ctx);
+                // 将限次与折扣验证数据合并到主上下文
+                mergeValidationResults(ctx, discountCtx, limitedCtx);
+                // 限额验证
+                R<ErrorInfo> result = dealCardQuota(ctx);
+                if (R.isError(result)) {
+                    return result;
+                }
+                return R.ok();
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -123,8 +143,71 @@ public class CardConsumeValidation {
         }
     }
 
+    private String getTaskName(int taskIndex) {
+        return switch (taskIndex) {
+            case 0 -> "卡类折扣验证";
+            case 1 -> "卡类限次验证";
+            case 2 -> "卡类限额验证";
+            default -> "未知任务";
+        };
+    }
+
+    private void triggerCancellation(AtomicBoolean cancelled, int triggerTask) {
+        // 确保只触发一次取消操作
+        if (cancelled.compareAndSet(false, true)) {
+            // 记录触发取消的任务信息
+            String taskName = getTaskName(triggerTask);
+            if (triggerTask >= 0) {
+                log.warn("任务 [{}] 触发取消操作: {}", triggerTask, taskName);
+            } else {
+                log.warn("超时触发取消操作");
+            }
+
+            // 清空任务队列(取消未开始的任务)
+            int cancelledCount = taskExecutor.getThreadPoolExecutor().getQueue().size();
+            taskExecutor.getThreadPoolExecutor().getQueue().clear();
+            log.info("已取消 {} 个待执行任务", cancelledCount);
+
+            // 中断正在运行的任务
+            // 获取所有活动线程
+            Set<Thread> activeThreads = Thread.getAllStackTraces().keySet();
+            String threadPoolPrefix = taskExecutor.getThreadNamePrefix();
+
+            int interruptedCount = 0;
+
+            for (Thread thread : activeThreads) {
+                // 只中断本线程池创建的线程
+                if (thread.getName().startsWith(threadPoolPrefix)) {
+                    if (!thread.isInterrupted()) {
+                        thread.interrupt();
+                        interruptedCount++;
+                        log.debug("已发送中断信号给线程: {}", thread.getName());
+                    }
+                }
+            }
+
+            log.info("已向 {} 个运行中的任务发送中断信号", interruptedCount);
+        }
+
+    }
+
+    private void mergeValidationResults(CardConsumeValidationContext mainCtx,
+                                        CardConsumeValidationContext discountCtx,
+                                        CardConsumeValidationContext limitedCtx) {
+        // 合并折扣计算结果
+        mainCtx.setHasDiscount(discountCtx.getHasDiscount());
+        mainCtx.setDiscountMoney(discountCtx.getDiscountMoney());
+        // 合并限次计算结果
+        mainCtx.setHasLimited(limitedCtx.getHasLimited());
+    }
+
     // region 卡片折扣处理
     private R<ErrorInfo> dealCardDiscount(CardConsumeValidationContext ctx) {
+        if (Thread.currentThread().isInterrupted()) {
+            log.debug("折扣计算任务被取消,提前退出");
+            return commonCheck.createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "折扣计算任务被取消,提前退出", "折扣计算任务被取消,提前退出");
+        }
         if (!validationParam.getRATE_CONSUME()) {
             log.debug("全局折扣功能未启用,跳过折扣验证");
             return R.ok();
@@ -145,21 +228,26 @@ public class CardConsumeValidation {
         String cardType = String.valueOf(ctx.getCardType());
         Long mealType = Long.valueOf(ctx.getLastMeal());
         RemoteDiscountVo discountVo = discountVos.stream()
-                                          .filter(p -> cardType.equals(p.getCardType())
-                                                           && mealType.equals(p.getMealType())
-                                                           && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
-        ;
-
-        XfCardLimitedVo cardLimitedVo = ctx.getCardLimitedVo();
-        if (discountVo != null) {
-            ctx.setHasDiscount(Boolean.TRUE);
-            ctx.discountMoney = getDisCountMoney(cardLimitedVo, ctx.getConsumeMoney(), discountVo);
+            .filter(p -> cardType.equals(p.getCardType())
+                && mealType.equals(p.getMealType())
+                && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
+        try {
+            XfCardLimitedVo cardLimitedVo = ctx.getCardLimitedVo();
+            if (discountVo != null) {
+                ctx.setHasDiscount(Boolean.TRUE);
+                ctx.discountMoney = getDisCountMoney(cardLimitedVo, ctx.getConsumeMoney(), discountVo);
+                return R.ok();
+            } else {
+                log.debug("卡片[{}]未配置折扣,跳过折扣验证", ctx.getCardNo());
+            }
+
             return R.ok();
-        } else {
-            log.debug("卡片[{}]未配置折扣,跳过折扣验证", ctx.getCardNo());
+        } catch (Exception e) {
+            Thread.currentThread().interrupt();
+            log.debug("折扣计算任务异常", e);
+            return commonCheck.createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "折扣计算任务异常", e.getMessage());
         }
-
-        return R.ok();
     }
 
     /**
@@ -191,13 +279,18 @@ public class CardConsumeValidation {
         // 4. 计算折扣金额(统一处理精度)
         BigDecimal discountFactor = selectedRate.divide(PERCENT_DIVISOR, 2, RoundingMode.HALF_UP);
         return consumeValue
-                   .multiply(discountFactor)
-                   .setScale(2, RoundingMode.HALF_UP);
+            .multiply(discountFactor)
+            .setScale(2, RoundingMode.HALF_UP);
     }
     // endregion
 
     //region 卡片限次处理
     private R<ErrorInfo> dealCardLimited(CardConsumeValidationContext ctx) {
+        if (Thread.currentThread().isInterrupted()) {
+            log.debug("卡片限次验证任务被取消,提前退出");
+            return commonCheck.createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "卡片限次验证任务被取消,提前退出", "卡片限次验证任务被取消,提前退出");
+        }
         if (!validationParam.getXC_CONSUME()) {
             log.debug("全局限次功能未启用,跳过限次验证");
             return R.ok();
@@ -216,33 +309,40 @@ public class CardConsumeValidation {
         }
         Long cardType = ctx.getCardType();
         RemoteLimitedVo limitedVo = limitedCards.stream()
-                                        .filter(p -> cardType.equals(p.getCardType())
-                                                         && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
+            .filter(p -> cardType.equals(p.getCardType())
+                && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
         if (limitedVo == null) {
             return R.ok();
         }
 
         ctx.setHasLimited(Boolean.TRUE);
-        // 日限次校验
-        R<ErrorInfo> dailyCheck = checkDailyLimit(limitedVo, ctx.getCardLimitedVo().getDayCount());
-        if (dailyCheck != null) {
-            return dailyCheck;
-        }
+        try {
+            // 日限次校验
+            R<ErrorInfo> dailyCheck = checkDailyLimit(limitedVo, ctx.getCardLimitedVo().getDayCount());
+            if (dailyCheck != null) {
+                return dailyCheck;
+            }
 
-        // 餐类限次检查
-        return checkMealLimit(limitedVo, ctx.getLastMeal(), ctx.getCardLimitedVo().getMealCount());
+            // 餐类限次检查
+            return checkMealLimit(limitedVo, ctx.getLastMeal(), ctx.getCardLimitedVo().getMealCount());
+        } catch (Exception e) {
+            Thread.currentThread().interrupt();
+            log.debug("卡片限次验证任务异常", e);
+            return commonCheck.createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "卡片限次验证任务异常", e.getMessage());
+        }
     }
 
     /**
      * 检查每日消费次数限制。
      *
-     * @param limitedVo     卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费次数或金额)。
-     *                      通过此对象可以获取卡片的每日消费限制规则。
+     * @param limitedVo       卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费次数或金额)。
+     *                        通过此对象可以获取卡片的每日消费限制规则。
      * @param currentDayCount 当前日期内的消费次数或金额,表示在当天已经发生的消费总量。
      *                        这个参数用于与限制规则进行比较,以判断是否超出限制。
      * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
-     *         如果未超出限制,返回成功结果;
-     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     * 如果未超出限制,返回成功结果;
+     * 如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
      */
     private R<ErrorInfo> checkDailyLimit(RemoteLimitedVo limitedVo, Long currentDayCount) {
         Long dayLimit = limitedVo.getDailyCount();
@@ -255,15 +355,15 @@ public class CardConsumeValidation {
     /**
      * 检查每餐消费次数限制。
      *
-     * @param limitedVo       卡片的限制信息对象,包含有关餐次消费限额的数据(如每餐最大消费次数或金额)。
-     *                        通过此对象可以获取卡片的餐次消费限制规则。
-     * @param mealType        餐类标识符,用于区分不同的餐次类型(例如早餐、午餐、晚餐等)。
-     *                        这个参数决定了要针对哪一类餐次进行消费限制的检查。
+     * @param limitedVo        卡片的限制信息对象,包含有关餐次消费限额的数据(如每餐最大消费次数或金额)。
+     *                         通过此对象可以获取卡片的餐次消费限制规则。
+     * @param mealType         餐类标识符,用于区分不同的餐次类型(例如早餐、午餐、晚餐等)。
+     *                         这个参数决定了要针对哪一类餐次进行消费限制的检查。
      * @param currentMealCount 当前餐次内的消费次数或金额,表示在当前餐次已经发生的消费总量。
      *                         这个参数用于与限制规则进行比较,以判断是否超出限制。
      * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
-     *         如果未超出限制,返回成功结果;
-     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     * 如果未超出限制,返回成功结果;
+     * 如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
      */
     private R<ErrorInfo> checkMealLimit(RemoteLimitedVo limitedVo, String mealType, Long currentMealCount) {
         Map<String, Long> mealLimits = Map.of(
@@ -280,7 +380,7 @@ public class CardConsumeValidation {
 
         if (currentMealCount >= mealLimit) {
             String mealName = MEAL_TYPE_NAMES.getOrDefault(mealType, "未知餐次");
-            return commonCheck.createError(TradeStatusEnum.MealLimitTimes,String.format("卡类%s次数限制", mealName));
+            return commonCheck.createError(TradeStatusEnum.MealLimitTimes, String.format("卡类%s次数限制", mealName));
         }
         return R.ok();
     }
@@ -306,37 +406,37 @@ public class CardConsumeValidation {
         }
         Long cardType = ctx.getCardType();
         RemoteQuotaVo quotaVo = quotaCards.stream()
-                                        .filter(p -> cardType.equals(p.getCardType())
-                                                         && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
+            .filter(p -> cardType.equals(p.getCardType())
+                && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
         if (quotaVo == null) {
             return R.ok();
         }
 
         ctx.setHasQuota(Boolean.TRUE);
         // 日限额校验
-        R<ErrorInfo> dailyCheck = checkDailyQuota(quotaVo, ctx.getCardLimitedVo().getDayMoney(),ctx.getConsumeMoney());
+        R<ErrorInfo> dailyCheck = checkDailyQuota(quotaVo, ctx.getCardLimitedVo().getDayMoney(), ctx.getDiscountMoney());
         if (dailyCheck != null) {
             return dailyCheck;
         }
 
         // 餐类限额检查
-        return checkMealQuota(quotaVo, ctx.getLastMeal(),ctx.getCardLimitedVo().getMealMoney(),ctx.getConsumeMoney());
+        return checkMealQuota(quotaVo, ctx.getLastMeal(), ctx.getCardLimitedVo().getMealMoney(), ctx.getDiscountMoney());
     }
 
     /**
      * 检查每日消费额度限制。
      * 如果设置的日限额>0 并且当天已消费金额+当餐消费的金额比设置的大,则超额不能消费
-     * @param quotaVo     卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费额度或金额)。
-     *                      通过此对象可以获取卡片的每日消费限制规则。
-     * @param dayMoney    当前日期内的消费额度或金额,表示在当天已经发生的消费总量。
-     *                    这个参数用于与限制规则进行比较,以判断是否超出限制。
-     * @param consumeMoney  消费金额
      *
+     * @param quotaVo      卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费额度或金额)。
+     *                     通过此对象可以获取卡片的每日消费限制规则。
+     * @param dayMoney     当前日期内的消费额度或金额,表示在当天已经发生的消费总量。
+     *                     这个参数用于与限制规则进行比较,以判断是否超出限制。
+     * @param consumeMoney 消费金额
      * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
-     *         如果未超出限制,返回成功结果;
-     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     * 如果未超出限制,返回成功结果;
+     * 如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
      */
-    private R<ErrorInfo> checkDailyQuota(RemoteQuotaVo quotaVo, BigDecimal dayMoney,BigDecimal consumeMoney) {
+    private R<ErrorInfo> checkDailyQuota(RemoteQuotaVo quotaVo, BigDecimal dayMoney, BigDecimal consumeMoney) {
         BigDecimal dayQuotaMoney = quotaVo.getDailyMoney();
         if (dayQuotaMoney.compareTo(BigDecimal.ZERO) > 0 && dayQuotaMoney.compareTo(dayMoney.add(consumeMoney)) < 0) {
             return commonCheck.createError(TradeStatusEnum.DayLimitMoney, "卡类日限制额度");
@@ -347,17 +447,17 @@ public class CardConsumeValidation {
     /**
      * 检查每餐消费额度限制。
      *
-     * @param quotaVo       卡片的限制信息对象,包含有关餐次消费限额的数据(如每餐最大消费额度或金额)。
-     *                        通过此对象可以获取卡片的餐次消费限制规则。
-     * @param mealType        餐类标识符,用于区分不同的餐次类型(例如早餐、午餐、晚餐等)。
-     *                        这个参数决定了要针对哪一类餐次进行消费限制的检查。
-     * @param mealMoney 当   当前餐次内的消费额度或金额,表示在当前餐次已经发生的消费总量。
+     * @param quotaVo      卡片的限制信息对象,包含有关餐次消费限额的数据(如每餐最大消费额度或金额)。
+     *                     通过此对象可以获取卡片的餐次消费限制规则。
+     * @param mealType     餐类标识符,用于区分不同的餐次类型(例如早餐、午餐、晚餐等)。
+     *                     这个参数决定了要针对哪一类餐次进行消费限制的检查。
+     * @param mealMoney    当   当前餐次内的消费额度或金额,表示在当前餐次已经发生的消费总量。
      * @param consumeMoney 当 当前餐次的消费金额。表示在当前餐次中再次消费的金额
      * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
-     *         如果未超出限制,返回成功结果;
-     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     * 如果未超出限制,返回成功结果;
+     * 如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
      */
-    private R<ErrorInfo> checkMealQuota(RemoteQuotaVo quotaVo, String mealType, BigDecimal mealMoney,BigDecimal consumeMoney) {
+    private R<ErrorInfo> checkMealQuota(RemoteQuotaVo quotaVo, String mealType, BigDecimal mealMoney, BigDecimal consumeMoney) {
         Map<String, BigDecimal> mealQuotas = Map.of(
             "1", quotaVo.getOneMoney(),
             "2", quotaVo.getTwoMoney(),
@@ -370,7 +470,7 @@ public class CardConsumeValidation {
             return null;
         }
 
-        if (mealQuota.compareTo(BigDecimal.ZERO)>0) {
+        if (mealQuota.compareTo(BigDecimal.ZERO) > 0) {
             if (mealQuota.compareTo(mealMoney.add(consumeMoney)) < 0) {
                 String mealName = MEAL_TYPE_NAMES.getOrDefault(mealType, "未知餐次");
                 return commonCheck.createError(TradeStatusEnum.MealLimitMoney, String.format("卡类%s额度限制", mealName));

+ 4 - 3
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidationContext.java

@@ -134,8 +134,9 @@ public class CardConsumeValidationContext {
 
         // 从缓存获取卡片限制数据
         String strCardNo = String.valueOf(userCardVo.getCardNo());
-        if (RedisUtils.getCacheMapValue(CacheNames.T_XF_CARD_LIMITED, strCardNo) != null) {
-            context.cardLimitedVo = JsonUtils.parseObject(RedisUtils.getCacheMapValue(CacheNames.T_XF_CARD_LIMITED, strCardNo).toString(), XfCardLimitedVo.class);
+        Object objXfCardLimited = RedisUtils.getCacheMapValue(CacheNames.T_XF_CARD_LIMITED, strCardNo);
+        if (objXfCardLimited != null) {
+            context.cardLimitedVo = JsonUtils.parseObject(objXfCardLimited.toString(), XfCardLimitedVo.class);
         }
         // 如果缓存中没有则初始化为当天当餐
         if (ObjectUtil.isEmpty(context.cardLimitedVo)) {
@@ -146,7 +147,7 @@ public class CardConsumeValidationContext {
         if (!lastPayLimitLocalDt.toLocalDate().isEqual(context.lastPayTime.toLocalDate())) {
             // 如果和消费时间不是同天,初始化为当天
             initCardDayLimitedData(context.cardLimitedVo, Long.valueOf(context.lastMeal), context.consumeDate);
-        }else {
+        } else {
             // 不是同一餐,更新到当前餐
             Long lastPayLimitMealType = context.cardLimitedVo.getLastMeal();
             if (!Objects.equals(lastPayLimitMealType, Long.valueOf(context.lastMeal))) {

+ 201 - 69
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CommonCheck.java

@@ -33,6 +33,7 @@ import java.time.Duration;
 import java.util.*;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
@@ -55,10 +56,13 @@ public class CommonCheck {
     // 卡片校验模式 0-userId 1- cardNo
     private static final Integer cardIdMode = 0;
     private static final Integer cardNoMode = 1;
+    // 异步线程执行超时时间,以毫秒为单位
+    private static final long VALIDATION_TIMEOUT = 300;
 
     private final BaseBusiness baseBusiness;
     private final ThreadPoolTaskExecutor taskExecutor;
 
+    //region 账户、卡片与设备验证
     /**
      * 账户、设备有效性验证
      *
@@ -84,31 +88,39 @@ public class CommonCheck {
         // 创建验证任务列表
         List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
 
+        AllowConsumeValidationContext termCtx = AllowConsumeValidationContext.create(ctx.getBo());
+        AllowConsumeValidationContext userAccountCtx = AllowConsumeValidationContext.create(ctx.getBo());
+
         validationTasks.add(() -> checkParam(ctx));
-        validationTasks.add(() -> checkTerm(ctx));
-        validationTasks.add(() -> checkUserAccount(ctx));
+        validationTasks.add(() -> checkTerm(termCtx));
+        validationTasks.add(() -> checkUserAccount(userAccountCtx));
 
         // 用于存储第一个错误结果
         AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
-
         // 使用CountDownLatch跟踪任务完成
         CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        AtomicBoolean cancelled = new AtomicBoolean(false);
 
         // 提交所有验证任务
-        for (Supplier<R<ErrorInfo>> task : validationTasks) {
+        for (int i = 0, j = validationTasks.size(); i < j; i++) {
+            final int taskIndex = i;
+            final Supplier<R<ErrorInfo>> task = validationTasks.get(taskIndex);
             taskExecutor.execute(() -> {
+                if (cancelled.get()) {
+                    latch.countDown();
+                    return;
+                }
                 try {
                     R<ErrorInfo> result = task.get();
                     // 如果发现错误且尚未设置错误结果
-                    if (result != null && R.isError(result) &&
-                        firstError.compareAndSet(null, result)) {
-                        // 取消其他任务(通过中断)
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    if (result != null && R.isError(result) && firstError.compareAndSet(null, result)) {
+                        log.warn("任务 {} 验证失败: {}", getTaskName(taskIndex), result);
+                        triggerCancellation(cancelled, taskIndex);
                     }
                 } catch (Exception e) {
-                    log.info("验证失败-{}", e.getMessage(), e);
+                    log.error("任务 {} 执行异常", getTaskName(taskIndex), e);
                     if (firstError.compareAndSet(null, createError(TradeStatusEnum.SysError))) {
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                        triggerCancellation(cancelled, taskIndex);
                     }
                 } finally {
                     latch.countDown();
@@ -118,19 +130,100 @@ public class CommonCheck {
 
         try {
             // 等待所有任务完成或超时
-            if (!latch.await(300, TimeUnit.MILLISECONDS)) {
-                return createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+            if (!latch.await(VALIDATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
+                log.warn("验证任务超时 ({} ms)", VALIDATION_TIMEOUT);
+                triggerCancellation(cancelled, -1);
             }
-
-            // 返回第一个发现的错误,如果没有错误则返回成功
-            return firstError.get() != null ? firstError.get() : R.ok();
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
-            log.error("error:{}", e.getMessage(), e);
+            log.error("等待任务完成时被中断", e);
+            // 检查是否有部分成功的结果
+            if (firstError.get() != null) {
+                return firstError.get();
+            }
             return createError(TradeStatusEnum.SysError);
         }
+
+        // 返回第一个发现的错误,如果没有错误则返回成功
+        if (firstError.get() != null) {
+            return firstError.get();
+        }
+
+        mergeValidationResults(ctx, termCtx, userAccountCtx);
+        return R.ok();
     }
 
+    private void triggerCancellation(AtomicBoolean cancelled, int triggerTask) {
+        // 确保只触发一次取消操作
+        if (cancelled.compareAndSet(false, true)) {
+            // 记录触发取消的任务信息
+            String taskName = getTaskName(triggerTask);
+            if (triggerTask >= 0) {
+                log.warn("任务 [{}] 触发取消操作: {}", triggerTask, taskName);
+            } else {
+                log.warn("超时触发取消操作");
+            }
+
+            // 清空任务队列(取消未开始的任务)
+            int cancelledCount = taskExecutor.getThreadPoolExecutor().getQueue().size();
+            taskExecutor.getThreadPoolExecutor().getQueue().clear();
+            log.info("已取消 {} 个待执行任务", cancelledCount);
+
+            // 中断正在运行的任务
+            // 获取所有活动线程
+            Set<Thread> activeThreads = Thread.getAllStackTraces().keySet();
+            String threadPoolPrefix = taskExecutor.getThreadNamePrefix();
+
+            int interruptedCount = 0;
+
+            for (Thread thread : activeThreads) {
+                // 只中断本线程池创建的线程
+                if (thread.getName().startsWith(threadPoolPrefix)) {
+                    if (!thread.isInterrupted()) {
+                        thread.interrupt();
+                        interruptedCount++;
+                        log.debug("已发送中断信号给线程: {}", thread.getName());
+                    }
+                }
+            }
+
+            log.info("已向 {} 个运行中的任务发送中断信号", interruptedCount);
+        }
+
+    }
+
+    private void mergeValidationResults(AllowConsumeValidationContext mainCtx,
+                                        AllowConsumeValidationContext termCtx,
+                                        AllowConsumeValidationContext accountCtx) {
+        // 合并终端验证结果
+        if (termCtx.getUseTermVo() != null) {
+            mainCtx.setUseTermVo(termCtx.getUseTermVo());
+        }
+
+        // 合并账户验证结果
+        if (accountCtx.getUserCardVo() != null) {
+            mainCtx.setUserCardVo(accountCtx.getUserCardVo());
+        }
+        if (accountCtx.getUserAccountVo() != null) {
+            mainCtx.setUserAccountVo(accountCtx.getUserAccountVo());
+        }
+        if (accountCtx.getBo() != null) {
+            mainCtx.setBo(accountCtx.getBo());
+        }
+        // 参数验证不需要合并
+        log.debug("验证结果已合并到主上下文");
+    }
+
+    private String getTaskName(int taskIndex) {
+        return switch (taskIndex) {
+            case 0 -> "参数验证";
+            case 1 -> "终端验证";
+            case 2 -> "账户验证";
+            default -> "未知任务";
+        };
+    }
+    //endregion
+
     // region 消费参数校验
 
     /**
@@ -142,21 +235,33 @@ public class CommonCheck {
      * @return 校验结果,包含错误信息或成功标识
      */
     public R<ErrorInfo> checkParam(AllowConsumeValidationContext ctx) {
-        // 1. 校验设备标识参数
-        if (isTerminalInvalid(ctx)) {
+        if (Thread.currentThread().isInterrupted()) {
+            log.debug("参数验证任务被取消,提前退出");
             return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
-                "设备机号不正确", "设备机号必须大于零或MAC地址不能为空!");
+                "参数验证任务被取消,提前退出", "参数验证任务被取消,提前退出");
         }
+        try {
+            // 1. 校验设备标识参数
+            if (isTerminalInvalid(ctx)) {
+                return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                    "设备机号不正确", "设备机号必须大于零或MAC地址不能为空!");
+            }
 
-        // 2. 校验用户标识参数
-        if (isUserIdentifierInvalid(ctx)) {
+            // 2. 校验用户标识参数
+            if (isUserIdentifierInvalid(ctx)) {
+                return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                    "交易人员标识不满足",
+                    "必须提供 [CardNo | FactoryId | userNo | userNumb]  中至少1项来标识交易用户");
+            }
+
+            // 3. 所有参数校验通过
+            return R.ok();
+        } catch (Exception e) {
+            Thread.currentThread().interrupt();
+            log.debug("参数验证任务异常", e);
             return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
-                "交易人员标识不满足",
-                "必须提供 [CardNo | FactoryId | userNo | userNumb]  中至少1项来标识交易用户");
+                "参数验证任务异常", e.getMessage());
         }
-
-        // 3. 所有参数校验通过
-        return R.ok();
     }
 
     /**
@@ -195,36 +300,48 @@ public class CommonCheck {
      * @return 检查结果
      */
     public R<ErrorInfo> checkTerm(AllowConsumeValidationContext ctx) {
-        String msgInfo = ObjectUtil.isEmpty(ctx.getTermMac()) ? ctx.getTermNo().toString() : ctx.getTermMac();
-        // 先从缓存获取数据
-        List<XfTermVo> list = RedisUtils.getCacheList(CacheNames.PT_TERM_LIST);
-        if (CollectionUtil.isEmpty(list)) {
-            return createErrorResponse(1, ApiErrorTypeConstants.OBJECT_NOT_EXISTS,
-                "未获取到设备数据",
-                "获取消费设备失败");
-        }
-        XfTermVo termVo;
-        String mac = ctx.getTermMac();
-        Long termNo = ctx.getTermNo();
-        if (ObjectUtil.isNotEmpty(mac)) {
-            termVo = list.stream().filter(x -> ObjectUtil.equals(x.getTermMac(), mac)).findFirst().orElse(null);
-        } else {
-            termVo = list.stream().filter(x -> ObjectUtil.equals(x.getTermNo(), termNo)).findFirst().orElse(null);
-        }
-        if (ObjectUtil.isEmpty(termVo)) {
-            return createErrorResponse(400, ApiErrorTypeConstants.OBJECT_NOT_EXISTS,
-                "设备不存在",
-                MessageFormat.format("机号或MAC为[{0}]的设备不存在,不允许交易", msgInfo));
-        }
-        // 因为后续处理都是用的机号,如果消费机上传的是mac地址,则将termNo更新到消费业务对象
-        if (termVo != null) {
-            ctx.getBo().setTermNo(termVo.getTermNo());
-            ctx.getBo().setTermName(termVo.getTermName());
+        if (Thread.currentThread().isInterrupted()) {
+            log.debug("设备验证任务被取消,提前退出");
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "设备验证任务被取消,提前退出", "设备证任务被取消,提前退出");
         }
-        // 将消费机数据复制提供后续业务使用
-        ctx.setUseTermVo(termVo);
+        try {
+            String msgInfo = ObjectUtil.isEmpty(ctx.getTermMac()) ? ctx.getTermNo().toString() : ctx.getTermMac();
+            // 先从缓存获取数据
+            List<XfTermVo> list = RedisUtils.getCacheList(CacheNames.PT_TERM_LIST);
+            if (CollectionUtil.isEmpty(list)) {
+                return createErrorResponse(1, ApiErrorTypeConstants.OBJECT_NOT_EXISTS,
+                    "未获取到设备数据",
+                    "获取消费设备失败");
+            }
+            XfTermVo termVo;
+            String mac = ctx.getTermMac();
+            Long termNo = ctx.getTermNo();
+            if (ObjectUtil.isNotEmpty(mac)) {
+                termVo = list.stream().filter(x -> ObjectUtil.equals(x.getTermMac(), mac)).findFirst().orElse(null);
+            } else {
+                termVo = list.stream().filter(x -> ObjectUtil.equals(x.getTermNo(), termNo)).findFirst().orElse(null);
+            }
+            if (ObjectUtil.isEmpty(termVo)) {
+                return createErrorResponse(400, ApiErrorTypeConstants.OBJECT_NOT_EXISTS,
+                    "设备不存在",
+                    MessageFormat.format("机号或MAC为[{0}]的设备不存在,不允许交易", msgInfo));
+            }
+            // 因为后续处理都是用的机号,如果消费机上传的是mac地址,则将termNo更新到消费业务对象
+            if (termVo != null) {
+                ctx.getBo().setTermNo(termVo.getTermNo());
+                ctx.getBo().setTermName(termVo.getTermName());
+            }
+            // 将消费机数据复制提供后续业务使用
+            ctx.setUseTermVo(termVo);
 
-        return R.ok();
+            return R.ok();
+        } catch (Exception e) {
+            Thread.currentThread().interrupt();
+            log.debug("终端验证任务异常", e);
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "终端验证任务异常", e.getMessage());
+        }
     }
     // endregion
 
@@ -237,21 +354,33 @@ public class CommonCheck {
      * @return 如果校验失败,则返回包含错误信息的 R 对象;如果校验成功,则返回表示成功的 R 对象
      */
     public R<ErrorInfo> checkUserAccount(AllowConsumeValidationContext ctx) {
-        long cardNo = ObjectUtil.isEmpty(ctx.getCardNo()) ? 0 : ctx.getCardNo();
-        long userNo = ObjectUtil.isEmpty(ctx.getUserNo()) ? 0 : ctx.getUserNo();
-
-        // 如果卡流水号>0验证卡信息
-        if (cardNo > 0) {
-            return checkCardNo(ctx);
+        if (Thread.currentThread().isInterrupted()) {
+            log.debug("账户与卡片验证任务被取消,提前退出");
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "账户与卡片验证任务被取消,提前退出", "账户与卡片证任务被取消,提前退出");
         }
-        if (userNo > 0) {
-            return checkUserNo(ctx);
+        try {
+            long cardNo = ObjectUtil.isEmpty(ctx.getCardNo()) ? 0 : ctx.getCardNo();
+            long userNo = ObjectUtil.isEmpty(ctx.getUserNo()) ? 0 : ctx.getUserNo();
+
+            // 如果卡流水号>0验证卡信息
+            if (cardNo > 0) {
+                return checkCardNo(ctx);
+            }
+            if (userNo > 0) {
+                return checkUserNo(ctx);
+            }
+            return R.ok();
+        } catch (Exception e) {
+            Thread.currentThread().interrupt();
+            log.debug("账户与卡片验证任务异常", e);
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                "账户与卡片验证任务异常", e.getMessage());
         }
-        return R.ok();
     }
 
     /**
-     * 根据h卡流水号校验账户是否可以消费。
+     * 根据卡流水号校验账户是否可以消费。
      *
      * @param ctx 消费有效性校验上下文,包含消费相关的基础信息(如用户流水号、消费金额等)
      * @return 如果校验失败,则返回包含错误信息的 R 对象;如果校验成功,则返回表示成功的 R 对象
@@ -405,7 +534,7 @@ public class CommonCheck {
     }
 
     /**
-     * 从缓存获取账户信息
+     * 从缓存获取卡片信息
      *
      * @param checkParam 用户标识
      * @param checkMode  校验模式
@@ -413,10 +542,13 @@ public class CommonCheck {
      */
     private RemoteCardVo getCardFromCache(Long checkParam, Integer checkMode) {
         String cacheKey = checkParam.toString();
-        return checkMode.equals(cardIdMode) ?
-            JsonUtils.parseObject(RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, cacheKey).toString(), RemoteCardVo.class) :
-            JsonUtils.parseObject(RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_NO, cacheKey).toString(), RemoteCardVo.class);
-        // RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_NO, cacheKey);
+        Object cacheCard = checkMode.equals(cardIdMode) ?
+            RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, cacheKey) :
+            RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_NO, cacheKey);
+        if (cacheCard == null) {
+            return null;
+        }
+        return JsonUtils.parseObject(cacheCard.toString(), RemoteCardVo.class);
     }
     // endregion