yaorongkeji il y a 6 mois
Parent
commit
4fed12818f
19 fichiers modifiés avec 1148 ajouts et 4 suppressions
  1. 18 4
      hcp-app/src/main/java/com/yingyangfly/app/controller/LoginController.java
  2. 8 0
      hcp-app/src/main/resources/application-dev.yml
  3. 8 0
      hcp-app/src/main/resources/application-prod.yml
  4. 1 0
      hcp-app/src/main/resources/application.yml
  5. 309 0
      hcp-core/src/main/java/com/yingyangfly/core/api/impl/FaceContrastServer.java
  6. 25 0
      hcp-core/src/main/java/com/yingyangfly/core/bean/CreatePersonResult.java
  7. 68 0
      hcp-core/src/main/java/com/yingyangfly/core/bean/FaceSearchResult.java
  8. 72 0
      hcp-core/src/main/java/com/yingyangfly/core/domain/Face.java
  9. 49 0
      hcp-core/src/main/java/com/yingyangfly/core/domain/FaceVefLog.java
  10. 13 0
      hcp-core/src/main/java/com/yingyangfly/core/mapper/FaceMapper.java
  11. 11 0
      hcp-core/src/main/java/com/yingyangfly/core/mapper/FaceVefLogMapper.java
  12. 47 0
      hcp-core/src/main/java/com/yingyangfly/core/service/FaceService.java
  13. 11 0
      hcp-core/src/main/java/com/yingyangfly/core/service/FaceVefLogService.java
  14. 42 0
      hcp-core/src/main/java/com/yingyangfly/core/service/impl/AppUserService.java
  15. 116 0
      hcp-core/src/main/java/com/yingyangfly/core/service/impl/FaceServiceImpl.java
  16. 279 0
      hcp-core/src/main/java/com/yingyangfly/core/util/OSSImageUtils.java
  17. 53 0
      hcp-platform/src/main/java/com/yingyangfly/platform/controller/FaceContrller.java
  18. 9 0
      hcp-platform/src/main/resources/application-dev.yml
  19. 9 0
      hcp-platform/src/main/resources/application-prod.yml

+ 18 - 4
hcp-app/src/main/java/com/yingyangfly/app/controller/LoginController.java

@@ -11,11 +11,9 @@ import com.yingyangfly.core.service.impl.AppUserService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
 import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import java.time.LocalDate;
 
@@ -45,6 +43,22 @@ public class LoginController {
     }
 
 
+    @Log(title = "人脸登录",operatorType = OperatorType.MOBILE)
+    @TraceLog
+    @PostMapping(value = "humanFace/login",consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+    public ResultResponse humanFaceLogin(@RequestParam("imageBase") String imageBase){
+        return  ResultResponse.success(appUserService.humanFaceLogin(imageBase));
+    }
+
+    @Log(title = "人脸录入",operatorType = OperatorType.MOBILE)
+    @TraceLog
+    @PostMapping("Face/registration")
+    public ResultResponse FaceRegistration(@RequestBody String imageBase) {
+        return ResultResponse.success();
+    }
+
+
+
     @Log(title = "手机短信登录",operatorType = OperatorType.MOBILE)
     @ApiOperation("手机短信登录")
     @PostMapping("/loginMsg")

+ 8 - 0
hcp-app/src/main/resources/application-dev.yml

@@ -78,6 +78,14 @@ oss:
   endpoint:  oss-cn-beijing.aliyuncs.com #填写自己oss endpoint
   outDomain: https://yaorongoss.yaorongmedical.com #填写自己oss 外网域名
 
+tencent:
+  cloudapi:
+    secretid: AKIDPo5KNCoCLFoJb1HMFvQirpfJj0nSvAA4
+    secretkey: 1whR4UimcDakfMo2gCW9bppaHm371kET
+    endpoint:  iai.tencentcloudapi.com
+    region: ap-beijing
+    groupid: 1
+
 live:
   stream: 187278.push.tlivecloud.com
   broadcast: pull.dustlee.com

+ 8 - 0
hcp-app/src/main/resources/application-prod.yml

@@ -78,6 +78,14 @@ oss:
   endpoint:  oss-cn-beijing.aliyuncs.com #填写自己oss endpoint
   outDomain: https://yaorongoss.yaorongmedical.com #填写自己oss 外网域名
 
+tencent:
+  cloudapi:
+    secretid: AKIDPo5KNCoCLFoJb1HMFvQirpfJj0nSvAA4
+    secretkey: 1whR4UimcDakfMo2gCW9bppaHm371kET
+    endpoint:  iai.tencentcloudapi.com
+    region: ap-beijing
+    groupid: prod
+
 live:
   stream: 187278.push.tlivecloud.com
   broadcast: pull.dustlee.com

+ 1 - 0
hcp-app/src/main/resources/application.yml

@@ -41,3 +41,4 @@ security:
       - /app/getChangePasswordCode
       - /app/reset/password
       - /app/sysOrg/getHospitalList
+      - /app/humanFace/login

+ 309 - 0
hcp-core/src/main/java/com/yingyangfly/core/api/impl/FaceContrastServer.java

@@ -0,0 +1,309 @@
+package com.yingyangfly.core.api.impl;
+
+import com.yingyangfly.core.bean.CreatePersonResult;
+import com.yingyangfly.core.bean.FaceSearchResult;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.iai.v20200303.IaiClient;
+import com.tencentcloudapi.iai.v20200303.models.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Arrays;
+
+@ConfigurationProperties(prefix = "tencent.cloudapi")
+@Component
+@Data
+@Slf4j
+public class FaceContrastServer {
+
+    private static final Logger logger = LoggerFactory.getLogger(FaceContrastServer.class);
+
+    private String secretId;
+
+    private String secretKey;
+
+    private String endpoint;
+
+    private String region;
+
+    private String groupId;
+
+    // 搜索配置
+    private static final Long MAX_FACE_NUM = 1L; // 最多检测人脸数
+    private static final Long MAX_PERSON_NUM = 1L; // 最多返回人员数
+    private static final Long QUALITY_CONTROL = 1L; // 质量控制等级 1:较高
+    private static final Long NEED_PERSON_INFO = 1L; // 是否需要返回人员信息
+    private static final float FACE_MATCH_THRESHOLD = 80.0f; // 人脸匹配阈值(0-100)
+
+    /**
+     * 在人员库中搜索最相似的人员
+     * @param base64Image Base64格式的人脸图片
+     * @return FaceSearchResult 搜索结果
+     */
+    public FaceSearchResult searchPersonByFace(String base64Image) {
+        return searchPersonByFace(base64Image, new String[]{groupId});
+    }
+
+
+    /**
+     * 在人员库中搜索最相似的人员
+     * @param base64Image Base64格式的人脸图片
+     * @param groupIds 要搜索的人员库ID数组
+     * @return FaceSearchResult 搜索结果
+     */
+    public FaceSearchResult searchPersonByFace(String base64Image, String[] groupIds) {
+        FaceSearchResult result = new FaceSearchResult();
+
+        try {
+            // 1. 初始化客户端
+            Credential cred = new Credential(secretId, secretKey);
+            HttpProfile httpProfile = new HttpProfile();
+            httpProfile.setEndpoint(endpoint);
+            ClientProfile clientProfile = new ClientProfile();
+            clientProfile.setHttpProfile(httpProfile);
+            IaiClient client = new IaiClient(cred, region, clientProfile);
+
+            // 2. 构建搜索请求
+            SearchFacesRequest req = new SearchFacesRequest();
+
+            // 设置要搜索的人员库列表
+            req.setGroupIds(groupIds);
+
+            // 设置Base64图片
+            req.setImage(base64Image);
+
+            // 设置搜索参数
+            req.setMaxFaceNum(MAX_FACE_NUM);
+            req.setMaxPersonNum(MAX_PERSON_NUM);
+            req.setQualityControl(QUALITY_CONTROL);
+            req.setNeedPersonInfo(NEED_PERSON_INFO);
+            req.setFaceMatchThreshold(FACE_MATCH_THRESHOLD);
+
+            logger.info("开始人脸搜索,人员库: {}", Arrays.toString(groupIds));
+
+            // 3. 调用接口
+            SearchFacesResponse resp = client.SearchFaces(req);
+
+            // 4. 处理响应
+            processSearchResponse(resp, result);
+
+        } catch (TencentCloudSDKException e) {
+            logger.error("腾讯云人脸搜索失败: {}", e.getMessage(), e);
+            result.setSuccess(false);
+            result.setErrorCode(e.getErrorCode());
+            result.setErrorMessage(e.getMessage());
+        } catch (Exception e) {
+            logger.error("人脸搜索异常: {}", e.getMessage(), e);
+            result.setSuccess(false);
+            result.setErrorMessage("系统异常: " + e.getMessage());
+        }
+
+        return result;
+    }
+
+
+    /**
+     * 处理搜索结果
+     */
+    private void processSearchResponse(SearchFacesResponse resp, FaceSearchResult result) {
+        // 检查人脸检测结果
+        if (resp == null) {
+            result.setSuccess(false);
+            result.setErrorCode("NO_FACE_DETECTED");
+            result.setErrorMessage("未检测到人脸");
+            logger.warn("未在图片中检测到人脸");
+            return;
+        }
+
+        // 检查人脸质量
+        if (resp.getFaceModelVersion() != null) {
+            result.setFaceModelVersion(resp.getFaceModelVersion());
+        }
+
+        // 获取人脸框信息
+        if (resp.getResults() != null && resp.getResults().length > 0) {
+            FaceRect rect = resp.getResults()[0].getFaceRect();
+            result.setFaceRect(rect);
+        }
+
+        // 检查搜索结果
+        if (resp.getResults() == null || resp.getResults().length == 0) {
+            result.setSuccess(false);
+            result.setErrorCode("NO_MATCH_FOUND");
+            result.setErrorMessage("在人员库中未找到匹配的人员");
+            logger.info("在人员库中未找到匹配的人员");
+            return;
+        }
+
+        // 获取第一个(最相似)的结果
+        Result respResult = resp.getResults()[0];
+
+        // 设置结果
+        result.setSuccess(true);
+        result.setPersonId(respResult.getCandidates()[0].getPersonId());
+        result.setPersonName(respResult.getCandidates()[0].getPersonName());
+
+        // 腾讯云返回的分数是0-100,转换为0-1的小数
+        float similarity = respResult.getCandidates()[0].getScore() != null ? respResult.getCandidates()[0].getScore() / 100.0f : 0f;
+        result.setSimilarity(similarity);
+
+        // 设置人脸ID
+        if (respResult.getCandidates()[0].getFaceId() != null) {
+            result.setFaceId(respResult.getCandidates()[0].getFaceId());
+        }
+
+        logger.info("人脸搜索成功: 人员ID={}, 姓名={}, 相似度={}%",
+                respResult.getCandidates()[0].getPersonId(),
+                respResult.getCandidates()[0].getPersonName(),
+                String.format("%.2f", similarity * 100));
+    }
+
+
+
+    /**
+     * 创建人员(注册人脸)
+     * @param personId 人员ID
+     * @param personName 人员姓名
+     * @param base64Image Base64人脸图片
+     * @return 创建结果
+     */
+    public CreatePersonResult createPerson(String personId, String personName, String base64Image) {
+        CreatePersonResult result = new CreatePersonResult();
+
+        // 1. 输入校验
+        if (!validateInput(personId, personName, base64Image, result)) {
+            return result; // 校验失败,直接返回
+        }
+
+        try {
+            // 2. 初始化客户端 (与您的代码一致)
+            Credential cred = new Credential(secretId, secretKey);
+            HttpProfile httpProfile = new HttpProfile();
+            httpProfile.setEndpoint(endpoint);
+            ClientProfile clientProfile = new ClientProfile();
+            clientProfile.setHttpProfile(httpProfile);
+            IaiClient client = new IaiClient(cred, region, clientProfile);
+
+            // 3. 构建请求,并考虑添加防重复参数
+            CreatePersonRequest req = new CreatePersonRequest();
+            req.setGroupId(groupId);
+            req.setPersonId(personId);
+            req.setPersonName(personName);
+            req.setImage(base64Image);
+            req.setQualityControl(1L); // 启用图片质量控制
+            // 建议添加:防止同一人重复入库(根据业务选择级别,如2)
+            req.setUniquePersonControl(2L);
+            req.setNeedRotateDetection(1L);
+
+            // 4. 发送请求
+            CreatePersonResponse resp = client.CreatePerson(req);
+
+            // 5. 处理成功响应
+            result.setSuccess(true);
+            result.setFaceId(resp.getFaceId());
+            result.setFaceRect(resp.getFaceRect());
+            logger.info("创建人员成功: ID={}, 姓名={}, 人脸ID={}", personId, personName, resp.getFaceId());
+
+        } catch (TencentCloudSDKException e) { // 更精确地捕获SDK异常
+            logger.error("调用腾讯云SDK失败,错误码: {}, 错误信息: {}", e.getErrorCode(), e.getMessage(), e);
+            result.setSuccess(false);
+            result.setErrorMessage("系统服务调用异常: " + e.getErrorCode());
+        } catch (Exception e) { // 捕获其他未知异常
+            logger.error("创建人员时发生未知异常: {}", e.getMessage(), e);
+            result.setSuccess(false);
+            result.setErrorMessage("系统内部异常");
+        }
+        return result;
+    }
+
+    /**
+     * 存在人员添加人脸
+     * @param personId
+     * @param imageBase64Array
+     * @param imageUrlArray
+     * @return
+     * @throws TencentCloudSDKException
+     */
+    public CreateFaceResponse addFace(String personId, String[] imageBase64Array,
+                                             String[] imageUrlArray) throws TencentCloudSDKException {
+        // 2. 初始化客户端 (与您的代码一致)
+        Credential cred = new Credential(secretId, secretKey);
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint(endpoint);
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        IaiClient client = new IaiClient(cred, region, clientProfile);
+        CreateFaceRequest req = new CreateFaceRequest();
+
+        req.setPersonId(personId);
+
+        // 设置图片:Images.N 和 Urls.N 必须提供一个[citation:2]
+        if (imageUrlArray != null && imageUrlArray.length > 0) {
+            req.setUrls(imageUrlArray);
+        } else if (imageBase64Array != null && imageBase64Array.length > 0) {
+            req.setImages(imageBase64Array);
+        } else {
+            throw new IllegalArgumentException("必须提供Images或Urls其中之一");
+        }
+
+        // 可选参数:人脸相似度阈值,只有超过此值的人脸才能添加成功,默认60[citation:2]
+        req.setFaceMatchThreshold(60.0F);
+        req.setQualityControl(4L);
+
+        CreateFaceResponse resp = client.CreateFace(req);
+        System.out.println("AddFace 成功,添加了 " + resp.getSucFaceNum() + " 张人脸。请求ID: " + resp.getRequestId());
+        return resp;
+    }
+
+    /**
+     * 删除人脸
+     * @param personId
+     * @param faceIds
+     * @return
+     * @throws TencentCloudSDKException
+     */
+    public DeleteFaceResponse deleteFaces(String personId, String[] faceIds)
+            throws TencentCloudSDKException {
+
+        // 2. 初始化客户端 (与您的代码一致)
+        Credential cred = new Credential(secretId, secretKey);
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint(endpoint);
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        IaiClient client = new IaiClient(cred, region, clientProfile);
+        DeleteFaceRequest req = new DeleteFaceRequest();
+
+        // 设置必填参数
+        req.setPersonId(personId);
+        req.setFaceIds(faceIds);
+
+        // 调用删除人脸接口
+        DeleteFaceResponse resp = client.DeleteFace(req);
+        return resp;
+    }
+
+
+    private boolean validateInput(String personId, String personName, String base64Image, CreatePersonResult result) {
+        // 校验人员ID格式(只支持英文、数字、-%@#&_)
+        if (personId == null || !personId.matches("^[A-Za-z0-9\\-%@#&_]+$")) {
+            result.setSuccess(false);
+            result.setErrorMessage("人员ID格式非法,只支持英文、数字、-%@#&_");
+            return false;
+        }
+        // 校验Base64图片大小(不超过5M)[citation:3]
+        if (base64Image == null || base64Image.length() > 5 * 1024 * 1024) {
+            result.setSuccess(false);
+            result.setErrorMessage("人脸图片数据不能为空且大小不能超过5M");
+            return false;
+        }
+        // 可以添加更多校验...
+        return true;
+    }
+}

+ 25 - 0
hcp-core/src/main/java/com/yingyangfly/core/bean/CreatePersonResult.java

@@ -0,0 +1,25 @@
+package com.yingyangfly.core.bean;
+
+
+import com.tencentcloudapi.iai.v20200303.models.FaceRect;
+
+public class CreatePersonResult {
+
+    private boolean success;
+    private String faceId;
+    private FaceRect faceRect;
+    private String errorMessage;
+
+    // Getters and Setters
+    public boolean isSuccess() { return success; }
+    public void setSuccess(boolean success) { this.success = success; }
+
+    public String getFaceId() { return faceId; }
+    public void setFaceId(String faceId) { this.faceId = faceId; }
+
+    public FaceRect getFaceRect() { return faceRect; }
+    public void setFaceRect(FaceRect faceRect) { this.faceRect = faceRect; }
+
+    public String getErrorMessage() { return errorMessage; }
+    public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
+}

+ 68 - 0
hcp-core/src/main/java/com/yingyangfly/core/bean/FaceSearchResult.java

@@ -0,0 +1,68 @@
+package com.yingyangfly.core.bean;
+
+
+import com.tencentcloudapi.iai.v20200303.models.FaceRect;
+
+public class FaceSearchResult {
+
+    private boolean success;
+    private String personId;
+    private String personName;
+    private float similarity; // 0-1
+    private String groupId;
+    private String faceId;
+    private String faceModelVersion;
+    private FaceRect faceRect;
+    private String errorCode;
+    private String errorMessage;
+
+    // 是否匹配成功(相似度大于阈值)
+    public boolean isMatched() {
+        return success && similarity >= 0.8f; // 80%相似度阈值
+    }
+
+    // Getters and Setters
+    public boolean isSuccess() { return success; }
+    public void setSuccess(boolean success) { this.success = success; }
+
+    public String getPersonId() { return personId; }
+    public void setPersonId(String personId) { this.personId = personId; }
+
+    public String getPersonName() { return personName; }
+    public void setPersonName(String personName) { this.personName = personName; }
+
+    public float getSimilarity() { return similarity; }
+    public void setSimilarity(float similarity) { this.similarity = similarity; }
+
+    public String getGroupId() { return groupId; }
+    public void setGroupId(String groupId) { this.groupId = groupId; }
+
+    public String getFaceId() { return faceId; }
+    public void setFaceId(String faceId) { this.faceId = faceId; }
+
+    public String getFaceModelVersion() { return faceModelVersion; }
+    public void setFaceModelVersion(String faceModelVersion) { this.faceModelVersion = faceModelVersion; }
+
+    public FaceRect getFaceRect() { return faceRect; }
+    public void setFaceRect(FaceRect faceRect) { this.faceRect = faceRect; }
+
+    public String getErrorCode() { return errorCode; }
+    public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
+
+    public String getErrorMessage() { return errorMessage; }
+    public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
+
+    @Override
+    public String toString() {
+        return "FaceSearchResult{" +
+                "success=" + success +
+                ", personId='" + personId + '\'' +
+                ", personName='" + personName + '\'' +
+                ", similarity=" + similarity +
+                ", groupId='" + groupId + '\'' +
+                ", faceId='" + faceId + '\'' +
+                ", errorCode='" + errorCode + '\'' +
+                ", errorMessage='" + errorMessage + '\'' +
+                '}';
+    }
+}

+ 72 - 0
hcp-core/src/main/java/com/yingyangfly/core/domain/Face.java

@@ -0,0 +1,72 @@
+package com.yingyangfly.core.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ *
+ * @TableName face
+ */
+@TableName(value ="face")
+@Data
+public class Face implements Serializable {
+
+    /**
+     * 主键
+     */
+    private Long fid;
+
+    /**
+     * 用户关联id
+     */
+    private Long userId;
+
+    /**
+     * 图片数据 base_64编码
+     */
+    private String faceBase;
+
+    /**
+     * 插入时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+
+    /**
+     * 人脸名称
+     */
+    private String faceName;
+
+    /**
+     * 人脸备注
+     */
+    private String remark;
+
+    /**
+     * 人脸是否可用,(0==可用,1,不可用)
+     */
+    private Integer faceStatus;
+
+    /**
+     * 扩展字段1
+     */
+    private String updateExtend1;
+
+    /**
+     * 扩展字段2
+     */
+    private String updateExtend2;
+
+    /**
+     * 扩展字段3
+     */
+    private String updateExtend3;
+}

+ 49 - 0
hcp-core/src/main/java/com/yingyangfly/core/domain/FaceVefLog.java

@@ -0,0 +1,49 @@
+package com.yingyangfly.core.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ *
+ * @TableName face_vef_log
+ */
+@TableName(value ="face_vef_log")
+@Data
+public class FaceVefLog {
+
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Integer lid;
+
+    /**
+     * 验证时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date vefTime;
+
+    /**
+     * 返回code
+     */
+    private Integer vefCode;
+
+    /**
+     * 返回的消息
+     */
+    private String vefMsg;
+
+    /**
+     * 验证人
+     */
+    private String loginName;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 13 - 0
hcp-core/src/main/java/com/yingyangfly/core/mapper/FaceMapper.java

@@ -0,0 +1,13 @@
+package com.yingyangfly.core.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingyangfly.core.domain.Face;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @description 针对表【face】的数据库操作Mapper
+ */
+@Mapper
+public interface FaceMapper extends BaseMapper<Face> {
+
+}

+ 11 - 0
hcp-core/src/main/java/com/yingyangfly/core/mapper/FaceVefLogMapper.java

@@ -0,0 +1,11 @@
+package com.yingyangfly.core.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingyangfly.core.domain.FaceVefLog;
+
+/**
+ * @description 针对表【face_vef_log】的数据库操作Mapper
+ */
+public interface FaceVefLogMapper extends BaseMapper<FaceVefLog> {
+
+}

+ 47 - 0
hcp-core/src/main/java/com/yingyangfly/core/service/FaceService.java

@@ -0,0 +1,47 @@
+package com.yingyangfly.core.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.yingyangfly.core.domain.Face;
+
+import java.util.List;
+
+/**
+ * @description 针对表【face】的数据库操作Service
+ */
+public interface FaceService extends IService<Face> {
+
+
+    /**
+     * 根据患者id查询对应登录人脸
+     * @param UserId
+     * @return
+     */
+    List<Face> selectByUserId(Long UserId);
+
+//    /**
+//     * 人员注册
+//     * @param imageBase
+//     * @return
+//     */
+//    Boolean initFace(String imageBase,String UserName,Long UserId);
+
+
+    /**
+     * 新增人脸
+     * @param imageBase
+     * @param UserId
+     * @param UserName
+     * @return
+     */
+    Boolean saveFace(String imageBase,Long UserId,String UserName) throws TencentCloudSDKException;
+
+
+    /**
+     * 删除人脸
+     * @param UserId
+     * @param faceId
+     * @return
+     */
+    Boolean deleteFace(Long UserId,Long faceId) throws TencentCloudSDKException;
+}

+ 11 - 0
hcp-core/src/main/java/com/yingyangfly/core/service/FaceVefLogService.java

@@ -0,0 +1,11 @@
+package com.yingyangfly.core.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yingyangfly.core.domain.FaceVefLog;
+
+/**
+ * @description 针对表【face_vef_log】的数据库操作Service
+ */
+public interface FaceVefLogService extends IService<FaceVefLog> {
+
+}

+ 42 - 0
hcp-core/src/main/java/com/yingyangfly/core/service/impl/AppUserService.java

@@ -2,6 +2,7 @@ package com.yingyangfly.core.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import com.alibaba.excel.util.StringUtils;
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -17,6 +18,8 @@ import com.yingyangfly.common.exception.ServiceException;
 import com.yingyangfly.common.utils.DateUtils;
 import com.yingyangfly.common.utils.ids.IdUtils;
 import com.yingyangfly.core.api.ImApi;
+import com.yingyangfly.core.api.impl.FaceContrastServer;
+import com.yingyangfly.core.bean.FaceSearchResult;
 import com.yingyangfly.core.domain.*;
 import com.yingyangfly.core.dto.*;
 import com.yingyangfly.core.enums.MsgTemplateEnums;
@@ -31,6 +34,7 @@ import com.yingyangfly.redis.client.RedisClient;
 import lombok.extern.slf4j.Slf4j;
 import org.assertj.core.util.Sets;
 import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.userdetails.UserDetails;
@@ -733,6 +737,44 @@ public class AppUserService extends ServiceImpl<AppUserMapper, AppUser> implemen
         return ResultResponse.success(token);
     }
 
+    @Autowired
+    FaceContrastServer faceContrastServer;
+
+    public String humanFaceLogin(String imageBase){
+        FaceSearchResult result = faceContrastServer.searchPersonByFace(imageBase);
+
+        if (result.isSuccess() && result.isMatched()) {
+            // 验证通过
+            AppUser appUser = this.appUserMapper.selectById(result.getPersonId());
+            AppCurrentLoginUser userDetails = (AppCurrentLoginUser) createAppLoginUser(appUser);
+
+            String token = tokenUtil.generateToken(userDetails, false,"app");
+            String mobile = Sm4Util.decrypt(appUser.getMobile());
+            String tokenRedis = redisClient.get(String.format("%s%s", "token:app:", mobile),"");
+            if (StringUtils.isNotBlank(tokenRedis)){
+                redisClient.del(String.format("%s%s", "token:app:", mobile));
+            }
+            redisClient.set(String.format("%s%s", "token:app:", mobile),token);
+
+            //记录登录历史
+            LoginRecord loginRecord = new LoginRecord();
+            loginRecord.setCreateTime(new Date());
+            loginRecord.setUpdateTime(new Date());
+            loginRecord.setLoginName(userDetails.getUsername());
+            loginRecord.setUserType("0");
+            loginRecord.setLoginUserId(userDetails.getId());
+            loginRecord.setOrgCode(userDetails.getOrgCode());
+            loginRecord.setOrgName(userDetails.getOrgName());
+            loginRecordMapper.insert(loginRecord);
+            return token;
+        } else {
+            // 验证失败
+            throw new RuntimeException("人脸原则失败");
+        }
+    }
+
+
+
     public ResultResponse getCheckCode(String mobile,String appType) {
         String redisKey = "hcp:sms:mobile:" + mobile;
         String checkCodeRedis = redisClient.get(redisKey, "");

+ 116 - 0
hcp-core/src/main/java/com/yingyangfly/core/service/impl/FaceServiceImpl.java

@@ -0,0 +1,116 @@
+package com.yingyangfly.core.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.iai.v20200303.models.CreateFaceResponse;
+import com.tencentcloudapi.iai.v20200303.models.DeleteFaceResponse;
+import com.yingyangfly.core.api.impl.FaceContrastServer;
+import com.yingyangfly.core.bean.CreatePersonResult;
+import com.yingyangfly.core.bean.FaceSearchResult;
+import com.yingyangfly.core.domain.Face;
+import com.yingyangfly.core.mapper.FaceMapper;
+import com.yingyangfly.core.service.FaceService;
+import com.yingyangfly.core.util.OSSImageUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FaceServiceImpl extends ServiceImpl<FaceMapper, Face> implements FaceService {
+
+    @Autowired
+    FaceContrastServer faceContrastServer;
+
+
+    /**
+     * 根据用户id查询登录人脸照片
+     * @param UserId
+     * @return
+     */
+    @Override
+    public List<Face> selectByUserId(Long UserId) {
+        LambdaQueryWrapper<Face> faceLambdaQueryWrapper = new LambdaQueryWrapper<>();
+        faceLambdaQueryWrapper.eq(Face::getUserId,UserId);
+        return list(faceLambdaQueryWrapper);
+    }
+
+    /**
+     * 新建人员
+     * @param imageUrl
+     * @param UserName
+     * @param UserId
+     * @return
+     */
+    public Boolean initFace(String imageUrl,String UserName,Long UserId){
+        String imageBase = OSSImageUtils.getBase64FromOssUrl(imageUrl, 30, 3);
+        Face face = new Face();
+        face.setFaceBase(imageUrl);
+        face.setCreateTime(new Date());
+        face.setFaceName(UserName);
+        face.setFaceStatus(0);
+        face.setUserId(UserId);
+        CreatePersonResult person = faceContrastServer.createPerson(String.valueOf(UserId), UserName, imageBase);
+        if (person.isSuccess()){
+            face.setFid(Long.parseLong(person.getFaceId()));
+            return save(face);
+        }else {
+            throw new RuntimeException("添加失败");
+        }
+    }
+
+
+    /**
+     * 新增人脸
+     * @param imageUrl
+     * @param UserId
+     * @param UserName
+     * @return
+     * @throws TencentCloudSDKException
+     */
+    public Boolean saveFace(String imageUrl,Long UserId,String UserName) throws TencentCloudSDKException {
+        String imageBase = OSSImageUtils.getBase64FromOssUrl(imageUrl, 30, 3);
+        List<Face> faces = selectByUserId(UserId);
+        if (faces.size() == 0) {
+            return initFace(imageUrl,UserName,UserId);
+        }else {
+            Face face = new Face();
+            face.setFaceBase(imageUrl);
+            face.setCreateTime(new Date());
+            face.setFaceName(UserName);
+            face.setFaceStatus(0);
+            face.setUserId(UserId);
+
+            String[] imageBases = new String[]{imageBase};
+            CreateFaceResponse createFaceResponse = faceContrastServer.addFace(String.valueOf(UserId),imageBases,null);
+            if (createFaceResponse.getSucFaceNum()>0) {
+                face.setFid(Long.parseLong(createFaceResponse.getSucFaceIds()[0]));
+                return save(face);
+            }else {
+                throw new RuntimeException("添加失败");
+            }
+        }
+    }
+
+    /**
+     * 删除人脸
+     * @param UserId
+     * @param faceId
+     * @return
+     * @throws TencentCloudSDKException
+     */
+    public Boolean deleteFace(Long UserId,Long faceId) throws TencentCloudSDKException {
+        String[] faceIds = new String[]{String.valueOf(faceId)};
+        DeleteFaceResponse deleteFaceResponse = faceContrastServer.deleteFaces(String.valueOf(UserId), faceIds);
+        if (deleteFaceResponse.getSucDeletedNum()>0) {
+            LambdaQueryWrapper<Face> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+            lambdaQueryWrapper.eq(Face::getFid,faceId);
+            return remove(lambdaQueryWrapper);
+        }else {
+            throw new RuntimeException("删除失败");
+        }
+    }
+}

+ 279 - 0
hcp-core/src/main/java/com/yingyangfly/core/util/OSSImageUtils.java

@@ -0,0 +1,279 @@
+package com.yingyangfly.core.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Base64;
+import java.util.concurrent.*;
+
+public class OSSImageUtils {
+
+    private static final Logger logger = LoggerFactory.getLogger(OSSImageUtils.class);
+
+    // 线程池用于异步处理
+    private static final ExecutorService executorService = Executors.newFixedThreadPool(10);
+
+    /**
+     * 从OSS URL获取Base64(带超时控制)
+     */
+    public static String getBase64FromOssUrl(String imageUrl, int timeoutSeconds) {
+        return getBase64FromOssUrl(imageUrl, timeoutSeconds, 3);
+    }
+
+    /**
+     * 从OSS URL获取Base64(带超时和重试)
+     */
+    public static String getBase64FromOssUrl(String imageUrl, int timeoutSeconds, int maxRetries) {
+        Exception lastException = null;
+
+        for (int i = 0; i < maxRetries; i++) {
+            try {
+                if (i > 0) {
+                    logger.info("第{}次重试获取图片: {}", i + 1, imageUrl);
+                    Thread.sleep(1000 * i); // 重试间隔
+                }
+
+                return getBase64FromOssUrlWithTimeout(imageUrl, timeoutSeconds);
+
+            } catch (Exception e) {
+                lastException = e;
+                logger.warn("获取图片失败(第{}次): {}", i + 1, e.getMessage());
+            }
+        }
+
+        throw new RuntimeException("获取图片失败,重试" + maxRetries + "次后仍失败: " + imageUrl, lastException);
+    }
+
+    /**
+     * 异步获取Base64
+     */
+    public static CompletableFuture<String> getBase64FromOssUrlAsync(String imageUrl) {
+        return CompletableFuture.supplyAsync(() ->
+                        getBase64FromOssUrl(imageUrl, 30),
+                executorService
+        );
+    }
+
+    /**
+     * 带超时控制的获取方法
+     */
+    private static String getBase64FromOssUrlWithTimeout(String imageUrl, int timeoutSeconds)
+            throws Exception {
+
+        Future<String> future = executorService.submit(() ->
+                getBase64FromOssUrlInternal(imageUrl)
+        );
+
+        try {
+            return future.get(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            future.cancel(true);
+            throw new RuntimeException("获取图片超时: " + timeoutSeconds + "秒", e);
+        } catch (ExecutionException e) {
+            throw new RuntimeException("获取图片执行异常: " + e.getMessage(), e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("获取图片被中断", e);
+        }
+    }
+
+    /**
+     * 实际的获取逻辑
+     */
+    private static String getBase64FromOssUrlInternal(String imageUrl) throws IOException {
+        long startTime = System.currentTimeMillis();
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        ByteArrayOutputStream outputStream = null;
+
+        try {
+            logger.debug("开始获取图片: {}", imageUrl);
+
+            // 创建URL连接
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+
+            // 配置连接
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(30000);
+            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+            connection.setRequestProperty("Accept", "image/*");
+
+            // 建立连接
+            connection.connect();
+
+            // 检查响应码
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new IOException("HTTP " + responseCode + ": " + connection.getResponseMessage());
+            }
+
+            // 获取Content-Type
+            String contentType = connection.getContentType();
+            if (contentType == null || !contentType.startsWith("image/")) {
+                throw new IOException("非图片Content-Type: " + contentType);
+            }
+
+            // 获取Content-Length
+            int contentLength = connection.getContentLength();
+            if (contentLength > 10 * 1024 * 1024) { // 10MB限制
+                throw new IOException("图片太大: " + contentLength + " bytes");
+            }
+
+            // 读取数据
+            inputStream = connection.getInputStream();
+            outputStream = new ByteArrayOutputStream();
+
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            long totalBytes = 0;
+
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+                totalBytes += bytesRead;
+
+                // 进度监控(可选)
+                if (contentLength > 0) {
+                    int progress = (int) ((totalBytes * 100) / contentLength);
+                    logger.debug("下载进度: {}%", progress);
+                }
+            }
+
+            // 转换为Base64
+            byte[] imageBytes = outputStream.toByteArray();
+            String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+            // 获取MIME类型
+            String mimeType = extractMimeType(contentType);
+
+            long endTime = System.currentTimeMillis();
+            logger.info("图片获取完成: {} bytes, 耗时: {}ms",
+                    imageBytes.length, (endTime - startTime));
+
+            return "data:" + mimeType + ";base64," + base64Image;
+
+        } finally {
+            // 清理资源
+            closeQuietly(inputStream);
+            closeQuietly(outputStream);
+
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 从Content-Type中提取MIME类型
+     */
+    private static String extractMimeType(String contentType) {
+        if (contentType == null) {
+            return "application/octet-stream";
+        }
+
+        // 移除charset等参数
+        int semicolonIndex = contentType.indexOf(';');
+        if (semicolonIndex > 0) {
+            return contentType.substring(0, semicolonIndex).trim();
+        }
+
+        return contentType.trim();
+    }
+
+    /**
+     * 获取图片信息(不下载完整图片)
+     */
+    public static ImageInfo getImageInfo(String imageUrl) throws IOException {
+        HttpURLConnection connection = null;
+
+        try {
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+
+            connection.setRequestMethod("HEAD");
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(5000);
+
+            connection.connect();
+
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new IOException("HTTP " + responseCode);
+            }
+
+            String contentType = connection.getContentType();
+            String contentLengthStr = connection.getHeaderField("Content-Length");
+            long contentLength = contentLengthStr != null ? Long.parseLong(contentLengthStr) : -1;
+
+            return new ImageInfo(contentType, contentLength);
+
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 关闭资源
+     */
+    private static void closeQuietly(InputStream is) {
+        if (is != null) {
+            try {
+                is.close();
+            } catch (IOException e) {
+                logger.warn("关闭输入流失败", e);
+            }
+        }
+    }
+
+    private static void closeQuietly(ByteArrayOutputStream os) {
+        if (os != null) {
+            try {
+                os.close();
+            } catch (IOException e) {
+                logger.warn("关闭输出流失败", e);
+            }
+        }
+    }
+
+    /**
+     * 图片信息类
+     */
+    public static class ImageInfo {
+        private final String contentType;
+        private final long contentLength;
+
+        public ImageInfo(String contentType, long contentLength) {
+            this.contentType = contentType;
+            this.contentLength = contentLength;
+        }
+
+        public String getContentType() { return contentType; }
+        public long getContentLength() { return contentLength; }
+        public boolean isImage() {
+            return contentType != null && contentType.startsWith("image/");
+        }
+    }
+
+    /**
+     * 关闭线程池
+     */
+    public static void shutdown() {
+        executorService.shutdown();
+        try {
+            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
+                executorService.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executorService.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+    }
+}

+ 53 - 0
hcp-platform/src/main/java/com/yingyangfly/platform/controller/FaceContrller.java

@@ -0,0 +1,53 @@
+package com.yingyangfly.platform.controller;
+
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.yingyangfly.common.dto.ResultResponse;
+import com.yingyangfly.common.log.annotation.TraceLog;
+import com.yingyangfly.core.annotation.Log;
+import com.yingyangfly.core.api.impl.FaceContrastServer;
+import com.yingyangfly.core.domain.Face;
+import com.yingyangfly.core.service.FaceService;
+import com.yingyangfly.file.api.FileClient;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+@RestController
+@Api(tags = "人脸照片")
+@RequestMapping("/face")
+public class FaceContrller {
+
+    @Resource
+    private FaceService faceService;
+
+
+    @PostMapping("/selectByUserId")
+    @ApiOperation("根据用户id查询人脸照片列表")
+    @TraceLog
+    public ResultResponse selectByUserId(Long UserId) {
+        return ResultResponse.success(faceService.selectByUserId(UserId));
+    }
+
+    @ApiOperation("新增人脸照片")
+    @PostMapping("/save")
+    @TraceLog
+    public ResultResponse saveFave(@RequestBody Face face) throws TencentCloudSDKException {
+        return ResultResponse.success(faceService.saveFace(face.getFaceBase(),face.getUserId(),face.getFaceName()));
+    }
+
+    @ApiOperation("删除人脸照片")
+    @PostMapping("delete")
+    @TraceLog
+    public ResultResponse delete(@RequestBody Face face) throws TencentCloudSDKException {
+        return ResultResponse.success(faceService.deleteFace(face.getUserId(),face.getFid()));
+    }
+}

+ 9 - 0
hcp-platform/src/main/resources/application-dev.yml

@@ -72,6 +72,15 @@ oss:
   bucket-name: hcp-yaorong
   endpoint:  oss-cn-beijing.aliyuncs.com #填写自己oss endpoint
   outDomain: https://yaorongoss.yaorongmedical.com #填写自己oss 外网域名
+
+tencent:
+  cloudapi:
+    secretid: AKIDPo5KNCoCLFoJb1HMFvQirpfJj0nSvAA4
+    secretkey: 1whR4UimcDakfMo2gCW9bppaHm371kET
+    endpoint:  iai.tencentcloudapi.com
+    region: ap-beijing
+    groupid: 1
+
 wx:
   app-id: wxe6dbe98e8c6d1a4b
   app-secret: 9012fccacb9a71d5d39ff2881b35388c

+ 9 - 0
hcp-platform/src/main/resources/application-prod.yml

@@ -72,6 +72,15 @@ oss:
   bucket-name: hcp-yaorong
   endpoint:  oss-cn-beijing.aliyuncs.com #填写自己oss endpoint
   outDomain: https://yaorongoss.yaorongmedical.com #填写自己oss 外网域名
+
+tencent:
+  cloudapi:
+    secretid: AKIDPo5KNCoCLFoJb1HMFvQirpfJj0nSvAA4
+    secretkey: 1whR4UimcDakfMo2gCW9bppaHm371kET
+    endpoint:  iai.tencentcloudapi.com
+    region: ap-beijing
+    groupid: prod
+
 wx:
   app-id: wxe6dbe98e8c6d1a4b
   app-secret: 9012fccacb9a71d5d39ff2881b35388c