'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;