package cn.iocoder.yudao.module.hrm.service.approval; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; 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; @Resource private SecurityFrameworkService securityFrameworkService; @Override @Transactional(rollbackFor = Exception.class) public Long create(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) { HrmEmployeeDO employee = resolveApplicant(reqVO.getEmployeeId(), 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); // 未重新选择申请人时,沿用原申请人,避免代申请单被改回登录人 Long requestedEmployeeId = reqVO.getEmployeeId() != null ? reqVO.getEmployeeId() : existing.getEmployeeId(); applyApplicantAndCalculatedTime(update, resolveApplicant(requestedEmployeeId, 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) && !canManageOvertime()) { throw exception(OVERTIME_APPLICATION_OPERATOR_NOT_ALLOWED); } if (!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); // 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。 if (bpmProcessInstanceApi.isGlobalAutoApprove()) { mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).processInstanceId("") .status(HrmAuditStatusEnum.PROCESS.getStatus()).build()); updateAuditStatus(id, HrmAuditStatusEnum.APPROVE.getStatus()); return; } 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(application.getUserId(), new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(processDefinitionKey) .setBusinessKey(String.valueOf(id)) .setVariables(Map.of("businessNo", application.getNo()))); mapper.update(null, new LambdaUpdateWrapper() .eq(HrmOvertimeApplicationDO::getId, id) .set(HrmOvertimeApplicationDO::getProcessInstanceId, processInstanceId) .set(HrmOvertimeApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus()) .set(HrmOvertimeApplicationDO::getApproveTime, null)); } @Override @Transactional(rollbackFor = Exception.class) public void cancel(Long id, Long loginUserId) { HrmOvertimeApplicationDO application = require(id); if (!Objects.equals(application.getUserId(), loginUserId) && !canManageOvertime()) { throw exception(OVERTIME_APPLICATION_OPERATOR_NOT_ALLOWED); } if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS); } // 流程由申请人员工发起,撤销也必须以该员工账号作为发起人 bpmProcessInstanceService.cancelProcessInstanceByStartUser(application.getUserId(), 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.update(null, new LambdaUpdateWrapper() .eq(HrmOvertimeApplicationDO::getId, id) .set(HrmOvertimeApplicationDO::getStatus, status) .set(HrmOvertimeApplicationDO::getApproveTime, Objects.equals(status, HrmAuditStatusEnum.APPROVE.getStatus()) ? LocalDateTime.now() : null)); } 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); // 持有 hrm:overtime:manage 权限可操作全部人的单子;否则只能操作本人单子 if (!Objects.equals(value.getUserId(), userId) && !canManageOvertime()) { throw exception(OVERTIME_APPLICATION_OPERATOR_NOT_ALLOWED); } if (!Objects.equals(value.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus()) && !Objects.equals(value.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) { throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } return value; } /** * 解析本次申请对应的员工档案。 * * @param requestedEmployeeId 前端选择的申请人员工ID;为空表示本人 * @param loginUserId 当前登录用户ID * @return 申请人对应的员工档案 */ private HrmEmployeeDO resolveApplicant(Long requestedEmployeeId, Long loginUserId) { HrmEmployeeDO self = employeeMapper.selectByUserId(loginUserId); if (requestedEmployeeId == null || (self != null && Objects.equals(self.getId(), requestedEmployeeId))) { if (self == null) { throw exception(OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS); } return self; } // 代申请:必须有管理权限 if (!canManageOvertime()) { throw exception(OVERTIME_APPLICATION_PROXY_NOT_ALLOWED); } HrmEmployeeDO target = employeeMapper.selectById(requestedEmployeeId); if (target == null) { throw exception(OVERTIME_APPLICATION_SELECTED_EMPLOYEE_NOT_EXISTS); } return target; } private boolean canManageOvertime() { return securityFrameworkService.hasPermission("hrm:overtime:manage"); } 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()); resp.setDeptName(dept == null ? null : dept.getName()); // 岗位多选:解析 postIds JSON,拼接岗位名称 if (StrUtil.isNotBlank(employee.getPostIds())) { List postIds = JSONUtil.toList(employee.getPostIds(), Long.class); if (CollUtil.isNotEmpty(postIds)) { Map postMap = postApi.getPostMap(postIds); resp.setPostName(postIds.stream() .filter(postMap::containsKey) .map(id -> postMap.get(id).getName()) .collect(Collectors.joining(","))); } } } }