|
@@ -0,0 +1,244 @@
|
|
|
+'use strict';
|
|
|
+const { CrudService } = require('naf-framework-mongoose-free/lib/service');
|
|
|
+const { BusinessError, ErrorCode } = require('naf-core').Error;
|
|
|
+const _ = require('lodash');
|
|
|
+const { ObjectId } = require('mongoose').Types;
|
|
|
+const axios = require('axios');
|
|
|
+
|
|
|
+// 结果
|
|
|
+class AnswerService extends CrudService {
|
|
|
+ constructor(ctx) {
|
|
|
+ super(ctx, 'answer');
|
|
|
+ this.model = this.ctx.model.Answer;
|
|
|
+ this.configModel = this.ctx.model.Config;
|
|
|
+ this.questionModel = this.ctx.model.Question;
|
|
|
+ this.questTypeModel = this.ctx.model.QuestionType;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成试卷,不是考试就不创建数据
|
|
|
+ * @param {Object} body 参数
|
|
|
+ * @property {Boolean} exam 是否考试
|
|
|
+ * @property {String} exam_place_id 考场id
|
|
|
+ * @property {String} user_id 保安员id
|
|
|
+ */
|
|
|
+ async getExamQuestion({ exam = false, ...info }) {
|
|
|
+ const config = await this.configModel.findOne();
|
|
|
+ const questionTypeList = await this.questTypeModel.find();
|
|
|
+ // 是否真的生成数据
|
|
|
+ let save = false;
|
|
|
+ if (exam && _.get(config, 'can_exam')) save = true;
|
|
|
+ // 分为;练习试卷和考试试卷2套题型分配比例,规则是为考试试卷提供额外条件
|
|
|
+ const { proportion, exam_proportion, score: maxScore } = config;
|
|
|
+ const questions = [];
|
|
|
+ let plan;
|
|
|
+ if (save) {
|
|
|
+ // 因为有 额外的规则会影响正常进行分配的方式.所以需要处额外规则与正常规则之间问题
|
|
|
+ // 额外规则: exam_proportion中is_exam的总和加一起不能超过5%
|
|
|
+ const pMinScore = exam_proportion.reduce((p, n) => p + _.multiply(_.get(n, 'number_min', 0), _.get(n, 'score', 0)), 0);
|
|
|
+ if (pMinScore > maxScore) throw new BusinessError(ErrorCode.DATA_INVALID, '考试题型设置出现错误');
|
|
|
+ plan = this.getPlan(exam_proportion, maxScore, true);
|
|
|
+ } else {
|
|
|
+ const pMinScore = proportion.reduce((p, n) => p + _.multiply(_.get(n, 'number_min', 0), _.get(n, 'score', 0)), 0);
|
|
|
+ if (pMinScore > maxScore) throw new BusinessError(ErrorCode.DATA_INVALID, '练习题型设置出现错误');
|
|
|
+ // 循环题型,抽题,生成答案字段放到questions中
|
|
|
+ // 先计算出方案,再取出题目
|
|
|
+ plan = this.getPlan(proportion, maxScore);
|
|
|
+ }
|
|
|
+ for (const p of plan) {
|
|
|
+ const { type, num } = p;
|
|
|
+ for (let i = 0; i < num; i++) {
|
|
|
+ const notIn = questions.map(i => i._id);
|
|
|
+ const quest = await this.getQuestion(type, notIn);
|
|
|
+ if (quest && quest.type) {
|
|
|
+ const questionType = questionTypeList.find(f => ObjectId(f._id).equals(quest.type));
|
|
|
+ if (questionType) {
|
|
|
+ quest.type = _.omit(questionType, [ 'meta', '__v' ]);
|
|
|
+ // 生成试卷,把答案的位置也给放上,免去前端生成或者没生成报错
|
|
|
+ if (questionType.type === 'checkbox') quest.answer = [];
|
|
|
+ else quest.answer = null;
|
|
|
+ questions.push(quest);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!save) return { questions };
|
|
|
+ // 组织数据,生成答案
|
|
|
+ const obj = { ...info, config: _.pick(config, [ 'can_exam', 'exam_proportion', 'rules', 'score' ]), questions };
|
|
|
+ // 创建答案
|
|
|
+ const result = await this.model.create(obj);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 计算计划:什么类型多少题
|
|
|
+ * @param {Array} proportion 比例
|
|
|
+ * @param {Number} maxScore 总分数
|
|
|
+ */
|
|
|
+ getPlan(proportion, maxScore) {
|
|
|
+ const plan = [];
|
|
|
+ // 剩余分数
|
|
|
+ let lessScore = maxScore;
|
|
|
+ // 分为2种,1种是考试专用类型,另一种是通常考试类型
|
|
|
+ proportion = proportion.map(i => ({ ...i, num: 0 }));
|
|
|
+ const exam_proportion = proportion.filter(f => f.is_exam);
|
|
|
+ let mscore = this.computedPercent(5, maxScore);
|
|
|
+ // 考试类型不能超过5%
|
|
|
+ let limit = 0;
|
|
|
+ do {
|
|
|
+ for (const p of exam_proportion) {
|
|
|
+ const { score } = p;
|
|
|
+ if (mscore - score < 0) {
|
|
|
+ // 减完成负数了,不能减
|
|
|
+ limit++;
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ p.num++;
|
|
|
+ mscore -= score;
|
|
|
+ lessScore -= score;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // limit到100次基本说明无限循环了,跳出来,有问题咱可以外面抛异常.别这里搞事消耗
|
|
|
+ } while (mscore > 0 && limit < 100);
|
|
|
+ // 整理成plan,合并进去么
|
|
|
+ const exam_plan = exam_proportion.map(i => {
|
|
|
+ const { score, _id: type, num } = i;
|
|
|
+ const obj = { type, score, num, overRange: true };
|
|
|
+ return obj;
|
|
|
+ });
|
|
|
+ plan.push(...exam_plan);
|
|
|
+ const usual_proportion = proportion.filter(f => !f.is_exam);
|
|
|
+ for (const p of usual_proportion) {
|
|
|
+ const { number_min, number_max, score, _id: type } = p;
|
|
|
+ let { max = 0, min = 0 } = p;
|
|
|
+ // 为0说明是没限制,需要计算下
|
|
|
+ if (min === 0) min = this.computedNum(number_min, score, maxScore);
|
|
|
+ if (max === 0) max = this.computedNum(number_max, score, maxScore);
|
|
|
+ const num = _.random(min, max);
|
|
|
+ lessScore -= _.multiply(score, num);
|
|
|
+ plan.push({ type, num, score, min, max });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 小于0相当于重来一遍
|
|
|
+ if (lessScore < 0) return this.getPlan(proportion, maxScore);
|
|
|
+ // 等于0,直接返回就行
|
|
|
+ else if (lessScore === 0) return plan;
|
|
|
+ // 大于0,还有分数,需要继续分配,拿着plan挨个轮,轮到没分为止
|
|
|
+ let breakWhile = false;
|
|
|
+ // 能不能用该计划
|
|
|
+ let cantUse = true;
|
|
|
+ do {
|
|
|
+ if (lessScore > 0) {
|
|
|
+ // 这里,不一定是所有的题型都超出范围.还有可能是 差1分 但是1分的题已经到上限了,只剩下2分的题了
|
|
|
+ // 所以只看所有类型超没超出范围没有意义,而是需要看没超出范围的题目,能不能把剩下的 分 填上
|
|
|
+ const inRange = plan.filter(f => !f.overRange);
|
|
|
+ let next = false;
|
|
|
+ for (const ir of inRange) {
|
|
|
+ const { score } = ir;
|
|
|
+ if (lessScore - score >= 0) {
|
|
|
+ // 至少这个类型能填上,可以往下继续
|
|
|
+ next = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!next) {
|
|
|
+ breakWhile = true;
|
|
|
+ cantUse = false;
|
|
|
+ } else {
|
|
|
+ for (const p of plan) {
|
|
|
+ if (lessScore === 0) {
|
|
|
+ breakWhile = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ const { score, overRange, min, max } = p;
|
|
|
+ // 是否超出范围,超出下一个
|
|
|
+ if (overRange) continue;
|
|
|
+
|
|
|
+ // 先计算分数够不够减的
|
|
|
+ if (lessScore - score < 0) continue;
|
|
|
+ // 再计算 该类型加完 1道题后,会不会超出范围
|
|
|
+ if (!_.inRange(p.num + 1, min, max + 1)) {
|
|
|
+ p.overRange = true;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ lessScore -= score;
|
|
|
+ p.num += 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else breakWhile = true;
|
|
|
+ } while (!breakWhile);
|
|
|
+ if (cantUse) return plan;
|
|
|
+ return this.getPlan(proportion, maxScore);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算该类型的题数
|
|
|
+ * @param {Number} num 该类型占比
|
|
|
+ * @param {Number} score 该类型题分数
|
|
|
+ * @param {Number} maxScore 试卷满分
|
|
|
+ * @return {Number} 返回多少道题
|
|
|
+ */
|
|
|
+ computedNum(num, score, maxScore) {
|
|
|
+ return _.ceil(_.divide(_.multiply(_.divide(_.multiply(score, num), maxScore), 100), score));
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 算该百分比的分数
|
|
|
+ * @param {Number} num 占比
|
|
|
+ * @param {Number} maxScore 满分
|
|
|
+ */
|
|
|
+ computedPercent(num, maxScore) {
|
|
|
+ return _.ceil(_.multiply(_.divide(num, maxScore), 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据题型查找,考试调用次数最少的题
|
|
|
+ * @param {ObjectId} type 题型
|
|
|
+ * @param {Array} notIn 已经被取出来的题.不要再拿了
|
|
|
+ */
|
|
|
+ async getQuestion(type, notIn) {
|
|
|
+ // TODO 正式环境需要将这个删了,不能出重复的题
|
|
|
+ notIn = [];
|
|
|
+ const data = await this.questionModel.findOne({ type, _id: { $nin: notIn } }).sort({ times: 1 });
|
|
|
+ if (data) {
|
|
|
+ data.times = data.times + 1;
|
|
|
+ await data.save();
|
|
|
+ }
|
|
|
+
|
|
|
+ return _.pick(data, '_id', 'title', 'type', 'selects');
|
|
|
+ }
|
|
|
+
|
|
|
+ async update(filter, update, { projection } = {}) {
|
|
|
+ const { _id, id } = filter;
|
|
|
+ if (_id || id) filter = { _id: ObjectId(_id || id) };
|
|
|
+ const entity = await this.model.findOne(filter).exec();
|
|
|
+ if (!entity) throw new BusinessError(ErrorCode.DATA_NOT_EXIST);
|
|
|
+ entity.set(update);
|
|
|
+ await entity.save();
|
|
|
+ // 需要处理原数据
|
|
|
+ const r = await this.updateScore(entity);
|
|
|
+ if (!r) throw new BusinessError(ErrorCode.DATABASE_FAULT, '未能同步数据,交卷失败');
|
|
|
+ const res = await this.model.findOne(filter, projection).exec();
|
|
|
+
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 修改原数据的考试成绩
|
|
|
+ * @param {Object} data 数据
|
|
|
+ */
|
|
|
+ async updateScore(data) {
|
|
|
+ const id = _.get(data, 'exam_info.id');
|
|
|
+ const exam_achieve = _.get(data, 'score');
|
|
|
+ const body = { data: { exam_achieve, status: '3' }, query: { id } };
|
|
|
+ const middleConfig = this.app.config.middleServer;
|
|
|
+ const { ip, port } = middleConfig;
|
|
|
+ const res = await axios({
|
|
|
+ method: 'post',
|
|
|
+ url: `http://${ip}:${port}/db/update?table=examination_examinee`,
|
|
|
+ data: body,
|
|
|
+ responseType: 'json',
|
|
|
+ });
|
|
|
+ if (res && _.get(res, 'data.code') === 0) return true;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = AnswerService;
|