apply.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. 'use strict';
  2. const assert = require('assert');
  3. const { after } = require('lodash');
  4. const _ = require('lodash');
  5. const moment = require('moment');
  6. const { ObjectId } = require('mongoose').Types;
  7. const { CrudService } = require('naf-framework-mongoose/lib/service');
  8. const { BusinessError, ErrorCode } = require('naf-core').Error;
  9. class ApplyService extends CrudService {
  10. constructor(ctx) {
  11. super(ctx, 'apply');
  12. this.model = this.ctx.model.Apply;
  13. this.tmodel = this.ctx.model.Teacher;
  14. this.submodel = this.ctx.model.Subject;
  15. this.trainmodel = this.ctx.model.Trainplan;
  16. this.umodel = this.ctx.model.User;
  17. this.nmodel = this.ctx.model.Notice;
  18. this.dayList = [ '日', '一', '二', '三', '四', '五', '六' ];
  19. }
  20. // 查询
  21. async queryteacher(query) {
  22. const { termid, subid, date } = query;
  23. const data = await this.model
  24. .find({ termid, subid, date })
  25. .sort({ msscore: -1 });
  26. const teachers = [];
  27. for (const _data of data) {
  28. const teacherid = _data.teacherid;
  29. const teacher = await this.tmodel.findById(teacherid);
  30. teachers.push(teacher);
  31. }
  32. return teachers;
  33. }
  34. /**
  35. * 教师计划初步课表安排,可反复使用
  36. * @param {Object} body planid:计划id,ids:期数id列表;classtype:班级类型
  37. */
  38. async arrangeteacher({ planid, ids, classtype }) {
  39. assert(planid, '缺少计划信息');
  40. const trainplan = await this.trainmodel.findById(planid);
  41. if (!trainplan) {
  42. throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在');
  43. }
  44. // trainplan = JSON.parse(JSON.stringify(trainplan));
  45. // 查找所有教师列表
  46. let teacherList = await this.tmodel.find({ xsscore: { $exists: true } });
  47. teacherList = JSON.parse(JSON.stringify(teacherList));
  48. // 查找所有教师上报列表(今年)
  49. const year = moment().format('YYYY');
  50. const start = `${year}-01-01`;
  51. const end = `${year}-12-31`;
  52. let teaplanList = await this.model.find({ date: { $gte: start, $lte: end } });
  53. teaplanList = JSON.parse(JSON.stringify(teaplanList));
  54. // 课程
  55. let subjectList = await this.submodel.find();
  56. subjectList = JSON.parse(JSON.stringify(subjectList));
  57. const termList = _.cloneDeep(trainplan);
  58. let { termnum } = termList;
  59. if (!termnum) return;
  60. termnum = JSON.parse(JSON.stringify(termnum));
  61. // 整理出课表
  62. const arr = this.setLessonList(termnum);
  63. // 安排后的课表
  64. const afterList = [];
  65. // 教师排课计数对象 key:teacherid;value:次数
  66. let teacherCount = {};
  67. // 循环拍平的课表
  68. for (const l of arr) {
  69. const { termid, subid, day: date, status, type, classid } = l;
  70. // 检验是否在需要安排的期内
  71. if (!ids.includes(termid)) {
  72. // 不在要求安排的期内,教师计数后就直接放回去
  73. teacherCount = this.countTeacher(l, teacherCount);
  74. afterList.push(l);
  75. continue;
  76. }
  77. // 检查是否符合要求的班级类型
  78. if (classtype || `${classtype}` === '0') {
  79. // 不符合,教师计数,放回去,下一个
  80. if (`${classtype}` !== `${type}`) {
  81. teacherCount = this.countTeacher(l, teacherCount);
  82. afterList.push(l);
  83. continue;
  84. }
  85. }
  86. // 检查是否确定
  87. if (status && `${status}` === '1') {
  88. // 确定了,教师计数,放回去
  89. teacherCount = this.countTeacher(l, teacherCount);
  90. afterList.push(l);
  91. continue;
  92. }
  93. // 重置教师:为什么在这?如果不需要教师,那也得重置;需要教师,下面重排,也重置,所以在这!
  94. l.teaid = null;
  95. l.teaname = null;
  96. // 查看这科要不要教师
  97. const subject = subjectList.find(f => ObjectId(subid).equals(f._id));
  98. if (subject.need_teacher !== '0') {
  99. afterList.push(l);
  100. continue;
  101. }
  102. // 到这了,就说明差不多能排了
  103. // 整份上报教师列表
  104. let applyList = _.cloneDeep(teaplanList);
  105. // 申请该天,该科目的教师,并查出教师的名字,分数;
  106. applyList = applyList.filter(f => f.date === date && f.subid === subid);
  107. applyList = applyList.map(i => {
  108. let obj = { ...JSON.parse(JSON.stringify(i)) };
  109. const r = teacherList.find(f => i.teacherid === f._id);
  110. if (r) {
  111. const { name: teaname, xsscore: score } = r;
  112. const nscore = _.isNaN(parseFloat(score)) ? 0 : parseFloat(score);
  113. i.teaname = teaname;
  114. i.score = nscore;
  115. obj = { ...obj, teaname, score: nscore };
  116. obj = _.omit(obj, [ 'meta' ]);
  117. }
  118. return obj;
  119. });
  120. // 过滤出没有分数的,不排
  121. applyList = applyList.filter(f => f.score && f.score !== 0);
  122. // 21-04-26添加:找到教师已经排列的次数,也加上
  123. for (const apply of applyList) apply.count = _.get(teacherCount, apply.teacherid, 0);
  124. // 按已经安排的次数-升序;成绩-降序 排序
  125. applyList = _.orderBy(applyList, [ 'count', 'score' ], [ 'asc', 'desc' ]);
  126. // 教师准备完了,之后就该排了
  127. let reserve;// 最优备选解
  128. // 条件:1(强制),同一天,一个教师只能有一次,因为一上一天课
  129. for (const atea of applyList) {
  130. // 条件1处理:
  131. // tr为该教师在该天(现在安排的这天)有课:数据为2种:Object/undefined:object表示有课了,反之没有课
  132. const tr = afterList.find(f => f.teaid === atea.teacherid && f.day === atea.date);
  133. if (tr) continue;
  134. // TODO:条件2(非强制)教师尽量不教同一个班.
  135. // 不强制的原因是:因为只按这个条件走,真的可能会出现空缺,但是还是有教师报了.
  136. // 处理:将该教师放入备选,继续向下找,如果找到了,说明那个教师就是最优解;如果没有找到,说明,备选教师仍是最优解
  137. // 找该教师在该班是否有课(遵循先排不动原则)
  138. const already_teach = afterList.find(f => f.teaid === atea.teacherid && f.classid === classid);
  139. if (already_teach) {
  140. // 说明这个教师已经教过这个班了,再看下最优解有没有
  141. if (reserve) {
  142. // 已经有最优解了,直接下个教师
  143. continue;
  144. } else {
  145. // 还没有最优解,这个就是最优解
  146. reserve = atea;
  147. }
  148. } else {
  149. // 这个教师没有教过这个班,直接上
  150. l.teaid = atea.teacherid;
  151. l.teaname = atea.teaname;
  152. break;
  153. }
  154. // 直接选取这个教师即可,因为已经是上报的教师中次数最少的了
  155. }
  156. // 上面处理完了,该检查有没有教师了:2种情况,有教师;没有教师,有备选最优解;
  157. if (!l.teaid && reserve) {
  158. // 没有教师,但是有备选最优解,那就用备选最优解
  159. l.teaid = reserve.teacherid;
  160. l.teaname = reserve.teaname;
  161. }
  162. // 教师计数
  163. teacherCount = this.countTeacher(l, teacherCount);
  164. afterList.push(l);
  165. }
  166. const newTermnum = this.returnTermnum(afterList, termnum);
  167. // 下面代码是检查本次计划教师都排了多少次
  168. // const keys = Object.keys(teacherCount);
  169. // for (const key of keys) {
  170. // const r = teacherList.find(f => f._id === key);
  171. // if (r) console.log(`${r.name}:${teacherCount[key]}`);
  172. // }
  173. // 保存至计划
  174. trainplan.termnum = newTermnum;
  175. await trainplan.save();
  176. }
  177. // 确认计划安排
  178. async arrangeConfirm({ planid, ids, classtype }) {
  179. const trainplan = await this.trainmodel.findById(planid);
  180. if (!trainplan) {
  181. throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在');
  182. }
  183. const plan = _.cloneDeep(trainplan);
  184. let { termnum } = plan;
  185. if (!termnum) return;
  186. termnum = JSON.parse(JSON.stringify(termnum));
  187. // 过滤出确认的期,TODO:没有做通知
  188. // termnum = termnum.filter(f => );
  189. // 找到每个教师的位置,然后把状态(status)改成1=>已确认
  190. for (const t of termnum) {
  191. if (!ids.includes(t._id)) continue;
  192. const { term } = t;
  193. if (!(t.batchnum && _.isArray(t.batchnum))) continue;
  194. for (const b of t.batchnum) {
  195. const { batch } = b;
  196. if (!(b.class && _.isArray(b.class))) continue;
  197. for (const c of b.class) {
  198. // 检查是否要求班级类型
  199. if (classtype || `${classtype}` === '0') {
  200. // 获取这个班级的班级类型
  201. const { type } = c;
  202. // 判断班级类型与要求的符不符合,不符合就跳过不改
  203. if (`${type}` !== `${classtype}`) continue;
  204. }
  205. if (!(c.lessons && _.isArray(c.lessons))) continue;
  206. for (const l of c.lessons) {
  207. l.status = '1';
  208. }
  209. }
  210. }
  211. }
  212. trainplan.termnum = termnum;
  213. await trainplan.save();
  214. }
  215. /**
  216. * 给教师计数
  217. * @param {Object} data 拍平的数据
  218. * @param {Object} countObj 教师计数对象
  219. */
  220. countTeacher(data, countObj) {
  221. const { teaid, teaname } = data;
  222. if (teaid) { countObj[`${teaid}`] = _.add(_.get(countObj, `${teaid}`, 0), 1); }
  223. return countObj;
  224. }
  225. /**
  226. * 拍平了的课表=>termnum
  227. * @param {Array} list 拍平了的课表,详情参考页面的初步课表的数据
  228. * @param {Array} termnum 原termnum
  229. */
  230. returnTermnum(list, termnum) {
  231. let newTermnum = [];
  232. for (const l of list) {
  233. const { termid, batchid, classid, ...info } = l;
  234. const updata = _.pick(info, [
  235. 'day',
  236. 'subid',
  237. 'subname',
  238. 'teaid',
  239. 'teaname',
  240. 'time',
  241. 'status',
  242. ]);
  243. newTermnum = termnum.map(t => {
  244. // 找到期
  245. if (termid === t._id) {
  246. t.batchnum = t.batchnum.map(b => {
  247. if (batchid === b._id) {
  248. // 找到批次
  249. b.class = b.class.map(c => {
  250. if (classid === c._id) {
  251. if (c.lessons) {
  252. // 说明有课程安排,找有没有重复的,没有就推进去,有就更改,subid查
  253. const r = c.lessons.find(f => f.subid === updata.subid);
  254. if (r) {
  255. const rindex = c.lessons.findIndex(
  256. f => f.subid === updata.subid
  257. );
  258. c.lessons[rindex] = updata;
  259. } else {
  260. c.lessons.push(updata);
  261. }
  262. } else {
  263. // 说明没有课程安排,放进去一条保存
  264. c.lessons = [ updata ];
  265. }
  266. }
  267. return c;
  268. });
  269. }
  270. return b;
  271. });
  272. }
  273. return t;
  274. });
  275. }
  276. return newTermnum;
  277. }
  278. /**
  279. * 将课表拍平了,从多维=>一维
  280. * @param {Array} termnum 计划的termnum
  281. */
  282. setLessonList(termnum) {
  283. let arr = [];
  284. for (const t of termnum) {
  285. const { batchnum, term, _id: termid } = t;
  286. // 班级和课程一一匹
  287. for (const b of batchnum) {
  288. const { class: classes, lessons, _id: batchid } = b;
  289. const claslesList = this.setList(
  290. term * 1,
  291. termid,
  292. batchid,
  293. classes,
  294. lessons
  295. );
  296. arr.push(...claslesList);
  297. }
  298. }
  299. arr = _.orderBy(arr, [ 'term', 'day' ], [ 'asc', 'asc' ]);
  300. return arr;
  301. }
  302. /**
  303. * 将课表模板和班级整理成一维数组
  304. * @param {String} term 期数
  305. * @param {String} termid 期id
  306. * @param {String} batchid 批id
  307. * @param {Array} classes 班级列表
  308. * @param {Array} lessonTemplate 课表模板
  309. */
  310. setList(term, termid, batchid, classes, lessonTemplate) {
  311. const arr = [];
  312. // 班级和课程匹配
  313. for (const cla of classes) {
  314. let { lessons } = cla;
  315. if (!lessons) lessons = lessonTemplate;
  316. for (const i of lessons) {
  317. let nobj = {};
  318. nobj.term = term;
  319. nobj.termid = termid;
  320. nobj.batchid = batchid;
  321. nobj.type = cla.type;
  322. const obj = _.omit(cla, [ 'lessons' ]);
  323. nobj.classid = _.clone(cla._id);
  324. nobj = _.assign(nobj, obj);
  325. nobj = _.assign(nobj, i);
  326. arr.push(nobj);
  327. }
  328. }
  329. return arr;
  330. }
  331. /**
  332. * 发送消息
  333. * @param {Object} param planid:年度计划id,ids,发送的期列表;classtype:发送班级类型 undefined 都发,有的话就找指定班级类型发
  334. */
  335. async arrangeSendMsg({ planid, ids, classtype }) {
  336. const trainplan = await this.trainmodel.findById(planid);
  337. if (!trainplan) {
  338. throw new BusinessError(ErrorCode.DATA_EXISTED, '年度计划不存在');
  339. }
  340. // 大批次id,年度计划id
  341. const plan = _.cloneDeep(trainplan);
  342. let { termnum, planyearid } = plan;
  343. if (!termnum) return;
  344. termnum = JSON.parse(JSON.stringify(termnum));
  345. // 整理出课表
  346. let arr = this.setLessonList(termnum);
  347. // 过滤出需要发送的教师
  348. arr = arr.filter(f => ids.find(id => f.termid === id) && f.teaid);
  349. // && f.status !== '1'
  350. // 整理出要发送的教师列表
  351. let teaids = arr.map(i => i.teaid);
  352. teaids = _.uniq(teaids);
  353. // 找到教师信息
  354. let teaList = await this.tmodel.find({ _id: teaids });
  355. // 找到教师用户信息
  356. let teauserList = await this.umodel.find({ uid: teaids });
  357. if (teaList) teaList = JSON.parse(JSON.stringify(teaList));
  358. if (teauserList) teauserList = JSON.parse(JSON.stringify(teauserList));
  359. // 发送,此处是根据安排,给教师发.还有一种方案是根据教师,整理安排一起发送
  360. // 查询是否发送过这期的通知
  361. // 排序
  362. arr = _.orderBy(arr, [ 'day' ], [ 'asc' ]);
  363. for (const l of arr) {
  364. // 教师id,期数,班级名,上课的日期,课程名
  365. const {
  366. teaid,
  367. term,
  368. name,
  369. day,
  370. subname,
  371. termid,
  372. classid,
  373. type,
  374. status,
  375. } = l;
  376. // 已确认的教师不发信息
  377. if (status === '1') continue;
  378. // 判断发送的班级类型
  379. if (!(classtype && classtype === type)) continue;
  380. const tea = teaList.find(f => f._id === teaid);
  381. const teauser = teauserList.find(f => f.uid === teaid);
  382. // 文案
  383. let msg = `${_.get(tea, 'name', '')}老师您好:
  384. 吉林省高等学校毕业生就业指导中心-双困生培训系统提醒您:
  385. ${term}期-${name.includes('班') ? name : `${name}班`}
  386. ${day}(星期${this.dayList[moment(day).days()]})
  387. 有您的课程安排:${subname}`;
  388. msg = `${msg}\n 如果您无法进行授课,请及时联系中心负责人`;
  389. const { openid } = teauser;
  390. let tourl;
  391. let to_send = false;
  392. if (openid) {
  393. let notice = await this.nmodel.findOne({
  394. planid,
  395. termid,
  396. classid,
  397. type: '6',
  398. });
  399. // 找下是否发过信息
  400. if (notice) {
  401. // 发过信息,找有没有这个教师
  402. const { notified } = notice;
  403. if (_.isArray(notified)) {
  404. const has_notice = notified.find(f => f.notifiedid === teaid);
  405. if (has_notice) {
  406. const { status } = has_notice;
  407. if (status !== '1') to_send = true;
  408. } else {
  409. const obj = {
  410. notifiedid: teaid,
  411. username: _.get(tea, 'name', ''),
  412. content: msg,
  413. };
  414. notice.notified.push(obj);
  415. await notice.save();
  416. to_send = true;
  417. }
  418. }
  419. } else {
  420. const notified = [
  421. {
  422. notifiedid: teaid,
  423. username: _.get(tea, 'name', ''),
  424. content: msg,
  425. },
  426. ];
  427. const noticeObj = {
  428. planyearid,
  429. planid,
  430. termid,
  431. classid,
  432. noticeid: 'system',
  433. type: '6',
  434. content: `${term}期-${
  435. name.includes('班') ? name : `${name}班`
  436. }教师计划初步信息确认`,
  437. notified,
  438. };
  439. await this.nmodel.create(noticeObj);
  440. notice = await this.nmodel.findOne({
  441. planid,
  442. termid,
  443. classid,
  444. type: '6',
  445. });
  446. to_send = true;
  447. }
  448. tourl =
  449. this.ctx.app.config.baseUrl +
  450. '/msgconfirm/?userid=' +
  451. teaid +
  452. '&noticeid=' +
  453. notice._id;
  454. }
  455. if (to_send) {
  456. // 邮箱与微信都发送
  457. const { email } = tea;
  458. if (email) {
  459. this.toSendEmail(email, msg, tea.name);
  460. }
  461. if (openid) {
  462. this.toSendWxMsg(openid, msg, tea.name, tourl);
  463. }
  464. }
  465. }
  466. }
  467. /**
  468. * 计划-教师初步课表发送邮件
  469. * @param {String} email 邮件
  470. * @param {String} content 内容
  471. * @param {String} teaname 教师姓名
  472. */
  473. async toSendEmail(email, content, teaname) {
  474. if (!email) {
  475. console.error(`计划教师发送通知:${teaname}没有email`);
  476. return;
  477. }
  478. const subject = '吉林省高等学校毕业生就业指导中心通知(系统邮件,请勿回复)'; //
  479. this.ctx.service.util.sendMail(email, subject, content);
  480. }
  481. /**
  482. * 计划-教师初步课表发送微信推送
  483. * @param {String} openid 微信公众号的openid
  484. * @param {String} content 内容
  485. * @param {String} teaname 教师姓名
  486. * @param {String} tourl 确认地址
  487. */
  488. async toSendWxMsg(openid, content, teaname, tourl) {
  489. if (!openid) {
  490. console.error(`计划教师发送微信推送:${teaname}没有openid`);
  491. return;
  492. }
  493. // TODO or notTODO 发送微信推送记录
  494. await this.ctx.service.weixin.sendTemplateDesign(
  495. this.ctx.app.config.REVIEW_TEMPLATE_ID,
  496. openid,
  497. '您有一个新的通知',
  498. '您有新的安排',
  499. content,
  500. '感谢您的使用',
  501. tourl
  502. );
  503. }
  504. async repealConfirm({ planid, ids }) {
  505. // 将指定计划,期数的教师状态解除确认
  506. let trainPlan = await this.ctx.model.Trainplan.findById(planid);
  507. trainPlan = JSON.parse(JSON.stringify(trainPlan));
  508. let terms = {};
  509. for (const term of trainPlan.termnum) {
  510. const in_ids = ids.find(f => ObjectId(f).equals(term._id));
  511. if (in_ids) {
  512. for (const batch of term.batchnum) {
  513. for (const c of batch.class) {
  514. for (const l of c.lessons) {
  515. l.status = '0';
  516. }
  517. }
  518. }
  519. terms = term;
  520. }
  521. }
  522. // const i = trainPlan.termnum.findIndex(f => ObjectId('5f5aed5e69b4221aedaa5005').equals(f._id));
  523. // trainPlan.termnum[i] = terms;
  524. delete trainPlan.meta;
  525. const r = await this.ctx.model.Trainplan.update(
  526. { _id: ObjectId('5f5adb337ceb003386c9b0d4') },
  527. { ...trainPlan }
  528. );
  529. }
  530. }
  531. module.exports = ApplyService;