# AttendanceEmployeeServiceImpl.java

    package cloud.wconnect.workperformance.service.sva.employees.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.exception.ListenerException;
    import cloud.wconnect.commonlib.exception.ParseReportException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.workperformance.entity.sva.employees.AttendanceEmployee;
    import cloud.wconnect.workperformance.entity.sva.setting.DepCodeToCategory;
    import cloud.wconnect.workperformance.mapper.sva.employees.AttendanceEmployeeMapper;
    import cloud.wconnect.workperformance.pojo.bo.sva.employees.EmployeesListBO;
    import cloud.wconnect.workperformance.pojo.dto.sva.employee.AttendanceEmployeeListDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.employee.UpdateShownDTO;
    import cloud.wconnect.workperformance.pojo.query.sva.employee.AttendanceEmployeeQuery;
    import cloud.wconnect.workperformance.service.sva.employees.IAttendanceEmployeeService;
    import cloud.wconnect.workperformance.service.sva.employees.ISvaEmployeesImportFilterConfigService;
    import cloud.wconnect.workperformance.service.sva.employees.employee.ISvaEmployeeParseService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepCodeToCategoryService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import jakarta.annotation.Resource;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.io.InputStream;
    import java.text.MessageFormat;
    import java.time.ZoneId;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    import static java.util.stream.Collectors.toList;
    import static java.util.stream.Collectors.toMap;

    /**
     * <p>
     * 服务实现类：考勤系统员工信息表
     * </p>
     *
     * @author zc
     * @since 2023-02-21
     */
    @Service
    @DS("work_performance")
    public class AttendanceEmployeeServiceImpl extends ServiceImpl<AttendanceEmployeeMapper, AttendanceEmployee> implements IAttendanceEmployeeService {
        @Lazy
        @Resource
        private IDepCodeToCategoryService depCodeToCategoryService;
        @Resource
        private ISvaEmployeesImportFilterConfigService filterConfigService;
        @Resource
        private AttendanceEmployeeMapper attendanceEmployeeMapper;
        @Resource
        private SqlSessionFactory sqlSessionFactory;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ApplicationContext applicationContext;

        @Value("${default.timezone}")
        private String defaultTimezone;

        /**
         * 根据检索条件查询考勤系统员工信息表信息
         *
         * @param query 检索条件
         * @return Employee集合
         * @author zc
         * @date 2023/2/21
         */
        @Override
        public ListPagedDTO<AttendanceEmployeeListDTO> list(AttendanceEmployeeQuery query) {
            Page<AttendanceEmployee> page = new Page<>(query.getPage(), query.getSize());
            List<AttendanceEmployee> attendanceEmployees = attendanceEmployeeMapper.listByQueryPaged(page, query);
            if (CollectionUtils.isEmpty(attendanceEmployees)) {
                return new ListPagedDTO<>();
            }

            return new ListPagedDTO<>(page.getTotal(), beanMapper.mapList(attendanceEmployees, AttendanceEmployeeListDTO.class));
        }

        /**
         * 修改 Employee 信息的可见性
         *
         * @param operatorId  操作人 id
         * @param updateShown 更新所需内容
         * @author zc
         * @date 2023/2/23
         */
        @Override
        public void updateVisibility(int operatorId, UpdateShownDTO updateShown) {
            AttendanceEmployee employee = new AttendanceEmployee();
            employee.setId(updateShown.getId());
            employee.setShown(updateShown.getShownValue());
            employee.setShownUpdatedBy(operatorId);
            employee.setShownUpdatedTime(DateTimeUtils.nowTimestamp());
            attendanceEmployeeMapper.updateById(employee);
        }

        /**
         * 根据部门 code 修改部门名称
         *
         * @param codeToCategoryList 部门 code 对应关系集合
         * @author yyt
         * @date 2025/3/26
         **/
        @Override
        public void updateDepartmentNameBatch(List<DepCodeToCategory> codeToCategoryList) {
            if (CollectionUtils.isEmpty(codeToCategoryList)) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            AttendanceEmployeeMapper mapper = sqlSession.getMapper(AttendanceEmployeeMapper.class);

            for (int i = 0; i < codeToCategoryList.size(); i++) {
                mapper.updateDepartmentName(codeToCategoryList.get(i));
                // 每 50 条提交一次
                if ((i + 1) % 50 == 0) {
                    sqlSession.flushStatements();
                }
            }
            sqlSession.flushStatements();
        }

        /**
         * 解析报告 - Employee Report
         *
         * @param propertyDTOList 资产集合
         * @param in              文件流
         * @param source          数据源
         * @return EmployeesListBO 集合
         * @author zc
         * @date 2024/8/17
         */
        @Override
        public List<EmployeesListBO> parseReport(List<PropertyDTO> propertyDTOList, InputStream in, String source) {
            Collection<ISvaEmployeeParseService> parserList = applicationContext.getBeansOfType(ISvaEmployeeParseService.class).values();
            for (ISvaEmployeeParseService parser : parserList) {
                if (parser.supports(source)) {
                    return parser.parse(propertyDTOList, in);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 处理 Employees 数据
         *
         * @param propertyList 影响的 property 范围
         * @param boList       文件数据 BO
         * @param operatorId   操作人 id（0：系统）
         * @author zc
         * @date 2023/2/24
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleEmployees(List<PropertyDTO> propertyList, List<EmployeesListBO> boList, int operatorId) throws ParseReportException {
            // 格式化 BO 数据
            List<AttendanceEmployee> attendanceEmployeeList = formatBoList(propertyList, boList);
            // 过滤数据
            filterConfigService.filterEmployees(attendanceEmployeeList);
            if (CollectionUtils.isEmpty(attendanceEmployeeList)) {
                return;
            }

            Map<Integer, List<AttendanceEmployee>> employeesByPropertyId = attendanceEmployeeList.stream()
                    .collect(Collectors.groupingBy(AttendanceEmployee::getPropertyId));

            List<Integer> propertyIds = Lists.newArrayList(employeesByPropertyId.keySet());

            // 保存导入的指定 property 下的 Employee 信息，并做 Department Category 的匹配处理
            employeesByPropertyId.forEach((propertyId, employees) ->
                    saveEmployeesWithDepCategoryMatch(propertyIds, propertyId, employees, operatorId));
        }

        /**
         * 格式化 BO 数据
         *
         * @param propertyList 资产
         * @param boList       报告数据 BO
         * @return AttendanceEmployee 集合
         * @author zc
         * @date 2023/10/23
         */
        private List<AttendanceEmployee> formatBoList(List<PropertyDTO> propertyList, List<EmployeesListBO> boList) {
            Map<String, PropertyDTO> abbr2Property = propertyList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getAbbr, v -> v));

            return beanMapper.mapList(boList, AttendanceEmployee.class,
                    (bo, item) -> {
                        PropertyDTO propertyDTO = abbr2Property.get(bo.getProperty());
                        item.setPropertyId(propertyDTO.getId());
                        item.setDepCategoryId(BaseConstant.ZERO);
                        item.setHireDate(DateTimeUtils.strToLocalDate(bo.getHireDay(), BaseConstant.DATE_FORMAT_YYYY_MM_DD)
                                .atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond());
                        item.setRehireDate(DateTimeUtils.strToLocalDate(bo.getRehireDay(), BaseConstant.DATE_FORMAT_YYYY_MM_DD)
                                .atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond());
                        item.setWorkCategoryCode(bo.getWorkerCategoryCode());
                        item.setShown(Boolean.TRUE);
                        String infoSign = DigestUtils.sha256Hex(StringUtils.join(item.getPropertyId(),
                                item.getPositionId(), item.getEmpName(), item.getDepartmentCode(),
                                item.getDepartmentDesc(), item.getHireDate(), item.getRehireDate(),
                                item.getPayClass(), item.getWorkCategoryCode(), item.getHourlyRate()));
                        item.setInfoSign(infoSign);
                    });
        }

        /**
         * 根据 positionId 获取员工数据
         *
         * @param propertyId  资产 id
         * @param positionIds position id 集合
         * @param shown       是否显示
         * @return AttendanceEmployee 集合
         * @author zc
         * @date 2023/6/24
         */
        @Override
        public List<AttendanceEmployee> listByPositionIds(int propertyId, List<String> positionIds, @Nullable Boolean shown) {
            if (CollectionUtils.isEmpty(positionIds)) {
                return Collections.emptyList();
            }
            return list(new QueryWrapper<AttendanceEmployee>().lambda()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .in(AttendanceEmployee::getPositionId, positionIds)
                    .eq(Objects.nonNull(shown), AttendanceEmployee::getShown, shown));
        }

        /**
         * 根据员工编号获取员工数据
         *
         * @param propertyId 资产 id
         * @param positionId 员工编号
         * @return AttendanceEmployee
         * @author zc
         * @date 2025/6/28
         */
        @Override
        public AttendanceEmployee getByPositionId(int propertyId, String positionId) {
            return getOne(new QueryWrapper<AttendanceEmployee>().lambda()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .eq(AttendanceEmployee::getPositionId, positionId));
        }

        /**
         * 获取指定日期范围内 没有考勤、未隐藏、未离职或在选择日期后离职的员工数据
         *
         * @param propertyId             资产 id
         * @param positionIds            position id 集合
         * @param fromDate               开始日期
         * @param excludeDepartmentCodes 需要排除的部门编码集合（可空）
         * @return AttendanceEmployee 集合
         * @author zc
         * @date 2023/6/24
         */
        @Override
        public List<AttendanceEmployee> listForNoAttendance(int propertyId, List<String> positionIds, long fromDate,
                                                            @Nullable List<String> excludeDepartmentCodes) {
            return list(new QueryWrapper<AttendanceEmployee>().lambda()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .eq(AttendanceEmployee::getShown, Boolean.TRUE)
                    .notIn(CollectionUtils.isNotEmpty(excludeDepartmentCodes), AttendanceEmployee::getDepartmentCode, excludeDepartmentCodes)
                    .notIn(CollectionUtils.isNotEmpty(positionIds), AttendanceEmployee::getPositionId, positionIds)
                    .and(query -> query
                            .eq(AttendanceEmployee::getTerminationDate, 0)
                            .or()
                            .gt(AttendanceEmployee::getTerminationDate, fromDate)
                    ));
        }

        /**
         * 保存导入的指定 property 下的EMP信息，并做 Department Category 的匹配处理
         *
         * @param propertyIdRange        报告文件中包含的指定范围内的 property id
         * @param propertyId             property id
         * @param newAttendanceEmployees 指定 property 下导入的EMP信息集合
         * @param operatorId             操作人的 id
         * @author zc
         * @date 2023/2/24
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveEmployeesWithDepCategoryMatch(List<Integer> propertyIdRange, int propertyId, List<AttendanceEmployee> newAttendanceEmployees, int operatorId) {
            if (CollectionUtils.isEmpty(newAttendanceEmployees)) {
                return;
            }
            // 获取 DB、报告文件中同时存在的员工信息
            Map<String, AttendanceEmployee> existedEmployeeByPositionId = getExistedEmployeeByPositionId(newAttendanceEmployees);

            // 累积需要匹配 Department Category 的 Department Code 和 Department Desc.
            HashMap<String, String> depCode2DescriptionToKeep = Maps.newHashMapWithExpectedSize(newAttendanceEmployees.size());
            newAttendanceEmployees.forEach(item -> depCode2DescriptionToKeep.put(item.getDepartmentCode(), item.getDepartmentDesc()));

            // 获取指定 Department Code 集合在指定 property 下对应的 Department Category Id
            Map<String, Integer> depCategoryIdMapByDepCode = depCodeToCategoryService.generateCategoryId4DepCodes(propertyId, depCode2DescriptionToKeep);

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            // 需要新增的员工数据
            List<AttendanceEmployee> employeesToAdd = getEmployeesToAdd(newAttendanceEmployees, existedEmployeeByPositionId, operatorId, nowTimestamp);
            // 匹配 Department Code 对应的 Category id
            employeesToAdd.forEach(attendanceEmployee -> attendanceEmployee.setDepCategoryId(depCategoryIdMapByDepCode.getOrDefault(attendanceEmployee.getDepartmentCode(), DepCodeToCategory.DEPARTMENT_ID_NONE)));
            // 需要更新的员工数据
            List<AttendanceEmployee> employeesToUpdate = getEmployeesToUpdate(newAttendanceEmployees, existedEmployeeByPositionId, depCategoryIdMapByDepCode, operatorId, nowTimestamp);

            // 把 DB 中有 且 报告中没有的员工更新离职日期为当前日
            Set<String> existedPositionIds = existedEmployeeByPositionId.keySet();
            if (!existedPositionIds.isEmpty()) {
                updateNotInReportEmployeeTermination(propertyId, existedPositionIds, operatorId, nowTimestamp);
            }

            // 批量保存新增的员工信息
            saveNewEmployeesBatch(employeesToAdd);
            // 批量更新已存在的员工信息
            updateEmployeesBatch(employeesToUpdate);
        }

        /**
         * 获取需要新增的员工数据
         *
         * @param employees                   员工数据
         * @param existedEmployeeByPositionId 已存在的员工信息
         * @param operatorId                  操作人 id
         * @param nowTimestamp                操作时间
         * @return 员工数据数组
         * @author zc
         * @date 2023/3/2
         */
        private List<AttendanceEmployee> getEmployeesToAdd(List<AttendanceEmployee> employees, Map<String, AttendanceEmployee> existedEmployeeByPositionId, int operatorId, long nowTimestamp) {
            List<AttendanceEmployee> result = Lists.newArrayList();
            employees.forEach(employee -> {
                AttendanceEmployee existedEmp = existedEmployeeByPositionId.get(employee.getPositionId());
                if (Objects.isNull(existedEmp)) {
                    employee.setCreatedBy(operatorId);
                    employee.setCreatedTime(nowTimestamp);
                    result.add(employee);
                }
            });
            return result;
        }

        /**
         * 获取需要更新的员工数据
         *
         * @param employees                   员工数据
         * @param existedEmployeeByPositionId 已存在的员工信息
         * @param depCategoryIdMapByDepCode   部门 code , category id
         * @param operatorId                  操作人 id
         * @param nowTimestamp                操作时间
         * @return 员工数据数组
         * @author zc
         * @date 2023/3/2
         */
        private List<AttendanceEmployee> getEmployeesToUpdate(List<AttendanceEmployee> employees, Map<String, AttendanceEmployee> existedEmployeeByPositionId,
                                                              Map<String, Integer> depCategoryIdMapByDepCode, int operatorId, long nowTimestamp) {
            List<AttendanceEmployee> result = Lists.newArrayList();
            employees.forEach(employee -> {
                AttendanceEmployee existedEmp = existedEmployeeByPositionId.get(employee.getPositionId());
                if (Objects.nonNull(existedEmp)) {
                    boolean toUpdate = false;
                    // 之前存在的员工信息如果是已离职，那么需要去除原先数据的离职日期
                    if (existedEmp.getTerminationDate() > 0) {
                        toUpdate = true;
                        employee.setTerminationDate(0L);
                    }

                    // 比对员工信息的内容摘要是否一致，内容摘要不一致，说明员工信息有变化
                    if (!StringUtils.equals(employee.getInfoSign(), existedEmp.getInfoSign())) {
                        toUpdate = true;

                        // 对比员工信息中显示状态如果有变化，新的显示状态不能覆盖之前手动设置的结果
                        if (!employee.getShown().equals(existedEmp.getShown()) && (existedEmp.getShownUpdatedTime() > 0)) {
                            employee.setShown(existedEmp.getShown());
                        }

                        employee.setDepCategoryId(existedEmp.getDepCategoryId());
                    }

                    // 检查 Department Category 是否发生变化，发生变化则需要更新 Department Category
                    if (!Objects.equals(existedEmp.getDepCategoryId(), depCategoryIdMapByDepCode.get(employee.getDepartmentCode()))) {
                        toUpdate = true;
                        employee.setDepCategoryId(depCategoryIdMapByDepCode.get(employee.getDepartmentCode()));
                    }

                    if (toUpdate) {
                        employee.setId(existedEmp.getId());
                        employee.setLastUpdatedBy(operatorId);
                        employee.setLastUpdatedTime(nowTimestamp);
                        result.add(employee);
                    }
                }
            });
            return result;
        }

        /**
         * 查询已存在的员工信息
         *
         * @param attendanceEmployees 指定 property 下导入的 EMP 信息集合
         * @return 根据员工编号分组的员工信息
         * @author zc
         * @date 2023/2/24
         */
        private Map<String, AttendanceEmployee> getExistedEmployeeByPositionId(List<AttendanceEmployee> attendanceEmployees) {
            if (CollectionUtils.isEmpty(attendanceEmployees)) {
                return Collections.emptyMap();
            }
            List<String> positionIds = attendanceEmployees.stream().map(AttendanceEmployee::getPositionId).collect(toList());
            LambdaQueryWrapper<AttendanceEmployee> queryWrapper = new QueryWrapper<AttendanceEmployee>().lambda()
                    .in(AttendanceEmployee::getPositionId, positionIds)
                    .select(AttendanceEmployee::getId, AttendanceEmployee::getPositionId, AttendanceEmployee::getDepartmentCode,
                            AttendanceEmployee::getDepCategoryId, AttendanceEmployee::getTerminationDate, AttendanceEmployee::getPayClass,
                            AttendanceEmployee::getInfoSign, AttendanceEmployee::getShown, AttendanceEmployee::getShownUpdatedTime);
            return list(queryWrapper).stream().collect(toMap(AttendanceEmployee::getPositionId, Function.identity()));
        }

        /**
         * 更新员工离职日期
         *
         * @param propertyId  property id
         * @param positionIds 员工编号
         * @param updateBy    操作人 id
         * @param updateTime  操作时间
         * @author zc
         * @date 2023/2/24
         */
        private void updateNotInReportEmployeeTermination(int propertyId, Set<String> positionIds,
                                                          int updateBy, long updateTime) {
            LambdaQueryWrapper<AttendanceEmployee> queryWrapper = new QueryWrapper<AttendanceEmployee>().lambda()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .notIn(AttendanceEmployee::getPositionId, positionIds);
            AttendanceEmployee updateParam = new AttendanceEmployee();
            updateParam.setTerminationDate(DateTimeUtils.dateNowTimestamp(defaultTimezone));
            updateParam.setLastUpdatedBy(updateBy);
            updateParam.setLastUpdatedTime(updateTime);
            updateParam.setShown(false);
            attendanceEmployeeMapper.update(updateParam, queryWrapper);
        }

        /**
         * 批量保存新增的员工信息
         *
         * @param employees 新增的员工信息集合
         * @author zc
         * @date 2023/2/24
         */
        @Transactional(rollbackFor = Exception.class)
        public void saveNewEmployeesBatch(List<AttendanceEmployee> employees) {
            if (employees.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            AttendanceEmployeeMapper mapper = sqlSession.getMapper(AttendanceEmployeeMapper.class);
            for (int i = 0; i < employees.size(); i++) {
                mapper.insert(employees.get(i));

                if ((i + 1) % 50 == 0) {
                    sqlSession.flushStatements();
                }
            }

            sqlSession.flushStatements();
        }

        /**
         * 批量更新员工信息
         *
         * @param employees 更新的员工信息集合
         * @author zc
         * @date 2023/2/24
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateEmployeesBatch(List<AttendanceEmployee> employees) {
            if (employees.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            AttendanceEmployeeMapper mapper = sqlSession.getMapper(AttendanceEmployeeMapper.class);
            for (int i = 0; i < employees.size(); i++) {
                mapper.updateById(employees.get(i));

                if ((i + 1) % 50 == 0) {
                    sqlSession.flushStatements();
                }
            }

            sqlSession.flushStatements();
        }

        /**
         * 检查 department category 是否被员工占用
         *
         * @param propertyId 资产 id
         * @param categoryId 部门分类 id
         * @return true：被占用，false：未被占用
         * @author yyt
         * @date 2025/3/25
         **/
        @Override
        public boolean checkOccupied(Integer propertyId, Integer categoryId) {
            AttendanceEmployee employee = getOne(new QueryWrapper<AttendanceEmployee>().lambda()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .eq(AttendanceEmployee::getDepCategoryId, categoryId)
                    .last("limit 1"));
            return Objects.nonNull(employee);
        }

        /**
         * 获取指定 property 下的未显示的 position id 集合
         *
         * @param propertyId 资产 id
         * @return position id 集合
         * @author zc
         * @date 2025/4/23
         */
        @Override
        public List<String> listNoShowPositionIds(int propertyId) {
            return list(new LambdaQueryWrapper<AttendanceEmployee>()
                    .eq(AttendanceEmployee::getPropertyId, propertyId)
                    .eq(AttendanceEmployee::getShown, false)).stream()
                    .map(AttendanceEmployee::getPositionId).toList();
        }

        /**
         * 校验归属
         *
         * @param propertyId 资产 id
         * @param employee   员工信息
         * @author zc
         * @date 2025/7/31
         */
        @Override
        public void checkBelong(int propertyId, AttendanceEmployee employee) {
            if (!employee.getPropertyId().equals(propertyId)) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("Cannot operate the employee. paramPropertyId:{0}; propertyId: {1}",
                                propertyId, employee.getPropertyId()));
            }
        }

    }