const schedule = require('node-schedule'); const moment = require('moment') module.exports = function (app, opts) { const updateAttendance = app.fs.scheduleInit( // 妥妥流水账 (*^▽^*) { // interval: '34 21 4 * * *', interval: '34 */15 * * * *', immediate: true, proRun: true, }, async () => { console.log('假勤数据更新 ', moment().format('YYYY-MM-DD HH:mm:ss')); const { workFlow: { processState } } = opts try { const startTime = moment() const { models } = app.fs.dc const { judgeHoliday } = app.fs.utils const { clickHouse } = app.fs const { database: camWorkflow } = clickHouse.camWorkflow.opts.config const { } = app.fs.utils let overtimeNeedData = { reason: { keyWord: ['加班事由'] }, begainTime: { keyWord: ['加班开始时间'], require: true, }, endTime: { keyWord: ['加班结束时间'], require: true, }, duration: { keyWord: ['总时长(小时)'] }, compensate: { keyWord: ['加班补偿'], require: true, }, hrAffirmDuration: { keyWord: ['人事核定加班小时', '核定加班小时'], require: true, }, } let vacateNeedData = { reason: { keyWord: ['请假事由'] }, begainTime: { keyWord: ['请假开始时间', '请假起始时间'], keys: ['leaveStartTime'], require: true, }, endTime: { keyWord: ['请假结束时间', '请假终止时间'], keys: ['leaveEndTime'], require: true, }, type: { keyWord: ['请假类别', '请假类型'], keys: ['leaveType'], require: true, }, hrAffirmType: { keyWord: ['核定请假类别'], }, duration: { keyWord: ['请假时长(小时)', '请假小时'], keys: ['leaveTime'] }, hrAffirmDuration: { keyWord: ['核定请假小时'], }, } const schemaRecursionObj = ({ jsonSchema, keyWord, targetKeys = [], schemaPath, require }) => { let schemaPath_ = JSON.parse(JSON.stringify(schemaPath)) if (jsonSchema.properties) { for (let prKey in jsonSchema.properties) { if ( keyWord.includes(jsonSchema.properties[prKey].title) || targetKeys.includes(prKey) ) { schemaPath_.push({ prKey, ...jsonSchema.properties[prKey] }) return schemaPath_ } else if (jsonSchema.properties[prKey].properties) { schemaPath_.push({ prKey, ...jsonSchema.properties[prKey] }) schemaPath_ = schemaRecursionObj({ jsonSchema: jsonSchema.properties[prKey], keyWord, targetKeys, schemaPath: schemaPath_, require, }) if (!schemaPath_.length && require) { console.warn('数据字段查找错误:', jsonSchema.properties); } if (schemaPath_.length > schemaPath.length) { return schemaPath_ } } } } else { return schemaPath_ } } const dataRecursionObj = (dataObj, index, needData, lastKeyObj, nd) => { const keyObj = needData[nd].schemaPath[index] if (dataObj.hasOwnProperty(keyObj.prKey)) { if (lastKeyObj.prKey == keyObj.prKey) { let gotValue = dataObj[keyObj.prKey] if (keyObj.enum) { let vIndex = keyObj.enum.findIndex(ke => ke == gotValue) gotValue = keyObj.enumNames[vIndex] } return gotValue } else { return dataRecursionObj( dataObj[keyObj.prKey], index + 1, needData, lastKeyObj, nd ) } } } const getData = (applyDetail, needData) => { for (let nd in needData) { if (needData[nd].noProcess) { continue } if (applyDetail.formSchema) { const { jsonSchema } = JSON.parse(applyDetail.formSchema) needData[nd].schemaPath = schemaRecursionObj({ jsonSchema: jsonSchema || {}, keyWord: needData[nd]['keyWord'], targetKeys: needData[nd]['keys'], schemaPath: [], require: nd.require }) if ( needData[nd].schemaPath && needData[nd].schemaPath.length ) { const lastKeyObj = needData[nd] .schemaPath[needData[nd].schemaPath.length - 1] if (applyDetail.formData) { const formData = JSON.parse(applyDetail.formData) needData[nd].value = dataRecursionObj( formData, 0, needData, lastKeyObj, nd ) } else { console.warn( `表单数据缺失:[${nd}]`, applyDetail ); } } else { if (needData[nd].require) { // 记录错误 关键数据没找到 console.warn( `数据字段查找错误:${nd.needData[nd]['keyWord'].join('、')}`, jsonSchema ); } } } } } const existOvertimeCount = await models.Overtime.count() const existVacateCount = await models.Vacate.count() const attendanceRes = await clickHouse.pepEmis.query( ` SELECT story.id AS historyId, story.apply_user AS pepUserId, story.form_data AS formData, story.submit_form_data AS submitFormData, fform.form_schema AS formSchema, fprocess.name AS processName, procin.state_ AS state, fform.id AS formId ` + `,fversion.id AS versionId` + `,fgroup.name AS groupName` + ` FROM workflow_process_history AS story INNER JOIN workflow_process_version AS fversion ON fversion.id = story.version_id INNER JOIN workflow_process_form AS fform ON fform.id = fversion.form_id INNER JOIN workflow_process AS fprocess ON fprocess.id = fform.process_id INNER JOIN workflow_group AS fgroup ON fgroup.id = fprocess.group_id AND fgroup.name = '假勤管理' INNER JOIN ${camWorkflow}.act_hi_procinst AS procin ON procin.id_ = story.procinst_id ` + ` ${existOvertimeCount || existVacateCount ? `WHERE story.create_at > '${moment().subtract(1, 'month').format('YYYY-MM-DD HH:mm:ss')}'` : ''} ` ).toPromise() let insertCount = 0, updateCount = 0, invalidCount = 0, unCompletedCount = 0, unknowCount = 0 for (let a of attendanceRes) { console.log(`处理 ${a.pepUserId}·s ${a.processName}(form:${a.formId} story:${a.historyId}) `); if (a.processName) { if ('COMPLETED' == a.state) { if (a.processName.indexOf('请假') > -1) { let needData = JSON.parse(JSON.stringify(vacateNeedData)) getData(a, needData) const { begainTime, endTime, type, hrAffirmType, duration, hrAffirmDuration, reason } = needData if ( begainTime.value && endTime.value && type.value && (duration.value || hrAffirmDuration.value) ) { let durationSec = 0 if (hrAffirmDuration.value) { durationSec = parseFloat(hrAffirmDuration.value) * 3600 } else if (duration.value) { durationSec = parseFloat(duration.value) * 3600 } if ( typeof durationSec != 'number' || isNaN(durationSec) || durationSec <= 0 ) { console.warn('请假时长计算结果错误', hrAffirmDuration, duration); invalidCount++ } else { // 计算每个工作日请了多少假 let begainTime_ = moment(begainTime.value) let endTime_ = moment(endTime.value) let curTime = begainTime_.clone() let packageDay = [] let durationDayAddDay = 0 let forenoonDuration = 3600 * 3.5 let afternoonDuration = 3600 * 4.5 let lunchBreakDuration = 3600 * 1 while (curTime.isBefore(endTime_) && durationDayAddDay < durationSec) { let curDayStr = curTime.format('YYYY-MM-DD') const holidayRes = await judgeHoliday(curDayStr, true) if (holidayRes && holidayRes.workday) { // 只有上班的日子才计算 let workdayForenoonStart = moment(curDayStr + ' 08:30:00') let workdayForenoonEnd = moment(curDayStr + ' 12:00:00') let workdayAfternoonStart = moment(curDayStr + ' 13:00:00') let workdayAfternoonEnd = moment(curDayStr + ' 17:30:00') let calcStartTime = workdayForenoonStart.clone() if (calcStartTime.isSame(begainTime_, 'day')) { // 和开始日期是同一天就取开始日期 calcStartTime = curTime.clone() } if (calcStartTime.isBefore(workdayForenoonStart)) { // 在上班开始之前 取上班开始时间 calcStartTime = workdayForenoonStart.clone() } else if (calcStartTime.isBetween(workdayForenoonEnd, workdayAfternoonStart, null, '()')) { // 在午休时间内取下午上班时间 calcStartTime = workdayAfternoonStart.clone() } let calcEndTime = workdayAfternoonEnd.clone() if (calcEndTime.isSame(endTime_, 'day')) { // 和结束日期是同一天就取结束时间 calcEndTime = endTime_.clone() } if (calcEndTime.isBefore(workdayForenoonStart)) { // 在上班开始之前 calcEndTime = null } else if (calcEndTime.isBetween(workdayForenoonEnd, workdayAfternoonStart, null, '()')) { // 在午休时间内取上午下班时间 calcEndTime = workdayAfternoonStart.clone() } else if (calcEndTime.isAfter(workdayAfternoonEnd)) { // 在下午下班之后 取下午下班时间 calcEndTime = workdayAfternoonEnd.clone() } if (calcStartTime && calcEndTime && calcStartTime.isBefore(calcEndTime)) { let durationDay = calcEndTime.diff(calcStartTime, 'seconds') if (calcStartTime.isBefore(workdayForenoonEnd) && calcEndTime.isAfter(workdayAfternoonStart)) { // 跨越了午休时间 durationDay -= lunchBreakDuration } durationDayAddDay += durationDay if (durationDayAddDay > durationSec) { console.log(`请假每日持续时间之和已超过认定时间:day ${curDayStr} duration ${durationDay}`); durationDay = durationSec - durationDayAddDay } if (durationDay > 0) { packageDay.push({ day: curDayStr, duration: durationDay }) } } } curTime = curTime.add(1, 'day').startOf('day') } if (durationDayAddDay < durationSec) { console.log(`请假每日持续时间之和少于认定时间,补偿之`); // 每日持续时间比认定时间少 if (packageDay.length) { packageDay[packageDay.length - 1].duration += durationSec - durationDayAddDay } } let typeStorage = '' if (hrAffirmType.value) { typeStorage = hrAffirmType.value } else { typeStorage = type.value } const existRes = await models.Vacate.findOne({ where: { pepProcessStoryId: a.historyId } }) let storageD = { pepUserId: a.pepUserId, pepProcessStoryId: a.historyId, startTime: moment(begainTime.value).format('YYYY-MM-DD HH:mm:ss'), endTime: moment(endTime.value).format('YYYY-MM-DD HH:mm:ss'), duration: durationSec, type: typeStorage, wfProcessState: a.state, reason: reason.value } let storagedId = null if (existRes) { // 结束且存在 当前条件下没必要更新 // await models.Vacate.update(storageD, { // where: { // id: existRes.id // } // }) // storagedId = existRes.id updateCount++ } else { let res = await models.Vacate.create(storageD) storagedId = res.id insertCount++ } if (storagedId) { await models.VacateDay.destroy({ where: { vacateId: storagedId } }) if (packageDay.length) { await models.VacateDay.bulkCreate(packageDay.map(p => { return { ...p, vacateId: storagedId } })) } } } } else { console.warn('必填 value 缺失:', needData,); console.warn('流程数据:', a); invalidCount++ } } else if (a.processName.indexOf('加班') > -1) { // 深拷贝所需查询数据 let needData = JSON.parse(JSON.stringify(overtimeNeedData)) // 获取表单里的数据并插入 needData getData(a, needData) const { begainTime, endTime, duration, compensate, hrAffirmDuration, reason } = needData if (begainTime.value && endTime.value && hrAffirmDuration.value && compensate.value) { let durationSec = parseFloat(hrAffirmDuration.value) * 3600 if (typeof durationSec != 'number' || isNaN(durationSec || durationSec <= 0)) { console.warn('加班时长计算结果错误', duration); invalidCount++ } else { // 开始时间 let begainTime_ = moment(begainTime.value) // 加上人事确定的时间的结束时间 let endTimeWithHrAffirm = begainTime_.clone().add(durationSec, 'seconds') // 定义需要统计的各类变量 let takeRestWorkday = 0, takeRestDayoff = 0, takeRestFestivals = 0, payWorkday = 0, payDayoff = 0, payFestivals = 0 // 判断 结束时间在加班当天的时间节点后 也就是说跨天加班了 if (endTimeWithHrAffirm.isSameOrAfter(begainTime_)) { let packageSuccess = true // 考虑加了好多天的流程 let curday = begainTime_.clone() let packageDay = [] while (curday.isSameOrBefore(endTimeWithHrAffirm)) { let duration_ = 0 let curDayStr = curday.format('YYYY-MM-DD') if (curday.isSame(endTimeWithHrAffirm, 'day')) { // 是同一天 duration_ = endTimeWithHrAffirm.diff(curday, 'seconds') } else if (curday.isSame(begainTime_, 'day')) { // 和开始日期是同一天 // 同时也说明和结束日期不是同一天 duration_ = begainTime_.clone() .endOf('day') .diff(begainTime_, 'seconds') + 1 } else { // 跨了好几天 // 不但 curday 加 后一天也加 duration_ = 24 * 3600 } packageDay.push({ day: curDayStr, duration: duration_ }) const holidayRes = await judgeHoliday(curDayStr, true) if (holidayRes) { if (compensate.value && compensate.value.indexOf('调休') >= 0) { if (holidayRes.workday) { takeRestWorkday += duration_ } else if (holidayRes.dayoff) { takeRestDayoff += duration_ } else if (holidayRes.festivals) { takeRestFestivals += duration_ } else { let a = 4 } } else if (compensate.value && compensate.value.indexOf('补偿') >= 0) { if (holidayRes.workday) { payWorkday += duration_ } else if (holidayRes.dayoff) { payDayoff += duration_ } else if (holidayRes.festivals) { payFestivals += duration_ } } else { console.warn(`加班补偿字段未知:`, compensate.value); invalidCount++ packageSuccess = false break } } else { console.warn(`节假日信息获取失败`, curday.format('YYYY-MM-DD')); invalidCount++ packageSuccess = false break } curday = curday.add(1, 'day').startOf('day') } if (packageSuccess) { const existRes = await models.Overtime.findOne({ where: { pepProcessStoryId: a.historyId } }) let storageD = { pepUserId: a.pepUserId, pepProcessStoryId: a.historyId, startTime: moment(begainTime.value).format('YYYY-MM-DD HH:mm:ss'), endTime: moment(endTime.value).format('YYYY-MM-DD HH:mm:ss'), duration: durationSec, wfProcessState: a.state, takeRestWorkday, takeRestDayoff, takeRestFestivals, payWorkday, payDayoff, payFestivals, compensate: compensate.value, reason: reason.value } let storagedId = null if (existRes) { // 结束且存在 当前条件下没必要更新 // storagedId = existRes.id // await models.Overtime.update(storageD, { // where: { // id: existRes.id // } // }) updateCount++ } else { let res = await models.Overtime.create(storageD) storagedId = res.id insertCount++ } if (storagedId) { await models.OvertimeDay.destroy({ where: { overtimeId: storagedId } }) if (packageDay.length) { await models.OvertimeDay.bulkCreate(packageDay.map(p => { return { ...p, overtimeId: storagedId } })) } } } } else { console.warn(`结束时间在开始时间之前(unbelievable)`, '开始' + begainTime.value, '结束' + endTime.value, '人事确认结束' + endTimeWithHrAffirm.format()); invalidCount++ } } } else { console.warn('必填 value 缺失:', needData,); console.warn('流程数据:', a); invalidCount++ } } else { console.warn('假勤分组内不明流程'); unknowCount++ } } else { unCompletedCount++ } } else { invalidCount++ } } console.log(` 假勤数据更新 用时 ${moment().diff(startTime, 'seconds')} s `) console.log(` 共:${attendanceRes.length}; 新增:${insertCount}; 更新数据:${updateCount}; 非完成状态流程:${unCompletedCount}; 不明流程:${unknowCount}; 无效(warning):${invalidCount}; `); } catch (error) { app.fs.logger.error(`sechedule: updateAttendance, error: ${error} `); } }); return { updateAttendance, } }