WANG 5 年 前
コミット
8a42de1624
40 ファイル変更2735 行追加13 行削除
  1. 110 0
      examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/ExamCaptureController.java
  2. 109 0
      examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/ExamCaptureQueueController.java
  3. 72 0
      examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/bean/GetExamCaptureResultDomain.java
  4. 72 0
      examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/bean/UpyunDomain.java
  5. 75 0
      examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/provider/ExamCaptureQueueProvider.java
  6. 23 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureCommonService.java
  7. 14 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureQueueFailedService.java
  8. 41 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureQueueService.java
  9. 61 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureService.java
  10. 26 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/FaceCompareService.java
  11. 18 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/FaceLivenessService.java
  12. 20 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/CallType.java
  13. 65 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/CompareFaceSyncInfo.java
  14. 83 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/SaveExamCaptureQueueInfo.java
  15. 51 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/BaiduFaceLivenessWorker.java
  16. 60 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureCommonServiceImpl.java
  17. 144 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureQueueFailedServiceImpl.java
  18. 173 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureQueueServiceImpl.java
  19. 478 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureServiceImpl.java
  20. 183 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FaceCompareServiceImpl.java
  21. 67 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FaceLivenessServiceImpl.java
  22. 49 0
      examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FacePPCompareWorker.java
  23. 31 0
      examcloud-core-oe-face-starter/assembly.xml
  24. 1 0
      examcloud-core-oe-face-starter/shell/start.args
  25. 36 0
      examcloud-core-oe-face-starter/shell/start.sh
  26. 1 0
      examcloud-core-oe-face-starter/shell/start.vmoptions
  27. 18 0
      examcloud-core-oe-face-starter/shell/stop.sh
  28. 77 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/CoreOeStudentFaceApp.java
  29. 129 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ExamCloudResourceManager.java
  30. 53 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ExamCloudWebMvcConfigurer.java
  31. 89 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ProcessBaiduFacelivenessTask.java
  32. 91 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ProcessFaceCompareQueueTask.java
  33. 44 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/SwaggerConfig.java
  34. 49 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/SystemStartup.java
  35. 27 0
      examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/TaskExecutorConfigure.java
  36. 0 13
      examcloud-core-oe-face-starter/src/main/java/org/examcloud/core/oe/face/starter/App.java
  37. 6 0
      examcloud-core-oe-face-starter/src/main/resources/application.properties
  38. 0 0
      examcloud-core-oe-face-starter/src/main/resources/limited.properties
  39. 89 0
      examcloud-core-oe-face-starter/src/main/resources/log4j2.xml
  40. 0 0
      examcloud-core-oe-face-starter/src/main/resources/security.properties

+ 110 - 0
examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/ExamCaptureController.java

@@ -0,0 +1,110 @@
+package cn.com.qmth.examcloud.core.oe.student.face.controller;
+
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+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 cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.UrlUtil;
+import cn.com.qmth.examcloud.core.basic.api.FaceCloudService;
+import cn.com.qmth.examcloud.core.basic.api.request.GetStudentFaceReq;
+import cn.com.qmth.examcloud.core.basic.api.response.GetStudentFaceResp;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.student.face.controller.bean.GetExamCaptureResultDomain;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.web.upyun.UpYunHttpRequest;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+
+/**
+ * @author chenken
+ * @date 2018年9月6日 上午10:14:23
+ * @company QMTH
+ * @description ExamCaptureController.java
+ */
+@Api(tags = "考试抓拍")
+@RestController
+@RequestMapping("${app.api.oe.student.face}/examCaptureQueue")
+public class ExamCaptureController extends ControllerSupport {
+
+    @Autowired
+    private ExamCaptureService examCaptureService;
+
+    @Autowired
+    private FaceCloudService faceCloudService;
+    @Autowired
+    RedisClient redisClient;
+
+//	private static AES aes = new AES();
+
+    @ApiOperation(value = "同步比较人脸:用于进入考试")
+    @PostMapping("/compareFaceSync")
+    public CompareFaceSyncInfo compareFaceSync(@RequestParam String fileUrl, @RequestParam(required = false) String signIdentifier) {
+        User user = getAccessUser();
+        Check.isBlank(fileUrl, "文件Url不能为空");
+        validateUpyunSign(signIdentifier, fileUrl, user.getUserId());
+
+        StudentCacheBean studentCache = CacheHelper.getStudent(user.getUserId());
+        String baseFaceToken = studentCache.getFaceToken();
+        Check.isBlank(baseFaceToken, "学生底照的faceToken为空");
+//		fileUrl = aes.decrypt(fileUrl);
+        fileUrl = UrlUtil.decode(fileUrl);
+        return examCaptureService.compareFaceSyncByFileUrl(user.getUserId(), baseFaceToken, fileUrl);
+    }
+
+    /**
+     * 校验又拍云签名
+     *
+     * @param signIndentifier 签名标识
+     * @param fileUrl         文件路径
+     * @param userId          用户id
+     */
+    private void validateUpyunSign(String signIndentifier, String fileUrl, Long userId) {
+//		if (StringUtils.isBlank(signIndentifier)){
+//			throw new StatusException("300001", "签名标识不能为空");
+//		}
+        if (!StringUtils.isBlank(signIndentifier)) {
+            String upyunSignRedisKey = Constants.EXAM_CAPTURE_PHOTO_UPYUN_SIGN_PREFIX + userId + "_" + signIndentifier;
+            UpYunHttpRequest upYunHttpRequest = redisClient.get(upyunSignRedisKey, UpYunHttpRequest.class);
+            if (upYunHttpRequest == null) {
+                throw new StatusException("300002", "无效的请求,请检查签名标识");
+            }
+            if (!upYunHttpRequest.getAccessUrl().equals(fileUrl)) {
+                throw new StatusException("300003", "文件路径格式不正确");
+            }
+        }
+
+    }
+
+    @ApiOperation(value = "获取抓拍结果")
+    @GetMapping("/getExamCaptureResult")
+    public GetExamCaptureResultDomain getExamCaptureResult(@RequestParam Long examRecordDataId, @RequestParam String fileName) {
+        Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+        Check.isBlank(fileName, "fileName不能为空");
+        ExamCaptureEntity examCaptureEntity = examCaptureService.getExamCaptureResult(examRecordDataId, fileName);
+        GetExamCaptureResultDomain domain = new GetExamCaptureResultDomain();
+        if (examCaptureEntity == null) {
+            domain.setIsCompleted(false);
+        } else {
+            domain.setIsCompleted(true);
+            domain.setIsPass(examCaptureEntity.getIsPass());
+            domain.setIsStranger(examCaptureEntity.getIsStranger());
+            domain.setExamRecordDataId(examRecordDataId);
+            domain.setFileName(fileName);
+        }
+        return domain;
+    }
+}

+ 109 - 0
examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/ExamCaptureQueueController.java

@@ -0,0 +1,109 @@
+package cn.com.qmth.examcloud.core.oe.student.face.controller;
+
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+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 cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.SaveExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.web.upyun.UpYunHttpRequest;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * @author chenken
+ * @date 2018/8/17 15:11
+ * @company QMTH
+ * @description ExamCaptureQueueController
+ */
+@Api(tags = "考试抓拍队列")
+@RestController
+@RequestMapping("${app.api.oe.student.face}/examCaptureQueue")
+public class ExamCaptureQueueController extends ControllerSupport{
+
+	@Autowired
+	private ExamCaptureQueueService examCaptureQueueService;
+	
+	@Autowired
+	private RedisTemplate<String,Object> redisTemplate;
+	@Autowired
+	private RedisClient redisClient;
+	private static final Logger log = LoggerFactory.getLogger(ExamCaptureQueueController.class);
+	
+	@ApiOperation(value = "保存考试抓拍照片队列")
+	@PostMapping("/uploadExamCapture")
+	public String uploadExamCapture(@RequestBody SaveExamCaptureQueueInfo saveExamCaptureQueueInfo){
+		User user = getAccessUser();
+		//不存在考试会话,直接返回
+		if(!redisTemplate.hasKey(Constants.OE_STUDENT_EXAM_SESSION_PREFIX+user.getUserId())){
+			return null;
+		}
+		//参数校验
+		if(saveExamCaptureQueueInfo == null){
+			throw new StatusException("ExamCaptureQueueController-uploadExamCapture-001", "对象不能为空");
+		}
+		if(saveExamCaptureQueueInfo.getExamRecordDataId() == null){
+			throw new StatusException("ExamCaptureQueueController-uploadExamCapture-002", "examRecordDataId不能为空");
+		}
+		if(StringUtils.isBlank(saveExamCaptureQueueInfo.getFileUrl())){
+			throw new StatusException("ExamCaptureQueueController-uploadExamCapture-003", "fileUrl不能为空");
+		}
+		//校验虚拟摄像头格式,必须 是Json数组,如果格式不正确,则置为null
+		if(StringUtils.isNoneBlank(saveExamCaptureQueueInfo.getCameraInfos())) {
+			try {
+				JSONArray jsonArray = new JSONArray(saveExamCaptureQueueInfo.getCameraInfos());
+				if(jsonArray.length()==0) {
+					saveExamCaptureQueueInfo.setCameraInfos(null);
+				}
+			} catch (JSONException e) {
+				saveExamCaptureQueueInfo.setCameraInfos(null);
+				log.error("ExamCaptureQueueController-uploadExamCapture-004:虚拟摄像头信息格式不正确", e);
+			}
+		}
+		validateUpyunSign(saveExamCaptureQueueInfo.getSignIdentifier(),saveExamCaptureQueueInfo.getFileUrl(), user.getUserId());
+		//查看redis中是否存在锁(交卷时如果照片已经处理完后,就会上锁,此时不允许再向库中存照片)
+		if(redisTemplate.hasKey(Constants.EXAM_CAPTURE_PHOTO_LOCK_PREFIX+saveExamCaptureQueueInfo.getExamRecordDataId())){
+			return null;
+		}
+		
+		return examCaptureQueueService.saveExamCaptureQueue(saveExamCaptureQueueInfo, user.getUserId());
+	}
+
+	/**
+	 * 校验又拍云签名
+	 * @param signIndentifier 签名标识
+	 * @param fileUrl 文件路径
+	 * @param userId 用户id
+	 */
+	private void validateUpyunSign(String signIndentifier,String fileUrl, Long userId) {
+//		if (StringUtils.isBlank(signIndentifier)){
+//			throw new StatusException("300001", "签名标识不能为空");
+//		}
+		if (!StringUtils.isBlank(signIndentifier)){
+			String upyunSignRedisKey = Constants.EXAM_CAPTURE_PHOTO_UPYUN_SIGN_PREFIX + userId + "_" + signIndentifier;
+			UpYunHttpRequest upYunHttpRequest = redisClient.get(upyunSignRedisKey, UpYunHttpRequest.class);
+			if (upYunHttpRequest==null){
+				throw new StatusException("300002", "无效的请求,请检查签名标识");
+			}
+			if(!upYunHttpRequest.getAccessUrl().equals(fileUrl)){
+				throw new StatusException("300003", "文件路径格式不正确");
+			}
+		}
+
+	}
+
+
+}

+ 72 - 0
examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/bean/GetExamCaptureResultDomain.java

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.core.oe.student.face.controller.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class GetExamCaptureResultDomain implements JsonSerializable{
+	
+	
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7258224010169602773L;
+	
+	/**
+	 * 是否处理完成
+	 */
+	private Boolean isCompleted;
+
+	private Long examRecordDataId;
+	
+	/**
+	 * 是否通过
+	 */
+	private Boolean isPass;
+	
+	/**
+	 * 是否有陌生人
+	 */
+	private Boolean isStranger;
+	
+	private String fileName;
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Boolean getIsPass() {
+		return isPass;
+	}
+
+	public void setIsPass(Boolean isPass) {
+		this.isPass = isPass;
+	}
+
+	public Boolean getIsStranger() {
+		return isStranger;
+	}
+
+	public void setIsStranger(Boolean isStranger) {
+		this.isStranger = isStranger;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+	public Boolean getIsCompleted() {
+		return isCompleted;
+	}
+
+	public void setIsCompleted(Boolean isCompleted) {
+		this.isCompleted = isCompleted;
+	}
+
+}

+ 72 - 0
examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/controller/bean/UpyunDomain.java

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.core.oe.student.face.controller.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class UpyunDomain implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7181095234349843905L;
+
+
+	private String upyunOperator;
+	
+	private String upyunCred;
+	
+	private String bucket;
+	
+	private String bucketUrl;
+	
+	private String downloadPrefix;
+
+
+	public String getUpyunOperator() {
+		return upyunOperator;
+	}
+
+
+	public void setUpyunOperator(String upyunOperator) {
+		this.upyunOperator = upyunOperator;
+	}
+
+	public String getUpyunCred() {
+		return upyunCred;
+	}
+
+
+	public void setUpyunCred(String upyunCred) {
+		this.upyunCred = upyunCred;
+	}
+
+
+	public String getBucket() {
+		return bucket;
+	}
+
+
+	public void setBucket(String bucket) {
+		this.bucket = bucket;
+	}
+
+
+	public String getBucketUrl() {
+		return bucketUrl;
+	}
+
+
+	public void setBucketUrl(String bucketUrl) {
+		this.bucketUrl = bucketUrl;
+	}
+
+
+	public String getDownloadPrefix() {
+		return downloadPrefix;
+	}
+
+
+	public void setDownloadPrefix(String downloadPrefix) {
+		this.downloadPrefix = downloadPrefix;
+	}
+
+}

+ 75 - 0
examcloud-core-oe-face-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/provider/ExamCaptureQueueProvider.java

@@ -0,0 +1,75 @@
+package cn.com.qmth.examcloud.core.oe.student.face.provider;
+
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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 cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.api.ExamCaptureQueueCloudService;
+import cn.com.qmth.examcloud.core.oe.student.face.api.request.ExamCaptureQueueReq;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureQueueFailedService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CallType;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.ApiOperation;
+
+
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student.face}/examCaptureQueue")
+public class ExamCaptureQueueProvider  extends ControllerSupport implements ExamCaptureQueueCloudService{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -4141691118761857199L;
+
+	@Autowired
+	private ExamCaptureQueueService examCaptureQueueService;
+	
+	@Autowired
+	private ExamCaptureService examCaptureService;
+	
+	@Autowired
+	private ExamCaptureQueueRepo examCaptureQueueRepo;
+	
+	@Autowired
+	private ExamCaptureQueueFailedService examCaptureQueueFailedService;
+
+	@Override
+	@ApiOperation(value = "处理抓拍照片队列")
+	@PostMapping("/processingExamCaptureQueue")
+	public void processingExamCaptureQueue() {
+//		examCaptureQueueService.processingExamCaptureQueue();
+	}
+
+	@Override
+	@ApiOperation(value = "交卷阶段:根据examRecordDataId处理相关抓拍照片")
+	@PostMapping("/disposeExamCapturePhoto")
+	public void disposeExamCapturePhoto(@RequestBody ExamCaptureQueueReq examCaptureQueueReq) {
+//		List<ExamCaptureQueueEntity> examCaptureQueueEntities = examCaptureQueueRepo.findByExamRecordDataId(examCaptureQueueReq.getExamRecordDataId());
+//		for(ExamCaptureQueueEntity examCaptureQueueEntity:examCaptureQueueEntities){
+//			examCaptureService.disposeExamCapture(examCaptureQueueEntity,CallType.SYNC);
+//		}
+	}
+
+	@Override
+	@ApiOperation(value = "将超过XX分钟,还在处理中的状态修改为未处理")
+	@PostMapping("/changeExamCaptureQueueStatus")
+	public void changeExamCaptureQueueStatus() {
+		examCaptureQueueService.changeExamCaptureQueueStatus();
+	}
+	
+	@Override
+	@ApiOperation(value = "考试抓拍队列失败处理")
+	@PostMapping("/examCaptureQueueFailedDispose")
+	public void examCaptureQueueFailedDispose() throws Exception{
+		examCaptureQueueFailedService.examCaptureQueueFailedDispose();
+	}
+}

+ 23 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureCommonService.java

@@ -0,0 +1,23 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CallType;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年12月18日 下午4:35:53
+ * @company 	QMTH
+ * @description ExamCaptureCommonService.java
+ */
+public interface ExamCaptureCommonService {
+
+	/**
+	 * 检查锁是否存在,并做处理
+	 * @param examCaptureQueue
+	 * @param callType
+	 * @return
+	 */
+	public boolean checkLockIsExistsAndDispose(ExamCaptureQueueEntity examCaptureQueue,CallType callType);
+	
+}

+ 14 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureQueueFailedService.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年11月29日 下午12:40:49
+ * @company 	QMTH
+ * @description 抓拍失败处理,例如第三方服务不可用,face++一直超并发,导致照片不能被处理
+ */
+public interface ExamCaptureQueueFailedService {
+	
+	public void examCaptureQueueFailedDispose() throws Exception;
+	
+}

+ 41 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureQueueService.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.SaveExamCaptureQueueInfo;
+
+/**
+ * @author chenken
+ * @date 2018/8/17 14:45
+ * @company QMTH
+ * @description 抓拍队列服务接口
+ */
+public interface ExamCaptureQueueService {
+
+    /**
+     * 保存考试抓拍照片队列
+     * @param examRecordDataId
+     * @param baseFaceToken
+     * @param fileUrl
+     * @param fileName
+     */
+    String saveExamCaptureQueue(SaveExamCaptureQueueInfo saveExamCaptureQueueInfo, Long studentId);
+    /**
+     * 处理照片队列
+     */
+    void processingExamCaptureQueue();
+
+    /**
+     * 将超过XX分钟,还在处理中的状态修改为未处理
+     */
+    void changeExamCaptureQueueStatus();
+    
+    /**
+     * 处理失败时,保存队列信息
+     * @param examCaptureQueueEntity
+     * @param errorMsg
+     */
+    void saveExamCaptureQueueEntityByFailed(ExamCaptureQueueEntity examCaptureQueueEntity,
+                                            String errorMsg, ExamCaptureQueueStatus examCaptureQueueStatus);
+
+}

+ 61 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/ExamCaptureService.java

@@ -0,0 +1,61 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+import org.json.JSONException;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.commons.CommonsMultipartFile;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CallType;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CompareFaceSyncInfo;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月3日 下午4:42:35
+ * @company 	QMTH
+ * @description 考试抓拍服务接口 
+ */
+public interface ExamCaptureService {
+
+    //face++人脸比对失败时,需要将状态置为失败,并清空批次号
+    void disposeFaceCompareFaild(ExamCaptureQueueEntity examCaptureQueue);
+
+	//百度活体检测失败时,需要将状态置为失败,并清空批次号
+	void disposeBaiDuFaceLivenessFaild(ExamCaptureQueueEntity examCaptureQueue);
+
+	void disposeBaiDuFaceLiveness(ExamCaptureQueueEntity examCaptureQueue) throws JSONException;
+
+    @Transactional
+    void saveExamCaptureAndDeleteQueue(ExamCaptureQueueEntity examCaptureQueue);
+
+    /**
+	 * 同步比较人脸:用于进入考试
+	 * @param studentId			学生ID
+	 * @param baseFaceToken		学生底照faceToken
+	 * @param file				抓拍照片 File
+	 * @return
+	 */
+	public CompareFaceSyncInfo compareFaceSyncByFile(Long studentId,String baseFaceToken,CommonsMultipartFile file);
+	
+	/**
+	 * 同步比较人脸:用于进入考试
+	 * @param studentId			学生ID
+	 * @param baseFaceToken		学生底照faceToken
+	 * @param fileUrl				抓拍照片Url
+	 * @return
+	 */
+	public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId,String baseFaceToken,String fileUrl);
+
+	/**
+	 * 获取考试抓拍结果
+	 * @param examRecordDataId
+	 * @return
+	 */
+	public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId,String fileName);
+	/**
+	 * 处理单个考试抓拍照片数据
+	 * @param examCaptureQueueEntity
+	 */
+	void disposeFaceCompare(ExamCaptureQueueEntity examCaptureQueueEntity) throws JSONException;
+}

+ 26 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/FaceCompareService.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+import org.json.JSONObject;
+
+import java.io.File;
+
+/**
+ * @Description 人脸对比接口
+ * @Author lideyin
+ * @Date 2019/8/5 17:37
+ * @Version 1.0
+ */
+public interface FaceCompareService {
+
+	/**
+	 * 通过face++得到人脸比对结果
+	 *
+	 * @param baseFaceToken 第一张人脸照片token
+	 * @param image_url2 第二张人脸照片路径
+	 * @return JSONObject
+	 * @see https://console.faceplusplus.com.cn/documents/4887586
+	 */
+	JSONObject getFaceppCompareResultByUrl(String baseFaceToken, String image_url2);
+
+	JSONObject getFaceppCompareResultByFile(String baseFaceToken, File tempFile);
+}

+ 18 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/FaceLivenessService.java

@@ -0,0 +1,18 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service;
+
+import org.json.JSONObject;
+/**
+ * @Description 百度活体检测结果类
+ * @Author lideyin
+ * @Date 2019/8/5 15:38
+ * @Version 1.0
+ */
+public interface FaceLivenessService {
+
+	/**
+	 * 获取百度活体检测结果
+	 * @param fileUrl 文件路径
+	 * @return JSONObject
+	 */
+	JSONObject getBaiduFaceLivenessResultJson(String fileUrl);
+}

+ 20 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/CallType.java

@@ -0,0 +1,20 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.bean;
+
+public enum CallType {
+
+	/**
+	 * 异步方式
+	 */
+	ASYNC,
+	
+	/**
+	 * 同步方式
+	 */
+	SYNC,
+	
+	/**
+	 * 处理失败照片
+	 */
+	FAILED_DISPOSE;
+	
+}

+ 65 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/CompareFaceSyncInfo.java

@@ -0,0 +1,65 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class CompareFaceSyncInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 802462624028077528L;
+
+	
+	/**
+	 * 人脸比对是否通过
+	 */
+	private Boolean isPass;
+	
+	/**
+	 * 学生ID
+	 */
+	private Long studentId;
+	
+	/**
+	 * 是否有陌生人
+	 */
+	private Boolean isStranger;
+	
+	/**
+	 * 错误信息
+	 */
+	private String errorMsg;
+
+	public Boolean getIsPass() {
+		return isPass;
+	}
+
+	public void setIsPass(Boolean isPass) {
+		this.isPass = isPass;
+	}
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+	public String getErrorMsg() {
+		return errorMsg;
+	}
+
+	public void setErrorMsg(String errorMsg) {
+		this.errorMsg = errorMsg;
+	}
+
+	public Boolean getIsStranger() {
+		return isStranger;
+	}
+
+	public void setIsStranger(Boolean isStranger) {
+		this.isStranger = isStranger;
+	}
+	
+}

+ 83 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/bean/SaveExamCaptureQueueInfo.java

@@ -0,0 +1,83 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class SaveExamCaptureQueueInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7434669407796418900L;
+
+	private Long examRecordDataId;
+	
+	private String fileUrl;
+	/**
+     * 是否存在虚拟摄像头
+     */
+    private Boolean hasVirtualCamera;
+    /**
+     * 摄像头信息  json字符串数组
+     */
+    private String cameraInfos;
+    /**
+     * 其他信息
+     * Json格式
+     * {
+     * 	"":""
+     * }
+     */
+    private String extMsg;
+	@ApiModelProperty("又拍云签名唯一标识")
+	private String signIdentifier;
+
+	public String getSignIdentifier() {
+		return signIdentifier;
+	}
+
+	public void setSignIdentifier(String signIdentifier) {
+		this.signIdentifier = signIdentifier;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public String getFileUrl() {
+		return fileUrl;
+	}
+
+	public void setFileUrl(String fileUrl) {
+		this.fileUrl = fileUrl;
+	}
+
+	public Boolean getHasVirtualCamera() {
+		return hasVirtualCamera;
+	}
+
+	public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+		this.hasVirtualCamera = hasVirtualCamera;
+	}
+
+	public String getCameraInfos() {
+		return cameraInfos;
+	}
+
+	public void setCameraInfos(String cameraInfos) {
+		this.cameraInfos = cameraInfos;
+	}
+
+	public String getExtMsg() {
+		return extMsg;
+	}
+
+	public void setExtMsg(String extMsg) {
+		this.extMsg = extMsg;
+	}
+    
+}

+ 51 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/BaiduFaceLivenessWorker.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.Worker;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.WorkerController;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureService;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 百度活体检测工作线程
+ * 
+ * @author lideyin 20190620
+ */
+public class BaiduFaceLivenessWorker implements Worker {
+	private static final Logger log = LoggerFactory.getLogger(BaiduFaceLivenessWorker.class);
+	private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+	@Override
+	public void process(WorkerController controller, Object element) {
+		ExamCaptureService examCaptureService = SpringContextHolder.getBean(ExamCaptureService.class);
+		ExamCaptureQueueEntity examCaptureQueue = (ExamCaptureQueueEntity) element;
+		try {
+			examCaptureService.disposeBaiDuFaceLiveness(examCaptureQueue);
+		} catch (StatusException e) {
+
+			//异常处理
+			examCaptureQueue.setErrorMsg(e.getDesc());
+			examCaptureService.disposeBaiDuFaceLivenessFaild(examCaptureQueue);
+			if ((e.getCode().equals(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE))) {
+				// 如果超过并发次数,则添加异常次数
+				controller.addConcurrencyWarn();
+			}else {
+				captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 自定义异常 "+e.getDesc(),e);
+			}
+		} catch (Exception e) {
+			//异常处理
+			examCaptureQueue.setErrorMsg(e.getMessage());
+			examCaptureService.disposeBaiDuFaceLivenessFaild(examCaptureQueue);
+			log.error("300001", "处理图片发生异常", e);
+			captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 系统异常 "+e.getMessage(),e);
+		}
+	}
+}

+ 60 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureCommonServiceImpl.java

@@ -0,0 +1,60 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureCommonService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CallType;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年12月18日 下午4:34:46
+ * @company 	QMTH
+ * @description ExamCaptureCommonServiceImpl.java
+ */
+@Service("examCaptureCommonService")
+public class ExamCaptureCommonServiceImpl implements ExamCaptureCommonService {
+
+	private static final Logger log = LoggerFactory.getLogger(ExamCaptureCommonServiceImpl.class);
+	
+	@Autowired
+	private RedisTemplate<String,Object> redisTemplate;
+	
+	@Autowired
+	private ExamCaptureQueueRepo examCaptureQueueRepo;
+	
+	@Override
+	public boolean checkLockIsExistsAndDispose(ExamCaptureQueueEntity examCaptureQueue,CallType callType){
+		if (callType!=CallType.FAILED_DISPOSE)
+		//非失败处理且存在交卷锁
+		if(redisTemplate.hasKey(Constants.EXAM_CAPTURE_PHOTO_LOCK_PREFIX+examCaptureQueue.getExamRecordDataId()) ||
+				!redisTemplate.hasKey(Constants.OE_STUDENT_EXAM_SESSION_PREFIX+examCaptureQueue.getStudentId())){
+			examCaptureQueueRepo.delete(examCaptureQueue);//删除队列中的记录
+			return true;
+		}
+		return false;
+	}
+	
+	private void dispose(ExamCaptureQueueEntity examCaptureQueue){
+		if(examCaptureQueue.getStatus() == ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED){
+			/**
+			 * 存在锁且记录已经失败过,将错误次数设置为4,不在此时处理,交给失败任务处理
+			 */
+			int errorNum = examCaptureQueue.getErrorNum();
+			examCaptureQueue.setErrorNum(errorNum<4?4:errorNum);
+			examCaptureQueueRepo.save(examCaptureQueue);
+		}else{
+			log.info("redis中存在交卷锁或考试会话不存在,抓拍记录被删除:"+examCaptureQueue.getId());
+			examCaptureQueueRepo.delete(examCaptureQueue);//删除队列中的记录
+		}
+	}
+
+}

+ 144 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureQueueFailedServiceImpl.java

@@ -0,0 +1,144 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.api.OeHandleByExamCaptureQueueFailedDisposeService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureQueueFailedService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureService;
+import cn.com.qmth.examcloud.exchange.inner.api.SmsCloudService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chenken
+ * @date 2018年11月29日 下午12:39:43
+ * @company QMTH
+ * @description 抓拍失败处理
+ */
+@Service("examCaptureQueueFailedService")
+public class ExamCaptureQueueFailedServiceImpl implements ExamCaptureQueueFailedService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamCaptureQueueFailedServiceImpl.class);
+
+    @Autowired
+    private ExamCaptureService examCaptureService;
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+
+    @Autowired
+//    private SendSmsCloudService sendSmsCloudService;
+    private SmsCloudService smsCloudService;
+
+    @Autowired
+    private OeHandleByExamCaptureQueueFailedDisposeService oeHandleByExamCaptureQueueFailedDisposeService;
+
+    @Value("${$capture.queue.failed.max.monitor.num}")
+    private Long queueFailedMaxMonitorNum;
+
+//    @Value("${$capture.sms.phones}")
+//    private String captureSmsPhones;
+
+//    @Value("${$capture.sms.sign}")
+//    private String captureSmsSign;
+
+//    @Value("${$capture.sms.template.code}")
+//    private String captureSmsTemplateCode;
+
+    @Override
+    public void examCaptureQueueFailedDispose() throws Exception {
+//        //处理前失败照片数量
+//        String currentDay = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+//
+//        long failedQueueSizeBefore = examCaptureQueueRepo.countFailedExamCaptureQueue(currentDay);
+//        List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo.findFailedExamCaptureQueue();
+//        if (examCaptureQueueList == null || examCaptureQueueList.size() == 0) {
+//            return;
+//        }
+//        Set<Long> examRecordDataIdList = new HashSet<Long>();
+//        for (ExamCaptureQueueEntity examCaptureQueue : examCaptureQueueList) {
+//            examRecordDataIdList.add(examCaptureQueue.getExamRecordDataId());
+//        }
+//        for (Long examRecordDataId : examRecordDataIdList) {
+//            List<ExamCaptureQueueEntity> currentQueueList = examCaptureQueueList.stream().filter((e) -> {
+//                return e.getExamRecordDataId().equals(examRecordDataId);
+//            }).collect(Collectors.toList());
+//            ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId, ExamRecordDataEntity.class);
+//            /**
+//             * 不在考试中 && 有警告未审核
+//             */
+//            if (examRecordData != null
+//                    && examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_ING
+//                    && (examRecordData.getIsWarn() && !examRecordData.getIsAudit())) {
+//                int flag = 0;
+//                for (ExamCaptureQueueEntity examCaptureQueue : currentQueueList) {
+//                    String errorMsg = examCaptureQueue.getErrorMsg();
+//                    //如果处理不了的图片直接跳过,不过任何处理,只有可能误处理的才去处理
+//                    if (CommonUtil.isJson(errorMsg)) {
+//                        JSONObject jsonObject = new JSONObject(errorMsg);
+//                        if (jsonObject.has(Constants.ERROR_MSG)) {
+//                            String errorMsgContent = jsonObject.getString(Constants.ERROR_MSG);
+//                            //这些错误不重新检测,具体错误看:https://console.faceplusplus.com.cn/documents/4887586
+//                            if (errorMsgContent.indexOf("IMAGE_ERROR_UNSUPPORTED_FORMAT") > -1
+//                                    || errorMsgContent.indexOf("INVALID_IMAGE_URL") > -1
+//                                    || errorMsgContent.indexOf("INVALID_IMAGE_SIZE") > -1
+//                                    || errorMsgContent.indexOf("IMAGE_FILE_TOO_LARGE") > -1) {
+//                                continue;//跳过处理不了的照片
+//                            }
+//                        }
+//                    }
+//                    qexamCaptureService.disposeFaceCompare(examCaptureQueue);
+//                    if (examCaptureQueue.getStatus()== ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_COMPLETE){
+//                        examCaptureService.disposeBaiDuFaceLiveness(examCaptureQueue);
+//                    }
+//
+//                    flag++;
+//                }
+//                //表示处理过
+//                if (flag > 0) {
+//                    HandleByExamCaptureQueueFailedDisposeReq req = new HandleByExamCaptureQueueFailedDisposeReq();
+//                    req.setExamRecordDataId(examRecordDataId);
+//                    //如果有重新处理过的图片,则需要重新计算考试记录相关数据。类似于交卷
+//                    oeHandleByExamCaptureQueueFailedDisposeService.handleByExamCaptureQueueFailedDispose(req);
+//                }
+//            }
+//        }
+//        //处理后失败照片数量
+//        long failedQueueSizeAfter = examCaptureQueueRepo.countFailedExamCaptureQueue(currentDay);
+//        //处理后的数量>queueFailedMaxMonitorNum,而且大于处理之前的数量
+//        if (failedQueueSizeAfter > queueFailedMaxMonitorNum && (failedQueueSizeAfter > failedQueueSizeBefore)) {
+//            log.info("本次抓拍照片失败处理,处理前数量:" + failedQueueSizeBefore + ",处理后数量:" + failedQueueSizeAfter);
+//            CaptureFailedAlarmReq req = new CaptureFailedAlarmReq();
+//            req.setTotalCount(Integer.valueOf(failedQueueSizeBefore + ""));
+//            req.setErrorCount(Integer.valueOf(failedQueueSizeAfter + ""));
+//            req.setTemplateCode(captureSmsTemplateCode);
+//            req.setSign(captureSmsSign);
+//            req.setPhone(captureSmsPhones);
+//            SendSmsReq sendSmsReq = new SendSmsReq();
+//            if (StringUtils.isNoneBlank(captureSmsPhones)) {
+//                sendSmsReq.setPhoneList(Arrays.asList(captureSmsPhones.split(",")));
+//                HashMap<String, String> paramMap = new HashMap<>();
+//                paramMap.put("totalCount", failedQueueSizeBefore + "");
+//                paramMap.put("errorCount", failedQueueSizeAfter + "");
+//                sendSmsReq.setParams(paramMap);
+//                sendSmsReq.setSmsAssemblyCode("SMS");
+//                smsCloudService.sendSms(sendSmsReq);
+//            }
+//
+//        }
+    }
+}

+ 173 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureQueueServiceImpl.java

@@ -0,0 +1,173 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.AES;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.UrlUtil;
+import cn.com.qmth.examcloud.core.basic.api.FaceCloudService;
+import cn.com.qmth.examcloud.core.basic.api.request.GetStudentFaceReq;
+import cn.com.qmth.examcloud.core.basic.api.response.GetStudentFaceResp;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.SaveExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * @author chenken
+ * @date 2018/8/17 14:48
+ * @company QMTH
+ * @description 考试抓拍处理队列服务实现
+ */
+@Service("examCaptureQueueService")
+public class ExamCaptureQueueServiceImpl implements ExamCaptureQueueService {
+	
+	private static final Logger log = LoggerFactory.getLogger(ExamCaptureQueueServiceImpl.class);
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+    
+	@Autowired
+	private FaceCloudService faceCloudService;
+    
+    /**
+     * 一次启动的线程个数
+     */
+    @Value("${$capture.thread.num}")
+    private int threadNum; 
+    
+    /**
+     * 一次从数据库取出的最大记录数
+     */
+    @Value("${$capture.queue.limit:200}")
+    private int queueLimit; 
+
+
+    @Override
+    public void processingExamCaptureQueue() {
+//        List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo.findExamCaptureQueuesLimit(queueLimit);
+//        int queueLength = examCaptureQueueList.size();
+//        if(queueLength>0){
+//        	int subSize = queueLength/threadNum == 0?1:queueLength/threadNum;
+//        	for(int i = 0;i<threadNum;i++){
+//        		if(examCaptureQueueList.size() == 0){
+//        			return;
+//        		}
+//    			List<ExamCaptureQueueEntity> subExamCaptureQueueList = examCaptureQueueList.subList(0,subSize);
+//    			examCaptureQueueList = examCaptureQueueList.subList(subSize,examCaptureQueueList.size());
+//    			disposeExamCaptureThread.disposeExamCaptureQueue(subExamCaptureQueueList);
+//    		}
+//        	//循环结束后还剩下的,新增加一个线程处理
+//        	if(examCaptureQueueList.size()>0){
+//        		disposeExamCaptureThread.disposeExamCaptureQueue(examCaptureQueueList);
+//        	}
+//        }
+    }
+
+    @Override
+    public void changeExamCaptureQueueStatus() {
+        long nowTime = System.currentTimeMillis();
+        int queueTimeOut = PropertyHolder.getInt("$capture.process.timeout",5);
+        List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo.findByStatusOrderByCreationTimeAsc(ExamCaptureQueueStatus.PROCESSING);
+        List<ExamCaptureQueueEntity> newExamCaptureQueueList = new ArrayList<ExamCaptureQueueEntity>();
+        for(ExamCaptureQueueEntity examCaptureQueue:examCaptureQueueList){
+            if(nowTime-examCaptureQueue.getCreationTime().getTime()>queueTimeOut*60*1000){
+            	examCaptureQueue.setStatus(ExamCaptureQueueStatus.PENDING);//重置为 待处理
+                newExamCaptureQueueList.add(examCaptureQueue);
+            }
+        }
+        examCaptureQueueRepo.saveAll(newExamCaptureQueueList);
+    }
+
+	@Override
+	public void saveExamCaptureQueueEntityByFailed(ExamCaptureQueueEntity examCaptureQueueEntity,  String errorMsg,ExamCaptureQueueStatus examCaptureQueueStatus) {
+		examCaptureQueueEntity.setErrorMsg(errorMsg);
+		examCaptureQueueEntity.setStatus(examCaptureQueueStatus);
+		examCaptureQueueEntity.setUpdateTime(new Date());
+		int errorNum = examCaptureQueueEntity.getErrorNum()==null?0:examCaptureQueueEntity.getErrorNum();
+		examCaptureQueueEntity.setErrorNum(errorNum+1);
+		examCaptureQueueRepo.save(examCaptureQueueEntity);
+	}
+
+	@Override
+	public String saveExamCaptureQueue(SaveExamCaptureQueueInfo saveExamCaptureQueueInfo,Long studentId) {
+		//查询学生底照faceToken
+		StudentCacheBean studentCache = CacheHelper.getStudent(studentId);
+		String baseFaceToken = studentCache.getFaceToken();
+		Check.isBlank(baseFaceToken, "学生底照的faceToken为空");
+
+//		AES aes = new AES();
+		//解密fileUrl
+//		String fileUrl = aes.decrypt(saveExamCaptureQueueInfo.getFileUrl());
+		String fileUrl = UrlUtil.decode(saveExamCaptureQueueInfo.getFileUrl());
+		if(!StringUtil.isAscString(fileUrl)){
+			log.error("The fileUrl is invalid:"+saveExamCaptureQueueInfo.getFileUrl());
+			throw new StatusException("300001","文件路径格式不正确");
+		}
+		String fileName = fileUrl.substring(fileUrl.lastIndexOf("/")+1, fileUrl.length());
+		
+		ExamCaptureQueueEntity examCaptureQueue = new ExamCaptureQueueEntity();
+		examCaptureQueue.setStudentId(studentId);
+		examCaptureQueue.setFileUrl(fileUrl);
+		examCaptureQueue.setFileName(fileName);
+		examCaptureQueue.setBaseFaceToken(baseFaceToken);
+		examCaptureQueue.setExamRecordDataId(saveExamCaptureQueueInfo.getExamRecordDataId());
+		examCaptureQueue.setHasVirtualCamera(saveExamCaptureQueueInfo.getHasVirtualCamera());
+		examCaptureQueue.setCameraInfos(saveExamCaptureQueueInfo.getCameraInfos());
+		examCaptureQueue.setExtMsg(saveExamCaptureQueueInfo.getExtMsg());
+        examCaptureQueue.setStatus(ExamCaptureQueueStatus.PENDING);
+        examCaptureQueue.setErrorNum(0);
+        examCaptureQueue.setCreationTime(new Date());
+        examCaptureQueueRepo.save(examCaptureQueue);
+        return fileName;
+	}
+
+	public static void main(String[] args) {
+		Map<String, String> maps = new HashMap<String, String>();
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d374bbd39e3f9ea015abee61fa4f76633cc25fc7ed1c78e238504ce96d4ae7387c", "3128451");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d374bbd39e3f9ea015abee61fa4f76633c29837bbd269707405ea488f66da1ac62","3128451");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d374bbd39e3f9ea015abee61fa4f76633c6a82cb34ce4255959c94631810163f50","3128451 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d374bbd39e3f9ea015abee61fa4f76633c9769c30d115e755f3cbc1f9d0a39e736","3128490 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d374bbd39e3f9ea015abee61fa4f76633cf53d05df77c751ff631ed2f2d97e2b6c","3128490 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f0bd747235a4510d179d2e46db7ef1a0e","3128531 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f73c618429fb5902a19e793ab5b1a341f","3128531 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f2145e4f9fd766e07d11784d6eea6708a","3128545 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f5f5045b1a9e92a1a58672a1588112cc4","3128545 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f9cc90589d78a800afa44d9e28f5e0165","3128563 ");
+		maps.put("35d2ccfa922c406f45acc7b1bad1d9edf72391cd2208312906129712f5215f75db9c6f1ab31974bf2a7d590e677655d38781c711ec73032b53bfefd151d9960f56cd9130daba0ba797e74614e3f809d7","3128563 ");
+		
+		Set<String> fileUrls = maps.keySet();
+		Map<String,Integer> countMap = new HashMap<String, Integer>();
+		for(String fileUrl:fileUrls){
+			AES aes = new AES();
+			String url = aes.decrypt(fileUrl.trim());
+			String fileName = url.substring(url.lastIndexOf("/")+1, url.length());
+			String examRecordDataId = maps.get(fileUrl).trim();
+			String sql = "INSERT INTO `ec_oe_exam_capture` (`exam_record_data_id`, `file_name`, `file_url`, `is_stranger`, `is_pass`, `creation_time`, `update_time`,`ext_msg`) "
+						+ " VALUES"
+						+"('"+examRecordDataId+"', '"+fileName+"', '"+url+"', b'0', '1',NOW(),NOW(), '手动修改');";
+			System.out.println(sql);
+			Integer num = countMap.get(examRecordDataId);
+			countMap.put(examRecordDataId, num==null?1:num+1);
+		}
+		
+		Set<String> examRecordDataIdNums = countMap.keySet();
+		for(String examRecordDataId:examRecordDataIdNums){
+			int num = countMap.get(examRecordDataId);
+			String sql = "update ec_oe_exam_record_data set face_total_count = "+num+",face_success_count = "+num+",face_success_percent=100,baidu_face_liveness_success_percent=100 where id = "+examRecordDataId+";";
+			System.out.println(sql);
+		}
+	}
+	
+}

+ 478 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/ExamCaptureServiceImpl.java

@@ -0,0 +1,478 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamProperties;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.face.service.*;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.ExamRecordPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Example;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.commons.CommonsMultipartFile;
+
+import java.io.*;
+
+/**
+ * @author chenken
+ * @date 2018年9月5日 下午3:31:37
+ * @company QMTH
+ * @description 考试抓拍服务实现
+ */
+@SuppressWarnings("ALL")
+@Service("examCaptureService")
+public class ExamCaptureServiceImpl implements ExamCaptureService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamCaptureServiceImpl.class);
+    private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Autowired
+    private ExamCaptureQueueService examCaptureQueueService;
+
+    @Autowired
+    private FaceLivenessService faceLivenessService;
+    @Autowired
+    private FaceCompareService faceCompareService;
+
+
+    @Autowired
+    private ExamCaptureCommonService examCaptureCommonService;
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    public static final String TEMP_FILE_EXP = "face_compare/";
+
+    /**
+     * 对图片进行人脸对比
+     *
+     * @param examCaptureQueue
+     * @param callType         同步或异步
+     */
+    @Override
+    @Transactional
+    public void disposeFaceCompare(ExamCaptureQueueEntity examCaptureQueue) throws JSONException {
+        //将队列记录修改为处理中
+        examCaptureQueueRepo.updateExamCaptureQueueStatusWithProcessing(examCaptureQueue.getId());
+        examCaptureQueue.setFaceCompareStartTime(System.currentTimeMillis());
+
+        //facepp超时最大重试次数
+        int maxRetryTimes = PropertyHolder.getInt("facepp.compare.timeOut.maxRetryTimes", 3);
+        boolean retry = false;
+        JSONObject faceCompareResult;
+        //人脸比对超时次数
+        int faceCompareTimeOutTimes = 0;
+        do {
+            retry = false;
+
+            //仅用于日志时间计算
+            long startTime = System.currentTimeMillis();
+            captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对开始...");
+
+            //调用face++API执行人脸比对,得到返回结果
+            faceCompareResult = faceCompareService.getFaceppCompareResultByUrl(examCaptureQueue.getBaseFaceToken(),
+                    examCaptureQueue.getFileUrl());
+            examCaptureQueue.setFaceCompareResult(faceCompareResult.toString());
+
+            if (faceCompareResult.has(Constants.ERROR_MSG)) {
+                String errMsg = faceCompareResult.getString(Constants.ERROR_MSG);
+
+                //如果API并发次数超过上限,则保存错误信息到队列,并抛出异常,用于协调满载队列线程
+                if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED)) {
+                    examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue,
+                            faceCompareResult.toString(), ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对接口超过最大并发次数");
+
+                    throw new StatusException(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED, "face++ API接口超过最大并发次数");
+                }
+
+                //face++无需重试的错误信息
+                SysPropertyCacheBean objNotRetryErrMsg = CacheHelper.getSysProperty("facePlusPlus.faceCompare.notRetry.errMsg");
+                if (!objNotRetryErrMsg.getHasValue()){
+                    throw new StatusException("100001","未找到face++人脸比对错误消息的相关配置");
+                }
+                String objNotRetryErrMsgs = objNotRetryErrMsg.getValue().toString();
+                String[] notRetryErrMsgsArr = objNotRetryErrMsgs.split(",");
+                for (int i=0;i<notRetryErrMsgsArr.length;i++){
+                    //如果是配置中的无法处理的图片,则保存人脸检测最终结果并删除队列
+                    if (errMsg.contains(notRetryErrMsgsArr[i])){
+                        saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                        captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片,保存人脸检测最终结果并删除队列,errMsg=" + errMsg);
+
+                        return;
+                    }
+                }
+
+                //超时错误特殊处理,重试3次后
+                if (errMsg.contains(Constants.FACE_COMPARE_IMAGE_DOWNLOAD_TIMEOUT)) {
+                    faceCompareTimeOutTimes++;
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超时,将进行第" + faceCompareTimeOutTimes + "次重试");
+
+                    //如果没有达到最大重试次数,则继续重试
+                    if (faceCompareTimeOutTimes < maxRetryTimes) {
+                        retry = true;
+                        continue;
+                    }
+                    //超过最大重试次数,则直接保存最终结果
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
+                    return;
+                }
+
+                captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,即将重试,errMsg:" + errMsg);
+                // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
+                examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue,
+                        faceCompareResult.toString(), ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
+            } else {
+                //face++的结果检测到人脸,才执行百度活体检测
+                if (faceCompareResult.has("confidence")) {
+                    examCaptureQueue.setIsPass(calculateFaceCompareIsPass(faceCompareResult));
+                    examCaptureQueue.setIsStranger(calculateFaceCompareIsStranger(examCaptureQueue.getExamRecordDataId(), faceCompareResult));
+                    //更新队列状态为face++比对完成
+                    examCaptureQueue.setStatus(ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_COMPLETE);
+                    examCaptureQueueRepo.save(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,即将进行百度活体检测,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+                }
+                //face++ 没有检测到人脸,直接保存人脸检测最终结果并删除队列
+                else {
+                    examCaptureQueue.setIsPass(false);
+                    examCaptureQueue.setIsStranger(false);
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,且未检测到人脸,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+                }
+            }
+        } while (retry);
+    }
+
+    @Override
+    //face++人脸比对失败时,需要将状态置为失败,并清空批次号
+    public void disposeFaceCompareFaild(ExamCaptureQueueEntity examCaptureQueue){
+        examCaptureQueue.setStatus(ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
+        examCaptureQueue.setProcessBatchNum("000000");
+        examCaptureQueueRepo.save(examCaptureQueue);
+    }
+
+    @Override
+    //百度活体检测失败时,需要将状态置为失败,并清空批次号
+    public void disposeBaiDuFaceLivenessFaild(ExamCaptureQueueEntity examCaptureQueue){
+        examCaptureQueue.setStatus(ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
+        examCaptureQueue.setProcessBatchNum("000000");
+        examCaptureQueueRepo.save(examCaptureQueue);
+    }
+
+    /**
+     * 对照片进行百度活体检测
+     *
+     * @param examCapture      抓拍照片最终检测最终结果实体
+     * @param examCaptureQueue 抓拍照片队列表
+     */
+    @Override
+    public void disposeBaiDuFaceLiveness(ExamCaptureQueueEntity examCaptureQueue) throws JSONException {
+        //活体检测超时次数
+        int facelivenessTimeOutTimes = 0;
+        //百度活检超时最大重试次数
+        int maxRetryTimes = PropertyHolder.getInt("baidu.faceliveness.timeOut.maxRetryTimes", 3);
+        boolean retry = false;
+        JSONObject faceLivenessResultJson;
+        do {
+            retry=false;
+
+            //仅用于日志时间计算
+            long startTime = System.currentTimeMillis();
+            captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测开始...");
+
+            //获取百度活检结果
+            faceLivenessResultJson = faceLivenessService.getBaiduFaceLivenessResultJson(examCaptureQueue.getFileUrl());
+
+            //如果百度活体检测执行失败,调用队列失败处理,程序退出,失败的数据,后续会有自动服务重新处理
+            if (faceLivenessResultJson.has(Constants.BAIDU_ERROR_CODE) &&
+                    !faceLivenessResultJson.getString(Constants.BAIDU_ERROR_CODE).equals(Constants.BAIDU_SUCCESS_ERROR_CODE_VALUE)) {
+                String errCode = faceLivenessResultJson.getString(Constants.BAIDU_ERROR_CODE);
+
+                //如果API并发次数超过上限,则保存错误信息到队列,并抛出异常,用于协调满载队列线程
+                if (errCode.equals(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE)) {
+                    examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue,
+                            faceLivenessResultJson.toString(), ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度在线活体API接口超过最大并发次数");
+
+                    throw new StatusException(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE, "百度在线活体API接口超过最大并发次数");
+                }
+
+                //百度无需重试的错误信息
+                SysPropertyCacheBean objNotRetryErrMsg = CacheHelper.getSysProperty("baidu.faceLiveness.notRetry.errCode");
+                if (!objNotRetryErrMsg.getHasValue()){
+                    throw new StatusException("100002","未找到百度活体检测错误代码的相关配置");
+                }
+                String objNotRetryErrMsgs =  objNotRetryErrMsg.getValue().toString();
+                String[] notRetryErrMsgsArr = objNotRetryErrMsgs.split(",");
+                for (int i=0;i<notRetryErrMsgsArr.length;i++){
+                    //如果是配置中的无法处理的图片,则保存人脸检测最终结果并删除队列
+                    if (errCode.equals(notRetryErrMsgsArr[i])){
+                        captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测,无法处理的图片,将保存人脸检测最终结果并删除队列,errCode=" + errCode);
+
+                        saveExamCaptureAndDeleteQueue(examCaptureQueue);
+                        return;
+                    }
+                }
+
+                //超时错误特殊处理,重试3次后
+                if (errCode.equals(Constants.BAIDU_FACELIVENESS_CONNECTION_OR_READ_DATA_TIME_OUT_CODE)) {
+                    facelivenessTimeOutTimes++;
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超时,将进行第" + facelivenessTimeOutTimes + "次重试");
+
+                    //如果没有达到最大重试次数,则继续重试
+                    if (facelivenessTimeOutTimes < maxRetryTimes) {
+                        retry = true;
+                        continue;
+                    }
+
+                    //超过最大重试次数,则直接保存最终结果
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
+                    return;
+                }
+
+                captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,即将重试,错误码:" + errCode);
+                // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
+                examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue,
+                        faceLivenessResultJson.toString(), ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
+
+            }
+            //百度活体检测成功,则保存最终检测结果,并删除临时的图片处理队列
+            else {
+                examCaptureQueue.setFacelivenessResult(faceLivenessResultJson.toString());
+                saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测完成,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+            }
+        } while (retry);
+    }
+
+    /**
+     * 保存人脸检测最终结果并删除队列
+     *
+     * @param examCapture 抓拍照片最终检测最终结果实体
+     *                    * @param examCaptureQueue 抓拍照片队列表
+     */
+    @Override
+    @Transactional
+    public void saveExamCaptureAndDeleteQueue(ExamCaptureQueueEntity examCaptureQueue) {
+        ExamCaptureEntity examCapture = getExamCaptureFromQueue(examCaptureQueue);
+
+        //同一考试记录下如果有重复的照片,则直接跳过
+        ExamCaptureEntity query = new ExamCaptureEntity();
+        query.setExamRecordDataId(examCapture.getExamRecordDataId());
+        query.setFileName(examCapture.getFileName());
+        Example<ExamCaptureEntity> example = Example.of(query);
+        //照片处理结果中如果已存在,则以已有的数据为准
+        if (!examCaptureRepo.exists(example)) {
+            examCaptureRepo.save(examCapture);
+        }
+        //删除队列中的记录
+        examCaptureQueueRepo.delete(examCaptureQueue);
+    }
+
+    private ExamCaptureEntity getExamCaptureFromQueue(ExamCaptureQueueEntity queue) {
+        long currentTimeMillis = System.currentTimeMillis();
+        long createTimeMillis = queue.getCreationTime().getTime();
+        long faceCompareStartTimeMillis = queue.getFaceCompareStartTime();
+
+        ExamCaptureEntity resultEntity = new ExamCaptureEntity();
+        resultEntity.setCameraInfos(queue.getCameraInfos());
+        resultEntity.setExamRecordDataId(queue.getExamRecordDataId());
+        resultEntity.setExtMsg(queue.getExtMsg());
+        resultEntity.setFaceCompareResult(queue.getFaceCompareResult());
+        resultEntity.setFacelivenessResult(queue.getFacelivenessResult());
+        resultEntity.setFileName(queue.getFileName());
+        resultEntity.setFileUrl(queue.getFileUrl());
+        resultEntity.setHasVirtualCamera(queue.getHasVirtualCamera());
+        resultEntity.setIsStranger(queue.getIsStranger());
+        resultEntity.setIsPass(queue.getIsPass());
+        resultEntity.setProcessTime(currentTimeMillis - createTimeMillis);//从进队列到处理完毕的时间
+        resultEntity.setUsedTime(currentTimeMillis - faceCompareStartTimeMillis);//从开始处理到处理完毕的时间
+        return resultEntity;
+    }
+
+
+    /**
+     * 同步比较人脸:用于进入考试
+     * 只需要判断是否通过
+     */
+    @Override
+    public CompareFaceSyncInfo compareFaceSyncByFile(Long studentId, String baseFaceToken, CommonsMultipartFile file) {
+        CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
+        compareFaceSyncInfo.setStudentId(studentId);
+        File tempFile = getUploadFile(file);
+
+        try {
+            JSONObject facePPResult = faceCompareService.getFaceppCompareResultByFile(baseFaceToken, tempFile);
+            if (facePPResult.has(Constants.ERROR_MSG)) {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setErrorMsg(facePPResult.toString());
+            } else {
+                if (facePPResult.has("confidence")) {
+                    double confidence = facePPResult.getDouble("confidence");
+                    JSONObject thresholdsJsonObject = facePPResult.getJSONObject("thresholds");
+                    double le4 = thresholdsJsonObject.getDouble("1e-4");
+                    compareFaceSyncInfo.setIsPass(confidence >= le4);//是否通过
+                    compareFaceSyncInfo.setErrorMsg(confidence >= le4 ? null : "confidence<le4");
+                } else {
+                    compareFaceSyncInfo.setIsPass(false);
+                    compareFaceSyncInfo.setErrorMsg(facePPResult.toString());
+                }
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+            throw new StatusException("JsonFormat001", "同步比较人脸失败");
+        }
+
+        tempFile.delete();
+        return compareFaceSyncInfo;
+    }
+
+    @Override
+    public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl) {
+        CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
+        compareFaceSyncInfo.setStudentId(studentId);
+        try {
+            JSONObject facePPResult = faceCompareService.getFaceppCompareResultByUrl(baseFaceToken, fileUrl);
+            if (facePPResult.has(Constants.ERROR_MSG)) {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setErrorMsg(facePPResult.toString());
+            } else {
+                if (facePPResult.has("confidence")) {
+                    double confidence = facePPResult.getDouble("confidence");
+                    JSONObject thresholdsJsonObject = facePPResult.getJSONObject("thresholds");
+                    double le4 = thresholdsJsonObject.getDouble("1e-4");
+                    JSONArray face2Array = facePPResult.getJSONArray("faces2");
+                    compareFaceSyncInfo.setIsStranger(face2Array.length() > 1);//是否有陌生人
+                    compareFaceSyncInfo.setIsPass(confidence >= le4);//是否通过
+                    compareFaceSyncInfo.setErrorMsg(confidence >= le4 ? null : "confidence<le4");
+                } else {
+                    compareFaceSyncInfo.setIsPass(false);
+                    compareFaceSyncInfo.setErrorMsg(facePPResult.toString());
+                }
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+            throw new StatusException("JsonFormat002", "同步比较人脸失败");
+        }
+
+        return compareFaceSyncInfo;
+    }
+
+    private File getUploadFile(MultipartFile file) {
+        //建临时文件夹
+        File dirFile = new File(TEMP_FILE_EXP);
+        if (!dirFile.exists()) {
+            dirFile.mkdirs();
+        }
+        String fileName = file.getOriginalFilename();
+        File tempFile = new File(TEMP_FILE_EXP + fileName);
+        OutputStream os = null;
+        try {
+            os = new FileOutputStream(tempFile);
+            IOUtils.copyLarge(file.getInputStream(), os);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            IOUtils.closeQuietly(os);
+        }
+        return tempFile;
+    }
+
+    @Override
+    public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName) {
+        return examCaptureRepo.findByExamRecordDataIdAndFileName(examRecordDataId, fileName);
+    }
+
+
+    /**
+     * 校验是否有陌生人脸
+     *
+     * @param examCaptureQueue
+     * @param jsonObject
+     * @return
+     * @throws JSONException
+     */
+    private boolean calculateFaceCompareIsStranger(Long examRecordDataId, JSONObject jsonObject) throws JSONException {
+        JSONArray face2Array = jsonObject.getJSONArray("faces2");
+        //添加是否有陌生人开关功能
+        ExamRecordPropertyCacheBean examRecordPropertyCache = CacheHelper.getExamRecordProperty(examRecordDataId);
+        //默认开启了陌生人检测
+        String isStrangerEnableStr = "true";
+        if (examRecordPropertyCache != null) {
+            isStrangerEnableStr = CacheHelper.getExamOrgProperty(examRecordPropertyCache.getExamId(),
+                    examRecordPropertyCache.getOrgId(),ExamProperties.IS_STRANGER_ENABLE.name()).getValue();
+        }
+        boolean isStranger;
+        // 如果开启了陌生人检测才记录陌生人数据,否则认为没有陌生人
+        if (Constants.isTrue.equals(isStrangerEnableStr)) {
+            isStranger = face2Array.length() > 1;//是否有陌生人
+        } else {
+            isStranger = false;
+        }
+        return isStranger;
+    }
+
+    /**
+     * 计算人脸比对是否通过
+     *
+     * @param jsonObject
+     * @return
+     * @throws JSONException
+     */
+    private boolean calculateFaceCompareIsPass(JSONObject jsonObject) throws JSONException {
+        //比对结果置信度,范围 [0,100],小数点后3位有效数字,数字越大表示两个人脸越可能是同一个人。
+        double confidence = jsonObject.getDouble("confidence");
+        //一组用于参考的置信度阈值,包含以下三个字段。每个字段的值为一个 [0,100] 的浮点数,小数点后 3 位有效数字。
+        //1e-3:误识率为千分之一的置信度阈值;
+        //1e-4:误识率为万分之一的置信度阈值;
+        //1e-5:误识率为十万分之一的置信度阈值;
+        JSONObject thresholdsJsonObject = jsonObject.getJSONObject("thresholds");
+        double le4 = thresholdsJsonObject.getDouble("1e-4");
+        //如果置信值低于“千分之一”阈值则不建议认为是同一个人;如果置信值超过“十万分之一”阈值,则是同一个人的几率非常高。
+        return confidence >= le4;
+    }
+
+}

+ 183 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FaceCompareServiceImpl.java

@@ -0,0 +1,183 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.HttpPoolUtil;
+import cn.com.qmth.examcloud.core.oe.student.face.service.FaceCompareService;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.net.URI;
+
+/**
+ * @author chenken
+ * @date 2018年10月30日 下午5:17:28
+ * @company QMTH
+ * @description 百度活体检测执行service
+ */
+@Service("faceCompareService")
+public class FaceCompareServiceImpl implements FaceCompareService {
+    private static final Logger log = LoggerFactory.getLogger(FaceCompareServiceImpl.class);
+    private static URI COMPARE_URI;
+
+    static {
+        COMPARE_URI = URI.create("https://api-cn.faceplusplus.com/facepp/v3/compare");
+    }
+
+    @Value("${$facepp.apiKey}")
+    private String faceppApiKey;
+    @Value("${$facepp.apiSecret}")
+    private String faceppApiSecret;
+
+    @Override
+    public JSONObject getFaceppCompareResultByUrl(String baseFaceToken, String image_url2) {
+        CloseableHttpClient httpClient = HttpPoolUtil.getHttpClient();
+        CloseableHttpResponse httpResponse = null;
+
+        HttpPost httpPost = new HttpPost(getFaceCompareUri());
+        log.debug("[getFaceppCompareResultByUrl] .getHost=" + getFaceCompareUri().getHost());
+        MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
+        multipartEntityBuilder.addTextBody("api_key", faceppApiKey);
+        multipartEntityBuilder.addTextBody("api_secret", faceppApiSecret);
+        multipartEntityBuilder.addTextBody("face_token1", baseFaceToken);
+        multipartEntityBuilder.addTextBody("image_url2", image_url2);
+        HttpEntity httpEntity = multipartEntityBuilder.build();
+
+        httpPost.setEntity(httpEntity);
+        String result = "";
+        try {
+            httpResponse = httpClient.execute(httpPost);
+            HttpEntity responseEntity = httpResponse.getEntity();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent()));
+            StringBuffer buffer = new StringBuffer();
+            String str = "";
+            while (StringUtils.isNoneBlank((str = reader.readLine()))) {
+                buffer.append(str);
+            }
+            result = buffer.toString();
+        } catch (Exception e) {
+            log.error("facepp call failed", e);
+            JSONObject resultJson = new JSONObject();
+            String errorMsg = e.toString();
+            if (errorMsg != null && errorMsg.length() > 500) {
+                errorMsg = errorMsg.substring(0, 500);
+            }
+            try {
+                resultJson.put(Constants.ERROR_MSG, "facepp call failed:" + errorMsg);
+            } catch (JSONException e1) {
+                e1.printStackTrace();
+            }
+            result = resultJson.toString();
+        } finally {
+            try {
+                if (httpResponse != null) {
+                    httpResponse.close();
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        log.debug("[getFaceppCompareResultByUrl] .result=" + result);
+        try {
+            return new JSONObject(result);
+        } catch (JSONException e) {
+            throw new StatusException("300002","人脸比对结果json串对比失败");
+        }
+    }
+
+    /**
+     * 通过face++得到人脸比较结果
+     *
+     * @param baseFaceToken
+     * @param file
+     * @return
+     */
+    @Override
+    public JSONObject getFaceppCompareResultByFile(String baseFaceToken, File file) {
+//		CloseableHttpClient httpClient = HttpClients.createDefault();
+        CloseableHttpClient httpClient = HttpPoolUtil.getHttpClient();
+        CloseableHttpResponse httpResponse = null;
+        HttpPost httpPost = new HttpPost(getFaceCompareUri());
+        MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
+        multipartEntityBuilder.addTextBody("api_key", faceppApiKey);
+        multipartEntityBuilder.addTextBody("api_secret", faceppApiSecret);
+        multipartEntityBuilder.addTextBody("face_token1", baseFaceToken);
+        multipartEntityBuilder.addBinaryBody("image_file2", file, ContentType.IMAGE_JPEG, file.getName());
+        HttpEntity httpEntity = multipartEntityBuilder.build();
+
+        httpPost.setEntity(httpEntity);
+        String result = "";
+        try {
+            httpResponse = httpClient.execute(httpPost);
+            HttpEntity responseEntity = httpResponse.getEntity();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent()));
+            StringBuffer buffer = new StringBuffer();
+            String str = "";
+            while (StringUtils.isNoneBlank((str = reader.readLine()))) {
+                buffer.append(str);
+            }
+            result = buffer.toString();
+        } catch (Exception e) {
+            log.error("facepp call failed", e);
+            JSONObject resultJson = new JSONObject();
+            String errorMsg = e.toString();
+            if (errorMsg != null && errorMsg.length() > 500) {
+                errorMsg = errorMsg.substring(0, 500);
+            }
+            try {
+                resultJson.put(Constants.ERROR_MSG, "facepp call failed:" + errorMsg);
+            } catch (JSONException e1) {
+                e1.printStackTrace();
+            }
+            result = resultJson.toString();
+        } finally {
+            try {
+                if (httpResponse != null) {
+                    httpResponse.close();
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        try {
+            return new JSONObject(result);
+        } catch (JSONException e) {
+            throw new StatusException("300003","人脸比对结果json串对比失败");
+        }
+    }
+
+    /**
+     * 获取人脸比对uri
+     *
+     * @return
+     */
+    private URI getFaceCompareUri() {
+        SysPropertyCacheBean sysProperty = CacheHelper.getSysProperty("facePlusPlus.faceCompare.url");
+        if (sysProperty.getHasValue()) {
+            Object value = sysProperty.getValue();
+            if (null != value) {
+                String url = value.toString();
+                log.debug("[getFaceCompareUri] .sysProperty.getValue()=" + sysProperty.getValue());
+                return URI.create(url);
+            }
+        }
+        throw new StatusException("300004","未找到face++人脸比对的配置路径");
+//        return COMPARE_URI;
+    }
+}

+ 67 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FaceLivenessServiceImpl.java

@@ -0,0 +1,67 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.baiduFaceVerify.BaiduFaceVerifyUtil;
+import cn.com.qmth.examcloud.core.oe.student.face.service.FaceLivenessService;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chenken
+ * @date 2018年10月30日 下午5:17:28
+ * @company QMTH
+ * @description 百度活体检测执行service
+ */
+@Service("faceLivenessService")
+public class FaceLivenessServiceImpl implements FaceLivenessService {
+
+
+    /**
+     * 获取百度在线活体检测
+     *
+     * @param fileUrl 文件路径
+     * @return JSONObject
+     */
+    @Override
+    public JSONObject getBaiduFaceLivenessResultJson(String fileUrl) {
+        JSONObject errorMsgJson = new JSONObject();
+        String accessTokenResult = BaiduFaceVerifyUtil.getAccessToken();
+        //拿不到token
+        if (StringUtils.isBlank(accessTokenResult)) {
+            try {
+                errorMsgJson.put(Constants.BAIDU_ERROR_CODE,"APP100001");
+                errorMsgJson.put(Constants.BAIDU_ERROR_MSG, "baidu_faceliveness_error:getAccessToken failed");
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+            return errorMsgJson;
+        }
+        JSONObject jsonObject;
+        try {
+            jsonObject = new JSONObject(accessTokenResult);
+            //拿不到token
+            if (!jsonObject.has("access_token") || StringUtils.isBlank(jsonObject.getString("access_token"))) {
+                errorMsgJson.put(Constants.BAIDU_ERROR_CODE,"APP100002");
+                errorMsgJson.put(Constants.BAIDU_ERROR_MSG, "baidu_faceliveness_error:invalid accessToken result. accessTokenResult:" + accessTokenResult);
+                return errorMsgJson;
+            }
+            String accessToken = jsonObject.getString("access_token");
+            String faceLivenessResult = BaiduFaceVerifyUtil.faceVerify(accessToken, fileUrl);
+            //拿不到结果
+            if (StringUtils.isBlank(faceLivenessResult)) {
+                errorMsgJson.put(Constants.BAIDU_ERROR_CODE,"APP100003");
+                errorMsgJson.put(Constants.BAIDU_ERROR_MSG, "baidu_faceliveness_error:faceLivess failed");
+                return errorMsgJson;
+            }
+            return new JSONObject(faceLivenessResult);
+        } catch (JSONException e) {
+            e.printStackTrace();
+            throw new StatusException("JsonFormat002", "获取百度在线活体检测失败");
+        }
+
+    }
+
+}

+ 49 - 0
examcloud-core-oe-face-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/service/impl/FacePPCompareWorker.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.core.oe.student.face.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.Worker;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.WorkerController;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.student.face.service.ExamCaptureService;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * face++人脸比对工作线程
+ * 
+ * @author lideyin 20190620
+ */
+public class FacePPCompareWorker implements Worker {
+	private static final Logger log = LoggerFactory.getLogger(FacePPCompareWorker.class);
+	private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+	@Override
+	public void process(WorkerController controller, Object element) {
+		ExamCaptureService examCaptureService = SpringContextHolder
+				.getBean(ExamCaptureService.class);
+		ExamCaptureQueueEntity examCaptureQueueEntity = (ExamCaptureQueueEntity) element;
+		try {
+			examCaptureService.disposeFaceCompare(examCaptureQueueEntity);
+		} catch (StatusException e) {
+			//异常处理
+			examCaptureQueueEntity.setErrorMsg(e.getDesc());
+			examCaptureService.disposeFaceCompareFaild(examCaptureQueueEntity);
+			if ((e.getCode().equals(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED))) {
+				// 如果超过并发次数,则添加异常次数
+				controller.addConcurrencyWarn();
+			}else {
+				captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 自定义异常 "+e.getDesc(),e);
+			}
+		} catch (Exception e) {
+			//异常处理
+			examCaptureQueueEntity.setErrorMsg(e.getMessage());
+			examCaptureService.disposeFaceCompareFaild(examCaptureQueueEntity);
+			log.error("300001", "处理图片发生异常", e);
+			captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 系统异常 "+e.getMessage(),e);
+		}
+	}
+}

+ 31 - 0
examcloud-core-oe-face-starter/assembly.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <id>distribution</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}/src/main/resources</directory>
+            <outputDirectory>/config</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${project.basedir}/shell</directory>
+            <excludes>
+                <exclude>start.args</exclude>
+                <exclude>start.vmoptions</exclude>
+            </excludes>
+            <outputDirectory>/</outputDirectory>
+            <fileMode>0777</fileMode>
+        </fileSet>
+    </fileSets>
+    <dependencySets>
+        <dependencySet>
+            <useProjectArtifact>true</useProjectArtifact>
+            <outputDirectory>lib</outputDirectory>
+            <scope>runtime</scope>
+        </dependencySet>
+    </dependencySets>
+</assembly>

+ 1 - 0
examcloud-core-oe-face-starter/shell/start.args

@@ -0,0 +1 @@
+--spring.profiles.active=test

+ 36 - 0
examcloud-core-oe-face-starter/shell/start.sh

@@ -0,0 +1,36 @@
+#!/bin/bash
+
+APP_MAIN_JAR="examcloud-core-oe-face-starter-2019-SNAPSHOT.jar"
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+
+JAVA_OPTS=`cat $FILE_PATH/start.vmoptions`
+APP_ARGS=`cat $FILE_PATH/start.args`
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "[ERROR] : APP is already running!"
+    exit -1
+fi
+
+if [ "$1" ];then
+    echo "startupCode:"$1;
+else
+    echo "[ERROR] : no arguments"
+    exit -1
+fi
+
+APP_ARGS=$APP_ARGS" --examcloud.startup.startupCode="$1
+
+echo "java options:"
+echo "$JAVA_OPTS"
+echo "args:"
+echo "$APP_ARGS"
+    
+nohup java $JAVA_OPTS -jar $FILE_PATH/lib/$APP_MAIN_JAR $APP_ARGS >/dev/null 2>&1 &
+
+echo "starting......"
+
+exit 0
+

+ 1 - 0
examcloud-core-oe-face-starter/shell/start.vmoptions

@@ -0,0 +1 @@
+-server -Xms1g -Xmx1g

+ 18 - 0
examcloud-core-oe-face-starter/shell/stop.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+APP_MAIN_JAR="examcloud-core-oe-face-starter-2019-SNAPSHOT.jar"
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "Runnable jar is $APP_MAIN_JAR."
+    for PID in $PID_LIST 
+    do
+        kill -9 $PID
+    done
+    echo "stopped !"
+fi
+
+exit 0

+ 77 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/CoreOeStudentFaceApp.java

@@ -0,0 +1,77 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter;
+
+import cn.com.qmth.examcloud.core.oe.student.face.service.FaceLivenessService;
+import cn.com.qmth.examcloud.web.bootstrap.AppBootstrap;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.web.multipart.MultipartResolver;
+import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+
+
+@SpringBootApplication
+@Configuration
+@EnableAutoConfiguration(exclude = {MultipartAutoConfiguration.class})
+@EnableJpaAuditing
+@EnableTransactionManagement
+@EnableEurekaClient
+@EnableDiscoveryClient
+@ComponentScan(basePackages = {"cn.com.qmth"})
+@EntityScan(basePackages = {"cn.com.qmth"})
+@EnableJpaRepositories(basePackages = {"cn.com.qmth"})
+public class CoreOeStudentFaceApp {
+
+	static {
+		String runtimeLevel = System.getProperty("log.commonLevel");
+		if (null == runtimeLevel) {
+			System.setProperty("log.commonLevel", "DEBUG");
+		}
+		System.setProperty("hibernate.dialect.storage_engine", "innodb");
+	}
+	/**
+	 * main
+	 * 
+	 * @param args
+	 * @throws Exception
+	 */
+	public static void main(String[] args) throws Exception {
+		AppBootstrap.run(CoreOeStudentFaceApp.class, args);
+		test();
+
+	}
+
+	private static void test(){
+//		for (int i=0;i<9999;i++){
+//			FaceLivenessService bean = SpringContextHolder.getBean(FaceLivenessService.class);
+//			String url="https://ecs-static.qmth.com.cn/capture_photo/371/238506/1566876134275.png";
+//			System.out.println(bean.getBaiduFaceLivenessResultJson(url));
+//
+//			url="https://ecs-static.qmth.com.cn/capture_photo/371/238506/1566876142935.png";
+//			System.out.println(bean.getBaiduFaceLivenessResultJson(url));
+//
+//			url="https://ecs-static.qmth.com.cn/capture_photo/371/238506/1566876143331.png";
+//			System.out.println(bean.getBaiduFaceLivenessResultJson(url));
+//
+//		}
+	}
+
+	@Bean(name = "multipartResolver")
+	public MultipartResolver multipartResolver() {
+		CommonsMultipartResolver resolver = new CommonsMultipartResolver();
+		resolver.setDefaultEncoding("UTF-8");
+		resolver.setResolveLazily(true);
+		resolver.setMaxInMemorySize(2);
+		resolver.setMaxUploadSize(200 * 1024 * 1024);
+		return resolver;
+	}
+}

+ 129 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ExamCloudResourceManager.java

@@ -0,0 +1,129 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
+import cn.com.qmth.examcloud.api.commons.security.bean.Role;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.api.commons.security.bean.UserType;
+import cn.com.qmth.examcloud.api.commons.security.enums.RoleMeta;
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.util.RegExpUtil;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.AppCacheBean;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.security.ResourceManager;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+
+/**
+ * Demo资源管理器
+ *
+ * @author WANGWEI
+ * @date 2019年2月18日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Component
+public class ExamCloudResourceManager implements ResourceManager {
+
+	@Autowired
+	RedisClient redisClient;
+
+	static {
+		PropertiesUtil.loadFromPath(PathUtil.getResoucePath("security.properties"));
+	}
+
+	@Override
+	public AccessApp getAccessApp(Long appId) {
+		AppCacheBean appCacheBean = CacheHelper.getApp(appId);
+		AccessApp app = new AccessApp();
+		app.setAppId(appCacheBean.getId());
+		app.setAppCode(appCacheBean.getCode());
+		app.setAppName(appCacheBean.getName());
+		app.setSecretKey(appCacheBean.getSecretKey());
+		app.setTimeRange(appCacheBean.getTimeRange());
+		return app;
+	}
+
+	@Override
+	public boolean isNaked(ApiInfo apiInfo, String mapping) {
+		if (null == apiInfo) {
+			return true;
+		}
+
+		if (null != apiInfo) {
+			if (apiInfo.isNaked()) {
+				return true;
+			}
+		}
+		if(mapping.matches(".*swagger.*")) {
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public boolean hasPermission(User user, ApiInfo apiInfo, String mapping) {
+
+		// 学生鉴权
+		if (user.getUserType().equals(UserType.STUDENT)) {
+			return true;
+		}
+
+		List<Role> roleList = user.getRoleList();
+
+		if (CollectionUtils.isEmpty(roleList)) {
+			return false;
+		}
+
+		for (Role role : roleList) {
+			if (role.getRoleCode().equals(RoleMeta.SUPER_ADMIN.name())) {
+				return true;
+			}
+		}
+
+		// 权限组集合
+		String privilegeGroups = PropertiesUtil.getString(mapping);
+		if (StringUtils.isBlank(privilegeGroups)) {
+			return true;
+		}
+
+		// 用户权限集合
+		Set<String> rolePrivilegeList = Sets.newHashSet();
+		Long rootOrgId = user.getRootOrgId();
+		for (Role role : roleList) {
+			String key = "$_P_" + rootOrgId + "_" + role.getRoleId();
+			String rolePrivileges = redisClient.get(key, String.class);
+
+			List<String> rpList = RegExpUtil.findAll(rolePrivileges, "\\w+");
+			rolePrivilegeList.addAll(rpList);
+		}
+
+		List<String> privilegeGroupList = RegExpUtil.findAll(privilegeGroups, "[^\\;]+");
+
+		for (String pg : privilegeGroupList) {
+			pg = pg.trim();
+			if (StringUtils.isBlank(pg)) {
+				continue;
+			}
+
+			List<String> pList = RegExpUtil.findAll(pg, "[^\\,]+");
+			if (rolePrivilegeList.containsAll(pList)) {
+				return true;
+			} else {
+				continue;
+			}
+		}
+
+		return false;
+	}
+
+}

+ 53 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ExamCloudWebMvcConfigurer.java

@@ -0,0 +1,53 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import cn.com.qmth.examcloud.web.interceptor.ApiFlowLimitedInterceptor;
+import cn.com.qmth.examcloud.web.interceptor.ApiStatisticInterceptor;
+import cn.com.qmth.examcloud.web.interceptor.SeqlockInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import cn.com.qmth.examcloud.web.interceptor.FirstInterceptor;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.security.RequestPermissionInterceptor;
+import cn.com.qmth.examcloud.web.security.ResourceManager;
+import cn.com.qmth.examcloud.web.security.RpcInterceptor;
+
+/**
+ * WebMvcConfigurer
+ *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+public class ExamCloudWebMvcConfigurer implements WebMvcConfigurer {
+
+	@Autowired
+	ResourceManager resourceManager;
+
+	@Autowired
+	RedisClient redisClient;
+
+	@Override
+	public void addInterceptors(InterceptorRegistry registry) {
+		registry.addInterceptor(new FirstInterceptor()).addPathPatterns("/**");
+		registry.addInterceptor(new ApiFlowLimitedInterceptor()).addPathPatterns("/**");
+		registry.addInterceptor(new RpcInterceptor(resourceManager)).addPathPatterns("/**");
+		RequestPermissionInterceptor permissionInterceptor = new RequestPermissionInterceptor(
+				resourceManager, redisClient);
+		registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");
+		registry.addInterceptor(new SeqlockInterceptor(redisClient)).addPathPatterns("/**");
+		registry.addInterceptor(new ApiStatisticInterceptor()).addPathPatterns("/**");
+
+	}
+
+	@Override
+	public void addCorsMappings(CorsRegistry registry) {
+		registry.addMapping("/**").allowedOrigins("*").allowCredentials(false).allowedMethods("*")
+				.maxAge(3600);
+	}
+
+}

+ 89 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ProcessBaiduFacelivenessTask.java

@@ -0,0 +1,89 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.ConcurrentTask;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.impl.BaiduFaceLivenessWorker;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 启动百度活体检测任务
+ */
+@Component
+@Order(50)
+public class ProcessBaiduFacelivenessTask implements ApplicationRunner {
+
+    private static final Logger log = LoggerFactory.getLogger(ProcessBaiduFacelivenessTask.class);
+    @Autowired
+    ExamCaptureQueueRepo examCaptureQueueRepo;
+    private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    private void start() {
+        ConcurrentTask concurrentTask = new ConcurrentTask();
+        concurrentTask.setMaxActiveThreadSize(PropertyHolder.getInt("$capture.thread.maxActiveThreadSize", 100));
+        concurrentTask.setMinThreadSize(PropertyHolder.getInt("$capture.thread.minThreadSize", 2));
+        concurrentTask.setWorker(new BaiduFaceLivenessWorker());
+        concurrentTask.start();
+        //当前获取数据的批次号(默认用时间戳)
+        String processBatchNum = Thread.currentThread().getId()+"";
+
+        captureLog.debug("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 启动百度处理服务...");
+        while (true) {
+            try {
+                //如果队列没满,则从数据库中查数据并插入
+                List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo.
+                        findNeedFacelivenessDetectExamCaptureQueuesLimit(PropertyHolder.getInt("$capture.queue.limit", 100), processBatchNum);
+                //如果队列中没取到数据,则2秒钟后再取
+                if (null == examCaptureQueueList || examCaptureQueueList.isEmpty()) {
+                    captureLog.debug("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 抓拍队列中没有取到数据,2秒后重试");
+
+                    Util.sleep(PropertyHolder.getInt("$capture.queue.read.sleepSeconds.", 2));
+                    continue;
+                }
+
+                captureLog.debug("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 抓拍队列中的数据条数为:"+examCaptureQueueList.size());
+
+                for (ExamCaptureQueueEntity offeredQueueEntity : examCaptureQueueList) {
+                    while (true) {
+                        boolean offerSuccess = concurrentTask.offerElement(offeredQueueEntity);
+                        if (offerSuccess) {
+                            offeredQueueEntity.setProcessBatchNum(processBatchNum);
+                            examCaptureQueueRepo.save(offeredQueueEntity);
+
+                            captureLog.debug("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 向工作队列中添加数据成功:fileUrl="+offeredQueueEntity.getFileUrl());
+                            break;
+                        }
+
+                        captureLog.debug("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 向工作队列中添加数据失败,30秒后重试:fileUrl="+offeredQueueEntity.getFileUrl());
+
+                        Util.sleep(PropertyHolder.getInt("$capture.queue.offer.sleepSeconds.", 30));
+                    }
+                }
+            } catch (Exception e) {
+                log.error("300002", "启动百度活体检测队列出现异常", e);
+                captureLog.error("[PROCESS_BAIDUFACELIVENESS."+processBatchNum+"] 百度活体检测出出异常,3秒后重试",e);
+                Util.sleep(3);
+            }
+        }
+    }
+
+    @Override
+    public void run(ApplicationArguments args) {
+        new Thread(()->{
+            start();
+        }).start();
+    }
+
+}

+ 91 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/ProcessFaceCompareQueueTask.java

@@ -0,0 +1,91 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.ConcurrentTask;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.impl.FacePPCompareWorker;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 启动人脸比对任务
+ */
+@Component
+@Order(49)
+public class ProcessFaceCompareQueueTask implements ApplicationRunner {
+
+	@Autowired
+    ExamCaptureQueueRepo examCaptureQueueRepo;
+	private static final Logger log = LoggerFactory.getLogger(ProcessFaceCompareQueueTask.class);
+	private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+	private void start() {
+		ConcurrentTask concurrentTask = new ConcurrentTask();
+		concurrentTask.setMaxActiveThreadSize(PropertyHolder.getInt("$capture.thread.maxActiveThreadSize", 100));
+		concurrentTask.setMinThreadSize(PropertyHolder.getInt("$capture.thread.minThreadSize", 2));
+		concurrentTask.setWorker(new FacePPCompareWorker());
+		concurrentTask.start();
+		//当前获取数据的批次号(默认用时间戳)
+		String processBatchNum=String.valueOf(System.currentTimeMillis());
+
+		captureLog.debug("[PROCESS_FACEPP."+processBatchNum+"] 启动face++人脸比对服务...");
+		while (true) {
+			try {
+				//如果队列没满,则从数据库中查数据并插入
+				List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo.
+						findNeedFaceCompareExamCaptureQueuesLimitByProcessBatchNum(PropertyHolder.getInt("$capture.queue.limit", 100),processBatchNum);
+				//如果队列中没取到数据,则2秒钟后再取
+				if (null == examCaptureQueueList || examCaptureQueueList.isEmpty()) {
+					captureLog.debug("[PROCESS_FACEPP."+processBatchNum+"] 抓拍队列中没有取到数据,2秒后重试");
+
+					Util.sleep(PropertyHolder.getInt("$capture.queue.read.sleepSeconds.", 2));
+					continue;
+				}
+
+				captureLog.debug("[PROCESS_FACEPP."+processBatchNum+"] 抓拍队列中的数据条数为:"+examCaptureQueueList.size());
+
+				for (ExamCaptureQueueEntity offeredQueueEntity : examCaptureQueueList) {
+					while (true) {
+						boolean offerSuccess = concurrentTask.offerElement(offeredQueueEntity);
+						//如果向队列中添加数据成功,则更新标识
+						if (offerSuccess) {
+							offeredQueueEntity.setProcessBatchNum(processBatchNum);
+							examCaptureQueueRepo.save(offeredQueueEntity);
+
+							captureLog.debug("[PROCESS_FACEPP."+processBatchNum+"] 向工作队列中添加数据成功:fileUrl="+offeredQueueEntity.getFileUrl());
+							break;
+						}
+
+						captureLog.debug("[PROCESS_FACEPP."+processBatchNum+"] 向工作队列中添加数据失败,30秒后重试:fileUrl="+offeredQueueEntity.getFileUrl());
+
+						Util.sleep(PropertyHolder.getInt("$capture.queue.offer.sleepSeconds.", 30));
+					}
+				}
+			} catch (Exception e) {
+				log.error("300001","启动图片队列出现异常",e);
+
+				captureLog.error("[PROCESS_FACEPP."+processBatchNum+"] 百度活体检测出出异常,3秒后重试",e);
+				Util.sleep(3);
+			}
+		}
+	}
+
+	@Override
+	public void run(ApplicationArguments args) {
+		new Thread(()->{
+			start();
+		}).start();
+	}
+
+}

+ 44 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/SwaggerConfig.java

@@ -0,0 +1,44 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-15 16:17:41.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import io.swagger.annotations.ApiOperation;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+
+    @Bean
+    public Docket buildDocket() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .groupName("Version 3.0")
+                .apiInfo(buildApiInfo())
+                .useDefaultResponseMessages(false)
+                .select()
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    public ApiInfo buildApiInfo() {
+        return new ApiInfoBuilder()
+                .title("网考学生端照片抓拍处理接口文档")
+                .version("3.0")
+                .build();
+    }
+
+}

+ 49 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/SystemStartup.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * 系统启动
+ *
+ * @author WANGWEI
+ * @date 2018年11月29日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@Order(99)
+public class SystemStartup implements ApplicationRunner {
+
+	@Autowired
+	private DiscoveryClient discoveryClient;
+
+	public void start() {
+
+		String appName = PropertyHolder.getString("spring.application.name");
+
+		if (StringUtils.isNotBlank(appName)) {
+			List<ServiceInstance> instances = discoveryClient.getInstances(appName);
+			if (!instances.isEmpty()) {
+				throw new ExamCloudRuntimeException("multiple instances!");
+			}
+		}
+
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+		start();
+	}
+
+}

+ 27 - 0
examcloud-core-oe-face-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/face/starter/config/TaskExecutorConfigure.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.core.oe.student.face.starter.config;
+
+import java.util.concurrent.Executor;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+@Configuration
+@EnableAsync
+public class TaskExecutorConfigure extends WebMvcConfigurerAdapter{
+
+	 @Bean 
+     public Executor getExecutor() {  
+	      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
+	      executor.setCorePoolSize(30);//线程池创建时候初始化的线程数
+	      executor.setMaxPoolSize(100);//线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
+	      executor.setQueueCapacity(250);//用来缓冲执行任务的队列
+	      executor.setKeepAliveSeconds(10);//允许线程的空闲时间10秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
+	      executor.setThreadNamePrefix("OE-STUDENT-FACE-");
+	      executor.initialize();
+	      return executor;
+     }
+	 
+}

+ 0 - 13
examcloud-core-oe-face-starter/src/main/java/org/examcloud/core/oe/face/starter/App.java

@@ -1,13 +0,0 @@
-package org.examcloud.core.oe.face.starter;
-
-/**
- * Hello world!
- *
- */
-public class App 
-{
-    public static void main( String[] args )
-    {
-        System.out.println( "Hello World!" );
-    }
-}

+ 6 - 0
examcloud-core-oe-face-starter/src/main/resources/application.properties

@@ -0,0 +1,6 @@
+spring.profiles.active=dev
+examcloud.startup.startupCode=3000
+examcloud.startup.configCenterHost=localhost
+#examcloud.startup.configCenterHost=192.168.10.201
+examcloud.startup.configCenterPort=9999
+examcloud.startup.appSimpleName=oe-face

+ 0 - 0
examcloud-core-oe-face-starter/src/main/resources/limited.properties


+ 89 - 0
examcloud-core-oe-face-starter/src/main/resources/log4j2.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" monitorInterval="30">
+
+	<Properties>
+		<Property name="commonLevel" value="${sys:log.commonLevel}" />
+	</Properties>
+
+	<Appenders>
+		<!-- 控制台 日志 -->
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+		</Console>
+		<!-- debug 日志 -->
+		<RollingFile name="DEBUG_APPENDER" fileName="./logs/debug/debug.log" filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+            <DefaultRolloverStrategy max="10000">
+                <Delete basePath="./logs/debug" maxDepth="1">
+                    <IfFileName glob="debug-*.log">
+                        <IfAccumulatedFileSize exceeds="2 GB" />
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 接口日志 -->
+		<RollingFile name="INTERFACE_APPENDER" fileName="./logs/interface/interface.log" filePattern="./logs/interface/interface-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+            <DefaultRolloverStrategy max="10000">
+                <Delete basePath="./logs/interface" maxDepth="1">
+                    <IfFileName glob="interface-*.log">
+                        <IfAccumulatedFileSize exceeds="10 GB" />
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 处理照片 日志 -->
+		<RollingFile name="PROCESS_EXAM_CAPTURE_TASK_APPENDER" fileName="./logs/task/capture.log" filePattern="./logs/task/capture-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+            <DefaultRolloverStrategy max="10000">
+                <Delete basePath="./logs/task" maxDepth="1">
+                    <IfFileName glob="capture-*.log">
+                        <IfAccumulatedFileSize exceeds="2 GB" />
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+		</RollingFile>
+	</Appenders>
+
+	<Loggers>
+		<Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+			<AppenderRef ref="DEBUG_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="INTERFACE_LOGGER" level="INFO" additivity="false">
+			<AppenderRef ref="INTERFACE_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="PROCESS_EXAM_CAPTURE_TASK_LOGGER" level="ERROR" additivity="false">
+			<AppenderRef ref="PROCESS_EXAM_CAPTURE_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="${commonLevel}" />
+		<Logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="${commonLevel}" />
+		<Logger name="org.hibernate.SQL" level="${commonLevel}" />
+		<Logger name="org.hibernate.type" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.QueryParameters" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.query.HQLQueryPlan" level="${commonLevel}" />
+
+		<Root level="INFO">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="DEBUG_APPENDER" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 0 - 0
examcloud-core-oe-face-starter/src/main/resources/security.properties