school.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. 'use strict';
  2. const assert = require('assert');
  3. const _ = require('lodash');
  4. const { ObjectId } = require('mongoose').Types;
  5. const { CrudService } = require('naf-framework-mongoose/lib/service');
  6. const { BusinessError, ErrorCode } = require('naf-core').Error;
  7. const moment = require('moment');
  8. const XLSX = require('xlsx');
  9. const Excel = require('exceljs');
  10. class SchoolService extends CrudService {
  11. constructor(ctx) {
  12. super(ctx, 'schoolctrl');
  13. this.model = this.ctx.model.School;
  14. this.smodel = this.ctx.model.Student;
  15. this.umodel = this.ctx.model.User;
  16. this.tmodel = this.ctx.model.Trainplan;
  17. this.jmodel = this.ctx.model.Job;
  18. this.schmodel = this.ctx.model.Schtime;
  19. }
  20. async create(data) {
  21. const { code, name } = data;
  22. assert(code, '缺少学校代码');
  23. assert(name, '缺少学校名称');
  24. const res = await this.model.create(data);
  25. if (res) {
  26. const obj = {
  27. mobile: code,
  28. name,
  29. type: '2',
  30. uid: res._id,
  31. passwd: { secret: '12345678' },
  32. };
  33. await this.umodel.create(obj);
  34. }
  35. return res;
  36. }
  37. async query({ name, ...data }, { skip, limit }) {
  38. const query = { ...data };
  39. if (name) {
  40. query.name = { $regex: name };
  41. }
  42. let res = await this.model.find(query).skip(parseInt(skip)).limit(parseInt(limit));
  43. if (res && res.length > 0) {
  44. res = JSON.parse(JSON.stringify(res));
  45. const ids = res.map(i => i._id);
  46. const users = await this.umodel.find({ uid: { $in: ids } }, '+passwd');
  47. for (const tea of res) {
  48. const r = users.find(f => f.uid === tea._id);
  49. if (r) {
  50. const passwd = _.get(r.passwd, 'secret');
  51. if (passwd) tea.passwd = passwd;
  52. }
  53. }
  54. }
  55. return res;
  56. }
  57. async count({ name, ...data } = {}) {
  58. const query = { ...data };
  59. if (name) {
  60. query.name = { $regex: name };
  61. }
  62. return await this.model.count(query);
  63. }
  64. async stuimport(data) {
  65. const { filepath, termid, schid, type, batchid } = data;
  66. assert(filepath, 'filepath不能为空');
  67. assert(termid, 'termid不能为空');
  68. assert(schid, 'schid不能为空');
  69. // 根据termid取得计划信息
  70. const plan = await this.tmodel.findOne({ 'termnum._id': ObjectId(termid) });
  71. if (!plan) {
  72. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '计划信息不存在');
  73. }
  74. const term = plan.termnum.id(termid);
  75. const planid = plan.id;
  76. const planyearid = plan.planyearid;
  77. // 检查这个范围的学生是否存在,存在的话是否更改过(classid,bedroomid这两项存不存在可以放过,但凡有一个人,就不行了)
  78. let dbStuList = await this.ctx.model.Student.find({ termid, batchid, schid });
  79. if (dbStuList.length > 0) {
  80. // 查这个学校的这期学生是否修改过班级 或 寝室
  81. const is_change = dbStuList.find(f => f.classid || f.bedroomid);
  82. if (is_change) throw new BusinessError(ErrorCode.BUSINESS, '上报过的学生已经安排班级或寝室!若需要替换学生,让同性别的学生直接来和班主任说,修改信息即可.若还是有疑问,请和中心负责人联系(最好联系下)');
  83. }
  84. // 2021-06-07 如果学生已经绑定,那也不允许修改名单了
  85. const countOpenid = await this.ctx.model.Student.count({ termid, batchid, schid, openid: { $exists: true } });
  86. if (countOpenid > 0) throw new BusinessError(ErrorCode.BUSINESS, '已有学生绑定账号,名单无法修改.若有问题请联系中心负责人!');
  87. // 获取学校名称
  88. let school_name;
  89. const sch = await this.ctx.model.School.findOne({ code: schid });
  90. if (sch) school_name = sch.name;
  91. let domain = 'http://127.0.0.1';
  92. if (process.env.NODE_ENV === 'development') domain = 'http://jytz.jilinjobs.cn';
  93. const fullUrl = domain + filepath; // this.ctx.app.config.baseUrl http://127.0.0.1 http://jytz.jilinjobs.cn
  94. let studentList = await this.getDataFromExcel(fullUrl);
  95. const checkRes = await this.checkData(studentList);
  96. const { errorcode } = checkRes;
  97. if (errorcode === '1') {
  98. return checkRes;
  99. }
  100. // 2021-05-26 添加与数据库的对比,如果数据库里已经有这个身份证号,就需要提示
  101. const countStudent = await this.countStudent(studentList, planid);
  102. const { errorcode: csec } = countStudent;
  103. if (csec === '1') {
  104. return countStudent;
  105. }
  106. // 整理数据
  107. studentList = this.lastSetData(studentList, {
  108. planyearid,
  109. planid,
  110. batchid,
  111. termid,
  112. type,
  113. schid,
  114. school_name,
  115. });
  116. const num = await this.getschnum(plan, schid, batchid);
  117. // 查看要求人数和整理完最后的人数能不能对上
  118. if (studentList.length !== num) {
  119. const res = await this.jmodel.findOne({ code: schid, batchid });
  120. const reason = `学校上传人数${studentList.length > num ? '多于' : '少于'}预期人数,请联系中心管理员`;
  121. if (res) {
  122. res.reason = reason;
  123. res.filepath = filepath;
  124. await res.save();
  125. } else {
  126. const job = {
  127. code: schid,
  128. name: school_name,
  129. planid,
  130. termid,
  131. term: term.term,
  132. batchid,
  133. filepath,
  134. studs: JSON.stringify(studentList),
  135. plannum: num,
  136. schnum: studentList.length,
  137. isstore: '0',
  138. createtime: moment().format('YYYY-MM-DD HH:SS:mm'),
  139. type,
  140. };
  141. job.reason = reason;
  142. await this.jmodel.create(job);
  143. }
  144. throw new BusinessError(ErrorCode.SERVICE_FAULT, reason);
  145. } else {
  146. // 复制,删除,添加
  147. if (dbStuList.length > 0) {
  148. dbStuList = JSON.parse(JSON.stringify(dbStuList));
  149. dbStuList = dbStuList.map(i => {
  150. delete i.meta;
  151. i.studentid = _.clone(i._id);
  152. delete i.id;
  153. delete i._id;
  154. return i;
  155. });
  156. await this.smodel.deleteMany({ termid, batchid, schid });
  157. await this.ctx.model.Dstudent.insertMany(dbStuList);
  158. }
  159. await this.smodel.insertMany(studentList);
  160. }
  161. return 'ok';
  162. }
  163. /**
  164. * 检查学生是否参加过这个计划以外的计划,参加过就不让来了
  165. * @param {Array} studentList 学生列表
  166. * @param {String} planid 计划id
  167. */
  168. async countStudent(studentList, planid) {
  169. let errorcode = '0';
  170. const errormsg = [];
  171. for (const stu of studentList) {
  172. const { name, id_number } = stu;
  173. let error = false;
  174. let msg = '';
  175. const count = await this.smodel.count({ id_number, planid: { $ne: planid } });
  176. if (count > 0) {
  177. error = true;
  178. msg = `${msg}${name}已经参加过培训`;
  179. }
  180. if (error) {
  181. errorcode = '1';
  182. stu.msg = msg;
  183. errormsg.push(stu);
  184. }
  185. }
  186. return { errorcode, errormsg };
  187. }
  188. // 取得学校预计人数
  189. async getschnum(plan, schid, batchid) {
  190. const schtime = await this.schmodel.findOne({ schid, planid: plan.id });
  191. const { arrange } = schtime;
  192. const r = arrange.find(f => f.batchid === batchid);
  193. if (!r) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '没有找到该学校的计划'); }
  194. const { number } = r;
  195. return parseInt(number);
  196. }
  197. // 整理excel数据
  198. async getDataFromExcel(url) {
  199. // 请求文件
  200. const file = await this.ctx.curl(`${url}`);
  201. if (!(file && file.data)) {
  202. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到上传的名单');
  203. }
  204. const workbook = new Excel.Workbook();
  205. // 读取文件
  206. await workbook.xlsx.load(file.data);
  207. const worksheet = workbook.getWorksheet(1);
  208. if (!worksheet) {
  209. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未发现excel中有工作表');
  210. }
  211. // 获取表头,通过方法的返回值,将写死的表头数组返回 回来
  212. const cols = this.getStucolumn();
  213. // 第一行(表头)
  214. const headRow = worksheet.getRow(1);
  215. // 设置,检查表头
  216. headRow.eachCell((cell, coli) => {
  217. if (cell.value !== '序号') {
  218. const r = cols.find(f => f.key === cell.value);
  219. if (r) {
  220. const ri = cols.findIndex(f => f.key === cell.value);
  221. // 表头符合要求,做上标记
  222. r.colIndex = coli;
  223. cols[ri] = r;
  224. } else {
  225. throw new BusinessError(`模板中"${cell.value}"列错误,请检查excel!`);
  226. }
  227. }
  228. });
  229. // 检查表头结果,如果有没有 colIndex,说明表头里有不符合要求的,退回去
  230. const excelIsRigth = cols.find(f => f.colIndex);
  231. if (!excelIsRigth) throw new BusinessError(ErrorCode.DATA_INVALID, 'Excel表格格式不正确,请使用系统提供的模板!');
  232. // 删除掉第一行 表头行,这不是数据
  233. worksheet.spliceRows(0, 1);
  234. const stuList = [];
  235. const noWhite = str => str.replace(/\s*/g, '');
  236. // 整理数据,根据检查合格的表头行,获取每个格子的数据,制成[object]格式
  237. worksheet.eachRow(row => {
  238. const stu = {};
  239. for (let i = 0; i < cols.length; i++) {
  240. const col = cols[i];
  241. if (!col) break;
  242. let val = noWhite(row.getCell(col.colIndex));
  243. if (col.column === 'id_number') val = val.toUpperCase();
  244. stu[col.column] = val;
  245. }
  246. stuList.push(stu);
  247. });
  248. return stuList;
  249. }
  250. // 数据校验
  251. async checkData(stuList) {
  252. const cols = this.getStucolumn();
  253. let errorcode = '0';
  254. const errormsg = [];
  255. for (const stu of stuList) {
  256. const { name } = stu;
  257. let error = false;
  258. let msg = '';
  259. // 各个字段检查,最低为非空检查
  260. for (const col of cols) {
  261. const { key, column } = col;
  262. if (!column) throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到导出的字段名');
  263. const val = _.get(stu, column);
  264. // 空校验
  265. if (!val || val === '') {
  266. error = true;
  267. msg = `${msg}"${key}"不能为空;`;
  268. continue;
  269. }
  270. // 性别校验
  271. if (column === 'gender') {
  272. if (!(val.includes('男') || val.includes('女'))) {
  273. error = true;
  274. msg = `${msg}性别错误;`;
  275. }
  276. continue;
  277. }
  278. // 身份证号校验
  279. if (column === 'id_number') {
  280. // 因为删除再添加的流程导致此处 不能 校验数据库中是否有这个身份证号
  281. // const res = await this.ctx.model.Student.findOne({ id_number: val });
  282. // if (!res) {
  283. const { pass, msg: idmsg } = this.ctx.service.school.idCodeValid(val);
  284. if (!pass) {
  285. error = true;
  286. msg = `${msg}${idmsg};`;
  287. }
  288. // } else {
  289. // error = true;
  290. // msg = `${msg}学生已存在`;
  291. // }
  292. const have_same = stuList.filter(f => f.id_number === val && f.name !== name);
  293. if (have_same.length > 0) {
  294. error = true;
  295. const h = _.head(have_same);
  296. const num = have_same.length;
  297. if (num === 1) {
  298. msg = `${msg}身份证号与本次名单的"${h.name}"重复;`;
  299. } else msg = `${msg}身份证号与本次名单中"${h.name}"等${num}人重复;`;
  300. }
  301. continue;
  302. }
  303. // 手机号校验
  304. if (column === 'phone') {
  305. // 因为删除再添加的流程导致此处 不能 校验数据库中是否有这个手机号
  306. // const res = await this.ctx.model.Student.findOne({ phone: val });
  307. // if (!res) {
  308. if (!/^\d{11}$/i.test(val)) {
  309. error = true;
  310. msg = `${msg}手机号位数不正确;`;
  311. }
  312. // } else {
  313. // error = true;
  314. // msg = `${msg}学生库中已有该手机号,请检查手机号是否正确,若无误,请联系中心负责人`;
  315. // }
  316. const have_same = stuList.filter(f => f.phone === val && f.name !== name);
  317. if (have_same.length > 0) {
  318. error = true;
  319. const h = _.head(have_same);
  320. const num = have_same.length;
  321. if (num === 1) {
  322. msg = `${msg}手机号与本次名单的"${h.name}"重复;`;
  323. } else msg = `${msg}手机号与本次名单中"${h.name}"等${num}人重复;`;
  324. }
  325. continue;
  326. }
  327. // 专业校验
  328. if (column === 'major') {
  329. if (val.includes('专业')) {
  330. error = true;
  331. msg = `${msg}专业列不能含有"专业"二字;`;
  332. }
  333. continue;
  334. }
  335. // 入学年份
  336. if (column === 'entry_year') {
  337. const m = /^\w{4}$/;
  338. if (!val.match(m)) {
  339. error = true;
  340. msg = `${msg}入学年份格式不正确,只填写4位数字;`;
  341. }
  342. continue;
  343. }
  344. // 毕业年份
  345. if (column === 'finish_year') {
  346. const m = /^\w{4}$/;
  347. if (!val.match(m)) {
  348. error = true;
  349. msg = `${msg}毕业年份格式不正确,只填写4位数字;`;
  350. }
  351. continue;
  352. }
  353. // 双困检查
  354. if (column === 'family_is_hard') {
  355. if (!(val.includes('是') || val.includes('否'))) {
  356. error = true;
  357. msg = `${msg}家庭是否困难填写"是"或"否";`;
  358. }
  359. continue;
  360. }
  361. if (column === 'have_grant') {
  362. if (!(val.includes('是') || val.includes('否'))) {
  363. error = true;
  364. msg = `${msg}是否获得过助学金填写"是"或"否";`;
  365. }
  366. continue;
  367. }
  368. }
  369. if (error) {
  370. errorcode = '1';
  371. stu.msg = msg;
  372. errormsg.push(stu);
  373. }
  374. }
  375. return { errorcode, errormsg };
  376. }
  377. // 最后整合数据
  378. lastSetData(stuList, data) {
  379. const cols = this.getStucolumn();
  380. const needChange = cols.filter(f => f.change);
  381. stuList = stuList.map(i => {
  382. const d = { ...i, ...data };
  383. for (const col of needChange) {
  384. const { column, change } = col;
  385. if (!column && change && _.isArray(change)) continue;
  386. const val = _.get(d, column);
  387. if (!val) continue;
  388. const r = change.find(f => f.key === val);
  389. if (!r) continue;
  390. const { value } = r;
  391. d[column] = value;
  392. }
  393. return d;
  394. });
  395. return stuList;
  396. }
  397. // excel中学生字段
  398. getStucolumn() {
  399. const arr = [
  400. { key: '姓名', column: 'name' },
  401. { key: '性别', column: 'gender' },
  402. { key: '民族', column: 'nation' },
  403. { key: '身份证号', column: 'id_number' },
  404. { key: '学校名称', column: 'school_name' },
  405. { key: '学历层次', column: 'edua_level' },
  406. { key: '学制', column: 'edua_system' },
  407. { key: '院(系)', column: 'faculty' },
  408. { key: '专业', column: 'major' },
  409. { key: '入学年份', column: 'entry_year' },
  410. { key: '毕业年份', column: 'finish_year' },
  411. { key: '在校曾担任何种职务', column: 'school_job' },
  412. { key: '手机号', column: 'phone' },
  413. { key: 'QQ号', column: 'qq' },
  414. { key: '家庭所在地', column: 'family_place' },
  415. {
  416. key: '家庭是否困难',
  417. column: 'family_is_hard',
  418. change: [
  419. { key: '否', value: '0' },
  420. { key: '是', value: '1' },
  421. ],
  422. },
  423. {
  424. key: '是否获得过助学金',
  425. column: 'have_grant',
  426. change: [
  427. { key: '否', value: '0' },
  428. { key: '是', value: '1' },
  429. ],
  430. },
  431. ];
  432. return arr;
  433. }
  434. // 导出学校名单
  435. async exportSchool({ trainplanId }) {
  436. // 批次期次都在这里面
  437. const trainplan = await this.tmodel.find({ _id: trainplanId });
  438. const _headers = [{ key: 'title', title: '计划标题' }];
  439. // 需要打出的列表
  440. const _data = trainplan;
  441. const headers = _headers
  442. .map(({ title }) => title)
  443. .map((v, i) =>
  444. Object.assign({}, { v, position: String.fromCharCode(65 + i) + 1 })
  445. )
  446. .reduce(
  447. (prev, next) =>
  448. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  449. {}
  450. );
  451. const data = _data
  452. .map((v, i) =>
  453. _headers.map(({ key }, j) =>
  454. Object.assign(
  455. {},
  456. { v: v[key], position: String.fromCharCode(65 + j) + (i + 2) }
  457. )
  458. )
  459. )
  460. .reduce((prev, next) => prev.concat(next))
  461. .reduce(
  462. (prev, next) =>
  463. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  464. {}
  465. );
  466. // 合并 headers 和 data
  467. const output = Object.assign({}, headers, data);
  468. // 获取所有单元格的位置
  469. const outputPos = Object.keys(output);
  470. // 计算出范围
  471. const ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];
  472. // 构建 workbook 对象
  473. const nowDate = new Date().getTime();
  474. const path =
  475. 'D:\\wwwroot\\service\\service-file\\upload\\train\\' + nowDate + '.xlsx';
  476. const respath =
  477. 'http://free.liaoningdoupo.com:80/files/train/' + nowDate + '.xlsx';
  478. const wb = {
  479. SheetNames: [ 'sheet0' ],
  480. Sheets: { sheet0: Object.assign({}, output, { '!ref': ref }) },
  481. };
  482. // 导出 Excel
  483. XLSX.writeFile(wb, path);
  484. return respath;
  485. }
  486. async updateclass({ trainplanid, classid, rightHeader }) {
  487. assert(trainplanid && classid && rightHeader, '缺少参数项');
  488. // 根据全年计划表id查出对应的全年计划详细信息
  489. const trainplan = await this.model.findById(trainplanid);
  490. if (!trainplan) {
  491. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  492. }
  493. for (const term of trainplan.termnum) {
  494. for (const batch of term.batchnum) {
  495. const class_ = await batch.class.id(classid);
  496. if (class_) {
  497. class_.headteacherid = rightHeader;
  498. }
  499. }
  500. }
  501. return await trainplan.save();
  502. }
  503. async updatereteacher({ trainplanid, termid, reteacher }) {
  504. assert(trainplanid && termid && reteacher, '缺少参数项');
  505. // 根据全年计划表id查出对应的全年计划详细信息
  506. const trainplan = await this.model.findById(trainplanid);
  507. if (!trainplan) {
  508. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  509. }
  510. const term = await trainplan.termnum.id(termid);
  511. if (term) {
  512. term.reteacher = reteacher;
  513. }
  514. return await trainplan.save();
  515. }
  516. // 身份证验证
  517. idCodeValid(code) {
  518. // 身份证号合法性验证
  519. // 支持15位和18位身份证号
  520. // 支持地址编码、出生日期、校验位验证
  521. const city = {
  522. 11: '北京',
  523. 12: '天津',
  524. 13: '河北',
  525. 14: '山西',
  526. 15: '内蒙古',
  527. 21: '辽宁',
  528. 22: '吉林',
  529. 23: '黑龙江 ',
  530. 31: '上海',
  531. 32: '江苏',
  532. 33: '浙江',
  533. 34: '安徽',
  534. 35: '福建',
  535. 36: '江西',
  536. 37: '山东',
  537. 41: '河南',
  538. 42: '湖北 ',
  539. 43: '湖南',
  540. 44: '广东',
  541. 45: '广西',
  542. 46: '海南',
  543. 50: '重庆',
  544. 51: '四川',
  545. 52: '贵州',
  546. 53: '云南',
  547. 54: '西藏 ',
  548. 61: '陕西',
  549. 62: '甘肃',
  550. 63: '青海',
  551. 64: '宁夏',
  552. 65: '新疆',
  553. 71: '台湾',
  554. 81: '香港',
  555. 82: '澳门',
  556. 91: '国外 ',
  557. };
  558. let row = {
  559. pass: true,
  560. msg: '验证成功',
  561. };
  562. if (
  563. !code ||
  564. !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/.test(
  565. code
  566. )
  567. ) {
  568. row = {
  569. pass: false,
  570. msg: '身份证号格式错误',
  571. };
  572. } else if (!city[code.substr(0, 2)]) {
  573. row = {
  574. pass: false,
  575. msg: '身份证号地址编码错误',
  576. };
  577. } else {
  578. // 18位身份证需要验证最后一位校验位
  579. if (code.length == 18) {
  580. code = code.split('');
  581. // ∑(ai×Wi)(mod 11)
  582. // 加权因子
  583. const factor = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ];
  584. // 校验位
  585. const parity = [ 1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2 ];
  586. let sum = 0;
  587. let ai = 0;
  588. let wi = 0;
  589. for (let i = 0; i < 17; i++) {
  590. ai = code[i];
  591. wi = factor[i];
  592. sum += ai * wi;
  593. }
  594. if (parity[sum % 11] != code[17].toUpperCase()) {
  595. row = {
  596. pass: false,
  597. msg: '身份证号校验位错误',
  598. };
  599. }
  600. }
  601. }
  602. return row;
  603. }
  604. }
  605. module.exports = SchoolService;