Răsfoiți Sursa

增加人脸比对接口

wujiefeng 1 an în urmă
părinte
comite
09e88f420c
18 a modificat fișierele cu 773 adăugiri și 49 ștergeri
  1. 6 1
      centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/content/ResultState.java
  2. 5 5
      centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/model/UserFaceUploadModel.java
  3. 25 0
      centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/model/UserNFCCardUpdateModel.java
  4. 2 1
      centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/service/UserFaceService.java
  5. 33 0
      centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/service/UserNFCCardService.java
  6. 11 0
      centers/AuthCenter/AuthServer/pom.xml
  7. 43 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/conf/RedisearchConf.java
  8. 19 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/controller/manager/UserNFCCardController.java
  9. 24 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/controller/user/UserController.java
  10. 12 1
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/dao/UserDao.java
  11. 17 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/dao/UserFaceDao.java
  12. 31 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/domain/UserFace.java
  13. 23 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/model/RedisearchResultModel.java
  14. 35 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/model/ResultsModel.java
  15. 315 41
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/service/local/UserFaceServiceImpl.java
  16. 82 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/service/local/UserNFCCardServiceImpl.java
  17. 81 0
      centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/util/FaceUtil.java
  18. 9 0
      centers/AuthCenter/AuthServer/src/main/resources/application-dev.yml

+ 6 - 1
centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/content/ResultState.java

@@ -46,7 +46,12 @@ public enum ResultState {
 
     TokenLoginParmError("令牌登陆的参数类型错误 "),
 
-    FaceNotExists("人脸图片不存在")
+    FaceNotExists("人脸图片不存在"),
+    FaceNoMatches("无匹配的人脸"),
+
+    NFCCardNotExists("NFC卡号不存在"),
+    NFCCardExists("NFC卡号已存在"),
+    NFCCardNotBind("NFC卡号未绑定"),
     ;
 
     @Getter

+ 5 - 5
centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/model/UserFaceUploadModel.java

@@ -3,6 +3,7 @@ package com.github.microservice.auth.client.model;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotNull;
 import lombok.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
@@ -19,12 +20,11 @@ public class UserFaceUploadModel {
     private String userId;
 
     @Schema(description = "人脸图片")
-    @NotNull(message = "userFace不能为null")
-    private String userFace;
+    private MultipartFile file;
 
-    @Schema(description = "对比人脸照片")
-    @NotNull(message = "compareFace不能为null")
-    private String compareFace;
+//    @Schema(description = "对比人脸照片")
+//    @NotNull(message = "compareFace不能为null")
+//    private String compareFace;
 
     @Schema(description = "更新的图片文件id")
     private String updateFaceFileId;

+ 25 - 0
centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/model/UserNFCCardUpdateModel.java

@@ -0,0 +1,25 @@
+package com.github.microservice.auth.client.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+
+@Data
+@ToString(callSuper = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode
+@Schema(name = "userNFCCardUpdateModel", description = "NFC卡号更新模型")
+public class UserNFCCardUpdateModel {
+
+    @Schema(description = "用户id")
+    @NotNull(message = "userId不能为null")
+    private String userId;
+
+    @Schema(description = "NFC卡号")
+    @NotNull(message = "nfcCard不能为null")
+    private String nfcCard;
+
+    @Schema(description = "需要修改的NFC卡号,新增时不传值")
+    private String updateCard;
+}

+ 2 - 1
centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/service/UserFaceService.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
@@ -31,5 +32,5 @@ public interface UserFaceService {
 
     @Operation(summary = "比对人脸图片", description = "比对人脸图片")
     @RequestMapping(value = "matches", method = RequestMethod.POST)
-    ResultContent<String> matches(@RequestParam("userFace") String userFace);
+    ResultContent<Object> matches(@RequestParam("file") MultipartFile file);
 }

+ 33 - 0
centers/AuthCenter/AuthClient/src/main/java/com/github/microservice/auth/client/service/UserNFCCardService.java

@@ -0,0 +1,33 @@
+package com.github.microservice.auth.client.service;
+
+import com.github.microservice.auth.client.content.ResultContent;
+import com.github.microservice.auth.client.model.UserNFCCardUpdateModel;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = "authserver/manager/userNFCCard")
+public interface UserNFCCardService {
+
+    @Operation(summary = "更新绑定NFC卡号", description = "更新绑定NFC卡号")
+    @RequestMapping(value = "update", method = RequestMethod.POST)
+    ResultContent update(@RequestBody UserNFCCardUpdateModel userNFCCardUpdateModel);
+
+    @Operation(summary = "删除用户NFC卡号", description = "删除用户NFC卡号")
+    @RequestMapping(value = "delete", method = RequestMethod.POST)
+    ResultContent delete(@RequestParam("userId") String userId, @RequestParam("nfcCard") String nfcCard);
+
+    @Operation(summary = "获取用户NFC卡号", description = "获取用户NFC卡号")
+    @RequestMapping(value = "get", method = RequestMethod.POST)
+    ResultContent<List<String>> get(@RequestParam("userId") String userId);
+
+    @Operation(summary = "通过卡号查询用户", description = "通过卡号查询用户")
+    @RequestMapping(value = "matches", method = RequestMethod.POST)
+    ResultContent<String> matches(@RequestParam("nfcCard") String nfcCard);
+}

+ 11 - 0
centers/AuthCenter/AuthServer/pom.xml

@@ -130,6 +130,17 @@
             <scope>runtime</scope>
         </dependency>
 
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>5.2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.redislabs</groupId>
+            <artifactId>jredisearch</artifactId>
+            <version>2.2.0</version>
+        </dependency>
 
     </dependencies>
 

+ 43 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/conf/RedisearchConf.java

@@ -0,0 +1,43 @@
+package com.github.microservice.auth.server.core.conf;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Component
+@ConfigurationProperties(prefix = "redisearch")
+public class RedisearchConf {
+
+    /**/
+    private String host;
+
+    private int port;
+
+    private String username;
+
+    private String password;
+
+
+    /*******************索引参数***********************/
+
+    /*索引名*/
+    private String indexName;
+
+    /*索引字段*/
+    private String fieldName;
+
+    /*索引key前缀*/
+    private String prefix;
+
+    /*向量维度*/
+    private String dim;
+
+    /*相似度*/
+    private String score;
+}

+ 19 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/controller/manager/UserNFCCardController.java

@@ -0,0 +1,19 @@
+package com.github.microservice.auth.server.core.controller.manager;
+
+import com.github.microservice.auth.client.service.UserFaceService;
+import com.github.microservice.auth.client.service.UserNFCCardService;
+import com.github.microservice.core.delegate.DelegateMapping;
+import lombok.experimental.Delegate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("manager/userNFCCard")
+@DelegateMapping(types = UserNFCCardService.class)
+public class UserNFCCardController implements UserNFCCardService {
+
+    @Autowired
+    @Delegate
+    private UserNFCCardService userNFCCardService;
+}

+ 24 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/controller/user/UserController.java

@@ -1,13 +1,18 @@
 package com.github.microservice.auth.server.core.controller.user;
 
+import com.github.microservice.auth.client.content.ResultContent;
+import com.github.microservice.auth.client.model.UserFaceUploadModel;
 import com.github.microservice.auth.client.service.UserService;
+import com.github.microservice.auth.server.core.service.local.UserFaceServiceImpl;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import jakarta.validation.constraints.NotNull;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 
 @RestController
 @RequestMapping("user")
@@ -16,11 +21,30 @@ public class UserController {
     @Autowired
     private UserService userService;
 
+    @Autowired
+    private UserFaceServiceImpl userFaceService;
+
     @Operation(summary = "刷新令牌", description = "刷新用户访问令牌")
     @RequestMapping(value = "refreshToken", method = {RequestMethod.GET, RequestMethod.POST})
     public Object refreshToken(@Parameter(name = "refreshToken", description = "刷新令牌", example = "ShkUxSATO2SNrQ7CLixCHTwA354") @RequestParam("refreshToken") String refreshToken) {
         return this.userService.refreshToken(refreshToken);
     }
 
+    @Operation(summary = "上传人脸", description = "上传人脸")
+    @RequestMapping(value = "uploadFace", method = {RequestMethod.POST})
+    public ResultContent<String> upload(UserFaceUploadModel userFaceUploadModel){
+        return userFaceService.upload(userFaceUploadModel);
+    }
+
+    @Operation(summary = "比对人脸", description = "比对人脸")
+    @RequestMapping(value = "matchesFace", method = {RequestMethod.POST})
+    public ResultContent<Object> matchesFace(@RequestParam("file") MultipartFile file){
+        return userFaceService.matches(file);
+    }
 
+    @Operation(summary = "删除人脸", description = "删除人脸")
+    @RequestMapping(value = "deleteFace", method = {RequestMethod.GET})
+    public ResultContent deleteFace(@RequestParam("userId") String userId, @RequestParam("faceFileId") String faceFileId){
+        return userFaceService.delete(userId, faceFileId);
+    }
 }

+ 12 - 1
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/dao/UserDao.java

@@ -4,6 +4,8 @@ import com.github.microservice.auth.server.core.dao.extend.UserDaoExtend;
 import com.github.microservice.auth.server.core.domain.User;
 import com.github.microservice.components.data.mongo.mongo.dao.MongoDao;
 
+import java.util.List;
+
 public interface UserDao extends MongoDao<User>, UserDaoExtend {
 
     /**
@@ -66,6 +68,15 @@ public interface UserDao extends MongoDao<User>, UserDaoExtend {
      */
     User findTop1ById(String id);
 
+    /**
+     * nfc卡号是否存在
+     */
+    boolean existsByNfcCardContains(String nfcCard);
 
-
+    /**
+     * 通过卡号查询
+     * @param nfcCard
+     * @return
+     */
+    User findTopByNfcCardContains(String nfcCard);
 }

+ 17 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/dao/UserFaceDao.java

@@ -0,0 +1,17 @@
+package com.github.microservice.auth.server.core.dao;
+
+import com.github.microservice.auth.server.core.domain.UserFace;
+import com.github.microservice.components.data.mongo.mongo.dao.MongoDao;
+
+import java.util.List;
+
+public interface UserFaceDao extends MongoDao<UserFace> {
+
+    boolean existsByUserIdAndFaceFSId(String userId, String facesId);
+
+    UserFace findTopByUserIdAndFaceFSId(String userId, String facesId);
+
+    List<UserFace> findByUserId(String userId);
+
+    UserFace findTopByFaceFSId(String facesId);
+}

+ 31 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/domain/UserFace.java

@@ -0,0 +1,31 @@
+package com.github.microservice.auth.server.core.domain;
+
+import com.github.microservice.components.data.mongo.mongo.domain.SuperEntity;
+import lombok.*;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@Document
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserFace extends SuperEntity {
+
+    //用户id
+    @Indexed
+    private String userId;
+
+    //人脸图片文件id
+    @Indexed
+    private String faceFSId;
+
+    //人脸向量
+    private List<Float> vector;
+
+    private String faceDataKey;
+
+}

+ 23 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/model/RedisearchResultModel.java

@@ -0,0 +1,23 @@
+package com.github.microservice.auth.server.core.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+/**
+ * redisearch向量查询结果,只适用于返回一个结果的时候
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class RedisearchResultModel {
+
+    private Long count;
+
+    private String dataKey;
+
+    private String scoreKey;
+
+    private String scoreValue;
+}

+ 35 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/model/ResultsModel.java

@@ -0,0 +1,35 @@
+package com.github.microservice.auth.server.core.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ResultsModel {
+    private List<Result> results;
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Result {
+        private List<Float> embedding;
+        private double faceConfidence;
+        private FacialArea facialArea;
+
+        @Data
+        @AllArgsConstructor
+        @NoArgsConstructor
+        public static class FacialArea {
+            private int h;
+            private int w;
+            private int x;
+            private int y;
+            private List<Integer> leftEye;
+            private List<Integer> rightEye;
+        }
+    }
+}

+ 315 - 41
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/service/local/UserFaceServiceImpl.java

@@ -2,11 +2,18 @@ package com.github.microservice.auth.server.core.service.local;
 
 import com.github.microservice.auth.client.content.ResultContent;
 import com.github.microservice.auth.client.content.ResultState;
+import com.github.microservice.auth.server.core.conf.RedisearchConf;
+import com.github.microservice.auth.server.core.model.RedisearchResultModel;
 import com.github.microservice.auth.client.model.UserFaceQueryModel;
 import com.github.microservice.auth.client.model.UserFaceUploadModel;
 import com.github.microservice.auth.client.service.UserFaceService;
 import com.github.microservice.auth.server.core.dao.UserDao;
+import com.github.microservice.auth.server.core.dao.UserFaceDao;
 import com.github.microservice.auth.server.core.domain.User;
+import com.github.microservice.auth.server.core.domain.UserFace;
+import com.github.microservice.auth.server.core.model.ResultsModel;
+import com.github.microservice.auth.server.core.util.FaceUtil;
+import com.github.microservice.core.util.JsonUtil;
 import com.mongodb.client.gridfs.GridFSFindIterable;
 import com.mongodb.client.gridfs.model.GridFSFile;
 import jakarta.validation.constraints.NotNull;
@@ -18,11 +25,12 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Query;
-import org.springframework.data.mongodb.gridfs.GridFsResource;
 import org.springframework.data.mongodb.gridfs.GridFsTemplate;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+import redis.clients.jedis.JedisPooled;
 
 import javax.imageio.ImageIO;
 import java.awt.*;
@@ -31,6 +39,9 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.math.BigDecimal;
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
 import java.util.*;
 import java.util.List;
 
@@ -44,6 +55,9 @@ public class UserFaceServiceImpl implements UserFaceService {
     @Autowired
     GridFsTemplate gridFsTemplate;
 
+    @Autowired
+    UserFaceDao userFaceDao;
+
     @Autowired
     RestTemplate restTemplate;
 
@@ -55,6 +69,8 @@ public class UserFaceServiceImpl implements UserFaceService {
     @Autowired
     private PasswordEncoder passwordEncoder;
 
+    @Autowired
+    RedisearchConf redisearchConf;
 
     /**
      * 上传人脸图片
@@ -64,37 +80,146 @@ public class UserFaceServiceImpl implements UserFaceService {
      */
     @SneakyThrows
     public ResultContent<String> upload(UserFaceUploadModel userFaceUploadModel) {
+
+        if (userFaceUploadModel.getFile().isEmpty()) {
+            return ResultContent.build(ResultState.Fail);
+        }
+
         User user = userDao.findTop1ById(userFaceUploadModel.getUserId());
         if (user == null) {
             return ResultContent.build(ResultState.UserNotExists);
         }
-        //TODO 调整图片大小
-        byte[] bytes = imageResizer(userFaceUploadModel.getUserFace());
-        //TODO 上传人脸库,对比成功后入库
-        @Cleanup ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
-        String fileId = gridFsTemplate.store(byteArrayInputStream, UUID.randomUUID().toString(),Map.of("userId", userFaceUploadModel.getUserId())).toHexString();
-
-        List<String> facePic = user.getFacePic();
-        if (facePic == null || facePic.isEmpty()) {
-            facePic = new ArrayList<>();
-        }
-        if (StringUtils.isNotBlank(userFaceUploadModel.getUpdateFaceFileId())){
+        boolean isUpdate = StringUtils.isNotBlank(userFaceUploadModel.getUpdateFaceFileId());
+        if (isUpdate) {
             //替换原人脸图片
-            if (!facePic.contains(userFaceUploadModel.getUpdateFaceFileId())){
+            if (userFaceDao.existsByUserIdAndFaceFSId(userFaceUploadModel.getUserId(), userFaceUploadModel.getUpdateFaceFileId())) {
                 return ResultContent.build(ResultState.FaceNotExists);
             }
-            int index = facePic.indexOf(userFaceUploadModel.getUpdateFaceFileId());
-            facePic.set(index, fileId);
-        }else {
-            facePic.add(fileId);
         }
-        user.setFacePic(facePic);
-        userDao.save(user);
+
+        MultipartFile file = userFaceUploadModel.getFile();
+        InputStream inputStream = file.getInputStream();
+
+
+        //TODO 调整图片大小
+//        byte[] bytes = imageResizer(userFaceUploadModel.getUserFace());
+        //TODO 上传人脸库,获取向量
+        HttpResponse<String> response = FaceUtil.uploadFs("http://127.0.0.1:5001/v2/represent", inputStream, "yunet");
+
+        if (response.statusCode() != 200) {
+            return ResultContent.build(ResultState.Fail, "人脸特征提取失败");
+        }
+
+        final String body = response.body();
+
+        ResultsModel  resultsModel = JsonUtil.toObject(body, ResultsModel.class);
+        System.out.println(resultsModel);
+
+
+
+        System.out.println(body);
+        List<Float> embedding = resultsModel.getResults().get(0).getEmbedding();
+
+
+
+        JedisPooled jedis = new JedisPooled(redisearchConf.getHost(), redisearchConf.getPort(), null, redisearchConf.getPassword());
+
+        // 动态设置变量
+        String indexName = redisearchConf.getIndexName();
+        String prefix = redisearchConf.getPrefix();
+        String fieldName = redisearchConf.getFieldName();
+        String dim = redisearchConf.getDim();
+        initIndex(jedis, indexName, prefix, fieldName, dim);
+
+
+        // 将 List<Float> 转换为 float[]
+        float[] vector = listToFloatArray(embedding);
+        // 归一化向量
+        double norm = calculateNorm(vector);
+        for (int i = 0; i < vector.length; i++) {
+            vector[i] /= norm;
+        }
+
+        // 转换为字节数组 (FLOAT32,每个值占 4 字节)
+        ByteBuffer buffer = ByteBuffer.allocate(vector.length * 4);
+        for (float value : vector) {
+            buffer.putFloat(value);
+        }
+
+        String key = prefix + user.getId() + System.currentTimeMillis();
+
+        long hset = jedis.hset(key.getBytes(), fieldName.getBytes(), buffer.array());
+
+        System.out.println(hset);
+        jedis.close();
+        //图片入库
+        String fileId = gridFsTemplate.store(inputStream, UUID.randomUUID().toString(), Map.of("userId", userFaceUploadModel.getUserId())).toHexString();
+        UserFace userFace = new UserFace();
+        if (isUpdate) {
+            userFace = userFaceDao.findTopByUserIdAndFaceFSId(userFaceUploadModel.getUserId(), userFaceUploadModel.getUpdateFaceFileId());
+        }
+        //向量入库
+        userFace.setUserId(user.getId());
+        userFace.setFaceFSId(fileId);
+        //设置向量
+        userFace.setVector(embedding);
+        userFace.setFaceDataKey(key);
+        userFaceDao.save(userFace);
         return ResultContent.buildContent(fileId);
     }
 
+    private static void initIndex(JedisPooled jedis, String indexName, String prefix, String fieldName, String dim) {
+        try {
+            Map<String, Object> stringObjectMap = jedis.ftInfo(indexName);
+            //TODO 检查参数是否正确
+        }catch (Exception e) {
+            //创建索引
+            // Lua 脚本模板
+            String createIndexScript = "local indexName = KEYS[1] " +
+                    "local prefix = KEYS[2] " +
+                    "local fieldName = KEYS[3] " +
+                    "local dim = ARGV[1] " +
+                    "redis.call('FT.CREATE', indexName, 'ON', 'HASH', 'PREFIX', '1', prefix, " +
+                    "'SCHEMA', fieldName, 'VECTOR', 'FLAT', '6', " +
+                    "'TYPE', 'FLOAT32', 'DIM', dim, 'DISTANCE_METRIC', 'COSINE')";
+
+
+            String a = """
+                    
+                   """;
+
+                // 执行 Lua 脚本并传递变量
+                Object result = jedis.eval(createIndexScript,
+                        // KEYS 参数:索引名、前缀、字段名
+                        List.of(indexName, prefix, fieldName), List.of(dim));
+                System.out.println("Index created successfully: " + result);
+        }
+    }
+
+
+    private static float calculateNorm(float[] vector) {
+        float sum = 0;
+        for (float v : vector) {
+            sum += v * v;  // 对每个分量平方求和
+        }
+        return (float) Math.sqrt(sum);  // 求平方和的平方根
+    }
+
+    public static float[] listToFloatArray(List<Float> list) {
+        // 创建一个与 List 大小相同的 float 数组
+        float[] array = new float[list.size()];
+
+        // 使用普通的 for 循环将 List 中的每个元素放入数组中
+        for (int i = 0; i < list.size(); i++) {
+            array[i] = list.get(i);  // 自动拆箱,将 Float 转换为 float
+        }
+
+        return array;
+    }
+
     /**
      * 删除用户指定人脸图片
+     *
      * @param userId
      * @param faceFileId
      * @return
@@ -105,37 +230,39 @@ public class UserFaceServiceImpl implements UserFaceService {
             return ResultContent.build(ResultState.UserNotExists);
         }
 
-        List<String> facePic = user.getFacePic();
-        if (facePic == null || facePic.isEmpty()) {
+        UserFace userFace = userFaceDao.findTopByUserIdAndFaceFSId(userId, faceFileId);
+        if (userFace == null) {
             return ResultContent.build(ResultState.FaceNotExists);
         }
 
-        if (!facePic.contains(faceFileId)) {
-            return ResultContent.build(ResultState.FaceNotExists);
-        }
+        //删除人脸库的人脸
+        JedisPooled jedis = new JedisPooled(redisearchConf.getHost(), redisearchConf.getPort(), null, redisearchConf.getPassword());
 
-        facePic.remove(faceFileId);
-        user.setFacePic(facePic);
-        userDao.save(user);
+        long del = jedis.del(userFace.getFaceDataKey());
+        if (del == 0) {
+            log.error("人脸库删除数据失败:key:{}", userFace.getFaceDataKey());
+            return ResultContent.build(ResultState.Fail);
+        }
+        userFaceDao.deleteById(userFace.getId());
         return ResultContent.build(ResultState.Success);
     }
 
     /**
      * 获取用户人脸认证图片
+     *
      * @param userId
      * @return
      */
     @SneakyThrows
-    public ResultContent<List<UserFaceQueryModel>> get(String userId){
-        User user = userDao.findTop1ById(userId);
-        if (user == null) {
-            return ResultContent.build(ResultState.UserNotExists);
-        }
+    public ResultContent<List<UserFaceQueryModel>> get(String userId) {
         //初始化返回结果
         List<UserFaceQueryModel> userFaceBase64List = new ArrayList<>();
 
-        List<String> facePic = user.getFacePic();
-        if (facePic != null && !facePic.isEmpty()) {
+        List<UserFace> userFaceList = userFaceDao.findByUserId(userId);
+
+        List<String> facePic = userFaceList.stream().map(UserFace::getFaceFSId).toList();
+
+        if (!facePic.isEmpty()) {
             Query query = new Query(Criteria.where("_id").in(facePic));
             GridFSFindIterable gridFSFiles = gridFsTemplate.find(query);
             for (GridFSFile gridFS : gridFSFiles) {
@@ -150,9 +277,140 @@ public class UserFaceServiceImpl implements UserFaceService {
         return ResultContent.buildContent(userFaceBase64List);
     }
 
-    public ResultContent<String> matches(String userFace){
+    /**
+     * 对比人脸图片
+     *
+     * @param
+     * @return
+     */
+    @SneakyThrows
+    public ResultContent<Object> matches(MultipartFile file) {
+        //TODO 上传人脸服务,获取向量
+        InputStream inputStream = file.getInputStream();
+
+        //TODO 调整图片大小
+//        byte[] bytes = imageResizer(userFaceUploadModel.getUserFace());
+        //TODO 上传人脸库,获取向量
+        HttpResponse<String> response = FaceUtil.uploadFs("http://127.0.0.1:5001/v2/represent", inputStream, "yunet");
+
+        if (response.statusCode() != 200) {
+            return ResultContent.build(ResultState.Fail, "人脸特征提取失败");
+        }
+
+        final String body = response.body();
+
+        ResultsModel  resultsModel = JsonUtil.toObject(body, ResultsModel.class);
+        System.out.println(resultsModel);
+
+
+
+        System.out.println(body);
+        List<Float> embedding = resultsModel.getResults().get(0).getEmbedding();
+        float[] vector = listToFloatArray(embedding);
 
-        return ResultContent.buildContent("userId");
+        float norm = calculateNorm(vector);
+        for (int i = 0; i < vector.length; i++) {
+            vector[i] /= norm;
+        }
+
+        // 转换为字节数组 (FLOAT32,每个值占 4 字节)
+        ByteBuffer buffer = ByteBuffer.allocate(vector.length * 4);
+        for (float value : vector) {
+            buffer.putFloat(value);
+        }
+        // 打印字节数组的内容
+        byte[] queryVector = buffer.array();
+        System.out.println("queryVector.length");
+        System.out.println(queryVector.length);
+
+        // 定义查询参数
+        String indexName = redisearchConf.getIndexName();
+        String prefix = redisearchConf.getPrefix();
+        String fieldName = redisearchConf.getFieldName();
+        String dim = redisearchConf.getDim();
+//        String queryVectorString = "\x8d5\xf7=\x91w:=*\x19h=\x05u\xc8=\xfa\x06\xb0=+\x00\xa8=\x96\x9b\x02>\xd5\xb4\x19>\xf1Pf=\xb5U\xf7=\xbc\xaf\x10>\xd9\x96\x18>\xa5hk=\xcd\xd3\x83=\x86\xcd\x18>\xbd\x80\x0b>A$\xf1=\xb2\xcf\xc5=K\xd4\xe2=\x83c\x01>\x02\x85\xbb=c$'=\x00-\x14>\x0c[i=\x80\x0b\xaf=\x15r\x0c>\xb2\x1f\xbf<\xe78\x8e=\x9b\xb9\xdf=\xce\xd9\xa3=\xe7\xaa\r>\xee\\\xaa=\x83\x95g=g/R=x\xc0\x0b>\xfd\xe7\x07>M\xb8\x0f=\x05\xbcA=q\x0e\xbd=R\x98\x8c;b\xfe\x85=\xea%+=\xe7\x98\x81:0H\x07>\xd9g\x04>\xe5\xe3\x8e=oM]=%\xbe\x92<\x1e\xf7\x05>\x11\xa9\x1a>\xce1\xe9<Z\x8d\x0e>\xe0<\x07>\xcar\x86=\xde\x0e\x9e=l\x8e\xf6=\xa9\x04\xa7=7\xe9G=\n[\x05<\xcd\xb3\x1c=\xf26\xe2=\xfd\xbd\xa4=\x94\x95\xea=\xb3\x9f\xa7<*Z\xfb;o\xbb\xb7<\xae\xea\xdb=\x03\x82\xcb<\xc8\x12\x1a<\xfa\xf9\x91<\xacJ\x18=\x18\xcb\xb0=n\x87\x92=6j\xf0=\xa7\xf0\x92<]!\xb4=\xf8\x01\x85=\xaf\xfa\xd7=\xbbE\xe5;f\x9d\xeb<^\xe1\x00>\xa3\xb9\x93=\x83\xcb\x14>\xdb\x86\x1a>\xe1\xa0\xd7=Cu*=9\xfd\x8f=e\xe2\xe7<y1.=2T\x91=59\x92=\xd8\xb5\xbd=\xd5\xf1\xa1=\xf4\xd6\x08>m\xd3K<\x1b\xc5M=\x93#\xb2=\xc0\x8b\xb4=\xa0Ph=\xe4s\x92=\xd7\x93\xb4=\xa3x\\;Q\x84\x08>\x8b&\xdc<a\x0e\x1d=\x83\x9c\x9e<\x9a\x84\xac<\xceS\xdf=\x8bT\xab;\xa7\xfd\r=\x88j\x10<\xcd\x07\xee<\x07\x8e\x81<t^\xdc=\xcf\xcf\xbd=\xeb\x92\xa1<[\x88\xd7=\xf0\xbb\x00>\x8e*\x18>\x82\xad\x11>yG\xbb<g\x07\x06>\xa08\x8d<\x01C\x1b>\xc7\xfd:=\x0b\x93\x10;\xbce>=\xb9\xdba=";
+        String topK = "1";  // 返回Top 10个结果
+//        String vectorField = "doc_embedding";  // 向量字段名称
+
+        String queryScript  = "local indexName = KEYS[1] "
+                + "local queryVector = ARGV[1] "
+                + "local topK = tonumber(ARGV[2]) "
+                + "local vectorField = ARGV[3] "
+                + "local query = '*=>[KNN ' .. topK .. ' @' .. vectorField .. ' $BLOB AS score]' "
+                + "local result = redis.call('FT.SEARCH', indexName, query, "
+                + "'PARAMS', '2', 'BLOB', queryVector, "
+                + "'RETURN', '1', 'score', "
+                + "'SORTBY', 'score', "
+                + "'DIALECT', '2') "
+                + "return result";
+        // 执行Redis查询
+
+
+        JedisPooled jedis = new JedisPooled( redisearchConf.getHost(), redisearchConf.getPort(), null, redisearchConf.getPassword());
+
+        // 动态设置变量
+
+        initIndex(jedis, indexName, prefix, fieldName, dim);
+
+        Object result = jedis.eval(queryScript.getBytes(), 1, indexName.getBytes(), queryVector, topK.getBytes(), fieldName.getBytes());
+        // 关闭连接
+        jedis.close();
+        // 打印结果
+        System.out.println("Query Result: " + JsonUtil.toJson(result));
+        RedisearchResultModel redisearchResultModel = processQueryResult(result);
+        //TODO 解析结果,返回userId
+        if (redisearchResultModel.getCount() == 0L) {
+            return ResultContent.build(ResultState.FaceNoMatches);
+        }
+
+        if (redisearchResultModel.getScoreValue().equals("nan") || redisearchResultModel.getScoreValue().equals("-nan")) {
+            return ResultContent.build(ResultState.FaceNoMatches);
+        }
+
+        BigDecimal scoreValue = new BigDecimal(redisearchResultModel.getScoreValue());
+        if (scoreValue.compareTo(BigDecimal.ZERO) == 0 || scoreValue.compareTo(new BigDecimal(redisearchConf.getScore())) > 0) {
+            String dataKey = redisearchResultModel.getDataKey();
+            dataKey = dataKey.replace(prefix, "");
+            String userId = dataKey.substring(0, 24);
+            return ResultContent.buildContent(userId);
+        }else {
+            return ResultContent.build(ResultState.FaceNoMatches);
+        }
+
+    }
+
+
+    private static RedisearchResultModel processQueryResult(Object result) {
+        RedisearchResultModel redisearchResultModel = new RedisearchResultModel();
+
+        if (result instanceof List){
+            List<Object> list = (List) result;
+            for (Object item : list) {
+                if (item instanceof byte[]) {// data key
+                    byte[] bytes = ((byte[]) item);
+                    System.out.println("Found vector: " + new String(bytes)); // 可以根据需要进一步处理
+                    redisearchResultModel.setDataKey(new String(bytes));
+                } else if (item instanceof Long){// 返回的结果数
+                    System.out.println("Found Long: " + item);
+                    redisearchResultModel.setCount((Long)item);
+                }else if (item instanceof List) {// 对应的 距离
+                    List<Object> itemList = (List) item;
+                    for (Object item1 : itemList) {
+                        if (item1 instanceof byte[]){
+                            byte[] bytes = ((byte[]) item1);
+                            System.out.println("Found vector: " + new String(bytes)); // 可以根据需要进一步处理
+                            String str = new String(bytes);
+                            if (str.equals("score")){
+                                redisearchResultModel.setScoreKey(str);
+                            }else {
+                                redisearchResultModel.setScoreValue(str);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return redisearchResultModel;
     }
 
     /**
@@ -163,7 +421,7 @@ public class UserFaceServiceImpl implements UserFaceService {
      * @throws IOException 如果发生 I/O 错误
      */
     @SneakyThrows
-    public static String convertInputStreamToBase64(InputStream inputStream){
+    public static String convertInputStreamToBase64(InputStream inputStream) {
         @Cleanup ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         byte[] buffer = new byte[1024];
         int bytesRead;
@@ -175,7 +433,7 @@ public class UserFaceServiceImpl implements UserFaceService {
     }
 
 
-    private byte[] imageResizer(String base64Image){
+    private byte[] imageResizer(String base64Image) {
         // 解码 Base64 字符串为 BufferedImage
         BufferedImage originalImage = decodeBase64ToImage(base64Image);
 
@@ -187,6 +445,22 @@ public class UserFaceServiceImpl implements UserFaceService {
 
     }
 
+//    public static void resizeImage(InputStream inputStream, int width, int height) throws IOException {
+//        BufferedImage inputImage = ImageIO.read(inputStream);
+//
+//        // 创建一个输出的 BufferedImage
+//        BufferedImage outputImage = new BufferedImage(width, height, inputImage.getType());
+//
+//        // 使用 Graphics2D 绘制缩放后的图片
+//        Graphics2D g2d = outputImage.createGraphics();
+//        g2d.drawImage(inputImage, 0, 0, width, height, null);
+//        g2d.dispose();
+//
+//        // 输出到文件
+//        String formatName = outputFile.getName().substring(outputFile.getName().lastIndexOf(".") + 1);
+//        ImageIO.write(outputImage, formatName, outputFile);
+//    }
+
     private static BufferedImage resizeImage(BufferedImage originalImage, int width, int height) {
         BufferedImage resizedImage = new BufferedImage(width, height, originalImage.getType());
         Graphics2D g = resizedImage.createGraphics();
@@ -206,8 +480,8 @@ public class UserFaceServiceImpl implements UserFaceService {
 
     @SneakyThrows
     private static byte[] encodeImageToBytes(BufferedImage image) {
-      @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
-      ImageIO.write(image, "png", baos);
-      return baos.toByteArray();
+        @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", baos);
+        return baos.toByteArray();
     }
 }

+ 82 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/service/local/UserNFCCardServiceImpl.java

@@ -0,0 +1,82 @@
+package com.github.microservice.auth.server.core.service.local;
+
+import com.github.microservice.auth.client.content.ResultContent;
+import com.github.microservice.auth.client.content.ResultState;
+import com.github.microservice.auth.client.model.UserNFCCardUpdateModel;
+import com.github.microservice.auth.client.service.UserNFCCardService;
+import com.github.microservice.auth.server.core.dao.UserDao;
+import com.github.microservice.auth.server.core.domain.User;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class UserNFCCardServiceImpl implements UserNFCCardService {
+
+    @Autowired
+    UserDao userDao;
+
+    public ResultContent update(UserNFCCardUpdateModel userNFCCardUpdateModel) {
+        User user = userDao.findTop1ById(userNFCCardUpdateModel.getUserId());
+        if (user == null){
+            return ResultContent.build(ResultState.UserNotExists);
+        }
+        //判断卡号是否重复
+        if (userDao.existsByNfcCardContains(userNFCCardUpdateModel.getNfcCard())){
+            return ResultContent.build(ResultState.NFCCardExists);
+        }
+        List<String> nfcCardList = user.getNfcCard();
+        if (nfcCardList == null){
+            nfcCardList = new ArrayList<>();
+        }
+
+        if (StringUtils.isNotBlank(userNFCCardUpdateModel.getUpdateCard())){
+            if (!nfcCardList.contains(userNFCCardUpdateModel.getUpdateCard())){
+                return ResultContent.build(ResultState.NFCCardNotExists);
+            }
+            nfcCardList.set(nfcCardList.indexOf(userNFCCardUpdateModel.getUpdateCard()), userNFCCardUpdateModel.getNfcCard());
+        }else {
+            nfcCardList.add(userNFCCardUpdateModel.getNfcCard());
+        }
+        user.setNfcCard(nfcCardList);
+        userDao.save(user);
+        return ResultContent.build(ResultState.Success);
+    }
+
+    public ResultContent delete(String userId, String nfcCard){
+        User user = userDao.findTop1ById(userId);
+        if (user == null){
+            return ResultContent.build(ResultState.UserNotExists);
+        }
+        List<String> nfcCardList = user.getNfcCard();
+        if (nfcCardList!=null && nfcCardList.contains(nfcCard)){
+            nfcCardList.remove(nfcCard);
+            user.setNfcCard(nfcCardList);
+            userDao.save(user);
+            return ResultContent.build(ResultState.Success);
+        }else {
+            return ResultContent.build(ResultState.NFCCardNotExists);
+        }
+    }
+
+    public ResultContent<List<String>> get(String userId){
+        User user = userDao.findTop1ById(userId);
+        if (user == null){
+            return ResultContent.build(ResultState.UserNotExists);
+        }
+        List<String> nfcCard = user.getNfcCard();
+        return ResultContent.buildContent(nfcCard!=null?nfcCard:new ArrayList<String>());
+    }
+
+    public ResultContent<String> matches(String nfcCard){
+
+        User cardContainsUser = userDao.findTopByNfcCardContains(nfcCard);
+        if (cardContainsUser == null){
+            return ResultContent.build(ResultState.NFCCardNotBind);
+        }
+        return ResultContent.buildContent(cardContainsUser.getId());
+    }
+}

+ 81 - 0
centers/AuthCenter/AuthServer/src/main/java/com/github/microservice/auth/server/core/util/FaceUtil.java

@@ -0,0 +1,81 @@
+package com.github.microservice.auth.server.core.util;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.util.StreamUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+public class FaceUtil {
+
+    public static HttpResponse<String> uploadFs(String url, InputStream inputStream, String detectorBackend) throws Exception {
+
+        // Generate a boundary for multipart/form-data
+        String boundary = UUID.randomUUID().toString();
+
+        // Build the multipart body as byte array
+        byte[] multipartBody = buildMultipartBody(boundary, Map.of(
+                "img", inputStream,
+                "detector_backend", detectorBackend
+        ));
+
+        // Create the HttpClient instance
+        HttpClient client = HttpClient.newHttpClient();
+
+        // Build the HTTP request
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url))
+                .header("Content-Type", "multipart/form-data; boundary=" + boundary)
+                .POST(HttpRequest.BodyPublishers.ofByteArray(multipartBody))
+                .build();
+
+        // Send the request and get the response
+        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+        // Print the response
+        System.out.println("Response Code: " + response.statusCode());
+        System.out.println("Response Body: " + response.body());
+        return response;
+    }
+
+    private static byte[] buildMultipartBody(String boundary, Map<String, Object> formData) throws Exception {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+        for (Map.Entry<String, Object> entry : formData.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+
+            outputStream.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
+
+            if (value instanceof InputStream) {
+                InputStream inputStream = (InputStream) value;
+
+                outputStream.write(("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"uploaded_file\"\r\n").getBytes(StandardCharsets.UTF_8));
+                outputStream.write("Content-Type: application/octet-stream\r\n\r\n".getBytes(StandardCharsets.UTF_8));
+
+//                byte[] buffer = new byte[8192];
+//                int bytesRead;
+//                while ((bytesRead = inputStream.read(buffer)) != -1) {
+//                    outputStream.write(buffer, bytesRead);
+//                }
+                StreamUtils.copy(inputStream,outputStream);
+
+                outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
+            } else {
+                outputStream.write(("Content-Disposition: form-data; name=\"" + key + "\"\r\n\r\n").getBytes(StandardCharsets.UTF_8));
+                outputStream.write((value.toString() + "\r\n").getBytes(StandardCharsets.UTF_8));
+            }
+        }
+
+        outputStream.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+
+        return outputStream.toByteArray();
+    }
+}

+ 9 - 0
centers/AuthCenter/AuthServer/src/main/resources/application-dev.yml

@@ -52,6 +52,15 @@ faceImage:
   width: 1280
   height: 720
 
+redisearch:
+  host: 172.24.50.53
+  port: 6379
+  password: 8756redis2024
+  index-name: face_index
+  field-name: doc_embedding
+  dim: 4096
+  score: 0.85
+  prefix: 'docs:'
 
 springdoc:
   api-docs: