util.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  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. //2024-07-25 寝室相关移出
  642. let dbStuList = await this.ctx.model.Student.find({ planid, termid, batchid });
  643. if (dbStuList.length > 0) {
  644. // 查这个学校的这期学生是否修改过班级 或 寝室
  645. const is_change = dbStuList.find(f => f.classid ); // f.bedroomid
  646. if (is_change) {
  647. throw new BusinessError(
  648. ErrorCode.BUSINESS,
  649. '该计划该期该批次有学生已经安排班级或寝室 无法进行导入,请对该批次学生进行修改重新导入!'
  650. );
  651. }
  652. }
  653. // 2021-06-07 如果学生已经绑定,那也不允许修改名单了
  654. const countOpenid = await this.ctx.model.Student.count({ planid, termid, batchid, openid: { $exists: true } });
  655. if (countOpenid > 0) throw new BusinessError(ErrorCode.BUSINESS, '已有学生绑定账号,名单无法修改!');
  656. let domain = 'http://127.0.0.1';
  657. if (process.env.NODE_ENV === 'development') domain = 'http://jytz.jilinjobs.cn';
  658. const fullUrl = domain + filepath; // this.ctx.app.config.baseUrl http://127.0.0.1 http://jytz.jilinjobs.cn
  659. let studentList = await this.getDataFromExcel(fullUrl);
  660. const checkRes = await this.checkData(studentList);
  661. const { errorcode } = checkRes;
  662. if (errorcode === '1') {
  663. return checkRes;
  664. }
  665. // 2021-05-26 添加与数据库的对比,如果数据库里已经有这个身份证号,就需要提示
  666. const countStudent = await this.countStudent(studentList, planid);
  667. const { errorcode: csec } = countStudent;
  668. if (csec === '1') {
  669. return countStudent;
  670. }
  671. // 整理数据
  672. studentList = await this.lastSetData(studentList, {
  673. planyearid,
  674. planid,
  675. batchid,
  676. termid,
  677. type,
  678. });
  679. // 复制,删除,添加
  680. if (dbStuList.length > 0) {
  681. dbStuList = JSON.parse(JSON.stringify(dbStuList));
  682. dbStuList = dbStuList.map(i => {
  683. delete i.meta;
  684. i.studentid = _.clone(i._id);
  685. delete i.id;
  686. delete i._id;
  687. return i;
  688. });
  689. await this.ctx.model.Student.deleteMany({ planid, termid, batchid });
  690. await this.ctx.model.Dstudent.insertMany(dbStuList);
  691. }
  692. await this.ctx.model.Student.insertMany(studentList);
  693. return 'ok';
  694. }
  695. // 整理excel数据
  696. async getDataFromExcel(url) {
  697. // 请求文件
  698. const file = await this.ctx.curl(`${url}`);
  699. if (!(file && file.data)) {
  700. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到上传的名单');
  701. }
  702. const workbook = new Excel.Workbook();
  703. // 读取文件
  704. await workbook.xlsx.load(file.data);
  705. const worksheet = workbook.getWorksheet(1);
  706. if (!worksheet) {
  707. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未发现excel中有工作表');
  708. }
  709. // 获取表头,通过方法的返回值,将写死的表头数组返回 回来
  710. const cols = this.getStucolumn();
  711. // 第一行(表头)
  712. const headRow = worksheet.getRow(1);
  713. // 设置,检查表头
  714. headRow.eachCell((cell, coli) => {
  715. console.log(cell.value);
  716. if (cell.value !== '序号') {
  717. const r = cols.find(f => f.key === cell.value);
  718. if (r) {
  719. const ri = cols.findIndex(f => f.key === cell.value);
  720. // 表头符合要求,做上标记
  721. r.colIndex = coli;
  722. cols[ri] = r;
  723. } else {
  724. throw new BusinessError(`模板中"${cell.value}"列错误,请检查excel!`);
  725. }
  726. }
  727. });
  728. // 检查表头结果,如果有没有 colIndex,说明表头里有不符合要求的,退回去
  729. const excelIsRigth = cols.every(f => f.colIndex);
  730. if (!excelIsRigth) throw new BusinessError(ErrorCode.DATA_INVALID, 'Excel表格格式不正确,请使用系统提供的模板,或重新下载模板!');
  731. // 删除掉第一行 表头行,这不是数据
  732. worksheet.spliceRows(0, 1);
  733. const stuList = [];
  734. const noWhite = str => str.replace(/\s*/g, '');
  735. // 整理数据,根据检查合格的表头行,获取每个格子的数据,制成[object]格式
  736. worksheet.eachRow(row => {
  737. const stu = {};
  738. for (let i = 0; i < cols.length; i++) {
  739. const col = cols[i];
  740. if (!col) break;
  741. let val = _.trim(row.getCell(col.colIndex));
  742. if (col.column === 'id_number') val = val.toUpperCase();
  743. if (val && val !== '') val = noWhite(val);
  744. stu[col.column] = val;
  745. }
  746. stuList.push(stu);
  747. });
  748. return stuList;
  749. }
  750. // 数据校验
  751. async checkData(stuList) {
  752. const cols = this.getStucolumn();
  753. let errorcode = '0';
  754. const errormsg = [];
  755. for (const stu of stuList) {
  756. const { name } = stu;
  757. let error = false;
  758. let msg = '';
  759. // 各个字段检查,最低为非空检查
  760. for (const col of cols) {
  761. const { key, column } = col;
  762. if (!column) throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到导出的字段名');
  763. const val = _.get(stu, column);
  764. // 空校验
  765. if (!val || val === '') {
  766. error = true;
  767. msg = `${msg}"${key}"不能为空;`;
  768. continue;
  769. }
  770. // 性别校验
  771. if (column === 'gender') {
  772. if (!(val.includes('男') || val.includes('女'))) {
  773. error = true;
  774. msg = `${msg}性别错误;`;
  775. }
  776. continue;
  777. }
  778. // 学校名称
  779. if (column === 'school_name') {
  780. const school = await this.sModel.findOne({ name: { $regex: val } });
  781. if (!school) {
  782. error = true;
  783. msg = `${msg}系统中无此学校;`;
  784. }
  785. continue;
  786. }
  787. // 身份证号校验
  788. if (column === 'id_number') {
  789. const { pass, msg: idmsg } = this.ctx.service.school.idCodeValid(val);
  790. if (!pass) {
  791. error = true;
  792. msg = `${msg}${idmsg};`;
  793. }
  794. const have_same = stuList.filter(f => f.id_number === val && f.name !== name);
  795. if (have_same.length > 0) {
  796. error = true;
  797. const h = _.head(have_same);
  798. const num = have_same.length;
  799. if (num === 1) {
  800. msg = `${msg}身份证号与本次名单的"${h.name}"重复;`;
  801. } else msg = `${msg}身份证号与本次名单中"${h.name}"等${num}人重复;`;
  802. }
  803. continue;
  804. }
  805. // 手机号校验
  806. if (column === 'phone') {
  807. if (!/^\d{11}$/i.test(val)) {
  808. error = true;
  809. msg = `${msg}手机号位数不正确;`;
  810. }
  811. const have_same = stuList.filter(f => f.phone === val && f.name !== name);
  812. if (have_same.length > 0) {
  813. error = true;
  814. const h = _.head(have_same);
  815. const num = have_same.length;
  816. if (num === 1) {
  817. msg = `${msg}手机号与本次名单的"${h.name}"重复;`;
  818. } else msg = `${msg}手机号与本次名单中"${h.name}"等${num}人重复;`;
  819. }
  820. continue;
  821. }
  822. // 专业校验
  823. if (column === 'major') {
  824. if (val.includes('专业')) {
  825. error = true;
  826. msg = `${msg}专业列不能含有"专业"二字;`;
  827. }
  828. continue;
  829. }
  830. // 入学年份
  831. if (column === 'entry_year') {
  832. const m = /^\w{4}$/;
  833. if (!val.match(m)) {
  834. error = true;
  835. msg = `${msg}入学年份格式不正确,只填写4位数字;`;
  836. }
  837. continue;
  838. }
  839. // 毕业年份
  840. if (column === 'finish_year') {
  841. const m = /^\w{4}$/;
  842. if (!val.match(m)) {
  843. error = true;
  844. msg = `${msg}毕业年份格式不正确,只填写4位数字;`;
  845. }
  846. continue;
  847. }
  848. // 双困检查
  849. if (column === 'family_is_hard') {
  850. if (!(val.includes('是') || val.includes('否'))) {
  851. error = true;
  852. msg = `${msg}家庭是否困难填写"是"或"否";`;
  853. }
  854. continue;
  855. }
  856. if (column === 'have_grant') {
  857. if (!(val.includes('是') || val.includes('否'))) {
  858. error = true;
  859. msg = `${msg}是否获得过助学金填写"是"或"否";`;
  860. }
  861. continue;
  862. }
  863. }
  864. if (error) {
  865. errorcode = '1';
  866. stu.msg = msg;
  867. errormsg.push(stu);
  868. }
  869. }
  870. return { errorcode, errormsg };
  871. }
  872. // excel中学生字段
  873. getStucolumn() {
  874. const arr = [
  875. { key: '姓名', column: 'name' },
  876. { key: '性别', column: 'gender' },
  877. { key: '民族', column: 'nation' },
  878. { key: '身份证号', column: 'id_number' },
  879. { key: '学校名称', column: 'school_name' },
  880. { key: '学历层次', column: 'edua_level' },
  881. { key: '学制', column: 'edua_system' },
  882. { key: '院(系)', column: 'faculty' },
  883. { key: '专业', column: 'major' },
  884. { key: '入学年份', column: 'entry_year' },
  885. { key: '毕业年份', column: 'finish_year' },
  886. { key: '在校曾担任何种职务', column: 'school_job' },
  887. { key: '手机号', column: 'phone' },
  888. { key: 'QQ号', column: 'qq' },
  889. { key: '家庭所在地', column: 'family_place' },
  890. {
  891. key: '家庭是否困难',
  892. column: 'family_is_hard',
  893. change: [
  894. { key: '否', value: '0' },
  895. { key: '是', value: '1' },
  896. ],
  897. },
  898. {
  899. key: '是否获得过助学金',
  900. column: 'have_grant',
  901. change: [
  902. { key: '否', value: '0' },
  903. { key: '是', value: '1' },
  904. ],
  905. },
  906. ];
  907. return arr;
  908. }
  909. // 最后整合数据
  910. async lastSetData(stuList, data) {
  911. const cols = this.getStucolumn();
  912. const needChange = cols.filter(f => f.change);
  913. const studentList = [];
  914. for (const i of stuList) {
  915. const d = { ...i, ...data };
  916. for (const col of needChange) {
  917. const { column, change } = col;
  918. if (!column && change && _.isArray(change)) continue;
  919. const val = _.get(d, column);
  920. if (!val) continue;
  921. const r = change.find(f => f.key === val);
  922. if (!r) continue;
  923. const { value } = r;
  924. d[column] = value;
  925. }
  926. if (d.school_name) {
  927. const school = await this.sModel.findOne({ name: { $regex: d.school_name } }).lean();
  928. if (school) d.schid = school.code || '';
  929. }
  930. studentList.push(d);
  931. }
  932. return studentList;
  933. }
  934. // 身份证验证
  935. idCodeValid(code) {
  936. // 身份证号合法性验证
  937. // 支持15位和18位身份证号
  938. // 支持地址编码、出生日期、校验位验证
  939. const city = {
  940. 11: '北京',
  941. 12: '天津',
  942. 13: '河北',
  943. 14: '山西',
  944. 15: '内蒙古',
  945. 21: '辽宁',
  946. 22: '吉林',
  947. 23: '黑龙江 ',
  948. 31: '上海',
  949. 32: '江苏',
  950. 33: '浙江',
  951. 34: '安徽',
  952. 35: '福建',
  953. 36: '江西',
  954. 37: '山东',
  955. 41: '河南',
  956. 42: '湖北 ',
  957. 43: '湖南',
  958. 44: '广东',
  959. 45: '广西',
  960. 46: '海南',
  961. 50: '重庆',
  962. 51: '四川',
  963. 52: '贵州',
  964. 53: '云南',
  965. 54: '西藏 ',
  966. 61: '陕西',
  967. 62: '甘肃',
  968. 63: '青海',
  969. 64: '宁夏',
  970. 65: '新疆',
  971. 71: '台湾',
  972. 81: '香港',
  973. 82: '澳门',
  974. 91: '国外 ',
  975. };
  976. let row = {
  977. pass: true,
  978. msg: '验证成功',
  979. };
  980. 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)) {
  981. row = {
  982. pass: false,
  983. msg: '身份证号格式错误',
  984. };
  985. } else if (!city[code.substr(0, 2)]) {
  986. row = {
  987. pass: false,
  988. msg: '身份证号地址编码错误',
  989. };
  990. } else {
  991. // 18位身份证需要验证最后一位校验位
  992. if (code.length === 18) {
  993. code = code.split('');
  994. // ∑(ai×Wi)(mod 11)
  995. // 加权因子
  996. const factor = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ];
  997. // 校验位
  998. const parity = [ 1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2 ];
  999. let sum = 0;
  1000. let ai = 0;
  1001. let wi = 0;
  1002. for (let i = 0; i < 17; i++) {
  1003. ai = code[i];
  1004. wi = factor[i];
  1005. sum += ai * wi;
  1006. }
  1007. if (parity[sum % 11] !== code[17].toUpperCase()) {
  1008. row = {
  1009. pass: false,
  1010. msg: '身份证号校验位错误',
  1011. };
  1012. }
  1013. }
  1014. }
  1015. return row;
  1016. }
  1017. /**
  1018. * 检查学生是否参加过这个计划以外的计划,参加过就不让来了
  1019. * @param {Array} studentList 学生列表
  1020. * @param {String} planid 计划id
  1021. */
  1022. async countStudent(studentList, planid) {
  1023. let errorcode = '0';
  1024. const errormsg = [];
  1025. for (const stu of studentList) {
  1026. const { name, id_number } = stu;
  1027. let error = false;
  1028. let msg = '';
  1029. const count = await this.ctx.model.Student.count({ name, id_number, planid: { $ne: planid } });
  1030. if (count > 0) {
  1031. error = true;
  1032. msg = `${msg}${name}已经参加过培训`;
  1033. }
  1034. if (error) {
  1035. errorcode = '1';
  1036. stu.msg = msg;
  1037. errormsg.push(stu);
  1038. }
  1039. }
  1040. return { errorcode, errormsg };
  1041. }
  1042. }
  1043. module.exports = UtilService;