school.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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. class SchoolService extends CrudService {
  10. constructor(ctx) {
  11. super(ctx, 'schoolctrl');
  12. this.model = this.ctx.model.School;
  13. this.smodel = this.ctx.model.Student;
  14. this.umodel = this.ctx.model.User;
  15. this.tmodel = this.ctx.model.Trainplan;
  16. this.jmodel = this.ctx.model.Job;
  17. this.schmodel = this.ctx.model.Schtime;
  18. }
  19. async create(data) {
  20. const { code, name } = data;
  21. assert(code, '缺少学校代码');
  22. assert(name, '缺少学校名称');
  23. const res = await this.model.create(data);
  24. if (res) {
  25. const obj = {
  26. mobile: code,
  27. name,
  28. type: '2',
  29. uid: res._id,
  30. passwd: { secret: '12345678' },
  31. };
  32. await this.umodel.create(obj);
  33. }
  34. return res;
  35. }
  36. async stuimport(data) {
  37. const { filepath, termid, schid, type, batchid } = data;
  38. assert(filepath, 'filepath不能为空');
  39. assert(termid, 'termid不能为空');
  40. assert(schid, 'schid不能为空');
  41. // 根据termid取得计划信息
  42. const plan = await this.tmodel.findOne({ 'termnum._id': ObjectId(termid) });
  43. if (!plan) {
  44. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '计划信息不存在');
  45. }
  46. // const isOutOfDate = this.outOfDate(plan, termid, batchid);
  47. // if (!isOutOfDate) {
  48. // throw new BusinessError(
  49. // ErrorCode.BUSINESS,
  50. // '已经超过上报时间,不允许上报名单'
  51. // );
  52. // }
  53. // 取得学校预计人数
  54. const num_ = await this.getschnum(plan, type, schid, termid, batchid);
  55. // console.log('*******************');
  56. // console.log(num_);
  57. // console.log('*******************');
  58. // 检查学校是否上传过学生
  59. const schstu = await this.smodel.count({ schid, batchid });
  60. if (schstu && schstu > 0) throw new BusinessError(ErrorCode.BUSINESS, '该批次已经上传过学生,无需重复上传,若人员有变化,请联系中心负责人');
  61. const planid = plan.id;
  62. const planyearid = plan.planyearid;
  63. // 取得excle中数据
  64. const _filepath = 'http://127.0.0.1' + filepath; // this.ctx.app.config.baseUrl http://127.0.0.1 http://jytz.jilinjobs.cn
  65. const studatas = await this.getImportXLSXData(
  66. _filepath,
  67. termid,
  68. schid,
  69. planid,
  70. planyearid,
  71. type,
  72. batchid
  73. );
  74. // 将得到的数据校验
  75. const datacheck = await this.datacheck(studatas);
  76. if (datacheck.errorcode === '1') {
  77. return datacheck;
  78. }
  79. const school_ = await this.model.findOne({ code: schid });
  80. let schname = '';
  81. if (school_) {
  82. schname = school_.name;
  83. }
  84. const trem_ = await plan.termnum.id(termid);
  85. if (!trem_) {
  86. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '期信息不存在');
  87. }
  88. const nowtime = moment().locale('zh-cn').format('YYYY-MM-DD HH:mm:ss');
  89. if (studatas.length > num_) {
  90. const jobdata = {
  91. code: schid,
  92. name: schname,
  93. planid: plan.id,
  94. termid,
  95. term: trem_.term,
  96. batchid,
  97. filepath,
  98. studs: JSON.stringify(studatas),
  99. plannum: num_,
  100. schnum: studatas.length,
  101. isstore: '0',
  102. createtime: nowtime,
  103. type,
  104. reason: '学校上传人数超过预期人数,请联系中心管理员',
  105. };
  106. await this.jmodel.create(jobdata);
  107. throw new BusinessError(
  108. ErrorCode.SERVICE_FAULT,
  109. '学校上传人数超过预期人数,请联系中心管理员'
  110. );
  111. } else if (studatas.length < num_) {
  112. const jobdata = {
  113. code: schid,
  114. name: schname,
  115. planid: plan.id,
  116. termid,
  117. term: trem_.term,
  118. batchid,
  119. filepath,
  120. studs: JSON.stringify(studatas),
  121. plannum: num_,
  122. schnum: studatas.length,
  123. isstore: '0',
  124. createtime: nowtime,
  125. type,
  126. reason: '学校上传人数少于预期人数,请联系中心管理员',
  127. };
  128. await this.jmodel.create(jobdata);
  129. throw new BusinessError(
  130. ErrorCode.SERVICE_FAULT,
  131. '学校上传人数少于预期人数,请联系中心管理员'
  132. );
  133. }
  134. // 将数据存入数据库中
  135. for (const stu of studatas) {
  136. const res = await this.smodel.create(stu);
  137. // if (res) {
  138. // const newdata = { name: stu.name, mobile: stu.phone, type: '4', uid: res.id };
  139. // newdata.passwd = { secret: '12345678' };
  140. // await this.umodel.create(newdata);
  141. // }
  142. }
  143. return datacheck;
  144. }
  145. // 取得学校预计人数
  146. async getschnum(plan, type, schid, termid, batchid) {
  147. const schtime = await this.schmodel.findOne({ schid, planid: plan.id });
  148. const { arrange } = schtime;
  149. const r = arrange.find(f => f.batchid === batchid);
  150. if (!r) { throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '没有找到该学校的计划'); }
  151. const { number } = r;
  152. return parseInt(number);
  153. // const { termnum } = plan;
  154. // arrange = _.groupBy(arrange, 'termid');
  155. // const keys = Object.keys(arrange);
  156. // let arr = keys.map(key => {
  157. // const rt = termnum.find(f => ObjectId(key).equals(f._id));
  158. // let ar = arrange[key];
  159. // ar = ar.map(a => {
  160. // const rb = rt.batchnum.find(f => ObjectId(a.batchid).equals(f._id));
  161. // if (rb) {
  162. // const bh = _.head(rb.class);
  163. // const { type } = bh;
  164. // a.type = type;
  165. // return a;
  166. // }
  167. // });
  168. // let garr = _.groupBy(ar, 'type');
  169. // const gks = Object.keys(garr);
  170. // garr = gks.map(gk => {
  171. // const { term, termid } = _.head(garr[gk]);
  172. // const number = garr[gk].reduce((p, n) => p + n.number * 1, 0);
  173. // return { term, termid, number, type: gk };
  174. // });
  175. // return garr;
  176. // });
  177. // arr = arr.flat();
  178. // const obj_ = _.find(arr, { termid, type });
  179. return obj_.number;
  180. }
  181. // 获取导入的XLSX文件中的数据
  182. async getImportXLSXData(
  183. filepath,
  184. termid,
  185. schid,
  186. planid,
  187. planyearid,
  188. type,
  189. batchid
  190. ) {
  191. const file = await this.ctx.curl(filepath);
  192. const workbook = XLSX.read(file.data);
  193. // 读取内容
  194. let exceldata = [];
  195. const sheetNames = workbook.SheetNames; // 获取表名
  196. const sheet = workbook.Sheets[sheetNames[0]]; // 通过表名得到表对象
  197. // 遍历26个字母
  198. const theadRule = [];
  199. const range = XLSX.utils.decode_range(sheet['!ref']);
  200. const col_start = range.s.c;
  201. const col_end = range.e.c;
  202. for (let i = col_start; i <= col_end; i++) {
  203. const addr = XLSX.utils.encode_col(i) + XLSX.utils.encode_row(0);
  204. theadRule.push(sheet[addr].v);
  205. }
  206. // const theadRule = [ sheet.A1.v, sheet.B1.v, sheet.C1.v, sheet.D1.v, sheet.E1.v, sheet.F1.v, sheet.G1.v, sheet.H1.v, sheet.I1.v, sheet.J1.v, sheet.K1.v, sheet.L1.v, sheet.M1.v, sheet.N1.v, sheet.O1.v, sheet.P1.v, sheet.Q1.v, sheet.R1.v ];
  207. const params = XLSX.utils.sheet_to_json(sheet); // 通过工具将表对象的数据读出来并转成json
  208. // const theadRule = [ '序号', '姓名', '性别', '民族', '身份证号', '学校名称', '院系', '专业', '入学年份', '毕业年份', '在校曾担任何种职务', '手机号', 'QQ号', '家庭所在地', '家庭是否困难', '是否获得过助学金' ];
  209. if (!params) return [];
  210. let i = 0;
  211. const length = params.length;
  212. const _datas = [];
  213. let data = {};
  214. for (i; i < length; i++) {
  215. data = params[i];
  216. const diy_ = [];
  217. if (theadRule.length > 18) {
  218. for (let j = 18; j < theadRule.length; j++) {
  219. const newdata = {
  220. itemname: theadRule[j],
  221. itemvalue: data[theadRule[j]],
  222. };
  223. diy_.push(newdata);
  224. }
  225. }
  226. _datas.push({
  227. name: _.trim(data[theadRule[1]]),
  228. gender: _.trim(data[theadRule[2]]),
  229. nation: _.trim(data[theadRule[3]]),
  230. id_number: _.trim(data[theadRule[4]]),
  231. school_name: _.trim(data[theadRule[5]]),
  232. faculty: _.trim(data[theadRule[6]]),
  233. major: _.trim(data[theadRule[7]]),
  234. entry_year: _.trim(data[theadRule[8]]),
  235. finish_year: _.trim(data[theadRule[9]]),
  236. school_job: _.trim(data[theadRule[10]]),
  237. phone: _.trim(data[theadRule[11]]),
  238. qq: _.trim(data[theadRule[12]]),
  239. family_place: _.trim(data[theadRule[13]]),
  240. family_is_hard: _.trim(data[theadRule[14]]),
  241. have_grant: _.trim(data[theadRule[15]]),
  242. edua_level: _.trim(data[theadRule[16]]),
  243. edua_system: _.trim(data[theadRule[17]]),
  244. diy: diy_,
  245. termid,
  246. batchid,
  247. schid,
  248. planid,
  249. planyearid,
  250. type,
  251. });
  252. }
  253. exceldata = [ ...exceldata, ..._datas ];
  254. return exceldata;
  255. }
  256. // 获取导入的XLSX文件中的数据
  257. async datacheck(studatas) {
  258. let errorcode = '0';
  259. const errormsg = [];
  260. for (const data of studatas) {
  261. // 判断是否为空
  262. if (!data.name) {
  263. errorcode = '1';
  264. data.msg = (data.msg || '') + '姓名不允许为空;';
  265. }
  266. if (!data.gender) {
  267. errorcode = '1';
  268. data.msg = (data.msg || '') + '性别不允许为空;';
  269. }
  270. // 判断性别是否是1个字
  271. if (data.gender.length !== 1) {
  272. errorcode = '1';
  273. data.msg(data.msg || '') + '性别内容超出1个字.可能含有空格;';
  274. }
  275. if (!data.nation) {
  276. errorcode = '1';
  277. data.msg = (data.msg || '') + '民族不允许为空;';
  278. }
  279. if (!data.id_number) {
  280. errorcode = '1';
  281. data.msg = (data.msg || '') + '身份证号不允许为空;';
  282. } else {
  283. const res = await this.smodel.findOne({ id_number: data.id_number });
  284. if (res) {
  285. errorcode = '1';
  286. data.msg = (data.msg || '') + '学生已经存在请检查;';
  287. } else {
  288. const { pass, msg } = this.idCodeValid(data.id_number);
  289. if (!pass) {
  290. errorcode = '1';
  291. data.msg = (data.msg || '') + `${msg},`;
  292. }
  293. }
  294. }
  295. if (!data.school_name) {
  296. errorcode = '1';
  297. data.msg = (data.msg || '') + '学校名称不允许为空;';
  298. }
  299. if (!data.phone) {
  300. errorcode = '1';
  301. data.msg = (data.msg || '') + '手机号不允许为空;';
  302. }
  303. if (!data.faculty) {
  304. errorcode = '1';
  305. data.msg = (data.msg || '') + '院系不允许为空;';
  306. }
  307. if (!data.major) {
  308. errorcode = '1';
  309. data.msg = (data.msg || '') + '专业不允许为空;';
  310. } else {
  311. // 限制专业字段中不能含有 '专业' 字样
  312. if (data.major.includes('专业')) {
  313. errorcode = '1';
  314. data.msg = (data.msg || '') + '专业列不能含有"专业"二字;';
  315. }
  316. }
  317. if (!data.entry_year) {
  318. errorcode = '1';
  319. data.msg = (data.msg || '') + '入学年份不允许为空;';
  320. } else {
  321. const m = /^\w{4}$/;
  322. if (!data.entry_year.match(m)) {
  323. errorcode = '1';
  324. data.msg = (data.msg || '') + '入学年份格式不正确,只填写4位数字即可;';
  325. }
  326. }
  327. // 限制是4位数字
  328. if (!data.finish_year) {
  329. errorcode = '1';
  330. data.msg = (data.msg || '') + '毕业年份不允许为空;';
  331. } else {
  332. const m = /^\w{4}$/;
  333. if (!data.finish_year.match(m)) {
  334. errorcode = '1';
  335. data.msg = (data.msg || '') + '毕业年份格式不正确,只填写4位数字即可;';
  336. }
  337. }
  338. if (!data.school_job) {
  339. errorcode = '1';
  340. data.msg = (data.msg || '') + '职务不允许为空;';
  341. }
  342. if (!data.qq) {
  343. errorcode = '1';
  344. data.msg = (data.msg || '') + 'QQ号不允许为空;';
  345. }
  346. if (!data.family_place) {
  347. errorcode = '1';
  348. data.msg = (data.msg || '') + '家庭所在地不允许为空;';
  349. }
  350. if (!data.family_is_hard) {
  351. errorcode = '1';
  352. data.msg = (data.msg || '') + '家庭是否困难不允许为空;';
  353. }
  354. if (!data.have_grant) {
  355. errorcode = '1';
  356. data.msg = (data.msg || '') + '是否获得过助学金不允许为空;';
  357. }
  358. if (!/^\d{11}$/i.test(data.phone)) {
  359. errorcode = '1';
  360. data.msg = (data.msg || '') + '手机号不正确;';
  361. }
  362. if (errorcode === '1') {
  363. errormsg.push(data);
  364. }
  365. }
  366. return { errorcode, errormsg };
  367. }
  368. // 导出学校名单
  369. async exportSchool({ trainplanId }) {
  370. // 批次期次都在这里面
  371. const trainplan = await this.tmodel.find({ _id: trainplanId });
  372. const _headers = [{ key: 'title', title: '计划标题' }];
  373. // 需要打出的列表
  374. const _data = trainplan;
  375. const headers = _headers
  376. .map(({ title }) => title)
  377. .map((v, i) =>
  378. Object.assign({}, { v, position: String.fromCharCode(65 + i) + 1 })
  379. )
  380. .reduce(
  381. (prev, next) =>
  382. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  383. {}
  384. );
  385. const data = _data
  386. .map((v, i) =>
  387. _headers.map(({ key }, j) =>
  388. Object.assign(
  389. {},
  390. { v: v[key], position: String.fromCharCode(65 + j) + (i + 2) }
  391. )
  392. )
  393. )
  394. .reduce((prev, next) => prev.concat(next))
  395. .reduce(
  396. (prev, next) =>
  397. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  398. {}
  399. );
  400. // 合并 headers 和 data
  401. const output = Object.assign({}, headers, data);
  402. // 获取所有单元格的位置
  403. const outputPos = Object.keys(output);
  404. // 计算出范围
  405. const ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];
  406. // 构建 workbook 对象
  407. const nowDate = new Date().getTime();
  408. const path =
  409. 'D:\\wwwroot\\service\\service-file\\upload\\train\\' + nowDate + '.xlsx';
  410. const respath =
  411. 'http://free.liaoningdoupo.com:80/files/train/' + nowDate + '.xlsx';
  412. const wb = {
  413. SheetNames: [ 'sheet0' ],
  414. Sheets: { sheet0: Object.assign({}, output, { '!ref': ref }) },
  415. };
  416. // 导出 Excel
  417. XLSX.writeFile(wb, path);
  418. return respath;
  419. }
  420. async updateclass({ trainplanid, classid, rightHeader }) {
  421. assert(trainplanid && classid && rightHeader, '缺少参数项');
  422. // 根据全年计划表id查出对应的全年计划详细信息
  423. const trainplan = await this.model.findById(trainplanid);
  424. if (!trainplan) {
  425. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  426. }
  427. for (const term of trainplan.termnum) {
  428. for (const batch of term.batchnum) {
  429. const class_ = await batch.class.id(classid);
  430. if (class_) {
  431. class_.headteacherid = rightHeader;
  432. }
  433. }
  434. }
  435. return await trainplan.save();
  436. }
  437. async updatereteacher({ trainplanid, termid, reteacher }) {
  438. assert(trainplanid && termid && reteacher, '缺少参数项');
  439. // 根据全年计划表id查出对应的全年计划详细信息
  440. const trainplan = await this.model.findById(trainplanid);
  441. if (!trainplan) {
  442. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  443. }
  444. const term = await trainplan.termnum.id(termid);
  445. if (term) {
  446. term.reteacher = reteacher;
  447. }
  448. return await trainplan.save();
  449. }
  450. // 身份证验证
  451. idCodeValid(code) {
  452. // 身份证号合法性验证
  453. // 支持15位和18位身份证号
  454. // 支持地址编码、出生日期、校验位验证
  455. const city = {
  456. 11: '北京',
  457. 12: '天津',
  458. 13: '河北',
  459. 14: '山西',
  460. 15: '内蒙古',
  461. 21: '辽宁',
  462. 22: '吉林',
  463. 23: '黑龙江 ',
  464. 31: '上海',
  465. 32: '江苏',
  466. 33: '浙江',
  467. 34: '安徽',
  468. 35: '福建',
  469. 36: '江西',
  470. 37: '山东',
  471. 41: '河南',
  472. 42: '湖北 ',
  473. 43: '湖南',
  474. 44: '广东',
  475. 45: '广西',
  476. 46: '海南',
  477. 50: '重庆',
  478. 51: '四川',
  479. 52: '贵州',
  480. 53: '云南',
  481. 54: '西藏 ',
  482. 61: '陕西',
  483. 62: '甘肃',
  484. 63: '青海',
  485. 64: '宁夏',
  486. 65: '新疆',
  487. 71: '台湾',
  488. 81: '香港',
  489. 82: '澳门',
  490. 91: '国外 ',
  491. };
  492. let row = {
  493. pass: true,
  494. msg: '验证成功',
  495. };
  496. if (
  497. !code ||
  498. !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/.test(
  499. code
  500. )
  501. ) {
  502. row = {
  503. pass: false,
  504. msg: '身份证号格式错误',
  505. };
  506. } else if (!city[code.substr(0, 2)]) {
  507. row = {
  508. pass: false,
  509. msg: '身份证号地址编码错误',
  510. };
  511. } else {
  512. // 18位身份证需要验证最后一位校验位
  513. if (code.length == 18) {
  514. code = code.split('');
  515. // ∑(ai×Wi)(mod 11)
  516. // 加权因子
  517. const factor = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ];
  518. // 校验位
  519. const parity = [ 1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2 ];
  520. let sum = 0;
  521. let ai = 0;
  522. let wi = 0;
  523. for (let i = 0; i < 17; i++) {
  524. ai = code[i];
  525. wi = factor[i];
  526. sum += ai * wi;
  527. }
  528. if (parity[sum % 11] != code[17].toUpperCase()) {
  529. row = {
  530. pass: false,
  531. msg: '身份证号校验位错误',
  532. };
  533. }
  534. }
  535. }
  536. return row;
  537. }
  538. // 判断是否超出该期前3天
  539. outOfDate(plan, termid, batchid) {
  540. const term = plan.termnum.find(f => ObjectId(termid).equals(f._id));
  541. const { batchnum } = term;
  542. if (!batchnum) throw new BusinessError(ErrorCode.BUSINESS, '没有找到该期下的批次');
  543. const batch = batchnum.find(f => ObjectId(batchid).equals(f._id));
  544. if (!batch) throw new BusinessError(ErrorCode.BUSINESS, '没有找到该批次');
  545. // let startList = batchnum.map(i => ({ start: i.startdate }));
  546. // startList = _.orderBy(startList, [ 'start' ], [ 'asc' ]);
  547. // const start = _.get(_.head(startList), 'start');
  548. const start = _.get(batch, 'startdate');
  549. if (!start) throw new BusinessError(ErrorCode.BUSINESS, '没有找到开始日期');
  550. const limit = moment(start).subtract(3, 'days').format('YYYY-MM-DD');
  551. const now = moment().format('YYYY-MM-DD');
  552. const res = moment(now).isBefore(limit);
  553. return res;
  554. }
  555. }
  556. module.exports = SchoolService;