yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java
@@ -1,16 +1,18 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.hutool.core.util.StrUtil;
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.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceExceptionMapper;
import cn.iocoder.yudao.module.system.api.approval.ApprovalConfigApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceExceptionTypeEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
@@ -20,9 +22,10 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
@@ -35,18 +38,27 @@
@Slf4j
public class HrmAttendanceExceptionServiceImpl implements HrmAttendanceExceptionService {
    /**
     * 考勤异常申请审批的业务类型编码
     *
     * 与 system_approval_config.biz_type 对应,审批人在「系统管理 - 审批配置」中维护。
     */
    private static final String ATTENDANCE_APPROVE_BIZ_TYPE = "hrm_attendance_approve";
    @Resource
    private HrmAttendanceExceptionMapper attendanceExceptionMapper;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    private ApprovalConfigApi approvalConfigApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private HrmAttendanceRecordService attendanceRecordService;
    @Override
    public Long createAttendanceException(HrmAttendanceExceptionSaveReqVO createReqVO) {
@@ -71,7 +83,10 @@
    @Override
    public void updateAttendanceException(HrmAttendanceExceptionSaveReqVO updateReqVO) {
        validateAttendanceExceptionExists(updateReqVO.getId());
        HrmAttendanceExceptionDO attendanceException = validateAttendanceExceptionExists(updateReqVO.getId());
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_UPDATE_FAIL_NOT_DRAFT);
        }
        HrmAttendanceExceptionDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceExceptionDO.class);
        attendanceExceptionMapper.updateById(updateObj);
        storageAttachmentApi.updateAttachments("file", "hrm_attendance_exception", updateReqVO.getId(), updateReqVO.getBlobIds());
@@ -79,7 +94,10 @@
    @Override
    public void deleteAttendanceException(Long id) {
        validateAttendanceExceptionExists(id);
        HrmAttendanceExceptionDO attendanceException = validateAttendanceExceptionExists(id);
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(attendanceException.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_DELETE_FAIL_NOT_DRAFT);
        }
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_attendance_exception", id);
        attendanceExceptionMapper.deleteById(id);
    }
@@ -104,47 +122,73 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitAttendanceException(Long id, String processDefinitionKey, Long userId) {
        // 1. 校验异常申请存在且为草稿状态
    public void submitAttendanceException(Long id) {
        // 1. 校验存在 + 可提交状态(草稿 / 审核不通过)
        HrmAttendanceExceptionDO exception = validateAttendanceExceptionExists(id);
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(exception.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_SUBMIT_FAIL_NOT_DRAFT);
        if (!HrmAuditStatusEnum.DRAFT.getStatus().equals(exception.getStatus())
                && !HrmAuditStatusEnum.REJECT.getStatus().equals(exception.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 2. 校验流程定义存在且为激活状态
        if (processDefinitionKey != null) {
            bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        }
        // 2. 校验审批配置已启用且配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(ATTENDANCE_APPROVE_BIZ_TYPE);
        // 3. 创建审批流程实例
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 3. 状态置为审批中,并清空上一轮审核结果,避免残留「审核不通过」的原因
        attendanceExceptionMapper.submitAndResetAuditInfo(id, HrmAuditStatusEnum.PROCESS.getStatus());
    }
        // 4. 更新异常申请状态
        attendanceExceptionMapper.updateById(HrmAttendanceExceptionDO.builder()
                .id(id)
                .processInstanceId(processInstanceId)
                .status(HrmAuditStatusEnum.PROCESS.getStatus())
                .build());
    @Override
    public Set<Long> getAttendanceExceptionApproverUserIds() {
        return approvalConfigApi.getApproverUserIds(ATTENDANCE_APPROVE_BIZ_TYPE);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateAttendanceExceptionAuditStatus(Long id, Integer auditStatus) {
        // 1. 校验异常申请存在
    public void auditAttendanceException(Long id, Boolean pass, String reviewRemark) {
        // 1. 校验存在
        HrmAttendanceExceptionDO exception = validateAttendanceExceptionExists(id);
        // 2. 校验当前状态为审批中
        // 2. 校验处于审批中状态
        if (!HrmAuditStatusEnum.PROCESS.getStatus().equals(exception.getStatus())) {
            throw exception(ATTENDANCE_EXCEPTION_UPDATE_AUDIT_STATUS_FAIL);
            throw exception(ATTENDANCE_EXCEPTION_AUDIT_FAIL_NOT_PROCESS);
        }
        // 3. 更新状态
        attendanceExceptionMapper.updateById(HrmAttendanceExceptionDO.builder()
                .id(id)
                .status(auditStatus)
                .approveTime(LocalDateTime.now())
                .build());
        // 3. 校验当前登录用户是该业务类型的审批人(或签:任一人均可审核)
        Long userId = getLoginUserId();
        approvalConfigApi.validateApprover(ATTENDANCE_APPROVE_BIZ_TYPE, userId);
        // 4. 审核不通过:必须填写原因,状态置为审核不通过,退回提交人修改后可重新提交
        if (!Boolean.TRUE.equals(pass)) {
            if (StrUtil.isBlank(reviewRemark)) {
                throw exception(ATTENDANCE_EXCEPTION_AUDIT_REJECT_REASON_REQUIRED);
            }
            attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.REJECT.getStatus(),
                    userId, getUserNickname(userId), reviewRemark);
            return;
        }
        // 5. 审核通过:记录审核人与审核意见,状态置为审核通过
        attendanceExceptionMapper.auditAttendanceException(id, HrmAuditStatusEnum.APPROVE.getStatus(),
                userId, getUserNickname(userId), reviewRemark);
        // 6. 审核通过后置业务逻辑:补卡回填上下班时间 / 加班回写加班时长
        if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())) {
            log.info("[auditAttendanceException] 补卡申请({}) 审批通过,开始更新考勤记录", id);
            attendanceRecordService.supplementClockIn(exception);
        } else if (HrmAttendanceExceptionTypeEnum.OVERTIME.getType().equals(exception.getType())) {
            log.info("[auditAttendanceException] 加班申请({}) 审批通过,开始回写加班时长", id);
            attendanceRecordService.recordOvertime(exception);
        }
    }
    /**
     * 获取用户昵称
     *
     * @param userId 用户编号
     * @return 昵称,用户不存在时返回 null
     */
    private String getUserNickname(Long userId) {
        if (userId == null) {
            return null;
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        return user == null ? null : user.getNickname();
    }
}