package cn.iocoder.yudao.module.hrm.service.approval;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
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.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.hrm.controller.admin.approval.vo.HrmTransferApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmTransferApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmTransferApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmTransferTypeEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
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.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
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 org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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 实现类
*
* @author 超级管理员
*/
@Service
@Validated
@Slf4j
public class HrmTransferApplicationServiceImpl implements HrmTransferApplicationService {
@Resource
private HrmTransferApplicationMapper transferApplicationMapper;
@Resource
private HrmNoRedisDAO noRedisDAO;
@Resource
private DeptApi deptApi;
@Resource
private PostApi postApi;
@Resource
private AdminUserApi adminUserApi;
@Resource
private HrmEmployeeService employeeService;
@Resource
private BpmProcessInstanceApi bpmProcessInstanceApi;
@Resource
private BpmCategoryService bpmCategoryService;
@Resource
private StorageAttachmentApi storageAttachmentApi;
@Resource
private BpmProcessDefinitionService bpmProcessDefinitionService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTransferApplication(HrmTransferApplicationSaveReqVO createReqVO) {
// 1. 解析申请人 & 补全原部门/原岗位
// 前端表单只让用户选择「被调岗员工」,不直接提交 currentDeptId/currentPostId,
// 而这两个字段在库里是 NOT NULL,缺省会直接抛 SQL 异常(Field 'current_dept_id' doesn't have a default value)
Long userId = createReqVO.getUserId();
Long employeeId = createReqVO.getEmployeeId();
HrmEmployeeDO employee = null;
if (employeeId != null) {
employee = employeeService.getEmployee(employeeId);
}
if (employee == null && userId != null) {
// 约定 user_id 存 system_users.id;但历史上前端也有把「员工主键」塞进 userId 的情况,两种都兼容
employee = employeeService.getEmployeeByUserId(userId);
if (employee == null) {
employee = employeeService.getEmployee(userId);
}
}
// 以员工档案上的 user_id 为准(前端可能把员工主键塞在 userId 字段里,直接用会写错申请人)
if (employee != null && employee.getUserId() != null) {
userId = employee.getUserId();
}
if (userId == null && employee != null) {
userId = employee.getUserId();
}
if (userId == null) {
userId = getCurrentUserId();
if (employee == null) {
employee = employeeService.getEmployeeByUserId(userId);
}
}
if (adminUserApi.getUser(userId) == null) {
userId = getCurrentUserId();
}
if (createReqVO.getCurrentDeptId() == null && employee != null) {
createReqVO.setCurrentDeptId(employee.getDeptId());
}
if (createReqVO.getCurrentPostId() == null && employee != null) {
createReqVO.setCurrentPostId(parseFirstPostId(employee.getPostIds()));
}
if (createReqVO.getCurrentDeptId() == null) {
throw exception(TRANSFER_APPLICATION_CURRENT_DEPT_REQUIRED);
}
if (createReqVO.getCurrentPostId() == null) {
throw exception(TRANSFER_APPLICATION_CURRENT_POST_REQUIRED);
}
// 2. 校验目标部门与原部门不能相同(放在补全之后,否则补全出来的原部门绕过了校验)
if (Objects.equals(createReqVO.getTargetDeptId(), createReqVO.getCurrentDeptId())) {
throw exception(TRANSFER_APPLICATION_SAME_DEPT);
}
// 3. 校验目标岗位与原岗位不能相同
if (Objects.equals(createReqVO.getTargetPostId(), createReqVO.getCurrentPostId())) {
throw exception(TRANSFER_APPLICATION_SAME_POST);
}
// 4. 生成申请单号
String no = generateUniqueNo();
// 5. 插入
HrmTransferApplicationDO application = BeanUtils.toBean(createReqVO, HrmTransferApplicationDO.class);
application.setNo(no);
application.setUserId(userId);
application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
transferApplicationMapper.insert(application);
storageAttachmentApi.bindAttachments("file", "hrm_transfer_application",
application.getId(), createReqVO.getBlobIds());
return application.getId();
}
/**
* 取员工岗位列表中的第一个作为「原岗位」
*
*
hrm_employee.post_ids 以 JSON 数组字符串存储,例如 {@code [20,21]}
*
* @param postIds 岗位编号 JSON 字符串
* @return 第一个岗位编号;解析不出来时返回 {@code null}
*/
private Long parseFirstPostId(String postIds) {
if (StrUtil.isBlank(postIds)) {
return null;
}
try {
List ids = JSONUtil.toList(postIds, Long.class);
return CollUtil.isEmpty(ids) ? null : ids.get(0);
} catch (Exception ex) {
log.warn("[parseFirstPostId][postIds({}) 解析失败]", postIds, ex);
return null;
}
}
private Long getCurrentUserId() {
return 1L;
}
private String generateUniqueNo() {
String no = noRedisDAO.generateTransferApplicationNo();
if (transferApplicationMapper.selectByNo(no) != null) {
return generateUniqueNo();
}
return no;
}
@Override
public void updateTransferApplication(HrmTransferApplicationSaveReqVO updateReqVO) {
HrmTransferApplicationDO application = validateTransferApplicationExists(updateReqVO.getId());
if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
&& !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
}
if (Objects.equals(updateReqVO.getTargetDeptId(), updateReqVO.getCurrentDeptId())) {
throw exception(TRANSFER_APPLICATION_SAME_DEPT);
}
if (Objects.equals(updateReqVO.getTargetPostId(), updateReqVO.getCurrentPostId())) {
throw exception(TRANSFER_APPLICATION_SAME_POST);
}
HrmTransferApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmTransferApplicationDO.class);
transferApplicationMapper.updateById(updateObj);
storageAttachmentApi.updateAttachments("file", "hrm_transfer_application", updateReqVO.getId(), updateReqVO.getBlobIds());
}
@Override
public void deleteTransferApplication(Long id) {
HrmTransferApplicationDO application = validateTransferApplicationExists(id);
if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
}
storageAttachmentApi.deleteAttachmentsByRecord("hrm_transfer_application", id);
transferApplicationMapper.deleteById(id);
}
private HrmTransferApplicationDO validateTransferApplicationExists(Long id) {
HrmTransferApplicationDO application = transferApplicationMapper.selectById(id);
if (application == null) {
throw exception(TRANSFER_APPLICATION_NOT_EXISTS);
}
return application;
}
@Override
public HrmTransferApplicationDO getTransferApplication(Long id) {
return transferApplicationMapper.selectById(id);
}
@Override
public HrmTransferApplicationRespVO getTransferApplicationDetail(Long id) {
HrmTransferApplicationDO application = getTransferApplication(id);
if (application == null) {
return null;
}
HrmTransferApplicationRespVO respVO = BeanUtils.toBean(application, HrmTransferApplicationRespVO.class);
if (application.getUserId() != null) {
// user_id 正常存 system_users.id;历史数据也有存「员工主键」的,两种都兼容
HrmEmployeeDO employee = employeeService.getEmployeeByUserId(application.getUserId());
if (employee == null) {
employee = employeeService.getEmployee(application.getUserId());
}
if (employee != null) {
respVO.setUserName(employee.getName());
respVO.setEmployeeId(employee.getId());
}
}
fillDeptNames(respVO, application);
fillPostNames(respVO, application);
respVO.setTransferTypeName(HrmTransferTypeEnum.getNameByStatus(application.getTransferType()));
respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_transfer_application", id));
return respVO;
}
private void fillDeptNames(HrmTransferApplicationRespVO respVO, HrmTransferApplicationDO application) {
Set deptIds = Set.of(application.getCurrentDeptId(), application.getTargetDeptId())
.stream().filter(Objects::nonNull).collect(Collectors.toSet());
Map deptMap = deptApi.getDeptMap(deptIds);
if (application.getCurrentDeptId() != null && deptMap.containsKey(application.getCurrentDeptId())) {
respVO.setCurrentDeptName(deptMap.get(application.getCurrentDeptId()).getName());
}
if (application.getTargetDeptId() != null && deptMap.containsKey(application.getTargetDeptId())) {
respVO.setTargetDeptName(deptMap.get(application.getTargetDeptId()).getName());
}
}
private void fillPostNames(HrmTransferApplicationRespVO respVO, HrmTransferApplicationDO application) {
Set postIds = Set.of(application.getCurrentPostId(), application.getTargetPostId())
.stream().filter(Objects::nonNull).collect(Collectors.toSet());
Map postMap = postApi.getPostMap(postIds);
if (application.getCurrentPostId() != null && postMap.containsKey(application.getCurrentPostId())) {
respVO.setCurrentPostName(postMap.get(application.getCurrentPostId()).getName());
}
if (application.getTargetPostId() != null && postMap.containsKey(application.getTargetPostId())) {
respVO.setTargetPostName(postMap.get(application.getTargetPostId()).getName());
}
}
@Override
public PageResult getTransferApplicationPage(HrmTransferApplicationPageReqVO pageReqVO) {
PageResult pageResult = transferApplicationMapper.selectPage(pageReqVO);
if (pageResult.getList().isEmpty()) {
return new PageResult<>(Collections.emptyList(), pageResult.getTotal());
}
Set userIds = pageResult.getList().stream()
.map(HrmTransferApplicationDO::getUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// userId 存的是员工ID,按员工表查询姓名
Map employeeNameMap = new HashMap<>();
for (Long userId : userIds) {
HrmEmployeeDO employee = employeeService.getEmployee(userId);
if (employee != null) {
employeeNameMap.put(userId, employee.getName());
}
}
Set deptIds = pageResult.getList().stream()
.flatMap(app -> Stream.of(app.getCurrentDeptId(), app.getTargetDeptId()))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map deptMap = deptApi.getDeptMap(deptIds);
Set postIds = pageResult.getList().stream()
.flatMap(app -> Stream.of(app.getCurrentPostId(), app.getTargetPostId()))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map postMap = postApi.getPostMap(postIds);
List list = pageResult.getList().stream().map(app -> {
HrmTransferApplicationRespVO vo = BeanUtils.toBean(app, HrmTransferApplicationRespVO.class);
if (app.getUserId() != null && employeeNameMap.containsKey(app.getUserId())) {
vo.setUserName(employeeNameMap.get(app.getUserId()));
}
if (app.getCurrentDeptId() != null && deptMap.containsKey(app.getCurrentDeptId())) {
vo.setCurrentDeptName(deptMap.get(app.getCurrentDeptId()).getName());
}
if (app.getTargetDeptId() != null && deptMap.containsKey(app.getTargetDeptId())) {
vo.setTargetDeptName(deptMap.get(app.getTargetDeptId()).getName());
}
if (app.getCurrentPostId() != null && postMap.containsKey(app.getCurrentPostId())) {
vo.setCurrentPostName(postMap.get(app.getCurrentPostId()).getName());
}
if (app.getTargetPostId() != null && postMap.containsKey(app.getTargetPostId())) {
vo.setTargetPostName(postMap.get(app.getTargetPostId()).getName());
}
vo.setTransferTypeName(HrmTransferTypeEnum.getNameByStatus(app.getTransferType()));
vo.setStatusName(HrmAuditStatusEnum.getNameByStatus(app.getStatus()));
return vo;
}).collect(Collectors.toList());
return new PageResult<>(list, pageResult.getTotal());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitTransferApplication(Long id, String processDefinitionKey, Long userId) {
HrmTransferApplicationDO application = validateTransferApplicationExists(id);
if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
throw exception(TRANSFER_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
}
validateApproveCategoryAndProcessDefinition();
ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
if (processDefinition == null) {
throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
}
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
transferApplicationMapper.updateById(HrmTransferApplicationDO.builder()
.id(id)
.processInstanceId(processInstanceId)
.status(HrmAuditStatusEnum.PROCESS.getStatus())
.build());
}
private void validateApproveCategoryAndProcessDefinition() {
List codes = Collections.singletonList(APPROVE_CATEGORY_CODE);
Map categoryMap = bpmCategoryService.getCategoryMap(codes);
if (!categoryMap.containsKey(APPROVE_CATEGORY_CODE)) {
throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
}
List definitionInfoList = bpmProcessDefinitionService
.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
if (CollUtil.isEmpty(definitionInfoList)) {
throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
}
Set processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
List processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
boolean hasActiveDefinition = processDefinitions.stream().anyMatch(pd -> !pd.isSuspended());
if (!hasActiveDefinition) {
throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
}
}
@Override
public List