# SvaWidgetServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.workperformance.entity.sva.avhours.SvaActualHour;
    import cloud.wconnect.workperformance.entity.sva.employees.SvaDepartmentIgnoreConfig;
    import cloud.wconnect.workperformance.entity.sva.operatingstatistics.OperatingStatistics;
    import cloud.wconnect.workperformance.entity.sva.setting.SalaryPayConfig;
    import cloud.wconnect.workperformance.mapper.sva.setting.SalaryPayConfigMapper;
    import cloud.wconnect.workperformance.pojo.dto.sva.widget.SvaActualValueAmountDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.widget.SvaActualValueCountDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.widget.SvaActualValueDTO;
    import cloud.wconnect.workperformance.pojo.query.sva.widget.SvaWidgetDateQuery;
    import cloud.wconnect.workperformance.service.sva.actualschedulehour.ISvaActualHourService;
    import cloud.wconnect.workperformance.service.sva.employees.ISvaDepartmentIgnoreConfigService;
    import cloud.wconnect.workperformance.service.sva.operatingstatistics.IOperatingStatisticsService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.IScheduleVsActualService;
    import cloud.wconnect.workperformance.service.sva.widget.ISvaWidgetService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.time.ZonedDateTime;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * Service: sva widget
     *
     * @author zc
     * @date 2023/12/08
     */
    @Service
    @DS("work_performance")
    public class SvaWidgetServiceImpl implements ISvaWidgetService {
        @Resource
        private IScheduleVsActualService scheduleVsActualService;
        @Resource
        private IOperatingStatisticsService operatingStatisticsService;
        @Resource
        private ISvaActualHourService actualHourService;
        @Resource
        private ISvaDepartmentIgnoreConfigService ignoreConfigService;
        @Resource
        private SalaryPayConfigMapper salaryPayConfigMapper;

        /**
         * 获取实际数据【营收，工资，售房数量】
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return SvaWidgetActualValueDTO
         * @author zc
         * @date 2023/12/8
         */
        @Override
        public SvaActualValueDTO getActualValue(PropertyDTO propertyDTO, SvaWidgetDateQuery query) {
            long fromDate = DateTimeUtils.dateStrToEpochSecond(query.getStartDate().toString(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone());
            ZonedDateTime fromZonedDate = DateTimeUtils.toZonedDateTime(fromDate, propertyDTO.getTimezone());

            long toDate = DateTimeUtils.dateStrToEpochSecond(query.getEndDate().toString(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone());
            ZonedDateTime toZonedDate = DateTimeUtils.toZonedDateTime(toDate, propertyDTO.getTimezone());

            // 查询指定日期范围内 Operating statistics
            Map<Long, OperatingStatistics> theDate2OperatingStatistics = operatingStatisticsService.list(propertyDTO.getId(),
                    fromDate, toDate).stream().collect(Collectors.toMap(OperatingStatistics::getTheDate, Function.identity()));
            // 获取实际营收
            List<SvaActualValueAmountDTO> actualRevenue = listActualRevenue(fromZonedDate, toZonedDate, theDate2OperatingStatistics);
            // 获取实际售房数量
            List<SvaActualValueCountDTO> actualRoomSold = listActualRoomSold(fromZonedDate, toZonedDate, theDate2OperatingStatistics);

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

            // 获取 Actual Hour
            List<SvaActualHour> actualHourList = actualHourService.listEffective(propertyDTO.getId(),
                    query.getStartDate(), query.getEndDate(), null, excludeDepartmentCodes,
                    null, null, null);

            // 获取 salary pay
            List<SalaryPayConfig> salaryPayConfigs = salaryPayConfigMapper.listByDate(propertyDTO.getId(), fromDate, toDate);

            // 获取实际工资支出
            List<SvaActualValueAmountDTO> actualPayroll = listActualPayroll(fromZonedDate, toZonedDate, actualHourList, salaryPayConfigs);

            return SvaActualValueDTO.builder()
                    .actualRevenueList(actualRevenue)
                    .actualRoomsSoldList(actualRoomSold)
                    .actualPayrollList(actualPayroll)
                    .build();
        }

        /**
         * 获取实际营收
         *
         * @param fromZonedDate               开始日期
         * @param toZonedDate                 结束日期
         * @param theDate2OperatingStatistics theDate 2 OperatingStatistics
         * @return 实际营收集合
         * @author zc
         * @date 2023/12/8
         */
        private List<SvaActualValueAmountDTO> listActualRevenue(ZonedDateTime fromZonedDate, ZonedDateTime toZonedDate,
                                                                Map<Long, OperatingStatistics> theDate2OperatingStatistics) {
            List<SvaActualValueAmountDTO> revenueList = Lists.newArrayList();
            for (ZonedDateTime date = fromZonedDate; !date.isAfter(toZonedDate); date = date.plusDays(1)) {
                long theDate = date.toEpochSecond();
                OperatingStatistics operatingStatistics = theDate2OperatingStatistics.get(theDate);
                revenueList.add(SvaActualValueAmountDTO.builder()
                        .date(DateTimeUtils.parseZonedDateTimeToStr(date, BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                        .value(Objects.nonNull(operatingStatistics) ? operatingStatistics.getTotalRev() : BigDecimal.ZERO)
                        .build());
            }
            return revenueList;
        }

        /**
         * 获取实际售房数量
         *
         * @param fromZonedDate               开始日期
         * @param toZonedDate                 结束日期
         * @param theDate2OperatingStatistics theDate 2 OperatingStatistics
         * @return 实际售房数量集合
         * @author zc
         * @date 2023/12/8
         */
        private List<SvaActualValueCountDTO> listActualRoomSold(ZonedDateTime fromZonedDate, ZonedDateTime toZonedDate,
                                                                Map<Long, OperatingStatistics> theDate2OperatingStatistics) {
            List<SvaActualValueCountDTO> roomSoldList = Lists.newArrayList();
            for (ZonedDateTime date = fromZonedDate; !date.isAfter(toZonedDate); date = date.plusDays(1)) {
                long theDate = date.toEpochSecond();
                OperatingStatistics operatingStatistics = theDate2OperatingStatistics.get(theDate);
                roomSoldList.add(SvaActualValueCountDTO.builder()
                        .date(DateTimeUtils.parseZonedDateTimeToStr(date, BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                        .value(Objects.nonNull(operatingStatistics) ? operatingStatistics.getRoomsRented() : BaseConstant.ZERO)
                        .build());
            }
            return roomSoldList;
        }

        /**
         * 获取实际工资支出
         *
         * @param fromZonedDate       开始日期
         * @param toZonedDate         结束日期
         * @param actualHourList      Actual Hour 数据
         * @param salaryPayConfigs    salary payroll config
         * @return 实际工资支出集合
         * @author zc
         * @date 2023/12/8
         */
        private List<SvaActualValueAmountDTO> listActualPayroll(ZonedDateTime fromZonedDate, ZonedDateTime toZonedDate,
                                                                List<SvaActualHour> actualHourList, List<SalaryPayConfig> salaryPayConfigs) {
            Map<Long, List<SvaActualHour>> payDate2ActualHours = actualHourList.stream()
                    .collect(Collectors.groupingBy(SvaActualHour::getPayDate));

            List<SvaActualValueAmountDTO> payrollList = Lists.newArrayList();
            for (ZonedDateTime date = fromZonedDate; !date.isAfter(toZonedDate); date = date.plusDays(1)) {
                long payDate = DateTimeUtils.parseToLong(date, BaseConstant.DATE_FORMAT_YYYYMMDD);

                BigDecimal salaryPayroll = null;
                if (CollectionUtils.isNotEmpty(salaryPayConfigs)) {
                    // 获取适用当前日期的 salary pay config 设置的工资
                    salaryPayroll = scheduleVsActualService.getAppropriateSalaryPayMoney(salaryPayConfigs, date);
                }

                BigDecimal actualPayroll = payDate2ActualHours.getOrDefault(payDate, Collections.emptyList()).stream()
                        .map(SvaActualHour::getPayroll).filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                // 累加 salary pay
                if (Objects.nonNull(salaryPayroll)) {
                    if (Objects.isNull(actualPayroll)) {
                        actualPayroll = salaryPayroll;
                    } else {
                        actualPayroll = actualPayroll.add(salaryPayroll);
                    }
                }

                payrollList.add(SvaActualValueAmountDTO.builder()
                        .date(DateTimeUtils.parseZonedDateTimeToStr(date, BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                        .value(Objects.nonNull(actualPayroll) ? actualPayroll : BigDecimal.ZERO)
                        .build());
            }

            return payrollList;
        }
    }