util.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. 'use strict';
  2. const assert = require('assert');
  3. const _ = require('lodash');
  4. const fs = require('fs');
  5. const Excel = require('exceljs');
  6. const Path = require('path');
  7. const XLSX = require('xlsx');
  8. const { CrudService } = require('naf-framework-mongoose/lib/service');
  9. const { BusinessError, ErrorCode } = require('naf-core').Error;
  10. const moment = require('moment');
  11. const nodemailer = require('nodemailer');
  12. const docx = require('docx');
  13. const archiver = require('archiver');
  14. const { ObjectId } = require('mongoose').Types;
  15. class UtilService extends CrudService {
  16. constructor(ctx) {
  17. super(ctx);
  18. this.mq = this.ctx.mq;
  19. this.sModel = this.ctx.model.School;
  20. }
  21. async updatedate() {
  22. let date = new Date();
  23. date = moment(date).format('YYYY-MM-DD HH:mm:ss');
  24. return date;
  25. }
  26. async sendMail(email, subject, text, html) {
  27. const setting = await this.ctx.model.Setting.findOne();
  28. let user_email = this.ctx.app.config.user_email;
  29. let auth_code = this.ctx.app.config.auth_code;
  30. if (setting) {
  31. user_email = setting.user_email;
  32. auth_code = setting.auth_code;
  33. }
  34. const transporter = nodemailer.createTransport({
  35. host: 'smtp.exmail.qq.com',
  36. secureConnection: true,
  37. port: 465,
  38. auth: {
  39. user: user_email, // 账号
  40. pass: auth_code, // 授权码
  41. },
  42. });
  43. if (process.env.NODE_ENV === 'development') email = '402788946@qq.com';
  44. const mailOptions = {
  45. from: user_email, // 发送者,与上面的user一致
  46. to: email, // 接收者,可以同时发送多个,以逗号隔开
  47. subject, // 标题
  48. text, // 文本
  49. html,
  50. };
  51. try {
  52. await transporter.sendMail(mailOptions);
  53. return true;
  54. } catch (err) {
  55. return false;
  56. }
  57. }
  58. async findone({ modelname }, data) {
  59. // 查询单条
  60. const _model = _.capitalize(modelname);
  61. const res = await this.ctx.model[_model].findOne({ ...data });
  62. return res;
  63. }
  64. async findbyids({ modelname }, { data }) {
  65. // 共通批量查询方法
  66. const _model = _.capitalize(modelname);
  67. const res = [];
  68. for (const elm of data) {
  69. const result = await this.ctx.model[_model].findById(elm);
  70. res.push(result);
  71. }
  72. return res;
  73. }
  74. async findmodel({ modelname }) {
  75. const _model = _.capitalize(modelname);
  76. const data = this.ctx.model[_model].prototype.schema.obj;
  77. const keys = Object.keys(data);
  78. const res = {};
  79. for (const k of keys) {
  80. const obj = data[k];
  81. if (_.get(obj, 'zh')) res[k] = obj;
  82. }
  83. return res;
  84. }
  85. async utilMethod(query, body) {
  86. const crypto = require('crypto');
  87. // const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  88. // modulusLength: 2048,
  89. // publicKeyEncoding: {
  90. // type: 'spki',
  91. // format: 'pem',
  92. // },
  93. // privateKeyEncoding: {
  94. // type: 'pkcs8',
  95. // format: 'pem',
  96. // cipher: 'aes-256-cbc',
  97. // passphrase: 'sks_secret',
  98. // },
  99. // });
  100. // console.log(publicKey);
  101. // console.log('-------------------');
  102. // console.log(privateKey);
  103. const privateKey = `-----BEGIN ENCRYPTED PRIVATE KEY-----
  104. MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIDrmClOodJAACAggA
  105. MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDFUSMr1yfAK9Lt9r4Idi5EBIIE
  106. 0JmkwwNWz+ya4IffVvzQS2fEbIRoAcxoBEKaZjLd2cCBQbE/0BODdlEWcJ7Z4Z3k
  107. 1NfDaqQToh+k9sE2C108q2jCETLiLPWodTMAMO0pVHBiiJeh2ZOoDxwyDWsyHYcA
  108. 6S7EhuQPOY7Fd8WKWeYcH1m8TLyL40fmWq0cseCoyvP1M0LRQ5OFKD7+uOYJBQhq
  109. 7pPwuJouorKMUHbxpribrOmr8fspfkglhP6+TZ7p7XzAs6e92mNz1SfiflmChoPH
  110. sqcJb0SJaHCdzblMT/yCF1Gm+h91gpU9L3sJUbivBtjFkX4LsE8DMs15dkPIbWHq
  111. YTJx+2QSOpGp6cK1D7LS8LInptHxI/UGRVYH3FLP1oUJuuFj+Wl7PKvOpZcc5uk1
  112. u5tbHmUkhpJeklqsCWi6brQpPxiQAiCeiqMiTOQLlwiRNTT51waOqrvb60onDHAw
  113. bEWfb0FQ0o3kb1yEQd/qPTJXWPZ96WhM3/7VNGoEDKwCzv5m8G1JbO4ct79iPF5x
  114. Y7rx0uKJFPbekNACRmb9U0f3DpXm8hdgCKVKmbkgG2ZZnnmJg2lzmT72lwQfdcnv
  115. O6IjXVLoDLjAYqjGe6HXQchi5rBHgmoguyHP6nGJ67n50hQ+upecVyp7k7LvuXi/
  116. jHF8/zJfDZaEvjf6CwkePYk9XbOnvyls+RWZM1ZwX4xbk6nbONzo71uRrI0E8sKS
  117. kWFCtvdbNJ//PaugrQNRMTs5z8Q3qMIyXRPXrxPFM/3lECaIEYvRrPyEYfdANqod
  118. JrMru+hbHvutTPhDuOgJ+RVgirwfZpxPQS5aHHXQu8bMps9DEL15i6xKOVX5lqvL
  119. lEL0YmqmRo7Idb9D+l0pyPAg9X1AwTziION/1cOPvWmjGVZqqaBv+jsMVlB+1o0o
  120. BrJBy9SEzw5ya7sYi1BCwO6k7qAsB1qSmnu8Gg/RXAIz6D9nJiJbB5kwusVQretn
  121. RqTj8ldBOgo+1la3hHzW8INTAVrlnmZjNMEGN4olYcJv6vHK9XTk7ruIkteJYD8Y
  122. 2jIFU3evsPTs4ugYxArEmzYsr1ZJR91aWf7naKcUlsap9k2zgHVw7Ks31D8T1OT3
  123. mXQZdkY9hx+0kdh8iSYOkCSa237usV/TbYg12ncB8xSVsMQMNnEQHWIcgFUBeZoo
  124. KQ27ijLmy5YYx8KYDAm7UJZ3Z00OrFa1D5qjippXkrDKbBwM3SrDSSVBtfKMGxyi
  125. epF9hMBs6ecBBSIF9sZg0LHfMlbOIWyNLklHUtdBQ4+vavzbG1V66p0pB7HJp6rp
  126. YC3moNRhHXyUFpvkIis7gFQ4xdE6FMEJsrHeZQHuOzRIB4XlwiCiAIg+IDmnP0hd
  127. 8Sxv0drpFU+zjR2aMr4bfhYdBG35NE/xfOp+ba25yCWCilljCIj46+jD0E2UvSrw
  128. 9iDtf75OXF2njqaLwmnFtHyAOA54IcXi9Y3NaXUXFxmK9iif5UbKa/dR+8rmOCF2
  129. cozCs4Ujribcpgs3cqjkVX0Z12B7YdCZM1Tpj7BgPOLjpi0h0s5wbP9QMm3yb2S2
  130. SKteoIypVcUXdpPqSwXtsUOUSuj7MeNXW2mkmQsH+W/n2Zn5OxgFKZ0JZ4z/OHei
  131. hK1UFYPQ9BEBMzsJ2ttBLwak//hAMaUJDT8EKxEoBdqb
  132. -----END ENCRYPTED PRIVATE KEY-----`;
  133. const publicKey = `-----BEGIN PUBLIC KEY-----
  134. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqIvXeEZJ2kx0xZxgcWeC
  135. rRQ3+JQjgt7UOUN45PKnw5XNuRCxCbEvnjjLJwJZcrXo0qO54f5rafbPqYoSA5ed
  136. Wt07dY5dVJ02OUl56uIwhoFArJpUzMywFq94BvTaO5Gz58uVUDKgFRYYx9z4/t10
  137. 6AliNHb3KEbovwMqIpGYLIr5JmImfyaCi7eAqwHsLH/HALqGFLHcr6oFaquA9abP
  138. J/6wp465qmQuIUhqR9Nf3HIa/hjcF6ZTBlZhV//tzFH1wDIsUeMn8vS9Gi9vpwYa
  139. AQsB8qxp2yXhZrEbMdD+E27te67SxedRK3IZ13xAeuQ+XB0kVDIPkfRw8eUg3mK2
  140. XQIDAQAB
  141. -----END PUBLIC KEY-----`;
  142. const data = this.ctx.request.body;
  143. const enc = crypto.publicEncrypt(
  144. {
  145. key: publicKey,
  146. padding: crypto.constants.RSA_PKCS1_PADDING,
  147. },
  148. Buffer.from(JSON.stringify(data))
  149. );
  150. const str = enc.toString('base64');
  151. // const sb = Buffer.from(str, 'base64');
  152. // const decpub = crypto.privateDecrypt(
  153. // {
  154. // key: privateKey,
  155. // passphrase: 'sks_secret',
  156. // },
  157. // sb
  158. // );
  159. return enc.toString('base64');
  160. // for (const trainPlan of allTranPlan) {
  161. // const { termnum = [] } = trainPlan;
  162. // const termids = termnum.map(f => ObjectId(f._id).toString());
  163. // // 查出该期所有的申请
  164. // const termApplys = await applyModel.find({ termid: termids }).lean();
  165. // const needDeletes = termApplys.filter(f => !(allSubject.find(s => s._id === f.subid) && allTeacher.find(t => t._id === f.teacherid)));
  166. // const deleteIds = needDeletes.map(i => i._id);
  167. // console.log(deleteIds.length);
  168. // // await applyModel.deleteMany({ _id: deleteIds });
  169. // }
  170. // // // 重置班级
  171. // // 找到要删除的学生
  172. // const res = await this.ctx.model.Student.find({ school_name: '吉林职业技术学院', planid: '60769b703cead37068645fb2', termid: { $ne: '60779c0b2ec7ac704ce301ab' } });
  173. // // const test = res.map(f => {
  174. // // return { name: f.name, openid: f.openid, id: f._id };
  175. // // });
  176. // const ids = res.map(i => i._id);
  177. // await this.ctx.model.User.deleteMany({ uid: ids });
  178. // await this.ctx.model.Student.deleteMany({ school_name: '吉林职业技术学院', planid: '60769b703cead37068645fb2', termid: { $ne: '60779c0b2ec7ac704ce301ab' } });
  179. // const ids = res.map(i => i.openid);
  180. // // console.log(ids);
  181. //
  182. // console.log(userRes.length);
  183. // const { planid, termid, batchid, classid } = query;
  184. // const filters = {};
  185. // if (classid) filters.classid = classid;
  186. // else if (batchid)filters.batchid = batchid;
  187. // else if (termid)filters.termid = termid;
  188. // else if (planid)filters.planid = planid;
  189. // else throw new BusinessError(ErrorCode.BADPARAM, '需要重置范围');
  190. // await this.ctx.model.Student.updateMany(filters, { classid: undefined });
  191. // // 重置班级结束
  192. }
  193. async getQueryOptions({ skip, limit, sort, desc } = {}) {
  194. if (sort && _.isString(sort)) {
  195. sort = { [sort]: desc ? -1 : 1 };
  196. } else if (sort && _.isArray(sort)) {
  197. sort = sort.map(f => ({ [f]: desc ? -1 : 1 })).reduce((p, c) => ({ ...p, ...c }), {});
  198. }
  199. return { skip, limit, sort };
  200. }
  201. /**
  202. * 更新进度,状态 不存在/完成以外(不是2的值)的数值, 不添加参数,只更新进度
  203. * @param {String} missionid 任务id
  204. * @param {String} progress 进度(人为划分)
  205. * @param {String} status 状态
  206. * @param {Object} params 参数
  207. */
  208. async updateProcess(missionid, progress, status, params) {
  209. try {
  210. if (!missionid) return;
  211. const { baseUrl } = _.get(this.ctx.app.config, 'mission');
  212. let url = `${baseUrl}`;
  213. let data;
  214. if (!baseUrl) return;
  215. if (!status || status !== '2') {
  216. url = `${url}/api/mission/progress`;
  217. data = { id: missionid, progress };
  218. } else if (status === '3') {
  219. url = `${url}/api/mission/update/${missionid}`;
  220. data = { status };
  221. } else {
  222. url = `${url}/api/mission/update/${missionid}`;
  223. data = { progress: '100', params, status };
  224. }
  225. await this.ctx.curl(url, {
  226. method: 'post',
  227. headers: {
  228. 'content-type': 'application/json',
  229. },
  230. data,
  231. dataType: 'json',
  232. });
  233. } catch (error) {
  234. console.error(`任务更新进度报错,missionid:${missionid}\n`);
  235. }
  236. }
  237. async teacherImport() {
  238. // const filepath = './teacherlist.xlsx';
  239. // const workbook = new Excel.Workbook();
  240. // await workbook.xlsx.readFile(filepath);
  241. // const worksheet = workbook.getWorksheet(1);
  242. // if (!worksheet) return;
  243. // let arr = [];
  244. // worksheet.eachRow((row, ri) => {
  245. // if (ri !== 1) {
  246. // const obj = {};
  247. // obj.name = row.getCell(3).value || undefined;
  248. // obj.department = row.getCell(4).value || undefined;
  249. // if (row.getCell(5).value) obj.job = row.getCell(5).value;
  250. // obj.phone = row.getCell(6).value || undefined;
  251. // obj.status = '4';
  252. // arr.push(obj);
  253. // }
  254. // });
  255. // // 检查谁生成过了, user表和teacher表
  256. // let ur = await this.ctx.model.User.find({ mobile: { $in: arr.map(i => i.phone) }, type: '3' });
  257. // let tr = await this.ctx.model.Teacher.find({ phone: { $in: arr.map(i => i.phone) } });
  258. // // 将有的老师过滤出去
  259. // if (ur) {
  260. // ur = JSON.parse(JSON.stringify(ur));
  261. // arr = arr.filter(f => !ur.find(uf => `${uf.mobile}` === `${f.phone}`));
  262. // }
  263. // if (tr) {
  264. // tr = JSON.parse(JSON.stringify(tr));
  265. // arr = arr.filter(f => !(tr.find(tf => `${tf.phone}` === `${f.phone}`)));
  266. // }
  267. // for (const tea of arr) {
  268. // const ctr = await this.ctx.model.Teacher.create(tea);
  269. // if (ctr) {
  270. // const obj = { name: tea.name, mobile: tea.phone, type: '3', uid: ctr._id };
  271. // const cur = await this.ctx.model.User.create(obj);
  272. // }
  273. // }
  274. // const user = await this.ctx.model.User.find({ passwd: { $exists: false } });
  275. // for (const u of user) {
  276. // u.passwd = { secret: '12345678' };
  277. // u.save();
  278. // }
  279. }
  280. /**
  281. * 导出excel
  282. * @param {Array} dataList 数据集合
  283. * @param {Array} meta 表头(可以没有)
  284. * @param {String} fn 文件名
  285. * @param {Array} opera 单元格操作
  286. */
  287. async toExcel(dataList, meta, fn = '导出结果', opera) {
  288. // 导出excel
  289. const { app } = this;
  290. const nowDate = new Date().getTime();
  291. const filename = `${fn}-${nowDate}.xlsx`;
  292. // 取出预设存储地址
  293. const rootPath = `${app.config.cdn.repos_root_path}`;
  294. const rooturl = `${app.config.cdn.repos_root_url_excel}`;
  295. const path = `${rootPath}${rooturl}`;
  296. if (!path) {
  297. throw new BusinessError(ErrorCode.BUSINESS, '服务端没有设置存储路径');
  298. }
  299. // if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  300. if (!fs.existsSync(path)) {
  301. // 如果不存在文件夹,就创建
  302. fs.mkdirSync(path);
  303. }
  304. // 生成文件
  305. const filepath = `${path}${filename}`;
  306. fs.createWriteStream(filepath);
  307. const workbook = new Excel.Workbook();
  308. const sheet = workbook.addWorksheet('sheet');
  309. if (meta) sheet.columns = meta;
  310. sheet.addRows(dataList);
  311. if (_.isArray(opera)) {
  312. for (const o of opera) {
  313. const { startRow, startCol, endRow, endCol } = o;
  314. sheet.mergeCells(startRow, startCol, endRow, endCol);
  315. }
  316. }
  317. // 垂直居中
  318. const length = dataList.length;
  319. const alignment = { vertical: 'middle', horizontal: 'center' };
  320. for (let i = 1; i <= length; i++) {
  321. sheet.getRow(i).alignment = alignment;
  322. }
  323. await workbook.xlsx.writeFile(filepath);
  324. return `/files/excel/${filename}`;
  325. }
  326. /**
  327. * 创建/重复写入excel
  328. * @param {Array} dataList 数据
  329. * @param {String} fn 文件名, 第一次进入时,只是单纯的名,第二次开始,有后缀与时间戳
  330. * @param {String} downloadPath 下载文件路径
  331. * @param {String} type 方向,没有默认横向
  332. */
  333. async toAsyncExcel(dataList, fn = '导出结果', downloadPath, type) {
  334. const { app } = this;
  335. const workbook = new Excel.Workbook();
  336. let sheet;
  337. // 取出预设存储地址
  338. const rootPath = `${app.config.cdn.repos_root_path}`;
  339. const rooturl = `${app.config.cdn.repos_root_url_excel}`;
  340. let path = `${rootPath}${rooturl}`;
  341. let filepath = '';
  342. if (!path) {
  343. throw new BusinessError(ErrorCode.BUSINESS, '服务端没有设置存储路径');
  344. }
  345. if (process.env.NODE_ENV === 'development') path = 'D:\\temp\\upload\\excel\\';
  346. if (!fs.existsSync(path)) {
  347. // 如果不存在文件夹,就创建
  348. fs.mkdirSync(path);
  349. }
  350. if (!downloadPath) {
  351. // 第一次进入,文件还未生成
  352. const nowDate = new Date().getTime();
  353. fn = `${fn}-${nowDate}.xlsx`;
  354. sheet = workbook.addWorksheet('sheet');
  355. } else {
  356. const domain = 'http://127.0.0.1';
  357. const file = await this.ctx.curl(`${domain}${downloadPath}`);
  358. if (!(file && file.data)) {
  359. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找导出的excel');
  360. }
  361. // 读取文件
  362. await workbook.xlsx.load(file.data);
  363. sheet = workbook.getWorksheet('sheet');
  364. }
  365. if (!type || type === 'horizontal') sheet.addRows(dataList);
  366. else if (type === 'vertical') {
  367. for (let i = 1; i <= dataList.length; i++) {
  368. const element = dataList[i - 1];
  369. const rows = sheet.getRow(i);
  370. if (rows.values.length <= 0) rows.values = element;
  371. else rows.values = rows.values.concat(element);
  372. rows.commit();
  373. }
  374. }
  375. filepath = `${path}${fn}`;
  376. await workbook.xlsx.writeFile(filepath);
  377. return { downloadPath: `/files/excel/${fn}`, fn };
  378. }
  379. /**
  380. * 导出docx
  381. * @param {Array} data 数据[{title,content([]),author}]
  382. * @param {String} fn 文件名
  383. */
  384. async toDocx(data, fn = '培训心得') {
  385. const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = docx;
  386. const doc = new Document();
  387. const children = [];
  388. // 整理数据
  389. for (let i = 0; i < data.length; i++) {
  390. const obj = data[i];
  391. const { title, content, author } = obj;
  392. const c = [];
  393. if (title) {
  394. const tit = new Paragraph({
  395. children: [ new TextRun({ text: title, bold: true }) ],
  396. heading: HeadingLevel.TITLE,
  397. alignment: AlignmentType.CENTER,
  398. });
  399. c.push(tit);
  400. }
  401. if (author) {
  402. const auth = new Paragraph({
  403. children: [ new TextRun({ color: '#000000', text: author }) ],
  404. heading: HeadingLevel.HEADING_2,
  405. alignment: AlignmentType.RIGHT,
  406. });
  407. c.push(auth);
  408. }
  409. if (content && _.isArray(content) && content.length > 0) {
  410. for (const cont of content) {
  411. const p = new Paragraph({
  412. children: [ new TextRun({ text: cont, bold: true }) ],
  413. });
  414. c.push(p);
  415. }
  416. }
  417. if (i !== data.length - 1) {
  418. // 换页
  419. const last = new Paragraph({
  420. pageBreakBefore: true,
  421. });
  422. c.push(last);
  423. }
  424. if (c.length > 0) children.push(...c);
  425. }
  426. doc.addSection({
  427. properties: {},
  428. children,
  429. });
  430. const { app } = this;
  431. const rootPath = `${app.config.cdn.repos_root_path}`;
  432. const rooturl = `${app.config.cdn.repos_root_url_experience}`;
  433. const path = `${rootPath}${rooturl}`;
  434. // 如果不存在文件夹,就创建
  435. // if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  436. if (!fs.existsSync(path)) {
  437. fs.mkdirSync(path);
  438. }
  439. const num = new Date().getTime();
  440. const buffer = await Packer.toBuffer(doc);
  441. fs.writeFileSync(`${path}${fn}-${num}.docx`, buffer);
  442. // Packer.toBuffer(doc).then(buffer => {
  443. // fs.writeFileSync(`${path}${fn}-${num}.docx`, buffer);
  444. // });
  445. return `/files${rooturl}${fn}-${num}.docx`;
  446. }
  447. /**
  448. * 将选择的文件导出到zip压缩包中,提供下载
  449. * @param {*} fileList 需要导入到zip中的列表,格式有2中: [{url:""}]或[String]
  450. * @param {*} fn 文件名,默认为 "导出结果"
  451. */
  452. async toZip(fileList, fn = '导出结果') {
  453. if (!_.isArray(fileList)) {
  454. throw new BusinessError(ErrorCode.DATA_INVALID, '需要压缩的文件数据格式错误');
  455. }
  456. fn = `${fn}.zip`;
  457. // zip文件夹创建
  458. const { app } = this;
  459. const rootPath = `${app.config.cdn.repos_root_path}`;
  460. const zipPath = `${app.config.cdn.repos_root_url_zip}`;
  461. let path = `${rootPath}${zipPath}`;
  462. if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  463. if (!fs.existsSync(path)) {
  464. fs.mkdirSync(path);
  465. }
  466. // 文件请求后将数据整理到这里
  467. const resetFileList = [];
  468. for (const file of fileList) {
  469. let uri = '';
  470. let filename = '';
  471. let prefixs;
  472. if (_.isString(file)) {
  473. uri = file;
  474. const arr = file.split('/');
  475. const last = _.last(arr);
  476. if (last) filename = last;
  477. } else if (_.isObject(file)) {
  478. const { uri: furi, url: furl, name, prefix } = file;
  479. if (furi) uri = furi;
  480. else if (furl) uri = furl;
  481. if (name) filename = name;
  482. else {
  483. const arr = uri.split('/');
  484. const last = _.last(arr);
  485. if (last) filename = last;
  486. }
  487. if (prefix) prefixs = prefix;
  488. }
  489. const obj = {};
  490. if (uri) obj.uri = uri;
  491. if (filename) obj.filename = filename;
  492. if (prefixs) obj.prefix = prefixs;
  493. resetFileList.push(obj);
  494. }
  495. // 导出
  496. const output = fs.createWriteStream(`${path}${fn}`);
  497. const archive = archiver('zip', {
  498. zlib: { level: 9 },
  499. });
  500. archive.pipe(output);
  501. // 请求文件,追加进压缩包
  502. for (const file of resetFileList) {
  503. const { uri, filename, prefix } = file;
  504. const res = await this.ctx.curl(`http://127.0.0.1${uri}`);
  505. if (res && res.data) {
  506. const options = {};
  507. if (filename) options.name = filename;
  508. if (prefix) options.prefix = prefix;
  509. if (filename) {
  510. archive.append(res.data, options);
  511. }
  512. }
  513. }
  514. await archive.finalize();
  515. return `/files${zipPath}${fn}`;
  516. }
  517. // 学校计划人数导出
  518. async schoolDownload() {
  519. const { app } = this;
  520. const headList = [
  521. { key: 'name', header: '学校', width: 50, style: { alignment: { wrapText: true, vertical: 'middle', horizontal: 'center' } } },
  522. { key: 'code', header: '学校代码', width: 30, style: { alignment: { wrapText: true, vertical: 'middle', horizontal: 'center' } } },
  523. { key: 'class_special', header: '特殊班', width: 30, style: { alignment: { wrapText: true, vertical: 'middle', horizontal: 'center' } } },
  524. { key: 'class_common', header: '普通班', width: 30, style: { alignment: { wrapText: true, vertical: 'middle', horizontal: 'center' } } },
  525. ];
  526. const nowDate = new Date().getTime();
  527. const filename = `学校计划人数信息导出-${nowDate}.xlsx`;
  528. const rootPath = `${app.config.cdn.repos_root_path}`;
  529. const rooturl = `${app.config.cdn.repos_root_url_experience}`;
  530. const path = `${rootPath}${rooturl}`;
  531. if (!path) {
  532. throw new BusinessError(ErrorCode.BUSINESS, '服务端没有设置存储路径');
  533. }
  534. if (!fs.existsSync(path)) {
  535. // 如果不存在文件夹,就创建
  536. this.mkdir(path);
  537. }
  538. const AggregateInfo = [
  539. // 去除重复数据
  540. { $group: { _id: '$code', uniqueData: { $first: '$$ROOT' } } },
  541. { $replaceRoot: { newRoot: '$uniqueData' } },
  542. { $addFields: { id: '$_id' } },
  543. ];
  544. const list = await this.sModel.aggregate(AggregateInfo);
  545. const workbook = new Excel.Workbook();
  546. const sheet = workbook.addWorksheet('sheet');
  547. const meta = headList;
  548. sheet.columns = meta;
  549. sheet.addRows(list);
  550. // 生成excel
  551. const filepath = `${path}${filename}`;
  552. if (list.length <= 0) return;
  553. await workbook.xlsx.writeFile(filepath);
  554. return `/files/excel/${filename}`;
  555. }
  556. // 学校计划人数导入
  557. async schoolImport(data) {
  558. const { filepath } = data;
  559. assert(filepath, 'filepath不能为空');
  560. // 取得excle中数据
  561. const _filepath = this.ctx.app.config.baseUrl + filepath;
  562. const teadatas = await this.getImportXLSXData(_filepath);
  563. const school = [];
  564. for (const val of teadatas) {
  565. const schoolInfo = await this.sModel.findOne({ name: val.name }).lean();
  566. const classnum = [];
  567. let num;
  568. if (val.class_special) classnum.push({ number: val.class_special, name: '特殊班', code: '1' });
  569. if (val.class_common) classnum.push({ number: val.class_common, name: '普通班', code: '0' });
  570. if (val.class_special && val.class_common) num = parseInt(val.class_special) || 0 + parseInt(val.class_common) || 0;
  571. school.push({
  572. _id: schoolInfo._id.toString(),
  573. num,
  574. code: val.code,
  575. classnum,
  576. });
  577. }
  578. return { school };
  579. }
  580. // 获取导入的XLSX文件中的数据
  581. async getImportXLSXData(filepath) {
  582. const file = await this.ctx.curl(filepath);
  583. const workbook = XLSX.read(file.data);
  584. // 读取内容
  585. let exceldata = [];
  586. const sheetNames = workbook.SheetNames; // 获取表名
  587. const sheet = workbook.Sheets[sheetNames[0]]; // 通过表名得到表对象
  588. // 遍历26个字母
  589. const theadRule = [];
  590. const range = XLSX.utils.decode_range(sheet['!ref']);
  591. const col_start = range.s.c;
  592. const col_end = range.e.c;
  593. for (let i = col_start; i <= col_end; i++) {
  594. const addr = XLSX.utils.encode_col(i) + XLSX.utils.encode_row(0);
  595. theadRule.push(sheet[addr].v);
  596. }
  597. const params = XLSX.utils.sheet_to_json(sheet); // 通过工具将表对象的数据读出来并转成json
  598. if (!params) return [];
  599. const length = params.length;
  600. const _datas = [];
  601. let data = {};
  602. for (let i = 0; i < length; i++) {
  603. data = params[i];
  604. _datas.push({
  605. name: data[theadRule[0]],
  606. code: data[theadRule[1]],
  607. class_special: data[theadRule[2]],
  608. class_common: data[theadRule[3]],
  609. });
  610. }
  611. exceldata = [...exceldata, ..._datas];
  612. return exceldata;
  613. }
  614. // 创建文件夹
  615. mkdir(dirname) {
  616. if (fs.existsSync(dirname)) {
  617. return true;
  618. }
  619. if (this.mkdir(Path.dirname(dirname))) {
  620. fs.mkdirSync(dirname);
  621. return true;
  622. }
  623. }
  624. // 学生参培名单导入
  625. async stuimport(data) {
  626. const { filepath, planid, termid, batchid } = data;
  627. assert(filepath, '文件不能为空');
  628. assert(planid, '计划不能为空');
  629. assert(termid, '期不能为空');
  630. assert(batchid, '批次不能为空');
  631. // 根据termid取得计划信息
  632. const plan = await this.ctx.model.Trainplan.findOne({ 'termnum._id': ObjectId(termid) });
  633. if (!plan) {
  634. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '计划信息不存在');
  635. }
  636. const term = plan.termnum.id(termid);
  637. let type = '0';
  638. if (term.batchnum && term.batchnum.length > 0 && term.batchnum[0].class && term.batchnum[0].class.length > 0) type = term.batchnum[0].class[0].type;
  639. const planyearid = plan.planyearid;
  640. // 检查这个范围的学生是否存在,存在的话是否更改过(classid,bedroomid这两项存不存在可以放过,但凡有一个人,就不行了)
  641. let dbStuList = await this.ctx.model.Student.find({ planid, termid, batchid });
  642. if (dbStuList.length > 0) {
  643. // 查这个学校的这期学生是否修改过班级 或 寝室
  644. const is_change = dbStuList.find(f => f.classid || f.bedroomid);
  645. if (is_change) {
  646. throw new BusinessError(
  647. ErrorCode.BUSINESS,
  648. '该计划该期该批次有学生已经安排班级或寝室 无法进行导入,请对该批次学生进行修改重新导入!'
  649. );
  650. }
  651. }
  652. // 2021-06-07 如果学生已经绑定,那也不允许修改名单了
  653. const countOpenid = await this.ctx.model.Student.count({ planid, termid, batchid, openid: { $exists: true } });
  654. if (countOpenid > 0) throw new BusinessError(ErrorCode.BUSINESS, '已有学生绑定账号,名单无法修改!');
  655. let domain = 'http://127.0.0.1';
  656. if (process.env.NODE_ENV === 'development') domain = 'http://jytz.jilinjobs.cn';
  657. const fullUrl = domain + filepath; // this.ctx.app.config.baseUrl http://127.0.0.1 http://jytz.jilinjobs.cn
  658. let studentList = await this.getDataFromExcel(fullUrl);
  659. const checkRes = await this.checkData(studentList);
  660. const { errorcode } = checkRes;
  661. if (errorcode === '1') {
  662. return checkRes;
  663. }
  664. // 2021-05-26 添加与数据库的对比,如果数据库里已经有这个身份证号,就需要提示
  665. const countStudent = await this.countStudent(studentList, planid);
  666. const { errorcode: csec } = countStudent;
  667. if (csec === '1') {
  668. return countStudent;
  669. }
  670. // 整理数据
  671. studentList = await this.lastSetData(studentList, {
  672. planyearid,
  673. planid,
  674. batchid,
  675. termid,
  676. type,
  677. });
  678. // 复制,删除,添加
  679. if (dbStuList.length > 0) {
  680. dbStuList = JSON.parse(JSON.stringify(dbStuList));
  681. dbStuList = dbStuList.map(i => {
  682. delete i.meta;
  683. i.studentid = _.clone(i._id);
  684. delete i.id;
  685. delete i._id;
  686. return i;
  687. });
  688. await this.ctx.model.Student.deleteMany({ planid, termid, batchid });
  689. await this.ctx.model.Dstudent.insertMany(dbStuList);
  690. }
  691. await this.ctx.model.Student.insertMany(studentList);
  692. return 'ok';
  693. }
  694. // 整理excel数据
  695. async getDataFromExcel(url) {
  696. // 请求文件
  697. const file = await this.ctx.curl(`${url}`);
  698. if (!(file && file.data)) {
  699. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到上传的名单');
  700. }
  701. const workbook = new Excel.Workbook();
  702. // 读取文件
  703. await workbook.xlsx.load(file.data);
  704. const worksheet = workbook.getWorksheet(1);
  705. if (!worksheet) {
  706. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未发现excel中有工作表');
  707. }
  708. // 获取表头,通过方法的返回值,将写死的表头数组返回 回来
  709. const cols = this.getStucolumn();
  710. // 第一行(表头)
  711. const headRow = worksheet.getRow(1);
  712. // 设置,检查表头
  713. headRow.eachCell((cell, coli) => {
  714. console.log(cell.value);
  715. if (cell.value !== '序号') {
  716. const r = cols.find(f => f.key === cell.value);
  717. if (r) {
  718. const ri = cols.findIndex(f => f.key === cell.value);
  719. // 表头符合要求,做上标记
  720. r.colIndex = coli;
  721. cols[ri] = r;
  722. } else {
  723. throw new BusinessError(`模板中"${cell.value}"列错误,请检查excel!`);
  724. }
  725. }
  726. });
  727. // 检查表头结果,如果有没有 colIndex,说明表头里有不符合要求的,退回去
  728. const excelIsRigth = cols.every(f => f.colIndex);
  729. if (!excelIsRigth) throw new BusinessError(ErrorCode.DATA_INVALID, 'Excel表格格式不正确,请使用系统提供的模板,或重新下载模板!');
  730. // 删除掉第一行 表头行,这不是数据
  731. worksheet.spliceRows(0, 1);
  732. const stuList = [];
  733. const noWhite = str => str.replace(/\s*/g, '');
  734. // 整理数据,根据检查合格的表头行,获取每个格子的数据,制成[object]格式
  735. worksheet.eachRow(row => {
  736. const stu = {};
  737. for (let i = 0; i < cols.length; i++) {
  738. const col = cols[i];
  739. if (!col) break;
  740. let val = _.trim(row.getCell(col.colIndex));
  741. if (col.column === 'id_number') val = val.toUpperCase();
  742. if (val && val !== '') val = noWhite(val);
  743. stu[col.column] = val;
  744. }
  745. stuList.push(stu);
  746. });
  747. return stuList;
  748. }
  749. // 数据校验
  750. async checkData(stuList) {
  751. const cols = this.getStucolumn();
  752. let errorcode = '0';
  753. const errormsg = [];
  754. for (const stu of stuList) {
  755. const { name } = stu;
  756. let error = false;
  757. let msg = '';
  758. // 各个字段检查,最低为非空检查
  759. for (const col of cols) {
  760. const { key, column } = col;
  761. if (!column) throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到导出的字段名');
  762. const val = _.get(stu, column);
  763. // 空校验
  764. if (!val || val === '') {
  765. error = true;
  766. msg = `${msg}"${key}"不能为空;`;
  767. continue;
  768. }
  769. // 性别校验
  770. if (column === 'gender') {
  771. if (!(val.includes('男') || val.includes('女'))) {
  772. error = true;
  773. msg = `${msg}性别错误;`;
  774. }
  775. continue;
  776. }
  777. // 学校名称
  778. if (column === 'school_name') {
  779. const school = await this.sModel.findOne({ name: { $regex: val } });
  780. if (!school) {
  781. error = true;
  782. msg = `${msg}系统中无此学校;`;
  783. }
  784. continue;
  785. }
  786. // 身份证号校验
  787. if (column === 'id_number') {
  788. const { pass, msg: idmsg } = this.ctx.service.school.idCodeValid(val);
  789. if (!pass) {
  790. error = true;
  791. msg = `${msg}${idmsg};`;
  792. }
  793. const have_same = stuList.filter(f => f.id_number === val && f.name !== name);
  794. if (have_same.length > 0) {
  795. error = true;
  796. const h = _.head(have_same);
  797. const num = have_same.length;
  798. if (num === 1) {
  799. msg = `${msg}身份证号与本次名单的"${h.name}"重复;`;
  800. } else msg = `${msg}身份证号与本次名单中"${h.name}"等${num}人重复;`;
  801. }
  802. continue;
  803. }
  804. // 手机号校验
  805. if (column === 'phone') {
  806. if (!/^\d{11}$/i.test(val)) {
  807. error = true;
  808. msg = `${msg}手机号位数不正确;`;
  809. }
  810. const have_same = stuList.filter(f => f.phone === val && f.name !== name);
  811. if (have_same.length > 0) {
  812. error = true;
  813. const h = _.head(have_same);
  814. const num = have_same.length;
  815. if (num === 1) {
  816. msg = `${msg}手机号与本次名单的"${h.name}"重复;`;
  817. } else msg = `${msg}手机号与本次名单中"${h.name}"等${num}人重复;`;
  818. }
  819. continue;
  820. }
  821. // 专业校验
  822. if (column === 'major') {
  823. if (val.includes('专业')) {
  824. error = true;
  825. msg = `${msg}专业列不能含有"专业"二字;`;
  826. }
  827. continue;
  828. }
  829. // 入学年份
  830. if (column === 'entry_year') {
  831. const m = /^\w{4}$/;
  832. if (!val.match(m)) {
  833. error = true;
  834. msg = `${msg}入学年份格式不正确,只填写4位数字;`;
  835. }
  836. continue;
  837. }
  838. // 毕业年份
  839. if (column === 'finish_year') {
  840. const m = /^\w{4}$/;
  841. if (!val.match(m)) {
  842. error = true;
  843. msg = `${msg}毕业年份格式不正确,只填写4位数字;`;
  844. }
  845. continue;
  846. }
  847. // 双困检查
  848. if (column === 'family_is_hard') {
  849. if (!(val.includes('是') || val.includes('否'))) {
  850. error = true;
  851. msg = `${msg}家庭是否困难填写"是"或"否";`;
  852. }
  853. continue;
  854. }
  855. if (column === 'have_grant') {
  856. if (!(val.includes('是') || val.includes('否'))) {
  857. error = true;
  858. msg = `${msg}是否获得过助学金填写"是"或"否";`;
  859. }
  860. continue;
  861. }
  862. }
  863. if (error) {
  864. errorcode = '1';
  865. stu.msg = msg;
  866. errormsg.push(stu);
  867. }
  868. }
  869. return { errorcode, errormsg };
  870. }
  871. // excel中学生字段
  872. getStucolumn() {
  873. const arr = [
  874. { key: '姓名', column: 'name' },
  875. { key: '性别', column: 'gender' },
  876. { key: '民族', column: 'nation' },
  877. { key: '身份证号', column: 'id_number' },
  878. { key: '学校名称', column: 'school_name' },
  879. { key: '学历层次', column: 'edua_level' },
  880. { key: '学制', column: 'edua_system' },
  881. { key: '院(系)', column: 'faculty' },
  882. { key: '专业', column: 'major' },
  883. { key: '入学年份', column: 'entry_year' },
  884. { key: '毕业年份', column: 'finish_year' },
  885. { key: '在校曾担任何种职务', column: 'school_job' },
  886. { key: '手机号', column: 'phone' },
  887. { key: 'QQ号', column: 'qq' },
  888. { key: '家庭所在地', column: 'family_place' },
  889. {
  890. key: '家庭是否困难',
  891. column: 'family_is_hard',
  892. change: [
  893. { key: '否', value: '0' },
  894. { key: '是', value: '1' },
  895. ],
  896. },
  897. {
  898. key: '是否获得过助学金',
  899. column: 'have_grant',
  900. change: [
  901. { key: '否', value: '0' },
  902. { key: '是', value: '1' },
  903. ],
  904. },
  905. ];
  906. return arr;
  907. }
  908. // 最后整合数据
  909. async lastSetData(stuList, data) {
  910. const cols = this.getStucolumn();
  911. const needChange = cols.filter(f => f.change);
  912. const studentList = [];
  913. for (const i of stuList) {
  914. const d = { ...i, ...data };
  915. for (const col of needChange) {
  916. const { column, change } = col;
  917. if (!column && change && _.isArray(change)) continue;
  918. const val = _.get(d, column);
  919. if (!val) continue;
  920. const r = change.find(f => f.key === val);
  921. if (!r) continue;
  922. const { value } = r;
  923. d[column] = value;
  924. }
  925. if (d.school_name) {
  926. const school = await this.sModel.findOne({ name: { $regex: d.school_name } }).lean();
  927. if (school) d.schid = school.code || '';
  928. }
  929. studentList.push(d);
  930. }
  931. return studentList;
  932. }
  933. // 身份证验证
  934. idCodeValid(code) {
  935. // 身份证号合法性验证
  936. // 支持15位和18位身份证号
  937. // 支持地址编码、出生日期、校验位验证
  938. const city = {
  939. 11: '北京',
  940. 12: '天津',
  941. 13: '河北',
  942. 14: '山西',
  943. 15: '内蒙古',
  944. 21: '辽宁',
  945. 22: '吉林',
  946. 23: '黑龙江 ',
  947. 31: '上海',
  948. 32: '江苏',
  949. 33: '浙江',
  950. 34: '安徽',
  951. 35: '福建',
  952. 36: '江西',
  953. 37: '山东',
  954. 41: '河南',
  955. 42: '湖北 ',
  956. 43: '湖南',
  957. 44: '广东',
  958. 45: '广西',
  959. 46: '海南',
  960. 50: '重庆',
  961. 51: '四川',
  962. 52: '贵州',
  963. 53: '云南',
  964. 54: '西藏 ',
  965. 61: '陕西',
  966. 62: '甘肃',
  967. 63: '青海',
  968. 64: '宁夏',
  969. 65: '新疆',
  970. 71: '台湾',
  971. 81: '香港',
  972. 82: '澳门',
  973. 91: '国外 ',
  974. };
  975. let row = {
  976. pass: true,
  977. msg: '验证成功',
  978. };
  979. if (!code || !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/.test(code)) {
  980. row = {
  981. pass: false,
  982. msg: '身份证号格式错误',
  983. };
  984. } else if (!city[code.substr(0, 2)]) {
  985. row = {
  986. pass: false,
  987. msg: '身份证号地址编码错误',
  988. };
  989. } else {
  990. // 18位身份证需要验证最后一位校验位
  991. if (code.length === 18) {
  992. code = code.split('');
  993. // ∑(ai×Wi)(mod 11)
  994. // 加权因子
  995. const factor = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ];
  996. // 校验位
  997. const parity = [ 1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2 ];
  998. let sum = 0;
  999. let ai = 0;
  1000. let wi = 0;
  1001. for (let i = 0; i < 17; i++) {
  1002. ai = code[i];
  1003. wi = factor[i];
  1004. sum += ai * wi;
  1005. }
  1006. if (parity[sum % 11] !== code[17].toUpperCase()) {
  1007. row = {
  1008. pass: false,
  1009. msg: '身份证号校验位错误',
  1010. };
  1011. }
  1012. }
  1013. }
  1014. return row;
  1015. }
  1016. /**
  1017. * 检查学生是否参加过这个计划以外的计划,参加过就不让来了
  1018. * @param {Array} studentList 学生列表
  1019. * @param {String} planid 计划id
  1020. */
  1021. async countStudent(studentList, planid) {
  1022. let errorcode = '0';
  1023. const errormsg = [];
  1024. for (const stu of studentList) {
  1025. const { name, id_number } = stu;
  1026. let error = false;
  1027. let msg = '';
  1028. const count = await this.ctx.model.Student.count({ name, id_number, planid: { $ne: planid } });
  1029. if (count > 0) {
  1030. error = true;
  1031. msg = `${msg}${name}已经参加过培训`;
  1032. }
  1033. if (error) {
  1034. errorcode = '1';
  1035. stu.msg = msg;
  1036. errormsg.push(stu);
  1037. }
  1038. }
  1039. return { errorcode, errormsg };
  1040. }
  1041. }
  1042. module.exports = UtilService;