index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. <template>
  2. <div style='height: 100%;'>
  3. <div class='import-header'>
  4. <img src='./images/nav_images.png' alt=''>照片批量上传工具 - {{server}}
  5. <button class='exit-btn' @click='logout' title="退出"></button>
  6. </div>
  7. <div class='import-body'>
  8. <div class='import-div'>
  9. <a href='javascript:;'>
  10. <!-- webkitdirectory-->
  11. <button class='import-btn' @click="importPhotos">
  12. 上传照片
  13. </button>
  14. </a>
  15. <div>
  16. <input class="base-id-input" type="text" v-model="baseID" placeholder="不处理小于此数字的身份证照片">
  17. </div>
  18. <div class='progress-div'>
  19. <div>
  20. <span>
  21. 成功(含跳过):
  22. <span style="color:green;">{{successNum}}({{skipNum}})</span>/{{allNum}}
  23. </span>
  24. <span>
  25. 失败:
  26. <span style="color:red;">{{errorNum}}</span>/{{allNum}}
  27. </span>
  28. <span>
  29. 并发参数:
  30. <span style="color:black;">{{batchConcurrency}}</span>
  31. </span>
  32. <span>
  33. 并发请求:
  34. <span style="color:black;">{{reqNum}}</span>
  35. </span>
  36. <span>
  37. 并发报警:
  38. <span style="color:red;">{{faceppConcurrencyErrorNum}}</span>
  39. </span>
  40. <span>
  41. 报警频率:
  42. <span style="color:red;">{{faceppConcurrencyErrorNumPerMinute}}个/分</span>
  43. </span>
  44. </div>
  45. <div>
  46. <span>开始时间:{{startProcessTimeFormat}} </span>
  47. <span>结束时间:{{endProcessTimeFormat}} </span>
  48. <span>处理速度:{{(1000*processSpeed).toFixed(2)}}个/秒</span>
  49. </div>
  50. <div>
  51. <span v-show="completeShow" style="color: green;font-weight: bold;">全部处理完成</span>
  52. <span v-show="!completeShow&&(successNum>0||errorNum>0)" style="color: red;font-weight: bold;">处理中...</span>
  53. </div>
  54. </div>
  55. </div>
  56. <div class='console-panel'>
  57. <div v-for='msgInfo in returnMsgList200' :key="msgInfo.fileName">
  58. <p class='console-line'>
  59. <span>{{msgInfo.fileName}}:{{msgInfo.msg}}</span>
  60. </p>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </template>
  66. <script>
  67. const CryptoJS = require("crypto-js");
  68. const Base64 = require("js-base64").Base64;
  69. const fs = window.nodeRequire("fs");
  70. const path = window.nodeRequire("path");
  71. const async = require("async");
  72. const moment = require("moment");
  73. function isImageFile(fileName) {
  74. const lowercaseName = fileName.toLowerCase();
  75. return (
  76. lowercaseName.endsWith(".jpg") ||
  77. lowercaseName.endsWith(".jpeg") ||
  78. lowercaseName.endsWith(".png")
  79. );
  80. }
  81. // (async () => window.DB.updateFaceSet("fbd41e3e320f21c9f35467c268f86b4c", 22))();
  82. //读取文件,返回网站路径. 只扫描当前文件夹,不递归扫描
  83. function readImageFiles(folderPath) {
  84. try {
  85. const filesToRename = fs.readdirSync(folderPath);
  86. filesToRename
  87. .filter(isImageFile)
  88. .forEach(fileName =>
  89. fs.renameSync(
  90. folderPath + "/" + fileName,
  91. folderPath + "/" + fileName.toUpperCase()
  92. )
  93. );
  94. } catch (e) {
  95. alert("将身份证号转为大写出错。");
  96. }
  97. const files = fs.readdirSync(folderPath);
  98. return files
  99. .filter(isImageFile)
  100. .sort()
  101. .map(fileName => folderPath + "/" + fileName);
  102. }
  103. let CONCURRENCY = 4; //同时处理的照片数量; 深夜增加并发
  104. export default {
  105. data() {
  106. return {
  107. server: process.env.VUE_APP_ENV,
  108. photoList: [], //总的照片队列
  109. returnMsgList: [], //返回信息
  110. successNum: 0, //成功数量
  111. errorNum: 0, //失败数量
  112. skipNum: 0, //跳过数量
  113. allNum: 0, //总数
  114. completeShow: false,
  115. baseID: "", // 从大于这个身份证号开始处理,不能在init初始化
  116. reqNum: 0, // 并发请求数
  117. faceppConcurrencyErrorNum: 0, // face++的并发数报错
  118. startProcessTime: null, // 开始处理的时间
  119. endProcessTime: null, // 结束处理的时间
  120. processSpeed: 0, // 处理的速度:个/秒
  121. batchConcurrency: 0,
  122. faceppConcurrencyErrorNumPerMinute: 0 // 报警频率
  123. };
  124. },
  125. methods: {
  126. init() {
  127. this.photoList = [];
  128. this.returnMsgList = [];
  129. this.successNum = 0;
  130. this.errorNum = 0;
  131. this.skipNum = 0;
  132. this.allNum = 0;
  133. this.completeShow = false;
  134. this.reqNum = 0;
  135. this.faceppConcurrencyErrorNum = 0;
  136. this.startProcessTime = null;
  137. this.endProcessTime = null;
  138. this.processSpeed = 0;
  139. window.faceppConcurrencyErrorNum = 0;
  140. },
  141. importPhotos() {
  142. this.init();
  143. var electron = window.nodeRequire("electron");
  144. let dialog = electron.remote.dialog;
  145. dialog.showOpenDialog(
  146. {
  147. title: "选择照片所在目录",
  148. properties: ["openDirectory"]
  149. },
  150. folderPaths => {
  151. if (folderPaths) {
  152. this.photoList = readImageFiles(folderPaths[0]);
  153. this.allNum = this.photoList.length;
  154. this.startProcessTime = Date.now();
  155. this.processQueue();
  156. }
  157. }
  158. );
  159. },
  160. processQueue() {
  161. let taskQueue = [];
  162. let total = 0;
  163. for (
  164. let i = this.successNum + this.errorNum;
  165. i < this.photoList.length && total < 100;
  166. i++, total++
  167. ) {
  168. taskQueue.push(this.processStudentPhoto.bind(this, this.photoList[i]));
  169. }
  170. this.processQueueSingle(taskQueue);
  171. },
  172. processQueueSingle(taskQueue100) {
  173. // 并发处理请求
  174. // CONCURRENCY = new Date().getHours() < 6 ? 9 : 5;
  175. // 根据并发错误频率来决定下一次多少并发
  176. if (window.faceppConcurrencyErrorNumPerMinute < 7 && CONCURRENCY < 9) {
  177. CONCURRENCY++;
  178. } else {
  179. CONCURRENCY--;
  180. }
  181. this.batchConcurrency = CONCURRENCY;
  182. async.parallelLimit(taskQueue100, CONCURRENCY, (err, results) => {
  183. if (err) {
  184. alert(err);
  185. console.log(err);
  186. } else {
  187. console.log(results);
  188. if (this.successNum + this.errorNum < this.allNum) {
  189. console.log("处理完100张图片了,0.5秒后继续...间隔提供给GC");
  190. const delay = Date.now() - this.startProcessTime < 10000 ? 0 : 500; // 如果有跳过的图标则不等待0.5秒
  191. setTimeout(this.processQueue, delay);
  192. } else {
  193. console.log("photoList处理完毕");
  194. this.endProcessTime = Date.now();
  195. this.completeShow = true;
  196. }
  197. }
  198. });
  199. },
  200. //处理单个照片
  201. async processStudentPhoto(studentPhotoPath) {
  202. const fileSuffix = path.extname(studentPhotoPath); //文件后缀
  203. const identityNumber = path
  204. .basename(studentPhotoPath)
  205. .replace(fileSuffix, ""); //文件名就是身份证号码
  206. // 根据用户输入来跳过部分图片
  207. if (this.lessThanBaseID(identityNumber)) {
  208. this.finishOnePhotoSuccess("跳过处理", studentPhotoPath);
  209. this.skipNum++;
  210. return;
  211. }
  212. const photoFile = fs.readFileSync(studentPhotoPath);
  213. const rootOrgId = localStorage.getItem("rootOrgId");
  214. //生成新名称
  215. let serverPhotoPath = null;
  216. const upyunPhotoPath = (() => {
  217. const md5Hash = CryptoJS.MD5(
  218. Base64.encode(identityNumber + new Date().getTime())
  219. ).toString();
  220. serverPhotoPath = md5Hash + fileSuffix;
  221. return (
  222. rootOrgId +
  223. "/" +
  224. encodeURIComponent(identityNumber) +
  225. "/" +
  226. md5Hash +
  227. fileSuffix
  228. );
  229. })();
  230. // 核心流程:
  231. // 1. get studentId from ecs
  232. // 2. get faceToken from facepp
  233. // 3. get faceSetToken from ecs
  234. // 4. add faceToken to faceSetToken
  235. // 5. save photo to upyun
  236. // 6. 根据以上信息,保存到服务器
  237. // 每一步出错都会保存到错误日志
  238. try {
  239. // 不用Promise.all的原因是每一步失败就不用进行下一步了
  240. let studentId = await this.getStudentId(rootOrgId, identityNumber);
  241. let faceToken = await this.detectFace(photoFile);
  242. this.faceSetToken = await this.getFaceSetToken();
  243. let faceCount = await this.addFaceToSet(this.faceSetToken, faceToken);
  244. await this.saveImageToUpyun({ upyunPhotoPath, photoFile });
  245. const photoInfo = {
  246. studentId: studentId,
  247. faceSetToken: this.faceSetToken,
  248. faceToken: faceToken,
  249. studentPhotoPath: studentPhotoPath,
  250. rootOrgId: rootOrgId,
  251. faceCount,
  252. photoName: serverPhotoPath
  253. };
  254. await this.saveStudentFaceInfoByPut(photoInfo);
  255. this.finishOnePhotoSuccess("处理成功", studentPhotoPath);
  256. {
  257. // 执行过程中的元信息
  258. this.reqNum = window.requestInProcessingTotal;
  259. this.faceppConcurrencyErrorNum = window.faceppConcurrencyErrorNum;
  260. this.faceppConcurrencyErrorNumPerMinute =
  261. window.faceppConcurrencyErrorNumPerMinute;
  262. this.processSpeed =
  263. (this.successNum + this.errorNum - this.skipNum) /
  264. (Date.now() - this.startProcessTime);
  265. }
  266. } catch (err) {
  267. console.log(err);
  268. this.finishOnePhotoFail(err, studentPhotoPath);
  269. }
  270. },
  271. async getStudentId(rootOrgId, identityNumber) {
  272. return new Promise((resolve, reject) => {
  273. this.$http
  274. .get(
  275. "/api/ecs_core/student/getStudentInfo?orgId=" +
  276. rootOrgId +
  277. "&identityNumber=" +
  278. identityNumber
  279. )
  280. .then(res => {
  281. var studentFaceInfo = res.data;
  282. if (studentFaceInfo && studentFaceInfo.id) {
  283. resolve(studentFaceInfo.id);
  284. } else {
  285. reject("查询身份证不存在");
  286. }
  287. })
  288. .catch(err => {
  289. console.log(err);
  290. reject("根据身份证号码查询失败");
  291. });
  292. });
  293. },
  294. //保存文件至又拍云
  295. async saveImageToUpyun({ upyunPhotoPath, photoFile }) {
  296. const url = process.env.VUE_APP_UPYUN_BUCKETURL + upyunPhotoPath;
  297. const authorization =
  298. "Basic " +
  299. Base64.encode(
  300. process.env.VUE_APP_UPYUN_OPERATOR +
  301. ":" +
  302. process.env.VUE_APP_UPYUN_PASSWORD
  303. );
  304. const headers = {
  305. headers: {
  306. Authorization: authorization,
  307. "Content-Type": "image/jpeg"
  308. }
  309. };
  310. return this.$http.put(url, photoFile, headers).catch(err => {
  311. console.log(err);
  312. throw "saveImageToUpyun失败";
  313. });
  314. },
  315. //获取faceSetToken
  316. async getFaceSetToken() {
  317. if (this.faceSetToken) {
  318. return this.faceSetToken;
  319. } else {
  320. return new Promise((resolve, reject) => {
  321. this.$http
  322. .get("/api/ecs_core/face/getUsableFacesetList")
  323. .then(res => {
  324. if (res.data.length < 1) {
  325. reject("获取facesetToken失败: 没有可用的facesetToken");
  326. } else {
  327. resolve(res.data[0].facesetToken);
  328. }
  329. })
  330. .catch(err => {
  331. console.log(err);
  332. reject("获取facesetToken失败: 接口出错");
  333. });
  334. });
  335. }
  336. },
  337. //faceToken加入faceSetToken
  338. async addFaceToSet(faceset_token, face_token) {
  339. let formData_addface = new FormData();
  340. formData_addface.append("api_key", process.env.VUE_APP_FACEPP_API_KEY);
  341. formData_addface.append(
  342. "api_secret",
  343. process.env.VUE_APP_FACEPP_API_SECRET
  344. );
  345. formData_addface.append("faceset_token", faceset_token);
  346. formData_addface.append("face_tokens", face_token);
  347. return new Promise((resolve, reject) => {
  348. this.$http
  349. .post("/facepp/v3/faceset/addface", formData_addface)
  350. .then(res => {
  351. // console.log(
  352. // `res.data.face_added: ${
  353. // res.data.face_added
  354. // }, res.data.face_count: ${res.data.face_count}`
  355. // );
  356. if (res.data.face_added !== 1) {
  357. reject(
  358. "faceToken加入faceSetToken失败: face_added为" +
  359. res.data.face_added
  360. );
  361. }
  362. if (res.data.face_count > 8000) {
  363. this.faceSetToken = undefined;
  364. }
  365. resolve(res.data.face_count);
  366. })
  367. .catch(err => {
  368. console.log(err);
  369. reject("faceToken加入faceSetToken失败: addface catch error");
  370. });
  371. });
  372. },
  373. //face++分析人脸
  374. async detectFace(file) {
  375. let fileBlob = new Blob([file]);
  376. let formData_face_token = new FormData();
  377. formData_face_token.append("api_key", process.env.VUE_APP_FACEPP_API_KEY);
  378. formData_face_token.append(
  379. "api_secret",
  380. process.env.VUE_APP_FACEPP_API_SECRET
  381. );
  382. formData_face_token.append("image_file", fileBlob);
  383. return new Promise((resolve, reject) => {
  384. this.$http
  385. .post("/facepp/v3/detect", formData_face_token)
  386. .then(res => {
  387. if (res.data.faces.length >= 1) {
  388. resolve(res.data.faces[0].face_token);
  389. } else {
  390. reject("face++没有检测到人脸;");
  391. }
  392. })
  393. .catch(err => {
  394. console.log(err);
  395. reject("调用face++检测人脸失败");
  396. });
  397. });
  398. },
  399. async saveStudentFaceInfoByPut({
  400. rootOrgId,
  401. studentId,
  402. faceSetToken,
  403. faceToken,
  404. faceCount,
  405. photoName
  406. }) {
  407. return this.$http
  408. .post("/api/ecs_core/face/saveStudentFace", {
  409. rootOrgId,
  410. studentId,
  411. facesetToken: faceSetToken,
  412. faceToken,
  413. faceCount,
  414. photoName,
  415. operator: "客户端工具上传-" + localStorage.getItem("userName")
  416. })
  417. .catch(err => {
  418. console.log(err);
  419. throw "saveStudentFaceInfoByPut失败";
  420. });
  421. },
  422. //成功或失败处理
  423. finishOnePhotoFail(msg, studentPhotoPath) {
  424. try {
  425. const fileName = path.basename(studentPhotoPath);
  426. this.returnMsgList.push({
  427. success: false,
  428. fileName,
  429. msg
  430. });
  431. this.errorNum++;
  432. //移动照片到errorfiles文件夹
  433. const errorfilePath = path.join(
  434. path.dirname(studentPhotoPath),
  435. "errorfiles"
  436. );
  437. if (!fs.existsSync(errorfilePath)) {
  438. fs.mkdirSync(errorfilePath);
  439. }
  440. fs.copyFileSync(studentPhotoPath, path.join(errorfilePath, fileName));
  441. fs.appendFileSync(
  442. path.join(errorfilePath, "errorPhotos.txt"),
  443. fileName + ":" + msg + "\n"
  444. );
  445. } catch (error) {
  446. console.log(error);
  447. }
  448. },
  449. //成功处理一张照片
  450. finishOnePhotoSuccess(msg, studentPhotoPath) {
  451. this.returnMsgList.push({
  452. success: true,
  453. fileName: path.basename(studentPhotoPath),
  454. msg: msg
  455. });
  456. this.successNum++;
  457. fs.appendFileSync(
  458. path.join(path.dirname(studentPhotoPath), "successPhotos.txt"),
  459. path.basename(studentPhotoPath) + ":" + msg + "\n"
  460. );
  461. },
  462. logout() {
  463. localStorage.removeItem("rootOrgId");
  464. localStorage.removeItem("userName");
  465. localStorage.removeItem("user_token");
  466. this.$router.push({
  467. path: "/login"
  468. });
  469. },
  470. lessThanBaseID(identityNumber) {
  471. return identityNumber < this.baseID; //字符串比较,从第一个字符比较起
  472. }
  473. },
  474. computed: {
  475. returnMsgList200() {
  476. return this.returnMsgList.slice(this.returnMsgList.length - 200);
  477. },
  478. startProcessTimeFormat() {
  479. return this.startProcessTime
  480. ? moment(this.startProcessTime).format("YYYY-MM-DD HH:mm:ss")
  481. : "-";
  482. },
  483. endProcessTimeFormat() {
  484. return this.endProcessTime
  485. ? moment(this.endProcessTime).format("YYYY-MM-DD HH:mm:ss")
  486. : "-";
  487. }
  488. }
  489. };
  490. </script>
  491. <style scoped>
  492. .import-header {
  493. background-color: #3ed798;
  494. color: white;
  495. height: 80px;
  496. padding: 0 30px;
  497. line-height: 80px;
  498. text-align: left;
  499. }
  500. .import-header > img {
  501. height: 34px;
  502. margin-right: 10px;
  503. }
  504. .exit-btn {
  505. position: absolute;
  506. top: 0;
  507. right: 0;
  508. border: none;
  509. width: 60px;
  510. height: 36px;
  511. background: url("./images/btn_closed.png") no-repeat center;
  512. background-size: cover;
  513. outline: none;
  514. }
  515. .exit-btn:hover {
  516. background: url("./images/btn_closed_hover.png") no-repeat center;
  517. cursor: pointer;
  518. }
  519. .import-body {
  520. display: grid;
  521. height: calc(100vh - 100px);
  522. grid-template-rows: 270px 1fr;
  523. }
  524. .import-div {
  525. text-align: center;
  526. padding: 20px 0;
  527. }
  528. .import-btn {
  529. width: 316px;
  530. height: 70px;
  531. border-radius: 35px;
  532. border: none;
  533. font-size: 30px;
  534. color: #e3e3e3;
  535. background-color: #3ed798;
  536. }
  537. .import-btn:hover {
  538. color: #777777;
  539. cursor: pointer;
  540. }
  541. .base-id-input {
  542. margin-top: 10px;
  543. width: 300px;
  544. font-size: 20px;
  545. }
  546. .progress-div {
  547. display: flex;
  548. flex-direction: column;
  549. line-height: 40px;
  550. margin: 20px 0px;
  551. font-size: 20px;
  552. border: 1px solid gainsboro;
  553. cursor: default;
  554. }
  555. .console-panel {
  556. background-color: #e3e3e3;
  557. overflow: auto;
  558. }
  559. .console-panel .console-line {
  560. font-size: 14px;
  561. text-align: center;
  562. display: flex;
  563. justify-content: center;
  564. align-items: center;
  565. }
  566. .console-panel .console-line > span {
  567. margin: 0 10px;
  568. }
  569. .console-panel .console-line > span.red {
  570. color: #fc7156;
  571. }
  572. .console-panel .console-line > span.green {
  573. color: #3ed798;
  574. }
  575. </style>