school.js 18 KB

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