util.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. 'use strict';
  2. const _ = require('lodash');
  3. const fs = require('fs');
  4. const Excel = require('exceljs');
  5. const { CrudService } = require('naf-framework-mongoose/lib/service');
  6. const { BusinessError, ErrorCode } = require('naf-core').Error;
  7. const moment = require('moment');
  8. const nodemailer = require('nodemailer');
  9. const docx = require('docx');
  10. const archiver = require('archiver');
  11. class UtilService extends CrudService {
  12. constructor(ctx) {
  13. super(ctx);
  14. this.mq = this.ctx.mq;
  15. }
  16. async updatedate() {
  17. let date = new Date();
  18. date = moment(date).format('YYYY-MM-DD HH:mm:ss');
  19. return date;
  20. }
  21. async sendMail(email, subject, text, html) {
  22. const setting = await this.ctx.model.Setting.findOne();
  23. let user_email = this.ctx.app.config.user_email;
  24. let auth_code = this.ctx.app.config.auth_code;
  25. if (setting) {
  26. user_email = setting.user_email;
  27. auth_code = setting.auth_code;
  28. }
  29. const transporter = nodemailer.createTransport({
  30. host: 'smtp.exmail.qq.com',
  31. secureConnection: true,
  32. port: 465,
  33. auth: {
  34. user: user_email, // 账号
  35. pass: auth_code, // 授权码
  36. },
  37. });
  38. const mailOptions = {
  39. from: user_email, // 发送者,与上面的user一致
  40. to: email, // 接收者,可以同时发送多个,以逗号隔开
  41. subject, // 标题
  42. text, // 文本
  43. html,
  44. };
  45. try {
  46. await transporter.sendMail(mailOptions);
  47. return true;
  48. } catch (err) {
  49. return false;
  50. }
  51. }
  52. async findone({ modelname }, data) {
  53. // 查询单条
  54. const _model = _.capitalize(modelname);
  55. const res = await this.ctx.model[_model].findOne({ ...data });
  56. return res;
  57. }
  58. async findbyids({ modelname }, { data }) {
  59. // 共通批量查询方法
  60. const _model = _.capitalize(modelname);
  61. const res = [];
  62. for (const elm of data) {
  63. const result = await this.ctx.model[_model].findById(elm);
  64. res.push(result);
  65. }
  66. return res;
  67. }
  68. async findmodel({ modelname }) {
  69. const _model = _.capitalize(modelname);
  70. const data = this.ctx.model[_model].prototype.schema.obj;
  71. const keys = Object.keys(data);
  72. const res = {};
  73. for (const k of keys) {
  74. const obj = data[k];
  75. if (_.get(obj, 'zh')) res[k] = obj;
  76. }
  77. return res;
  78. }
  79. async utilMethod(query, body) {
  80. // // 重置班级
  81. // 找到要删除的学生
  82. const res = await this.ctx.model.Student.find({ school_name: '吉林职业技术学院', planid: '60769b703cead37068645fb2', termid: { $ne: '60779c0b2ec7ac704ce301ab' } });
  83. // const test = res.map(f => {
  84. // return { name: f.name, openid: f.openid, id: f._id };
  85. // });
  86. const ids = res.map(i => i._id);
  87. await this.ctx.model.User.deleteMany({ uid: ids });
  88. await this.ctx.model.Student.deleteMany({ school_name: '吉林职业技术学院', planid: '60769b703cead37068645fb2', termid: { $ne: '60779c0b2ec7ac704ce301ab' } });
  89. // const ids = res.map(i => i.openid);
  90. // // console.log(ids);
  91. //
  92. // console.log(userRes.length);
  93. // const { planid, termid, batchid, classid } = query;
  94. // const filters = {};
  95. // if (classid) filters.classid = classid;
  96. // else if (batchid)filters.batchid = batchid;
  97. // else if (termid)filters.termid = termid;
  98. // else if (planid)filters.planid = planid;
  99. // else throw new BusinessError(ErrorCode.BADPARAM, '需要重置范围');
  100. // await this.ctx.model.Student.updateMany(filters, { classid: undefined });
  101. // // 重置班级结束
  102. }
  103. async getQueryOptions({ skip, limit, sort, desc } = {}) {
  104. if (sort && _.isString(sort)) {
  105. sort = { [sort]: desc ? -1 : 1 };
  106. } else if (sort && _.isArray(sort)) {
  107. sort = sort
  108. .map(f => ({ [f]: desc ? -1 : 1 }))
  109. .reduce((p, c) => ({ ...p, ...c }), {});
  110. }
  111. return { skip, limit, sort };
  112. }
  113. /**
  114. * 更新进度,状态 不存在/完成以外(不是2的值)的数值, 不添加参数,只更新进度
  115. * @param {String} missionid 任务id
  116. * @param {String} progress 进度(人为划分)
  117. * @param {String} status 状态
  118. * @param {Object} params 参数
  119. */
  120. async updateProcess(missionid, progress, status, params) {
  121. try {
  122. if (!missionid) return;
  123. const { baseUrl } = _.get(this.ctx.app.config, 'mission');
  124. let url = `${baseUrl}`;
  125. let data;
  126. if (!baseUrl) return;
  127. if (!status || status !== '2') {
  128. url = `${url}/api/mission/progress`;
  129. data = { id: missionid, progress };
  130. } else if (status === '3') {
  131. url = `${url}/api/mission/update/${missionid}`;
  132. data = { status };
  133. } else {
  134. url = `${url}/api/mission/update/${missionid}`;
  135. data = { progress: '100', params, status };
  136. }
  137. await this.ctx.curl(url, {
  138. method: 'post',
  139. headers: {
  140. 'content-type': 'application/json',
  141. },
  142. data,
  143. dataType: 'json',
  144. });
  145. } catch (error) {
  146. console.error(`任务更新进度报错,missionid:${missionid}\n`);
  147. }
  148. }
  149. async teacherImport() {
  150. // const filepath = './teacherlist.xlsx';
  151. // const workbook = new Excel.Workbook();
  152. // await workbook.xlsx.readFile(filepath);
  153. // const worksheet = workbook.getWorksheet(1);
  154. // if (!worksheet) return;
  155. // let arr = [];
  156. // worksheet.eachRow((row, ri) => {
  157. // if (ri !== 1) {
  158. // const obj = {};
  159. // obj.name = row.getCell(3).value || undefined;
  160. // obj.department = row.getCell(4).value || undefined;
  161. // if (row.getCell(5).value) obj.job = row.getCell(5).value;
  162. // obj.phone = row.getCell(6).value || undefined;
  163. // obj.status = '4';
  164. // arr.push(obj);
  165. // }
  166. // });
  167. // // 检查谁生成过了, user表和teacher表
  168. // let ur = await this.ctx.model.User.find({ mobile: { $in: arr.map(i => i.phone) }, type: '3' });
  169. // let tr = await this.ctx.model.Teacher.find({ phone: { $in: arr.map(i => i.phone) } });
  170. // // 将有的老师过滤出去
  171. // if (ur) {
  172. // ur = JSON.parse(JSON.stringify(ur));
  173. // arr = arr.filter(f => !ur.find(uf => `${uf.mobile}` === `${f.phone}`));
  174. // }
  175. // if (tr) {
  176. // tr = JSON.parse(JSON.stringify(tr));
  177. // arr = arr.filter(f => !(tr.find(tf => `${tf.phone}` === `${f.phone}`)));
  178. // }
  179. // for (const tea of arr) {
  180. // const ctr = await this.ctx.model.Teacher.create(tea);
  181. // if (ctr) {
  182. // const obj = { name: tea.name, mobile: tea.phone, type: '3', uid: ctr._id };
  183. // const cur = await this.ctx.model.User.create(obj);
  184. // }
  185. // }
  186. // const user = await this.ctx.model.User.find({ passwd: { $exists: false } });
  187. // for (const u of user) {
  188. // u.passwd = { secret: '12345678' };
  189. // u.save();
  190. // }
  191. }
  192. /**
  193. * 导出excel
  194. * @param {Array} dataList 数据集合
  195. * @param {Array} meta 表头(可以没有)
  196. * @param {String} fn 文件名
  197. * @param {Array} opera 单元格操作
  198. */
  199. async toExcel(dataList, meta, fn = '导出结果', opera) {
  200. // 导出excel
  201. const { app } = this;
  202. const nowDate = new Date().getTime();
  203. const filename = `${fn}-${nowDate}.xlsx`;
  204. // 取出预设存储地址
  205. const rootPath = `${app.config.cdn.repos_root_path}`;
  206. const rooturl = `${app.config.cdn.repos_root_url_excel}`;
  207. let path = `${rootPath}${rooturl}`;
  208. if (!path) {
  209. throw new BusinessError(ErrorCode.BUSINESS, '服务端没有设置存储路径');
  210. }
  211. if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  212. if (!fs.existsSync(path)) {
  213. // 如果不存在文件夹,就创建
  214. fs.mkdirSync(path);
  215. }
  216. // 生成文件
  217. const filepath = `${path}${filename}`;
  218. fs.createWriteStream(filepath);
  219. const workbook = new Excel.Workbook();
  220. const sheet = workbook.addWorksheet('sheet');
  221. if (meta)sheet.columns = meta;
  222. sheet.addRows(dataList);
  223. if (_.isArray(opera)) {
  224. for (const o of opera) {
  225. const { startRow, startCol, endRow, endCol } = o;
  226. sheet.mergeCells(startRow, startCol, endRow, endCol);
  227. }
  228. }
  229. // 垂直居中
  230. const length = dataList.length;
  231. const alignment = { vertical: 'middle', horizontal: 'center' };
  232. for (let i = 1; i <= length; i++) {
  233. sheet.getRow(i).alignment = alignment;
  234. }
  235. await workbook.xlsx.writeFile(filepath);
  236. return `/files/excel/${filename}`;
  237. }
  238. /**
  239. * 创建/重复写入excel
  240. * @param {Array} dataList 数据
  241. * @param {String} fn 文件名, 第一次进入时,只是单纯的名,第二次开始,有后缀与时间戳
  242. * @param {String} downloadPath 下载文件路径
  243. * @param {String} type 方向,没有默认横向
  244. */
  245. async toAsyncExcel(dataList, fn = '导出结果', downloadPath, type) {
  246. const { app } = this;
  247. const workbook = new Excel.Workbook();
  248. let sheet;
  249. // 取出预设存储地址
  250. const rootPath = `${app.config.cdn.repos_root_path}`;
  251. const rooturl = `${app.config.cdn.repos_root_url_excel}`;
  252. let path = `${rootPath}${rooturl}`;
  253. let filepath = '';
  254. if (!path) {
  255. throw new BusinessError(ErrorCode.BUSINESS, '服务端没有设置存储路径');
  256. }
  257. if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\excel\\';
  258. if (!fs.existsSync(path)) {
  259. // 如果不存在文件夹,就创建
  260. fs.mkdirSync(path);
  261. }
  262. if (!downloadPath) {
  263. // 第一次进入,文件还未生成
  264. const nowDate = new Date().getTime();
  265. fn = `${fn}-${nowDate}.xlsx`;
  266. sheet = workbook.addWorksheet('sheet');
  267. } else {
  268. let domain = 'http://127.0.0.1';
  269. if (process.env.NODE_ENV === 'development')domain = `${domain}:8000`;
  270. const file = await this.ctx.curl(`${domain}${downloadPath}`);
  271. if (!(file && file.data)) {
  272. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找导出的excel');
  273. }
  274. // 读取文件
  275. await workbook.xlsx.load(file.data);
  276. sheet = workbook.getWorksheet('sheet');
  277. }
  278. if (!type || type === 'horizontal') sheet.addRows(dataList);
  279. else if (type === 'vertical') {
  280. for (let i = 1; i <= dataList.length; i++) {
  281. const element = dataList[i - 1];
  282. const rows = sheet.getRow(i);
  283. if (rows.values.length <= 0) rows.values = element;
  284. else rows.values = rows.values.concat(element);
  285. rows.commit();
  286. }
  287. }
  288. filepath = `${path}${fn}`;
  289. await workbook.xlsx.writeFile(filepath);
  290. return { downloadPath: `/files/excel/${fn}`, fn };
  291. }
  292. /**
  293. * 导出docx
  294. * @param {Array} data 数据[{title,content([]),author}]
  295. * @param {String} fn 文件名
  296. */
  297. async toDocx(data, fn = '培训心得') {
  298. const {
  299. Document,
  300. Packer,
  301. Paragraph,
  302. TextRun,
  303. HeadingLevel,
  304. AlignmentType,
  305. } = docx;
  306. const doc = new Document();
  307. const children = [];
  308. // 整理数据
  309. for (let i = 0; i < data.length; i++) {
  310. const obj = data[i];
  311. const { title, content, author } = obj;
  312. const c = [];
  313. if (title) {
  314. const tit = new Paragraph({
  315. children: [ new TextRun({ text: title, bold: true }) ],
  316. heading: HeadingLevel.TITLE,
  317. alignment: AlignmentType.CENTER,
  318. });
  319. c.push(tit);
  320. }
  321. if (author) {
  322. const auth = new Paragraph({
  323. children: [ new TextRun({ color: '#000000', text: author }) ],
  324. heading: HeadingLevel.HEADING_2,
  325. alignment: AlignmentType.RIGHT,
  326. });
  327. c.push(auth);
  328. }
  329. if (content && _.isArray(content) && content.length > 0) {
  330. for (const cont of content) {
  331. const p = new Paragraph({
  332. children: [ new TextRun({ text: cont, bold: true }) ],
  333. });
  334. c.push(p);
  335. }
  336. }
  337. if (i !== data.length - 1) {
  338. // 换页
  339. const last = new Paragraph({
  340. pageBreakBefore: true,
  341. });
  342. c.push(last);
  343. }
  344. if (c.length > 0) children.push(...c);
  345. }
  346. doc.addSection({
  347. properties: {},
  348. children,
  349. });
  350. const { app } = this;
  351. const rootPath = `${app.config.cdn.repos_root_path}`;
  352. const rooturl = `${app.config.cdn.repos_root_url_experience}`;
  353. let path = `${rootPath}${rooturl}`;
  354. // 如果不存在文件夹,就创建
  355. if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  356. if (!fs.existsSync(path)) {
  357. fs.mkdirSync(path);
  358. }
  359. const num = new Date().getTime();
  360. const buffer = await Packer.toBuffer(doc);
  361. fs.writeFileSync(`${path}${fn}-${num}.docx`, buffer);
  362. // Packer.toBuffer(doc).then(buffer => {
  363. // fs.writeFileSync(`${path}${fn}-${num}.docx`, buffer);
  364. // });
  365. return `/files${rooturl}${fn}-${num}.docx`;
  366. }
  367. /**
  368. * 将选择的文件导出到zip压缩包中,提供下载
  369. * @param {*} fileList 需要导入到zip中的列表,格式有2中: [{url:""}]或[String]
  370. * @param {*} fn 文件名,默认为 "导出结果"
  371. */
  372. async toZip(fileList, fn = '导出结果') {
  373. if (!_.isArray(fileList)) {
  374. throw new BusinessError(
  375. ErrorCode.DATA_INVALID,
  376. '需要压缩的文件数据格式错误'
  377. );
  378. }
  379. fn = `${fn}.zip`;
  380. // zip文件夹创建
  381. const { app } = this;
  382. const rootPath = `${app.config.cdn.repos_root_path}`;
  383. const zipPath = `${app.config.cdn.repos_root_url_zip}`;
  384. let path = `${rootPath}${zipPath}`;
  385. if (process.env.NODE_ENV === 'development') path = 'E:\\exportFile\\';
  386. if (!fs.existsSync(path)) {
  387. fs.mkdirSync(path);
  388. }
  389. // 文件请求后将数据整理到这里
  390. const resetFileList = [];
  391. for (const file of fileList) {
  392. let uri = '';
  393. let filename = '';
  394. let prefixs;
  395. if (_.isString(file)) {
  396. uri = file;
  397. const arr = file.split('/');
  398. const last = _.last(arr);
  399. if (last) filename = last;
  400. } else if (_.isObject(file)) {
  401. const { uri: furi, url: furl, name, prefix } = file;
  402. if (furi) uri = furi;
  403. else if (furl) uri = furl;
  404. if (name) filename = name;
  405. else {
  406. const arr = uri.split('/');
  407. const last = _.last(arr);
  408. if (last) filename = last;
  409. }
  410. if (prefix) prefixs = prefix;
  411. }
  412. const obj = {};
  413. if (uri) obj.uri = uri;
  414. if (filename) obj.filename = filename;
  415. if (prefixs) obj.prefix = prefixs;
  416. resetFileList.push(obj);
  417. }
  418. // 导出
  419. const output = fs.createWriteStream(`${path}${fn}`);
  420. const archive = archiver('zip', {
  421. zlib: { level: 9 },
  422. });
  423. archive.pipe(output);
  424. // 请求文件,追加进压缩包
  425. for (const file of resetFileList) {
  426. const { uri, filename, prefix } = file;
  427. const res = await this.ctx.curl(`http://127.0.0.1${uri}`);
  428. if (res && res.data) {
  429. const options = {};
  430. if (filename) options.name = filename;
  431. if (prefix) options.prefix = prefix;
  432. if (filename) {
  433. archive.append(res.data, options);
  434. }
  435. }
  436. }
  437. await archive.finalize();
  438. return `/files${zipPath}${fn}`;
  439. }
  440. }
  441. module.exports = UtilService;