1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
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;
    }
 
    /**
     * 统一手机号唯一性校验
     *
     * <p>背景:hrm_employee.uk_phone 是建在 phone 列上的唯一索引(不含 deleted),
     * 因此「已软删除但手机号未清空」的历史档案会长期占着这个手机号。
     * 此时 MyBatis-Plus 的 selectByPhone 因逻辑删除过滤查不到它,预校验通过,
     * 但写库时数据库仍会抛 DuplicateKeyException,接口表现为 500「系统异常」。</p>
     *
     * <p>处理策略(不改库结构):
     * 1. 若手机号被「已软删除」的历史记录占用 → 自动清空该历史记录的手机号,释放唯一索引;
     * 2. 若被「未删除」的其他员工占用 → 抛业务异常「手机号已存在」,不再返回 500。</p>
     *
     * @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<HrmEmployeeDO>()
                    .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<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) {
        // 校验存在
        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<HrmEmployeeDO>()
                .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<Long, PostRespDTO> 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<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;
    }
 
    @Override
    public HrmEmployeeDO validateEmployee(Long id) {
        HrmEmployeeDO employee = employeeMapper.selectById(id);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS);
        }
        return employee;
    }
 
    @Override
    public PageResult<HrmEmployeeDO> getEmployeePage(HrmEmployeePageReqVO pageReqVO) {
        return employeeMapper.selectPage(pageReqVO);
    }
 
    @Override
    public List<HrmEmployeeDO> getEmployeeList(Collection<Long> ids) {
        return employeeMapper.selectByIds(ids);
    }
 
    @Override
    public List<HrmEmployeeSimpleRespVO> getEmployeeSimpleList(Integer employeeStatus) {
        List<HrmEmployeeDO> list = employeeMapper.selectListByStatus(employeeStatus);
        // 填充部门名称
        Set<Long> deptIds = list.stream()
                .map(HrmEmployeeDO::getDeptId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, DeptRespDTO> 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<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);
                // 导入前先释放被软删除历史记录占用的手机号,避免整行导入因唯一索引冲突失败
                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<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;
    }
 
 
}