云
2026-10-31 bf5f52656e128f578ed42c7979c31cf4b476ce46
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -3,10 +3,17 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.enums.HrmEmploymentTypeEnum;
import cn.iocoder.yudao.module.mes.api.cal.MesCalTeamApi;
import cn.iocoder.yudao.module.mes.api.cal.dto.MesCalTeamRespDTO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
@@ -21,6 +28,7 @@
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeWorkHistoryMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
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;
@@ -29,13 +37,19 @@
import cn.iocoder.yudao.module.system.api.permission.dto.RoleRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserSyncReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -48,10 +62,11 @@
/**
 * 员工信息 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@Slf4j
public class HrmEmployeeServiceImpl implements HrmEmployeeService {
    @Resource
@@ -72,6 +87,18 @@
    private AdminUserApi adminUserApi;
    @Resource
    private RoleApi roleApi;
    @Resource
    private MesCalTeamApi mesCalTeamApi;
    @Resource
    private ConfigApi configApi;
    /**
     * 用户初始密码的系统参数配置项
     */
    private static final String USER_INIT_PASSWORD_KEY = "system.user.init-password";
    @Resource
    @Lazy // 自注入需延迟加载,否则 bean 被代理包装后注入原始版本导致启动失败
    private HrmEmployeeService self;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -91,7 +118,8 @@
        userCreateReqDTO.setDeptId(createReqVO.getDeptId());
        userCreateReqDTO.setPostIds(Collections.singleton(createReqVO.getPostId()));
        userCreateReqDTO.setMobile(createReqVO.getPhone());
        userCreateReqDTO.setPassword(createReqVO.getPhone());
        // 初始密码:优先用导入/新增时指定的密码,否则读取系统参数;两者都没有则报错
        userCreateReqDTO.setPassword(resolveInitialPassword(createReqVO.getPassword()));
        userCreateReqDTO.setSex(createReqVO.getGender());
        userCreateReqDTO.setEmail(createReqVO.getEmail());
        userCreateReqDTO.setRemark(createReqVO.getRemark());
@@ -113,24 +141,24 @@
        employeeMapper.insert(employee);
        Long employeeId = employee.getId();
        // 7. 同步添加教育经历
        if (CollUtil.isNotEmpty(createReqVO.getEducationList())) {
            createReqVO.getEducationList().forEach(educationVO -> {
        if (CollUtil.isNotEmpty(createReqVO.getEducations())) {
            createReqVO.getEducations().forEach(educationVO -> {
                HrmEmployeeEducationDO education = BeanUtils.toBean(educationVO, HrmEmployeeEducationDO.class);
                education.setEmployeeId(employeeId);
                educationMapper.insert(education);
            });
        }
        // 8. 同步添加工作经历
        if (CollUtil.isNotEmpty(createReqVO.getWorkHistoryList())) {
            createReqVO.getWorkHistoryList().forEach(workHistoryVO -> {
        if (CollUtil.isNotEmpty(createReqVO.getWorkHistories())) {
            createReqVO.getWorkHistories().forEach(workHistoryVO -> {
                HrmEmployeeWorkHistoryDO workHistory = BeanUtils.toBean(workHistoryVO, HrmEmployeeWorkHistoryDO.class);
                workHistory.setEmployeeId(employeeId);
                workHistoryMapper.insert(workHistory);
            });
        }
        // 9. 同步添加紧急联系人
        if (CollUtil.isNotEmpty(createReqVO.getEmergencyContactList())) {
            createReqVO.getEmergencyContactList().forEach(emergencyContactVO -> {
        if (CollUtil.isNotEmpty(createReqVO.getEmergencyContacts())) {
            createReqVO.getEmergencyContacts().forEach(emergencyContactVO -> {
                HrmEmployeeEmergencyContactDO emergencyContact = BeanUtils.toBean(emergencyContactVO, HrmEmployeeEmergencyContactDO.class);
                emergencyContact.setEmployeeId(employeeId);
                emergencyContactMapper.insert(emergencyContact);
@@ -148,6 +176,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateEmployee(HrmEmployeeSaveReqVO updateReqVO) {
        // 校验存在
        HrmEmployeeDO employee = validateEmployeeExists(updateReqVO.getId());
@@ -160,21 +189,130 @@
                && employeeMapper.selectByPhone(updateReqVO.getPhone()) != null) {
            throw exception(EMPLOYEE_PHONE_EXISTS);
        }
        // 更新
        // 更新员工主表
        HrmEmployeeDO updateObj = BeanUtils.toBean(updateReqVO, HrmEmployeeDO.class);
        // roleIds 存储为 JSON
        if (CollUtil.isNotEmpty(updateReqVO.getRoleIds())) {
            updateObj.setRoleIds(JSONUtil.toJsonStr(updateReqVO.getRoleIds()));
        }
        employeeMapper.updateById(updateObj);
        Long employeeId = updateReqVO.getId();
        // 同步系统用户密码:仅当显式填写了密码时才更新,未填写保持原密码不变
        if (StrUtil.isNotBlank(updateReqVO.getPassword()) && employee.getUserId() != null) {
            adminUserApi.updateUserPassword(employee.getUserId(), updateReqVO.getPassword());
        }
        // 同步系统账号可对应字段(姓名 / 手机号 / 部门 / 性别 / 邮箱)
        if (employee.getUserId() != null) {
            syncSystemUserForEmployee(employee, updateReqVO);
        }
        // 同步教育经历
        if (updateReqVO.getEducations() != null) syncEducationList(employeeId, updateReqVO.getEducations());
        // 同步工作经历
        if (updateReqVO.getWorkHistories() != null) syncWorkHistoryList(employeeId, updateReqVO.getWorkHistories());
        // 同步紧急联系人
        if (updateReqVO.getEmergencyContacts() != null) syncEmergencyContactList(employeeId, updateReqVO.getEmergencyContacts());
    }
    /**
     * 员工修改后,把可一一对应的字段同步到系统账号。
     *
     * 只同步:姓名↔昵称、手机号↔mobile、部门、性别、邮箱。
     * 登录名 username、岗位、角色、状态不同步。
     * 同步失败时回滚员工更新,避免员工和系统账号不一致。
     */
    private void syncSystemUserForEmployee(HrmEmployeeDO employee, HrmEmployeeSaveReqVO reqVO) {
        try {
            AdminUserSyncReqDTO dto = new AdminUserSyncReqDTO();
            dto.setUserId(employee.getUserId());
            dto.setNickname(reqVO.getName() != null ? reqVO.getName() : employee.getName());
            dto.setMobile(reqVO.getPhone() != null ? reqVO.getPhone() : employee.getPhone());
            dto.setDeptId(reqVO.getDeptId() != null ? reqVO.getDeptId() : employee.getDeptId());
            dto.setSex(reqVO.getGender() != null ? reqVO.getGender() : employee.getGender());
            dto.setEmail(reqVO.getEmail() != null ? reqVO.getEmail() : employee.getEmail());
            adminUserApi.updateUserForEmployee(dto);
        } catch (Exception ex) {
            log.warn("[updateEmployee] 员工账号同步失败,员工编号={}", employee.getId());
            throw exception(EMPLOYEE_USER_SYNC_FAILED);
        }
    }
    private void syncEducationList(Long employeeId, List<HrmEmployeeEducationSaveReqVO> newList) {
        educationMapper.deleteByEmployeeId(employeeId);
        if (CollUtil.isNotEmpty(newList)) {
            newList.forEach(vo -> {
                HrmEmployeeEducationDO education = BeanUtils.toBean(vo, HrmEmployeeEducationDO.class);
                education.setId(null);
                education.setEmployeeId(employeeId);
                educationMapper.insert(education);
            });
        }
    }
    private void syncWorkHistoryList(Long employeeId, List<HrmEmployeeWorkHistorySaveReqVO> newList) {
        workHistoryMapper.deleteByEmployeeId(employeeId);
        if (CollUtil.isNotEmpty(newList)) {
            newList.forEach(vo -> {
                HrmEmployeeWorkHistoryDO workHistory = BeanUtils.toBean(vo, HrmEmployeeWorkHistoryDO.class);
                workHistory.setId(null);
                workHistory.setEmployeeId(employeeId);
                workHistoryMapper.insert(workHistory);
            });
        }
    }
    private void syncEmergencyContactList(Long employeeId, List<HrmEmployeeEmergencyContactSaveReqVO> newList) {
        emergencyContactMapper.deleteByEmployeeId(employeeId);
        if (CollUtil.isNotEmpty(newList)) {
            newList.forEach(vo -> {
                HrmEmployeeEmergencyContactDO emergencyContact = BeanUtils.toBean(vo, HrmEmployeeEmergencyContactDO.class);
                emergencyContact.setId(null);
                emergencyContact.setEmployeeId(employeeId);
                emergencyContactMapper.insert(emergencyContact);
            });
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteEmployee(Long id) {
        // 校验存在
        validateEmployeeExists(id);
        // 删除
        HrmEmployeeDO employee = validateEmployeeExists(id);
        // 1. 联动删除系统账号(不发布事件,避免回环)
        deleteLinkedSystemUser(employee);
        // 2. 级联删除子表
        educationMapper.deleteByEmployeeId(id);
        workHistoryMapper.deleteByEmployeeId(id);
        emergencyContactMapper.deleteByEmployeeId(id);
        // 3. 删除员工
        employeeMapper.deleteById(id);
    }
    /**
     * 删除员工时联动删除其系统账号。
     *
     * 守卫:
     * 1. 未绑定账号 -> 跳过
     * 2. 内置 admin(id=1) -> 跳过
     * 3. 同一账号被多条员工档案引用 -> 跳过(一员工一账号约定下不应出现)
     */
    private void deleteLinkedSystemUser(HrmEmployeeDO employee) {
        Long userId = employee.getUserId();
        if (userId == null) {
            return;
        }
        if (Long.valueOf(1L).equals(userId)) {
            log.warn("[deleteEmployee][员工({}) 绑定的是内置管理员账号({}),不自动删除账号]", employee.getId(), userId);
            return;
        }
        Long count = employeeMapper.selectCount(HrmEmployeeDO::getUserId, userId);
        if (count != null && count > 1) {
            log.warn("[deleteEmployee][账号({}) 关联了 {} 条员工档案,不自动删除账号]", userId, count);
            return;
        }
        adminUserApi.deleteUserSilently(userId);
    }
    private HrmEmployeeDO validateEmployeeExists(Long id) {
@@ -221,6 +359,17 @@
            List<RoleRespDTO> roles = roleApi.getRoleList(respVO.getRoleIds());
            if (CollUtil.isNotEmpty(roles)) {
                respVO.setRoleNames(roles.stream().map(RoleRespDTO::getName).collect(Collectors.joining(",")));
            }
        }
        // 填充用工性质名称
        if (employee.getEmploymentType() != null) {
            respVO.setEmploymentTypeName(HrmEmploymentTypeEnum.getNameByType(employee.getEmploymentType()));
        }
        // 填充所属班组名称
        if (employee.getTeamId() != null) {
            List<MesCalTeamRespDTO> teams = mesCalTeamApi.getTeamList(Collections.singleton(employee.getTeamId()));
            if (CollUtil.isNotEmpty(teams)) {
                respVO.setTeamName(teams.get(0).getName());
            }
        }
        return respVO;
@@ -272,4 +421,137 @@
        return employeeMapper.selectByUserId(userId);
    }
    @Override
    public HrmEmployeeDO getEmployeeByName(String name) {
        return employeeMapper.selectByName(name);
    }
    @Override
    public List<HrmEmployeeDO> getEmployeeListByName(String name) {
        return employeeMapper.selectListByName(name);
    }
    @Override
    public HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> rows, HrmEmployeeImportReqVO req) {
        if (CollUtil.isEmpty(rows)) throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY);
        Map<String, List<DeptRespDTO>> depts = deptApi.getDeptList().stream()
                .filter(v -> StrUtil.isNotBlank(v.getName())).collect(Collectors.groupingBy(v -> v.getName().trim()));
        Map<String, List<PostRespDTO>> posts = postApi.getPostList().stream()
                .filter(v -> StrUtil.isNotBlank(v.getName())).collect(Collectors.groupingBy(v -> v.getName().trim()));
        Map<String, List<RoleRespDTO>> roles = roleApi.getRoleList().stream()
                .filter(v -> StrUtil.isNotBlank(v.getName())).collect(Collectors.groupingBy(v -> v.getName().trim()));
        HrmEmployeeImportRespVO result = HrmEmployeeImportRespVO.builder()
                .createNames(new ArrayList<>()).updateNames(new ArrayList<>()).failureNames(new LinkedHashMap<>()).build();
        Set<String> phones = new LinkedHashSet<>();
        boolean hasData = false;
        for (int i = 0; i < rows.size(); i++) {
            HrmEmployeeImportExcelVO row = rows.get(i);
            if (row == null || cn.hutool.core.bean.BeanUtil.beanToMap(row).values().stream()
                    .noneMatch(v -> v != null && (!(v instanceof String) || StrUtil.isNotBlank((String) v)))) continue;
            hasData = true;
            String label = "第" + (i + 2) + "行" + (StrUtil.isBlank(row.getName()) ? "" : "(" + row.getName().trim() + ")");
            try {
                if (StrUtil.isBlank(row.getName()) || StrUtil.isBlank(row.getPhone()) || row.getHireDate() == null) {
                    throw exception(EMPLOYEE_IMPORT_INVALID, "员工姓名、手机号和入职日期不能为空");
                }
                row.setName(row.getName().trim()); row.setPhone(row.getPhone().trim());
                if (!phones.add(row.getPhone())) throw exception(EMPLOYEE_IMPORT_INVALID, "文件内手机号重复");
                if (StrUtil.isBlank(row.getDeptName()) || StrUtil.isBlank(row.getPostName())) {
                    throw exception(EMPLOYEE_IMPORT_INVALID, "部门和岗位不能为空");
                }
                List<DeptRespDTO> deptMatches = depts.get(row.getDeptName().trim());
                List<PostRespDTO> postMatches = posts.get(row.getPostName().trim());
                if (deptMatches == null || deptMatches.size() != 1) throw exception(EMPLOYEE_IMPORT_INVALID, "部门名称不存在或不唯一");
                if (postMatches == null || postMatches.size() != 1) throw exception(EMPLOYEE_IMPORT_INVALID, "岗位名称不存在或不唯一");
                Set<Long> roleIds = new LinkedHashSet<>();
                if (StrUtil.isNotBlank(row.getRoleNames())) {
                    for (String value : row.getRoleNames().split("[,,]")) {
                        String name = value.trim();
                        if (name.isEmpty()) continue;
                        List<RoleRespDTO> matches = roles.get(name);
                        if (matches == null || matches.size() != 1) throw exception(EMPLOYEE_IMPORT_INVALID, "角色名称不存在或不唯一:" + name);
                        roleIds.add(matches.get(0).getId());
                    }
                }
                HrmEmployeeSaveReqVO save = buildImportSaveReqVO(row, deptMatches.get(0).getId(), postMatches.get(0).getId(), roleIds);
                HrmEmployeeDO existing = employeeMapper.selectByPhone(row.getPhone());
                if (existing == null) {
                    if (row.getGender() == null) throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "性别");
                    self.createEmployee(save);
                    result.getCreateNames().add(label);
                } else {
                    if (!Boolean.TRUE.equals(req.getUpdateSupport())) throw exception(EMPLOYEE_PHONE_EXISTS);
                    if (StrUtil.isNotBlank(row.getPassword())) throw exception(EMPLOYEE_IMPORT_INVALID, "覆盖已有员工时初始密码须留空,密码请在账号管理中修改");
                    if (!roleIds.isEmpty()) throw exception(EMPLOYEE_IMPORT_INVALID, "覆盖员工时不支持修改用户角色,请留空并在账号管理中维护");
                    if (!Objects.equals(existing.getPostId(), save.getPostId())) throw exception(EMPLOYEE_IMPORT_INVALID, "覆盖员工时暂不支持变更岗位,请先处理账号岗位配置");
                    save.setId(existing.getId());
                    if (row.getGender() == null) save.setGender(existing.getGender());
                    if (row.getEmployeeStatus() == null) save.setEmployeeStatus(existing.getEmployeeStatus());
                    self.updateEmployee(save);
                    result.getUpdateNames().add(label);
                }
            } catch (ServiceException ex) {
                result.getFailureNames().put(label, ex.getMessage());
            } catch (RuntimeException ex) {
                log.warn("[importEmployeeList] 员工导入失败,行号={}", i + 2);
                result.getFailureNames().put(label, "保存或账号同步失败,请检查该行数据及账号配置");
            }
        }
        if (!hasData) throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY);
        return result;
    }
    /**
     * 解析员工初始密码:
     * 1. 导入/新增时显式指定了密码,优先使用;
     * 2. 未指定则读取系统参数 {@code system.user.init-password};
     * 3. 两者都没有时明确报错,避免悄悄用手机号等弱口令兜底。
     */
    private String resolveInitialPassword(String specifiedPassword) {
        if (StrUtil.isNotBlank(specifiedPassword)) {
            return specifiedPassword;
        }
        String initPassword = configApi.getConfigValueByKey(USER_INIT_PASSWORD_KEY);
        if (StrUtil.isBlank(initPassword)) {
            throw exception(EMPLOYEE_INIT_PASSWORD_NOT_CONFIGURED);
        }
        return initPassword;
    }
    private HrmEmployeeSaveReqVO buildImportSaveReqVO(HrmEmployeeImportExcelVO importEmployee,
                                                      Long deptId, Long postId, Set<Long> roleIds) {
        if (StrUtil.isBlank(importEmployee.getName())) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "员工姓名");
        }
        if (StrUtil.isBlank(importEmployee.getPhone())) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "手机号");
        }
        if (importEmployee.getHireDate() == null) {
            throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "入职日期");
        }
        HrmEmployeeSaveReqVO reqVO = new HrmEmployeeSaveReqVO();
        reqVO.setName(importEmployee.getName());
        reqVO.setGender(importEmployee.getGender() != null ? importEmployee.getGender() : 1); // 默认男
        reqVO.setPhone(importEmployee.getPhone());
        reqVO.setPassword(importEmployee.getPassword());
        reqVO.setDeptId(deptId);
        reqVO.setPostId(postId);
        reqVO.setRoleIds(CollUtil.isEmpty(roleIds) ? null : new ArrayList<>(roleIds));
        reqVO.setHireDate(importEmployee.getHireDate());
        reqVO.setEmployeeStatus(importEmployee.getEmployeeStatus() != null
                ? importEmployee.getEmployeeStatus() : 1); // 默认在职
        reqVO.setEmail(importEmployee.getEmail());
        reqVO.setBirthday(importEmployee.getBirthday());
        reqVO.setIdCard(importEmployee.getIdCard());
        reqVO.setNation(importEmployee.getNation());
        reqVO.setMarriageStatus(importEmployee.getMarriageStatus());
        reqVO.setAge(importEmployee.getAge());
        reqVO.setEducation(importEmployee.getEducation());
        reqVO.setPoliticalStatus(importEmployee.getPoliticalStatus());
        reqVO.setRemark(importEmployee.getRemark());
        return reqVO;
    }
}