school.js 18 KB

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