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