Jelajahi Sumber

feat: 空白卷检查以及特殊检查调整

zhangjie 8 bulan lalu
induk
melakukan
ca639abd81

TEMPAT SAMPAH
extra/checkBlack/CheckBlack.exe


+ 5 - 0
extra/checkBlack/Config.ini

@@ -0,0 +1,5 @@
+[sys]
+//值越小,标识背景色越黑,一般不考虑做修改
+BitPer=190
+// 黑点总数/(宽*高)*100;比 Range 大时返回1 否则返回0
+Range=30

TEMPAT SAMPAH
extra/checkBlack/GdiPlus.dll


+ 1 - 3
package.json

@@ -1,9 +1,7 @@
 {
   "name": "msyj-client",
-  "version": "3.0.0",
-  "private": true,
+  "version": "3.1.0",
   "description": "scan client",
-  "author": "chulinice",
   "scripts": {
     "start": "yarn run e:serve",
     "serve": "vue-cli-service serve",

+ 3 - 0
src/modules/client/api.js

@@ -33,3 +33,6 @@ export const saveCollectLog = datas => {
 export const getLevelList = examId => {
   return $get(`/api/level/${examId}`);
 };
+export const checkAdminPwd = password => {
+  return $post(`/api/level`, { password });
+};

+ 12 - 6
src/modules/client/components/ScanAreaSteps.vue

@@ -31,11 +31,12 @@
 </template>
 
 <script>
-import CodeArea from "./steps/CodeArea";
-import CoverArea from "./steps/CoverArea";
-import ImageOrientation from "./steps/ImageOrientation";
-import OriginTailorArea from "./steps/OriginTailorArea";
-import TailorTailorArea from "./steps/TailorTailorArea";
+import CodeArea from "./steps/CodeArea.vue";
+import CoverArea from "./steps/CoverArea.vue";
+import ImageOrientation from "./steps/ImageOrientation.vue";
+import OriginTailorArea from "./steps/OriginTailorArea.vue";
+import TailorTailorArea from "./steps/TailorTailorArea.vue";
+import AnswerCheckArea from "./steps/AnswerCheckArea.vue";
 import { deepCopy, objTypeOf } from "../../../plugins/utils";
 
 const STEPS_LIST = [
@@ -55,6 +56,10 @@ const STEPS_LIST = [
     name: "tailor-tailor-area",
     title: "阅卷图切图设置"
   },
+  {
+    name: "answer-check-area",
+    title: "是否作答检测区域"
+  },
   {
     name: "image-orientation",
     title: "图片方向设置"
@@ -68,7 +73,8 @@ export default {
     CoverArea,
     ImageOrientation,
     OriginTailorArea,
-    TailorTailorArea
+    TailorTailorArea,
+    AnswerCheckArea
   },
   props: {
     imageUrl: {

+ 105 - 0
src/modules/client/components/ScanCoverWarningDialog.vue

@@ -0,0 +1,105 @@
+<template>
+  <Modal
+    class="scan-cover-warning-dialog"
+    v-model="modalIsShow"
+    :mask-closable="false"
+    :closable="false"
+    :mask="false"
+    width="300px"
+    @on-visible-change="visibleChange"
+  >
+    <div slot="header"></div>
+
+    <Form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      :label-width="0"
+    >
+      <div class="warning-tips">
+        <img src="@/assets/images/icon-error-big.png" />
+        <p>试卷重复扫描作答有异常,请联系工作人员处理</p>
+      </div>
+      <FormItem prop="password" label="密码">
+        <Input
+          v-model.trim="modalForm.password"
+          placeholder="请输入管理员密码"
+          type="password"
+          clearable
+        ></Input>
+      </FormItem>
+    </Form>
+
+    <div slot="footer">
+      <Button type="primary" :disabled="isSubmit" @click="submit">确认</Button>
+    </div>
+  </Modal>
+</template>
+
+<script>
+import { checkAdminPwd } from "../api";
+import { password } from "@/plugins/formRules";
+
+export default {
+  name: "scan-cover-warning-dialog",
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { password: "" },
+      rules: {
+        password
+      }
+    };
+  },
+  methods: {
+    visibleChange(visible) {
+      if (visible) {
+        this.modalForm = { password: "" };
+      }
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate();
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await checkAdminPwd(this.modalForm).catch(() => {
+        this.isSubmit = false;
+      });
+
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$emit("confirm");
+      this.cancel();
+    }
+  }
+};
+</script>
+
+<style lang="less" scoped>
+.warning-tips {
+  font-size: 20px;
+  color: #fff;
+  line-height: 1.5;
+
+  > img {
+    float: left;
+    display: block;
+    width: 30px;
+    height: 30px;
+  }
+
+  > p {
+    margin-left: 40px;
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 69 - 0
src/modules/client/components/steps/AnswerCheckArea.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="tailor-area">
+    <div class="code-area-cont">
+      <img :src="imageUrl" ref="editImage" />
+    </div>
+  </div>
+</template>
+
+<script>
+import Cropper from "cropperjs";
+
+export default {
+  name: "answer-check-area",
+  props: {
+    imageUrl: {
+      type: String,
+      require: true
+    },
+    curSetting: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      cropper: ""
+    };
+  },
+  mounted() {
+    this.initCropper();
+  },
+  methods: {
+    initCropper() {
+      const _this = this;
+      const defTailorArea =
+        (this.curSetting && this.curSetting.answerCheckArea) || {};
+
+      this.cropper = new Cropper(this.$refs.editImage, {
+        viewMode: 1,
+        checkCrossOrigin: false,
+        zoomable: false,
+        minCropBoxWidth: 10,
+        minCropBoxHeight: 10,
+        ready() {
+          _this.cropper.setData(defTailorArea);
+          _this.$emit("on-ready");
+        }
+      });
+    },
+    checkValid() {
+      const answerCheckArea = {
+        ...this.cropper.getData()
+      };
+      this.$emit("on-next", { answerCheckArea });
+    },
+    pass() {
+      this.$emit("on-next", { answerCheckArea: {} });
+    }
+  },
+  beforeDestroy() {
+    if (this.cropper) {
+      this.cropper.destroy();
+      this.cropper = false;
+    }
+  }
+};
+</script>

+ 1 - 1
src/modules/client/components/steps/OriginTailorArea.vue

@@ -10,7 +10,7 @@
 import Cropper from "cropperjs";
 
 export default {
-  name: "tailor-area",
+  name: "origin-tailor-area",
   props: {
     imageUrl: {
       type: String,

+ 1 - 1
src/modules/client/components/steps/TailorTailorArea.vue

@@ -10,7 +10,7 @@
 import Cropper from "cropperjs";
 
 export default {
-  name: "tailor-area",
+  name: "tailor-tailor-area",
   props: {
     imageUrl: {
       type: String,

+ 12 - 0
src/modules/client/store.js

@@ -45,6 +45,8 @@ const mutations = {
   }
 };
 
+const isDevelopment = process.env.NODE_ENV !== "production";
+
 const actions = {
   async updateCurSubject({ state, commit }, curSubject) {
     const scanArea = Object.assign({}, state.scanArea, {
@@ -60,6 +62,16 @@ const actions = {
     await db.setDict("startCountTime", JSON.stringify(startCountTime));
   },
   checkEncrypt({ state }) {
+    // 注意: mac开发的时候,跳过加密狗检查
+    if (isDevelopment) {
+      state.encryptResult = {
+        success: true,
+        type: "success",
+        msg: ""
+      };
+      return;
+    }
+
     startEncryptCheck(result => {
       state.encryptResult = result;
     });

+ 46 - 7
src/modules/client/views/GroupScan.vue

@@ -103,6 +103,11 @@
       @on-next="toNextPaper"
       ref="SimpleImagePreview"
     ></simple-image-preview>
+    <!-- ScanCoverWarningDialog -->
+    <scan-cover-warning-dialog
+      ref="ScanCoverWarningDialog"
+      @confirm="coverWarningConfirm"
+    ></scan-cover-warning-dialog>
   </div>
 </template>
 
@@ -111,11 +116,13 @@ import { getStudentGroupByExamNumber } from "../api";
 import {
   decodeImageCode,
   getEarliestFile,
-  saveOutputImage
+  saveOutputImage,
+  checkEmptyCoverFilled
 } from "../../../plugins/imageOcr";
 import { deepCopy } from "../../../plugins/utils";
 import ScanAreaDialog from "../components/ScanAreaDialog";
 import ScanExceptionDialog from "../components/ScanExceptionDialog";
+import ScanCoverWarningDialog from "../components/ScanCoverWarningDialog.vue";
 import SimpleImagePreview from "@/components/SimpleImagePreview.vue";
 import { mapState, mapActions } from "vuex";
 import log4js from "@/plugins/logger";
@@ -126,7 +133,12 @@ const { ipcRenderer } = require("electron");
 
 export default {
   name: "group-scan",
-  components: { ScanAreaDialog, ScanExceptionDialog, SimpleImagePreview },
+  components: {
+    ScanAreaDialog,
+    ScanExceptionDialog,
+    SimpleImagePreview,
+    ScanCoverWarningDialog
+  },
   data() {
     return {
       isWaiting: true,
@@ -304,11 +316,7 @@ export default {
     },
     async examNumberValid(examNumber, type = "AUTO") {
       const validRes = await this.checkStudentValid(examNumber);
-      if (validRes.valid) {
-        // 保存扫描到的试卷
-        logger.info(`03考号校验合法:[${type}] ${examNumber}`);
-        this.toSaveStudent(examNumber, type);
-      } else {
+      if (!validRes.valid) {
         logger.error(`03考号校验不合法:[${type}] ${validRes.message}`);
         if (!this.canScan) return;
         // 考号不合法异常
@@ -318,7 +326,17 @@ export default {
           collectConfig: this.getCurCollectConfig()
         };
         this.$refs.ScanExceptionDialog.open();
+        return;
+      }
+      // 检查是否存在空白卷覆盖有作答卷
+      const covered = await this.checkUnaswerCoverAnswer(examNumber);
+      if (covered) {
+        return;
       }
+
+      // 保存扫描到的试卷
+      logger.info(`03考号校验合法:[${type}] ${examNumber}`);
+      this.toSaveStudent(examNumber, type);
     },
     async checkStudentValid(examNumber) {
       let validInfo = { valid: true, message: "" };
@@ -362,6 +380,27 @@ export default {
       }
       return validInfo;
     },
+    async checkUnaswerCoverAnswer(examNumber) {
+      const historyStudent = this.historyList.find(
+        item => item.examNumber === examNumber
+      );
+      if (!historyStudent) return false;
+
+      const covered = await checkEmptyCoverFilled(
+        historyStudent.originImgPath,
+        this.curImage.url,
+        this.curSubject.collectConfig.answerCheckArea
+      );
+      return covered;
+    },
+    coverWarningConfirm() {
+      logger.info(`05采集结束:重复扫描空白卷 - ${this.curImage.name}`);
+
+      // 删除扫描文件,继续开始下一个任务
+      fs.unlinkSync(this.curImage.url);
+      this.scrollTaskList();
+      this.restartInitFile();
+    },
     async toSaveStudent(examNumber, type) {
       const compressRate = this.GLOBAL.compressRate;
       const result = await saveOutputImage(

+ 0 - 19
src/plugins/encryptProcess.js

@@ -1,8 +1,6 @@
 import { getEncryptPath } from "./env";
 import { formatDate } from "./utils";
 const request = require("request");
-// const isDevelopment = true;
-const isDevelopment = process.env.NODE_ENV !== "production";
 
 const childProcess = require("child_process");
 const encryptTool = getEncryptPath();
@@ -126,20 +124,7 @@ const encryptCheck = async resultCallback => {
   }, interval);
 };
 
-const encryptCheckDev = resultCallback => {
-  resultCallback({
-    success: true,
-    type: "success",
-    msg: ""
-  });
-};
-
 const startEncryptCheck = async resultCallback => {
-  if (isDevelopment) {
-    encryptCheckDev(resultCallback);
-    return;
-  }
-
   checkResults = [];
   lastConfirmResult = "";
   if (setT) clearTimeout(setT);
@@ -154,10 +139,6 @@ const closeEncryptCheck = async () => {
   await requestGet("close").catch(() => {});
 };
 
-// if (!isDevelopment) {
-//   startEncryptTool();
-// }
-
 export { closeEncryptCheck, startEncryptCheck };
 
 /**

+ 5 - 0
src/plugins/env.js

@@ -59,6 +59,10 @@ function getImgDecodeTool() {
   return path.join(getExtraDir("zxingA"), "zxing.exe");
 }
 
+function getCheckBlackTool() {
+  return path.join(getExtraDir("checkBlack"), "CheckBlack.exe");
+}
+
 function getFontPath() {
   // 文件名必须是英文,否则会报错
   return path.join(getExtraDir("font"), "simhei-subfont.ttf");
@@ -123,6 +127,7 @@ export {
   makeDirSync,
   getDatabaseDir,
   getImgDecodeTool,
+  getCheckBlackTool,
   getFontPath,
   getEncryptPath,
   getHardwareCheckPath,

+ 0 - 4
src/plugins/hardwareCheck.js

@@ -3,7 +3,6 @@ const path = require("path");
 const net = require("net");
 const childProcess = require("child_process");
 
-// const isDevelopment = process.env.NODE_ENV !== "production";
 const hardwareCheckTool = getHardwareCheckPath();
 const hardwareCheckToolDir = path.dirname(hardwareCheckTool);
 // const HOST = "192.168.11.143";
@@ -258,9 +257,6 @@ const startHardwareCheckTool = async () => {
 
 // tcp
 async function startConnect(resultCallback) {
-  // if (isDevelopment) {
-  //   return;
-  // }
   const res = await startHardwareCheckTool().catch(error => {
     console.error(error);
     resultCallback({ success: false, type: "start", error });

+ 54 - 1
src/plugins/imageOcr.js

@@ -4,6 +4,7 @@ import {
   getOutputDir,
   getExtraDir,
   getImgDecodeTool,
+  getCheckBlackTool,
   getFontPath,
   makeDirSync
 } from "./env";
@@ -70,6 +71,57 @@ function decodeImageCode(imgPath, codeArea) {
   });
 }
 
+function checkImageAnswerArea(imgPath, codeArea) {
+  const tmpFile = path.join(
+    getTmpDir(),
+    `${formatDate("YYYYMMDDHHmmss")}_${randomCode(8)}.jpg`
+  );
+
+  const imgObj = gm(imgPath);
+  // 裁剪条形码区域
+  imgObj.crop(codeArea.width, codeArea.height, codeArea.x, codeArea.y);
+
+  return new Promise((resolve, reject) => {
+    // 写入临时文件
+    imgObj.write(tmpFile, function(err) {
+      if (err) {
+        reject("作答区域临时文件获取失败");
+        return;
+      }
+      // 获取黑点检测工具工具
+      const exec = getCheckBlackTool();
+      // 解析条形码
+      let code;
+      try {
+        const DecodeResult = childProcess.execSync(`${exec} ${tmpFile}`);
+        // console.log(DecodeResult.toString());
+        const codes = DecodeResult.toString()
+          .replace(/\r/, "")
+          .replace(/\n/, "")
+          .split(":");
+        console.dir(codes);
+        if (codes.length <= 2) {
+          code = codes[codes.length - 1].replace(/\s+/g, "");
+        }
+        console.log(code);
+      } catch (e) {
+        console.log(e);
+        reject("作答区域检测错误");
+        return;
+      }
+
+      fs.unlink(tmpFile, () => {});
+      resolve(code);
+    });
+  });
+}
+
+async function checkEmptyCoverFilled(firstImage, secondImage, codeArea) {
+  const result1 = await checkImageAnswerArea(firstImage, codeArea);
+  const result2 = await checkImageAnswerArea(secondImage, codeArea);
+  return result1 === "1" && result2 === "0";
+}
+
 /**
  * 旋转图片,并保存为正式文件
  * @param {*} imgPath 图片路径
@@ -313,5 +365,6 @@ export {
   rotateImage,
   compressImage,
   downloadFile,
-  downloadServerFile
+  downloadServerFile,
+  checkEmptyCoverFilled
 };

+ 7 - 0
src/store.js

@@ -7,6 +7,8 @@ Vue.use(Vuex);
 // modules
 import client from "./modules/client/store";
 
+const isDevelopment = process.env.NODE_ENV !== "production";
+
 export default new Vuex.Store({
   state: {
     user: {},
@@ -23,6 +25,11 @@ export default new Vuex.Store({
   },
   actions: {
     checkHardware({ commit }) {
+      // 注意: mac开发的时候,跳过硬件检查
+      if (isDevelopment) {
+        commit("setHardwareCheckResult", { success: true, type: "connect" });
+        return;
+      }
       startConnect(result => {
         commit("setHardwareCheckResult", result);
       });