school.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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 { pass, msg } = this.idCodeValid(data.id_number);
  284. if (!pass) {
  285. errorcode = '1';
  286. data.msg = (data.msg || '') + `${msg},`;
  287. }
  288. }
  289. if (!data.school_name) {
  290. errorcode = '1';
  291. data.msg = (data.msg || '') + '学校名称不允许为空;';
  292. }
  293. if (!data.phone) {
  294. errorcode = '1';
  295. data.msg = (data.msg || '') + '手机号不允许为空;';
  296. }
  297. if (!data.faculty) {
  298. errorcode = '1';
  299. data.msg = (data.msg || '') + '院系不允许为空;';
  300. }
  301. if (!data.major) {
  302. errorcode = '1';
  303. data.msg = (data.msg || '') + '专业不允许为空;';
  304. } else {
  305. // 限制专业字段中不能含有 '专业' 字样
  306. if (data.major.includes('专业')) {
  307. errorcode = '1';
  308. data.msg = (data.msg || '') + '专业列不能含有"专业"二字;';
  309. }
  310. }
  311. if (!data.entry_year) {
  312. errorcode = '1';
  313. data.msg = (data.msg || '') + '入学年份不允许为空;';
  314. } else {
  315. const m = /^\w{4}$/;
  316. if (!data.entry_year.match(m)) {
  317. errorcode = '1';
  318. data.msg = (data.msg || '') + '入学年份格式不正确,只填写4位数字即可;';
  319. }
  320. }
  321. // 限制是4位数字
  322. if (!data.finish_year) {
  323. errorcode = '1';
  324. data.msg = (data.msg || '') + '毕业年份不允许为空;';
  325. } else {
  326. const m = /^\w{4}$/;
  327. if (!data.finish_year.match(m)) {
  328. errorcode = '1';
  329. data.msg = (data.msg || '') + '毕业年份格式不正确,只填写4位数字即可;';
  330. }
  331. }
  332. if (!data.school_job) {
  333. errorcode = '1';
  334. data.msg = (data.msg || '') + '职务不允许为空;';
  335. }
  336. if (!data.qq) {
  337. errorcode = '1';
  338. data.msg = (data.msg || '') + 'QQ号不允许为空;';
  339. }
  340. if (!data.family_place) {
  341. errorcode = '1';
  342. data.msg = (data.msg || '') + '家庭所在地不允许为空;';
  343. }
  344. if (!data.family_is_hard) {
  345. errorcode = '1';
  346. data.msg = (data.msg || '') + '家庭是否困难不允许为空;';
  347. }
  348. if (!data.have_grant) {
  349. errorcode = '1';
  350. data.msg = (data.msg || '') + '是否获得过助学金不允许为空;';
  351. }
  352. if (!/^\d{11}$/i.test(data.phone)) {
  353. errorcode = '1';
  354. data.msg = (data.msg || '') + '手机号不正确;';
  355. }
  356. const res = await this.smodel.findOne({ id_number: data.id_number });
  357. if (res) {
  358. errorcode = '1';
  359. data.msg = (data.msg || '') + '学生已经存在请检查;';
  360. }
  361. if (errorcode === '1') {
  362. errormsg.push(data);
  363. }
  364. }
  365. return { errorcode, errormsg };
  366. }
  367. // 导出学校名单
  368. async exportSchool({ trainplanId }) {
  369. // 批次期次都在这里面
  370. const trainplan = await this.tmodel.find({ _id: trainplanId });
  371. const _headers = [{ key: 'title', title: '计划标题' }];
  372. // 需要打出的列表
  373. const _data = trainplan;
  374. const headers = _headers
  375. .map(({ title }) => title)
  376. .map((v, i) =>
  377. Object.assign({}, { v, position: String.fromCharCode(65 + i) + 1 })
  378. )
  379. .reduce(
  380. (prev, next) =>
  381. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  382. {}
  383. );
  384. const data = _data
  385. .map((v, i) =>
  386. _headers.map(({ key }, j) =>
  387. Object.assign(
  388. {},
  389. { v: v[key], position: String.fromCharCode(65 + j) + (i + 2) }
  390. )
  391. )
  392. )
  393. .reduce((prev, next) => prev.concat(next))
  394. .reduce(
  395. (prev, next) =>
  396. Object.assign({}, prev, { [next.position]: { v: next.v } }),
  397. {}
  398. );
  399. // 合并 headers 和 data
  400. const output = Object.assign({}, headers, data);
  401. // 获取所有单元格的位置
  402. const outputPos = Object.keys(output);
  403. // 计算出范围
  404. const ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];
  405. // 构建 workbook 对象
  406. const nowDate = new Date().getTime();
  407. const path =
  408. 'D:\\wwwroot\\service\\service-file\\upload\\train\\' + nowDate + '.xlsx';
  409. const respath =
  410. 'http://free.liaoningdoupo.com:80/files/train/' + nowDate + '.xlsx';
  411. const wb = {
  412. SheetNames: [ 'sheet0' ],
  413. Sheets: { sheet0: Object.assign({}, output, { '!ref': ref }) },
  414. };
  415. // 导出 Excel
  416. XLSX.writeFile(wb, path);
  417. return respath;
  418. }
  419. async updateclass({ trainplanid, classid, rightHeader }) {
  420. assert(trainplanid && classid && rightHeader, '缺少参数项');
  421. // 根据全年计划表id查出对应的全年计划详细信息
  422. const trainplan = await this.model.findById(trainplanid);
  423. if (!trainplan) {
  424. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  425. }
  426. for (const term of trainplan.termnum) {
  427. for (const batch of term.batchnum) {
  428. const class_ = await batch.class.id(classid);
  429. if (class_) {
  430. class_.headteacherid = rightHeader;
  431. }
  432. }
  433. }
  434. return await trainplan.save();
  435. }
  436. async updatereteacher({ trainplanid, termid, reteacher }) {
  437. assert(trainplanid && termid && reteacher, '缺少参数项');
  438. // 根据全年计划表id查出对应的全年计划详细信息
  439. const trainplan = await this.model.findById(trainplanid);
  440. if (!trainplan) {
  441. throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
  442. }
  443. const term = await trainplan.termnum.id(termid);
  444. if (term) {
  445. term.reteacher = reteacher;
  446. }
  447. return await trainplan.save();
  448. }
  449. // 身份证验证
  450. idCodeValid(code) {
  451. // 身份证号合法性验证
  452. // 支持15位和18位身份证号
  453. // 支持地址编码、出生日期、校验位验证
  454. const city = {
  455. 11: '北京',
  456. 12: '天津',
  457. 13: '河北',
  458. 14: '山西',
  459. 15: '内蒙古',
  460. 21: '辽宁',
  461. 22: '吉林',
  462. 23: '黑龙江 ',
  463. 31: '上海',
  464. 32: '江苏',
  465. 33: '浙江',
  466. 34: '安徽',
  467. 35: '福建',
  468. 36: '江西',
  469. 37: '山东',
  470. 41: '河南',
  471. 42: '湖北 ',
  472. 43: '湖南',
  473. 44: '广东',
  474. 45: '广西',
  475. 46: '海南',
  476. 50: '重庆',
  477. 51: '四川',
  478. 52: '贵州',
  479. 53: '云南',
  480. 54: '西藏 ',
  481. 61: '陕西',
  482. 62: '甘肃',
  483. 63: '青海',
  484. 64: '宁夏',
  485. 65: '新疆',
  486. 71: '台湾',
  487. 81: '香港',
  488. 82: '澳门',
  489. 91: '国外 ',
  490. };
  491. let row = {
  492. pass: true,
  493. msg: '验证成功',
  494. };
  495. if (
  496. !code ||
  497. !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/.test(
  498. code
  499. )
  500. ) {
  501. row = {
  502. pass: false,
  503. msg: '身份证号格式错误',
  504. };
  505. } else if (!city[code.substr(0, 2)]) {
  506. row = {
  507. pass: false,
  508. msg: '身份证号地址编码错误',
  509. };
  510. } else {
  511. // 18位身份证需要验证最后一位校验位
  512. if (code.length == 18) {
  513. code = code.split('');
  514. // ∑(ai×Wi)(mod 11)
  515. // 加权因子
  516. const factor = [ 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 ];
  517. // 校验位
  518. const parity = [ 1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2 ];
  519. let sum = 0;
  520. let ai = 0;
  521. let wi = 0;
  522. for (let i = 0; i < 17; i++) {
  523. ai = code[i];
  524. wi = factor[i];
  525. sum += ai * wi;
  526. }
  527. if (parity[sum % 11] != code[17].toUpperCase()) {
  528. row = {
  529. pass: false,
  530. msg: '身份证号校验位错误',
  531. };
  532. }
  533. }
  534. }
  535. return row;
  536. }
  537. // 判断是否超出该期前3天
  538. outOfDate(plan, termid, batchid) {
  539. const term = plan.termnum.find(f => ObjectId(termid).equals(f._id));
  540. const { batchnum } = term;
  541. if (!batchnum) throw new BusinessError(ErrorCode.BUSINESS, '没有找到该期下的批次');
  542. const batch = batchnum.find(f => ObjectId(batchid).equals(f._id));
  543. if (!batch) throw new BusinessError(ErrorCode.BUSINESS, '没有找到该批次');
  544. // let startList = batchnum.map(i => ({ start: i.startdate }));
  545. // startList = _.orderBy(startList, [ 'start' ], [ 'asc' ]);
  546. // const start = _.get(_.head(startList), 'start');
  547. const start = _.get(batch, 'startdate');
  548. if (!start) throw new BusinessError(ErrorCode.BUSINESS, '没有找到开始日期');
  549. const limit = moment(start).subtract(3, 'days').format('YYYY-MM-DD');
  550. const now = moment().format('YYYY-MM-DD');
  551. const res = moment(now).isBefore(limit);
  552. return res;
  553. }
  554. }
  555. module.exports = SchoolService;