apply.js 17 KB

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