questionnaire.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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.findOne(
  128. { _id: ObjectId(questionnaireid) },
  129. 'name'
  130. );
  131. if (!questionnaire) {
  132. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到问卷信息');
  133. }
  134. let fn = await this.toSetFileName(questionnaire.name, range);
  135. // this.ctx.service.util.updateProcess(missionid, '25');
  136. // 修改条件,termid变成数组了,需要一个一个查出来
  137. const { planid, batchid, classid } = range;
  138. let { termid } = range;
  139. const queryObject = {};
  140. if (planid) queryObject.planid = planid;
  141. if (batchid) queryObject.batchid = batchid;
  142. if (classid) queryObject.classid = classid;
  143. let count = 0;
  144. let dp;
  145. const qkeys = Object.keys(range);
  146. if (qkeys.length === 2 && termid.length <= 0) {
  147. // 说明只有planid
  148. const plan = await this.ctx.model.Trainplan.findOne({ _id: ObjectId(planid) });
  149. if (!plan) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到指定年度计划');
  150. termid = plan.termnum.map(i => JSON.parse(JSON.stringify(i._id)));
  151. }
  152. for (const t of termid) {
  153. queryObject.termid = t;
  154. let data = await this.toGetData(
  155. queryObject,
  156. questionnaireid,
  157. direction,
  158. modelList
  159. );
  160. if (!data) continue;
  161. if (count !== 0 && direction === 'horizontal') data.shift();
  162. if (count !== 0 && direction === 'vertical') {
  163. data = data.map(i => {
  164. i.shift();
  165. return i;
  166. });
  167. }
  168. const {
  169. downloadPath,
  170. fn: newFileName,
  171. } = await this.ctx.service.util.toAsyncExcel(data, fn, dp, direction);
  172. dp = downloadPath;
  173. fn = newFileName;
  174. count++;
  175. }
  176. this.ctx.service.util.updateProcess(missionid, '100', '2', {
  177. uri: dp,
  178. });
  179. } catch (error) {
  180. console.log(error);
  181. this.ctx.service.util.updateProcess(missionid, undefined, '3');
  182. }
  183. }
  184. /**
  185. * 获取学生与学生回答的问卷
  186. * @param {Object} condition 查询学生和学生回答问卷的条件
  187. * @param {String} questionnaireid 问卷id
  188. * @param {String} direction 方向
  189. * @param {Array} modelList 导出字段
  190. */
  191. async toGetData(condition, questionnaireid, direction, modelList) {
  192. // 获取学生
  193. let studentList = await this.ctx.service.student.query(condition);
  194. if (studentList.length <= 0) {
  195. this.ctx.logger.error(`${JSON.stringify(condition)}条件下,未找到任何信息`);
  196. return;
  197. // throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到任何学生信息');
  198. }
  199. studentList = JSON.parse(JSON.stringify(studentList));
  200. // 再获取问卷答案
  201. const questAnswerList = await this.ctx.model.Uploadquestion.find({
  202. ...condition,
  203. questionnaireid,
  204. });
  205. if (!questAnswerList) {
  206. this.ctx.logger.error(`${JSON.stringify(condition)}&${questionnaireid}条件下,未找到任何完成的问卷`);
  207. return;
  208. }
  209. let res = [];
  210. let data = [];
  211. if (direction === 'horizontal') {
  212. const head = this.getHead(direction, modelList, studentList.length);
  213. data = this.horizontalSetData(studentList, questAnswerList, modelList);
  214. res.push(head.map(i => i.header));
  215. for (const d of data) {
  216. const mid = [];
  217. for (const h of head) {
  218. const { key } = h;
  219. mid.push(_.get(d, key, ''));
  220. }
  221. res.push(mid);
  222. }
  223. } else if (direction === 'vertical') {
  224. data = this.verticaSetData(studentList, questAnswerList, modelList);
  225. res = data.map(i => Object.values(i));
  226. } else {
  227. throw new BusinessError(
  228. ErrorCode.DATA_NOT_EXIST,
  229. '未找到excel的表头排列方式'
  230. );
  231. }
  232. return res;
  233. }
  234. /**
  235. * 获得导出文件的文件名
  236. * @param {String} questName 问卷名
  237. * @param {Object} range 范围(计划-期-批-班)
  238. * @return {String} fn 文件名
  239. */
  240. async toSetFileName(questName, range) {
  241. let fn = `-${questName}`;
  242. const { planid, termid, batchid, classid } = range;
  243. if (classid) {
  244. const cla = await this.ctx.service.class.fetch({ id: classid });
  245. if (cla) {
  246. const { name, term } = cla;
  247. if (name) fn = `${name.includes('班') ? name : `${name}班`}${fn}`;
  248. if (term) fn = `第${term}期${fn}`;
  249. }
  250. } else if (batchid) {
  251. const tid = _.head(termid);
  252. const obj = await this.toGetFn(tid, batchid);
  253. if (obj) {
  254. const { term, batch } = obj;
  255. if (batch) fn = `第${batch}批${fn}`;
  256. if (term) fn = `第${term}期${fn}`;
  257. }
  258. } else if (termid) {
  259. if (termid.length === 1) {
  260. const obj = await this.toGetFn(_.head(termid));
  261. if (obj) {
  262. const { term } = obj;
  263. if (term) fn = `第${term}期${fn}`;
  264. }
  265. } else {
  266. let tStr = '';
  267. for (let i = 0; i < termid.length; i++) {
  268. const tid = termid[i];
  269. const obj = await this.toGetFn(tid);
  270. if (obj) {
  271. const { term } = obj;
  272. if (term) {
  273. if (i === 0) {
  274. tStr += `${term}期`;
  275. } else {
  276. tStr += `,${term}期`;
  277. }
  278. }
  279. }
  280. }
  281. fn = `${tStr}${fn}`;
  282. }
  283. } else {
  284. const trainPlan = await this.ctx.model.Trainplan.findById(planid);
  285. if (trainPlan) {
  286. const { title } = trainPlan;
  287. if (title) fn = `${title}${fn}`;
  288. }
  289. }
  290. return fn;
  291. }
  292. /**
  293. * 获取文件的期/期批
  294. * @param {String} termid 期id
  295. * @param {String} batchid 批id
  296. */
  297. async toGetFn(termid, batchid) {
  298. const trainPlan = await this.ctx.model.Trainplan.findOne({
  299. 'termnum._id': ObjectId(termid),
  300. });
  301. if (!trainPlan) {
  302. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到年度计划信息');
  303. }
  304. const { termnum } = trainPlan;
  305. if (!termnum) {
  306. throw new BusinessError(
  307. ErrorCode.DATA_NOT_EXIST,
  308. '未找到年度计划的期信息'
  309. );
  310. }
  311. const obj = {};
  312. if (termid) {
  313. const term = termnum.id(termid);
  314. if (term) obj.term = term.term;
  315. if (batchid) {
  316. const { batchnum } = term;
  317. const batch = batchnum.id(batchid);
  318. if (batch) obj.batch = batch.batch;
  319. }
  320. }
  321. return obj;
  322. }
  323. /**
  324. * 取出对应方向的表头
  325. * @param {String} type 横纵向类型
  326. * @param {Array} modelList 导出字段
  327. * @param {Number} studentList count的学生数量
  328. */
  329. getHead(type, modelList, studentList) {
  330. let head = [];
  331. if (type === 'horizontal') {
  332. // 横向头
  333. head = modelList.map(i => {
  334. const { zh, model, _id } = i;
  335. const headObj = { header: zh };
  336. if (model) headObj.key = model;
  337. else if (_id) headObj.key = _id;
  338. headObj.width = 20;
  339. return headObj;
  340. });
  341. } else {
  342. // 纵向头
  343. const head = [{ key: 'c', width: 20 }];
  344. for (let i = 1; i <= studentList.length; i++) {
  345. head.push({ key: `c${i}`, width: 20 });
  346. }
  347. }
  348. return head;
  349. }
  350. /**
  351. * 横向组织数据
  352. * @param {Array} studentList 学生列表
  353. * @param {Array} questAnswerList 学生回答问卷列表
  354. * @param {Array} modelList 需要导出的字段
  355. */
  356. horizontalSetData(studentList, questAnswerList, modelList) {
  357. // 组织数据需要按照modelList的顺序进行整理
  358. const data = [];
  359. for (const stu of studentList) {
  360. const obj = {};
  361. for (const m of modelList) {
  362. const { model, table, _id } = m;
  363. if (table === 'student') {
  364. const prop = _.get(stu, model, '');
  365. obj[model] = prop;
  366. } else if (table === 'questionnaire') {
  367. const qar = questAnswerList.find(f => f.studentid === stu._id);
  368. // 没找到该学生的问卷,下一个
  369. if (!qar) continue;
  370. const { answers } = qar;
  371. // 回答的数据不存在/不是数组,数据格式不对,下一个
  372. if (!(answers && _.isArray(answers))) continue;
  373. const ar = answers.find(f => f.questionid === _id);
  374. // 没找到答案,下个
  375. if (!ar) continue;
  376. const { answer } = ar;
  377. // 没有答案,下个
  378. if (!answer) continue;
  379. obj[_id] = _.trim(answer, '"');
  380. }
  381. }
  382. data.push(obj);
  383. }
  384. // const obj = {};
  385. // if (head) obj.head = head;
  386. // if (data) obj.data = data;
  387. return data;
  388. }
  389. /**
  390. * 纵向组织数据
  391. * @param {Array} studentList 学生列表
  392. * @param {Array} questAnswerList 学生回答问卷列表
  393. * @param {Array} modelList 需要导出的字段
  394. */
  395. verticaSetData(studentList, questAnswerList, modelList) {
  396. // 整理数据
  397. const data = [];
  398. for (const m of modelList) {
  399. const { table, model, _id, zh } = m;
  400. const obj = { c: zh };
  401. if (table === 'student') {
  402. for (let i = 0; i < studentList.length; i++) {
  403. const stu = studentList[i];
  404. const value = _.get(stu, model, '');
  405. if (value) obj[`c${i + 1}`] = value;
  406. }
  407. } else if (table === 'questionnaire') {
  408. for (let i = 0; i < studentList.length; i++) {
  409. const stu = studentList[i];
  410. const qar = questAnswerList.find(f => f.studentid === stu._id);
  411. // 没找到该学生的问卷,下一个
  412. if (!qar) continue;
  413. const { answers } = qar;
  414. // 回答的数据不存在/不是数组,数据格式不对,下一个
  415. if (!(answers && _.isArray(answers))) continue;
  416. const ar = answers.find(f => f.questionid === _id);
  417. // 没找到答案,下个
  418. if (!ar) continue;
  419. const { answer } = ar;
  420. // 没有答案,下个
  421. if (!answer) continue;
  422. obj[`c${i + 1}`] = _.trim(answer, '"');
  423. }
  424. }
  425. data.push(obj);
  426. }
  427. return data;
  428. }
  429. }
  430. module.exports = QuestionnaireService;