|
@@ -0,0 +1,478 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import {
|
|
|
+ FACEID_LINENESS_URL,
|
|
|
+ WEBSOCKET_FOR_FACE_ID,
|
|
|
+} from "@/constants/constants";
|
|
|
+import { httpApp } from "@/plugins/axiosApp";
|
|
|
+import { useTimers } from "@/setups/useTimers";
|
|
|
+import { useWebSocket } from "@/setups/useWebSocket";
|
|
|
+import { store } from "@/store/store";
|
|
|
+import { showLogout } from "@/utils/utils";
|
|
|
+import { onUnmounted } from "vue";
|
|
|
+
|
|
|
+const emit = defineEmits<{ (e: "close-faceid"): void }>();
|
|
|
+
|
|
|
+const { addTimeout, addInterval } = useTimers();
|
|
|
+
|
|
|
+let electronDir = "";
|
|
|
+if (typeof window.nodeRequire != "undefined") {
|
|
|
+ const remote: import("electron").Remote =
|
|
|
+ window.nodeRequire("electron").remote;
|
|
|
+ electronDir = "file://" + remote.app.getAppPath() + "/";
|
|
|
+ logger({ cnl: ["server"], pgn: "活体检测弹出框", ext: { electronDir } });
|
|
|
+}
|
|
|
+
|
|
|
+logger({ cnl: ["server", "local"], pgn: "活体检测弹出框", act: "弹出框" });
|
|
|
+
|
|
|
+const { startWS } = useWebSocket();
|
|
|
+startWS(
|
|
|
+ WEBSOCKET_FOR_FACE_ID + `?key=${store.user.key}&token=${store.user.token}`,
|
|
|
+ onFaceIdMessage,
|
|
|
+ "活体检测socket"
|
|
|
+);
|
|
|
+
|
|
|
+// 退出faceid的办法只能二选一。下面是socket返回结果退出。
|
|
|
+let processedBySocketMsg = $ref(false);
|
|
|
+function onFaceIdMessage(event: MessageEvent<string>) {
|
|
|
+ logger({ cnl: ["server"], act: "活体检测FaceId response", dtl: event.data });
|
|
|
+ if (processedByTimeout) {
|
|
|
+ logger({ cnl: ["server"], act: "活体检测websocket消息来迟了,拒绝处理" });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (event.data.indexOf("verifyResult") > -1) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ dtl: "websocket得到verifyResult消息",
|
|
|
+ });
|
|
|
+ processedBySocketMsg = true;
|
|
|
+ try {
|
|
|
+ const receivedMsgData: { content: { returnMsgJson: string } } =
|
|
|
+ JSON.parse(event.data);
|
|
|
+ const receivedMsg: FaceIDMessage = JSON.parse(
|
|
|
+ receivedMsgData.content.returnMsgJson
|
|
|
+ );
|
|
|
+ // 两个结束点。第二个结束点:从websocket得到消息。
|
|
|
+ void faceTestEnd(receivedMsg);
|
|
|
+ } catch (error) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "处理websocket消息时",
|
|
|
+ possibleError: error,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ emit("close-faceid");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+store.exam.isDoingFaceLiveness = true;
|
|
|
+onUnmounted(() => {
|
|
|
+ logger({ cnl: ["server"], pgn: "关闭活体检测弹出框" });
|
|
|
+ clearTimeout(iframeDomReadyTimeout);
|
|
|
+ store.exam.isDoingFaceLiveness = false;
|
|
|
+});
|
|
|
+
|
|
|
+let showIframe = $ref(false);
|
|
|
+let redoBtnDisabled = $ref(true);
|
|
|
+let redoBtnShow = $ref(false);
|
|
|
+let redoBtnMsg = $ref("");
|
|
|
+
|
|
|
+/** 设置重做按钮的文本 */
|
|
|
+function showRedo(redoMsg: string) {
|
|
|
+ showIframe = false;
|
|
|
+ redoBtnDisabled = false;
|
|
|
+ redoBtnShow = true;
|
|
|
+ redoBtnMsg = redoMsg || "系统繁忙,请手动点击重试";
|
|
|
+ logger({ cnl: ["server"], pgn: "活体检测弹出框", act: redoBtnMsg });
|
|
|
+}
|
|
|
+
|
|
|
+async function updateFaceVerifyMsg(errorMsg: string) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "faceid页面报错",
|
|
|
+ dtl: errorMsg,
|
|
|
+ });
|
|
|
+ await httpApp.get(
|
|
|
+ "/api/ecs_oe_student/examFaceLivenessVerify/updateFaceLivenessVerify/" +
|
|
|
+ store.exam.examRecordDataId,
|
|
|
+ { params: { errorMsg: errorMsg } }
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 退出faceid的办法只能二选一。下面是timeout退出。
|
|
|
+let processedByTimeout = false;
|
|
|
+async function faceidLoadedButTimeouted() {
|
|
|
+ if (!processedBySocketMsg) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ act: "faceidLoadedButTimeouted",
|
|
|
+ dtl: "已被websocket处理了",
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ processedByTimeout = true;
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ act: "faceidLoadedButTimeouted",
|
|
|
+ dtl: "进入超时处理",
|
|
|
+ });
|
|
|
+ await httpApp
|
|
|
+ .get(
|
|
|
+ "/api/ecs_oe_student/examFaceLivenessVerify/getFaceVerifyResult/" +
|
|
|
+ faceVerifyId,
|
|
|
+ { "axios-retry": { retries: 4 } }
|
|
|
+ )
|
|
|
+ .then((response) => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "60秒超时非websocket处理结果",
|
|
|
+ });
|
|
|
+ const receivedMsg: FaceIDMessage = response.data;
|
|
|
+ void faceTestEnd(receivedMsg);
|
|
|
+ emit("close-faceid");
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "60秒超时非websocket处理结果--api失败",
|
|
|
+ possibleError: error,
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+type FaceIDMessage = {
|
|
|
+ verifyCount: number;
|
|
|
+ verifyResult: string;
|
|
|
+};
|
|
|
+/** 指定动作检测结束 */
|
|
|
+async function faceTestEnd(receivedMsg: FaceIDMessage) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "指定动作检测结束",
|
|
|
+ ext: {
|
|
|
+ verifyCount: receivedMsg.verifyCount,
|
|
|
+ verifyResult: receivedMsg.verifyResult,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (receivedMsg.verifyCount == 1) {
|
|
|
+ if (receivedMsg.verifyResult == "TIME_OUT") {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ act: "第一次指定动作检测超时,检测失败,系统退出,请重新登录",
|
|
|
+ });
|
|
|
+ showLogout("第一次指定动作检测超时,检测失败,系统退出,请重新登录");
|
|
|
+ } else if (receivedMsg.verifyResult == "VERIFY_FAILED") {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ act: "第一次指定动作检测失败,系统退出,请重新登录",
|
|
|
+ });
|
|
|
+ showLogout("第一次指定动作检测失败,系统退出,请重新登录");
|
|
|
+ } else if (receivedMsg.verifyResult == "NOT_ONESELF") {
|
|
|
+ logger({ cnl: ["server"], act: "指定动作检测不合格,结束考试" });
|
|
|
+ $message.error("指定动作检测不合格,结束考试");
|
|
|
+ return faceTestUploadResult("FAILED");
|
|
|
+ } else if (receivedMsg.verifyResult == "VERIFY_SUCCESS") {
|
|
|
+ logger({ cnl: ["server"], act: "指定动作检测成功,请继续完成考试" });
|
|
|
+ $message.info("指定动作检测成功,请继续完成考试");
|
|
|
+ return faceTestUploadResult("SUCCESS");
|
|
|
+ } else if (receivedMsg.verifyResult == "UNKNOWN") {
|
|
|
+ showLogout("第一次指定动作检测异常(fid),系统退出,请重新登录");
|
|
|
+ }
|
|
|
+ } else if (receivedMsg.verifyCount >= 2) {
|
|
|
+ if (receivedMsg.verifyResult == "VERIFY_SUCCESS") {
|
|
|
+ logger({ cnl: ["server"], act: "指定动作检测成功,请继续完成考试" });
|
|
|
+ $message.info("指定动作检测成功,请继续完成考试");
|
|
|
+ return faceTestUploadResult("SUCCESS");
|
|
|
+ } else {
|
|
|
+ logger({ cnl: ["server"], act: "指定动作检测不合格,结束考试" });
|
|
|
+ $message.error("指定动作检测不合格,结束考试");
|
|
|
+ return faceTestUploadResult("FAILED");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 指定动作检测结果返回后台处理 */
|
|
|
+async function faceTestUploadResult(result: string) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "上传活体检测结果",
|
|
|
+ ext: { result },
|
|
|
+ });
|
|
|
+ return httpApp
|
|
|
+ .get(
|
|
|
+ "/api/ecs_oe_student/examFaceLivenessVerify/faceLivenessVerifyEnd/" +
|
|
|
+ store.exam.examRecordDataId +
|
|
|
+ "?result=" +
|
|
|
+ result,
|
|
|
+ { "axios-retry": { retries: 4 } }
|
|
|
+ )
|
|
|
+ .then(() => {
|
|
|
+ if (result != "SUCCESS") {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "活体检测失败-退出登录",
|
|
|
+ });
|
|
|
+ showLogout("活体检测失败");
|
|
|
+ } else {
|
|
|
+ logger({ cnl: ["server"], pgn: "活体检测弹出框", act: "活体检测成功" });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "上传指定动作检测结果出错!--退出登录",
|
|
|
+ });
|
|
|
+ showLogout("上传指定动作检测结果出错!");
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+let timeCount = $ref(60); //指定动作检测倒计时60秒
|
|
|
+function iframeLoadSuccess() {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "FaceID页面iframe加载成功",
|
|
|
+ });
|
|
|
+ // @ts-expect-error
|
|
|
+ if (!iframeLoadSuccess.loaded) {
|
|
|
+ // @ts-expect-error
|
|
|
+ iframeLoadSuccess.loaded = true;
|
|
|
+ } else {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ key: "不可能的事情发生了",
|
|
|
+ dtl: "iframeLoadSuccess.loaded",
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const timeCountInterval = addInterval(() => {
|
|
|
+ timeCount--;
|
|
|
+ if (timeCount === 0) {
|
|
|
+ clearInterval(timeCountInterval);
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ // 两个结束点。第一个结束点:超时。先传后台,再根据后台信息进行处理。可能ws没有收到处理结果,会通过http接收一遍。
|
|
|
+ //定时事件,如果1分钟内未完成指定动作检测,执行内部程序
|
|
|
+ addTimeout(faceidLoadedButTimeouted, 60 * 1000); //60000
|
|
|
+}
|
|
|
+
|
|
|
+async function startFaceVerifyClicked() {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "点击重试按钮",
|
|
|
+ });
|
|
|
+ await startFaceVerify();
|
|
|
+}
|
|
|
+
|
|
|
+let faceVerifyId: string | null = null;
|
|
|
+let iframeDomReadyTimeout = -1;
|
|
|
+async function startFaceVerify() {
|
|
|
+ redoBtnDisabled = true;
|
|
|
+ redoBtnMsg = "正在进入指定动作检测...";
|
|
|
+
|
|
|
+ let response = null;
|
|
|
+ if (faceVerifyId) {
|
|
|
+ try {
|
|
|
+ response = await httpApp.get(
|
|
|
+ "/api/ecs_oe_student/examFaceLivenessVerify/getFaceVerifyToken/" +
|
|
|
+ faceVerifyId,
|
|
|
+ { "axios-retry": { retries: 4 } }
|
|
|
+ );
|
|
|
+ } catch (error) {
|
|
|
+ showRedo("网络异常,请手动点击重试");
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "网络异常-getFaceVerifyToken",
|
|
|
+ possibleError: error,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ response = await httpApp.get(
|
|
|
+ "/api/ecs_oe_student/examFaceLivenessVerify/startFaceVerify/" +
|
|
|
+ store.exam.examRecordDataId,
|
|
|
+ { "axios-retry": { retries: 4 } }
|
|
|
+ );
|
|
|
+ faceVerifyId = response.data.faceVerifyId;
|
|
|
+ } catch (error) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "获取底照token失败,请重新登录!",
|
|
|
+ possibleError: error,
|
|
|
+ });
|
|
|
+ showLogout("获取底照token失败,请重新登录!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!response.data.success) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "您上传的底照不适合做活体检测,请联系老师!",
|
|
|
+ dtl: JSON.stringify(response.data),
|
|
|
+ });
|
|
|
+ showLogout("您上传的底照不适合做活体检测,请联系老师!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ showIframe = true;
|
|
|
+ const iframe = <HTMLIFrameElement>document.getElementById("myFrame");
|
|
|
+ try {
|
|
|
+ iframe.src = FACEID_LINENESS_URL + response.data.faceLivenessToken;
|
|
|
+ } catch (err) {
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "set iframe.src",
|
|
|
+ key: "不可能的事情发生了",
|
|
|
+ possibleError: err,
|
|
|
+ });
|
|
|
+ showLogout("网络错误,请重试!");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!iframe) return;
|
|
|
+
|
|
|
+ {
|
|
|
+ // iframe 状态管理
|
|
|
+ clearTimeout(iframeDomReadyTimeout);
|
|
|
+
|
|
|
+ // 网络异常,最后的管理
|
|
|
+ iframeDomReadyTimeout = window.setTimeout(() => {
|
|
|
+ if (iframeDomReady === false) {
|
|
|
+ clearTimeout(iframeDomReadyTimeout);
|
|
|
+ showRedo("网络异常,请手动点击重试");
|
|
|
+ logger({ cnl: ["server"], pgn: "活体检测弹出框", act: "网络异常" });
|
|
|
+ }
|
|
|
+ }, 30 * 1000);
|
|
|
+
|
|
|
+ let iframeDomReady = false;
|
|
|
+ iframe.addEventListener("did-start-loading", () => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server", "local", "console"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "loading faceid iframe",
|
|
|
+ });
|
|
|
+ console.log(iframe);
|
|
|
+ });
|
|
|
+
|
|
|
+ iframe.addEventListener("did-fail-load", () => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server", "local", "console"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "failed loading faceid iframe",
|
|
|
+ });
|
|
|
+ clearTimeout(iframeDomReadyTimeout);
|
|
|
+ if (iframe.src.includes(FACEID_LINENESS_URL)) {
|
|
|
+ showRedo("网络异常,请手动点击重试");
|
|
|
+ logger({
|
|
|
+ cnl: ["server"],
|
|
|
+ pgn: "活体检测弹出框",
|
|
|
+ act: "网络异常-加载失败",
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ iframe.addEventListener("dom-ready", () => {
|
|
|
+ iframeDomReady = true;
|
|
|
+ logger({ cnl: ["server", "console"], act: "faceid iframe dom ready" });
|
|
|
+ // TODO: need verify
|
|
|
+ // iframe.insertCSS(".copyright { display: none !important;}");
|
|
|
+ // iframe.openDevTools();
|
|
|
+ });
|
|
|
+
|
|
|
+ iframe.addEventListener("ipc-message", (event: any) => {
|
|
|
+ logger({
|
|
|
+ cnl: ["server", "console"],
|
|
|
+ act: "got ipc-message",
|
|
|
+ dtl: event.channel,
|
|
|
+ });
|
|
|
+ clearTimeout(iframeDomReadyTimeout);
|
|
|
+ const iframeLoadMsg: string = event.channel;
|
|
|
+ if (iframeLoadMsg.indexOf("error_message") > -1) {
|
|
|
+ showRedo("");
|
|
|
+ void updateFaceVerifyMsg(iframeLoadMsg);
|
|
|
+ }
|
|
|
+ if (iframeLoadMsg === "success") {
|
|
|
+ iframeLoadSuccess();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 页面渲染后自动进入流程
|
|
|
+void startFaceVerify();
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <n-modal
|
|
|
+ :show="true"
|
|
|
+ :closable="false"
|
|
|
+ :maskClosable="false"
|
|
|
+ preset="card"
|
|
|
+ style="width: 800px"
|
|
|
+ >
|
|
|
+ <div class="col-md-12 text-center" style="padding: 8px">
|
|
|
+ <div style="font-size: 30px">
|
|
|
+ <span>指定动作检测</span>
|
|
|
+ <span v-if="showIframe">({{ timeCount }})</span>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="showIframe"
|
|
|
+ class="text-center"
|
|
|
+ style="color: red; font-size: 16px"
|
|
|
+ >
|
|
|
+ (注意:请点击下方“开始比对”按钮并在60秒内完成指定动作检测,超时将退出考试)
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ id="faceIdDiv"
|
|
|
+ style="
|
|
|
+ position: relative;
|
|
|
+ height: 710px;
|
|
|
+ background-color: #6e6f72 !important;
|
|
|
+ background-image: radial-gradient(circle at 50% 0, #a9a9a9, #34363c);
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-show="!showIframe"
|
|
|
+ width="100%"
|
|
|
+ height="200px"
|
|
|
+ style="text-align: center; line-height: 100px; margin-top: 5px"
|
|
|
+ >
|
|
|
+ <div style="color: white; font-weight: bold; font-size: 20px">
|
|
|
+ {{ redoBtnMsg }}
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ v-if="redoBtnShow"
|
|
|
+ type="button"
|
|
|
+ class="qm-primary-button"
|
|
|
+ :disabled="redoBtnDisabled"
|
|
|
+ @click="startFaceVerifyClicked"
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <webview
|
|
|
+ v-show="showIframe"
|
|
|
+ id="myFrame"
|
|
|
+ :preload="electronDir + 'manipulateFaceID.js'"
|
|
|
+ style="position: absolute; width: 100%; height: 710px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </n-modal>
|
|
|
+</template>
|