Explorar o código

fix: 消费服务
1.请求时是否可以消费验证线程处理优化
2.增加了人脸服务框架

luoyb hai 10 meses
pai
achega
057ef6c487
Modificáronse 87 ficheiros con 7811 adicións e 45 borrados
  1. 12 12
      pom.xml
  2. 1 0
      ruoyi-server/pom.xml
  3. 137 33
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeRequestCheck.java
  4. 176 0
      ruoyi-server/ruoyi-server-face/pom.xml
  5. 26 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/RuoYiServerFaceApplication.java
  6. 53 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/ErrCodeConstants.java
  7. 40 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikApiConstants.java
  8. 16 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikDefaultConstants.java
  9. 12 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikEventTypeConstant.java
  10. 482 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/controller/TestController.java
  11. 106 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/controller/XfFailedRecordController.java
  12. 205 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfConsumeDetail.java
  13. 171 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfConsumeDetailOriginal.java
  14. 88 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfFailedRecord.java
  15. 367 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfTerm.java
  16. 89 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/bo/XfFailedRecordBo.java
  17. 74 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/DeviceDto.java
  18. 44 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/QueryDto.java
  19. 54 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/UploadEmpDto.java
  20. 33 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/UserInfoDto.java
  21. 41 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/CardDto.java
  22. 37 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/CardListDto.java
  23. 70 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/EmpInfoDto.java
  24. 54 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/FaceDto.java
  25. 36 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/FaceListDto.java
  26. 41 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/ValidDto.java
  27. 152 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/query/QueryEmpMatchDto.java
  28. 47 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/query/QueryEmpResultDto.java
  29. 38 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/remote/RemoteCardDto.java
  30. 162 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfConsumeDetailOriginalVo.java
  31. 199 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfConsumeDetailVo.java
  32. 96 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfFailedRecordVo.java
  33. 338 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfTermVo.java
  34. 189 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/AuthenticationEnum.java
  35. 79 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/CardTypeEnum.java
  36. 68 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ConsumeModeEnum.java
  37. 89 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ContentTypeEnum.java
  38. 59 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/EmpTypeEnum.java
  39. 60 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ModeTypeEnum.java
  40. 61 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/StatusCodeEnum.java
  41. 57 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/EventHandleRouter.java
  42. 11 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/HikEventHandler.java
  43. 136 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmBo.java
  44. 17 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmingReceive.java
  45. 59 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventDetail.java
  46. 31 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventReceive.java
  47. 30 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/EventResponseInfo.java
  48. 19 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/FileContent.java
  49. 24 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/HeatBeatData.java
  50. 19 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/PictureResolution.java
  51. 19 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/RecordImageInfo.java
  52. 43 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventConfirmBo.java
  53. 92 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventDetail.java
  54. 31 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventReceive.java
  55. 193 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/ConsumptionEventHandler.java
  56. 92 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/HeatBeatHandler.java
  57. 221 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/TransactionRecordEventHandler.java
  58. 123 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/timedtask/HandleTask.java
  59. 21 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/ConsumeDetailOriginalMapper.java
  60. 13 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/XfConsumeDetailMapper.java
  61. 15 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/XfFailedRecordMapper.java
  62. 15 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/XfTermMapper.java
  63. 95 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/consumer/HikKafkaConsumer.java
  64. 20 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/event/IHIKEventStrategy.java
  65. 63 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/event/impl/HikBackStageEventImpl.java
  66. 66 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IConsumeDetailOriginalService.java
  67. 220 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/ISendDeviceService.java
  68. 36 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IXfConsumeDetailService.java
  69. 77 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IXfFailedRecordService.java
  70. 35 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IXfTermService.java
  71. 117 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/ConsumeDetailOriginalServiceImpl.java
  72. 799 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/SendDeviceServiceImpl.java
  73. 71 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/XfConsumeDetailServiceImpl.java
  74. 181 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/XfFailedRecordServiceImpl.java
  75. 111 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/XfTermServiceImpl.java
  76. 61 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/task/ScheduledTasks.java
  77. 133 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/utils/DigestHttpUtil.java
  78. 24 0
      ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/utils/JsonConfig.java
  79. 0 0
      ruoyi-server/ruoyi-server-face/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  80. 34 0
      ruoyi-server/ruoyi-server-face/src/main/resources/application.yml
  81. 10 0
      ruoyi-server/ruoyi-server-face/src/main/resources/banner.txt
  82. 61 0
      ruoyi-server/ruoyi-server-face/src/main/resources/logback-plus.xml
  83. 25 0
      ruoyi-server/ruoyi-server-face/src/main/resources/mapper/FailedRecord/XfFailedRecordMapper.xml
  84. 57 0
      ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/ConsumeDetailOriginalMapper.xml
  85. 49 0
      ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/XfConsumeDetailMapper.xml
  86. 80 0
      ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/XfTermMapper.xml
  87. 3 0
      ruoyi-server/ruoyi-server-face/src/main/resources/mapper/package-info.md

+ 12 - 12
pom.xml

@@ -88,31 +88,31 @@
             </properties>
             <activation>
                 <!-- 默认环境 -->
-                <activeByDefault>true</activeByDefault>
+                <activeByDefault>false</activeByDefault>
             </activation>
         </profile>
         <profile>
             <id>prod</id>
             <properties>
-                <profiles.active>prod</profiles.active>
-                <nacos.server>10.32.23.155:8845,10.32.23.156:8847,10.32.16.215:8849</nacos.server>
-                <nacos.discovery.group>DEFAULT_GROUP</nacos.discovery.group>
-                <nacos.config.group>DEFAULT_GROUP</nacos.config.group>
-                <nacos.username>nacos</nacos.username>
-                <nacos.password>nacos</nacos.password>
-                <logstash.address>10.32.23.155:4560</logstash.address>
-                <!-- 党校本地环境 -->
 <!--                <profiles.active>prod</profiles.active>-->
-<!--                <nacos.server>172.16.137.72:8845,172.16.137.73:8847,172.16.137.74:8849</nacos.server>-->
+<!--                <nacos.server>10.32.23.155:8845,10.32.23.156:8847,10.32.16.215:8849</nacos.server>-->
 <!--                <nacos.discovery.group>DEFAULT_GROUP</nacos.discovery.group>-->
 <!--                <nacos.config.group>DEFAULT_GROUP</nacos.config.group>-->
 <!--                <nacos.username>nacos</nacos.username>-->
 <!--                <nacos.password>nacos</nacos.password>-->
-<!--                <logstash.address>172.16.137.72:4560</logstash.address>-->
+<!--                <logstash.address>10.32.23.155:4560</logstash.address>-->
+                <!-- 党校本地环境 -->
+                <profiles.active>prod</profiles.active>
+                <nacos.server>172.16.137.175:8848</nacos.server>
+                <nacos.discovery.group>DEFAULT_GROUP</nacos.discovery.group>
+                <nacos.config.group>DEFAULT_GROUP</nacos.config.group>
+                <nacos.username>nacos</nacos.username>
+                <nacos.password>123456</nacos.password>
+                <logstash.address>172.16.137.175:4560</logstash.address>
             </properties>
             <activation>
                 <!-- 默认环境 -->
-                <activeByDefault>false</activeByDefault>
+                <activeByDefault>true</activeByDefault>
             </activation>
         </profile>
     </profiles>

+ 1 - 0
ruoyi-server/pom.xml

@@ -12,6 +12,7 @@
         <module>ruoyi-server-mqdata</module>
         <module>ruoyi-server-consume</module>
         <module>ruoyi-server-hik</module>
+        <module>ruoyi-server-face</module>
     </modules>
     <modelVersion>4.0.0</modelVersion>
 

+ 137 - 33
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/check/ConsumeRequestCheck.java

@@ -14,6 +14,7 @@ import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.enums.ConsumeRecordTypeEnum;
 import org.dromara.common.core.enums.TradeStatusEnum;
 import org.dromara.common.core.utils.RecordIdUtils;
+import org.dromara.common.json.utils.JsonUtils;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
 import org.dromara.server.common.util.CardDateUtils;
@@ -33,7 +34,10 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
@@ -50,7 +54,7 @@ import java.util.function.Supplier;
 @Service
 @RequiredArgsConstructor
 public class ConsumeRequestCheck {
-    private final ThreadPoolTaskExecutor taskExecutor;
+    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
     private final CommonCheck commonCheck;
     private final CardConsumeValidation cardConsumevalidation;
     private final BaseBusiness baseBusiness;
@@ -68,10 +72,13 @@ public class ConsumeRequestCheck {
             bo.setConsumeDate(consumeDate);
         }
         // 检查是否重复请求
+        long startTime = System.currentTimeMillis();
+        log.info("检查重复原始消费记录");
         R<ErrorInfo> result = checkRepeatOriginalId(bo);
         if (R.isError(result)) {
             return result;
         }
+        log.info("检查重复原始消费记录,耗时:{} ms ", System.currentTimeMillis()-startTime);
         // 获取餐类
         RemoteMealTypeVo remoteMealTypeVo = commonCheck.getMealType(consumeDate);
         if (ObjectUtil.isEmpty(remoteMealTypeVo)) {
@@ -81,6 +88,7 @@ public class ConsumeRequestCheck {
         }
         // 设置消费记录的当前餐类
         bo.setMealType(Long.valueOf(remoteMealTypeVo.getTypeId()));
+        log.info("获取餐类,耗时:{} ms ", System.currentTimeMillis()-startTime);
         // 如果消费记录标识为消费机消费则需要进行限次、限额和折扣验证
         XfCardLimitedVo xfCardLimitedVo = new XfCardLimitedVo();
         int statusFlag = bo.getStatusFlag();
@@ -88,22 +96,27 @@ public class ConsumeRequestCheck {
             || statusFlag == Integer.parseInt(ConsumeRecordTypeEnum.XFJXF_4.code())) {
 
             // 设备是否可以消费验证
+            long startTime1 = System.currentTimeMillis();
             result = checkTermLimitedAndOther(bo, useTermVo, userCardVo, remoteMealTypeVo);
+            log.info("检查是否设备是否可以消费,耗时:{} ms ", System.currentTimeMillis()-startTime1);
             if (R.isError(result)) {
                 return obtainResult(result);
             }
             // 卡片是否可以消费验证
+            long startTime2 = System.currentTimeMillis();
             result = cardConsumevalidation.cardValidation(bo, useTermVo, userCardVo, remoteMealTypeVo, xfCardLimitedVo, mapCardLimited);
+            log.info("检查是卡片是否可以消费,耗时:{} ms ", System.currentTimeMillis()-startTime2);
             if (R.isError(result)) {
                 return obtainResult(result);
             }
         }
-
+        log.info("检查是否可以消费,耗时:{} ms ", System.currentTimeMillis()-startTime);
         // 余额校验,余额不足不能交易,如果有折扣,则消费金额是以折扣金额为准的,所以余额验证放在最后
         result = checkWalletBalance(bo);
         if (R.isError(result)) {
             return result;
         }
+        log.info("检查钱包,耗时:{} ms ", System.currentTimeMillis()-startTime);
         RemoteVoConvert.INSTANCE.copyXfCardLimitedVo(cardLimitedVo, xfCardLimitedVo);
         return R.ok();
     }
@@ -173,6 +186,7 @@ public class ConsumeRequestCheck {
      * @return 如果原始消费记录ID重复,返回包含错误信息的响应对象;否则返回成功的响应对象
      */
     private R<ErrorInfo> checkRepeatOriginalId(ConsumptionBo bo) {
+
         String originalId = bo.getOriginalId();
         if (ObjectUtil.isEmpty(originalId)) {
             originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().intValue(),
@@ -194,15 +208,18 @@ public class ConsumeRequestCheck {
     }
 
     private R<ErrorInfo> checkTermLimitedAndOther(ConsumptionBo bo, XfTermVo termVo, RemoteCardVo userCardVo, RemoteMealTypeVo mealTypeVo) {
+        log.info("检查是否设备是否可以消费开始");
         // 1. 前置检查与数据初始化
         if (isSpecialScenario(termVo, mealTypeVo)) return R.ok();
+        long startTime = System.currentTimeMillis();
         initializeCardData(userCardVo, mealTypeVo);
-
+        log.info("初始化日卡数据,耗时:{} ms",System.currentTimeMillis()-startTime);
         // 2. 准备验证上下文
         TermConsumeValidationContext validationContext = TermConsumeValidationContext.create(bo, termVo, userCardVo, mealTypeVo);
-
+        log.info("准备验证上下文,耗时:{} ms",System.currentTimeMillis()-startTime);
         // 3. 执行异步验证链
         return executeTermValidationChain(validationContext);
+
     }
 
     /**
@@ -266,8 +283,8 @@ public class ConsumeRequestCheck {
      * @return 如果校验成功,返回表示成功的 R 对象;如果校验失败,返回包含错误信息的 R 对象
      */
     private R<ErrorInfo> executeTermValidationChain(TermConsumeValidationContext context) {
-        // 创建验证任务列表
-        List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
+        long startTime = System.currentTimeMillis();
+        List<Callable<R<ErrorInfo>>> validationTasks = new ArrayList<>();
 
         validationTasks.add(() -> validateSwipeInterval(context));
         validationTasks.add(() -> validateSingleLimit(context));
@@ -280,42 +297,128 @@ public class ConsumeRequestCheck {
         AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
 
         // 使用CountDownLatch跟踪任务完成
-        CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        // CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        // AtomicBoolean cancelled = new AtomicBoolean(false);
+        List<Future<R<ErrorInfo>>> futures = new ArrayList<>();
+        // 提交所有任务
+        for (Callable<R<ErrorInfo>> task : validationTasks) {
+            futures.add(threadPoolTaskExecutor.submit(task));
+        }
+        int taskIndex = 0;
+        try {
+            for (Future<R<ErrorInfo>> future : futures) {
+                long starTime = System.currentTimeMillis();
+                if (firstError.get() != null) {
+
+                    future.cancel(true); // 取消剩余任务
+                    continue;
+                }
 
-        // 提交所有验证任务
-        for (Supplier<R<ErrorInfo>> task : validationTasks) {
-            taskExecutor.execute(() -> {
                 try {
-                    R<ErrorInfo> result = task.get();
-                    // 如果发现错误且尚未设置错误结果
-                    if (result != null && R.isError(result) &&
-                        firstError.compareAndSet(null, result)) {
-                        // 取消其他任务(通过中断)
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                    // R<ErrorInfo> result = future.get(VALIDATION_TIMEOUT, TimeUnit.MILLISECONDS);
+                    R<ErrorInfo> result = future.get();
+                    // 发现错误,立即取消其他任务
+                    if (result != null && R.isError(result)) {
+
+                        if (firstError.compareAndSet(null, result)) {
+                            futures.forEach(f -> f.cancel(true));
+                        }
                     }
-                } catch (Exception e) {
-                    if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
-                        taskExecutor.getThreadPoolExecutor().getQueue().clear();
+                } catch (ExecutionException e) {
+                    log.error("{}验证执行异常", getTaskName(taskIndex), e);
+                    if (firstError.compareAndSet(null, createError(TradeStatusEnum.SysError))) {
+                        futures.forEach(f -> f.cancel(true));
                     }
                 } finally {
-                    latch.countDown();
+                    log.info("{}结束,耗时:{} ms", getTaskName(taskIndex), System.currentTimeMillis() - starTime);
+                    taskIndex++;
                 }
-            });
-        }
-
-        try {
-            // 等待所有任务完成或超时
-            //if (!latch.await(300, TimeUnit.MILLISECONDS)) {
-            //    return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
-            //}
-            latch.await();
-
-            // 返回第一个发现的错误,如果没有错误则返回成功
+            }
             return firstError.get() != null ? firstError.get() : R.ok();
+
         } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            return commonCheck.createError(TradeStatusEnum.SysError);
+            Thread.currentThread().interrupt(); // 保留中断状态
+            log.error("验证过程被中断", e);
+            return createError(TradeStatusEnum.SysError);
         }
+        //// 创建验证任务列表
+        //List<Supplier<R<ErrorInfo>>> validationTasks = new ArrayList<>();
+        //
+        //validationTasks.add(() -> validateSwipeInterval(context));
+        //validationTasks.add(() -> validateSingleLimit(context));
+        //validationTasks.add(() -> validateCardValidity(context));
+        //validationTasks.add(() -> validateCardType(context));
+        //validationTasks.add(() -> validateMealLimit(context));
+        //validationTasks.add(() -> validateDailyLimit(context));
+        //
+        //// 用于存储第一个错误结果
+        //AtomicReference<R<ErrorInfo>> firstError = new AtomicReference<>(null);
+        //
+        //// 使用CountDownLatch跟踪任务完成
+        //CountDownLatch latch = new CountDownLatch(validationTasks.size());
+        //
+        //// 提交所有验证任务
+        //for (Supplier<R<ErrorInfo>> task : validationTasks) {
+        //    threadPoolTaskExecutor.execute(() -> {
+        //        try {
+        //            R<ErrorInfo> result = task.get();
+        //            // 如果发现错误且尚未设置错误结果
+        //            if (result != null && R.isError(result) &&
+        //                firstError.compareAndSet(null, result)) {
+        //                // 取消其他任务(通过中断)
+        //                threadPoolTaskExecutor.getThreadPoolExecutor().getQueue().clear();
+        //            }
+        //        } catch (Exception e) {
+        //            if (firstError.compareAndSet(null, commonCheck.createError(TradeStatusEnum.SysError))) {
+        //                threadPoolTaskExecutor.getThreadPoolExecutor().getQueue().clear();
+        //            }
+        //        } finally {
+        //            latch.countDown();
+        //        }
+        //    });
+        //}
+        //
+        //try {
+        //    // 等待所有任务完成或超时
+        //    //if (!latch.await(300, TimeUnit.MILLISECONDS)) {
+        //    //    return commonCheck.createError(TradeStatusEnum.VALIDATION_TIMEOUT);
+        //    //}
+        //    latch.await();
+        //
+        //    // 返回第一个发现的错误,如果没有错误则返回成功
+        //    log.info("校验设备完成,耗时:{} ms",System.currentTimeMillis()-startTime);
+        //    return firstError.get() != null ? firstError.get() : R.ok();
+        //} catch (InterruptedException e) {
+        //    Thread.currentThread().interrupt();
+        //    return commonCheck.createError(TradeStatusEnum.SysError);
+        //}
+    }
+    //validationTasks.add(() -> validateSwipeInterval(context));
+    //validationTasks.add(() -> validateSingleLimit(context));
+    //validationTasks.add(() -> validateCardValidity(context));
+    //validationTasks.add(() -> validateCardType(context));
+    //validationTasks.add(() -> validateMealLimit(context));
+    //validationTasks.add(() -> validateDailyLimit(context));
+    private String getTaskName(int taskIndex) {
+        return switch (taskIndex) {
+            case 0 -> "消费间隔验证";
+            case 1 -> "单次验证";
+            case 2 -> "卡验证";
+            case 3 -> "卡类验证";
+            case 4 -> "餐类验证";
+            case 5 -> "日限制验证";
+            default -> "未知任务";
+        };
+    }
+    public R<ErrorInfo> createError(TradeStatusEnum status) {
+        return createError(status, status.getName());
+    }
+    public R<ErrorInfo> createError(TradeStatusEnum status, String message) {
+        return R.fail(new ErrorInfo(
+            400,
+            status.toString(),
+            message,
+            status.getName()));
     }
 
     private R<ErrorInfo> validateSwipeInterval(TermConsumeValidationContext ctx) {
@@ -343,6 +446,7 @@ public class ConsumeRequestCheck {
 
     private R<ErrorInfo> validateCardType(TermConsumeValidationContext ctx) {
         int mask = 1 << (ctx.getCardType() - 1);
+        log.info ("卡类是否消费验证");
         return (mask & ctx.getTermCardType()) == 0 ?
             commonCheck.createError(TradeStatusEnum.CardTypeLimit) : null;
     }

+ 176 - 0
ruoyi-server/ruoyi-server-face/pom.xml

@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-server</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-server-face</artifactId>
+
+    <description>
+        ruoyi-service-face 一卡通人脸服务
+    </description>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-nacos</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-sentinel</artifactId>
+        </dependency>
+
+        <!-- RuoYi Common Log -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-log</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-dict</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-doc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-dubbo</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-seata</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-idempotent</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-tenant</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-translation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-sensitive</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-encrypt</artifactId>
+        </dependency>
+
+        <!-- RuoYi Api System -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-system</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-backstage</artifactId>
+        </dependency>
+         <dependency>
+            <groupId>cn.com.kingbase</groupId>
+            <artifactId>kingbase8</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-server-base</artifactId>
+            <version>2.2.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-api-consume</artifactId>
+            <version>2.2.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.arcsoft.face</groupId>
+            <artifactId>arcsoft-sdk-face</artifactId>
+            <version>4.1.1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.14</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.27</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 26 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/RuoYiServerFaceApplication.java

@@ -0,0 +1,26 @@
+package org.dromara.server.hik;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 系统模块
+ *
+ * @author ruoyi
+ */
+@EnableDubbo
+@EnableAsync
+@EnableScheduling
+@SpringBootApplication
+public class RuoYiServerFaceApplication {
+    public static void main(String[] args) {
+        SpringApplication application = new SpringApplication(RuoYiServerFaceApplication.class);
+        application.setApplicationStartup(new BufferingApplicationStartup(2048));
+        application.run(args);
+        System.out.println("(♥◠‿◠)ノ゙  人脸服务启动成功   ლ(´ڡ`ლ)゙  ");
+    }
+}

+ 53 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/ErrCodeConstants.java

@@ -0,0 +1,53 @@
+package org.dromara.server.hik.constant;
+
+/**
+ * 错误码
+ * <p>
+ * 错误码定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+public interface ErrCodeConstants {
+    /**
+     * ok
+     */
+    Integer OK = 0;
+
+    /**
+     * ok
+     */
+    Integer YES = 1;
+
+    /**
+     * 设备繁忙
+     */
+    Integer DEVICE_BUSY = 2;
+
+    /**
+     * 设备错误
+     */
+    Integer DEVICE_ERROR = 3;
+
+    /**
+     * 无效操作
+     */
+    Integer INVALID_OPERATION = 4;
+
+    /**
+     * 无效XML格式
+     */
+    Integer INVALID_XML_FORMAT = 5;
+
+    /**
+     * 无效XML内容
+     */
+    Integer INVALID_XML_CONTENT = 6;
+
+    /**
+     * 设备需要重启
+     */
+    Integer REBOOT_REQUIRED = 7;
+}

+ 40 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikApiConstants.java

@@ -0,0 +1,40 @@
+package org.dromara.server.hik.constant;
+
+/**
+ * api接口
+ * <p>
+ * 访问设备的api接口定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+public interface HikApiConstants {
+
+    /**
+     * POST 向设备下发人员、卡片、照片信息
+     */
+    String SEND_EMP_INFO = "/ISAPI/AccessControl/userInfoAndRight/setup?format=json";
+
+    /**
+     * POST 从设备查询所有 人员信息
+     */
+    String QUERY_EMP_ALL = "/ISAPI/AccessControl/userInfoAndRight/searchList?format=json";
+
+    /**
+     * GET 给设备配置http监听的IP和端口
+     */
+    String SET_HTTP_HOSTS = "/ISAPI/Event/notification/httpHosts";
+
+
+    /**
+     * PUT 确认交易记录; 应答 ConsumptionEvent
+     */
+   String Consumption_Event_confirm = "/ISAPI/Consume/consumptionEventConfirm?format=json";
+
+    /**
+     * PUT 确认交易记录; 应答TransactionRecordEvent
+     */
+    String TRANSACTION_RECORD_Event_confirm = "/ISAPI/Consume/transactionRecordEventConfirm?format=json";
+}

+ 16 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikDefaultConstants.java

@@ -0,0 +1,16 @@
+package org.dromara.server.hik.constant;
+
+/**
+ * 海康默认配置
+ * <p>
+ * 海康默认配置
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-23
+ * @since JDK17
+ */
+public interface HikDefaultConstants {
+
+    String EMP_END_TIME = "2037-12-31 23:59:59";
+}

+ 12 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/constant/HikEventTypeConstant.java

@@ -0,0 +1,12 @@
+package org.dromara.server.hik.constant;
+
+public interface HikEventTypeConstant {
+
+    // 交易记录事件
+    String TransactionRecordEvent = "TransactionRecordEvent";
+
+    // 交易请求事件   minor:transactionConfirmingRequest 为 交易确认请求事件 minor:transactionPreprocessingRequest 为 交易预处理请求事件
+    String ConsumptionEvent = "ConsumptionEvent";
+
+    String HeartBeat = "heartBeat";
+}

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

@@ -0,0 +1,482 @@
+package org.dromara.server.hik.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.Part;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+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.domain.model.ErrorResult;
+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.dto.DeviceDto;
+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;
+import org.springframework.util.StreamUtils;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 海康消费机
+ * <p>
+ * 海康消费机对接测试控制器
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/hik/test")
+@SaIgnore
+@Slf4j
+public class TestController {
+    private final ISendDeviceService sendDeviceService;
+
+    private final EventHandleRouter eventHandleRouter;
+
+    private final HandleTask handleTask;
+
+    @DubboReference
+    private final RemoteConsumeService remoteConsumeService;
+
+    //region 设备监听相关
+    /**
+     * 设置指定设备监听服务地址
+     * @param dto 设备信息
+     * @return 设置结果
+     */
+    @PostMapping("/set/hosts")
+    public R<Void> setHosts(@RequestBody DeviceDto dto) {
+        return sendDeviceService.setHttpHostByDto(dto);
+    }
+
+    /**
+     * 设置所有终端设备的监听服务地址
+     * @return 设置结果
+     */
+    @PostMapping("/set/hosts/all")
+    public R<Void> setHosts() {
+        return sendDeviceService.setHttpHostAll();
+    }
+
+    /**
+     * 设置指定终端设备的监听服务地址。
+     *
+     * @param termNo 终端编号,用于标识目标设备
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @PostMapping("/set/hosts/{termNo}")
+    public R<Void> setHosts(@PathVariable("termNo") Long termNo) {
+        return sendDeviceService.setHttpHostByTermNo(termNo);
+    }
+    //endregion
+
+    /**
+     * 查询设备上所有人员信息
+     * @param dto 设备信息
+     * @return 查询结果
+     */
+    @PostMapping("/query/emp/all")
+    public R<Void> queryEmpAll(@RequestBody QueryDto dto) {
+        return sendDeviceService.queryBatchEmpFormDevice(dto);
+    }
+
+    //region 人员删除相关
+    /**
+     * 删除指定终端设备上的指定员工信息。
+     * <p>
+     * 根据终端编号和用户编号,从对应的设备中删除指定员工的信息。
+     *
+     * @param termNo 终端编号,用于标识目标设备
+     * @param userId 用户编号,用于标识需要删除的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/del/{termNo}/{userId}")
+    public R<Void> deleteEmpFromDevice(@PathVariable("termNo") Long termNo,@PathVariable("userId") Long userId) {
+        return sendDeviceService.deleteEmpFromDevice(termNo,userId);
+    }
+
+    /**
+     * 删除指定终端设备上的所有员工信息。
+     * <p>
+     * 根据终端编号,从对应的设备中删除
+        return sendDeviceService.deleteEmpFromDevice(termNo);
+    }
+
+    /**
+     * 删除指定终端设备上的员工信息。
+     * <p>
+     * 根据终端编号,从对应的设备中删除员工信息。该方法通过HTTP POST请求触发,
+     * 并将终端编号作为路径变量传递。
+     *
+     * @param termNo 终端编号,用于标识目标设备
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/del/{termNo}")
+    public R<Void> deleteEmpByFromDevice(@PathVariable("termNo") Long termNo) {
+        return sendDeviceService.deleteEmpFromDevice(termNo);
+    }
+
+    /**
+     * 从所有终端设备上删除指定员工信息
+     * 根据终端编号和用户编号,从所有的设备中删除指定员工的信息。
+     *
+      * @param userId 用户编号,用于标识需要删除的员工信息
+     *
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/del/all/{userId}")
+    public R<Void> deleteAllEmpByFromDevice(@PathVariable("userId") Long userId) {
+        return sendDeviceService.deleteOneEmpFromDevice(userId);
+    }
+
+    /**
+     * 删除所有设备上的所有员工
+     *
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/del/all")
+    public R<Void> deleteAllEmpByFromDevice() {
+        return sendDeviceService.deleteEmpFromDevice();
+    }
+    //endregion
+
+    //region 人员信息上传有关
+    /**
+     * 上传员工信息到指定设备。
+     * <p>
+     * 该方法接收包含员工信息和设备信息的数据传输对象,将员工信息上传至指定设备。
+     *
+     * @param uploadEmpDto 包含员工信息和设备信息的数据传输对象
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @PostMapping("/emp/upload")
+    public R<Void> uploadEmpToDevice(@RequestBody UploadEmpDto uploadEmpDto) {
+        return sendDeviceService.upLoadEmpToDevice(uploadEmpDto);
+    }
+
+    /**
+     * 上传指定员工信息到指定设备。
+     * <p>
+     * 根据终端编号和用户编号,将对应的员工信息上传至指定设备。
+     *
+     * @param termNo 终端编号,用于标识目标设备
+     * @param userId 用户编号,用于标识需要上传的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/upload/{termNo}/{userId}")
+    public R<Void> upLoadEmpToDevice(@PathVariable("termNo") Long termNo,@PathVariable("userId") Long userId){
+        return sendDeviceService.upLoadEmpToDevice(termNo, userId);
+    }
+
+    /**
+     * 上传所有员工信息到指定设备。
+     * <p>
+     * 根据终端编号,将员工信息上传至指定设备。该方法通过HTTP GET请求触发,
+     * 并将终端编号作为路径变量传递。
+     *
+     * @param termNo 终端编号,用于标识目标设备
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/upload/all/{termNo}")
+    public R<Void> upLoadAllEmpToDevice(@PathVariable("termNo") Long termNo){
+        return sendDeviceService.upLoadEmpToDevice(termNo);
+    }
+
+    /**
+     * 上传指定员工信息到所有设备。
+     * <p>
+     * 根据用户编号,将对应的员工信息上传至设备。该方法通过HTTP GET请求触发,
+     * 并将用户编号作为路径变量传递。
+     *
+     * @param userId 用户编号,用于标识需要上传的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/upload/{userId}")
+    public R<Void> upLoadEmpToDevice(@PathVariable("userId") Long userId){
+        return sendDeviceService.upLoadEmpToAllDevice(userId);
+    }
+
+    /**
+     *  通常不要调用此接口,不能同时给所有设备上传员工信息,只能给指定设备上传员工信息
+     * 上传所有员工信息到所有设备。
+     * <p>
+     * 该方法通过HTTP GET请求触发,将系统中的所有员工信息上传至所有关联的设备。
+     * 具体上传逻辑由服务层实现,返回操作结果的状态信息。
+     *
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @GetMapping("/emp/upload/all")
+    public R<Void> upLoadAllEmpToAllDevice(){
+        return sendDeviceService.upLoadEmpToDevice(true);
+    }
+    //endregion
+
+    /**
+     * 删除指定设备上某个用户的所有卡片信息。
+     * <p>
+     * 根据用户编号和设备信息,删除该用户在指定设备上的所有关联卡片数据。
+     *
+     * @param userNo 用户编号,用于标识需要删除卡片信息的用户
+     * @param device 设备信息,包含设备的详细配置数据
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    @PostMapping("/card/deleteAll/{userNo}")
+    public R<Void> deleteAllCardByUserNo(@PathVariable("userNo") String userNo, @RequestBody DeviceDto device) {
+        return sendDeviceService.deleteAllCardByUserNo(device,userNo);
+    }
+
+    /**
+     * 接收并处理发送到服务器的设备推送数据。
+     * 该方法处理不同类型的 content,包括 multipart/form-data 和 application/json。
+     * 对于 multipart/form-data,它分别处理 JSON 和 image/jpeg 部分。
+     * 对于 application/json,它直接处理 JSON 负载。
+     * 如果内容类型不受支持,则记录警告并返回 null。
+     *
+     * @param request 包含传入数据和元数据的 HttpServletRequest 对象。
+     *                它必须包含适合处理的内容类型和有效负载。
+     * @return 一个包含由 eventHandleRouter 路由的处理数据的 Map<String, Object>。
+     *         如果在处理过程中发生错误或内容类型不受支持,则返回 null。
+     */
+    @PostMapping(("/info"))
+    public Map<String, Object> receiveDevicePushData(HttpServletRequest request) {
+        try {
+            // 判断请求类型
+            List<String> jsonListString = new ArrayList<>();
+            List<FileContent> fileByteList = new ArrayList<>();
+            String s = request.getContentType().toLowerCase();
+            if (s.contains("multipart/form-data")) {
+                Collection<Part> parts = request.getParts();
+
+                for (Part part : parts) {
+                    String contentType = part.getContentType().toLowerCase();
+                    InputStream inputStream = part.getInputStream();
+                    switch (contentType) {
+                        case "application/json" -> {
+                            String jsonData = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
+                            jsonListString.add(jsonData);
+                        }
+                        case "image/jpeg" -> {
+                            // 流转字节数组
+                            byte[] bytes = IOUtils.toByteArray(inputStream);
+                            fileByteList.add(new FileContent(".jpg", bytes));
+                            /*String image_dir_path = "C:\\image\\";
+                            File image_dir = new File(image_dir_path);
+                            if (!image_dir.exists()) {
+                                image_dir.mkdir();
+                            }
+                            String file_name = UUID.randomUUID().toString();
+                            File image_file = new File(image_dir_path + file_name + ".jpg");
+
+                            FileUtils.copyInputStreamToFile(inputStream, image_file);*/
+                        }
+                        default -> {
+                            log.warn("out contentType : multipart/form-data; unknown contentType " + contentType + " 跳过");
+                        }
+                    }
+
+                    /*MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+
+                    multipartRequest.getParameterMap().forEach((fieldName, values) -> {
+                        System.out.println("字段 [" + fieldName + "] 值: " + String.join(", ", values));
+                    });
+
+                    multipartRequest.getFileMap().forEach((fieldName, file) -> {
+                        if (!file.isEmpty()) {
+                            String fileName = file.getOriginalFilename();
+                            try {
+                                file.transferTo(Path.of("f:/uploads", fileName));
+                            } catch (IOException e) {
+                                throw new RuntimeException("文件保存失败: " + fileName, e);
+                            }
+                        }
+                    });*/
+
+                }
+            }else if(s.contains("application/json")){
+                    // 处理 JSON 请求
+                    String jsonData = handleJsonRequest(request);
+                    jsonListString.add(jsonData);
+            }else{
+                log.warn("unknown out contentType " + s);
+                return null;
+            }
+            return eventHandleRouter.route(jsonListString,fileByteList);
+            } catch(Exception e){
+                e.printStackTrace();
+                log.error(e.getMessage(), e);
+            }
+        return null;
+    }
+
+    /**
+     * 根据提供的消费信息处理海康消费机的消费请求。
+     * 该方法处理传入的请求,验证消费详情,
+     * 并执行必要的操作以完成消费请求。
+     *
+     * @param mapConsumeInfo 包含消费详细信息的键值对的 Map
+     * @return 表示消费请求处理结果的 Object
+     */
+    @PostMapping("/requestConsume")
+    public Object requestConsume(@RequestBody Map<String, Object> mapConsumeInfo) {
+        RemoteConsumeBo remoteBo = new RemoteConsumeBo();
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("userNo"))){
+            remoteBo.setUserNo(Long.valueOf(mapConsumeInfo.get("userNo").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("factoryId"))){
+            remoteBo.setFactoryId(Long.valueOf(mapConsumeInfo.get("factoryId").toString()));
+        } else {
+            remoteBo.setFactoryId(0L);
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("consumeDate"))){
+            remoteBo.setConsumeDate(DateUtil.parseDateTime(mapConsumeInfo.get("consumeDate").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("termRecordId"))){
+            remoteBo.setTermRecordId(Long.valueOf(mapConsumeInfo.get("termRecordId").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("consumeMoney"))){
+            remoteBo.setConsumeMoney(new BigDecimal(mapConsumeInfo.get("consumeMoney").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("termMac"))){
+            remoteBo.setTermMac(mapConsumeInfo.get("termMac").toString());
+        }
+        remoteBo.setCardNo(0L);
+        remoteBo.setStatusFlag(4);
+        remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
+
+        RemoteResultDto result = remoteConsumeService.dealHikRequestConsume(remoteBo);
+        R<ErrorInfo> errorInfo = result.getErrorInfo();
+        if (R.isError(errorInfo)) {
+            ErrorResult errResult = new ErrorResult();
+            errResult.setStatusCode(HttpStatus.NOT_FOUND.value());
+            errResult.setMessage(errorInfo.getMsg());
+            errResult.getErrors().add(errorInfo.getData());
+            return new ResponseEntity<Object>(errResult, null, HttpStatus.NOT_FOUND);
+        }
+        // if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("qrCode"))){
+        //     remoteBo.setUserNo(Long.valueOf(mapConsumeInfo.get("qrCode").toString()));
+        // }
+        return result.getUpdatedRemoteBo();
+    }
+
+    /**
+     * 将消费记录上传到服务器。
+     *
+     * @param mapConsumeInfo 包含要上传的消费信息的 Map。
+     *                       键表示字段名称,值表示相应的数据。
+     * @return 表示上传操作结果的 Object。
+     *         这可能是成功消息、错误详情或其他相关信息。
+     */
+    @PostMapping("/uploadRecord")
+    public Object uploadConsumeRecord(@RequestBody Map<String, Object> mapConsumeInfo) {
+        RemoteConsumeBo remoteBo = new RemoteConsumeBo();
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("userNo"))){
+            remoteBo.setUserNo(Long.valueOf(mapConsumeInfo.get("userNo").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("factoryId"))){
+            remoteBo.setFactoryId(Long.valueOf(mapConsumeInfo.get("factoryId").toString()));
+        } else {
+            remoteBo.setFactoryId(0L);
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("consumeDate"))){
+            remoteBo.setConsumeDate(DateUtil.parseDateTime(mapConsumeInfo.get("consumeDate").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("termRecordId"))){
+            remoteBo.setTermRecordId(Long.valueOf(mapConsumeInfo.get("termRecordId").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("consumeMoney"))){
+            remoteBo.setConsumeMoney(new BigDecimal(mapConsumeInfo.get("consumeMoney").toString()));
+        }
+        if(ObjectUtil.isNotEmpty(mapConsumeInfo.get("termMac"))){
+            remoteBo.setTermMac(mapConsumeInfo.get("termMac").toString());
+        }
+        remoteBo.setCardNo(0L);
+        remoteBo.setStatusFlag(4);
+        remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
+        remoteBo.setRecordStatus(364L);
+        remoteBo.setRecordId(0L);
+
+        RemoteResultDto result = remoteConsumeService.dealHikUploadRecord(remoteBo);
+        R<ErrorInfo> errorInfo = result.getErrorInfo();
+        if (R.isError(errorInfo)) {
+            ErrorResult errResult = new ErrorResult();
+            errResult.setStatusCode(HttpStatus.NOT_FOUND.value());
+            errResult.setMessage(errorInfo.getMsg());
+            errResult.getErrors().add(errorInfo.getData());
+            return new ResponseEntity<Object>(errResult, null, HttpStatus.NOT_FOUND);
+        }
+         return result.getUpdatedRemoteBo();
+    }
+
+    /**
+     * 将离线记录上传到服务器。
+     * 该方法处理提供的消费信息并负责上传过程。
+     *
+     * @param mapConsumeInfo 包含要上传的消费信息的 Map
+     * @return 表示上传操作结果的 Object
+     */
+    @PostMapping("/offlineRecord")
+    public Object uploadOfflineRecord(@RequestBody Map<String, Object> mapConsumeInfo) {
+
+
+        return null;
+    }
+
+    // 处理 JSON 请求
+    private String handleJsonRequest(HttpServletRequest request) throws IOException {
+
+        if (request.getContentType() != null
+                && request.getContentType().contains("application/json")) {
+
+            try (BufferedReader reader = request.getReader()) {
+                StringBuilder requestData = new StringBuilder();
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    requestData.append(line);
+                }
+
+                return requestData.toString();
+                // 使用任意 JSON 库解析
+                //                JSONObject jsonObject = JSONUtil.parseObj(requestData.toString());
+                //                System.out.println("收到 JSON 数据:");
+                //                System.out.println(jsonObject.toString());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 手动执行定时任务
+     */
+    @PostMapping("/executeTask")
+    public void executeTask() {
+        handleTask.handleTransactionRecordEventTask();
+    }
+
+}

+ 106 - 0
ruoyi-server/ruoyi-server-face/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));
+    }
+}

+ 205 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfConsumeDetail.java

@@ -0,0 +1,205 @@
+package org.dromara.server.hik.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 消费明细对象 t_xf_consumeDetail
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_xf_consumeDetail")
+public class XfConsumeDetail extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消费记录Id,主键
+     */
+    @TableId(value = "consume_id")
+    private String consumeId;
+
+    /**
+     * 原始记录Id,主键
+     */
+    private String originalId;
+
+    /**
+     * 记录Id
+     */
+    private Long recordId;
+
+    /**
+     * 人员Id
+     */
+    private Long userId;
+
+    /**
+     * 学/工号
+     */
+    private String userNumb;
+
+    /**
+     * 用户姓名
+     */
+    private String realName;
+
+    /**
+     * 部门Id
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 消费日期
+     */
+    private Date consumeDate;
+
+    /**
+     * 消费金额
+     */
+    private BigDecimal consumeMoney;
+
+    /**
+     * 卡流水号
+     */
+    private Long cardNo;
+
+    /**
+     * 物理卡号
+     */
+    private Long factoryId;
+
+    /**
+     * 卡上余额
+     */
+    private BigDecimal cardValue;
+
+    /**
+     * 卡使用次数
+     */
+    private Long cardCount;
+
+    /**
+     * 消费账户金额
+     */
+    private BigDecimal consumeBalance;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 机器流水号
+     */
+    private Long termRecordId;
+
+    /**
+     * 是否已处理(和灰记录处理有关)
+     */
+    private Long analysisFlag;
+
+    /**
+     * 状态标识(记录消费类型?)
+     */
+    private Long statusFlag;
+
+    /**
+     * 营业员Id
+     */
+    private Long operatorId;
+
+    /**
+     * 营业员名称
+     */
+    private String operatorName;
+
+    /**
+     * 结算账户Id
+     */
+    private Long accountId;
+
+    /**
+     * 结算账户名称
+     */
+    private String accountName;
+
+    /**
+     * 房间Id
+     */
+    private Long roomId;
+
+    /**
+     * 房间名称
+     */
+    private String roomName;
+
+    /**
+     * 钱包类型
+     */
+    private String bagType;
+
+    /**
+     * 餐类Id
+     */
+    private Long mealType;
+
+    /**
+     * 餐类名称
+     */
+    private String mealName;
+
+    /**
+     * 是否发送短信,见 sys_yes_no字典类别
+     */
+    private String smsSend;
+
+    /**
+     * 消费记录标志位?
+     */
+    private Long recordStatus;
+
+    /**
+     * 同步标志,0-未同步,1-已同步
+     */
+    private Long syncStatus;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+    /**
+     * 针对错扣补款的消费记录的id
+     */
+    private String detailId;
+
+}

+ 171 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfConsumeDetailOriginal.java

@@ -0,0 +1,171 @@
+package org.dromara.server.hik.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 原始消费记录对象 t_xf_consumeDetailOriginal
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_xf_consumeDetailOriginal")
+public class XfConsumeDetailOriginal extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 原始记录Id,主键
+     */
+    @TableId(value = "original_id")
+    private String originalId;
+
+    /**
+     * 记录Id,自增长,唯一id值,用于应答给消费设备
+     */
+    private Long recordId;
+
+    /**
+     * 人员Id
+     */
+    private Long userId;
+
+    /**
+     * 学/工号
+     */
+    private String userNumb;
+
+    /**
+     * 用户姓名
+     */
+    private String realName;
+
+    /**
+     * 部门Id
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 消费日期
+     */
+    private Date consumeDate;
+
+    /**
+     * 消费金额
+     */
+    private BigDecimal consumeMoney;
+
+    /**
+     * 卡流水号
+     */
+    private Long cardNo;
+
+    /**
+     * 物理卡号
+     */
+    private Long factoryId;
+
+    /**
+     * 卡上余额
+     */
+    private BigDecimal cardValue;
+
+    /**
+     * 卡使用次数
+     */
+    private Long cardCount;
+
+    /**
+     * 消费账户金额
+     */
+    private BigDecimal consumeBalance;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 机器流水号
+     */
+    private Long termRecordId;
+
+    /**
+     * 是否已处理(和灰记录处理有关)
+     */
+    private Long analysisFlag;
+
+    /**
+     * 消费记录标志
+     */
+    private Long dataFlag;
+
+    /**
+     * 状态标识(记录消费类型?)
+     */
+    private Long statusFlag;
+
+    /**
+     * 防伪验证码(记录消费模式的中文信息)
+     */
+    private String digitalSign;
+
+    /**
+     * 水控金额
+     */
+    private BigDecimal waterValue;
+
+    /**
+     * 水控历史余额
+     */
+    private BigDecimal waterHistoryValue;
+
+    /**
+     * 水控当天总和
+     */
+    private BigDecimal waterDaySum;
+
+    /**
+     * 水控错误余额
+     */
+    private BigDecimal waterErrValue;
+
+    /**
+     * 水控错误消费金额
+     */
+    private BigDecimal waterErrMoney;
+
+    /**
+     * 营业员Id
+     */
+    private Long operatorId;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+
+}

+ 88 - 0
ruoyi-server/ruoyi-server-face/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;
+
+
+
+
+}

+ 367 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/XfTerm.java

@@ -0,0 +1,367 @@
+package org.dromara.server.hik.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 消费设备对象 t_xf_term
+ *
+ * @author bing
+ * @date 2024-08-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_xf_term")
+public class XfTerm extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备Id,主键
+     */
+    @TableId(value = "term_id")
+    private Long termId;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 所属餐厅Id
+     */
+    private Long roomId;
+
+    /**
+     * 结算账户Id
+     */
+    private Long accountId;
+
+    /**
+     * 设备类型,见XF_TermType字典类型
+     */
+    private String termType;
+
+    /**
+     * 设备二维码
+     */
+    private String qrCode;
+
+    /**
+     * 工作站Id
+     */
+    private Long stationId;
+
+    /**
+     * 扣费类型,见XF_ConsumeType字典类型
+     */
+    private String consumeType;
+
+    /**
+     * 是否自动下载参数,见sys_yew_no字典项
+     */
+    private String autoDown;
+
+    /**
+     * 应用系统,见YYXT数据字典类别
+     */
+    private String useType;
+
+    /**
+     * 允许使用的卡类
+     */
+    private Long cardType;
+
+    /**
+     * 设备IP
+     */
+    private String termIp;
+
+    /**
+     * 设备MAC地址
+     */
+    private String termMac;
+
+    /**
+     * 设备通讯端口
+     */
+    private Long commPort;
+
+    /**
+     * 子网掩码
+     */
+    private String mask;
+
+    /**
+     * 服务器IP
+     */
+    private String serverIp;
+
+    /**
+     * 服务器端口
+     */
+    private Long serverPort;
+
+    /**
+     * 网关IP
+     */
+    private String gatewayIp;
+
+    /**
+     * 心跳间隔,以秒为单位
+     */
+    private Long beatInterval;
+
+    /**
+     * 通讯超时时间,以100毫秒为单位
+     */
+    private Long timeout;
+
+    /**
+     * 允许脱机时间
+     */
+    private Long offlineTime;
+
+    /**
+     * 高级参数
+     */
+    private String advParam;
+
+    /**
+     * 设备参数
+     */
+    private String posParam;
+
+    /**
+     * 费率参数
+     */
+    private String rateParam;
+
+    /**
+     * 消费工作模式,见XF_WorkMode字典类别
+     */
+    private String workMode;
+
+    /**
+     * 开机模式,0-不需要营业员开机,1-需要营业员开机
+     */
+    private String openMode;
+
+    /**
+     * 卡上最大金额
+     */
+    private BigDecimal maxCardMoney;
+
+    /**
+     * 定值消费金额
+     */
+    private BigDecimal constantValue;
+
+    /**
+     * 编号0代表的金额
+     */
+    private BigDecimal rationZero;
+
+    /**
+     * 编号1代表的金额
+     */
+    private BigDecimal rationOne;
+
+    /**
+     * 编号2代表的金额
+     */
+    private BigDecimal rationTwo;
+
+    /**
+     * 编号3代表的金额
+     */
+    private BigDecimal rationThree;
+
+    /**
+     * 编号4代表的金额
+     */
+    private BigDecimal rationFour;
+
+    /**
+     * 编号5代表的金额
+     */
+    private BigDecimal rationFive;
+
+    /**
+     * 编号6代表的金额
+     */
+    private BigDecimal rationSix;
+
+    /**
+     * 编号7代表的金额
+     */
+    private BigDecimal rationSeven;
+
+    /**
+     * 编号8代表的金额
+     */
+    private BigDecimal rationEight;
+
+    /**
+     * 编号9代表的金额
+     */
+    private BigDecimal rationNine;
+
+    /**
+     * 每天最大消费次数,0-不限
+     */
+    private BigDecimal dayCount;
+
+    /**
+     * 每天最大消费金额
+     */
+    private BigDecimal dayMoney;
+
+    /**
+     * 每餐最大消费次数,0-不限
+     */
+    private Long mealCount;
+
+    /**
+     * 单次最大消费金额
+     */
+    private BigDecimal singleMoney;
+
+    /**
+     * 早餐消费金额
+     */
+    private BigDecimal breakfastMoney;
+
+    /**
+     * 午餐消费金额
+     */
+    private BigDecimal lunchMoney;
+
+    /**
+     * 晚餐消费金额
+     */
+    private BigDecimal supperMoney;
+
+    /**
+     * 夜宵消费金额
+     */
+    private BigDecimal nightMoney;
+
+    /**
+     * 早餐开始时间
+     */
+    private Long breakfastBegin;
+
+    /**
+     * 早餐结束时间
+     */
+    private Long breakfastEnd;
+
+    /**
+     * 午餐开始时间
+     */
+    private Long lunchBegin;
+
+    /**
+     * 午餐结束时间
+     */
+    private Long lunchEnd;
+
+    /**
+     * 晚餐开始时间
+     */
+    private Long supperBegin;
+
+    /**
+     * 晚餐结束时间
+     */
+    private Long supperEnd;
+
+    /**
+     * 宵夜开始时间
+     */
+    private Long nightBegin;
+
+    /**
+     * 宵夜结束时间
+     */
+    private Long nightEnd;
+
+    /**
+     * 两次刷卡间隔,0-不限制
+     */
+    private Long swipeInterval;
+
+    /**
+     * 是否启用卡片有效限制,0-禁用 1-启用
+     */
+    private String termValidity;
+
+    /**
+     * 设备记录流水号
+     */
+    private Long recordId;
+
+    /**
+     * 数据上传时间
+     */
+    private Date uploadTime;
+
+    /**
+     * 黑名单下载时间
+     */
+    private Date blackDownTime;
+
+    /**
+     * 最后校时时间
+     */
+    private Date lastCheck;
+
+    /**
+     * 重启时间
+     */
+    private String rebootTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+    @TableField(exist = false)
+    private Long roomName;
+
+    @TableField(exist = false)
+    private Long accountName;
+
+    /*消费机管理账号*/
+    private String adminName;
+
+    /*消费机管理密码*/
+    private String adminPwd;
+
+    /*消费机品牌*/
+    private String brand;
+
+
+}

+ 89 - 0
ruoyi-server/ruoyi-server-face/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;
+
+
+}

+ 74 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/DeviceDto.java

@@ -0,0 +1,74 @@
+package org.dromara.server.hik.domain.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 设备信息数据传输对象
+ * <p>
+ * 交互设备信息数据传输对象定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Data
+public class DeviceDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 585302132129673041L;
+
+    /**
+     * 设备编号
+     */
+    private Integer termNo;
+
+    /**
+     * 设备MAC 海康设备 必须要有
+     */
+    private String termMac;
+
+    /**
+     * 管理账号
+     */
+    @NotBlank(message = "管理账号不能为空")
+    private String adminName;
+
+    /**
+     * 管理密码
+     */
+    @NotBlank(message = "管理密码不能为空")
+    private String adminPwd;
+
+    /**
+     * 通讯IP
+     */
+    @NotBlank(message = "设备通讯IP不能为空")
+    private String deviceIp;
+
+    /**
+     * 通讯端口
+     */
+    @NotNull(message = "设备通讯端口不能为空")
+    private Integer devicePort;
+
+    /**
+     * 服务监听IP
+     */
+    private String serverIp;
+
+    /**
+     * 服务监听端口
+     */
+    private Integer serverPort;
+
+    /**
+     * 服务监听地址
+     */
+    private String serverUrl;
+}

+ 44 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/QueryDto.java

@@ -0,0 +1,44 @@
+package org.dromara.server.hik.domain.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 查询条件传输对象
+ * <p>
+ * 向设备请求查询时的查询参数设定
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class QueryDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 搜索记录标识
+     */
+    private String searchID;
+
+    /**
+     * 查询起始值
+     */
+    private Integer pageNo;
+
+    /**
+     * 查询的记录条数
+     */
+    private Integer pageSize;
+
+    /**
+     * 查询设备信息
+     */
+    private DeviceDto device;
+}

+ 54 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/UploadEmpDto.java

@@ -0,0 +1,54 @@
+package org.dromara.server.hik.domain.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.server.hik.domain.dto.base.CardDto;
+import org.dromara.server.hik.domain.dto.base.EmpInfoDto;
+import org.dromara.server.hik.domain.dto.base.FaceDto;
+import org.dromara.server.hik.domain.dto.base.ValidDto;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 下发人员信息数据传输对象
+ * <p>
+ * 向设备下发的人员信息数据传输对象定义。数据格式包括了下发的设备信息、待下发的人员信息
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-23
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class UploadEmpDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 8195137760207081790L;
+
+    /**
+     * 下发设备信息
+     */
+    private DeviceDto device;
+
+    /**
+     * 下发人员信息
+     */
+    private EmpInfoDto employee;
+
+    /**
+     * 下发人员有效期
+     */
+    private ValidDto valid;
+
+    /**
+     * 下发卡片信息
+     */
+    private CardDto card;
+
+    /**
+     * 下发人脸信息
+     */
+    private FaceDto face;
+
+}

+ 33 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/UserInfoDto.java

@@ -0,0 +1,33 @@
+package org.dromara.server.hik.domain.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.server.hik.domain.dto.base.EmpInfoDto;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 人员数据数据传输类
+ * <p>
+ * 定义下发至设备的人员数据传输对象
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @see EmpInfoDto
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class UserInfoDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -2638389177956051530L;
+
+    /**
+     * 下发到至设备的人员信息对象
+     */
+    private EmpInfoDto UserInfoAndRight;
+
+}

+ 41 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/CardDto.java

@@ -0,0 +1,41 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.server.hik.enums.CardTypeEnum;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 卡片数据传输对象
+ * <p>
+ * 单张卡片的数据传输对象定义类
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @see Object
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class CardDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -2123963727462678121L;
+
+    /**
+     * 物理卡号
+     */
+    private String cardNo;
+
+    /**
+     * 是否删除卡片 为true时删除设备上员工此物理卡号对应的卡片
+     */
+    private Boolean deleteCard = Boolean.FALSE;
+
+    /**
+     * 卡类型 默认为普通卡(normalCard)
+     */
+    private String cardType = CardTypeEnum.NORMAL_CARD.getCode();
+}

+ 37 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/CardListDto.java

@@ -0,0 +1,37 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 卡片数据传输对象
+ * <p>
+ * 多张张卡片的数据传输对象定义类
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @see Object
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class CardListDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -2123963727462678121L;
+
+    /**
+     * 是否删除所有卡片,默认为false;
+     * 如为true则删除设备上对应人员的所有卡片信息;此时如果lit配置了数据,将list中的卡下载到设备
+     */
+    private Boolean deleteAllCard = Boolean.FALSE;
+
+    /**
+     * 卡片对象清单
+     */
+    private List<CardDto> List;
+}

+ 70 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/EmpInfoDto.java

@@ -0,0 +1,70 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.server.hik.enums.AuthenticationEnum;
+import org.dromara.server.hik.enums.EmpTypeEnum;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 人员基本信息数据传输类
+ * <p>
+ * 定义了人员硒信息数据传输对象
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class EmpInfoDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -8026955153254941625L;
+
+    /**
+     * 人员编号,对应一卡通系统中人员流水号
+     */
+    private String employeeNo;
+
+    /**
+     * 人员姓名
+     */
+    private String name;
+
+    /**
+     * 人员类型 默认为普通人
+     *
+     * @see EmpTypeEnum
+     */
+    private String userType = EmpTypeEnum.NORMAL.getCode();
+
+    /**
+     * 删除人员信息,为true时删除该人员及其关联的所有信息,默认为false
+     */
+    private Boolean deleteUser = Boolean.FALSE;
+
+    /**
+     * 人员验证方式
+     *
+     * @see AuthenticationEnum
+     */
+    private String userVerifyMode = AuthenticationEnum.FACE_OR_FP_OR_CARD_OR_PW.getCode();
+
+    /**
+     * 人员的人脸图片信息
+     */
+    private FaceListDto FaceInfo;
+
+    /**
+     * 人员的卡片信息
+     */
+    private CardListDto CardInfo;
+
+    /**
+     * 有效期参数
+     */
+    private ValidDto Valid;
+}

+ 54 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/FaceDto.java

@@ -0,0 +1,54 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 人脸数据传输对象
+ * <p>
+ * 单个人脸数据传输对象定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class FaceDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -85247027566308465L;
+
+    /**
+     * 人脸库ID,范围1-32 默认为1
+     */
+    private String FDID = "1";
+
+    /**
+     * 人脸Id,从1开始,目前仅支持1
+     */
+    private Long faceID = 1L;
+
+    /**
+     * 人脸照片名称,下发照片数据时使用
+     */
+    private String faceName;
+
+    /**
+     * 人脸图片URL,如果使用picUrl
+     */
+    private String facePicURL;
+
+    /**
+     * 人脸特征库数据
+     */
+    private String modelData;
+
+    /**
+     * 是否删除人脸 为true时删除设备上员工当前人脸信息
+     */
+    private Boolean deleteFace = Boolean.FALSE;
+}

+ 36 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/FaceListDto.java

@@ -0,0 +1,36 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 人脸图片数据传输对象
+ * <p>
+ * 多条人脸图片数据传输对象定义类
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class FaceListDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 7030951136309820564L;
+
+    /**
+     * 是否删除所有人脸,默认为false;
+     * 如为true则删除设备上对应人员的所有人脸信息;此时如果lit配置了数据,将list中的卡下载到设备
+     */
+    private Boolean deleteAllFace = Boolean.FALSE;
+
+    /**
+     * 人脸对象清单
+     */
+    private List<FaceDto> List;
+}

+ 41 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/base/ValidDto.java

@@ -0,0 +1,41 @@
+package org.dromara.server.hik.domain.dto.base;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 有效期数据传输对象
+ * <p>
+ * 人员有效期数据传输对象定义类
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class ValidDto implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 3233182268013893354L;
+
+    /**
+     * 是否启用校验,默认为true
+     */
+    private Boolean enable = Boolean.TRUE;
+
+    /**
+     * 有效期开始时间
+     */
+    private Date beginTime;
+
+    /**
+     * 有效期结束时间
+     */
+    private Date endTime;
+
+}

+ 152 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/query/QueryEmpMatchDto.java

@@ -0,0 +1,152 @@
+package org.dromara.server.hik.domain.dto.query;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.server.hik.enums.EmpTypeEnum;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 人员查询命中结果数据传输对象
+ * <p>
+ * 人员查询命中结果数据传输对象定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class QueryEmpMatchDto {
+    /**
+     * 人员编号,对应一卡通系统中人员的 user_no字段
+     */
+    private Long employeeNo;
+
+    /**
+     * 人员姓名,对应一卡通系统中人员的real_name字段
+     */
+    private String name;
+
+    /**
+     * 人员类型 默认为普通人
+     *
+     * @see EmpTypeEnum
+     */
+    private String userType;
+
+    /**
+     * 人员有效期
+     */
+    private Valid Valid;
+
+    /**
+     * 人员的人脸图片信息
+     */
+    private FaceInfo FaceInfo;
+
+    /**
+     * 人员的卡片信息
+     */
+    private CardInfo CardInfo;
+
+    /**
+     * 权限计划
+     */
+    private List<RightPlan> rightPlan;
+
+    /**
+     * 本地UI权限
+     */
+    private Boolean localUIRight;
+
+    /**
+     * 人员扩展信息
+     */
+    private List<?> personInfoExtends;
+
+    @Data
+    @Accessors(chain = true)
+    public static class Valid {
+        /**
+         * 是否启用校验
+         */
+        private Boolean enable;
+
+        /**
+         * 有效期开始时间
+         */
+        private Date beginTime;
+
+        /**
+         * 有效期结束时间
+         */
+        private Date endTime;
+    }
+
+    @Data
+    @Accessors(chain = true)
+    public static class FaceInfo {
+        /**
+         * 人脸列表
+         */
+        private List<faceBean> List;
+
+        @Data
+        @Accessors(chain = true)
+        public static class faceBean {
+
+            /**
+             * 人脸库ID,范围1-32 默认为1
+             */
+            private String FDID;
+
+            /**
+             * 人脸Id,从1开始,目前仅支持1
+             */
+            private Long faceID;
+
+            /**
+             * 人脸照片名称,下发照片数据时使用
+             */
+            private String faceName;
+
+            /**
+             * 人脸图片URL,如果使用picUrl
+             */
+            private String facePicURL;
+
+            /**
+             * 人脸特征库数据
+             */
+            private String modelData;
+        }
+    }
+
+    @Data
+    @Accessors(chain = true)
+    public static class CardInfo {
+        private List<cardBean> List;
+
+        @Data
+        @Accessors(chain = true)
+        public static class cardBean {
+            /**
+             * 物理卡号
+             */
+            private Long cardNo;
+        }
+    }
+
+    @Data
+    @Accessors(chain = true)
+    public static class RightPlan {
+
+        private Integer doorNo;
+
+        private List<?> planTemplateNo;
+
+    }
+}

+ 47 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/query/QueryEmpResultDto.java

@@ -0,0 +1,47 @@
+package org.dromara.server.hik.domain.dto.query;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+/**
+ * 人员查询返回数据传输对象
+ * <p>
+ * 人员查询返回数据传输对象定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @see QueryEmpMatchDto
+ * @since JDK17
+ */
+@Data
+@Accessors(chain = true)
+public class QueryEmpResultDto {
+
+    /**
+     * 查询响应状态
+     * OK-本次数据全部取完
+     * MORE-MORE 数据未能一次取完
+     */
+    private String responseStatus;
+
+    /**
+     * 当次获取的记录数
+     */
+    private Integer numOfMatches;
+
+    /**
+     * 总记录数
+     */
+    private Integer totalMatches;
+
+    /**
+     * 查询命中的人员结果列表。
+     * 包含本次查询中所有命中的人员信息,每个元素为一个 QueryEmpMatchDto 对象,
+     * 表示单个人员的详细信息,包括员工编号、姓名、有效性、权限计划、人脸信息、卡信息等。
+     */
+    private List<QueryEmpMatchDto> matchResults;
+
+}

+ 38 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/dto/remote/RemoteCardDto.java

@@ -0,0 +1,38 @@
+package org.dromara.server.hik.domain.dto.remote;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 远程调用卡片数据传输对象
+ * <p>
+ * 远程调用卡片数据传输对象
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-25
+ * @since JDK17
+ */
+@Data
+public class RemoteCardDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 776456160182525438L;
+
+    /**
+     * 所属账户Id
+     */
+    private Long userId;
+
+    /**
+     * 物理卡号
+     */
+    private Long factoryId;
+
+    /**
+     * 卡片状态,见KZT字典类别
+     */
+    private String status;
+}

+ 162 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfConsumeDetailOriginalVo.java

@@ -0,0 +1,162 @@
+package org.dromara.server.hik.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.server.hik.domain.XfConsumeDetailOriginal;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 原始消费记录视图对象 t_xf_consumeDetailOriginal
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = XfConsumeDetailOriginal.class)
+public class XfConsumeDetailOriginalVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 原始记录Id,主键
+     */
+    private String originalId;
+
+    /**
+     * 记录Id
+     */
+    private Long recordId;
+
+    /**
+     * 人员Id
+     */
+    private Long userId;
+
+    /**
+     * 学/工号
+     */
+    private String userNumb;
+
+    /**
+     * 用户姓名
+     */
+    private String realName;
+
+    /**
+     * 部门Id
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 消费日期
+     */
+    private Date consumeDate;
+
+    /**
+     * 消费金额
+     */
+    private Long consumeMoney;
+
+    /**
+     * 卡流水号
+     */
+    private Long cardNo;
+
+    /**
+     * 物理卡号
+     */
+    private Long factoryId;
+
+    /**
+     * 卡上余额
+     */
+    private Long cardValue;
+
+    /**
+     * 卡使用次数
+     */
+    private Long cardCount;
+
+    /**
+     * 消费账户金额
+     */
+    private Long consumeBalance;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 机器流水号
+     */
+    private Long termRecordId;
+
+    /**
+     * 是否已处理(和灰记录处理有关)
+     */
+    private Long anlysFlag;
+
+    /**
+     * 消费记录标志
+     */
+    private Long dataFlag;
+
+    /**
+     * 状态标识(记录消费类型?)
+     */
+    private Long statusFlag;
+
+    /**
+     * 防伪验证码(记录消费模式的中文信息)
+     */
+    private String digitalSign;
+
+    /**
+     * 水控金额
+     */
+    private Long waterValue;
+
+    /**
+     * 水控历史余额
+     */
+    private Long waterHistoryValue;
+
+    /**
+     * 水控当天总和
+     */
+    private Long waterDaySum;
+
+    /**
+     * 水控错误余额
+     */
+    private Long waterErrValue;
+
+    /**
+     * 水控错误消费金额
+     */
+    private Long waterErrMoney;
+
+    /**
+     * 营业员Id
+     */
+    private Long operatorId;
+
+}

+ 199 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfConsumeDetailVo.java

@@ -0,0 +1,199 @@
+package org.dromara.server.hik.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.server.hik.domain.XfConsumeDetail;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+
+/**
+ * 消费明细视图对象 t_xf_consumeDetail
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = XfConsumeDetail.class)
+public class XfConsumeDetailVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消费记录Id,主键
+     */
+    private String consumeId;
+
+    /**
+     * 原始记录Id,主键
+     */
+    private String originalId;
+
+    /**
+     * 记录Id
+     */
+    private Long recordId;
+
+    /**
+     * 人员Id
+     */
+    private Long userId;
+
+    /**
+     * 学/工号
+     */
+    private String userNumb;
+
+    /**
+     * 用户姓名
+     */
+    private String realName;
+
+    /**
+     * 部门Id
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 消费日期
+     */
+    private Date consumeDate;
+
+    /**
+     * 消费金额
+     */
+    private BigDecimal consumeMoney;
+
+    /**
+     * 卡流水号
+     */
+    private Long cardNo;
+
+    /**
+     * 物理卡号
+     */
+    private Long factoryId;
+
+    /**
+     * 卡上余额
+     */
+    private BigDecimal cardValue;
+
+    /**
+     * 卡使用次数
+     */
+    private Long cardCount;
+
+    /**
+     * 消费账户金额
+     */
+    private BigDecimal consumeBalance;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+
+    /**
+     * 机器流水号
+     */
+    private Long termRecordId;
+
+    /**
+     * 是否已处理(和灰记录处理有关)
+     */
+    private Long analysisFlag;
+
+    /**
+     * 状态标识(记录消费类型?)
+     */
+    private Long statusFlag;
+
+    /**
+     * 营业员Id
+     */
+    private Long operatorId;
+
+    /**
+     * 营业员名称
+     */
+    private String operatorName;
+
+    /**
+     * 结算账户Id
+     */
+    private Long accountId;
+
+    /**
+     * 结算账户名称
+     */
+    private String accountName;
+
+    /**
+     * 房间Id
+     */
+    private Long roomId;
+
+    /**
+     * 房间名称
+     */
+    private String roomName;
+
+    /**
+     * 钱包类型
+     */
+    private String bagType;
+
+    /**
+     * 餐类Id
+     */
+    private Long mealType;
+
+    /**
+     * 餐类名称
+     */
+    private String mealName;
+
+    /**
+     * 是否发送短信,见 sys_yes_no字典类别
+     */
+    private String smsSend;
+
+    /**
+     * 消费记录标志位?
+     */
+    private Long recordStatus;
+
+    /**
+     * 同步标志,0-未同步,1-已同步
+     */
+    private Long syncStatus;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 针对错扣补款的消费记录的id
+     */
+    private String detailId;
+
+
+}

+ 96 - 0
ruoyi-server/ruoyi-server-face/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;
+
+
+}

+ 338 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/domain/vo/XfTermVo.java

@@ -0,0 +1,338 @@
+package org.dromara.server.hik.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.server.hik.domain.XfTerm;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+
+/**
+ * 消费设备视图对象 t_xf_term
+ *
+ * @author bing
+ * @date 2024-08-21
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = XfTerm.class)
+public class XfTermVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备Id,主键
+     */
+    private Long termId;
+
+    /**
+     * 设备机号
+     */
+    private Long termNo;
+
+    /**
+     * 工作站Id
+     */
+    private Long stationId;
+
+    /**
+     * 设备名称
+     */
+    private String termName;
+    /**
+     * 设备二维码
+     */
+    private String qrCode;
+    /**
+     * 所属餐厅Id
+     */
+    private Long roomId;
+    /**
+     * 所属餐厅名称
+     */
+    private String roomName;
+
+    /**
+     * 结算账户Id
+     */
+    private Long accountId;
+    /**
+     * 结算账户名称
+     */
+    private String accountName;
+
+    /**
+     * 设备类型,见XF_TermType字典类型
+     */
+    private String termType;
+
+    /**
+     * 扣费类型,见XF_ConsumeType字典类型
+     */
+    private String consumeType;
+
+    /**
+     * 设备IP
+     */
+    private String termIp;
+
+    /**
+     * 设备MAC地址
+     */
+    private String termMac;
+
+    /**
+     * 设备通讯端口
+     */
+    private Long commPort;
+    /**
+     * 子网掩码
+     */
+    private String mask;
+
+    /**
+     * 网关IP
+     */
+    private String gatewayIp;
+
+    /**
+     * 服务器IP
+     */
+    private String serverIp;
+
+    /**
+     * 服务器端口
+     */
+    private Long serverPort;
+
+    /**
+     * 是否自动下载参数
+     */
+    private String autoDown;
+
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+    /**
+     * 应用系统,见YYXT数据字典类别
+     */
+    private String useType;
+    //region 以下为消费机的参数属性
+    private Integer cardType;
+
+    /**
+     * 消费工作模式,见XF_WorkMode字典类别
+     */
+    private String workMode;
+
+    /**
+     * 开机模式,0-不需要营业员开机,1-需要营业员开机
+     */
+    private String openMode;
+
+    /**
+     * 卡上最大金额
+     */
+    private BigDecimal maxCardMoney;
+
+    /**
+     * 定值消费金额
+     */
+    private BigDecimal constantValue;
+
+    /**
+     * 编号0代表的金额
+     */
+    private BigDecimal rationZero;
+
+    /**
+     * 编号1代表的金额
+     */
+    private BigDecimal rationOne;
+
+    /**
+     * 编号2代表的金额
+     */
+    private BigDecimal rationTwo;
+
+    /**
+     * 编号3代表的金额
+     */
+    private BigDecimal rationThree;
+
+    /**
+     * 编号4代表的金额
+     */
+    private BigDecimal rationFour;
+
+    /**
+     * 编号5代表的金额
+     */
+    private BigDecimal rationFive;
+
+    /**
+     * 编号6代表的金额
+     */
+    private BigDecimal rationSix;
+
+    /**
+     * 编号7代表的金额
+     */
+    private BigDecimal rationSeven;
+
+    /**
+     * 编号8代表的金额
+     */
+    private BigDecimal rationEight;
+
+    /**
+     * 编号9代表的金额
+     */
+    private BigDecimal rationNine;
+
+    /**
+     * 每天最大消费次数,0-不限
+     */
+    private Integer dayCount;
+
+    /**
+     * 每天最大消费金额
+     */
+    private BigDecimal dayMoney;
+
+    /**
+     * 每餐最大消费次数,0-不限
+     */
+    private Integer mealCount;
+
+    /**
+     * 单次最大消费金额
+     */
+    private BigDecimal singleMoney;
+
+    /**
+     * 早餐消费金额
+     */
+    private BigDecimal breakfastMoney;
+
+    /**
+     * 午餐消费金额
+     */
+    private BigDecimal lunchMoney;
+
+    /**
+     * 晚餐消费金额
+     */
+    private BigDecimal supperMoney;
+
+    /**
+     * 夜宵消费金额
+     */
+    private BigDecimal nightMoney;
+
+    /**
+     * 早餐开始时间
+     */
+    private Long breakfastBegin;
+
+    /**
+     * 早餐结束时间
+     */
+    private Long breakfastEnd;
+
+    /**
+     * 午餐开始时间
+     */
+    private Long lunchBegin;
+
+    /**
+     * 午餐结束时间
+     */
+    private Long lunchEnd;
+
+    /**
+     * 晚餐开始时间
+     */
+    private Long supperBegin;
+
+    /**
+     * 晚餐结束时间
+     */
+    private Long supperEnd;
+
+    /**
+     * 宵夜开始时间
+     */
+    private Long nightBegin;
+
+    /**
+     * 宵夜结束时间
+     */
+    private Long nightEnd;
+
+    /**
+     * 两次刷卡间隔,0-不限制
+     */
+    private Integer swipeInterval;
+
+    /**
+     * 是否启用卡片有效限制,0-禁用 1-启用
+     */
+    private String termValidity;
+
+    /**
+     * 重启时间
+     */
+    private String rebootTime;
+
+    /**
+     * 最后校时时间
+     */
+    private Date lastCheck;
+    /**
+     * 允许脱机时间
+     */
+    private Long offlineTime;
+    /**
+     * 通讯超时时间,以100毫秒为单位
+     */
+    private Long timeout;
+    /**
+     * 心跳间隔,以秒为单位
+     */
+    private Long beatInterval;
+    /**
+     * 设备记录流水号
+     */
+    private Long recordId;
+
+    /**
+     * 数据上传时间
+     */
+    private Date uploadTime;
+
+    /**
+     * 黑名单下载时间
+     */
+    private Date blackDownTime;
+    //endregion
+
+    /*消费机管理账号*/
+    private String adminName;
+
+    /*消费机管理密码*/
+    private String adminPwd;
+
+    /*消费机品牌*/
+    private String brand;
+}

+ 189 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/AuthenticationEnum.java

@@ -0,0 +1,189 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 人员验证方式
+ * <p>
+ * 定义终端设备的人员验证方式枚举
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Getter
+@AllArgsConstructor
+public enum AuthenticationEnum {
+    /**
+     * 刷卡+密码
+     */
+    CARD_AND_PW("cardAndPw", "刷卡+密码"),
+
+    /**
+     * 刷卡
+     */
+    CARD("card", "刷卡"),
+
+    /**
+     * 刷卡或密码
+     */
+    CARD_OR_PW("cardOrPw", "刷卡或密码"),
+
+    /**
+     * 指纹
+     */
+    FP("fp", "指纹"),
+
+    /**
+     * 指纹+密码
+     */
+    FP_AND_PW("fpAndPw", "指纹+密码"),
+
+    /**
+     * 指纹或刷卡
+     */
+    FP_OR_CARD("fpOrCard", "指纹或刷卡"),
+
+    /**
+     * 指纹+刷卡
+     */
+    FP_AND_CARD("fpAndCard", "指纹+刷卡"),
+
+    /**
+     * 指纹+刷卡+密码
+     */
+    FP_AND_CARD_AND_PW("fpAndCardAndPw", "指纹+刷卡+密码"),
+
+    /**
+     * 人脸或指纹或刷卡或密码
+     */
+    FACE_OR_FP_OR_CARD_OR_PW("faceOrFpOrCardOrPw", "人脸或指纹或刷卡或密码"),
+
+    /**
+     * 人脸+指纹
+     */
+    FACE_AND_FP("faceAndFp", "人脸+指纹"),
+
+    /**
+     * 人脸+密码
+     */
+    FACE_AND_PW("faceAndPw", "人脸+密码"),
+
+    /**
+     * 人脸+刷卡
+     */
+    FACE_AND_CARD("faceAndCard", "人脸+刷卡"),
+
+    /**
+     * 人脸
+     */
+    FACE("face", "人脸"),
+
+    /**
+     * 工号+密码
+     */
+    EMPLOYEE_NO_AND_PW("employeeNoAndPw", "工号+密码"),
+
+    /**
+     * 刷卡或密码
+     */
+    FP_OR_PW("fpOrPw", "刷卡或密码"),
+
+    /**
+     * 工号+指纹
+     */
+    EMPLOYEE_NO_AND_FP("employeeNoAndFp", "工号+指纹"),
+
+    /**
+     * 工号+指纹+密码
+     */
+    EMPLOYEE_NO_AND_FP_AND_PW("employeeNoAndFpAndPw", "工号+指纹+密码"),
+
+    /**
+     * 人脸+指纹+刷卡
+     */
+    FACE_AND_FP_AND_CARD("faceAndFpAndCard", "人脸+指纹+刷卡"),
+
+    /**
+     * 人脸+密码+指纹
+     */
+    FACE_AND_PW_AND_FP("faceAndPwAndFp", "人脸+密码+指纹"),
+
+    /**
+     * 工号+人脸
+     */
+    EMPLOYEE_NO_AND_FACE("employeeNoAndFace", "工号+人脸"),
+
+    /**
+     * 人脸或人脸+刷卡
+     */
+    FACE_OR_FACE_AND_CARD("faceOrfaceAndCard", "人脸或人脸+刷卡"),
+
+    /**
+     * 指纹或人脸
+     */
+    FP_OR_FACE("fpOrface", "指纹或人脸"),
+
+    /**
+     * 刷卡或人脸或密码
+     */
+    CARD_OR_FACE_OR_PW("cardOrfaceOrPw", "刷卡或人脸或密码"),
+
+    /**
+     * 刷卡或人脸
+     */
+    CARD_OR_FACE("cardOrFace", "刷卡或人脸"),
+
+    /**
+     * 刷卡或人脸或指纹
+     */
+    CARD_OR_FACE_OR_FP("cardOrFaceOrFp", "刷卡或人脸或指纹");
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据类型编码获取类型信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(AuthenticationEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .map(AuthenticationEnum::getMessage)
+                   .orElse(StrUtil.EMPTY);
+    }
+
+    /**
+     * 根据编码获取枚举值
+     * @param code 编码
+     * @return 编码对应的枚举值
+     */
+    public static AuthenticationEnum fromCode(String code) {
+        if (StringUtils.isBlank(code)) {
+            return AuthenticationEnum.FACE_OR_FP_OR_CARD_OR_PW;
+        }
+        return Arrays.stream(AuthenticationEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .orElse(AuthenticationEnum.FACE_OR_FP_OR_CARD_OR_PW);
+    }
+
+}

+ 79 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/CardTypeEnum.java

@@ -0,0 +1,79 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 卡片类型枚举
+ * <p>
+ * 向设备终端下发的卡片的类型枚举
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Getter
+@AllArgsConstructor
+public enum CardTypeEnum {
+    /**
+     * 普通卡
+     */
+    NORMAL_CARD("normalCard", "普通卡"),
+
+    /**
+     * 巡更卡
+     */
+    PATROL_CARD("patrolCard", "巡更卡"),
+
+    /**
+     * 胁迫卡
+     */
+    HIJACK_CARD("hijackCard", "胁迫卡"),
+
+    /**
+     * 超级卡
+     */
+    SUPER_CARD("superCard", "超级卡"),
+
+    /**
+     * 解除卡
+     */
+    DISMISSING_CARD("dismissingCard", "解除卡"),
+
+    /**
+     * 应急管理卡
+     */
+    EMERGENCY_CARD("emergencyCard", "应急管理卡");
+
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据编码获取描述信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(CardTypeEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .map(CardTypeEnum::getMessage)
+                   .orElse(StrUtil.EMPTY);
+    }
+}

+ 68 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ConsumeModeEnum.java

@@ -0,0 +1,68 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 消费机的消费模式
+ *
+ * @author bing
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Getter
+@AllArgsConstructor
+public enum ConsumeModeEnum {
+
+
+    /**
+     * 金额模式
+     */
+    amount("amount", "金额模式"),
+
+    /**
+     * 定额模式
+     */
+    quota("quota", "定额模式"),
+
+    /**
+     * 计次模式
+     */
+    count("count", "计次模式"),
+
+
+   ;
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据类型编码获取类型信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(ConsumeModeEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .map(ConsumeModeEnum::getMessage)
+                   .orElse(StrUtil.EMPTY);
+    }
+
+
+}

+ 89 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ContentTypeEnum.java

@@ -0,0 +1,89 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * HTTP 请求头内容类型定义
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum ContentTypeEnum {
+    /**
+     * XHTML格式
+     */
+    XHTML_XML("application/xhtml+xml ", "XHTML格式"),
+
+    /**
+     * XML数据格式
+     */
+    XML("application/xml", "XML数据格式"),
+
+    /**
+     * Atom XML聚合格式
+     */
+    ATOM_XML("application/atom+xml ", "Atom XML聚合格式"),
+
+    /**
+     * JSON数据格式
+     */
+    JSON("application/json ", "JSON数据格式"),
+
+    /**
+     * pdf格式
+     */
+    PDF("application/pdf ", "pdf格式"),
+
+    /**
+     * Word文档格式
+     */
+    MSWORD("application/msword", "Word文档格式"),
+
+    /**
+     * 二进制流数据
+     */
+    OCTET_STREAM("application/octet-stream", "二进制流数据"),
+
+    /**
+     * 表单默认的提交数据的格式
+     */
+    FORM_URLENCODED("application/x-www-form-urlencoded", "表单默认的提交数据的格式"),
+
+    /**
+     * 文件上传格式
+     */
+    FORM_DATA("multipart/form-data", "文件上传格式");
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据类型编码获取类型信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(ContentTypeEnum.values())
+                   .filter(typeEnum -> typeEnum.getCode().equals(code))
+                   .findFirst()
+                   .map(ContentTypeEnum::getMessage)
+                   .orElse(StrUtil.EMPTY);
+    }
+}
+

+ 59 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/EmpTypeEnum.java

@@ -0,0 +1,59 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 人员类型枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum EmpTypeEnum {
+    /**
+     * 普通人
+     */
+    NORMAL("normal", "普通人"),
+
+    /**
+     * 来宾
+     */
+    VISITOR("visitor", "来宾"),
+
+    /**
+     * 非授权名单
+     */
+    BLACKLIST("blackList", "非授权名单");
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据类型编码获取类型信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(EmpTypeEnum.values())
+            .filter(typeEnum -> typeEnum.getCode().equals(code))
+            .findFirst()
+            .map(EmpTypeEnum::getMessage)
+            .orElse(StrUtil.EMPTY);
+    }
+}
+

+ 60 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/ModeTypeEnum.java

@@ -0,0 +1,60 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 消费机的交易模式
+ *
+ * @author bing
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Getter
+@AllArgsConstructor
+public enum ModeTypeEnum {
+    /**
+     * 实时交易
+     */
+    current("current", "实时交易"),
+
+    /**
+     * 离线交易
+     */
+    offLine("offLine", "离线交易"),
+
+   ;
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据类型编码获取类型信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (StringUtils.isBlank(code)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(ModeTypeEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .map(ModeTypeEnum::getMessage)
+                   .orElse(StrUtil.EMPTY);
+    }
+
+
+}

+ 61 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/enums/StatusCodeEnum.java

@@ -0,0 +1,61 @@
+package org.dromara.server.hik.enums;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 状态码枚举
+ * <p>
+ * 请求设备后返回的状态码
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @see Object
+ * @since JDK17
+ */
+@Getter
+@AllArgsConstructor
+public enum StatusCodeEnum {
+    /**
+     * 正常
+     */
+    OK("0", "ok"),
+    YES("1","ok"),
+    DEVICE_BUSY("2","设备繁忙"),
+    DEVICE_ERROR("3","设备错误"),
+    INVALID_OPERATION("4","无效操作"),
+    INVALID_XML_FORMAT("5","无效XML格式"),
+    INVALID_XML_CONTENT("6","无效XML内容"),
+    REBOOT_REQUIRED("7","设备需要重启"),
+    USER_CHECK_401("userCheck_401","设备未授权");
+
+    /**
+     * 编码
+     */
+    private final String code;
+
+    /**
+     * 描述
+     */
+    private final String message;
+
+    /**
+     * 获根据编码获取描述信息
+     *
+     * @param code 编码
+     */
+    public static String getMessage(String code) {
+        if (ObjectUtil.isEmpty(code)) {
+            return "未知异常";
+        }
+        return Arrays.stream(StatusCodeEnum.values())
+                   .filter(item -> item.getCode().equals(code))
+                   .findFirst()
+                   .map(StatusCodeEnum::getMessage)
+                   .orElse("未知异常");
+    }
+}

+ 57 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/EventHandleRouter.java

@@ -0,0 +1,57 @@
+package org.dromara.server.hik.event;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.mysql.cj.TransactionEventHandler;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.server.hik.event.domain.FileContent;
+import org.dromara.server.hik.event.handler.ConsumptionEventHandler;
+import org.dromara.server.hik.event.handler.HeatBeatHandler;
+import org.dromara.server.hik.event.handler.TransactionRecordEventHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.dromara.server.hik.constant.HikEventTypeConstant.*;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class EventHandleRouter {
+
+    private final ConsumptionEventHandler consumptionEventHandler;
+    private final TransactionRecordEventHandler transactionRecordEventHandler;
+    private final HeatBeatHandler heatBeatHandler;
+
+    public Map<String, Object> route(List<String> jsonListString, List<FileContent> fileByteList) {
+        //目前格式最多 一个json 一个文件
+        //根据eventType分发到对应的事件处理器
+        if(CollectionUtil.isNotEmpty(jsonListString) && jsonListString.size() == 1){
+            String s = jsonListString.get(0);
+            FileContent fileContent = CollectionUtil.isEmpty(fileByteList)?null:fileByteList.get(0);
+            // 字符串装换为json对象
+            JSONObject jsonObject = JSONObject.parseObject(s);
+            String eventType = jsonObject.getString("eventType");
+            HikEventHandler handler;
+            switch (eventType) {
+                case ConsumptionEvent -> handler = consumptionEventHandler;
+                case TransactionRecordEvent -> handler = transactionRecordEventHandler;
+                case HeartBeat -> handler = heatBeatHandler;
+                default -> throw new ServiceException("暂不支持该事件");
+            }
+            return handler.handleEvent(jsonObject, fileContent);
+        } else {
+            if(CollectionUtil.isEmpty(fileByteList)){
+                throw new ServiceException("json 为null,无法处理");
+            }else{
+                throw new ServiceException("暂不支持多个事件同时处理");
+            }
+        }
+    }
+}

+ 11 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/HikEventHandler.java

@@ -0,0 +1,11 @@
+package org.dromara.server.hik.event;
+
+import com.alibaba.fastjson.JSONObject;
+import org.dromara.server.hik.event.domain.FileContent;
+
+import java.util.Map;
+
+public interface HikEventHandler {
+
+    Map<String,  Object> handleEvent(JSONObject jsonObject, FileContent fileContent);
+}

+ 136 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmBo.java

@@ -0,0 +1,136 @@
+package org.dromara.server.hik.event.domain;
+
+
+import io.seata.common.util.StringUtils;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 应答 ConsumptionEvent 的 参数类
+ */
+@Data
+public class ConsumptionEventConfirmBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /*wo, req, object, 消费事件确认*/
+    private Integer serialNo;
+    /*req, int, 消费流水号, desc:交易预处理、确认请求流水号保持一致,与交易记录流水号保持一致*/
+    private String result;
+    /*wo, req, enum, 结果, subType:string, [success#交易成功,balanceNotEnough#余额不足,remainingTimesNotEnough#剩余次数不足,noSuchPerson#查无此人,currentTimeNoRight#当前时间段无消费权限,refundSuccess#纠错成功,transactionNotCheckedByDevice#交易未经过设备校验,failed#失败,paying#正在支付,QRCodeExist#重复二维码,QRCodeExpired#二维码过期,amountExceededTheLimit#单次消费超出限额,QRCodeInvalid#无效二维码,continuePay#继续支付],
+    desc:1、当结果为paying时,表示平台与三方服务的支付没有完成,需要设备通过订单状态查询协议获取支付结果。
+    2、平台二维码消费,同一个二维码只能消费一次,多次刷会消费失败,对应QRCodeExist。
+    3、continuePay:当组合支付时,平台账户余额不足时,返回该字段,并携带remainingAmount字段下发*/
+    private String reason;
+    /*wo, opt, enum, 失败原因, subType:string, [noPermissionForWeChat#平台未开通微信支付,noPermissionForAlipay#平台未开通支付宝支付,noPermissionForCurrentPayment#平台不支持当前支付方式,deviceNotBoundToMerchant#设备未在平台绑定商户,deviceNotRegistered#当前设备未注册,platformException#支付平台异常,other#其他], dep:or,{$.ConsumptionEventConfirm.result,eq,failed}, desc:二维码支付失败原因*/
+    private String mode;
+    /*wo, opt, enum, 消费模式, subType:string, [amount#金额,quota#定额,count#计次,setMeal#套餐,box#格口消费]*/
+    private String actualPayment;
+    /*wo, opt, string, 实付金额(单位:分), desc:敏感字段加密。当mode为amount/quota/setMeal/box时,该字段必填*/
+    private String remainingAmount;
+    /*opt, string, 组合支付方式下需要二次扣费的金额(单位:分), desc:敏感字段加密。组合支付时使用:设备预处理上报后,等待平台返回结果,若虚拟账户余额不足的情况下,平台支持返回剩余还需要扣费的金额,由设备进行二次展示,第二次扣费支持平台端移动支付和设备端移动支付,当二次扣费成功后,虚拟账户才实际扣费。*/
+    private String balanceBeforeDeduct;
+    /*wo, opt, string, 未扣款前的余额(单位:分),
+    desc:当mode为amount/quota/setMeal/box时,该字段必填;
+    当result为success,mode为amout或quota,balanceBeforeDeduct为0时,消费成功 ,设备认为最大消费金额配置为0*/
+    private String name;
+    /*wo, opt, string, 姓名, range:[0,128], desc:敏感字段加密。*/
+    private String employeeNoString;
+    /*wo, opt, string, 工号, range:[0,32], desc:敏感字段加密。*/
+    private String cardNo;
+    /*wo, opt, string, 卡号, range:[0,32], desc:敏感字段加密。*/
+    private Integer times;
+    //*wo, opt, int, 将要抵扣的次数, desc:当mode为count时,该字段必填。纠错退次时,该字段表示需要回退的次数*//*
+
+    private String customTTSBroadcastVoice;
+    //*opt, string, 自定义TTS播报语音, range:[1,64], desc:消费事件确认的时候,平台下发该字段。存在该字段且校验成功则优先播报,否则按照设备原本播报规则播报。平台消费结果为成功,该字段为消费成功的自定义语音;平台消费结果为失败,该字段为消费失败的自定义语音*//*
+    private Integer remainingTimes;
+    //*wo, opt, int, 未抵扣前的次数,
+//    desc:当mode为count时,该字段可选;
+//    当result为success,mode为count,remaining times为0时,消费成功 ,设备认为最大消费次数配置为0*//*
+    private ContentInfo contentInfo;
+
+    /*
+
+
+    private String QRCodeType;
+    *//*opt, enum, 二维码类型, subType:string, [Alipay#支付宝,WeChat#微信,DigitalCurrencyICBC#工行数字人民币,Private#自研二维码,integrationPayment#聚合支付二维码],
+    desc:当用户使用支付宝或微信或工行数字人民币支付时,平台会返回该字段,用于告知设备,当前用户采用的支付方式,此时工号和卡号无效。
+    如果返回的是自研二维码,设备走本地支付流程。*//*
+    private String boxID;
+    *//*ro, opt, string, 格口编号, range:[0,7], dep:and,{$.ConsumptionEventConfirm.mode,eq,box},
+    desc:第1位 L 表示格口箱在控制柜左侧,R表示格口箱在控制柜右侧,和控制柜同一列为M
+    2,3位表示格口箱编号
+    4,5位表示格口所在格口箱的列号,靠近控制柜的为第一列
+    6,7位表示格口的行号,从上至下排序*//*
+    private String currencyType;
+    *//*opt, enum, 币种类型, subType:string, [CNY#人民币,USD#美元,EUR#欧元,GBP#英镑,HKD#中国香港元,SGD#新加坡元,KRW#元(韩元),PLN#波兰兹罗提,BYR#白俄罗斯卢布,RUB#俄罗斯卢布,THB#泰国泰铢,TRY#土耳其里拉,VND#越南盾,ILS#以色列新谢克尔,UAH#乌克兰格里夫纳,CZK#捷克克朗,DDK#丹麦克朗,NOK#挪威克朗,SEK#瑞典克朗,BRL#巴西雷亚尔,IQD#伊拉克第纳尔,IDR#印尼卢比,INR#印度卢比,AED#阿联酋迪拉姆,IRR#伊朗土曼,BGN#保加利亚列弗,HUF#匈牙利福林,MYR#马来西亚林吉特,MXN#墨西哥比索], desc:默认是USD。该字段和decimalPlace字段搭配使用,无该字段和decimalPlace字段表示使用的是人民币,小数位为2*//*
+    private Integer decimalPlace;
+    *//*opt, int, 小数位, range:[0,3], desc:表示货币的小数位,默认为2*//*
+
+
+    private String paymentOrderNo;
+    *//*opt, string, 三方支付订单号, range:[1,64], desc:移动支付(微信、支付宝、工号数字人民币)时的订单编号。如果是平台三方支付,需要当查询结果为成功时,需要返回平台维护的三方支付订单号*//*
+    private String secondaryConfirmType;
+    *//*opt, enum, 二次确认类型, subType:string, [none#不需二次确认,highPaymentAmount#大额消费], desc:1、无该字段,表示不需二次确认。2、设备在线时,是否做二次确认,以平台返回该字段为准。3、在/ISAPI/Consume/defaultMode?format=json中配置的highPaymentAmountInfo仅在设备离线时有效。*//*
+    private String userPassword;
+    *//*opt, string, 人员凭证密码, range:[4,8], dep:or,{$.ConsumptionEventConfirm.secondaryConfirmType,eq,highPaymentAmount}, desc:人员凭证密码,用于二次确认,敏感信息加密。*//*
+*/
+    @Data
+    public static class ContentInfo implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+        /*opt, object, 自定义消费结果文字信息, desc:消费平台可以下发自定义的主副标题。设备收到后会将这个字段透传显示在消费结果提示界面*/
+        private String title;
+        /*opt, string, 标题, range:[0,32]*/
+        private String content;
+        /*opt, string, 内容, range:[0,32]*/
+
+        private Map<String, Object> toMap(){
+            Map<String, Object> map = new HashMap<>();
+            if(StringUtils.isNotBlank(title)){
+                map.put("title", title);
+            }
+            if(StringUtils.isNotBlank(content)){
+                map.put("content", content);
+            }
+            if (map.isEmpty()){
+                return null;
+            }
+            return map;
+        }
+    }
+
+    public Map<String, Object> toMap() {
+        Map<String, Object> map = new HashMap<>();
+        putIfNotEmpty(map, "serialNo", serialNo);
+        putIfNotEmpty(map, "result", result);
+        putIfNotEmpty(map, "reason", reason);
+        putIfNotEmpty(map, "mode", mode);
+        putIfNotEmpty(map, "name", name);
+        putIfNotEmpty(map, "employeeNoString", employeeNoString);
+        putIfNotEmpty(map, "cardNo", cardNo);
+        putIfNotEmpty(map, "actualPayment", actualPayment);
+        putIfNotEmpty(map, "balanceBeforeDeduct", balanceBeforeDeduct);
+        putIfNotEmpty(map, "times", times);
+        putIfNotEmpty(map, "remainingTimes", remainingTimes);
+        putIfNotEmpty(map, "customTTSBroadcastVoice", customTTSBroadcastVoice);
+        putIfNotEmpty(map, "contentInfo", contentInfo != null ? contentInfo.toMap() : null);
+        return map;
+    }
+
+    private void putIfNotEmpty(Map<String, Object> map, String key, Object value) {
+        if (value instanceof String && ((String) value).isEmpty()) {
+            return;
+        }
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+
+}

+ 17 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmingReceive.java

@@ -0,0 +1,17 @@
+package org.dromara.server.hik.event.domain;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 消费机 交易确认请求事件上报 的内容
+ */
+@Data
+public class ConsumptionEventConfirmingReceive implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+}

+ 59 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventDetail.java

@@ -0,0 +1,59 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class ConsumptionEventDetail implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /*ro, req, object, 事件信息*/
+
+    private String minor;
+    /*ro, req, enum, 次类型, subType:string, [transactionPreprocessingRequest#交易预处理请求,transactionConfirmingRequest#交易确认请求]*/
+    private Boolean cancel;
+    /*ro, opt, bool, 交易是否取消, desc:当minor为transactionConfirmingRequest时,该字段生效*/
+    private Integer serialNo;
+    /*ro, req, int, 交易记录流水号, desc:交易预处理、确认请求流水号保持一致,与交易记录流水号保持一致*/
+    private String name;
+    /*ro, opt, string, 姓名, range:[0,128]*/
+    private String authenticationMode;
+    /*ro, opt, enum, 认证模式, subType:string, [QRCode#二维码,employeeNo#工号,card#卡片],desc:该字段不存在时,解析优先级:工号>卡号>二维码类型 该字段存在时,以该字段为准*/
+    private String employeeNoString;
+    /*ro, opt, string, 工号, range:[0,32], dep:and,{$.ConsumptionEvent.authenticationMode,eq,employeeNo}, desc:当QRCodeType为私有二维码时,工号也可以返回*/
+    private String cardNo;
+    /*ro, opt, string, 卡号, range:[0,32], dep:and,{$.ConsumptionEvent.authenticationMode,eq,card}, desc:当QRCodeType为私有二维码时,工号也可以返回*/
+    private String QRCode;
+    /*ro, opt, string, 二维码, range:[0,32], dep:and,{$.ConsumptionEvent.authenticationMode,eq,QRCode}, desc:自研及三方二维码*/
+    private String QRCodeType;
+    /*ro, opt, enum, 二维码类型, subType:string, [WeChat#微信,Alipay#支付宝,Private#私有二维码,DigitalCurrencyICBC#工行数字人民币,integrationPayment#聚合支付], dep:and,{$.ConsumptionEvent.authenticationMode,eq,QRCode}*/
+    private String type;
+    /*ro, req, enum, 交易类型, subType:string, [transaction#交易,refund#纠错]*/
+    private Integer refundSerialNo;
+    /*ro, opt, int, 需要纠错的流水号, desc:type为refund该字段有效,且必须返回*/
+    private String mode;
+    /*ro, req, enum, 消费模式, subType:string, [amount#金额,quota#定额,count#计次,setMeal#套餐,box#格口消费]*/
+    private String totalPayment;
+    /*ro, opt, string, 应付金额,
+    desc:当交易类型为交易时表示交易金额、且mode为amount或quota或box时,该字段必填;当交易类型为纠错类型、且mode为amount或quota或setMeal时,该字段必填,并与纠错金额refundPayment结合使用,使用方法:平台对账户先加上纠错金额再减去应付金额。
+    纠错举例1:(修改消费金额)
+    比如第一次消费10元。
+    发起纠错。应该消费5元。
+    那么应付金额应该是5,纠错金额是10.
+    这样平台对账户先加上纠错金额10,再减去应付金额5.*/
+    private String refundPayment;
+    /*ro, opt, string, 纠错金额, desc:1、当交易类型为纠错类型、且mode为amount或quota或setMeal时,该字段必填,并与应付金额结合使用,使用方法:平台对账户先加上纠错金额再减去应付金额;2、当交易类型为纠错类型、且mode为count时,该字段表示纠错回退次数*/
+    private Integer times;
+    /*ro, opt, int, 将要抵扣的次数, desc:当mode为count时,该字段必填。纠错退次时,该字段表示需要回退的次数*/
+
+    private String boxID;
+		/*ro, opt, string, 格口编号, range:[0,7], dep:and,{$.ConsumptionEvent.mode,eq,box},
+		desc:第1位 L 表示格口箱在控制柜左侧,R表示格口箱在控制柜右侧,和控制柜同一列为M
+		2,3位表示格口箱编号
+		4,5位表示格口所在格口箱的列号,靠近控制柜的为第一列
+		6,7位表示格口的行号,从上至下排序*/
+    private Boolean localCheck;
+
+}

+ 31 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventReceive.java

@@ -0,0 +1,31 @@
+package org.dromara.server.hik.event.domain;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 消费机 交易预处理请求事件上报
+ */
+@Data
+public class ConsumptionEventReceive implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String ipAddress;
+    private String ipv6Address;
+    private Integer portNo;
+    private String protocol;
+    private String macAddress;
+    private Integer channelID; /*ro, opt, int, 触发报警的设备通道号, desc:触发的视频通道号(1、    在SDK透传ISAPI协议的时候,上传的是 私有协议对应的视频通道号;(2、    在萤石透传ISAPI协议的时候,上传的是 萤石协议对应的视频通道号;(3、    在ISUP透传ISAPI协议的时候,上传的是 ISUP协议对应的视频通道号;*/
+    private Date dateTime;
+    private Integer activePostCount;
+    private String eventType;
+    private String eventState; /*[active#有效事件,inactive#无效事件]*/
+    private String eventDescription;
+
+    private ConsumptionEventDetail  ConsumptionEvent;
+}

+ 30 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/EventResponseInfo.java

@@ -0,0 +1,30 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class EventResponseInfo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private Integer statusCode;
+
+    private String statusString;
+
+    private String subStatusCode;
+
+    private Integer errorCode;
+
+    private String errorMsg;
+
+    private String MErrCode;
+
+    private String MErrDevSelfEx;
+
+
+
+}

+ 19 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/FileContent.java

@@ -0,0 +1,19 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class FileContent implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String suffix;
+    private byte[] content;
+}

+ 24 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/HeatBeatData.java

@@ -0,0 +1,24 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+public class HeatBeatData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String portNo;
+    private String protocol;
+    private Date dateTime;
+    private String macAddress;
+    private String eventState;
+    private String ipAddress;
+    private String eventDescription;
+    private String activePostCount;
+    private String eventType;
+}

+ 19 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/PictureResolution.java

@@ -0,0 +1,19 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class PictureResolution implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /*ro, opt, object, 图片资源的分辨率, desc:归一化坐标转换成实际分辨率时依赖此节点。*/
+    private Integer height;
+    /*ro, req, int, 分辨率高, range:[1,65535], unit:px*/
+    private Integer width;
+    /*ro, req, int, 分辨率框, range:[1,65535], unit:px*/
+
+}

+ 19 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/RecordImageInfo.java

@@ -0,0 +1,19 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class RecordImageInfo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /*ro, opt, object, 图片节点, desc:当报文中带有图片时,该节点必须返回,指消费认证时的人脸抓拍图*/
+    private String resourcesContentType;
+    /*ro, req, enum, 源传输类型, subType:string, [url#url方式传输,binary#二进制方式传输]*/
+    private String resourcesContent;
+    /*ro, req, string, 资源标识, desc:当报文中带有图片时,该节点必须返回:当resourcesContentType为binary时,该节点与图片的Content-ID严格对应;当resourcesContentType为url时,该节点填写具体的url,使用该URI从设备下载文件的时候,需要进行认证,认证方式和登录设备的方式保持一致。如果该URI的参数中有token(?token=)可以直接请求,除token以外的其他方式,认证方式为登录设备的方式。设备返回URI中的IP和Port,仅代表设备直连网络下可以被访问,建议集成方基于设备网络部署,替换URI中的IP和Port;设备实际上传是随机字符串。*/
+    private PictureResolution pictureResolution;
+}

+ 43 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventConfirmBo.java

@@ -0,0 +1,43 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 应答TransactionRecordEvent 的 参数类
+ */
+@Data
+public class TransactionRecordEventConfirmBo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /*req, object, 报文*/
+    private Integer serialNo;
+    /*req, int, 消费流水号, range:[1,2100000000], step:1, unitType:无*/
+    private String result;
+    /*req, enum, 结果, subType:string, [success#成功,failed#失败]*/
+    private String employeeNo;
+    /*opt, string, 工号, range:[1,32], desc:敏感信息加密*/
+    private String balance;
+    /*opt, string, 余额, range:[0,8], desc:1、单位:分,敏感信息加密。2、应用场景:设备从离线消费转在线消费时,平台返回消费人员的余额和余次,供设备进行记录刷新。*/
+    private String actualPayment;
+    //*opt, string, 实付金额(单位:分)*//*
+   /* private Integer remainingTimes;
+    *//*opt, int, 余次*//*
+    private String currencyType;
+    *//*opt, enum, 币种类型, subType:string, [CNY#人民币,USD#美元,EUR#欧元,GBP#英镑,HKD#中国香港元,SGD#新加坡元,KRW#元(韩元),PLN#波兰兹罗提,BYR#白俄罗斯卢布,RUB#俄罗斯卢布,THB#泰国泰铢,TRY#土耳其里拉,VND#越南盾,ILS#以色列新谢克尔,UAH#乌克兰格里夫纳,CZK#捷克克朗,DDK#丹麦克朗,NOK#挪威克朗,SEK#瑞典克朗,BRL#巴西雷亚尔,IQD#伊拉克第纳尔,IDR#印尼卢比,INR#印度卢比,AED#阿联酋迪拉姆,IRR#伊朗土曼,BGN#保加利亚列弗,HUF#匈牙利福林,MYR#马来西亚林吉特,MXN#墨西哥比索], desc:默认是USD。该字段和decimalPlace字段搭配使用,无该字段和decimalPlace字段表示使用的是人民币,小数位为2*//*
+    private Integer decimalPlace;
+    *//*opt, int, 小数位, range:[0,3], desc:表示货币的小数位,默认为2*//*
+    private String actualPayment;
+    *//*opt, string, 实付金额(单位:分)*//*
+    private Boolean isCombinationPayment;
+    *//*opt, bool, 是否为组合支付方式, desc:不返回该字段表示不是组合支付方式*//*
+    private String platformAmount;
+    *//*opt, string, 组合支付方式下平台扣费金额, dep:or,{$.TransactionRecordEventConfirm.isCombinationPayment,eq,true}, desc:敏感信息加密*//*
+    private String threePartyAmount;
+    *//*opt, string, 组合支付方式下三方支付扣费金额, dep:or,{$.TransactionRecordEventConfirm.isCombinationPayment,eq,true}, desc:三方支付包含微信、支付宝、工行数字人民币,desc:敏感信息加密*//*
+    private Integer times;
+    *//*wo, opt, int, 将要抵扣的次数*/
+}

+ 92 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventDetail.java

@@ -0,0 +1,92 @@
+package org.dromara.server.hik.event.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class TransactionRecordEventDetail implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /*ro, req, object, 事件信息*/
+    private String remoteHostAddr;
+    /*ro, opt, string, 远程主机地址*/
+    private Integer serialNo;
+    /*ro, req, int, 交易记录流水号, desc:交易预处理、确认请求流水号保持一致,与交易记录流水号保持一致*/
+    private Integer frontSerialNo;
+    /*ro, req, int, 上一条交易记录流水号*/
+    private String name;
+    /*ro, opt, string, 姓名, range:[0,128]*/
+    private String authenticationMode;
+    /*ro, opt, enum, 认证模式, subType:string, [QRCode#二维码,employeeNo#工号,card#卡片],desc:该字段不存在时,解析优先级:工号>卡号>二维码类型
+    该字段存在时,以该字段为准*/
+    private String employeeNoString;
+    /*ro, opt, string, 工号, range:[0,32], dep:and,{$.ConsumptionEvent.authenticationMode,eq,employeeNo}, desc:当QRCodeType为私有二维码时,工号也可以返回*/
+    private String cardNo;
+    /*ro, opt, string, 卡号, range:[0,32], dep:and,{$.ConsumptionEvent.authenticationMode,eq,card}, desc:当QRCodeType为私有二维码时,工号也可以返回*/
+    private String QRCodeType;
+    /*ro, opt, enum, 二维码类型, subType:string, [WeChat#微信,Alipay#支付宝,Private#私有二维码,DigitalCurrencyICBC#工行数字人民币,integrationPayment#聚合支付], dep:and,{$.ConsumptionEvent.authenticationMode,eq,QRCode}*/
+    private String paymentMethod;
+    /*ro, opt, enum, 支付方式, subType:string, [weChat#微信,alipay#支付宝,digitalCurrencyICBC#工行数字人民币,integrationPayment#聚合支付]*/
+    private String modeType;
+    /*ro, opt, enum, 交易模式类型, subType:string, [current#实时交易,offLine#离线记账交易]*/
+    private Boolean isBindSupplement;
+    /*ro, opt, bool, 是否是绑盘称重消费补扣交易, desc:绑盘称重消费,实际上是属于离线记账交易的一种消费场景。如果某个菜品因为称重机异常没有扣费,支持后续补扣,补扣订单会生成一个新的交易流水号,并关联原订单流水号*/
+    private Integer relationSerialNo;
+    /*ro, opt, int, 绑盘称重消费补扣时关联的交易流水号, dep:or,{$.TransactionRecordEvent.isBindSupplement,eq,true}, desc:,补扣订单所关联的原订单流水号*/
+    private Boolean localCheck;
+    /*ro, req, bool, 是否本地校验, desc:true-校验(默认),false-未校验*/
+    private String type;
+    /*ro, req, enum, 交易类型, subType:string, [transaction#交易,refund#纠错]*/
+    private String refundSerialNo;
+    /*ro, opt, int, 需要纠错的流水号, desc:type为refund该字段有效,且必须返回*/
+    private String verifyMode;
+    /*ro, opt, enum, 验证方式, subType:string, [card#刷卡,face#刷脸]*/
+    private String mode;
+    /*ro, req, enum, 消费模式, subType:string, [amount#金额,quota#定额,count#计次,setMeal#套餐,box#格口消费]*/
+    private String totalPayment;
+    /*ro, opt, string, 应付金额(单位:分),
+    desc:当交易类型为交易、且mode为amount或quota或box时,该字段必填,表示交易金额;当交易类型为纠错类型、且mode为amount或quota或setMeal时,该字段必填,并与纠错金额refundPayment结合使用,使用方法:平台对账户先加上纠错金额再减去应付金额
+    纠错举例1:(修改消费金额)
+    比如第一次消费10元。
+    发起纠错。应该消费5元。
+    那么应付金额应该是5,纠错金额是10.
+    这样平台对账户先加上纠错金额10,再减去应付金额5.*/
+    private String refundPayment;
+    /*ro, opt, string, 纠错金额(单位:分), desc:1、当交易类型为纠错类型、且mode为amount或quota或setMeal时,该字段必填,并与应付金额结合使用,使用方法:平台对账户先加上纠错金额再减去应付金额;2、当交易类型为纠错类型、且mode为count时,该字段表示纠错回退次数*/
+    private String actualPayment;
+    /*ro, opt, string, 实付金额(单位:分), desc:当交易类型为交易、且mode为amount或quota或setMeal时有效*/
+    private Boolean isCombinationPayment;
+    /*ro, opt, bool, 是否为组合支付方式, desc:不返回该字段表示不是组合支付方式*/
+    private String platformAmount;
+    /*ro, opt, string, 组合支付方式下平台扣费金额, dep:or,{$.TransactionRecordEvent.isCombinationPayment,eq,true}*/
+    private String threePartyAmount;
+    /*ro, opt, string, 组合支付方式下三方支付扣费金额, dep:or,{$.TransactionRecordEvent.isCombinationPayment,eq,true}, desc:三方支付包含微信、支付宝、工行数字人民币*/
+    private String balanceAfterDeduct;
+    /*ro, opt, string, 本次交易后余额(单位:分)*/
+    private Integer times;
+    /*ro, opt, int, 已消费次数, desc:本字段后续废弃。老设备实现逻辑:已配置消费计划模版,且当前时间段处于计次模式,表示时间段内人员的累积消费次数,时段变化时会重新从1累加。除上述情况times均是1。*/
+    private Integer actualTimes;
+    /*ro, opt, int, 实际(本次)消费次数, desc:与消费事件中的times(将要抵扣的次数)保持一致,新设备一律实现本字段,且平台集成时优先解析本字段。*/
+    private Integer remainingTimes;
+    /*ro, opt, int, 剩余次数*/
+
+    private String boxID;
+    /*ro, opt, string, 格口编号, range:[0,7], dep:and,{$.TransactionRecordEvent.mode,eq,box},
+    desc:第1位 L 表示格口箱在控制柜左侧,R表示格口箱在控制柜右侧,和控制柜同一列为M
+    2,3位表示格口箱编号
+    4,5位表示格口所在格口箱的列号,靠近控制柜的为第一列
+    6,7位表示格口的行号,从上至下排序*/
+    private RecordImageInfo RecordImage;
+
+    private String paymentOrderNo;
+    /*ro, opt, string, 三方支付订单编号, range:[1,64], desc:消费时,若为移动支付,设备需上传该字段,传输的是移动支付(微信、支付宝、工号数字人民币)时的订单编号。*/
+    private String merchantName;
+    /*ro, opt, string, 商户名称, range:[0,256], desc:当为移动支付时(设备的三方支付),可上传该字段,与paymentOrderNo配合使用,可进行后续消费查询与追溯。*/
+    private String currencyType;
+    /*ro, opt, enum, 币种类型, subType:string, [CNY#人民币,USD#美元,EUR#欧元,GBP#英镑,HKD#中国香港元,SGD#新加坡元,KRW#元(韩元),PLN#波兰兹罗提,BYR#白俄罗斯卢布,RUB#俄罗斯卢布,THB#泰国泰铢,TRY#土耳其里拉,VND#越南盾,ILS#以色列新谢克尔,UAH#乌克兰格里夫纳,CZK#捷克克朗,DDK#丹麦克朗,NOK#挪威克朗,SEK#瑞典克朗,BRL#巴西雷亚尔,IQD#伊拉克第纳尔,IDR#印尼卢比,INR#印度卢比,AED#阿联酋迪拉姆,IRR#伊朗土曼,BGN#保加利亚列弗,HUF#匈牙利福林,MYR#马来西亚林吉特,MXN#墨西哥比索], desc:默认是USD。该字段和decimalPlace字段搭配使用,无该字段和decimalPlace字段表示使用的是人民币,小数位为2*/
+    private Integer decimalPlace;
+    /*ro, opt, int, 小数位, range:[0,3], desc:表示货币的小数位,默认为2*/
+
+}

+ 31 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventReceive.java

@@ -0,0 +1,31 @@
+package org.dromara.server.hik.event.domain;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 接收 设备上报TransactionRecordEvent 的内容
+ */
+@Data
+public class TransactionRecordEventReceive implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String ipAddress;
+    private String ipv6Address;
+    private Integer portNo;
+    private String protocol;
+    private String macAddress;
+    private Integer channelID; /*ro, opt, int, 触发报警的设备通道号, desc:触发的视频通道号(1、    在SDK透传ISAPI协议的时候,上传的是 私有协议对应的视频通道号;(2、    在萤石透传ISAPI协议的时候,上传的是 萤石协议对应的视频通道号;(3、    在ISUP透传ISAPI协议的时候,上传的是 ISUP协议对应的视频通道号;*/
+    private Date dateTime;
+    private Integer activePostCount;
+    private String eventType;
+    private String eventState; /*[active#有效事件,inactive#无效事件]*/
+    private String eventDescription;
+    private Integer picturesNumber;
+    private TransactionRecordEventDetail TransactionRecordEvent;
+}

+ 193 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/ConsumptionEventHandler.java

@@ -0,0 +1,193 @@
+package org.dromara.server.hik.event.handler;
+
+import com.alibaba.fastjson.JSONObject;
+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.common.core.enums.SystemUseTypeEnum;
+import org.dromara.common.core.utils.StringUtils;
+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.vo.XfTermVo;
+import org.dromara.server.hik.enums.ConsumeModeEnum;
+import org.dromara.server.hik.event.HikEventHandler;
+import org.dromara.server.hik.event.domain.ConsumptionEventConfirmBo;
+import org.dromara.server.hik.event.domain.ConsumptionEventDetail;
+import org.dromara.server.hik.event.domain.ConsumptionEventReceive;
+import org.dromara.server.hik.event.domain.FileContent;
+import org.dromara.server.hik.service.IXfTermService;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ConsumptionEventHandler implements HikEventHandler {
+
+    public static final String TRANSACTION_PREPROCESSING_REQUEST = "transactionPreprocessingRequest";
+    public static final String TRANSACTION_CONFIRMING_REQUEST = "transactionConfirmingRequest";
+
+    private final IXfTermService termService;
+
+    @DubboReference
+    private final RemoteConsumeService remoteConsumeService;
+
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        ConsumptionEventReceive receive = jsonObject.toJavaObject(ConsumptionEventReceive.class);
+        ConsumptionEventDetail consumptionEvent = receive.getConsumptionEvent();
+        log.info("消费机事件,类型:{}", consumptionEvent.getMinor());
+        XfTermVo termVo = termService.getByMac(receive.getMacAddress());
+        if (TRANSACTION_PREPROCESSING_REQUEST.equals(consumptionEvent.getMinor())) {
+            if (termVo == null) {
+                log.error("消费机交易记录事件,设备信息为空,mac:{}, 不存在系统中", receive.getMacAddress());
+                // 直接应答失败
+                return answerEvent(consumptionEvent, false,null, null, "设备未注册", "");
+            }
+            /*
+             * 消费机 交易预处理请求事件上报
+             * eventType: ConsumptionEvent
+             * minor:transactionPreprocessingRequest
+             */
+            // 业务逻辑处理的结果
+            boolean bussiRs  = true;
+            // 处理交易预处理请求事件上报 根据业务数据校验 能不能消费 权限、账号、金额、设备、业务数据
+            // 调用现有原始消费请求接口
+            RemoteConsumeBo remoteBo = setParamBo(receive, consumptionEvent);
+            remoteBo.setTermNo(termVo.getTermNo());
+            RemoteResultDto result = remoteConsumeService.dealHikRequestConsume(remoteBo);
+            R<ErrorInfo> errorInfo = result.getErrorInfo();
+            RemoteConsumeBo updatedRemoteBo = result.getUpdatedRemoteBo();
+            String reason = "";
+            BigDecimal balance = updatedRemoteBo.getBalance();
+            BigDecimal consumeMoney = updatedRemoteBo.getConsumeMoney();
+//            BigDecimal consumeMoney = new BigDecimal("10");
+            String deptName =updatedRemoteBo.getDeptName();
+            if (R.isError(errorInfo)) {
+                // 应答失败
+                bussiRs = false;
+                reason = errorInfo.getMsg();
+                ErrorInfo data = errorInfo.getData();
+                if(data != null){
+                    reason = data.getMessage();
+                }
+            }
+            // 应答
+            return answerEvent(consumptionEvent, bussiRs, balance,consumeMoney, reason,deptName);
+        }else if(TRANSACTION_CONFIRMING_REQUEST.equals(consumptionEvent.getMinor())){
+            HashMap<String, Object> rs = new HashMap<>(1);
+            rs.put("result", "success");
+            /*
+              消费机 交易确认请求事件上报
+              eventType: ConsumptionEvent
+              minor:transactionConfirmingRequest
+             */
+            Boolean cancel = consumptionEvent.getCancel();
+            if(cancel !=null && cancel){
+                log.info("消费机消费请求确认事件,取消交易");
+                return rs;
+            }
+
+            // 进行扣费逻辑处理 调用现有的消费入库的接口
+            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();
+            if (R.isError(errorInfo)) {
+                // 应答失败
+                rs.put("result", "failed");
+            }
+            return rs;
+        }else{
+            log.warn("消费机事件,未知类型:{}", consumptionEvent.getMinor());
+        }
+
+        return null;
+    }
+
+    private RemoteConsumeBo setParamBo(ConsumptionEventReceive receive, ConsumptionEventDetail consumptionEvent) {
+        RemoteConsumeBo remoteBo = new RemoteConsumeBo();
+        TransactionRecordEventHandler.setCommonFields(remoteBo, consumptionEvent.getEmployeeNoString(), consumptionEvent.getCardNo(),
+            receive.getDateTime(), consumptionEvent.getSerialNo(), consumptionEvent.getTotalPayment(), receive.getMacAddress());
+        remoteBo.setCardNo(0L);
+        remoteBo.setStatusFlag(4);
+        remoteBo.setUseType(SystemUseTypeEnum.CONSUME.code());
+        remoteBo.setCreditType(CreditTypeEnum.TERM_CONSUME.code());
+        return remoteBo;
+    }
+
+
+    public Map<String, Object>  answerEvent(ConsumptionEventDetail consumptionEvent, boolean answerResult, BigDecimal balance,BigDecimal consumeMoney, String reason,String deptName)
+    {
+        ConsumptionEventConfirmBo confirmBo = new ConsumptionEventConfirmBo();
+        confirmBo.setSerialNo(consumptionEvent.getSerialNo());
+        confirmBo.setResult(answerResult ?"success":"failed");
+        confirmBo.setMode(consumptionEvent.getMode());
+        log.info("消费机交易预处理请求事件,消费模式:{}", consumptionEvent.getMode());
+        confirmBo.setName(consumptionEvent.getName());
+        confirmBo.setEmployeeNoString(consumptionEvent.getEmployeeNoString());
+        if(StringUtils.isNotBlank(consumptionEvent.getCardNo())){
+            confirmBo.setCardNo(consumptionEvent.getCardNo());
+        }else{
+            log.info("消费机交易预处理请求事件,方式:{}", "人脸或者二维码,不带卡号");
+        }
+        boolean isCountMode = ConsumeModeEnum.count.getCode().equals(consumptionEvent.getMode());
+        if(isCountMode){
+            confirmBo.setTimes(0);
+            if(answerResult){
+                confirmBo.setTimes(1);
+            }
+        }else{
+            if(answerResult){
+                BigDecimal con = consumeMoney.multiply(new BigDecimal("100"));
+                confirmBo.setActualPayment(con.toString().split("\\.")[0]);
+                BigDecimal balanceBeforeDeduct = balance.multiply(new BigDecimal("100")).add(con);
+                confirmBo.setBalanceBeforeDeduct(balanceBeforeDeduct.toString().split("\\.")[0]); // 未扣款前的余额,要根据余额加上扣款金额,单位为分 ,金额模式必填
+            }else{
+                confirmBo.setActualPayment("0");
+                confirmBo.setBalanceBeforeDeduct("0");
+            }
+        }
+
+        String broadcastVoice = isCountMode ? "刷卡成功":"支付成功";
+        String newReason = reason;
+        if(!answerResult){
+            broadcastVoice = isCountMode ? "刷卡失败":"支付失败";
+            // 提示刷卡失败的原因
+            if(StringUtils.isNotBlank(reason) && reason.length() > 30){
+                log.info("海康消费失败原因:{}", reason);
+                newReason = reason.substring(0,30);
+            }
+            broadcastVoice += newReason;
+            confirmBo.setReason("platformException");
+        }
+        confirmBo.setCustomTTSBroadcastVoice(broadcastVoice);
+
+        /*
+          这可以显示刷卡人的信息,比如姓名,部门等
+         */
+        ConsumptionEventConfirmBo.ContentInfo contentInfo = new ConsumptionEventConfirmBo.ContentInfo();
+        contentInfo.setTitle(consumptionEvent.getName());
+        contentInfo.setContent(deptName);
+        if(!answerResult){
+            contentInfo.setTitle(consumptionEvent.getName()+ "  "+deptName);
+            contentInfo.setContent("失败原因:"+ newReason);
+        }
+        confirmBo.setContentInfo(contentInfo);
+
+        // confirmBo 转Map,如果字段为空则丢弃
+        HashMap<String, Object> rs = new HashMap<>(1);
+        rs.put("ConsumptionEventConfirm", confirmBo.toMap());
+        return rs;
+    }
+}

+ 92 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/HeatBeatHandler.java

@@ -0,0 +1,92 @@
+package org.dromara.server.hik.event.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import io.seata.common.util.StringUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.RemoteUserAccountService;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.constant.CacheConstants;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.server.hik.domain.vo.XfTermVo;
+import org.dromara.server.hik.event.HikEventHandler;
+import org.dromara.server.hik.event.domain.FileContent;
+import org.dromara.server.hik.event.domain.HeatBeatData;
+import org.dromara.server.hik.service.ISendDeviceService;
+import org.dromara.server.hik.service.IXfTermService;
+import org.springframework.stereotype.Component;
+
+import java.time.*;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HeatBeatHandler implements HikEventHandler {
+
+    public final IXfTermService xfTermService;
+
+    public final ISendDeviceService sendDeviceService;
+
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        // 1.JSONObject 转换成心跳 HeadBeatData对象
+        HeatBeatData heatBeatData = jsonObject.toJavaObject(HeatBeatData.class);
+        log.info("接收到心跳数据:{}", heatBeatData);
+
+        //更新设备的IP地址
+        // mac -> ip 和 心跳的IP 是否相等,不相等则更新
+        String ipAddress = heatBeatData.getIpAddress();
+        String macAddress = heatBeatData.getMacAddress();
+        XfTermVo termVo = xfTermService.getByMac(macAddress);
+        if(termVo == null) return null;
+        if(!StringUtils.equals(ipAddress, termVo.getTermIp())){
+            log.info("设备IP地址更新:{} -> {}", ipAddress, termVo.getTermIp());
+            xfTermService.updateByMac(macAddress, ipAddress);
+        }
+
+        //增量下发 人脸数据和卡数据
+        Long cacheObject = RedisUtils.getCacheObject(CacheNames.XF_TERM_IP + ipAddress);
+        if(cacheObject == null){
+            // 第一次上线 默认只下发最近1天的 新增或修改的数据
+            //记录每天第一次上来的心跳数据时 下发最近3天的 新增或修改的数据
+            //        t_pt_useraccount表 update_time  更新人脸
+            //        t_pt_card表   change_time、create_time  更新卡片
+            //下发数据
+            // 获取当前时间的前一天的时间
+            LocalDateTime localDate = LocalDateTime.now();
+            LocalDateTime minus = localDate.minus(1, ChronoUnit.DAYS);
+            // LocalDateTime 转 date
+            Date date = Date.from(minus.atZone(ZoneId.systemDefault()).toInstant());
+//            System.err.println("minus: " + minus);
+//            System.err.println("date: " + date);
+            sendDeviceService.upLoadEmpToDevice(macAddress,date,true);
+
+            RedisUtils.setCacheObject(CacheNames.XF_TERM_IP + ipAddress, date.getTime());
+        }else{
+            // 当前时间-上次下发时间 >= 1天 就下发
+            Date now = new Date();
+            Date last = new Date(cacheObject);
+            // 舍弃时分秒,只保留日期部分
+            LocalDate nowDate = now.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+            LocalDate lastDate = last.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+//            System.err.println("nowDate: " + nowDate);
+//            System.err.println("lastDate: " + lastDate);
+            // 计算两个日期之间的天数差
+            long daysBetween = ChronoUnit.DAYS.between(lastDate, nowDate);
+
+            // 判断是否相差 1 天或以上
+            if (daysBetween >= 1) {
+                // 相差一天以上,执行下发逻辑
+                sendDeviceService.upLoadEmpToDevice(macAddress, last, true);
+                RedisUtils.setCacheObject(CacheNames.XF_TERM_IP + ipAddress, now.getTime());
+            }
+        }
+
+        return null;
+    }
+}

+ 221 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/handler/TransactionRecordEventHandler.java

@@ -0,0 +1,221 @@
+package org.dromara.server.hik.event.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import io.seata.common.util.CollectionUtils;
+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.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.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.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.*;
+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.*;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionRecordEventHandler implements HikEventHandler {
+
+    private final IXfTermService termService;
+
+    private final IXfConsumeDetailService  consumeDetailService;
+
+    private final IConsumeDetailOriginalService consumeDetailOriginalService;
+
+    private final IXfFailedRecordService failedRecordService;
+
+    @Value("${upload.upload-path}/")
+    private String uploadPath;
+
+    @DubboReference
+    private final RemoteConsumeService remoteConsumeService;
+
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        TransactionRecordEventReceive transactionRecordEventReceive = jsonObject.toJavaObject(TransactionRecordEventReceive.class);
+        TransactionRecordEventDetail transactionRecordEvent = transactionRecordEventReceive.getTransactionRecordEvent();
+        String modeType = transactionRecordEvent.getModeType();
+        log.info("消费机交易记录事件,类型,在线or离线:{},{}", modeType, jsonObject.toJSONString());
+
+        //设备信息
+        XfTermVo termVo = termService.getByMac(transactionRecordEventReceive.getMacAddress());
+        Long termNo = termVo.getTermNo();
+        // 消费抓拍图片保存
+        if (fileContent != null) {
+            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);
+            if(!image_dir.exists()){
+                image_dir.mkdir();
+            }
+            FileUtils.writeBytes(content, image_dir_path+fileName);
+        }
+
+        BigDecimal balance = null;
+        String actualPayment = transactionRecordEvent.getActualPayment();
+        BigDecimal consumeMoney = BigDecimal.ZERO;
+        if(StringUtils.isNotBlank(actualPayment)){
+            balance = new BigDecimal(actualPayment).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
+        }
+        // 这里只能应答成功,如果是业务异常(余额不足、脏数据等)失败,要存入表中,使用定时任务处理失败,所以即使失败的 也要回复成功
+        // 如果是在线交易的,平台根据流水号判断当前交易记录事件是否为未处理事件,若为未处理事件,则平台进行处理并进行扣费;
+        // 如果是离线交易记录事件,则平台进行处理并进行扣费;
+        RemoteConsumeBo remoteBo = getRemoteBo(transactionRecordEventReceive);
+        remoteBo.setTermNo(termNo);
+        R<ErrorInfo> errorInfo = R.ok("处理成功");
+        try  {
+            if (ModeTypeEnum.offLine.getCode().equals(modeType)) {
+                // 离线交易,调用原始消费记录请求+消费记录入库的接口,如果失败要记录入库
+                RemoteResultDto remoteResultDto = remoteConsumeService.dealHikUploadOffLineRecord(remoteBo);
+                errorInfo = remoteResultDto.getErrorInfo();
+                RemoteConsumeBo updatedRemoteBo = remoteResultDto.getUpdatedRemoteBo();
+                if(updatedRemoteBo !=null) {
+                    balance  = updatedRemoteBo.getBalance();
+                    consumeMoney = updatedRemoteBo.getConsumeMoney();
+                }
+            }else{
+                // 在线交易,调用消费记录入库的接口
+                // 根据机号和交易记录流水号查询消费记录
+                // 睡眠一秒,等待消费记录入库成功
+                Thread.sleep(1000);
+                List<XfConsumeDetailVo> vos = consumeDetailService.queryByTermNoAndRecordId(termNo, transactionRecordEvent.getSerialNo().longValue());
+                if (CollectionUtils.isEmpty(vos)){
+                    // 组装参数,调用消费记录入库的接口 // 如果失败要记录入库
+                    RemoteResultDto remoteResultDto = remoteConsumeService.dealHikUploadRecord(remoteBo);
+                    errorInfo = remoteResultDto.getErrorInfo();
+                    RemoteConsumeBo updatedRemoteBo = remoteResultDto.getUpdatedRemoteBo();
+                    if(updatedRemoteBo !=null) {
+                        balance  = updatedRemoteBo.getBalance();
+                        consumeMoney = updatedRemoteBo.getConsumeMoney();
+                    }
+                }else{
+                    XfConsumeDetailVo consumeDetailVo = vos.get(0);
+                    balance = consumeDetailVo.getCardValue();
+                    consumeMoney = consumeDetailVo.getConsumeMoney();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("消费机交易记录事件处理异常:{},参数:{}", e.getMessage(),  JSONObject.toJSONString(remoteBo));
+            // 失败,记录入库
+            errorInfo = R.fail(e.getMessage());
+        }
+
+        insertFailedRecordBo(transactionRecordEventReceive, termNo, errorInfo, modeType);
+        //应答
+        return answerEvent(transactionRecordEventReceive,balance,consumeMoney);
+    }
+
+    /**
+     * @param info 事件信息
+     * 应答事件 put /ISAPI/Consume/transactionRecordEventConfirm?format=json 应答TransactionRecordEvent
+     */
+    public Map<String, Object> answerEvent(TransactionRecordEventReceive info, BigDecimal balance, BigDecimal consumeMoney){
+        TransactionRecordEventConfirmBo confirmBo = new TransactionRecordEventConfirmBo();
+
+        TransactionRecordEventDetail transactionDetail  = info.getTransactionRecordEvent();
+        confirmBo.setSerialNo(transactionDetail.getSerialNo());
+        confirmBo.setResult("success");
+        confirmBo.setEmployeeNo(transactionDetail.getEmployeeNoString());
+        //余额的单位为分
+        // 使用 BigDecimal 的 null 安全处理工具类简化逻辑,并避免重复代码
+        BigDecimal newBalance = Optional.ofNullable(balance).orElse(BigDecimal.ZERO);
+        confirmBo.setBalance(newBalance.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.DOWN).toString());
+
+        BigDecimal newConsumeMoney = Optional.ofNullable(consumeMoney).orElse(BigDecimal.ZERO);
+        confirmBo.setActualPayment(newConsumeMoney.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.DOWN).toString());
+
+        HashMap<String, Object> map = new HashMap<>(1);
+        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);
+    }
+
+}

+ 123 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/event/timedtask/HandleTask.java

@@ -0,0 +1,123 @@
+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事件 业务失败的记录 于凌晨执行即可
+ */
+@RequiredArgsConstructor
+@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事件的消费失败记录,");
+        //  获取所有失败的 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);
+            }
+        }
+    }
+}

+ 21 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/ConsumeDetailOriginalMapper.java

@@ -0,0 +1,21 @@
+package org.dromara.server.hik.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.hik.domain.XfConsumeDetailOriginal;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailOriginalVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 原始消费记录Mapper接口
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+public interface ConsumeDetailOriginalMapper extends BaseMapperPlus<XfConsumeDetailOriginal, XfConsumeDetailOriginalVo> {
+
+//    List<ConsumptionBo> selectReconciliationData(@Param("consumeDate") Date consumeDate);
+}

+ 13 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mapper/XfConsumeDetailMapper.java

@@ -0,0 +1,13 @@
+package org.dromara.server.hik.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.server.hik.domain.XfConsumeDetail;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailVo;
+
+/**
+ * 消费明细Mapper接口
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+public interface XfConsumeDetailMapper extends BaseMapperPlus<XfConsumeDetail, XfConsumeDetailVo> {}

+ 15 - 0
ruoyi-server/ruoyi-server-face/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> {
+
+}

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

@@ -0,0 +1,15 @@
+package org.dromara.server.hik.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.server.hik.domain.XfTerm;
+import org.dromara.server.hik.domain.vo.XfTermVo;
+
+/**
+ * 消费设备Mapper接口
+ *
+ * @author bing
+ * @date 2024-08-21
+ */
+public interface XfTermMapper extends BaseMapperPlus<XfTerm, XfTermVo> {
+
+}

+ 95 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/consumer/HikKafkaConsumer.java

@@ -0,0 +1,95 @@
+package org.dromara.server.hik.mq.consumer;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.dromara.backstage.api.domain.bo.RemoteSendMessageRecordBo;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.message.kafka.domain.KafkaHeader;
+import org.dromara.common.message.kafka.domain.KafkaMessage;
+import org.dromara.server.base.service.yktOperation.SyncRemoteSendMessageRecordService;
+import org.dromara.server.hik.mq.event.IHIKEventStrategy;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+import static org.dromara.common.message.kafka.constant.KafkaTopicConstants.SYNC_DATA_TOPIC;
+
+/**
+ * 海康服务消费kafka
+ * <p>
+ *  海康消费服务作为消费者消费人员与卡片的kafka消息
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-25
+ * @since JDK17
+ */
+@RequiredArgsConstructor
+@Slf4j
+@Component
+@ConditionalOnExpression("'local'.equals('${locationFlag}')")
+public class HikKafkaConsumer {
+    private final SyncRemoteSendMessageRecordService syncRemoteSendMessageRecordService;
+
+    @Value("${system.default-config.tenantId}")
+    private String tenantId;
+
+    /**
+     * 一卡通云端业务操作本地同步处理
+     * @param record kafka消息
+     */
+    @KafkaListener(topics = SYNC_DATA_TOPIC, groupId = "HIK_${system.default-config.tenantId}")
+    public void cloudOperationSync(ConsumerRecord<String, String> record){
+        String value = record.value();
+
+        KafkaMessage<?> receiveMsg = JSONUtil.toBean(value, KafkaMessage.class);
+        try{
+            KafkaHeader header = receiveMsg.getHeader();
+            String tenantId;
+            if (StringUtils.isNotEmpty(header.getTenantId())) {
+                tenantId = header.getTenantId();
+            } else {
+                JSONObject bodyObj = JSONUtil.parseObj(receiveMsg.getBody());
+                tenantId = bodyObj.getStr("tenantId");
+            }
+            if(tenantId.equals(this.tenantId)){
+                String eventType = receiveMsg.getHeader().getEventType();
+                String sender = receiveMsg.getHeader().getSender();
+                IHIKEventStrategy eventStrategy = SpringUtils.getBean(sender, IHIKEventStrategy.class);
+                eventStrategy.doMsgHandle(eventType, receiveMsg.getBody());
+                try {
+                    syncRemoteSendMessageRecordService.insertSendMessageRecord(initBo(receiveMsg, "Y"));
+                }catch (Exception e){
+                    log.error("消息发送记录更新失败", e);
+                }
+            }
+        } catch (Exception e){
+            try {
+                syncRemoteSendMessageRecordService.insertSendMessageRecord(initBo(receiveMsg, "N"));
+            } catch (Exception ex) {
+                log.error("消息发送记录更新失败", e);
+            }
+            log.error("[kafka消息处理失败]-[消息:{}-[错误:{}]", record.value(), e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 初始化消息记录Bo
+     * @param consumeStatus 消费状态
+     */
+    private static RemoteSendMessageRecordBo initBo(KafkaMessage<?> msg, String consumeStatus){
+        RemoteSendMessageRecordBo bo = new RemoteSendMessageRecordBo();
+        KafkaHeader header = msg.getHeader();
+        bo.setEventId(header.getEventId());
+        bo.setSender(header.getSender());
+        bo.setEventType(header.getEventType());
+        bo.setMessage(msg.getBody().toString());
+        bo.setConsumeStatus(consumeStatus);
+        return bo;
+    }
+}

+ 20 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/event/IHIKEventStrategy.java

@@ -0,0 +1,20 @@
+package org.dromara.server.hik.mq.event;
+
+/**
+ * 海康服务消费kafka策略
+ * <p>
+ *  海康消费服务作为消费者消费人员与卡片的kafka消息时的消息处理策略
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-25
+ * @since JDK17
+ */
+public interface IHIKEventStrategy {
+    /**
+     *
+     * @param eventType 事件类型
+     * @param msg 消息体
+     */
+    void doMsgHandle(String eventType,Object msg) throws Exception;
+}

+ 63 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/mq/event/impl/HikBackStageEventImpl.java

@@ -0,0 +1,63 @@
+package org.dromara.server.hik.mq.event.impl;
+
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.enums.CardStatusEnum;
+import org.dromara.common.message.kafka.constant.EventSenderConstants;
+import org.dromara.common.message.kafka.constant.EventTypeConstants;
+import org.dromara.server.hik.domain.dto.remote.RemoteCardDto;
+import org.dromara.server.hik.mq.event.IHIKEventStrategy;
+import org.dromara.server.hik.service.ISendDeviceService;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+/**
+ * 海康服务消费管理平台kafka消息策略实现
+ * <p>
+ *  海康消费服务作为消费者消费人员与卡片的kafka消息时的消息处理策略实现
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-25
+ * @since JDK17
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service(EventSenderConstants.BACKSTAGE)
+public class HikBackStageEventImpl implements IHIKEventStrategy {
+    private final ISendDeviceService sendDeviceService;
+
+    // 实时下发数据不下发人脸,避免引起卡机
+    @Override
+    public void doMsgHandle(String eventType, Object msg) {
+        switch (eventType) {
+            case EventTypeConstants.ACCOUNT -> {
+                RemoteUserAccountVo remoteVo = JSONUtil.toBean(JSONUtil.parseObj(msg), RemoteUserAccountVo.class);
+                log.info("[海康服务处理云端->本地一卡通账户同步请求]-[账户信息:{}]", JSONUtil.toJsonStr(remoteVo));
+
+                Long userNo = remoteVo.getUserNo();
+                Boolean deleteUser = !"0".equals(remoteVo.getDelFlag());
+                Date lifespan = remoteVo.getLifespan();
+                R<Void> result = sendDeviceService.upLoadEmpToAllDeviceByUserNo(userNo, lifespan, deleteUser);
+                log.info(result.getMsg());
+            }
+            case EventTypeConstants.CARD -> {
+                RemoteCardDto remoteCard = JSONUtil.toBean(JSONUtil.parseObj(msg), RemoteCardDto.class);
+                Long userId = remoteCard.getUserId();
+                Long factorId = remoteCard.getFactoryId();
+                String status = remoteCard.getStatus();
+                Boolean deleteCard = !CardStatusEnum.NORMAL.code().toString().equals(status);
+
+                log.info("[海康服务处理云端->本地卡片同步请求]-[卡片信息:{}]", JSONUtil.toJsonStr(remoteCard));
+
+                R<Void> result =  sendDeviceService.upLoadEmpCardToAllDevice(userId, factorId, false, deleteCard);
+
+                log.info(result.getMsg());
+            }
+        }
+    }
+}

+ 66 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IConsumeDetailOriginalService.java

@@ -0,0 +1,66 @@
+package org.dromara.server.hik.service;
+
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailOriginalVo;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 原始消费记录Service接口
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+public interface IConsumeDetailOriginalService {
+    /**
+     * 查询原始消费记录
+     *
+     * @param originalId 主键
+     * @return 原始消费记录
+     */
+    XfConsumeDetailOriginalVo queryById(String originalId);
+
+
+
+    /**
+     * 根据记录ID查询原始消费记录
+     *
+     * @param recordId 记录ID
+     * @return 原始消费记录视图对象
+     */
+    XfConsumeDetailOriginalVo queryByRecordId(Long recordId);
+
+    /**
+     * 根据卡号、设备机号和消费金额查询原始消费记录
+     *
+     * @param cardNo       卡流水号
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @param consumeMoney 消费金额
+     * @return 原始消费记录视图对象
+     */
+    XfConsumeDetailOriginalVo queryByConsumeMoney(Long cardNo, Long termNo, Long termRecordId, BigDecimal consumeMoney);
+
+
+    /**
+     * 根据卡号、设备机号、机器流水号和消费日期查询原始消费记录。
+     *
+     * @param cardNo       卡流水号
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @param consumeDate  消费日期
+     * @return 原始消费记录视图对象,如果未找到匹配的记录则返回null
+     */
+    XfConsumeDetailOriginalVo queryByConsumeDate(Long cardNo, Long termNo, Long termRecordId, Date consumeDate);
+
+
+    /**
+     * 根据设备机号和机器流水号查询原始消费记录。
+     * @param termNo 设备机号
+     * @param termRecordId 机器流水号
+     * @return
+     */
+    XfConsumeDetailOriginalVo queryByTermNoAndRecordId(Long termNo, Long termRecordId);
+}

+ 220 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/ISendDeviceService.java

@@ -0,0 +1,220 @@
+package org.dromara.server.hik.service;
+
+import org.dromara.common.core.domain.R;
+import org.dromara.server.hik.domain.dto.DeviceDto;
+import org.dromara.server.hik.domain.dto.QueryDto;
+import org.dromara.server.hik.domain.dto.UploadEmpDto;
+
+import java.util.Date;
+
+
+/**
+ * 向消费机发送信息服务接口
+ * <p>
+ * 定义向消费机发磅的服务接口
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+public interface ISendDeviceService {
+
+    /**
+     * 给指定设务设置监听服务地址
+     *
+     * @param dto 设备信息
+     * @return 设置结果
+     */
+    R<Void> setHttpHostByDto(DeviceDto dto);
+
+    /**
+     * 给所有设备设置监听服务地址
+     *
+     * @return 设置结果
+     */
+    R<Void> setHttpHostAll();
+
+    /**
+     * 根据设备编号设置监听服务地址。
+     *
+     * @param termNo 设备编号,用于标识需要设置监听服务地址的设备
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> setHttpHostByTermNo(Long termNo);
+
+    /**
+     * 批量查询设备上人员信息。
+     * <p>
+     * 该方法用于向指定设备发起批量查询人员信息的请求,基于提供的查询条件传输对象。
+     *
+     * @param dto 查询条件传输对象,包含查询所需的设备信息及分页参数等
+     * @return 响应信息主体,表示查询操作的结果状态及可能的附加信息
+     */
+    R<Void> queryBatchEmpFormDevice(QueryDto dto);
+
+    /**
+     * 删除指定设备上的员工信息。
+     * <p>
+     * 根据用户编号从指定设备中删除对应的员工信息。
+     *
+     * @param device 设备信息,包含设备的详细配置数据
+     * @param userNo 用户编号,用于标识需要删除的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteEmpByUserNo(DeviceDto device, String userNo);
+
+    /**
+     * 删除指定设备上的指定员工信息。
+     *
+     * @param termNo 设备编号,用于标识需要删除员工信息的设备
+     * @param userId 用户ID,用于标识需要删除的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteEmpFromDevice(Long termNo, Long userId);
+
+    /**
+     * 删除指定设备上的所有员工信息。
+     *
+     * @param termNo 设备编号,用于标识需要删除员工信息的设备
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteEmpFromDevice(Long termNo);
+
+    /**
+     * 删除所有设备上的所有员工信息。
+     * <p>
+     * 该方法用于从设备中删除员工信息,具体删除逻辑由实现类定义。
+     *
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteEmpFromDevice();
+
+    /**
+     * 删除所有设备上的指定员工信息。
+     *
+     * @param userId 用户ID,用于标识需要删除的员工信息
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteOneEmpFromDevice(Long userId);
+
+    /**
+     * 删除指定用户在设备上的所有卡片信息。
+     * <p>
+     * 根据用户编号从指定设备中删除该用户的所有卡片信息。
+     *
+     * @param device 设备信息,包含设备的详细配置数据
+     * @param userNo 用户编号,用于标识需要删除卡片信息的用户
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteAllCardByUserNo(DeviceDto device, String userNo);
+
+    /**
+     * 删除指定用户在设备上的指定卡片信息。
+     * <p>
+     * 根据用户编号和物理卡号从指定设备中删除该用户的卡片信息。
+     *
+     * @param device    设备信息,包含设备的详细配置数据
+     * @param userNo    用户编号,用于标识需要删除卡片信息的用户
+     * @param factoryId 物理卡号,用于标识需要删除卡片的物理卡号
+     * @return 响应信息主体,包含操作结果的状态码、消息内容以及可能的附加数据
+     */
+    R<Void> deleteCardByUserNo(DeviceDto device, String userNo, String factoryId);
+
+    /**
+     * 删除指定用户在设备上的所有人脸数据。
+     *
+     * @param device 设备信息
+     * @param userNo 用户编号
+     * @return 操作结果
+     */
+    R<Void> deleteAllFaceByUserNo(DeviceDto device, String userNo);
+
+    /**
+     * 删除指定用户在设备上的指定的人脸数据。
+     *
+     * @param device 设备信息
+     * @param userNo 用户编号
+     * @param faceId 人脸ID
+     * @return 操作结果
+     */
+    R<Void> deleteFaceByUserNo(DeviceDto device, String userNo, String faceId);
+
+    /**
+     * 上传员工信息到指定设备。
+     * <p>
+     * 该方法用于将员工信息传输到指定的设备,基于提供的设备信息和员工信息数据传输对象。
+     *
+     * @param uploadEmpDto@return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToDevice(UploadEmpDto uploadEmpDto);
+
+    /**
+     * 上传所有员工信息到指定设备。
+     * <p>
+     * 该方法用于将员工信息传输到指定的设备,基于提供的设备编号。
+     *
+     * @param termNo 设备编号,用于标识需要上传员工信息的目标设备
+     * @return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToDevice(Long termNo);
+
+    /**
+     * 上传指定员工信息到指定设备。
+     * <p>
+     * 该方法用于将特定员工的信息传输到指定的设备,基于提供的设备编号和用户编号。
+     *
+     * @param termNo 设备编号,用于标识需要上传员工信息的目标设备
+     * @param userId 用户编号,用于标识需要上传的员工信息
+     * @return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToDevice(Long termNo, Long userId);
+
+    /**
+     * 上传员工信息到设备。
+     * <p>
+     * 该方法用于将所有员工信息传输到所有设备,具体设备和员工信息管理后台接口提供。
+     *
+     * @return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToDevice(Boolean uploadPhoto);
+
+    /**
+     * 上传days内的数据至到指定设备
+     * @param startDate 天数
+     * @param uploadPhoto 是否上传照片
+     * @return 无
+     */
+    R<Void> upLoadEmpToDevice(String macAddress,Date startDate, Boolean uploadPhoto);
+
+    /**
+     * 上传指定员工信息到所有设备。
+     * <p>
+     * 该方法用于将特定员工的信息传输到所有设备,基于提供的用户编号。
+     *
+     * @param userId 用户编号,用于标识需要上传的员工信息
+     * @return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToAllDevice(Long userId);
+
+    /**
+     * 上传指定员工信息到所有设备,并设置相关信息。
+     *
+     * @param userNo 用户编号,用于标识需要上传的员工信息
+     * @param lifeSpan 生效期限,表示员工信息在设备上的有效时间范围
+     * @param deleteUser 是否删除用户标志,用于指示是否在设备上删除该用户的相关信息
+     * @return 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpToAllDeviceByUserNo(Long userNo, Date lifeSpan, Boolean deleteUser);
+
+    /**
+     * 上传指定员工的卡片到所有设备,并设置相关信息。
+     *
+     * @param userId 用户Id,用于标识需要上传的员工信息
+     * @param factorId 物理卡号
+     * @param deleteAllCard 是否删除所有卡片 false-不删除
+     * @param deleteCard 是否删除物理卡号对应的卡片, false-不删除
+     * @return n 响应信息主体,表示上传操作的结果状态及可能的附加信息
+     */
+    R<Void> upLoadEmpCardToAllDevice(Long userId, Long factorId,Boolean deleteAllCard,Boolean deleteCard);
+}

+ 36 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IXfConsumeDetailService.java

@@ -0,0 +1,36 @@
+package org.dromara.server.hik.service;
+
+
+import org.dromara.server.hik.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailVo;
+
+import java.util.List;
+
+/**
+ * 消费明细Service接口
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+public interface IXfConsumeDetailService {
+
+    /**
+     * 查询消费明细
+     *
+     * @param consumeId 主键
+     * @return 消费明细
+     */
+    XfConsumeDetailVo queryById(String consumeId);
+
+
+
+    XfConsumeDetailVo queryVoByOriginalId(String originalId);
+
+    /**
+     * 根据设备机号和机器流水号查询消费记录。
+     * @param termNo 设备机号
+     * @param termRecordId 机器流水号
+     * @return
+     */
+    List<XfConsumeDetailVo> queryByTermNoAndRecordId(Long termNo, Long termRecordId);
+}

+ 77 - 0
ruoyi-server/ruoyi-server-face/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);
+}

+ 35 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/IXfTermService.java

@@ -0,0 +1,35 @@
+package org.dromara.server.hik.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.server.hik.domain.vo.XfTermVo;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 消费设备Service接口
+ *
+ * @author bing
+ * @date 2024-08-21
+ */
+public interface IXfTermService {
+
+
+    /**
+     * 根据设备的mac查询设备信息
+     */
+//    XfTermVo queryByMac(String termMac);
+
+    public XfTermVo getByMac(String termMac);
+
+    /**
+     *  修改设备ip
+     * @param termMac 设备mac
+     * @param newIP 新ip
+     * @return 修改成功
+     */
+    XfTermVo updateByMac(String termMac,String newIP);
+
+}

+ 117 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/ConsumeDetailOriginalServiceImpl.java

@@ -0,0 +1,117 @@
+package org.dromara.server.hik.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.hik.domain.XfConsumeDetailOriginal;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.hik.mapper.ConsumeDetailOriginalMapper;
+import org.dromara.server.hik.service.IConsumeDetailOriginalService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 原始消费记录Service业务层处理
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ConsumeDetailOriginalServiceImpl implements IConsumeDetailOriginalService {
+
+    private final ConsumeDetailOriginalMapper baseMapper;
+
+    /**
+     * 查询原始消费记录
+     *
+     * @param originalId 主键
+     * @return 原始消费记录
+     */
+    @Override
+    public XfConsumeDetailOriginalVo queryById(String originalId) {
+        return baseMapper.selectVoById(originalId);
+    }
+
+
+    /**
+     * 根据记录ID查询原始消费记录
+     *
+     * @param recordId 记录ID
+     * @return 原始消费记录视图对象
+     */
+    @Override
+    public XfConsumeDetailOriginalVo queryByRecordId(Long recordId) {
+        return baseMapper.selectVoOne(new LambdaQueryWrapper<XfConsumeDetailOriginal>().eq(XfConsumeDetailOriginal::getRecordId, recordId));
+    }
+
+    /**
+     * 根据卡号、设备机号和消费金额查询原始消费记录
+     *
+     * @param cardNo       卡流水号
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @param consumeMoney 消费金额
+     * @return 原始消费记录视图对象
+     */
+    @Override
+    public XfConsumeDetailOriginalVo queryByConsumeMoney(Long cardNo, Long termNo, Long termRecordId, BigDecimal consumeMoney) {
+        return baseMapper.selectVoOne(new LambdaQueryWrapper<XfConsumeDetailOriginal>().eq(XfConsumeDetailOriginal::getCardNo, cardNo)
+                                          .eq(XfConsumeDetailOriginal::getTermNo, termNo)
+                                          .eq(XfConsumeDetailOriginal::getTermRecordId, termRecordId)
+                                          .eq(XfConsumeDetailOriginal::getConsumeMoney, consumeMoney));
+    }
+
+
+    /**
+     * 根据卡号、设备机号、机器流水号和消费日期查询原始消费记录。
+     *
+     * @param cardNo       卡流水号
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @param consumeDate  消费日期
+     * @return 原始消费记录视图对象,如果未找到匹配的记录则返回null
+     */
+    @Override
+    public XfConsumeDetailOriginalVo queryByConsumeDate(Long cardNo, Long termNo, Long termRecordId, Date consumeDate) {
+        return baseMapper.selectVoOne(new LambdaQueryWrapper<XfConsumeDetailOriginal>().eq(XfConsumeDetailOriginal::getCardNo, cardNo)
+                                          .eq(XfConsumeDetailOriginal::getTermNo, termNo)
+                                          .eq(XfConsumeDetailOriginal::getTermRecordId, termRecordId)
+                                          .eq(XfConsumeDetailOriginal::getConsumeDate, consumeDate));
+    }
+
+    /**
+     * 根据设备机号和机器流水号查询原始消费记录。
+     *
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @return
+     */
+    @Override
+    public XfConsumeDetailOriginalVo queryByTermNoAndRecordId(Long termNo, Long termRecordId) {
+        LambdaQueryWrapper<XfConsumeDetailOriginal> eq = Wrappers.lambdaQuery(XfConsumeDetailOriginal.class)
+            .eq(XfConsumeDetailOriginal::getTermNo, termNo)
+            .eq(XfConsumeDetailOriginal::getTermRecordId, termRecordId);
+        List<XfConsumeDetailOriginalVo> xfConsumeDetailVos = this.baseMapper.selectVoList(eq);
+        if(CollectionUtil.isEmpty(xfConsumeDetailVos)){
+            return null;
+        }
+        if (xfConsumeDetailVos.size() > 1){
+            log.error("根据设备机号: {} 和机器流水号: {} 找到多条原始请求数据",  termNo, termRecordId);
+            throw new ServiceException("根据设备机号和机器流水号找到多条原始请求数据");
+        }
+        return xfConsumeDetailVos.get(0);
+    }
+}

+ 799 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/SendDeviceServiceImpl.java

@@ -0,0 +1,799 @@
+package org.dromara.server.hik.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemotePtXfTermService;
+import org.dromara.backstage.api.RemoteUserAccountService;
+import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.backstage.api.domain.vo.RemoteXfTermVo;
+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.DeviceBrandEnum;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.server.hik.constant.ErrCodeConstants;
+import org.dromara.server.hik.constant.HikApiConstants;
+import org.dromara.server.hik.constant.HikDefaultConstants;
+import org.dromara.server.hik.domain.dto.DeviceDto;
+import org.dromara.server.hik.domain.dto.QueryDto;
+import org.dromara.server.hik.domain.dto.UploadEmpDto;
+import org.dromara.server.hik.domain.dto.UserInfoDto;
+import org.dromara.server.hik.domain.dto.base.*;
+import org.dromara.server.hik.domain.dto.query.QueryEmpResultDto;
+import org.dromara.server.hik.domain.vo.XfTermVo;
+import org.dromara.server.hik.enums.ContentTypeEnum;
+import org.dromara.server.hik.enums.StatusCodeEnum;
+import org.dromara.server.hik.service.ISendDeviceService;
+import org.dromara.server.hik.service.IXfTermService;
+import org.dromara.server.hik.utils.DigestHttpUtil;
+import org.dromara.server.hik.utils.JsonConfig;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.text.MessageFormat;
+import java.util.*;
+
+/**
+ * 向消费机发送信息服务接口实现
+ * <p>
+ * 向消费机发磅的服务接口的功能实现
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-21
+ * @since JDK17
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class SendDeviceServiceImpl implements ISendDeviceService {
+    private final DigestHttpUtil digestHttpUtil;
+    private final DefaultConfig defaultConfig;
+    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
+
+    private final IXfTermService xfTermService;
+
+    @DubboReference
+    private final RemotePtXfTermService remotePtXfTermService;
+    @DubboReference
+    private final RemoteUserAccountService remoteUserAccountService;
+
+    // region 私有方法
+
+    /**
+     * 根据提供的 RemoteUserAccountVo 对象和删除标志,构造并返回一个 EmpInfoDto 对象。
+     *
+     * @param accountVo     包含用户账户信息的 RemoteUserAccountVo 对象
+     * @param deleteUser    布尔标志,指示是否应将用户标记为删除
+     * @param deleteAllFace 布尔标志,指示是否应删除与用户相关的所有人脸数据
+     * @param deleteFace    布尔标志,指示是否应删除特定的人脸数据
+     * @param deleteAllCard 布尔标志,指示是否应删除与用户相关的所有卡片数据
+     * @param deleteCard    布尔标志,指示是否应删除特定的卡片数据
+     * @param uploadPhoto   布尔标志,指示是否应上传用户照片
+     * @return 一个填充了用户信息、有效性详情、卡片信息和人脸数据的 EmpInfoDto 对象
+     */
+    @NotNull
+    private static EmpInfoDto getEmpInfoDto(@NotNull RemoteUserAccountVo accountVo, Boolean deleteUser, Boolean deleteAllFace, Boolean deleteFace,
+                                            Boolean deleteAllCard, Boolean deleteCard, Boolean uploadPhoto) {
+        EmpInfoDto empDto = createEmpInfoDto(accountVo, deleteUser);
+
+        // 设置用户卡片信息,有物理卡号并且物理卡号>0时设置
+        Long factoryId = accountVo.getFactoryId();
+        setCardToEmpInfoDto(empDto, factoryId, deleteAllCard, deleteCard);
+
+        if(uploadPhoto){
+            // TODO 2025-05-24 因为人员照片原因,暂时不将人脸照片上传到消费机
+            // 设置用户人脸图片信息
+            String photo = accountVo.getFacePicUrl();
+            setPhotoToEmpInfoDto(empDto, photo, deleteAllFace, deleteFace);
+        }
+
+        return empDto;
+
+    }
+
+    private static EmpInfoDto createEmpInfoDto(@NotNull RemoteUserAccountVo accountVo,Boolean deleteUser) {
+        EmpInfoDto empDto = new EmpInfoDto();
+        // 设置用户基本信息
+        empDto.setEmployeeNo(accountVo.getUserNo().toString()).setName(accountVo.getRealName());
+        empDto.setDeleteUser(deleteUser);
+        // 设置有效期,海康设备支持有效期最大2037-12-31 23:59:59,所以要和系统的有效期比较取较小值
+        Date endTime = accountVo.getLifespan();
+        if (ObjectUtil.isNotEmpty(endTime)) {
+            Date hikEndTime = DateUtil.parse(HikDefaultConstants.EMP_END_TIME);
+            if (endTime.compareTo(hikEndTime) > 0) {
+                endTime = hikEndTime;
+            }
+            ValidDto validDto = new ValidDto().setBeginTime(getBeginTime()).setEndTime(DateUtil.date(endTime));
+            empDto.setValid(validDto);
+        }
+        return empDto;
+    }
+
+    // 更新人脸
+    private static void setPhotoToEmpInfoDto(EmpInfoDto empDto,String photo,Boolean deleteAllFace, Boolean deleteFace) {
+        if (ObjectUtil.isNotEmpty(photo)) {
+            FaceDto faceDto = new FaceDto().setFDID("1").setFaceID(1L).setFacePicURL(photo);
+            faceDto.setDeleteFace(deleteFace);
+
+            List<FaceDto> faceList = new ArrayList<>();
+            faceList.add(faceDto);
+            FaceListDto faceListDto = new FaceListDto().setList(faceList);
+            faceListDto.setDeleteAllFace(deleteAllFace);
+
+            empDto.setFaceInfo(faceListDto);
+        }
+    }
+
+    // 更新卡片
+    private static void setCardToEmpInfoDto(EmpInfoDto empDto,Long factoryId,Boolean deleteAllCard, Boolean deleteCard) {
+        if (ObjectUtil.isNotEmpty(factoryId) && factoryId > 0L) {
+            CardDto cardDto = new CardDto().setCardNo(factoryId.toString());
+            cardDto.setDeleteCard(deleteCard);
+
+            List<CardDto> cardList = new ArrayList<>();
+            cardList.add(cardDto);
+
+            CardListDto cardListDto = new CardListDto();
+            cardListDto.setList(cardList);
+            cardListDto.setDeleteAllCard(deleteAllCard);
+
+            empDto.setCardInfo(cardListDto);
+        }
+    }
+
+    /**
+     * 将 RemoteXfTermVo 对象转换为 DeviceDto 对象。
+     * 它提取了终端号、管理员名称、管理员密码、设备 IP、设备端口、服务器 IP 和服务器端口等信息,并将其设置到 DeviceDto 实例中。
+     *
+     * @param termVo 包含终端信息的 RemoteXfTermVo 对象
+     * @return 转换后的 DeviceDto 对象
+     */
+    @NotNull
+    private static DeviceDto getDeviceDto(@NotNull RemoteXfTermVo termVo) {
+        return getDeviceDto(termVo.getTermNo(), termVo.getAdminName(), termVo.getAdminPwd(), termVo.getTermIp(),
+            termVo.getCommPort(), termVo.getServerIp(), termVo.getServerPort());
+    }
+
+    @NotNull
+    private static DeviceDto getDeviceDto(@NotNull XfTermVo termVo) {
+        return getDeviceDto(termVo.getTermNo(), termVo.getAdminName(), termVo.getAdminPwd(), termVo.getTermIp(),
+            termVo.getCommPort(), termVo.getServerIp(), termVo.getServerPort());
+    }
+
+    @NotNull
+    private static DeviceDto getDeviceDto(Long termNo, String adminName, String adminPwd, String termIp, Long commPort, String serverIp, Long serverPort) {
+        DeviceDto dto = new DeviceDto();
+        dto.setTermNo(termNo.intValue());
+        dto.setAdminName(adminName);
+        dto.setAdminPwd(adminPwd);
+        dto.setDeviceIp(termIp);
+        dto.setDevicePort(commPort.intValue());
+        dto.setServerIp(serverIp);
+        dto.setServerPort(serverPort.intValue());
+
+        return dto;
+    }
+
+    /**
+     * 返回当前日期的开始时间,格式化为当天 00:00:00,
+     *
+     * @return 当前日期的开始时间
+     */
+    private static Date getBeginTime() {
+        String temp = DateUtil.format(DateUtil.date(), DefaultConstants.DATE_FORMAT);
+        temp = temp + " 00:00:00";
+        return DateUtil.parse(temp);
+    }
+
+    /**
+     * 动态构建下发监听服务地址的请求报文
+     *
+     * @param dto 设备信息
+     * @return 下发的请求报文
+     */
+    private String createHostXml(@NotNull DeviceDto dto) {
+        String xmlTemplate = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                                 "<HttpHostNotification version=\"2.0\"\n" +
+                                 "    xmlns=\"http://www.isapi.org/ver20/XMLSchema\">\n" +
+                                 "    <id>1</id>\n" +
+                                 "    <url>{}</url>\n" +
+                                 "    <protocolType>HTTP</protocolType>\n" +
+                                 "    <parameterFormatType>XML</parameterFormatType>\n" +
+                                 "    <addressingFormatType>ipaddress</addressingFormatType>\n" +
+                                 "    <ipAddress>{}</ipAddress>\n" +
+                                 "    <portNo>{}</portNo>\n" +
+                                 "    <httpAuthenticationMethod>none</httpAuthenticationMethod>\n" +
+                                 "    <SubscribeEvent>\n" +
+                                 "        <heartbeat>30</heartbeat>\n" +
+                                 "        <eventMode>list</eventMode>\n" +
+                                 "        <EventList>\n" +
+                                 "            <Event>\n" +
+                                 "                <type>ConsumptionAndTransactionRecordEvent</type>\n" +
+                                 "            </Event>\n" +
+                                 "        </EventList>\n" +
+                                 "    </SubscribeEvent>\n" +
+                                 "</HttpHostNotification>\n";
+
+        return StringUtils.format(xmlTemplate,dto.getServerUrl(), dto.getServerIp(), dto.getServerPort());
+    }
+
+    /**
+     * 处理设置监听服务地址返回数据
+     * <p>
+     * 对返回的json对象进行处理,根据内容的不同返回不同的处理结果,已有的情况
+     * 1.有 ResponseStatus 对象,获取对象中的 statusCode,根据 statusCode 返回成功或错误消息
+     * 2.有 userCheck 对象,获取对象的 statusValue ,根据 statusValue 返回成功或错误消息
+     * 3.有 code 对象,获取 code 的值 ,根据 code 返回成功或错误消息
+     * </p>
+     *
+     * @param obj 返回数据
+     * @return 处理结果
+     */
+    private R<Void> doSetHostReturnData(JSONObject obj) {
+        if (obj == null) {
+            return R.fail("请求失败,设备无返回");
+        }
+        Integer statusCode;
+        if (ObjectUtil.isNotEmpty(obj.getObj("ResponseStatus"))) {
+            JSONObject responseStatus = obj.getJSONObject("ResponseStatus");
+            statusCode = responseStatus.getInt("statusCode");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(StatusCodeEnum.getMessage(statusCode.toString()));
+            }
+        } else if (ObjectUtil.isNotEmpty(obj.getObj("userCheck"))) {
+            JSONObject responseStatus = obj.getJSONObject("userCheck");
+            statusCode = responseStatus.getInt("statusValue");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(StatusCodeEnum.getMessage("userCheck_" + statusCode.toString()));
+            }
+        } else if (ObjectUtil.isNotEmpty(obj.getObj("code"))) {
+            statusCode = obj.getInt("code");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(obj.getStr("msg"));
+            }
+        }
+        return R.ok();
+    }
+
+    /**
+     * 处理设备返回的人员查询结果
+     * <p>
+     * 该方法目前只处理了设备授权造成的查询错误和获取本次查询的记录数和总记录数
+     * 对数据的其它处理,如将人员信息解析出来供其它业务使用则根据具体情况处理
+     *
+     * @param obj 待处理 JSONObject对象
+     * @return 处理结果
+     */
+    private R<Void> doQueryEmpReturnData(@NotNull JSONObject obj) {
+        Integer statusCode;
+        if (ObjectUtil.isNotEmpty(obj.getObj("userCheck"))) {
+            JSONObject responseStatus = obj.getJSONObject("userCheck");
+            statusCode = responseStatus.getInt("statusValue");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(StatusCodeEnum.getMessage("userCheck_" + statusCode.toString()));
+            } else {
+                return R.fail("未知异常");
+            }
+        } else {
+            QueryEmpResultDto result = JSONUtil.toBean(obj, QueryEmpResultDto.class);
+            String msg = StringUtils.format("共[{}]条,本次[{}]条", result.getTotalMatches(), result.getNumOfMatches());
+
+            return R.ok(msg);
+        }
+    }
+
+    /**
+     * 根据提供的 JSON 对象验证用户检查异常。
+     * 该方法检查给定 JSON 对象中的 "userCheck" 字段,以确定状态值是否表示错误条件。
+     * 如果检测到错误,则返回带有适当消息的失败响应。
+     *
+     * @param obj 一个非空的 JSON 对象,包含要验证的 "userCheck" 字段
+     * @return 如果未发现错误则返回成功响应;如果状态值表示错误或发生未知异常,则返回带错误消息的失败响应
+     */
+    private R<Void> validUserCheckException(@NotNull JSONObject obj) {
+        if (ObjectUtil.isNotEmpty(obj.getObj("userCheck"))) {
+            JSONObject responseStatus = obj.getJSONObject("userCheck");
+            Integer statusCode = responseStatus.getInt("statusValue");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(StatusCodeEnum.getMessage("userCheck_" + statusCode.toString()));
+            } else {
+                return R.fail("未知异常");
+            }
+        }
+        return R.ok();
+    }
+
+    /**
+     * 根据提供的 JSON 对象验证状态码异常。
+     * 如果 JSON 对象中的 "errorMsg" 字段不为空,则返回带有错误消息的失败响应。
+     * 否则,返回成功响应。
+     *
+     * @param obj 包含要验证的错误消息的 JSON 对象;不能为空
+     * @return 一个响应对象,根据验证结果指示成功或失败
+     */
+    private R<Void> validStatusCodeException(@NotNull JSONObject obj) {
+        String errorMsg = obj.getStr("errorMsg");
+        if (ObjectUtil.isNotEmpty(errorMsg)) {
+            return R.fail(errorMsg);
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 验证提供的 JSON 对象中包含的详细信息。
+     * 检查 "DetailInfo" 对象是否存在,并评估其错误码。
+     * 如果错误码与可接受的值(ErrCodeConstants.OK 或 ErrCodeConstants.YES)不匹配,
+     * 则返回失败响应以及相关的用户错误消息。
+     * 否则,返回成功响应。
+     *
+     * @param obj 包含要验证的 "DetailInfo" 的 JSON 对象;不能为空
+     * @return 如果验证通过则返回成功响应,如果验证失败则返回包含错误消息的失败响应
+     */
+    private R<Void> validDetailInfo(@NotNull JSONObject obj) {
+        if (ObjectUtil.isNotEmpty(obj.getObj("DetailInfo"))) {
+            JSONObject responseStatus = obj.getJSONObject("DetailInfo");
+            Integer statusCode = responseStatus.getInt("errorCode");
+            if (ObjectUtil.notEqual(statusCode, ErrCodeConstants.OK) && ObjectUtil.notEqual(statusCode, ErrCodeConstants.YES)) {
+                return R.fail(responseStatus.getStr("userErrorMsg"));
+            }
+        }
+        return R.ok();
+    }
+
+    /**
+     * 创建并发送需要操作的员工信息到指定设备
+     *
+     * @param device  员工信息将被发送到的设备
+     * @param empInfo 要创建并传输的员工信息
+     * @return 一个表示操作结果的 R 实例,封装了任何可能的异常或验证结果
+     */
+    private R<Void> createOperatorEmpInfo(DeviceDto device, EmpInfoDto empInfo) {
+        UserInfoDto sendDto = new UserInfoDto();
+        sendDto.setUserInfoAndRight(empInfo);
+        String setData = JSONUtil.toJsonStr(sendDto, JsonConfig.getConfig());
+
+        JSONObject sendResult = digestHttpUtil.sendPost(device, setData, HikApiConstants.SEND_EMP_INFO, ContentTypeEnum.JSON.getCode());
+        R<Void> check = this.validUserCheckException(sendResult);
+        if (R.isError(check)) {
+            return R.fail(
+                MessageFormat.format("[上传人员信息失败]-[设备IP:{0}, 人员信息:{1}, 错误信息:{2}", device.getDeviceIp(), empInfo,
+                                     check.getMsg()));
+        }
+        check = this.validStatusCodeException(sendResult);
+        if (R.isError(check)) {
+            return R.fail(
+                MessageFormat.format("[上传人员信息失败]-[设备IP:{0}, 人员信息:{1}, 错误信息:{2}", device.getDeviceIp(), empInfo,
+                                     check.getMsg()));
+        }
+        check = this.validDetailInfo(sendResult);
+        if (R.isError(check)) {
+            return R.fail(
+                MessageFormat.format("[上传人员信息失败]-[设备IP:{0}, 人员信息:{1}, 错误信息:{2}", device.getDeviceIp(), empInfo,
+                                     check.getMsg()));
+        }
+        return R.ok(MessageFormat.format("[上传人员信息成功]-[设备IP:{0}, 人员信息:{1}]", device.getDeviceIp(), empInfo));
+    }
+
+    /**
+     * 根据提供的终端编号获取一个 DeviceDto 对象。
+     *
+     * @param termNo 用于查询和检索设备信息的终端编号
+     * @return 与查询的终端编号对应的 DeviceDto 对象
+     * @throws ServiceException 如果设备不存在,或者设备不是海康威视(Hikvision)品牌的设备
+     */
+    @NotNull
+    private DeviceDto getDeviceDto(Long termNo) {
+        RemoteXfTermVo termVo = remotePtXfTermService.queryByNo(termNo, defaultConfig.getTenantId());
+        if (ObjectUtil.isEmpty(termVo)) {
+            throw new ServiceException(MessageFormat.format("设备不存在,设备编号:{0}", termNo));
+        }
+        if (ObjectUtil.notEqual(DeviceBrandEnum.HK.getCode(), termVo.getBrand())) {
+            throw new ServiceException(MessageFormat.format("无法处理非海康设备,设备编号:{0}", termNo));
+        }
+        return getDeviceDto(termVo);
+    }
+
+    /**
+     * 发送指定员工信息到所有设备
+     *
+     * @param empDto 待发送员工信息
+     */
+    private void sendEmpToAllDevice(EmpInfoDto empDto) {
+        List<RemoteXfTermVo> termList = remotePtXfTermService.queryListByBrand("hk");
+        if (CollectionUtil.isEmpty(termList)) {
+            throw new ServiceException("没有要处理的设备");
+        }
+        termList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                DeviceDto deviceDto = getDeviceDto(p);
+                R<Void> result = this.createOperatorEmpInfo(deviceDto, empDto);
+                log.info(result.getMsg());
+            });
+        });
+    }
+
+    // endregion
+
+    // region 设置监听相关
+    @Override
+    public R<Void> setHttpHostByDto(@Validated DeviceDto dto) {
+        ValidatorUtils.validate(dto);
+
+        if (ObjectUtil.isEmpty(dto.getServerIp())) {
+            return R.fail("监听IP不能为空");
+        }
+        if (ObjectUtil.isEmpty(dto.getServerPort())) {
+            return R.fail("监听端口不能为空");
+        }
+        String setData = this.createHostXml(dto);
+
+        JSONObject sendResult = digestHttpUtil.sendPost(dto, setData, HikApiConstants.SET_HTTP_HOSTS, ContentTypeEnum.XML.getCode());
+        R<Void> doResult = this.doSetHostReturnData(sendResult);
+        if (R.isError(doResult)) {
+            return R.fail(MessageFormat.format("[IP:{0}的设备设置失败,原因:{1}]", dto.getDeviceIp(), doResult.getMsg()));
+        }
+        return R.ok(MessageFormat.format("[IP:{0}的设备设置成功]", dto.getDeviceIp()));
+    }
+
+    @Override
+    public R<Void> setHttpHostByTermNo(Long termNo) {
+        DeviceDto dto = getDeviceDto(termNo);
+
+        return this.setHttpHostByDto(dto);
+    }
+
+    @Override
+    public R<Void> setHttpHostAll() {
+        List<RemoteXfTermVo> termList = remotePtXfTermService.queryListByBrand("hk");
+        if (CollectionUtil.isEmpty(termList)) {
+            return R.warn("没有要配置的设备");
+        }
+
+        termList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                DeviceDto dto = getDeviceDto(p);
+                R<Void> result = setHttpHostByDto(dto);
+                log.info(result.getMsg());
+            });
+        });
+        return R.ok("处理完成,详情见处理日志");
+    }
+    // endregion
+
+    // region 从消费机删除人员相关
+    @Override
+    public R<Void> deleteEmpByUserNo(DeviceDto device, String userNo) {
+        EmpInfoDto delEmpDto = new EmpInfoDto();
+        delEmpDto.setDeleteUser(Boolean.TRUE);
+        delEmpDto.setEmployeeNo(userNo);
+
+        R<Void> delResult = this.createOperatorEmpInfo(device, delEmpDto);
+        if (R.isError(delResult)) {
+            return R.fail(
+                MessageFormat.format("[处理人员失败]-[设备IP:{0}, 人员编号:{1}, 错误信息:{2}", device.getDeviceIp(), userNo, delResult.getMsg()));
+        }
+        return R.ok(MessageFormat.format("[处理人员成功]-[设备IP:{0}, 人员编号:{1}]", device.getDeviceIp(), userNo));
+    }
+
+    @Override
+    public R<Void> deleteEmpFromDevice(Long termNo, Long userId) {
+        DeviceDto device = getDeviceDto(termNo);
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoBy(userId);
+        if (ObjectUtil.isEmpty(accountVo)) {
+            return R.fail(
+                MessageFormat.format("[处理人员失败]-[设备IP:{0}, 人员Id:{1}, 错误信息:无此Id对应的人员信息", device.getDeviceIp(), userId));
+        }
+        EmpInfoDto empInfo = getEmpInfoDto(accountVo, true, false, false, false, false, true);
+        R<Void> result = createOperatorEmpInfo(device, empInfo);
+
+        log.info(result.getMsg());
+
+        return result;
+    }
+
+    @Override
+    public R<Void> deleteEmpFromDevice(Long termNo) {
+        DeviceDto deviceDto = getDeviceDto(termNo);
+        List<RemoteUserAccountVo> accountVoList = remoteUserAccountService.getUserAccountVoList();
+        accountVoList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                EmpInfoDto empDto = getEmpInfoDto(p, true, false, false, false, false, true);
+                R<Void> result = createOperatorEmpInfo(deviceDto, empDto);
+                log.info(result.getMsg());
+            });
+        });
+
+        return R.ok();
+    }
+
+    public R<Void> deleteOneEmpFromDevice(Long userId) {
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoBy(userId);
+        if (ObjectUtil.isEmpty(accountVo)) {
+            return R.fail(
+                MessageFormat.format("[处理人员失败]-[人员Id:{0}, 错误信息:无此Id对应的人员信息", userId));
+        }
+        EmpInfoDto empInfo = getEmpInfoDto(accountVo, true, false, false, false, false, true);
+
+        List<RemoteXfTermVo> termList = remotePtXfTermService.queryListByBrand("hk");
+        if (CollectionUtil.isEmpty(termList)) {
+            return R.warn("没有要处理人员的设备");
+        }
+
+        termList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                DeviceDto device = getDeviceDto(p);
+                R<Void> result = createOperatorEmpInfo(device, empInfo);
+                log.info(result.getMsg());
+            });
+        });
+        return R.ok();
+    }
+
+    @Override
+    public R<Void> deleteEmpFromDevice() {
+        // 获取所有设备
+        List<RemoteXfTermVo> termList = remotePtXfTermService.queryListByBrand("hk");
+        if (CollectionUtil.isEmpty(termList)) {
+            return R.warn("没有要处理人员的设备");
+        }
+        // 获取所有人员
+        List<RemoteUserAccountVo> accountVoList = remoteUserAccountService.getUserAccountVoList();
+        if (CollectionUtil.isEmpty(accountVoList)) {
+            return R.warn("没有要处理的人员");
+        }
+
+        // 并行处理
+        termList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                DeviceDto device = getDeviceDto(p);
+                accountVoList.parallelStream().forEach(t -> {
+                    EmpInfoDto empInfo = getEmpInfoDto(t, true, false, false, false, false, true);
+                    R<Void> result = createOperatorEmpInfo(device, empInfo);
+                    log.info(result.getMsg());
+                });
+            });
+        });
+        return R.ok();
+    }
+    // endregion
+
+    @Override
+    public R<Void> deleteAllCardByUserNo(DeviceDto device, String userNo) {
+        CardListDto cardList = new CardListDto();
+        cardList.setDeleteAllCard(Boolean.TRUE);
+
+        EmpInfoDto delEmpDto = new EmpInfoDto();
+        delEmpDto.setEmployeeNo(userNo);
+        delEmpDto.setName("胡哲");
+        delEmpDto.setCardInfo(cardList);
+
+        R<Void> check = this.createOperatorEmpInfo(device, delEmpDto);
+        if (R.isError(check)) {
+            return R.fail(
+                MessageFormat.format("[处理人员所有卡片失败]-[设备IP:{0}, 人员编号:{1}, 错误信息:{2}", device.getDeviceIp(), userNo, check.getMsg()));
+        }
+        return R.ok(MessageFormat.format("[处理人员所有卡片成功]-[设备IP:{0}, 人员编号:{1}]", device.getDeviceIp(), userNo));
+    }
+
+    @Override
+    public R<Void> deleteCardByUserNo(DeviceDto device, String userNo, String factoryId) {
+        return null;
+    }
+
+    @Override
+    public R<Void> deleteAllFaceByUserNo(DeviceDto device, String userNo) {
+        return null;
+    }
+
+    @Override
+    public R<Void> deleteFaceByUserNo(DeviceDto device, String userNo, String faceId) {
+        return null;
+    }
+
+
+    // region 向消费机上传人员相关
+
+    @Override
+    public R<Void> upLoadEmpToDevice(UploadEmpDto uploadEmpDto) {
+        DeviceDto device = uploadEmpDto.getDevice();
+        EmpInfoDto empDto = uploadEmpDto.getEmployee();
+        ValidDto validDto = uploadEmpDto.getValid();
+        CardDto cardDto = uploadEmpDto.getCard();
+        FaceDto faceDto = uploadEmpDto.getFace();
+        if (ObjectUtil.isNotEmpty(cardDto)) {
+            List<CardDto> cardList = new ArrayList<>();
+            cardList.add(cardDto);
+
+            CardListDto cardListDto = new CardListDto();
+            cardListDto.setList(cardList);
+            empDto.setCardInfo(cardListDto);
+        }
+        if (ObjectUtil.isNotEmpty(faceDto)) {
+            List<FaceDto> faceList = new ArrayList<>();
+            faceList.add(faceDto);
+            FaceListDto faceListDto = new FaceListDto();
+            faceListDto.setList(faceList);
+            empDto.setFaceInfo(faceListDto);
+        }
+        if (ObjectUtil.isNotEmpty(validDto)) {
+            empDto.setValid(validDto);
+        }
+        String strEmpInfo = JSONUtil.toJsonStr(empDto, JsonConfig.getConfig());
+        R<Void> check = this.createOperatorEmpInfo(device, empDto);
+        if (R.isError(check)) {
+            return R.fail(
+                MessageFormat.format("[上传人员信息失败]-[设备IP:{0}, 人员信息:{1}, 错误信息:{2}", device.getDeviceIp(), strEmpInfo, check.getMsg()));
+        }
+        return R.ok(MessageFormat.format("[上传人员信息成功]-[设备IP:{0}, 人员信息:{1}]", device.getDeviceIp(), strEmpInfo));
+    }
+
+    @Override
+    public R<Void> upLoadEmpToDevice(Long termNo) {
+        DeviceDto deviceDto = getDeviceDto(termNo);
+        List<RemoteUserAccountVo> accountVoList = remoteUserAccountService.getUserAccountVoList();
+        if (CollectionUtil.isEmpty(accountVoList)) {
+            return R.warn("没有要处理的人员");
+        }
+
+        // 处理人员列表
+        accountVoList.forEach(p -> {
+            threadPoolTaskExecutor.submit(() -> {
+                try {
+                    EmpInfoDto empDto = getEmpInfoDto(p, false, false, false, false, false,true);
+                    R<Void> result = createOperatorEmpInfo(deviceDto, empDto);
+                    log.info(result.getMsg());
+                } catch (Exception e) {
+                    log.error("处理人员{}异常: {}", p.getUserId(), e.getMessage(), e);
+                }
+            });
+        });
+        return R.ok();
+    }
+
+    @Override
+    public R<Void> upLoadEmpToDevice(Long termNo, Long userId) {
+        DeviceDto deviceDto = getDeviceDto(termNo);
+
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoBy(userId);
+
+        EmpInfoDto empDto = getEmpInfoDto(accountVo, false, false, false, false, false,true);
+
+        return this.createOperatorEmpInfo(deviceDto, empDto);
+    }
+
+    @Override
+    public R<Void> upLoadEmpToDevice(Boolean uploadPhoto) {
+        // 获取所有设备
+        List<RemoteXfTermVo> termList = remotePtXfTermService.queryListByBrand("hk");
+        if (CollectionUtil.isEmpty(termList)) {
+            return R.warn("没有要处理人员的设备");
+        }
+        // 获取所有人员
+        List<RemoteUserAccountVo> accountVoList = remoteUserAccountService.getUserAccountVoList();
+        if (CollectionUtil.isEmpty(accountVoList)) {
+            return R.warn("没有要处理的人员");
+        }
+
+        // 循环处理
+        termList.forEach(p -> {
+            DeviceDto device = getDeviceDto(p);
+            accountVoList.forEach(t -> {
+                threadPoolTaskExecutor.submit(()->{
+                    EmpInfoDto empInfo = getEmpInfoDto(t, false, false, false, false, false,uploadPhoto);
+                    R<Void> result = createOperatorEmpInfo(device, empInfo);
+                    log.info(result.getMsg());
+                });
+            });
+        });
+        return R.ok();
+    }
+
+    /**
+     * 上传days内的数据至指定mac设备
+     *
+     * @param startDate        天数
+     * @param uploadPhoto 是否上传照片
+     * @return 无
+     */
+    @Override
+    public R<Void> upLoadEmpToDevice(String macAddress,Date startDate, Boolean uploadPhoto) {
+        List<RemoteUserAccountVo> vos = remoteUserAccountService.getUpdateUserAccountVo(startDate);
+        if (CollectionUtil.isEmpty(vos)) {
+            return R.warn("没有要处理人员数据");
+        }
+        log.info("day处理人员数据条数" + vos.size());
+        XfTermVo termVo = xfTermService.getByMac(macAddress);
+        if (ObjectUtil.isEmpty(termVo)) {
+            return R.warn(MessageFormat.format("设备未找到,mac:{0}", macAddress));
+        }
+        DeviceDto device = getDeviceDto(termVo);
+        vos.forEach(t -> {
+            threadPoolTaskExecutor.submit(()->{
+                try{
+                    EmpInfoDto empInfo = getEmpInfoDto(t, false, false, false, false, false,uploadPhoto);
+                    R<Void> result = createOperatorEmpInfo(device, empInfo);
+                    log.info(result.getMsg());
+                }catch (Exception e){
+                    e.printStackTrace();
+                    log.error("heartBeat处理人员{}异常: {}", t.getUserId(), e.getMessage(), e);
+                }
+            });
+        });
+        return R.ok();
+    }
+
+
+    @Override
+    public R<Void> upLoadEmpToAllDevice(Long userId) {
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoBy(userId);
+        if (ObjectUtil.isEmpty(accountVo)) {
+            return R.warn(MessageFormat.format("没有要处理的人员信息,userId:{0}", userId));
+        }
+        EmpInfoDto empDto = getEmpInfoDto(accountVo, false, false, false, false, false,true);
+
+        sendEmpToAllDevice(empDto);
+
+        return R.ok();
+    }
+
+    @Override
+    public R<Void> upLoadEmpToAllDeviceByUserNo(Long userNo, Date lifeSpan, Boolean deleteUser) {
+        RemoteUserAccountVo accountVo = new RemoteUserAccountVo();
+        accountVo.setUserNo(userNo);
+        accountVo.setLifespan(lifeSpan);
+
+        // 实时下发数据不下发人脸,避免引起卡机
+        EmpInfoDto empDto = getEmpInfoDto(accountVo, deleteUser, false, false, false, false, false);
+
+        sendEmpToAllDevice(empDto);
+
+        return R.ok();
+    }
+
+    // endregion
+
+    @Override
+    public R<Void> upLoadEmpCardToAllDevice(Long userId, Long factorId, Boolean deleteAllCard, Boolean deleteCard) {
+        RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoBy(userId);
+        if (ObjectUtil.isEmpty(accountVo)) {
+            return R.warn(MessageFormat.format("没有要处理的人员信息,userId:{0}", userId));
+        }
+        accountVo.setFactoryId(factorId);
+        // 实时下发数据不下发人脸,避免引起卡机
+        EmpInfoDto empDto = getEmpInfoDto(accountVo, false, false, false, deleteAllCard, deleteCard, false);
+
+        sendEmpToAllDevice(empDto);
+
+        return R.ok();
+    }
+
+    @Override
+    public R<Void> queryBatchEmpFormDevice(@NotNull QueryDto dto) {
+        DeviceDto device = dto.getDevice();
+        Map<String, Object> params = new HashMap<>();
+        params.put("searchID", dto.getSearchID());
+        params.put("searchResultPosition", (dto.getPageNo() - 1) * dto.getPageSize());
+        params.put("maxResults", dto.getPageSize());
+
+        String setData = JSONUtil.toJsonStr(params);
+        JSONObject sendResult = digestHttpUtil.sendPost(dto.getDevice(), setData, HikApiConstants.QUERY_EMP_ALL, ContentTypeEnum.JSON.getCode());
+
+        R<Void> doResult = this.doQueryEmpReturnData(sendResult);
+        if (R.isError(doResult)) {
+            return R.fail(StringUtils.format("[IP:{}的设备查询失败,原因:{}]", device.getDeviceIp(), doResult.getMsg()));
+        }
+        return R.ok(StringUtils.format("[IP:{}的设备查询成功],结果:{}", device.getDeviceIp(), doResult.getMsg()));
+    }
+}

+ 71 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/XfConsumeDetailServiceImpl.java

@@ -0,0 +1,71 @@
+package org.dromara.server.hik.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.server.hik.domain.XfConsumeDetail;
+import org.dromara.server.hik.domain.vo.XfConsumeDetailVo;
+import org.dromara.server.hik.mapper.XfConsumeDetailMapper;
+import org.dromara.server.hik.service.IXfConsumeDetailService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 消费明细Service业务层处理
+ *
+ * @author LionLi
+ * @date 2024-08-15
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
+
+    private final XfConsumeDetailMapper baseMapper;
+
+    /**
+     * 查询消费明细
+     *
+     * @param consumeId 主键
+     * @return 消费明细
+     */
+    @Override
+    public XfConsumeDetailVo queryById(String consumeId) {
+        return baseMapper.selectVoById(consumeId);
+    }
+
+
+
+    @Override
+    public XfConsumeDetailVo queryVoByOriginalId(String originalId) {
+        LambdaQueryWrapper<XfConsumeDetail> eq = Wrappers.lambdaQuery(XfConsumeDetail.class).eq(XfConsumeDetail::getOriginalId, originalId);
+        List<XfConsumeDetailVo> vos = this.baseMapper.selectVoList(eq);
+        if(CollectionUtil.isEmpty(vos)){
+            return null;
+        }
+        return vos.get(0);
+    }
+
+    /**
+     * 根据设备机号和机器流水号查询消费记录。
+     *
+     * @param termNo       设备机号
+     * @param termRecordId 机器流水号
+     * @return
+     */
+    @Override
+    public List<XfConsumeDetailVo> queryByTermNoAndRecordId(Long termNo, Long termRecordId) {
+        LambdaQueryWrapper<XfConsumeDetail> eq = Wrappers.lambdaQuery(XfConsumeDetail.class)
+            .eq(XfConsumeDetail::getTermNo, termNo).eq(XfConsumeDetail::getTermRecordId, termRecordId)
+            .orderByDesc(XfConsumeDetail::getCreateTime);
+        return this.baseMapper.selectVoList(eq);
+    }
+}

+ 181 - 0
ruoyi-server/ruoyi-server-face/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;
+    }
+}

+ 111 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/service/impl/XfTermServiceImpl.java

@@ -0,0 +1,111 @@
+package org.dromara.server.hik.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemotePtAccountService;
+import org.dromara.backstage.api.RemotePtRoomService;
+import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
+import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
+import org.dromara.common.core.config.DefaultConfig;
+import org.dromara.common.core.constant.CacheNames;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.server.hik.domain.XfTerm;
+import org.dromara.server.hik.domain.vo.XfTermVo;
+import org.dromara.server.hik.mapper.XfTermMapper;
+import org.dromara.server.hik.service.IXfTermService;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 消费设备Service业务层处理
+ *
+ * @author bing
+ * @date 2024-08-21
+ */
+@RequiredArgsConstructor
+@Service
+public class XfTermServiceImpl implements IXfTermService {
+
+    private final XfTermMapper baseMapper;
+    private final DefaultConfig defaultConfig;
+
+    public static final String cache = CacheNames.PT_TERM_MAC_MAP + "_hik";
+
+
+//    @Override
+    public XfTermVo queryByMac(String termMac) {
+        boolean existsObject = RedisUtils.isExistsObject(cache);
+        if(!existsObject){
+            // 缓存设备mac地址
+            XfTermVo byMac = getByMac(termMac);
+            if(ObjUtil.isNull(byMac)){
+                return null;
+            }
+            Map<String, XfTermVo> map = new HashMap<>();
+            map.put(termMac, byMac);
+            RedisUtils.setCacheMap(cache, map);
+            RedisUtils.expire(cache, Duration.ofHours(1));
+            return byMac;
+        }
+        XfTermVo value = RedisUtils.getCacheMapValue(cache, termMac);
+        if(ObjUtil.isNull(value)){
+            // 缓存设备mac地址
+            XfTermVo byMac = getByMac(termMac);
+            if(ObjUtil.isNull(byMac)){
+                return null;
+            }
+            RedisUtils.setCacheMapValue(cache, termMac, byMac);
+            return byMac;
+        }
+        return value;
+
+    }
+
+    @Override
+    @Cacheable(cacheNames = XfTermServiceImpl.cache, key = "#termMac", unless = "#result == null", condition = "#termMac != null")
+    public XfTermVo getByMac(String termMac){
+        List<XfTermVo> termVos = baseMapper.selectVoList(new LambdaQueryWrapper<XfTerm>().eq(XfTerm::getTermMac, termMac));
+        if(CollectionUtil.isEmpty(termVos)){
+            return null;
+        }else if(termVos.size()>1){
+            throw new ServiceException("设备mac地址重复");
+        }
+        return termVos.get(0);
+    }
+
+    /**
+     * 修改设备ip
+     *
+     * @param termMac 设备mac
+     * @param newIP   新ip
+     * @return 修改成功
+     */
+    @Override
+    @CachePut(cacheNames = XfTermServiceImpl.cache, key = "#termMac", condition = "#result != null")
+    public XfTermVo updateByMac(String termMac, String newIP) {
+        if(StringUtils.isBlank(termMac)) return null;
+        XfTerm xfTerm = new XfTerm();
+        xfTerm.setTermMac(termMac);
+        xfTerm.setTermIp(newIP);
+        LambdaUpdateWrapper<XfTerm> set = Wrappers.lambdaUpdate(XfTerm.class).eq(XfTerm::getTermMac, termMac).set(XfTerm::getTermIp, newIP);
+        baseMapper.update(set);
+        return getByMac(termMac);
+    }
+
+}

+ 61 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/task/ScheduledTasks.java

@@ -0,0 +1,61 @@
+package org.dromara.server.hik.task;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.server.hik.service.ISendDeviceService;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * 海康设备定时任务
+ * <p>
+ * 海康设备相关的定时任务
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-22
+ * @see Object
+ * @since JDK17
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ScheduledTasks {
+    private final ISendDeviceService sendDeviceService;
+    private final ScheduledExecutorService scheduledExecutorService;
+
+    /**
+     * 定时任务方法,用于每天凌晨1点30分将员工信息上传至设备。
+     * 该方法通过调用 {@link ISendDeviceService#upLoadEmpToDevice(Boolean uploadPhoto)} 实现具体的上传逻辑。
+     * 此任务由 Spring 的定时任务机制驱动,按照指定的 cron 表达式执行。
+     * <p>
+     * 注意:此方法依赖于外部服务接口的具体实现,确保目标设备和员工信息管理后台接口可用。
+     */
+    // 每天11:10和17:10、7:00执行
+//    @Scheduled(cron = "0 20 11,17 * * *")
+    public void upLoadEmpToDevice() {
+        scheduledExecutorService.submit(() -> {
+            log.info("向所有海康消费机下发人员定时任务开始执行");
+            // 下发人脸会是设备卡机,所以这里设置为false
+            sendDeviceService.upLoadEmpToDevice(false);
+        });
+    }
+
+    /**
+     * 定时任务方法,用于每天早上7点执行。
+     * 该方法通过调用 {@link ISendDeviceService#upLoadEmpToDevice(Boolean uploadPhoto)} 实现具体的上传逻辑。
+     * 此任务由 Spring 的定时任务机制驱动,按照指定的 cron 表达式执行。
+     * <p>
+     * 注意:此方法依赖于外部服务接口的具体实现,确保目标设备和员工信息管理后台接口可用。
+     */
+//    @Scheduled(cron = "0 0 7 * * *")
+    public void upLoadEmpToDevice2() {
+        scheduledExecutorService.submit(() -> {
+            log.info("向所有海康消费机下发人员定时任务开始执行");
+            // 下发人脸会是设备卡机,所以这里设置为false
+            sendDeviceService.upLoadEmpToDevice(false);
+        });
+    }
+}

+ 133 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/utils/DigestHttpUtil.java

@@ -0,0 +1,133 @@
+package org.dromara.server.hik.utils;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.json.XML;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.server.hik.domain.dto.DeviceDto;
+import org.dromara.server.hik.enums.ContentTypeEnum;
+import org.springframework.stereotype.Component;
+
+/**
+ * 摘要http工具类
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DigestHttpUtil {
+
+    /**
+     * 创建摘要 httpClient
+     *
+     * @param deviceInfo 设备信息
+     * @return 摘要httpClient
+     */
+    private static CloseableHttpClient createDigestHttpClient(DeviceDto deviceInfo) {
+        CredentialsProvider provider = new BasicCredentialsProvider();
+        provider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
+                                new UsernamePasswordCredentials(deviceInfo.getAdminName(), deviceInfo.getAdminPwd()));
+
+        return HttpClients.custom().setDefaultCredentialsProvider(provider).build();
+    }
+
+    /**
+     * 以POST方式 发送请求
+     *
+     * @param device      设备信息
+     * @param data        发送的数据
+     * @param apiUrl      接口地址
+     * @param contentType 请求内容格式
+     * @return 请求结果
+     * @see org.dromara.server.hik.enums.ContentTypeEnum
+     */
+    public JSONObject sendPost(DeviceDto device, String data, String apiUrl, String contentType) {
+        String requestUrl = StringUtils.format("http://{}:{}{}", device.getDeviceIp(), device.getDevicePort(), apiUrl);
+        HttpPost httpPost = new HttpPost(requestUrl);
+        httpPost.setHeader("Content-Type", contentType);
+        httpPost.setHeader("Accept", "*/*");
+        StringEntity entity = new StringEntity(data, "UTF-8");
+        httpPost.setEntity(entity);
+
+        try (CloseableHttpClient httpClient = createDigestHttpClient(device)) {
+            JSONObject jsonObject;
+            String returnContentType = contentType;
+            HttpResponse response = httpClient.execute(httpPost);
+            Header contentTypeHeader = response.getFirstHeader("Content-Type");
+            if (contentTypeHeader != null) {
+                returnContentType = contentTypeHeader.getValue();
+            }
+            String str = EntityUtils.toString(response.getEntity());
+            if (returnContentType.equals(ContentTypeEnum.XML.getCode())) {
+                jsonObject = XML.toJSONObject(str);
+            } else {
+                jsonObject = JSONUtil.parseObj(str);
+            }
+            // TODO 2025-05-21 luoyibo 开发过程中打印,正式发布时不再打印
+            // log.info("str={}", jsonObject);
+
+            return jsonObject;
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 以POST方式 发送请求
+     *
+     * @param device      设备信息
+     * @param data        发送的数据
+     * @param apiUrl      接口地址
+     * @param contentType 请求内容格式
+     * @return 请求结果
+     * @see org.dromara.server.hik.enums.ContentTypeEnum
+     */
+    public JSONObject sendPut(DeviceDto device, String data, String apiUrl, String contentType) {
+        String requestUrl = StringUtils.format("http://{}:{}{}", device.getDeviceIp(), device.getDevicePort(), apiUrl);
+        HttpPut httpPut = new HttpPut(requestUrl);
+        httpPut.setHeader("Content-Type", contentType);
+        httpPut.setHeader("Accept", "*/*");
+        StringEntity entity = new StringEntity(data, "UTF-8");
+        httpPut.setEntity(entity);
+
+        try (CloseableHttpClient httpClient = createDigestHttpClient(device)) {
+            JSONObject jsonObject;
+            String returnContentType = contentType;
+            HttpResponse response = httpClient.execute(httpPut);
+            Header contentTypeHeader = response.getFirstHeader("Content-Type");
+            if (contentTypeHeader != null) {
+                returnContentType = contentTypeHeader.getValue();
+            }
+            String str = EntityUtils.toString(response.getEntity());
+            if (returnContentType.equals(ContentTypeEnum.XML.getCode())) {
+                jsonObject = XML.toJSONObject(str);
+            } else {
+                jsonObject = JSONUtil.parseObj(str);
+            }
+            // TODO 2025-05-21 luoyibo 开发过程中打印,正式发布时不再打印
+             log.info("str={}", jsonObject);
+
+            return jsonObject;
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+
+}

+ 24 - 0
ruoyi-server/ruoyi-server-face/src/main/java/org/dromara/server/hik/utils/JsonConfig.java

@@ -0,0 +1,24 @@
+package org.dromara.server.hik.utils;
+
+import cn.hutool.json.JSONConfig;
+
+/**
+ * Json序列化配置
+ * <p>
+ * Hutool工具Json序列化配置定义
+ *
+ * @author luoyibo
+ * @version 2.2.0
+ * @date 2025-05-23
+ * @since JDK17
+ */
+public class JsonConfig {
+
+    public static JSONConfig getConfig() {
+        JSONConfig jsonConfig = new JSONConfig();
+        jsonConfig.setIgnoreNullValue(true);
+        jsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
+        return jsonConfig;
+    }
+
+}

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


+ 34 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/application.yml

@@ -0,0 +1,34 @@
+# Tomcat
+server:
+  port: 9110
+
+# Spring
+spring:
+  application:
+    # 应用名称
+    name: ruoyi-server-face
+  profiles:
+    # 环境配置
+    active: @profiles.active@
+
+--- # nacos 配置
+spring:
+  cloud:
+    nacos:
+      # nacos 服务地址
+      server-addr: @nacos.server@
+      username: @nacos.username@
+      password: @nacos.password@
+      discovery:
+        # 注册组
+        group: @nacos.discovery.group@
+        namespace: ${spring.profiles.active}
+      config:
+        # 配置组
+        group: @nacos.config.group@
+        namespace: ${spring.profiles.active}
+  config:
+    import:
+      - optional:nacos:application-common.yml
+      - optional:nacos:datasource.yml
+      - optional:nacos:${spring.application.name}.yml

+ 10 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/banner.txt

@@ -0,0 +1,10 @@
+Spring Boot Version: ${spring-boot.version}
+Spring Application Name: ${spring.application.name}
+                            _                           _                    
+                           (_)                         | |                   
+ _ __  _   _   ___   _   _  _  ______  ___  _   _  ___ | |_   ___  _ __ ___  
+| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ 
+| |   | |_| || (_) || |_| || |        \__ \| |_| |\__ \| |_ |  __/| | | | | |
+|_|    \__,_| \___/  \__, ||_|        |___/ \__, ||___/ \__| \___||_| |_| |_|
+                      __/ |                  __/ |                           
+                     |___/                  |___/                            

+ 61 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/logback-plus.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="logs/${project.artifactId}" />
+   <!-- 日志输出格式 -->
+    <property name="console.log.pattern"
+              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${console.log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+    </appender>
+
+    <appender name="file_warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/warn.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>WARN</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- info异步输出 -->
+    <appender name="async_warn" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+        <queueSize>512</queueSize>
+        <!-- 添加附加的appender,最多只能添加一个 -->
+        <appender-ref ref="file_warn"/>
+    </appender>
+    <include resource="logback-common.xml" />
+
+    <include resource="logback-logstash.xml" />
+
+    <!-- 开启 skywalking 日志收集 -->
+    <include resource="logback-skylog.xml" />
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+        <appender-ref ref="async_warn" />
+    </root>
+</configuration>

+ 25 - 0
ruoyi-server/ruoyi-server-face/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>

+ 57 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/ConsumeDetailOriginalMapper.xml

@@ -0,0 +1,57 @@
+<?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.ConsumeDetailOriginalMapper">
+
+    <resultMap type="org.dromara.server.hik.domain.XfConsumeDetailOriginal" id="ConsumeDetailOriginalResult">
+            <result property="originalId"    column="original_id"    />
+            <result property="tenantId"    column="tenant_id"    />
+            <result property="recordId"    column="record_id"    />
+            <result property="userId"    column="user_id"    />
+            <result property="userNumb"    column="user_numb"    />
+            <result property="realName"    column="real_name"    />
+            <result property="deptId"    column="dept_id"    />
+            <result property="deptName"    column="dept_name"    />
+            <result property="consumeDate"    column="consume_date"    />
+            <result property="consumeMoney"    column="consume_money"    />
+            <result property="cardNo"    column="card_no"    />
+            <result property="factoryId"    column="factory_id"    />
+            <result property="cardValue"    column="card_value"    />
+            <result property="cardCount"    column="card_count"    />
+            <result property="consumeBalance"    column="consume_balance"    />
+            <result property="termNo"    column="term_no"    />
+            <result property="termName"    column="term_name"    />
+            <result property="termRecordId"    column="term_record_id"    />
+            <result property="analysisFlag"    column="analysis_flag"    />
+            <result property="dataFlag"    column="data_flag"    />
+            <result property="statusFlag"    column="status_flag"    />
+            <result property="digitalSign"    column="digital_sign"    />
+            <result property="waterValue"    column="water_value"    />
+            <result property="waterHistoryValue"    column="water_history_value"    />
+            <result property="waterDaySum"    column="water_day_sum"    />
+            <result property="waterErrValue"    column="water_err_value"    />
+            <result property="waterErrMoney"    column="water_err_money"    />
+            <result property="operatorId"    column="operator_id"    />
+            <result property="delFlag"    column="del_flag"    />
+            <result property="createDept"    column="create_dept"    />
+            <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>
+
+    <!--<select id="selectReconciliationData" resultType="org.dromara.server.common.domain.consume.bo.ConsumptionBo">
+        SELECT term_no as termNo,user_numb as userNumb,card_no as cardNo,factory_id as factoryId
+        ,consume_date as consumeDate,consume_money as consumeMoney,term_record_id as termRecordId,status_flag as
+        statusFlag,record_id as recordId
+        FROM t_xf_consumedetailoriginal txc
+        <where>
+            and original_id NOT IN (SELECT original_id FROM t_xf_consumedetail where consume_date>#{consumeDate})
+            <if test="consumeDate != null">
+                AND txc.consume_date>#{consumeDate}
+            </if>
+        </where>
+        ORDER BY consume_date ASC;
+    </select>-->
+</mapper>

+ 49 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/XfConsumeDetailMapper.xml

@@ -0,0 +1,49 @@
+<?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.XfConsumeDetailMapper">
+
+    <resultMap type="org.dromara.server.hik.domain.XfConsumeDetail" id="XfConsumeDetailResult">
+            <result property="consumeId"    column="consume_id"    />
+            <result property="tenantId"    column="tenant_id"    />
+            <result property="originalId"    column="original_id"    />
+            <result property="recordId"    column="record_id"    />
+            <result property="userId"    column="user_id"    />
+            <result property="userNumb"    column="user_numb"    />
+            <result property="realName"    column="real_name"    />
+            <result property="deptId"    column="dept_id"    />
+            <result property="deptName"    column="dept_name"    />
+            <result property="consumeDate"    column="consume_date"    />
+            <result property="consumeMoney"    column="consume_money"    />
+            <result property="cardNo"    column="card_no"    />
+            <result property="factoryId"    column="factory_id"    />
+            <result property="cardValue"    column="card_value"    />
+            <result property="cardCount"    column="card_count"    />
+            <result property="consumeBalance"    column="consume_balance"    />
+            <result property="termNo"    column="term_no"    />
+            <result property="termName"    column="term_name"    />
+            <result property="termRecordId"    column="term_record_id"    />
+            <result property="analysisFlag"    column="analysis_flag"    />
+            <result property="statusFlag"    column="status_flag"    />
+            <result property="operatorId"    column="operator_id"    />
+            <result property="operatorName"    column="operator_name"    />
+            <result property="accountId"    column="account_id"    />
+            <result property="accountName"    column="account_name"    />
+            <result property="roomId"    column="room_id"    />
+            <result property="roomName"    column="room_name"    />
+            <result property="bagType"    column="bag_type"    />
+            <result property="mealType"    column="meal_type"    />
+            <result property="mealName"    column="meal_name"    />
+            <result property="smsSend"    column="sms_send"    />
+            <result property="recordStatus"    column="record_status"    />
+            <result property="syncStatus"    column="sync_status"    />
+            <result property="remark"    column="remark"    />
+            <result property="delFlag"    column="del_flag"    />
+            <result property="createDept"    column="create_dept"    />
+            <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>

+ 80 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/mapper/hik/XfTermMapper.xml

@@ -0,0 +1,80 @@
+<?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.XfTermMapper">
+
+    <resultMap type="org.dromara.server.hik.domain.XfTerm" id="XfTermResult">
+            <result property="termId"    column="term_id"    />
+            <result property="tenantId"    column="tenant_id"    />
+            <result property="termNo"    column="term_no"    />
+            <result property="termName"    column="term_name"    />
+            <result property="roomId"    column="room_id"    />
+            <result property="accountId"    column="account_id"    />
+            <result property="termType"    column="term_type"    />
+            <result property="qrCode"    column="qr_code"    />
+            <result property="stationId"    column="station_id"    />
+            <result property="consumeType"    column="consume_type"    />
+            <result property="autoDown"    column="auto_down"    />
+            <result property="useType"    column="use_type"    />
+            <result property="cardType"    column="card_type"    />
+            <result property="termIp"    column="term_ip"    />
+            <result property="termMac"    column="term_mac"    />
+            <result property="commPort"    column="comm_port"    />
+            <result property="mask"    column="mask"    />
+            <result property="serverIp"    column="server_ip"    />
+            <result property="serverPort"    column="server_port"    />
+            <result property="gatewayIp"    column="gateway_ip"    />
+            <result property="beatInterval"    column="beat_interval"    />
+            <result property="timeout"    column="timeout"    />
+            <result property="offlineTime"    column="offline_time"    />
+            <result property="advParam"    column="adv_param"    />
+            <result property="posParam"    column="pos_param"    />
+            <result property="rateParam"    column="rate_param"    />
+            <result property="workMode"    column="work_mode"    />
+            <result property="openMode"    column="open_mode"    />
+            <result property="maxCardMoney"    column="max_card_money"    />
+            <result property="constantValue"    column="constant_value"    />
+            <result property="rationZero"    column="ration_zero"    />
+            <result property="rationOne"    column="ration_one"    />
+            <result property="rationTwo"    column="ration_two"    />
+            <result property="rationThree"    column="ration_three"    />
+            <result property="rationFour"    column="ration_four"    />
+            <result property="rationFive"    column="ration_five"    />
+            <result property="rationSix"    column="ration_six"    />
+            <result property="rationSeven"    column="ration_seven"    />
+            <result property="rationEight"    column="ration_eight"    />
+            <result property="rationNine"    column="ration_nine"    />
+            <result property="dayCount"    column="day_count"    />
+            <result property="dayMoney"    column="day_money"    />
+            <result property="mealCount"    column="meal_count"    />
+            <result property="singleMoney"    column="single_money"    />
+            <result property="breakfastMoney"    column="breakfast_money"    />
+            <result property="lunchMoney"    column="lunch_money"    />
+            <result property="supperMoney"    column="supper_money"    />
+            <result property="nightMoney"    column="night_money"    />
+            <result property="breakfastBegin"    column="breakfast_begin"    />
+            <result property="breakfastEnd"    column="breakfast_end"    />
+            <result property="lunchBegin"    column="lunch_begin"    />
+            <result property="lunchEnd"    column="lunch_end"    />
+            <result property="supperBegin"    column="supper_begin"    />
+            <result property="supperEnd"    column="supper_end"    />
+            <result property="nightBegin"    column="night_begin"    />
+            <result property="nightEnd"    column="night_end"    />
+            <result property="swipeInterval"    column="swipe_interval"    />
+            <result property="termValidity"    column="term_validity"    />
+            <result property="recordId"    column="record_id"    />
+            <result property="uploadTime"    column="upload_time"    />
+            <result property="blackDownTime"    column="black_down_time"    />
+            <result property="lastCheck"    column="last_check"    />
+            <result property="rebootTime"    column="reboot_time"    />
+            <result property="remark"    column="remark"    />
+            <result property="delFlag"    column="del_flag"    />
+            <result property="createDept"    column="create_dept"    />
+            <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>

+ 3 - 0
ruoyi-server/ruoyi-server-face/src/main/resources/mapper/package-info.md

@@ -0,0 +1,3 @@
+java包使用 `.` 分割 resource 目录使用 `/` 分割
+<br>
+此文件目的 防止文件夹粘连找不到 `xml` 文件