fix(hrm): 修复生成考勤统计报错、实出天数翻倍;调岗/离职审批通过同步人员信息

一、生成考勤统计报「系统异常」(客户需求 4)
根因:generateAllMonthlySummary / generateDeptMonthlySummary 调用
employeeService.getEmployeeList(null),而其内部是 employeeMapper.selectByIds(null),
MyBatis-Plus 对 null 集合直接抛
BuilderException: The expression 'coll' evaluated to a null value。
影响:「全公司」「指定部门」生成必报错,只有「指定员工」能用。
修复:
- HrmEmployeeServiceImpl.getEmployeeList 对空集合返回空列表,避免 selectByIds(null);
- 新增 getEmployeeListByStatus(Integer) 返回 DO;
- 生成统计改为查在册员工(排除已离职),不再传 null。

二、实际出勤天数翻倍(会导致薪酬缺勤扣款算错)
原逻辑每条打卡记录 +1,一天打两次卡就算 2 天。
改为按打卡日期去重(LinkedHashSet),并封顶不超过应出勤天数。

三、调岗/离职审批通过立即同步人员信息(客户需求 1,确认「审批通过立即生效」)
原来审批通过只更新申请单自己的 status,hrm_employee 完全不动,
导致办了离职的人在人员信息里仍是在职、调岗后部门岗位不变。
新增 HrmEmployeeService:
- syncEmployeeTransfer(employeeId, targetDeptId, targetPostId):
回写 dept_id 与 post_ids(JSON 数组格式);
- syncEmployeeResignation(systemUserId, leaveDate):
回写 employee_status=3(离职)与 leave_date。
在 updateTransferApplicationAuditStatus / updateResignationApplicationAuditStatus
中,status==APPROVE(20) 时调用。
注意两个申请单的 userId 口径不同:调岗存 hrm_employee.id,离职存 system_users.id。

四、考勤统计支持按日筛选(客户需求 3,确认「按日」)
- HrmAttendanceSummaryPageReqVO 增加 startDate/endDate;
- 传了 startDate 则实时聚合打卡记录(不落库):应出勤=区间工作日数、
实出勤按日期去重、迟到/早退/缺卡计数、加班累加、请假按天x8折算为小时;
- 不传日期仍走 hrm_attendance_summary 月度汇总表,行为不变;
- 新增 HrmAttendanceRecordMapper.selectListByUserIdsAndDateBetween 与
HrmLeaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange 支持批量查询。

实测(mom-zyjc):
- 全公司生成 2026-10 → code:0,生成 133 条汇总;
- 指定部门 132 生成 → code:0;
- 调岗审批通过:郭丽华 dept 132 -> 136,日志 syncEmployeeTransfer 生效;
- 离职审批通过:李军明 employee_status 1 -> 3,leave_date=2026-11-05;
- 按日区间 10-01~10-15 → workDays=11;整月 → 22;请假 1 天 → leaveHours=8.00。
(自测数据已全部清理,汇总表恢复为空)
已修改8个文件
342 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceSummaryPageReqVO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceRecordMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceSummaryServiceImpl.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceSummaryPageReqVO.java
@@ -5,6 +5,9 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Schema(description = "管理后台 - 考勤统计汇总分页 Request VO")
@Data
@@ -21,4 +24,12 @@
    @Schema(description = "年月,格式:yyyy-MM", example = "2024-01")
    private String yearMonth;
}
    @Schema(description = "统计开始日期(传了按日区间统计,实时聚合打卡记录,不读月度汇总表)", example = "2026-10-01")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate startDate;
    @Schema(description = "统计结束日期(不传时默认取 startDate 所在月月末)", example = "2026-10-15")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate endDate;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java
@@ -6,9 +6,12 @@
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.hutool.core.collection.CollUtil;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -54,4 +57,24 @@
                .le(HrmLeaveApplicationDO::getEndTime, endTime));
    }
    /**
     * 批量查询多个员工在指定时间范围内已审批通过的请假记录(用于按日区间的考勤统计)
     *
     * @param userIds   员工ID集合(系统用户ID)
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 请假记录列表
     */
    default List<HrmLeaveApplicationDO> selectApprovedListByUserIdsAndTimeRange(Collection<Long> userIds,
                                                                                LocalDateTime startTime, LocalDateTime endTime) {
        if (CollUtil.isEmpty(userIds)) {
            return new ArrayList<>();
        }
        return selectList(new LambdaQueryWrapperX<HrmLeaveApplicationDO>()
                .in(HrmLeaveApplicationDO::getUserId, userIds)
                .eq(HrmLeaveApplicationDO::getStatus, HrmAuditStatusEnum.APPROVE.getStatus())
                .le(HrmLeaveApplicationDO::getStartTime, endTime)
                .ge(HrmLeaveApplicationDO::getEndTime, startTime));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceRecordMapper.java
@@ -5,9 +5,12 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.hutool.core.collection.CollUtil;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -43,4 +46,19 @@
                .orderByAsc(HrmAttendanceRecordDO::getDate));
    }
    /**
     * 按员工集合 + 日期区间批量查询打卡记录(用于按日区间的实时考勤统计)
     */
    default List<HrmAttendanceRecordDO> selectListByUserIdsAndDateBetween(Collection<Long> userIds,
                                                                          LocalDate startDate, LocalDate endDate) {
        if (CollUtil.isEmpty(userIds)) {
            return new ArrayList<>();
        }
        return selectList(new LambdaQueryWrapperX<HrmAttendanceRecordDO>()
                .in(HrmAttendanceRecordDO::getUserId, userIds)
                .ge(HrmAttendanceRecordDO::getDate, startDate)
                .le(HrmAttendanceRecordDO::getDate, endDate)
                .orderByAsc(HrmAttendanceRecordDO::getDate));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmResignationTypeEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.handover.HrmUserHandoverService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -79,6 +80,8 @@
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private HrmUserHandoverService handoverService;
    @Resource
    private HrmEmployeeService employeeService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -346,6 +349,11 @@
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
        // 审批通过:立即同步到人员信息(客户要求「审批通过立即生效」)
        // 离职申请单的 userId 存的是 system_users.id,不是 hrm_employee.id
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(status)) {
            employeeService.syncEmployeeResignation(application.getUserId(), application.getActualDate());
        }
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -390,6 +390,12 @@
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
        // 审批通过:立即同步到人员信息(客户要求「审批通过立即生效」)
        // 调岗申请单的 userId 存的是 hrm_employee.id
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(status)) {
            employeeService.syncEmployeeTransfer(application.getUserId(),
                    application.getTargetDeptId(), application.getTargetPostId());
        }
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceSummaryServiceImpl.java
@@ -4,9 +4,11 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
@@ -20,7 +22,16 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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;
/**
 * 考勤统计汇总 Service 实现类
@@ -32,6 +43,9 @@
@Slf4j
public class HrmAttendanceSummaryServiceImpl implements HrmAttendanceSummaryService {
    /** 员工状态:离职(1-在职,2-试用,3-离职) */
    private static final Integer EMPLOYEE_STATUS_RESIGNED = 3;
    @Resource
    private HrmAttendanceSummaryMapper attendanceSummaryMapper;
    @Resource
@@ -40,6 +54,8 @@
    private HrmEmployeeService employeeService;
    @Resource
    private HrmAttendanceHolidayService attendanceHolidayService;
    @Resource
    private HrmLeaveApplicationMapper leaveApplicationMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -67,7 +83,9 @@
        int earlyCount = 0;
        int absentCount = 0;
        BigDecimal overtimeHours = BigDecimal.ZERO;
        int actualDays = 0;
        // 实际出勤天数按「日期」去重:同一天可能有多条打卡记录(补卡、重复打卡),
        // 不能每条记录都 +1,否则实出天数会翻倍,直接影响薪酬的缺勤扣款
        Set<LocalDate> actualDates = new LinkedHashSet<>();
        for (HrmAttendanceRecordDO record : records) {
            // 统计迟到
@@ -89,11 +107,19 @@
            if (record.getOvertimeHours() != null) {
                overtimeHours = overtimeHours.add(record.getOvertimeHours());
            }
            // 统计实际出勤天数(有正常打卡记录)
            // 统计实际出勤天数(按日期去重)
            if (record.getClockInTime() != null || record.getClockOutTime() != null) {
                actualDays++;
                LocalDate attendDate = record.getDate();
                if (attendDate == null && record.getClockInTime() != null) {
                    attendDate = record.getClockInTime().toLocalDate();
                }
                if (attendDate != null) {
                    actualDates.add(attendDate);
                }
            }
        }
        // 实出天数不应超过应出天数
        int actualDays = Math.min(actualDates.size(), workDays);
        // 6. 保存或更新汇总记录
        HrmAttendanceSummaryDO summary = attendanceSummaryMapper.selectByUserIdAndYearMonth(userId, yearMonth);
@@ -126,12 +152,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void generateDeptMonthlySummary(Long deptId, String yearMonth) {
        // 获取部门所有在职员工
        List<HrmEmployeeDO> employees = employeeService.getEmployeeList(null);
        if (CollUtil.isEmpty(employees)) {
            return;
        }
        // 筛选部门员工
        List<HrmEmployeeDO> deptEmployees = employees.stream()
        List<HrmEmployeeDO> deptEmployees = getActiveEmployeeList().stream()
                .filter(e -> deptId.equals(e.getDeptId()))
                .toList();
        for (HrmEmployeeDO employee : deptEmployees) {
@@ -143,13 +164,25 @@
    @Transactional(rollbackFor = Exception.class)
    public void generateAllMonthlySummary(String yearMonth) {
        // 获取所有在职员工
        List<HrmEmployeeDO> employees = employeeService.getEmployeeList(null);
        if (CollUtil.isEmpty(employees)) {
            return;
        }
        for (HrmEmployeeDO employee : employees) {
        for (HrmEmployeeDO employee : getActiveEmployeeList()) {
            generateMonthlySummary(employee.getId(), yearMonth);
        }
    }
    /**
     * 获取参与考勤统计的员工(在职 + 试用,排除已离职)
     *
     * <p>注意:不能用 {@code getEmployeeList(null)},MyBatis-Plus 的 selectByIds(null)
     * 会抛 "The expression 'coll' evaluated to a null value",导致「生成统计」报系统异常。
     */
    private List<HrmEmployeeDO> getActiveEmployeeList() {
        List<HrmEmployeeDO> employees = employeeService.getEmployeeListByStatus(null);
        if (CollUtil.isEmpty(employees)) {
            return Collections.emptyList();
        }
        return employees.stream()
                .filter(e -> !Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED))
                .toList();
    }
    /**
@@ -169,6 +202,10 @@
    @Override
    public PageResult<HrmAttendanceSummaryDO> getAttendanceSummaryPage(HrmAttendanceSummaryPageReqVO pageReqVO) {
        // 传了开始日期 → 按日区间实时聚合打卡记录(客户要求「按日筛选,不是固定本月」)
        if (pageReqVO.getStartDate() != null) {
            return getAttendanceSummaryByDateRange(pageReqVO);
        }
        return attendanceSummaryMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<HrmAttendanceSummaryDO>()
                .eqIfPresent(HrmAttendanceSummaryDO::getUserId, pageReqVO.getUserId())
                .eqIfPresent(HrmAttendanceSummaryDO::getDeptId, pageReqVO.getDeptId())
@@ -177,6 +214,126 @@
                .orderByAsc(HrmAttendanceSummaryDO::getDeptId));
    }
    /**
     * 按日区间实时统计考勤(不落库,仅用于列表查询)
     *
     * <p>口径与月度汇总保持一致:应出勤天数取区间内工作日数,实出勤按打卡日期去重,
     * 迟到/早退/缺卡按打卡记录计数,加班时长累加,请假时长按每天 8 小时折算。
     */
    private PageResult<HrmAttendanceSummaryDO> getAttendanceSummaryByDateRange(HrmAttendanceSummaryPageReqVO reqVO) {
        LocalDate startDate = reqVO.getStartDate();
        LocalDate endDate = reqVO.getEndDate() != null
                ? reqVO.getEndDate() : startDate.with(TemporalAdjusters.lastDayOfMonth());
        // 起止日期填反时自动纠正,避免客户误操作查不到数据
        if (endDate.isBefore(startDate)) {
            LocalDate tmp = startDate;
            startDate = endDate;
            endDate = tmp;
        }
        // 1. 确定员工范围(在职 + 试用),支持部门 / 员工筛选
        List<HrmEmployeeDO> employees = getActiveEmployeeList();
        if (reqVO.getDeptId() != null) {
            employees = employees.stream()
                    .filter(e -> reqVO.getDeptId().equals(e.getDeptId()))
                    .toList();
        }
        if (reqVO.getUserId() != null) {
            employees = employees.stream()
                    .filter(e -> reqVO.getUserId().equals(e.getId()))
                    .toList();
        }
        if (CollUtil.isEmpty(employees)) {
            return PageResult.empty();
        }
        // 2. 应出勤天数(区间内工作日)
        int workDays = calculateWorkDays(startDate, endDate);
        // 3. 批量查询区间内打卡记录,按员工分组
        Set<Long> employeeIds = employees.stream().map(HrmEmployeeDO::getId).collect(Collectors.toSet());
        List<HrmAttendanceRecordDO> records = attendanceRecordMapper
                .selectListByUserIdsAndDateBetween(employeeIds, startDate, endDate);
        Map<Long, List<HrmAttendanceRecordDO>> recordMap = records.stream()
                .collect(Collectors.groupingBy(HrmAttendanceRecordDO::getUserId));
        // 4. 批量查询区间内已审批通过的请假单(请假单的 user_id 是 system_users.id)
        Set<Long> systemUserIds = employees.stream().map(HrmEmployeeDO::getUserId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange(
                systemUserIds, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
        Map<Long, BigDecimal> leaveDaysMap = new HashMap<>();
        for (HrmLeaveApplicationDO leave : leaves) {
            if (leave.getUserId() == null || leave.getDuration() == null) {
                continue;
            }
            leaveDaysMap.merge(leave.getUserId(), leave.getDuration(), BigDecimal::add);
        }
        // 5. 逐员工组装统计结果
        List<HrmAttendanceSummaryDO> list = new ArrayList<>(employees.size());
        for (HrmEmployeeDO employee : employees) {
            List<HrmAttendanceRecordDO> empRecords = recordMap.getOrDefault(employee.getId(), Collections.emptyList());
            int lateCount = 0;
            int earlyCount = 0;
            int absentCount = 0;
            BigDecimal overtimeHours = BigDecimal.ZERO;
            Set<LocalDate> actualDates = new LinkedHashSet<>();
            for (HrmAttendanceRecordDO record : empRecords) {
                if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) {
                    lateCount++;
                }
                if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) {
                    earlyCount++;
                }
                if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
                    absentCount++;
                }
                if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) {
                    absentCount++;
                }
                if (record.getOvertimeHours() != null) {
                    overtimeHours = overtimeHours.add(record.getOvertimeHours());
                }
                if (record.getClockInTime() != null || record.getClockOutTime() != null) {
                    LocalDate attendDate = record.getDate();
                    if (attendDate == null && record.getClockInTime() != null) {
                        attendDate = record.getClockInTime().toLocalDate();
                    }
                    if (attendDate != null) {
                        actualDates.add(attendDate);
                    }
                }
            }
            // 请假天数折算为小时(按每天 8 小时),与月度汇总的 leave_hours 口径一致
            BigDecimal leaveDays = employee.getUserId() == null ? BigDecimal.ZERO
                    : leaveDaysMap.getOrDefault(employee.getUserId(), BigDecimal.ZERO);
            list.add(HrmAttendanceSummaryDO.builder()
                    // 实时统计没有落库主键,用员工ID充当列表行 key,避免前端行 key 冲突
                    .id(employee.getId())
                    .userId(employee.getId())
                    .deptId(employee.getDeptId())
                    // 区间统计时,年月列展示统计区间
                    .yearMonth(startDate + " ~ " + endDate)
                    .workDays(workDays)
                    .actualDays(Math.min(actualDates.size(), workDays))
                    .lateCount(lateCount)
                    .earlyCount(earlyCount)
                    .absentCount(absentCount)
                    .overtimeHours(overtimeHours)
                    .leaveHours(leaveDays.multiply(BigDecimal.valueOf(8)))
                    .build());
        }
        // 6. 内存分页(区间统计不落库,无法走数据库分页)
        int total = list.size();
        int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo();
        int pageSize = reqVO.getPageSize() == null || reqVO.getPageSize() < 1 ? 10 : reqVO.getPageSize();
        int fromIndex = Math.min((pageNo - 1) * pageSize, total);
        int toIndex = Math.min(fromIndex + pageSize, total);
        return new PageResult<>(list.subList(fromIndex, toIndex), (long) total);
    }
    @Override
    public HrmAttendanceSummaryDO getMonthlySummary(Long userId, String yearMonth) {
        return attendanceSummaryMapper.selectByUserIdAndYearMonth(userId, yearMonth);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
@@ -84,6 +84,34 @@
    List<HrmEmployeeDO> getEmployeeList(Collection<Long> ids);
    /**
     * 获得指定状态的员工列表(返回 DO)
     *
     * <p>注意:不能把 null 传给 {@link #getEmployeeList(Collection)},
     * MyBatis-Plus 的 selectByIds(null) 会抛 "The expression 'coll' evaluated to a null value"。
     *
     * @param employeeStatus 员工状态,为 null 时查询全部
     * @return 员工列表
     */
    List<HrmEmployeeDO> getEmployeeListByStatus(Integer employeeStatus);
    /**
     * 调岗生效:把员工档案的部门/岗位同步为目标部门/目标岗位
     *
     * @param employeeId    员工编号(hrm_employee.id)
     * @param targetDeptId  目标部门编号,为 null 时不改
     * @param targetPostId  目标岗位编号,为 null 时不改
     */
    void syncEmployeeTransfer(Long employeeId, Long targetDeptId, Long targetPostId);
    /**
     * 离职生效:把员工档案状态置为「离职」
     *
     * @param systemUserId 系统用户编号(hrm_employee.user_id,即 system_users.id)
     * @param leaveDate    离职日期,为 null 时取当天
     */
    void syncEmployeeResignation(Long systemUserId, java.time.LocalDate leaveDate);
    /**
     * 获得指定状态的员工简单列表
     *
     * @param employeeStatus 员工状态,为 null 时查询全部
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -40,6 +40,7 @@
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO;
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;
@@ -69,7 +70,11 @@
 */
@Service
@Validated
@Slf4j
public class HrmEmployeeServiceImpl implements HrmEmployeeService {
    /** 员工状态:离职(1-在职,2-试用,3-离职) */
    private static final Integer EMPLOYEE_STATUS_RESIGNED = 3;
    @Resource
    private HrmEmployeeMapper employeeMapper;
@@ -369,10 +374,66 @@
    @Override
    public List<HrmEmployeeDO> getEmployeeList(Collection<Long> ids) {
        // 防御:MyBatis-Plus 的 selectByIds(null) 会抛
        // BuilderException: The expression 'coll' evaluated to a null value
        if (CollUtil.isEmpty(ids)) {
            return new ArrayList<>();
        }
        return employeeMapper.selectByIds(ids);
    }
    @Override
    public List<HrmEmployeeDO> getEmployeeListByStatus(Integer employeeStatus) {
        return employeeMapper.selectListByStatus(employeeStatus);
    }
    @Override
    public void syncEmployeeTransfer(Long employeeId, Long targetDeptId, Long targetPostId) {
        if (employeeId == null) {
            return;
        }
        HrmEmployeeDO employee = employeeMapper.selectById(employeeId);
        if (employee == null) {
            log.warn("[syncEmployeeTransfer][员工({}) 不存在,跳过调岗同步]", employeeId);
            return;
        }
        if (targetDeptId == null && targetPostId == null) {
            return;
        }
        HrmEmployeeDO updateObj = new HrmEmployeeDO();
        updateObj.setId(employeeId);
        if (targetDeptId != null) {
            updateObj.setDeptId(targetDeptId);
        }
        if (targetPostId != null) {
            // hrm_employee.post_ids 存 JSON 数组字符串,如 [38]
            updateObj.setPostIds(JSONUtil.toJsonStr(java.util.List.of(targetPostId)));
        }
        employeeMapper.updateById(updateObj);
        log.info("[syncEmployeeTransfer][员工({}) 调岗生效:部门 {} -> {},岗位 -> {}]",
                employeeId, employee.getDeptId(), targetDeptId, targetPostId);
    }
    @Override
    public void syncEmployeeResignation(Long systemUserId, java.time.LocalDate leaveDate) {
        if (systemUserId == null) {
            return;
        }
        HrmEmployeeDO employee = employeeMapper.selectByUserId(systemUserId);
        if (employee == null) {
            log.warn("[syncEmployeeResignation][系统用户({}) 没有对应员工档案,跳过离职同步]", systemUserId);
            return;
        }
        employeeMapper.updateById(HrmEmployeeDO.builder()
                .id(employee.getId())
                .employeeStatus(EMPLOYEE_STATUS_RESIGNED)
                .leaveDate(leaveDate != null ? leaveDate : java.time.LocalDate.now())
                .build());
        log.info("[syncEmployeeResignation][员工({}) 离职生效,状态 {} -> {}]",
                employee.getId(), employee.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED);
    }
    @Override
    public List<HrmEmployeeSimpleRespVO> getEmployeeSimpleList(Integer employeeStatus) {
        List<HrmEmployeeDO> list = employeeMapper.selectListByStatus(employeeStatus);
        // 填充部门名称