Browse Source

人脸识别及人脸识别登录改造

wukai 1 year ago
parent
commit
0b53ce900c
21 changed files with 761 additions and 95 deletions
  1. 29 6
      doc-admin/src/main/java/com/doc/web/controller/system/SysLoginController.java
  2. 14 3
      doc-admin/src/main/java/com/doc/web/controller/system/SysProfileController.java
  3. 15 0
      doc-admin/src/main/resources/application-dev.yml
  4. 231 0
      doc-admin/src/test/java/com/test/FaceEngineTest.java
  5. BIN
      doc-biz/lib/arcsoft-sdk-face-4.1.1.0.jar
  6. 50 0
      doc-biz/src/main/java/com/doc/face/config/FaceAuthenticationProvider.java
  7. 21 0
      doc-biz/src/main/java/com/doc/face/config/FaceAuthenticationToken.java
  8. 52 0
      doc-biz/src/main/java/com/doc/face/controller/FaceController.java
  9. 27 0
      doc-biz/src/main/java/com/doc/face/service/IFaceService.java
  10. 96 0
      doc-biz/src/main/java/com/doc/face/service/impl/FaceServiceImpl.java
  11. 93 0
      doc-biz/src/main/java/com/doc/face/service/init/FaceEngineInit.java
  12. 5 0
      doc-common/src/main/java/com/doc/common/constant/Constants.java
  13. 6 47
      doc-common/src/main/java/com/doc/common/core/domain/entity/SysUserExpand.java
  14. 1 1
      doc-common/src/main/java/com/doc/common/utils/encrypt/Sm3Util.java
  15. 6 1
      doc-framework/src/main/java/com/doc/framework/config/SecurityConfig.java
  16. 42 0
      doc-framework/src/main/java/com/doc/framework/web/service/SysLoginService.java
  17. 5 1
      doc-framework/src/main/java/com/doc/framework/web/service/SysPasswordService.java
  18. 15 9
      doc-system/src/main/java/com/doc/system/mapper/SysUserExpandMapper.java
  19. 10 8
      doc-system/src/main/java/com/doc/system/service/ISysUserExpandService.java
  20. 10 0
      doc-system/src/main/java/com/doc/system/service/impl/SysUserExpandServiceImpl.java
  21. 33 19
      doc-system/src/main/resources/mapper/system/SysUserExpandMapper.xml

+ 29 - 6
doc-admin/src/main/java/com/doc/web/controller/system/SysLoginController.java

@@ -9,16 +9,18 @@ import com.doc.common.core.domain.entity.SysMenu;
 import com.doc.common.core.domain.entity.SysUser;
 import com.doc.common.core.domain.model.LoginBody;
 import com.doc.common.utils.SecurityUtils;
+import com.doc.face.service.IFaceService;
 import com.doc.framework.web.service.SysLoginService;
 import com.doc.framework.web.service.SysPermissionService;
 import com.doc.system.service.ISysMenuService;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
+import com.doc.system.service.ISysUserService;
+import io.swagger.annotations.ApiOperation;
+import javafx.util.Pair;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -44,7 +46,9 @@ public class SysLoginController {
     @Resource
     private IChatMsgService msgService;
     @Resource
-    private StringRedisTemplate stringRedisTemplate;
+    private IFaceService faceService;
+    @Resource
+    private ISysUserService userService;
 
     /**
      * 登录方法
@@ -122,4 +126,23 @@ public class SysLoginController {
         List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
         return AjaxResult.success(menuService.buildMenus(menus));
     }
+
+    @ApiOperation("人脸识别")
+    @PostMapping("/face-login")
+    public AjaxResult face(@RequestParam("file") MultipartFile file) throws Exception {
+        String originalFilename = file.getOriginalFilename();
+        String[] filename = originalFilename.split("\\.");
+        File upFile = File.createTempFile("temp_" + filename[0], filename[1] + ".");
+        file.transferTo(upFile);
+
+        Pair<Long, Float> pair = faceService.searchFaces(upFile);
+        if (pair != null && pair.getValue() > 0.75f) {
+            SysUser user = userService.selectUserById(pair.getKey());
+            AjaxResult ajax = AjaxResult.success();
+            // 生成令牌
+            String token = loginService.faceLogin(user.getUserName());
+            return AjaxResult.success("人脸识别成功,用户ID:" + pair.getKey() + "相似度:" + pair.getValue()).put(Constants.TOKEN, token);
+        }
+        return AjaxResult.error("未找到对应用户");
+    }
 }

+ 14 - 3
doc-admin/src/main/java/com/doc/web/controller/system/SysProfileController.java

@@ -3,6 +3,7 @@ package com.doc.web.controller.system;
 import com.doc.biz.service.IDocSpaceService;
 import com.doc.common.annotation.Log;
 import com.doc.common.config.RuoYiConfig;
+import com.doc.common.constant.Constants;
 import com.doc.common.core.controller.BaseController;
 import com.doc.common.core.domain.AjaxResult;
 import com.doc.common.core.domain.entity.SysUser;
@@ -14,6 +15,7 @@ import com.doc.common.utils.SecurityUtils;
 import com.doc.common.utils.StringUtils;
 import com.doc.common.utils.file.FileUploadUtils;
 import com.doc.common.utils.file.MimeTypeUtils;
+import com.doc.face.service.IFaceService;
 import com.doc.framework.web.service.TokenService;
 import com.doc.system.service.ISysUserExpandService;
 import com.doc.system.service.ISysUserService;
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import java.io.File;
 import java.util.Date;
 
 /**
@@ -43,6 +46,9 @@ public class SysProfileController extends BaseController {
     @Resource
     private ISysUserExpandService expandService;
 
+    @Resource
+    private IFaceService faceService;
+
     /**
      * 个人信息
      */
@@ -59,7 +65,7 @@ public class SysProfileController extends BaseController {
     /**
      * 修改用户
      */
-    @Log(title = "个人信息", businessType = BusinessType.UPDATE,eventLevel = EventLevel.MIDDLE)
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE, eventLevel = EventLevel.MIDDLE)
     @PutMapping
     public AjaxResult updateProfile(@RequestBody SysUser user) {
         LoginUser loginUser = getLoginUser();
@@ -92,7 +98,7 @@ public class SysProfileController extends BaseController {
     /**
      * 重置密码
      */
-    @Log(title = "个人信息", businessType = BusinessType.UPDATE,eventLevel = EventLevel.HIGH)
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE, eventLevel = EventLevel.HIGH)
     @PutMapping("/updatePwd")
     public AjaxResult updatePwd(String oldPassword, String newPassword) {
         LoginUser loginUser = getLoginUser();
@@ -123,7 +129,7 @@ public class SysProfileController extends BaseController {
     /**
      * 头像上传
      */
-    @Log(title = "用户头像", businessType = BusinessType.UPDATE,eventLevel = EventLevel.MIDDLE)
+    @Log(title = "用户头像", businessType = BusinessType.UPDATE, eventLevel = EventLevel.MIDDLE)
     @PostMapping("/avatar")
     public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception {
         if (!file.isEmpty()) {
@@ -134,6 +140,11 @@ public class SysProfileController extends BaseController {
                 ajax.put("imgUrl", avatar);
                 // 更新缓存用户头像
                 loginUser.getUser().setAvatar(avatar);
+                // 本地资源路径
+                String localPath = RuoYiConfig.getProfile();
+                // 数据库资源地址
+                String path = localPath + StringUtils.substringAfter(avatar, Constants.RESOURCE_PREFIX);
+                faceService.detectFaces(new File(path));
                 tokenService.setLoginUser(loginUser);
                 return ajax;
             }

+ 15 - 0
doc-admin/src/main/resources/application-dev.yml

@@ -9,10 +9,25 @@ ruoyi:
 
 #大语言模型配置
 chat-glm3:
+  # 是否开启
   flag: true
+  # OPENAI地址
   host: http://8.142.173.95:17020/
+  # 最大会话字符数
   max-token: 2048
 
+#人脸识别配置
+face-engine:
+  # 是否开启
+  flag: true
+  # 引擎库存放位置
+  lib: D:\arcsoft_lib
+  app-id: D86z2YwLmGT1upsnxzGSdUKGuLyGp27VjEdPaVJepbGo
+  sdk-key: ALyL4ErWU6FrBX1GTj7UCZrtUEpgKgUYXBLT3XroMbV9
+  active-key: 086L-11C9-315C-PKCN
+  # 激活文件存放位置
+  active-file: D:\arcsoft_lib\086L11C9315CPKCN.dat
+
 # Spring配置
 spring:
   #es配置

+ 231 - 0
doc-admin/src/test/java/com/test/FaceEngineTest.java

@@ -0,0 +1,231 @@
+package com.test;
+
+import com.arcsoft.face.*;
+import com.arcsoft.face.enums.*;
+import com.arcsoft.face.toolkit.ImageFactory;
+import com.arcsoft.face.toolkit.ImageInfo;
+import com.arcsoft.face.toolkit.ImageInfoEx;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FaceEngineTest {
+
+
+    public static void main(String[] args) {
+
+        //激活码,从官网获取
+        String appId = "D86z2YwLmGT1upsnxzGSdUKGuLyGp27VjEdPaVJepbGo";
+        String sdkKey = "ALyL4ErWU6FrBX1GTj7UCZrtUEpgKgUYXBLT3XroMbV9";
+        String activeKey = "086L-11C9-315C-PKCN";
+
+        System.err.println("注意,如果返回的errorCode不为0,可查看com.arcsoft.face.enums.ErrorInfo类获取相应的错误信息");
+
+        //人脸识别引擎库存放路径
+        FaceEngine faceEngine = new FaceEngine("d:\\arcsoft_lib");
+        //激活引擎
+        int errorCode = faceEngine.activeOnline(appId, sdkKey, activeKey);
+        System.out.println("引擎激活errorCode:" + errorCode);
+
+        ActiveDeviceInfo activeDeviceInfo = new ActiveDeviceInfo();
+        //采集设备信息(可离线)
+        errorCode = faceEngine.getActiveDeviceInfo(activeDeviceInfo);
+        System.out.println("采集设备信息errorCode:" + errorCode);
+        System.out.println("设备信息:" + activeDeviceInfo.getDeviceInfo());
+
+        faceEngine.activeOffline("D:\\arcsoft_lib\\086L11C9315CPKCN.dat");
+
+        ActiveFileInfo activeFileInfo = new ActiveFileInfo();
+        errorCode = faceEngine.getActiveFileInfo(activeFileInfo);
+        System.out.println("获取激活文件errorCode:" + errorCode);
+        System.out.println("激活文件信息:" + activeFileInfo.toString());
+
+        //引擎配置
+        EngineConfiguration engineConfiguration = new EngineConfiguration();
+        engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
+        engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);
+        engineConfiguration.setDetectFaceMaxNum(10);
+        //功能配置
+        FunctionConfiguration functionConfiguration = new FunctionConfiguration();
+        functionConfiguration.setSupportAge(true);
+        functionConfiguration.setSupportFaceDetect(true);
+        functionConfiguration.setSupportFaceRecognition(true);
+        functionConfiguration.setSupportGender(true);
+        functionConfiguration.setSupportLiveness(true);
+        functionConfiguration.setSupportIRLiveness(true);
+        functionConfiguration.setSupportImageQuality(true);
+        functionConfiguration.setSupportMaskDetect(true);
+        functionConfiguration.setSupportUpdateFaceData(true);
+        engineConfiguration.setFunctionConfiguration(functionConfiguration);
+
+        //初始化引擎
+        errorCode = faceEngine.init(engineConfiguration);
+        System.out.println("初始化引擎errorCode:" + errorCode);
+        VersionInfo version = faceEngine.getVersion();
+        System.out.println(version);
+
+        //人脸检测
+        ImageInfo imageInfo = ImageFactory.getRGBData(new File("D:\\SYSTEM\\Desktop\\temp\\face\\111.jpeg"));
+        List<FaceInfo> faceInfoList = new ArrayList<FaceInfo>();
+        errorCode = faceEngine.detectFaces(imageInfo, faceInfoList);
+        System.out.println("人脸检测errorCode:" + errorCode);
+        System.out.println("检测到人脸数:" + faceInfoList.size());
+
+        ImageQuality imageQuality = new ImageQuality();
+        errorCode = faceEngine.imageQualityDetect(imageInfo, faceInfoList.get(0), 0, imageQuality);
+        System.out.println("图像质量检测errorCode:" + errorCode);
+        System.out.println("图像质量分数:" + imageQuality.getFaceQuality());
+
+        //特征提取
+        FaceFeature faceFeature = new FaceFeature();
+        errorCode = faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.REGISTER, 0, faceFeature);
+        System.out.println("特征提取errorCode:" + errorCode);
+        byte[] fd = faceFeature.getFeatureData();
+        String xx = new String(fd, StandardCharsets.UTF_8);
+        System.out.println("特征值:" + fd);
+        System.out.println("特征值:" + xx);
+        System.out.println("特征值:" + xx.getBytes(StandardCharsets.UTF_8));
+
+        //人脸检测2
+        ImageInfo imageInfo2 = ImageFactory.getRGBData(new File("D:\\SYSTEM\\Desktop\\temp\\face\\222.jpg"));
+        List<FaceInfo> faceInfoList2 = new ArrayList<FaceInfo>();
+        errorCode = faceEngine.detectFaces(imageInfo2, faceInfoList2);
+        System.out.println("222人脸检测errorCode:" + errorCode);
+        System.out.println("222检测到人脸数:" + faceInfoList2.size());
+
+        //特征提取2
+        FaceFeature faceFeature2 = new FaceFeature();
+        errorCode = faceEngine.extractFaceFeature(imageInfo2, faceInfoList2.get(0), ExtractType.RECOGNIZE, 0, faceFeature2);
+        System.out.println("特征提取errorCode:" + errorCode);
+
+        //特征比对
+        FaceFeature targetFaceFeature = new FaceFeature();
+        targetFaceFeature.setFeatureData(faceFeature.getFeatureData());
+        FaceFeature sourceFaceFeature = new FaceFeature();
+        sourceFaceFeature.setFeatureData(faceFeature2.getFeatureData());
+        FaceSimilar faceSimilar = new FaceSimilar();
+
+        errorCode = faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar);
+        System.out.println("特征比对errorCode:" + errorCode);
+        System.out.println("人脸相似度:" + faceSimilar.getScore());
+
+
+        //人脸属性检测
+        FunctionConfiguration configuration = new FunctionConfiguration();
+        configuration.setSupportAge(true);
+        configuration.setSupportGender(true);
+        configuration.setSupportLiveness(true);
+        configuration.setSupportMaskDetect(true);
+        errorCode = faceEngine.process(imageInfo, faceInfoList, configuration);
+        System.out.println("图像属性处理errorCode:" + errorCode);
+
+        //性别检测
+        List<GenderInfo> genderInfoList = new ArrayList<GenderInfo>();
+        errorCode = faceEngine.getGender(genderInfoList);
+        System.out.println("性别:" + genderInfoList.get(0).getGender());
+
+        //年龄检测
+        List<AgeInfo> ageInfoList = new ArrayList<AgeInfo>();
+        errorCode = faceEngine.getAge(ageInfoList);
+        System.out.println("年龄:" + ageInfoList.get(0).getAge());
+
+        //活体检测
+        List<LivenessInfo> livenessInfoList = new ArrayList<LivenessInfo>();
+        errorCode = faceEngine.getLiveness(livenessInfoList);
+        System.out.println("活体:" + livenessInfoList.get(0).getLiveness());
+
+        //口罩检测
+        List<MaskInfo> maskInfoList = new ArrayList<MaskInfo>();
+        errorCode = faceEngine.getMask(maskInfoList);
+        System.out.println("口罩:" + maskInfoList.get(0).getMask());
+
+
+        //IR属性处理
+        ImageInfo imageInfoGray = ImageFactory.getGrayData(new File("d:\\IR_480p.jpg"));
+        List<FaceInfo> faceInfoListGray = new ArrayList<FaceInfo>();
+        errorCode = faceEngine.detectFaces(imageInfoGray, faceInfoListGray);
+
+        FunctionConfiguration configuration2 = new FunctionConfiguration();
+        configuration2.setSupportIRLiveness(true);
+        errorCode = faceEngine.processIr(imageInfoGray, faceInfoListGray, configuration2);
+        //IR活体检测
+        List<IrLivenessInfo> irLivenessInfo = new ArrayList<>();
+        errorCode = faceEngine.getLivenessIr(irLivenessInfo);
+        System.out.println("IR活体:" + irLivenessInfo.get(0).getLiveness());
+
+        //获取激活文件信息
+        ActiveFileInfo activeFileInfo2 = new ActiveFileInfo();
+        errorCode = faceEngine.getActiveFileInfo(activeFileInfo2);
+
+        //更新人脸数据
+        errorCode = faceEngine.updateFaceData(imageInfo, faceInfoList);
+
+        //高级人脸图像处理接口
+        ImageInfoEx imageInfoEx = new ImageInfoEx();
+        imageInfoEx.setHeight(imageInfo.getHeight());
+        imageInfoEx.setWidth(imageInfo.getWidth());
+        imageInfoEx.setImageFormat(imageInfo.getImageFormat());
+        imageInfoEx.setImageDataPlanes(new byte[][]{imageInfo.getImageData()});
+        imageInfoEx.setImageStrides(new int[]{imageInfo.getWidth() * 3});
+        List<FaceInfo> faceInfoList1 = new ArrayList<>();
+        errorCode = faceEngine.detectFaces(imageInfoEx, DetectModel.ASF_DETECT_MODEL_RGB, faceInfoList1);
+        ImageQuality imageQuality1 = new ImageQuality();
+        errorCode = faceEngine.imageQualityDetect(imageInfoEx, faceInfoList1.get(0), 0, imageQuality1);
+        FunctionConfiguration fun = new FunctionConfiguration();
+        fun.setSupportAge(true);
+        errorCode = faceEngine.process(imageInfoEx, faceInfoList1, fun);
+        List<AgeInfo> ageInfoList1 = new ArrayList<>();
+        int age = faceEngine.getAge(ageInfoList1);
+        FaceFeature feature = new FaceFeature();
+        errorCode = faceEngine.extractFaceFeature(imageInfoEx, faceInfoList1.get(0), ExtractType.REGISTER, 0, feature);
+        errorCode = faceEngine.updateFaceData(imageInfoEx, faceInfoList1);
+
+        //设置活体测试
+        errorCode = faceEngine.setLivenessParam(0.5f, 0.7f, 0.3f);
+        System.out.println("设置活体活体阈值errorCode:" + errorCode);
+
+        LivenessParam livenessParam = new LivenessParam();
+        errorCode = faceEngine.getLivenessParam(livenessParam);
+
+        //注册人脸信息1
+        FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo();
+        faceFeatureInfo.setSearchId(5);
+        faceFeatureInfo.setFaceTag("FeatureData1");
+        faceFeatureInfo.setFeatureData(faceFeature.getFeatureData());
+        errorCode = faceEngine.registerFaceFeature(faceFeatureInfo);
+
+        //注册人脸信息2
+        FaceFeatureInfo faceFeatureInfo2 = new FaceFeatureInfo();
+        faceFeatureInfo2.setSearchId(6);
+        faceFeatureInfo2.setFaceTag("FeatureData2");
+        faceFeatureInfo2.setFeatureData(faceFeature2.getFeatureData());
+        errorCode = faceEngine.registerFaceFeature(faceFeatureInfo2);
+
+        //获取注册人脸个数
+        FaceSearchCount faceSearchCount = new FaceSearchCount();
+        errorCode = faceEngine.getFaceCount(faceSearchCount);
+        System.out.println("注册人脸个数:" + faceSearchCount.getCount());
+
+        //搜索最相似人脸
+        SearchResult searchResult = new SearchResult();
+        errorCode = faceEngine.searchFaceFeature(faceFeature, CompareModel.LIFE_PHOTO, searchResult);
+        System.out.println("最相似人脸Id:" + searchResult.getFaceFeatureInfo().getSearchId());
+
+        //更新人脸信息
+        FaceFeatureInfo faceFeatureInfo3 = new FaceFeatureInfo();
+        faceFeatureInfo3.setSearchId(6);
+        faceFeatureInfo3.setFaceTag("FeatureData2Update");
+        faceFeatureInfo3.setFeatureData(faceFeature2.getFeatureData());
+        errorCode = faceEngine.updateFaceFeature(faceFeatureInfo3);
+
+        //移除人脸信息
+        errorCode = faceEngine.removeFaceFeature(6);
+
+        //引擎卸载
+        errorCode = faceEngine.unInit();
+
+
+    }
+}

BIN
doc-biz/lib/arcsoft-sdk-face-4.1.1.0.jar


+ 50 - 0
doc-biz/src/main/java/com/doc/face/config/FaceAuthenticationProvider.java

@@ -0,0 +1,50 @@
+package com.doc.face.config;
+
+import com.doc.common.constant.Constants;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 自定义Spring-Security身份验证提供者(扩展单点登录)
+ *
+ * @author wukai
+ */
+@Component
+public class FaceAuthenticationProvider implements AuthenticationProvider {
+
+    @Resource
+    private UserDetailsService userDetailsService;
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        String userName = authentication.getName();
+        String password = authentication.getCredentials().toString();
+
+        // 密码是 faceLogin 则强制登录
+        if (Constants.FACE_LOGIN.equals(password)) {
+            UserDetails user = userDetailsService.loadUserByUsername(userName);
+            return new FaceAuthenticationToken(user, password, user.getAuthorities());
+        }
+
+        // 使用用户详情服务进行正常的身份认证
+        UserDetails user = userDetailsService.loadUserByUsername(userName);
+        if (user != null && user.getPassword().equals(password)) {
+            return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
+        }
+
+        // 返回 null 表示认证失败
+        return null;
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return authentication.equals(UsernamePasswordAuthenticationToken.class);
+    }
+}

+ 21 - 0
doc-biz/src/main/java/com/doc/face/config/FaceAuthenticationToken.java

@@ -0,0 +1,21 @@
+package com.doc.face.config;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+/**
+ * 自定义Spring-Security身份验证令牌
+ *
+ * @author wukai
+ */
+public class FaceAuthenticationToken extends UsernamePasswordAuthenticationToken {
+    public FaceAuthenticationToken(Object principal, Object credentials) {
+        super(principal, credentials);
+    }
+
+    public FaceAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
+        super(principal, credentials, authorities);
+    }
+}

+ 52 - 0
doc-biz/src/main/java/com/doc/face/controller/FaceController.java

@@ -0,0 +1,52 @@
+//package com.doc.face.controller;
+//
+//import com.doc.common.core.controller.BaseController;
+//import com.doc.common.core.domain.AjaxResult;
+//import com.doc.face.service.IFaceService;
+//import io.swagger.annotations.Api;
+//import io.swagger.annotations.ApiOperation;
+//import javafx.util.Pair;
+//import org.springframework.web.bind.annotation.PostMapping;
+//import org.springframework.web.bind.annotation.RequestMapping;
+//import org.springframework.web.bind.annotation.RequestParam;
+//import org.springframework.web.bind.annotation.RestController;
+//import org.springframework.web.multipart.MultipartFile;
+//
+//import javax.annotation.Resource;
+//import java.io.File;
+//
+///**
+// * 聊天消息管理Controller
+// *
+// * @author wukai
+// * @date 2023-08-24
+// */
+//@Api(tags = "人脸识别")
+//@RestController
+//@RequestMapping("/face/")
+//public class FaceController extends BaseController {
+//    @Resource
+//    private IFaceService faceService;
+//    @Resource
+//
+//    @ApiOperation("人脸识别")
+//    @PostMapping("")
+//    public AjaxResult face(@RequestParam("file") MultipartFile file) throws Exception {
+//        //选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。
+//        String originalFilename = file.getOriginalFilename();
+//        String[] filename = originalFilename.split("\\.");
+//        File upFile = File.createTempFile("temp_" + filename[0], filename[1] + ".");
+//        file.transferTo(upFile);
+//
+//        Pair<Long, Float> pair = faceService.searchFaces(upFile);
+//        if (pair == null) {
+//            return error("未找到对应用户");
+//        }
+//
+//        if (pair.getValue() > 0.75f) {
+//
+//        }
+//
+//        return success("人脸识别成功,用户ID:" + pair.getKey() + "相似度:" + pair.getValue());
+//    }
+//}

+ 27 - 0
doc-biz/src/main/java/com/doc/face/service/IFaceService.java

@@ -0,0 +1,27 @@
+package com.doc.face.service;
+
+import javafx.util.Pair;
+
+import java.io.File;
+
+/**
+ * 聊天消息管理;Service接口
+ *
+ * @author wukai
+ * @date 2023-08-24
+ */
+public interface IFaceService {
+    /**
+     * 人脸检测
+     *
+     * @param file 人脸图片
+     */
+    void detectFaces(File file);
+
+    /**
+     * 人脸匹配
+     *
+     * @param file 人脸图片
+     */
+    Pair<Long, Float> searchFaces(File file);
+}

+ 96 - 0
doc-biz/src/main/java/com/doc/face/service/impl/FaceServiceImpl.java

@@ -0,0 +1,96 @@
+package com.doc.face.service.impl;
+
+import com.arcsoft.face.*;
+import com.arcsoft.face.enums.CompareModel;
+import com.arcsoft.face.enums.ExtractType;
+import com.arcsoft.face.toolkit.ImageFactory;
+import com.arcsoft.face.toolkit.ImageInfo;
+import com.doc.common.core.domain.entity.SysUserExpand;
+import com.doc.common.utils.SecurityUtils;
+import com.doc.face.service.IFaceService;
+import com.doc.face.service.init.FaceEngineInit;
+import com.doc.system.service.ISysUserExpandService;
+import javafx.util.Pair;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 聊天消息管理Service业务层处理
+ *
+ * @author wukai
+ * @date 2023-08-24
+ */
+@Service
+public class FaceServiceImpl implements IFaceService {
+    @Resource
+    private FaceEngineInit faceEngineInit;
+    @Resource
+    private ISysUserExpandService expandService;
+
+    @Override
+    public void detectFaces(File file) {
+        byte[] featureData = faceFeature(file);
+        if (featureData != null) {
+            String feature = new String(featureData, StandardCharsets.ISO_8859_1);
+            SysUserExpand expand = new SysUserExpand();
+            expand.setUserId(SecurityUtils.getUserId());
+            expand.setFaceFeature(feature);
+            expandService.updateSysUserExpand(expand);
+        }
+    }
+
+    /**
+     * 提取特征值
+     *
+     * @param file 人脸图片
+     * @return 特征值
+     */
+    private byte[] faceFeature(File file) {
+        FaceEngine faceEngine = faceEngineInit.getFaceEngine();
+        ImageInfo imageInfo = ImageFactory.getRGBData(file);
+        List<FaceInfo> faceInfoList = new ArrayList<>();
+        faceEngine.detectFaces(imageInfo, faceInfoList);
+
+        if (faceInfoList.size() == 1) {
+            //有且只有一个人脸信息时才提取特征值
+            //提取特征值
+            FaceFeature faceFeature = new FaceFeature();
+            faceEngine.extractFaceFeature(imageInfo, faceInfoList.get(0), ExtractType.REGISTER, 0, faceFeature);
+            return faceFeature.getFeatureData();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 人脸识别
+     *
+     * @param file 人脸图片
+     */
+    @Override
+    public Pair<Long, Float> searchFaces(File file) {
+        FaceEngine faceEngine = faceEngineInit.getFaceEngine();
+        byte[] featureData = faceFeature(file);
+        if (featureData != null) {
+            FaceFeature faceFeature = new FaceFeature();
+            faceFeature.setFeatureData(featureData);
+            SearchResult searchResult = new SearchResult();
+            faceEngine.searchFaceFeature(faceFeature, CompareModel.LIFE_PHOTO, searchResult);
+            int userId = searchResult.getFaceFeatureInfo().getSearchId();
+            System.err.println(searchResult.getFaceFeatureInfo().getSearchId());
+            //特征比对
+            FaceSimilar faceSimilar = new FaceSimilar();
+            FaceFeature targetFace = new FaceFeature();
+            targetFace.setFeatureData(searchResult.getFaceFeatureInfo().getFeatureData());
+            faceEngine.compareFaceFeature(faceFeature, targetFace, faceSimilar);
+            return new Pair<>((long) userId, faceSimilar.getScore());
+        } else {
+            return null;
+        }
+    }
+}

+ 93 - 0
doc-biz/src/main/java/com/doc/face/service/init/FaceEngineInit.java

@@ -0,0 +1,93 @@
+package com.doc.face.service.init;
+
+import com.arcsoft.face.EngineConfiguration;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeatureInfo;
+import com.arcsoft.face.FunctionConfiguration;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.DetectOrient;
+import com.doc.system.service.ISysUserExpandService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 聊天消息管理Service业务层处理
+ *
+ * @author wukai
+ * @date 2023-08-24
+ */
+@Component
+@Slf4j
+@Data
+public class FaceEngineInit implements ApplicationListener<ContextRefreshedEvent> {
+    @Value("${face-engine.lib}")
+    private String lib;
+    @Value("${face-engine.sdk-key}")
+    private String appId;
+    @Value("${face-engine.sdk-key}")
+    private String sdkKey;
+    @Value("${face-engine.active-key}")
+    private String activeKey;
+    @Value("${face-engine.active-file}")
+    private String activeFile;
+    private FaceEngine faceEngine;
+    @Resource
+    private ISysUserExpandService expandService;
+
+    private void initEngine() {
+        //人脸识别引擎库存放路径
+        faceEngine = new FaceEngine(lib);
+        //离线激活引擎
+        faceEngine.activeOffline(activeFile);
+        //引擎配置
+        EngineConfiguration engineConfiguration = new EngineConfiguration();
+        engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
+        engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);
+        engineConfiguration.setDetectFaceMaxNum(10);
+        //功能配置
+        FunctionConfiguration functionConfiguration = new FunctionConfiguration();
+        functionConfiguration.setSupportAge(true);
+        functionConfiguration.setSupportFaceDetect(true);
+        functionConfiguration.setSupportFaceRecognition(true);
+        functionConfiguration.setSupportGender(true);
+        functionConfiguration.setSupportLiveness(true);
+        functionConfiguration.setSupportIRLiveness(true);
+        functionConfiguration.setSupportImageQuality(true);
+        functionConfiguration.setSupportMaskDetect(true);
+        functionConfiguration.setSupportUpdateFaceData(true);
+        engineConfiguration.setFunctionConfiguration(functionConfiguration);
+
+        //初始化引擎
+        faceEngine.init(engineConfiguration);
+    }
+
+    /**
+     * 注册人脸信息
+     */
+    private void register() {
+        expandService.selectFaceFeatureList().forEach(expand -> {
+            FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo();
+            faceFeatureInfo.setSearchId(expand.getUserId().intValue());
+            faceFeatureInfo.setFaceTag(expand.getRemark());
+            String feature = expand.getFaceFeature();
+            faceFeatureInfo.setFeatureData(feature.getBytes(StandardCharsets.ISO_8859_1));
+            faceEngine.registerFaceFeature(faceFeatureInfo);
+        });
+    }
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        if (event.getApplicationContext().getParent() == null) {
+            log.info("SpringBoot项目加载后执行人脸识别初始化!");
+            initEngine();
+            register();
+        }
+    }
+}

+ 5 - 0
doc-common/src/main/java/com/doc/common/constant/Constants.java

@@ -187,4 +187,9 @@ public class Constants {
     public static final String[] ALLOW_EDIT = {"doc", "docm", "docx", "dot", "dotm", "dotx", "epub", "fodt", "htm", "html", "mht", "odt", "ott", "pdf", "rtf", "txt", "djvu", "xps", "wps",
             "csv", "fods", "ods", "ots", "xls", "xlsm", "xlsx", "xlt", "xltm", "xltx",
             "fodp", "odp", "otp", "pot", "potm", "potx", "pps", "ppsm", "ppsx", "ppt", "pptm", "pptx"};
+
+    /**
+     * 人脸识别登录
+     */
+    public static final String FACE_LOGIN = "faceLogin";
 }

+ 6 - 47
doc-common/src/main/java/com/doc/common/core/domain/entity/SysUserExpand.java

@@ -1,13 +1,9 @@
 package com.doc.common.core.domain.entity;
 
 import com.baomidou.mybatisplus.annotation.TableId;
-import com.doc.common.annotation.Excel;
 import com.doc.common.core.domain.BaseEntity;
 import com.fasterxml.jackson.annotation.JsonFormat;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
+import lombok.Data;
 
 import java.util.Date;
 
@@ -17,6 +13,7 @@ import java.util.Date;
  * @author wukai
  * @date 2023-10-11
  */
+@Data
 public class SysUserExpand extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
@@ -41,46 +38,8 @@ public class SysUserExpand extends BaseEntity {
      * 登录时间限制
      */
     private String loginTime;
-
-    public void setUserId(Long userId) {
-        this.userId = userId;
-    }
-
-    public Long getUserId() {
-        return userId;
-    }
-
-    public void setLastUpdateTime(Date lastUpdateTime) {
-        this.lastUpdateTime = lastUpdateTime;
-    }
-
-    public Date getLastUpdateTime() {
-        return lastUpdateTime;
-    }
-
-    public void setLoginIp(String loginIp) {
-        this.loginIp = loginIp;
-    }
-
-    public String getLoginIp() {
-        return loginIp;
-    }
-
-    public void setLoginTime(String loginTime) {
-        this.loginTime = loginTime;
-    }
-
-    public String getLoginTime() {
-        return loginTime;
-    }
-
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
-                .append("userId", getUserId())
-                .append("lastUpdateTime", getLastUpdateTime())
-                .append("loginIp", getLoginIp())
-                .append("loginTime", getLoginTime())
-                .toString();
-    }
+    /**
+     * 人脸特征值
+     */
+    private String faceFeature;
 }

+ 1 - 1
doc-common/src/main/java/com/doc/common/utils/encrypt/Sm3Util.java

@@ -35,7 +35,7 @@ public class Sm3Util {
         Random random = new Random();
         int number = random.nextInt(9000) + 1000;
         System.out.println("随机数字: " + number);
-        String message = "1qaz@WSX";
+        String message = "123456";
         String en1 = encrypt(message);
         String en2 = SecurityUtils.encryptPassword(en1);
         System.out.println("SM3 Digest 1: " + en1);

+ 6 - 1
doc-framework/src/main/java/com/doc/framework/config/SecurityConfig.java

@@ -7,6 +7,7 @@ import com.doc.framework.security.handle.LogoutSuccessHandlerImpl;
 import org.springframework.context.annotation.Bean;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -28,6 +29,8 @@ import javax.annotation.Resource;
  */
 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
+    @Resource
+    private AuthenticationProvider authenticationProvider;
     /**
      * 自定义用户认证逻辑
      */
@@ -109,7 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/login", "/register", "/captchaImage").permitAll()
+                .antMatchers("/login", "/register", "/captchaImage", "/face-login").permitAll()
                 // 增加API接口允许匿名访问
                 .antMatchers("/api/**").permitAll()
                 // 增加API接口允许匿名访问
@@ -148,5 +151,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
+        //face登录
+        auth.authenticationProvider(authenticationProvider);
     }
 }

+ 42 - 0
doc-framework/src/main/java/com/doc/framework/web/service/SysLoginService.java

@@ -106,6 +106,48 @@ public class SysLoginService {
     }
 
     /**
+     * 登录验证
+     *
+     * @param username 用户名
+     * @return 结果
+     */
+    public String faceLogin(String username) {
+        // 用户验证
+        Authentication authentication = null;
+        try {
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, Constants.FACE_LOGIN);
+            AuthenticationContextHolder.setContext(authenticationToken);
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager.authenticate(authenticationToken);
+        } catch (Exception e) {
+            if (e instanceof BadCredentialsException) {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            } else {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        } finally {
+            AuthenticationContextHolder.clearContext();
+        }
+        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+        recordLoginInfo(loginUser.getUserId());
+        // 判断是否允许账户多终端同时登录
+        if (!soloLogin) {
+            // 如果用户不允许多终端同时登录,清除缓存信息
+            String userIdKey = Constants.LOGIN_USERID_KEY + loginUser.getUser().getUserId();
+            String userKey = redisCache.getCacheObject(userIdKey);
+            if (StringUtils.isNotEmpty(userKey)) {
+                redisCache.deleteObject(userIdKey);
+                redisCache.deleteObject(userKey);
+            }
+        }
+        // 生成token
+        return tokenService.createToken(loginUser);
+    }
+
+    /**
      * 校验验证码
      *
      * @param username 用户名

+ 5 - 1
doc-framework/src/main/java/com/doc/framework/web/service/SysPasswordService.java

@@ -54,7 +54,11 @@ public class SysPasswordService {
         String password = usernamePasswordAuthenticationToken.getCredentials().toString();
 
         Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
-
+        //扩展人脸识别登录
+        if (Constants.FACE_LOGIN.equals(password)) {
+            clearLoginRecordCache(username);
+            return;
+        }
         if (retryCount == null) {
             retryCount = 0;
         }

+ 15 - 9
doc-system/src/main/java/com/doc/system/mapper/SysUserExpandMapper.java

@@ -7,15 +7,14 @@ import java.util.List;
 
 /**
  * 用户登录限制Mapper接口
- * 
+ *
  * @author wukai
  * @date 2023-10-11
  */
-public interface SysUserExpandMapper extends BaseMapper<SysUserExpand>
-{
+public interface SysUserExpandMapper extends BaseMapper<SysUserExpand> {
     /**
      * 查询用户登录限制
-     * 
+     *
      * @param userId 用户登录限制主键
      * @return 用户登录限制
      */
@@ -23,15 +22,22 @@ public interface SysUserExpandMapper extends BaseMapper<SysUserExpand>
 
     /**
      * 查询用户登录限制列表
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 用户登录限制集合
      */
     public List<SysUserExpand> selectSysUserExpandList(SysUserExpand sysUserExpand);
 
     /**
+     * 查询用户人脸特征
+     *
+     * @return 用户登录限制
+     */
+    public List<SysUserExpand> selectFaceFeatureList();
+
+    /**
      * 新增用户登录限制
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 结果
      */
@@ -39,7 +45,7 @@ public interface SysUserExpandMapper extends BaseMapper<SysUserExpand>
 
     /**
      * 修改用户登录限制
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 结果
      */
@@ -47,7 +53,7 @@ public interface SysUserExpandMapper extends BaseMapper<SysUserExpand>
 
     /**
      * 删除用户登录限制
-     * 
+     *
      * @param userId 用户登录限制主键
      * @return 结果
      */
@@ -55,7 +61,7 @@ public interface SysUserExpandMapper extends BaseMapper<SysUserExpand>
 
     /**
      * 批量删除用户登录限制
-     * 
+     *
      * @param userIds 需要删除的数据主键集合
      * @return 结果
      */

+ 10 - 8
doc-system/src/main/java/com/doc/system/service/ISysUserExpandService.java

@@ -6,15 +6,15 @@ import java.util.List;
 
 /**
  * 用户登录限制Service接口
- * 
+ *
  * @author wukai
  * @date 2023-10-11
  */
-public interface ISysUserExpandService 
+public interface ISysUserExpandService
 {
     /**
      * 查询用户登录限制
-     * 
+     *
      * @param userId 用户登录限制主键
      * @return 用户登录限制
      */
@@ -22,15 +22,17 @@ public interface ISysUserExpandService
 
     /**
      * 查询用户登录限制列表
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 用户登录限制集合
      */
     public List<SysUserExpand> selectSysUserExpandList(SysUserExpand sysUserExpand);
 
+    List<SysUserExpand> selectFaceFeatureList();
+
     /**
      * 新增用户登录限制
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 结果
      */
@@ -38,7 +40,7 @@ public interface ISysUserExpandService
 
     /**
      * 修改用户登录限制
-     * 
+     *
      * @param sysUserExpand 用户登录限制
      * @return 结果
      */
@@ -46,7 +48,7 @@ public interface ISysUserExpandService
 
     /**
      * 批量删除用户登录限制
-     * 
+     *
      * @param userIds 需要删除的用户登录限制主键集合
      * @return 结果
      */
@@ -54,7 +56,7 @@ public interface ISysUserExpandService
 
     /**
      * 删除用户登录限制信息
-     * 
+     *
      * @param userId 用户登录限制主键
      * @return 结果
      */

+ 10 - 0
doc-system/src/main/java/com/doc/system/service/impl/SysUserExpandServiceImpl.java

@@ -42,6 +42,16 @@ public class SysUserExpandServiceImpl implements ISysUserExpandService {
     }
 
     /**
+     * 查询用户人脸特征
+     *
+     * @return 用户登录限制
+     */
+    @Override
+    public List<SysUserExpand> selectFaceFeatureList() {
+        return sysUserExpandMapper.selectFaceFeatureList();
+    }
+
+    /**
      * 新增用户登录限制
      *
      * @param sysUserExpand 用户登录限制

+ 33 - 19
doc-system/src/main/resources/mapper/system/SysUserExpandMapper.xml

@@ -1,34 +1,43 @@
 <?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">
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.doc.system.mapper.SysUserExpandMapper">
-    
+
     <resultMap type="SysUserExpand" id="SysUserExpandResult">
-        <result property="userId"    column="USER_ID"    />
-        <result property="lastUpdateTime"    column="LAST_UPDATE_TIME"    />
-        <result property="loginIp"    column="LOGIN_IP"    />
-        <result property="loginTime"    column="LOGIN_TIME"    />
+        <result property="userId" column="USER_ID"/>
+        <result property="lastUpdateTime" column="LAST_UPDATE_TIME"/>
+        <result property="loginIp" column="LOGIN_IP"/>
+        <result property="loginTime" column="LOGIN_TIME"/>
+        <result property="faceFeature" column="FACE_FEATURE"/>
     </resultMap>
 
     <sql id="selectSysUserExpandVo">
-        select USER_ID, LAST_UPDATE_TIME, LOGIN_IP, LOGIN_TIME from sys_user_expand
+        select USER_ID, LAST_UPDATE_TIME, LOGIN_IP, LOGIN_TIME, FACE_FEATURE
+        from sys_user_expand
     </sql>
 
     <select id="selectSysUserExpandList" parameterType="SysUserExpand" resultMap="SysUserExpandResult">
         <include refid="selectSysUserExpandVo"/>
-        <where>  
-            <if test="lastUpdateTime != null "> and LAST_UPDATE_TIME = #{lastUpdateTime}</if>
-            <if test="loginIp != null  and loginIp != ''"> and LOGIN_IP = #{loginIp}</if>
-            <if test="loginTime != null  and loginTime != ''"> and LOGIN_TIME = #{loginTime}</if>
+        <where>
+            <if test="lastUpdateTime != null ">and LAST_UPDATE_TIME = #{lastUpdateTime}</if>
+            <if test="loginIp != null  and loginIp != ''">and LOGIN_IP = #{loginIp}</if>
+            <if test="loginTime != null  and loginTime != ''">and LOGIN_TIME = #{loginTime}</if>
         </where>
     </select>
-    
+
     <select id="selectSysUserExpandByUserId" parameterType="Long" resultMap="SysUserExpandResult">
         <include refid="selectSysUserExpandVo"/>
         where USER_ID = #{userId}
     </select>
-        
+    <select id="selectFaceFeatureList" resultType="com.doc.common.core.domain.entity.SysUserExpand">
+        select a.USER_ID, a.LAST_UPDATE_TIME, a.LOGIN_IP, a.LOGIN_TIME, a.FACE_FEATURE, b.nick_name remark
+        from sys_user_expand a,
+             sys_user b
+        where a.user_id = b.user_id
+          and a.face_feature is not null
+    </select>
+
     <insert id="insertSysUserExpand" parameterType="SysUserExpand">
         insert into sys_user_expand
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -36,13 +45,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="lastUpdateTime != null">LAST_UPDATE_TIME,</if>
             <if test="loginIp != null">LOGIN_IP,</if>
             <if test="loginTime != null">LOGIN_TIME,</if>
-         </trim>
+            <if test="faceFeature != null">FACE_FEATURE,</if>
+        </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="userId != null">#{userId},</if>
             <if test="lastUpdateTime != null">#{lastUpdateTime},</if>
             <if test="loginIp != null">#{loginIp},</if>
             <if test="loginTime != null">#{loginTime},</if>
-         </trim>
+            <if test="faceFeature != null">#{faceFeature},</if>
+        </trim>
     </insert>
 
     <update id="updateSysUserExpand" parameterType="SysUserExpand">
@@ -51,18 +62,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="lastUpdateTime != null">LAST_UPDATE_TIME = #{lastUpdateTime},</if>
             <if test="loginIp != null">LOGIN_IP = #{loginIp},</if>
             <if test="loginTime != null">LOGIN_TIME = #{loginTime},</if>
+            <if test="faceFeature != null">FACE_FEATURE = #{faceFeature},</if>
         </trim>
         where USER_ID = #{userId}
     </update>
 
     <delete id="deleteSysUserExpandByUserId" parameterType="Long">
-        delete from sys_user_expand where USER_ID = #{userId}
+        delete
+        from sys_user_expand
+        where USER_ID = #{userId}
     </delete>
 
     <delete id="deleteSysUserExpandByUserIds" parameterType="String">
-        delete from sys_user_expand where USER_ID in 
+        delete from sys_user_expand where USER_ID in
         <foreach item="userId" collection="array" open="(" separator="," close=")">
             #{userId}
         </foreach>
     </delete>
-</mapper>
+</mapper>