answer.js 9.0 KB

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