|
@@ -1,343 +0,0 @@
|
|
|
-package cn.com.qmth.examcloud.core.oe.task.service;
|
|
|
-
|
|
|
-import cn.com.qmth.examcloud.commons.exception.StatusException;
|
|
|
-import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
|
|
|
-import cn.com.qmth.examcloud.core.oe.student.dao.ExamCaptureQueueRepo;
|
|
|
-import cn.com.qmth.examcloud.core.oe.student.dao.enums.ExamCaptureQueueStatus;
|
|
|
-import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
|
|
|
-import cn.com.qmth.examcloud.core.oe.task.service.worker.FaceVerifyCounter;
|
|
|
-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.web.baidu.BaiduClient;
|
|
|
-import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
|
|
|
-import cn.com.qmth.examcloud.web.facepp.FaceppClient;
|
|
|
-import com.alibaba.fastjson.JSONArray;
|
|
|
-import com.alibaba.fastjson.JSONObject;
|
|
|
-import org.json.JSONException;
|
|
|
-import org.slf4j.Logger;
|
|
|
-import org.slf4j.LoggerFactory;
|
|
|
-import org.springframework.beans.factory.annotation.Autowired;
|
|
|
-import org.springframework.stereotype.Component;
|
|
|
-import org.springframework.transaction.annotation.Transactional;
|
|
|
-
|
|
|
-import java.util.Date;
|
|
|
-
|
|
|
-@Component
|
|
|
-public class FaceVerifyService {
|
|
|
-
|
|
|
- private static final Logger log = LoggerFactory.getLogger(FaceVerifyService.class);
|
|
|
-
|
|
|
- @Autowired
|
|
|
- private ExamCaptureQueueRepo examCaptureQueueRepo;
|
|
|
-
|
|
|
- @Autowired
|
|
|
- private ExamCaptureService examCaptureService;
|
|
|
-
|
|
|
- @Autowired
|
|
|
- private ExamCaptureQueueService examCaptureQueueService;
|
|
|
-
|
|
|
- @Autowired
|
|
|
- private ExamRecordDataService examRecordDataService;
|
|
|
-
|
|
|
- /**
|
|
|
- * 处理单个考试抓拍照片数据
|
|
|
- * 对图片进行人脸对比
|
|
|
- */
|
|
|
- @Deprecated
|
|
|
- @Transactional
|
|
|
- public void disposeFaceCompare(ExamCaptureQueueInfo 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();
|
|
|
- log.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());
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片地址,保存人脸检测最终结果并删除队列,errMsg=" + e.getDesc());
|
|
|
- return;
|
|
|
- }
|
|
|
- throw e;
|
|
|
- }
|
|
|
-
|
|
|
- if (log.isDebugEnabled()) {
|
|
|
- log.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)) {
|
|
|
-
|
|
|
- log.error("[FaceCompareQueue] FacePlusPlus_QPS_CONCURRENCY_LIMITED");
|
|
|
-
|
|
|
- examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
|
|
|
- "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceCompareResult.toString(),
|
|
|
- ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
|
|
|
-
|
|
|
- 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])) {
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片,保存人脸检测最终结果并删除队列,errMsg=" + errMsg);
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- //超时错误特殊处理,重试3次后
|
|
|
- if (errMsg.contains(Constants.FACE_COMPARE_IMAGE_DOWNLOAD_TIMEOUT)) {
|
|
|
- faceCompareTimeOutTimes++;
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超时,将进行第" + faceCompareTimeOutTimes + "次重试");
|
|
|
-
|
|
|
- //如果没有达到最大重试次数,则继续重试
|
|
|
- if (faceCompareTimeOutTimes < maxRetryTimes) {
|
|
|
- retry = true;
|
|
|
- continue;
|
|
|
- }
|
|
|
- //超过最大重试次数,则直接保存最终结果
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,即将重试,errMsg:" + errMsg);
|
|
|
- // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
|
|
|
- examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
|
|
|
- "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceCompareResult.toString(),
|
|
|
- ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
|
|
|
- FaceVerifyCounter.increaseFaceCompareFailedCount();//增加错误次数
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,增加错误次数后failedCount:" +
|
|
|
- FaceVerifyCounter.getFaceCompareFailedCount() + ",totalCount=" +
|
|
|
- FaceVerifyCounter.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);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,即将进行百度活体检测,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
|
|
|
- }
|
|
|
- //face++ 没有检测到人脸,直接保存人脸检测最终结果并删除队列
|
|
|
- else {
|
|
|
- examCaptureQueue.setPass(false);
|
|
|
- examCaptureQueue.setStranger(false);
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,且未检测到人脸,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
|
|
|
- }
|
|
|
- }
|
|
|
- } while (retry);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 对照片进行百度活体检测
|
|
|
- *
|
|
|
- * @param examCaptureQueue 抓拍照片队列表
|
|
|
- */
|
|
|
- @Deprecated
|
|
|
- @Transactional
|
|
|
- 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();
|
|
|
- log.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());
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.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)) {
|
|
|
- log.error("[FaceLivenessQueue] Baidu_QPS_CONCURRENCY_LIMITED");
|
|
|
-
|
|
|
- examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
|
|
|
- "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceLivenessResultJson.toString(),
|
|
|
- ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
|
|
|
-
|
|
|
- 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])) {
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测,无法处理的图片,将保存人脸检测最终结果并删除队列,errCode=" + errCode);
|
|
|
-
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- //超时错误特殊处理,重试3次后
|
|
|
- if (errCode.equals(Constants.BAIDU_FACELIVENESS_CONNECTION_OR_READ_DATA_TIME_OUT_CODE)) {
|
|
|
- facelivenessTimeOutTimes++;
|
|
|
-
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超时,将进行第" + facelivenessTimeOutTimes + "次重试");
|
|
|
-
|
|
|
- //如果没有达到最大重试次数,则继续重试
|
|
|
- if (facelivenessTimeOutTimes < maxRetryTimes) {
|
|
|
- retry = true;
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- //超过最大重试次数,则直接保存最终结果
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,即将重试,错误码:" + errCode);
|
|
|
- // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
|
|
|
- examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
|
|
|
- "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceLivenessResultJson.toString(), ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
|
|
|
- FaceVerifyCounter.increaseFaceLivenessDetectFailedCount();//增加错误次数
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,增加错误次数后failedCount:" + FaceVerifyCounter.getFaceLivenessDetectFailedCount());
|
|
|
- }
|
|
|
- //百度活体检测成功,则保存最终检测结果,并删除临时的图片处理队列
|
|
|
- else {
|
|
|
- examCaptureQueue.setFacelivenessResult(faceLivenessResultJson.toString());
|
|
|
- examCaptureService.saveExamCaptureAndDeleteQueue(examCaptureQueue);
|
|
|
-
|
|
|
- log.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测完成,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
|
|
|
- }
|
|
|
- } while (retry);
|
|
|
- }
|
|
|
-
|
|
|
- //人脸比较成功时的处理
|
|
|
- private void disposeFaceCompareSuccessful(ExamCaptureQueueInfo examCaptureQueueInfo) {
|
|
|
- examCaptureQueueRepo.saveExamCaptureQueueEntityBySuccessful(examCaptureQueueInfo.getId(),
|
|
|
- examCaptureQueueInfo.getPass(), examCaptureQueueInfo.getStranger(),
|
|
|
- examCaptureQueueInfo.getStatus().toString(), examCaptureQueueInfo.getFaceCompareResult(),
|
|
|
- examCaptureQueueInfo.getFaceCompareStartTime(), new Date());
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 校验是否有陌生人脸
|
|
|
- */
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 计算人脸比对是否通过
|
|
|
- */
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
-}
|