Преглед на файлове

fix: 消费服务
1.照片上传与人脸识别优化

luo.yibo@datuai.com преди 1 година
родител
ревизия
97fe9e4ba2
променени са 32 файла, в които са добавени 1883 реда и са изтрити 439 реда
  1. 1 0
      ruoyi-common/pom.xml
  2. 104 13
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java
  3. 40 0
      ruoyi-common/ruoyi-common-face/pom.xml
  4. 31 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/IFaceAlgorithm.java
  5. 26 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/confg/AlgorithmConstants.java
  6. 49 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/Face3DAngle.java
  7. 49 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceAttributeInfo.java
  8. 61 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceInfo.java
  9. 41 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceRect.java
  10. 49 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/convert/IFaceInfoConvertStrategy.java
  11. 133 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/convert/iml/ArcFaceInfoConvertImpl.java
  12. 27 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcFaceConfig.java
  13. 66 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcFaceEngineFactory.java
  14. 56 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcResultCodeEnum.java
  15. 182 0
      ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/service/ArcFaceAlgorithmIml.java
  16. 3 0
      ruoyi-common/ruoyi-common-face/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  17. 6 0
      ruoyi-modules/ruoyi-backstage/pom.xml
  18. 278 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/accouunt/UserFaceBusiness.java
  19. 42 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/SelfController.java
  20. 59 46
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/controller/PtUserAccountController.java
  21. 59 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/PtFaceFeature.java
  22. 60 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/bo/PtFaceFeatureBo.java
  23. 84 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/vo/PtFaceFeatureVo.java
  24. 36 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/mapper/PtFaceFeatureMapper.java
  25. 72 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/IPtFaceFeatureService.java
  26. 116 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtFaceFeatureServiceImpl.java
  27. 1 8
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/contorller/WxController.java
  28. 0 23
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/FaceEngineService.java
  29. 1 3
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/IWxService.java
  30. 0 134
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/FaceEngineServiceImpl.java
  31. 94 212
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/WxServiceImpl.java
  32. 57 0
      ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/payment/PtFaceFeatureMapper.xml

+ 1 - 0
ruoyi-common/pom.xml

@@ -45,6 +45,7 @@
         <module>ruoyi-common-nacos</module>
         <module>ruoyi-common-bus</module>
         <module>ruoyi-common-message</module>
+        <module>ruoyi-common-face</module>
     </modules>
 
     <artifactId>ruoyi-common</artifactId>

+ 104 - 13
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/file/FileUtils.java

@@ -1,21 +1,20 @@
 package org.dromara.common.core.utils.file;
 
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.img.Img;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.UUID;
-import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ObjectUtil;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.StringUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.File;
-import java.io.IOException;
+import java.io.*;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * 文件处理工具类
@@ -52,32 +51,124 @@ public class FileUtils extends FileUtil {
 
     /**
      * 根据旧文件名生成新文件名,使用 uuid 生成
+     *
      * @param oldName 旧文件名
      * @return 新文件名
      */
-    public static String getNewFileName(String oldName){
+    public static String getNewFileName(String oldName) {
         String extension = StringUtils.substringAfter(oldName, ".");
         return UUID.randomUUID().toString() + "." + extension;
     }
 
     /**
      * 上传文件
+     *
      * @param file
      * @param path
      * @param fileName
      * @throws IOException
      */
     public static void upload(MultipartFile file, String path, String fileName) throws IOException {
-        //1. 判断文件夹是否存在,不存在则创建
+        // 1. 判断文件夹是否存在,不存在则创建
         File imageDir = new File(path);
-        if (!imageDir.exists()){
+        if (!imageDir.exists()) {
             imageDir.mkdirs();
         }
 
-        //2. 拼接文件完整路径
-        String imageFilePath = path + fileName;
-
-        //3. 保存照片
+        // 3. 保存照片
         file.transferTo(new File(imageDir.getAbsoluteFile(), fileName));
     }
+
+    public static String toBase64(String filePath) throws Exception {
+        if (ObjectUtil.isEmpty(filePath)) {
+            throw new Exception("[文件转换错误]-[文件路径为空]");
+        } else {
+            return toBase64(new File(filePath));
+        }
+    }
+
+    public static String toBase64(File file) throws Exception {
+        if (ObjectUtil.isEmpty(file)) {
+            throw new Exception("[文件转换错误]-[文件为空]");
+        } else if (!file.exists()) {
+            throw new Exception("[文件转换错误]-[文件不存在]");
+        } else {
+            byte[] data = FileUtil.readBytes(file);
+            return Base64.encode(data);
+        }
+    }
+
+    public static String toBase64(MultipartFile file) throws Exception {
+        if (ObjectUtil.isEmpty(file)) {
+            throw new Exception("[文件转换错误]-[文件为空]");
+        } else {
+            byte[] data = file.getBytes();
+            return Base64.encode(data);
+        }
+    }
+
+    public static byte[] imgCompression(byte[] imageBytes) {
+        // 小于1M就不进行压缩里,浪费执行时间
+        float quality = 0f;
+        if (imageBytes.length > 1024 * 1024 * 10) { // 大于10M
+            quality = 0.1f;
+        } else if (imageBytes.length > 1024 * 1024 * 5) { // 大于5M
+            quality = 0.2f;
+        } else if (imageBytes.length > 1024 * 1024) {// 大于1M
+            quality = 0.5f;
+        }
+
+        if (quality != 0) {
+            ByteArrayInputStream bis = null;
+            ByteArrayOutputStream bos = null;
+            try {
+                bis = new ByteArrayInputStream(imageBytes);
+                bos = new ByteArrayOutputStream();
+                Img.from(bis).setQuality(quality).write(bos);
+                imageBytes = bos.toByteArray();
+            } catch (Exception e) {
+                throw new ServiceException("图片处理失败,请稍后重试!");
+            } finally {
+                if (bis != null) {
+                    try {
+                        bis.close();
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+                if (bos != null) {
+                    try {
+                        bos.close();
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+        return imageBytes;
+    }
+
+    public static void saveFileToDisk(String fileName, byte[] fileData) throws IOException {
+        // 1.上传照片到指定目录
+        ByteArrayInputStream bis = new ByteArrayInputStream(fileData);
+        byte[] bytes = new byte[1024];
+        int index;
+
+        FileOutputStream downloadFile = null;
+        try {
+            downloadFile = new FileOutputStream(fileName);
+            while ((index = bis.read(bytes)) != -1) {
+                downloadFile.write(bytes, 0, index);
+                downloadFile.flush();
+            }
+        } catch (IOException e) {
+            throw new ServiceException("文件处理失败,请稍后重试!");
+        } finally {
+            bis.close();
+            if (downloadFile != null) {
+
+                downloadFile.close();
+            }
+        }
+    }
 }

+ 40 - 0
ruoyi-common/ruoyi-common-face/pom.xml

@@ -0,0 +1,40 @@
+<?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-common</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-common-face</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-core</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.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-json</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 31 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/IFaceAlgorithm.java

@@ -0,0 +1,31 @@
+package org.dromara.common.face;
+
+
+import org.dromara.common.core.domain.R;
+
+/**
+ * Defines the contract for a face recognition algorithm.
+ * Implementations of this interface are expected to provide methods for
+ * detecting, recognizing, and processing faces in images or video streams.
+ * This interface serves as a base for various face recognition algorithms,
+ * allowing for flexibility and extensibility in different applications.
+ */
+public interface IFaceAlgorithm {
+
+    /**
+     * Detects faces in the provided image data.
+     *
+     * @param imageData the base64 encoded string of the image to be processed
+     * @return a response object indicating the success or failure of the face detection operation
+     */
+    R<Void> detectFaces(String imageData);
+
+    /**
+     * Extracts facial features from the provided image data.
+     *
+     * @param imageData the base64 encoded string of the image to be processed
+     * @return a response object indicating the success or failure of the feature extraction operation
+     */
+    R<String> extractFeatures(String imageData);
+
+}

+ 26 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/confg/AlgorithmConstants.java

@@ -0,0 +1,26 @@
+package org.dromara.common.face.confg;
+
+/**
+ * The AlgorithmConstants interface defines a set of constants used across various algorithms.
+ * These constants are intended to standardize values such as default parameters, threshold values,
+ * or other configuration settings that are shared among different algorithm implementations.
+ * Implementations and classes that use these constants should import this interface to ensure
+ * consistency and maintainability in the codebase.
+ */
+public interface AlgorithmConstants {
+
+    /**
+     * 虹软人脸
+     */
+    String ARC_FACE = "arc_face";
+
+    /**
+     * 虹软人脸算法
+     */
+    String ARC_FACE_ALGORITHM = "arc_face_algorithm";
+
+    /**
+     * 虹软人脸数据转换
+     */
+    String ARC_FACE_CONVERT = "arc_face_convert";
+}

+ 49 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/Face3DAngle.java

@@ -0,0 +1,49 @@
+package org.dromara.common.face.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 人脸3D角度定义
+ */
+@Data
+public class Face3DAngle implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -1204120641547016294L;
+
+    /**
+     * 绕垂直轴(Z轴)的旋转
+     */
+    private float yaw;
+
+    /**
+     * 绕纵向轴(X轴)的旋转(侧倾)
+     */
+    private float roll;
+
+    /**
+     * 绕横向轴(Y轴)的旋转(抬头/低头)
+     */
+    private float pitch;
+
+    public Face3DAngle() {
+        yaw = 0.0F;
+        roll = 0.0F;
+        pitch = 0.0F;
+    }
+
+    public Face3DAngle(Face3DAngle obj) {
+        if (obj == null) {
+            yaw = 0.0F;
+            roll = 0.0F;
+            pitch = 0.0F;
+        } else {
+            yaw = obj.getYaw();
+            roll = obj.getRoll();
+            pitch = obj.getPitch();
+        }
+    }
+}

+ 49 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceAttributeInfo.java

@@ -0,0 +1,49 @@
+package org.dromara.common.face.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @ClassName FaceAttributeInfo
+ * @Description 人脸属性信息
+ * @Author luoyibo
+ * @Date 2025-04-14 12:48
+ * @Version 1.0
+ * @since jdk17
+ */
+@Data
+public class FaceAttributeInfo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -3581367114147385416L;
+
+    /**
+     * 戴眼镜状态, 0 未戴眼镜;1 戴眼镜;2 墨镜
+     */
+    private float wearGlasses;
+
+    /**
+     * 左眼状态, 0 闭眼;1 睁眼
+     */
+    private float leftEyeOpen;
+
+    /**
+     * 右眼状态, 0 闭眼;1 睁眼
+     */
+    private float rightEyeOpen;
+
+    /**
+     * 张嘴状态, 0 张嘴;1 合嘴
+     */
+    private int mouthClose;
+
+
+    public FaceAttributeInfo() {
+        wearGlasses = 0.0F;
+        leftEyeOpen = 1.0F;
+        rightEyeOpen = 1.0F;
+        mouthClose = 0;
+    }
+}

+ 61 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceInfo.java

@@ -0,0 +1,61 @@
+package org.dromara.common.face.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @ClassName FaceInfo
+ * @Description 图片识别的人脸信息
+ * @Author luoyibo
+ * @Date 2025-04-14 12:54
+ * @Version 1.0
+ * @since jdk17
+ */
+@Data
+public class FaceInfo implements Serializable {
+    /**
+     * 人脸数据的长度,固定5000长度
+     */
+    public static final int FACE_DATA_SIZE = 5000;
+    @Serial
+    private static final long serialVersionUID = 8437137140360048860L;
+    /**
+     * 人脸位置信息
+     */
+    private FaceRect rect = new FaceRect();
+    /**
+     * 人脸角度信息
+     */
+    private int orient = 0;
+    /**
+     * 人脸id
+     */
+    private int faceId = -1;
+    /**
+     * 人脸数据
+     */
+    private byte[] faceData = new byte[5000];
+    /**
+     * 人脸是否在边界内 0 人脸溢出;1 人脸在图像边界
+     * 内
+     */
+    private int isWithinBoundary;
+
+    /**
+     * 人脸额头区域
+     */
+    private FaceRect foreheadRect;
+
+    /**
+     * 人脸属性信息
+     */
+    private FaceAttributeInfo faceAttributeInfo;
+
+    /**
+     * 人脸3D角度信息
+     */
+    private Face3DAngle face3DAngle;
+
+}

+ 41 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/FaceRect.java

@@ -0,0 +1,41 @@
+package org.dromara.common.face.domain;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @ClassName FaceRect
+ * @Description 人脸矩形区域
+ * @Author luoyibo
+ * @Date 2025-04-14 13:41
+ * @Version 1.0
+ * @since jdk17
+ */
+@Data
+public class FaceRect implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -8072412789667338427L;
+
+    /**
+     * 左边距
+     */
+    private int left;
+
+    /**
+     * 上边距
+     */
+    private int top;
+
+    /**
+     * 右边距
+     */
+    private int right;
+
+    /**
+     * 底边距
+     */
+    private int bottom;
+}

+ 49 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/convert/IFaceInfoConvertStrategy.java

@@ -0,0 +1,49 @@
+package org.dromara.common.face.domain.convert;
+
+import org.dromara.common.face.domain.Face3DAngle;
+import org.dromara.common.face.domain.FaceAttributeInfo;
+import org.dromara.common.face.domain.FaceInfo;
+import org.dromara.common.face.domain.FaceRect;
+
+/**
+ * @ClassName IFaceInfoConvertStrategy
+ * @Description 将不同人脸算法进行人脸识别的人脸数据转换成统一应用数据
+ * @Author luoyibo
+ * @Date 2025-04-14 13:05
+ * @Version 1.0
+ * @since jdk17
+ */
+public interface IFaceInfoConvertStrategy {
+
+    /**
+     * 人脸3D角度数据转换
+     *
+     * @param source 人脸算法生成的数据
+     * @return 转换成标准数据
+     */
+    Face3DAngle convertFace3DAngle(Object source);
+
+    /**
+     * 人脸属性数据转换
+     *
+     * @param source 人脸属性数据
+     * @return 转换后标准数据
+     */
+    FaceAttributeInfo convertFaceAttributeInfo(Object source);
+
+    /**
+     * 人脸位置数据转换
+     *
+     * @param source 人脸位置数据
+     * @return 转换后标准数据
+     */
+    FaceRect convertFaceRect(Object source);
+
+    /**
+     * 人脸数据转换
+     *
+     * @param source 人脸数据
+     * @return 转换后数据
+     */
+    FaceInfo convertFaceInfo(Object source);
+}

+ 133 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/domain/convert/iml/ArcFaceInfoConvertImpl.java

@@ -0,0 +1,133 @@
+package org.dromara.common.face.domain.convert.iml;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.face.confg.AlgorithmConstants;
+import org.dromara.common.face.domain.Face3DAngle;
+import org.dromara.common.face.domain.FaceAttributeInfo;
+import org.dromara.common.face.domain.FaceInfo;
+import org.dromara.common.face.domain.FaceRect;
+import org.dromara.common.face.domain.convert.IFaceInfoConvertStrategy;
+import org.springframework.stereotype.Service;
+
+/**
+ * @ClassName ArcFaceInfoConvertImpl
+ * @Description 虹软算法数据转换
+ * @Author luoyibo
+ * @Date 2025-04-14 13:18
+ * @Version 1.0
+ * @since jdk17
+ */
+@RequiredArgsConstructor
+@Service(AlgorithmConstants.ARC_FACE_CONVERT)
+public class ArcFaceInfoConvertImpl implements IFaceInfoConvertStrategy {
+    @Override
+    public Face3DAngle convertFace3DAngle(Object source) {
+        Face3DAngle face3DAngle = new Face3DAngle();
+        JSONObject jsonObject = JSONUtil.parseObj(source);
+        if (ObjectUtil.isNotEmpty(jsonObject.get("yaw"))) {
+            face3DAngle.setYaw(jsonObject.getFloat("yaw"));
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("pitch"))) {
+            face3DAngle.setPitch(jsonObject.getFloat("pitch"));
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("roll"))) {
+            face3DAngle.setYaw(jsonObject.getFloat("roll"));
+        }
+
+        return face3DAngle;
+    }
+
+    @Override
+    public FaceAttributeInfo convertFaceAttributeInfo(Object source) {
+        FaceAttributeInfo faceAttributeInfo = new FaceAttributeInfo();
+        JSONObject jsonObject = JSONUtil.parseObj(source);
+        if (ObjectUtil.isNotEmpty(jsonObject.get("wearGlasses"))) {
+            faceAttributeInfo.setWearGlasses(jsonObject.getFloat("wearGlasses"));
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("leftEyeOpen"))) {
+            faceAttributeInfo.setLeftEyeOpen(jsonObject.getFloat("leftEyeOpen"));
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("rightEyeOpen"))) {
+            faceAttributeInfo.setRightEyeOpen(jsonObject.getFloat("rightEyeOpen"));
+        }
+
+        if (ObjectUtil.isNotEmpty(jsonObject.get("mouthClose"))) {
+            faceAttributeInfo.setMouthClose(jsonObject.getInt("mouthClose"));
+        }
+
+        return faceAttributeInfo;
+    }
+
+    @Override
+    public FaceRect convertFaceRect(Object source) {
+        FaceRect faceRect = new FaceRect();
+        JSONObject jsonObject = JSONUtil.parseObj(source);
+        if (ObjectUtil.isNotEmpty(jsonObject.get("left"))) {
+            faceRect.setLeft(jsonObject.getInt("left"));
+        } else {
+            faceRect.setLeft(0);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("top"))) {
+            faceRect.setTop(jsonObject.getInt("top"));
+        } else {
+            faceRect.setTop(0);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("right"))) {
+            faceRect.setRight(jsonObject.getInt("right"));
+        } else {
+            faceRect.setRight(0);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("bottom"))) {
+            faceRect.setBottom(jsonObject.getInt("bottom"));
+        } else {
+            faceRect.setBottom(0);
+        }
+
+        return faceRect;
+    }
+
+    @Override
+    public FaceInfo convertFaceInfo(Object source) {
+        FaceInfo faceInfo = new FaceInfo();
+        JSONObject jsonObject = JSONUtil.parseObj(source);
+        if (ObjectUtil.isNotEmpty(jsonObject.get("orient"))) {
+            faceInfo.setOrient(jsonObject.getInt("orient"));
+        } else {
+            faceInfo.setOrient(0);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("faceId"))) {
+            faceInfo.setFaceId(jsonObject.getInt("faceId"));
+        } else {
+            faceInfo.setFaceId(-1);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("isWithinBoundary"))) {
+            faceInfo.setIsWithinBoundary(jsonObject.getInt("isWithinBoundary"));
+        } else {
+            faceInfo.setIsWithinBoundary(0);
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("face3DAngle"))) {
+            faceInfo.setFace3DAngle(convertFace3DAngle(jsonObject.getObj("face3DAngle")));
+        } else {
+            faceInfo.setFace3DAngle(new Face3DAngle());
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("faceAttributeInfo"))) {
+            faceInfo.setFaceAttributeInfo(convertFaceAttributeInfo(jsonObject.getObj("faceAttributeInfo")));
+        } else {
+            faceInfo.setFaceAttributeInfo(new FaceAttributeInfo());
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("rect"))) {
+            faceInfo.setRect(convertFaceRect(jsonObject.getObj("rect")));
+        } else {
+            faceInfo.setRect(new FaceRect());
+        }
+        if (ObjectUtil.isNotEmpty(jsonObject.get("foreheadRect"))) {
+            faceInfo.setForeheadRect(convertFaceRect(jsonObject.getObj("foreheadRect")));
+        } else {
+            faceInfo.setForeheadRect(null);
+        }
+        return faceInfo;
+    }
+}

+ 27 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcFaceConfig.java

@@ -0,0 +1,27 @@
+package org.dromara.common.face.iml.arcsoft.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration class for ArcFace SDK, used to manage and provide configuration properties required for the initialization and operation of the
+ * ArcFace face recognition engine.
+ * This class is annotated with {@link Configuration} and {@link ConfigurationProperties} to allow it to be automatically loaded by Spring and bind
+ * the application's configuration properties to its fields.
+ * The properties are prefixed with "arcconfig.arcface-sdk" in the application's configuration file.
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "arcconfig.arcface-sdk")
+public class ArcFaceConfig {
+    // 获取配置参数
+    private String sdkLibPath = "";
+    private String appId = "";
+    private String sdkKey = "";
+    private String activeKey = "";
+    private Integer detectPooSize = 5;
+    private Integer comparePooSize = 5;
+    private Float faceQuality = 0.6F;
+    private boolean faceMask = false;
+}

+ 66 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcFaceEngineFactory.java

@@ -0,0 +1,66 @@
+package org.dromara.common.face.iml.arcsoft.config;
+
+import com.arcsoft.face.EngineConfiguration;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.enums.ErrorInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.dromara.common.core.exception.ServiceException;
+
+/**
+ * Factory class for creating and managing instances of {@link FaceEngine} using a pool.
+ * This factory is responsible for the creation, initialization, and destruction of {@link FaceEngine} instances,
+ * ensuring that each engine is properly activated and initialized before being used.
+ */
+@Slf4j
+public class ArcFaceEngineFactory extends BasePooledObjectFactory<FaceEngine> {
+    private final String libPath;
+    private final String appId;
+    private final String sdkKey;
+    private final String activeKey;
+    private final EngineConfiguration engineConfiguration;
+
+
+    public ArcFaceEngineFactory(String libPath, String appId, String sdkKey, String activeKey,
+                                EngineConfiguration engineConfiguration) {
+        this.appId = appId;
+        this.sdkKey = sdkKey;
+        this.activeKey = activeKey;
+        this.libPath = libPath;
+        this.engineConfiguration = engineConfiguration;
+    }
+
+    @Override
+    public FaceEngine create() {
+        String message;
+        FaceEngine faceEngine = new FaceEngine(this.libPath);
+        // sdk4.0以上的激活方式
+        int activeCode = faceEngine.activeOnline(this.appId, this.sdkKey, this.activeKey);
+        if (activeCode != ErrorInfo.MOK.getValue() && activeCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
+            message = String.format("[人脸识别引擎激活]-[%s]", ArcResultCodeEnum.getMessage(activeCode));
+            log.error(message);
+            throw new ServiceException(message);
+        }
+        int initCode = faceEngine.init(this.engineConfiguration);
+        if (initCode != ErrorInfo.MOK.getValue()) {
+            message = String.format("[人脸识别引擎初始化]-[%s]", ArcResultCodeEnum.getMessage(initCode));
+            log.error(message);
+            throw new ServiceException(message);
+        }
+        return faceEngine;
+    }
+
+    @Override
+    public PooledObject<FaceEngine> wrap(FaceEngine faceEngine) {
+        return new DefaultPooledObject<>(faceEngine);
+    }
+
+    @Override
+    public void destroyObject(PooledObject<FaceEngine> pool) throws Exception {
+        FaceEngine faceEngine = pool.getObject();
+        faceEngine.unInit();
+        super.destroyObject(pool);
+    }
+}

+ 56 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/config/ArcResultCodeEnum.java

@@ -0,0 +1,56 @@
+package org.dromara.common.face.iml.arcsoft.config;
+
+/**
+ * 虹软人脸算结果码法枚举
+ */
+public enum ArcResultCodeEnum {
+    MOK(0, "成功"),
+    MERR_UNKNOWN(1, "错误原因不明"),
+    MERR_INVALID_PARAM(2, "无效的参数"),
+    MERR_UNSUPPORTED(3, "引擎不支持"),
+    MERR_FSDK_FR_INVALID_IMAGE_INFO(73730, "无效的输入图像参数"),
+    MERR_FSDK_FR_INVALID_FACE_INFO(73731, "无效的脸部信息"),
+    MERR_ASF_EX_INVALID_IMAGE_INFO(86021, "无效的输入图像"),
+    MERR_ASF_EX_INVALID_FACE_INFO(86022, "无效的脸部信息");
+
+
+    private final Integer code;
+    private final String message;
+
+    ArcResultCodeEnum(Integer code, String name) {
+        this.code = code;
+        this.message = name;
+    }
+
+    public static String getMessage(String name) {
+        for (ArcResultCodeEnum item : ArcResultCodeEnum.values()) {
+            if (item.name().equals(name)) {
+                return item.message;
+            }
+        }
+        return name;
+    }
+
+    public static String getMessage(Integer code) {
+        for (ArcResultCodeEnum item : ArcResultCodeEnum.values()) {
+            if (item.code().equals(code)) {
+                return item.message;
+            }
+        }
+        return "未知";
+    }
+
+    public Integer code() {
+        return this.code;
+    }
+
+    public String message() {
+        return this.message;
+    }
+
+    @Override
+    public String toString() {
+        return this.name();
+    }
+
+}

+ 182 - 0
ruoyi-common/ruoyi-common-face/src/main/java/org/dromara/common/face/iml/arcsoft/service/ArcFaceAlgorithmIml.java

@@ -0,0 +1,182 @@
+/*  */
+package org.dromara.common.face.iml.arcsoft.service;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ObjectUtil;
+import com.arcsoft.face.*;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.DetectOrient;
+import com.arcsoft.face.enums.ExtractType;
+import com.arcsoft.face.toolkit.ImageInfo;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.face.IFaceAlgorithm;
+import org.dromara.common.face.confg.AlgorithmConstants;
+import org.dromara.common.face.iml.arcsoft.config.ArcFaceConfig;
+import org.dromara.common.face.iml.arcsoft.config.ArcFaceEngineFactory;
+import org.dromara.common.face.iml.arcsoft.config.ArcResultCodeEnum;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.arcsoft.face.toolkit.ImageFactory.getRGBData;
+
+/**
+ * 虹软人脸算法实现
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service(AlgorithmConstants.ARC_FACE_ALGORITHM)
+public class ArcFaceAlgorithmIml implements IFaceAlgorithm {
+    private final ArcFaceConfig arcFaceConfig;
+    private GenericObjectPool<FaceEngine> faceEngineGeneralPool;
+
+    /**
+     * 获取引擎配置
+     *
+     * @return 引擎配置
+     */
+    private static EngineConfiguration getEngineConfiguration() {
+        FunctionConfiguration detectFunctionCfg = new FunctionConfiguration();
+        detectFunctionCfg.setSupportFaceDetect(true);
+        detectFunctionCfg.setSupportFaceRecognition(true);
+        detectFunctionCfg.setSupportImageQuality(true);
+        detectFunctionCfg.setSupportLiveness(true);
+        detectFunctionCfg.setSupportIRLiveness(true);
+        detectFunctionCfg.setSupportMaskDetect(true);
+        // detectFunctionCfg.setSupportUpdateFaceData(true);
+
+        EngineConfiguration detectCfg = new EngineConfiguration();
+        detectCfg.setFunctionConfiguration(detectFunctionCfg);
+        // IMAGE检测模式,用于处理单张的图像数据
+        detectCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
+        // 人脸检测角度,逆时针0度
+        detectCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);
+        detectCfg.setDetectFaceMaxNum(10);
+
+        return detectCfg;
+    }
+
+    /**
+     * 人脸识别
+     *
+     * @param imageData 人脸图片 the base64 encoded string of the image to be processed
+     * @return 识别结果
+     */
+    @Override
+    public R<Void> detectFaces(String imageData) {
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[图片人脸识别失败]-[无图片信息,请上传图片]");
+        }
+        byte[] imageBytes = Base64.decode(imageData);
+        ImageInfo imageInfo = getRGBData(imageBytes);
+        if (ObjectUtil.isEmpty(imageInfo)) {
+            return R.fail("[图片人脸识别失败]-[无效图片信息,请重新上传图片]");
+        }
+        FaceEngine faceEngine = null;
+        List<FaceInfo> faceInfoList = new ArrayList<>();
+        try {
+            faceEngine = faceEngineGeneralPool.borrowObject();
+            if (ObjectUtil.isEmpty(faceEngine)) {
+                return R.fail("[图片人脸识别失败]-[人脸识别引擎创建错误]");
+            }
+
+            int resultCode = faceEngine.detectFaces(imageInfo, faceInfoList);
+            if (resultCode == ArcResultCodeEnum.MOK.code()) {
+                return R.ok();
+            } else {
+                return R.fail(String.format("[人脸识别失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail("[图片人脸识别失败]-[服务响应异常]");
+        } finally {
+            // 释放人脸引擎
+            if (ObjectUtil.isNotEmpty(faceEngine)) {
+                faceEngineGeneralPool.returnObject(faceEngine);
+            }
+        }
+    }
+
+    /**
+     * 人脸特征提取
+     *
+     * @param imageData the base64 encoded string of the image to be processed
+     * @return 提取的人脸特征串
+     */
+    @Override
+    public R<String> extractFeatures(String imageData) {
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[人脸特征码提取失败]-[无图片信息,请上传图片]");
+        }
+        byte[] imageBytes = Base64.decode(imageData);
+        ImageInfo imageInfo = getRGBData(imageBytes);
+        if (ObjectUtil.isEmpty(imageInfo)) {
+            return R.fail("[人脸特征码提取失败]-[无效图片信息,请重新上传图片]");
+        }
+
+        FaceEngine faceEngine = null;
+        List<FaceInfo> faceInfoList = new ArrayList<>();
+        try {
+            // 创建人脸引擎
+            faceEngine = faceEngineGeneralPool.borrowObject();
+            if (ObjectUtil.isEmpty(faceEngine)) {
+                return R.fail("[人脸特征码提取失败]-[人脸识别引擎创建错误]");
+            }
+            // 检测图片是否满足人脸识别要求
+            int resultCode = faceEngine.detectFaces(imageInfo, faceInfoList);
+            if (resultCode != ArcResultCodeEnum.MOK.code()) {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+            // 图像质量检测
+            ImageQuality imageQuality = new ImageQuality();
+            resultCode = faceEngine.imageQualityDetect(imageInfo, faceInfoList.get(0), 0, imageQuality);
+            if (resultCode != ArcResultCodeEnum.MOK.code() || (imageQuality.getFaceQuality() < this.arcFaceConfig.getFaceQuality())) {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+            // 提取特征码
+            FaceFeature faceFeature = new FaceFeature();
+            if (this.arcFaceConfig.isFaceMask()) {
+                resultCode = faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.REGISTER, 1, faceFeature);
+            } else {
+                resultCode = faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.RECOGNIZE, 0, faceFeature);
+            }
+
+            if (resultCode == ArcResultCodeEnum.MOK.code()) {
+                return R.ok("[人脸特征码提取成功]", Base64.encode(faceFeature.getFeatureData()));
+            } else {
+                return R.fail(String.format("[人脸特征码提取失败]-[%s]", ArcResultCodeEnum.getMessage(resultCode)));
+            }
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail("[人脸特征码提取失败]-[服务响应异常]");
+        } finally {
+            // 释放人脸引擎
+            if (ObjectUtil.isNotEmpty(faceEngine)) {
+                faceEngineGeneralPool.returnObject(faceEngine);
+            }
+        }
+    }
+
+    /**
+     * 初始化人脸引擎
+     */
+    @PostConstruct
+    public void init() {
+        GenericObjectPoolConfig<FaceEngine> detectPoolConfig = new GenericObjectPoolConfig<>();
+        detectPoolConfig.setMaxIdle(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setMaxTotal(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setMinIdle(arcFaceConfig.getDetectPooSize());
+        detectPoolConfig.setLifo(false);
+        EngineConfiguration detectCfg = getEngineConfiguration();
+        faceEngineGeneralPool = new GenericObjectPool<>(
+            new ArcFaceEngineFactory(arcFaceConfig.getSdkLibPath(), arcFaceConfig.getAppId(), arcFaceConfig.getSdkKey(),
+                                     arcFaceConfig.getActiveKey(), detectCfg), detectPoolConfig);
+    }
+}

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

@@ -0,0 +1,3 @@
+org.dromara.common.face.domain.convert.iml.ArcFaceInfoConvertImpl
+org.dromara.common.face.iml.arcsoft.service.ArcFaceAlgorithmIml
+org.dromara.common.face.iml.arcsoft.config.ArcFaceConfig

+ 6 - 0
ruoyi-modules/ruoyi-backstage/pom.xml

@@ -173,6 +173,12 @@
             <version>2.2.0</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-face</artifactId>
+            <version>2.2.0</version>
+            <scope>compile</scope>
+        </dependency>
 
 
     </dependencies>

+ 278 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/accouunt/UserFaceBusiness.java

@@ -0,0 +1,278 @@
+package org.dromara.backstage.business.accouunt;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.payment.domain.bo.PtFaceFeatureBo;
+import org.dromara.backstage.payment.domain.bo.PtUserAccountBo;
+import org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo;
+import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
+import org.dromara.backstage.payment.service.IPtFaceFeatureService;
+import org.dromara.backstage.payment.service.IPtUserAccountService;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.file.FileUtils;
+import org.dromara.common.face.IFaceAlgorithm;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @ClassName UserFaceBusiness
+ * @Description 用户人脸照片处理业务
+ * @Author luoyibo
+ * @Date 2025-04-14 14:34
+ * @Version 1.0
+ * @since jdk17
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class UserFaceBusiness {
+    private final IPtUserAccountService userAccountService;
+    private final IPtFaceFeatureService faceFeatureService;
+    // 应用的人脸算法
+    @Value("${face-algorithm}")
+    String justFaceAlgorithm;
+    // 文件上传路径
+    @Value("${upload.upload-path}/")
+    private String uploadPath;
+    // 用户头像路径
+    @Value("${upload.image.user}/")
+    private String userPath;
+    // 外部人脸处理接口地址
+    @Value("${face-url}")
+    private String faceUrl;
+
+    /**
+     * 检测图片是否达到人脸识别的要求.
+     *
+     * @param userId 图片所属用户Id
+     * @return 检测结果
+     * @throws Exception 程序运行异常.
+     */
+    public R<Void> detectFaces(Long userId) throws Exception {
+        R<Map<String, String>> result = this.checkUserFaces(userId, "人脸识别");
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        String imageData = result.getData().get("imageData");
+        String faceAlgorithmName = justFaceAlgorithm + "_algorithm";
+        IFaceAlgorithm faceAlgorithm = SpringUtils.getBean(faceAlgorithmName, IFaceAlgorithm.class);
+
+        return faceAlgorithm.detectFaces(imageData);
+    }
+
+    /**
+     * 提取指定用户的人脸特征码
+     * 此处的照片数据是根据用户已有的图片地址获取到物理文件后进行base64编码得到
+     *
+     * @param userId 用户Id
+     * @return 成功时返回提取到的特征码数据,失败时返回相应错误
+     * @throws Exception 程序运行异常
+     */
+    public R<Void> extractFeatures(Long userId) throws Exception {
+        R<Map<String, String>> result = this.checkUserFaces(userId, "特征码提取");
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+
+        return this.extractUserFaces(userId, result.getData().get("imageData"));
+    }
+
+    /**
+     * 提取指定用户的人脸特征码
+     * 1.根据照片数据进行人脸识别与特征码提取
+     * 2.提取的特征码入数据库
+     * 3.根据照片数据生成人员的照片访问链接并将照片在本地硬盘存储
+     *
+     * @param userId    用户Id
+     * @param imageData 用户图片数据
+     * @return 成功时返回提取到的特征码数据,失败时返回相应错误
+     */
+    public R<Void> extractUserFaces(Long userId, String imageData) {
+        PtUserAccountVo vo = userAccountService.queryById(userId);
+        if (ObjectUtil.isEmpty(vo)) {
+            return R.fail(String.format("[照片处理失败]-[无此Id:%s对应的人员信息]", userId));
+        }
+        if (ObjectUtil.isEmpty(imageData)) {
+            return R.fail("[照片处理失败]-[无图片数据]");
+        }
+        // 提取特征码
+        String faceAlgorithmName = justFaceAlgorithm + "_algorithm";
+        IFaceAlgorithm faceAlgorithm = SpringUtils.getBean(faceAlgorithmName, IFaceAlgorithm.class);
+        R<String> createResult = faceAlgorithm.extractFeatures(imageData);
+        if (R.isError(createResult)) {
+            return R.fail(createResult.getMsg());
+        }
+        // 更新数据库
+        String photoUrl = vo.getPhoto();
+        if (ObjectUtil.isEmpty(photoUrl)) {
+            photoUrl = userId + ".jpg";
+        }
+        R<Void> result = this.saveFaceData(userId, photoUrl, createResult.getData());
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        // 保存文件到物理路径
+        result = this.saveImageData(userId, imageData);
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        return R.ok(createResult.getData());
+    }
+
+    /**
+     * 请求外部接口提取指定用户的人脸特征码
+     *
+     * @param userId    用户Id
+     * @param imageData 用户图片数据
+     * @return 成功时返回提取到的特征码数据,失败时返回相应错误
+     */
+
+    public R<Void> extractUserFacesForCloud(Long userId, String imageData) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("userId", userId.toString());
+        map.put("imageData", imageData);
+        HttpRequest req = HttpUtil.createPost(faceUrl);
+        req.body(JSONUtil.toJsonStr(map));
+
+        try (HttpResponse res = req.execute()) {
+            String newFileName = userId + ".jpg";
+            String sendResult = res.body();
+            if (ObjectUtil.isNotEmpty(sendResult)) {
+                // 外部接口返回
+                int code = JSONUtil.parseObj(sendResult).getInt("code");
+                String featureData = JSONUtil.parseObj(sendResult).getStr("msg");
+                if (code == 200) {
+                    // 外部接口调用成功,将返回的特征码及照片地址入库
+                    R<Void> result = this.saveFaceData(userId, newFileName, featureData);
+                    if (R.isError(result)) {
+                        return R.fail(result.getMsg());
+                    }
+                    // 保存图片到物理硬盘
+                    return this.saveImageData(userId, imageData);
+                } else {
+                    return R.fail("上传图片异常,请联系管理员");
+                }
+            } else {
+                return R.fail("上传图片异常,请联系管理员");
+            }
+        } catch (Exception e) {
+            log.error("上传图片异常,请联系管理员!{}", e.getMessage());
+            return R.fail("上传图片异常,请联系管理员");
+        }
+    }
+
+    /**
+     * 人脸照片相关数据入库
+     * 1.入人脸特征表,更新或增加特征数据与照片地址
+     * 2.入人员账户表,更新人员照片地址
+     *
+     * @param userId      用户Id
+     * @param photoUrl    照片地址
+     * @param featureData 人脸特征数据
+     * @return 操作结果
+     */
+    public R<Void> saveFaceData(Long userId, String photoUrl, String featureData) {
+        // 入人脸特征表
+        R<Void> result = this.insertFaceFeatureData(userId, photoUrl, featureData);
+        if (R.isError(result)) {
+            return R.fail(result.getMsg());
+        }
+        // 更新人员账户表
+        PtUserAccountBo bo = new PtUserAccountBo();
+        bo.setUserId(userId);
+        bo.setPhoto(photoUrl);
+        boolean update = userAccountService.updateByBo(bo);
+        if (update) {
+            return R.ok();
+        } else {
+            return R.fail("[更新账户照片地址失败]");
+        }
+    }
+
+    /**
+     * 保存人脸照片到物理硬盘
+     *
+     * @param userId    用户Id
+     * @param imageData 用户图片数据
+     * @return 保存结果
+     */
+    public R<Void> saveImageData(Long userId, String imageData) {
+        try {
+            // 对图片进行压缩处理
+            byte[] imageBytes = Base64.decode(imageData);
+            imageBytes = FileUtils.imgCompression(imageBytes);
+
+            // 保存图片到物理目录
+            String fileName = uploadPath + userPath + userId + ".jpg";
+            FileUtils.saveFileToDisk(fileName, imageBytes);
+            return R.ok();
+        } catch (Exception e) {
+            log.error(Arrays.toString(e.getStackTrace()));
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 人员验证以及根据人员已有的照片地址获取提取人脸特征的图片数据
+     *
+     * @param userId  用户Id
+     * @param message 验证消费
+     * @return 成功返回人员照片地址与特征码
+     * @throws Exception 处理异常
+     */
+    private R<Map<String, String>> checkUserFaces(Long userId, String message) throws Exception {
+        PtUserAccountVo vo = userAccountService.queryById(userId);
+        if (ObjectUtil.isEmpty(vo)) {
+            return R.fail(String.format("[%s失败]-[无此Id:%s对应的人员信息]", message, userId));
+        }
+        if (ObjectUtil.isEmpty(vo.getPhoto())) {
+            return R.fail(String.format("[%s失败]-[无此Id:%s对应的人员照片信息]", message, userId));
+        }
+        String fileUrl = uploadPath + userPath + vo.getPhoto();
+        File imageFile = new File(fileUrl);
+        if (!imageFile.exists()) {
+            return R.fail(String.format("[%s失败]-[无此Id:%s对应的人员照片信息]", message, userId));
+        }
+        String imageData = FileUtils.toBase64(imageFile);
+        Map<String, String> map = new HashMap<>();
+        map.put("photoUrl", vo.getPhoto());
+        map.put("imageData", imageData);
+        return R.ok(String.format("[%s成功]", message), map);
+    }
+
+    /**
+     * 将特征码数据保存入库
+     *
+     * @param userId      用户Id
+     * @param photoUrl    照片访问地址
+     * @param featureData 特征码数据
+     * @return 保存结果
+     */
+    private R<Void> insertFaceFeatureData(Long userId, String photoUrl, String featureData) {
+        PtFaceFeatureVo vo = faceFeatureService.getOneFeatureDataUser(userId);
+        int iCount;
+        if (ObjectUtil.isEmpty(vo)) {
+            PtFaceFeatureBo bo = new PtFaceFeatureBo();
+            bo.setUserId(userId);
+            bo.setPhotoUrl(photoUrl);
+            bo.setFeatureData(featureData);
+            bo.setFaceAlgorithm(justFaceAlgorithm);
+            iCount = faceFeatureService.insertByBo(bo);
+        } else {
+            iCount = faceFeatureService.updateFeatureDataByUserId(userId, featureData, photoUrl, justFaceAlgorithm);
+        }
+        return iCount > 0 ? R.ok("[特征码保存成功]") : R.fail("[特征码保存失败]");
+    }
+}

+ 42 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/controller/self/SelfController.java

@@ -2,6 +2,7 @@ package org.dromara.backstage.controller.self;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.business.accouunt.UserFaceBusiness;
 import org.dromara.backstage.business.payments.ThirdPayBusiness;
 import org.dromara.backstage.business.self.SelfBusiness;
 import org.dromara.backstage.business.self.TraineeBusiness;
@@ -30,6 +31,8 @@ public class SelfController {
     private final SelfBusiness selfBusiness;
     private final ThirdPayBusiness thirdPayBusiness;
     private final TraineeBusiness traineeBusiness;
+    private final UserFaceBusiness userFaceBusiness;
+
     /**
      * 发送手机短信
      *
@@ -92,5 +95,44 @@ public class SelfController {
         String roomData = mapInfo.get("roomData");
 
         return selfBusiness.getUserCardInfo(userFixId, roomFixId, roomData);
+
+    }
+
+    /**
+     * 指定人员人脸识别的结果
+     *
+     * @param userId 人员Id
+     * @return 人脸识别结果
+     * @throws Exception 识别异常
+     */
+    @RequestMapping(value = "api/v1/face/{userId}", method = RequestMethod.GET)
+    public R<Void> detectUserFaces(@PathVariable("userId") Long userId) throws Exception {
+        return userFaceBusiness.detectFaces(userId);
+    }
+
+    /**
+     * 生成指定人员的人脸特征
+     *
+     * @param userId 人员Id
+     * @return 人脸特征数据
+     * @throws Exception 提取异常
+     */
+    @RequestMapping(value = "api/v1/face/feature/{userId}", method = RequestMethod.GET)
+    public R<Void> extractFeatures(@PathVariable("userId") Long userId) throws Exception {
+        return userFaceBusiness.extractFeatures(userId);
+    }
+
+    /**
+     * 提供给外部进行人脸识别与特征码提取接口
+     *
+     * @param mapInfo 参数map,userId和imageData,imageData去掉了前面的base64标识
+     * @return 操作结果
+     */
+    @RequestMapping(value = "api/v1/face/feature", method = RequestMethod.POST)
+    public R<Void> extractFeatures(@RequestBody Map<String, String> mapInfo) {
+        Long userId = Long.valueOf(mapInfo.get("userId"));
+        String imageData = mapInfo.get("imageData");
+
+        return userFaceBusiness.extractUserFaces(userId, imageData);
     }
 }

+ 59 - 46
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/controller/PtUserAccountController.java

@@ -2,11 +2,14 @@ package org.dromara.backstage.payment.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.util.ObjectUtil;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.business.accouunt.UserFaceBusiness;
 import org.dromara.backstage.cardCenter.domain.bo.PtCardBo;
 import org.dromara.backstage.cardCenter.service.IPtCardService;
 import org.dromara.backstage.domain.vo.card.PtCardVo;
@@ -18,6 +21,8 @@ import org.dromara.backstage.payment.domain.vo.PtUserAccountInfoVo;
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.payment.service.IPtBagService;
 import org.dromara.backstage.payment.service.IPtUserAccountService;
+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.utils.StringUtils;
 import org.dromara.common.core.utils.file.FileUtils;
@@ -32,7 +37,6 @@ import org.dromara.common.message.kafka.aop.annotation.SyncDataToLocal;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.web.core.BaseController;
-import org.dromara.consume.api.RemoteConsumeService;
 import org.dromara.system.api.RemoteDeptService;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.MediaType;
@@ -54,6 +58,7 @@ import static org.dromara.common.message.kafka.constant.MessageEventTypeConstant
  * @author LionLi
  * @date 2024-08-05
  */
+@Slf4j
 @Validated
 @RequiredArgsConstructor
 @RestController
@@ -63,27 +68,27 @@ public class PtUserAccountController extends BaseController {
     private final IPtUserAccountService ptUserAccountService;
     private final IPtBagService ptBagService;
     private final IPtCardService ptCardService;
+    private final UserFaceBusiness userFaceBusiness;
+    private final DefaultConfig defaultConfig;
 
     @DubboReference
     private final RemoteDeptService remoteDeptService;
 
-    @DubboReference
-    private final RemoteConsumeService remoteConsumeService;
-
-    @Value("${upload.upload-path}/")    // 文件上传路径
-    private String uploadPath;
-    @Value("${upload.image.user}/")     // 用户头像路径
-    private String userPath;
-
     /**
      * 日期路径格式
      */
     private final String datePathFormat = "yyyy/MM/dd/";
-
     /**
      * 日期格式
      */
     private final SimpleDateFormat sdf = new SimpleDateFormat(datePathFormat);
+    @Value("${upload.upload-path}/")    // 文件上传路径
+    private String uploadPath;
+    @Value("${upload.image.user}/")     // 用户头像路径
+    private String userPath;
+    @Value("${face-url}")     // 用户头像路径
+    private String faceUrl;
+
     /**
      * 查询一卡通账户列表
      */
@@ -118,7 +123,7 @@ public class PtUserAccountController extends BaseController {
     @SaCheckPermission("payment:ptUserAccount:query")
     @GetMapping("/{userId}")
     public R<PtUserAccountVo> getInfo(@NotNull(message = "主键不能为空")
-                                     @PathVariable Long userId) {
+                                          @PathVariable Long userId) {
         return R.ok(ptUserAccountService.queryById(userId));
     }
 
@@ -152,24 +157,28 @@ public class PtUserAccountController extends BaseController {
     @RepeatSubmit()
     @PostMapping(value = "/photoUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public R<String> photoUpload(@RequestPart("file") MultipartFile file,
-                            @RequestParam("userId") Long userId) throws IOException {
+                                 @RequestParam("userId") Long userId) throws Exception {
         if (!file.isEmpty()) {
             String extension = FileUtils.extName(file.getOriginalFilename());
             if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
                 return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
             }
 
-            //1. 拼接文件完整路径
-            String path = uploadPath + userPath;
-            String newFileName = userId + "." + extension;
-            //2. 保存照片
-            FileUtils.upload(file, path, newFileName);
-
-            //4.保存图片路径到数据库
-//            String photoUrl = userPath + newFileName;
-            ptUserAccountService.updateByBo(PtUserAccountBo.builder().userId(userId).photo(newFileName).build());
-            remoteConsumeService.createFeatureDataOne(userId, newFileName);
-            return R.ok("上传图片成功");
+            // 2. 图片人脸识别验证
+            String imgData = FileUtils.toBase64(file);
+            String locationFlag = defaultConfig.getLocationFlag();
+            R<Void> result;
+            // 如果是本地部署
+            if (ObjectUtil.equals(locationFlag, DefaultConstants.LOCAL_FLAG)) {
+                result = userFaceBusiness.extractUserFaces(userId, imgData);
+            } else {
+                result = userFaceBusiness.extractUserFacesForCloud(userId, imgData);
+            }
+            if (R.isError(result)) {
+                return R.fail("图片上传异常", result.getMsg());
+            } else {
+                return R.ok("上传图片成功");
+            }
         }
         return R.fail("上传图片异常,请联系管理员");
     }
@@ -180,7 +189,7 @@ public class PtUserAccountController extends BaseController {
     @PostMapping(value = "/photoBatchUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public R<String> photoBatchUpload(@RequestPart("file") MultipartFile[] files) throws IOException {
         int okNum = ptUserAccountService.photoBatchUpload(files, uploadPath, userPath);
-        return R.ok("上传图片成功数量: "+ okNum);
+        return R.ok("上传图片成功数量: " + okNum);
     }
 
     /**
@@ -250,86 +259,90 @@ public class PtUserAccountController extends BaseController {
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
     @PutMapping("/changeFreezeStatus")
     public R<Void> changeFreezeStatus(@RequestBody PtUserAccountBo account) {
-//        userService.checkUserAllowed(user.getUserId());
-//        userService.checkUserDataScope(user.getUserId());
+        //        userService.checkUserAllowed(user.getUserId());
+        //        userService.checkUserDataScope(user.getUserId());
         return toAjax(ptUserAccountService.updateFreezeStatus(account.getUserId(), account.getFreezeStatus()));
     }
+
     /**
      * 批量冻结结/解冻账户
      *
      * @param userIds 账户ID串
-     *        freezeStatus 冻结状态
+     *                freezeStatus 冻结状态
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
-    @SyncDataToLocal(eventType = ptUserAccount_FREEZE_EDIT,sender = USER_ACCOUNT_SENDER)
+    @SyncDataToLocal(eventType = ptUserAccount_FREEZE_EDIT, sender = USER_ACCOUNT_SENDER)
     @PutMapping("/{freezeStatus}/{userIds}")
-    public R<Void> batchChangeFreezeStatus(@PathVariable String freezeStatus,@PathVariable Long[] userIds) {
-        return toAjax(ptUserAccountService.updateFreezeStatus(userIds,freezeStatus));
+    public R<Void> batchChangeFreezeStatus(@PathVariable String freezeStatus, @PathVariable Long[] userIds) {
+        return toAjax(ptUserAccountService.updateFreezeStatus(userIds, freezeStatus));
     }
+
     /**
      * 重置有效期
      *
      * @param userIds 账户ID串
-     *        lifespan 有效期
+     *                lifespan 有效期
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
-    @SyncDataToLocal(eventType = ptUserAccount_RESETTIME_EDIT,sender = USER_ACCOUNT_SENDER)
+    @SyncDataToLocal(eventType = ptUserAccount_RESETTIME_EDIT, sender = USER_ACCOUNT_SENDER)
     @PutMapping("/resetLifespan/{lifespan}/{userIds}")
-    public R<Void> batchResetLifespan(@PathVariable String lifespan,@PathVariable Long[] userIds) {
-        return toAjax(ptUserAccountService.resetLifespan(userIds,lifespan));
+    public R<Void> batchResetLifespan(@PathVariable String lifespan, @PathVariable Long[] userIds) {
+        return toAjax(ptUserAccountService.resetLifespan(userIds, lifespan));
     }
+
     /**
      * 重置卡类
      *
      * @param userIds 账户ID串
-     *        cardType 卡片类型
+     *                cardType 卡片类型
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
-    @SyncDataToLocal(eventType = ptUserAccount_RESETTYPE_EDIT,sender = USER_ACCOUNT_SENDER)
+    @SyncDataToLocal(eventType = ptUserAccount_RESETTYPE_EDIT, sender = USER_ACCOUNT_SENDER)
     @PutMapping("/resetCardType/{cardType}/{userIds}")
-    public R<Void> batchResetCardType(@PathVariable String cardType,@PathVariable Long[] userIds) {
-        return toAjax(ptUserAccountService.resetCardType(userIds,cardType));
+    public R<Void> batchResetCardType(@PathVariable String cardType, @PathVariable Long[] userIds) {
+        return toAjax(ptUserAccountService.resetCardType(userIds, cardType));
     }
+
     /**
      * 重置消费密码
      *
      * @param userIds 账户ID串
-     *        consumePwd 消费密码
+     *                consumePwd 消费密码
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
     @PutMapping("/resetConsumePwd/{consumePwd}/{userIds}")
-    public R<Void> batchResetConsumePwd(@PathVariable String consumePwd,@PathVariable Long[] userIds) {
-        return toAjax(ptUserAccountService.resetConsumePwd(userIds,consumePwd));
+    public R<Void> batchResetConsumePwd(@PathVariable String consumePwd, @PathVariable Long[] userIds) {
+        return toAjax(ptUserAccountService.resetConsumePwd(userIds, consumePwd));
     }
+
     /**
      * 开户
      *
      * @param userIds 账户ID串
-     *
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
-    @SyncDataToLocal(eventType = ptUserAccount_OPEN_EDIT,sender = USER_ACCOUNT_SENDER)
+    @SyncDataToLocal(eventType = ptUserAccount_OPEN_EDIT, sender = USER_ACCOUNT_SENDER)
     @PutMapping("/open/{userIds}")
     public R<Void> batchOpenAccount(@PathVariable Long[] userIds) {
         return toAjax(ptUserAccountService.openAccount(userIds));
     }
+
     /**
      * 销户
      *
      * @param userIds 账户ID串
-     *
      */
     @SaCheckPermission("payment:ptUserAccount:edit")
     @Log(title = "支付账户管理", businessType = BusinessType.UPDATE)
-    @SyncDataToLocal(eventType = ptUserAccount_CLOSE_EDIT,sender = USER_ACCOUNT_SENDER)
+    @SyncDataToLocal(eventType = ptUserAccount_CLOSE_EDIT, sender = USER_ACCOUNT_SENDER)
     @PutMapping("/close/{userIds}")
     public R<Void> batchCloseAccount(@PathVariable Long[] userIds) {
-        if(ptUserAccountService.closeAccount(userIds)){
+        if (ptUserAccountService.closeAccount(userIds)) {
             return R.ok();
         } else {
             return R.fail("账户余额不为0,销户失败");

+ 59 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/PtFaceFeature.java

@@ -0,0 +1,59 @@
+package org.dromara.backstage.payment.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * name: PtFaceFeature
+ * package: org.dromara.backstage.payment.domain
+ * description: 人脸特征对象实体
+ * date: 2025-02-26 16:01:34 16:01
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_pt_arcFaceFeature")
+public class PtFaceFeature extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 特征Id,主键
+     */
+    @TableId(value = "feature_id")
+    private Long featureId;
+
+    /**
+     * 人员Id
+     */
+    private Long userId;
+
+    /**
+     * 人员照片地址
+     */
+    private String photoUrl;
+
+    /**
+     * 人脸特征数据
+     */
+    private String featureData;
+
+    /**
+     * 人脸序列号
+     */
+    private Integer faceSn;
+    /**
+     * 使用的人脸算法
+     */
+    private String faceAlgorithm;
+}
+

+ 60 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/bo/PtFaceFeatureBo.java

@@ -0,0 +1,60 @@
+package org.dromara.backstage.payment.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.backstage.payment.domain.PtFaceFeature;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+/**
+ * name: PtArcFaceFeatureBo
+ * package: org.dromara.server.consume.domain.bo
+ * description: 人脸特征业务对象
+ * date: 2025-02-26 16:03:22 16:03
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = PtFaceFeature.class, reverseConvertGenerate = false)
+public class PtFaceFeatureBo extends BaseEntity {
+
+    /**
+     * 特征Id,主键
+     */
+    @NotNull(message = "特征Id,主键不能为空", groups = {EditGroup.class})
+    private Long featureId;
+
+    /**
+     * 人员Id
+     */
+    @NotNull(message = "人员Id不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Long userId;
+
+    /**
+     * 人员照片地址
+     */
+    @NotBlank(message = "人员照片地址不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String photoUrl;
+
+    /**
+     * 人脸特征数据
+     */
+    @NotBlank(message = "人脸特征数据不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String featureData;
+    /**
+     * 人脸序列号
+     */
+    private Integer faceSn;
+    /**
+     * 使用的人脸算法
+     */
+    private String faceAlgorithm;
+
+}

+ 84 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/domain/vo/PtFaceFeatureVo.java

@@ -0,0 +1,84 @@
+package org.dromara.backstage.payment.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.backstage.payment.domain.PtFaceFeature;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * name: PtArcFaceFeatureVo
+ * package: org.dromara.server.consume.domain.vo
+ * description: 人脸特征视图对象
+ * date: 2025-02-26 16:04:53 16:04
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = PtFaceFeature.class)
+public class PtFaceFeatureVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 特征Id,主键
+     */
+    @ExcelProperty(value = "特征Id,主键")
+    private Long featureId;
+
+    /**
+     * 人员Id
+     */
+    @ExcelProperty(value = "人员Id")
+    private Long userId;
+
+    /**
+     * 人员照片地址
+     */
+    @ExcelProperty(value = "人员照片地址")
+    private String photoUrl;
+
+    /**
+     * 人脸特征数据
+     */
+    @ExcelProperty(value = "人脸特征数据")
+    private String featureData;
+
+    /**
+     * 用户流水号
+     */
+    private String userNo;
+
+    /**
+     * 用户学工号
+     */
+    private String userNumb;
+
+    /**
+     * 用户姓名
+     */
+    private String realName;
+
+    /**
+     * 人脸序列号
+     */
+    private Integer faceSn;
+
+    /**
+     * 删除标志
+     */
+    private String delFlag;
+
+    /**
+     * 使用的人脸算法
+     */
+    private String faceAlgorithm;
+
+}

+ 36 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/mapper/PtFaceFeatureMapper.java

@@ -0,0 +1,36 @@
+package org.dromara.backstage.payment.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import org.dromara.backstage.payment.domain.PtFaceFeature;
+import org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * name: PtArcFaceFeatureMapper
+ * package: org.dromara.server.consume.mapper
+ * description:
+ * date: 2025-02-26 16:08:16 16:08
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+public interface PtFaceFeatureMapper extends BaseMapperPlus<PtFaceFeature, PtFaceFeatureVo> {
+    /**
+     * 获取指定设备的增量人脸特征数据
+     *
+     * @param lastTime 上次获取时间
+     * @return 增量数据
+     */
+    List<PtFaceFeatureVo> getIncrementFeatureDataUser(@Param("lastTime") Date lastTime);
+
+    /**
+     * 获取全量人脸特征数据
+     *
+     * @return 全量数据
+     */
+    List<PtFaceFeatureVo> getAllFeatureDataUser();
+}

+ 72 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/IPtFaceFeatureService.java

@@ -0,0 +1,72 @@
+package org.dromara.backstage.payment.service;
+
+import org.dromara.backstage.payment.domain.bo.PtFaceFeatureBo;
+import org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * name: PtArcFaceFeatureService
+ * package: org.dromara.server.consume.service
+ * description:
+ * date: 2025-02-26 16:10:01 16:10
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+public interface IPtFaceFeatureService {
+
+    /**
+     * 获取上次下载后更新的人脸特征数据
+     *
+     * @param lastTime 上次更新时间
+     * @return 人脸特征数据
+     */
+    List<PtFaceFeatureVo> getIncrFeatureDataUser(Date lastTime);
+
+    /**
+     * 获取全量人脸特征数据
+     *
+     * @return 全量数据
+     */
+    List<PtFaceFeatureVo> getAllFeatureDataUser();
+
+    /**
+     * 获取指定人员的人脸特征数据
+     *
+     * @param userId 人员Id
+     * @return 人脸特征数据
+     */
+    PtFaceFeatureVo getOneFeatureDataUser(Long userId);
+
+    /**
+     * 更新指定用户的人脸特征数据和照片URL。
+     *
+     * @param userId      用户ID
+     * @param featureData 人脸特征数据
+     * @param photoUrl    照片URL
+     * @return 更新操作的结果,成功返回1,失败返回0或负数
+     */
+    Integer updateFeatureDataByUserId(Long userId, String featureData, String photoUrl);
+
+    /**
+     * 插入人脸特征业务对象到数据库。
+     *
+     * @param bo 人脸特征业务对象,包含特征Id、人员Id、人员照片地址和人脸特征数据等信息
+     * @return 插入操作的结果,成功返回1,失败返回0或负数
+     */
+    Integer insertByBo(PtFaceFeatureBo bo);
+
+    /**
+     * 更新指定用户的人脸特征数据和照片URL。
+     *
+     * @param userId        用户ID
+     * @param featureData   人脸特征数据
+     * @param photoUrl      照片URL
+     * @param faceAlgorithm 应用的人脸算法
+     * @return 更新操作的结果,成功返回1,失败返回0或负数
+     */
+    Integer updateFeatureDataByUserId(Long userId, String featureData, String photoUrl, String faceAlgorithm);
+}

+ 116 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/payment/service/impl/PtFaceFeatureServiceImpl.java

@@ -0,0 +1,116 @@
+package org.dromara.backstage.payment.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import org.dromara.backstage.payment.domain.PtFaceFeature;
+import org.dromara.backstage.payment.domain.bo.PtFaceFeatureBo;
+import org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo;
+import org.dromara.backstage.payment.mapper.PtFaceFeatureMapper;
+import org.dromara.backstage.payment.service.IPtFaceFeatureService;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * name: PtArcFaceFeatureServiceImpl
+ * package: org.dromara.server.consume.service.impl
+ * description:
+ * date: 2025-02-26 16:10:47 16:10
+ *
+ * @author luoyibo
+ * @version 0.1
+ * @since JDK 1.8
+ */
+@RequiredArgsConstructor
+@Service
+public class PtFaceFeatureServiceImpl implements IPtFaceFeatureService {
+    private final PtFaceFeatureMapper baseMapper;
+
+    /**
+     * 获取上次下载后更新的人脸特征数据
+     *
+     * @param lastTime 上次更新时间
+     * @return 人脸特征数据
+     */
+    @Override
+    public List<PtFaceFeatureVo> getIncrFeatureDataUser(Date lastTime) {
+        return baseMapper.getIncrementFeatureDataUser(lastTime);
+    }
+
+    /**
+     * 获取全量人脸特征数据
+     *
+     * @return 全量数据
+     */
+    @Override
+    public List<PtFaceFeatureVo> getAllFeatureDataUser() {
+        return baseMapper.getAllFeatureDataUser();
+    }
+
+    /**
+     * 获取指定人员的人脸特征数据
+     *
+     * @param userId 人员Id
+     * @return 人脸特征数据
+     */
+    @Override
+    public PtFaceFeatureVo getOneFeatureDataUser(Long userId) {
+        return baseMapper.selectVoOne(new LambdaQueryWrapper<PtFaceFeature>()
+                                          .eq(PtFaceFeature::getUserId, userId));
+    }
+
+    /**
+     * 更新指定用户的人脸特征数据和照片URL。
+     *
+     * @param userId      用户ID
+     * @param featureData 人脸特征数据
+     * @param photoUrl    照片URL
+     * @return 更新操作的结果,成功返回1,失败返回0或负数
+     */
+    @Override
+    public Integer updateFeatureDataByUserId(Long userId, String featureData, String photoUrl) {
+        LambdaUpdateWrapper<PtFaceFeature> luw = new LambdaUpdateWrapper<PtFaceFeature>()
+                                                     .set(PtFaceFeature::getFeatureData, featureData)
+                                                     .set(PtFaceFeature::getPhotoUrl, photoUrl)
+                                                     .eq(PtFaceFeature::getUserId, userId);
+        return baseMapper.update(null, luw);
+    }
+
+    /**
+     * 插入人脸特征业务对象到数据库。
+     *
+     * @param bo 人脸特征业务对象,包含特征Id、人员Id、人员照片地址和人脸特征数据等信息
+     * @return 插入操作的结果,成功返回1,失败返回0或负数
+     */
+    @Override
+    public Integer insertByBo(PtFaceFeatureBo bo) {
+        PtFaceFeature entity = MapstructUtils.convert(bo, PtFaceFeature.class);
+        int iCount = baseMapper.insert(entity);
+        if (iCount > 0) {
+            bo.setFeatureId(entity != null ? entity.getFeatureId() : null);
+        }
+        return iCount;
+    }
+
+    /**
+     * 更新指定用户的人脸特征数据和照片URL。
+     *
+     * @param userId        用户ID
+     * @param featureData   人脸特征数据
+     * @param photoUrl      照片URL
+     * @param faceAlgorithm 应用的人脸算法
+     * @return 更新操作的结果,成功返回1,失败返回0或负数
+     */
+    @Override
+    public Integer updateFeatureDataByUserId(Long userId, String featureData, String photoUrl, String faceAlgorithm) {
+        LambdaUpdateWrapper<PtFaceFeature> luw = new LambdaUpdateWrapper<PtFaceFeature>()
+                                                     .set(PtFaceFeature::getFeatureData, featureData)
+                                                     .set(PtFaceFeature::getPhotoUrl, photoUrl)
+                                                     .set(PtFaceFeature::getFaceAlgorithm, faceAlgorithm)
+                                                     .eq(PtFaceFeature::getUserId, userId);
+        return baseMapper.update(null, luw);
+    }
+}

+ 1 - 8
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/contorller/WxController.java

@@ -1,10 +1,6 @@
 package org.dromara.backstage.wx.contorller;
 
 import cn.hutool.core.codec.Base64;
-import cn.hutool.core.util.URLUtil;
-import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
-import cn.hutool.http.HttpUtil;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -12,17 +8,14 @@ import org.dromara.backstage.business.payments.ThirdPayBusiness;
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.wx.domain.vo.WxCreditAccountVo;
 import org.dromara.backstage.wx.service.IWxService;
-import org.dromara.common.core.api.ResponseResult;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.web.core.BaseController;
-import org.dromara.system.api.model.LoginUser;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -79,7 +72,7 @@ public class WxController extends BaseController {
      * 人脸照片采集
      */
     @PostMapping("/uploadUserPhoto")
-    public R<String> uploadUserPhoto(@RequestParam("imgData") String imgData) {
+    public R<String> uploadUserPhoto(@RequestParam("imgData") String imgData) throws Exception {
         Long userId = LoginHelper.getUserId();
         return wxService.uploadUserPhoto(userId, imgData);
     }

+ 0 - 23
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/FaceEngineService.java

@@ -1,23 +0,0 @@
-package org.dromara.backstage.wx.service;
-
-import com.arcsoft.face.FaceInfo;
-import com.arcsoft.face.toolkit.ImageInfo;
-
-import java.util.List;
-
-
-/**
- * @author flysheep
- * @date 2020/7/27 0027
- * @time 15:37
- */
-//@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" }, matchIfMissing = false)
-public interface FaceEngineService {
-
-	// 检测脸部信息
-	List<FaceInfo> detectFaces(ImageInfo imageInfo);
-
-	// 生成特征码
-	String createFeatureData(ImageInfo imageInfo);
-
-}

+ 1 - 3
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/IWxService.java

@@ -6,8 +6,6 @@ import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 
-import java.math.BigDecimal;
-
 /**
  * 微信Service接口
  *
@@ -21,7 +19,7 @@ public interface IWxService {
 
     boolean updateCardStatus(Long userId, String cardStatus);
 
-    R<String> uploadUserPhoto(Long userId, String imgData);
+    R<String> uploadUserPhoto(Long userId, String imgData) throws Exception;
 
     R<String> getIdCode(Long userId);
 }

+ 0 - 134
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/FaceEngineServiceImpl.java

@@ -1,134 +0,0 @@
-package org.dromara.backstage.wx.service.impl;
-
-import cn.hutool.core.codec.Base64;
-import com.arcsoft.face.*;
-import com.arcsoft.face.enums.DetectMode;
-import com.arcsoft.face.enums.DetectOrient;
-import com.arcsoft.face.enums.ExtractType;
-import com.arcsoft.face.toolkit.ImageInfo;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.pool2.impl.GenericObjectPool;
-import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
-import org.dromara.backstage.config.ArcFaceConfig;
-import org.dromara.backstage.config.FaceEngineFactory;
-import org.dromara.backstage.wx.service.FaceEngineService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.PostConstruct;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * @author flysheep
- * @date 2020/7/27 0027
- * @time 15:40
- */
-@Service
-@Slf4j
-@RequiredArgsConstructor
-//@ConditionalOnProperty(value = { "arcConfig.arcface-sdk.enable" }, matchIfMissing = false)
-public class FaceEngineServiceImpl implements FaceEngineService {
-	private final ArcFaceConfig arcFaceConfig;
-
-	public final static Logger logger = LoggerFactory.getLogger(FaceEngineServiceImpl.class);
-
-	// 通用人脸识别引擎池
-	private GenericObjectPool<FaceEngine> faceEngineGeneralPool;
-
-	// 引擎配置
-	@PostConstruct
-	public void init() {
-		GenericObjectPoolConfig detectPoolConfig = new GenericObjectPoolConfig();
-		detectPoolConfig.setMaxIdle(arcFaceConfig.getDetectPooSize());
-		detectPoolConfig.setMaxTotal(arcFaceConfig.getDetectPooSize());
-		detectPoolConfig.setMinIdle(arcFaceConfig.getDetectPooSize());
-		detectPoolConfig.setLifo(false);
-		EngineConfiguration detectCfg = new EngineConfiguration();
-		FunctionConfiguration detectFunctionCfg = new FunctionConfiguration();
-		detectFunctionCfg.setSupportFaceDetect(true);// 开启人脸检测功能
-		detectFunctionCfg.setSupportFaceRecognition(true);// 开启人脸识别功能
-		detectFunctionCfg.setSupportAge(true);// 开启年龄检测功能
-	    detectFunctionCfg.setSupportGender(true);// 开启性别检测功能
-		detectFunctionCfg.setSupportLiveness(true);// 开启活体检测功能
-
-		detectCfg.setFunctionConfiguration(detectFunctionCfg);
-		detectCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);// 图片检测模式,如果是连续帧的视频流图片,那么改成VIDEO模式
-		detectCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);// 人脸旋转角度
-		faceEngineGeneralPool = new GenericObjectPool(new FaceEngineFactory(arcFaceConfig.getSdkLibPath(),
-				arcFaceConfig.getAppId(), arcFaceConfig.getSdkKey(), null, detectCfg,arcFaceConfig.activeFile), detectPoolConfig);// 底层库算法对象池
-
-	}
-
-	// 人脸检测
-	@Override
-	public List<FaceInfo> detectFaces(ImageInfo imageInfo) {
-		// 参数判断
-		if (imageInfo == null)
-			return null;
-		// FaceEngine人脸引擎类
-		FaceEngine faceEngine = null;
-		try {
-			// 这里进行获取
-			faceEngine = faceEngineGeneralPool.borrowObject();
-			if (faceEngine == null) {
-				return null;
-			}
-			List<FaceInfo> faceInfoList = new ArrayList<>();
-			// 我们进行人脸检测
-
-			int errorCode = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(),
-					imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList);
-			if (errorCode == 0) {
-				return faceInfoList;
-			}
-		} catch (Exception e) {
-			logger.error(e.getMessage());
-			logger.error(Arrays.toString(e.getStackTrace()));
-		} finally {
-			if (faceEngine != null) {
-				// 释放引擎对象
-				faceEngineGeneralPool.returnObject(faceEngine);
-			}
-		}
-		return null;
-	}
-
-	// 生成特征码
-	// 这里还需要改,关于图片处理
-	@Override
-	public String createFeatureData(ImageInfo imageInfo) {
-		// 获取脸部算法
-		// FaceEngine人脸引擎类
-		FaceEngine faceEngine = null;
-		try {
-			// 这里进行获取人脸引擎
-			faceEngine = faceEngineGeneralPool.borrowObject();
-			List<FaceInfo> faceInfoList = new ArrayList<FaceInfo>();
-			// 人脸检测返回值
-			int code = faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
-					imageInfo.getImageFormat(), faceInfoList);
-			if (code == 0 && faceInfoList.size() > 0) {
-				FaceFeature faceFeature = new FaceFeature();
-                faceEngine.extractFaceFeature(imageInfo,faceInfoList.get(0), ExtractType.REGISTER,0, faceFeature);
-				String featureData = Base64.encode(faceFeature.getFeatureData());
-				return featureData;
-			}
-		} catch (Exception e) {
-			logger.error(e.getMessage());
-			logger.error(Arrays.toString(e.getStackTrace()));
-		} finally {
-			if (faceEngine != null) {
-				// 释放引擎对象
-				faceEngineGeneralPool.returnObject(faceEngine);
-			}
-		}
-		return null;
-	}
-
-}

+ 94 - 212
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/wx/service/impl/WxServiceImpl.java

@@ -1,60 +1,44 @@
 package org.dromara.backstage.wx.service.impl;
 
-import cn.hutool.core.codec.Base64;
 import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.img.Img;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.hutool.crypto.digest.MD5;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
-import com.arcsoft.face.FaceInfo;
-import com.arcsoft.face.toolkit.ImageInfo;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateFormatUtils;
-import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.business.accouunt.UserFaceBusiness;
 import org.dromara.backstage.cardCenter.domain.PtCard;
 import org.dromara.backstage.cardCenter.mapper.PtCardMapper;
 import org.dromara.backstage.consumption.mapper.XfCreditAccountMapper;
-import org.dromara.backstage.payment.domain.PtUserAccount;
-import org.dromara.backstage.payment.domain.bo.PtUserAccountBo;
 import org.dromara.backstage.payment.domain.vo.PtUserAccountVo;
 import org.dromara.backstage.payment.mapper.PtUserAccountMapper;
 import org.dromara.backstage.wx.domain.vo.WxCreditAccountVo;
-import org.dromara.backstage.wx.service.FaceEngineService;
 import org.dromara.backstage.wx.service.IWxService;
+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.exception.ServiceException;
 import org.dromara.common.core.utils.ByteArrayUtilByYC;
-import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtilsByYC;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
-import org.dromara.consume.api.RemoteConsumeService;
 import org.dromara.system.api.RemoteDictService;
 import org.dromara.system.api.domain.vo.RemoteDictDataVo;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
-import static com.arcsoft.face.toolkit.ImageFactory.getRGBData;
-
 /**
  * 微信Service业务层处理
- *
  */
 @RequiredArgsConstructor
 @Service
@@ -64,15 +48,9 @@ public class WxServiceImpl implements IWxService {
     private final XfCreditAccountMapper creditAccountMapper;
     private final PtCardMapper cardMapper;
     private final RemoteDictService dictService;
-    private final FaceEngineService faceEngineService;
-
-    @DubboReference
-    private final RemoteConsumeService remoteConsumeService;
+    private final DefaultConfig defaultConfig;
+    private final UserFaceBusiness userFaceBusiness;
 
-    @Value("${upload.upload-path}/")    // 文件上传路径
-    private String uploadPath;
-    @Value("${upload.image.user}/")     // 用户头像路径
-    private String userPath;
     @Value("${dzbp.sync-img.url}/")     // 电子班牌照片推送接口
     private String syncImgToDzbpUrl;
     @Value("${yc.sf.customerName}")
@@ -80,20 +58,73 @@ public class WxServiceImpl implements IWxService {
     @Value("${yc.sf.customerNo}")
     private String customerNo;
 
+    /**
+     * 物理卡号转二维码数据
+     *
+     * @param fixId
+     * @return
+     */
+    private static String getCode(Long fixId) {
+        // Long fixId=123456789l;
+
+        String fixStr = Long.toHexString(fixId);
+        if (fixStr.length() < 8)
+            fixStr = StringUtilsByYC.addString(fixStr, "0", 8, "L");
+        byte[] fixBytes = ByteArrayUtilByYC.parseHexStr2Byte(fixStr);
+
+        // 当前时间
+        String time = DateFormatUtils.format(new Date(), "yy-MM-dd-HH-mm-ss");
+        String[] timeStrs = time.split("-");
+
+        byte[] bytes = new byte[15];
+        bytes[0] = 0;
+        ByteArrayUtilByYC.copy(fixBytes, 0, bytes, 1, 4);
+        bytes[5] = Byte.valueOf(timeStrs[5]);
+        bytes[6] = Byte.valueOf(timeStrs[0]);
+        bytes[7] = Byte.valueOf(timeStrs[1]);
+        bytes[8] = Byte.valueOf(timeStrs[2]);
+        bytes[9] = Byte.valueOf(timeStrs[3]);
+        bytes[10] = Byte.valueOf(timeStrs[4]);
+        bytes[11] = 10;
+        bytes[12] = 5;
+        bytes[13] = 100;
+        bytes[14] = 0;
+
+        // 计算crc校验值
+        byte crcByte = 0;
+        for (int i = 0; i < bytes.length; i++) {
+            crcByte = (byte) ((crcByte ^ (bytes[i])) & 0xFF);
+        }
+
+        // 加密后9个字节
+        bytes[5] = (byte) ((bytes[0] ^ bytes[5]) & 0xFF);
+        bytes[6] = (byte) ((bytes[1] ^ bytes[6]) & 0xFF);
+        bytes[7] = (byte) ((bytes[2] ^ bytes[7]) & 0xFF);
+        bytes[8] = (byte) ((bytes[3] ^ bytes[8]) & 0xFF);
+        bytes[9] = (byte) ((bytes[4] ^ bytes[9]) & 0xFF);
+        bytes[10] = (byte) ((bytes[0] ^ bytes[10]) & 0xFF);
+        bytes[11] = (byte) ((bytes[1] ^ bytes[11]) & 0xFF);
+        bytes[12] = (byte) ((bytes[2] ^ bytes[12]) & 0xFF);
+        bytes[13] = (byte) ((bytes[3] ^ bytes[13]) & 0xFF);
+        bytes[14] = crcByte;
+
+        return ByteArrayUtilByYC.parseByte2HexStr(bytes, false);
+    }
+
     @Override
     public PtUserAccountVo getUserInfoByUserId(Long userId) {
         Map<String, String> dictMap = dictService.selectDictDataByType("account_open_status")
-            .stream()
-            .collect(
-                Collectors.toMap(RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel));
+                                          .stream()
+                                          .collect(
+                                              Collectors.toMap(RemoteDictDataVo::getDictValue, RemoteDictDataVo::getDictLabel));
         PtUserAccountVo vo = accountMapper.selectVoById(userId);
         vo.setAccountStatus(dictMap.getOrDefault(vo.getAccountStatus(), ""));
-        //根据userId查询卡片信息
+        // 根据userId查询卡片信息
         PtCard card = cardMapper.selectOne(new LambdaUpdateWrapper<PtCard>()
-            .eq(PtCard::getUserId, userId)
-            .eq(PtCard::getStatus, "1"));
+                                               .eq(PtCard::getUserId, userId)
+                                               .eq(PtCard::getStatus, "1"));
         if (ObjectUtil.isNotEmpty(card)) {
-          vo.setCardNo(card.getCardNo());
+            vo.setCardNo(card.getCardNo());
         }
         vo.setCustomerName(customerName);
         vo.setCustomerNo(customerNo);
@@ -106,62 +137,52 @@ public class WxServiceImpl implements IWxService {
                                                               PageQuery pageQuery) {
 
         return TableDataInfo.build(creditAccountMapper.selectCreditAccountPage(pageQuery.build(), type, userId,
-            startTime,
-            endTime));
+                                                                               startTime,
+                                                                               endTime));
     }
 
     @Override
     public boolean updateCardStatus(Long userId, String cardStatus) {
         int count = cardMapper.update(new LambdaUpdateWrapper<PtCard>()
-            .set(PtCard::getStatus, cardStatus)
-            .set(PtCard::getChangeTime, DateUtil.date())
-            .eq(PtCard::getUserId, userId));
+                                          .set(PtCard::getStatus, cardStatus)
+                                          .set(PtCard::getChangeTime, DateUtil.date())
+                                          .eq(PtCard::getUserId, userId));
         return count > 0;
     }
 
     @Override
-    public R<String> uploadUserPhoto(Long userId, String imgData) {
-        //1.查询用户信息,用于判断身份
-        PtUserAccountVo vo = accountMapper.selectVoById(userId);
-        if (ObjectUtil.isEmpty(vo)) {
-            throw new RuntimeException("用户不存在");
-        }
-        //2.调用虹软,识别人脸是否正确
+    public R<String> uploadUserPhoto(Long userId, String imgData) throws Exception {
         int strIndex = imgData.indexOf(";base64,");
         if (strIndex > 0) {
-            imgData = imgData.substring(strIndex + 8); // 过滤掉data:image/jpg;base64,字符串
+            // 过滤掉data:image/jpg;base64,字符串
+            imgData = imgData.substring(strIndex + 8);
         }
-
-        byte[] imageBytes = Base64.decode(imgData);
-        ImageInfo imageInfo = getRGBData(imageBytes);
-        List<FaceInfo> faceInfosList = faceEngineService.detectFaces(imageInfo);
-        if (faceInfosList == null || faceInfosList.size() == 0) {
-            return R.fail("人脸识别不成功!");
+        String locationFlag = defaultConfig.getLocationFlag();
+        R<Void> result;
+        // 如果是本地部署
+        if (ObjectUtil.equals(locationFlag, DefaultConstants.LOCAL_FLAG)) {
+            result = userFaceBusiness.extractUserFaces(userId, imgData);
+        } else {
+            result = userFaceBusiness.extractUserFacesForCloud(userId, imgData);
         }
-
-        try {
-            //3.图片压缩处理
-            imageBytes = imgCompression(imageBytes);
-
-            //4.更新账户表人脸照片地址,上传照片到服务器
-            uploadUserPhoto(userId, imageBytes);
-
-            //5.如果用户是学员身份,则将照片同步给电子班牌
-            if("2".equals(vo.getCategory())){
+        if (R.isError(result)) {
+            return R.fail("图片上传异常", result.getMsg());
+        } else {
+            PtUserAccountVo vo = accountMapper.selectVoById(userId);
+            if ("2".equals(vo.getCategory())) {
                 syncImgToDZBP(vo, imgData);
             }
-        }catch (ServiceException e){
-            return R.fail(e.getMessage());
+            return R.ok("上传图片成功");
         }
-       return R.ok("上传图片成功");
     }
 
     /**
      * 同步照片到电子班牌
+     *
      * @param vo
      * @param imgData
      */
-    private void syncImgToDZBP(PtUserAccountVo vo, String imgData){
+    private void syncImgToDZBP(PtUserAccountVo vo, String imgData) {
         String timestamp = Long.toString(System.currentTimeMillis());
         TreeMap<String, Object> params = new TreeMap<String, Object>();
         params.put("pid", "hnswdx");
@@ -193,158 +214,19 @@ public class WxServiceImpl implements IWxService {
         face.put("data", array);
 
         String res = HttpRequest.post(syncImgToDzbpUrl + "?" + s1.toString())
-            .body(face.toString())
-            .execute().body();
+                         .body(face.toString())
+                         .execute().body();
         log.info("同步照片到电子班牌返回结果:{}", res);
     }
-    private void uploadUserPhoto(Long userId, byte[] imageBytes) {
-        //1.上传照片到指定目录
-        ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes);
-        byte[] bytes = new byte[1024];
-        int index;
-
-        String localFileName = uploadPath + userPath + userId + ".jpg";
-        FileOutputStream downloadFile = null;
-        try {
-            downloadFile = new FileOutputStream(localFileName);
-            while ((index = bis.read(bytes)) != -1) {
-                downloadFile.write(bytes, 0, index);
-                downloadFile.flush();
-            }
-        } catch (IOException e) {
-            log.error("图片处理失败,请稍后重试!", e);
-            throw new ServiceException("图片处理失败,请稍后重试!");
-        } finally {
-            try {
-                bis.close();
-            } catch (IOException e) {
-                log.error("inputStream关闭失败!", e);
-            }
-            if (downloadFile != null) {
-                try {
-                    downloadFile.close();
-                } catch (IOException e) {
-                    log.error("downloadFile关闭失败!", e);
-                }
-            }
-        }
-
-        //2.保存图片路径到数据库
-        String photoUrl = userId + ".jpg";
-        PtUserAccountBo bo = PtUserAccountBo.builder().userId(userId).photo(photoUrl).build();
-        accountMapper.updateById(MapstructUtils.convert(bo, PtUserAccount.class));
-        // 生成人脸特征库
-        remoteConsumeService.createFeatureDataOne(userId, photoUrl);
-    }
-    /**
-     * 图片压缩
-     * @param imageBytes
-     * @return
-     */
-    private static byte[] imgCompression(byte[] imageBytes) {
-        // 小于1M就不进行压缩里,浪费执行时间
-        float quality = 0f;
-        if (imageBytes.length > 1024 * 1024 * 10) { // 大于10M
-            quality = 0.1f;
-        } else if (imageBytes.length > 1024 * 1024 * 5) { // 大于5M
-            quality = 0.2f;
-        } else if (imageBytes.length > 1024 * 1024 * 1) {// 大于1M
-            quality = 0.5f;
-        }
-
-        if (quality != 0) {
-            ByteArrayInputStream bis = null;
-            ByteArrayOutputStream bos = null;
-            try {
-                bis = new ByteArrayInputStream(imageBytes);
-                bos = new ByteArrayOutputStream();
-                Img.from(bis).setQuality(quality).write(bos);
-                imageBytes =  bos.toByteArray();
-            } catch (Exception e) {
-                throw new ServiceException("图片处理失败,请稍后重试!");
-            } finally {
-                if (bis != null) {
-                    try {
-                        bis.close();
-                    } catch (IOException e) {
-                        // TODO Auto-generated catch block
-                        e.printStackTrace();
-                    }
-                }
-                if (bos != null) {
-                    try {
-                        bos.close();
-                    } catch (IOException e) {
-                        // TODO Auto-generated catch block
-                        e.printStackTrace();
-                    }
-                }
-            }
-        }
-        return imageBytes;
-    }
 
     @Override
     public R<String> getIdCode(Long userId) {
-        //根据userId 查询卡片信息
+        // 根据userId 查询卡片信息
         PtCard card = cardMapper.selectOne(new LambdaQueryWrapper<PtCard>().eq(PtCard::getUserId, userId));
-        if(ObjectUtil.isEmpty(card) || ObjectUtil.isEmpty(card.getFactoryId())){
+        if (ObjectUtil.isEmpty(card) || ObjectUtil.isEmpty(card.getFactoryId())) {
             return R.fail("该用户未领取卡片");
-        }else {
-            return  R.ok("操作成功",getCode(card.getFactoryId()));
+        } else {
+            return R.ok("操作成功", getCode(card.getFactoryId()));
         }
     }
-
-    /**
-     *物理卡号转二维码数据
-     * @param fixId
-     * @return
-     */
-    private  static String getCode(Long fixId)
-    {
-        //Long fixId=123456789l;
-
-        String fixStr=Long.toHexString(fixId);
-        if(fixStr.length()<8)
-            fixStr= StringUtilsByYC.addString(fixStr, "0",8, "L");
-        byte[] fixBytes= ByteArrayUtilByYC.parseHexStr2Byte(fixStr);
-
-        //当前时间
-        String time= DateFormatUtils.format(new Date(), "yy-MM-dd-HH-mm-ss");
-        String[] timeStrs=time.split("-");
-
-        byte[] bytes=new byte[15];
-        bytes[0]=0;
-        ByteArrayUtilByYC.copy(fixBytes , 0, bytes, 1, 4);
-        bytes[5]=Byte.valueOf(timeStrs[5]);
-        bytes[6]=Byte.valueOf(timeStrs[0]);
-        bytes[7]=Byte.valueOf(timeStrs[1]);
-        bytes[8]=Byte.valueOf(timeStrs[2]);
-        bytes[9]=Byte.valueOf(timeStrs[3]);
-        bytes[10]=Byte.valueOf(timeStrs[4]);
-        bytes[11]=10;
-        bytes[12]=5;
-        bytes[13]=100;
-        bytes[14]=0;
-
-        //计算crc校验值
-        byte crcByte=0;
-        for(int i=0;i<bytes.length;i++) {
-            crcByte=(byte) ((crcByte^(bytes[i]))&0xFF);
-        }
-
-        //加密后9个字节
-        bytes[5]=(byte) ((bytes[0]^bytes[5])&0xFF);
-        bytes[6]=(byte) ((bytes[1]^bytes[6])&0xFF);
-        bytes[7]=(byte) ((bytes[2]^bytes[7])&0xFF);
-        bytes[8]=(byte) ((bytes[3]^bytes[8])&0xFF);
-        bytes[9]=(byte) ((bytes[4]^bytes[9])&0xFF);
-        bytes[10]=(byte) ((bytes[0]^bytes[10])&0xFF);
-        bytes[11]=(byte) ((bytes[1]^bytes[11])&0xFF);
-        bytes[12]=(byte) ((bytes[2]^bytes[12])&0xFF);
-        bytes[13]=(byte) ((bytes[3]^bytes[13])&0xFF);
-        bytes[14]=crcByte;
-
-        return ByteArrayUtilByYC.parseByte2HexStr(bytes, false);
-    }
 }

+ 57 - 0
ruoyi-modules/ruoyi-backstage/src/main/resources/mapper/payment/PtFaceFeatureMapper.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.backstage.payment.mapper.PtFaceFeatureMapper">
+
+    <resultMap type="org.dromara.backstage.payment.domain.PtFaceFeature" id="PtFaceFeatureResult">
+        <result property="featureId" column="feature_id"/>
+        <result property="tenantId" column="tenant_id"/>
+        <result property="userId" column="user_id"/>
+        <result property="photoUrl" column="photo_url"/>
+        <result property="featureData" column="feature_data"/>
+        <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"/>
+        <result property="faceSn" column="face_sn"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="faceAlgorithm" column="face_algorithm"/>
+    </resultMap>
+
+    <select id="getIncrementFeatureDataUser" resultType="org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo">
+        SELECT tpua.user_id,
+               tpua.user_no,
+               tpua.user_numb,
+               tpua.real_name,
+               tpaff.photo_url,
+               tpaff.feature_data,
+               tpaff.feature_id,
+               tpaff.face_sn,
+               tpaff.del_flag,
+               tpaff.face_algorithm
+        FROM t_pt_arcFaceFeature tpaff
+                 INNER JOIN t_pt_userAccount tpua ON tpua.user_id = tpaff.user_id
+        WHERE tpaff.update_time > #{lastTime}
+          AND tpua.del_flag = '0'
+        ORDER BY tpua.user_no
+    </select>
+
+    <select id="getAllFeatureDataUser" resultType="org.dromara.backstage.payment.domain.vo.PtFaceFeatureVo">
+        SELECT tpua.user_id,
+               tpua.user_no,
+               tpua.user_numb,
+               tpua.real_name,
+               tpaff.photo_url,
+               tpaff.feature_data,
+               tpaff.feature_id,
+               tpaff.face_sn,
+               tpaff.del_flag,
+               tpaff.face_algorithm
+        FROM t_pt_arcFaceFeature tpaff
+                 INNER JOIN t_pt_userAccount tpua ON tpua.user_id = tpaff.user_id
+        WHERE tpua.del_flag = '0'
+        ORDER BY tpua.user_no
+    </select>
+</mapper>