Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

luoyb 11 месяцев назад
Родитель
Сommit
06404f1358
35 измененных файлов с 1314 добавлено и 196 удалено
  1. 12 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java
  2. 4 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java
  3. 23 12
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/lock/LockBusiness.java
  4. 23 7
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfDiscountServiceImpl.java
  5. 6 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfDiscountTermServiceImpl.java
  6. 6 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfLimitedServiceImpl.java
  7. 6 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfLimitedTermServiceImpl.java
  8. 6 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfQuotaServiceImpl.java
  9. 5 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfQuotaTermServiceImpl.java
  10. 11 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/lock/LockController.java
  11. 9 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/task/ScheduledTasks.java
  12. 4 3
      ruoyi-modules/ruoyi-hotel/src/main/java/org/dromara/hotel/service/impl/KfDoorOpenHisServiceImpl.java
  13. 42 24
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/BaseBusiness.java
  14. 134 57
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java
  15. 6 13
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java
  16. 0 6
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/ConsumeController.java
  17. 32 21
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/TermsController.java
  18. 6 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/convert/RemoteVoConvert.java
  19. 2 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IConsumeDetailOriginalService.java
  20. 13 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/ConsumeDetailOriginalServiceImpl.java
  21. 1 1
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfConsumeDetailServiceImpl.java
  22. 12 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/TestController.java
  23. 106 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/XfFailedRecordController.java
  24. 88 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/XfFailedRecord.java
  25. 89 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/bo/XfFailedRecordBo.java
  26. 96 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/vo/XfFailedRecordVo.java
  27. 8 25
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/ConsumptionEventHandler.java
  28. 104 24
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/TransactionRecordEventHandler.java
  29. 100 3
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/timedtask/HandleTask.java
  30. 15 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/mapper/XfFailedRecordMapper.java
  31. 77 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/IXfFailedRecordService.java
  32. 181 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/impl/XfFailedRecordServiceImpl.java
  33. 25 0
      ruoyi-server/ruoyi-server-hik/src/main/resources/mapper/FailedRecord/XfFailedRecordMapper.xml
  34. 41 0
      sql/2025-6-9/kingbase/ddl.sql
  35. 21 0
      sql/2025-6-9/mysql/ddl.sql

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

@@ -152,4 +152,16 @@ public interface CacheNames {
     String PT_TERM_LIST = "pt_term_list";
 
     String PT_TERM_MAC_MAP = "pt_term_mac_map";
+
+    String T_XF_LIMITEDTERM = "t_xf_limitedTerm";
+
+    String T_XF_LIMITED = "t_xf_limited";
+
+    String T_XF_QUOTATERM = "t_xf_quotaTerm";
+
+    String T_XF_QUOTA = "t_xf_quota";
+
+    String T_XF_DISCOUNTTERM = "t_xf_discountTerm";
+
+    String T_XF_DISCOUNT = "t_xf_discount";
 }

+ 4 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/Constants.java

@@ -86,5 +86,9 @@ public interface Constants {
      * 自动创建一卡通账户
      */
     String AUTO_USER_ACCOUNT = "1";
+
+    String SYS_YES = "Y";
+
+    String SYS_NO = "N";
 }
 

+ 23 - 12
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/lock/LockBusiness.java

@@ -1,6 +1,8 @@
 package org.dromara.backstage.business.lock;
 
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUnit;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.RandomUtil;
@@ -169,7 +171,8 @@ public class LockBusiness {
         if (CollectionUtil.isNotEmpty(roomVos)) {
             roomVos.parallelStream().forEach(p -> {
                 String lockId = p.getLockId();
-                openDoorRecord.updateAndGet(v -> v + this.queryOpenDoorRecord(lockId, p.getRoomCode(), p.getRoomName(),pageNo));
+                // 定时任务只查询最近1天的数据
+                openDoorRecord.updateAndGet(v -> v + this.queryOpenDoorRecord(lockId, p.getRoomCode(), p.getRoomName(),pageNo,true));
             });
         }
         return openDoorRecord.get();
@@ -186,7 +189,7 @@ public class LockBusiness {
     public Integer queryOpenDoorRecordByRoomCode(String RoomCode,int pageNo) {
         PtRoomVo vo = roomService.selectHotelRoom(RoomCode);
         if (ObjectUtil.isNotEmpty(vo)) {
-            return queryOpenDoorRecord(vo.getLockId(), vo.getRoomCode(), vo.getRoomName(),pageNo);
+            return queryOpenDoorRecord(vo.getLockId(), vo.getRoomCode(), vo.getRoomName(),pageNo,false);
         } else {
             return 0;
         }
@@ -211,7 +214,7 @@ public class LockBusiness {
         if (ObjectUtil.equals(flag, "0")) {
             int totalPage = JSONUtil.parseObj(result).getInt("totalPage");
             for (int i = 0; i < totalPage; i++) {
-                syncCount += queryOpenDoorRecord(lockId, roomCode, roomName, i + 1);
+                syncCount += queryOpenDoorRecord(lockId, roomCode, roomName, i + 1,  false);
             }
         }
         return syncCount;
@@ -224,9 +227,10 @@ public class LockBusiness {
      * @param roomCode
      * @param roomName
      * @param pageNo   页码
+     * @param isSchedule 是否定时任务
      * @return 记录条数
      */
-    private Integer queryOpenDoorRecord(String lockId, String roomCode, String roomName, int pageNo) {
+    private Integer queryOpenDoorRecord(String lockId, String roomCode, String roomName, int pageNo, boolean isSchedule) {
         Map<String, Object> formMap = new HashMap<>();
         formMap.put("KEYLOCKID", lockId);
         formMap.put("CARDPSWBIT", "1");
@@ -238,16 +242,23 @@ public class LockBusiness {
         String flag = JSONUtil.parseObj(result).getStr("result");
         if (ObjectUtil.equals(flag, "0")) {
             JSONArray pageData = JSONUtil.parseObj(result).getJSONArray("pageData");
-            pageData.parallelStream().forEach(p -> {
-                JSONObject record = JSONUtil.parseObj(p);
+            pageData.parallelStream().map(JSONUtil::parseObj).filter(p -> {
+                if(isSchedule){
+                    // 当前时间与开门时间相差大于25小时,则continue
+                    DateTime operateTime = DateUtil.parse(p.getStr("operateTime"), DefaultConstants.DATE_TIME_FORMAT);
+                    return operateTime != null && DateUtil.between(DateUtil.date(), operateTime, DateUnit.HOUR) <= 25;
+                }
+                return true;
+            }).forEach(p -> {
+//                JSONObject record = JSONUtil.parseObj(p);
                 RemoteDoorOpenBo remoteBo = new RemoteDoorOpenBo();
                 remoteBo.setRoomCode(roomCode);
                 remoteBo.setRoomName(roomName);
-                remoteBo.setOpenDoorMode(record.getStr("openWay"));
-                remoteBo.setOpenTime(DateUtil.parse(record.getStr("operateTime"), DefaultConstants.DATE_TIME_FORMAT));
-                remoteBo.setOpenResult(record.getStr("openResult"));
-                if (ObjectUtil.isNotEmpty(record.get("cardId"))) {
-                    remoteBo.setFactoryId(record.getStr("cardId"));
+                remoteBo.setOpenDoorMode(p.getStr("openWay"));
+                remoteBo.setOpenTime(DateUtil.parse(p.getStr("operateTime"), DefaultConstants.DATE_TIME_FORMAT));
+                remoteBo.setOpenResult(p.getStr("openResult"));
+                if (ObjectUtil.isNotEmpty(p.get("cardId"))) {
+                    remoteBo.setFactoryId(p.getStr("cardId"));
                 } else {
                     remoteBo.setFactoryId("");
                 }
@@ -258,7 +269,7 @@ public class LockBusiness {
                         syncCount.getAndSet(syncCount.get() + 1);
                     }
                 } catch (Exception e) {
-                    log.error("[同步开门记录异常]-[{}]", record, e);
+                    log.error("[同步开门记录异常]-[{}]", p, e);
                 }
             });
         }

+ 23 - 7
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfDiscountServiceImpl.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.consumption.service.impl;
 
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -10,11 +11,14 @@ import org.dromara.backstage.consumption.domain.bo.XfDiscountBo;
 import org.dromara.backstage.consumption.domain.vo.XfDiscountVo;
 import org.dromara.backstage.consumption.mapper.XfDiscountMapper;
 import org.dromara.backstage.consumption.service.IXfDiscountService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -103,6 +107,7 @@ public class XfDiscountServiceImpl implements IXfDiscountService {
      * @return 是否修改成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_DISCOUNT, allEntries = true)
     public Boolean updateByBo(XfDiscountBo bo) {
         XfDiscount update = MapstructUtils.convert(bo, XfDiscount.class);
         validEntityBeforeSave(update);
@@ -130,6 +135,7 @@ public class XfDiscountServiceImpl implements IXfDiscountService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_DISCOUNT, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -144,15 +150,25 @@ public class XfDiscountServiceImpl implements IXfDiscountService {
      * @return 卡类折扣信息
      */
     @Override
+    // @Cacheable(cacheNames = CacheNames.T_XF_DISCOUNT, key = "#cardType-#mealType")
     public XfDiscountVo queryByCardType(Integer cardType, String mealType) {
-        XfDiscountBo bo = new XfDiscountBo();
-        bo.setCardType(Long.valueOf(cardType));
-        bo.setMealType(mealType);
+        String key = String.format("%s-%s", cardType, mealType);
+        XfDiscountVo vo = RedisUtils.getCacheMapValue(CacheNames.T_XF_DISCOUNT, key);
+        if (ObjectUtil.isEmpty(vo)) {
+            XfDiscountBo bo = new XfDiscountBo();
+            bo.setCardType(Long.valueOf(cardType));
+            bo.setMealType(mealType);
 
-        List<XfDiscountVo> list = this.queryList(bo);
-        if (CollectionUtil.isNotEmpty(list)) {
-            return list.get(0);
+            List<XfDiscountVo> list = this.queryList(bo);
+            if (CollectionUtil.isNotEmpty(list)) {
+                vo = list.get(0);
+                RedisUtils.setCacheMapValue(CacheNames.T_XF_DISCOUNT, key, vo);
+                return list.get(0);
+            }
+            return null;
+        } else {
+            return vo;
         }
-        return null;
+
     }
 }

+ 6 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfDiscountTermServiceImpl.java

@@ -11,10 +11,13 @@ import org.dromara.backstage.consumption.domain.vo.XfDiscountTermVo;
 import org.dromara.backstage.consumption.domain.vo.XfTermVo;
 import org.dromara.backstage.consumption.mapper.XfDiscountTermMapper;
 import org.dromara.backstage.consumption.service.IXfDiscountTermService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -142,6 +145,7 @@ public class XfDiscountTermServiceImpl implements IXfDiscountTermService {
      * @return 是否修改成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_DISCOUNTTERM, allEntries = true)
     public Boolean updateByBo(XfDiscountTermBo bo) {
         XfDiscountTerm update = MapstructUtils.convert(bo, XfDiscountTerm.class);
         validEntityBeforeSave(update);
@@ -168,6 +172,7 @@ public class XfDiscountTermServiceImpl implements IXfDiscountTermService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_DISCOUNTTERM, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -182,6 +187,7 @@ public class XfDiscountTermServiceImpl implements IXfDiscountTermService {
      * @return 折扣设备信息
      */
     @Override
+    @Cacheable(cacheNames = CacheNames.T_XF_DISCOUNTTERM, key = "#termId")
     public XfDiscountTermVo queryByTermId(Long termId) {
         XfDiscountTermBo bo = new XfDiscountTermBo();
         bo.setTermId(termId);

+ 6 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfLimitedServiceImpl.java

@@ -10,11 +10,14 @@ import org.dromara.backstage.consumption.domain.bo.XfLimitedBo;
 import org.dromara.backstage.consumption.domain.vo.XfLimitedVo;
 import org.dromara.backstage.consumption.mapper.XfLimitedMapper;
 import org.dromara.backstage.consumption.service.IXfLimitedService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -102,6 +105,7 @@ public class XfLimitedServiceImpl implements IXfLimitedService {
      * @return 是否修改成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_LIMITED, allEntries = true)
     public Boolean updateByBo(XfLimitedBo bo) {
         XfLimited update = MapstructUtils.convert(bo, XfLimited.class);
         validEntityBeforeSave(update);
@@ -128,6 +132,7 @@ public class XfLimitedServiceImpl implements IXfLimitedService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_LIMITED, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -140,6 +145,7 @@ public class XfLimitedServiceImpl implements IXfLimitedService {
      * @return 卡类限次信息
      */
     @Override
+    @Cacheable(cacheNames = CacheNames.T_XF_LIMITED , key = "#cardType")
     public XfLimitedVo queryByCardType(Integer cardType) {
         XfLimitedBo bo = new XfLimitedBo();
         bo.setCardType(cardType.longValue());

+ 6 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfLimitedTermServiceImpl.java

@@ -11,10 +11,13 @@ import org.dromara.backstage.consumption.domain.vo.XfLimitedTermVo;
 import org.dromara.backstage.consumption.domain.vo.XfTermVo;
 import org.dromara.backstage.consumption.mapper.XfLimitedTermMapper;
 import org.dromara.backstage.consumption.service.IXfLimitedTermService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -140,6 +143,7 @@ public class XfLimitedTermServiceImpl implements IXfLimitedTermService {
      * @return 是否修改成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_LIMITEDTERM, allEntries = true)
     public Boolean updateByBo(XfLimitedTermBo bo) {
         XfLimitedTerm update = MapstructUtils.convert(bo, XfLimitedTerm.class);
         validEntityBeforeSave(update);
@@ -166,6 +170,7 @@ public class XfLimitedTermServiceImpl implements IXfLimitedTermService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_LIMITEDTERM, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -181,6 +186,7 @@ public class XfLimitedTermServiceImpl implements IXfLimitedTermService {
      * @return 限次设备信息
      */
     @Override
+    @Cacheable(cacheNames = CacheNames.T_XF_LIMITEDTERM , key = "#termId")
     public XfLimitedTermVo queryByTermId(Long termId) {
         XfLimitedTermBo bo = new XfLimitedTermBo();
         bo.setTermId(termId);

+ 6 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfQuotaServiceImpl.java

@@ -10,11 +10,14 @@ import org.dromara.backstage.consumption.domain.bo.XfQuotaBo;
 import org.dromara.backstage.consumption.domain.vo.XfQuotaVo;
 import org.dromara.backstage.consumption.mapper.XfQuotaMapper;
 import org.dromara.backstage.consumption.service.IXfQuotaService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -102,6 +105,7 @@ public class XfQuotaServiceImpl implements IXfQuotaService {
      * @return 是否修改成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_QUOTA, allEntries = true)
     public Boolean updateByBo(XfQuotaBo bo) {
         XfQuota update = MapstructUtils.convert(bo, XfQuota.class);
         validEntityBeforeSave(update);
@@ -128,6 +132,7 @@ public class XfQuotaServiceImpl implements IXfQuotaService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_QUOTA, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -140,6 +145,7 @@ public class XfQuotaServiceImpl implements IXfQuotaService {
      * @return 卡类限额信息
      */
     @Override
+    @Cacheable(cacheNames = CacheNames.T_XF_QUOTA, key = "#cardType")
     public XfQuotaVo queryByCardType(Integer cardType) {
         XfQuotaBo bo = new XfQuotaBo();
         bo.setCardType(cardType.longValue());

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

@@ -11,10 +11,13 @@ import org.dromara.backstage.consumption.domain.vo.XfQuotatermVo;
 import org.dromara.backstage.consumption.domain.vo.XfTermVo;
 import org.dromara.backstage.consumption.mapper.XfQuotaTermMapper;
 import org.dromara.backstage.consumption.service.IXfQuotaTermService;
+import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -166,6 +169,7 @@ public class XfQuotaTermServiceImpl implements IXfQuotaTermService {
      * @return 是否删除成功
      */
     @Override
+    @CacheEvict(cacheNames = CacheNames.T_XF_QUOTATERM, allEntries = true)
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
         if(isValid){
             //可以删除
@@ -180,6 +184,7 @@ public class XfQuotaTermServiceImpl implements IXfQuotaTermService {
      * @return 限额设备信息
      */
     @Override
+    @Cacheable(cacheNames = CacheNames.T_XF_QUOTATERM, key = "#termId")
     public XfQuotatermVo queryByTermId(Long termId) {
         XfQuotaTermBo bo = new XfQuotaTermBo();
         bo.setTermId(termId);

+ 11 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/lock/LockController.java

@@ -4,8 +4,11 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.business.lock.LockBusiness;
 import org.dromara.backstage.domain.bo.lock.RoomCardBo;
+import org.dromara.backstage.task.ScheduledTasks;
 import org.dromara.common.core.api.ResponseResult;
 import org.dromara.common.core.api.ReturnResult;
+import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.DefaultConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.enums.ResultCodeEnum;
 import org.springframework.web.bind.annotation.*;
@@ -30,6 +33,8 @@ import java.text.MessageFormat;
 public class LockController {
     private final LockBusiness lockBusiness;
 
+    private final ScheduledTasks tasks;
+
     /**
      * 查询门锁电量
      *
@@ -119,4 +124,10 @@ public class LockController {
         log.info(message);
         return ReturnResult.success(message);
     }
+
+    @GetMapping(value = "/open/record/executeTask")
+    public void queryOpenDoorRecordExectueTask() {
+        tasks.syncDoorOpenRecord();
+    }
+
 }

+ 9 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/task/ScheduledTasks.java

@@ -4,6 +4,8 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.business.lock.LockBusiness;
 import org.dromara.backstage.business.self.SelfBusiness;
+import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.DefaultConstants;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
@@ -26,6 +28,8 @@ public class ScheduledTasks {
     private final LockBusiness lockBusiness;
     private final SelfBusiness selfBusiness;
 
+    private final DefaultConfig defaultConfig;
+
     /**
      * 定时刷新短信token
      */
@@ -43,6 +47,11 @@ public class ScheduledTasks {
 
     @Scheduled(cron = "0 0 23 * * *")
     public void syncDoorOpenRecord(){
+        if(DefaultConstants.LOCAL_FLAG.equals(defaultConfig.getLocationFlag())){
+            // 校内不允许此任务运行
+            log.warn("syncDoorOpenRecord: 不允许在本地运行");
+            return;
+        }
         int iCount = lockBusiness.queryOpenDoorRecord(1);
         String message = MessageFormat.format("开门记录同步完成,本次同步了 {0} 条记录", iCount);
         log.info(message);

+ 4 - 3
ruoyi-modules/ruoyi-hotel/src/main/java/org/dromara/hotel/service/impl/KfDoorOpenHisServiceImpl.java

@@ -158,10 +158,11 @@ public class KfDoorOpenHisServiceImpl implements IKfDoorOpenHisService {
     @Override
     public Integer insertOrUpdateByBo(KfDoorOpenHisBo bo) {
         KfDoorOpenHis add = MapstructUtils.convert(bo, KfDoorOpenHis.class);
-        List<KfDoorOpenHisVo> vos = baseMapper.selectVoList(new LambdaQueryWrapper<KfDoorOpenHis>().eq(KfDoorOpenHis::getFactoryId, bo.getFactoryId())
+        // 优化,只查询数据的条数
+        Long count = baseMapper.selectCount(new LambdaQueryWrapper<KfDoorOpenHis>().eq(KfDoorOpenHis::getFactoryId, bo.getFactoryId())
             .eq(KfDoorOpenHis::getRoomCode, bo.getRoomCode()).eq(KfDoorOpenHis::getOpenTime, bo.getOpenTime())
-            .eq(KfDoorOpenHis::getOpenDoorMode, bo.getOpenDoorMode()).eq(KfDoorOpenHis::getOpenResult, bo.getOpenResult()));
-        if (CollectionUtil.isEmpty(vos)) {
+            .eq(KfDoorOpenHis::getOpenDoorMode, bo.getOpenDoorMode()).eq(KfDoorOpenHis::getOpenResult, bo.getOpenResult()).last("limit 1"));
+        if (count == null || count == 0) {
             return baseMapper.insert(add);
         }
         return 0;

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

@@ -20,7 +20,6 @@ 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.enums.ResultCodeEnum;
 import org.dromara.common.core.exception.consume.ConsumeException;
 import org.dromara.common.core.utils.RecordIdUtils;
 import org.dromara.common.core.utils.SpringUtils;
@@ -34,9 +33,11 @@ import org.dromara.server.base.service.yktOperation.SyncRemoteSendMessageRecordS
 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.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.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -92,40 +93,56 @@ public class BaseBusiness {
                                                    accountVo.getUserNo().intValue(), 0);
         }
 
-        XfConsumeDetailOriginalBo originalBo = new XfConsumeDetailOriginalBo();
-        BeanUtil.copyProperties(consumeBo, originalBo);
-        if (ObjectUtil.isNotEmpty(originalBo.getRecordId()) && originalBo.getRecordId() == 0) {
-            originalBo.setRecordId(null);
-        }
-        originalBo.setOriginalId(originalId);
-        originalBo.setDataFlag(consumeBo.getRecordStatus());
-        originalBo.setAnalysisFlag(0L);
-        originalBo.setCardValue(consumeBo.getBalance());
-        originalBo.setConsumeBalance(consumeBo.getBalance());
-        originalBo.setDigitalSign(consumeBo.getDigitalSign());
-        originalBo.setUserId(accountVo.getUserId());
-        originalBo.setDeptId(accountVo.getDeptId());
+        XfConsumeDetailOriginalBo originalBo = getXfConsumeDetailOriginalBo(consumeBo, accountVo, originalId);
+
         XfConsumeDetailOriginalVo vo = originalService.queryById(originalId);
         if (ObjUtil.isNotEmpty(vo)) {
-            // 如果原始消费记录表中已有该笔消费,则检查是否入账
-            XfConsumeDetailVo detailVo = consumeDetailService.queryById(originalId);
-            if (ObjUtil.isNotEmpty(detailVo)) {
-                // 该笔消费记录已入账
-                return R.fail(new ErrorInfo(ResultCodeEnum.DATA_ALREADY_EXISTED.code(), "该笔交易已入账", "该笔消费记录已入账,不能重复入账"));
-            }
-            // 未入账,直接返回入账数据
-            BeanUtil.copyProperties(vo, originalVo);
+            RemoteVoConvert.INSTANCE.copyXfConsumeDetailOriginalVo(originalVo, vo);
             return R.ok();
         }
         // 原始记录表不存在此消费记录,直接插入
         vo = originalService.insertByBo(originalBo);
         if (ObjUtil.isNotEmpty(vo)) {
-            BeanUtil.copyProperties(vo, originalVo);
+            RemoteVoConvert.INSTANCE.copyXfConsumeDetailOriginalVo(originalVo, vo);
             return R.ok();
         }
+
         return R.fail();
     }
 
+    @NotNull
+    private XfConsumeDetailOriginalBo getXfConsumeDetailOriginalBo(ConsumptionBo consumeBo, RemoteUserAccountVo accountVo, String originalId) {
+        XfConsumeDetailOriginalBo originalBo = new XfConsumeDetailOriginalBo();
+        originalBo.setOriginalId(originalId);
+        originalBo.setUserId(accountVo.getUserId());
+        originalBo.setUserNumb(accountVo.getUserNumb());
+        originalBo.setRealName(accountVo.getRealName());
+        originalBo.setDeptId(accountVo.getDeptId());
+        originalBo.setDeptName(accountVo.getDeptName());
+        originalBo.setConsumeDate(consumeBo.getConsumeDate());
+        originalBo.setConsumeMoney(consumeBo.getConsumeMoney());
+        originalBo.setCardNo(consumeBo.getCardNo());
+        originalBo.setFactoryId(consumeBo.getFactoryId());
+        originalBo.setCardValue(consumeBo.getBalance());
+        originalBo.setConsumeBalance(consumeBo.getBalance());
+        originalBo.setTermNo(consumeBo.getTermNo().intValue());
+        // originalBo.setTermName(consumeBo.ter);
+        originalBo.setTermRecordId(consumeBo.getTermRecordId());
+        originalBo.setAnalysisFlag(0L);
+        originalBo.setDataFlag(consumeBo.getRecordStatus());
+        originalBo.setStatusFlag(consumeBo.getStatusFlag());
+        originalBo.setDigitalSign(consumeBo.getDigitalSign());
+        originalBo.setWaterValue(new BigDecimal("0"));
+        originalBo.setWaterHistoryValue(new BigDecimal("0"));
+        originalBo.setWaterDaySum(new BigDecimal("0"));
+        originalBo.setWaterErrValue(new BigDecimal("0"));
+        originalBo.setWaterErrMoney(new BigDecimal("0"));
+        originalBo.setOperatorId(0L);
+        originalBo.setTenantId(defaultConfig.getTenantId());
+
+        return originalBo;
+    }
+
     /**
      * 消费记录入库
      * 1.写入消费明细表
@@ -295,6 +312,7 @@ public class BaseBusiness {
     public Boolean updateConsumeStatusById(Long messageId) {
         return syncRemoteSendMessageRecordService.updateConsumeStatusById(messageId);
     }
+
     /**
      * 创建一条消费明细记录
      *
@@ -309,7 +327,7 @@ public class BaseBusiness {
     private XfConsumeDetailVo createConsumeRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
                                                   RemoteCardVo cardVo, PtBagVo bagVo, XfTermVo termVo,
                                                   RemoteMealTypeVo mealTypeVo, String remark) {
-        String recordId = RecordIdUtils.getRecordId(new Date(), Short.parseShort(bo.getTermNo().toString()),
+        String recordId = RecordIdUtils.getRecordId(bo.getConsumeDate(), Short.parseShort(bo.getTermNo().toString()),
                                                     bo.getTermRecordId().intValue(),
                                                     userAccountVo.getUserNo().intValue(),
                                                     Integer.parseInt(bagVo.getBagCode()));

+ 134 - 57
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java

@@ -13,6 +13,7 @@ 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;
@@ -81,7 +82,6 @@ public class CheckBusiness {
     private final IXfCardLimitedService cardLimitedService;
     private final DefaultConfig defaultConfig;
     private final BaseBusiness baseBusiness;
-    // private final SyncRemoteXfTermService syncRemoteXfTermService;
 
     /**
      * 计算折扣金额
@@ -222,7 +222,6 @@ public class CheckBusiness {
             return R.fail(errorInfo);
         }
         bo.setTermNo(remoteXfTermVo.getTermNo());
-        BeanUtil.copyProperties(remoteXfTermVo, useTermVo);
         RemoteVoConvert.INSTANCE.copyRemoteTermVo(useTermVo, remoteXfTermVo);
         return R.ok();
     }
@@ -239,8 +238,6 @@ public class CheckBusiness {
     public R<ErrorInfo> checkConsume(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo, XfTermVo useTermVo) {
         ErrorInfo errorInfo;
         R<ErrorInfo> result;
-        // 如果折扣验证,消费金额会更新成折扣金额,所以先保存
-        BigDecimal consumeMoney = bo.getConsumeMoney();
         // 1.消费账户状态验证,验证账户是否已开户、是否冻结、状态是否正常、是否过有效期
         if ("Y".equals(userAccountVo.getFreezeStatus())) {
             errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户已被冻结",
@@ -262,31 +259,49 @@ public class CheckBusiness {
                                       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 = checkCardNo(bo, userAccountVo, userCardVo);
+        R<ErrorInfo> result = checkUser(bo, userAccountVo, userCardVo);
         if (R.isError(result)) {
             return result;
         }
@@ -324,6 +339,7 @@ public class CheckBusiness {
      * @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());
@@ -342,46 +358,58 @@ public class CheckBusiness {
      */
     @NotNull
     private R<ErrorInfo> checkCardNo(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo) {
-        RemoteUserAccountVo accountVo;
-        RemoteCardVo cardVo;
+        ErrorInfo errorInfo;
         Long cardNo = bo.getCardNo();
-        if (cardNo > 0L) {
-            ErrorInfo errorInfo;
-            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));
+        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);
             }
-            if (bo.getFactoryId() > 0) {
-                if (!Objects.equals(cardVo.getFactoryId(), bo.getFactoryId())) {
-                    errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不正确", "物理卡号不一致,不允许交易");
+        }
+        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);
-                }
-            }
-            accountVo = remoteUserAccountService.getUserAccountVoById(cardVo.getUserId());
-        } else {
-            accountVo = remoteUserAccountService.getUserAccountVoByUserNo(bo.getUserNo());
-            cardVo = remoteCardService.queryMainCardByUserId(accountVo.getUserId());
+            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());
-        bo.setExpireDate(accountVo.getLifespan());
-        bo.setDeptName(accountVo.getDeptName());
-        // BeanUtil.copyProperties(accountVo, userAccountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo,accountVo);
-        // BeanUtil.copyProperties(cardVo, userCardVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo,cardVo);
+        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
+        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
         return R.ok();
     }
 
@@ -409,15 +437,31 @@ public class CheckBusiness {
             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());
-        bo.setExpireDate(accountVo.getLifespan());
-        bo.setDeptName(accountVo.getDeptName());
-        // BeanUtil.copyProperties(accountVo, userAccountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo,accountVo);
-        // BeanUtil.copyProperties(cardVo, userCardVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo,cardVo);
+        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
+        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
         return R.ok();
     }
 
@@ -440,6 +484,20 @@ public class CheckBusiness {
 
             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, "卡片不存在",
@@ -458,12 +516,8 @@ public class CheckBusiness {
         bo.setCardNo(cardVo.getCardNo());
         bo.setFactoryId(cardVo.getFactoryId());
         bo.setCardTypeName(cardVo.getCardTypeName());
-        bo.setExpireDate(accountVo.getLifespan());
-        bo.setDeptName(accountVo.getDeptName());
-        // BeanUtil.copyProperties(accountVo, userAccountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo,accountVo);
-        // BeanUtil.copyProperties(cardVo, userCardVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo,cardVo);
+        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
+        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
         return R.ok();
     }
 
@@ -487,17 +541,31 @@ public class CheckBusiness {
 
             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));
+                                      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));
+                                      MessageFormat.format("编号为[{0}]人员卡片状态不正确,不允许交易", userNumb));
 
             return R.fail(errorInfo);
         }
@@ -507,10 +575,8 @@ public class CheckBusiness {
         bo.setFactoryId(cardVo.getFactoryId());
         bo.setCardTypeName(cardVo.getCardTypeName());
 
-        // BeanUtil.copyProperties(accountVo, userAccountVo);
-        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo,accountVo);
-        // BeanUtil.copyProperties(cardVo, userCardVo);
-        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo,cardVo);
+        RemoteVoConvert.INSTANCE.copyRemoteUserAccountVo(userAccountVo, accountVo);
+        RemoteVoConvert.INSTANCE.copyRemoteCardVo(userCardVo, cardVo);
 
         return R.ok();
     }
@@ -668,9 +734,9 @@ public class CheckBusiness {
         // 计算扣费钱包的总金额
         for (String bagCode : bagCodes) {
             PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
-            if (ObjectUtil.isNotEmpty(bagVo)) {
+            // if (ObjectUtil.isNotEmpty(bagVo)) {
                 totalBalance = totalBalance.add(bagVo.getBalance());
-            }
+            // }
         }
         // 如果扣费钱包总余额<消费金额,则不允许消费
         if (consumeMoney.compareTo(totalBalance) > 0) {
@@ -724,8 +790,8 @@ public class CheckBusiness {
 
         // 设备消费间隔验证
         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()));
+                 - 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) {// 限制金额
@@ -785,15 +851,18 @@ public class CheckBusiness {
      * @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"));
         // 最后交易餐类
@@ -806,21 +875,29 @@ public class CheckBusiness {
         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();
     }
 

+ 6 - 13
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java

@@ -64,23 +64,13 @@ public class ConsumeBusiness {
      * @return 请求结果
      */
     public R<ErrorInfo> createOrder(ConsumptionBo bo, String mac, String xfPwd) {
-        //检查下客户的公钥与么钥
-        // baseBusiness.getRegisterInfo();
-
+        long startTime;
         log.warn("[请求交易]-[开始参数验证]-[{}]", JSONUtil.toJsonStr(bo));
         R<ErrorInfo> result = checkBusiness.checkParam(bo);
         if (R.isError(result)) {
             log.error("[请求交易]-[参数验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
-
-        //log.info("[请求交易]-[mac校验]-[{}]", JSONUtil.toJsonStr(bo));
-        //result = checkBusiness.checkMac(bo, mac);
-        //if (R.isError(result)) {
-        //    log.error("[请求交易]-[mac校验失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
-        //    return result;
-        //}
-
         log.warn("[请求交易]-[用户信息验证]-[{}]", JSONUtil.toJsonStr(bo));
         RemoteUserAccountVo userAccountVo = new RemoteUserAccountVo();
         RemoteCardVo userCardVo = new RemoteCardVo();
@@ -99,23 +89,26 @@ public class ConsumeBusiness {
         }
 
         log.warn("[请求交易]-[交易流程验证]-[{}]", JSONUtil.toJsonStr(bo));
+        startTime = System.currentTimeMillis();
         result = checkBusiness.checkConsume(bo, userAccountVo, userCardVo, termVo);
         if (R.isError(result)) {
             log.error("[请求交易]-[交易验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
+        log.warn("[请求交易]-[交易流程验证]-[耗时: {} 毫秒]",System.currentTimeMillis()-startTime);
 
         log.warn("[请求交易]-[生成原始消费记录]-[{}]", JSONUtil.toJsonStr(bo));
+        startTime = System.currentTimeMillis();
         XfConsumeDetailOriginalVo originalVo = new XfConsumeDetailOriginalVo();
         result = baseBusiness.createOriginalOrder(bo, userAccountVo, originalVo);
         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());
-        bo.setDeptName(userAccountVo.getDeptName());
+
         return R.ok();
     }
 

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

@@ -179,11 +179,6 @@ public class ConsumeController {
 
         R<ErrorInfo> errorInfo;
         if (Objects.equals(type, "requestConsume")) {
-            // int recordType = bo.getRecordStatus().intValue();
-            //if (ObjectUtil.equals(bo.getCreditType(), CreditTypeEnum.TERM_CONSUME.code()) && recordType == 364) {
-            //    // 如果是消费机请求消费,而且是正常消费记录将消费时间设置为当前时间,以防消费时时钟不对造成实际消费时间不正确
-            //    bo.setConsumeDate(DateUtil.date());
-            //}
             errorInfo = consumeBusiness.createOrder(bo, mac, xfPwd);
         } else if (Objects.equals(type, "uploadRecord")) {
             threadPoolTaskExecutor.submit(() -> consumeBusiness.postOrderAsync(bo, mac, xfPwd));
@@ -208,7 +203,6 @@ public class ConsumeController {
             // 发送消息
             if (ObjectUtil.equals(defaultConfig.getLocationFlag(), DefaultConstants.LOCAL_FLAG)) {
                 ConsumptionBo bo = recordConvertStrategy.convert(record, "YC");
-                // bo.setConsumeDate(jsonObject.getDate("consumeDate"));
                 ThreadUtil.execAsync(() -> baseBusiness.sendCloudConsume(bo));
             }
         }

+ 32 - 21
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/TermsController.java

@@ -5,6 +5,7 @@ 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.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;
@@ -18,41 +19,42 @@ import java.util.Map;
 
 @RestController
 @RequiredArgsConstructor
-@RequestMapping(path = { "/v1/Terms" })
+@RequestMapping(path = {"/v1/Terms"})
 public class TermsController {
 
-	private final TermBusiness termBusiness;
+    private final TermBusiness termBusiness;
 
-	/**
-	 * 获取设备信息(根据机号)
-	 *
-	 * @param termNo 设备的机号(兼容UP)
-	 * @return token
-	 */
-	@GetMapping("/{termNo}")
-	public Object getTermByTermNo(@PathVariable("termNo") Long termNo) {
+    /**
+     * 获取设备信息(根据机号)
+     *
+     * @param termNo 设备的机号(兼容UP)
+     * @return token
+     */
+    @GetMapping("/{termNo}")
+    public Object getTermByTermNo(@PathVariable("termNo") Long termNo) {
 
         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()));
+        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()));
 
-			return new ResponseEntity<Object>(result, null, HttpStatus.BAD_REQUEST);
-		}
+            return new ResponseEntity<Object>(result, null, HttpStatus.BAD_REQUEST);
+        }
 
-		return mapResult.getData();
-	}
+        return mapResult.getData();
+    }
 
     /**
      * 设备校时
+     *
      * @param termNo 设备编号
      * @return 校时
      */
     @GetMapping("/CheckTime/{termNo}")
-    public Object checkTermTime(@PathVariable("termNo") Integer termNo){
-        R<Map<String,Object>> mapResult = termBusiness.checkTermTime(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());
@@ -65,4 +67,13 @@ public class TermsController {
         return mapResult.getData();
     }
 
+    /**
+     * 缓存测试
+     * @return 测试结果
+     */
+    @GetMapping("/cache/test")
+    public R<Void> cacheTest(){
+        CacheUtils.put("test", "01", "测试数据");
+        return R.ok();
+    }
 }

+ 6 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/convert/RemoteVoConvert.java

@@ -3,6 +3,8 @@ package org.dromara.server.consume.domain.convert;
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.backstage.api.domain.vo.RemoteXfTermVo;
+import org.dromara.server.consume.domain.bo.XfConsumeDetailOriginalBo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
 import org.dromara.server.consume.domain.vo.XfTermVo;
 import org.mapstruct.Mapper;
 import org.mapstruct.MappingConstants;
@@ -28,4 +30,8 @@ public interface RemoteVoConvert {
     void copyRemoteCardVo(@MappingTarget RemoteCardVo target, RemoteCardVo source);
 
     void copyRemoteTermVo(@MappingTarget XfTermVo target, RemoteXfTermVo source);
+
+    void copyXfConsumeDetailOriginalVo(@MappingTarget XfConsumeDetailOriginalVo target, XfConsumeDetailOriginalVo source);
+
+    void toXfConsumeDetailOriginalVo(@MappingTarget XfConsumeDetailOriginalVo target, XfConsumeDetailOriginalBo source);
 }

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

@@ -39,6 +39,8 @@ public interface IConsumeDetailOriginalService {
      */
     XfConsumeDetailOriginalVo insertByBo(XfConsumeDetailOriginalBo bo);
 
+    Boolean insertByBoBool(XfConsumeDetailOriginalBo bo);
+
     /**
      * 修改原始消费记录
      *

+ 13 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/ConsumeDetailOriginalServiceImpl.java

@@ -188,4 +188,17 @@ public class ConsumeDetailOriginalServiceImpl implements IConsumeDetailOriginalS
                                           .eq(XfConsumeDetailOriginal::getTermRecordId, termRecordId)
                                           .eq(XfConsumeDetailOriginal::getConsumeDate, consumeDate));
     }
+
+    @Override
+    public Boolean insertByBoBool(XfConsumeDetailOriginalBo bo) {
+        XfConsumeDetailOriginal add = MapstructUtils.convert(bo, XfConsumeDetailOriginal.class);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            if (add != null) {
+                bo.setOriginalId(add.getOriginalId());
+                bo.setRecordId(add.getRecordId());
+            }
+        }
+        return flag;
+    }
 }

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

@@ -107,7 +107,7 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
     public Boolean insertByBo(XfConsumeDetailBo bo) {
         XfConsumeDetail add = MapstructUtils.convert(bo, XfConsumeDetail.class);
         validEntityBeforeSave(add);
-        boolean flag = baseMapper.insert(add) > 0;
+        boolean flag = baseMapper.insertOrUpdate(add);
         if (flag) {
             if (add != null) {
                 bo.setConsumeId(add.getConsumeId());

+ 12 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/TestController.java

@@ -21,6 +21,7 @@ import org.dromara.server.hik.domain.dto.QueryDto;
 import org.dromara.server.hik.domain.dto.UploadEmpDto;
 import org.dromara.server.hik.event.EventHandleRouter;
 import org.dromara.server.hik.event.domain.FileContent;
+import org.dromara.server.hik.event.timedtask.HandleTask;
 import org.dromara.server.hik.service.ISendDeviceService;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -59,6 +60,8 @@ public class TestController {
 
     private final EventHandleRouter eventHandleRouter;
 
+    private final HandleTask handleTask;
+
     @DubboReference
     private final RemoteConsumeService remoteConsumeService;
 
@@ -466,4 +469,13 @@ public class TestController {
         }
         return null;
     }
+
+    /**
+     * 手动执行定时任务
+     */
+    @PostMapping("/executeTask")
+    public void executeTask() {
+        handleTask.handleTransactionRecordEventTask();
+    }
+
 }

+ 106 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/XfFailedRecordController.java

@@ -0,0 +1,106 @@
+package org.dromara.server.hik.controller;
+
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.server.hik.domain.vo.XfFailedRecordVo;
+import org.dromara.server.hik.domain.bo.XfFailedRecordBo;
+import org.dromara.server.hik.service.IXfFailedRecordService;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * 海康消费失败记录
+ * 前端访问路由地址为:/FailedRecord/xfFailedRecord
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/FailedRecord/xfFailedRecord")
+public class XfFailedRecordController extends BaseController {
+
+    private final IXfFailedRecordService xfFailedRecordService;
+
+    /**
+     * 查询海康消费失败记录列表
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:list")
+    @GetMapping("/list")
+    public TableDataInfo<XfFailedRecordVo> list(XfFailedRecordBo bo, PageQuery pageQuery) {
+        return xfFailedRecordService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出海康消费失败记录列表
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:export")
+    @Log(title = "海康消费失败记录", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(XfFailedRecordBo bo, HttpServletResponse response) {
+        List<XfFailedRecordVo> list = xfFailedRecordService.queryList(bo);
+        ExcelUtil.exportExcel(list, "海康消费失败记录", XfFailedRecordVo.class, response);
+    }
+
+    /**
+     * 获取海康消费失败记录详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:query")
+    @GetMapping("/{id}")
+    public R<XfFailedRecordVo> getInfo(@NotNull(message = "主键不能为空")
+                                     @PathVariable String id) {
+        return R.ok(xfFailedRecordService.queryById(id));
+    }
+
+    /**
+     * 新增海康消费失败记录
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:add")
+    @Log(title = "海康消费失败记录", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody XfFailedRecordBo bo) {
+        return toAjax(xfFailedRecordService.insertByBo(bo));
+    }
+
+    /**
+     * 修改海康消费失败记录
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:edit")
+    @Log(title = "海康消费失败记录", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody XfFailedRecordBo bo) {
+        return toAjax(xfFailedRecordService.updateByBo(bo));
+    }
+
+    /**
+     * 删除海康消费失败记录
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("FailedRecord:xfFailedRecord:remove")
+    @Log(title = "海康消费失败记录", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable String[] ids) {
+        return toAjax(xfFailedRecordService.deleteWithValidByIds(List.of(ids), true));
+    }
+}

+ 88 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/XfFailedRecord.java

@@ -0,0 +1,88 @@
+package org.dromara.server.hik.domain;
+
+import org.dromara.common.tenant.core.TenantEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.io.Serial;
+
+/**
+ * 海康消费失败记录对象 t_xf_failed_record
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_xf_failed_record")
+public class XfFailedRecord extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id")
+    private String id;
+
+    /**
+     * 用户流水号
+     */
+    private String userNo;
+
+    /**
+     * 物理卡号
+     */
+    private String factoryId;
+
+    /**
+     * 消费时间
+     */
+    private Date consumeDate;
+
+    /**
+     * 设备记录流水号
+     */
+    private String termRecordId;
+
+    /**
+     * 消费金额
+     */
+    private String consumeMoney;
+
+    /**
+     * 设备mac地址
+     */
+    private String termMac;
+
+    /**
+     * 设备机号
+     */
+    private String termNo;
+
+    /**
+     * 上次失败的原因
+     */
+    private String failMsg;
+
+    /**
+     * 处理成功或者失败,s成功f失败
+     */
+    private String status;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+    private String consumeType;
+
+
+
+
+}

+ 89 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/bo/XfFailedRecordBo.java

@@ -0,0 +1,89 @@
+package org.dromara.server.hik.domain.bo;
+
+import org.dromara.server.hik.domain.XfFailedRecord;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import jakarta.validation.constraints.*;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+/**
+ * 海康消费失败记录业务对象 t_xf_failed_record
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = XfFailedRecord.class, reverseConvertGenerate = false)
+public class XfFailedRecordBo extends BaseEntity {
+
+    /**
+     * 主键
+     */
+    @NotBlank(message = "主键不能为空", groups = { EditGroup.class })
+    private String id;
+
+    /**
+     * 用户流水号
+     */
+    @NotBlank(message = "用户流水号不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String userNo;
+
+    /**
+     * 物理卡号
+     */
+    @NotBlank(message = "物理卡号不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String factoryId;
+
+    /**
+     * 消费时间
+     */
+    @NotNull(message = "消费时间不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Date consumeDate;
+
+    /**
+     * 设备记录流水号
+     */
+    @NotBlank(message = "设备记录流水号不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String termRecordId;
+
+    /**
+     * 消费金额
+     */
+    @NotBlank(message = "消费金额不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String consumeMoney;
+
+    /**
+     * 设备mac地址
+     */
+    @NotBlank(message = "设备mac地址不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String termMac;
+
+    /**
+     * 设备机号
+     */
+    @NotBlank(message = "设备机号不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String termNo;
+
+    /**
+     * 上次失败的原因
+     */
+    @NotBlank(message = "上次失败的原因不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String failMsg;
+
+    /**
+     * 处理成功或者失败,s成功f失败
+     */
+    @NotBlank(message = "处理成功或者失败,s成功f失败不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String status;
+
+    @NotBlank(message = "消费类型", groups = { AddGroup.class, EditGroup.class })
+    private String consumeType;
+
+
+}

+ 96 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/vo/XfFailedRecordVo.java

@@ -0,0 +1,96 @@
+package org.dromara.server.hik.domain.vo;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.dromara.server.hik.domain.XfFailedRecord;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+
+/**
+ * 海康消费失败记录视图对象 t_xf_failed_record
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = XfFailedRecord.class)
+public class XfFailedRecordVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @ExcelProperty(value = "主键")
+    private String id;
+
+    /**
+     * 用户流水号
+     */
+    @ExcelProperty(value = "用户流水号")
+    private String userNo;
+
+    /**
+     * 物理卡号
+     */
+    @ExcelProperty(value = "物理卡号")
+    private String factoryId;
+
+    /**
+     * 消费时间
+     */
+    @ExcelProperty(value = "消费时间")
+    private Date consumeDate;
+
+    /**
+     * 设备记录流水号
+     */
+    @ExcelProperty(value = "设备记录流水号")
+    private String termRecordId;
+
+    /**
+     * 消费金额
+     */
+    @ExcelProperty(value = "消费金额")
+    private String consumeMoney;
+
+    /**
+     * 设备mac地址
+     */
+    @ExcelProperty(value = "设备mac地址")
+    private String termMac;
+
+    /**
+     * 设备机号
+     */
+    @ExcelProperty(value = "设备机号")
+    private String termNo;
+
+    /**
+     * 上次失败的原因
+     */
+    @ExcelProperty(value = "上次失败的原因")
+    private String failMsg;
+
+    /**
+     * 处理成功或者失败,s成功f失败
+     */
+    @ExcelProperty(value = "处理成功或者失败,s成功f失败")
+    private String status;
+
+    private String consumeType;
+
+
+}

+ 8 - 25
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/ConsumptionEventHandler.java

@@ -22,8 +22,6 @@ import org.dromara.server.hik.service.IXfTermService;
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -45,8 +43,8 @@ public class ConsumptionEventHandler implements HikEventHandler {
         ConsumptionEventReceive receive = jsonObject.toJavaObject(ConsumptionEventReceive.class);
         ConsumptionEventDetail consumptionEvent = receive.getConsumptionEvent();
         log.info("消费机事件,类型:{}", consumptionEvent.getMinor());
+        XfTermVo termVo = termService.queryByMac(receive.getMacAddress());
         if (TRANSACTION_PREPROCESSING_REQUEST.equals(consumptionEvent.getMinor())) {
-            XfTermVo termVo = termService.queryByMac(receive.getMacAddress());
             if (termVo == null) {
                 log.error("消费机交易记录事件,设备信息为空,mac:{}, 不存在系统中", receive.getMacAddress());
                 // 直接应答失败
@@ -97,9 +95,10 @@ public class ConsumptionEventHandler implements HikEventHandler {
             }
 
             // 进行扣费逻辑处理 调用现有的消费入库的接口
-            RemoteConsumeBo remoteBo = setParamBo(receive, consumptionEvent);;
+            RemoteConsumeBo remoteBo = setParamBo(receive, consumptionEvent);
             remoteBo.setRecordStatus(364L);
             remoteBo.setRecordId(0L);
+            remoteBo.setTermNo(termVo.getTermNo());
             RemoteResultDto result = remoteConsumeService.dealHikUploadRecord(remoteBo);
 //            log.info("消费机消费请求确认事件,结果:{}", JSONObject.toJSONString(result.getUpdatedRemoteBo()));
             R<ErrorInfo> errorInfo = result.getErrorInfo();
@@ -117,24 +116,8 @@ public class ConsumptionEventHandler implements HikEventHandler {
 
     private RemoteConsumeBo setParamBo(ConsumptionEventReceive receive, ConsumptionEventDetail consumptionEvent) {
         RemoteConsumeBo remoteBo = new RemoteConsumeBo();
-        remoteBo.setUserNo(Long.valueOf(consumptionEvent.getEmployeeNoString()));
-        String cardNo = consumptionEvent.getCardNo();
-        if(StringUtils.isNotBlank(cardNo)){
-            remoteBo.setFactoryId(Long.valueOf(cardNo));
-        } else {
-            remoteBo.setFactoryId(0L);
-        }
-        Date dateTime = receive.getDateTime();
-        remoteBo.setConsumeDate(dateTime);
-        remoteBo.setTermRecordId(Long.valueOf(consumptionEvent.getSerialNo()));
-        if(StringUtils.isNotBlank(consumptionEvent.getTotalPayment())){
-            BigDecimal money = new BigDecimal(consumptionEvent.getTotalPayment()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
-            remoteBo.setConsumeMoney(money);
-        }else{
-            remoteBo.setConsumeMoney(BigDecimal.ZERO);
-        }
-
-        remoteBo.setTermMac(receive.getMacAddress());
+        TransactionRecordEventHandler.setCommonFields(remoteBo, consumptionEvent.getEmployeeNoString(), consumptionEvent.getCardNo(),
+            receive.getDateTime(), consumptionEvent.getSerialNo(), consumptionEvent.getTotalPayment(), receive.getMacAddress());
         remoteBo.setCardNo(0L);
         remoteBo.setStatusFlag(4);
         remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
@@ -173,9 +156,9 @@ public class ConsumptionEventHandler implements HikEventHandler {
             }
         }
 
-        String broadcastVoice = isCountMode ? "刷卡成功":"消费成功";
+        String broadcastVoice = isCountMode ? "刷卡成功":"支付成功";
         if(!answerResult){
-            broadcastVoice = isCountMode ? "刷卡失败":"消费失败";
+            broadcastVoice = isCountMode ? "刷卡失败":"支付失败";
             // 提示刷卡失败的原因
             broadcastVoice += reason;
             confirmBo.setReason("platformException");
@@ -190,7 +173,7 @@ public class ConsumptionEventHandler implements HikEventHandler {
         contentInfo.setContent(deptName);
         if(!answerResult){
             contentInfo.setTitle(consumptionEvent.getName()+ "  "+deptName);
-            contentInfo.setContent("消费失败原因:"+reason);
+            contentInfo.setContent("失败原因:"+reason);
         }
         confirmBo.setContentInfo(contentInfo);
 

+ 104 - 24
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/TransactionRecordEventHandler.java

@@ -5,38 +5,33 @@ import io.seata.common.util.CollectionUtils;
 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.exception.ServiceException;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.CreditTypeEnum;
 import org.dromara.common.core.utils.DateUtils;
+import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.core.utils.file.FileUtils;
 import org.dromara.consume.api.RemoteConsumeService;
-import org.dromara.server.hik.domain.dto.DeviceDto;
-import org.dromara.server.hik.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.consume.api.domain.bo.RemoteConsumeBo;
+import org.dromara.consume.api.domain.bo.RemoteResultDto;
+import org.dromara.server.hik.domain.bo.XfFailedRecordBo;
 import org.dromara.server.hik.domain.vo.XfConsumeDetailVo;
 import org.dromara.server.hik.domain.vo.XfTermVo;
-import org.dromara.server.hik.enums.ContentTypeEnum;
 import org.dromara.server.hik.enums.ModeTypeEnum;
 import org.dromara.server.hik.event.HikEventHandler;
 import org.dromara.server.hik.event.domain.FileContent;
 import org.dromara.server.hik.event.domain.TransactionRecordEventConfirmBo;
 import org.dromara.server.hik.event.domain.TransactionRecordEventDetail;
 import org.dromara.server.hik.event.domain.TransactionRecordEventReceive;
-import org.dromara.server.hik.service.IConsumeDetailOriginalService;
-import org.dromara.server.hik.service.ISendDeviceService;
-import org.dromara.server.hik.service.IXfConsumeDetailService;
-import org.dromara.server.hik.service.IXfTermService;
-import org.dromara.server.hik.service.impl.SendDeviceServiceImpl;
-import org.dromara.server.hik.utils.DigestHttpUtil;
+import org.dromara.server.hik.service.*;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 import java.io.File;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.*;
 
-import static org.dromara.server.hik.constant.HikApiConstants.TRANSACTION_RECORD_Event_confirm;
-
 @Slf4j
 @Component
 @RequiredArgsConstructor
@@ -48,6 +43,8 @@ public class TransactionRecordEventHandler implements HikEventHandler {
 
     private final IConsumeDetailOriginalService consumeDetailOriginalService;
 
+    private final IXfFailedRecordService failedRecordService;
+
     @Value("${upload.upload-path}/")
     private String uploadPath;
 
@@ -63,9 +60,10 @@ public class TransactionRecordEventHandler implements HikEventHandler {
 
         //设备信息
         XfTermVo termVo = termService.queryByMac(transactionRecordEventReceive.getMacAddress());
+        Long termNo = termVo.getTermNo();
         // 消费抓拍图片保存
         if (fileContent != null) {
-            String fileName = termVo.getTermNo()+"_"+transactionRecordEvent.getSerialNo()+  "_" + DateUtils.dateTimeNow() + fileContent.getSuffix();
+            String fileName = termNo +"_"+transactionRecordEvent.getSerialNo()+  "_" + DateUtils.dateTimeNow() + fileContent.getSuffix();
             byte[] content = fileContent.getContent();
             String image_dir_path = uploadPath + "snapshot-pic/";
             File image_dir = new File(image_dir_path);
@@ -79,30 +77,45 @@ public class TransactionRecordEventHandler implements HikEventHandler {
         // 这里只能应答成功,如果是业务异常(余额不足、脏数据等)失败,要存入表中,使用定时任务处理失败,所以即使失败的 也要回复成功
         // 如果是在线交易的,平台根据流水号判断当前交易记录事件是否为未处理事件,若为未处理事件,则平台进行处理并进行扣费;
         // 如果是离线交易记录事件,则平台进行处理并进行扣费;
+        RemoteConsumeBo remoteBo = getRemoteBo(transactionRecordEventReceive);
+        remoteBo.setTermNo(termNo);
+        R<ErrorInfo> errorInfo = R.fail("处理失败");
         try  {
             if (ModeTypeEnum.offLine.getCode().equals(modeType)) {
-                // todo 离线交易,调用原始消费记录请求+消费记录入库的接口
-                // 如果失败要记录入库
-
+                // 离线交易,调用原始消费记录请求+消费记录入库的接口,如果失败要记录入库
+                RemoteResultDto remoteResultDto = remoteConsumeService.dealHikUploadOffLineRecord(remoteBo);
+                errorInfo = remoteResultDto.getErrorInfo();
+                RemoteConsumeBo updatedRemoteBo = remoteResultDto.getUpdatedRemoteBo();
+                if(updatedRemoteBo !=null) {
+                    balance  = updatedRemoteBo.getBalance();
+                }
             }else{
                 // 在线交易,调用消费记录入库的接口
                 // 根据机号和交易记录流水号查询消费记录
-                List<XfConsumeDetailVo> vos = consumeDetailService.queryByTermNoAndRecordId(termVo.getTermNo(), transactionRecordEvent.getSerialNo().longValue());
+                // 睡眠一秒,等待消费记录入库成功
+                Thread.sleep(1000);
+                List<XfConsumeDetailVo> vos = consumeDetailService.queryByTermNoAndRecordId(termNo, transactionRecordEvent.getSerialNo().longValue());
                 if (CollectionUtils.isEmpty(vos)){
-                    // 调用消费记录入库的接口
-                    // todo 组装参数,调用消费记录入库的接口
-                    // 如果失败要记录入库
-
+                    // 组装参数,调用消费记录入库的接口 // 如果失败要记录入库
+                    RemoteResultDto remoteResultDto = remoteConsumeService.dealHikUploadRecord(remoteBo);
+                    errorInfo = remoteResultDto.getErrorInfo();
+                    RemoteConsumeBo updatedRemoteBo = remoteResultDto.getUpdatedRemoteBo();
+                    if(updatedRemoteBo !=null) {
+                        balance  = updatedRemoteBo.getBalance();
+                    }
                 }else{
                     XfConsumeDetailVo consumeDetailVo = vos.get(0);
                     balance = consumeDetailVo.getCardValue();
                 }
             }
         } catch (Exception e) {
-            log.error("消费机交易记录事件处理异常:{}", e.getMessage());
+            e.printStackTrace();
+            log.error("消费机交易记录事件处理异常:{},参数:{}", e.getMessage(),  JSONObject.toJSONString(remoteBo));
             // 失败,记录入库
+            errorInfo = R.fail(e.getMessage());
         }
 
+        insertFailedRecordBo(transactionRecordEventReceive, termNo, errorInfo, modeType);
         //应答
         return answerEvent(transactionRecordEventReceive,balance);
     }
@@ -128,4 +141,71 @@ public class TransactionRecordEventHandler implements HikEventHandler {
         map.put("TransactionRecordEventConfirm", confirmBo);
         return map;
     }
+
+
+    private RemoteConsumeBo getRemoteBo(TransactionRecordEventReceive info){
+        RemoteConsumeBo remoteBo = new RemoteConsumeBo();
+        remoteBo.setStatusFlag(4);
+        remoteBo.setCardNo(0L);
+        remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
+        remoteBo.setRecordStatus(364L);
+        remoteBo.setRecordId(0L);
+
+        TransactionRecordEventDetail recordEvent = info.getTransactionRecordEvent();
+        setCommonFields(remoteBo, recordEvent.getEmployeeNoString(), recordEvent.getCardNo(), info.getDateTime(),
+            recordEvent.getSerialNo(), recordEvent.getTotalPayment(), info.getMacAddress());
+
+        return remoteBo;
+    }
+
+    static void setCommonFields(RemoteConsumeBo remoteBo, String employeeNoString, String cardNo2, Date dateTime2, Integer serialNo,
+                                String totalPayment, String macAddress) {
+        remoteBo.setUserNo(Long.valueOf(employeeNoString));
+        if(StringUtils.isNotBlank(cardNo2)){
+            remoteBo.setFactoryId(Long.valueOf(cardNo2));
+        } else {
+            remoteBo.setFactoryId(0L);
+        }
+        remoteBo.setConsumeDate(dateTime2);
+        remoteBo.setTermRecordId(Long.valueOf(serialNo));
+        if(StringUtils.isNotBlank(totalPayment)){
+            BigDecimal money = new BigDecimal(totalPayment).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+            remoteBo.setConsumeMoney(money);
+        }else{
+            remoteBo.setConsumeMoney(BigDecimal.ZERO);
+        }
+        remoteBo.setTermMac(macAddress);
+    }
+
+    private void insertFailedRecordBo(TransactionRecordEventReceive info, Long termNo, R<ErrorInfo> errorInfo, String modeType){
+        Boolean error = R.isError(errorInfo);
+        if (!error) {
+            return;
+        }
+        XfFailedRecordBo bo = new XfFailedRecordBo();
+        TransactionRecordEventDetail recordEvent = info.getTransactionRecordEvent();
+        bo.setUserNo(recordEvent.getEmployeeNoString());
+        bo.setConsumeDate(info.getDateTime());
+        bo.setTermRecordId(recordEvent.getSerialNo().toString());
+        bo.setConsumeMoney(recordEvent.getTotalPayment());
+        bo.setTermMac(info.getMacAddress());
+        if(termNo != null){
+            bo.setTermNo(termNo.toString());
+        }
+        bo.setStatus("f");
+        bo.setFactoryId(recordEvent.getCardNo());
+        String msg = errorInfo.getMsg();
+        ErrorInfo data = errorInfo.getData();
+        if(data != null){
+            msg = data.getMessage();
+        }
+        if(StringUtils.isNotBlank(msg) && msg.length() >= 2000){
+            msg = msg.substring(0, 2000);
+        }
+        bo.setFailMsg(msg);
+        bo.setConsumeType(modeType);
+
+        failedRecordService.insertByBo(bo);
+    }
+
 }

+ 100 - 3
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/timedtask/HandleTask.java

@@ -1,10 +1,30 @@
 package org.dromara.server.hik.event.timedtask;
 
+import com.alibaba.fastjson.JSONObject;
+import io.seata.common.util.CollectionUtils;
+import io.seata.common.util.StringUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.CreditTypeEnum;
+import org.dromara.consume.api.RemoteConsumeService;
+import org.dromara.consume.api.domain.bo.RemoteConsumeBo;
+import org.dromara.consume.api.domain.bo.RemoteResultDto;
+import org.dromara.server.hik.domain.bo.XfFailedRecordBo;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailVo;
+import org.dromara.server.hik.domain.vo.XfFailedRecordVo;
+import org.dromara.server.hik.enums.ModeTypeEnum;
+import org.dromara.server.hik.service.IXfConsumeDetailService;
+import org.dromara.server.hik.service.IXfFailedRecordService;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+
 /**
  * 定时任务: 主要用于处理 TransactionRecordEvent事件 业务失败的记录 于凌晨执行即可
  */
@@ -13,14 +33,91 @@ import org.springframework.stereotype.Service;
 @Slf4j
 public class HandleTask {
 
+    private final IXfFailedRecordService failedRecordService;
+
+    private final IXfConsumeDetailService consumeDetailService;
+
+    @DubboReference
+    private final RemoteConsumeService remoteConsumeService;
 
     // 每天凌晨3点执行一次
     @Scheduled(cron = "0 0 3 * * ?")
     public void handleTransactionRecordEventTask()
     {
         /*有离线的,也有非离线的,重复在执行下 处理处理TransactionRecordEvent的逻辑 */
-        log.info("开始处理 TransactionRecordEvent事件,");
-        // todo 获取所有失败的 TransactionRecordEvent
-        // 设计t_xf_failed_record表  ID 、event_type、event_data、fail_msg、status、create_time、update_time、delete_flag、create_by、update_by
+        log.info("开始处理 TransactionRecordEvent事件的消费失败记录,");
+        //  获取所有失败的 TransactionRecordEvent
+        // 设计t_xf_failed_record表  ID 、tenant_id、user_no、factory_id、consume_date、term_record_id、consume_money、term_mac、term_no、fail_msg、status、create_time、update_time、delete_flag、create_by、update_by
+        XfFailedRecordBo bo = new XfFailedRecordBo();
+        bo.setStatus("f");
+        List<XfFailedRecordVo> vos = failedRecordService.queryList(bo);
+        RemoteConsumeBo remoteBo = new RemoteConsumeBo();
+        remoteBo.setStatusFlag(4);
+        remoteBo.setCardNo(0L);
+        remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
+        remoteBo.setRecordStatus(364L);
+        remoteBo.setRecordId(0L);
+        for (XfFailedRecordVo vo : vos) {
+            boolean result = false;
+            try{
+                if(StringUtils.isNotBlank(vo.getUserNo())){
+                    remoteBo.setUserNo(Long.valueOf(vo.getUserNo()));
+                }else{
+                    remoteBo.setUserNo(0L);
+                }
+                if(StringUtils.isNotBlank(vo.getFactoryId())){
+                    remoteBo.setFactoryId(Long.valueOf(vo.getFactoryId()));
+                }else{
+                    remoteBo.setFactoryId(0L);
+                }
+                if(StringUtils.isNotBlank(vo.getConsumeMoney())){
+                    BigDecimal money = new BigDecimal(vo.getConsumeMoney()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+                    remoteBo.setConsumeMoney(money);
+                }else{
+                    remoteBo.setConsumeMoney(BigDecimal.ZERO);
+                }
+                remoteBo.setTermMac(vo.getTermMac());
+                if(StringUtils.isNotBlank(vo.getTermNo())){
+                    remoteBo.setTermNo(Long.valueOf(vo.getTermNo()));
+                }
+                if(StringUtils.isNotBlank(vo.getTermRecordId())){
+                    remoteBo.setTermRecordId(Long.valueOf(vo.getTermRecordId()));
+                }else{
+                    remoteBo.setTermRecordId(0L);
+                }
+                remoteBo.setConsumeDate(vo.getConsumeDate());
+                RemoteResultDto rs;
+                if(ModeTypeEnum.offLine.getCode().equals(vo.getConsumeType())){
+                    //  离线交易,调用原始消费记录请求+消费记录入库的接口
+                    rs = remoteConsumeService.dealHikUploadOffLineRecord(remoteBo);
+                }else{
+                    //  在线交易,调用消费记录入库的接口
+                    // 在查询一次 消费记录,如果有则不再执行
+                    List<XfConsumeDetailVo> list = consumeDetailService.queryByTermNoAndRecordId(Long.valueOf(vo.getTermNo()), Long.valueOf(vo.getTermRecordId()));
+                    if(CollectionUtils.isEmpty(vos)){
+                        rs = remoteConsumeService.dealHikUploadRecord(remoteBo);
+                    }else{
+                        log.info("消费机事件,已经存在消费记录,不再处理:{}", vo);
+                        rs = new RemoteResultDto(R.ok(),null);
+                    }
+                }
+
+                R<ErrorInfo> errorInfo = rs.getErrorInfo();
+                if (!R.isError(errorInfo)) {
+                    result = true;
+                }
+
+            }catch (Exception e){
+                e.printStackTrace();
+                log.error("定时处理 TransactionRecordEvent事件失败:{}, 参数: {}", e.getMessage(), JSONObject.toJSONString(vo));
+            }
+            if(result){
+                // 如果成功则更新status为f
+                XfFailedRecordBo recordBo = new XfFailedRecordBo();
+                recordBo.setId(vo.getId());
+                recordBo.setStatus("s");
+                failedRecordService.updateByBo(recordBo);
+            }
+        }
     }
 }

+ 15 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/mapper/XfFailedRecordMapper.java

@@ -0,0 +1,15 @@
+package org.dromara.server.hik.mapper;
+
+import org.dromara.server.hik.domain.XfFailedRecord;
+import org.dromara.server.hik.domain.vo.XfFailedRecordVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * 海康消费失败记录Mapper接口
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+public interface XfFailedRecordMapper extends BaseMapperPlus<XfFailedRecord, XfFailedRecordVo> {
+
+}

+ 77 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/IXfFailedRecordService.java

@@ -0,0 +1,77 @@
+package org.dromara.server.hik.service;
+
+import org.dromara.server.hik.domain.XfFailedRecord;
+import org.dromara.server.hik.domain.vo.XfFailedRecordVo;
+import org.dromara.server.hik.domain.bo.XfFailedRecordBo;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 海康消费失败记录Service接口
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+public interface IXfFailedRecordService {
+
+    /**
+     * 查询海康消费失败记录
+     *
+     * @param id 主键
+     * @return 海康消费失败记录
+     */
+    XfFailedRecordVo queryById(String id);
+
+    /**
+     * 分页查询海康消费失败记录列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 海康消费失败记录分页列表
+     */
+    TableDataInfo<XfFailedRecordVo> queryPageList(XfFailedRecordBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询符合条件的海康消费失败记录列表
+     *
+     * @param bo 查询条件
+     * @return 海康消费失败记录列表
+     */
+    List<XfFailedRecordVo> queryList(XfFailedRecordBo bo);
+
+    /**
+     * 新增海康消费失败记录
+     *
+     * @param bo 海康消费失败记录
+     * @return 是否新增成功
+     */
+    Boolean insertByBo(XfFailedRecordBo bo);
+
+    /**
+     * 新增海康消费失败记录
+     *
+     * @param record 海康消费失败记录
+     * @return 是否新增成功
+     */
+    Boolean insertByEntity(XfFailedRecord record);
+
+    /**
+     * 修改海康消费失败记录
+     *
+     * @param bo 海康消费失败记录
+     * @return 是否修改成功
+     */
+    Boolean updateByBo(XfFailedRecordBo bo);
+
+    /**
+     * 校验并批量删除海康消费失败记录信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
+}

+ 181 - 0
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/impl/XfFailedRecordServiceImpl.java

@@ -0,0 +1,181 @@
+package org.dromara.server.hik.service.impl;
+
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.dromara.server.hik.domain.bo.XfFailedRecordBo;
+import org.dromara.server.hik.domain.vo.XfFailedRecordVo;
+import org.dromara.server.hik.domain.XfFailedRecord;
+import org.dromara.server.hik.mapper.XfFailedRecordMapper;
+import org.dromara.server.hik.service.IXfFailedRecordService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Collection;
+
+/**
+ * 海康消费失败记录Service业务层处理
+ *
+ * @author LionLi
+ * @date 2025-06-09
+ */
+@RequiredArgsConstructor
+@Service
+public class XfFailedRecordServiceImpl implements IXfFailedRecordService {
+
+    private final XfFailedRecordMapper baseMapper;
+
+    /**
+     * 查询海康消费失败记录
+     *
+     * @param id 主键
+     * @return 海康消费失败记录
+     */
+    @Override
+    public XfFailedRecordVo queryById(String id){
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 分页查询海康消费失败记录列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 海康消费失败记录分页列表
+     */
+    @Override
+    public TableDataInfo<XfFailedRecordVo> queryPageList(XfFailedRecordBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<XfFailedRecord> lqw = buildQueryWrapper(bo);
+        Page<XfFailedRecordVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询符合条件的海康消费失败记录列表
+     *
+     * @param bo 查询条件
+     * @return 海康消费失败记录列表
+     */
+    @Override
+    public List<XfFailedRecordVo> queryList(XfFailedRecordBo bo) {
+        LambdaQueryWrapper<XfFailedRecord> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<XfFailedRecord> buildQueryWrapper(XfFailedRecordBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<XfFailedRecord> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getUserNo()), XfFailedRecord::getUserNo, bo.getUserNo());
+        lqw.eq(StringUtils.isNotBlank(bo.getFactoryId()), XfFailedRecord::getFactoryId, bo.getFactoryId());
+        lqw.eq(bo.getConsumeDate() != null, XfFailedRecord::getConsumeDate, bo.getConsumeDate());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermRecordId()), XfFailedRecord::getTermRecordId, bo.getTermRecordId());
+        lqw.eq(StringUtils.isNotBlank(bo.getConsumeMoney()), XfFailedRecord::getConsumeMoney, bo.getConsumeMoney());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermMac()), XfFailedRecord::getTermMac, bo.getTermMac());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermNo()), XfFailedRecord::getTermNo, bo.getTermNo());
+        lqw.eq(StringUtils.isNotBlank(bo.getFailMsg()), XfFailedRecord::getFailMsg, bo.getFailMsg());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XfFailedRecord::getStatus, bo.getStatus());
+        return lqw;
+    }
+
+    private QueryWrapper<XfFailedRecord> buildQueryWrapper(XfFailedRecordBo bo,String tableAlias) {
+        QueryWrapper<XfFailedRecord> lqw = new QueryWrapper<>();
+        String columnPrefix = "";
+        if(StringUtils.isNotBlank(tableAlias)){
+            columnPrefix = tableAlias + ".";
+        }
+        lqw.eq(StringUtils.isNotBlank(bo.getUserNo()), columnPrefix+"user_no", bo.getUserNo());
+        lqw.eq(StringUtils.isNotBlank(bo.getFactoryId()), columnPrefix+"factory_id", bo.getFactoryId());
+        lqw.eq(bo.getConsumeDate() != null, columnPrefix+"consume_date", bo.getConsumeDate());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermRecordId()), columnPrefix+"term_record_id", bo.getTermRecordId());
+        lqw.eq(StringUtils.isNotBlank(bo.getConsumeMoney()), columnPrefix+"consume_money", bo.getConsumeMoney());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermMac()), columnPrefix+"term_mac", bo.getTermMac());
+        lqw.eq(StringUtils.isNotBlank(bo.getTermNo()), columnPrefix+"term_no", bo.getTermNo());
+        lqw.eq(StringUtils.isNotBlank(bo.getFailMsg()), columnPrefix+"fail_msg", bo.getFailMsg());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), columnPrefix+"status", bo.getStatus());
+        return lqw;
+    }
+
+    /**
+     * 新增海康消费失败记录
+     *
+     * @param bo 海康消费失败记录
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByBo(XfFailedRecordBo bo) {
+        XfFailedRecord add = MapstructUtils.convert(bo, XfFailedRecord.class);
+        validEntityBeforeSave(add);
+        //先查询一遍在新增,避免重复新增
+        if (isExist(bo.getTermRecordId(), bo.getTermNo(), bo.getTermMac())) return false;
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    private boolean isExist(String termRecordId, String termNo, String termMac) {
+        LambdaQueryWrapper<XfFailedRecord> eq = Wrappers.lambdaQuery(XfFailedRecord.class).eq(XfFailedRecord::getTermRecordId, termRecordId)
+            .eq(StringUtils.isNotBlank(termNo), XfFailedRecord::getTermNo, termNo)
+            .eq(XfFailedRecord::getTermMac, termMac);
+        if (baseMapper.selectCount(eq) > 0) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 新增海康消费失败记录, 先查询一遍在新增,避免重复新增
+     *
+     * @param record 海康消费失败记录
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByEntity(XfFailedRecord record) {
+        //先查询一遍在新增,避免重复新增
+        if (isExist(record.getTermRecordId(), record.getTermNo(), record.getTermMac())) return false;
+        return baseMapper.insert(record) > 0;
+    }
+
+    /**
+     * 修改海康消费失败记录
+     *
+     * @param bo 海康消费失败记录
+     * @return 是否修改成功
+     */
+    @Override
+    public Boolean updateByBo(XfFailedRecordBo bo) {
+        XfFailedRecord update = MapstructUtils.convert(bo, XfFailedRecord.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(XfFailedRecord entity){
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 校验并批量删除海康消费失败记录信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
+        if(isValid){
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+}

+ 25 - 0
ruoyi-server/ruoyi-server-hik/src/main/resources/mapper/FailedRecord/XfFailedRecordMapper.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.server.hik.mapper.XfFailedRecordMapper">
+
+    <resultMap type="org.dromara.server.hik.domain.XfFailedRecord" id="XfFailedRecordResult">
+            <result property="id"    column="id"    />
+            <result property="tenantId"    column="tenant_id"    />
+            <result property="userNo"    column="user_no"    />
+            <result property="factoryId"    column="factory_id"    />
+            <result property="consumeDate"    column="consume_date"    />
+            <result property="termRecordId"    column="term_record_id"    />
+            <result property="consumeMoney"    column="consume_money"    />
+            <result property="termMac"    column="term_mac"    />
+            <result property="termNo"    column="term_no"    />
+            <result property="failMsg"    column="fail_msg"    />
+            <result property="status"    column="status"    />
+            <result property="delFlag"    column="del_flag"    />
+            <result property="createBy"    column="create_by"    />
+            <result property="createTime"    column="create_time"    />
+            <result property="updateBy"    column="update_by"    />
+            <result property="updateTime"    column="update_time"    />
+    </resultMap>
+</mapper>

+ 41 - 0
sql/2025-6-9/kingbase/ddl.sql

@@ -0,0 +1,41 @@
+CREATE TABLE "dbo"."t_xf_failed_record" (
+    "id" character varying(64 char) NOT NULL,
+    "tenant_id" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+    "consume_type" character varying(10 char) NULL,
+	"user_no" character varying(20 char) NULL,
+	"factory_id" character varying(20 char) NULL,
+	"consume_date" timestamp NULL,
+	"term_record_id" character varying(20 char) NULL,
+	"consume_money" character varying(10 char) NULL,
+	"term_mac" character varying(20 char) NOT NULL DEFAULT ''::varchar,
+	"term_no" character varying(20 char) NULL,
+	"fail_msg" character varying(2000 char) NULL,
+	"status" character(1 char) NOT NULL DEFAULT 'f'::bpchar,
+	"del_flag" character(1 char) NOT NULL DEFAULT '0'::bpchar,
+	"create_by" bigint NULL,
+	"create_dept" bigint NULL,
+	"create_time" timestamp NULL,
+	"update_by" bigint NULL,
+	"update_time" timestamp NULL,
+	CONSTRAINT "t_xf_failed_record_pkey" PRIMARY KEY (id)
+);
+ALTER TABLE "dbo"."t_xf_failed_record" COMMENT '海康消费失败记录';
+
+-- Column comments
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "id" COMMENT '主键';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "tenant_id" COMMENT '租户Id';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "user_no" COMMENT '用户流水号';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "factory_id" COMMENT '物理卡号';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "term_record_id" COMMENT '设备记录流水号';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "consume_money" COMMENT '消费金额';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "consume_date" COMMENT '消费时间';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "term_mac" COMMENT '设备mac地址';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "term_no" COMMENT '设备机号';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "fail_msg" COMMENT '上次失败的原因';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "status" COMMENT '处理结果,s成功f失败';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "del_flag" COMMENT '删除标志(0-未删除 2-已删除)';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "create_by" COMMENT '创建者';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "create_time" COMMENT '创建时间';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "update_by" COMMENT '更新者';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "update_time" COMMENT '更新时间';
+ALTER TABLE "dbo"."t_xf_failed_record" MODIFY "consume_type" COMMENT '消费类型,在线current离线offLine';

+ 21 - 0
sql/2025-6-9/mysql/ddl.sql

@@ -0,0 +1,21 @@
+CREATE TABLE `ykt_cloud`.`t_xf_failed_record`  (
+                                         `id` varchar(64) NOT NULL COMMENT '主键',
+                                         `tenant_id` varchar(20) NOT NULL DEFAULT '' COMMENT '租户Id',
+                                         `user_no` varchar(20) NULL COMMENT '用户流水号',
+                                         `factory_id` varchar(20) NULL COMMENT '物理卡号',
+                                         `consume_date` datetime NULL COMMENT '消费时间',
+                                         `consume_type` varchar(10) NULL COMMENT '消费类型,在线current离线offLine',
+                                         `term_record_id` varchar(20) NULL COMMENT '设备记录流水号',
+                                         `consume_money` varchar(10) NULL COMMENT '消费金额',
+                                         `term_mac` varchar(20) NULL COMMENT '设备mac地址',
+                                         `term_no` varchar(20) NULL COMMENT '设备机号',
+                                         `fail_msg` varchar(2000) NULL COMMENT '失败原因',
+                                         `status` char(1) NOT NULL DEFAULT 'f' COMMENT '处理结果,s成功f失败',
+                                         `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志',
+                                         `create_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人',
+                                         `create_dept` bigint(0) NULL DEFAULT NULL COMMENT '创建人部门',
+                                         `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
+                                         `update_by` bigint(0) NULL DEFAULT NULL COMMENT '修改人',
+                                         `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
+                                         PRIMARY KEY (`id`)
+) COMMENT = '海康消费失败记录';