# ScheduleInfoServiceImpl.java

    package cloud.wconnect.workperformance.service.schedule.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.cons.DataPermissionConfigConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.workperformance.cons.schedule.ScheduleErrorFiledCons;
    import cloud.wconnect.workperformance.entity.schedule.*;
    import cloud.wconnect.workperformance.mapper.schedule.ScheduleInfoMapper;
    import cloud.wconnect.workperformance.pojo.bo.schedule.ScheduleBO;
    import cloud.wconnect.workperformance.pojo.dto.schedule.ScheduleContentDTO;
    import cloud.wconnect.workperformance.pojo.dto.schedule.ScheduleDepartmentDTO;
    import cloud.wconnect.workperformance.pojo.dto.schedule.ScheduleDepartmentEmployeeDTO;
    import cloud.wconnect.workperformance.pojo.dto.schedule.ScheduleUpdateDTO;
    import cloud.wconnect.workperformance.pojo.query.schedule.ScheduleQuery;
    import cloud.wconnect.workperformance.service.schedule.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Nullable;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 排班信息 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-06-28
     */
    @Service
    @DS("work_performance")
    public class ScheduleInfoServiceImpl extends ServiceImpl<ScheduleInfoMapper, ScheduleInfo> implements IScheduleInfoService {

        @Autowired
        private BeanMapper beanMapper;
        @Lazy
        @Autowired
        private IScheduleDepartmentEmployeeService scheduleDepartmentEmployeeService;
        @Autowired
        private IScheduleDepartmentPermissionConfigService scheduleDepartmentPermissionConfigService;
        @Autowired
        private IScheduleDepartmentService scheduleDepartmentService;
        @Autowired
        private IScheduleImportEmployeeFilterConfigService scheduleImportEmployeeFilterConfigService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;

        public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        /**
         * 获取排班信息
         *
         * @param propertyId    资产
         * @param scheduleQuery 查询条件
         * @param userId        操作人
         * @return 排班信息
         * @author yyt
         * @date 2023/6/29
         */
        @Override
        public List<ScheduleDepartmentDTO> listSchedule(Integer propertyId, ScheduleQuery scheduleQuery, int userId) {
            Long startDate = scheduleQuery.getStartDate();
            Long endDate = scheduleQuery.getEndDate();
            // 以是否是 杂项部门分组
            Map<Boolean, List<ScheduleInfo>> scheduleByIsMiscellaneous = list(new QueryWrapper<ScheduleInfo>().lambda()
                    .between(ScheduleInfo::getTheDate, startDate, endDate).eq(ScheduleInfo::getPropertyId, propertyId)).stream()
                    .collect(Collectors.partitioningBy(item -> item.getDepartmentId() == ScheduleDepartment.MISCELLANEOUS_ID));

            // 设置 非杂项部门下的排班信息
            List<ScheduleDepartmentDTO> scheduleDepartmentDTOs = new ArrayList<>(packageNotMiscellaneousScheduleInfo(propertyId,
                    startDate, endDate, scheduleByIsMiscellaneous.getOrDefault(false, Collections.emptyList()), userId));

            // 设置 杂项部门下的排班信息
            ScheduleDepartmentDTO scheduleDepartmentDTOByMiscellaneous = packageMiscellaneousScheduleInfo(propertyId, startDate, endDate,
                    scheduleByIsMiscellaneous.getOrDefault(true, Collections.emptyList()));
            if (scheduleDepartmentDTOByMiscellaneous != null) {
                // 杂项部门排在 其他部门 最下面
                scheduleDepartmentDTOs.sort(Comparator.comparing(ScheduleDepartmentDTO::getSort));
                scheduleDepartmentDTOByMiscellaneous.setSort(scheduleDepartmentDTOs.isEmpty() ? 1 :
                        scheduleDepartmentDTOs.get(scheduleDepartmentDTOs.size() - 1).getSort() + 1);

                scheduleDepartmentDTOs.add(scheduleDepartmentDTOByMiscellaneous);
            }

            // sort,name 排序
            scheduleDepartmentDTOs.sort(Comparator.comparing(ScheduleDepartmentDTO::getSort)
                    .thenComparing(ScheduleDepartmentDTO::getName));

            return scheduleDepartmentDTOs;
        }

        /**
         * 封装杂项部门下的排班信息
         *
         * @param propertyId    资产
         * @param startDate     开始时间
         * @param endDate       结束时间
         * @param scheduleInfos 排班信息
         * @return 排班信息
         * @author yyt
         * 2023/6/29
         */
        private ScheduleDepartmentDTO packageMiscellaneousScheduleInfo(Integer propertyId, Long startDate, Long endDate,
                                                                       List<ScheduleInfo> scheduleInfos) {
            if (CollectionUtils.isEmpty(scheduleInfos)) {
                return null;
            }

            List<Integer> employeeIds = scheduleInfos.stream().map(ScheduleInfo::getEmployeeId).distinct()
                    .collect(Collectors.toList());

            List<ScheduleDepartmentEmployeeDTO> scheduleDepartmentEmployeeDTOs =
                    packageEmployeeScheduleInfo(propertyId, startDate, endDate, getEmployeeByIds(employeeIds), scheduleInfos, null);

            return new ScheduleDepartmentDTO(ScheduleDepartment.MISCELLANEOUS_ID, ScheduleDepartment.MISCELLANEOUS_NAME,
                    null, true, scheduleDepartmentEmployeeDTOs);
        }

        /**
         * 封装非杂项部门下的排班信息
         *
         * @param propertyId    资产
         * @param startDate     开始时间
         * @param endDate       结束时间
         * @param scheduleInfos 排班信息
         * @param userId        操作人
         * @return 排班信息
         * @author yyt
         * 2023/1/7
         */
        private List<ScheduleDepartmentDTO> packageNotMiscellaneousScheduleInfo(Integer propertyId, Long startDate, Long endDate,
                                                                                List<ScheduleInfo> scheduleInfos, int userId) {
            // 当前登录用户只能看自己有权限（用户、所属用户组、所属角色，与部门绑定过且状态是有效）的部门信息，如果当前用户没有权限查看任何部门的排班信息，则直接 return
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            Map<Integer, List<ScheduleDepartmentPermissionConfig>> permissionConfigsByDepartmentId =
                    scheduleDepartmentPermissionConfigService.getHasAccessPermissionConfigs(propertyDTO.getTenantId(),
                                    propertyId, userId, null)
                            .stream().collect(Collectors.groupingBy(ScheduleDepartmentPermissionConfig::getDepartmentId));
            if (permissionConfigsByDepartmentId.isEmpty()) {
                return Collections.emptyList();
            }

            Map<Integer, ScheduleDepartment> departmentById = scheduleDepartmentService.listByIds(permissionConfigsByDepartmentId.keySet())
                    .stream().collect(Collectors.toMap(ScheduleDepartment::getId, v -> v));

            List<Integer> employeeIds = scheduleInfos.stream().map(ScheduleInfo::getEmployeeId).distinct()
                    .collect(Collectors.toList());
            Map<Integer, List<ScheduleDepartmentEmployee>> employeeByDepartmentId = getEmployeeByIds(employeeIds)
                    .stream().collect(Collectors.groupingBy(ScheduleDepartmentEmployee::getDepartmentId));

            Map<Integer, List<ScheduleInfo>> scheduleInfosByDepartmentId = scheduleInfos.stream()
                    .collect(Collectors.groupingBy(ScheduleInfo::getDepartmentId));

            List<ScheduleDepartmentDTO> scheduleDepartmentDTOs = Lists.newArrayListWithExpectedSize(departmentById.size());

            // 查询 过滤员工配置，配置中出现的员工在后续排班中不显示
            Map<String, Long> insertConfigTimeByPositionId = scheduleImportEmployeeFilterConfigService.list(new QueryWrapper<ScheduleImportEmployeeFilterConfig>()
                            .lambda().eq(ScheduleImportEmployeeFilterConfig::getPropertyId, propertyId)).stream()
                    .collect(Collectors.toMap(ScheduleImportEmployeeFilterConfig::getPositionId, ScheduleImportEmployeeFilterConfig::getUpdatedTime));

            departmentById.forEach((departmentId, department) -> {
                List<ScheduleInfo> currentDepartmentScheduleInfos = scheduleInfosByDepartmentId.getOrDefault(departmentId,
                        Collections.emptyList());

                List<ScheduleDepartmentEmployeeDTO> scheduleDepartmentEmployeeDTOs =
                        packageEmployeeScheduleInfo(propertyId, startDate, endDate,
                                employeeByDepartmentId.getOrDefault(departmentId, Collections.emptyList()),
                                currentDepartmentScheduleInfos, insertConfigTimeByPositionId);

                // sort 为 null 的设置 -1，防止下面排序时出现 NPE
                if (department.getSort() == null) {
                    department.setSort(-1);
                }

                scheduleDepartmentDTOs.add(new ScheduleDepartmentDTO(departmentId, department.getName(), department.getSort(),
                        permissionConfigsByDepartmentId.get(departmentId).stream().anyMatch(item -> item.getPermissionType()
                                == DataPermissionConfigConstant.PERMISSION_TYPE_WRITE),
                        scheduleDepartmentEmployeeDTOs));
            });

            return scheduleDepartmentDTOs;
        }

        /**
         * 封装员工排班信息
         *
         * @param propertyId                   资产
         * @param startDate                    开始日期
         * @param endDate                      结束日期
         * @param employees                    员工
         * @param scheduleInfos                员工排班
         * @param insertConfigTimeByPositionId 员工添加到过滤配置中的时间
         * @return 员工排班信息
         * @author yyt
         * 2023/6/29
         */
        private List<ScheduleDepartmentEmployeeDTO> packageEmployeeScheduleInfo(Integer propertyId, Long startDate, Long endDate,
                                                                                List<ScheduleDepartmentEmployee> employees,
                                                                                List<ScheduleInfo> scheduleInfos,
                                                                                @Nullable Map<String, Long> insertConfigTimeByPositionId) {
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            ZonedDateTime startDateTime = DateTimeUtils.toZonedDateTime(startDate, timezone);
            ZonedDateTime endDateTime = DateTimeUtils.toZonedDateTime(endDate, timezone);

            List<ScheduleDepartmentEmployeeDTO> scheduleDepartmentEmployeeDTOs = Lists.newArrayListWithExpectedSize(employees.size());

            Map<Integer, Map<Long, ScheduleInfo>> scheduleInfosByEmployeeId = scheduleInfos.stream()
                    .collect(Collectors.groupingBy(ScheduleInfo::getEmployeeId, Collectors.toMap(ScheduleInfo::getTheDate, v -> v)));

            employees.forEach(employee -> {
                // 选定日期内没有排班信息，则当前员工不显示在页面中
                Map<Long, ScheduleInfo> currentEmployeeScheduleInfos =
                        scheduleInfosByEmployeeId.getOrDefault(employee.getId(), Collections.emptyMap());
                if (currentEmployeeScheduleInfos.isEmpty()) {
                    return;
                }

                // 检查当前员工是否出现在过滤配置中，如果出现，则后续排班不显示
                Long insertConfigTime = Objects.isNull(insertConfigTimeByPositionId) ? null : insertConfigTimeByPositionId.get(employee.getPositionId());

                // 用户信息
                ScheduleDepartmentEmployeeDTO scheduleDepartmentEmployeeDto = beanMapper.map(employee, ScheduleDepartmentEmployeeDTO.class);
                List<ScheduleContentDTO> scheduleContentDtos = Lists.newArrayListWithExpectedSize(7);
                // 封装选定日期内的排班，当日没有排班信息的设置空对象
                for (ZonedDateTime date = startDateTime; !date.isAfter(endDateTime); date = date.plusDays(1)) {
                    long currentDateSecond = date.toEpochSecond();
                    ScheduleInfo scheduleInfo = currentEmployeeScheduleInfos.getOrDefault(currentDateSecond, new ScheduleInfo());

                    if (Objects.nonNull(insertConfigTime)) {
                        if (currentDateSecond > insertConfigTime) {
                            scheduleInfo = new ScheduleInfo();
                        }
                    }

                    scheduleContentDtos.add(new ScheduleContentDTO(scheduleInfo.getContent(), scheduleInfo.getUpdatedBy(),
                            scheduleInfo.getUpdatedTime()));
                }
                // sort 为 null 的设置 -1，防止下面排序时出现 NPE
                if (scheduleDepartmentEmployeeDto.getSort() == null) {
                    scheduleDepartmentEmployeeDto.setSort(-1);
                }
                scheduleDepartmentEmployeeDto.setContentList(scheduleContentDtos);

                // 当前员工在选择日期内有排班时，才显示到页面中
                if (scheduleContentDtos.stream().anyMatch(scheduleContent -> Objects.nonNull(scheduleContent.getContent()))) {
                    scheduleDepartmentEmployeeDTOs.add(scheduleDepartmentEmployeeDto);
                }
            });

            // sort,name 排序
            scheduleDepartmentEmployeeDTOs.sort(Comparator.comparing(ScheduleDepartmentEmployeeDTO::getSort)
                    .thenComparing(ScheduleDepartmentEmployeeDTO::getName));

            return scheduleDepartmentEmployeeDTOs;
        }

        /**
         * 根据 id 获取 员工
         *
         * @param employeeIds 员工 id
         * @return 员工
         * @author yyt
         * 2023/6/29
         */
        private List<ScheduleDepartmentEmployee> getEmployeeByIds(List<Integer> employeeIds) {
            if (CollectionUtils.isEmpty(employeeIds)) {
                return Collections.emptyList();
            }
            return scheduleDepartmentEmployeeService.listByIds(employeeIds);
        }

        /**
         * 编辑部门排班信息
         *
         * @param propertyId         资产
         * @param departmentId       部门
         * @param scheduleUpdateDtos 排班
         * @param userId             操作人
         * @author yyt
         * @date 2023/6/29
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateScheduleInfo(Integer propertyId, int departmentId, List<ScheduleUpdateDTO> scheduleUpdateDtos, int userId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 获取部门信息
            if (departmentId > 0) {
                ScheduleDepartment scheduleDepartment = scheduleDepartmentService.getByIdForUpdate(departmentId);
                if (scheduleDepartment == null) {
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_EXIST,
                            i18nMessageUtils.getErrorParamMapNotNeedTranslate("department"),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the scheduleDepartment is not found;id:" + departmentId);
                }
                if (!scheduleDepartment.getPropertyId().equals(propertyId)) {
                    throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                            BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(), "the property id parameter is different from " +
                            "the property id of the scheduleDepartment;propertyId:" + propertyId
                            + ",scheduleDepartmentPropertyId:" + scheduleDepartment.getPropertyId() + ",scheduleDepartmentId:" + departmentId);
                }

                // 校验当前用户是否有 当前部门 可改权限
                List<ScheduleDepartmentPermissionConfig> permissionConfigs =
                        scheduleDepartmentPermissionConfigService.getHasAccessPermissionConfigs(propertyDTO.getTenantId(),
                                scheduleDepartment.getPropertyId(), userId, departmentId);
                if (CollectionUtils.isEmpty(permissionConfigs) || permissionConfigs.stream().noneMatch(item -> item.getPermissionType()
                        == DataPermissionConfigConstant.PERMISSION_TYPE_WRITE)) {
                    // 无权限修改
                    throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                            BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(), "Modify without permission;userId:" + userId);
                }
            }

            // 校验用户存在且有效
            List<Integer> employeeIds = scheduleUpdateDtos.stream().map(ScheduleUpdateDTO::getEmployeeId).distinct()
                    .collect(Collectors.toList());
            List<ScheduleDepartmentEmployee> employees = getEmployeeByIds(employeeIds);
            if (employees.isEmpty() || employees.size() != employeeIds.size()) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_EXIST,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("employee"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the employeesIds parameter error;" +
                        "employeeId:" + employeeIds);
            }

            // 校验用户都属于当前部门
            Optional<ScheduleDepartmentEmployee> differentDepartmentEmployee = employees.stream()
                    .filter(item -> item.getDepartmentId() != departmentId).findAny();
            if (differentDepartmentEmployee.isPresent()) {
                throw new BadRequestException(ScheduleErrorFiledCons.SCHEDULE_DIFFERENT_DEPARTMENT_EMPLOYEE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Failed to modify the scheduling information, only employees in the same department can be modified;" +
                                "departmentId:" + departmentId + ";employeeId:" + employeeIds);
            }

            long nowTime = DateTimeUtils.nowTimestamp();
            List<ScheduleInfo> scheduleInfos = scheduleUpdateDtos.stream()
                    .map(item -> new ScheduleInfo(null, propertyId, item.getTheDate(), departmentId, item.getEmployeeId(),
                            item.getContent(), userId, nowTime))
                    .sorted(Comparator.comparing(ScheduleInfo::getTheDate).thenComparing(ScheduleInfo::getEmployeeId)
                            .thenComparing(ScheduleInfo::getDepartmentId).thenComparing(ScheduleInfo::getPropertyId))
                    .collect(Collectors.toList());

            baseMapper.insertOrUpdateBatch(scheduleInfos);
        }

        /**
         * 处理插入排班信息
         *
         * @param propertyDTO 资产
         * @param schedules   排班信息
         * @param operatorId  操作人 id
         * @author yyt
         * @date 2023/7/8
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleInsertSchedule(PropertyDTO propertyDTO, List<ScheduleBO> schedules, int operatorId) {

            List<String> userNames = schedules.stream().map(ScheduleBO::getName).distinct().collect(Collectors.toList());
            Map<String, ScheduleDepartmentEmployee> employeeByLowerCaseName =
                    scheduleDepartmentEmployeeService.matchUser(propertyDTO.getId(), userNames);

            updateEmployeeSchedule(propertyDTO, employeeByLowerCaseName, schedules, operatorId);
        }

        /**
         * 修改员工排班
         *
         * @param propertyDTO             资产信息
         * @param employeeByLowerCaseName 员工信息
         * @param schedules               排班信息
         * @param operatorId              操作人 id
         * @author yyt
         * @date 2023/7/10
         */
        private void updateEmployeeSchedule(PropertyDTO propertyDTO, Map<String, ScheduleDepartmentEmployee> employeeByLowerCaseName,
                                            List<ScheduleBO> schedules, int operatorId) {
            // 清除选择日期区间内的排班信息
            clearScheduleInfo(propertyDTO, schedules);

            // 新增排班
            long nowTime = DateTimeUtils.nowTimestamp();
            ZoneId zoneId = ZoneId.of(propertyDTO.getTimezone());
            saveBatch(schedules.stream().map(item -> {
                ScheduleDepartmentEmployee employee = employeeByLowerCaseName.get(item.getName().toLowerCase());
                return new ScheduleInfo(null, propertyDTO.getId(), LocalDate.parse(item.getTargetDate(), DATE_TIME_FORMATTER)
                        .atStartOfDay(zoneId).toEpochSecond(), employee.getDepartmentId(), employee.getId(), item.getContent(),
                        operatorId, nowTime);
            }).collect(Collectors.toList()));
        }

        /**
         * 清除选择日期区间内的排班信息
         *
         * @param propertyDTO 资产
         * @param schedules   排班
         * @author yyt
         * @date 2023/7/10
         */
        private void clearScheduleInfo(PropertyDTO propertyDTO, List<ScheduleBO> schedules) {
            List<Long> dates = schedules.stream().map(item -> LocalDate.parse(item.getTargetDate(), DATE_TIME_FORMATTER)
                    .atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond()).sorted().collect(Collectors.toList());

            remove(new QueryWrapper<ScheduleInfo>().lambda().eq(ScheduleInfo::getPropertyId, propertyDTO.getId())
                    .between(ScheduleInfo::getTheDate, dates.get(0), dates.get(dates.size() - 1)));
        }

    }