Prechádzať zdrojové kódy

对接海康设备,初版,没有消费逻辑

xiari 1 rok pred
rodič
commit
579421673e
30 zmenil súbory, kde vykonal 1110 pridanie a 47 odobranie
  1. 7 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtXfTermService.java
  2. 11 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
  3. 16 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfTermServiceImpl.java
  4. 5 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/IXfTermService.java
  5. 20 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/service/impl/XfTermServiceImpl.java
  6. 1 1
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtUserAccountServiceImpl.java
  7. 11 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/constant/HikApiConstants.java
  8. 12 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/constant/HikEventTypeConstant.java
  9. 90 33
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/TestController.java
  10. 5 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/domain/dto/DeviceDto.java
  11. 57 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/EventHandleRouter.java
  12. 11 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/HikEventHandler.java
  13. 136 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmBo.java
  14. 17 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventConfirmingReceive.java
  15. 59 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventDetail.java
  16. 31 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/ConsumptionEventReceive.java
  17. 30 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/EventResponseInfo.java
  18. 19 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/FileContent.java
  19. 24 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/HeatBeatData.java
  20. 19 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/PictureResolution.java
  21. 19 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/RecordImageInfo.java
  22. 41 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventConfirmBo.java
  23. 92 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventDetail.java
  24. 31 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/domain/TransactionRecordEventReceive.java
  25. 120 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/ConsumptionEventHandler.java
  26. 23 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/HeatBeatHandler.java
  27. 122 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/handler/TransactionRecordEventHandler.java
  28. 24 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/event/timedtask/HandleTask.java
  29. 13 13
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/impl/SendDeviceServiceImpl.java
  30. 44 0
      ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/utils/DigestHttpUtil.java

+ 7 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtXfTermService.java

@@ -42,6 +42,13 @@ public interface RemotePtXfTermService {
      */
     RemoteXfTermVo queryByNo(Long termNo,String tenantId);
 
+    /**
+     * 根据设备MAC查询设备信息
+     * @param termMac 设备MAC
+     * @return 设备信息
+     */
+    RemoteXfTermVo queryByMac(String termMac);
+
     /**
      * 根据品牌查询设备列表
      * @param brand 设备品牌

+ 11 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java

@@ -171,4 +171,15 @@ public class FileUtils extends FileUtil {
             }
         }
     }
+
+    // 本地压缩图片
+    public static void main(String[] args) throws IOException {
+        String fileName = "C:\\Users\\LENOVO\\Desktop\\1978.jpg";
+        File file = new File(fileName);
+        // 获取bytes
+        FileOutputStream fileOutputStream = new FileOutputStream("C:\\file\\upload\\image\\avatar\\1978.jpg");
+        Img.from(file).setQuality(0.5f).write(fileOutputStream);
+        fileOutputStream.close();
+
+    }
 }

+ 16 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/consumption/dubbo/RemoteXfTermServiceImpl.java

@@ -65,6 +65,22 @@ public class RemoteXfTermServiceImpl implements RemotePtXfTermService {
         return null;
     }
 
+    /**
+     * 根据设备MAC查询设备信息
+     *
+     * @param termMac 设备MAC
+     * @return 设备信息
+     */
+    @Override
+    public RemoteXfTermVo queryByMac(String termMac) {
+         XfTermVo vo = xfTermService.queryByMac(termMac);
+         if (ObjectUtil.isNotEmpty(vo)) {
+              return MapstructUtils.convert(vo, RemoteXfTermVo.class);
+         }
+        return null;
+    }
+
+
     /**
      * 根据品牌查询设备列表
      *

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

@@ -100,4 +100,9 @@ public interface IXfTermService {
      */
      XfTermVo queryByNo(Long termNo, String tenantId);
 
+    /**
+     * 根据设备的mac查询设备信息
+     */
+    XfTermVo queryByMac(String termMac);
+
 }

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

@@ -289,4 +289,24 @@ public class XfTermServiceImpl implements IXfTermService {
         }
         return null;
     }
+
+    /**
+     * 根据设备的mac查询设备信息
+     *
+     * @param termMac 设备的mac地址
+     */
+    @Override
+    public XfTermVo queryByMac(String termMac) {
+        List<XfTermVo> termVos = TenantHelper.ignore(() -> {
+            return 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);
+    }
+
+
 }

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

@@ -79,7 +79,7 @@ public class PtUserAccountServiceImpl implements IPtUserAccountService {
     @DubboReference
     private final RemoteDeptService remoteDeptService;
 
-    @Value("${photo-prefix}")
+    @Value("${photo-prefix:}")
     private String photoPrefix;
 
     /**

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

@@ -26,4 +26,15 @@ public interface HikApiConstants {
      * 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";
 }

+ 12 - 0
ruoyi-server/ruoyi-server-hik/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";
+}

+ 90 - 33
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/controller/TestController.java

@@ -1,23 +1,44 @@
 package org.dromara.server.hik.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import io.undertow.servlet.spec.PartImpl;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.Part;
+import jodd.util.StringUtil;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
 import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.StringUtils;
 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.service.ISendDeviceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StreamUtils;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartHttpServletRequest;
+import org.springframework.web.multipart.MultipartResolver;
+import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
 import java.nio.file.Path;
+import java.util.*;
 
 /**
  * 海康消费机
@@ -34,9 +55,12 @@ import java.nio.file.Path;
 @RestController
 @RequestMapping("/hik/test")
 @SaIgnore
+@Slf4j
 public class TestController {
     private final ISendDeviceService sendDeviceService;
 
+    private final EventHandleRouter eventHandleRouter;
+
     //region 设备监听相关
     /**
      * 设置指定设备监听服务地址
@@ -223,47 +247,78 @@ public class TestController {
     }
 
     @PostMapping(("/info"))
-    public void receiveDevicePushData(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    public Map<String, Object> receiveDevicePushData(HttpServletRequest request) throws IOException {
         try {
             // 判断请求类型
-            if (isMultipartRequest(request)) {
-                // 转换为 Multipart 请求对象
-                MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+            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();
 
-                multipartRequest.getParameterMap().forEach((fieldName, values) -> {
-                    System.out.println("字段 [" + fieldName + "] 值: " + String.join(", ", values));
-                });
+                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");
 
-                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);
+                            FileUtils.copyInputStreamToFile(inputStream, image_file);*/
+                        }
+                        default -> {
+                            log.warn("out contentType : multipart/form-data; unknown contentType " + contentType + " 跳过");
                         }
                     }
-                });
-            } else {
-                // 处理 JSON 请求
-                handleJsonRequest(request);
-            }
 
-            response.getWriter().write("success");
-        } catch (Exception e) {
-            e.printStackTrace();
-            response.getWriter().write("false");
-        }
-    }
+                    /*MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
 
-    // 判断是否是 multipart 请求
-    private boolean isMultipartRequest(HttpServletRequest request) {
-        return request.getContentType() != null
-                   && request.getContentType().startsWith("multipart/form-data");
+                    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;
     }
 
       // 处理 JSON 请求
-    private void handleJsonRequest(HttpServletRequest request) throws IOException {
+    private String handleJsonRequest(HttpServletRequest request) throws IOException {
         if (request.getContentType() != null
                 && request.getContentType().contains("application/json")) {
 
@@ -274,11 +329,13 @@ public class TestController {
                     requestData.append(line);
                 }
 
+                return requestData.toString();
                 // 使用任意 JSON 库解析
-                JSONObject jsonObject = JSONUtil.parseObj(requestData.toString());
-                System.out.println("收到 JSON 数据:");
-                System.out.println(jsonObject.toString());
+//                JSONObject jsonObject = JSONUtil.parseObj(requestData.toString());
+//                System.out.println("收到 JSON 数据:");
+//                System.out.println(jsonObject.toString());
             }
         }
+        return null;
     }
 }

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

@@ -28,6 +28,11 @@ public class DeviceDto implements Serializable {
      */
     private Integer termNo;
 
+    /**
+     * 设备MAC 海康设备 必须要有
+     */
+    private String termMac;
+
     /**
      * 管理账号
      */

+ 57 - 0
ruoyi-server/ruoyi-server-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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-hik/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;
+}

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

@@ -0,0 +1,41 @@
+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 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-hik/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-hik/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;
+}

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

@@ -0,0 +1,120 @@
+package org.dromara.server.hik.event.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.StringUtils;
+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.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class ConsumptionEventHandler implements HikEventHandler {
+
+    public static final String TRANSACTION_PREPROCESSING_REQUEST = "transactionPreprocessingRequest";
+    public static final String TRANSACTION_CONFIRMING_REQUEST = "transactionConfirmingRequest";
+
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        ConsumptionEventReceive receive = jsonObject.toJavaObject(ConsumptionEventReceive.class);
+        ConsumptionEventDetail consumptionEvent = receive.getConsumptionEvent();
+        log.info("消费机事件,类型:{}", consumptionEvent.getMinor());
+        if (TRANSACTION_PREPROCESSING_REQUEST.equals(consumptionEvent.getMinor())) {
+            /**
+             * 消费机 交易预处理请求事件上报
+             * eventType: ConsumptionEvent
+             * minor:transactionPreprocessingRequest
+             */
+            // 业务逻辑处理的结果
+            boolean bussiRs  = true;
+            // todo 处理交易预处理请求事件上报 根据业务数据校验 能不能消费 权限、账号、金额、设备、业务数据
+
+
+            // 应答
+            ConsumptionEventConfirmBo confirmBo = new ConsumptionEventConfirmBo();
+            confirmBo.setSerialNo(consumptionEvent.getSerialNo());
+            confirmBo.setResult(bussiRs ?"success":"failed");
+//            confirmBo.setReason("platformException");
+            confirmBo.setMode(consumptionEvent.getMode());
+            log.info("消费机交易预处理请求事件,moshi:{}", consumptionEvent.getMode());
+             confirmBo.setName(consumptionEvent.getName());
+            confirmBo.setEmployeeNoString(consumptionEvent.getEmployeeNoString());
+            if(StringUtils.isNotBlank(consumptionEvent.getCardNo())){
+                confirmBo.setCardNo(consumptionEvent.getCardNo());
+            }else{
+                log.info("消费机交易预处理请求事件,方式:{}", "人脸或者二维码,不带卡号");
+            }
+            confirmBo.setActualPayment(consumptionEvent.getTotalPayment());
+            confirmBo.setBalanceBeforeDeduct("10000"); // 未扣款前的余额,要根据余额加上扣款金额,单位为分 ,金额模式必填
+//            confirmBo.setTimes(0); // 次数模式必填,有这个字段时可以不填
+                      /*
+            confirmBo.setQRCodeType("Alipay");
+            confirmBo.setPaymentOrderNo("1234567890");
+            confirmBo.setSecondaryConfirmType("none");*/
+//            confirmBo.setRemainingTimes(0); // 次数模式必填,有这个字段时必填
+
+           /* String broadcastVoice = "成功消费了"
+                + new BigDecimal(consumptionEvent.getTotalPayment()).divide(new BigDecimal("100.0"),  2, RoundingMode.HALF_UP)
+                +  "元";*/
+
+            String broadcastVoice = "count".equals(consumptionEvent.getMode())  ? "刷卡成功":"消费成功";
+
+
+            if(!bussiRs){
+                broadcastVoice = "count".equals(consumptionEvent.getMode())  ? "刷卡失败":"消费失败";
+                // 提示刷卡失败的原因
+                broadcastVoice += "就是不让你刷卡";
+                confirmBo.setReason("platformException");
+            }
+
+            confirmBo.setCustomTTSBroadcastVoice(broadcastVoice);
+
+            /**
+             * 这可以显示刷卡人的信息,比如姓名,工号,卡号,金额,消费时间等
+             */
+            ConsumptionEventConfirmBo.ContentInfo contentInfo = new ConsumptionEventConfirmBo.ContentInfo();
+            contentInfo.setTitle("****这是一个标题*****");
+            contentInfo.setContent("*****这是一个偶然的内容********");
+            confirmBo.setContentInfo(contentInfo);
+
+            // confirmBo 转Map,如果字段为空则丢弃
+
+
+
+            HashMap<String, Object> rs = new HashMap<>(1);
+            rs.put("ConsumptionEventConfirm", confirmBo.toMap());
+            return rs;
+        }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;
+            }
+
+            // todo 进行扣费逻辑处理
+
+            return rs;
+
+
+        }else{
+            log.warn("消费机事件,未知类型:{}", consumptionEvent.getMinor());
+        }
+
+        return null;
+    }
+}

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

@@ -0,0 +1,23 @@
+package org.dromara.server.hik.event.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+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.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Slf4j
+@Component
+public class HeatBeatHandler implements HikEventHandler {
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        // 1.JSONObject 转换成心跳 HeadBeatData对象
+        HeatBeatData heatBeatData = jsonObject.toJavaObject(HeatBeatData.class);
+        log.info("接收到心跳数据:{}", heatBeatData);
+
+        return null;
+    }
+}

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

@@ -0,0 +1,122 @@
+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.backstage.api.RemotePtXfTermService;
+import org.dromara.backstage.api.domain.vo.RemoteXfTermVo;
+import org.dromara.common.core.utils.file.FileUtils;
+import org.dromara.server.hik.domain.dto.DeviceDto;
+import org.dromara.server.hik.enums.ContentTypeEnum;
+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.ISendDeviceService;
+import org.dromara.server.hik.service.impl.SendDeviceServiceImpl;
+import org.dromara.server.hik.utils.DigestHttpUtil;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.dromara.server.hik.constant.HikApiConstants.TRANSACTION_RECORD_Event_confirm;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionRecordEventHandler implements HikEventHandler {
+
+    @DubboReference
+    private RemotePtXfTermService ptXfTermService;
+
+    private final DigestHttpUtil httpUtil;
+
+    @Override
+    public Map<String, Object> handleEvent(JSONObject jsonObject, FileContent fileContent) {
+        TransactionRecordEventReceive transactionRecordEventReceive = jsonObject.toJavaObject(TransactionRecordEventReceive.class);
+        TransactionRecordEventDetail transactionRecordEvent = transactionRecordEventReceive.getTransactionRecordEvent();
+        log.info("消费机交易记录事件,类型,在线or离线:{},{}", transactionRecordEvent.getModeType(), jsonObject.toJSONString());
+
+
+        //查询设备信息
+        RemoteXfTermVo remoteXfTermVo = ptXfTermService.queryByMac(transactionRecordEventReceive.getMacAddress());
+        if (remoteXfTermVo == null) {
+            log.error("消费机交易记录事件,设备信息为空,mac:{}, 不存在系统中", transactionRecordEventReceive.getMacAddress());
+
+            // 直接应答失败
+//            answerEvent(transactionRecordEventReceive, false,null,  null);
+            return null;
+        }
+        DeviceDto deviceDto = SendDeviceServiceImpl.getDeviceDto(remoteXfTermVo);
+
+
+
+        boolean answerResult;
+        // todo 业务处理
+        // 如果是在线交易的,平台根据流水号判断当前交易记录事件是否为未处理事件,若为未处理事件,则平台进行处理并进行扣费;
+        // 如果是离线交易记录事件,则平台进行处理并进行扣费;
+
+
+        //todo 这里只能应答成功,如果是业务异常(余额不足、脏数据等)失败,要存入表中,使用定时任务处理失败,所以即使失败的 也要回复成功
+
+        // 判断流水号是奇数还是偶数,这里做测试用
+        if (transactionRecordEvent.getSerialNo() % 2 == 0) {
+            answerResult = false;
+        } else {
+            answerResult = true;
+        }
+
+        // 图片保存
+        if (fileContent != null) {
+            String fileName = deviceDto.getTermNo()+"_"+transactionRecordEvent.getSerialNo()+  "_" + UUID.randomUUID().toString() + fileContent.getSuffix();
+            byte[] content = fileContent.getContent();
+            String image_dir_path = "C:\\image\\";
+            FileUtils.writeBytes(content, image_dir_path+fileName);
+            log.info("消费机交易记录事件,图片名称:{}", fileName);
+        }
+
+        //应答
+        return answerEvent(transactionRecordEventReceive, answerResult,new BigDecimal("1000"));
+
+    }
+
+    /**
+     * @param info 事件信息
+     * @param answerResult 应答结果
+     * 应答事件 put /ISAPI/Consume/transactionRecordEventConfirm?format=json 应答TransactionRecordEvent
+     */
+    public Map<String, Object>  answerEvent(TransactionRecordEventReceive info, boolean answerResult, BigDecimal balance){
+        TransactionRecordEventConfirmBo confirmBo = new TransactionRecordEventConfirmBo();
+
+//        String ipAddress = info.getIpAddress();
+//        Integer port = info.getPortNo() != null ? info.getPortNo() : 80;
+
+        TransactionRecordEventDetail transactionDetail  = info.getTransactionRecordEvent();
+        confirmBo.setSerialNo(transactionDetail.getSerialNo());
+        confirmBo.setResult(answerResult ? "success" : "failed");
+        confirmBo.setEmployeeNo(transactionDetail.getEmployeeNoString());
+        //余额的单位为分
+        if(balance == null){
+            balance = new BigDecimal("0");
+        }
+        confirmBo.setBalance(balance.multiply(new BigDecimal("100")).toString());
+
+        HashMap<String, Object> map = new HashMap<>(1);
+        if(answerResult){
+            map.put("TransactionRecordEventConfirm", confirmBo);
+        }else{
+            HashMap<String, Object> hashMap = new HashMap<>(2);
+            hashMap.put("serialNo", transactionDetail.getSerialNo());
+            hashMap.put("result", "failed");
+            map.put("TransactionRecordEventConfirm", hashMap);
+        }
+
+        return map;
+    }
+}

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

@@ -0,0 +1,24 @@
+package org.dromara.server.hik.event.timedtask;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+/**
+ * 定时任务: 主要用于处理 TransactionRecordEvent事件 业务失败的记录 于凌晨执行即可
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class HandleTask {
+
+
+    // 每天凌晨3点执行一次
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void handleTransactionRecordEventTask()
+    {
+        /*有离线的,也有非离线的,重复在执行下 处理处理TransactionRecordEvent的逻辑 */
+        log.info("开始处理 TransactionRecordEvent事件,");
+    }
+}

+ 13 - 13
ruoyi-server/ruoyi-server-hik/src/main/java/org/dromara/server/hik/service/impl/SendDeviceServiceImpl.java

@@ -112,18 +112,18 @@ public class SendDeviceServiceImpl implements ISendDeviceService {
 
         // TODO 2025-05-24 因为人员照片原因,暂时不将人脸照片上传到消费机
         // 设置用户人脸图片信息
-        // String photo = accountVo.getFacePicUrl();
-        // 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);
-        // }
+         String photo = accountVo.getFacePicUrl();
+         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);
+         }
 
         return empDto;
 
@@ -137,7 +137,7 @@ public class SendDeviceServiceImpl implements ISendDeviceService {
      * @return 转换后的 DeviceDto 对象
      */
     @NotNull
-    private static DeviceDto getDeviceDto(@NotNull RemoteXfTermVo termVo) {
+    public static DeviceDto getDeviceDto(@NotNull RemoteXfTermVo termVo) {
         DeviceDto dto = new DeviceDto();
         dto.setTermNo(termVo.getTermNo().intValue());
         dto.setAdminName(termVo.getAdminName());

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

@@ -11,6 +11,7 @@ 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;
@@ -86,4 +87,47 @@ public class DigestHttpUtil {
         }
     }
 
+    /**
+     * 以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());
+        }
+    }
+
+
 }