# SvaDepCategoryHoursStatisticsServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.entity.sva.CategoryRecomHourSum;
    import cloud.wconnect.workperformance.entity.sva.SvaDepartmentCategoryStatisticsComment;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaActualHour;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaScheduleHour;
    import cloud.wconnect.workperformance.entity.sva.employees.AttendanceEmployee;
    import cloud.wconnect.workperformance.entity.sva.employees.SvaDepartmentIgnoreConfig;
    import cloud.wconnect.workperformance.entity.sva.saleforecast.SaleForecast;
    import cloud.wconnect.workperformance.entity.sva.setting.DepCategoryRecommendHour;
    import cloud.wconnect.workperformance.entity.sva.setting.DepartmentCategory;
    import cloud.wconnect.workperformance.entity.sva.setting.SpecialEvent;
    import cloud.wconnect.workperformance.pojo.dto.sva.hoursstatistics.*;
    import cloud.wconnect.workperformance.pojo.dto.sva.internal.departmenthour.DepHourStatisticsDailyInternalDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.internal.departmenthour.DepHourStatisticsInternalDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.internal.departmenthour.DepHourStatisticsRecommendSumInternalDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.internal.departmenthour.DepHourStatisticsValueInternalDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.recommendhours.RecommendHoursConfigurableDTO;
    import cloud.wconnect.workperformance.pojo.query.sva.schedulevsactual.SvaHoursStatisticsDailyQuery;
    import cloud.wconnect.workperformance.pojo.query.sva.schedulevsactual.SvaHoursStatisticsQuery;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaActualHourService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaPayCodeExcludeConfigService;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaScheduleHourService;
    import cloud.wconnect.workperformance.service.sva.employees.IAttendanceEmployeeService;
    import cloud.wconnect.workperformance.service.sva.employees.ISvaDepartmentIgnoreConfigService;
    import cloud.wconnect.workperformance.service.sva.saleforecast.ISaleForecastService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.ICategoryRecomHourSumService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.ISvaDepCategoryHoursStatisticsService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.ISvaDepartmentCategoryStatisticsCommentService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepCategoryRecommendHourService;
    import cloud.wconnect.workperformance.service.sva.setting.IDepartmentCategoryService;
    import cloud.wconnect.workperformance.service.sva.setting.ISpecialEventService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.ZonedDateTime;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * @author zc
     * @date 2025/02/07
     */
    @Service
    @DS("work_performance")
    public class SvaDepCategoryHoursStatisticsServiceImpl implements ISvaDepCategoryHoursStatisticsService {
        @Resource
        private ISaleForecastService saleForecastService;
        @Lazy
        @Resource
        private IDepartmentCategoryService departmentCategoryService;
        @Resource
        private IAttendanceEmployeeService employeeService;
        @Resource
        private ISvaActualHourService actualHourService;
        @Resource
        private ISvaScheduleHourService scheduleHourService;
        @Resource
        private ICategoryRecomHourSumService categoryRecommendHourSumService;
        @Resource
        private IDepCategoryRecommendHourService depCategoryRecommendHourService;
        @Resource
        private ISvaPayCodeExcludeConfigService payCodeExcludeConfigService;
        @Resource
        private ISvaDepartmentCategoryStatisticsCommentService commentService;
        @Resource
        private ISpecialEventService specialEventService;
        @Resource
        private ISvaDepartmentIgnoreConfigService ignoreConfigService;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 获取指定部门每日的工时统计数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return DepHourStatisticsInternalDTO
         * @author zc
         * @date 2025/4/4
         */
        @Override
        public DepHourStatisticsInternalDTO getHoursStatisticsDaily(PropertyDTO propertyDTO, SvaHoursStatisticsQuery query) {
            // 获取需要排除统计的部门代码
            List<String> excludeDepartmentCodes = ignoreConfigService.listConfig(propertyDTO.getId()).stream()
                    .map(SvaDepartmentIgnoreConfig::getCode).toList();
            // 如果部门代码在排除列表中，则直接返回
            if (excludeDepartmentCodes.contains(query.getDepartmentCode())) {
                return new DepHourStatisticsInternalDTO();
            }

            // 获取隐藏的 Position Id
            List<String> noShowPositionIds = employeeService.listNoShowPositionIds(propertyDTO.getId());

            // 格式化时间戳日期为 yyyyMMdd
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getFromDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getToDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));

            // 指定 Department Code 集合
            List<String> targetDepartmentCodes = Lists.newArrayList(query.getDepartmentCode());
            // 获取 Actual Hour
            List<SvaScheduleHour> scheduleHourList = scheduleHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, targetDepartmentCodes, null);
            // 获取 Schedule Hour
            List<SvaActualHour> actualHourList = actualHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, targetDepartmentCodes, null);

            // 获取启用日期在选择日期范围内或选择日期之前的最新的一条的 Department Category 推荐工时
            List<DepCategoryRecommendHour> recommendHourList = depCategoryRecommendHourService.listByDateRangeWithNewOne(propertyDTO.getId(),
                    query.getFromDate(), query.getToDate());

            // 获取指定日期范围内特殊活动设置的工时
            Map<Long, List<SpecialEvent>> theDate2EventList = specialEventService.listByDate(propertyDTO.getId(), query.getFromDate(),
                    query.getToDate()).stream().collect(Collectors.groupingBy(SpecialEvent::getTheDate));

            ZonedDateTime fromZonedDate = DateTimeUtils.toZonedDateTime(query.getFromDate(), propertyDTO.getTimezone());
            ZonedDateTime toZonedDate = DateTimeUtils.toZonedDateTime(query.getToDate(), propertyDTO.getTimezone());

            // 获取 sale forecast 数据
            Map<Long, SaleForecast> theDate2SaleForecast = saleForecastService.listByDateRange(propertyDTO.getId(),
                            fromZonedDate.minusDays(1).toEpochSecond(), toZonedDate.minusDays(1).toEpochSecond())
                    .stream().collect(Collectors.toMap(SaleForecast::getTheDate, Function.identity()));

            // 获取计数保留规则
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO.getInternationalization().getNumber().getCalculationRule());

            // 获取排除 Pay Code 配置
            List<String> excludePayCodeList = payCodeExcludeConfigService.listExcludePayCode(propertyDTO.getId());

            // 获取 Actual Hour、Schedule Hour 的 DepartmentCategoryId 集合
            List<Integer> depCategoryIds = Lists.newArrayList(getActualScheduleDepCategoryIds(scheduleHourList, actualHourList));

            Map<Long, List<SvaScheduleHour>> payDate2ScheduleHourList = scheduleHourList.stream()
                    .collect(Collectors.groupingBy(SvaScheduleHour::getPayDate));
            Map<Long, List<SvaActualHour>> payDate2ActualHourList = actualHourList.stream()
                    .collect(Collectors.groupingBy(SvaActualHour::getPayDate));

            List<DepHourStatisticsDailyInternalDTO> statisticsInternalDTOList = Lists.newArrayList();
            for (ZonedDateTime indexZonedDateTime = fromZonedDate; !indexZonedDateTime.isAfter(toZonedDate); indexZonedDateTime = indexZonedDateTime.plusDays(1)) {
                DepHourStatisticsDailyInternalDTO statisticsInternalDTO = new DepHourStatisticsDailyInternalDTO();
                long date = DateTimeUtils.parseToLong(indexZonedDateTime, BaseConstant.DATE_FORMAT_YYYYMMDD);
                statisticsInternalDTO.setDate(date);

                List<BigDecimal> recommendHours = Lists.newArrayList();
                for (int categoryId : depCategoryIds) {
                    // recommend hours
                    recommendHours.add(getRecommendHoursForOneDay(categoryId, indexZonedDateTime,
                            theDate2SaleForecast, recommendHourList, roundingMode));

                    recommendHours.add(getSpecialHoursForOneDay(categoryId, indexZonedDateTime.toEpochSecond(),
                            theDate2EventList, roundingMode));
                }
                // 设置 recommend hours
                BigDecimal recommendHoursTotal = recommendHours.stream().filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                statisticsInternalDTO.setRecommend(recommendHoursTotal);

                long payDate = DateTimeUtils.parseToLong(indexZonedDateTime, BaseConstant.DATE_FORMAT_YYYYMMDD);

                List<SvaScheduleHour> scheduleHours = payDate2ScheduleHourList.getOrDefault(payDate, Collections.emptyList());
                // 设置 scheduledHours
                BigDecimal scheduledHoursTotal = scheduleHours.stream().map(SvaScheduleHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                BigDecimal scheduledExcludePayCodeHoursTotal = scheduleHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaScheduleHour::getHours).reduce(BigDecimal::add).orElse(null);
                statisticsInternalDTO.setSchedule(new DepHourStatisticsValueInternalDTO(scheduledHoursTotal, scheduledExcludePayCodeHoursTotal));

                List<SvaActualHour> actualHours = payDate2ActualHourList.getOrDefault(payDate, Collections.emptyList());
                // 设置 actualHours
                BigDecimal actualHoursTotal = actualHours.stream().map(SvaActualHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                BigDecimal actualExcludePayCodeHoursTotal = actualHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaActualHour::getHours).reduce(BigDecimal::add).orElse(null);
                statisticsInternalDTO.setActual(new DepHourStatisticsValueInternalDTO(actualHoursTotal, actualExcludePayCodeHoursTotal));

                statisticsInternalDTOList.add(statisticsInternalDTO);
            }
            DepHourStatisticsInternalDTO depHourStatisticsInternalDTO = new DepHourStatisticsInternalDTO();
            depHourStatisticsInternalDTO.setDailyList(statisticsInternalDTOList);
            // 获取手动设置的推荐工时
            List<DepHourStatisticsRecommendSumInternalDTO> recommendSumInternalDTOList = listRecommendHourSum(propertyDTO, query, depCategoryIds);
            depHourStatisticsInternalDTO.setRecommendSumList(recommendSumInternalDTOList);

            return depHourStatisticsInternalDTO;
        }

        /**
         * 获取指定日期范围内手动设置的推荐工时
         *
         * @param propertyDTO    资产
         * @param query          查询条件
         * @param depCategoryIds 部门分类id集合
         * @return 指定日期范围内手动设置的推荐工时集合
         * @author zc
         * @date 2025/4/10
         */
        private List<DepHourStatisticsRecommendSumInternalDTO> listRecommendHourSum(PropertyDTO propertyDTO, SvaHoursStatisticsQuery query, List<Integer> depCategoryIds) {
            List<CategoryRecomHourSum> categoryRecomHourSumList = categoryRecommendHourSumService.listByDateRange(propertyDTO.getId(),
                    query.getFromDate(), query.getToDate(), depCategoryIds);
            Map<String, List<CategoryRecomHourSum>> key2SumList = categoryRecomHourSumList.stream()
                    .collect(Collectors.groupingBy(item -> item.getFromDate() + "," + item.getToDate()));
            return key2SumList.entrySet().stream().map(item -> {
                String[] dateRange = item.getKey().split(",");
                DepHourStatisticsRecommendSumInternalDTO recommendSumInternalDTO = new DepHourStatisticsRecommendSumInternalDTO();
                long fromDate = DateTimeUtils.parseToLong(DateTimeUtils.toZonedDateTime(Long.parseLong(dateRange[0]),
                        propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD);
                recommendSumInternalDTO.setFromDate(fromDate);
                long toDate = DateTimeUtils.parseToLong(DateTimeUtils.toZonedDateTime(Long.parseLong(dateRange[1]),
                        propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD);
                recommendSumInternalDTO.setToDate(toDate);
                BigDecimal recommendHourSum = item.getValue().stream().map(CategoryRecomHourSum::getRecomHourSum)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                recommendSumInternalDTO.setRecommendHourSum(recommendHourSum);
                return recommendSumInternalDTO;
            }).toList();
        }

        /**
         * 获取 Actual Hour、Schedule Hour 的 PositionId 集合
         *
         * @param scheduleHourList Schedule Hour 数据
         * @param actualHourList   Actual Hour 数据
         * @return PositionId 集合
         * @author zc
         * @date 2025/4/23
         */
        private Set<String> getActualSchedulePositionIds(List<SvaScheduleHour> scheduleHourList, List<SvaActualHour> actualHourList) {
            Set<String> positionIds = Sets.newHashSet();
            positionIds.addAll(scheduleHourList.stream().map(SvaScheduleHour::getPositionId).toList());
            positionIds.addAll(actualHourList.stream().map(SvaActualHour::getPositionId).toList());
            return positionIds;
        }

        /**
         * 获取 Actual Hour、Schedule Hour 的 DepartmentCategoryId 集合
         *
         * @param scheduleHourList Schedule Hour 数据
         * @param actualHourList   Actual Hour 数据
         * @return DepartmentCategoryId 集合
         * @author zc
         * @date 2025/4/23
         */
        private Set<Integer> getActualScheduleDepCategoryIds(List<SvaScheduleHour> scheduleHourList, List<SvaActualHour> actualHourList) {
            Set<Integer> depCategoryIds = Sets.newHashSet();
            depCategoryIds.addAll(scheduleHourList.stream().map(SvaScheduleHour::getDepCategoryId).toList());
            depCategoryIds.addAll(actualHourList.stream().map(SvaActualHour::getDepCategoryId).toList());
            return depCategoryIds;
        }

        /**
         * 获取部门分类指定日期范围内的时数统计数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return DepCategoryStatisticDTO 集合
         * @author zc
         * @date 2025/4/23
         */
        @Override
        public List<SvaDepCategoryStatisticDTO> getHoursStatistics(PropertyDTO propertyDTO, SvaHoursStatisticsQuery query) {
            // 获取隐藏的 Position Id
            List<String> noShowPositionIds = employeeService.listNoShowPositionIds(propertyDTO.getId());

            // 获取需要排除统计的部门代码
            List<String> excludeDepartmentCodes = ignoreConfigService.listConfig(propertyDTO.getId()).stream()
                    .map(SvaDepartmentIgnoreConfig::getCode).toList();

            // 格式化时间戳日期为 yyyyMMdd
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getFromDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getToDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));

            // 获取 Actual Hour
            List<SvaScheduleHour> scheduleHourList = scheduleHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, null, null);
            // 获取 Schedule Hour
            List<SvaActualHour> actualHourList = actualHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, null, null);

            // 获取 Actual Hour、Schedule Hour 的 PositionId 集合
            List<String> positionIds = Lists.newArrayList(getActualSchedulePositionIds(scheduleHourList, actualHourList));

            // 获取指定日期范围内 没有考勤、未隐藏、未离职或在选择日期后离职的员工数据（排除部门代码）
            List<AttendanceEmployee> noAttendanceEmployeeList = employeeService.listForNoAttendance(propertyDTO.getId(),
                    positionIds, query.getFromDate(), excludeDepartmentCodes);
            if (CollectionUtils.isEmpty(scheduleHourList) && CollectionUtils.isEmpty(actualHourList)
                    && CollectionUtils.isEmpty(noAttendanceEmployeeList)) {
                return Collections.emptyList();
            }

            // 获取 Actual Hour、Schedule Hour 的 DepartmentCategoryId 集合
            Set<Integer> departmentCategoryIdSet = getActualScheduleDepCategoryIds(scheduleHourList, actualHourList);
            departmentCategoryIdSet.addAll(noAttendanceEmployeeList.stream().map(AttendanceEmployee::getDepCategoryId).toList());
            List<Integer> departmentCategoryIdList = Lists.newArrayList(departmentCategoryIdSet);

            // 根据 categoryIds 获取分类信息
            Map<Integer, String> depId2DepName = departmentCategoryService.listByIds(departmentCategoryIdList).stream()
                    .collect(Collectors.toMap(DepartmentCategory::getId, DepartmentCategory::getName));

            // 获取启用日期在选择日期范围内或选择日期之前的最新的一条的 Department Category 推荐工时配置数据
            List<DepCategoryRecommendHour> depCategoryRecommendHours = depCategoryRecommendHourService.listByDateRangeWithNewOne(propertyDTO.getId(),
                    query.getFromDate(), query.getToDate());

            // 获取指定日期范围内手动设置的工时
            Map<Integer, CategoryRecomHourSum> categoryId2RecommendHoursSum = categoryRecommendHourSumService.listByDate(propertyDTO.getId(),
                            query.getFromDate(), query.getToDate(), departmentCategoryIdList).stream()
                    .collect(Collectors.toMap(CategoryRecomHourSum::getDepCategoryId, Function.identity()));

            // 获取指定日期范围内特殊活动设置的工时
            Map<Long, List<SpecialEvent>> theDate2EventList = specialEventService.listByDate(propertyDTO.getId(), query.getFromDate(),
                    query.getToDate()).stream().collect(Collectors.groupingBy(SpecialEvent::getTheDate));

            ZonedDateTime fromZonedDate = DateTimeUtils.toZonedDateTime(query.getFromDate(), propertyDTO.getTimezone());
            ZonedDateTime toZonedDate = DateTimeUtils.toZonedDateTime(query.getToDate(), propertyDTO.getTimezone());

            // 获取 sale forecast 数据
            Map<Long, SaleForecast> theDate2SaleForecast = saleForecastService.listByDateRange(propertyDTO.getId(),
                            fromZonedDate.minusDays(1).toEpochSecond(), toZonedDate.minusDays(1).toEpochSecond())
                    .stream().collect(Collectors.toMap(SaleForecast::getTheDate, v -> v));

            // 获取指定日期范围内的评论数据
            Map<Integer, Long> departmentCategoryId2CommentCount = commentService.listByDateRangeAndDepCategoryId(propertyDTO.getId(),
                            query.getFromDate(), query.getToDate(), departmentCategoryIdList).stream()
                    .collect(Collectors.groupingBy(SvaDepartmentCategoryStatisticsComment::getDepartmentCategoryId, Collectors.counting()));

            // 获取计数保留规则
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO.getInternationalization().getNumber().getCalculationRule());

            Map<Integer, List<SvaScheduleHour>> depCategoryId2ScheduleHourList = scheduleHourList.stream()
                    .collect(Collectors.groupingBy(SvaScheduleHour::getDepCategoryId));
            Map<Integer, List<SvaActualHour>> depCategoryId2ActualHourList = actualHourList.stream()
                    .collect(Collectors.groupingBy(SvaActualHour::getDepCategoryId));

            // 获取排除 Pay Code 配置
            List<String> excludePayCodeList = payCodeExcludeConfigService.listExcludePayCode(propertyDTO.getId());

            // 生成指定日期范围内部门分类考勤时数统计数据
            List<SvaDepCategoryStatisticDTO> depCategoryStatisticList = Lists.newArrayList();
            for (int departmentCategoryId : departmentCategoryIdList) {
                SvaDepCategoryStatisticDTO categoryStatisticDTO = new SvaDepCategoryStatisticDTO();
                // 设置分类 id
                categoryStatisticDTO.setDepartmentCategoryId(departmentCategoryId);

                // 设置分类名称
                String categoryName = depId2DepName.getOrDefault(departmentCategoryId, DepartmentCategory.DEFAULT_CATEGORY_NAME);
                categoryStatisticDTO.setCategoryName(categoryName);

                // 设置 recommend hours
                List<BigDecimal> recommendHours = Lists.newArrayList();
                CategoryRecomHourSum categoryRecomHourSum = categoryId2RecommendHoursSum.get(departmentCategoryId);
                if (Objects.nonNull(categoryRecomHourSum)) {
                    recommendHours.add(categoryRecomHourSum.getRecomHourSum());
                } else {
                    for (ZonedDateTime date = fromZonedDate; !date.isAfter(toZonedDate); date = date.plusDays(1)) {
                        recommendHours.add(getRecommendHoursForOneDay(departmentCategoryId, date,
                                theDate2SaleForecast, depCategoryRecommendHours, roundingMode));

                        recommendHours.add(getSpecialHoursForOneDay(departmentCategoryId, date.toEpochSecond(),
                                theDate2EventList, roundingMode));
                    }
                }
                BigDecimal recommendHoursTotal = recommendHours.stream().filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                categoryStatisticDTO.setRecommendHours(recommendHoursTotal);

                List<SvaScheduleHour> scheduleHours = depCategoryId2ScheduleHourList.getOrDefault(departmentCategoryId, Collections.emptyList());
                // 设置 scheduledHours
                BigDecimal scheduledHoursTotal = scheduleHours.stream().map(SvaScheduleHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                BigDecimal scheduledExcludePayCodeHoursTotal = scheduleHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaScheduleHour::getHours).reduce(BigDecimal::add).orElse(null);
                categoryStatisticDTO.setSchedule(new SvaDepCategoryStatisticValueDTO(scheduledHoursTotal, scheduledExcludePayCodeHoursTotal));

                List<SvaActualHour> actualHours = depCategoryId2ActualHourList.getOrDefault(departmentCategoryId, Collections.emptyList());
                // 设置 actualHours
                BigDecimal actualHoursTotal = actualHours.stream().map(SvaActualHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                BigDecimal actualExcludePayCodeHoursTotal = actualHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaActualHour::getHours).reduce(BigDecimal::add).orElse(null);
                categoryStatisticDTO.setActual(new SvaDepCategoryStatisticValueDTO(actualHoursTotal, actualExcludePayCodeHoursTotal));

                // 设置 saVariances
                BigDecimal saVariances = (Objects.nonNull(scheduledHoursTotal) && Objects.nonNull(actualHoursTotal)) ?
                        scheduledHoursTotal.subtract(actualHoursTotal) : null;
                BigDecimal saExcludePayCodeVariances = (Objects.nonNull(scheduledExcludePayCodeHoursTotal) && Objects.nonNull(actualExcludePayCodeHoursTotal)) ?
                        scheduledExcludePayCodeHoursTotal.subtract(actualExcludePayCodeHoursTotal) : null;
                categoryStatisticDTO.setSaVariances(new SvaDepCategoryStatisticVariancesDTO(saVariances, saExcludePayCodeVariances));

                // 设置 arVariances
                BigDecimal arVariances = (Objects.nonNull(actualHoursTotal) && Objects.nonNull(recommendHoursTotal)) ?
                        actualHoursTotal.subtract(recommendHoursTotal) : null;
                BigDecimal arExcludePayCodeVariances = (Objects.nonNull(actualExcludePayCodeHoursTotal) && Objects.nonNull(recommendHoursTotal)) ?
                        actualExcludePayCodeHoursTotal.subtract(recommendHoursTotal) : null;
                categoryStatisticDTO.setArVariances(new SvaDepCategoryStatisticVariancesDTO(arVariances, arExcludePayCodeVariances));

                // 设置 srVariances
                BigDecimal srVariances = (Objects.nonNull(scheduledHoursTotal) && Objects.nonNull(recommendHoursTotal)) ?
                        scheduledHoursTotal.subtract(recommendHoursTotal) : null;
                BigDecimal srExcludePayCodeVariances = (Objects.nonNull(scheduledExcludePayCodeHoursTotal) && Objects.nonNull(recommendHoursTotal)) ?
                        scheduledExcludePayCodeHoursTotal.subtract(recommendHoursTotal) : null;
                categoryStatisticDTO.setSrVariances(new SvaDepCategoryStatisticVariancesDTO(srVariances, srExcludePayCodeVariances));

                // 设置 commentCount
                categoryStatisticDTO.setCommentCount(departmentCategoryId2CommentCount.getOrDefault(departmentCategoryId, 0L));

                // 设置 category 在当前选择日期范围内对应的版本
                categoryStatisticDTO.setVersions(collectVersions(departmentCategoryId, fromZonedDate, toZonedDate, depCategoryRecommendHours));

                depCategoryStatisticList.add(categoryStatisticDTO);
            }
            return depCategoryStatisticList;
        }

        /**
         * 汇总 category 在当前选择日期范围内对应的版本
         *
         * @param categoryId                category id
         * @param fromDate                  开始日期
         * @param toDate                    结束日期
         * @param depCategoryRecommendHours 所有的推荐工时
         * @return 版本
         * @author yyt
         * @date 2025/4/1
         **/
        private Set<Long> collectVersions(Integer categoryId, ZonedDateTime fromDate, ZonedDateTime toDate, List<DepCategoryRecommendHour> depCategoryRecommendHours) {
            if (CollectionUtils.isEmpty(depCategoryRecommendHours)) {
                new HashSet<>();
            }

            Set<Long> versions = Sets.newHashSet();
            for (ZonedDateTime date = fromDate; !date.isAfter(toDate); date = date.plusDays(1)) {
                // 获取适用指定日期的 DepCategoryRecommendHour
                ZonedDateTime finalDate = date;
                Optional<DepCategoryRecommendHour> depCategoryRecommendHourOptional = depCategoryRecommendHours.stream()
                        .filter(item -> item.getStartingDate() <= finalDate.toEpochSecond())
                        .max(Comparator.comparing(DepCategoryRecommendHour::getStartingDate));
                if (depCategoryRecommendHourOptional.isEmpty()) {
                    continue;
                }

                DepCategoryRecommendHour depCategoryRecommendHour = depCategoryRecommendHourOptional.get();
                Map<String, Map<String, Object>> categoryId2RecommendHour = JsonUtils.convertStringByTypeReference(objectMapper,
                        depCategoryRecommendHour.getCategoryRecomHours(), new TypeReference<>() {
                        });

                Map<String, Object> dayOfWeek2RecommendHourObject = categoryId2RecommendHour.get(categoryId + StringUtils.EMPTY);
                if (Objects.isNull(dayOfWeek2RecommendHourObject)) {
                    continue;
                }

                versions.add(depCategoryRecommendHour.getStartingDate());
            }
            return versions;
        }

        /**
         * 获取指定分类在指定日期的推荐工时
         *
         * @param categoryId                部门分类 id
         * @param date                      指定日期
         * @param theDate2SaleForecast      theDate 2 SaleForecast 数据
         * @param depCategoryRecommendHours 推荐工时数据
         * @param roundingMode              计数保留规则
         * @return 推荐工时
         * @author zc
         * @date 2023/6/27
         */
        private BigDecimal getRecommendHoursForOneDay(int categoryId, ZonedDateTime date, Map<Long, SaleForecast> theDate2SaleForecast,
                                                      List<DepCategoryRecommendHour> depCategoryRecommendHours, RoundingMode roundingMode) {
            if (CollectionUtils.isEmpty(depCategoryRecommendHours)) {
                return null;
            }

            // 获取适用指定日期的 DepCategoryRecommendHour
            Optional<DepCategoryRecommendHour> depCategoryRecommendHourOptional = depCategoryRecommendHours.stream()
                    .filter(item -> item.getStartingDate() <= date.toEpochSecond())
                    .max(Comparator.comparing(DepCategoryRecommendHour::getStartingDate));
            if (depCategoryRecommendHourOptional.isEmpty()) {
                return null;
            }

            DepCategoryRecommendHour depCategoryRecommendHour = depCategoryRecommendHourOptional.get();
            Map<String, Map<String, Object>> categoryId2RecommendHour = JsonUtils.convertStringByTypeReference(objectMapper,
                    depCategoryRecommendHour.getCategoryRecomHours(), new TypeReference<>() {
                    });

            Map<String, Object> dayOfWeek2RecommendHourObject = categoryId2RecommendHour.get(categoryId + StringUtils.EMPTY);
            if (Objects.isNull(dayOfWeek2RecommendHourObject)) {
                return null;
            }

            Object recommendHourObject = dayOfWeek2RecommendHourObject.get(date.getDayOfWeek().getValue() + StringUtils.EMPTY);
            BigDecimal hours;
            if (Objects.isNull(recommendHourObject)) {
                hours = null;
            } else if (recommendHourObject instanceof String) {
                hours = null;
            } else if (recommendHourObject instanceof Integer) {
                hours = new BigDecimal(recommendHourObject.toString());
            } else if (recommendHourObject instanceof Double) {
                hours = new BigDecimal(recommendHourObject.toString());
            } else {
                // 获取可配置类型 RecommendHours 的考勤时数
                String recommendHourObjectStr = JsonUtils.convertObjToString(objectMapper, recommendHourObject);
                RecommendHoursConfigurableDTO recommendHoursConfigurableDTO = JsonUtils.convertStringToObject(objectMapper,
                        recommendHourObjectStr, RecommendHoursConfigurableDTO.class);
                hours = getRecommendHoursConfigurableHour(date.minusDays(1).toEpochSecond(), theDate2SaleForecast, recommendHoursConfigurableDTO);
            }

            if (Objects.nonNull(hours)) {
                hours = hours.setScale(BaseConstant.DATA_PRECISION, roundingMode);
            }
            return hours;
        }

        /**
         * 获取指定分类在指定日期特殊活动的工时
         *
         * @param categoryId        部门分类 id
         * @param date              指定日期
         * @param theDate2EventList 特殊活动工时
         * @param roundingMode      计数保留规则
         * @return java.math.BigDecimal
         * @author zc
         * @date 2023/6/28
         */
        private BigDecimal getSpecialHoursForOneDay(int categoryId, long date, Map<Long, List<SpecialEvent>> theDate2EventList, RoundingMode roundingMode) {
            List<SpecialEvent> specialEvents = theDate2EventList.get(date);
            if (CollectionUtils.isEmpty(specialEvents)) {
                return null;
            }

            BigDecimal totalHours = null;
            for (SpecialEvent specialEvent : specialEvents) {
                Map<String, BigDecimal> categoryId2Hours = JsonUtils.convertStringByTypeReference(objectMapper,
                        specialEvent.getCategoryRecomHours(), new TypeReference<>() {
                        });
                BigDecimal hours = categoryId2Hours.get(categoryId + StringUtils.EMPTY);
                if (Objects.nonNull(hours)) {
                    if (Objects.isNull(totalHours)) {
                        totalHours = hours;
                    } else {
                        totalHours = totalHours.add(hours);
                    }
                }
            }

            if (Objects.nonNull(totalHours)) {
                totalHours = totalHours.setScale(BaseConstant.DATA_PRECISION, roundingMode);
            }
            return totalHours;
        }

        /**
         * 获取可配置类型 RecommendHours 的考勤时数
         *
         * @param date                          指定日期
         * @param theDate2SaleForecast          theDate 2 SaleForecast 数据
         * @param recommendHoursConfigurableDTO 可配置类型 RecommendHours 数据
         * @return 推荐工时
         * @author zc
         * @date 2023/6/27
         */
        private BigDecimal getRecommendHoursConfigurableHour(long date, Map<Long, SaleForecast> theDate2SaleForecast,
                                                             RecommendHoursConfigurableDTO recommendHoursConfigurableDTO) {
            SaleForecast saleForecast = theDate2SaleForecast.get(date);
            if (Objects.isNull(saleForecast)) {
                return null;
            }
            Integer estiRoomSales = saleForecast.getEstiRoomSales();
            if (Objects.isNull(estiRoomSales)) {
                return null;
            }
            BigDecimal hour = recommendHoursConfigurableDTO.getHours();
            if (Objects.isNull(hour)) {
                return null;
            }

            // 前一天的预计房间售量 * work time(小时)
            return BigDecimal.valueOf(estiRoomSales).multiply(recommendHoursConfigurableDTO.getHours());
        }

        /**
         * 根据部门分类获取指定日期范围内每日的时数统计数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return DepCategoryDailyDTO
         * @author zc
         * @date 2023/6/28
         */
        @Override
        public SvaDepCategoryDailyDTO getHoursStatisticsDaily(PropertyDTO propertyDTO, SvaHoursStatisticsDailyQuery query) {
            // 获取隐藏的 Position Id
            List<String> noShowPositionIds = employeeService.listNoShowPositionIds(propertyDTO.getId());

            // 获取需要排除统计的部门代码
            List<String> excludeDepartmentCodes = ignoreConfigService.listConfig(propertyDTO.getId()).stream()
                    .map(SvaDepartmentIgnoreConfig::getCode).toList();

            // 格式化时间戳日期为 yyyyMMdd
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getFromDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(query.getToDate(),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));

            List<Integer> targetDepCategoryIds = Lists.newArrayList(query.getDepartmentCategoryId());
            // 获取 Actual Hour
            List<SvaScheduleHour> scheduleHourList = scheduleHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, null, targetDepCategoryIds);
            // 获取 Schedule Hour
            List<SvaActualHour> actualHourList = actualHourService.listEffective(propertyDTO.getId(),
                    startDate, endDate, noShowPositionIds, excludeDepartmentCodes,
                    null, null, targetDepCategoryIds);

            ZonedDateTime fromZonedDate = DateTimeUtils.toZonedDateTime(query.getFromDate(), propertyDTO.getTimezone());
            ZonedDateTime toZonedDate = DateTimeUtils.toZonedDateTime(query.getToDate(), propertyDTO.getTimezone());

            // 判断指定日期范围内是否手动设置了工时
            boolean noSetRecommendHourSum = CollectionUtils.isEmpty(categoryRecommendHourSumService.listByDate(propertyDTO.getId(),
                    query.getFromDate(), query.getToDate(), query.getDepartmentCategoryId()));

            Map<Long, SaleForecast> theDate2SaleForecast = Collections.emptyMap();
            List<DepCategoryRecommendHour> depCategoryRecommendHours = Collections.emptyList();
            Map<Long, List<SpecialEvent>> theDate2EventList = Collections.emptyMap();
            if (noSetRecommendHourSum) {
                // 获取 sale forecast 数据
                theDate2SaleForecast = saleForecastService.listByDateRange(propertyDTO.getId(),
                                fromZonedDate.minusDays(1).toEpochSecond(), toZonedDate.minusDays(1).toEpochSecond())
                        .stream().collect(Collectors.toMap(SaleForecast::getTheDate, v -> v));

                // 获取启用日期在选择日期范围内或选择日期之前的最新的一条的 Department Category Recommend Hour
                depCategoryRecommendHours = depCategoryRecommendHourService.listByDateRangeWithNewOne(propertyDTO.getId(), query.getFromDate(), query.getToDate());

                // 获取指定日期范围内特殊活动设置的工时
                theDate2EventList = specialEventService.listByDate(propertyDTO.getId(), query.getFromDate(), query.getToDate()).stream()
                        .collect(Collectors.groupingBy(SpecialEvent::getTheDate));
            }

            Map<Long, List<SvaScheduleHour>> payDate2ScheduleHourList = scheduleHourList.stream()
                    .collect(Collectors.groupingBy(SvaScheduleHour::getPayDate));
            Map<Long, List<SvaActualHour>> payDate2ActualHourList = actualHourList.stream()
                    .collect(Collectors.groupingBy(SvaActualHour::getPayDate));

            // 获取资产国际化配置数据
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO.getInternationalization().getNumber().getCalculationRule());

            // 获取排除 Pay Code 配置
            List<String> excludePayCodeList = payCodeExcludeConfigService.listExcludePayCode(propertyDTO.getId());

            List<SvaDepCategoryStatisticValueDTO> scheduleHoursList = Lists.newArrayList();
            List<SvaDepCategoryStatisticValueDTO> actualHoursList = Lists.newArrayList();
            List<SvaRecommendHoursDTO> recommendHoursList = Lists.newArrayList();
            for (ZonedDateTime date = fromZonedDate; !date.isAfter(toZonedDate); date = date.plusDays(1)) {
                Long payDate = DateTimeUtils.parseToLong(date, BaseConstant.DATE_FORMAT_YYYYMMDD);
                List<SvaScheduleHour> scheduleHours = payDate2ScheduleHourList.getOrDefault(payDate, Collections.emptyList());
                // 设置 Schedule Hours 数据
                BigDecimal scheduledHoursTotal = scheduleHours.stream().map(SvaScheduleHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                BigDecimal scheduledExcludePayCodeHoursTotal = scheduleHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaScheduleHour::getHours).reduce(BigDecimal::add).orElse(null);
                scheduleHoursList.add(new SvaDepCategoryStatisticValueDTO(scheduledHoursTotal, scheduledExcludePayCodeHoursTotal));

                List<SvaActualHour> actualHours = payDate2ActualHourList.getOrDefault(payDate, Collections.emptyList());
                // 设置 Actual Hours 数据
                BigDecimal actualHoursTotal = actualHours.stream().map(SvaActualHour::getHours)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                BigDecimal actualExcludePayCodeHoursTotal = actualHours.stream().filter(item ->
                                excludePayCodeList.stream().noneMatch(s -> s.equalsIgnoreCase(item.getPayCode())) && Objects.nonNull(item.getHours()))
                        .map(SvaActualHour::getHours).reduce(BigDecimal::add).orElse(null);
                actualHoursList.add(new SvaDepCategoryStatisticValueDTO(actualHoursTotal, actualExcludePayCodeHoursTotal));

                // 如果手动设置了推荐工时，则推荐工时显示为空
                if (noSetRecommendHourSum) {
                    // 获取当前日期推荐工时
                    BigDecimal recommendedHours = getRecommendHoursForOneDay(query.getDepartmentCategoryId(), date, theDate2SaleForecast,
                            depCategoryRecommendHours, roundingMode);

                    // 获取当前日期特殊活动设置的工时
                    BigDecimal specialHours = getSpecialHoursForOneDay(query.getDepartmentCategoryId(), date.toEpochSecond(),
                            theDate2EventList, roundingMode);

                    recommendHoursList.add(SvaRecommendHoursDTO.builder()
                            .categoryRecommendHours(recommendedHours)
                            .specialEventsRecommendHours(specialHours)
                            .build());
                } else {
                    recommendHoursList.add(new SvaRecommendHoursDTO());
                }
            }
            return SvaDepCategoryDailyDTO.builder()
                    .scheduleHoursList(scheduleHoursList)
                    .actualHoursList(actualHoursList)
                    .recommendHoursList(recommendHoursList)
                    .build();
        }
    }