# SvaScheduleHourServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.MoudleEventSubjectEnum;
    import cloud.wconnect.commonlib.exception.ListenerException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.TransactionUtils;
    import cloud.wconnect.sns.http.processor.Sender;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaActualScheduleRecord;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaScheduleHour;
    import cloud.wconnect.workperformance.mapper.sva.avhours.SvaScheduleHourMapper;
    import cloud.wconnect.workperformance.pojo.bo.modelevent.dephourstatistics.SvaDepHourStatisticsEventBO;
    import cloud.wconnect.workperformance.pojo.bo.sva.actualschedulehour.SvaScheduleHoursBO;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaActualScheduleRecordService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaScheduleHourService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.schedulehour.IScheduleHourParseService;
    import cloud.wconnect.workperformance.service.sva.employees.ISvaEmployeesImportFilterConfigService;
    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.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.compress.utils.Lists;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.ApplicationContext;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.io.InputStream;
    import java.time.LocalDate;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.Executor;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * SVA Schedule Work Hour 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-04-21
     */
    @Service
    @DS("work_performance")
    public class SvaScheduleHourServiceImpl extends ServiceImpl<SvaScheduleHourMapper, SvaScheduleHour> implements ISvaScheduleHourService {
        @Resource
        private ISvaEmployeesImportFilterConfigService excludeEmployeeConfigService;
        @Resource
        private IDepCodeToCategoryService depCodeToCategoryService;
        @Resource
        private ISvaActualScheduleRecordService actualScheduleRecordService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ApplicationContext applicationContext;
        @Resource
        private Executor executor;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        @Qualifier("moduleEventSender")
        private Sender moduleEventSender;
        @Resource
        private SqlSessionFactory sqlSessionFactory;

        /**
         * 解析报告
         *
         * @param propertyList         资产集合
         * @param in                   报告文件流
         * @param source               数据源
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @return SvaScheduleHoursBO 集合
         * @author zc
         * @date 2025/4/21
         */
        @Override
        public List<SvaScheduleHoursBO> parseReport(List<PropertyDTO> propertyList, InputStream in, String source,
                                                    Map<Integer, List<Long>> propertyId2DateRange) {
            Collection<IScheduleHourParseService> parserList = applicationContext.getBeansOfType(IScheduleHourParseService.class).values();
            for (IScheduleHourParseService parseService : parserList) {
                if (parseService.supports(source)) {
                    return parseService.parse(propertyList, in, propertyId2DateRange);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 处理 Schedule Hours
         *
         * @param propertyDTOList      资产集合
         * @param scheduleHoursBOList  排班工时
         * @param operatorId           操作人 id
         * @param propertyId2DateRange 资产 id 与 日期范围集合
         * @author zc
         * @date 2025/4/21
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleScheduleHours(List<PropertyDTO> propertyDTOList, List<SvaScheduleHoursBO> scheduleHoursBOList, int operatorId,
                                        Map<Integer, List<Long>> propertyId2DateRange) {
            Map<Integer, List<SvaScheduleHour>> propertyId2ScheduleHourList = formatScheduleHours(propertyDTOList, scheduleHoursBOList).stream()
                    .collect(Collectors.groupingBy(SvaScheduleHour::getPropertyId));
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<SvaScheduleHour> newScheduleHourList = Lists.newArrayList();
            for (PropertyDTO propertyDTO : propertyDTOList) {
                List<Long> dateRangeList = propertyId2DateRange.getOrDefault(propertyDTO.getId(), Collections.emptyList());
                // 删除指定日期范围内的数据
                deleteScheduleHourByDateRange(propertyDTO.getId(), dateRangeList);

                List<SvaScheduleHour> scheduleHourList = propertyId2ScheduleHourList.getOrDefault(propertyDTO.getId(), Collections.emptyList());
                if (CollectionUtils.isEmpty(scheduleHourList)) {
                    continue;
                }

                // 过滤掉排除的员工数据
                List<SvaScheduleHour> effectiveScheduleHourList = filterByExcludeEmployeeConfig(propertyDTO.getId(), scheduleHourList);

                // 累积需要匹配 Department Category 的 Department Code 和 Department Desc
                Map<Long, Map<String, String>> depCode2DepDescByPayDateTimestamp = effectiveScheduleHourList.stream()
                        .collect(Collectors.groupingBy(item -> DateTimeUtils.dateStrToEpochSecond(item.getPayDate().toString(),
                                BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone()), Collectors.toMap(SvaScheduleHour::getDepartmentCode,
                                SvaScheduleHour::getDepartmentDescription, (v1, v2) -> v2)));

                // 获取指定日期、指定 Department Code 集合在指定 Property 下对应的 Department Category Id
                Map<Long, Map<String, Integer>> depCode2CategoryIdByPayDateTimestamp =
                        depCodeToCategoryService.generateCategoryId4DepCodes(propertyDTO.getId(), depCode2DepDescByPayDateTimestamp);

                // 组装 Department Category Id
                effectiveScheduleHourList.forEach(item -> {
                    item.setUpdatedBy(operatorId);
                    item.setUpdatedTime(nowTimestamp);
                    long payDateTimestamp = DateTimeUtils.dateStrToEpochSecond(item.getPayDate().toString(),
                            BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone());
                    int departmentCategoryId = depCode2CategoryIdByPayDateTimestamp.getOrDefault(payDateTimestamp,
                            Collections.emptyMap()).getOrDefault(item.getDepartmentCode(), BaseConstant.ZERO);
                    item.setDepCategoryId(departmentCategoryId);
                });

                newScheduleHourList.addAll(effectiveScheduleHourList);
            }

            saveBatchRewrite(newScheduleHourList);
            // 更新记录
            actualScheduleRecordService.updateScheduleHourRecord(propertyDTOList, propertyId2DateRange, newScheduleHourList);

            // 指定日期范围内数据发生变化时，发送 SNS 事件
            TransactionUtils.afterCommitAsyncExecute(executor, () -> sendModuleEvent(propertyDTOList, propertyId2DateRange));
        }

        /**
         * 批量新增
         *
         * @param scheduleHourList Schedule Hour List
         * @author zc
         * @date 2025/4/25
         */
        public void saveBatchRewrite(List<SvaScheduleHour> scheduleHourList) {
            if (CollectionUtils.isEmpty(scheduleHourList)) {
                return;
            }

            try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
                SvaScheduleHourMapper mapper = sqlSession.getMapper(SvaScheduleHourMapper.class);
                for (int i = 0; i < scheduleHourList.size(); i++) {
                    SvaScheduleHour scheduleHour = scheduleHourList.get(i);
                    mapper.insert(scheduleHour);
                    // 每 50 条提交一次
                    if ((i + 1) % 50 == 0) {
                        sqlSession.flushStatements();
                    }
                }
                sqlSession.flushStatements();
            }
        }

        /**
         * 根据 PositionId 和 PayDate 查询 Schedule Hours
         *
         * @param propertyId 资产 id
         * @param recordList Actual Schedule Record 集合
         * @return SvaScheduleHour 集合
         * @author zc
         * @date 2025/4/22
         */
        @Override
        public List<SvaScheduleHour> listPositionIdAndPayDateInScheduleHour(int propertyId, List<SvaActualScheduleRecord> recordList) {
            if (CollectionUtils.isEmpty(recordList)) {
                return Collections.emptyList();
            }
            return baseMapper.listPositionIdAndPayDateInScheduleHour(propertyId, recordList);
        }

        /**
         * 根据日期范围查询 Schedule Hours
         *
         * @param propertyId             资产 id
         * @param startDate              开始日期
         * @param endDate                结束日期
         * @param excludeDepartmentCodes 需要排除的部门编码集合（可空）
         * @return SvaScheduleHour 集合
         * @author zc
         * @date 2025/4/22
         */
        @Override
        public List<SvaScheduleHour> listByDate(int propertyId, long startDate, long endDate, @Nullable List<String> excludeDepartmentCodes) {
            return list(new LambdaQueryWrapper<SvaScheduleHour>()
                    .between(SvaScheduleHour::getPayDate, startDate, endDate)
                    .notIn(CollectionUtils.isNotEmpty(excludeDepartmentCodes),
                            SvaScheduleHour::getDepartmentCode, excludeDepartmentCodes)
                    .eq(SvaScheduleHour::getPropertyId, propertyId));
        }

        /**
         * 获取有效的 Schedule Hours
         *
         * @param propertyId             资产 id
         * @param startDate              开始日期(yyyyMMdd)
         * @param endDate                结束日期(yyyyMMdd)
         * @param noShowPositionIds      隐藏的 PositionId 集合
         * @param excludeDepartmentCodes 需要排除的 DepartmentCode 集合
         * @param excludePayCodeList     需要排除的 PayCode 集合
         * @param targetDepartmentCodes  指定的 DepartmentCode 集合
         * @param targetDepCategoryIds   指定的 DepartmentCategoryIds 集合
         * @return SvaScheduleHour 集合
         * @author zc
         * @date 2025/4/23
         */
        @Override
        public List<SvaScheduleHour> listEffective(int propertyId, long startDate, long endDate, @Nullable List<String> noShowPositionIds,
                                                   @Nullable List<String> excludeDepartmentCodes, @Nullable List<String> excludePayCodeList,
                                                   @Nullable List<String> targetDepartmentCodes, @Nullable List<Integer> targetDepCategoryIds) {
            return list(new LambdaQueryWrapper<SvaScheduleHour>()
                    .between(SvaScheduleHour::getPayDate, startDate, endDate)
                    .notIn(CollectionUtils.isNotEmpty(noShowPositionIds), SvaScheduleHour::getPositionId, noShowPositionIds)
                    .notIn(CollectionUtils.isNotEmpty(excludeDepartmentCodes), SvaScheduleHour::getDepartmentCode, excludeDepartmentCodes)
                    .notIn(CollectionUtils.isNotEmpty(excludePayCodeList), SvaScheduleHour::getPayCode, excludePayCodeList)
                    .in(CollectionUtils.isNotEmpty(targetDepartmentCodes), SvaScheduleHour::getDepartmentCode, targetDepartmentCodes)
                    .in(CollectionUtils.isNotEmpty(targetDepCategoryIds), SvaScheduleHour::getDepCategoryId, targetDepCategoryIds)
                    .eq(SvaScheduleHour::getPropertyId, propertyId));
        }

        /**
         * 发送数据变更事件
         *
         * @param propertyList            资产集合
         * @param propertyId2DateRangeMap 报告数据日期范围(yyyyMMdd)
         * @author zc
         * @date 2025/4/4
         */
        private void sendModuleEvent(List<PropertyDTO> propertyList, Map<Integer, List<Long>> propertyId2DateRangeMap) {
            List<SvaDepHourStatisticsEventBO> eventBOList = propertyList.stream().map(propertyDTO -> {
                List<Long> dateRange = propertyId2DateRangeMap.get(propertyDTO.getId());
                return SvaDepHourStatisticsEventBO.builder()
                        .propertyId(propertyDTO.getId())
                        .startDate(dateRange.get(0))
                        .endDate(dateRange.get(1))
                        .build();
            }).toList();
            String messages = JsonUtils.convertObjListToString(objectMapper, eventBOList);
            moduleEventSender.publish(MoudleEventSubjectEnum.SVA_DEPARTMENT_HOUR_STATISTICS.getSubject(), messages);
        }

        /**
         * 删除日期范围内的 Schedule Hours 数据
         *
         * @param propertyId    资产 id
         * @param dateRangeList 日期范围
         * @author zc
         * @date 2025/4/21
         */
        private void deleteScheduleHourByDateRange(int propertyId, List<Long> dateRangeList) {
            if (CollectionUtils.isEmpty(dateRangeList)) {
                return;
            }
            remove(new LambdaQueryWrapper<SvaScheduleHour>()
                    .between(SvaScheduleHour::getPayDate, dateRangeList.get(0), dateRangeList.get(1))
                    .eq(SvaScheduleHour::getPropertyId, propertyId));
        }

        /**
         * 过滤掉排除的员工数据
         *
         * @param propertyId       资产 id
         * @param scheduleHourList 排班工时集合
         * @return SvaScheduleHour 集合
         * @author zc
         * @date 2025/4/21
         */
        private List<SvaScheduleHour> filterByExcludeEmployeeConfig(int propertyId, List<SvaScheduleHour> scheduleHourList) {
            List<String> excludePositionIds = excludeEmployeeConfigService.listPositionId(propertyId);
            return scheduleHourList.stream().filter(item -> !excludePositionIds.contains(item.getPositionId())).toList();
        }

        /**
         * 格式化 BO 数据
         *
         * @param propertyDTOList     资产集合
         * @param scheduleHoursBOList 实际工时 BO 集合
         * @return SvaScheduleHour 集合
         * @author zc
         * @date 2025/4/21
         */
        private List<SvaScheduleHour> formatScheduleHours(List<PropertyDTO> propertyDTOList, List<SvaScheduleHoursBO> scheduleHoursBOList) {
            Map<String, PropertyDTO> abbr2Property = propertyDTOList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getAbbr, Function.identity()));
            return beanMapper.mapList(scheduleHoursBOList, SvaScheduleHour.class, (bo, item) -> {
                PropertyDTO propertyDTO = abbr2Property.get(bo.getProperty());
                item.setPropertyId(propertyDTO.getId());
                item.setDepCategoryId(BaseConstant.ZERO);
                LocalDate payDateLocalDate = DateTimeUtils.strToLocalDate(bo.getPayDateStr(), BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                item.setPayDate(Long.parseLong(DateTimeUtils.localDateToStr(payDateLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
                item.setDepartmentDescription(bo.getDepartmentDesc());
            });
        }
    }