index.vue 17 KB

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