school.js 19 KB

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