school.js 17 KB

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