package cn.iocoder.yudao.module.hrm.service.approval; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.*; import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.PostApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Duration; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; @Service @Slf4j public class HrmOvertimeApplicationServiceImpl implements HrmOvertimeApplicationService { @Resource private HrmOvertimeApplicationMapper mapper; @Resource private HrmEmployeeMapper employeeMapper; @Resource private HrmNoRedisDAO noRedisDAO; @Resource private DeptApi deptApi; @Resource private PostApi postApi; @Resource private BpmProcessInstanceApi bpmProcessInstanceApi; @Resource private BpmProcessInstanceService bpmProcessInstanceService; @Resource private BpmTaskService bpmTaskService; @Resource private BpmCategoryService bpmCategoryService; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long create(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) { HrmEmployeeDO employee = requireEmployee(loginUserId); HrmOvertimeApplicationDO application = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class); applyApplicantAndCalculatedTime(application, employee); application.setNo(noRedisDAO.generateOvertimeApplicationNo()); application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus()); mapper.insert(application); return application.getId(); } @Override public void update(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) { HrmOvertimeApplicationDO existing = requireOwnedDraft(reqVO.getId(), loginUserId); HrmOvertimeApplicationDO update = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class); applyApplicantAndCalculatedTime(update, requireEmployee(loginUserId)); update.setId(existing.getId()); mapper.updateById(update); } @Override public void delete(Long id, Long loginUserId) { HrmOvertimeApplicationDO application = require(id); boolean deletable = Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus()) || Objects.equals(application.getStatus(), HrmAuditStatusEnum.CANCEL.getStatus()); if (!Objects.equals(application.getUserId(), loginUserId) || !deletable) { throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } mapper.deleteById(id); } @Override public HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage) { HrmOvertimeApplicationDO application = mapper.selectById(id); if (application != null && !canManage && !Objects.equals(application.getUserId(), loginUserId) && !isProcessParticipant(application.getProcessInstanceId(), loginUserId)) { return null; } return application == null ? null : buildResp(application); } @Override public HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId) { HrmEmployeeDO employee = requireEmployee(loginUserId); HrmOvertimeApplicationRespVO resp = new HrmOvertimeApplicationRespVO(); fillEmployee(resp, employee); return resp; } @Override public PageResult getPage(HrmOvertimeApplicationPageReqVO reqVO) { PageResult page = mapper.selectPage(reqVO); return new PageResult<>(page.getList().stream().map(this::buildResp).toList(), page.getTotal()); } @Override @Transactional(rollbackFor = Exception.class) public void submit(Long id, String processDefinitionKey, Long loginUserId) { HrmOvertimeApplicationDO application = requireOwnedDraft(id, loginUserId); validateProcessDefinitions(); ProcessDefinition definition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey); if (definition == null || !BPM_PROCESS_DEFINITION_KEY.equals(definition.getKey())) { throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } String processInstanceId = bpmProcessInstanceApi.createProcessInstance(loginUserId, new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(processDefinitionKey) .setBusinessKey(String.valueOf(id))); mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).processInstanceId(processInstanceId) .status(HrmAuditStatusEnum.PROCESS.getStatus()).build()); } @Override @Transactional(rollbackFor = Exception.class) public void cancel(Long id, Long loginUserId) { HrmOvertimeApplicationDO application = require(id); if (!Objects.equals(application.getUserId(), loginUserId) || !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS); } bpmProcessInstanceService.cancelProcessInstanceByStartUser(loginUserId, new BpmProcessInstanceCancelReqVO().setId(application.getProcessInstanceId()).setReason("申请人撤销加班申请")); } @Override public List> getApproveProcessDefinitionList() { validateProcessDefinitions(); List infos = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); Set ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId); Map latest = new HashMap<>(); for (ProcessDefinition definition : bpmProcessDefinitionService.getProcessDefinitionList(ids)) { if (definition.isSuspended()) continue; latest.compute(definition.getKey(), (key, old) -> old == null || definition.getVersion() > old.getVersion() ? definition : old); } return latest.values().stream().map(item -> { Map value = new HashMap<>(); value.put("id", item.getId()); value.put("key", item.getKey()); value.put("name", item.getName()); value.put("version", item.getVersion()); return value; }).collect(Collectors.toList()); } @Override public void updateAuditStatus(Long id, Integer status) { HrmOvertimeApplicationDO application = require(id); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS); } mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).status(status) .approveTime(LocalDateTime.now()).build()); } private void validateProcessDefinitions() { if (!bpmCategoryService.getCategoryMap(Collections.singleton(APPROVE_CATEGORY_CODE)).containsKey(APPROVE_CATEGORY_CODE)) { throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } List infos = bpmProcessDefinitionService.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(infos)) throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); Set ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId); if (bpmProcessDefinitionService.getProcessDefinitionList(ids).stream().noneMatch(item -> !item.isSuspended())) { throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } } private void applyApplicantAndCalculatedTime(HrmOvertimeApplicationDO application, HrmEmployeeDO employee) { if (HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType()) == null) { throw exception(OVERTIME_APPLICATION_TIME_INVALID); } application.setUserId(employee.getUserId()); application.setEmployeeId(employee.getId()); application.setDeptId(employee.getDeptId()); long minutes = Duration.between(application.getStartTime(), application.getEndTime()).toMinutes(); boolean crossDay = minutes <= 0; if (crossDay) minutes += 24 * 60; BigDecimal mealHours = Boolean.TRUE.equals(application.getMealDeduction()) ? Optional.ofNullable(application.getMealDeductionHours()).orElse(BigDecimal.ONE) : BigDecimal.ZERO; BigDecimal rawHours = BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); if (Boolean.TRUE.equals(application.getMealDeduction()) && (mealHours.compareTo(new BigDecimal("0.5")) < 0 || mealHours.compareTo(new BigDecimal("4")) > 0 || mealHours.compareTo(rawHours) >= 0)) { throw exception(OVERTIME_APPLICATION_TIME_INVALID); } BigDecimal hours = rawHours .subtract(mealHours).multiply(BigDecimal.valueOf(2)).setScale(0, RoundingMode.HALF_UP) .divide(BigDecimal.valueOf(2), 1, RoundingMode.UNNECESSARY); if (hours.compareTo(BigDecimal.ZERO) <= 0) throw exception(OVERTIME_APPLICATION_TIME_INVALID); application.setCrossDay(crossDay); application.setMealDeductionHours(mealHours); application.setDuration(hours); } private HrmOvertimeApplicationDO require(Long id) { HrmOvertimeApplicationDO value = mapper.selectById(id); if (value == null) throw exception(OVERTIME_APPLICATION_NOT_EXISTS); return value; } private HrmOvertimeApplicationDO requireOwnedDraft(Long id, Long userId) { HrmOvertimeApplicationDO value = require(id); if (!Objects.equals(value.getUserId(), userId) || !Objects.equals(value.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } return value; } private HrmEmployeeDO requireEmployee(Long userId) { HrmEmployeeDO employee = employeeMapper.selectByUserId(userId); if (employee == null) throw exception(OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS); return employee; } /** 当前待办审批人或曾处理过该流程的审批人可查看业务详情。 */ private boolean isProcessParticipant(String processInstanceId, Long userId) { if (processInstanceId == null || userId == null) { return false; } if (bpmTaskService.getTodoTask(userId, null, processInstanceId) != null) { return true; } return bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true).stream() .anyMatch(task -> Objects.equals(NumberUtil.parseLong(task.getAssignee(), null), userId) || Objects.equals(NumberUtil.parseLong(task.getOwner(), null), userId)); } private HrmOvertimeApplicationRespVO buildResp(HrmOvertimeApplicationDO application) { HrmOvertimeApplicationRespVO resp = BeanUtils.toBean(application, HrmOvertimeApplicationRespVO.class); HrmEmployeeDO employee = employeeMapper.selectById(application.getEmployeeId()); if (employee != null) fillEmployee(resp, employee); resp.setOvertimeTypeName(HrmOvertimeTypeEnum.getName(application.getOvertimeType())); resp.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus())); return resp; } private void fillEmployee(HrmOvertimeApplicationRespVO resp, HrmEmployeeDO employee) { resp.setUserId(employee.getUserId()); resp.setEmployeeId(employee.getId()); resp.setEmployeeName(employee.getName()); resp.setEmployeeNo(employee.getEmployeeNo()); resp.setDeptId(employee.getDeptId()); DeptRespDTO dept = employee.getDeptId() == null ? null : deptApi.getDept(employee.getDeptId()); PostRespDTO post = employee.getPostId() == null ? null : postApi.getPostMap(Collections.singleton(employee.getPostId())).get(employee.getPostId()); resp.setDeptName(dept == null ? null : dept.getName()); resp.setPostName(post == null ? null : post.getName()); } }