Explorar o código

Merge remote-tracking branch 'origin/master'

xiari hai 11 meses
pai
achega
8feac8d304
Modificáronse 67 ficheiros con 3652 adicións e 1796 borrados
  1. 4 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteCardService.java
  2. 3 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteMealTypeService.java
  3. 8 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtAccountService.java
  4. 8 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java
  5. 1 1
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteUserAccountService.java
  6. 14 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfDiscountService.java
  7. 13 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfLimitedService.java
  8. 15 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfQuotaService.java
  9. 2 0
      ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java
  10. 22 22
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/ApiErrorTypeConstants.java
  11. 27 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  12. 19 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/ErrorResult.java
  13. 2 1
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TradeStatusEnum.java
  14. 19 4
      ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java
  15. 8 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteMealTypeServiceImpl.java
  16. 9 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtAccountServiceImpl.java
  17. 7 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java
  18. 2 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/IPtRoomService.java
  19. 5 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/impl/PtRoomServiceImpl.java
  20. 2 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/lock/LockBusiness.java
  21. 24 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/domain/convert/CardRemoteConvert.java
  22. 15 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/dubbo/RemoteCardServiceImpl.java
  23. 4 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/service/IPtCardService.java
  24. 15 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/service/impl/PtCardServiceImpl.java
  25. 22 2
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfDiscountServiceImpl.java
  26. 24 13
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfLimitedServiceImpl.java
  27. 20 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfQuotaServiceImpl.java
  28. 11 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/dubbo/RemoteUserAccountServiceImpl.java
  29. 8 1
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java
  30. 6 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteDeptServiceImpl.java
  31. 2 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysDeptService.java
  32. 6 1
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java
  33. 22 0
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/constant/ConsumeConstants.java
  34. 6 0
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/domain/consume/bo/ConsumptionBo.java
  35. 13 0
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/util/CardDateUtils.java
  36. 212 155
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/BaseBusiness.java
  37. 8 2
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CardBusiness.java
  38. 0 1304
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java
  39. 61 58
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java
  40. 6 1
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/EmployeeBusiness.java
  41. 284 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/InitBusiness.java
  42. 138 125
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/TermBusiness.java
  43. 87 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/cache/ValidationParam.java
  44. 56 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/AllowConsumeValidationContext.java
  45. 383 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidation.java
  46. 227 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidationContext.java
  47. 547 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CommonCheck.java
  48. 367 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeRequestCheck.java
  49. 302 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeUploadCheck.java
  50. 76 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/TermConsumeValidationContext.java
  51. 33 20
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/AuthController.java
  52. 2 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/ConsumeController.java
  53. 151 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/InitDataController.java
  54. 52 31
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/TermsController.java
  55. 29 6
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v2/AuthController.java
  56. 10 2
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/convert/RemoteVoConvert.java
  57. 4 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IPtBagService.java
  58. 4 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfCardLimitedService.java
  59. 2 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfConsumeDetailService.java
  60. 1 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfTermService.java
  61. 88 33
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/PtBagServiceImpl.java
  62. 23 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfCardLimitedServiceImpl.java
  63. 19 4
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfConsumeDetailServiceImpl.java
  64. 25 9
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfTermServiceImpl.java
  65. 42 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/task/InitTasks.java
  66. 24 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/task/ScheduledTasks.java
  67. 1 1
      ruoyi-server/ruoyi-server-consume/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+ 4 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteCardService.java

@@ -5,6 +5,7 @@ import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
 
 /**
  * name: RemoteBagService
@@ -83,4 +84,7 @@ public interface RemoteCardService {
 
     RemoteCardVo insertOrUpdateLocalCard(RemoteCardBo bo);
 
+    List<RemoteCardVo> selectNormalCards();
+
+    Boolean updateCardDayData(RemoteCardVo remoteCardVo);
 }

+ 3 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteMealTypeService.java

@@ -5,6 +5,7 @@ import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 
 import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 餐类服务
@@ -26,4 +27,6 @@ public interface RemoteMealTypeService {
      * @return 餐类信息
      */
     RemoteMealTypeVo queryMealTypeVoByTime(Date mealTime);
+
+    List<RemoteMealTypeVo> selectMealTypeList();
 }

+ 8 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtAccountService.java

@@ -4,6 +4,7 @@ import org.dromara.backstage.api.domain.bo.RemotePtAccountBo;
 import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 结算账户服务层
@@ -22,4 +23,11 @@ public interface RemotePtAccountService {
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception;
 
     RemotePtAccountVo selectVoById(Long id) ;
+
+    /**
+     * 查询结算账户列表。
+     *
+     * @return 包含结算账户视图对象的列表,每个对象表示一个结算账户的详细信息
+     */
+    List<RemotePtAccountVo> selectAccountList();
 }

+ 8 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java

@@ -5,6 +5,7 @@ import org.dromara.backstage.api.domain.bo.RemotePtRoomBo;
 import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 房间信息远程调用接口
@@ -30,4 +31,11 @@ public interface RemotePtRoomService {
      * @return 更新结果
      */
     Boolean updateGuestRoomStatus(String roomCode, String tenantId, String roomStatus);
+
+    /**
+     * 查询房间列表信息。
+     *
+     * @return 包含房间信息的视图对象列表,每个对象代表一个房间的详细信息
+     */
+    List<RemotePtRoomVo> selectRoomList();
 }

+ 1 - 1
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteUserAccountService.java

@@ -87,5 +87,5 @@ public interface RemoteUserAccountService {
 
     RemoteUserAccountVo getUserAccountVoBy(Long userId);
 
-
+    List<Long> getUserAccountIdList();
 }

+ 14 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfDiscountService.java

@@ -52,4 +52,18 @@ public interface RemoteXfDiscountService {
      * @return 卡类折扣信息
      */
     RemoteDiscountVo queryDisCountByCardType(Integer cardType, String mealType);
+
+    /**
+     * 获取折扣卡类清单
+     *
+     * @return 折扣卡类清单
+     */
+    List<RemoteDiscountVo> selectDiscountCards();
+
+    /**
+     * 获取折扣设备Id清单
+     *
+     * @return 折扣设备Id清单
+     */
+    List<Long> selectDiscountTermIds();
 }

+ 13 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfLimitedService.java

@@ -52,4 +52,17 @@ public interface RemoteXfLimitedService {
      */
     RemoteLimitedVo queryLimitedByCardType(Integer cardType);
 
+    /**
+     * 获取限次卡类清单
+     *
+     * @return 限次卡类清单
+     */
+    List<RemoteLimitedVo> selectLimitedCards();
+
+    /**
+     * 获取限次设备Id清单
+     *
+     * @return 限次设备清单
+     */
+    List<Long> selectLimitedTermIds();
 }

+ 15 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteXfQuotaService.java

@@ -51,4 +51,19 @@ public interface RemoteXfQuotaService {
      * @return 卡类限额信息
      */
     RemoteQuotaVo queryQuotaByCardType(Integer cardType);
+
+    /**
+     * 获取限额卡类清单
+     *
+     * @return 限额卡类清单
+     */
+    List<RemoteQuotaVo> selectQuotaCards();
+
+    /**
+     * 芪取限额设备Id清单
+     *
+     * @return 限额设备Id清单
+     */
+    List<Long> selectQuotaTermIds();
+
 }

+ 2 - 0
ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/RemoteDeptService.java

@@ -123,4 +123,6 @@ public interface RemoteDeptService {
     Boolean insertOrUpdateLocalDept(RemoteDeptBo remoteDeptBo);
 
     Boolean deleteLocalDept(Long deptId);
+
+    List<RemoteDeptVo> selectDeptList();
 }

+ 22 - 22
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/ApiErrorTypeConstants.java

@@ -5,26 +5,26 @@ package org.dromara.common.core.constant;
  */
 public interface ApiErrorTypeConstants {
     String EXCEPTION = "EXCEPTION";
-     String BAD_REQUEST = "BAD_REQUEST";
-     String NOT_FOUND = "NOT_FOUND";
-     String PARAM_ERROR = "PARAM_ERROR";
-     String CONSUME_CHECK_FAIL = "CONSUME_CHECK_FAIL";
-     String CONSUME_FAIL = "CONSUME_FAIL";
-     String REFUND_FAIL = "REFUND_FAIL";
-     String CONDITION_DISSATISFY = "CONDITION_DISSATISFY";
-     String INVALID_SIGN = "INVALID_SIGN";
-     String INVALID_TOKEN = "INVALID_TOKEN";
-     String INVALID_MAC = "INVALID_MAC";
-     String AUTHENTICATION_FAIL = "AUTHENTICATION_FAIL";
-     String SQLDB_COMMIT_FAIL = "SQLDB_COMMIT_FAIL";
-     String REMOTE_OPEN_DOOR_FAIL = "REMOTE_OPEN_DOOR_FAIL";
-     String RECORD_IS_EXISTS = "RECORD_IS_EXISTS";
-     String RECORD_IS_DEALED = "RECORD_IS_DEALED";
-     String OBJECT_NOT_EXISTS = "OBJECT_NOT_EXISTS";
-     String CARD_NOT_EXISTS = "CARD_NOT_EXISTS";
-     String CARD_BAGS_NOT_EXISTS = "CARD_BAGS_NOT_EXISTS";
-     String CARD_STATUS_NOT_NORMAL = "CARD_STATUS_NOT_NORMAL";
-     String CREDIT_BACK_ID_NOT_EXISTS = "CREDIT_BACK_ID_NOT_EXISTS";
-     String DEAL_NOT_EXISTS = "DEAL_NOT_EXISTS";
-     String OPERATION_TOO_FREQUENT = "OPERATION_TOO_FREQUENT";
+    String BAD_REQUEST = "BAD_REQUEST";
+    String NOT_FOUND = "NOT_FOUND";
+    String PARAM_ERROR = "PARAM_ERROR";
+    String CONSUME_CHECK_FAIL = "CONSUME_CHECK_FAIL";
+    String CONSUME_FAIL = "CONSUME_FAIL";
+    String REFUND_FAIL = "REFUND_FAIL";
+    String CONDITION_DISSATISFY = "CONDITION_DISSATISFY";
+    String INVALID_SIGN = "INVALID_SIGN";
+    String INVALID_TOKEN = "INVALID_TOKEN";
+    String INVALID_MAC = "INVALID_MAC";
+    String AUTHENTICATION_FAIL = "AUTHENTICATION_FAIL";
+    String SQLDB_COMMIT_FAIL = "SQLDB_COMMIT_FAIL";
+    String REMOTE_OPEN_DOOR_FAIL = "REMOTE_OPEN_DOOR_FAIL";
+    String RECORD_IS_EXISTS = "RECORD_IS_EXISTS";
+    String RECORD_IS_DEALED = "RECORD_IS_DEALED";
+    String OBJECT_NOT_EXISTS = "OBJECT_NOT_EXISTS";
+    String CARD_NOT_EXISTS = "CARD_NOT_EXISTS";
+    String CARD_BAGS_NOT_EXISTS = "CARD_BAGS_NOT_EXISTS";
+    String CARD_STATUS_NOT_NORMAL = "CARD_STATUS_NOT_NORMAL";
+    String CREDIT_BACK_ID_NOT_EXISTS = "CREDIT_BACK_ID_NOT_EXISTS";
+    String DEAL_NOT_EXISTS = "DEAL_NOT_EXISTS";
+    String OPERATION_TOO_FREQUENT = "OPERATION_TOO_FREQUENT";
 }

+ 27 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java

@@ -97,11 +97,22 @@ public interface CacheNames {
      * 用户卡片
      */
     String PT_USER_CARD = "pt_user_card";
+
     /**
      * 用户卡片
      */
     String PT_USER_CARD_NO = "pt_user_card_no";
 
+    /**
+     * 用户卡片
+     */
+    String PT_USER_CARD_FACTORYID = "pt_user_card_factoryId";
+
+    /**
+     * 用户卡片
+     */
+    String PT_USER_CARD_USER_ID = "pt_user_card_user_id";
+
     /**
      * 营业时段/餐类
      */
@@ -164,4 +175,20 @@ public interface CacheNames {
     String T_XF_DISCOUNTTERM = "t_xf_discountTerm";
 
     String T_XF_DISCOUNT = "t_xf_discount";
+
+    String T_XF_CARD_LIMITED = "t_xf_card_limited";
+
+    String USER_TOTAL_BALANCE = "user_total_balance";
+
+    /**
+     * 一卡通账户
+     */
+    String PT_USER_ACCOUNT_ID = "pt_user_account_id";
+    String PT_USER_ACCOUNT_NO = "pt_user_account_no";
+    /**
+     * 营业时段/餐类
+     */
+    String PT_MEAL_TYPE_LIST = "pt_meal_type_list";
+
+    String XF_ORIGINAL_ID = "xf_original_id";
 }

+ 19 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/model/ErrorResult.java

@@ -3,7 +3,9 @@ package org.dromara.common.core.domain.model;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 
 import java.io.Serial;
 import java.io.Serializable;
@@ -50,4 +52,21 @@ public class ErrorResult implements Serializable {
 
         return error;
     }
+
+    // 封装400响应
+    public static ResponseEntity<Object> badRequestResponse(String message) {
+        ErrorResult result = new ErrorResult();
+        result.setStatusCode(HttpStatus.BAD_REQUEST.value());
+        result.setMessage(message);
+        result.getErrors().add(new ErrorInfo(1, message, ApiErrorTypeConstants.BAD_REQUEST, null));
+        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
+    }
+
+    // 封装500响应
+    public static ResponseEntity<Object> innternalErrorResponse(String message) {
+        ErrorResult result = new ErrorResult();
+        result.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
+        result.setMessage(message);
+        return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
 }

+ 2 - 1
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TradeStatusEnum.java

@@ -31,7 +31,8 @@ public enum TradeStatusEnum {
     NonScanCode("没有扫码数据", (byte)0x62),
     SysError("系统错误", (byte)0x63),
     MidasApiError("米大师接口调用失败", (byte)0x64),
-    NoDish("无订餐数据", (byte)0x70);
+    NoDish("无订餐数据", (byte)0x70),
+    VALIDATION_TIMEOUT("交易超时",(byte)0x80);
 
     private String name; // 名称
     private Byte value; // 值

+ 19 - 4
ruoyi-common/ruoyi-common-redis/src/main/java/org/dromara/common/redis/utils/RedisUtils.java

@@ -6,10 +6,7 @@ import org.dromara.common.core.utils.SpringUtils;
 import org.redisson.api.*;
 
 import java.time.Duration;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -546,4 +543,22 @@ public class RedisUtils {
         RKeys rKeys = CLIENT.getKeys();
         return rKeys.countExists(key) > 0;
     }
+
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @return Hash对象集合
+     */
+    public static <T> List<T> getMultiCacheMapValue(final String key) {
+        RMap<String, T> rMap = CLIENT.getMap(key);
+        return new ArrayList<>(rMap.readAllValues());
+    }
+
+    public static <K, V> List<String> getAllCacheMapKey(final String key) {
+        RMap<K, V> rMap = CLIENT.getMap(key);
+        Set<K> keys = rMap.keySet();
+
+        return keys.stream().map(K::toString).collect(Collectors.toList());
+    }
 }

+ 8 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteMealTypeServiceImpl.java

@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
 
 import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 
 @RequiredArgsConstructor
 @Service
@@ -52,4 +53,11 @@ public class RemoteMealTypeServiceImpl implements RemoteMealTypeService {
         PtMealTypeVo vo = mealTypeService.queryVoByTime(DateUtil.format(mealTime, "HH:mm:ss"));
         return MapstructUtils.convert(vo, RemoteMealTypeVo.class);
     }
+
+    @Override
+    public List<RemoteMealTypeVo> selectMealTypeList() {
+        PtMealTypeBo bo = new PtMealTypeBo();
+        List<PtMealTypeVo> list = mealTypeService.queryList(bo);
+        return MapstructUtils.convert(list, RemoteMealTypeVo.class);
+    }
 }

+ 9 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtAccountServiceImpl.java

@@ -13,6 +13,7 @@ import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 结算账户业务实现类
@@ -46,4 +47,12 @@ public class RemotePtAccountServiceImpl implements RemotePtAccountService {
         PtAccountVo vo = accountService.queryById(id);
         return MapstructUtils.convert(vo,RemotePtAccountVo.class);
     }
+
+    @Override
+    public List<RemotePtAccountVo> selectAccountList() {
+        PtAccountBo bo = new PtAccountBo();
+
+        List<PtAccountVo> list =  accountService.queryList(bo);
+        return MapstructUtils.convert(list,RemotePtAccountVo.class);
+    }
 }

+ 7 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java

@@ -16,6 +16,7 @@ import org.dromara.common.tenant.helper.TenantHelper;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * 房间信息远程服务实现类
@@ -64,4 +65,10 @@ public class RemotePtRoomServiceImpl implements RemotePtRoomService {
     public Boolean updateGuestRoomStatus(String roomCode, String tenantId, String roomStatus) {
         return TenantHelper.ignore(() -> roomService.updateGuestRoomStatus(roomCode, tenantId, roomStatus));
     }
+
+    @Override
+    public List<RemotePtRoomVo> selectRoomList() {
+        List<PtRoomVo> list = roomService.queryList();
+        return MapstructUtils.convert(list,RemotePtRoomVo.class);
+    }
 }

+ 2 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/IPtRoomService.java

@@ -131,4 +131,6 @@ public interface IPtRoomService {
      * @return 房间信息
      */
     PtRoomVo selectHotelRoom(String roomCode);
+
+    List<PtRoomVo> queryList();
 }

+ 5 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/impl/PtRoomServiceImpl.java

@@ -500,4 +500,9 @@ public class PtRoomServiceImpl implements IPtRoomService {
     public PtRoomVo selectHotelRoom(String roomCode) {
         return baseMapper.selectHotelRoom(roomCode);
     }
+
+    @Override
+    public List<PtRoomVo> queryList() {
+        return baseMapper.selectVoList();
+    }
 }

+ 2 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/lock/LockBusiness.java

@@ -186,6 +186,7 @@ public class LockBusiness {
             return 0;
         }
     }
+
     public Integer queryOpenDoorRecordByRoomCode(String RoomCode,int pageNo) {
         PtRoomVo vo = roomService.selectHotelRoom(RoomCode);
         if (ObjectUtil.isNotEmpty(vo)) {
@@ -194,6 +195,7 @@ public class LockBusiness {
             return 0;
         }
     }
+
     /**
      * 同步处理开门记录(指定门锁)
      *

+ 24 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/domain/convert/CardRemoteConvert.java

@@ -0,0 +1,24 @@
+package org.dromara.backstage.cardCenter.domain.convert;
+
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.cardCenter.domain.bo.PtCardBo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * 消费业务对象转换类
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-06
+ * @since JDK17
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface CardRemoteConvert {
+    CardRemoteConvert INSTANCE = Mappers.getMapper(CardRemoteConvert.class);
+
+    PtCardBo toBoFromRemoteVo(RemoteCardVo source);
+}

+ 15 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/dubbo/RemoteCardServiceImpl.java

@@ -7,6 +7,7 @@ import org.dromara.backstage.api.RemoteCardService;
 import org.dromara.backstage.api.domain.bo.RemoteCardBo;
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 import org.dromara.backstage.cardCenter.domain.bo.PtCardBo;
+import org.dromara.backstage.cardCenter.domain.convert.CardRemoteConvert;
 import org.dromara.backstage.cardCenter.service.IPtCardService;
 import org.dromara.backstage.domain.vo.card.PtCardVo;
 import org.dromara.common.core.utils.MapstructUtils;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
 
 /**
  * name: RemoteBagServiceImpl
@@ -115,5 +117,18 @@ public class RemoteCardServiceImpl implements RemoteCardService {
     public RemoteCardVo insertOrUpdateLocalCard(RemoteCardBo bo) {
         return null;
     }
+
+    @Override
+    public List<RemoteCardVo> selectNormalCards() {
+        List<PtCardVo> list = cardService.selectNormalCards();
+        return MapstructUtils.convert(list, RemoteCardVo.class);
+    }
+
+    @Override
+    public Boolean updateCardDayData(RemoteCardVo remoteCardVo) {
+        PtCardBo bo = CardRemoteConvert.INSTANCE.toBoFromRemoteVo(remoteCardVo);
+        return cardService.updateCardDayData(bo);
+
+    }
 }
 

+ 4 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/service/IPtCardService.java

@@ -219,4 +219,8 @@ public interface IPtCardService {
     Boolean cancelCard(Long userId, Long operatorId);
 
     PtCardVo insertOrUpdateLocalCard(PtCardBo bo);
+
+    List<PtCardVo> selectNormalCards();
+
+    Boolean updateCardDayData(PtCardBo bo);
 }

+ 15 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/cardCenter/service/impl/PtCardServiceImpl.java

@@ -587,4 +587,19 @@ public class PtCardServiceImpl implements IPtCardService {
         }
         return null;
     }
+
+    @Override
+    public List<PtCardVo> selectNormalCards() {
+        LambdaQueryWrapper<PtCard> lqw = new LambdaQueryWrapper<PtCard>()
+                                             .eq(PtCard::getStatus, CardStatusEnum.NORMAL.code().toString())
+                                             .eq(PtCard::getDelFlag,"0")
+                                             .ge(PtCard::getLifespan,DateUtil.date());
+        return baseMapper.selectVoList(lqw);
+    }
+
+    @Override
+    public Boolean updateCardDayData(PtCardBo bo) {
+        PtCard update = MapstructUtils.convert(bo, PtCard.class);
+        return baseMapper.updateById(update)>0;
+    }
 }

+ 22 - 2
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfDiscountServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.consumption.dubbo;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
@@ -8,6 +9,7 @@ import org.dromara.backstage.api.RemoteXfDiscountService;
 import org.dromara.backstage.api.domain.bo.RemoteXfDiscountBo;
 import org.dromara.backstage.api.domain.vo.RemoteDiscountVo;
 import org.dromara.backstage.consumption.domain.bo.XfDiscountBo;
+import org.dromara.backstage.consumption.domain.bo.XfDiscountTermBo;
 import org.dromara.backstage.consumption.domain.vo.XfDiscountTermVo;
 import org.dromara.backstage.consumption.domain.vo.XfDiscountVo;
 import org.dromara.backstage.consumption.service.IXfDiscountService;
@@ -43,7 +45,7 @@ public class RemoteXfDiscountServiceImpl implements RemoteXfDiscountService {
 
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception {
-        return discountService.deleteWithValidByIds(ids,isValid);
+        return discountService.deleteWithValidByIds(ids, isValid);
     }
 
     @Override
@@ -59,7 +61,7 @@ public class RemoteXfDiscountServiceImpl implements RemoteXfDiscountService {
 
     @Override
     public Long queryDisCountTermIdByTermId(Long termId) {
-        XfDiscountTermVo vo=  discountTermService.queryByTermId(termId);
+        XfDiscountTermVo vo = discountTermService.queryByTermId(termId);
         if (ObjectUtil.isNotEmpty(vo)) {
             return vo.getDiscountTermId();
         }
@@ -74,4 +76,22 @@ public class RemoteXfDiscountServiceImpl implements RemoteXfDiscountService {
         }
         return null;
     }
+
+    @Override
+    public List<RemoteDiscountVo> selectDiscountCards() {
+        XfDiscountBo bo = new XfDiscountBo();
+        bo.setStatus("1");
+        List<XfDiscountVo> list = discountService.queryList(bo);
+        return MapstructUtils.convert(list, RemoteDiscountVo.class);
+    }
+
+    @Override
+    public List<Long> selectDiscountTermIds() {
+        XfDiscountTermBo bo = new XfDiscountTermBo();
+        List<XfDiscountTermVo> list = discountTermService.queryList(bo);
+        if (CollUtil.isNotEmpty(list)) {
+            return list.stream().map(XfDiscountTermVo::getTermId).toList();
+        }
+        return null;
+    }
 }

+ 24 - 13
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfLimitedServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.consumption.dubbo;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
@@ -8,6 +9,7 @@ import org.dromara.backstage.api.RemoteXfLimitedService;
 import org.dromara.backstage.api.domain.bo.RemoteXfLimitedBo;
 import org.dromara.backstage.api.domain.vo.RemoteLimitedVo;
 import org.dromara.backstage.consumption.domain.bo.XfLimitedBo;
+import org.dromara.backstage.consumption.domain.bo.XfLimitedTermBo;
 import org.dromara.backstage.consumption.domain.vo.XfLimitedTermVo;
 import org.dromara.backstage.consumption.domain.vo.XfLimitedVo;
 import org.dromara.backstage.consumption.service.IXfLimitedService;
@@ -44,7 +46,7 @@ public class RemoteXfLimitedServiceImpl implements RemoteXfLimitedService {
 
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception {
-        return limitedService.deleteWithValidByIds(ids,isValid);
+        return limitedService.deleteWithValidByIds(ids, isValid);
     }
 
     @Override
@@ -54,13 +56,9 @@ public class RemoteXfLimitedServiceImpl implements RemoteXfLimitedService {
 
     @Override
     public Boolean deleteLimitedTerm(List<Long> ids) {
-        return limitedTermService.deleteWithValidByIds(ids,true);
+        return limitedTermService.deleteWithValidByIds(ids, true);
     }
-    /**
-     * 根据设备Id查询限次设备Id
-     * @param termId 设备Id
-     * @return 限制设备Id
-     */
+
     @Override
     public Long queryLimitedTermIdByTermId(Long termId) {
         XfLimitedTermVo vo = limitedTermService.queryByTermId(termId);
@@ -69,18 +67,31 @@ public class RemoteXfLimitedServiceImpl implements RemoteXfLimitedService {
         }
         return 0L;
     }
-    /**
-     * 根据卡类型查询限次信息
-     * @param cardType 卡类
-     * @return 卡类限次信息
-     */
+
     @Override
     public RemoteLimitedVo queryLimitedByCardType(Integer cardType) {
         XfLimitedVo vo = limitedService.queryByCardType(cardType);
-        if(ObjectUtil.isNotEmpty(vo)) {
+        if (ObjectUtil.isNotEmpty(vo)) {
             return MapstructUtils.convert(vo, RemoteLimitedVo.class);
         }
         return null;
     }
 
+    @Override
+    public List<RemoteLimitedVo> selectLimitedCards() {
+        XfLimitedBo bo = new XfLimitedBo();
+        bo.setStatus("1");
+        List<XfLimitedVo> list = limitedService.queryList(bo);
+        return MapstructUtils.convert(list, RemoteLimitedVo.class);
+    }
+
+    @Override
+    public List<Long> selectLimitedTermIds() {
+        XfLimitedTermBo bo = new XfLimitedTermBo();
+        List<XfLimitedTermVo> list = limitedTermService.queryList(bo);
+        if (CollUtil.isNotEmpty(list)) {
+            return list.stream().map(XfLimitedTermVo::getTermId).toList();
+        }
+        return null;
+    }
 }

+ 20 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfQuotaServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.consumption.dubbo;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
@@ -8,6 +9,7 @@ import org.dromara.backstage.api.RemoteXfQuotaService;
 import org.dromara.backstage.api.domain.bo.RemoteXfQuotaBo;
 import org.dromara.backstage.api.domain.vo.RemoteQuotaVo;
 import org.dromara.backstage.consumption.domain.bo.XfQuotaBo;
+import org.dromara.backstage.consumption.domain.bo.XfQuotaTermBo;
 import org.dromara.backstage.consumption.domain.vo.XfQuotaVo;
 import org.dromara.backstage.consumption.domain.vo.XfQuotatermVo;
 import org.dromara.backstage.consumption.service.IXfQuotaService;
@@ -72,4 +74,22 @@ public class RemoteXfQuotaServiceImpl implements RemoteXfQuotaService {
         }
         return null;
     }
+
+    @Override
+    public List<RemoteQuotaVo> selectQuotaCards() {
+        XfQuotaBo bo = new XfQuotaBo();
+        bo.setStatus("1");
+        List<XfQuotaVo> list = xfQuotaService.queryList(bo);
+        return MapstructUtils.convert(list, RemoteQuotaVo.class);
+    }
+
+    @Override
+    public List<Long> selectQuotaTermIds() {
+        XfQuotaTermBo bo = new XfQuotaTermBo();
+        List<XfQuotatermVo> list = xfQuotaTermService.queryList(bo);
+        if(CollUtil.isNotEmpty(list)){
+            return list.stream().map(XfQuotatermVo::getTermId).toList();
+        }
+        return null;
+    }
 }

+ 11 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/dubbo/RemoteUserAccountServiceImpl.java

@@ -11,6 +11,7 @@ import org.dromara.backstage.business.accouunt.UserAccountBusiness;
 import org.dromara.backstage.payment.domain.bo.PtUserAccountBo;
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.payment.service.IPtUserAccountService;
+import org.dromara.common.core.constant.Constants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
@@ -173,4 +174,14 @@ public class RemoteUserAccountServiceImpl implements RemoteUserAccountService {
     }
 
 
+    @Override
+    public List<Long> getUserAccountIdList() {
+        PtUserAccountBo bo = new PtUserAccountBo();
+        bo.setFreezeStatus(Constants.SYS_NO);
+        bo.setStatus("0");
+        bo.setAccountStatus("1");
+        List<PtUserAccountVo> voList = userAccountService.queryList(bo);
+
+        return voList.stream().map(PtUserAccountVo::getUserId).toList();
+    }
 }

+ 8 - 1
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java

@@ -145,7 +145,14 @@ public class PtUserAccountServiceImpl implements IPtUserAccountService {
     @Override
     public List<PtUserAccountVo> queryList(PtUserAccountBo bo) {
         LambdaQueryWrapper<PtUserAccount> lqw = buildQueryWrapper(bo);
-        return baseMapper.selectVoList(lqw);
+        List<PtUserAccountVo> accountVoList = baseMapper.selectVoList(lqw);
+        List<RemoteDeptVo> deptVoList = remoteDeptService.selectDeptList();
+        accountVoList.forEach(p->{
+            String deptName = deptVoList.parallelStream().filter(k -> k.getDeptId().equals(p.getDeptId()))
+                                  .findFirst().map(RemoteDeptVo::getDeptName).orElse("未知部门");
+            p.setDeptName(deptName);
+        });
+        return accountVoList;
     }
 
     private LambdaQueryWrapper<PtUserAccount> buildQueryWrapper(PtUserAccountBo bo) {

+ 6 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/dubbo/RemoteDeptServiceImpl.java

@@ -229,4 +229,10 @@ public class RemoteDeptServiceImpl implements RemoteDeptService {
     public Boolean deleteLocalDept(Long deptId) {
         return sysDeptService.deleteLocalDept(deptId);
     }
+
+    @Override
+    public List<RemoteDeptVo> selectDeptList() {
+        List<SysDeptVo> list = sysDeptService.selectAllDeptList();
+        return MapstructUtils.convert(list, RemoteDeptVo.class);
+    }
 }

+ 2 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysDeptService.java

@@ -208,4 +208,6 @@ public interface ISysDeptService {
     Boolean insertOrUpdateLocalDept(SysDeptBo bo);
 
     Boolean deleteLocalDept(Long deptId);
+
+    List<SysDeptVo> selectAllDeptList();
 }

+ 6 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDeptServiceImpl.java

@@ -491,6 +491,11 @@ public class SysDeptServiceImpl implements ISysDeptService {
 
     @Override
     public Boolean deleteLocalDept(Long deptId) {
-        return TenantHelper.ignore(() -> baseMapper.deleteById(deptId)>0);
+        return TenantHelper.ignore(() -> baseMapper.deleteById(deptId) > 0);
+    }
+
+    @Override
+    public List<SysDeptVo> selectAllDeptList() {
+        return baseMapper.selectVoList(new LambdaQueryWrapper<SysDept>().eq(SysDept::getDelFlag, "0"));
     }
 }

+ 22 - 0
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/constant/ConsumeConstants.java

@@ -0,0 +1,22 @@
+package org.dromara.server.common.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 消费相关的常量
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-11
+ * @since JDK17
+ */
+public interface ConsumeConstants {
+    Map<String, String> mealNameMap = new HashMap<String, String>() {{
+        put("1", "早餐");
+        put("2", "午餐");
+        put("3", "晚餐");
+        put("4", "夜宵");
+    }};
+}

+ 6 - 0
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/domain/consume/bo/ConsumptionBo.java

@@ -76,6 +76,11 @@ public class ConsumptionBo {
      */
     private String termMac;
 
+    /**
+     * 设备名称
+     */
+    private String termName;
+
     /**
      * 机器流水号
      */
@@ -151,6 +156,7 @@ public class ConsumptionBo {
      * 有效期
      */
     private Date expireDate;
+
     //endregion
 
     //region 错扣补款属性

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

@@ -3,6 +3,9 @@ package org.dromara.server.common.util;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.Locale;
 
@@ -23,4 +26,14 @@ public class CardDateUtils {
             return null;
         }
     }
+
+    /**
+     * 转换Date到LocalDateTime(固定东八区)
+     */
+    public static LocalDateTime toLocalDateTime(Date date) {
+        return LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("Asia/Shanghai"));
+    }
+    public static LocalDate toLocalDate(Date date) {
+        return LocalDate.ofInstant(date.toInstant(), ZoneId.of("Asia/Shanghai"));
+    }
 }

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

@@ -8,21 +8,20 @@ import cn.hutool.json.JSONUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemoteCardService;
 import org.dromara.backstage.api.domain.bo.RemoteSendMessageRecordBo;
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.common.core.config.DefaultConfig;
-import org.dromara.common.core.constant.ApiErrorTypeConstants;
 import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.constant.DefaultConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
-import org.dromara.common.core.enums.BalanceUpdateEnum;
 import org.dromara.common.core.enums.CreditTypeEnum;
 import org.dromara.common.core.exception.consume.ConsumeException;
+import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.RecordIdUtils;
-import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.message.kafka.constant.EventTypeConstants;
 import org.dromara.common.message.kafka.constant.KafkaTopicConstants;
 import org.dromara.common.message.kafka.enums.EventSenderEnum;
@@ -30,23 +29,25 @@ import org.dromara.common.message.kafka.producer.KafkaCommonProducer;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.server.base.service.yktOperation.SyncRemoteSendMessageRecordService;
+import org.dromara.server.common.constant.ConsumeConstants;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
 import org.dromara.server.common.domain.vo.yc.YcPushConsumeInfoVo;
-import org.dromara.server.consume.domain.bo.*;
+import org.dromara.server.consume.domain.bo.XfConsumeDetailBo;
+import org.dromara.server.consume.domain.bo.XfConsumeDetailOriginalBo;
+import org.dromara.server.consume.domain.bo.XfTermTotalBo;
+import org.dromara.server.consume.domain.bo.XfUserTotalBo;
 import org.dromara.server.consume.domain.convert.RemoteVoConvert;
 import org.dromara.server.consume.domain.vo.*;
 import org.dromara.server.consume.service.*;
 import org.dromara.system.api.RemoteRegisterInfoService;
 import org.jetbrains.annotations.NotNull;
-import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
+import java.text.ParseException;
+import java.util.*;
 
 /**
  * name: BaseBusiness
@@ -71,10 +72,13 @@ public class BaseBusiness {
     private final KafkaCommonProducer kafkaNormalProducer;
     private final DefaultConfig defaultConfig;
     private final SyncRemoteSendMessageRecordService syncRemoteSendMessageRecordService;
+    private final IXfCardLimitedService cardLimitedService;
+    private final ThreadPoolTaskExecutor taskExecutor;
 
     @DubboReference
     private final RemoteRegisterInfoService remoteRegisterInfoService;
-
+    @DubboReference
+    private final RemoteCardService remoteCardService;
 
     /**
      * 生成原始消费记录
@@ -95,13 +99,7 @@ public class BaseBusiness {
 
         XfConsumeDetailOriginalBo originalBo = getXfConsumeDetailOriginalBo(consumeBo, accountVo, originalId);
 
-        XfConsumeDetailOriginalVo vo = originalService.queryById(originalId);
-        if (ObjUtil.isNotEmpty(vo)) {
-            RemoteVoConvert.INSTANCE.copyXfConsumeDetailOriginalVo(originalVo, vo);
-            return R.ok();
-        }
-        // 原始记录表不存在此消费记录,直接插入
-        vo = originalService.insertByBo(originalBo);
+        XfConsumeDetailOriginalVo vo = originalService.insertByBo(originalBo);
         if (ObjUtil.isNotEmpty(vo)) {
             RemoteVoConvert.INSTANCE.copyXfConsumeDetailOriginalVo(originalVo, vo);
             return R.ok();
@@ -126,9 +124,8 @@ public class BaseBusiness {
         originalBo.setCardValue(consumeBo.getBalance());
         originalBo.setConsumeBalance(consumeBo.getBalance());
         originalBo.setTermNo(consumeBo.getTermNo().intValue());
-        // originalBo.setTermName(consumeBo.ter);
+        originalBo.setTermName(consumeBo.getTermName());
         originalBo.setTermRecordId(consumeBo.getTermRecordId());
-        originalBo.setAnalysisFlag(0L);
         originalBo.setDataFlag(consumeBo.getRecordStatus());
         originalBo.setStatusFlag(consumeBo.getStatusFlag());
         originalBo.setDigitalSign(consumeBo.getDigitalSign());
@@ -138,6 +135,7 @@ public class BaseBusiness {
         originalBo.setWaterErrValue(new BigDecimal("0"));
         originalBo.setWaterErrMoney(new BigDecimal("0"));
         originalBo.setOperatorId(0L);
+        originalBo.setAnalysisFlag(0L);
         originalBo.setTenantId(defaultConfig.getTenantId());
 
         return originalBo;
@@ -162,51 +160,37 @@ public class BaseBusiness {
     @Transactional(rollbackFor = ConsumeException.class)
     public R<ErrorInfo> postConsumeRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo cardVo,
                                           List<PtBagVo> bagVos, XfTermVo termVo, RemoteMealTypeVo mealTypeVo, String remark) {
-        ErrorInfo errorInfo;
         XfConsumeDetailVo consumeDetailVo = consumeDetailService.queryVoByOriginalId(bo.getOriginalId());
         if (ObjectUtil.isNotEmpty(consumeDetailVo)) {
             // 认为是重复上传,不再写入明细
             return R.ok();
         }
-        // 1.入消费明细表,根据消费金额与扣款方式及扣款钱包的余额,可能会从多个钱包扣钱,则对应的有多笔消费明细记录
-        AtomicReference<Boolean> result = new AtomicReference<>();
-        List<XfConsumeDetailVo> detailVos = new ArrayList<>();
-
+        List<XfConsumeDetailBo> detailBos = new ArrayList<>();
         for (PtBagVo bagVo : bagVos) {
-            XfConsumeDetailVo vo = createConsumeRecord(bo, userAccountVo, cardVo, bagVo, termVo, mealTypeVo, remark);
-            // 多钱包扣费时,只要有一个钱包入消费明细表失败,则都失败
-            if (ObjUtil.isEmpty(vo)) {
-                result.set(false);
-                break;
-            }
-            result.set(true);
-            detailVos.add(vo);
+            XfConsumeDetailBo detailBo = this.createConsumeDetailBo(bo, userAccountVo, cardVo, bagVo, termVo, mealTypeVo, remark);
+            detailBos.add(detailBo);
         }
-        if (!result.get()) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "入消费明细表失败", "");
-            return R.fail(errorInfo);
+        boolean flag = consumeDetailService.batchInsertByBo(detailBos);
+        if (!flag) {
+            throw new ConsumeException("入消费明细表失败");
         }
         // 2.更新人员日统计表
         log.warn("[上传交易]-[更新人员日统计表]-[{}]", JSONUtil.toJsonStr(bo));
         if (!createOrUpdateUserTotal(bo, userAccountVo, cardVo)) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新个人日统计表失败", ""));
+            throw new ConsumeException("更新个人日统计表失败");
         }
         // 3.更新设备日统计表
         log.warn("[上传交易]-[更新设备日统计表]-[{}]", JSONUtil.toJsonStr(bo));
         if (!createOrUpdateTermTotal(bo, termVo, mealTypeVo)) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新设备日统计表失败", ""));
+            throw new ConsumeException("更新设备日统计表失败");
         }
         // 4.更新钱包余额
         log.warn("[上传交易]-[更新钱包余额]-[{}]", JSONUtil.toJsonStr(bo));
-        if (!updateBagBalance(bagVos)) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新钱包余额表失败", ""));
-        }
+        bagService.updateConsumeBalance(bagVos);
+
+        // 5.消费记录入库完成后的一些后续任务
+        this.completeUploadRecord(bo, userAccountVo);
 
-        // 5.发送一条消费记录到kafka(教务就餐打卡)
-        if (ObjectUtil.equals(defaultConfig.getLocationFlag(), DefaultConstants.LOCAL_FLAG)) {
-            SpringUtils.getAopProxy(this).sendConsumeToKafka(detailVos, userAccountVo);
-            // this.sendConsumeToKafka(detailVos, userAccountVo);
-        }
         return R.ok();
     }
 
@@ -225,7 +209,7 @@ public class BaseBusiness {
         xfUserTotalBo.setRealName(accountVo.getRealName());
         xfUserTotalBo.setDeptName(accountVo.getDeptName());
         xfUserTotalBo.setCardNo(cardVo.getCardNo());
-        xfUserTotalBo.setDateDay(DateUtil.format(new Date(), DefaultConstants.DATE_FORMAT));
+        xfUserTotalBo.setDateDay(DateUtil.format(bo.getConsumeDate(), DefaultConstants.DATE_FORMAT));
         xfUserTotalBo.setUseType(bo.getUseType());
         xfUserTotalBo.setConsumeMoney(bo.getConsumeMoney());
 
@@ -244,7 +228,7 @@ public class BaseBusiness {
     public boolean createOrUpdateTermTotal(ConsumptionBo bo, XfTermVo termVo, RemoteMealTypeVo mealTypeVo) {
         XfTermTotalBo termTotalBo = new XfTermTotalBo();
         BeanUtil.copyProperties(termVo, termTotalBo);
-        termTotalBo.setDateDay(DateUtil.format(new Date(), "yyyy-MM-dd"));
+        termTotalBo.setDateDay(DateUtil.format(bo.getConsumeDate(), "yyyy-MM-dd"));
         termTotalBo.setMealType(Long.valueOf(mealTypeVo.getTypeId()));
         termTotalBo.setUseType(bo.getUseType());
         CreditTypeEnum creditType = CreditTypeEnum.fromCode(bo.getCreditType());
@@ -264,45 +248,15 @@ public class BaseBusiness {
         return !ObjUtil.isEmpty(vo);
     }
 
-    /**
-     * 更新账户钱包余额
-     *
-     * @param bagVos 账户钱包视图
-     * @return 更新后的账户钱包
-     */
-    public boolean updateBagBalance(List<PtBagVo> bagVos) {
-        AtomicReference<Boolean> result = new AtomicReference<>();
-        bagVos.parallelStream().forEach(bagVo -> {
-            if (bagVo.getReceiptMoney().compareTo(BigDecimal.ZERO) == 0) {
-                // 如果操作金额为0,不操作钱包余额
-                result.set(true);
-            } else {
-                PtBagBo bagBo = new PtBagBo();
-                bagBo.setUserId(bagVo.getUserId());
-                bagBo.setBagId(bagVo.getBagId());
-                bagBo.setBagCode(bagVo.getBagCode());
-                bagBo.setReceiptMoney(bagVo.getReceiptMoney());
-                bagBo.setOperationMode(BalanceUpdateEnum.CONSUME);
-                PtBagVo vo = bagService.updateBalanceByBo(bagBo);
-                // 多钱包更新余额时,只要有一个钱包更新余额失败,则都失败
-                if (ObjUtil.isEmpty(vo)) {
-                    result.set(false);
-                }
-                result.set(true);
-            }
-        });
-        return result.get();
-    }
+    public void completeUploadRecord(ConsumptionBo bo, RemoteUserAccountVo accountVo) {
+        // 消费记录上传完成,还有一些后续工作,不需要知道处理结果,采用异步任务提交
+        List<Runnable> afterTasks = new ArrayList<>();
+        afterTasks.add(() -> sendConsumeToKafka(bo, accountVo));
+        afterTasks.add(() -> sendCloudConsume(bo));
 
-    /**
-     * 请求云端消费业务的kafka消息推送
-     *
-     * @param bo 请求消费数据
-     */
-    @Async
-    public void sendCloudConsume(ConsumptionBo bo) {
-        log.info("[发送消费请求至云平台]-[{}]", bo);
-        kafkaNormalProducer.sendKafkaMessage(KafkaTopicConstants.TO_CLOUD_TOPIC, EventTypeConstants.CONSUME, EventSenderEnum.CONSUME.code(), bo);
+        for (Runnable task : afterTasks) {
+            taskExecutor.submit(() -> task);
+        }
     }
 
     public List<RemoteSendMessageRecordBo> queryConsumeErrorList(RemoteSendMessageRecordBo remoteBo) {
@@ -322,57 +276,57 @@ public class BaseBusiness {
      * @param bagVo         账户钱包视图
      * @param termVo        消费设备视图
      * @param mealTypeVo    消费餐类视图
-     * @return 消费明细视图
+     * @return 消费明细业务对象
      */
-    private XfConsumeDetailVo createConsumeRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
-                                                  RemoteCardVo cardVo, PtBagVo bagVo, XfTermVo termVo,
-                                                  RemoteMealTypeVo mealTypeVo, String remark) {
+    private XfConsumeDetailBo createConsumeDetailBo(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
+                                                    RemoteCardVo cardVo, PtBagVo bagVo, XfTermVo termVo,
+                                                    RemoteMealTypeVo mealTypeVo, String remark) {
         String recordId = RecordIdUtils.getRecordId(bo.getConsumeDate(), Short.parseShort(bo.getTermNo().toString()),
                                                     bo.getTermRecordId().intValue(),
                                                     userAccountVo.getUserNo().intValue(),
                                                     Integer.parseInt(bagVo.getBagCode()));
-        try {
-            XfConsumeDetailBo consumeDetailBo = new XfConsumeDetailBo();
-            BeanUtil.copyProperties(bo, consumeDetailBo);
-            consumeDetailBo.setConsumeId(recordId);
-            // 设置消费账户信息
-            BeanUtil.copyProperties(userAccountVo, consumeDetailBo);
-
-            // 设置消费信息
-            consumeDetailBo.setConsumeDate(bo.getConsumeDate());
-            consumeDetailBo.setConsumeMoney(bagVo.getReceiptMoney());
-            consumeDetailBo.setConsumeBalance(bo.getBalance());
-            consumeDetailBo.setCardValue(bo.getBalance());
-            // 设置卡片信息
-            consumeDetailBo.setCardNo(cardVo.getCardNo());
-            consumeDetailBo.setFactoryId(cardVo.getFactoryId());
-            consumeDetailBo.setCardValue(bo.getBalance());
-            // 设置设备信息
-            BeanUtil.copyProperties(termVo, consumeDetailBo);
-            // 设置操作员信息
-            consumeDetailBo.setOperatorId(bo.getOperatorId());
-            consumeDetailBo.setOperatorName(bo.getOperatorName());
-            // 设置餐类信息
-            consumeDetailBo.setMealType(Long.valueOf(mealTypeVo.getTypeId()));
-            consumeDetailBo.setMealName(mealTypeVo.getMealName());
-            // 设置钱包信息
-            consumeDetailBo.setBagType(bagVo.getBagCode());
-            consumeDetailBo.setStatusFlag(bo.getStatusFlag().longValue());
-            // 该字段为补款记录对应的消费明细Id,以便追查消费记录是否有补扣以及对应的补扣记录
-            consumeDetailBo.setDetailId(bo.getConsumeId());
-            consumeDetailBo.setRecordId(bo.getRecordId());
-            consumeDetailBo.setRemark(remark);
-
-            consumeDetailBo.setCreateTime(DateUtil.date());
-            consumeDetailBo.setUpdateTime(DateUtil.date());
-
-            consumeDetailBo.setTenantId(bo.getTenantId());
-
-            return consumeDetailService.createConsumeDetailRecord(consumeDetailBo);
-        } catch (Exception ex) {
-            log.error("消费明细入库错误", ex);
-            return null;
-        }
+        XfConsumeDetailBo consumeDetailBo = new XfConsumeDetailBo();
+        consumeDetailBo.setConsumeId(recordId);
+        consumeDetailBo.setTenantId(bo.getTenantId());
+        consumeDetailBo.setOriginalId(bo.getOriginalId());
+        consumeDetailBo.setTermRecordId(bo.getTermRecordId());
+        // 设置消费账户信息
+        consumeDetailBo.setUserId(userAccountVo.getUserId());
+        consumeDetailBo.setUserNumb(userAccountVo.getUserNumb());
+        consumeDetailBo.setRealName(userAccountVo.getRealName());
+        consumeDetailBo.setDeptId(userAccountVo.getDeptId());
+        consumeDetailBo.setDeptName(userAccountVo.getDeptName());
+        // 设置消费信息
+        consumeDetailBo.setConsumeDate(bo.getConsumeDate());
+        consumeDetailBo.setConsumeMoney(bagVo.getReceiptMoney());
+        consumeDetailBo.setConsumeBalance(bo.getBalance());
+        // 设置卡片信息
+        consumeDetailBo.setCardNo(cardVo.getCardNo());
+        consumeDetailBo.setFactoryId(cardVo.getFactoryId());
+        consumeDetailBo.setCardValue(bo.getBalance());
+        // 设置设备信息
+        consumeDetailBo.setTermNo(termVo.getTermNo());
+        consumeDetailBo.setTermName(termVo.getTermName());
+        consumeDetailBo.setRoomId(termVo.getRoomId());
+        consumeDetailBo.setRoomName(termVo.getRoomName());
+        consumeDetailBo.setAccountId(termVo.getAccountId());
+        consumeDetailBo.setAccountName(termVo.getAccountName());
+
+        // 设置餐类信息
+        consumeDetailBo.setMealType(Long.valueOf(mealTypeVo.getTypeId()));
+        consumeDetailBo.setMealName(mealTypeVo.getMealName());
+        // 设置钱包信息
+        consumeDetailBo.setBagType(bagVo.getBagCode());
+        consumeDetailBo.setStatusFlag(bo.getStatusFlag().longValue());
+        // 该字段为补款记录对应的消费明细Id,以便追查消费记录是否有补扣以及对应的补扣记录
+        consumeDetailBo.setDetailId(bo.getConsumeId());
+        consumeDetailBo.setRecordId(bo.getRecordId());
+        consumeDetailBo.setRemark(remark);
+
+        consumeDetailBo.setCreateTime(DateUtil.date());
+        consumeDetailBo.setUpdateTime(DateUtil.date());
+
+        return consumeDetailBo;
     }
 
     /**
@@ -415,39 +369,142 @@ public class BaseBusiness {
     /**
      * 将消费信息发送到kafka,教务消费此消息实现就餐打卡
      *
-     * @param consumeList 消费记录列表
-     * @param accountVo   消费人员信息
+     * @param bo        消费对象
+     * @param accountVo 消费人员信息
      */
-    @Async
-    protected void sendConsumeToKafka(List<XfConsumeDetailVo> consumeList, RemoteUserAccountVo accountVo) {
-        for (XfConsumeDetailVo vo : consumeList) {
+    private void sendConsumeToKafka(ConsumptionBo bo, RemoteUserAccountVo accountVo) {
+        // 只有本地消费完成后才会向教务发kafka消息
+        if (ObjectUtil.equals(defaultConfig.getLocationFlag(), DefaultConstants.LOCAL_FLAG)) {
             YcPushConsumeInfoVo ycSendConsumeInfo = new YcPushConsumeInfoVo();
-            ycSendConsumeInfo.setRecordId(vo.getRecordId().toString());
-            ycSendConsumeInfo.setUserId(vo.getUserId().toString());
-            ycSendConsumeInfo.setUserNumb(vo.getUserNumb());
-            ycSendConsumeInfo.setXm(vo.getRealName());
-            ycSendConsumeInfo.setDeptId(vo.getDeptId().toString());
-            ycSendConsumeInfo.setDeptName(vo.getDeptName());
-            ycSendConsumeInfo.setRoomId(vo.getRoomId().toString());
-            ycSendConsumeInfo.setRoomName(vo.getRoomName());
-            ycSendConsumeInfo.setCardNo(vo.getCardNo().toString());
-            ycSendConsumeInfo.setFactoryFixId(vo.getFactoryId().toString());
-            ycSendConsumeInfo.setConsumeValue(vo.getConsumeMoney().toString());
-            ycSendConsumeInfo.setCardValue(String.valueOf(vo.getConsumeBalance()));
-            ycSendConsumeInfo.setConsumeDate(vo.getConsumeDate().toString());
-            ycSendConsumeInfo.setMealTypeId(vo.getMealType().toString());
-            ycSendConsumeInfo.setMealName(vo.getMealName());
-            ycSendConsumeInfo.setTermNo(vo.getTermNo().toString());
-            ycSendConsumeInfo.setTermName(vo.getTermName());
+            ycSendConsumeInfo.setRecordId(bo.getRecordId().toString());
+            ycSendConsumeInfo.setUserId(bo.getUserId().toString());
+            ycSendConsumeInfo.setUserNumb(bo.getUserNumb());
+            ycSendConsumeInfo.setXm(bo.getRealName());
+            ycSendConsumeInfo.setDeptId(String.valueOf(accountVo.getDeptId()));
+            ycSendConsumeInfo.setDeptName(bo.getDeptName());
+            ycSendConsumeInfo.setRoomId("");
+            ycSendConsumeInfo.setRoomName("");
+            ycSendConsumeInfo.setCardNo(bo.getCardNo().toString());
+            ycSendConsumeInfo.setFactoryFixId(bo.getFactoryId().toString());
+            ycSendConsumeInfo.setConsumeValue(bo.getConsumeMoney().toString());
+            ycSendConsumeInfo.setCardValue(String.valueOf(bo.getBalance()));
+            ycSendConsumeInfo.setConsumeDate(bo.getConsumeDate().toString());
+            ycSendConsumeInfo.setMealTypeId(bo.getMealType().toString());
+            ycSendConsumeInfo.setMealName(ConsumeConstants.mealNameMap.getOrDefault(String.valueOf(bo.getMealType()), "未知餐类"));
+            ycSendConsumeInfo.setTermNo(bo.getTermNo().toString());
+            ycSendConsumeInfo.setTermName(bo.getTermName());
             ycSendConsumeInfo.setCategory(accountVo.getCategory());
             ycSendConsumeInfo.setOtherSysId(accountVo.getOtherId());
             ycSendConsumeInfo.setClassId(accountVo.getOtherDeptId());
-            ycSendConsumeInfo.setTermRecordID(vo.getTermRecordId());
-            ycSendConsumeInfo.setPosRecordState(vo.getRecordStatus().intValue());
+            ycSendConsumeInfo.setTermRecordID(bo.getTermRecordId());
+            ycSendConsumeInfo.setPosRecordState(bo.getRecordStatus().intValue());
 
             log.info("[向教务系统发送就餐打卡]-[{}]", ycSendConsumeInfo);
             kafkaNormalProducer.sendKafkaMessage(KafkaTopicConstants.OLD_SYNC_TOPIC, EventTypeConstants.CONSUME_RECORD, EventSenderEnum.OLD.code(),
                                                  ycSendConsumeInfo);
         }
     }
+
+    /**
+     * 请求云端消费业务的kafka消息推送
+     *
+     * @param bo 请求消费数据
+     */
+    public void sendCloudConsume(ConsumptionBo bo) {
+        log.info("[发送消费请求至云平台]-[{}]", bo);
+        kafkaNormalProducer.sendKafkaMessage(KafkaTopicConstants.TO_CLOUD_TOPIC, EventTypeConstants.CONSUME, EventSenderEnum.CONSUME.code(), bo);
+    }
+
+    public void resetCardConsumeInfo(RemoteCardVo userCardVo, Long mealType, BigDecimal consumeMoney, Date consumeDate) {
+        log.info("[请求交易完成]-[更新卡片日消费数据]");
+        // 更新缓存
+        String date1 = DateUtil.format(consumeDate, DefaultConstants.DATE_FORMAT);
+        String date2 = DateUtil.format(userCardVo.getLastPay(), DefaultConstants.DATE_FORMAT);
+
+        if (ObjUtil.equals(date1, date2)) {
+            userCardVo.setDayCount(userCardVo.getDayCount() + 1);
+            userCardVo.setDayTotal(userCardVo.getDayTotal().add(consumeMoney));
+            if (userCardVo.getLastMeal().equals(mealType)) {
+                userCardVo.setMealCount(userCardVo.getMealCount() + 1);
+                userCardVo.setMealTotal(userCardVo.getMealTotal().add(consumeMoney));
+            } else {
+                userCardVo.setMealCount(1L);
+                userCardVo.setMealTotal(consumeMoney);
+            }
+        } else {
+            userCardVo.setDayCount(1L);
+            userCardVo.setDayTotal(consumeMoney);
+            userCardVo.setMealCount(1L);
+            userCardVo.setMealTotal(consumeMoney);
+        }
+
+        userCardVo.setLastMeal(mealType);
+        userCardVo.setLastPay(consumeDate);
+        RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_NO, userCardVo.getCardNo().toString(), userCardVo);
+        RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, userCardVo.getUserId().toString(), userCardVo);
+
+        // 更新数据库
+        remoteCardService.updateCardDayData(userCardVo);
+    }
+
+    public void resetUserBalance(Long userId, BigDecimal balance) {
+        log.info("[请求交易完成]-[更新人员余额数据]");
+        String strUserId = String.valueOf(userId);
+        RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, strUserId, balance);
+    }
+
+    public void restCardLimitedInfo(Map<String, Boolean> mapCardLimited, XfCardLimitedVo cardLimitedVo, BigDecimal consumeMoney) {
+        if (mapCardLimited.get("hasDiscount")) {
+            cardLimitedVo.setDayDiscountCount(cardLimitedVo.getDayDiscountCount() + 1);
+            cardLimitedVo.setMealDiscountCount(cardLimitedVo.getMealDiscountCount() + 1);
+        }
+        if (mapCardLimited.get("hasQuota")) {
+            cardLimitedVo.setDayMoney(cardLimitedVo.getDayMoney().add(consumeMoney));
+            cardLimitedVo.setMealMoney(cardLimitedVo.getMealMoney().add(consumeMoney));
+        }
+        if (mapCardLimited.get("hasLimited")) {
+            cardLimitedVo.setMealCount(cardLimitedVo.getMealCount() + 1);
+            cardLimitedVo.setDayCount(cardLimitedVo.getDayCount() + 1);
+        }
+
+        RedisUtils.setCacheMapValue(CacheNames.T_XF_CARD_LIMITED, String.valueOf(cardLimitedVo.getCardNo()), cardLimitedVo);
+        cardLimitedService.updateByVo(cardLimitedVo);
+
+    }
+
+    public void dealUserTotal(Date date) throws ParseException {
+        Map<String, Object> mapParam = new HashMap<>();
+        String tempDate = DateUtil.format(date, DefaultConstants.DATE_FORMAT);
+        String beginTime = tempDate + " 00:00:00";
+        String endTime = tempDate + "23:59:59";
+        mapParam.put("beginTime", DateUtils.parseDate(beginTime, DefaultConstants.DATE_TIME_FORMAT));
+        mapParam.put("endTime", DateUtils.parseDate(endTime, DefaultConstants.DATE_TIME_FORMAT));
+        XfConsumeDetailBo detailBo = new XfConsumeDetailBo();
+        detailBo.setParams(mapParam);
+        List<XfConsumeDetailVo> detailVos = consumeDetailService.queryList(detailBo);
+
+        Map<String, BigDecimal> sumMap = new HashMap<>();
+        Map<String, XfConsumeDetailVo> firstRecordMap = new HashMap<>();
+
+        for (XfConsumeDetailVo vo : detailVos) {
+            String userId = String.valueOf(vo.getUserId());
+            firstRecordMap.putIfAbsent(userId, vo);
+
+            // 使用 BigDecimal 避免浮点误差
+            BigDecimal amount = vo.getConsumeMoney();
+            sumMap.merge(userId, amount, BigDecimal::add);
+        }
+
+        List<XfUserTotalBo> totalBoList = new ArrayList<>();
+        XfUserTotalBo bo;
+        for (Map.Entry<String, BigDecimal> entry : sumMap.entrySet()) {
+            XfConsumeDetailVo ref = firstRecordMap.get(entry.getKey());
+            bo = new XfUserTotalBo();
+            bo.setUserId(ref.getUserId());
+            bo.setUserNumb(ref.getUserNumb());
+            bo.setRealName(ref.getRealName());
+            bo.setConsumeMoney(entry.getValue());
+            totalBoList.add(bo);
+        }
+    }
 }

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

@@ -11,11 +11,13 @@ import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.common.core.api.ReturnResult;
 import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.domain.model.ErrorResult;
 import org.dromara.common.core.enums.BagNameEnum;
 import org.dromara.common.core.enums.ResultCodeEnum;
 import org.dromara.common.encrypt.utils.YcEncryptUtil;
+import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.consume.domain.bo.PtBagBo;
 import org.dromara.server.consume.domain.vo.PtBagVo;
 import org.dromara.server.consume.domain.vo.yc.BagInfo;
@@ -51,7 +53,9 @@ public class CardBusiness {
     private final IPtBagService ptBagService;
 
     public ReturnResult getCardBagsByCardNo(Long cardNo) {
-        RemoteCardVo remoteCardVo = remoteCardService.queryCardByCardNo(cardNo);
+        // RemoteCardVo remoteCardVo = remoteCardService.queryCardByCardNo(cardNo);
+        String strCardNo = String.valueOf(cardNo);
+        RemoteCardVo remoteCardVo = RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_NO, strCardNo);
         if (ObjectUtil.isEmpty(remoteCardVo)) {
             ErrorResult result = new ErrorResult();
             result.setStatusCode(HttpStatus.NOT_FOUND.value());
@@ -66,7 +70,9 @@ public class CardBusiness {
 
     private Object setCardBagInfo(RemoteCardVo remoteCardVo) {
         CardBagInfo cardBagInfo = new CardBagInfo();
-        RemoteUserAccountVo userAccountVo = remoteUserAccountService.getUserAccountVoById(remoteCardVo.getUserId());
+        // RemoteUserAccountVo userAccountVo = remoteUserAccountService.getUserAccountVoById(remoteCardVo.getUserId());
+        String strUserId = String.valueOf(remoteCardVo.getUserId());
+        RemoteUserAccountVo userAccountVo =  RedisUtils.getCacheMapValue(CacheNames.PT_USER_ACCOUNT_ID, strUserId);
         PtBagBo bagBo = new PtBagBo();
         bagBo.setUserId(remoteCardVo.getUserId());
         List<PtBagVo> bagVos = ptBagService.queryList(bagBo);

+ 0 - 1304
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java

@@ -1,1304 +0,0 @@
-package org.dromara.server.consume.business;
-
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSONUtil;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.time.DateFormatUtils;
-import org.apache.dubbo.config.annotation.DubboReference;
-import org.dromara.backstage.api.*;
-import org.dromara.backstage.api.domain.vo.*;
-import org.dromara.common.core.config.DefaultConfig;
-import org.dromara.common.core.constant.ApiErrorTypeConstants;
-import org.dromara.common.core.constant.Constants;
-import org.dromara.common.core.domain.R;
-import org.dromara.common.core.domain.model.ErrorInfo;
-import org.dromara.common.core.enums.BagNameEnum;
-import org.dromara.common.core.enums.CardStatusEnum;
-import org.dromara.common.core.enums.TradeStatusEnum;
-import org.dromara.common.core.enums.UserAccountStatusEnum;
-import org.dromara.common.core.utils.RecordIdUtils;
-import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
-import org.dromara.server.consume.cache.TokenManager;
-import org.dromara.server.consume.domain.bo.XfCardLimitedBo;
-import org.dromara.server.consume.domain.convert.RemoteVoConvert;
-import org.dromara.server.consume.domain.vo.PtBagVo;
-import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
-import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
-import org.dromara.server.consume.domain.vo.XfTermVo;
-import org.dromara.server.consume.domain.vo.yc.TermToken;
-import org.dromara.server.consume.service.IConsumeDetailOriginalService;
-import org.dromara.server.consume.service.IPtBagService;
-import org.dromara.server.consume.service.IXfCardLimitedService;
-import org.jetbrains.annotations.NotNull;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.text.MessageFormat;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.util.*;
-
-/**
- * name: CheckBusiness
- * package: org.dromara.server.consume.business
- * description: 消费数据验证业务处理
- * date: 2024-11-02 07:49:37 07:49
- *
- * @author luoyibo
- * @version 0.1
- * @since JDK 1.8
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class CheckBusiness {
-    @DubboReference
-    private final RemoteCardService remoteCardService;
-    @DubboReference
-    private final RemoteUserAccountService remoteUserAccountService;
-    @DubboReference
-    private final RemoteMealTypeService remoteMealTypeService;
-    @DubboReference
-    private final RemoteOperatorService remoteOperatorService;
-    @DubboReference
-    private final RemotePtParameterService remotePtParameterService;
-    @DubboReference
-    private final RemoteXfLimitedService remoteLimitedService;
-    @DubboReference
-    private final RemoteXfDiscountService remoteDisCountService;
-    @DubboReference
-    private final RemoteXfQuotaService remoteQuotaService;
-    @DubboReference
-    private final RemotePtXfTermService remoteXfTermService;
-
-    private final IConsumeDetailOriginalService consumeDetailOriginalService;
-    private final IPtBagService bagService;
-    private final TokenManager tokenManager;
-    private final IXfCardLimitedService cardLimitedService;
-    private final DefaultConfig defaultConfig;
-    private final BaseBusiness baseBusiness;
-
-    /**
-     * 计算折扣金额
-     *
-     * @param cardLimitedVo    卡片限制信息
-     * @param bo               消费记录
-     * @param remoteDiscountVo 折扣信息
-     * @return 折扣金额
-     */
-    @NotNull
-    private static BigDecimal getDisCountMoney(XfCardLimitedVo cardLimitedVo, ConsumptionBo bo, RemoteDiscountVo remoteDiscountVo) {
-        int rateCount = remoteDiscountVo.getRateType().equals(
-            "1") ? cardLimitedVo.getDayDiscountCount().intValue() : cardLimitedVo.getMealDiscountCount().intValue();
-        BigDecimal disCountMoney;
-        BigDecimal consumeMoney = bo.getConsumeMoney();
-        // 折扣率1
-        BigDecimal oneRate = remoteDiscountVo.getOneRate();
-        // 折扣率2
-        BigDecimal twoRate = remoteDiscountVo.getTwoRate();
-        // 折扣率3
-        BigDecimal threeRate = remoteDiscountVo.getThreeRate();
-        // 折扣率4
-        BigDecimal fourRate = remoteDiscountVo.getFourRate();
-        if (rateCount == 0) {
-            disCountMoney = consumeMoney.multiply(oneRate.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
-        } else if (rateCount == 1) {// 第二次
-            disCountMoney = consumeMoney.multiply(twoRate.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
-        } else if (rateCount == 2) {// 第三次
-            disCountMoney = consumeMoney.multiply(threeRate.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_UP)).setScale(2,
-                                                                                                                               RoundingMode.HALF_UP);
-        } else { // 第四次及以上
-            disCountMoney = consumeMoney.multiply(fourRate.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_UP)).setScale(2,
-                                                                                                                              RoundingMode.HALF_UP);
-        }
-        return disCountMoney;
-    }
-
-    /**
-     * 消费记录参数检检查
-     * 1.检查设备编号,设备编号必须大于0
-     * 2.检查交易人员标识:卡流水号、物理卡号、人员Id、人员编号至少有1项
-     *
-     * @param bo 消费记录业务对象
-     * @return 检查结果
-     */
-    public R<ErrorInfo> checkParam(ConsumptionBo bo) {
-        ErrorInfo errorInfo;
-        // 检查设备机号
-        if ((ObjectUtil.isEmpty(bo.getTermNo()) || bo.getTermNo() == 0) && ObjectUtil.isEmpty(bo.getTermMac())) {
-            errorInfo = new ErrorInfo(1, ApiErrorTypeConstants.PARAM_ERROR, "设备机号不正确", "设备机号必须大于零!");
-            return R.fail(errorInfo);
-        }
-        // 检查交易人员标识
-        if (bo.getCardNo() <= 0L && bo.getFactoryId() == 0L && bo.getUserNo() <= 0L && StrUtil.isEmpty(bo.getUserNumb())) {
-            errorInfo = new ErrorInfo(1, ApiErrorTypeConstants.PARAM_ERROR, "交易人员标识不满足",
-                                      "必须提供 [CardNo | FactoryId | userNo | userNumb] 中至少1项来标识交易用户");
-            return R.fail(errorInfo);
-        }
-        return R.ok();
-    }
-
-    /**
-     * 检查校验码
-     *
-     * @param bo  消费信息
-     * @param mac 校验码
-     * @return 检查结果
-     */
-    public R<ErrorInfo> checkMac(ConsumptionBo bo, String mac) {
-        ErrorInfo errorInfo;
-        Long termNo = bo.getTermNo();
-        TermToken token = tokenManager.getTermToken().get(String.valueOf(termNo));
-        if (ObjectUtil.isEmpty(token)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.INVALID_TOKEN, "Token不存在",
-                                      MessageFormat.format("没有找到机号为[{0}]的设备的Token!", termNo));
-            return R.fail(errorInfo);
-        }
-        if (ObjectUtil.isNotEmpty(mac)) {
-            // TODO 2024-12-25 luoyibo 如果mac不为空,需要校验
-            return R.ok();
-        }
-        return R.ok();
-    }
-
-    /**
-     * 消费记录人员、卡片检查,全部通过后获取到消费账户、卡片及设备的视图信息
-     *
-     * @param bo            消费记录
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @return 验证结果
-     */
-    public R<ErrorInfo> checkUser(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        long cardNo = ObjectUtil.isEmpty(bo.getCardNo()) ? 0 : bo.getCardNo();
-        long factoryId = ObjectUtil.isEmpty(bo.getFactoryId()) ? 0 : bo.getFactoryId();
-        long userNo = ObjectUtil.isEmpty(bo.getUserNo()) ? 0 : bo.getUserNo();
-        String userNumb = bo.getUserNumb() == null ? null : bo.getUserNumb();
-
-        // 卡流水号检查
-        if (cardNo > 0) {
-            return checkCardNo(bo, userAccountVo, userCardVo);
-        }
-        // 物理卡号检查
-        if (factoryId > 0) {
-            return checkFactoryId(bo, userAccountVo, userCardVo);
-        }
-        // 用户流水号检查
-        if (userNo > 0) {
-            return checkUserNo(bo, userAccountVo, userCardVo);
-        }
-        // 人员编号检查
-        if (StrUtil.isNotEmpty(userNumb)) {
-            return checkUserNumb(bo, userAccountVo, userCardVo);
-        }
-        return R.ok();
-    }
-
-    /**
-     * 消费设备检查
-     *
-     * @param bo        消费记录
-     * @param useTermVo 消费设备
-     * @return 检查结果
-     */
-    public R<ErrorInfo> checkTerm(ConsumptionBo bo, XfTermVo useTermVo) {
-        String msg = ObjectUtil.isEmpty(bo.getTermMac()) ? bo.getTermNo().toString() : bo.getTermMac();
-        String tenantId = ObjectUtil.isNotEmpty(bo.getTenantId()) ? bo.getTenantId() : defaultConfig.getTenantId();
-        RemoteXfTermVo remoteXfTermVo;
-        if ((ObjectUtil.isNotEmpty(bo.getTermNo()) && bo.getTermNo() > 0)) {
-            remoteXfTermVo = remoteXfTermService.queryByNo(bo.getTermNo(), tenantId);
-        } else {
-            remoteXfTermVo = remoteXfTermService.queryByMac(bo.getTermMac());
-        }
-        if (ObjectUtil.isEmpty(remoteXfTermVo)) {
-            ErrorInfo errorInfo = new ErrorInfo(400, "", "设备不存在",
-                                                MessageFormat.format("机号或MAC为[{0}]的设备不存在,不允许交易", msg));
-
-            return R.fail(errorInfo);
-        }
-        bo.setTermNo(remoteXfTermVo.getTermNo());
-        RemoteVoConvert.INSTANCE.copyRemoteTermVo(useTermVo, remoteXfTermVo);
-        return R.ok();
-    }
-
-    /**
-     * 消费逻辑检查,检查是否能消费
-     *
-     * @param bo            消费记录
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @param useTermVo     消费设备
-     * @return 验证结果
-     */
-    public R<ErrorInfo> checkConsume(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo, XfTermVo useTermVo) {
-        ErrorInfo errorInfo;
-        R<ErrorInfo> result;
-        // 1.消费账户状态验证,验证账户是否已开户、是否冻结、状态是否正常、是否过有效期
-        if ("Y".equals(userAccountVo.getFreezeStatus())) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户已被冻结",
-                                      MessageFormat.format("流水号为[{0}]的账户已被冻结,不允许交易", userAccountVo.getUserNo()));
-            return R.fail(errorInfo);
-        }
-        if ("N".equals(userAccountVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户状态不正常",
-                                      MessageFormat.format("流水号为[{0}]的账户状态不正常,不允许交易", userAccountVo.getUserNo()));
-            return R.fail(errorInfo);
-        }
-        if (!ObjectUtil.equal(userAccountVo.getAccountStatus(), UserAccountStatusEnum.IS_OPEN.code().toString())) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户尚未开户",
-                                      MessageFormat.format("流水号为[{0}]的账户尚未开户,不允许交易", userAccountVo.getUserNo()));
-            return R.fail(errorInfo);
-        }
-        if (userAccountVo.getLifespan() != null && bo.getConsumeDate().getTime() > userAccountVo.getLifespan().getTime()) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户已过有效期",
-                                      MessageFormat.format("流水号为[{0}]的账户已过有效期,不允许交易", userAccountVo.getUserNo()));
-            return R.fail(errorInfo);
-        }
-
-        // 2.餐类验证
-        long startTime=System.currentTimeMillis();
-        RemoteMealTypeVo mealType = remoteMealTypeService.queryMealTypeVoByTime(bo.getConsumeDate());
-        if (ObjectUtil.isEmpty(mealType)) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "不在交易时段", "不在交易时段"));
-        }
-
-        // 3.设备限制验证,只有消费机上传时会会进行验证,在手工补扣、错扣补款时不进行验证
-        startTime=System.currentTimeMillis();
-        if (bo.getStatusFlag() == 1 || bo.getStatusFlag() == 4) {
-            result = checkTermLimitDeal(bo, useTermVo, userCardVo, mealType);
-            if (R.isError(result)) {
-                return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "消费限制判断存在问题", JSONUtil.toJsonStr(result.getData())));
-            }
-        }
-        log.info("折扣验证耗时:{}毫秒", System.currentTimeMillis()-startTime);
-        // 4.根据消费机的消费模式验证余额,如果余额不足则返回
-        startTime=System.currentTimeMillis();
-        result = checkOrigDeductionBag(bo, userAccountVo, useTermVo);
-        if (R.isError(result)) {
-            return result;
-        }
-        log.info("卡余验证耗时:{}毫秒", System.currentTimeMillis()-startTime);
-        return R.ok();
-    }
-
-    /**
-     * 检查消费交易的账单,通过验证各种组件如卡片详情、原始记录、餐类信息、扣款钱包以及操作员详情。此外,它还会更新与用户卡片相关的其他消费信息
-     *
-     * @param bo 包含交易详情的消费业务对象
-     * @param userAccountVo 与交易关联的用户账户信息
-     * @param userCardVo 与用户关联的卡片信息。
-     * @param useTermVo 处理交易的终端信息
-     * @param bagVoList 扣款钱包信息的列表
-     * @param mealTypeVo 交易餐类信息
-     * @param operatorVo 为交易操作员信息
-     * @return 返回一个结果对象,包含成功或错误信息。如果发生验证失败,错误信息将包括有关验证失败的详细信息
-     */
-    public R<ErrorInfo> checkBill(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo,
-                                  XfTermVo useTermVo, List<PtBagVo> bagVoList, RemoteMealTypeVo mealTypeVo,
-                                  RemoteOperatorVo operatorVo) {
-        R<ErrorInfo> result = checkUser(bo, userAccountVo, userCardVo);
-        if (R.isError(result)) {
-            return result;
-        }
-        result = checkOriginalRecord(bo, userAccountVo);
-        if (R.isError(result)) {
-            return result;
-        }
-        // 获取餐类信息
-        RemoteMealTypeVo mealType = remoteMealTypeService.queryMealTypeVoByTime(bo.getConsumeDate());
-        if (ObjectUtil.isEmpty(mealType)) {
-            mealType.setTypeId("0");
-            mealType.setMealName("未知");
-        }
-        BeanUtil.copyProperties(mealType, mealTypeVo);
-        // 获取扣费钱包
-        List<PtBagVo> bagVos = new ArrayList<>();
-        result = checkDeductionBag(bo, userAccountVo, useTermVo, bagVos);
-        if (R.isError(result)) {
-            return result;
-        }
-        bagVoList.addAll(bagVos);
-        // 获取营业员信息
-        BeanUtil.copyProperties(getOperatorVo(bo), operatorVo);
-
-        // 更新卡片的折扣、限额与限次信息
-        updateOtherConsumeInfo(bo, userCardVo, mealTypeVo, useTermVo);
-
-        return R.ok();
-    }
-
-    /**
-     * 设置返回的消费用户信息
-     *
-     * @param bo        消费业务
-     * @param accountVo 消费账户
-     */
-    private void setUserInfo(ConsumptionBo bo, RemoteUserAccountVo accountVo) {
-        bo.setUserId(accountVo.getUserId());
-        bo.setRealName(StrUtil.isEmpty(accountVo.getRealName()) ? "----" : accountVo.getRealName());
-        bo.setUserNo(accountVo.getUserNo());
-        bo.setUserNumb(accountVo.getUserNumb());
-        bo.setTenantId(accountVo.getTenantId());
-        bo.setExpireDate(accountVo.getLifespan());
-        bo.setDeptName(accountVo.getDeptName());
-    }
-
-    /**
-     * 检查卡流水号
-     *
-     * @param bo            消费业务
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @return 检查结果
-     */
-    @NotNull
-    private R<ErrorInfo> checkCardNo(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        ErrorInfo errorInfo;
-        Long cardNo = bo.getCardNo();
-
-        RemoteCardVo cardVo = remoteCardService.queryCardByCardNo(cardNo);
-        if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
-                                      MessageFormat.format("流水号为[{0}]的卡片不存在,不允许交易", cardNo));
-
-            return R.fail(errorInfo);
-        }
-        if (!String.valueOf(CardStatusEnum.NORMAL.code()).equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
-                                      MessageFormat.format("流水号为[{0}]的卡片状态不正确,不允许交易", cardNo));
-
-            return R.fail(errorInfo);
-        }
-        if (bo.getFactoryId() > 0) {
-            if (!Objects.equals(cardVo.getFactoryId(), bo.getFactoryId())) {
-                errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不正确", "物理卡号不一致,不允许交易");
-
-                return R.fail(errorInfo);
-            }
-        }
-        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoById(cardVo.getUserId());
-        if (Objects.isNull(accountVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
-                                      MessageFormat.format("流水号为[{0}]的卡片无对应的人员信息,不允许交易", cardNo));
-
-            return R.fail(errorInfo);
-        }
-        if (Objects.equals(accountVo.getFreezeStatus(), Constants.SYS_YES)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员被冻结",
-                                      MessageFormat.format("流水号为[{0}]的卡片对应的人员被冻结,不允许交易", cardNo));
-
-            return R.fail(errorInfo);
-        }
-        Date nowDate = new Date();
-        long currentTime = nowDate.getTime();
-        if (accountVo.getLifespan().getTime()<currentTime) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "账户过期",
-                                      MessageFormat.format("流水号为[{0}]的卡片对应的人员账户过期,不允许交易", cardNo));
-
-            return R.fail(errorInfo);
-        }
-        // 设置返回的消费用户信息
-        setUserInfo(bo, accountVo);
-
-        bo.setCardNo(cardVo.getCardNo());
-        bo.setFactoryId(cardVo.getFactoryId());
-        bo.setCardTypeName(cardVo.getCardTypeName());
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
-        return R.ok();
-    }
-
-    /**
-     * 检查物理卡号
-     *
-     * @param bo            消费业务
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @return 检查结果
-     */
-    @NotNull
-    private R<ErrorInfo> checkFactoryId(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        ErrorInfo errorInfo;
-        Long factoryId = bo.getFactoryId();
-        RemoteCardVo cardVo = remoteCardService.queryCardByFactoryId(factoryId);
-        if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
-                                      MessageFormat.format("物理卡号为[{0}]的卡片不存在,不允许交易", factoryId));
-            return R.fail(errorInfo);
-        }
-        if (!String.valueOf(CardStatusEnum.NORMAL.code()).equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
-                                      MessageFormat.format("物理卡号为[{0}]的卡片卡片状态不正确,不允许交易", factoryId));
-            return R.fail(errorInfo);
-        }
-        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoById(cardVo.getUserId());
-        if (Objects.isNull(accountVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
-                                      MessageFormat.format("物理卡号为[{0}]的卡片无对应的人员信息,不允许交易", factoryId));
-
-            return R.fail(errorInfo);
-        }
-        if (Objects.equals(accountVo.getFreezeStatus(), Constants.SYS_YES)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员被冻结",
-                                      MessageFormat.format("物理卡号为[{0}]的卡片对应的人员被冻结,不允许交易", factoryId));
-
-            return R.fail(errorInfo);
-        }
-        Date nowDate = new Date();
-        long currentTime = nowDate.getTime();
-        if (accountVo.getLifespan().getTime()<currentTime) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "账户过期",
-                                      MessageFormat.format("物理卡号为[{0}]的卡片对应的人员账户过期,不允许交易", factoryId));
-
-            return R.fail(errorInfo);
-        }
-        setUserInfo(bo, accountVo);
-        bo.setCardNo(cardVo.getCardNo());
-        bo.setCardTypeName(cardVo.getCardTypeName());
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
-        return R.ok();
-    }
-
-    /**
-     * 检查用户流水号
-     *
-     * @param bo            消费业务
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @return 检查结果
-     */
-    @NotNull
-    private R<ErrorInfo> checkUserNo(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        ErrorInfo errorInfo;
-        Long userNo = bo.getUserNo();
-        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoByUserNo(userNo);
-        if (ObjectUtil.isEmpty(accountVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
-                                      MessageFormat.format("流水号为[{0}]的人员不存在,不允许交易", userNo));
-
-            return R.fail(errorInfo);
-        }
-        if (Objects.equals(accountVo.getFreezeStatus(), Constants.SYS_YES)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员被冻结",
-                                      MessageFormat.format("流水号为[{0}]的人员被冻结,不允许交易", userNo));
-
-            return R.fail(errorInfo);
-        }
-        Date nowDate = new Date();
-        long currentTime = nowDate.getTime();
-        if (accountVo.getLifespan().getTime()<currentTime) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "账户过期",
-                                      MessageFormat.format("流水号为[{0}]的人员账户过期,不允许交易", userNo));
-
-            return R.fail(errorInfo);
-        }
-        RemoteCardVo cardVo = remoteCardService.queryMainCardByUserId(accountVo.getUserId());
-        if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
-                                      MessageFormat.format("没有流水号为[{0}]人员的卡片信息,不允许交易", userNo));
-
-            return R.fail(errorInfo);
-        }
-        // 实体卡时需要验证卡状态
-        if (!String.valueOf(CardStatusEnum.NORMAL.code()).equals(cardVo.getStatus()) && cardVo.getFactoryId() > 0) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
-                                      MessageFormat.format("流水号为[{0}]的人员卡片状态不正确,不允许交易", userNo));
-
-            return R.fail(errorInfo);
-        }
-        setUserInfo(bo, accountVo);
-        bo.setCardNo(cardVo.getCardNo());
-        bo.setFactoryId(cardVo.getFactoryId());
-        bo.setCardTypeName(cardVo.getCardTypeName());
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
-        return R.ok();
-    }
-
-    /**
-     * 检查人员编号
-     *
-     * @param bo            消费业务
-     * @param userAccountVo 消费账户
-     * @param userCardVo    消费卡片
-     * @return 检查结果
-     */
-    @NotNull
-    private R<ErrorInfo> checkUserNumb(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        ErrorInfo errorInfo;
-        String userNumb = bo.getUserNumb();
-        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoByUserNumb(userNumb);
-
-        if (ObjectUtil.isEmpty(accountVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
-                                      MessageFormat.format("编号为[{0}]的人员不存在,不允许交易", userNumb));
-
-            return R.fail(errorInfo);
-        }
-        if (Objects.equals(accountVo.getFreezeStatus(), Constants.SYS_YES)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员被冻结",
-                                      MessageFormat.format("编号为[{0}]的人员被冻结,不允许交易", userNumb));
-
-            return R.fail(errorInfo);
-        }
-        Date nowDate = new Date();
-        long currentTime = nowDate.getTime();
-        if (accountVo.getLifespan().getTime()<currentTime) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "账户过期",
-                                      MessageFormat.format("编号为[{0}]的人员账户过期,不允许交易", userNumb));
-
-            return R.fail(errorInfo);
-        }
-        RemoteCardVo cardVo = remoteCardService.queryMainCardByUserId(accountVo.getUserId());
-        if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
-                                      MessageFormat.format("没有编号为[{0}]人员的卡片信息,不允许交易", userNumb));
-
-            return R.fail(errorInfo);
-        }
-        // 实体卡时需要验证卡状态
-        if (!String.valueOf(CardStatusEnum.NORMAL.code()).equals(cardVo.getStatus()) && cardVo.getFactoryId() > 0) {
-            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
-                                      MessageFormat.format("编号为[{0}]人员卡片状态不正确,不允许交易", userNumb));
-
-            return R.fail(errorInfo);
-        }
-
-        setUserInfo(bo, accountVo);
-        bo.setCardNo(cardVo.getCardNo());
-        bo.setFactoryId(cardVo.getFactoryId());
-        bo.setCardTypeName(cardVo.getCardTypeName());
-
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
-
-        return R.ok();
-    }
-
-    private R<ErrorInfo> checkOriginalRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo) {
-        String originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
-                                                      bo.getTermRecordId().shortValue(), bo.getUserNo().intValue(), 0);
-        //  补偿性措施:防止消费时间错乱问题(在线模式)
-        int recordType = bo.getRecordStatus().intValue();
-        long uniqueRecordId = bo.getRecordId();
-        bo.setOriginalId(originalId);
-        XfConsumeDetailOriginalVo originalVo;
-        if (!((recordType == 106 || recordType == 108 || recordType == 110 || recordType == 111))) {
-            originalVo = consumeDetailOriginalService.queryById(originalId);
-            if (ObjectUtil.isEmpty(originalVo)) {
-                boolean checkFlag = false;
-                if (uniqueRecordId > 0) {
-                    // 根据自增的记录Id查询
-                    originalVo = consumeDetailOriginalService.queryByRecordId(uniqueRecordId);
-                    if (ObjectUtil.isNotEmpty(originalVo)) {
-                        // 如果根据自增Id找到原始记录,以查找的数据重置消费对象的消费日期和原始记录Id
-                        bo.setConsumeDate(originalVo.getConsumeDate());
-                        bo.setOriginalId(originalVo.getOriginalId());
-                        originalId = originalVo.getOriginalId();
-                        checkFlag = true;
-                    }
-                }
-                if (!checkFlag) {
-                    // 根据卡流水号、机号、机器流水号和消费金额查询
-                    originalVo = consumeDetailOriginalService.queryByConsumeMoney(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(),
-                                                                                  bo.getConsumeMoney());
-                    if (ObjectUtil.isNotEmpty(originalVo)) {
-                        bo.setConsumeDate(originalVo.getConsumeDate());
-                        bo.setRecordId(originalVo.getRecordId());
-                        bo.setOriginalId(originalVo.getOriginalId());
-                        // originalId = originalVo.getOriginalId();
-                    } else {
-                        bo.setRecordStatus(108L);
-                        // 判断脱机消费
-                        R<ErrorInfo> result = doOfflineRecord(bo, originalId, userAccountVo);
-                        if (R.isError(result)) {
-                            String msg = MessageFormat.format("处理记录号为[{0}]的脱机消费数据失败", bo.getRecordId());
-                            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "处理脱机消费数据失败", msg));
-                        }
-                        // 重置这项数据
-                        bo.setConsumeDate(originalVo.getConsumeDate());
-                        bo.setRecordId(originalVo.getRecordId());
-                        bo.setOriginalId(originalVo.getOriginalId());
-                        // originalId = originalVo.getOriginalId();
-                    }
-                }
-            }
-            bo.setRecordId(originalVo.getRecordId());
-        } else {
-            R<ErrorInfo> result = doOfflineRecord(bo, originalId, userAccountVo);
-            if (R.isError(result)) {
-                return result;
-            }
-        }
-        // XfConsumeDetailVo consumeDetailVo = consumeDetailService.queryVoByOriginalId(originalId);
-        // XfConsumeDetailVo consumeDetailVo = consumeDetailService.queryVoByOriginalId(bo.getOriginalId());
-        // if (ObjectUtil.isNotEmpty(consumeDetailVo)) {
-        //    // 认为是重复上传,不再入账
-        //    return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "原始消费记录已处理",
-        //                                MessageFormat.format("标识为[{0}]的原始消费记录已处理", bo.getRecordId())));
-        //}
-        return R.ok();
-    }
-
-    public R<ErrorInfo> checkDeductionBag(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, XfTermVo termVo, List<PtBagVo> bagVos) {
-        R<ErrorInfo> result = this.checkOrigDeductionBag(bo, userAccountVo, termVo);
-        if (R.isError(result)) {
-            return result;
-        }
-        StringBuilder sb = new StringBuilder();
-        // 设备的扣费钱包字符串
-        String consumeType = termVo.getConsumeType();
-        // 分解扣费钱包
-        List<String> bagCodes = StrUtil.split(consumeType, ",");
-        Long userId = userAccountVo.getUserId();
-        // 可能会在处理过程中更改实际的消费金额,因此先取出来
-        BigDecimal consumeMoney = bo.getConsumeMoney();
-        // 扣费的过程金额
-        BigDecimal doMoney = bo.getConsumeMoney();
-        // 计算后实际需要扣费的钱包,会小于或等于指定的扣费钱包数
-        List<PtBagVo> doBagVos = new ArrayList<>();
-        BigDecimal totalBalance = BigDecimal.ZERO;
-        // 计算扣费钱包的总金额
-        for (String bagCode : bagCodes) {
-            PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
-            if (ObjectUtil.isNotEmpty(bagVo)) {
-                totalBalance = totalBalance.add(bagVo.getBalance());
-            }
-        }
-        for (String bagCode : bagCodes) {
-            // 1.查询对应的钱包
-            PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
-            if (ObjectUtil.isEmpty(bagVo)) {
-                log.warn("人员Id:[{}]没有代码:[{}]的钱包,无法从此钱包扣费", userId, bagCode);
-                continue;
-            }
-            // 2.比较扣费金额
-            BigDecimal balance = bagVo.getBalance();
-            if (consumeMoney.compareTo(BigDecimal.ZERO) == 0) {
-                // 如果是消费0元,设置为第一个钱包扣费
-                bagVo.setReceiptMoney(BigDecimal.ZERO);
-                bagVo.setBalance(balance);
-                sb.append(BagNameEnum.getMessage(Integer.parseInt(bagCode)));
-                doBagVos.add(bagVo);
-                log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, consumeMoney, doMoney,
-                         userAccountVo.getRealName());
-                break;
-            } else {
-                // 如果消费金额>0,则可能会需要多钱包扣费
-                if (balance.compareTo(BigDecimal.ZERO) > 0) {
-                    // 钱包有余额则扣费
-                    if (balance.compareTo(doMoney) >= 0) {
-                        // 如果钱包金额>=扣费金额,设置扣费结果并中断循环
-                        bagVo.setReceiptMoney(doMoney);
-                        bagVo.setBalance(balance.subtract(doMoney));
-                        sb.append(BagNameEnum.getMessage(Integer.parseInt(bagCode)));
-                        doBagVos.add(bagVo);
-                        log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, consumeMoney,
-                                 doMoney, userAccountVo.getRealName());
-                        break;
-                    } else {
-                        // 将钱包扣费为0,剩余待扣金额=消费金额-原钱包余额
-                        bagVo.setReceiptMoney(balance);
-                        bagVo.setBalance(BigDecimal.ZERO);
-                        doMoney = doMoney.subtract(balance);
-                        sb.append(BagNameEnum.getMessage(Integer.parseInt(bagCode)));
-                        doBagVos.add(bagVo);
-                        log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, balance,
-                                 doMoney, userAccountVo.getRealName());
-                    }
-                }
-            }
-        }
-
-        bo.setBalance(totalBalance.subtract(consumeMoney));
-        bo.setDigitalSign(sb.toString());
-        bagVos.addAll(doBagVos);
-
-        return R.ok();
-    }
-
-    public R<ErrorInfo> checkOrigDeductionBag(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, XfTermVo termVo) {
-        // 设备的扣费钱包字符串
-        String consumeType = termVo.getConsumeType();
-        // 分解扣费钱包
-        List<String> bagCodes = StrUtil.split(consumeType, ",");
-        Long userId = userAccountVo.getUserId();
-        BigDecimal consumeMoney = bo.getConsumeMoney();
-        BigDecimal totalBalance = BigDecimal.ZERO;
-        // 计算扣费钱包的总金额
-        for (String bagCode : bagCodes) {
-            PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
-            // if (ObjectUtil.isNotEmpty(bagVo)) {
-                totalBalance = totalBalance.add(bagVo.getBalance());
-            // }
-        }
-        // 如果扣费钱包总余额<消费金额,则不允许消费
-        if (consumeMoney.compareTo(totalBalance) > 0) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.CONSUME_CHECK_FAIL, "钱包余额不足",
-                                        MessageFormat.format("余额不足,总余额[{0}],消费金额[{1}]", totalBalance, consumeMoney)));
-        }
-        bo.setBalance(totalBalance.subtract(consumeMoney));
-        return R.ok();
-    }
-
-    /**
-     * 设备限额、限次与折扣验证
-     *
-     * @param bo         消费业务
-     * @param termVo     消费设备
-     * @param userCardVo 消费卡片
-     * @param mealTypeVo 消费类型
-     * @return 检查结果
-     */
-    public R<ErrorInfo> checkTermLimitDeal(ConsumptionBo bo, XfTermVo termVo, RemoteCardVo userCardVo, RemoteMealTypeVo mealTypeVo) {
-        Long termNo = termVo.getTermNo();
-        BigDecimal consumeValue = bo.getConsumeMoney();
-        Long factoryFixId = userCardVo.getFactoryId();
-        String mealTypeId = mealTypeVo.getTypeId();
-        if (termNo == 0 && ObjectUtil.equal("0", mealTypeId)) {
-            // 机号为0并且餐类不在正常餐类
-            return R.ok();
-        }
-        // 当前卡片类型
-        int cardTypeId = userCardVo.getCardType().intValue();
-        // 设备允许卡类
-        int termCardTypeId = termVo.getCardType() == null ? 0 : termVo.getCardType();
-        // 设备每天最大消费次数
-        int termDayCount = termVo.getDayCount() == null ? 0 : termVo.getDayCount();
-        // 每天最大消费金额
-        BigDecimal termDayMoney = termVo.getDayMoney() == null ? BigDecimal.ZERO : termVo.getDayMoney();
-        // 每餐最大消费次数
-        int termMealCount = termVo.getMealCount() == null ? 0 : termVo.getMealCount();
-        // 单次最大消费金额
-        BigDecimal termSingleMoney = termVo.getSingleMoney() == null ? BigDecimal.ZERO : termVo.getSingleMoney();
-        // 二次可用最大时间间隔
-        int termSwipeInterval = termVo.getSwipeInterval() == null ? 0 : termVo.getSwipeInterval();
-        // 设备是否启用了卡有效
-        boolean termUseValidity = Objects.equals(termVo.getTermValidity(), "0") ? Boolean.FALSE : Boolean.TRUE;
-        // 当前时间
-        LocalDateTime currentLocalDt = LocalDateTime.now();
-        // 最后消费时间
-        LocalDateTime lastPayLocalDt = LocalDateTime.ofInstant(userCardVo.getLastPay().toInstant(), ZoneOffset.of("+8"));
-        // 有效期时间
-        LocalDateTime expiryLocalDt = LocalDateTime.ofInstant(userCardVo.getLifespan().toInstant(), ZoneOffset.of("+8"));
-
-        // 设备消费间隔验证
-        if ((currentLocalDt.toEpochSecond(ZoneOffset.of("+8"))
-                 - lastPayLocalDt.toEpochSecond(ZoneOffset.of("+8"))) / 60 < termSwipeInterval && termSwipeInterval > 0) {
-            return R.fail(new ErrorInfo(400, TradeStatusEnum.TimeInterval.toString(), "消费间隔过短", TradeStatusEnum.TimeInterval.getName()));
-        }
-        // 设备单次限额验证
-        if (termSingleMoney.compareTo(consumeValue) < 0 && termSingleMoney.compareTo(BigDecimal.ZERO) > 0) {// 限制金额
-            return R.fail(new ErrorInfo(400, TradeStatusEnum.OnceBigMoney.toString(), "超过设备单次限额", TradeStatusEnum.OnceBigMoney.getName()));
-        }
-        // 设备启用了卡有效时进行卡有效期验证
-        if (factoryFixId != 0 && termUseValidity && currentLocalDt.isAfter(expiryLocalDt)) {
-            return R.fail(
-                new ErrorInfo(400, TradeStatusEnum.CardValidDate.toString(), "卡片已超过失效日期", TradeStatusEnum.CardValidDate.getName()));
-        }
-        // 消费卡类限制验证
-        int offsetTypeId = (int) Math.pow(2, (cardTypeId - 1));
-        int temp = offsetTypeId & termCardTypeId;
-        if (temp != offsetTypeId) {
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "设备卡类限制", TradeStatusEnum.CardTypeLimit.getName()));
-        }
-        // 餐限次验证
-        String lastMeal = userCardVo.getLastMeal().toString();
-        int mealCount = userCardVo.getMealCount().intValue();
-        if (!ObjectUtil.equals(lastMeal, mealTypeId) || !currentLocalDt.toLocalDate().isEqual(lastPayLocalDt.toLocalDate())) {
-            // 如果当前餐类!=消费餐类,初始化卡片的餐类消费数据
-            remoteCardService.initCardMealData(userCardVo.getCardNo(), mealTypeId);
-        } else {
-            if (termMealCount != 0 && mealCount >= termMealCount) {
-                return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitTimes.toString(), "设备餐限次", TradeStatusEnum.MealLimitTimes.getName()));
-            }
-        }
-        // 设备日限次 日限额效验(每日第一次消费)
-        int dayCount = userCardVo.getDayCount().intValue();
-        BigDecimal dayValue = userCardVo.getDayTotal();
-        if (!currentLocalDt.toLocalDate().isEqual(lastPayLocalDt.toLocalDate())) {
-            // 如果当前日期!=消费日期,初始化卡片的日消费数据
-            remoteCardService.initCardDayData(userCardVo.getCardNo());
-        } else {
-            if (termDayCount > 0 && dayCount >= termDayCount) {
-                return R.fail(new ErrorInfo(400, TradeStatusEnum.DayLimitTimes.toString(), "设备日限次", TradeStatusEnum.DayLimitTimes.getName()));
-            }
-            if (termDayMoney.compareTo(BigDecimal.ZERO) > 0 && termDayMoney.compareTo(dayValue.add(consumeValue)) < 0) {
-                return R.fail(new ErrorInfo(400, TradeStatusEnum.DayLimitMoney.toString(), "设备日限额", TradeStatusEnum.DayLimitMoney.getName()));
-            }
-        }
-        // 当前卡片校验处理
-        R<ErrorInfo> result = checkCardLimitDeal(bo, termVo, userCardVo, mealTypeVo);
-        if (R.isError(result)) {
-            return R.fail(result.getData());
-        }
-        return R.ok();
-    }
-
-    /**
-     * 卡类限额、限次与折扣验证
-     *
-     * @param bo         消费业务
-     * @param termVo     消费设备
-     * @param userCardVo 消费卡片
-     * @param mealTypeVo 消费类型
-     * @return 检查结果
-     */
-    public R<ErrorInfo> checkCardLimitDeal(ConsumptionBo bo, XfTermVo termVo, RemoteCardVo userCardVo, RemoteMealTypeVo mealTypeVo) {
-        long startTime=System.currentTimeMillis();
-        LocalDateTime currentLocalDt = LocalDateTime.now();
-        Long cardNo = userCardVo.getCardNo();
-        Long mealTypeId = Long.valueOf(mealTypeVo.getTypeId());
-        Date lastPayDate = userCardVo.getLastPay();
-        // 获取卡片的限制信息
-        XfCardLimitedVo cardLimitedVo = cardLimitedService.queryByCardNo(cardNo);
-        log.info("卡类限额、限次与折扣初始化耗时1:{}", System.currentTimeMillis()-startTime);
-        if (ObjectUtil.isEmpty(cardLimitedVo)) {
-            cardLimitedVo = initXfCardLimited(cardNo, mealTypeVo.getMealId(), lastPayDate);
-        }
-        log.info("卡类限额、限次与折扣初始化耗时:{}", System.currentTimeMillis()-startTime);
-        // 最后交易时间
-        LocalDateTime lastPayLimitLocalDt = LocalDateTime.ofInstant(cardLimitedVo.getLastPay().toInstant(), ZoneOffset.of("+8"));
-        // 最后交易餐类
-        Long lastPayLimitMealType = cardLimitedVo.getLastMeal();
-        // 判断交易是否为新的一天,如果是则需要重置日消费数据
-        if (!lastPayLimitLocalDt.toLocalDate().isEqual(currentLocalDt.toLocalDate())) {
-            cardLimitedVo = cardLimitedService.resetDayCardLimitedData(cardNo, mealTypeId.toString());
-        }
-        // 判断是否为新的餐类,如果是则需要重置餐类消费数据
-        if (!Objects.equals(lastPayLimitMealType, mealTypeId)) {
-            cardLimitedVo = cardLimitedService.resetMealCardLimitedData(cardNo, mealTypeId.toString());
-        }
-        log.info("卡类限额、限次与折扣初始化耗时2:{}", System.currentTimeMillis()-startTime);
-
-        // 卡类折扣检查
-        startTime=System.currentTimeMillis();
-        R<ErrorInfo> result = checkCardDisCount(termVo, userCardVo, cardLimitedVo, mealTypeVo, bo);
-        if (R.isError(result)) {
-            return R.fail(result.getData());
-        }
-        log.info("卡类折扣检查耗时:{}", System.currentTimeMillis()-startTime);
-        // 卡类限额检查
-        startTime=System.currentTimeMillis();
-        result = checkCardQuota(termVo, userCardVo, cardLimitedVo, mealTypeVo, bo.getConsumeMoney());
-        if (R.isError(result)) {
-            return R.fail(result.getData());
-        }
-        log.info("卡类限额检查耗时:{}", System.currentTimeMillis()-startTime);
-        // 卡类限次检
-        startTime=System.currentTimeMillis();
-        result = checkCardLimited(termVo, userCardVo, cardLimitedVo, mealTypeVo);
-        if (R.isError(result)) {
-            return R.fail(result.getData());
-        }
-        log.info("卡类限次检查耗时:{}", System.currentTimeMillis()-startTime);
-        return R.ok();
-    }
-
-    /**
-     * 卡类的限次验证处理
-     *
-     * @param termVo        设备信息
-     * @param userCardVo    卡片信息
-     * @param cardLimitedVo 卡片限制信息
-     * @param mealTypeVo    餐类信息
-     * @return 验证结果
-     */
-    private R<ErrorInfo> checkCardLimited(XfTermVo termVo, RemoteCardVo userCardVo, XfCardLimitedVo cardLimitedVo, RemoteMealTypeVo mealTypeVo) {
-        String useLimited = remotePtParameterService.getPtParameterByKey("XC_CONSUME");
-        String mealType = mealTypeVo.getTypeId();
-        if (StrUtil.isEmpty(useLimited) || ObjectUtil.notEqual(useLimited, "1")) {
-            // 如果没有启用限次,直接返回
-            return R.ok();
-        }
-        Long limitedTermId = remoteLimitedService.queryLimitedTermIdByTermId(termVo.getTermId());
-        if (ObjectUtil.equals(limitedTermId, 0L)) {
-            // 当前设备没有设置限次
-            return R.ok();
-        }
-        RemoteLimitedVo remoteLimitedVo = remoteLimitedService.queryLimitedByCardType(userCardVo.getCardType().intValue());
-        if (ObjectUtil.isEmpty(remoteLimitedVo)) {
-            // 当前卡类没有限次信息
-            return R.ok();
-        }
-        String limitedStatus = remoteLimitedVo.getStatus();
-        if (ObjectUtil.isEmpty(limitedStatus) || ObjectUtil.equals(limitedStatus, "0")) {
-            // 卡类限次未启用
-            return R.ok();
-        }
-        // 每日次数
-        Long dayCount = remoteLimitedVo.getDailyCount();
-        // 早餐次数
-        Long oneCount = remoteLimitedVo.getOneCount();
-        // 午餐次数
-        Long twoCount = remoteLimitedVo.getTwoCount();
-        // 晚餐次数
-        Long threeCount = remoteLimitedVo.getThreeCount();
-        // 宵夜次数
-        Long fourCount = remoteLimitedVo.getFourCount();
-
-        if (dayCount.compareTo(0L) > 0 || oneCount.compareTo(0L) > 0 || twoCount.compareTo(0L) > 0 || threeCount.compareTo(
-            0L) > 0 || fourCount.compareTo(0L) > 0) {
-            if (dayCount.compareTo(0L) > 0) {
-                // 日限次
-                if (cardLimitedVo.getDayCount().compareTo(dayCount) >= 0) {
-                    return R.fail(
-                        new ErrorInfo(400, TradeStatusEnum.DayLimitTimes.toString(), "卡类日限制次数", TradeStatusEnum.DayLimitTimes.getName()));
-                }
-                // 早餐限次
-                if (ObjectUtil.equals(mealType, "1")) {
-                    if (cardLimitedVo.getMealCount().compareTo(oneCount) >= 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitTimes.toString(), "卡类早餐限制次数",
-                                                    TradeStatusEnum.MealLimitTimes.getName()));
-                    }
-                }
-                // 午餐限次
-                if (ObjectUtil.equals(mealType, "2")) {
-                    if (cardLimitedVo.getMealCount().compareTo(twoCount) >= 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitTimes.toString(), "卡类午餐限制次数",
-                                                    TradeStatusEnum.MealLimitTimes.getName()));
-                    }
-                }
-                // 晚餐限次
-                if (ObjectUtil.equals(mealType, "3")) {
-                    if (cardLimitedVo.getMealCount().compareTo(threeCount) >= 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitTimes.toString(), "卡类晚餐限制次数",
-                                                    TradeStatusEnum.MealLimitTimes.getName()));
-                    }
-                }
-                // 宵夜限次
-                if (ObjectUtil.equals(mealType, "4")) {
-                    if (cardLimitedVo.getMealCount().compareTo(fourCount) >= 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitTimes.toString(), "卡类夜宵限制次数",
-                                                    TradeStatusEnum.MealLimitTimes.getName()));
-                    }
-                }
-            }
-        }
-        return R.ok();
-    }
-
-    /**
-     * 卡类的限额验证处理
-     *
-     * @param termVo        设备信息
-     * @param userCardVo    卡片信息
-     * @param cardLimitedVo 卡片限制信息
-     * @param mealTypeVo    餐类信息
-     * @param disCountMoney 折扣金额
-     * @return 验证结果
-     */
-    private R<ErrorInfo> checkCardQuota(XfTermVo termVo, RemoteCardVo userCardVo, XfCardLimitedVo cardLimitedVo, RemoteMealTypeVo mealTypeVo,
-                                        BigDecimal disCountMoney) {
-        String useQuota = remotePtParameterService.getPtParameterByKey("XE_CONSUME");
-        String mealType = mealTypeVo.getTypeId();
-        if (StrUtil.isEmpty(useQuota) || ObjectUtil.notEqual(useQuota, "1")) {
-            // 如果没有启用限额,直接返回
-            return R.ok();
-        }
-        Long quotaTermId = remoteQuotaService.queryQuotaTermIdByTermId(termVo.getTermId());
-        if (ObjectUtil.equals(quotaTermId, 0L)) {
-            // 当前设备没有设置限额
-            return R.ok();
-        }
-        RemoteQuotaVo remoteQuotaVo = remoteQuotaService.queryQuotaByCardType(userCardVo.getCardType().intValue());
-        if (ObjectUtil.isEmpty(remoteQuotaVo)) {
-            // 当前卡类没有限额信息
-            return R.ok();
-        }
-        String quotaStatus = remoteQuotaVo.getStatus();
-        if (ObjectUtil.isEmpty(quotaStatus) || ObjectUtil.equals(quotaStatus, "0")) {
-            // 卡类限额未启用
-            return R.ok();
-        }
-        // 每日金额
-        BigDecimal dayMoney = remoteQuotaVo.getDailyMoney();
-        // 早餐金额
-        BigDecimal oneMoney = remoteQuotaVo.getOneMoney();
-        // 午餐金额
-        BigDecimal twoMoney = remoteQuotaVo.getTwoMoney();
-        // 晚餐金额
-        BigDecimal threeMoney = remoteQuotaVo.getThreeMoney();
-        // 宵夜金额
-        BigDecimal fourMoney = remoteQuotaVo.getFourMoney();
-
-        if (dayMoney.compareTo(BigDecimal.ZERO) > 0 || oneMoney.compareTo(BigDecimal.ZERO) > 0 || twoMoney.compareTo(BigDecimal.ZERO) > 0
-                || threeMoney.compareTo(BigDecimal.ZERO) > 0 || fourMoney.compareTo(BigDecimal.ZERO) > 0) {
-            if (dayMoney.compareTo(BigDecimal.ZERO) > 0) {
-                BigDecimal mealQuotaMoney = cardLimitedVo.getMealMoney();
-                // 日限额
-                if (dayMoney.compareTo(cardLimitedVo.getDayMoney().add(disCountMoney)) < 0) {
-                    return R.fail(
-                        new ErrorInfo(400, TradeStatusEnum.DayLimitMoney.toString(), "卡类日限制额度", TradeStatusEnum.DayLimitMoney.getName()));
-                }
-                // 早餐限额
-                if (ObjectUtil.equals(mealType, "1") && oneMoney.compareTo(BigDecimal.ZERO) > 0) {
-                    if (oneMoney.compareTo(mealQuotaMoney.add(disCountMoney)) < 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitMoney.toString(), "卡类早餐限制额度",
-                                                    TradeStatusEnum.MealLimitMoney.getName()));
-                    }
-                }
-                // 午餐限额
-                if (ObjectUtil.equals(mealType, "2") && twoMoney.compareTo(BigDecimal.ZERO) > 0) {
-                    if (twoMoney.compareTo(mealQuotaMoney.add(disCountMoney)) < 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitMoney.toString(), "卡类午餐限制额度",
-                                                    TradeStatusEnum.MealLimitMoney.getName()));
-                    }
-                }
-                // 晚餐限额
-                if (ObjectUtil.equals(mealType, "3") && threeMoney.compareTo(BigDecimal.ZERO) > 0) {
-                    if (threeMoney.compareTo(mealQuotaMoney.add(disCountMoney)) < 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitMoney.toString(), "卡类晚餐限制额度",
-                                                    TradeStatusEnum.MealLimitMoney.getName()));
-                    }
-                }
-                // 宵夜限额
-                if (ObjectUtil.equals(mealType, "4") && threeMoney.compareTo(BigDecimal.ZERO) > 0) {
-                    if (fourMoney.compareTo(mealQuotaMoney.add(disCountMoney)) < 0) {
-                        return R.fail(new ErrorInfo(400, TradeStatusEnum.MealLimitMoney.toString(), "卡类夜宵限制额度",
-                                                    TradeStatusEnum.MealLimitMoney.getName()));
-                    }
-                }
-            }
-        }
-        return R.ok();
-    }
-
-    /**
-     * 卡类的折扣处理
-     *
-     * @param termVo        设备信息
-     * @param userCardVo    卡片信息
-     * @param cardLimitedVo 卡片限制信息
-     * @param mealTypeVo    餐类信息
-     * @return 验证结果
-     */
-    private R<ErrorInfo> checkCardDisCount(XfTermVo termVo, RemoteCardVo userCardVo, XfCardLimitedVo cardLimitedVo, RemoteMealTypeVo mealTypeVo,
-                                           ConsumptionBo bo) {
-        String useDisCount = remotePtParameterService.getPtParameterByKey("RATE_CONSUME");
-        if (StrUtil.isEmpty(useDisCount) || ObjectUtil.notEqual(useDisCount, "1")) {
-            // 如果没有启用折扣,直接返回
-            return R.ok();
-        }
-        Long disCountTermId = remoteDisCountService.queryDisCountTermIdByTermId(termVo.getTermId());
-        if (ObjectUtil.equals(disCountTermId, 0L)) {
-            // 当前设备没有设置折扣
-            return R.ok();
-        }
-        RemoteDiscountVo remoteDiscountVo = remoteDisCountService.queryDisCountByCardType(userCardVo.getCardType().intValue(),
-                                                                                          mealTypeVo.getTypeId());
-        if (ObjectUtil.isEmpty(remoteDiscountVo)) {
-            // 当前卡类没有折扣信息
-            return R.ok();
-        }
-        String disCountStatus = remoteDiscountVo.getStatus();
-        if (ObjectUtil.isEmpty(disCountStatus) || ObjectUtil.equals(disCountStatus, "0")) {
-            // 卡类折扣未启用
-            return R.ok();
-        }
-        // 计算折扣金额
-        BigDecimal disCountMoney = getDisCountMoney(cardLimitedVo, bo, remoteDiscountVo);
-        // 最后的消费金额为折扣后的金额
-        bo.setConsumeMoney(disCountMoney);
-        return R.ok();
-    }
-
-    /**
-     * 消费记录入库成功后更新其它消费信息
-     * 1.更新卡片交易信息
-     * 2.更新卡片的折扣、限次与限额信息
-     *
-     * @param bo         交易业务对象
-     * @param userCardVo 卡片视图对象
-     * @param mealTypeVo 餐类视图对象
-     * @param termVo     设备视图对象
-     */
-    private void updateOtherConsumeInfo(ConsumptionBo bo, RemoteCardVo userCardVo, RemoteMealTypeVo mealTypeVo, XfTermVo termVo) {
-        Long mealType = Long.parseLong(mealTypeVo.getTypeId());
-        Date consumeDate = bo.getConsumeDate();
-        String currentDateStr = DateFormatUtils.format(new Date(), "yyyy-MM-dd");
-        String consumeDateStr = DateFormatUtils.format(consumeDate, "yyyy-MM-dd");
-        if (ObjectUtil.equals(currentDateStr, consumeDateStr)) {
-            // 更新卡片交易信息
-            Boolean result = remoteCardService.updateCardDayData(userCardVo.getCardNo(), mealType, bo.getConsumeMoney(), consumeDate);
-
-            // 限次消费处理
-            String useLimited = remotePtParameterService.getPtParameterByKey("XC_CONSUME");
-            // 启用了限次
-            if (ObjectUtil.isNotEmpty(useLimited) && ObjectUtil.equals(useLimited, "1")) {
-                Long limitedTermId = remoteLimitedService.queryLimitedTermIdByTermId(termVo.getTermId());
-                // 当前设备也有限次
-                if (ObjectUtil.isNotEmpty(limitedTermId) && limitedTermId > 0) {
-                    // 查询卡类的限次信息
-                    RemoteLimitedVo remoteLimitedVo = remoteLimitedService.queryLimitedByCardType(userCardVo.getCardType().intValue());
-                    // 有卡类的限次信息并已启用
-                    if (ObjectUtil.isNotEmpty(remoteLimitedVo) && ObjectUtil.equals(remoteLimitedVo.getStatus(), "1")) {
-                        // 更新卡类的限次信息
-                        result = cardLimitedService.updateLimitedData(userCardVo.getCardNo());
-                    }
-                }
-            }
-            // 限额消费处理
-            String useQuota = remotePtParameterService.getPtParameterByKey("XE_CONSUME");
-            // 启用了限额
-            if (ObjectUtil.isNotEmpty(useQuota) && ObjectUtil.equals(useQuota, "1")) {
-                Long quotaId = remoteQuotaService.queryQuotaTermIdByTermId(termVo.getTermId());
-                // 当前设备也有限额
-                if (ObjectUtil.isNotEmpty(quotaId) && quotaId > 0) {
-                    // 查询卡类的限额信息
-                    RemoteQuotaVo remoteQuotaVo = remoteQuotaService.queryQuotaByCardType(userCardVo.getCardType().intValue());
-                    // 有卡类的限额信息并已启用
-                    if (ObjectUtil.isNotEmpty(remoteQuotaVo) && ObjectUtil.equals(remoteQuotaVo.getStatus(), "1")) {
-                        // 更新卡类的限额信息
-                        result = cardLimitedService.updateQuotaData(userCardVo.getCardNo(), bo.getConsumeMoney());
-                    }
-                }
-            }
-            // 折扣消费处理
-            String useDisCount = remotePtParameterService.getPtParameterByKey("RATE_CONSUME");
-            // 启用了折扣
-            if (ObjectUtil.isNotEmpty(useDisCount) && ObjectUtil.equals(useDisCount, "1")) {
-                Long disCountTermId = remoteDisCountService.queryDisCountTermIdByTermId(termVo.getTermId());
-                // 当前设备也有折扣
-                if (ObjectUtil.isNotEmpty(disCountTermId) && disCountTermId > 0) {
-                    // 查询卡类的折扣信息
-                    RemoteDiscountVo remoteDisCountVo = remoteDisCountService.queryDisCountByCardType(userCardVo.getCardType().intValue(),
-                                                                                                      mealTypeVo.getTypeId());
-                    // 有卡类的折扣信息并已启用
-                    if (ObjectUtil.isNotEmpty(remoteDisCountVo) && ObjectUtil.equals(remoteDisCountVo.getStatus(), "1")) {
-                        // 更新卡类的折扣信息
-                        result = cardLimitedService.updateDisCountData(userCardVo.getCardNo());
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * 初始化卡片限制信息
-     *
-     * @param cardNo      卡流水号
-     * @param mealType    餐类
-     * @param lastPayDate 最后消费时间
-     * @return 卡片限制信息
-     */
-    private XfCardLimitedVo initXfCardLimited(Long cardNo, Long mealType, Date lastPayDate) {
-        LocalDateTime lastPayLocalDt = LocalDateTime.ofInstant(lastPayDate.toInstant(), ZoneOffset.of("+8"));
-        XfCardLimitedBo bo = new XfCardLimitedBo();
-        bo.setCardNo(cardNo);
-        bo.setDayCount(0L);
-        bo.setDayMoney(BigDecimal.ZERO);
-        bo.setMealCount(0L);
-        bo.setMealMoney(BigDecimal.ZERO);
-        bo.setDayDiscountCount(0L);
-        bo.setMealDiscountCount(0L);
-        bo.setLastPay(Date.from(lastPayLocalDt.toInstant(ZoneOffset.of("+8"))));
-        bo.setLastMeal(mealType);
-
-        return cardLimitedService.insertReturnByBo(bo);
-    }
-
-    /**
-     * 获取营业员信息
-     *
-     * @param bo 消费业务对象
-     * @return 营业员信息
-     */
-    private RemoteOperatorVo getOperatorVo(ConsumptionBo bo) {
-        RemoteOperatorVo operatorVo = remoteOperatorService.getVoById(bo.getOperatorId());
-        if (ObjectUtil.isEmpty(operatorVo)) {
-            operatorVo = new RemoteOperatorVo();
-            operatorVo.setOperatorId(bo.getOperatorId());
-            operatorVo.setOperatorName(bo.getOperatorName());
-        }
-        return operatorVo;
-    }
-
-    /**
-     * 处理脱机消费记录.
-     *
-     * @param bo            消费记录对象
-     * @param originalId    原始记录Id
-     * @param userAccountVo 用户账户信息
-     * @return a Result object containing ErrorInfo if there is an error, otherwise null
-     */
-    private R<ErrorInfo> doOfflineRecord(ConsumptionBo bo, String originalId, RemoteUserAccountVo userAccountVo) {
-        try {
-            XfConsumeDetailOriginalVo vo;
-            Date currentDate = DateUtil.date();
-            int recordStatus = bo.getRecordStatus().intValue();
-            Long RecordId = bo.getRecordId();
-            if (RecordId == 0 && (recordStatus == 106 || recordStatus == 108 || recordStatus == 110 || recordStatus == 111)) {
-                if (recordStatus == 110 || recordStatus == 111) {
-                    vo = consumeDetailOriginalService.queryByConsumeMoney(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(),
-                                                                          bo.getOperatorMoney());
-                    if (ObjectUtil.isNotEmpty(vo)) {
-                        resetBoByOfflineResult(bo, vo);
-                    }
-                } else {
-                    vo = consumeDetailOriginalService.queryByConsumeDate(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(), bo.getConsumeDate());
-                    if (ObjectUtil.isNotEmpty(vo)) {
-                        resetBoByOfflineResult(bo, vo);
-                    } else {
-                        vo = consumeDetailOriginalService.queryById(originalId);
-                        resetBoByOfflineResult(bo, vo);
-                    }
-                }
-                if (bo.getRecordId() == 0) {
-                    if (bo.getConsumeDate().getTime() > currentDate.getTime()) {
-                        bo.setConsumeDate(currentDate);
-                        originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
-                                                               bo.getTermRecordId().shortValue(), bo.getUserNo().intValue(), 0);
-                        bo.setOriginalId(originalId);
-                    }
-                    vo = new XfConsumeDetailOriginalVo();
-                    R<ErrorInfo> result = baseBusiness.createOriginalOrder(bo, userAccountVo, vo);
-                    if (R.isError(result)) {
-                        return result;
-                    }
-                    bo.setRecordId(vo.getRecordId());
-                    bo.setOriginalId(vo.getOriginalId());
-                }
-            } else {
-                consumeDetailOriginalService.updateRecordStatusByOrginId(recordStatus, originalId);
-            }
-            return R.ok();
-        } catch (Exception e) {
-            log.error("[处理脱机记录错误]-[{}]-[{}]", e.getMessage(), Arrays.toString(e.getStackTrace()));
-            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "处理脱机记录错误",
-                                        MessageFormat.format("错误消息:{0}", e.getMessage())));
-        }
-    }
-
-    /**
-     * Resets the ConsumptionBo based on the offline result from XfConsumeDetailOriginalVo.
-     * If the provided vo is not null, it updates the bo's recordId and originalId with those from the vo.
-     * Additionally, if the dataFlag in the vo is 0, indicating a specific condition related to online transactions,
-     * it adjusts the recordStatus of the bo by adding 256 (to mark it as an online transaction) and updates this
-     * new status in the database for the given originalId.
-     *
-     * @param bo The ConsumptionBo object to be reset.
-     * @param vo The XfConsumeDetailOriginalVo containing the offline result information.
-     */
-    private void resetBoByOfflineResult(ConsumptionBo bo, XfConsumeDetailOriginalVo vo) {
-        if (ObjectUtil.isNotEmpty(vo)) {
-            bo.setRecordId(vo.getRecordId());
-            bo.setOriginalId(vo.getOriginalId());
-            if (vo.getDataFlag() == 0) {
-                /*
-                  如果库中原来的原始记录的dataFlag为零,则说明是在线交易时收到一次数据,然而应答之后又没有收到上传的记录,设备却写入了
-                  此种记录将采集时得到的DataFlag加上高字节的在线标识256后,得到正确DataFlag,并将之更新到库中
-                 */
-                Long recordStatus = bo.getRecordStatus() + 256;
-                bo.setRecordStatus(recordStatus);
-                consumeDetailOriginalService.updateRecordStatusByOrginId(recordStatus.intValue(), bo.getOriginalId());
-            }
-        }
-    }
-}

+ 61 - 58
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java

@@ -22,18 +22,25 @@ import org.dromara.common.core.enums.ConsumeRecordTypeEnum;
 import org.dromara.common.core.enums.CreditTypeEnum;
 import org.dromara.common.core.enums.ResultCodeEnum;
 import org.dromara.common.core.enums.SystemUseTypeEnum;
+import org.dromara.common.core.exception.consume.ConsumeException;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.check.AllowConsumeValidationContext;
+import org.dromara.server.consume.check.CommonCheck;
+import org.dromara.server.consume.check.ConsumeRequestCheck;
+import org.dromara.server.consume.check.ConsumeUploadCheck;
 import org.dromara.server.consume.domain.vo.PtBagVo;
-import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
 import org.dromara.server.consume.domain.vo.XfTermVo;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -50,64 +57,55 @@ import java.util.concurrent.atomic.AtomicInteger;
 @Service
 @RequiredArgsConstructor
 public class ConsumeBusiness {
-    private final CheckBusiness checkBusiness;
+    private final CommonCheck commonCheck;
+    private final ConsumeRequestCheck requestCheck;
+    private final ConsumeUploadCheck uploadCheck;
     private final BaseBusiness baseBusiness;
     private final DefaultConfig defaultConfig;
-    // private final KafkaNormalProducer kafkaProducer;
+
 
     /**
      * 请求消费
      *
      * @param bo    消费记录
-     * @param mac 校验码
+     * @param mac   校验码
      * @param xfPwd 消费密码
      * @return 请求结果
      */
     public R<ErrorInfo> createOrder(ConsumptionBo bo, String mac, String xfPwd) {
-        long startTime;
-        log.warn("[请求交易]-[开始参数验证]-[{}]", JSONUtil.toJsonStr(bo));
-        R<ErrorInfo> result = checkBusiness.checkParam(bo);
+        long startTime = System.currentTimeMillis();
+        log.warn("[请求交易]-[开始记录有效性验证]-[{}]", JSONUtil.toJsonStr(bo));
+        AllowConsumeValidationContext ctx = AllowConsumeValidationContext.create(bo);
+        R<ErrorInfo> result = commonCheck.consumeValidation(bo, ctx);
+        log.info("[请求交易]-[记录有效性验证完成]-[耗时: {} 毫秒]-[记录:{}]", System.currentTimeMillis() - startTime, JSONUtil.toJsonStr(bo));
         if (R.isError(result)) {
-            log.error("[请求交易]-[参数验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
-            return result;
-        }
-        log.warn("[请求交易]-[用户信息验证]-[{}]", JSONUtil.toJsonStr(bo));
-        RemoteUserAccountVo userAccountVo = new RemoteUserAccountVo();
-        RemoteCardVo userCardVo = new RemoteCardVo();
-        result = checkBusiness.checkUser(bo, userAccountVo, userCardVo);
-        if (R.isError(result)) {
-            log.error("[请求交易]-[用户信息验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
+            log.error("[请求交易]-[记录有效性验证失败]-[{}]", result.getData());
             return result;
         }
 
-        log.warn("[请求交易]-[设备信息验证]-[{}]", JSONUtil.toJsonStr(bo));
-        XfTermVo termVo = new XfTermVo();
-         result = checkBusiness.checkTerm(bo, termVo);
-        if (R.isError(result)) {
-            log.error("[请求交易]-[设备验证失败]-[{}]", result.getData());
-            return result;
-        }
+        RemoteUserAccountVo userAccountVo = ctx.getUserAccountVo();
+        RemoteCardVo userCardVo = ctx.getUserCardVo();
+        XfTermVo termVo = ctx.getUseTermVo();
 
         log.warn("[请求交易]-[交易流程验证]-[{}]", JSONUtil.toJsonStr(bo));
+        Map<String, Boolean> mapCardLimited = new HashMap<>(4);
+        XfCardLimitedVo cardLimitedVo = new XfCardLimitedVo();
         startTime = System.currentTimeMillis();
-        result = checkBusiness.checkConsume(bo, userAccountVo, userCardVo, termVo);
+        result = requestCheck.checkConsume(bo, userAccountVo, userCardVo, termVo, mapCardLimited, cardLimitedVo);
         if (R.isError(result)) {
             log.error("[请求交易]-[交易验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
-        log.warn("[请求交易]-[交易流程验证]-[耗时: {} 毫秒]",System.currentTimeMillis()-startTime);
+        log.warn("[请求交易]-[交易流程验证]-[耗时: {} 毫秒]", System.currentTimeMillis() - startTime);
 
         log.warn("[请求交易]-[生成原始消费记录]-[{}]", JSONUtil.toJsonStr(bo));
         startTime = System.currentTimeMillis();
-        XfConsumeDetailOriginalVo originalVo = new XfConsumeDetailOriginalVo();
-        result = baseBusiness.createOriginalOrder(bo, userAccountVo, originalVo);
+        result = requestCheck.completeConsumeRequest(bo, userAccountVo, userCardVo, termVo, mapCardLimited, cardLimitedVo);
         if (R.isError(result)) {
             log.error("[请求交易]-[消费原始记录表入库失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
-        log.warn("[请求交易]-[生成原始消费记录完成]-[耗时: {} 毫秒]",System.currentTimeMillis()-startTime);
-        bo.setRecordId(originalVo.getRecordId());
-        bo.setStatusFlag(originalVo.getStatusFlag().intValue());
+        log.info("[请求交易]-[生成原始消费记录完成]-[耗时: {} 毫秒]", System.currentTimeMillis() - startTime);
 
         return R.ok();
     }
@@ -116,48 +114,52 @@ public class ConsumeBusiness {
      * 上传消费记录
      *
      * @param bo    消费记录
-     * @param mac 校验码
+     * @param mac   校验码
      * @param xfPwd 消费密码
      * @return 上传结果
      */
     public R<ErrorInfo> postOrder(ConsumptionBo bo, String mac, String xfPwd) {
-        log.warn("[上传交易]-[开始参数验证]-[{}]", JSONUtil.toJsonStr(bo));
-        R<ErrorInfo> result = checkBusiness.checkParam(bo);
-        if (R.isError(result)) {
-            log.error("[上传交易]-[参数验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
-            return result;
-        }
-        log.warn("[上传交易]-[消费设备验证]-[{}]", JSONUtil.toJsonStr(bo));
-        XfTermVo termVo = new XfTermVo();
-        result = checkBusiness.checkTerm(bo, termVo);
+        long startTime = System.currentTimeMillis();
+        // log.warn("[上传交易]-[开始记录有效性验证]-[{}]", JSONUtil.toJsonStr(bo));
+        AllowConsumeValidationContext ctx = AllowConsumeValidationContext.create(bo);
+        R<ErrorInfo> result = commonCheck.consumeValidation(bo, ctx);
+        log.info("[上传交易]-[记录有效性验证完成]-[耗时: {} 毫秒]-[记录:{}]", System.currentTimeMillis() - startTime, JSONUtil.toJsonStr(bo));
         if (R.isError(result)) {
-            log.error("[上传交易]-[设备验证失败]-[{}]", result.getData());
+            log.error("[上传交易]-[记录有效性验证失败]-[{}]", result.getData());
             return result;
         }
+
         log.warn("[上传交易]-[交易账单处理]-[{}]", JSONUtil.toJsonStr(bo));
-        RemoteUserAccountVo userAccountVo = new RemoteUserAccountVo();
-        RemoteCardVo userCardVo = new RemoteCardVo();
         List<PtBagVo> bagVos = new ArrayList<>();
+        RemoteUserAccountVo userAccountVo = ctx.getUserAccountVo();
+        RemoteCardVo userCardVo = ctx.getUserCardVo();
+        XfTermVo termVo = ctx.getUseTermVo();
         RemoteMealTypeVo mealTypeVo = new RemoteMealTypeVo();
         RemoteOperatorVo operatorVo = new RemoteOperatorVo();
-        result = checkBusiness.checkBill(bo, userAccountVo, userCardVo, termVo, bagVos, mealTypeVo,operatorVo);
+        result = uploadCheck.checkBill(bo, userAccountVo, termVo, bagVos, mealTypeVo);
         if (R.isError(result)) {
             log.error("[上传交易]-[交易账单处理失败]-[{}]", result.getData());
             return result;
         }
         log.warn("[上传交易]-[交易入库]-[{}]", JSONUtil.toJsonStr(bo));
-        result = baseBusiness.postConsumeRecord(bo, userAccountVo, userCardVo, bagVos, termVo, mealTypeVo, "");
-        if (R.isError(result)) {
-            log.error("[上传交易]-[交易入库失败]-[{}]", result.getData());
-            return result;
+        try {
+            result = baseBusiness.postConsumeRecord(bo, userAccountVo, userCardVo, bagVos, termVo, mealTypeVo, "");
+            if (R.isError(result)) {
+                log.error("[上传交易]-[交易入库失败]-[{}]", result.getData());
+                return result;
+            }
+            return R.ok();
+        } catch (ConsumeException e) {
+            log.error("[[上传交易]-[交易入库失败]-[{}]", e.getMessage(), e);
+            return R.fail(new ErrorInfo(500, ApiErrorTypeConstants.EXCEPTION, e.getMessage(), ""));
         }
-        return R.ok();
     }
 
     /**
      * 全流程消费处理 将请求消费和上传消费记录一次性执行完成
-     * @param bo 消费业务对象
-     * @param mac 校验码
+     *
+     * @param bo    消费业务对象
+     * @param mac   校验码
      * @param xfPwd 消费密码
      * @return 处理结果
      */
@@ -173,12 +175,12 @@ public class ConsumeBusiness {
         }
 
         R<ErrorInfo> result = this.createOrder(bo, mac, xfPwd);
-        if(!R.isSuccess(result)) {
+        if (!R.isSuccess(result)) {
             log.error("[请求交易]-[请求交易处理失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
         result = this.postOrder(bo, mac, xfPwd);
-        if(!R.isSuccess(result)) {
+        if (!R.isSuccess(result)) {
             log.error("[交易上传]-[交易上传处理失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
@@ -187,20 +189,21 @@ public class ConsumeBusiness {
 
     /**
      * 原始消费对账,将有原始消费记录但没有消费明细的消费记录写入消费明细
+     *
      * @return 对账结果
      */
-    public R<ErrorInfo> originalReconciliation(String consumeDate){
-        //先查询没有写入消费明细的原始消费记录
+    public R<ErrorInfo> originalReconciliation(String consumeDate) {
+        // 先查询没有写入消费明细的原始消费记录
         List<ConsumptionBo> list = baseBusiness.selectOriginalReconciliation(DateUtil.parseDate(consumeDate));
         if (CollectionUtil.isEmpty(list)) {
             return R.ok(new ErrorInfo(ResultCodeEnum.DATA_NOT_FOUND.code(), ApiErrorTypeConstants.NOT_FOUND, "没有待入账的原始消费记录"));
         }
         List<String> doMessage = new ArrayList<>();
-        //循环写入原始消费记录
+        // 循环写入原始消费记录
         int total = list.size();
         AtomicInteger success = new AtomicInteger();
         AtomicInteger fail = new AtomicInteger();
-        list.forEach(p->{
+        list.forEach(p -> {
             p.setUseType(SystemUseTypeEnum.CONSUME.code());
             p.setCreditType(baseBusiness.getCreditType(p.getStatusFlag()));
             p.setRecordStatus(364L);
@@ -215,7 +218,7 @@ public class ConsumeBusiness {
                         ThreadUtil.execAsync(() -> baseBusiness.sendCloudConsume(bo));
                     }
                 } else {
-                    doMessage.add(MessageFormat.format("[入账失败]-[{0}]-[{1}]", JsonUtils.toJsonString(p),JSONUtil.toJsonStr(result.getData())));
+                    doMessage.add(MessageFormat.format("[入账失败]-[{0}]-[{1}]", JsonUtils.toJsonString(p), JSONUtil.toJsonStr(result.getData())));
                     fail.getAndIncrement();
                 }
             } catch (Exception e) {

+ 6 - 1
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/EmployeeBusiness.java

@@ -10,9 +10,11 @@ import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.common.core.api.ReturnResult;
 import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.domain.model.ErrorResult;
 import org.dromara.common.core.enums.ResultCodeEnum;
+import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.consume.domain.vo.yc.*;
 import org.dromara.system.api.RemoteDeptService;
 import org.dromara.system.api.domain.vo.RemoteDeptVo;
@@ -41,6 +43,7 @@ public class EmployeeBusiness {
 
     public ReturnResult getEmployeeVoByNumb(String userNumb) {
         RemoteUserAccountVo userAccountVo = remoteUserAccountService.getUserAccountVoByUserNumb(userNumb);
+        // RemoteUserAccountVo userAccountVo = RedisUtils.getCacheMapValue(CacheNames);
         if (ObjectUtil.isEmpty(userAccountVo)) {
             ErrorResult result = new ErrorResult();
             result.setStatusCode(HttpStatus.NOT_FOUND.value());
@@ -60,7 +63,9 @@ public class EmployeeBusiness {
         departmentVo.setParentDepartmentID(remoteDeptVo.getParentId().toString());
         departmentVo.setDepartmentName(remoteDeptVo.getDeptName());
 
-        RemoteCardVo remoteCardVo = remoteCardService.queryMainCardByUserId(userAccountVo.getUserId());
+        // RemoteCardVo remoteCardVo = remoteCardService.queryMainCardByUserId(userAccountVo.getUserId());
+        String userId = String.valueOf(userAccountVo.getUserId());
+        RemoteCardVo remoteCardVo = RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, userId);
         UserCardVo userCardVo = new UserCardVo();
         Long cardNo = 0L;
         if (ObjectUtil.isNotEmpty(remoteCardVo)) {

+ 284 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/InitBusiness.java

@@ -0,0 +1,284 @@
+package org.dromara.server.consume.business;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.*;
+import org.dromara.backstage.api.domain.vo.*;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.server.consume.domain.bo.XfCardLimitedBo;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.dromara.server.consume.service.IPtBagService;
+import org.dromara.server.consume.service.IXfCardLimitedService;
+import org.dromara.server.consume.service.IXfTermService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * 消费相关基础数据初始化类
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-17
+ * @since JDK17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InitBusiness {
+    private final IPtBagService bagService;
+    private final IXfCardLimitedService cardLimitedService;
+    private final IXfTermService xfTermService;
+
+    @DubboReference
+    private final RemotePtParameterService remotePtParameterService;
+    @DubboReference
+    private final RemoteXfLimitedService remoteLimitedService;
+    @DubboReference
+    private final RemoteXfDiscountService remoteDisCountService;
+    @DubboReference
+    private final RemoteXfQuotaService remoteQuotaService;
+    @DubboReference
+    private final RemoteUserAccountService remoteUserAccountService;
+    @DubboReference
+    private final RemoteCardService remoteCardService;
+    @DubboReference
+    private final RemoteMealTypeService remoteMealTypeService;
+
+    /**
+     * 初始化全局消费参数.
+     * 这部分数据系统初始化时设置后基本不会改动,只需要启动后初始化即可
+     * <p>
+     * 1.是否全局启用了消费限次
+     * 2.是否全局启用了消费限额
+     * 3.是否全局启用了消费折扣
+     * >
+     */
+    public void initGlobalData() {
+        String disCount = remotePtParameterService.getPtParameterByKey("RATE_CONSUME");
+        if (ObjectUtil.isNotEmpty(disCount)) {
+            RedisUtils.setCacheMapValue(CacheNames.PT_PARAMETER, "RATE_CONSUME", disCount);
+        }
+        String quota = remotePtParameterService.getPtParameterByKey("XE_CONSUME");
+        if (ObjectUtil.isNotEmpty(quota)) {
+            RedisUtils.setCacheMapValue(CacheNames.PT_PARAMETER, "XE_CONSUME", quota);
+        }
+        String limited = remotePtParameterService.getPtParameterByKey("XC_CONSUME");
+        if (ObjectUtil.isNotEmpty(limited)) {
+            RedisUtils.setCacheMapValue(CacheNames.PT_PARAMETER, "XC_CONSUME", limited);
+        }
+
+        log.info("初始化全局限次、限额和折扣参数完成");
+    }
+
+    /**
+     * 初始化折扣、限次、限额信息
+     * 需要折扣、限额和限次的设备与卡类在系统使用前就会初始化,后续除了增加新设备外,一般不再改动
+     * 在服务启动时加载一次,再根据是否有调整数据手工加载或第天定时加载一次
+     */
+    public void initDiscountAndOther() {
+        List<Long> limitedTermIds = remoteLimitedService.selectLimitedTermIds();
+        if (CollUtil.isNotEmpty(limitedTermIds)) {
+            limitedTermIds.forEach(p -> {
+                String key = String.valueOf(p);
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_LIMITEDTERM, key, p);
+            });
+        }
+        log.info("初始化限次设备参数完成");
+
+        List<RemoteLimitedVo> limitedCards = remoteLimitedService.selectLimitedCards();
+        if (CollUtil.isNotEmpty(limitedCards)) {
+            limitedCards.forEach(p -> {
+                String key = String.valueOf(p.getCardType());
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_LIMITED, key, p);
+            });
+        }
+        log.info("初始化限次卡类参数完成");
+
+        List<Long> discountTermIds = remoteDisCountService.selectDiscountTermIds();
+        if (CollUtil.isNotEmpty(discountTermIds)) {
+            discountTermIds.forEach(p -> {
+                String key = String.valueOf(p);
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_DISCOUNTTERM, key, p);
+            });
+        }
+        log.info("初始化折扣设备参数完成");
+
+        List<RemoteDiscountVo> discountCards = remoteDisCountService.selectDiscountCards();
+        if (CollUtil.isNotEmpty(discountCards)) {
+            discountCards.forEach(p -> {
+                String key = String.valueOf(p.getCardType());
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_DISCOUNT, key, p);
+            });
+        }
+        log.info("初始化折扣卡类参数完成");
+
+        List<Long> quotaTermIds = remoteQuotaService.selectQuotaTermIds();
+        if (CollUtil.isNotEmpty(quotaTermIds)) {
+            quotaTermIds.forEach(p -> {
+                String key = String.valueOf(p);
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_QUOTATERM, key, p);
+            });
+        }
+        log.info("初始化限额设备参数完成");
+
+        List<RemoteQuotaVo> quotaCards = remoteQuotaService.selectQuotaCards();
+        if (CollUtil.isNotEmpty(quotaCards)) {
+            quotaCards.forEach(p -> {
+                String key = String.valueOf(p.getCardType());
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_QUOTA, key, p);
+            });
+        }
+        log.info("初始化限额卡类参数完成");
+    }
+
+    /**
+     * 初始化用户总余额
+     * 余额会因充值、退款、错扣补款、消费等原因发生变化,所以需要定时初始化
+     */
+    public void initUserBalance() {
+        List<Long> idList = remoteUserAccountService.getUserAccountIdList();
+        idList.forEach(id -> {
+            String userId = String.valueOf(id);
+            BigDecimal balance = bagService.getUserBalance(id);
+            RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, userId, balance);
+        });
+        RedisUtils.expire(CacheNames.USER_TOTAL_BALANCE, Duration.ofHours(5));
+    }
+
+    /**
+     * 初始化指定人员余额
+     *
+     * @param id 人员Id
+     */
+    public void initUserBalanceByUserId(Long id) {
+        String userId = String.valueOf(id);
+        BigDecimal balance = bagService.getUserBalance(id);
+        RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, userId, balance);
+        log.info("初始化指定人员余额完成,Id:{}", id);
+    }
+
+    /**
+     * 初始化人员卡片
+     */
+    public void initUserCard() {
+        List<RemoteCardVo> list = remoteCardService.selectNormalCards();
+        RedisUtils.deleteKeys(CacheNames.PT_USER_CARD_NO);
+        RedisUtils.deleteKeys(CacheNames.PT_USER_CARD_USER_ID);
+        list.forEach(v -> {
+            String cardNo = String.valueOf(v.getCardNo());
+            String factoryId = String.valueOf(v.getFactoryId());
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_NO, cardNo, v);
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, factoryId, v);
+        });
+        RedisUtils.expire(CacheNames.PT_USER_CARD_NO, Duration.ofHours(4));
+        RedisUtils.expire(CacheNames.PT_USER_CARD_USER_ID, Duration.ofHours(4));
+    }
+
+    /**
+     * 初始化指定人员卡片
+     *
+     * @param id 人员Id
+     */
+    public void initUserCardByUserId(Long id) {
+        RemoteCardVo cardVo = remoteCardService.queryMainCardByUserId(id);
+        if (ObjectUtil.isNotEmpty(cardVo)) {
+            String cardNo = String.valueOf(cardVo.getCardNo());
+            String factoryId = String.valueOf(cardVo.getFactoryId());
+
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_NO, cardNo, cardVo);
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, factoryId, cardVo);
+        }
+    }
+
+    /**
+     * 初始化账户信息
+     * 账户基本信息、账户余额信息
+     */
+    public void initUserAccount() {
+        List<RemoteUserAccountVo> list = remoteUserAccountService.getAllUserAccountVo();
+        List<RemoteUserAccountVo> filterList = list.stream().filter(
+            p -> p.getDelFlag().equals("0")
+                     && p.getLifespan().getTime() > System.currentTimeMillis()).toList();
+
+        if (CollectionUtil.isNotEmpty(filterList)) {
+            RedisUtils.deleteKeys(CacheNames.USER_TOTAL_BALANCE);
+            RedisUtils.deleteKeys(CacheNames.PT_USER_ACCOUNT_ID);
+            RedisUtils.deleteKeys(CacheNames.PT_USER_ACCOUNT_NO);
+            list.forEach(v -> {
+                Long userId = v.getUserId();
+                String strUserId = String.valueOf(userId);
+                RedisUtils.setCacheMapValue(CacheNames.PT_USER_ACCOUNT_ID, strUserId, v);
+                RedisUtils.setCacheMapValue(CacheNames.PT_USER_ACCOUNT_NO, v.getUserNo().toString(), v);
+
+                BigDecimal balance = bagService.getUserBalance(userId);
+                RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, strUserId, balance);
+
+            });
+            RedisUtils.expire(CacheNames.PT_USER_ACCOUNT_ID, Duration.ofHours(5));
+            RedisUtils.expire(CacheNames.PT_USER_ACCOUNT_NO, Duration.ofHours(5));
+            RedisUtils.expire(CacheNames.USER_TOTAL_BALANCE, Duration.ofHours(5));
+        }
+    }
+
+    /**
+     * 初始化账户信息
+     * 账户基本信息、账户余额信息
+     * @param id 人员Id
+     */
+    public void initUserAccountById(Long id) {
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoById(id);
+        if (ObjectUtil.isNotEmpty(accountVo)) {
+            String strUserId = String.valueOf(id);
+            String strUserNo = String.valueOf(accountVo.getUserNo());
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_ACCOUNT_ID, strUserId, accountVo);
+            RedisUtils.setCacheMapValue(CacheNames.PT_USER_ACCOUNT_NO, strUserNo, accountVo);
+
+            BigDecimal balance = bagService.getUserBalance(id);
+            RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, strUserId, balance);
+        }
+    }
+
+    /**
+     * 初始化消费过程中的卡片限制数据
+     */
+    public void initXfCardLimited() {
+        XfCardLimitedBo bo = new XfCardLimitedBo();
+        List<XfCardLimitedVo> list = cardLimitedService.queryList(bo);
+        if (CollectionUtil.isNotEmpty(list)) {
+            list.forEach(p -> {
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_CARD_LIMITED, p.getCardNo().toString(), p);
+            });
+            RedisUtils.expire(CacheNames.T_XF_CARD_LIMITED, Duration.ofHours(5));
+        }
+    }
+
+    /**
+     * 初始化消费清单
+     */
+    public void initTermInfo() {
+        List<XfTermVo> list = xfTermService.queryList();
+        RedisUtils.deleteKeys(CacheNames.PT_TERM_LIST);
+        RedisUtils.setCacheList(CacheNames.PT_TERM_LIST, list);
+        RedisUtils.expire(CacheNames.PT_TERM_LIST, Duration.ofDays(1));
+    }
+
+    /**
+     * 初始化消费类
+     */
+    public void initMealTypeInfo() {
+        List<RemoteMealTypeVo> list = remoteMealTypeService.selectMealTypeList();
+        RedisUtils.deleteKeys(CacheNames.PT_MEAL_TYPE_LIST);
+        RedisUtils.setCacheList(CacheNames.PT_MEAL_TYPE_LIST, list);
+        RedisUtils.expire(CacheNames.PT_MEAL_TYPE_LIST, Duration.ofDays(1));
+    }
+}

+ 138 - 125
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/TermBusiness.java

@@ -1,18 +1,15 @@
 package org.dromara.server.consume.business;
 
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.dubbo.config.annotation.DubboReference;
-import org.dromara.backstage.api.RemotePtXfTermService;
-import org.dromara.backstage.api.domain.vo.RemoteXfTermVo;
 import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.constant.DefaultConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.enums.SystemUseTypeEnum;
-import org.dromara.common.core.exception.ServiceException;
-import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.consume.cache.TokenManager;
 import org.dromara.server.consume.domain.vo.XfTermVo;
@@ -20,63 +17,67 @@ import org.dromara.server.consume.domain.vo.yc.RoomInfo;
 import org.dromara.server.consume.domain.vo.yc.SettlementAccount;
 import org.dromara.server.consume.domain.vo.yc.TermInfo;
 import org.dromara.server.consume.domain.vo.yc.TermToken;
-import org.dromara.server.consume.service.IXfTermService;
-import org.dromara.system.api.RemoteUserService;
-import org.dromara.system.api.domain.vo.RemoteUserVo;
-import org.redisson.api.RLock;
 import org.springframework.stereotype.Service;
 
 import java.text.MessageFormat;
-import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 /**
- * @ClassName TermBusiness
- * @Description TODO
- * @Author luoyibo
- * @Date 2024-12-18 08:50
- * @Version 1.0
- * @since jdk17
+ * TermBusiness 类提供了与终端设备相关的业务逻辑处理功能。
+ * 该类封装了终端设备的认证、信息查询、时间校验以及设备令牌管理等操作。
+ * 所有方法均返回通用响应类 R,包含操作结果的状态和数据,便于统一处理成功或失败的情况。
+ * <p>
+ * 主要功能包括:
+ * - 获取终端设备的令牌(TermToken),用于身份验证和授权。
+ * - 校验终端设备的时间,确保设备时间与系统时间同步。
+ * - 根据终端编号或终端编号与租户ID获取终端设备的详细信息。
+ * - 获取指定类型应用的Logo信息。
+ * <p>
+ * 内部方法支持设备信息的转换、设备令牌的创建与刷新,确保终端设备与系统之间的交互顺畅。
+ * <p>
+ * 注意:部分方法为私有方法,仅在类内部使用,外部不可直接调用。
+ * @author      yiboLuo
+ * @date        2025-06-17
+ * @version     2.2.0
+ * @since       JDK17
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class TermBusiness {
-    private static final Object locker = new Object();
     private static final ConcurrentHashMap<Integer, Long> lastCheckModify = new ConcurrentHashMap<>();
-    private static final ConcurrentHashMap<Integer, Long> lastModify = new ConcurrentHashMap<>();
-    private static final ConcurrentHashMap<Integer, Long> lastCheckBlack = new ConcurrentHashMap<>();
-    private static final ConcurrentHashMap<Integer, Long> lastBlack = new ConcurrentHashMap<>();
-    @DubboReference
-    private final RemoteUserService remoteUserService;
-    @DubboReference
-    private final RemotePtXfTermService remoteTermService;
-    private final IXfTermService termService;
-    private final TokenManager tokenManager;
-    private final DefaultConfig defaultConfig;
     // 常量定义(2000-01-01 00:00:00+8 的固定时间戳)
     private static final long MIN_TIME = LocalDateTime.of(2000, 1, 1, 0, 0, 0)
-        .toInstant(ZoneOffset.of("+8")).toEpochMilli();
-    private static final long TOKEN_EXPIRE_HOURS = 2;
+                                             .toInstant(ZoneOffset.of("+8")).toEpochMilli();
+    private static final long TOKEN_EXPIRE_HOURS = 4;
     private static final long TOKEN_EXPIRE_MS = TOKEN_EXPIRE_HOURS * 3600000;
+    // 校时60过期
+    private static final long CACHE_EXPIRATION_MS = TimeUnit.MINUTES.toMillis(1);
+
+    private final TokenManager tokenManager;
+    private final DefaultConfig defaultConfig;
+
+    /**
+     * 获取终端令牌(TermToken)。
+     *
+     * @param termNo 终端编号,用于唯一标识一个终端。
+     * @param admin  管理员账号,用于身份验证。
+     * @param pwd    管理员密码,用于身份验证。
+     * @return 返回一个封装了 TermToken 的 R 对象。R 是通用的响应类,通常包含操作结果的状态和数据。
+     *         如果操作成功,则 TermToken 将包含在返回值中;如果失败,则可能包含错误信息。
+     */
     public R<TermToken> getTermToken(Long termNo, String admin, String pwd) {
-        RemoteXfTermVo remoteVo = remoteTermService.queryByNo(termNo, defaultConfig.getTenantId());
-        if (ObjectUtil.isEmpty(remoteVo)) {
-            return R.fail(MessageFormat.format("机号为[{0}]的设备不存在", termNo), null);
+        R<TermInfo> result = this.getTermInfoByTermNo(termNo);
+        if(R.isError(result)) {
+            return R.fail(result.getMsg());
         }
-        //RemoteUserVo userVo = remoteUserService.selectUserVoByUserName(admin, defaultConfig.getTenantId());
-        //if (ObjectUtil.isEmpty(userVo)) {
-        //    return R.fail(MessageFormat.format("用户为[{0}]的用户不存在", admin), null);
-        //}
+        TermInfo termInfo = result.getData();
         final String strTermNo = String.valueOf(termNo);
-        final String roomName = remoteVo.getRoomName();
+        final String roomName = termInfo == null ? "未知房间" : termInfo.getRoomName();
 
         // 使用termNo字符串作为锁对象,减小锁粒度
         synchronized (strTermNo.intern()) {
@@ -94,58 +95,98 @@ public class TermBusiness {
                     termToken.setAdmin(admin);
                 }
             }
-            return R.ok(MessageFormat.format("获取token成功,设备编号[{0}],账号[{1}]", termNo, admin), termToken);
+            return R.ok(termToken);
         }
     }
 
-    public R<TermToken> getTermTokenNew(Long termNo, String admin, String pwd){
-        String strTermNo = String.valueOf(termNo);
-        String tenantId = defaultConfig.getTenantId();
-        String cacheName = StringUtils.format("{}:term_token:{}", tenantId, termNo);
-        TermToken termToken =  RedisUtils.getCacheObject(cacheName);
-        if(ObjectUtil.isEmpty(termToken)|| termToken.getExpireTime() < System.currentTimeMillis()) {
-            String lockKey = "LOCK:" + cacheName + ":" + strTermNo;
-            RLock lock = RedisUtils.getClient().getLock(lockKey);
-            try {
-                // 尝试加锁,等待2秒,锁自动释放时间10秒
-                if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {
-                    // 双重检查(Double-Check)
-                    termToken = RedisUtils.getCacheObject(cacheName);
-                    if (ObjectUtil.isEmpty(termToken) || termToken.getExpireTime() < System.currentTimeMillis()) {
-                        // 创建新token
-                        termToken = createAndSaveToken(termNo, admin, strTermNo, cacheName);
-                    }
-                }
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("获取锁中断", e);
-            } finally {
-                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
-                    lock.unlock();
-                }
-            }
+    /**
+     * 终端设备校时。
+     *
+     * @param termNo 终端编号,用于唯一标识一个终端。
+     * @return 返回一个封装了 Map<String, Object> 的 R 对象。R 是通用的响应类,通常包含操作结果的状态和数据。
+     *         如果操作成功,Map 中可能包含与终端时间相关的键值对信息;如果失败,则可能包含错误信息。
+     */
+    public R<Map<String, Object>> checkTermTime(Integer termNo) {
+        // 1. 参数校验前置
+        if (termNo == null || termNo <= 0) {
+            log.warn("无效终端号: {}", termNo);
+            return R.fail("终端号不能为空");
         }
-        return R.ok(MessageFormat.format("获取token成功,设备编号[{0}],账号[{1}]", termNo, admin), termToken);
+
+        final long lTermNo = termNo.longValue();
+
+        // 2. 带缓存的终端信息获取
+        R<TermInfo> result = this.getTermInfoByTermNo(lTermNo);
+        if(R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        TermInfo termInfo = result.getData();
+        // 3. 原子化时间检查与更新
+        final long currentTime = System.currentTimeMillis();
+        final Long lastCheck = lastCheckModify.get(termNo);
+
+        if (lastCheck == null || currentTime > lastCheck) {
+            // 使用原子操作避免并发问题
+            lastCheckModify.compute(termNo, (key, oldVal) ->
+                                                (oldVal == null || currentTime > oldVal) ?
+                                                    currentTime + CACHE_EXPIRATION_MS :
+                                                    oldVal
+            );
+        }
+
+        // 4. 构建响应数据
+        Map<String, Object> resultMap = new HashMap<>(4);
+        resultMap.put("time", DateUtil.format(new Date(currentTime), DefaultConstants.DATE_TIME_FORMAT));
+        resultMap.put("type", "");
+        resultMap.put("data", termInfo != null ? termInfo.getTermName() : "未知设备");
+
+        return R.ok(resultMap);
     }
+
+    /**
+     * 根据终端编号获取终端信息。
+     *
+     * @param termNo 终端编号,用于唯一标识一个终端。
+     * @return 返回一个封装了 TermInfo 的 R 对象。R 是通用的响应类,通常包含操作结果的状态和数据。
+     *         如果操作成功,则 TermInfo 将包含在返回值中;如果失败,则可能包含错误信息。
+     */
     public R<TermInfo> getTermInfoByTermNo(Long termNo) {
         return this.getTermInfoByTermNo(termNo, defaultConfig.getTenantId());
     }
 
+    /**
+     * 根据终端编号和租户ID获取终端信息。
+     *
+     * @param termNo   终端编号,用于唯一标识一个终端。
+     * @param tenantId 租户ID,用于标识数据所属的租户,支持多租户架构。
+     * @return 返回一个封装了 TermInfo 的 R 对象。R 是通用的响应类,通常包含操作结果的状态和数据。
+     *         如果操作成功,则 TermInfo 将包含在返回值中;如果失败,则可能包含错误信息。
+     */
     public R<TermInfo> getTermInfoByTermNo(Long termNo, String tenantId) {
-        RemoteXfTermVo remoteVo = remoteTermService.queryByNo(termNo, tenantId);
-        if (ObjectUtil.isEmpty(remoteVo)) {
-            return R.fail(MessageFormat.format("机号为[{0}]的设备不存在", termNo), null);
+        List<XfTermVo> list = RedisUtils.getCacheList(CacheNames.PT_TERM_LIST);
+        if (CollectionUtil.isEmpty(list)) {
+            return R.fail("无设备清单,请重新加载数据");
         }
+        XfTermVo termVo = list.stream().filter(x -> ObjectUtil.equals(x.getTermNo(), termNo)
+                                                        && ObjectUtil.equals(x.getTenantId(), tenantId)).findFirst().orElse(null);
 
-        TermInfo termInfo = this.convertToYc(remoteVo);
-
-        return R.ok(termInfo);
+        if(termVo == null) {
+            return R.fail(MessageFormat.format("机号为[{0}]的设备不存在", termNo), null);
+        } else {
+            return R.ok(this.convertToYc(termVo));
+        }
     }
 
+    /**
+     * 获取APP的logo
+     *
+     * @param appType app类型
+     * @return app的logo信息
+     */
     public R<Map<String, Object>> getAppLogo(String appType) {
         Map<String, Object> resultMap = new HashMap<>();
 
-        resultMap.put("currentTime", DateUtil.date().getTime());
+        resultMap.put("currentTime", System.currentTimeMillis());
         resultMap.put("success", true);
         resultMap.put("data", null);
         resultMap.put("code", "1");
@@ -154,28 +195,12 @@ public class TermBusiness {
         return R.ok(resultMap);
     }
 
-    public R<Map<String, Object>> checkTermTime(Integer termNo) {
-        XfTermVo termVo = termService.queryVoOneByNo(Long.valueOf(termNo));
-        if (ObjectUtil.isEmpty(termVo)) {
-            return R.fail(MessageFormat.format("机号为[{0}]的设备不存在", termNo), null);
-        }
-        Map<String, Object> resultMap = new HashMap<>();
-        if (!lastCheckModify.containsKey(termNo)) {
-            lastCheckModify.put(termNo, 0L);
-        }
-        Date nowDate = new Date();
-        long currentTime = nowDate.getTime();
-        if (currentTime > lastCheckModify.get(termNo)) {
-            lastCheckModify.put(termNo, currentTime + 1000 * 60);
-        }
-        resultMap.put("time", DateUtil.format(nowDate, DefaultConstants.DATE_TIME_FORMAT));
-        resultMap.put("type", "");
-        resultMap.put("data", termVo == null ? "" : termVo.getTermName());
-
-        return R.ok(resultMap);
-    }
-
-    private TermInfo convertToYc(RemoteXfTermVo termVo) {
+    /**
+     * 将系统中的设备信息转换成消费机可接收的类型
+     * @param termVo 系统消费机信息
+     * @return 消费机可接收的数据
+     */
+    private TermInfo convertToYc(XfTermVo termVo) {
         TermInfo termInfo = new TermInfo();
         termInfo.setTermId(termVo.getTermId().toString());
         termInfo.setTermNo(termVo.getTermNo().intValue());
@@ -263,32 +288,14 @@ public class TermBusiness {
         return termInfo;
     }
 
-    private TermToken createAndSaveToken(Long termNo, String admin, String strTermNo, String cacheName) {
-        // 设备查询
-        RemoteXfTermVo remoteVo = remoteTermService.queryByNo(termNo, defaultConfig.getTenantId());
-        if (ObjectUtil.isEmpty(remoteVo)) {
-            throw new ServiceException(MessageFormat.format("机号为[{0}]的设备不存在", termNo));
-        }
-
-        // 创建token(简化时间处理)
-        long now = System.currentTimeMillis();
-        long expireTime = now + TimeUnit.HOURS.toMillis(4); // 4小时后过期
-
-        TermToken newToken = new TermToken(
-            strTermNo,
-            UUID.randomUUID().toString(),
-            admin,
-            now,
-            Date.from(LocalDateTime.of(2000, 1, 1, 0, 0).toInstant(ZoneOffset.of("+8"))).getTime(),
-            expireTime,
-            remoteVo.getRoomName()
-        );
-
-        // 写入Redis
-        RedisUtils.setCacheObject(cacheName, newToken, Duration.ofHours(4));
-        return newToken;
-    }
-    // 创建新令牌
+    /**
+     * 创建新的设备token
+     *
+     * @param termNo 设备机号
+     * @param admin 管理员账号
+     * @param roomName 设备所在房间号
+     * @return 设备token信息
+     */
     private TermToken createNewTermToken(String termNo, String admin, String roomName) {
         long currentTime = System.currentTimeMillis();
         return new TermToken(
@@ -302,7 +309,13 @@ public class TermBusiness {
         );
     }
 
-    // 重置过期令牌
+    /**
+     * 刷新设备token
+     *
+     * @param token 设备token信息
+     * @param admin 管理员账号
+     * @param roomName 设备所在房间号
+     */
     private void resetTermToken(TermToken token, String admin, String roomName) {
         long currentTime = System.currentTimeMillis();
         token.setToken(UUID.randomUUID().toString());

+ 87 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/cache/ValidationParam.java

@@ -0,0 +1,87 @@
+package org.dromara.server.consume.cache;
+
+import lombok.Getter;
+import org.dromara.backstage.api.domain.vo.RemoteDiscountVo;
+import org.dromara.backstage.api.domain.vo.RemoteLimitedVo;
+import org.dromara.backstage.api.domain.vo.RemoteQuotaVo;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 全局消费校验参数
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-14
+ * @since JDK17
+ */
+@Getter
+@Component
+public class ValidationParam {
+    private final String isUse = "1";
+    private final String noUse = "0";
+    /**
+     * 启用折扣消费标志
+     */
+    private Boolean RATE_CONSUME;
+    /**
+     * 启用限额消费标志
+     */
+    private Boolean XE_CONSUME;
+
+    /**
+     * 启用限次
+     */
+    private Boolean XC_CONSUME;
+
+    /**
+     * 折扣设备Id清单
+     */
+    private List<String> disCountTermIdList;
+
+    /**
+     * 折扣率
+     */
+    private List<RemoteDiscountVo> discountVos;
+
+    /**
+     * 限次设备Id清单
+     */
+    private List<String> limitedTermIdList;
+
+    /**
+     * 限次卡类清单
+     */
+    private List<RemoteLimitedVo> limitedCardList;
+    /**
+     * 限额设备Id清单
+     */
+    private List<String> quotaTermIdList;
+
+    /**
+     * 限额卡类清单
+     */
+    private List<RemoteQuotaVo> quotaCardList;
+
+    public void refresh() {
+        RATE_CONSUME = RedisUtils.getCacheMapValue(CacheNames.PT_PARAMETER, "RATE_CONSUME")
+                           .equals(isUse) ? Boolean.TRUE : Boolean.FALSE;
+        XE_CONSUME = RedisUtils.getCacheMapValue(CacheNames.PT_PARAMETER, "XE_CONSUME")
+                         .equals(isUse) ? Boolean.TRUE : Boolean.FALSE;
+        XC_CONSUME = RedisUtils.getCacheMapValue(CacheNames.PT_PARAMETER, "XC_CONSUME")
+                         .equals(isUse) ? Boolean.TRUE : Boolean.FALSE;
+
+        disCountTermIdList = RedisUtils.getAllCacheMapKey(CacheNames.T_XF_DISCOUNTTERM);
+        discountVos = RedisUtils.getMultiCacheMapValue(CacheNames.T_XF_DISCOUNT);
+
+        limitedTermIdList = RedisUtils.getAllCacheMapKey(CacheNames.T_XF_LIMITEDTERM);
+        limitedCardList = RedisUtils.getMultiCacheMapValue(CacheNames.T_XF_LIMITED);
+
+        quotaTermIdList = RedisUtils.getAllCacheMapKey(CacheNames.T_XF_QUOTATERM);
+        quotaCardList = RedisUtils.getMultiCacheMapValue(CacheNames.T_XF_QUOTA);
+    }
+}

+ 56 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/AllowConsumeValidationContext.java

@@ -0,0 +1,56 @@
+package org.dromara.server.consume.check;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+
+/**
+ * 卡片可消费验证上下文
+ * <p>
+ * 设置卡片可消费验证的相关参数
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-14
+ * @since JDK17
+ */
+@Data
+@Slf4j
+public class AllowConsumeValidationContext {
+    private Long cardNo;
+    private Long factoryId;
+    private Long userNo;
+    private String userNumb;
+    private Long termNo;
+    private String termMac;
+
+    private ConsumptionBo bo;
+    private RemoteUserAccountVo userAccountVo;
+    private RemoteCardVo userCardVo;
+    private XfTermVo useTermVo;
+
+    /**
+     * 创建消费卡片校验上下文。
+     * <p>
+     * 该方法用于根据传入的消费业务对象、终端设备信息、用户卡片信息和餐次类型信息,
+     * 构建一个消费卡片校验上下文对象,供后续校验逻辑使用。
+     *
+     * @param bo 消费业务对象,包含消费相关的基础信息(如消费金额、终端编号等)
+     * @return 返回一个初始化完成的 CardConsumeValidationContext 对象,包含所有必要的校验上下文信息
+     */
+    public static AllowConsumeValidationContext create(ConsumptionBo bo) {
+        AllowConsumeValidationContext context = new AllowConsumeValidationContext();
+        context.bo = bo;
+
+        context.cardNo = bo.getCardNo();
+        context.factoryId = bo.getFactoryId();
+        context.userNo = bo.getUserNo();
+        context.termNo = bo.getTermNo();
+        context.termMac=bo.getTermMac();
+
+        return context;
+    }
+}

+ 383 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidation.java

@@ -0,0 +1,383 @@
+package org.dromara.server.consume.check;
+
+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.domain.R;
+import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.TradeStatusEnum;
+import org.dromara.server.common.constant.ConsumeConstants;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.cache.ValidationParam;
+import org.dromara.server.consume.domain.convert.RemoteVoConvert;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+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.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * 卡片消费校验
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-15
+ * @since JDK17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CardConsumeValidation {
+    private static final BigDecimal PERCENT_DIVISOR = new BigDecimal("100.0");
+    private static final Map<String, String> MEAL_TYPE_NAMES = ConsumeConstants.mealNameMap;
+    private final CommonCheck commonCheck;
+    private final ValidationParam validationParam;
+    private final ThreadPoolTaskExecutor taskExecutor;
+
+    public R<ErrorInfo> cardValidation(ConsumptionBo bo, XfTermVo termVo, RemoteCardVo userCardVo,
+                                       RemoteMealTypeVo mealTypeVo,
+                                       XfCardLimitedVo cardLimitedVo, Map<String, Boolean> mapCardLimited) {
+
+    // public R<ErrorInfo> cardValidation(CardConsumeValidationContext validationContext) {
+        // 1.初始化验证上下文
+        CardConsumeValidationContext validationContext = CardConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo.getTypeId());
+
+        // 2. 执行异步验证链
+        R<ErrorInfo> result = executeCardValidationChain(validationContext);
+        // if (R.isSuccess(result)) {
+        //     dealCardQuota(validationContext);
+        // }
+        // 3.不管验证是否成功,获取验证后的折扣金额及卡验证限制数据
+        bo.setConsumeMoney(validationContext.discountMoney);
+        RemoteVoConvert.INSTANCE.copyXfCardLimitedVo(cardLimitedVo, validationContext.getCardLimitedVo());
+        mapCardLimited.put("hasDiscount", validationContext.getHasDiscount());
+        mapCardLimited.put("hasQuota", validationContext.getHasQuota());
+        mapCardLimited.put("hasLimited", validationContext.getHasLimited());
+
+        return result;
+    }
+
+    private R<ErrorInfo> executeCardValidationChain(CardConsumeValidationContext ctx) {
+        // 创建验证任务列表
+        List<Supplier<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());
+
+        // 提交所有验证任务
+        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(200, TimeUnit.MILLISECONDS)) {
+                return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+            }
+
+            // 返回第一个发现的错误,如果没有错误则返回成功
+            // 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);
+        }
+    }
+
+    // region 卡片折扣处理
+    private R<ErrorInfo> dealCardDiscount(CardConsumeValidationContext ctx) {
+        if (!validationParam.getRATE_CONSUME()) {
+            log.debug("全局折扣功能未启用,跳过折扣验证");
+            return R.ok();
+        }
+
+        List<String> termIds = validationParam.getDisCountTermIdList();
+        String currentTermId = String.valueOf(ctx.getTermId());
+        if (CollectionUtil.isEmpty(termIds) || !termIds.contains(currentTermId)) {
+            log.debug("终端[{}]未配置折扣功能,跳过折扣验证", ctx.getTermNo());
+            return R.ok();
+        }
+
+        List<RemoteDiscountVo> discountVos = validationParam.getDiscountVos();
+        if (CollectionUtil.isEmpty(discountVos)) {
+            log.debug("未配置卡类折扣功能,跳过折扣验证");
+            return R.ok();
+        }
+        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);
+            return R.ok();
+        } else {
+            log.debug("卡片[{}]未配置折扣,跳过折扣验证", ctx.getCardNo());
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 计算折扣金额
+     *
+     * @param cardLimitedVo    卡片限制信息
+     * @param consumeValue     消费记录
+     * @param remoteDiscountVo 折扣信息
+     * @return 折扣金额
+     */
+    @NotNull
+    private BigDecimal getDisCountMoney(XfCardLimitedVo cardLimitedVo, BigDecimal consumeValue, RemoteDiscountVo remoteDiscountVo) {
+        // 1. 确定折扣次数
+        boolean isDayDiscount = validationParam.getIsUse().equals(remoteDiscountVo.getRateType());
+        int rateCount = isDayDiscount ? cardLimitedVo.getDayDiscountCount().intValue() : cardLimitedVo.getMealDiscountCount().intValue();
+
+        // 2. 获取折扣率数组
+        BigDecimal[] discountRates = {
+            remoteDiscountVo.getOneRate(),
+            remoteDiscountVo.getTwoRate(),
+            remoteDiscountVo.getThreeRate(),
+            remoteDiscountVo.getFourRate()
+        };
+
+        // 3. 根据折扣次数选择折扣率
+        int rateIndex = Math.min(rateCount, discountRates.length - 1);
+        BigDecimal selectedRate = discountRates[rateIndex];
+
+        log.info("匹配到的折扣率:[{}]", selectedRate);
+        // 4. 计算折扣金额(统一处理精度)
+        BigDecimal discountFactor = selectedRate.divide(PERCENT_DIVISOR, 2, RoundingMode.HALF_UP);
+        return consumeValue
+                   .multiply(discountFactor)
+                   .setScale(2, RoundingMode.HALF_UP);
+    }
+    // endregion
+
+    //region 卡片限次处理
+    private R<ErrorInfo> dealCardLimited(CardConsumeValidationContext ctx) {
+        if (!validationParam.getXC_CONSUME()) {
+            log.debug("全局限次功能未启用,跳过限次验证");
+            return R.ok();
+        }
+
+        List<String> termIds = validationParam.getLimitedTermIdList();
+        String currentTermId = String.valueOf(ctx.getTermId());
+        if (CollectionUtil.isEmpty(termIds) || !termIds.contains(currentTermId)) {
+            log.debug("终端[{}]未配置限次功能,跳过限次验证", ctx.getTermNo());
+            return R.ok();
+        }
+        List<RemoteLimitedVo> limitedCards = validationParam.getLimitedCardList();
+        if (CollectionUtil.isEmpty(limitedCards)) {
+            log.debug("未配置卡类限次功能,跳过限次验证");
+            return R.ok();
+        }
+        Long cardType = ctx.getCardType();
+        RemoteLimitedVo limitedVo = limitedCards.stream()
+                                        .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;
+        }
+
+        // 餐类限次检查
+        return checkMealLimit(limitedVo, ctx.getLastMeal(), ctx.getCardLimitedVo().getMealCount());
+    }
+
+    /**
+     * 检查每日消费次数限制。
+     *
+     * @param limitedVo     卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费次数或金额)。
+     *                      通过此对象可以获取卡片的每日消费限制规则。
+     * @param currentDayCount 当前日期内的消费次数或金额,表示在当天已经发生的消费总量。
+     *                        这个参数用于与限制规则进行比较,以判断是否超出限制。
+     * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
+     *         如果未超出限制,返回成功结果;
+     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     */
+    private R<ErrorInfo> checkDailyLimit(RemoteLimitedVo limitedVo, Long currentDayCount) {
+        Long dayLimit = limitedVo.getDailyCount();
+        if (dayLimit > 0 && currentDayCount >= dayLimit) {
+            return commonCheck.createError(TradeStatusEnum.DayLimitTimes, "卡类日次数限制");
+        }
+        return null;
+    }
+
+    /**
+     * 检查每餐消费次数限制。
+     *
+     * @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(
+            "1", limitedVo.getOneCount(),
+            "2", limitedVo.getTwoCount(),
+            "3", limitedVo.getThreeCount(),
+            "4", limitedVo.getFourCount()
+        );
+
+        Long mealLimit = mealLimits.get(mealType);
+        if (mealLimit == null) {
+            return null;
+        }
+
+        if (currentMealCount >= mealLimit) {
+            String mealName = MEAL_TYPE_NAMES.getOrDefault(mealType, "未知餐次");
+            return commonCheck.createError(TradeStatusEnum.MealLimitTimes,String.format("卡类%s次数限制", mealName));
+        }
+        return R.ok();
+    }
+    //endregion
+
+    //region 卡片限额处理
+    private R<ErrorInfo> dealCardQuota(CardConsumeValidationContext ctx) {
+        if (!validationParam.getXE_CONSUME()) {
+            log.debug("全局限额功能未启用,跳过限额验证");
+            return R.ok();
+        }
+
+        List<String> termIds = validationParam.getQuotaTermIdList();
+        String currentTermId = String.valueOf(ctx.getTermId());
+        if (CollectionUtil.isEmpty(termIds) || !termIds.contains(currentTermId)) {
+            log.debug("终端[{}]未配置限额功能,跳过限额验证", ctx.getTermNo());
+            return R.ok();
+        }
+        List<RemoteQuotaVo> quotaCards = validationParam.getQuotaCardList();
+        if (CollectionUtil.isEmpty(quotaCards)) {
+            log.debug("未配置卡类限额功能,跳过限额验证");
+            return R.ok();
+        }
+        Long cardType = ctx.getCardType();
+        RemoteQuotaVo quotaVo = quotaCards.stream()
+                                        .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());
+        if (dailyCheck != null) {
+            return dailyCheck;
+        }
+
+        // 餐类限额检查
+        return checkMealQuota(quotaVo, ctx.getLastMeal(),ctx.getCardLimitedVo().getMealMoney(),ctx.getConsumeMoney());
+    }
+
+    /**
+     * 检查每日消费额度限制。
+     * 如果设置的日限额>0 并且当天已消费金额+当餐消费的金额比设置的大,则超额不能消费
+     * @param quotaVo     卡片的限制信息对象,包含有关每日消费限额的数据(如每日最大消费额度或金额)。
+     *                      通过此对象可以获取卡片的每日消费限制规则。
+     * @param dayMoney    当前日期内的消费额度或金额,表示在当天已经发生的消费总量。
+     *                    这个参数用于与限制规则进行比较,以判断是否超出限制。
+     * @param consumeMoney  消费金额
+     *
+     * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
+     *         如果未超出限制,返回成功结果;
+     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     */
+    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, "卡类日限制额度");
+        }
+        return null;
+    }
+
+    /**
+     * 检查每餐消费额度限制。
+     *
+     * @param quotaVo       卡片的限制信息对象,包含有关餐次消费限额的数据(如每餐最大消费额度或金额)。
+     *                        通过此对象可以获取卡片的餐次消费限制规则。
+     * @param mealType        餐类标识符,用于区分不同的餐次类型(例如早餐、午餐、晚餐等)。
+     *                        这个参数决定了要针对哪一类餐次进行消费限制的检查。
+     * @param mealMoney 当   当前餐次内的消费额度或金额,表示在当前餐次已经发生的消费总量。
+     * @param consumeMoney 当 当前餐次的消费金额。表示在当前餐次中再次消费的金额
+     * @return 返回一个泛型为 ErrorInfo 的结果对象 R<ErrorInfo>。
+     *         如果未超出限制,返回成功结果;
+     *         如果超出限制,返回包含错误信息的结果(例如超限的具体原因)。
+     */
+    private R<ErrorInfo> checkMealQuota(RemoteQuotaVo quotaVo, String mealType, BigDecimal mealMoney,BigDecimal consumeMoney) {
+        Map<String, BigDecimal> mealQuotas = Map.of(
+            "1", quotaVo.getOneMoney(),
+            "2", quotaVo.getTwoMoney(),
+            "3", quotaVo.getThreeMoney(),
+            "4", quotaVo.getFourMoney()
+        );
+
+        BigDecimal mealQuota = mealQuotas.get(mealType);
+        if (mealQuota == null) {
+            return null;
+        }
+
+        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));
+            }
+        }
+        return R.ok();
+    }
+    //endregion
+}

+ 227 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CardConsumeValidationContext.java

@@ -0,0 +1,227 @@
+package org.dromara.server.consume.check;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteDiscountVo;
+import org.dromara.backstage.api.domain.vo.RemoteLimitedVo;
+import org.dromara.backstage.api.domain.vo.RemoteQuotaVo;
+import org.dromara.common.core.constant.CacheNames;
+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.domain.vo.XfCardLimitedVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * 卡片可消费验证上下文
+ * <p>
+ * 设置卡片可消费验证的相关参数
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-14
+ * @since JDK17
+ */
+@Data
+@Slf4j
+public class CardConsumeValidationContext {
+    /**
+     * 设备Id
+     */
+    private Long termId;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+    /**
+     * 卡类
+     */
+    private Long cardType;
+
+    /**
+     * 消费日期
+     */
+    private Date consumeDate;
+
+    /**
+     * 消费金额
+     */
+    BigDecimal consumeMoney;
+
+    /**
+     * 折扣金额
+     */
+    BigDecimal discountMoney;
+    /**
+     * 卡流水号
+     */
+    private Long cardNo;
+    /**
+     * 最后餐类
+     */
+    private String lastMeal;
+
+    /**
+     * 当前时间,东八区
+     */
+    private LocalDateTime currentTime;
+    /**
+     * 最后支付时间,东八区
+     */
+    private LocalDateTime lastPayTime;
+
+    /**
+     * 卡片限制信息
+     */
+    private XfCardLimitedVo cardLimitedVo;
+
+    /**
+     * 折扣率
+     */
+    private RemoteDiscountVo discountVo;
+
+    /**
+     * 限次数据
+     */
+    private RemoteLimitedVo limitedVo;
+
+    /**
+     * 限额数据
+     */
+    private RemoteQuotaVo quotaVo;
+
+    /**
+     * 是否限次、限额和折扣
+     */
+    private Boolean hasDiscount = Boolean.FALSE;
+    private Boolean hasQuota = Boolean.FALSE;
+    private Boolean hasLimited = Boolean.FALSE;
+
+    /**
+     * 创建消费卡片校验上下文。
+     * <p>
+     * 该方法用于根据传入的消费业务对象、终端设备信息、用户卡片信息和餐次类型信息,
+     * 构建一个消费卡片校验上下文对象,供后续校验逻辑使用。
+     *
+     * @param bo         消费业务对象,包含消费相关的基础信息(如消费金额、终端编号等)
+     * @param xfTermVo   终端设备信息对象,包含终端的详细配置(如单次限额、日限次等)
+     * @param userCardVo 用户卡片信息对象,包含卡片的详细信息(如最后消费时间、有效期等)
+     * @param mealType   餐次类型信息对象,包含餐次类型的详细信息(如类型ID等)
+     * @return 返回一个初始化完成的 CardConsumeValidationContext 对象,包含所有必要的校验上下文信息
+     */
+    public static CardConsumeValidationContext create(ConsumptionBo bo, XfTermVo xfTermVo,
+                                                      RemoteCardVo userCardVo, String mealType) {
+        CardConsumeValidationContext context = new CardConsumeValidationContext();
+        context.termId = xfTermVo.getTermId();
+        context.termNo = xfTermVo.getTermNo();
+        context.cardType = userCardVo.getCardType();
+        context.cardNo = userCardVo.getCardNo();
+        context.consumeDate = bo.getConsumeDate();
+        context.consumeMoney = bo.getConsumeMoney();
+        context.discountMoney = bo.getConsumeMoney();
+        context.lastMeal = mealType;
+        context.currentTime = LocalDateTime.now();
+        context.lastPayTime = CardDateUtils.toLocalDateTime(userCardVo.getLastPay());
+
+        // 从缓存获取卡片数据
+        context.cardLimitedVo = RedisUtils.getCacheMapValue(CacheNames.T_XF_CARD_LIMITED, String.valueOf(userCardVo.getCardNo()));
+        // 如果缓存中没有则初始化为当天当餐
+        if (ObjectUtil.isEmpty(context.cardLimitedVo)) {
+            context.cardLimitedVo = initXfCardLimited(context.cardNo, context.consumeDate, context.lastMeal);
+        }
+        LocalDateTime lastPayLimitLocalDt = CardDateUtils.toLocalDateTime(context.cardLimitedVo.getLastPay());
+        if (!lastPayLimitLocalDt.toLocalDate().isEqual(context.lastPayTime.toLocalDate())) {
+            // 如果不是同天,初始化为当天
+            initCardDayLimitedData(context.cardLimitedVo, Long.valueOf(context.lastMeal), context.consumeDate);
+        }else {
+            // 不是同一餐,更新到当前餐
+            Long lastPayLimitMealType = context.cardLimitedVo.getLastMeal();
+            if (!Objects.equals(lastPayLimitMealType, Long.valueOf(context.lastMeal))) {
+                // 如果不是同一餐,初始化为当餐
+                initCardMealLimitedData(context.cardLimitedVo, Long.valueOf(context.lastMeal), context.consumeDate);
+            } else {
+                // 如果是同一餐,更新最后一次消费时间
+                context.cardLimitedVo.setLastPay(context.consumeDate);
+            }
+        }
+
+        return context;
+    }
+
+    /**
+     * 初始化卡片限制信息
+     *
+     * @param cardNo      卡流水号
+     * @param mealType    餐类
+     * @param consumeDate 最后消费时间
+     * @return 卡片限制信息
+     */
+    private static XfCardLimitedVo initXfCardLimited(Long cardNo, Date consumeDate, String mealType) {
+        XfCardLimitedVo vo = new XfCardLimitedVo();
+        vo.setCardNo(cardNo);
+        vo.setDayCount(0L);
+        vo.setDayMoney(BigDecimal.ZERO);
+        vo.setMealCount(0L);
+        vo.setMealMoney(BigDecimal.ZERO);
+        vo.setDayDiscountCount(0L);
+        vo.setMealDiscountCount(0L);
+        vo.setLastPay(consumeDate);
+        vo.setLastMeal(Long.valueOf(mealType));
+
+        return vo;
+    }
+
+    /**
+     * 初始化卡片的日消费限制数据。
+     * <p>
+     * 该方法用于根据指定的餐类ID和消费日期,初始化或更新卡片限制信息对象中的日消费限制相关数据。
+     * 它可能会检查在给定日期内是否已经达到了某种餐类的消费上限,并相应地调整卡片限制信息。
+     *
+     * @param cardLimitedVo 卡片限制信息对象,包含有关卡片使用限制的数据。
+     *                      方法执行后,此对象可能被修改以反映最新的日消费限制状态。
+     * @param mealTypeId    餐类ID,用于标识特定的餐类(例如早餐、午餐、晚餐等)。
+     *                      这个参数决定了要针对哪一类餐次进行日消费限制的初始化。
+     * @param consumeDate   消费日期,表示消费发生的具体时间。
+     *                      通过这个参数可以确定需要初始化哪一天的日消费限制数据。
+     */
+    private static void initCardDayLimitedData(XfCardLimitedVo cardLimitedVo, Long mealTypeId, Date consumeDate) {
+        cardLimitedVo.setDayCount(0L);
+        cardLimitedVo.setDayMoney(BigDecimal.ZERO);
+        cardLimitedVo.setMealCount(0L);
+        cardLimitedVo.setMealMoney(BigDecimal.ZERO);
+        cardLimitedVo.setDayDiscountCount(0L);
+        cardLimitedVo.setMealDiscountCount(0L);
+        cardLimitedVo.setLastPay(consumeDate);
+        cardLimitedVo.setLastMeal(mealTypeId);
+    }
+
+    /**
+     * 初始化卡片的餐次消费限制数据。
+     * <p>
+     * 该方法用于根据指定的餐类ID和消费日期,初始化或更新卡片限制信息对象中的餐次消费限制相关数据。
+     * 它可能会检查在给定日期内是否已经达到了某种餐类的消费次数或金额限制,并相应地调整卡片限制信息。
+     *
+     * @param cardLimitedVo 卡片限制信息对象,包含有关卡片使用限制的数据。
+     *                      方法执行后,此对象可能被修改以反映最新的餐次消费限制状态。
+     * @param mealTypeId    餐类ID,用于标识特定的餐类(例如早餐、午餐、晚餐等)。
+     *                      这个参数决定了要针对哪一类餐次进行消费限制的初始化。
+     * @param consumeDate   消费日期,表示消费发生的具体时间。
+     *                      通过这个参数可以确定需要初始化哪一天的餐次消费限制数据。
+     */
+    private static void initCardMealLimitedData(XfCardLimitedVo cardLimitedVo, Long mealTypeId, Date consumeDate) {
+        cardLimitedVo.setMealCount(0L);
+        cardLimitedVo.setMealMoney(BigDecimal.ZERO);
+        cardLimitedVo.setMealCount(0L);
+        cardLimitedVo.setMealDiscountCount(0L);
+        cardLimitedVo.setLastPay(consumeDate);
+        cardLimitedVo.setLastMeal(mealTypeId);
+    }
+}

+ 547 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/CommonCheck.java

@@ -0,0 +1,547 @@
+package org.dromara.server.consume.check;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.CardStatusEnum;
+import org.dromara.common.core.enums.TradeStatusEnum;
+import org.dromara.common.core.enums.UserAccountStatusEnum;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.business.BaseBusiness;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.text.MessageFormat;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * 消费业务通用验证类
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-13
+ * @since JDK17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CommonCheck {
+    // 用户校验模式 0-userId 1- userNo
+    private static final Integer userIdMode = 0;
+    private static final Integer userNoMode = 1;
+    // 卡片校验模式 0-userId 1- cardNo
+    private static final Integer cardIdMode = 0;
+    private static final Integer cardNoMode = 1;
+
+    private final BaseBusiness baseBusiness;
+    private final ThreadPoolTaskExecutor taskExecutor;
+
+    /**
+     * 账户、设备有效性验证
+     * @param bo 消费业务对象
+     * @param ctx 验证上下文
+     * @return 校验结果,包含错误信息或成功标识
+     */
+    public R<ErrorInfo> consumeValidation(ConsumptionBo bo, AllowConsumeValidationContext ctx) {
+        // 2. 执行异步验证链
+        R<ErrorInfo> result = executeTermValidationChain(ctx);
+        if (R.isError(result)) {
+            return result;
+        }
+        return R.ok();
+    }
+
+    /**
+     * 账户、设备有效性验证异步执行验证
+     *
+     * @param ctx 验证上下文
+     * @return 校验结果,包含错误信息或成功标识
+     */
+    private R<ErrorInfo> executeTermValidationChain(AllowConsumeValidationContext ctx) {
+        // 创建验证任务列表
+        List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
+
+        validationTasks.add(() -> checkParam(ctx));
+        validationTasks.add(() -> checkTerm(ctx));
+        validationTasks.add(() -> checkUserAccount(ctx));
+
+        // 用于存储第一个错误结果
+        AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
+
+        // 使用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) {
+                    if (firstError.compareAndSet(null, createError(TradeStatusEnum.SysError))) {
+                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    }
+                } finally {
+                    latch.countDown();
+                }
+            });
+        }
+
+        try {
+            // 等待所有任务完成或超时
+            if (!latch.await(200, TimeUnit.MILLISECONDS)) {
+                return createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+            }
+
+            // 返回第一个发现的错误,如果没有错误则返回成功
+            return firstError.get() != null ? firstError.get() : R.ok();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return createError(TradeStatusEnum.SysError);
+        }
+    }
+
+    // region 消费参数校验
+
+    /**
+     * 消费参数校验
+     * <p>1.必须有设备机号或mac地址,如果是机号则机号>0</p>
+     * <p>2.卡流水号(cardNo)、物理卡号(FactoryId)、人员流水号(userNo)、人员编号(userNumb)四项中必须有一项,前三项必须>0才有效</p>
+     *
+     * @param ctx 消费有效性校验上下文
+     * @return 校验结果,包含错误信息或成功标识
+     */
+    public R<ErrorInfo> checkParam(AllowConsumeValidationContext ctx) {
+        // 1. 校验设备标识参数
+        if (isTerminalInvalid(ctx)) {
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                                       "设备机号不正确", "设备机号必须大于零或MAC地址不能为空!");
+        }
+
+        // 2. 校验用户标识参数
+        if (isUserIdentifierInvalid(ctx)) {
+            return createErrorResponse(1, ApiErrorTypeConstants.PARAM_ERROR,
+                                       "交易人员标识不满足",
+                                       "必须提供 [CardNo | FactoryId | userNo | userNumb]  中至少1项来标识交易用户");
+        }
+
+        // 3. 所有参数校验通过
+        return R.ok();
+    }
+
+    /**
+     * 校验终端设备参数是否有效
+     *
+     * @param ctx 消费业务对象
+     * @return true-无效, false-有效
+     */
+    private boolean isTerminalInvalid(AllowConsumeValidationContext ctx) {
+        // 终端号为空或为0 且 MAC地址为空
+        return (ObjectUtil.isEmpty(ctx.getTermNo()) || ctx.getTermNo() == 0)
+                   && ObjectUtil.isEmpty(ctx.getTermMac());
+    }
+
+    /**
+     * 校验用户标识参数是否有效
+     *
+     * @param ctx 消费有效性校验上下文
+     * @return true-无效, false-有效
+     */
+    private boolean isUserIdentifierInvalid(AllowConsumeValidationContext ctx) {
+        // 所有用户标识字段均为空或无效值
+        return ctx.getCardNo() <= 0L
+                   && ctx.getFactoryId() == 0L
+                   && ctx.getUserNo() <= 0L
+                   && StrUtil.isEmpty(ctx.getUserNumb());
+    }
+    // endregion
+
+    // region 消费设备校验
+
+    /**
+     * 消费设备校验
+     *
+     * @param ctx 消费有效性校验上下文
+     * @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());
+        }
+        // 将消费机数据复制提供后续业务使用
+        ctx.setUseTermVo(termVo);
+
+        return R.ok();
+    }
+    // endregion
+
+    // region 消费账户校验
+
+    /**
+     * 校验账户是否可以消费。
+     *
+     * @param ctx 消费有效性校验上下文,包含消费相关的基础信息(如用户流水号、消费金额等)
+     * @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 (userNo > 0) {
+            return checkUserNo(ctx);
+        }
+        return R.ok();
+    }
+
+    /**
+     * 根据h卡流水号校验账户是否可以消费。
+     *
+     * @param ctx 消费有效性校验上下文,包含消费相关的基础信息(如用户流水号、消费金额等)
+     * @return 如果校验失败,则返回包含错误信息的 R 对象;如果校验成功,则返回表示成功的 R 对象
+     */
+    private R<ErrorInfo> checkCardNo(AllowConsumeValidationContext ctx) {
+        // 检查卡片
+        long cardNo = ctx.getCardNo();
+        R<ErrorInfo> result = checkCard(cardNo, cardNoMode, ctx);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 如果参数中有物理卡号,比较是否和系统中一致
+        Long factoryId = ctx.getFactoryId();
+        Long cardFactoryId = ctx.getUserCardVo().getFactoryId();
+        if (factoryId.compareTo(0L) > 0 && !cardFactoryId.equals(factoryId)) {
+            return createErrorResponse(400, ApiErrorTypeConstants.PARAM_ERROR,
+                                       "卡片不正确",
+                                       MessageFormat.format("物理卡号不一致,不允许交易。传入值[{0}],系统值[{1}]", factoryId,
+                                                            cardFactoryId));
+
+        }
+        // 获取消费账户信息
+        Long userId = ctx.getUserCardVo().getUserId();
+        result = checkUser(userId, userIdMode, ctx);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 更新bo中部分值息
+        setCheckAfterAccountData(ctx);
+
+        return R.ok();
+    }
+
+    /**
+     * 根据用户流水号校验账户是否可以消费。
+     *
+     * @param ctx 消费有效性校验上下文,包含消费相关的基础信息(如用户流水号、消费金额等)
+     * @return 如果校验失败,则返回包含错误信息的 R 对象;如果校验成功,则返回表示成功的 R 对象
+     */
+    private R<ErrorInfo> checkUserNo(AllowConsumeValidationContext ctx) {
+        // 校验账户信息
+        Long userNo = ctx.getUserNo();
+        R<ErrorInfo> result = checkUser(userNo, userNoMode, ctx);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 校验卡片信息
+        Long userId = ctx.getUserAccountVo().getUserId();
+        result = checkCard(userId, cardIdMode, ctx);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 更新bo中部分值息
+        setCheckAfterAccountData(ctx);
+
+        return R.ok();
+    }
+
+    /**
+     * 校验用户账户信息是否符合交易条件。
+     * 该方法会检查账户是否存在、是否被冻结、状态是否正常、是否已开户以及是否过期。
+     * 如果所有检查通过,则将缓存中的账户信息复制到目标对象中。
+     *
+     * @param checkParam 要校验的参数,通常为流水号或账户Id,用于定位账户信息
+     * @param checkMode  校验模式,用于指定获取账户信息的方式或来源 userIdMode-根据useId查询账户 userNoMode-根据用户流水号查询用户
+     * @param ctx        消费有效性校验上下文     *
+     * @return 包含错误信息的响应对象,如果校验失败则返回错误详情;如果校验成功则返回成功响应
+     */
+    private R<ErrorInfo> checkUser(Long checkParam, Integer checkMode, AllowConsumeValidationContext ctx) {
+        // 1. 从缓存获取账户信息
+        RemoteUserAccountVo accountVo = getAccountFromCache(checkParam, checkMode);
+        // 账户不存在,不允许交易
+        if (ObjectUtil.isEmpty(accountVo)) {
+            return createErrorResponse(400, ApiErrorTypeConstants.NOT_FOUND,
+                                       "账户不存在",
+                                       MessageFormat.format("流水号或Id为[{0}]的账户不存在,不允许交易", checkParam));
+        }
+        // 账户被冻结,不允许交易
+        if (ObjectUtil.equals(accountVo.getFreezeStatus(), Constants.SYS_YES)) {
+            return createErrorResponse(400, ApiErrorTypeConstants.BAD_REQUEST,
+                                       "账户被冻结",
+                                       MessageFormat.format("流水号或Id为[{0}]的账户被冻结,不允许交易", checkParam));
+        }
+        // 账户状态不正常,不允许交易
+        if (ObjectUtil.equals(accountVo.getStatus(), Constants.SYS_NO)) {
+            return createErrorResponse(400, ApiErrorTypeConstants.NOT_FOUND,
+                                       "账户状态不正常",
+                                       MessageFormat.format("流水号或Id为[{0}]的账户状态不正常,不允许交易", checkParam));
+        }
+        // 未开户,不允许交易
+        if (!ObjectUtil.equals(accountVo.getAccountStatus(), UserAccountStatusEnum.IS_OPEN.code().toString())) {
+            return createErrorResponse(400, ApiErrorTypeConstants.NOT_FOUND,
+                                       "账户尚未开户",
+                                       MessageFormat.format("流水号或Id为[{0}]的账户尚未开户,不允许交易", checkParam));
+        }
+        // 账户已过有效期,不允许消费
+        if (accountVo.getLifespan().getTime() < System.currentTimeMillis()) {
+            return createErrorResponse(400, ApiErrorTypeConstants.BAD_REQUEST,
+                                       "账户过期",
+                                       MessageFormat.format("流水号或Id为[{0}]的账户已过期,不允许交易", checkParam));
+        }
+        // 全部检查通过,复制账户信息
+        ctx.setUserAccountVo(accountVo);
+
+        return R.ok();
+    }
+
+    /**
+     * 检查卡片的有效性以及状态是否正常。
+     * 该方法会从缓存中获取卡片信息,并根据卡片是否存在及状态是否正常进行校验。
+     * 如果卡片信息无效或状态异常,则返回对应的错误响应;否则,将缓存中的卡片信息复制到用户卡片对象中。
+     *
+     * @param checkParam 卡片的查询参数,可以是流水号、用户Id或物理卡号,用于定位卡片信息
+     * @param checkMode  查询模式,指定查询卡片信息的方式 cardIdMode-根据useId查询卡片 cardNoMode-根据卡流水号查询卡片
+     * @param ctx        消费有效性校验上下文
+     * @return 如果卡片有效且状态正常,返回成功响应;否则返回包含错误信息的响应
+     */
+    private R<ErrorInfo> checkCard(Long checkParam, Integer checkMode, AllowConsumeValidationContext ctx) {
+        RemoteCardVo cardVo = getCardFromCache(checkParam, checkMode);
+        // 卡片不存在,不允许消费
+        if (ObjectUtil.isEmpty(cardVo)) {
+            return createErrorResponse(400, ApiErrorTypeConstants.CARD_NOT_EXISTS,
+                                       "卡片不存在",
+                                       MessageFormat.format("流水号或用户Id或物理卡号为[{0}]的卡片不存在,不允许交易", checkParam));
+        }
+        // 卡片状态不正常,不允许消费
+        if (!String.valueOf(CardStatusEnum.NORMAL.code()).equals(cardVo.getStatus())) {
+            return createErrorResponse(400, ApiErrorTypeConstants.CARD_STATUS_NOT_NORMAL,
+                                       "卡片状态不正确",
+                                       MessageFormat.format("流水号或用户Id或物理卡号为[{0}]的卡片状态不正确,不允许交易", checkParam));
+
+        }
+
+        ctx.setUserCardVo(cardVo);
+
+        return R.ok();
+    }
+
+    /**
+     * 从缓存获取账户信息
+     *
+     * @param checkParam 用户标识
+     * @param checkMode  校验模式
+     * @return 账户信息对象
+     */
+    private RemoteUserAccountVo getAccountFromCache(Long checkParam, Integer checkMode) {
+        String cacheKey = checkParam.toString();
+        return checkMode.equals(userIdMode) ?
+                   RedisUtils.getCacheMapValue(CacheNames.PT_USER_ACCOUNT_ID, cacheKey) :
+                   RedisUtils.getCacheMapValue(CacheNames.PT_USER_ACCOUNT_NO, cacheKey);
+    }
+
+    /**
+     * 从缓存获取账户信息
+     *
+     * @param checkParam 用户标识
+     * @param checkMode  校验模式
+     * @return 账户信息对象
+     */
+    private RemoteCardVo getCardFromCache(Long checkParam, Integer checkMode) {
+        String cacheKey = checkParam.toString();
+        return checkMode.equals(cardIdMode) ?
+                   RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_USER_ID, cacheKey) :
+                   RedisUtils.getCacheMapValue(CacheNames.PT_USER_CARD_NO, cacheKey);
+    }
+    // endregion
+
+    /**
+     * 创建错误响应对象
+     *
+     * @param code   错误码
+     * @param type   错误类型
+     * @param msg    错误消息
+     * @param detail 错误详情
+     * @return 封装好的错误响应
+     */
+    public R<ErrorInfo> createErrorResponse(int code, String type, String msg, String detail) {
+        ErrorInfo errorInfo = new ErrorInfo(code, type, msg, detail);
+        return R.fail(errorInfo);
+    }
+
+    /**
+     * 创建错误响应对象
+     *
+     * @param status 交易状态
+     * @return 封装好的错误响应
+     */
+    public R<ErrorInfo> createError(TradeStatusEnum status) {
+        return createError(status, status.getName());
+    }
+
+    /**
+     * 创建错误响应对象
+     *
+     * @param status  交易状态
+     * @param message 错误消息
+     * @return 封装好的错误响应
+     */
+    public R<ErrorInfo> createError(TradeStatusEnum status, String message) {
+        return R.fail(new ErrorInfo(
+            400,
+            status.toString(),
+            message,
+            status.getName()));
+    }
+
+    /**
+     * 根据消费日期获取对应的餐次类型。
+     * <p>
+     * 该方法用于根据传入的消费日期,查询并返回与该日期匹配的餐次类型信息。
+     * 餐次类型可能包括早餐、午餐、晚餐等,具体由业务逻辑决定。
+     *
+     * @param consumeDate 消费日期,表示需要查询餐次类型的日期
+     * @return 返回 RemoteMealTypeVo 对象,包含餐次类型的详细信息;
+     * 如果未找到对应的餐次类型,则可能返回 null 或默认值(取决于具体实现)
+     */
+    public RemoteMealTypeVo getMealType(Date consumeDate) {
+        List<RemoteMealTypeVo> list = RedisUtils.getCacheList(CacheNames.PT_MEAL_TYPE_LIST);
+        if (CollectionUtil.isEmpty(list)) {
+            return null;
+        }
+        String mealTime = DateUtil.format(consumeDate, "HH:mm:ss");
+        return list.stream().filter(p -> mealTime
+                                             .compareTo(p.getBeginTime()) >= 0 && mealTime.compareTo(p.getEndTime()) < 0)
+                   .findFirst().orElse(null);
+    }
+
+    /**
+     * 更新验证数据。
+     * <p>
+     * 该方法用于根据消费业务对象、用户卡片信息、终端信息、卡片限制映射以及卡片限制信息,
+     * 更新与消费验证相关的数据。它可能会检查或更新卡片的使用限制状态,并确保消费操作符合相关规则。
+     *
+     * @param bo             消费业务对象,包含与当前消费操作相关的业务数据(如消费金额、时间等)。
+     *                       该对象可能被用于验证消费是否符合规则。
+     * @param userCardVo     用户卡片信息对象,包含与用户卡片相关的数据(如余额、有效期等)。
+     *                       该对象可能被用于验证卡片的有效性或更新卡片状态。
+     * @param termVo         终端信息对象,包含与消费终端相关的数据(如终端编号、位置等)。
+     *                       该对象可能被用于记录消费终端的相关信息。
+     * @param mapCardLimited 卡片限制映射,包含卡片的限制条件(如每日限额、每餐限额等)。
+     *                       该映射可能以键值对的形式存储限制条件的状态(例如是否超出限制)。
+     * @param cardLimitedVo  卡片限制信息对象,包含详细的卡片限制规则(如每日最大消费次数、每餐最大消费金额等)。
+     *                       该对象可能被用于检查或更新卡片的限制状态。
+     */
+    public void updateValidationData(ConsumptionBo bo, RemoteCardVo userCardVo, XfTermVo termVo,
+                                     Map<String, Boolean> mapCardLimited,
+                                     XfCardLimitedVo cardLimitedVo) {
+        Long mealType = bo.getMealType();
+        Date consumeDate = bo.getConsumeDate();
+        BigDecimal consumeMoney = bo.getConsumeMoney();
+        String currentDateStr = DateFormatUtils.format(new Date(), "yyyy-MM-dd");
+        String consumeDateStr = DateFormatUtils.format(consumeDate, "yyyy-MM-dd");
+        Long userId = bo.getUserId();
+        BigDecimal balance = bo.getBalance();
+
+        // 将当笔原始消费记录标识放入缓存,1天过期
+        Set<String> originalId = Collections.singleton(bo.getOriginalId());
+        RedisUtils.setCacheSet(CacheNames.XF_ORIGINAL_ID, originalId);
+        RedisUtils.expire(CacheNames.XF_ORIGINAL_ID, Duration.ofHours(6));
+
+        if (ObjectUtil.equals(currentDateStr, consumeDateStr)) {
+            // 重置卡天当日消费数据
+            taskExecutor.submit(() -> baseBusiness.resetCardConsumeInfo(userCardVo, mealType, consumeMoney, consumeDate));
+            // 重置卡片当日限制数据
+            taskExecutor.submit(() -> baseBusiness.restCardLimitedInfo(mapCardLimited, cardLimitedVo, consumeMoney));
+            // 重置人员当日总卡余
+            taskExecutor.submit(() -> baseBusiness.resetUserBalance(userId, balance));
+        }
+    }
+
+    /**
+     * 设置校验后的账户与卡片数据。
+     * <p>
+     * 该方法用于在校验完成后,将相关的账户信息和卡片信息设置到业务对象中,以便后续的业务逻辑使用。
+     *
+     * @param ctx 消费有效性校验上下文,包含消费相关的基础信息
+     */
+    private void setCheckAfterAccountData(AllowConsumeValidationContext ctx) {
+        RemoteUserAccountVo accountVo = ctx.getUserAccountVo();
+        RemoteCardVo cardVo = ctx.getUserCardVo();
+        // 重置部部分卡片信息
+        ctx.getBo().setCardNo(cardVo.getCardNo());
+        ctx.getBo().setFactoryId(cardVo.getFactoryId());
+        ctx.getBo().setCardTypeName(cardVo.getCardTypeName());
+
+        // 重置部分账户信息
+        ctx.getBo().setUserId(accountVo.getUserId());
+        ctx.getBo().setRealName(StrUtil.isEmpty(accountVo.getRealName()) ? "----" : accountVo.getRealName());
+        ctx.getBo().setUserNo(accountVo.getUserNo());
+        ctx.getBo().setUserNumb(accountVo.getUserNumb());
+        ctx.getBo().setTenantId(accountVo.getTenantId());
+        ctx.getBo().setExpireDate(accountVo.getLifespan());
+        ctx.getBo().setDeptName(accountVo.getDeptName());
+
+    }
+}

+ 367 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeRequestCheck.java

@@ -0,0 +1,367 @@
+package org.dromara.server.consume.check;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.ConsumeRecordTypeEnum;
+import org.dromara.common.core.enums.TradeStatusEnum;
+import org.dromara.common.core.utils.RecordIdUtils;
+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.domain.convert.RemoteVoConvert;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.text.MessageFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * 请求消费业务处理类
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-13
+ * @since JDK17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ConsumeRequestCheck {
+    private final ThreadPoolTaskExecutor taskExecutor;
+    private final CommonCheck commonCheck;
+    private final CardConsumeValidation cardConsumevalidation;
+    private final BaseBusiness baseBusiness;
+
+    public R<ErrorInfo> checkConsume(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
+                                     RemoteCardVo userCardVo, XfTermVo useTermVo,
+                                     Map<String, Boolean> mapCardLimited, XfCardLimitedVo cardLimitedVo) {
+        // 如果消费时间为2000年,认为消费机时钟错误,更改消费时间为当前时间
+        Date consumeDate = bo.getConsumeDate();
+        LocalDate localDate = consumeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+        if (localDate.getYear() == 2000) {
+            consumeDate = new Date();
+            bo.setConsumeDate(consumeDate);
+        }
+        // 检查是否重复请求
+        R<ErrorInfo> result = checkRepeatOriginalId(bo);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 获取餐类
+        RemoteMealTypeVo remoteMealTypeVo = commonCheck.getMealType(consumeDate);
+        if (ObjectUtil.isEmpty(remoteMealTypeVo)) {
+            return commonCheck.createErrorResponse(400, ApiErrorTypeConstants.NOT_FOUND, "不在交易时段",
+                                                   MessageFormat.format("非营业时段,不允许交易。消费时间[{0}]",
+                                                                        DateUtil.format(consumeDate, "HH:mm:ss")));
+        }
+        // 设置消费记录的当前餐类
+        bo.setMealType(Long.valueOf(remoteMealTypeVo.getTypeId()));
+        // 如果消费记录标识为消费机消费则需要进行限次、限额和折扣验证
+        XfCardLimitedVo xfCardLimitedVo = new XfCardLimitedVo();
+        int statusFlag = bo.getStatusFlag();
+        if (statusFlag == Integer.parseInt(ConsumeRecordTypeEnum.XFJXF_1.code())
+                || statusFlag == Integer.parseInt(ConsumeRecordTypeEnum.XFJXF_4.code())) {
+
+            // 设备是否可以消费验证
+            result = checkTermLimitedAndOther(bo, useTermVo, userCardVo, remoteMealTypeVo);
+            if (R.isError(result)) {
+                ErrorInfo data = result.getData();
+                String details = Optional.ofNullable(data).map(ErrorInfo::getDetils).orElse("超过消费限制");
+                return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "消费限制判断存在问题", details));
+            }
+            // 卡片是否可以消费验证
+            result = cardConsumevalidation.cardValidation(bo, useTermVo, userCardVo, remoteMealTypeVo, xfCardLimitedVo, mapCardLimited);
+            if (R.isError(result)) {
+                ErrorInfo data = result.getData();
+                String details = Optional.ofNullable(data).map(ErrorInfo::getDetils).orElse("超过消费限制");
+                return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "消费限制判断存在问题", details));
+            }
+        }
+
+        // 余额校验,余额不足不能交易,如果有折扣,则消费金额是以折扣金额为准的,所以余额验证放在最后
+        result = checkWalletBalance(bo);
+        if (R.isError(result)) {
+            return result;
+        }
+        RemoteVoConvert.INSTANCE.copyXfCardLimitedVo(cardLimitedVo, xfCardLimitedVo);
+        return R.ok();
+    }
+
+    public R<ErrorInfo> completeConsumeRequest(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
+                                               RemoteCardVo userCardVo, XfTermVo useTermVo,
+                                               Map<String, Boolean> mapCardLimited,
+                                               XfCardLimitedVo cardLimitedVo) {
+        // 生成原始消费记录
+        XfConsumeDetailOriginalVo originalVo = new XfConsumeDetailOriginalVo();
+        R<ErrorInfo> result = baseBusiness.createOriginalOrder(bo, userAccountVo, originalVo);
+        if (R.isError(result)) {
+            return result;
+        }
+        bo.setRecordId(originalVo.getRecordId());
+        bo.setOriginalId(originalVo.getOriginalId());
+
+        commonCheck.updateValidationData(bo, userCardVo, useTermVo, mapCardLimited, cardLimitedVo);
+
+        return R.ok();
+    }
+
+    // region 设备可消费验证
+
+    /**
+     * 校验用户钱包余额是否足够支持当前消费。
+     * <p>
+     * 该方法用于检查消费业务对象中的消费金额是否小于或等于用户钱包的可用余额。
+     * 如果余额不足,则返回错误信息;否则返回成功结果。
+     *
+     * @param bo 消费业务对象,包含消费相关的基础信息(如消费金额、用户信息等)
+     * @return 如果校验失败,则返回包含错误信息的 R 对象;如果校验成功,则返回表示成功的 R 对象
+     */
+    private R<ErrorInfo> checkWalletBalance(ConsumptionBo bo) {
+        String userIdStr = bo.getUserId().toString();
+        BigDecimal consumeMoney = bo.getConsumeMoney();
+        BigDecimal totalBalance = RedisUtils.getCacheMapValue(CacheNames.USER_TOTAL_BALANCE, userIdStr);
+        if (ObjectUtil.isEmpty(totalBalance)) {
+            totalBalance = BigDecimal.ZERO;
+        }
+        if (consumeMoney.compareTo(totalBalance) > 0) {
+            return commonCheck.createErrorResponse(400, ApiErrorTypeConstants.CONSUME_CHECK_FAIL, "账户余额不足",
+                                                   MessageFormat.format("总余额[{0}],消费金额[{1}]", totalBalance, consumeMoney));
+        }
+        // 计算扣费后的余额
+        BigDecimal balance = totalBalance.subtract(consumeMoney);
+        bo.setBalance(balance);
+
+        // 更新余额缓存
+        RedisUtils.setCacheMapValue(CacheNames.USER_TOTAL_BALANCE, userIdStr, balance);
+
+        return R.ok();
+    }
+
+    /**
+     * 检查原始消费记录ID是否重复。防止重复请求
+     * <br>
+     * 该方法首先从缓存中获取所有已存在的原始消费记录ID集合,然后根据传入的业务对象生成或获取其原始ID。
+     * 如果生成的原始ID已存在于缓存集合中,则返回错误响应,表示原始消费记录已存在;
+     * 否则,将生成的原始ID设置到业务对象中并返回成功响应。
+     *
+     * @param bo 消费业务对象,包含消费相关数据,如消费日期、终端编号、终端记录ID、用户编号等信息
+     * @return 如果原始消费记录ID重复,返回包含错误信息的响应对象;否则返回成功的响应对象
+     */
+    private R<ErrorInfo> checkRepeatOriginalId(ConsumptionBo bo) {
+        Set<String> originalIdSet = RedisUtils.getCacheSet(CacheNames.XF_ORIGINAL_ID);
+
+        String originalId = bo.getOriginalId();
+        if (ObjectUtil.isEmpty(originalId)) {
+            originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
+                                                   bo.getTermRecordId().shortValue(), bo.getUserNo().shortValue(), 0);
+        }
+        if (originalIdSet.contains(originalId)) {
+            return commonCheck.createErrorResponse(400, ApiErrorTypeConstants.CONSUME_CHECK_FAIL, "原始消费记录存在",
+                                                   MessageFormat.format("原始消费记录已存在:{0}", originalId));
+        }
+        bo.setOriginalId(originalId);
+
+        return R.ok();
+    }
+
+    private R<ErrorInfo> checkTermLimitedAndOther(ConsumptionBo bo, XfTermVo termVo, RemoteCardVo userCardVo, RemoteMealTypeVo mealTypeVo) {
+        // 1. 前置检查与数据初始化
+        if (isSpecialScenario(termVo, mealTypeVo)) return R.ok();
+        initializeCardData(userCardVo, mealTypeVo);
+
+        // 2. 准备验证上下文
+        TermConsumeValidationContext validationContext = TermConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo);
+
+        // 3. 执行异步验证链
+        return executeTermValidationChain(validationContext);
+    }
+
+    /**
+     * 特殊场景检查
+     *
+     * @param termVo 设备信息
+     * @param mealVo 餐类信息
+     * @return 检查结果
+     */
+    private boolean isSpecialScenario(XfTermVo termVo, RemoteMealTypeVo mealVo) {
+        return termVo.getTermNo() == 0 && "0".equals(mealVo.getTypeId());
+    }
+
+    /**
+     * 初始化卡片消费数据。
+     * <p>
+     * 该方法用于初始化用户卡片的相消费关数据,包括餐次消费信息和每日消费信息。
+     * 具体操作包括:
+     * 1. 根据卡片编号和餐次类型ID初始化餐次消费数据。
+     * 2. 根据卡片编号初始化每日消费数据。
+     *
+     * @param cardVo 用户卡片信息对象,包含卡片的详细信息(如卡片编号、最后消费时间等)
+     * @param mealVo 餐次类型信息对象,包含餐次类型的详细信息(如类型ID等)
+     */
+    private void initializeCardData(RemoteCardVo cardVo, RemoteMealTypeVo mealVo) {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime lastPay = CardDateUtils.toLocalDateTime(cardVo.getLastPay());
+
+        // 餐类数据初始化
+        boolean sameMeal = mealVo.getTypeId().equals(cardVo.getLastMeal().toString());
+        boolean sameDay = now.toLocalDate().equals(lastPay.toLocalDate());
+
+        if (!sameMeal || !sameDay) {
+            // cardService.initCardMealData(cardVo.getCardNo(), mealVo.getTypeId());
+            cardVo.setMealCount(0L);
+            cardVo.setMealTotal(BigDecimal.ZERO);
+        }
+
+        // 日消费数据初始化
+        if (!sameDay) {
+            // cardService.initCardDayData(cardVo.getCardNo());
+            cardVo.setDayCount(0L);
+            cardVo.setDayTotal(BigDecimal.ZERO);
+        }
+    }
+
+    /**
+     * 执行消费终端校验链。
+     * <p>
+     * 该方法用于执行一系列与消费终端相关的校验逻辑,确保消费请求符合所有业务规则。
+     * 校验链包括但不限于以下内容:
+     * 1. 消费间隔校验。
+     * 2. 单次消费限额校验。
+     * 3. 卡片有效期校验。
+     * 4. 卡类限制校验。
+     * 5. 餐次限次校验。
+     * 6. 日限次和日限额校验。
+     * <p>
+     * 如果任意一个校验失败,则立即返回错误信息并停止后续校验。
+     *
+     * @param context 校验上下文对象,包含消费相关的所有必要信息(如消费金额、终端设备信息、用户卡片信息等)
+     * @return 如果校验成功,返回表示成功的 R 对象;如果校验失败,返回包含错误信息的 R 对象
+     */
+    private R<ErrorInfo> executeTermValidationChain(TermConsumeValidationContext context) {
+        // 创建验证任务列表
+        List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
+
+        validationTasks.add(() -> validateSwipeInterval(context));
+        validationTasks.add(() -> validateSingleLimit(context));
+        validationTasks.add(() -> validateCardValidity(context));
+        validationTasks.add(() -> validateCardType(context));
+        validationTasks.add(() -> validateMealLimit(context));
+        validationTasks.add(() -> validateDailyLimit(context));
+
+        // 用于存储第一个错误结果
+        AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
+
+        // 使用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) {
+                    if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
+                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    }
+                } finally {
+                    latch.countDown();
+                }
+            });
+        }
+
+        try {
+            // 等待所有任务完成或超时
+            if (!latch.await(200, TimeUnit.MILLISECONDS)) {
+                return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+            }
+
+            // 返回第一个发现的错误,如果没有错误则返回成功
+            return firstError.get() != null ? firstError.get() : R.ok();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return commonCheck.createError(TradeStatusEnum.SysError);
+        }
+    }
+
+    private R<ErrorInfo> validateSwipeInterval(TermConsumeValidationContext ctx) {
+        if (ctx.getTermSwipeInterval() <= 0) return null;
+
+        long lastPaySec = ctx.getLastPayTime().atZone(ZoneId.systemDefault()).toEpochSecond();
+        long currentSec = ctx.getCurrentTime().atZone(ZoneId.systemDefault()).toEpochSecond();
+        long intervalMin = (currentSec - lastPaySec) / 60;
+
+        return intervalMin < ctx.getTermSwipeInterval() ?
+                   commonCheck.createError(TradeStatusEnum.TimeInterval) : null;
+    }
+
+    private R<ErrorInfo> validateSingleLimit(TermConsumeValidationContext ctx) {
+        if (ctx.getTermSingleMoney().compareTo(BigDecimal.ZERO) <= 0) return null;
+        return ctx.getConsumeValue().compareTo(ctx.getTermSingleMoney()) > 0 ?
+                   commonCheck.createError(TradeStatusEnum.OnceBigMoney) : null;
+    }
+
+    private R<ErrorInfo> validateCardValidity(TermConsumeValidationContext ctx) {
+        if (ctx.getFactoryId() == 0 || !ctx.isTermUseValidity()) return null;
+        return ctx.getCurrentTime().isAfter(ctx.getExpiryTime()) ?
+                   commonCheck.createError(TradeStatusEnum.CardValidDate) : null;
+    }
+
+    private R<ErrorInfo> validateCardType(TermConsumeValidationContext ctx) {
+        int mask = 1 << (ctx.getCardType() - 1);
+        return (mask & ctx.getTermCardType()) == 0 ?
+                   commonCheck.createError(TradeStatusEnum.CardTypeLimit) : null;
+    }
+
+    private R<ErrorInfo> validateMealLimit(TermConsumeValidationContext ctx) {
+        return ctx.getTermMealCount() > 0 && ctx.getMealCount() >= ctx.getTermMealCount() ?
+                   commonCheck.createError(TradeStatusEnum.MealLimitTimes) : null;
+    }
+
+    private R<ErrorInfo> validateDailyLimit(TermConsumeValidationContext ctx) {
+        // 日限次验证
+        if (ctx.getTermDayCount() > 0 && ctx.getDayCount() >= ctx.getTermDayCount()) {
+            return commonCheck.createError(TradeStatusEnum.DayLimitTimes);
+        }
+
+        // 日限额验证
+        if (ctx.getTermDayMoney().compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal total = ctx.getDayValue().add(ctx.getConsumeValue());
+            if (total.compareTo(ctx.getTermDayMoney()) > 0) {
+                return commonCheck.createError(TradeStatusEnum.DayLimitMoney);
+            }
+        }
+        return null;
+    }
+
+    // endregion
+
+}

+ 302 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeUploadCheck.java

@@ -0,0 +1,302 @@
+package org.dromara.server.consume.check;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+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.BagNameEnum;
+import org.dromara.common.core.utils.RecordIdUtils;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.business.BaseBusiness;
+import org.dromara.server.consume.domain.convert.RemoteVoConvert;
+import org.dromara.server.consume.domain.vo.PtBagVo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailVo;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.dromara.server.consume.service.IConsumeDetailOriginalService;
+import org.dromara.server.consume.service.IPtBagService;
+import org.dromara.server.consume.service.IXfConsumeDetailService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 消费记录上传业务处理类
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-13
+ * @since JDK17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ConsumeUploadCheck {
+    private final IConsumeDetailOriginalService consumeDetailOriginalService;
+    private final IPtBagService bagService;
+    private final BaseBusiness baseBusiness;
+    private final IXfConsumeDetailService consumeDetailService;
+    private final CommonCheck commonCheck;
+
+    /**
+     * 检查消费交易的账单,
+     *
+     * @param bo            包含交易详情的消费业务对象
+     * @param userAccountVo 与交易关联的用户账户信息
+     * @param useTermVo     处理交易的终端信息
+     * @param bagVoList     扣款钱包信息的列表
+     * @param mealTypeVo    交易餐类信息
+     * @return 返回一个结果对象,包含成功或错误信息。如果发生验证失败,错误信息将包括有关验证失败的详细信息
+     */
+    public R<ErrorInfo> checkBill(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
+                                  XfTermVo useTermVo, List<PtBagVo> bagVoList, RemoteMealTypeVo mealTypeVo) {
+        R<ErrorInfo> result = checkOriginalRecord(bo, userAccountVo);
+        if (R.isError(result)) {
+            return result;
+        }
+
+        Date consumeDate = bo.getConsumeDate();
+        RemoteMealTypeVo mealType = commonCheck.getMealType(consumeDate);
+        if (ObjectUtil.isEmpty(mealType)) {
+            mealType.setTypeId("0");
+            mealType.setMealName("未知");
+        }
+        // 设置消费记录的当前餐类
+        bo.setMealType(Long.valueOf(mealType.getTypeId()));
+        RemoteVoConvert.INSTANCE.copyRemoteMealTypeVo(mealTypeVo, mealType);
+
+        // 获取扣费钱包
+        List<PtBagVo> bagVos = new ArrayList<>();
+        result = checkDeductionBag(bo, userAccountVo, useTermVo, bagVos);
+        if (R.isError(result)) {
+            return result;
+        }
+        bagVoList.addAll(bagVos);
+
+        return R.ok();
+    }
+
+    private R<ErrorInfo> checkOriginalRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo) {
+        String originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
+                                                      bo.getTermRecordId().shortValue(), bo.getUserNo().intValue(), 0);
+        //  补偿性措施:防止消费时间错乱问题(在线模式)
+        int recordType = bo.getRecordStatus().intValue();
+        long uniqueRecordId = bo.getRecordId();
+        bo.setOriginalId(originalId);
+        XfConsumeDetailOriginalVo originalVo;
+        if (!((recordType == 106 || recordType == 108 || recordType == 110 || recordType == 111))) {
+            originalVo = consumeDetailOriginalService.queryById(originalId);
+            if (ObjectUtil.isEmpty(originalVo)) {
+                boolean checkFlag = false;
+                if (uniqueRecordId > 0) {
+                    // 根据自增的记录Id查询
+                    originalVo = consumeDetailOriginalService.queryByRecordId(uniqueRecordId);
+                    if (ObjectUtil.isNotEmpty(originalVo)) {
+                        // 如果根据自增Id找到原始记录,以查找的数据重置消费对象的消费日期和原始记录Id
+                        bo.setConsumeDate(originalVo.getConsumeDate());
+                        bo.setOriginalId(originalVo.getOriginalId());
+                        originalId = originalVo.getOriginalId();
+                        checkFlag = true;
+                    }
+                }
+                if (!checkFlag) {
+                    // 根据卡流水号、机号、机器流水号和消费金额查询
+                    originalVo = consumeDetailOriginalService.queryByConsumeMoney(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(),
+                                                                                  bo.getConsumeMoney());
+                    if (ObjectUtil.isEmpty(originalVo)) {
+                        bo.setRecordStatus(108L);
+                        // 判断脱机消费
+                        R<ErrorInfo> result = doOfflineRecord(bo, originalId, userAccountVo);
+                        if (R.isError(result)) {
+                            String msg = MessageFormat.format("处理记录号为[{0}]的脱机消费数据失败", bo.getRecordId());
+                            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "处理脱机消费数据失败", msg));
+                        }
+
+                    }
+                    bo.setConsumeDate(originalVo.getConsumeDate());
+                    bo.setRecordId(originalVo.getRecordId());
+                    bo.setOriginalId(originalVo.getOriginalId());
+                }
+            }
+            bo.setRecordId(originalVo.getRecordId());
+        } else {
+            R<ErrorInfo> result = doOfflineRecord(bo, originalId, userAccountVo);
+            if (R.isError(result)) {
+                return result;
+            }
+        }
+        XfConsumeDetailVo consumeDetailVo = consumeDetailService.queryVoByOriginalId(originalId);
+        if (ObjectUtil.isNotEmpty(consumeDetailVo)) {
+            // 认为是重复上传,不再入账
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "原始消费记录已处理",
+                                        MessageFormat.format("标识为[{0}]的原始消费记录已处理", bo.getRecordId())));
+        }
+        return R.ok();
+    }
+
+    public R<ErrorInfo> checkDeductionBag(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, XfTermVo termVo, List<PtBagVo> bagVos) {
+        StringBuilder sb = new StringBuilder();
+        // 设备的扣费钱包字符串
+        String consumeType = termVo.getConsumeType();
+        // 分解扣费钱包,设置为可能有空值 ""或" ",要过滤掉
+        List<String> bagCodes = StrUtil.split(consumeType, ",").stream()
+                                        .filter(s -> s != null && !s.trim().isEmpty())
+                                        .toList();
+        Long userId = userAccountVo.getUserId();
+        // 可能会在处理过程中更改实际的消费金额,因此先取出来
+        BigDecimal consumeMoney = bo.getConsumeMoney();
+        // 扣费的过程金额
+        BigDecimal doMoney = bo.getConsumeMoney();
+        // 计算后实际需要扣费的钱包,会小于或等于指定的扣费钱包数
+        List<PtBagVo> doBagVos = new ArrayList<>();
+        List<PtBagVo> preDeductions = new ArrayList<>();
+        BigDecimal totalBalance = BigDecimal.ZERO;
+        // 计算扣费钱包的总金额
+        for (String bagCode : bagCodes) {
+            PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
+            if (ObjectUtil.isNotEmpty(bagVo)) {
+                totalBalance = totalBalance.add(bagVo.getBalance());
+                preDeductions.add(bagVo);
+            }
+        }
+        for (PtBagVo bagVo : preDeductions) {
+            // 1.比较扣费金额
+            String bagCode = bagVo.getBagCode();
+            BigDecimal balance = bagVo.getBalance();
+            if (consumeMoney.compareTo(BigDecimal.ZERO) == 0) {
+                // 如果是消费0元,设置为第一个钱包扣费
+                bagVo.setReceiptMoney(BigDecimal.ZERO);
+                bagVo.setBalance(balance);
+                sb.append(BagNameEnum.getMessage(Integer.valueOf(bagCode)));
+                doBagVos.add(bagVo);
+                log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, consumeMoney, doMoney,
+                         userAccountVo.getRealName());
+                break;
+            } else {
+                // 如果消费金额>0,则可能会需要多钱包扣费
+                if (balance.compareTo(BigDecimal.ZERO) > 0) {
+                    // 钱包有余额则扣费
+                    if (balance.compareTo(doMoney) >= 0) {
+                        // 如果钱包金额>=扣费金额,设置扣费结果并中断循环
+                        bagVo.setReceiptMoney(doMoney);
+                        bagVo.setBalance(balance.subtract(doMoney));
+                        sb.append(BagNameEnum.getMessage(Integer.parseInt(bagCode)));
+                        doBagVos.add(bagVo);
+                        log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, consumeMoney,
+                                 doMoney, userAccountVo.getRealName());
+                        break;
+                    } else {
+                        // 将钱包扣费为0,剩余待扣金额=消费金额-原钱包余额
+                        bagVo.setReceiptMoney(balance);
+                        bagVo.setBalance(BigDecimal.ZERO);
+                        doMoney = doMoney.subtract(balance);
+                        sb.append(BagNameEnum.getMessage(Integer.parseInt(bagCode)));
+                        doBagVos.add(bagVo);
+                        log.warn("[上传交易]-[扣费钱包]-[钱包代码:{},钱包余额:{},消费金额:{},扣款金额:{},姓名:{}]", bagCode, balance, balance,
+                                 doMoney, userAccountVo.getRealName());
+                    }
+                }
+            }
+        }
+
+        bo.setBalance(totalBalance.subtract(consumeMoney));
+        bo.setDigitalSign(sb.toString());
+        bagVos.addAll(doBagVos);
+
+        return R.ok();
+    }
+
+    /**
+     * 处理脱机消费记录.
+     *
+     * @param bo            消费记录对象
+     * @param originalId    原始记录Id
+     * @param userAccountVo 用户账户信息
+     * @return a Result object containing ErrorInfo if there is an error, otherwise null
+     */
+    private R<ErrorInfo> doOfflineRecord(ConsumptionBo bo, String originalId, RemoteUserAccountVo userAccountVo) {
+        try {
+            XfConsumeDetailOriginalVo vo;
+            Date currentDate = DateUtil.date();
+            int recordStatus = bo.getRecordStatus().intValue();
+            Long RecordId = bo.getRecordId();
+            if (RecordId == 0 && (recordStatus == 106 || recordStatus == 108 || recordStatus == 110 || recordStatus == 111)) {
+                if (recordStatus == 110 || recordStatus == 111) {
+                    vo = consumeDetailOriginalService.queryByConsumeMoney(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(),
+                                                                          bo.getOperatorMoney());
+                    if (ObjectUtil.isNotEmpty(vo)) {
+                        resetBoByOfflineResult(bo, vo);
+                    }
+                } else {
+                    vo = consumeDetailOriginalService.queryByConsumeDate(bo.getCardNo(), bo.getTermNo(), bo.getTermRecordId(), bo.getConsumeDate());
+                    if (ObjectUtil.isNotEmpty(vo)) {
+                        resetBoByOfflineResult(bo, vo);
+                    } else {
+                        vo = consumeDetailOriginalService.queryById(originalId);
+                        resetBoByOfflineResult(bo, vo);
+                    }
+                }
+                if (bo.getRecordId() == 0) {
+                    if (bo.getConsumeDate().getTime() > currentDate.getTime()) {
+                        bo.setConsumeDate(currentDate);
+                        originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
+                                                               bo.getTermRecordId().shortValue(), bo.getUserNo().intValue(), 0);
+                        bo.setOriginalId(originalId);
+                    }
+                    vo = new XfConsumeDetailOriginalVo();
+                    R<ErrorInfo> result = baseBusiness.createOriginalOrder(bo, userAccountVo, vo);
+                    if (R.isError(result)) {
+                        return result;
+                    }
+                    bo.setRecordId(vo.getRecordId());
+                    bo.setOriginalId(vo.getOriginalId());
+                }
+            } else {
+                consumeDetailOriginalService.updateRecordStatusByOrginId(recordStatus, originalId);
+            }
+            return R.ok();
+        } catch (Exception e) {
+            log.error("[处理脱机记录错误]-[{}]-[{}]", e.getMessage(), Arrays.toString(e.getStackTrace()));
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "处理脱机记录错误",
+                                        MessageFormat.format("错误消息:{0}", e.getMessage())));
+        }
+    }
+
+    /**
+     * Resets the ConsumptionBo based on the offline result from XfConsumeDetailOriginalVo.
+     * If the provided vo is not null, it updates the bo's recordId and originalId with those from the vo.
+     * Additionally, if the dataFlag in the vo is 0, indicating a specific condition related to online transactions,
+     * it adjusts the recordStatus of the bo by adding 256 (to mark it as an online transaction) and updates this
+     * new status in the database for the given originalId.
+     *
+     * @param bo The ConsumptionBo object to be reset.
+     * @param vo The XfConsumeDetailOriginalVo containing the offline result information.
+     */
+    private void resetBoByOfflineResult(ConsumptionBo bo, XfConsumeDetailOriginalVo vo) {
+        if (ObjectUtil.isNotEmpty(vo)) {
+            bo.setRecordId(vo.getRecordId());
+            bo.setOriginalId(vo.getOriginalId());
+            if (vo.getDataFlag() == 0) {
+                /*
+                  如果库中原来的原始记录的dataFlag为零,则说明是在线交易时收到一次数据,然而应答之后又没有收到上传的记录,设备却写入了
+                  此种记录将采集时得到的DataFlag加上高字节的在线标识256后,得到正确DataFlag,并将之更新到库中
+                 */
+                Long recordStatus = bo.getRecordStatus() + 256;
+                bo.setRecordStatus(recordStatus);
+                consumeDetailOriginalService.updateRecordStatusByOrginId(recordStatus.intValue(), bo.getOriginalId());
+            }
+        }
+    }
+}

+ 76 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/TermConsumeValidationContext.java

@@ -0,0 +1,76 @@
+package org.dromara.server.consume.check;
+
+import lombok.Data;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.common.util.CardDateUtils;
+import org.dromara.server.consume.domain.vo.XfTermVo;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * 设备可消费验证上下文
+ * <p>
+ * 设置设备可消费验证的相关参数
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-14
+ * @since JDK17
+ */
+@Data
+public class TermConsumeValidationContext {
+    // 消费核心数据
+    private BigDecimal consumeValue;
+    private int cardType;
+    private long factoryId;
+    private String lastMeal;
+
+    // 设备限制参数
+    private int termCardType;
+    private int termDayCount;
+    private BigDecimal termDayMoney;
+    private int termMealCount;
+    private BigDecimal termSingleMoney;
+    private int termSwipeInterval;
+    private boolean termUseValidity;
+
+    // 卡片状态数据
+    private int dayCount;
+    private BigDecimal dayValue;
+    private int mealCount;
+    private BigDecimal mealMoney;
+
+    // 时间数据
+    private LocalDateTime currentTime;
+    private LocalDateTime lastPayTime;
+    private LocalDateTime expiryTime;
+
+    public static TermConsumeValidationContext create (ConsumptionBo bo, XfTermVo termVo,
+                                                  RemoteCardVo cardVo, RemoteMealTypeVo mealVo) {
+        TermConsumeValidationContext context = new TermConsumeValidationContext();
+        context.consumeValue = bo.getConsumeMoney();
+        context.cardType = cardVo.getCardType().intValue();
+        context.factoryId = cardVo.getFactoryId();
+        context.lastMeal = mealVo.getTypeId();
+        context.termCardType = termVo.getCardType() == null ? 1 : termVo.getCardType();
+        context.termDayCount = termVo.getDayCount() == null ? 0 : termVo.getDayCount();
+        context.termDayMoney = termVo.getDayMoney() == null ? BigDecimal.ZERO : termVo.getDayMoney();
+        context.termMealCount = termVo.getMealCount() == null ? 0 : termVo.getMealCount();
+        context.termSingleMoney = termVo.getSingleMoney() == null ? BigDecimal.ZERO : termVo.getSingleMoney();
+        context.termSwipeInterval = termVo.getSwipeInterval() == null ? 0 : termVo.getSwipeInterval();
+        context.termUseValidity = Objects.equals(termVo.getTermValidity(), "0") ? Boolean.FALSE : Boolean.TRUE;
+        context.dayCount = cardVo.getDayCount().intValue();
+        context.dayValue = cardVo.getDayTotal();
+        context.mealCount = cardVo.getMealCount().intValue();
+        context.mealMoney = cardVo.getMealTotal();
+        context.currentTime = LocalDateTime.now();
+        context.lastPayTime = CardDateUtils.toLocalDateTime(cardVo.getLastPay());
+        context.expiryTime = CardDateUtils.toLocalDateTime(cardVo.getLastPay());
+
+        return context;
+    }
+}

+ 33 - 20
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/AuthController.java

@@ -2,16 +2,15 @@ package org.dromara.server.consume.controller.v1;
 
 import cn.hutool.core.util.ObjectUtil;
 import lombok.RequiredArgsConstructor;
-import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.domain.R;
-import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.domain.model.ErrorResult;
 import org.dromara.server.consume.business.TermBusiness;
 import org.dromara.server.consume.domain.vo.yc.TermToken;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+@Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping(path = {"/v1/Auth"})
@@ -26,27 +25,41 @@ public class AuthController {
      * @return token
      */
     @GetMapping("/token/term/{termId}")
-    public Object termToken(@PathVariable("termId") Long termId, @RequestHeader(name = "admin", required = false) String admin,
+    public Object getTermToken(@PathVariable("termId") Long termId,
+                            @RequestHeader(name = "admin", required = false) String admin,
                             @RequestHeader(name = "pwd", required = false) String pwd) {
+        final String logPrefix = String.format("[获取设备Token]-[termId:%s]", termId);
+        log.info("{}-[开始]", logPrefix);
+        long startTime = System.currentTimeMillis();
 
-        if (ObjectUtil.isEmpty(admin)) {
-            admin = "administrator";
-        }
-        if (ObjectUtil.isEmpty(pwd)) {
-            pwd = "123456";
-        }
-         R<TermToken> mapResult = termBusiness.getTermToken(termId, admin, pwd);
-        //R<TermToken> mapResult = termBusiness.getTermTokenNew(termId, admin, pwd);
+        try {
+            // 1. 参数校验
+            if (termId == null || termId <= 0) {
+                log.warn("{}-[参数错误: termId无效]", logPrefix);
+                return ErrorResult.badRequestResponse("设备ID不能为空");
+            }
 
-        if (R.isError(mapResult)) {
-            ErrorResult result = new ErrorResult();
-            result.setStatusCode(HttpStatus.BAD_REQUEST.value());
-            result.setMessage("获取Token失败");
-            result.getErrors().add(new ErrorInfo(1, "获取设备Token失败", ApiErrorTypeConstants.BAD_REQUEST, mapResult.getMsg()));
+            // 2. 设置默认凭证
+            String finalAdmin = ObjectUtil.defaultIfEmpty(admin, "administrator");
+            String finalPwd = ObjectUtil.defaultIfEmpty(pwd, "123456");
 
-            return new ResponseEntity<Object>(result, null, HttpStatus.BAD_REQUEST);
-        }
+            // 3. 执行业务逻辑
+            R<TermToken> result = termBusiness.getTermToken(termId, finalAdmin, finalPwd);
 
-        return mapResult.getData();
+            // 4. 处理业务结果
+            if (R.isError(result)) {
+                log.error("{}-[业务失败: {}]", logPrefix, result.getMsg());
+                return ErrorResult.badRequestResponse("获取Token失败: " + result.getMsg());
+            }
+            return ResponseEntity.ok(result.getData());
+
+        } catch (Exception e) {
+            // 5. 捕获所有未处理异常
+            log.error("{}-[系统异常: {}]-[详情: {}]",
+                      logPrefix, e.getClass().getSimpleName(), e.getMessage(), e);
+            return ErrorResult.innternalErrorResponse("服务暂时不可用,请稍后重试");
+        } finally {
+            log.info("{}-[结束,耗时: {}ms]", logPrefix, System.currentTimeMillis() - startTime);
+        }
     }
 }

+ 2 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/ConsumeController.java

@@ -18,6 +18,7 @@ import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
 import org.dromara.server.consume.business.BaseBusiness;
 import org.dromara.server.consume.business.ConsumeBusiness;
+import org.dromara.server.consume.cache.ValidationParam;
 import org.dromara.server.consume.convert.strategy.RecordConvertStrategyContent;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -50,6 +51,7 @@ public class ConsumeController {
     private final BaseBusiness baseBusiness;
     private final DefaultConfig defaultConfig;
     private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
+    private final ValidationParam validationParam;
 
     /**
      * 请求消费(校园码)

+ 151 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/InitDataController.java

@@ -0,0 +1,151 @@
+package org.dromara.server.consume.controller.v1;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.server.consume.business.InitBusiness;
+import org.dromara.server.consume.cache.ValidationParam;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 初始化消费使用的基础数据
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-17
+ * @since JDK17
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/v1/init")
+public class InitDataController {
+    private final InitBusiness initBusiness;
+    private final ValidationParam validationParam;
+
+    /**
+     * 初始化全局消费信息.
+     *
+     * @return 结果
+     */
+    @GetMapping("/global")
+    public R<Void> initGlobalData() {
+        initBusiness.initGlobalData();
+
+        return R.ok();
+    }
+    /**
+     * 初始化折扣、限额和限次的卡类与设备
+     *
+     * @return 结果
+     */
+    @GetMapping("/discount-limited-quota")
+    public R<Void> initDiscountAndOther() {
+        initBusiness.initDiscountAndOther();
+
+        return R.ok();
+    }
+    /**
+     * 初始化用户卡片
+     *
+     * @return 结果
+     */
+    @GetMapping("/card")
+    public R<Void> initUserCard() {
+        initBusiness.initUserCard();
+
+        return R.ok();
+    }
+    /**
+     * 初始化指定用户的卡片.
+     *
+     * @return 结果
+     */
+    @GetMapping("/card/{userId}")
+    public R<Void> initUserCardByUserId(@PathVariable("userId") Long userId) {
+        initBusiness.initUserCardByUserId(userId);
+
+        return R.ok();
+    }
+    /**
+     * 初始化人员余额信息.
+     *
+     * @return 结果
+     */
+    @GetMapping("/total-balance")
+    public R<Void> initUserTotalBalance() {
+        initBusiness.initUserBalance();
+
+        return R.ok();
+    }
+    /**
+     * 初始化人员余额信息.
+     *
+     * @return 结果
+     */
+    @GetMapping("/total-balance/{userId}")
+    public R<Void> initUserTotalBalanceByUserId(@PathVariable("userId") Long userId) {
+        initBusiness.initUserBalanceByUserId(userId);
+
+        return R.ok();
+    }
+    /**
+     * 初始化用户账户信息.包括基本信息、余额信息
+     *
+     * @return 结果
+     */
+    @GetMapping("/user-account")
+    public R<Void> initUserAccount() {
+        initBusiness.initUserAccount();
+
+        return R.ok();
+    }
+    /**
+     * 初始化用户账户信息.包括基本信息、余额信息
+     *
+     * @return 结果
+     */
+    @GetMapping("/user-account/{userId}")
+    public R<Void> initUserAccountById(@PathVariable("userId") Long userId) {
+        initBusiness.initUserAccountById(userId);
+
+        return R.ok();
+    }
+    /**
+     * 初始化卡片限制信息.
+     *
+     * @return 结果
+     */
+    @GetMapping("/card-limited")
+    public R<Void> initXfCardLimited() {
+        initBusiness.initXfCardLimited();
+
+        return R.ok();
+    }
+    /**
+     * 初始化餐类信息.
+     *
+     * @return 结果
+     */
+    @GetMapping("/meal-type")
+    public R<Void> initMealTypeInfo() {
+        initBusiness.initMealTypeInfo();
+
+        return R.ok();
+    }
+    /**
+     * 刷新卡片是否可消费的初始数据.
+     *
+     * @return 结果
+     */
+    @GetMapping("/refresh-card-param")
+    public R<Void> refreshValidationParam() {
+        validationParam.refresh();
+
+        return R.ok();
+    }
+}

+ 52 - 31
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/TermsController.java

@@ -1,14 +1,11 @@
 package org.dromara.server.consume.controller.v1;
 
 import lombok.RequiredArgsConstructor;
-import org.dromara.common.core.constant.ApiErrorTypeConstants;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.domain.R;
-import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.domain.model.ErrorResult;
-import org.dromara.common.redis.utils.CacheUtils;
 import org.dromara.server.consume.business.TermBusiness;
 import org.dromara.server.consume.domain.vo.yc.TermInfo;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -17,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.Map;
 
+@Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping(path = {"/v1/Terms"})
@@ -32,18 +30,34 @@ public class TermsController {
      */
     @GetMapping("/{termNo}")
     public Object getTermByTermNo(@PathVariable("termNo") Long termNo) {
+        final String logPrefix = String.format("[获取设备信息]-[termNo:%s]", termNo);
+        log.info("{}-[开始]", logPrefix);
+        long startTime = System.currentTimeMillis();
 
-        R<TermInfo> mapResult = termBusiness.getTermInfoByTermNo(termNo);
-        if (R.isError(mapResult)) {
-            ErrorResult result = new ErrorResult();
-            result.setStatusCode(HttpStatus.BAD_REQUEST.value());
-            result.setMessage("获取设备信息失败");
-            result.getErrors().add(new ErrorInfo(1, "获取设备信息失败", ApiErrorTypeConstants.BAD_REQUEST, mapResult.getMsg()));
+        try {
+            // 1. 参数校验
+            if (termNo == null || termNo <= 0) {
+                log.warn("{}-[参数错误: termNo无效]", logPrefix);
+                return ErrorResult.badRequestResponse("设备机号不能为空");
+            }
+            // 2. 执行业务逻辑
+            R<TermInfo> result =  termBusiness.getTermInfoByTermNo(termNo);
 
-            return new ResponseEntity<Object>(result, null, HttpStatus.BAD_REQUEST);
-        }
+            // 4. 处理业务结果
+            if (R.isError(result)) {
+                log.error("{}-[业务失败: {}]", logPrefix, result.getMsg());
+                return ErrorResult.badRequestResponse("获取设备信息失败: " + result.getMsg());
+            }
+            return ResponseEntity.ok(result.getData());
 
-        return mapResult.getData();
+        } catch (Exception e) {
+            // 5. 捕获所有未处理异常
+            log.error("{}-[系统异常: {}]-[详情: {}]",
+                      logPrefix, e.getClass().getSimpleName(), e.getMessage(), e);
+            return ErrorResult.innternalErrorResponse("服务暂时不可用,请稍后重试");
+        } finally {
+            log.info("{}-[结束,耗时: {}ms]", logPrefix, System.currentTimeMillis() - startTime);
+        }
     }
 
     /**
@@ -54,26 +68,33 @@ public class TermsController {
      */
     @GetMapping("/CheckTime/{termNo}")
     public Object checkTermTime(@PathVariable("termNo") Integer termNo) {
-        R<Map<String, Object>> mapResult = termBusiness.checkTermTime(termNo);
-        if (R.isError(mapResult)) {
-            ErrorResult result = new ErrorResult();
-            result.setStatusCode(HttpStatus.BAD_REQUEST.value());
-            result.setMessage("设备校时失败");
-            result.getErrors().add(new ErrorInfo(1, "设备校时失败", ApiErrorTypeConstants.BAD_REQUEST, mapResult.getMsg()));
+        final String logPrefix = String.format("[设备校时]-[termNo:%s]", termNo);
+        log.info("{}-[开始]", logPrefix);
+        long startTime = System.currentTimeMillis();
 
-            return new ResponseEntity<Object>(result, null, HttpStatus.BAD_REQUEST);
-        }
+        try {
+            // 1. 参数校验
+            if (termNo == null || termNo <= 0) {
+                log.warn("{}-[参数错误: termNo无效]", logPrefix);
+                return ErrorResult.badRequestResponse("设备机号不能为空");
+            }
+            // 2. 执行业务逻辑
+            R<Map<String, Object>> result = termBusiness.checkTermTime(termNo);
 
-        return mapResult.getData();
-    }
+            // 4. 处理业务结果
+            if (R.isError(result)) {
+                log.error("{}-[业务失败: {}]", logPrefix, result.getMsg());
+                return ErrorResult.badRequestResponse("设备校时失败: " + result.getMsg());
+            }
+            return ResponseEntity.ok(result.getData());
 
-    /**
-     * 缓存测试
-     * @return 测试结果
-     */
-    @GetMapping("/cache/test")
-    public R<Void> cacheTest(){
-        CacheUtils.put("test", "01", "测试数据");
-        return R.ok();
+        } catch (Exception e) {
+            // 5. 捕获所有未处理异常
+            log.error("{}-[系统异常: {}]-[详情: {}]",
+                      logPrefix, e.getClass().getSimpleName(), e.getMessage(), e);
+            return ErrorResult.innternalErrorResponse("服务暂时不可用,请稍后重试");
+        } finally {
+            log.info("{}-[结束,耗时: {}ms]", logPrefix, System.currentTimeMillis() - startTime);
+        }
     }
 }

+ 29 - 6
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v2/AuthController.java

@@ -1,12 +1,14 @@
 package org.dromara.server.consume.controller.v2;
 
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.api.ReturnResult;
 import org.dromara.common.core.domain.R;
 import org.dromara.server.consume.business.TermBusiness;
 import org.dromara.server.consume.domain.vo.yc.TermToken;
 import org.springframework.web.bind.annotation.*;
 
+@Slf4j
 @RestController("AuthControllerV2")
 @RequiredArgsConstructor
 @RequestMapping(path = { "/v2/Auth" })
@@ -23,13 +25,34 @@ public class AuthController {
 	@GetMapping("/token/term/{termId}")
 	public Object getTermToken(@PathVariable("termId") Long termId, @RequestHeader(name = "admin") String admin,
                                @RequestHeader(name = "pwd") String pwd) {
+        final String logPrefix = String.format("[获取设备TokenV2]-[termId:%s]", termId);
+        log.info("{}-[开始]", logPrefix);
+        long startTime = System.currentTimeMillis();
 
-		// R<TermToken> mapResult = termBusiness.getTermToken(termId, admin, pwd);
-		R<TermToken> mapResult = termBusiness.getTermTokenNew(termId, admin, pwd);
+        try {
+            // 1. 基础参数校验
+            if (termId == null || termId <= 0) {
+                log.warn("{} - 参数错误: 无效设备ID", logPrefix);
+                return new ReturnResult(false, 400, "设备ID不能为空", null, startTime);
+            }
+            // 2. 执行业务调用
+            R<TermToken> mapResult = termBusiness.getTermToken(termId, admin, pwd);
 
-		if (R.isError(mapResult)) {
-            return new ReturnResult(false, 1, "获取设备Token失败", null, System.currentTimeMillis());
-		}
-        return new ReturnResult(true, 1, "获取设备Token成功", mapResult.getData(), System.currentTimeMillis());
+            // 3. 处理业务结果
+            if (R.isError(mapResult)) {
+                log.error("{}-[业务失败: {}]", logPrefix, mapResult.getMsg());
+                return new ReturnResult(false, 500, "获取Token失败: " + mapResult.getMsg(), null, startTime);
+            }
+
+            return new ReturnResult(true, 200, "获取成功", mapResult.getData(), startTime);
+
+        } catch (Exception e) {
+            // 4. 异常处理
+            log.error("{}-[系统异常: {}]-[详情: {}]",
+                      logPrefix, e.getClass().getSimpleName(), e.getMessage(), e);
+            return new ReturnResult(false, 503, "服务暂时不可用,请稍后重试", null, startTime);
+        }finally {
+            log.info("{}-[结束,耗时: {}ms]", logPrefix, System.currentTimeMillis() - startTime);
+        }
 	}
 }

+ 10 - 2
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/convert/RemoteVoConvert.java

@@ -1,9 +1,11 @@
 package org.dromara.server.consume.domain.convert;
 
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
-import org.dromara.backstage.api.domain.vo.RemoteXfTermVo;
+import org.dromara.server.consume.domain.bo.XfCardLimitedBo;
 import org.dromara.server.consume.domain.bo.XfConsumeDetailOriginalBo;
+import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
 import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
 import org.dromara.server.consume.domain.vo.XfTermVo;
 import org.mapstruct.Mapper;
@@ -29,9 +31,15 @@ public interface RemoteVoConvert {
 
     void copyRemoteCardVo(@MappingTarget RemoteCardVo target, RemoteCardVo source);
 
-    void copyRemoteTermVo(@MappingTarget XfTermVo target, RemoteXfTermVo source);
+    void copyRemoteTermVo(@MappingTarget XfTermVo target, XfTermVo source);
 
     void copyXfConsumeDetailOriginalVo(@MappingTarget XfConsumeDetailOriginalVo target, XfConsumeDetailOriginalVo source);
 
     void toXfConsumeDetailOriginalVo(@MappingTarget XfConsumeDetailOriginalVo target, XfConsumeDetailOriginalBo source);
+
+    void  voToXfCardLimitedBo(@MappingTarget XfCardLimitedBo target, XfCardLimitedVo source);
+
+    void copyXfCardLimitedVo(@MappingTarget XfCardLimitedVo target, XfCardLimitedVo source);
+
+    void copyRemoteMealTypeVo(@MappingTarget RemoteMealTypeVo target, RemoteMealTypeVo source);
 }

+ 4 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IPtBagService.java

@@ -6,6 +6,7 @@ import org.dromara.server.consume.domain.PtBag;
 import org.dromara.server.consume.domain.bo.PtBagBo;
 import org.dromara.server.consume.domain.vo.PtBagVo;
 
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.List;
 
@@ -106,5 +107,8 @@ public interface IPtBagService {
      */
     PtBagVo queryByUserBagCode(Long userId,String bagCode);
 
+    BigDecimal getUserBalance(Long userId);
 
+    void updateConsumeBalance(List<PtBagVo> bagVos);
+    // boolean updateBalanceByByConsume(PtBagBo bo);
 }

+ 4 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfCardLimitedService.java

@@ -120,4 +120,8 @@ public interface IXfCardLimitedService {
      * @return 更新结果
      */
     Boolean updateDisCountData(Long cardNo);
+
+    void insertOrUpdate(XfCardLimitedBo bo);
+
+    void updateByVo(XfCardLimitedVo vo);
 }

+ 2 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfConsumeDetailService.java

@@ -64,4 +64,6 @@ public interface IXfConsumeDetailService {
     XfConsumeDetailVo queryVoByBo(XfConsumeDetailBo bo);
 
     XfConsumeDetailVo queryVoByOriginalId(String originalId);
+
+    Boolean batchInsertByBo(List<XfConsumeDetailBo> bos);
 }

+ 1 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfTermService.java

@@ -33,6 +33,7 @@ public interface IXfTermService {
      * @return 消费设备视图
      */
     XfTermVo queryVoOneByBo(XfTermBo bo);
+
     /**
      * 根据消费设备编号查询设备信息
      *

+ 88 - 33
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/PtBagServiceImpl.java

@@ -8,7 +8,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.enums.BalanceUpdateEnum;
 import org.dromara.common.core.exception.consume.BagException;
+import org.dromara.common.core.exception.consume.ConsumeException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -22,6 +25,8 @@ import org.dromara.server.consume.domain.vo.PtBagVo;
 import org.dromara.server.consume.mapper.PtBagMapper;
 import org.dromara.server.consume.service.IPtBagService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.util.*;
@@ -34,6 +39,7 @@ import java.util.*;
  */
 @RequiredArgsConstructor
 @Service
+@Slf4j
 public class PtBagServiceImpl implements IPtBagService {
 
     private final PtBagMapper baseMapper;
@@ -48,18 +54,19 @@ public class PtBagServiceImpl implements IPtBagService {
     public PtBagVo queryById(Long bagId) {
         return baseMapper.selectVoById(bagId);
     }
+
     /**
      * 查询账户钱包
      *
-     * @param userId 用户Id
+     * @param userId  用户Id
      * @param bagCode 敖包代码
      * @return 账户钱包
      */
     @Override
     public PtBagVo queryByUserBagCode(Long userId, String bagCode) {
         PtBagVo vo = TenantHelper.ignore(() -> baseMapper.selectVoOne(Wrappers.<PtBag>lambdaQuery()
-            .eq(PtBag::getUserId, userId)
-            .eq(PtBag::getBagCode, bagCode), PtBagVo.class));
+                                                                          .eq(PtBag::getUserId, userId)
+                                                                          .eq(PtBag::getBagCode, bagCode), PtBagVo.class));
         if (ObjectUtil.isNull(vo)) {
             return null;
         }
@@ -146,7 +153,7 @@ public class PtBagServiceImpl implements IPtBagService {
      * 保存前的数据校验
      */
     private void validEntityBeforeSave(PtBag entity) {
-        //TODO 做一些数据校验,如唯一约束
+        // TODO 做一些数据校验,如唯一约束
     }
 
     /**
@@ -159,7 +166,7 @@ public class PtBagServiceImpl implements IPtBagService {
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if (isValid) {
-            //TODO 做一些业务上的校验,判断是否需要校验
+            // TODO 做一些业务上的校验,判断是否需要校验
         }
         return baseMapper.deleteByIds(ids) > 0;
     }
@@ -180,12 +187,12 @@ public class PtBagServiceImpl implements IPtBagService {
             BigDecimal balance1 = BigDecimal.ZERO;
             BigDecimal balance2 = BigDecimal.ZERO;
             Optional<PtBagVo> vo = listVo.stream().filter(p -> "1".equals(p.getBagCode()))
-                .findFirst();
+                                       .findFirst();
             if (vo.isPresent()) {
                 balance1 = vo.get().getBalance();
             }
             vo = listVo.stream().filter(p -> "3".equals(p.getBagCode()))
-                .findFirst();
+                     .findFirst();
             if (vo.isPresent()) {
                 balance2 = vo.get().getBalance();
             }
@@ -208,7 +215,7 @@ public class PtBagServiceImpl implements IPtBagService {
         bo.setUserId(userId);
         List<PtBagVo> listVo = SpringUtils.getAopProxy(this).queryList(bo);
         if (listVo.isEmpty()) {
-            //没有钱包,初始化
+            // 没有钱包,初始化
             for (int i = 1; i < 7; i++) {
                 bo = new PtBagBo();
                 bo.setUserId(userId);
@@ -227,9 +234,9 @@ public class PtBagServiceImpl implements IPtBagService {
      */
     @Override
     public PtBagVo updateBalanceByBo(PtBagBo bo) {
-        //校验入库数据
+        // 校验入库数据
         PtBag entity = verification(bo);
-        //根据不同的操作组装入库数据
+        // 根据不同的操作组装入库数据
         switch (bo.getOperationMode()) {
             case REFUND:
                 refundBag(entity, bo);
@@ -247,13 +254,14 @@ public class PtBagServiceImpl implements IPtBagService {
             default:
                 rechargeBag(entity, bo);
         }
-        //数据入库
+        // 数据入库
         if (baseMapper.updateById(entity) > 0) {
             return queryById(entity.getBagId());
         } else {
             return null;
         }
     }
+
     /**
      * 钱包更新前的验证
      *
@@ -262,24 +270,66 @@ public class PtBagServiceImpl implements IPtBagService {
      */
     @Override
     public PtBag verification(PtBagBo bo) {
-        //获取数据库中对应的钱包
+        // 获取数据库中对应的钱包
         PtBag entity = baseMapper.selectOne(Wrappers.<PtBag>lambdaQuery()
-            .eq(PtBag::getUserId, bo.getUserId())
-            .eq(PtBag::getBagCode,bo.getBagCode()));
+                                                .eq(PtBag::getUserId, bo.getUserId())
+                                                .eq(PtBag::getBagCode, bo.getBagCode()));
 
-        //解密余额密文,得到加密的余额
-        //加密余额默认=明文余额
+        // 解密余额密文,得到加密的余额
+        // 加密余额默认=明文余额
         BigDecimal encryptValue = entity.getBalance();
         if (StrUtil.isNotBlank(entity.getEncryptBalance())) {
             String decryptValue = YcEncryptUtil.decryptBagBalanceByPublicKey(entity.getEncryptBalance(), entity.getUserId().toString());
             encryptValue = new BigDecimal(decryptValue);
         }
-        //如果明文余额与解密后余额不一致则验证不通过
+        // 如果明文余额与解密后余额不一致则验证不通过
         if (encryptValue.compareTo(entity.getBalance()) != 0) {
             throw new BagException("bag.balance.valid", JSONUtil.parse(bo));
         }
         return entity;
     }
+
+    @Override
+    public BigDecimal getUserBalance(Long userId) {
+        LambdaQueryWrapper<PtBag> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(PtBag::getUserId, userId);
+        queryWrapper.in(PtBag::getBagCode, Arrays.asList("1", "3"));
+        queryWrapper.select(PtBag::getBalance);
+
+        List<PtBag> list = baseMapper.selectList(queryWrapper);
+        BigDecimal balance = BigDecimal.ZERO;
+        for (PtBag ptBag : list) {
+            balance = balance.add(ptBag.getBalance());
+        }
+        return balance;
+    }
+
+    @Override
+    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = ConsumeException.class)
+    public void updateConsumeBalance(List<PtBagVo> bagVos) throws ConsumeException {
+        for (PtBagVo bagVo : bagVos) {
+            // 操作金额为0不更新,减少对数据数据的操作
+            if (bagVo.getReceiptMoney().compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+
+            PtBagBo bagBo = new PtBagBo();
+            bagBo.setUserId(bagVo.getUserId());
+            bagBo.setBagId(bagVo.getBagId());
+            bagBo.setBagCode(bagVo.getBagCode());
+            bagBo.setReceiptMoney(bagVo.getReceiptMoney());
+            bagBo.setOperationMode(BalanceUpdateEnum.CONSUME);
+
+            PtBag entity = verification(bagBo);
+            consumeBag(entity, bagBo);
+            int rows = baseMapper.updateById(entity);
+            if (rows == 0) {
+                log.warn("更新钱包失败,未影响任何记录,可能是并发冲突");
+                throw new ConsumeException("更新钱包余额失败");
+            }
+        }
+    }
+
     /**
      * 组装充值钱包数据
      * 1.设置充值后余额=账户原余额+充值金额
@@ -288,7 +338,7 @@ public class PtBagServiceImpl implements IPtBagService {
      * 4.对充值后余额进行加密处理
      *
      * @param bag 账户钱包(数据库)
-     * @param bo 账户钱包(业务)
+     * @param bo  账户钱包(业务)
      */
     private void rechargeBag(PtBag bag, PtBagBo bo) {
         bag.setBalance(bag.getBalance().add(bo.getReceiptMoney()));
@@ -296,43 +346,46 @@ public class PtBagServiceImpl implements IPtBagService {
         bag.setRechargeTotal(bag.getRechargeTotal().add(bo.getReceiptMoney()));
         setEncryptBalance(bag);
     }
+
     /**
      * 组装退款钱包数据
      * 1.如果原账户余额>退款金额,则设置退款后余额=账户原余额-退款金额,否则为0
      * 2.对退款后余额进行加密处理
      *
      * @param bag 账户钱包(数据库)
-     * @param bo 账户钱包(业务)
+     * @param bo  账户钱包(业务)
      */
     private void refundBag(PtBag bag, PtBagBo bo) {
-        //退款后的余额
+        // 退款后的余额
         BigDecimal after = BigDecimal.ZERO;
-        //账户现有余额
-        BigDecimal balance  = bag.getBalance();
-        //退款金额,传入的金额为负数,转换成正数进行余额处理
+        // 账户现有余额
+        BigDecimal balance = bag.getBalance();
+        // 退款金额,传入的金额为负数,转换成正数进行余额处理
         BigDecimal doValue = bo.getReceiptMoney().negate();
-        //如果账户现有余额比退款金额大,则最后余额为账户余额-退款金额,否则为0
-        if(balance.compareTo(doValue)>0){
+        // 如果账户现有余额比退款金额大,则最后余额为账户余额-退款金额,否则为0
+        if (balance.compareTo(doValue) > 0) {
             after = balance.subtract(doValue);
         }
         bag.setBalance(after);
         setEncryptBalance(bag);
     }
+
     /**
      * 组装重置钱包数据
      * 1.设置重置后余额=操作金额
      * 2.对退款后余额进行加密处理
      *
      * @param bag 账户钱包(数据库)
-     * @param bo 账户钱包(业务)
+     * @param bo  账户钱包(业务)
      */
     private void recoverBag(PtBag bag, PtBagBo bo) {
-        if(bag.getBalance().compareTo(bo.getReceiptMoney())>0){
-            //如果钱包余额比设置的金额大,则最后余额=设置的金额
+        if (bag.getBalance().compareTo(bo.getReceiptMoney()) > 0) {
+            // 如果钱包余额比设置的金额大,则最后余额=设置的金额
             bag.setBalance(bo.getReceiptMoney());
         }
         setEncryptBalance(bag);
     }
+
     /**
      * 组装消费钱包数据
      * 1.设置消费后余额=账户原余额-消费金额
@@ -341,7 +394,7 @@ public class PtBagServiceImpl implements IPtBagService {
      * 4.对消费后余额进行加密处理
      *
      * @param bag 账户钱包(数据库)
-     * @param bo 账户钱包(业务)
+     * @param bo  账户钱包(业务)
      */
     private void consumeBag(PtBag bag, PtBagBo bo) {
         bag.setBalance(bag.getBalance().subtract(bo.getReceiptMoney()));
@@ -349,6 +402,7 @@ public class PtBagServiceImpl implements IPtBagService {
         bag.setConsumeTotal(bag.getConsumeTotal().add(bo.getReceiptMoney()));
         setEncryptBalance(bag);
     }
+
     /**
      * 组装错扣补款钱包数据
      * 1.设置补款后余额=账户原余额+补款金额
@@ -357,17 +411,18 @@ public class PtBagServiceImpl implements IPtBagService {
      * 4.对补款后余额进行加密处理
      *
      * @param bag 账户钱包(数据库)
-     * @param bo 账户钱包(业务)
+     * @param bo  账户钱包(业务)
      */
     private void compensateBag(PtBag bag, PtBagBo bo) {
-        //余额=原余额+补款金额
+        // 余额=原余额+补款金额
         bag.setBalance(bag.getBalance().add(bo.getReceiptMoney()));
-        //消费次数+1
+        // 消费次数+1
         bag.setConsumeCount(bag.getConsumeCount() + 1);
-        //因为是多消费了才补款,所以总消费金额=原总消费金额-补款的金额
+        // 因为是多消费了才补款,所以总消费金额=原总消费金额-补款的金额
         bag.setConsumeTotal(bag.getConsumeTotal().subtract(bo.getReceiptMoney()));
         setEncryptBalance(bag);
     }
+
     /**
      * 设置加密卡余
      *

+ 23 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfCardLimitedServiceImpl.java

@@ -18,6 +18,7 @@ import org.dromara.server.consume.domain.bo.XfCardLimitedBo;
 import org.dromara.server.consume.domain.vo.XfCardLimitedVo;
 import org.dromara.server.consume.mapper.XfCardLimitedMapper;
 import org.dromara.server.consume.service.IXfCardLimitedService;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -302,4 +303,26 @@ public class XfCardLimitedServiceImpl implements IXfCardLimitedService {
         }
         return true;
     }
+
+    @Override
+    @Async
+    public void insertOrUpdate(XfCardLimitedBo bo) {
+        XfCardLimited entity = MapstructUtils.convert(bo, XfCardLimited.class);
+        XfCardLimited temp = baseMapper.selectOne(new LambdaQueryWrapper<XfCardLimited>().eq(XfCardLimited::getCardNo, bo.getCardNo()));
+        if(ObjUtil.isNotEmpty(temp)){
+            entity.setLimitedId(temp.getLimitedId());
+            baseMapper.updateById(entity);
+        } else {
+            baseMapper.insert(entity);
+        }
+    }
+
+    @Override
+    public void updateByVo(XfCardLimitedVo vo) {
+        XfCardLimited entity = MapstructUtils.convert(vo, XfCardLimited.class);
+        LambdaUpdateWrapper<XfCardLimited> luw = new LambdaUpdateWrapper<XfCardLimited>()
+                                                     .eq(XfCardLimited::getCardNo, vo.getCardNo());
+        baseMapper.update(entity, luw);
+
+    }
 }

+ 19 - 4
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfConsumeDetailServiceImpl.java

@@ -15,6 +15,7 @@ import org.dromara.server.consume.mapper.XfConsumeDetailMapper;
 import org.dromara.server.consume.service.IXfConsumeDetailService;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -90,7 +91,7 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
         lqw.eq(bo.getSyncStatus() != null, XfConsumeDetail::getSyncStatus, bo.getSyncStatus());
 
         lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
-            XfConsumeDetail::getConsumeDate, params.get("beginTime"), params.get("endTime"));
+                    XfConsumeDetail::getConsumeDate, params.get("beginTime"), params.get("endTime"));
 
         lqw.orderByDesc(XfConsumeDetail::getConsumeDate);
 
@@ -133,7 +134,7 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
      * 保存前的数据校验
      */
     private void validEntityBeforeSave(XfConsumeDetail entity) {
-        //TODO 做一些数据校验,如唯一约束
+        // TODO 做一些数据校验,如唯一约束
     }
 
     /**
@@ -163,13 +164,13 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
             updateWrapper.set(XfConsumeDetail::getCardValue, bo.getCardValue());
         }
         updateWrapper.eq(XfConsumeDetail::getConsumeId, bo.getConsumeId());
-        return baseMapper.update(null, updateWrapper)>0;
+        return baseMapper.update(null, updateWrapper) > 0;
     }
 
     @Override
     public XfConsumeDetailVo queryVoByBo(XfConsumeDetailBo bo) {
         List<XfConsumeDetailVo> list = this.queryList(bo);
-        if(CollectionUtil.isNotEmpty(list)){
+        if (CollectionUtil.isNotEmpty(list)) {
             return list.get(0);
         }
         return null;
@@ -182,4 +183,18 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
 
         return this.queryVoByBo(bo);
     }
+
+    @Override
+    public Boolean batchInsertByBo(List<XfConsumeDetailBo> bos) {
+        // return null;
+        List<XfConsumeDetail> addList = new ArrayList<>();
+        bos.forEach(p -> {
+            XfConsumeDetailVo vo = baseMapper.selectVoById(p.getConsumeId());
+            if (vo == null) {
+                XfConsumeDetail entity = MapstructUtils.convert(p, XfConsumeDetail.class);
+                addList.add(entity);
+            }
+        });
+        return baseMapper.insertBatch(addList);
+    }
 }

+ 25 - 9
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfTermServiceImpl.java

@@ -34,15 +34,13 @@ import java.util.Objects;
 @Service
 public class XfTermServiceImpl implements IXfTermService {
 
+    private final XfTermMapper baseMapper;
+    private final DefaultConfig defaultConfig;
     @DubboReference
     RemotePtRoomService remotePtRoomService;
-
     @DubboReference
     RemotePtAccountService remotePtAccountService;
 
-    private final XfTermMapper baseMapper;
-    private final DefaultConfig defaultConfig;
-
     /**
      * 查询消费设备列表
      *
@@ -64,7 +62,24 @@ public class XfTermServiceImpl implements IXfTermService {
     @Override
     public List<XfTermVo> queryList(XfTermBo bo) {
         LambdaQueryWrapper<XfTerm> lqw = buildQueryWrapper(bo);
-        return baseMapper.selectVoList(lqw);
+        List<XfTermVo> list = baseMapper.selectVoList(lqw);
+
+        List<RemotePtAccountVo> accountVoList = remotePtAccountService.selectAccountList();
+        List<RemotePtRoomVo> roomVoList = remotePtRoomService.selectRoomList();
+        list.forEach(p -> {
+            //设置结算账户名称
+            String accountName = accountVoList.parallelStream()
+                                     .filter(k -> k.getAccountId().equals(p.getAccountId()))
+                                     .findFirst().map(RemotePtAccountVo::getAccountName).orElse("未知结算账户");
+            p.setAccountName(accountName);
+
+            //设置设备所在房间名称
+            String roomName = roomVoList.parallelStream()
+                                     .filter(k -> k.getRoomId().equals(p.getRoomId()))
+                                     .findFirst().map(RemotePtRoomVo::getRoomName).orElse("未知房间");
+            p.setRoomName(roomName);
+        });
+        return list;
     }
 
     /**
@@ -76,14 +91,14 @@ public class XfTermServiceImpl implements IXfTermService {
     @Override
     public XfTermVo queryVoOneByBo(XfTermBo bo) {
         LambdaQueryWrapper<XfTerm> lqw = this.buildQueryWrapper(bo);
-        XfTermVo vo =  baseMapper.selectVoOne(lqw);
-        if(ObjUtil.isNotEmpty(vo)){
+        XfTermVo vo = baseMapper.selectVoOne(lqw);
+        if (ObjUtil.isNotEmpty(vo)) {
             RemotePtAccountVo accountVo = remotePtAccountService.selectVoById(vo.getAccountId());
-            if(ObjUtil.isNotEmpty(accountVo)){
+            if (ObjUtil.isNotEmpty(accountVo)) {
                 vo.setAccountName(accountVo.getAccountName());
             }
             RemotePtRoomVo roomVo = remotePtRoomService.selectVoById(vo.getRoomId());
-            if(ObjUtil.isNotEmpty(roomVo)){
+            if (ObjUtil.isNotEmpty(roomVo)) {
                 vo.setRoomName(roomVo.getRoomName());
             }
             List<XfTermVo> redisList = RedisUtils.getCacheList(CacheNames.PT_TERM_LIST);
@@ -130,6 +145,7 @@ public class XfTermServiceImpl implements IXfTermService {
         lqw.eq(bo.getTermNo() != null, XfTerm::getTermNo, bo.getTermNo());
         lqw.eq(StringUtils.isNotBlank(bo.getTenantId()), XfTerm::getTenantId, bo.getTenantId());
         lqw.eq(StringUtils.isNotBlank(bo.getTermIp()), XfTerm::getTermIp, bo.getTermIp());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermMac()), XfTerm::getTermMac, bo.getTermMac());
 
         lqw.like(StringUtils.isNotBlank(bo.getTermName()), XfTerm::getTermName, bo.getTermName());
 

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

@@ -0,0 +1,42 @@
+package org.dromara.server.consume.task;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.server.consume.business.InitBusiness;
+import org.dromara.server.consume.cache.ValidationParam;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 项目初始化任务
+ * <p>
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-06-15
+ * @since JDK17
+ */
+@Component
+@Slf4j
+public class InitTasks implements ApplicationRunner {
+    // @Autowired
+    @Resource(name = "validationParam")
+    private ValidationParam validationParam;
+
+    @Resource(name = "initBusiness")
+    private InitBusiness initBusiness;
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        log.info("初始化消费验证基础数据");
+        // initBusiness.initGlobalData();
+        // initBusiness.initDiscountAndOther();
+        // initBusiness.initUserCard();
+        // initBusiness.initUserAccount();
+        // initBusiness.initXfCardLimited();
+        // initBusiness.initTermInfo();
+         validationParam.refresh();
+    }
+}

+ 24 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/task/ScheduledTasks.java

@@ -1,6 +1,7 @@
 package org.dromara.server.consume.task;
 
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.json.JSONUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -9,6 +10,8 @@ import org.dromara.common.core.constant.DefaultConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.server.consume.business.ConsumeBusiness;
+import org.dromara.server.consume.business.InitBusiness;
+import org.dromara.server.consume.cache.ValidationParam;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
@@ -27,7 +30,9 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class ScheduledTasks {
     private final ConsumeBusiness consumeBusiness;
+    private final InitBusiness baseBusiness;
     private final DefaultConfig defaultConfig;
+    private final ValidationParam validationParam;
 
     /**
      * 执行原始消费对账任务。
@@ -62,4 +67,23 @@ public class ScheduledTasks {
             }
         }
     }
+
+    @Scheduled(cron = "0 0 4 * * ?")
+    public void initDiscountAndOther() {
+        ThreadUtil.execAsync(baseBusiness::initDiscountAndOther);
+        ThreadUtil.execAsync(baseBusiness::initTermInfo);
+        ThreadUtil.execAsync(baseBusiness::initMealTypeInfo);
+    }
+
+    @Scheduled(cron = "0 30 4 * * ?")
+    public void initValidationParam() {
+        validationParam.refresh();
+    }
+
+    @Scheduled(cron = "0 0 5,9,13,16,20 * * ?")
+    public void initConsumeInfo() {
+        ThreadUtil.execAsync(baseBusiness::initXfCardLimited);
+        ThreadUtil.execAsync(baseBusiness::initUserCard);
+        ThreadUtil.execAsync(baseBusiness::initUserAccount);
+    }
 }

+ 1 - 1
ruoyi-server/ruoyi-server-consume/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,2 +1,2 @@
-org.dromara.server.consume.business.CheckBusiness
+org.dromara.server.consume.check.ConsumeUploadCheck
 org.dromara.server.consume.dubbo.RemoteConsumeServiceImpl