answer.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. 'use strict';
  2. const { CrudService } = require('naf-framework-mongoose-free/lib/service');
  3. const { BusinessError, ErrorCode } = require('naf-core').Error;
  4. const _ = require('lodash');
  5. const { ObjectId } = require('mongoose').Types;
  6. const axios = require('axios');
  7. // 结果
  8. class AnswerService extends CrudService {
  9. constructor(ctx) {
  10. super(ctx, 'answer');
  11. this.model = this.ctx.model.Answer;
  12. this.configModel = this.ctx.model.Config;
  13. this.questionModel = this.ctx.model.Question;
  14. this.questTypeModel = this.ctx.model.QuestionType;
  15. }
  16. /**
  17. * 生成试卷,不是考试就不创建数据
  18. * @param {Object} body 参数
  19. * @property {Boolean} exam 是否考试
  20. * @property {String} exam_place_id 考场id
  21. * @property {String} user_id 保安员id
  22. */
  23. async getExamQuestion({ exam = false, ...info }) {
  24. let config = await this.configModel.findOne();
  25. if (config) config = JSON.parse(JSON.stringify(config));
  26. const questionTypeList = await this.questTypeModel.find();
  27. // 是否真的生成数据
  28. let save = false;
  29. if (exam && _.get(config, 'can_exam')) save = true;
  30. // 分为;练习试卷和考试试卷2套题型分配比例,规则是为考试试卷提供额外条件
  31. const { proportion, exam_proportion, score: maxScore } = config;
  32. const questions = [];
  33. let plan;
  34. if (save) {
  35. // 因为有 额外的规则会影响正常进行分配的方式.所以需要处额外规则与正常规则之间问题
  36. // 额外规则: exam_proportion中is_exam的总和加一起不能超过5%
  37. const pMinScore = exam_proportion.reduce((p, n) => p + _.multiply(_.get(n, 'number_min', 0), _.get(n, 'score', 0)), 0);
  38. if (pMinScore > maxScore) throw new BusinessError(ErrorCode.DATA_INVALID, '考试题型设置出现错误');
  39. plan = this.getPlan(exam_proportion, maxScore, true);
  40. } else {
  41. const pMinScore = proportion.reduce((p, n) => p + _.multiply(_.get(n, 'number_min', 0), _.get(n, 'score', 0)), 0);
  42. if (pMinScore > maxScore) throw new BusinessError(ErrorCode.DATA_INVALID, '练习题型设置出现错误');
  43. // 循环题型,抽题,生成答案字段放到questions中
  44. // 先计算出方案,再取出题目
  45. plan = this.getPlan(proportion, maxScore);
  46. }
  47. for (const p of plan) {
  48. const { type, num } = p;
  49. for (let i = 0; i < num; i++) {
  50. const notIn = questions.map(i => i._id);
  51. const quest = await this.getQuestion(type, notIn);
  52. if (quest && quest.type) {
  53. const questionType = questionTypeList.find(f => ObjectId(f._id).equals(quest.type));
  54. if (questionType) {
  55. quest.type = _.omit(questionType, [ 'meta', '__v' ]);
  56. // 生成试卷,把答案的位置也给放上,免去前端生成或者没生成报错
  57. if (questionType.type === 'checkbox') quest.answer = [];
  58. else quest.answer = null;
  59. questions.push(quest);
  60. }
  61. }
  62. }
  63. }
  64. if (!save) return { questions };
  65. // 组织数据,生成答案
  66. const obj = { ...info, config: _.pick(config, [ 'can_exam', 'exam_proportion', 'rules', 'score' ]), questions };
  67. // 创建答案
  68. const result = await this.model.create(obj);
  69. return result;
  70. }
  71. /**
  72. * 计算计划:什么类型多少题
  73. * @param {Array} proportion 比例
  74. * @param {Number} maxScore 总分数
  75. */
  76. getPlan(proportion, maxScore) {
  77. const plan = [];
  78. // 剩余分数
  79. let lessScore = maxScore;
  80. // 分为2种,1种是考试专用类型,另一种是通常考试类型
  81. proportion = proportion.map(i => ({ ...i, num: 0 }));
  82. const exam_proportion = proportion.filter(f => f.is_exam);
  83. let mscore = this.computedPercent(5, maxScore);
  84. // 考试类型不能超过5%
  85. let limit = 0;
  86. // do {
  87. // for (const p of exam_proportion) {
  88. // const { score } = p;
  89. // if (mscore - score < 0) {
  90. // // 减完成负数了,不能减
  91. // limit++;
  92. // continue;
  93. // } else {
  94. // p.num++;
  95. // mscore -= score;
  96. // lessScore -= score;
  97. // }
  98. // }
  99. // // limit到100次基本说明无限循环了,跳出来,有问题咱可以外面抛异常.别这里搞事消耗
  100. // } while (mscore > 0 && limit < 100);
  101. // 整理成plan,合并进去么
  102. const exam_plan = exam_proportion.map(i => {
  103. const { score, _id: type, num } = i;
  104. const obj = { type, score, num, overRange: true };
  105. return obj;
  106. });
  107. plan.push(...exam_plan);
  108. const usual_proportion = proportion.filter(f => !f.is_exam);
  109. for (const p of usual_proportion) {
  110. const { number_min, number_max, score, _id: type } = p;
  111. let { max = 0, min = 0 } = p;
  112. // 为0说明是没限制,需要计算下
  113. if (min === 0) min = this.computedNum(number_min, score, maxScore);
  114. if (max === 0) max = this.computedNum(number_max, score, maxScore);
  115. const num = _.random(min, max);
  116. lessScore -= _.multiply(score, num);
  117. plan.push({ type, num, score, min, max });
  118. }
  119. // 小于0相当于重来一遍
  120. if (lessScore < 0) return this.getPlan(proportion, maxScore);
  121. // 等于0,直接返回就行
  122. else if (lessScore === 0) return plan;
  123. // 大于0,还有分数,需要继续分配,拿着plan挨个轮,轮到没分为止
  124. let breakWhile = false;
  125. // 能不能用该计划
  126. let cantUse = true;
  127. do {
  128. if (lessScore > 0) {
  129. // 这里,不一定是所有的题型都超出范围.还有可能是 差1分 但是1分的题已经到上限了,只剩下2分的题了
  130. // 所以只看所有类型超没超出范围没有意义,而是需要看没超出范围的题目,能不能把剩下的 分 填上
  131. const inRange = plan.filter(f => !f.overRange);
  132. let next = false;
  133. for (const ir of inRange) {
  134. const { score } = ir;
  135. if (lessScore - score >= 0) {
  136. // 至少这个类型能填上,可以往下继续
  137. next = true;
  138. break;
  139. }
  140. }
  141. if (!next) {
  142. breakWhile = true;
  143. cantUse = false;
  144. } else {
  145. for (const p of plan) {
  146. if (lessScore === 0) {
  147. breakWhile = true;
  148. break;
  149. }
  150. const { score, overRange, min, max } = p;
  151. // 是否超出范围,超出下一个
  152. if (overRange) continue;
  153. // 先计算分数够不够减的
  154. if (lessScore - score < 0) continue;
  155. // 再计算 该类型加完 1道题后,会不会超出范围
  156. if (!_.inRange(p.num + 1, min, max + 1)) {
  157. p.overRange = true;
  158. continue;
  159. }
  160. lessScore -= score;
  161. p.num += 1;
  162. }
  163. }
  164. } else breakWhile = true;
  165. } while (!breakWhile);
  166. if (cantUse) return plan;
  167. return this.getPlan(proportion, maxScore);
  168. }
  169. /**
  170. * 计算该类型的题数
  171. * @param {Number} num 该类型占比
  172. * @param {Number} score 该类型题分数
  173. * @param {Number} maxScore 试卷满分
  174. * @return {Number} 返回多少道题
  175. */
  176. computedNum(num, score, maxScore) {
  177. return _.ceil(_.divide(_.multiply(_.divide(_.multiply(score, num), maxScore), 100), score));
  178. }
  179. /**
  180. * 算该百分比的分数
  181. * @param {Number} num 占比
  182. * @param {Number} maxScore 满分
  183. */
  184. computedPercent(num, maxScore) {
  185. return _.ceil(_.multiply(_.divide(num, maxScore), 100));
  186. }
  187. /**
  188. * 根据题型查找,考试调用次数最少的题
  189. * @param {ObjectId} type 题型
  190. * @param {Array} notIn 已经被取出来的题.不要再拿了
  191. */
  192. async getQuestion(type, notIn) {
  193. // TODO 正式环境需要将这个删了,不能出重复的题
  194. notIn = [];
  195. const data = await this.questionModel.findOne({ type, _id: { $nin: notIn } }).sort({ times: 1 });
  196. if (data) {
  197. data.times = data.times + 1;
  198. await data.save();
  199. }
  200. return _.pick(data, '_id', 'title', 'type', 'selects');
  201. }
  202. async update(filter, update, { projection } = {}) {
  203. const { _id, id } = filter;
  204. if (_id || id) filter = { _id: ObjectId(_id || id) };
  205. const entity = await this.model.findOne(filter).exec();
  206. if (!entity) throw new BusinessError(ErrorCode.DATA_NOT_EXIST);
  207. entity.set(update);
  208. await entity.save();
  209. // 需要处理原数据
  210. const r = await this.updateScore(entity);
  211. if (!r) throw new BusinessError(ErrorCode.DATABASE_FAULT, '未能同步数据,交卷失败');
  212. const res = await this.model.findOne(filter, projection).exec();
  213. return res;
  214. }
  215. /**
  216. * 修改原数据的考试成绩
  217. * @param {Object} data 数据
  218. */
  219. async updateScore(data) {
  220. const id = _.get(data, 'exam_info.id');
  221. const exam_achieve = _.get(data, 'score');
  222. const body = { data: { exam_achieve, status: '3' }, query: { id } };
  223. const middleConfig = this.app.config.middleServer;
  224. const { ip, port } = middleConfig;
  225. const res = await axios({
  226. method: 'post',
  227. url: `http://${ip}:${port}/db/update?table=examination_examinee`,
  228. data: body,
  229. responseType: 'json',
  230. });
  231. if (res && _.get(res, 'data.code') === 0) return true;
  232. }
  233. }
  234. module.exports = AnswerService;