123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- 'use strict';
- const assert = require('assert');
- const _ = require('lodash');
- const { ObjectId } = require('mongoose').Types;
- const { CrudService } = require('naf-framework-mongoose/lib/service');
- const { BusinessError, ErrorCode } = require('naf-core').Error;
- const moment = require('moment');
- const XLSX = require('xlsx');
- const Excel = require('exceljs');
- const { sep } = require('path');
- class SchoolService extends CrudService {
- constructor(ctx) {
- super(ctx, 'schoolctrl');
- this.model = this.ctx.model.School;
- this.smodel = this.ctx.model.Student;
- this.umodel = this.ctx.model.User;
- this.tmodel = this.ctx.model.Trainplan;
- this.jmodel = this.ctx.model.Job;
- this.schmodel = this.ctx.model.Schtime;
- }
- async findByCodes({ code }) {
- if (!_.isArray(code) || code.length <= 0) return [];
- const res = await this.model.find({ code }, { name: 1, code: 1, level: 1, hascar: 1, address: 1 });
- return res;
- }
- async create(data) {
- const { code, name } = data;
- assert(code, '缺少学校代码');
- assert(name, '缺少学校名称');
- const res = await this.model.create(data);
- if (res) {
- const obj = {
- mobile: code,
- name,
- type: '2',
- uid: res._id,
- passwd: { secret: '12345678' },
- };
- await this.umodel.create(obj);
- }
- return res;
- }
- async delete({ id }) {
- await this.model.findByIdAndDelete(id);
- await this.umodel.deleteOne({ uid: id, type: '2' });
- return 'deleted';
- }
- async query({ name, ...data }, { skip, limit }) {
- const query = { ...data };
- if (name) {
- query.name = { $regex: name };
- }
- let res = await this.model.find(query).skip(parseInt(skip)).limit(parseInt(limit));
- if (res && res.length > 0) {
- res = JSON.parse(JSON.stringify(res));
- const ids = res.map((i) => i._id);
- const users = await this.umodel.find({ uid: { $in: ids } }, '+passwd');
- for (const tea of res) {
- const r = users.find((f) => f.uid === tea._id);
- if (r) {
- const passwd = _.get(r.passwd, 'secret');
- if (passwd) tea.passwd = passwd;
- }
- }
- }
- return res;
- }
- async count({ name, ...data } = {}) {
- const query = { ...data };
- if (name) {
- query.name = { $regex: name };
- }
- return await this.model.count(query);
- }
- // 去重查询学校
- async findSchool({ name, skip, limit, ...info }) {
- let data = [];
- const query = { ...info };
- if (name) {
- query.name = { $regex: name };
- }
- const AggregateInfo = [
- // 这里可以添加其他查询条件,例如过滤特定字段等
- { $match: query },
- // 去除重复数据
- { $group: { _id: '$code', uniqueData: { $first: '$$ROOT' } } },
- { $replaceRoot: { newRoot: '$uniqueData' } },
- { $addFields: { id: '$_id' } },
- { $sort: { code: 1 } },
- // 分页查询
- { $skip: parseInt(skip) },
- ];
- if (limit) AggregateInfo.push({ $limit: parseInt(limit) });
- const res = await this.model.aggregate(AggregateInfo);
- if (res && res.length > 0) {
- data = JSON.parse(JSON.stringify(res));
- const ids = res.map((i) => i._id);
- const users = await this.umodel.find({ uid: { $in: ids } }, '+passwd').lean();
- for (const tea of data) {
- const r = users.find((f) => f.uid === tea._id);
- if (r) {
- const passwd = _.get(r.passwd, 'secret');
- if (passwd) tea.passwd = passwd;
- }
- }
- }
- const count = await this.model.aggregate([{ $match: query }, { $group: { _id: '$code' } }, { $count: 'distinctCount' }]);
- const total = _.get(count[0], 'distinctCount') || 0;
- return { total, data };
- }
- async stuimport(data) {
- const { filepath, termid, schid, type, batchid } = data;
- assert(filepath, 'filepath不能为空');
- assert(termid, 'termid不能为空');
- assert(schid, 'schid不能为空');
- // 根据termid取得计划信息
- const plan = await this.tmodel.findOne({ 'termnum._id': ObjectId(termid) });
- if (!plan) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '计划信息不存在');
- }
- const term = plan.termnum.id(termid);
- const planid = plan.id;
- const planyearid = plan.planyearid;
- // 检查这个范围的学生是否存在,存在的话是否更改过(classid,bedroomid这两项存不存在可以放过,但凡有一个人,就不行了)
- let dbStuList = await this.ctx.model.Student.find({ termid, batchid, schid });
- if (dbStuList.length > 0) {
- // 查这个学校的这期学生是否修改过班级 或 寝室
- const is_change = dbStuList.find((f) => f.classid ); // || 2024-07-05 修改:f.bedroomid去掉,因为现在不在中心培训了,不管寝室了
- if (is_change) {
- throw new BusinessError(
- ErrorCode.BUSINESS,
- '上报过的学生已经安排班级!若需要替换学生,让同性别的学生直接来和班主任说,修改信息即可.若还是有疑问,请和中心负责人联系(最好联系下)', // 或寝室
- );
- }
- }
- // 2021-06-07 如果学生已经绑定,那也不允许修改名单了
- const countOpenid = await this.ctx.model.Student.count({ termid, batchid, schid, openid: { $exists: true } });
- if (countOpenid > 0) throw new BusinessError(ErrorCode.BUSINESS, '已有学生绑定账号,名单无法修改.若有问题请联系中心负责人!');
- // 获取学校名称
- let school_name;
- const sch = await this.ctx.model.School.findOne({ code: schid });
- if (sch) school_name = sch.name;
- let domain = 'http://127.0.0.1';
- if (process.env.NODE_ENV === 'development') domain = 'http://jytz.jilinjobs.cn';
- const fullUrl = domain + filepath; // this.ctx.app.config.baseUrl http://127.0.0.1 http://jytz.jilinjobs.cn
- let studentList = await this.getDataFromExcel(fullUrl);
- const checkRes = await this.checkData(studentList);
- const { errorcode } = checkRes;
- if (errorcode === '1') {
- return checkRes;
- }
- // 2021-05-26 添加与数据库的对比,如果数据库里已经有这个身份证号,就需要提示
- const countStudent = await this.countStudent(studentList, planid);
- const { errorcode: csec } = countStudent;
- if (csec === '1') {
- return countStudent;
- }
- // 整理数据
- studentList = this.lastSetData(studentList, {
- planyearid,
- planid,
- batchid,
- termid,
- type,
- schid,
- school_name,
- });
- const num = await this.getschnum(plan, schid, batchid);
- // 查看要求人数和整理完最后的人数能不能对上
- if (studentList.length !== num) {
- const res = await this.jmodel.findOne({ code: schid, batchid });
- const reason = `学校上传人数${studentList.length > num ? '多于' : '少于'}预期人数,请联系中心管理员`;
- if (res) {
- res.reason = reason;
- res.filepath = filepath;
- await res.save();
- } else {
- const job = {
- code: schid,
- name: school_name,
- planid,
- termid,
- term: term.term,
- batchid,
- filepath,
- studs: JSON.stringify(studentList),
- plannum: num,
- schnum: studentList.length,
- isstore: '0',
- createtime: moment().format('YYYY-MM-DD HH:SS:mm'),
- type,
- };
- job.reason = reason;
- await this.jmodel.create(job);
- }
- throw new BusinessError(ErrorCode.SERVICE_FAULT, reason);
- } else {
- // 复制,删除,添加
- if (dbStuList.length > 0) {
- dbStuList = JSON.parse(JSON.stringify(dbStuList));
- dbStuList = dbStuList.map((i) => {
- delete i.meta;
- i.studentid = _.clone(i._id);
- delete i.id;
- delete i._id;
- return i;
- });
- await this.smodel.deleteMany({ termid, batchid, schid });
- await this.ctx.model.Dstudent.insertMany(dbStuList);
- }
- await this.smodel.insertMany(studentList);
- }
- return 'ok';
- }
- /**
- * 检查学生是否参加过这个计划以外的计划,参加过就不让来了
- * @param {Array} studentList 学生列表
- * @param {String} planid 计划id
- */
- async countStudent(studentList, planid) {
- let errorcode = '0';
- const errormsg = [];
- for (const stu of studentList) {
- const { name, id_number } = stu;
- let error = false;
- let msg = '';
- const count = await this.smodel.count({ id_number, planid: { $ne: planid } });
- if (count > 0) {
- error = true;
- msg = `${msg}${name}已经参加过培训`;
- }
- if (error) {
- errorcode = '1';
- stu.msg = msg;
- errormsg.push(stu);
- }
- }
- return { errorcode, errormsg };
- }
- // 取得学校预计人数
- async getschnum(plan, schid, batchid) {
- const schtime = await this.schmodel.findOne({ schid, planid: plan.id });
- const { arrange } = schtime;
- const r = arrange.find((f) => f.batchid === batchid);
- if (!r) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '没有找到该学校的计划');
- }
- const { number } = r;
- return parseInt(number);
- }
- // 整理excel数据
- async getDataFromExcel(url) {
- // 请求文件
- const file = await this.ctx.curl(`${url}`);
- if (!(file && file.data)) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到上传的名单');
- }
- const workbook = new Excel.Workbook();
- // 读取文件
- await workbook.xlsx.load(file.data);
- const worksheet = workbook.getWorksheet(1);
- if (!worksheet) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未发现excel中有工作表');
- }
- // 获取表头,通过方法的返回值,将写死的表头数组返回 回来
- const cols = this.getStucolumn();
- // 第一行(表头)
- const headRow = worksheet.getRow(1);
- // 设置,检查表头
- headRow.eachCell((cell, coli) => {
- console.log(cell.value);
- if (cell.value !== '序号') {
- const r = cols.find((f) => f.key === cell.value);
- if (r) {
- const ri = cols.findIndex((f) => f.key === cell.value);
- // 表头符合要求,做上标记
- r.colIndex = coli;
- cols[ri] = r;
- } else {
- throw new BusinessError(`模板中"${cell.value}"列错误,请检查excel!`);
- }
- }
- });
- // 检查表头结果,如果有没有 colIndex,说明表头里有不符合要求的,退回去
- const excelIsRigth = cols.every((f) => f.colIndex);
- if (!excelIsRigth) throw new BusinessError(ErrorCode.DATA_INVALID, 'Excel表格格式不正确,请使用系统提供的模板,或重新下载模板!');
- // 删除掉第一行 表头行,这不是数据
- worksheet.spliceRows(0, 1);
- const stuList = [];
- const noWhite = (str) => str.replace(/\s*/g, '');
- // 整理数据,根据检查合格的表头行,获取每个格子的数据,制成[object]格式
- worksheet.eachRow((row) => {
- const stu = {};
- for (let i = 0; i < cols.length; i++) {
- const col = cols[i];
- if (!col) break;
- let val = _.trim(row.getCell(col.colIndex));
- if (col.column === 'id_number') val = val.toUpperCase();
- if (val && val !== '') val = noWhite(val);
- stu[col.column] = val;
- }
- stuList.push(stu);
- });
- return stuList;
- }
- // 数据校验
- async checkData(stuList) {
- const cols = this.getStucolumn();
- let errorcode = '0';
- const errormsg = [];
- for (const stu of stuList) {
- const { name } = stu;
- let error = false;
- let msg = '';
- // 各个字段检查,最低为非空检查
- for (const col of cols) {
- const { key, column } = col;
- if (!column) throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到导出的字段名');
- const val = _.get(stu, column);
- // 空校验
- if (!val || val === '') {
- error = true;
- msg = `${msg}"${key}"不能为空;`;
- continue;
- }
- // 性别校验
- if (column === 'gender') {
- if (!(val.includes('男') || val.includes('女'))) {
- error = true;
- msg = `${msg}性别错误;`;
- }
- continue;
- }
- // 身份证号校验
- if (column === 'id_number') {
- // 因为删除再添加的流程导致此处 不能 校验数据库中是否有这个身份证号
- // const res = await this.ctx.model.Student.findOne({ id_number: val });
- // if (!res) {
- const { pass, msg: idmsg } = this.ctx.service.school.idCodeValid(val);
- if (!pass) {
- error = true;
- msg = `${msg}${idmsg};`;
- }
- // } else {
- // error = true;
- // msg = `${msg}学生已存在`;
- // }
- const have_same = stuList.filter((f) => f.id_number === val && f.name !== name);
- if (have_same.length > 0) {
- error = true;
- const h = _.head(have_same);
- const num = have_same.length;
- if (num === 1) {
- msg = `${msg}身份证号与本次名单的"${h.name}"重复;`;
- } else msg = `${msg}身份证号与本次名单中"${h.name}"等${num}人重复;`;
- }
- continue;
- }
- // 手机号校验
- if (column === 'phone') {
- // 因为删除再添加的流程导致此处 不能 校验数据库中是否有这个手机号
- // const res = await this.ctx.model.Student.findOne({ phone: val });
- // if (!res) {
- if (!/^\d{11}$/i.test(val)) {
- error = true;
- msg = `${msg}手机号位数不正确;`;
- }
- // } else {
- // error = true;
- // msg = `${msg}学生库中已有该手机号,请检查手机号是否正确,若无误,请联系中心负责人`;
- // }
- const have_same = stuList.filter((f) => f.phone === val && f.name !== name);
- if (have_same.length > 0) {
- error = true;
- const h = _.head(have_same);
- const num = have_same.length;
- if (num === 1) {
- msg = `${msg}手机号与本次名单的"${h.name}"重复;`;
- } else msg = `${msg}手机号与本次名单中"${h.name}"等${num}人重复;`;
- }
- continue;
- }
- // 专业校验
- if (column === 'major') {
- if (val.includes('专业')) {
- error = true;
- msg = `${msg}专业列不能含有"专业"二字;`;
- }
- continue;
- }
- // 入学年份
- if (column === 'entry_year') {
- const m = /^\w{4}$/;
- if (!val.match(m)) {
- error = true;
- msg = `${msg}入学年份格式不正确,只填写4位数字;`;
- }
- continue;
- }
- // 毕业年份
- if (column === 'finish_year') {
- const m = /^\w{4}$/;
- if (!val.match(m)) {
- error = true;
- msg = `${msg}毕业年份格式不正确,只填写4位数字;`;
- }
- continue;
- }
- // 双困检查
- if (column === 'family_is_hard') {
- if (!(val.includes('是') || val.includes('否'))) {
- error = true;
- msg = `${msg}家庭是否困难填写"是"或"否";`;
- }
- continue;
- }
- if (column === 'have_grant') {
- if (!(val.includes('是') || val.includes('否'))) {
- error = true;
- msg = `${msg}是否获得过助学金填写"是"或"否";`;
- }
- continue;
- }
- }
- if (error) {
- errorcode = '1';
- stu.msg = msg;
- errormsg.push(stu);
- }
- }
- return { errorcode, errormsg };
- }
- // 最后整合数据
- lastSetData(stuList, data) {
- const cols = this.getStucolumn();
- const needChange = cols.filter((f) => f.change);
- stuList = stuList.map((i) => {
- const d = { ...i, ...data };
- for (const col of needChange) {
- const { column, change } = col;
- if (!column && change && _.isArray(change)) continue;
- const val = _.get(d, column);
- if (!val) continue;
- const r = change.find((f) => f.key === val);
- if (!r) continue;
- const { value } = r;
- d[column] = value;
- }
- return d;
- });
- return stuList;
- }
- // excel中学生字段
- getStucolumn() {
- const arr = [
- { key: '姓名', column: 'name' },
- { key: '性别', column: 'gender' },
- { key: '民族', column: 'nation' },
- { key: '身份证号', column: 'id_number' },
- { key: '学校名称', column: 'school_name' },
- { key: '学历层次', column: 'edua_level' },
- { key: '学制', column: 'edua_system' },
- { key: '院(系)', column: 'faculty' },
- { key: '专业', column: 'major' },
- { key: '入学年份', column: 'entry_year' },
- { key: '毕业年份', column: 'finish_year' },
- { key: '在校曾担任何种职务', column: 'school_job' },
- { key: '手机号', column: 'phone' },
- { key: 'QQ号', column: 'qq' },
- { key: '家庭所在地', column: 'family_place' },
- {
- key: '家庭是否困难',
- column: 'family_is_hard',
- change: [
- { key: '否', value: '0' },
- { key: '是', value: '1' },
- ],
- },
- {
- key: '是否获得过助学金',
- column: 'have_grant',
- change: [
- { key: '否', value: '0' },
- { key: '是', value: '1' },
- ],
- },
- ];
- return arr;
- }
- // 导出学校名单
- async exportSchool({ trainplanId }) {
- // 批次期次都在这里面
- const trainplan = await this.tmodel.find({ _id: trainplanId });
- const _headers = [{ key: 'title', title: '计划标题' }];
- // 需要打出的列表
- const _data = trainplan;
- const headers = _headers
- .map(({ title }) => title)
- .map((v, i) => Object.assign({}, { v, position: String.fromCharCode(65 + i) + 1 }))
- .reduce((prev, next) => Object.assign({}, prev, { [next.position]: { v: next.v } }), {});
- const data = _data
- .map((v, i) => _headers.map(({ key }, j) => Object.assign({}, { v: v[key], position: String.fromCharCode(65 + j) + (i + 2) })))
- .reduce((prev, next) => prev.concat(next))
- .reduce((prev, next) => Object.assign({}, prev, { [next.position]: { v: next.v } }), {});
- // 合并 headers 和 data
- const output = Object.assign({}, headers, data);
- // 获取所有单元格的位置
- const outputPos = Object.keys(output);
- // 计算出范围
- const ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];
- // 构建 workbook 对象
- const nowDate = new Date().getTime();
- // 地址有问题,需要引用config变量
- const { repos_root_path: rp } = this.ctx.app.config.cdn;
- const { baseUrl: bu } = this.ctx.app.config;
- const path = `${rp}${sep}train${sep}${nowDate}.xlsx`;
- const respath = `${bu}/files/train/${nowDate}.xlsx`;
- const wb = {
- SheetNames: ['sheet0'],
- Sheets: { sheet0: Object.assign({}, output, { '!ref': ref }) },
- };
- // 导出 Excel
- XLSX.writeFile(wb, path);
- return respath;
- }
- async updateclass({ trainplanid, classid, rightHeader }) {
- assert(trainplanid && classid && rightHeader, '缺少参数项');
- // 根据全年计划表id查出对应的全年计划详细信息
- const trainplan = await this.model.findById(trainplanid);
- if (!trainplan) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
- }
- for (const term of trainplan.termnum) {
- for (const batch of term.batchnum) {
- const class_ = await batch.class.id(classid);
- if (class_) {
- class_.headteacherid = rightHeader;
- }
- }
- }
- return await trainplan.save();
- }
- async updatereteacher({ trainplanid, termid, reteacher }) {
- assert(trainplanid && termid && reteacher, '缺少参数项');
- // 根据全年计划表id查出对应的全年计划详细信息
- const trainplan = await this.model.findById(trainplanid);
- if (!trainplan) {
- throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '全年计划信息不存在');
- }
- const term = await trainplan.termnum.id(termid);
- if (term) {
- term.reteacher = reteacher;
- }
- return await trainplan.save();
- }
- // 身份证验证
- idCodeValid(code) {
- // 身份证号合法性验证
- // 支持15位和18位身份证号
- // 支持地址编码、出生日期、校验位验证
- const city = {
- 11: '北京',
- 12: '天津',
- 13: '河北',
- 14: '山西',
- 15: '内蒙古',
- 21: '辽宁',
- 22: '吉林',
- 23: '黑龙江 ',
- 31: '上海',
- 32: '江苏',
- 33: '浙江',
- 34: '安徽',
- 35: '福建',
- 36: '江西',
- 37: '山东',
- 41: '河南',
- 42: '湖北 ',
- 43: '湖南',
- 44: '广东',
- 45: '广西',
- 46: '海南',
- 50: '重庆',
- 51: '四川',
- 52: '贵州',
- 53: '云南',
- 54: '西藏 ',
- 61: '陕西',
- 62: '甘肃',
- 63: '青海',
- 64: '宁夏',
- 65: '新疆',
- 71: '台湾',
- 81: '香港',
- 82: '澳门',
- 91: '国外 ',
- };
- let row = {
- pass: true,
- msg: '验证成功',
- };
- if (!code || !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|[xX])$/.test(code)) {
- row = {
- pass: false,
- msg: '身份证号格式错误',
- };
- } else if (!city[code.substr(0, 2)]) {
- row = {
- pass: false,
- msg: '身份证号地址编码错误',
- };
- } else {
- // 18位身份证需要验证最后一位校验位
- if (code.length === 18) {
- code = code.split('');
- // ∑(ai×Wi)(mod 11)
- // 加权因子
- const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
- // 校验位
- const parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2];
- let sum = 0;
- let ai = 0;
- let wi = 0;
- for (let i = 0; i < 17; i++) {
- ai = code[i];
- wi = factor[i];
- sum += ai * wi;
- }
- if (parity[sum % 11] != code[17].toUpperCase()) {
- row = {
- pass: false,
- msg: '身份证号校验位错误',
- };
- }
- }
- }
- return row;
- }
- }
- module.exports = SchoolService;
|