package cn.iocoder.yudao.module.hrm.service.employee;
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;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSimpleRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeWorkHistorySaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEducationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEmergencyContactDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeWorkHistoryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEducationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEmergencyContactMapper;
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;
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
import cn.iocoder.yudao.module.system.api.permission.RoleApi;
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 com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
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;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
* 员工信息 Service 实现类
*
* @author 超级管理员
*/
@Service
@Validated
@Slf4j
public class HrmEmployeeServiceImpl implements HrmEmployeeService {
@Resource
private HrmEmployeeMapper employeeMapper;
@Resource
private HrmEmployeeEducationMapper educationMapper;
@Resource
private HrmEmployeeWorkHistoryMapper workHistoryMapper;
@Resource
private HrmEmployeeEmergencyContactMapper emergencyContactMapper;
@Resource
private HrmNoRedisDAO noRedisDAO;
@Resource
private DeptApi deptApi;
@Resource
private PostApi postApi;
@Resource
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)
public Long createEmployee(HrmEmployeeSaveReqVO createReqVO) {
// 1. 校验部门
deptApi.validateDeptList(Collections.singleton(createReqVO.getDeptId()));
// 2. 校验岗位
postApi.validPostList(Collections.singleton(createReqVO.getPostId()));
// 3. 校验手机号是否已存在(并释放软删除历史记录占用的手机号)
validatePhoneUniqueAndReleaseStale(null, createReqVO.getPhone());
// 4. 同步创建系统用户
AdminUserCreateReqDTO userCreateReqDTO = new AdminUserCreateReqDTO();
userCreateReqDTO.setUsername(createReqVO.getPhone());
userCreateReqDTO.setNickname(createReqVO.getName());
userCreateReqDTO.setDeptId(createReqVO.getDeptId());
userCreateReqDTO.setPostIds(Collections.singleton(createReqVO.getPostId()));
userCreateReqDTO.setMobile(createReqVO.getPhone());
// 初始密码:优先用导入/新增时指定的密码,否则读取系统参数;两者都没有则报错
userCreateReqDTO.setPassword(resolveInitialPassword(createReqVO.getPassword()));
userCreateReqDTO.setSex(createReqVO.getGender());
userCreateReqDTO.setEmail(createReqVO.getEmail());
userCreateReqDTO.setRemark(createReqVO.getRemark());
// 设置角色
if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) {
userCreateReqDTO.setRoleIds(createReqVO.getRoleIds().stream().collect(Collectors.toSet()));
}
Long userId = adminUserApi.createUser(userCreateReqDTO);
// 5. 生成员工编号
String employeeNo = generateUniqueEmployeeNo();
// 6. 插入员工记录
HrmEmployeeDO employee = BeanUtils.toBean(createReqVO, HrmEmployeeDO.class);
employee.setEmployeeNo(employeeNo);
employee.setUserId(userId); // 关联系统用户ID
// roleIds 存储为 JSON
if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) {
employee.setRoleIds(JSONUtil.toJsonStr(createReqVO.getRoleIds()));
}
employeeMapper.insert(employee);
Long employeeId = employee.getId();
// 7. 同步添加教育经历
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.getWorkHistories())) {
createReqVO.getWorkHistories().forEach(workHistoryVO -> {
HrmEmployeeWorkHistoryDO workHistory = BeanUtils.toBean(workHistoryVO, HrmEmployeeWorkHistoryDO.class);
workHistory.setEmployeeId(employeeId);
workHistoryMapper.insert(workHistory);
});
}
// 9. 同步添加紧急联系人
if (CollUtil.isNotEmpty(createReqVO.getEmergencyContacts())) {
createReqVO.getEmergencyContacts().forEach(emergencyContactVO -> {
HrmEmployeeEmergencyContactDO emergencyContact = BeanUtils.toBean(emergencyContactVO, HrmEmployeeEmergencyContactDO.class);
emergencyContact.setEmployeeId(employeeId);
emergencyContactMapper.insert(emergencyContact);
});
}
return employeeId;
}
/**
* 统一手机号唯一性校验
*
*
背景:hrm_employee.uk_phone 是建在 phone 列上的唯一索引(不含 deleted),
* 因此「已软删除但手机号未清空」的历史档案会长期占着这个手机号。
* 此时 MyBatis-Plus 的 selectByPhone 因逻辑删除过滤查不到它,预校验通过,
* 但写库时数据库仍会抛 DuplicateKeyException,接口表现为 500「系统异常」。
*
* 处理策略(不改库结构):
* 1. 若手机号被「已软删除」的历史记录占用 → 自动清空该历史记录的手机号,释放唯一索引;
* 2. 若被「未删除」的其他员工占用 → 抛业务异常「手机号已存在」,不再返回 500。
*
* @param selfId 当前正在新增/修改的员工编号(新增传 null);命中自身时跳过
* @param phone 待校验的手机号
*/
private void validatePhoneUniqueAndReleaseStale(Long selfId, String phone) {
if (StrUtil.isBlank(phone)) {
return;
}
// 1. 释放被软删除历史记录占用的手机号
HrmEmployeeDO staleHolder = employeeMapper.selectByPhoneIncludeDeleted(phone);
if (staleHolder != null && !Objects.equals(staleHolder.getId(), selfId)
&& Boolean.TRUE.equals(staleHolder.getDeleted())) {
employeeMapper.update(null, new LambdaUpdateWrapper()
.eq(HrmEmployeeDO::getId, staleHolder.getId())
.set(HrmEmployeeDO::getPhone, null));
log.info("[validatePhoneUniqueAndReleaseStale][释放历史软删除员工({})占用的手机号]",
staleHolder.getId());
}
// 2. 校验是否仍被活跃员工占用
HrmEmployeeDO exist = employeeMapper.selectByPhone(phone);
if (exist != null && !Objects.equals(exist.getId(), selfId)) {
throw exception(EMPLOYEE_PHONE_EXISTS);
}
}
private String generateUniqueEmployeeNo() {
String employeeNo = noRedisDAO.generateEmployeeNo();
if (employeeMapper.selectByEmployeeNo(employeeNo) != null) {
return generateUniqueEmployeeNo();
}
return employeeNo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateEmployee(HrmEmployeeSaveReqVO updateReqVO) {
// 校验存在
HrmEmployeeDO employee = validateEmployeeExists(updateReqVO.getId());
// 校验部门
deptApi.validateDeptList(Collections.singleton(updateReqVO.getDeptId()));
// 校验岗位
postApi.validPostList(Collections.singleton(updateReqVO.getPostId()));
// 校验手机号是否已存在(如果手机号有变更;并释放软删除历史记录占用的手机号)
if (!Objects.equals(employee.getPhone(), updateReqVO.getPhone())) {
validatePhoneUniqueAndReleaseStale(updateReqVO.getId(), updateReqVO.getPhone());
}
// 更新员工主表
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 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 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 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) {
// 校验存在
HrmEmployeeDO employee = validateEmployeeExists(id);
// 1. 联动删除系统账号(不发布事件,避免回环)
deleteLinkedSystemUser(employee);
// 2. 级联删除子表
educationMapper.deleteByEmployeeId(id);
workHistoryMapper.deleteByEmployeeId(id);
emergencyContactMapper.deleteByEmployeeId(id);
// 3. 软删除前释放唯一约束字段,避免历史数据长期占用 uk_phone / uk_user_id,
// 导致后续新档案使用同一手机号或绑定同一账号时撞数据库唯一索引(表现为 500)
// 注:employee_no 为 NOT NULL 且 uk_employee_no 未含 deleted,这里不置空,
// 但把 user_id 置空可释放 uk_user_id;手机号置空可释放 uk_phone。
employeeMapper.update(null, new LambdaUpdateWrapper()
.eq(HrmEmployeeDO::getId, id)
.set(HrmEmployeeDO::getPhone, null)
.set(HrmEmployeeDO::getUserId, null));
// 4. 删除员工
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) {
HrmEmployeeDO employee = employeeMapper.selectById(id);
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
return employee;
}
@Override
public HrmEmployeeDO getEmployee(Long id) {
return employeeMapper.selectById(id);
}
@Override
public HrmEmployeeRespVO getEmployeeDetail(Long id) {
HrmEmployeeDO employee = getEmployee(id);
if (employee == null) {
return null;
}
HrmEmployeeRespVO respVO = BeanUtils.toBean(employee, HrmEmployeeRespVO.class);
// 解析 roleIds JSON
if (StrUtil.isNotBlank(employee.getRoleIds())) {
respVO.setRoleIds(JSONUtil.toList(employee.getRoleIds(), Long.class));
}
// 填充部门名称
if (employee.getDeptId() != null) {
DeptRespDTO dept = deptApi.getDept(employee.getDeptId());
if (dept != null) {
respVO.setDeptName(dept.getName());
}
}
// 填充岗位名称
if (employee.getPostId() != null) {
Map postMap = postApi.getPostMap(Collections.singleton(employee.getPostId()));
PostRespDTO post = postMap.get(employee.getPostId());
if (post != null) {
respVO.setPostName(post.getName());
}
}
// 填充角色名称
if (CollUtil.isNotEmpty(respVO.getRoleIds())) {
List 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 teams = mesCalTeamApi.getTeamList(Collections.singleton(employee.getTeamId()));
if (CollUtil.isNotEmpty(teams)) {
respVO.setTeamName(teams.get(0).getName());
}
}
return respVO;
}
@Override
public HrmEmployeeDO validateEmployee(Long id) {
HrmEmployeeDO employee = employeeMapper.selectById(id);
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
return employee;
}
@Override
public PageResult getEmployeePage(HrmEmployeePageReqVO pageReqVO) {
return employeeMapper.selectPage(pageReqVO);
}
@Override
public List getEmployeeList(Collection ids) {
return employeeMapper.selectByIds(ids);
}
@Override
public List getEmployeeSimpleList(Integer employeeStatus) {
List list = employeeMapper.selectListByStatus(employeeStatus);
// 填充部门名称
Set deptIds = list.stream()
.map(HrmEmployeeDO::getDeptId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map deptMap = deptApi.getDeptMap(deptIds);
return list.stream().map(emp -> {
HrmEmployeeSimpleRespVO vo = new HrmEmployeeSimpleRespVO();
vo.setId(emp.getId());
vo.setEmployeeNo(emp.getEmployeeNo());
vo.setName(emp.getName());
vo.setEmployeeStatus(emp.getEmployeeStatus());
if (emp.getDeptId() != null && deptMap.containsKey(emp.getDeptId())) {
vo.setDeptName(deptMap.get(emp.getDeptId()).getName());
}
return vo;
}).collect(Collectors.toList());
}
@Override
public HrmEmployeeDO getEmployeeByUserId(Long userId) {
return employeeMapper.selectByUserId(userId);
}
@Override
public HrmEmployeeDO getEmployeeByName(String name) {
return employeeMapper.selectByName(name);
}
@Override
public List getEmployeeListByName(String name) {
return employeeMapper.selectListByName(name);
}
@Override
public HrmEmployeeImportRespVO importEmployeeList(List rows, HrmEmployeeImportReqVO req) {
if (CollUtil.isEmpty(rows)) throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY);
Map> depts = deptApi.getDeptList().stream()
.filter(v -> StrUtil.isNotBlank(v.getName())).collect(Collectors.groupingBy(v -> v.getName().trim()));
Map> posts = postApi.getPostList().stream()
.filter(v -> StrUtil.isNotBlank(v.getName())).collect(Collectors.groupingBy(v -> v.getName().trim()));
Map> 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 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 deptMatches = depts.get(row.getDeptName().trim());
List 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 roleIds = new LinkedHashSet<>();
if (StrUtil.isNotBlank(row.getRoleNames())) {
for (String value : row.getRoleNames().split("[,,]")) {
String name = value.trim();
if (name.isEmpty()) continue;
List 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);
// 导入前先释放被软删除历史记录占用的手机号,避免整行导入因唯一索引冲突失败
validatePhoneUniqueAndReleaseStale(null, row.getPhone());
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 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;
}
}