questionnaire.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. 'use strict';
  2. const assert = require('assert');
  3. const _ = require('lodash');
  4. const moment = require('moment');
  5. const { ObjectId } = require('mongoose').Types;
  6. const { CrudService } = require('naf-framework-mongoose/lib/service');
  7. const { BusinessError, ErrorCode } = require('naf-core').Error;
  8. class QuestionnaireService extends CrudService {
  9. constructor(ctx) {
  10. super(ctx, 'qusetionnaire');
  11. this.questionnairemodel = this.ctx.model.Questionnaire;
  12. this.questionmodel = this.ctx.model.Question;
  13. const { baseUrl } = _.get(this.ctx.app.config, 'mission');
  14. if (baseUrl) this.missionBase = baseUrl;
  15. }
  16. // 插入问卷
  17. async create(data) {
  18. const { name, num, type, question } = data;
  19. assert(name, '问卷名称不能为空');
  20. assert(num, '问卷序号不能为空');
  21. assert(type, '问卷类型不能为空');
  22. const quedata = [];
  23. for (const elm of question) {
  24. const ques = await this.questionmodel.findById(elm);
  25. if (ques) {
  26. quedata.push(ques);
  27. }
  28. }
  29. data.question = quedata;
  30. return await this.questionnairemodel.create(data);
  31. }
  32. // 根据id删除问卷
  33. async delete({ id }) {
  34. await this.questionnairemodel.findByIdAndDelete(id);
  35. return 'deleted';
  36. }
  37. // 根据id更新问卷信息
  38. async update({ id }, data) {
  39. const { name, num, type, question } = data;
  40. const questionnaire = await this.questionnairemodel.findById(id);
  41. if (name) {
  42. questionnaire.name = name;
  43. }
  44. if (num) {
  45. questionnaire.num = num;
  46. }
  47. if (type) {
  48. questionnaire.type = type;
  49. }
  50. if (question) {
  51. questionnaire.question = question;
  52. }
  53. return await questionnaire.save();
  54. }
  55. // 查询
  56. async query({ skip, limit, ...num }) {
  57. const total = await this.questionnairemodel.count(num);
  58. const data = await this.questionnairemodel
  59. .find(num)
  60. .skip(Number(skip))
  61. .limit(Number(limit));
  62. const result = { total, data };
  63. return result;
  64. }
  65. // 查询详情
  66. async show({ id }) {
  67. const questionnaire = await this.questionnairemodel.findById(id);
  68. return questionnaire;
  69. }
  70. // 建立任务
  71. async toExport(body) {
  72. const { range, questionnaireid } = body;
  73. const questionnaire = await this.questionnairemodel.findById(
  74. questionnaireid
  75. );
  76. if (!questionnaire) {
  77. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到问卷信息');
  78. }
  79. const fn = await this.toSetFileName(questionnaire.name, range);
  80. const data = {
  81. title: fn,
  82. params: {
  83. project: 'center',
  84. service: 'questionnaire',
  85. method: 'export',
  86. body,
  87. },
  88. };
  89. if (this.missionBase) {
  90. const url = `${this.missionBase}/api/mission`;
  91. const res = await this.ctx.curl(url, {
  92. method: 'post',
  93. headers: {
  94. 'content-type': 'application/json',
  95. },
  96. data,
  97. dataType: 'json',
  98. });
  99. if (res.status !== 200 || res.data.errcode !== 0) {
  100. throw new BusinessError(ErrorCode.SERVICE_FAULT, '创建任务失败');
  101. }
  102. } else {
  103. throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到任务项目设置');
  104. }
  105. }
  106. /**
  107. * 导出问卷
  108. * @param {Object} { range, direction, questionnaireid, modelList, missionid } 数据集合
  109. * @property Object range 学生的查询范围
  110. * @property String direction horizontal横向/vertical纵向
  111. * @property String questionnaireid 问卷id
  112. * @property Array modelList 要导出的字段,学生和问卷都在这里, 学生的table:student, 问卷的table:questionnaire
  113. * @property String missionid 任务id
  114. */
  115. async export({ range, direction, questionnaireid, modelList, missionid }) {
  116. assert(missionid, '缺少任务信息,无法执行任务');
  117. try {
  118. // 将期批班转换
  119. modelList = modelList.map(i => {
  120. const { model } = i;
  121. if (model === 'termid') i.model = 'termname';
  122. else if (model === 'batchid') i.model = 'batchname';
  123. else if (model === 'classid') i.model = 'classname';
  124. return i;
  125. });
  126. // 获取问卷
  127. const questionnaire = await this.questionnairemodel.findById(questionnaireid);
  128. if (!questionnaire) {
  129. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到问卷信息');
  130. }
  131. let fn = await this.toSetFileName(questionnaire.name, range);
  132. // this.ctx.service.util.updateProcess(missionid, '25');
  133. // 修改条件,termid变成数组了,需要一个一个查出来
  134. const { planid, batchid, classid } = range;
  135. let { termid } = range;
  136. const queryObject = {};
  137. if (planid) queryObject.planid = planid;
  138. if (batchid) queryObject.batchid = batchid;
  139. if (classid) queryObject.classid = classid;
  140. let count = 0;
  141. let dp;
  142. const qkeys = Object.keys(range);
  143. if (qkeys.length === 2 && termid.length <= 0) {
  144. // 说明只有planid
  145. const plan = await this.ctx.model.Trainplan.findOne({ _id: ObjectId(planid) });
  146. if (!plan) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到指定年度计划');
  147. termid = plan.termnum.map(i => JSON.parse(JSON.stringify(i._id)));
  148. }
  149. for (const t of termid) {
  150. queryObject.termid = t;
  151. let data = await this.toGetData(
  152. queryObject,
  153. questionnaireid,
  154. direction,
  155. modelList
  156. );
  157. if (!data) continue;
  158. if (count !== 0 && direction === 'horizontal') data.shift();
  159. if (count !== 0 && direction === 'vertical') {
  160. data = data.map(i => {
  161. i.shift();
  162. return i;
  163. });
  164. }
  165. const {
  166. downloadPath,
  167. fn: newFileName,
  168. } = await this.ctx.service.util.toAsyncExcel(data, fn, dp, direction);
  169. dp = downloadPath;
  170. fn = newFileName;
  171. count++;
  172. }
  173. this.ctx.service.util.updateProcess(missionid, '100', '2', {
  174. uri: dp,
  175. });
  176. } catch (error) {
  177. console.log(error);
  178. this.ctx.service.util.updateProcess(missionid, undefined, '3');
  179. }
  180. }
  181. /**
  182. * 获取学生与学生回答的问卷
  183. * @param {Object} condition 查询学生和学生回答问卷的条件
  184. * @param {String} questionnaireid 问卷id
  185. * @param {String} direction 方向
  186. * @param {Array} modelList 导出字段
  187. */
  188. async toGetData(condition, questionnaireid, direction, modelList) {
  189. // 获取学生
  190. let studentList = await this.ctx.service.student.query(condition);
  191. if (studentList.length <= 0) {
  192. this.ctx.logger.error(`${JSON.stringify(condition)}条件下,未找到任何信息`);
  193. return;
  194. // throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到任何学生信息');
  195. }
  196. studentList = JSON.parse(JSON.stringify(studentList));
  197. // 再获取问卷答案
  198. const questAnswerList = await this.ctx.model.Uploadquestion.find({
  199. ...condition,
  200. questionnaireid,
  201. });
  202. if (!questAnswerList) {
  203. this.ctx.logger.error(`${JSON.stringify(condition)}&${questionnaireid}条件下,未找到任何完成的问卷`);
  204. return;
  205. }
  206. let res = [];
  207. let data = [];
  208. if (direction === 'horizontal') {
  209. const head = this.getHead(direction, modelList, studentList.length);
  210. data = this.horizontalSetData(studentList, questAnswerList, modelList);
  211. res.push(head.map(i => i.header));
  212. for (const d of data) {
  213. const mid = [];
  214. for (const h of head) {
  215. const { key } = h;
  216. mid.push(_.get(d, key, ''));
  217. }
  218. res.push(mid);
  219. }
  220. } else if (direction === 'vertical') {
  221. data = this.verticaSetData(studentList, questAnswerList, modelList);
  222. res = data.map(i => Object.values(i));
  223. } else {
  224. throw new BusinessError(
  225. ErrorCode.DATA_NOT_EXIST,
  226. '未找到excel的表头排列方式'
  227. );
  228. }
  229. return res;
  230. }
  231. /**
  232. * 获得导出文件的文件名
  233. * @param {String} questName 问卷名
  234. * @param {Object} range 范围(计划-期-批-班)
  235. * @return {String} fn 文件名
  236. */
  237. async toSetFileName(questName, range) {
  238. let fn = `-${questName}`;
  239. const { planid, termid, batchid, classid } = range;
  240. if (classid) {
  241. const cla = await this.ctx.service.class.fetch({ id: classid });
  242. if (cla) {
  243. const { name, term } = cla;
  244. if (name) fn = `${name.includes('班') ? name : `${name}班`}${fn}`;
  245. if (term) fn = `第${term}期${fn}`;
  246. }
  247. } else if (batchid) {
  248. const tid = _.head(termid);
  249. const obj = await this.toGetFn(tid, batchid);
  250. if (obj) {
  251. const { term, batch } = obj;
  252. if (batch) fn = `第${batch}批${fn}`;
  253. if (term) fn = `第${term}期${fn}`;
  254. }
  255. } else if (termid) {
  256. if (termid.length === 1) {
  257. const obj = await this.toGetFn(_.head(termid));
  258. if (obj) {
  259. const { term } = obj;
  260. if (term) fn = `第${term}期${fn}`;
  261. }
  262. } else {
  263. let tStr = '';
  264. for (let i = 0; i < termid.length; i++) {
  265. const tid = termid[i];
  266. const obj = await this.toGetFn(tid);
  267. if (obj) {
  268. const { term } = obj;
  269. if (term) {
  270. if (i === 0) {
  271. tStr += `${term}期`;
  272. } else {
  273. tStr += `,${term}期`;
  274. }
  275. }
  276. }
  277. }
  278. fn = `${tStr}${fn}`;
  279. }
  280. } else {
  281. const trainPlan = await this.ctx.model.Trainplan.findById(planid);
  282. if (trainPlan) {
  283. const { title } = trainPlan;
  284. if (title) fn = `${title}${fn}`;
  285. }
  286. }
  287. return fn;
  288. }
  289. /**
  290. * 获取文件的期/期批
  291. * @param {String} termid 期id
  292. * @param {String} batchid 批id
  293. */
  294. async toGetFn(termid, batchid) {
  295. const trainPlan = await this.ctx.model.Trainplan.findOne({
  296. 'termnum._id': ObjectId(termid),
  297. });
  298. if (!trainPlan) {
  299. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到年度计划信息');
  300. }
  301. const { termnum } = trainPlan;
  302. if (!termnum) {
  303. throw new BusinessError(
  304. ErrorCode.DATA_NOT_EXIST,
  305. '未找到年度计划的期信息'
  306. );
  307. }
  308. const obj = {};
  309. if (termid) {
  310. const term = termnum.id(termid);
  311. if (term) obj.term = term.term;
  312. if (batchid) {
  313. const { batchnum } = term;
  314. const batch = batchnum.id(batchid);
  315. if (batch) obj.batch = batch.batch;
  316. }
  317. }
  318. return obj;
  319. }
  320. /**
  321. * 取出对应方向的表头
  322. * @param {String} type 横纵向类型
  323. * @param {Array} modelList 导出字段
  324. * @param {Number} studentList count的学生数量
  325. */
  326. getHead(type, modelList, studentList) {
  327. let head = [];
  328. if (type === 'horizontal') {
  329. // 横向头
  330. head = modelList.map(i => {
  331. const { zh, model, _id } = i;
  332. const headObj = { header: zh };
  333. if (model) headObj.key = model;
  334. else if (_id) headObj.key = _id;
  335. headObj.width = 20;
  336. return headObj;
  337. });
  338. } else {
  339. // 纵向头
  340. const head = [{ key: 'c', width: 20 }];
  341. for (let i = 1; i <= studentList.length; i++) {
  342. head.push({ key: `c${i}`, width: 20 });
  343. }
  344. }
  345. return head;
  346. }
  347. /**
  348. * 横向组织数据
  349. * @param {Array} studentList 学生列表
  350. * @param {Array} questAnswerList 学生回答问卷列表
  351. * @param {Array} modelList 需要导出的字段
  352. */
  353. horizontalSetData(studentList, questAnswerList, modelList) {
  354. // 组织数据需要按照modelList的顺序进行整理
  355. const data = [];
  356. for (const stu of studentList) {
  357. const obj = {};
  358. for (const m of modelList) {
  359. const { model, table, _id } = m;
  360. if (table === 'student') {
  361. const prop = _.get(stu, model, '');
  362. obj[model] = prop;
  363. } else if (table === 'questionnaire') {
  364. const qar = questAnswerList.find(f => f.studentid === stu._id);
  365. // 没找到该学生的问卷,下一个
  366. if (!qar) continue;
  367. const { answers } = qar;
  368. // 回答的数据不存在/不是数组,数据格式不对,下一个
  369. if (!(answers && _.isArray(answers))) continue;
  370. const ar = answers.find(f => f.questionid === _id);
  371. // 没找到答案,下个
  372. if (!ar) continue;
  373. const { answer } = ar;
  374. // 没有答案,下个
  375. if (!answer) continue;
  376. obj[_id] = _.trim(answer, '"');
  377. }
  378. }
  379. data.push(obj);
  380. }
  381. // const obj = {};
  382. // if (head) obj.head = head;
  383. // if (data) obj.data = data;
  384. return data;
  385. }
  386. /**
  387. * 纵向组织数据
  388. * @param {Array} studentList 学生列表
  389. * @param {Array} questAnswerList 学生回答问卷列表
  390. * @param {Array} modelList 需要导出的字段
  391. */
  392. verticaSetData(studentList, questAnswerList, modelList) {
  393. // 整理数据
  394. const data = [];
  395. for (const m of modelList) {
  396. const { table, model, _id, zh } = m;
  397. const obj = { c: zh };
  398. if (table === 'student') {
  399. for (let i = 0; i < studentList.length; i++) {
  400. const stu = studentList[i];
  401. const value = _.get(stu, model, '');
  402. if (value) obj[`c${i + 1}`] = value;
  403. }
  404. } else if (table === 'questionnaire') {
  405. for (let i = 0; i < studentList.length; i++) {
  406. const stu = studentList[i];
  407. const qar = questAnswerList.find(f => f.studentid === stu._id);
  408. // 没找到该学生的问卷,下一个
  409. if (!qar) continue;
  410. const { answers } = qar;
  411. // 回答的数据不存在/不是数组,数据格式不对,下一个
  412. if (!(answers && _.isArray(answers))) continue;
  413. const ar = answers.find(f => f.questionid === _id);
  414. // 没找到答案,下个
  415. if (!ar) continue;
  416. const { answer } = ar;
  417. // 没有答案,下个
  418. if (!answer) continue;
  419. obj[`c${i + 1}`] = _.trim(answer, '"');
  420. }
  421. }
  422. data.push(obj);
  423. }
  424. return data;
  425. }
  426. }
  427. module.exports = QuestionnaireService;