|
@@ -0,0 +1,671 @@
|
|
|
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+import org.json.JSONException;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.data.domain.Example;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+
|
|
|
+import cn.com.qmth.examcloud.commons.exception.StatusException;
|
|
|
+import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureRepo;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
|
|
|
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
|
|
|
+import cn.com.qmth.examcloud.support.Constants;
|
|
|
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
|
|
|
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
|
|
|
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
|
|
|
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
|
|
|
+import cn.com.qmth.examcloud.support.filestorage.FileStorageUtil;
|
|
|
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
|
|
|
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
|
|
|
+import cn.com.qmth.examcloud.web.baidu.BaiduClient;
|
|
|
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
|
|
|
+import cn.com.qmth.examcloud.web.facepp.FaceppClient;
|
|
|
+import cn.com.qmth.examcloud.web.redis.RedisClient;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @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 ExamRecordDataService examRecordDataService;
|
|
|
+
|
|
|
+ public static final String TEMP_FILE_EXP = "face_compare/";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisClient redisClient;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对图片进行人脸对比
|
|
|
+ *
|
|
|
+ * @param examCaptureQueue
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional
|
|
|
+ public void disposeFaceCompare(ExamCaptureQueueInfo examCaptureQueue) {
|
|
|
+ //将队列记录修改为处理中
|
|
|
+ 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执行人脸比对,得到返回结果
|
|
|
+ JsonHttpResponseHolder jsonHttpResponseHolder = null;
|
|
|
+ try {
|
|
|
+ jsonHttpResponseHolder = FaceppClient.getClient().
|
|
|
+ compareWithTokenAndImageUrl(examCaptureQueue.getBaseFaceToken(),
|
|
|
+ FileStorageUtil.realPath(examCaptureQueue.getFileUrl()),
|
|
|
+ FileStorageUtil.realPathBackup(examCaptureQueue.getFileUrl()));
|
|
|
+
|
|
|
+ faceCompareResult = jsonHttpResponseHolder.getRespBody();
|
|
|
+
|
|
|
+ } catch (StatusException e) {
|
|
|
+ //如果错误码是801,802,803直接结束,不重试
|
|
|
+ if (e.getCode().equals("801") || e.getCode().equals("802") || e.getCode().equals("803")) {
|
|
|
+ examCaptureQueue.setFaceCompareResult(e.getDesc());
|
|
|
+ saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
+
|
|
|
+ captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片地址,保存人脸检测最终结果并删除队列,errMsg=" + e.getDesc());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (captureLog.isDebugEnabled()) {
|
|
|
+ captureLog.debug("[DISPOSE_FACE_COMPARE] 调用face++API执行人脸比对,得到返回结果faceCompareResult:" + faceCompareResult);
|
|
|
+ }
|
|
|
+ examCaptureQueue.setFaceCompareResult(faceCompareResult.toString());
|
|
|
+
|
|
|
+ //人脸比对出错的处理
|
|
|
+ if (faceCompareResult.containsKey(Constants.ERROR_MSG)) {
|
|
|
+ String errMsg = faceCompareResult.getString(Constants.ERROR_MSG);
|
|
|
+
|
|
|
+ //如果API并发次数超过上限,则保存错误信息到队列,并抛出异常,用于协调满载队列线程
|
|
|
+ if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED)) {
|
|
|
+ examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
|
|
|
+ "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + 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.getId(),
|
|
|
+ "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceCompareResult.toString(),
|
|
|
+ ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
|
|
|
+ ExamCaptureProcessStatisticController.increaseFaceCompareFailedCount();//增加错误次数
|
|
|
+ captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,增加错误次数后failedCount:" +
|
|
|
+ ExamCaptureProcessStatisticController.getFaceCompareFailedCount() + ",totalCount=" +
|
|
|
+ ExamCaptureProcessStatisticController.getFaceCompareCount());
|
|
|
+ }
|
|
|
+ //人脸比对没有出错的情况
|
|
|
+ else {
|
|
|
+ //face++的结果检测到人脸,才执行百度活体检测
|
|
|
+ if (faceCompareResult.containsKey("confidence")) {
|
|
|
+ examCaptureQueue.setPass(calculateFaceCompareIsPass(faceCompareResult));
|
|
|
+ examCaptureQueue.setStranger(calculateFaceCompareIsStranger(examCaptureQueue.getExamRecordDataId(),
|
|
|
+ examCaptureQueue.getStudentId(), faceCompareResult));
|
|
|
+ //更新队列状态为face++比对完成
|
|
|
+ examCaptureQueue.setStatus(ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_COMPLETE);
|
|
|
+ disposeFaceCompareSuccessful(examCaptureQueue);
|
|
|
+
|
|
|
+ captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,即将进行百度活体检测,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
|
|
|
+ }
|
|
|
+ //face++ 没有检测到人脸,直接保存人脸检测最终结果并删除队列
|
|
|
+ else {
|
|
|
+ examCaptureQueue.setPass(false);
|
|
|
+ examCaptureQueue.setStranger(false);
|
|
|
+ saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
+
|
|
|
+ captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,且未检测到人脸,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } while (retry);
|
|
|
+ }
|
|
|
+
|
|
|
+ //人脸比较成功时的处理
|
|
|
+ public void disposeFaceCompareSuccessful(ExamCaptureQueueInfo examCaptureQueueInfo) {
|
|
|
+ examCaptureQueueRepo.saveExamCaptureQueueEntityBySuccessful(examCaptureQueueInfo.getId(),
|
|
|
+ examCaptureQueueInfo.getPass(), examCaptureQueueInfo.getStranger(),
|
|
|
+ examCaptureQueueInfo.getStatus().toString(), examCaptureQueueInfo.getFaceCompareResult(),
|
|
|
+ examCaptureQueueInfo.getFaceCompareStartTime(), new Date());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对照片进行百度活体检测
|
|
|
+ *
|
|
|
+ * @param examCapture 抓拍照片最终检测最终结果实体
|
|
|
+ * @param examCaptureQueue 抓拍照片队列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void disposeBaiDuFaceLiveness(ExamCaptureQueueInfo examCaptureQueue) {
|
|
|
+ //活体检测超时次数
|
|
|
+ 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] 百度活体检测开始...");
|
|
|
+
|
|
|
+ //获取百度活检结果
|
|
|
+ JsonHttpResponseHolder jsonHttpResponseHolder = null;
|
|
|
+ try {
|
|
|
+ jsonHttpResponseHolder = BaiduClient.getClient().
|
|
|
+ verifyFaceLiveness(FileStorageUtil.realPath(examCaptureQueue.getFileUrl()),
|
|
|
+ FileStorageUtil.realPathBackup(examCaptureQueue.getFileUrl()));
|
|
|
+ faceLivenessResultJson = jsonHttpResponseHolder.getRespBody();
|
|
|
+ } catch (StatusException e) {
|
|
|
+ //如果错误码是901,902,903直接结束,不重试
|
|
|
+ if (e.getCode().equals("901") || e.getCode().equals("902") || e.getCode().equals("903")) {
|
|
|
+ examCaptureQueue.setFacelivenessResult(e.getDesc());
|
|
|
+ saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
+
|
|
|
+ captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活检无法处理的图片地址,保存人脸检测最终结果并删除队列,errMsg=" + e.getDesc());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+// faceLivenessResultJson = faceLivenessService.getBaiduFaceLivenessResultJson(examCaptureQueue.getFileUrl());
|
|
|
+
|
|
|
+ //如果百度活体检测执行失败,调用队列失败处理,程序退出,失败的数据,后续会有自动服务重新处理
|
|
|
+ if (faceLivenessResultJson.containsKey(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.getId(),
|
|
|
+ "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + 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.getId(),
|
|
|
+ "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceLivenessResultJson.toString(), ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
|
|
|
+ ExamCaptureProcessStatisticController.increaseFaceLivenessDetectFailedCount();//增加错误次数
|
|
|
+ captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,增加错误次数后failedCount:" + ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount());
|
|
|
+ }
|
|
|
+ //百度活体检测成功,则保存最终检测结果,并删除临时的图片处理队列
|
|
|
+ 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(ExamCaptureQueueInfo 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.deleteById(examCaptureQueue.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl) {
|
|
|
+ CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
|
|
|
+ compareFaceSyncInfo.setStudentId(studentId);
|
|
|
+ JSONObject facePPResult = null;
|
|
|
+ JsonHttpResponseHolder jsonHttpResponseHolder = null;
|
|
|
+ try {
|
|
|
+ jsonHttpResponseHolder = FaceppClient.getClient().
|
|
|
+ compareWithTokenAndImageUrl(baseFaceToken,
|
|
|
+ FileStorageUtil.realPath(fileUrl),
|
|
|
+ FileStorageUtil.realPathBackup(fileUrl));
|
|
|
+ facePPResult = jsonHttpResponseHolder.getRespBody();
|
|
|
+ } catch (StatusException e) {
|
|
|
+ //如果错误码是801,802,803直接结束,不重试
|
|
|
+ if (e.getCode().equals("801") || e.getCode().equals("802") || e.getCode().equals("803")) {
|
|
|
+ compareFaceSyncInfo.setIsPass(false);
|
|
|
+ compareFaceSyncInfo.setExistsSystemError(true);
|
|
|
+ compareFaceSyncInfo.setErrorMsg(e.getDesc());
|
|
|
+
|
|
|
+ captureLog.error("[COMPARE_FACE_SYNC] face++人脸比对无法处理的图片地址,errMsg=" + e.getDesc(), e);
|
|
|
+ return compareFaceSyncInfo;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ compareFaceSyncInfo.setIsPass(false);
|
|
|
+ compareFaceSyncInfo.setExistsSystemError(true);
|
|
|
+ compareFaceSyncInfo.setErrorMsg("系统异常");
|
|
|
+
|
|
|
+ captureLog.error("[COMPARE_FACE_SYNC] 未处理的系统异常,errMsg=" + e.getMessage(), e);
|
|
|
+ return compareFaceSyncInfo;
|
|
|
+ }
|
|
|
+ if (facePPResult.containsKey(Constants.ERROR_MSG)) {
|
|
|
+ compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
|
|
|
+ compareFaceSyncInfo.setIsPass(false);
|
|
|
+ String errMsg = facePPResult.getString(Constants.ERROR_MSG);
|
|
|
+ if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED) ||
|
|
|
+ errMsg.contains(Constants.FACE_COMPARE_AUTHORIZATION_ERROR)) {
|
|
|
+ compareFaceSyncInfo.setExistsSystemError(true);
|
|
|
+ }
|
|
|
+ compareFaceSyncInfo.setErrorMsg("facePP called failed : " + facePPResult.toString());
|
|
|
+ return compareFaceSyncInfo;
|
|
|
+ } else {
|
|
|
+ compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
|
|
|
+ if (facePPResult.containsKey("confidence")) {
|
|
|
+ double confidence = facePPResult.getDouble("confidence");
|
|
|
+ JSONObject thresholdsJsonObject = facePPResult.getJSONObject("thresholds");
|
|
|
+ double le4 = thresholdsJsonObject.getDouble("1e-4");
|
|
|
+ JSONArray face2Array = facePPResult.getJSONArray("faces2");
|
|
|
+ boolean hasStranger = face2Array.size() > 1;
|
|
|
+ compareFaceSyncInfo.setIsStranger(hasStranger);//是否有陌生人
|
|
|
+ boolean isPass = confidence >= le4;
|
|
|
+ compareFaceSyncInfo.setIsPass(isPass);//是否通过
|
|
|
+ String errorMsg = null;
|
|
|
+ if (hasStranger && !isPass) {
|
|
|
+ errorMsg = "检测过程中相片非本人,且存在多人脸";
|
|
|
+ } else if (hasStranger) {
|
|
|
+ errorMsg = "检测过程中多人脸失败";
|
|
|
+ } else if (!isPass) {
|
|
|
+ errorMsg = "检测过程中相片非本人";
|
|
|
+ }
|
|
|
+ compareFaceSyncInfo.setErrorMsg(errorMsg);
|
|
|
+ } else {
|
|
|
+ compareFaceSyncInfo.setIsPass(false);
|
|
|
+ compareFaceSyncInfo.setErrorMsg("未检测到人脸");
|
|
|
+ }
|
|
|
+ return compareFaceSyncInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName) {
|
|
|
+ return examCaptureRepo.findByExamRecordDataIdAndFileName(examRecordDataId, fileName);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算人脸检测结果
|
|
|
+ * 相片数=0,系统判断为违纪,自动审核
|
|
|
+ * 考试记录为异常逻辑(进入待审):
|
|
|
+ * 1.陌生人次数>0
|
|
|
+ * 2.face++阈值 = 0 && 百度真实性阈值 > 0
|
|
|
+ * 真实性百分比<百度真实性阈值
|
|
|
+ * 3.face++阈值 > 0 && 百度真实性阈值 = 0
|
|
|
+ * face++成功率<face++阈值
|
|
|
+ * 4.face++阈值 > 0 && 百度真实性阈值 > 0
|
|
|
+ * face++成功率<face++阈值 ||
|
|
|
+ * 真实性百分比<百度真实性阈值
|
|
|
+ *
|
|
|
+ * @param examRecordData
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId) {
|
|
|
+ ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
|
|
|
+
|
|
|
+ Long rootOrgId = examRecordData.getRootOrgId();
|
|
|
+ Long examId = examRecordData.getExamId();
|
|
|
+ Long orgId = examRecordData.getOrgId();
|
|
|
+ Long studentId = examRecordData.getStudentId();
|
|
|
+
|
|
|
+ //人脸最终检测结果
|
|
|
+ CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
|
|
|
+
|
|
|
+ //未开启人脸检测,直接认为正常数据
|
|
|
+ if (!FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
|
|
|
+ resultInfo.setIsWarn(false);
|
|
|
+ return resultInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<ExamCaptureEntity> examCaptureList = examCaptureRepo.findByExamRecordDataId(examRecordData.getId());
|
|
|
+
|
|
|
+ //无照片违纪
|
|
|
+ if (examCaptureList == null || examCaptureList.size() == 0) {
|
|
|
+ resultInfo.setIsWarn(true);//有异常
|
|
|
+ resultInfo.setIsIllegality(true);//违纪
|
|
|
+ resultInfo.setNoPhotoAndIllegality(true);//无照片违纪
|
|
|
+ return resultInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ //根据照片结果计算
|
|
|
+ return calculateByCaptureResult(examCaptureList, examRecordData.getExamId(), examRecordData.getStudentId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算人脸检测数据
|
|
|
+ * 陌生人记录数、成功次数、失败次数、成功率
|
|
|
+ *
|
|
|
+ * @param examCaptureEntityList
|
|
|
+ * @param examId
|
|
|
+ * @param studentId
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private CalculateFaceCheckResultInfo calculateByCaptureResult(List<ExamCaptureEntity> examCaptureEntityList,
|
|
|
+ Long examId, Long studentId) {
|
|
|
+ int strangerCount = 0; // 人脸比较陌生人记录数
|
|
|
+ int succCount = 0; // 人脸比较成功次数
|
|
|
+ int falseCount = 0; // 人脸比较失败次数
|
|
|
+ double succPercent = 0d; // 人脸比较成功率
|
|
|
+ int livenessSuccessCount = 0;//百度活体检测成功次数
|
|
|
+ double livenessSuccessPercent = 0D;//百度活体检测成功率
|
|
|
+ for (ExamCaptureEntity examCaptureEntity : examCaptureEntityList) {
|
|
|
+ if (examCaptureEntity.getIsPass() != null && examCaptureEntity.getIsPass()) {
|
|
|
+ succCount++;
|
|
|
+ } else {
|
|
|
+ falseCount++;
|
|
|
+ }
|
|
|
+ if (examCaptureEntity.getIsStranger() != null && examCaptureEntity.getIsStranger()) {
|
|
|
+ strangerCount++;
|
|
|
+ }
|
|
|
+ livenessSuccessCount += calculateFacelivenessSuccessCount(examCaptureEntity.getFacelivenessResult());
|
|
|
+ }
|
|
|
+ int allNum = examCaptureEntityList.size();
|
|
|
+ BigDecimal bg = new BigDecimal(((double) succCount / allNum) * 100);
|
|
|
+ succPercent = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();// 人脸比较成功率
|
|
|
+
|
|
|
+ //活体检测最终结果
|
|
|
+ CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
|
|
|
+
|
|
|
+ resultInfo.setFaceTotalCount(allNum);//检测总次数
|
|
|
+ resultInfo.setFaceSuccessPercent(succPercent);//成功率
|
|
|
+ resultInfo.setFaceStrangerCount(strangerCount);//有陌生人的次数
|
|
|
+ resultInfo.setFaceSuccessCount(succCount);//成功次数
|
|
|
+ resultInfo.setFaceFailedCount(falseCount);//失败次数
|
|
|
+
|
|
|
+ //计算百度活体检测通过率
|
|
|
+ BigDecimal livenessSuccessBg = new BigDecimal(((double) livenessSuccessCount / allNum) * 100);
|
|
|
+ livenessSuccessPercent = livenessSuccessBg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//百度活体检测成功率
|
|
|
+ resultInfo.setBaiduFaceLivenessSuccessPercent(livenessSuccessPercent);
|
|
|
+
|
|
|
+ //陌生人个数>0
|
|
|
+ if (resultInfo.getFaceStrangerCount() > 0) {
|
|
|
+ resultInfo.setIsWarn(true);
|
|
|
+ return resultInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ //人脸识别阀值
|
|
|
+ String warnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
|
|
|
+ studentId, ExamProperties.WARN_THRESHOLD.name()).getValue();
|
|
|
+ if (StringUtils.isBlank(warnThresholdStr)) {
|
|
|
+ throw new StatusException("400101", "人脸检测预警阈值未设置");
|
|
|
+ }
|
|
|
+
|
|
|
+ //人脸真实性(百度活体检测)通过阀值
|
|
|
+ String liveWarnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
|
|
|
+ studentId, ExamProperties.LIVING_WARN_THRESHOLD.name()).getValue();
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(liveWarnThresholdStr)) {
|
|
|
+ throw new StatusException("400102", "人脸真实性阈值未设置");
|
|
|
+ }
|
|
|
+
|
|
|
+ double warnThreshold = Double.parseDouble(warnThresholdStr);
|
|
|
+ double livenessThreshold = Double.parseDouble(liveWarnThresholdStr);
|
|
|
+
|
|
|
+ if (warnThreshold == 0d && livenessThreshold > 0d) {
|
|
|
+ resultInfo.setIsWarn(livenessSuccessPercent < livenessThreshold);
|
|
|
+ } else if (warnThreshold > 0d && livenessThreshold == 0d) {
|
|
|
+ resultInfo.setIsWarn(succPercent < warnThreshold);
|
|
|
+ } else if (warnThreshold > 0d && livenessThreshold > 0d) {
|
|
|
+ resultInfo.setIsWarn(succPercent < warnThreshold || livenessSuccessPercent < livenessThreshold);
|
|
|
+ }
|
|
|
+
|
|
|
+ return resultInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算百度活体检测成功数量
|
|
|
+ *
|
|
|
+ * @param facelivenessResult
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private int calculateFacelivenessSuccessCount(String facelivenessResult) {
|
|
|
+ if (StringUtils.isNotBlank(facelivenessResult)) {
|
|
|
+ org.json.JSONObject jsonObject;
|
|
|
+ try {
|
|
|
+ jsonObject = new org.json.JSONObject(facelivenessResult);
|
|
|
+ if (jsonObject.has("error_code") && jsonObject.getInt("error_code") == 0 && jsonObject.has("result")) {
|
|
|
+ org.json.JSONObject resultJson = jsonObject.getJSONObject("result");
|
|
|
+ if (resultJson.has("face_liveness")) {
|
|
|
+ double faceLivenessVal = resultJson.getDouble("face_liveness");
|
|
|
+
|
|
|
+ Double baiduFacelivenessThreshold;
|
|
|
+ SysPropertyCacheBean baiduFacelivenessThresholdProperty = CacheHelper.getSysProperty("$baidu.faceliveness.threshold");
|
|
|
+ if (!baiduFacelivenessThresholdProperty.getHasValue()) {
|
|
|
+ baiduFacelivenessThreshold = Constants.DEFAULT_BAIDU_FACELIVENESS_THRESHOLD;
|
|
|
+ } else {
|
|
|
+ baiduFacelivenessThreshold = Double.valueOf(baiduFacelivenessThresholdProperty.getValue().toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (faceLivenessVal > baiduFacelivenessThreshold) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (JSONException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ExamCaptureEntity getExamCaptureFromQueue(ExamCaptureQueueInfo 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.getStranger());
|
|
|
+ resultEntity.setIsPass(queue.getPass());
|
|
|
+ resultEntity.setProcessTime(currentTimeMillis - createTimeMillis);//从进队列到处理完毕的时间
|
|
|
+ resultEntity.setUsedTime(currentTimeMillis - faceCompareStartTimeMillis);//从开始处理到处理完毕的时间
|
|
|
+ return resultEntity;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验是否有陌生人脸
|
|
|
+ *
|
|
|
+ * @param examCaptureQueue
|
|
|
+ * @param jsonObject
|
|
|
+ * @return
|
|
|
+ * @throws JSONException
|
|
|
+ */
|
|
|
+ private boolean calculateFaceCompareIsStranger(Long examRecordDataId, Long studentId, JSONObject jsonObject) {
|
|
|
+ JSONArray face2Array = jsonObject.getJSONArray("faces2");
|
|
|
+ //添加是否有陌生人开关功能
|
|
|
+ ExamRecordData examRecordDataCache = examRecordDataService.getExamRecordDataCache(examRecordDataId);
|
|
|
+ //默认开启了陌生人检测
|
|
|
+ String isStrangerEnableStr = "true";
|
|
|
+ if (examRecordDataCache != null) {
|
|
|
+ isStrangerEnableStr = ExamCacheTransferHelper.getCachedExamProperty(examRecordDataCache.getExamId(),
|
|
|
+ studentId,
|
|
|
+ ExamProperties.IS_STRANGER_ENABLE.name()).getValue();
|
|
|
+ }
|
|
|
+ boolean isStranger;
|
|
|
+ // 如果开启了陌生人检测才记录陌生人数据,否则认为没有陌生人
|
|
|
+ if (Constants.isTrue.equals(isStrangerEnableStr)) {
|
|
|
+ isStranger = face2Array.size() > 1;//是否有陌生人
|
|
|
+ } else {
|
|
|
+ isStranger = false;
|
|
|
+ }
|
|
|
+ return isStranger;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算人脸比对是否通过
|
|
|
+ *
|
|
|
+ * @param jsonObject
|
|
|
+ * @return
|
|
|
+ * @throws JSONException
|
|
|
+ */
|
|
|
+ private boolean calculateFaceCompareIsPass(JSONObject jsonObject) {
|
|
|
+ //比对结果置信度,范围 [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;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|