浏览代码

fix(消费服务): 记录有效性验证时,如果卡片的最后支付时间与餐类为空时,给默认值为前一天和0

autumnal_wind 11 月之前
父节点
当前提交
0ebba70e5f

+ 7 - 0
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/util/CardDateUtils.java

@@ -36,4 +36,11 @@ public class CardDateUtils {
     public static LocalDate toLocalDate(Date date) {
         return LocalDate.ofInstant(date.toInstant(), ZoneId.of("Asia/Shanghai"));
     }
+    public static Date toDate(LocalDateTime localDateTime) {
+        return toDate(localDateTime, ZoneId.systemDefault());
+    }
+    public static Date toDate(LocalDateTime localDateTime, ZoneId zoneId) {
+        if (localDateTime == null) return null;
+        return Date.from(localDateTime.atZone(zoneId).toInstant());
+    }
 }

+ 2 - 2
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/BaseBusiness.java

@@ -436,8 +436,8 @@ public class BaseBusiness {
      */
     public void completeUploadRecord(ConsumptionBo bo, RemoteUserAccountVo accountVo) {
         // 消费记录上传完成,还有一些后续工作,不需要知道处理结果,采用异步任务提交
-        taskExecutor.submit(() -> sendConsumeToKafka(bo, accountVo));
-        taskExecutor.submit(() -> sendCloudConsume(bo));
+        // taskExecutor.submit(() -> sendConsumeToKafka(bo, accountVo));
+        // taskExecutor.submit(() -> sendCloudConsume(bo));
     }
 
 

+ 135 - 71
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidation.java

@@ -22,8 +22,7 @@ import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
@@ -51,7 +50,7 @@ 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());
 
@@ -72,56 +71,121 @@ public class CardConsumeValidation {
 
     private R<ErrorInfo> executeCardValidationChain(CardConsumeValidationContext ctx) {
         // 创建验证任务列表
-        List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
+        List<Callable<R<ErrorInfo>>> validationTasks = new ArrayList<>();
         validationTasks.add(() -> dealCardDiscount(ctx));
         validationTasks.add(() -> dealCardLimited(ctx));
 
-        // 用于存储第一个错误结果
+        // 存储第一个错误结果
         AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
-
-        // 使用CountDownLatch跟踪任务完成
-        CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        List<Future<R<ErrorInfo>>> futures = new ArrayList<>();
 
         // 提交所有验证任务
-        for (Supplier<R<ErrorInfo>> task : validationTasks) {
-            taskExecutor.execute(() -> {
+        for (Callable<R<ErrorInfo>> task : validationTasks) {
+            futures.add(taskExecutor.submit(task));
+        }
+        int taskIndex = 0;
+        try {
+            // 等待所有任务完成或出现错误
+            for (Future<R<ErrorInfo>> future : futures) {
+                long starTime = System.currentTimeMillis();
+                if (firstError.get() != null) {
+                    future.cancel(true); // 取消剩余任务
+                    continue;
+                }
+
                 try {
-                    R<ErrorInfo> result = task.get();
-                    // 如果发现错误且尚未设置错误结果
-                    if (result != null && R.isError(result) &&
-                            firstError.compareAndSet(null, result)) {
-                        // 取消其他任务(通过中断)
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    R<ErrorInfo> result = future.get();
+                    if (result != null && R.isError(result)) {
+                        if (firstError.compareAndSet(null, result)) {
+                            // 发现错误,立即取消其他任务
+                            futures.forEach(f -> f.cancel(true)); // 发现错误立即取消其他任务
+                        }
                     }
-                } catch (Exception e) {
-                    log.error("系统错误1", e);
+                } catch (ExecutionException e) {
+                    log.error("{}执行异常", getTaskName(taskIndex), e);
                     if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                        futures.forEach(f -> f.cancel(true));
                     }
                 } finally {
-                    latch.countDown();
+                    log.info("{}结束,耗时:{} ms", getTaskName(taskIndex), System.currentTimeMillis() - starTime);
+                    taskIndex++;
                 }
-            });
-        }
+            }
 
-        try {
-            // 等待所有任务完成或超时
-            //if (!latch.await(VALIDATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
-            //    return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
-            //}
-            latch.await();
-            // 返回第一个发现的错误,如果没有错误则返回成功
-            // return firstError.get() != null ? firstError.get() : R.ok();
-            if(firstError.get() != null){
+            // 如果已有错误,直接返回
+            if (firstError.get() != null) {
                 return firstError.get();
-            } else {
+            }
+
+            // 所有前置任务通过,执行最后一个任务
+            long starTime = System.currentTimeMillis();
+            try {
                 return dealCardQuota(ctx);
+            } catch (Exception e) {
+                log.error("{}执行异常", getTaskName(2), e);
+                return commonCheck.createError(TradeStatusEnum.DayLimitMoney, "日限额验证失败");
+            } finally {
+                log.info("{}结束,耗时:{} ms", getTaskName(2), System.currentTimeMillis() - starTime);
             }
+
         } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("系统错误", e);
+            Thread.currentThread().interrupt(); // 保留中断状态
+            log.error("验证过程被中断", e);
             return commonCheck.createError(TradeStatusEnum.SysError);
         }
+
+        // 使用CountDownLatch跟踪任务完成
+        // CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        //
+        // // 提交所有验证任务
+        // for (Supplier<R<ErrorInfo>> task : validationTasks) {
+        //     taskExecutor.execute(() -> {
+        //         try {
+        //             R<ErrorInfo> result = task.get();
+        //             // 如果发现错误且尚未设置错误结果
+        //             if (result != null && R.isError(result) &&
+        //                     firstError.compareAndSet(null, result)) {
+        //                 // 取消其他任务(通过中断)
+        //                 taskExecutor.getThreadPoolExecutor().getQueue().clear();
+        //             }
+        //         } catch (Exception e) {
+        //             log.error("系统错误1", e);
+        //             if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
+        //                 taskExecutor.getThreadPoolExecutor().getQueue().clear();
+        //             }
+        //         } finally {
+        //             latch.countDown();
+        //         }
+        //     });
+        // }
+        //
+        // try {
+        //     // 等待所有任务完成或超时
+        //     //if (!latch.await(VALIDATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
+        //     //    return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+        //     //}
+        //     latch.await();
+        //     // 返回第一个发现的错误,如果没有错误则返回成功
+        //     // return firstError.get() != null ? firstError.get() : R.ok();
+        //     if(firstError.get() != null){
+        //         return firstError.get();
+        //     } else {
+        //         return dealCardQuota(ctx);
+        //     }
+        // } catch (InterruptedException e) {
+        //     Thread.currentThread().interrupt();
+        //     log.error("系统错误", e);
+        //     return commonCheck.createError(TradeStatusEnum.SysError);
+        // }
+    }
+
+    private String getTaskName(int taskIndex) {
+        return switch (taskIndex) {
+            case 0 -> "卡片折扣计算";
+            case 1 -> "卡片限次验证";
+            case 2 -> "卡片限额验证";
+            default -> "未知任务";
+        };
     }
 
     // region 卡片折扣处理
@@ -146,9 +210,9 @@ 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);
+            .filter(p -> cardType.equals(p.getCardType())
+                && mealType.equals(p.getMealType())
+                && validationParam.getIsUse().equals(p.getStatus())).findFirst().orElse(null);
         ;
 
         XfCardLimitedVo cardLimitedVo = ctx.getCardLimitedVo();
@@ -192,8 +256,8 @@ 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
 
@@ -217,8 +281,8 @@ 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();
         }
@@ -237,13 +301,13 @@ public class CardConsumeValidation {
     /**
      * 检查每日消费次数限制。
      *
-     * @param limitedVo     卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费次数或金额)。
-     *                      通过此对象可以获取卡片的每日消费限制规则。
+     * @param limitedVo       卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费次数或金额)。
+     *                        通过此对象可以获取卡片的每日消费限制规则。
      * @param currentDayCount 当前日期内的消费次数或金额,表示在当天已经发生的消费总量。
      *                        这个参数用于与限制规则进行比较,以判断是否超出限制。
      * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
-     *         如果未超出限制,返回成功结果;
-     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     * 如果未超出限制,返回成功结果;
+     * 如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
      */
     private R<ErrorInfo> checkDailyLimit(RemoteLimitedVo limitedVo, Long currentDayCount) {
         Long dayLimit = limitedVo.getDailyCount();
@@ -256,15 +320,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(
@@ -281,7 +345,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();
     }
@@ -307,8 +371,8 @@ 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();
         }
@@ -327,17 +391,17 @@ public class CardConsumeValidation {
     /**
      * 检查每日消费额度限制。
      * 如果设置的日限额>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, "卡类日限制额度");
@@ -348,17 +412,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(),
@@ -371,7 +435,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));

+ 11 - 1
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CommonCheck.java

@@ -21,6 +21,7 @@ import org.dromara.common.core.enums.UserAccountStatusEnum;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.common.util.CardDateUtils;
 import org.dromara.server.consume.business.BaseBusiness;
 import org.dromara.server.consume.business.InitBusiness;
 import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
@@ -31,6 +32,8 @@ import org.springframework.stereotype.Service;
 import java.math.BigDecimal;
 import java.text.MessageFormat;
 import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -483,7 +486,14 @@ public class CommonCheck {
                 MessageFormat.format("流水号或用户Id或物理卡号为[{0}]的卡片状态不正确,不允许交易", checkParam));
 
         }
-
+        if (cardVo.getLastPay() == null) {
+            LocalDateTime beforeTime = LocalDateTime.now().plusDays(-1);
+            Date lastPay = CardDateUtils.toDate(beforeTime, ZoneId.of("Asia/Shanghai"));
+            cardVo.setLastPay(lastPay);
+        }
+        if(cardVo.getLastMeal() == null) {
+            cardVo.setLastMeal(0L);
+        }
         ctx.setUserCardVo(cardVo);
 
         return R.ok();

+ 8 - 8
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/task/InitTasks.java

@@ -32,14 +32,14 @@ public class InitTasks implements ApplicationRunner {
     public void run(ApplicationArguments args) throws Exception {
         log.info("初始化消费验证基础数据");
         long startTime = System.currentTimeMillis();
-        // initBusiness.initGlobalData();
-        // initBusiness.initTermInfo();
-        // initBusiness.initMealTypeInfo();
-        // initBusiness.initDiscountAndOther();
-        // initBusiness.initUserCard();
-        // initBusiness.initUserAccount();
-        // initBusiness.initXfCardLimited();
-        // initBusiness.initUserBalance();
+        initBusiness.initGlobalData();
+        initBusiness.initTermInfo();
+        initBusiness.initMealTypeInfo();
+        initBusiness.initDiscountAndOther();
+        initBusiness.initUserCard();
+        initBusiness.initUserAccount();
+        initBusiness.initXfCardLimited();
+        initBusiness.initUserBalance();
         validationParam.refresh();
 
         log.info("初始化消费验证基础数据完成。耗时:{} ms", System.currentTimeMillis() - startTime);