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<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO) {
|
PageResult<HrmOvertimeApplicationDO> 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<Map<String, Object>> getApproveProcessDefinitionList() {
|
validateProcessDefinitions();
|
List<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService
|
.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
|
Set<String> ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
|
Map<String, ProcessDefinition> 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<String, Object> 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<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
|
if (CollUtil.isEmpty(infos)) throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
|
Set<String> 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());
|
}
|
}
|