'use strict'; const assert = require('assert'); const { after } = require('lodash'); const _ = require('lodash'); const moment = require('moment'); const { ObjectId } = require('mongoose').Types; const { CrudService } = require('naf-framework-mongoose/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; class ApplyService extends CrudService { constructor(ctx) { super(ctx, 'apply'); this.model = this.ctx.model.Apply; this.tmodel = this.ctx.model.Teacher; this.submodel = this.ctx.model.Subject; this.trainmodel = this.ctx.model.Trainplan; this.umodel = this.ctx.model.User; this.nmodel = this.ctx.model.Notice; this.dayList = [ '日', '一', '二', '三', '四', '五', '六' ]; } // 查询 async queryteacher(query) { const { termid, subid, date } = query; const data = await this.model .find({ termid, subid, date }) .sort({ msscore: -1 }); const teachers = []; for (const _data of data) { const teacherid = _data.teacherid; const teacher = await this.tmodel.findById(teacherid); teachers.push(teacher); } return teachers; } /** * 教师计划初步课表安排,可反复使用 * @param {Object} body planid:计划id,ids:期数id列表;classtype:班级类型 */ async arrangeteacher({ planid, ids, classtype }) { console.log(classtype); assert(planid, '缺少计划信息'); const trainplan = await this.trainmodel.findById(planid); if (!trainplan) { throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在'); } // trainplan = JSON.parse(JSON.stringify(trainplan)); // 查找所有教师列表 let teacherList = await this.tmodel.find({ xsscore: { $exists: true } }); teacherList = JSON.parse(JSON.stringify(teacherList)); // 查找所有教师上报列表 let teaplanList = await this.model.find(); teaplanList = JSON.parse(JSON.stringify(teaplanList)); // 课程 let subjectList = await this.submodel.find(); subjectList = JSON.parse(JSON.stringify(subjectList)); const termList = _.cloneDeep(trainplan); let { termnum } = termList; if (!termnum) return; termnum = JSON.parse(JSON.stringify(termnum)); // 整理出课表 const arr = this.setLessonList(termnum); // 安排后的课表 const afterList = []; // 排课 for (const l of arr) { const { termid, subid, day: date, status, type, classid } = l; // 检验是否在需要安排的期内 if (!ids.includes(termid)) { // 不在要求安排的期内,就直接放回去 afterList.push(l); continue; } // 检查是否符合要求的班级类型 if (classtype || `${classtype}` === '0') { // 不符合,放回去,下一个 if (`${classtype}` !== `${type}`) { afterList.push(l); continue; } } // 重置教师 l.teaid = null; l.teaname = null; if (status && `${status}` === '1') { afterList.push(l); continue; } const subject = subjectList.find(f => ObjectId(subid).equals(f._id)); if (subject.need_teacher !== '0') { afterList.push(l); continue; } // 申请该天,该科目的教师,并查出教师的名字,分数;并按分数排序 let applyList = teaplanList.filter( f => f.date === date && f.subid === subid ); applyList = applyList.map(i => { let obj = { ...JSON.parse(JSON.stringify(i)) }; const r = teacherList.find(f => i.teacherid === f._id); if (r) { const { name: teaname, xsscore: score } = r; i.teaname = teaname; i.score = score * 1; obj = { ...obj, teaname, score }; } return obj; }); // 过滤出没有分数的,不排 applyList = applyList.filter(f => f.score); // 按成绩排序 applyList = _.orderBy(applyList, [ 'score' ], [ 'desc' ]); // 本期超过2次的教师列表,如果没有人就用这里分最高的排 + 教过这个班的教师列表,不过内容都一样 let outTwoTimesList = []; // 依次循环申请的教师列表,往这个课程安排中放教师 for (const atea of applyList) { // 先查询,该教师,是否在今天有安排 条件1:同一天,一个教师只能有一次,因为一上一天课 const tr = afterList.find( f => f.teaid === atea.teacherid && f.day === atea.date ); if (tr) continue; // 条件2:优先排一期2次以内的教师,超过2次的教师延后排,根据 次数升序,分数降序找合适的; // 查看这期内,每个申请上课的教师时候超过2天(2条记录),如果超过,则不排,但是如果最后没有人了,就得硬排了 const r = afterList.filter( f => f.termid === termid && f.teaid === atea.teacherid ); if (r.length >= 2) { // 需要记录这个老师已经有几次 const obj = { ...atea, times: r.length }; outTwoTimesList = [ ...outTwoTimesList, obj ]; continue; } else { // 条件3:尽量不让一个教师在一个班出现2次及其以上,但是如果没人,之前又排完了,还是会出现一个教师教1个班2天的情况,所以需要最后收尾检查 const alreadyTeach = afterList.find( f => f.classid === classid && f.teaid === atea.teacherid ); if (alreadyTeach) { // 已经教过这个班了,也暂时放到2次以上列表中,将次数记录下来 const obj = { ...atea, times: r.length }; outTwoTimesList = [ ...outTwoTimesList, obj ]; continue; } l.teaid = atea.teacherid; l.teaname = atea.teaname; break; } } // 检查,该天,该科的课是否有教师 const has_teaid = _.get(l, 'teaid'); if (!has_teaid) { // 如果没有教师,就需要在outTowTimesList列表中找分最高的教师 // 排序需要根据,次数(后加的),分数排序,优先选次数最少,分数最高的教师 const list = _.orderBy( outTwoTimesList, [ 'times', 'score' ], [ 'asc', 'desc' ] ); for (const i of list) { const tr = afterList.find( f => f.teaid === i.teacherid && f.day === i.date ); if (tr) continue; else { l.teaid = i.teacherid; l.teaname = i.teaname; break; } } } afterList.push(l); } // 需要按classid分组,然后查每组下,有没有重复教师,如果有重复教师,提取出这个教师的日期,科目,分2种情况: // 1)可以和其他班教师换下位置;2)没法换,只能排下个教师 // 查找这天,这科的 课程安排 且 这些课程的教师 没给这个班级上过课 (undefined也算,每排教师) // =>如果有满足条件的教师, 找到该班, 然后查看,现在需要换的这个教师是否也满足 没给这个班上过课 的条件 // =>满足条件,两个班级的教师对调 // =>不满足条件直至没有教师,需要整理出 applyList,然后将现在这个教师排除掉(放到一个空间),然后找其他满足条件的教师,如果没有,那就只能有他了 const checkList = afterList.filter(f => ids.includes(f.termid) && f.type === classtype && f.teaid); const groupObj = _.groupBy(checkList, 'classid'); const keys = Object.keys(groupObj); const exchangeList = []; for (const key of keys) { const arr = groupObj[key]; const teaids = arr.map(i => _.pick(i, [ 'day', 'subid', 'teaid' ])); for (const l of arr) { const { day, subid, teaid, teaname, termid } = l; // 找下这个教师在这个班级中,是否教了多科 let findres = teaids.filter(f => f.teaid === teaid); // 2科以下,跳过不看 if (findres.length < 2) continue; // 2科以上,需要找到每一天的申请名单,可最多的那天先换 // 先查下同一天,同一科的教师能不能换 const toDaySubjectList = afterList.filter(f => f.subid === subid && f.day === day && f.teaid !== teaid); let sameSubChange = false; for (const tdstea of toDaySubjectList) { // 找一下,这个教师能不能换到这个班和当前重复教一个班的教师能不能带这个教师的班 // 为什么这么找:因为到这里之前,所有需要安排的课程,已经按照1,2原则排完,是最优解,所以尽可能在最优解之间进行调换, // 如果最优解没有办法调换,那就只能往下找了,对于两个班的情况可能常见,但是对于三个班来说基本就很少了,如果开4个班,基本就不存在这个问题了, // 循环的教师,需要在teaids中找有没有这个教师 const first = teaids.find(f => f.teaid === tdstea.teaid); if (first) continue; else { // 如果这个教师可以,需要进行反查,查当前有问题的这个教师,在没在要和他换的教师班级里讲过课 const { classid } = tdstea; const tdsLessonClass = groupObj[classid]; const sec = tdsLessonClass.find(f => f.teaid === teaid); // 如果这个教师教过要调换教师那个班,就继续找看下个老师 if (sec) continue; sameSubChange = tdstea; } } // 判断sameSubChange,如果不为false,则说明当天教师可以替换,接着continue就行 if (sameSubChange) { const { teaid: oteaid, teaname: oteaname } = sameSubChange; // 要替换的索引 const oindex = afterList.findIndex(f => _.isEqual(f, sameSubChange)); // 重复老师的索引 const tindex = afterList.findIndex(f => _.isEqual(f, l)); // 交换 afterList[oindex].teaid = teaid; afterList[oindex].teaname = teaname; afterList[tindex].teaid = oteaid; afterList[tindex].teaname = oteaname; continue; } // 如果到这里了,也就是说同天同科的教师都替换不了,那就是一点血找也没有了 for (const les of findres) { // 申请的教师 let applyList = teaplanList.filter( f => f.date === les.day && f.subid === les.subid && f.teacherid !== les.teaid ); applyList = applyList.map(i => { let obj = { ...JSON.parse(JSON.stringify(i)) }; const r = teacherList.find(f => i.teacherid === f._id); if (r) { const { name: teaname, xsscore: score } = r; i.teaname = teaname; i.score = score * 1; obj = { ...obj, teaname, score }; } return obj; }); // 过滤出没有分数的,不排 applyList = applyList.filter(f => f.score); // 过滤出当天有课的教师 applyList = applyList.filter(f => !(afterList.find(af => af.day === f.day && af.teaid === f.teaid))); // 过滤出这期有几次 applyList = applyList.map(i => { // 找到这个教师在这期有几次 const r = afterList.filter(f => f.termid === termid && f.teaid === i.teaid); i.times = r.length; return i; }); // 按成绩排序 applyList = _.orderBy(applyList, [ 'times', 'score' ], [ 'asc', 'desc' ]); // 整理出教师教这个班的所有科目的其他可选教师(排除已经排完课的教师) les.applyList = applyList; les.applynum = applyList.length; } // 按照申请人数排序,然后看看人最多那天是不是这天,如果是这天,就处理 findres = _.orderBy(findres, [ 'applynum' ], [ 'desc' ]); const head = _.head(findres); if (head.day === day) { // 如果当前日期,是申请人数最多的,就处理它 const { applyList } = head; if (applyList.length >= 0) { // 将排完序的第一个教师拿出来(一定是符合要求的) const apply = _.head(applyList); const { teaid, teaname } = apply; const tindex = afterList.findIndex(f => _.isEqual(f, l)); afterList[tindex].teaid = teaid; afterList[tindex].teaname = teaname; } } } } // 将afterList还原回正常的termnum; const newTermnum = this.returnTermnum(afterList, termnum); // 保存至计划 trainplan.termnum = newTermnum; await trainplan.save(); } // 确认计划安排 async arrangeConfirm({ planid, ids, classtype }) { const trainplan = await this.trainmodel.findById(planid); if (!trainplan) { throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在'); } const plan = _.cloneDeep(trainplan); let { termnum } = plan; if (!termnum) return; termnum = JSON.parse(JSON.stringify(termnum)); // 过滤出确认的期,TODO:没有做通知 // termnum = termnum.filter(f => ); // 找到每个教师的位置,然后把状态(status)改成1=>已确认 for (const t of termnum) { if (!ids.includes(t._id)) continue; const { term } = t; if (!(t.batchnum && _.isArray(t.batchnum))) continue; for (const b of t.batchnum) { const { batch } = b; if (!(b.class && _.isArray(b.class))) continue; for (const c of b.class) { // 检查是否要求班级类型 if (classtype || `${classtype}` === '0') { // 获取这个班级的班级类型 const { type } = c; // 判断班级类型与要求的符不符合,不符合就跳过不改 if (`${type}` !== `${classtype}`) continue; } if (!(c.lessons && _.isArray(c.lessons))) continue; for (const l of c.lessons) { l.status = '1'; } } } } // console.log(termnum); trainplan.termnum = termnum; await trainplan.save(); } /** * 拍平了的课表=>termnum * @param {Array} list 拍平了的课表,详情参考页面的初步课表的数据 * @param {Array} termnum 原termnum */ returnTermnum(list, termnum) { let newTermnum = []; for (const l of list) { const { termid, batchid, classid, ...info } = l; const updata = _.pick(info, [ 'day', 'subid', 'subname', 'teaid', 'teaname', 'time', 'status', ]); newTermnum = termnum.map(t => { // 找到期 if (termid === t._id) { t.batchnum = t.batchnum.map(b => { if (batchid === b._id) { // 找到批次 b.class = b.class.map(c => { if (classid === c._id) { if (c.lessons) { // 说明有课程安排,找有没有重复的,没有就推进去,有就更改,subid查 const r = c.lessons.find(f => f.subid === updata.subid); if (r) { const rindex = c.lessons.findIndex( f => f.subid === updata.subid ); c.lessons[rindex] = updata; } else { c.lessons.push(updata); } } else { // 说明没有课程安排,放进去一条保存 c.lessons = [ updata ]; } } return c; }); } return b; }); } return t; }); } return newTermnum; } /** * 将课表拍平了,从多维=>一维 * @param {Array} termnum 计划的termnum */ setLessonList(termnum) { let arr = []; for (const t of termnum) { const { batchnum, term, _id: termid } = t; // 班级和课程一一匹 for (const b of batchnum) { const { class: classes, lessons, _id: batchid } = b; const claslesList = this.setList( term * 1, termid, batchid, classes, lessons ); arr.push(...claslesList); } } arr = _.orderBy(arr, [ 'term', 'day' ], [ 'asc', 'asc' ]); return arr; } /** * 将课表模板和班级整理成一维数组 * @param {String} term 期数 * @param {String} termid 期id * @param {String} batchid 批id * @param {Array} classes 班级列表 * @param {Array} lessonTemplate 课表模板 */ setList(term, termid, batchid, classes, lessonTemplate) { const arr = []; // 班级和课程匹配 for (const cla of classes) { let { lessons } = cla; if (!lessons) lessons = lessonTemplate; for (const i of lessons) { let nobj = {}; nobj.term = term; nobj.termid = termid; nobj.batchid = batchid; nobj.type = cla.type; const obj = _.omit(cla, [ 'lessons' ]); nobj.classid = _.clone(cla._id); nobj = _.assign(nobj, obj); nobj = _.assign(nobj, i); arr.push(nobj); } } return arr; } /** * 发送消息 * @param {Object} param planid:年度计划id,ids,发送的期列表;classtype:发送班级类型 undefined 都发,有的话就找指定班级类型发 */ async arrangeSendMsg({ planid, ids, classtype }) { const trainplan = await this.trainmodel.findById(planid); if (!trainplan) { throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在'); } // 大批次id,年度计划id const plan = _.cloneDeep(trainplan); let { termnum, planyearid } = plan; if (!termnum) return; termnum = JSON.parse(JSON.stringify(termnum)); // 整理出课表 let arr = this.setLessonList(termnum); // 过滤出需要发送的教师 arr = arr.filter(f => ids.find(id => f.termid === id) && f.teaid); // && f.status !== '1' // 整理出要发送的教师列表 let teaids = arr.map(i => i.teaid); teaids = _.uniq(teaids); // 找到教师信息 let teaList = await this.tmodel.find({ _id: teaids }); // 找到教师用户信息 let teauserList = await this.umodel.find({ uid: teaids }); if (teaList) teaList = JSON.parse(JSON.stringify(teaList)); if (teauserList) teauserList = JSON.parse(JSON.stringify(teauserList)); // 发送,此处是根据安排,给教师发.还有一种方案是根据教师,整理安排一起发送 // 查询是否发送过这期的通知 // 排序 arr = _.orderBy(arr, [ 'day' ], [ 'asc' ]); for (const l of arr) { // 教师id,期数,班级名,上课的日期,课程名 const { teaid, term, name, day, subname, termid, classid, type, status, } = l; // 已确认的教师不发信息 if (status === '1') continue; // 判断发送的班级类型 if (!(classtype && classtype === type)) continue; const tea = teaList.find(f => f._id === teaid); const teauser = teauserList.find(f => f.uid === teaid); // 文案 let msg = `${_.get(tea, 'name', '')}老师您好: 吉林省高等学校毕业生就业指导中心-双困生培训系统提醒您: ${term}期-${name.includes('班') ? name : `${name}班`} ${day}(星期${this.dayList[moment(day).days()]}) 有您的课程安排:${subname}`; msg = `${msg}\n 如果您无法进行授课,请及时联系中心负责人`; const { openid } = teauser; let tourl; let to_send = false; if (openid) { let notice = await this.nmodel.findOne({ planid, termid, classid, type: '6', }); // 找下是否发过信息 if (notice) { // 发过信息,找有没有这个教师 const { notified } = notice; if (_.isArray(notified)) { const has_notice = notified.find(f => f.notifiedid === teaid); if (has_notice) { const { status } = has_notice; if (status !== '1') to_send = true; } else { const obj = { notifiedid: teaid, username: _.get(tea, 'name', ''), content: msg, }; notice.notified.push(obj); await notice.save(); to_send = true; } } } else { const notified = [ { notifiedid: teaid, username: _.get(tea, 'name', ''), content: msg, }, ]; const noticeObj = { planyearid, planid, termid, classid, noticeid: 'system', type: '6', content: `${term}期-${ name.includes('班') ? name : `${name}班` }教师计划初步信息确认`, notified, }; await this.nmodel.create(noticeObj); notice = await this.nmodel.findOne({ planid, termid, classid, type: '6', }); to_send = true; } tourl = this.ctx.app.config.baseUrl + '/msgconfirm/?userid=' + teaid + '¬iceid=' + notice._id; } if (to_send) { // 邮箱与微信都发送 const { email } = tea; if (email) { this.toSendEmail(email, msg, tea.name); } if (openid) { this.toSendWxMsg(openid, msg, tea.name, tourl); } } } } /** * 计划-教师初步课表发送邮件 * @param {String} email 邮件 * @param {String} content 内容 * @param {String} teaname 教师姓名 */ async toSendEmail(email, content, teaname) { if (!email) { console.error(`计划教师发送通知:${teaname}没有email`); return; } const subject = '吉林省高等学校毕业生就业指导中心通知(系统邮件,请勿回复)'; // this.ctx.service.util.sendMail(email, subject, content); } /** * 计划-教师初步课表发送微信推送 * @param {String} openid 微信公众号的openid * @param {String} content 内容 * @param {String} teaname 教师姓名 * @param {String} tourl 确认地址 */ async toSendWxMsg(openid, content, teaname, tourl) { if (!openid) { console.error(`计划教师发送微信推送:${teaname}没有openid`); return; } // TODO or notTODO 发送微信推送记录 await this.ctx.service.weixin.sendTemplateDesign( this.ctx.app.config.REVIEW_TEMPLATE_ID, openid, '您有一个新的通知', '您有新的安排', content, '感谢您的使用', tourl ); } } module.exports = ApplyService;