You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
586 lines
29 KiB
586 lines
29 KiB
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,
|
|
}
|
|
}
|