# HotelKpiMetricsStaffingLevelService.java

    package cloud.wconnect.workperformance.service.hotelkpi.impl.metrics;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyFteValueDTO;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiMetricsEnum;
    import cloud.wconnect.workperformance.entity.hotelkpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCrawlValueIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginDetailDTO;
    import cloud.wconnect.workperformance.service.hotelkpi.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.YearMonth;
    import java.time.ZoneId;
    import java.time.temporal.TemporalAdjusters;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.Collectors;

    /**
     * @author zc
     * @date 2025/09/13
     */
    @Service
    @DS("work_performance")
    public class HotelKpiMetricsStaffingLevelService implements IHotelKpiMetricsService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiWeeklyService weeklyService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiQuarterlyService quarterlyService;
        @Resource
        private IHotelKpiYearlyService yearlyService;
        @Resource
        private IHotelKpiWconnectEmployeeRequestService employeeRequestService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private PropertyInternalService propertyInternalService;
        @Resource
        private ObjectMapper objectMapper;

        private static final int METRICS_TYPE = HotelKpiMetricsEnum.STAFFING_LEVEL_FULL_TIME.getMetricsType();

        /**
         * 支持的指标类型
         *
         * @param metricsType 指标类型
         * @return boolean
         * @author zc
         * @date 2025/9/13
         */
        @Override
        public boolean supports(int metricsType) {
            return METRICS_TYPE == metricsType;
        }

        /**
         * 初始化
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void initData(PropertyDTO propertyDTO, LocalDate nowLocalDate) {
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.getMetricsConfigByTypeForUpdate(propertyDTO.getId(), METRICS_TYPE);
            if (Objects.isNull(metricsConfig)) {
                return;
            }

            // 查询资产的fte配置
            PropertyFteValueDTO propertyFte = propertyInternalService.getPropertyFte(propertyDTO.getId());

            initWeekly(propertyDTO, nowLocalDate, propertyFte);
            initMonthly(propertyDTO, nowLocalDate, propertyFte);
            initQuarterly(propertyDTO, nowLocalDate, propertyFte);
            initYearly(propertyDTO, nowLocalDate, propertyFte);
        }

        /**
         * 初始化 for Weekly
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        private void initWeekly(PropertyDTO propertyDTO, LocalDate nowLocalDate, PropertyFteValueDTO propertyFte) {
            // 当前日期若是周起始日，则生成上一周期的数据
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());
            if (nowLocalDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                return;
            }
            // 获取上一周期的起始日期
            LocalDate lastWeekStartLocalDate = nowLocalDate.minusDays(7);
            // 设置数据
            setWeekly(propertyDTO, lastWeekStartLocalDate, lastWeekStartLocalDate, propertyFte);
        }

        /**
         * 初始化 for Monthly
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        private void initMonthly(PropertyDTO propertyDTO, LocalDate nowLocalDate, PropertyFteValueDTO propertyFte) {
            // 获取当前日期，如果不是当月第一天，则不处理
            if (nowLocalDate.getDayOfMonth() != 1) {
                return;
            }
            // 当前时间生成上一周期数据，需要减去一个月
            LocalDate lastMonthLocalDate = nowLocalDate.minusMonths(1);
            // 设置数据
            setMonthly(propertyDTO, lastMonthLocalDate, lastMonthLocalDate, propertyFte);
        }

        /**
         * 初始化 for Quarterly
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        private void initQuarterly(PropertyDTO propertyDTO, LocalDate nowLocalDate, PropertyFteValueDTO propertyFte) {
            LocalDate firstLocalDateOfQuarter = LocalDate.of(nowLocalDate.getYear(),
                    nowLocalDate.getMonth().firstMonthOfQuarter(), 1);
            // 获取当前日期，若不是季度的第一天，则不处理
            if (!firstLocalDateOfQuarter.isEqual(nowLocalDate)) {
                return;
            }
            // 当前时间生成上一周期数据，需要减去3个月
            LocalDate lastQuarterLocalDate = nowLocalDate.minusMonths(3);
            // 设置数据
            setQuarterly(propertyDTO, lastQuarterLocalDate, lastQuarterLocalDate, propertyFte);
        }

        /**
         * 初始化 for Yearly
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        private void initYearly(PropertyDTO propertyDTO, LocalDate nowLocalDate, PropertyFteValueDTO propertyFte) {
            // 获取当前日期，如果不是当月第一天，则不处理
            if (nowLocalDate.getDayOfMonth() != 1) {
                return;
            }
            // 当前时间生成截至到上个月的 YTD 数据，需要减去一个月
            LocalDate lastMonthLocalDate = nowLocalDate.minusMonths(1);
            // 设置数据
            setYearly(propertyDTO, lastMonthLocalDate, lastMonthLocalDate, propertyFte);
        }

        /**
         * 触发设置数据
         *
         * @param propertyDTO 资产
         * @param triggerType 触发类型，不同类型的日期格式不同
         * @param startDate   开始日期(yyyyMMdd,yyyyMM,yyyy)
         * @param endDate     结束日期(yyyyMMdd,yyyyMM,yyyy)
         * @author zc
         * @date 2025/9/12
         */
        @Override
        public void triggerSetData(PropertyDTO propertyDTO, int triggerType, long startDate, long endDate) {
            // 无需处理
        }

        /**
         * 设置数据
         *
         * @param propertyDTO    资产
         * @param dateType       日期类型
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @author zc
         * @date 2025/9/11
         */
        @Override
        public void setData(PropertyDTO propertyDTO, int dateType, LocalDate startLocalDate, LocalDate endLocalDate) {
            // 无需处理
        }

        /**
         * 设置数据 for Weekly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param propertyFte    资产配置的FTE
         * @author zc
         * @date 2025/9/11
         */
        private void setWeekly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, PropertyFteValueDTO propertyFte) {
            int propertyId = propertyDTO.getId();
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 转换为周的开始日期
            LocalDate startWeekLocalDate = startLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));
            LocalDate endWeekLocalDate = endLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));

            // 获取 Staffing Level 数据
            HotelKpiWconnectEmployeeRequest employeeRequest = employeeRequestService.getByPropertyId(propertyId);

            // 获取原始KPI数据
            long startWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(startWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(endWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            Map<Long, String> date2OriginDetail = weeklyService.listByDateRangeAndMetricsType(propertyId,
                            startWeekDate, endWeekDate, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiWeekly::getWeek, HotelKpiWeekly::getDetail));
            List<HotelKpiWeekly> saveList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startWeekLocalDate; !indexLocalDate.isAfter(endWeekLocalDate); indexLocalDate = indexLocalDate.plusWeeks(1)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

                String originDetail = date2OriginDetail.get(indexDate);
                HotelKpiOriginDetailDTO originDetailDTO = hotelKpiService.convert2OriginDetailDTOList(originDetail);

                // 设置 actual
                setMetricsValue(originDetailDTO, employeeRequest, propertyFte, roundingMode);

                String detail = JsonUtils.convertObjToString(objectMapper, originDetailDTO);

                saveList.add(HotelKpiWeekly.builder()
                        .propertyId(propertyId)
                        .week(indexDate)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            weeklyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Monthly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param propertyFte    资产配置的FTE
         * @author zc
         * @date 2025/9/11
         */
        private void setMonthly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, PropertyFteValueDTO propertyFte) {
            int propertyId = propertyDTO.getId();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            YearMonth startYearMonth = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonthValue());
            YearMonth endYearMonth = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonthValue());

            // 获取 Staffing Level 数据
            HotelKpiWconnectEmployeeRequest employeeRequest = employeeRequestService.getByPropertyId(propertyId);

            // 获取原始KPI数据
            long startMonth = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            long endMonth = YearMonthUtils.parseYearMonthToLong(endYearMonth);
            Map<Long, String> date2OriginDetail = monthlyService.listByDateRangeAndMetricsType(propertyId,
                            startMonth, endMonth, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiMonthly::getMonth, HotelKpiMonthly::getDetail));

            List<HotelKpiMonthly> saveList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(1)) {
                long indexMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                String originDetail = date2OriginDetail.get(indexMonth);
                HotelKpiOriginDetailDTO originDetailDTO = hotelKpiService.convert2OriginDetailDTOList(originDetail);

                // 设置 actual
                setMetricsValue(originDetailDTO, employeeRequest, propertyFte, roundingMode);

                String detail = JsonUtils.convertObjToString(objectMapper, originDetailDTO);

                saveList.add(HotelKpiMonthly.builder()
                        .propertyId(propertyId)
                        .month(indexMonth)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            monthlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Quarterly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param propertyFte    资产配置的FTE
         * @author zc
         * @date 2025/9/11
         */
        private void setQuarterly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, PropertyFteValueDTO propertyFte) {
            int propertyId = propertyDTO.getId();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            YearMonth startYearMonthOfQuarter = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonth().firstMonthOfQuarter());
            YearMonth endYearMonthOfQuarter = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonth().firstMonthOfQuarter());

            // 获取 Staffing Level 数据
            HotelKpiWconnectEmployeeRequest employeeRequest = employeeRequestService.getByPropertyId(propertyId);

            // 获取原始KPI数据
            long startQuarterDate = YearMonthUtils.parseYearMonthToLong(startYearMonthOfQuarter);
            long endQuarterDate = YearMonthUtils.parseYearMonthToLong(endYearMonthOfQuarter);
            Map<Long, String> month2OriginDetail = quarterlyService.listByDateRangeAndMetricsType(propertyId,
                            startQuarterDate, endQuarterDate, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiQuarterly::getMonth, HotelKpiQuarterly::getDetail));

            List<HotelKpiQuarterly> saveList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonthOfQuarter; !indexYearMonth.isAfter(endYearMonthOfQuarter); indexYearMonth = indexYearMonth.plusMonths(3)) {
                long indexStartMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                String originDetail = month2OriginDetail.get(indexStartMonth);
                HotelKpiOriginDetailDTO originDetailDTO = hotelKpiService.convert2OriginDetailDTOList(originDetail);

                // 设置 actual
                setMetricsValue(originDetailDTO, employeeRequest, propertyFte, roundingMode);

                String detail = JsonUtils.convertObjToString(objectMapper, originDetailDTO);

                saveList.add(HotelKpiQuarterly.builder()
                        .propertyId(propertyId)
                        .month(indexStartMonth)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            quarterlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Yearly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param propertyFte    资产配置的FTE
         * @author zc
         * @date 2025/9/11
         */
        private void setYearly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, PropertyFteValueDTO propertyFte) {
            int propertyId = propertyDTO.getId();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());
            // 获取上个月的日期
            YearMonth lastMonthYearMonth = YearMonth.now(ZoneId.of(propertyDTO.getTimezone())).minusMonths(1);

            int startYear = startLocalDate.getYear();
            int endYear = endLocalDate.getYear();

            // 获取 Staffing Level 数据
            HotelKpiWconnectEmployeeRequest employeeRequest = employeeRequestService.getByPropertyId(propertyId);

            // 获取原始KPI数据
            Map<Long, String> year2OriginDetail = yearlyService.listByDateRangeAndMetricsType(propertyId,
                            startYear, endYear, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiYearly::getYear, HotelKpiYearly::getDetail));

            List<HotelKpiYearly> saveList = Lists.newArrayList();
            for (int indexYear = startYear; indexYear <= endYear; indexYear++) {
                // 若年份大于当前上个月的年份，则跳过
                if (indexYear > lastMonthYearMonth.getYear()) {
                    continue;
                }
                String originDetail = year2OriginDetail.get((long) indexYear);
                HotelKpiOriginDetailDTO originDetailDTO = hotelKpiService.convert2OriginDetailDTOList(originDetail);

                // 设置 actual
                setMetricsValue(originDetailDTO, employeeRequest, propertyFte, roundingMode);

                String detail = JsonUtils.convertObjToString(objectMapper, originDetailDTO);

                saveList.add(HotelKpiYearly.builder()
                        .propertyId(propertyId)
                        .year((long) indexYear)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            yearlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置指标值
         *
         * @param employeeRequest 数据集合
         * @param originDetailDTO 原始数据详情
         * @param propertyFte     资产配置的FTE
         * @param roundingMode    舍入模式
         * @author zc
         * @date 2025/9/15
         */
        private void setMetricsValue(HotelKpiOriginDetailDTO originDetailDTO, HotelKpiWconnectEmployeeRequest employeeRequest,
                                     PropertyFteValueDTO propertyFte, RoundingMode roundingMode) {
            // 当前全职员工数量
            BigDecimal currentFullTime = Objects.isNull(employeeRequest.getCurrentFullTime()) ?
                    null : new BigDecimal(employeeRequest.getCurrentFullTime().toString());
            // 资产配置的全职人力成本FTE
            BigDecimal configFullTimeFte = propertyFte.getLaborCostFte();
            // 当前兼职员工数量
            BigDecimal currentPartTime = Objects.isNull(employeeRequest.getCurrentPartTime()) ?
                    null : new BigDecimal(employeeRequest.getCurrentPartTime().toString());
            // 资产配置的兼职人力成本FTE
            BigDecimal configPartTimeFte = propertyFte.getPartTimeLaborCostFte();

            // 当前全职员工数量 * 全职的FTE + 当前兼职员工数量 * 兼职的FTE
            BigDecimal currentFte = null;
            if (Objects.nonNull(currentFullTime)) {
                //当前全职员工数量 * 全职的FTE
                currentFte = currentFullTime.multiply(configFullTimeFte);
            }

            if (Objects.nonNull(currentPartTime)) {
                // 当前兼职员工数量 * 兼职的FTE
                BigDecimal currentPartTimeFte = currentPartTime.multiply(configPartTimeFte);

                // 累计FTE
                if (Objects.nonNull(currentFte)) {
                    currentFte = currentFte.add(currentPartTimeFte);
                } else {
                    currentFte = currentPartTimeFte;
                }
            }
            if (Objects.nonNull(currentFte)) {
                currentFte = currentFte.setScale(BaseConstant.DATA_PRECISION, roundingMode);
            }

            // 计算因子集合
            HotelKpiOriginCalculationFactorDTO factorIndex1 = new HotelKpiOriginCalculationFactorDTO().setIndex(1).setCrawl(currentFte)
                    .setCrawlOrigin(Lists.newArrayList(
                            HotelKpiOriginCrawlValueIndexDTO.builder().index(1).value(currentFullTime).build(),
                            HotelKpiOriginCrawlValueIndexDTO.builder().index(2).value(configFullTimeFte).build(),
                            HotelKpiOriginCrawlValueIndexDTO.builder().index(3).value(currentPartTime).build(),
                            HotelKpiOriginCrawlValueIndexDTO.builder().index(4).value(configPartTimeFte).build()));
            // 替换指标原始数值的 Actual 计算因子 1
            hotelKpiService.replaceOriginFactorList(originDetailDTO.getActual().getCalculationFactor(), factorIndex1);

            // 组装新的 Actual 计算因子 2
            BigDecimal requestFte = Objects.isNull(employeeRequest.getRequestFte()) ?
                    null : new BigDecimal(employeeRequest.getRequestFte().toString());
            if (Objects.nonNull(requestFte)) {
                requestFte = requestFte.setScale(BaseConstant.DATA_PRECISION, roundingMode);
            }
            HotelKpiOriginCalculationFactorDTO factorIndex2 = new HotelKpiOriginCalculationFactorDTO().setIndex(2).setCrawl(requestFte)
                    .setCrawlOrigin(List.of(HotelKpiOriginCrawlValueIndexDTO.builder().index(1).value(requestFte).build()));

            // 替换指标原始数值的 Actual 计算因子 2
            hotelKpiService.replaceOriginFactorList(originDetailDTO.getActual().getCalculationFactor(), factorIndex2);
        }

    }