# HotelKpiController.java

    package cloud.wconnect.workperformance.controller.hotelkpi;

    import cloud.wconnect.commonlib.annotation.ApiId;
    import cloud.wconnect.commonlib.component.SessionContext;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.jsonview.Views;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiComment;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiWarningComment;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfigv2.HotelKpiV2FactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfigv2.HotelKpiV2FactorConfigDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiCommentDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiMetricsItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiWarningCommentDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.save.HotelKpiMetricsSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.save.HotelKpiMetricsSaveValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.HotelKpiV2DTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.editconfig.HotelKpiV2EditConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.editconfig.HotelKpiV2EditConfigDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.save.HotelKpiV2MetricsSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.strdata.week.HotelKpiStrWeekDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.strdata.HotelKpiStrImportReportDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.strdata.week.save.HotelKpiStrWeekSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.HotelKpiSettingCalculationDTO;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiCommentQuery;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiMetricsItemQuery;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiQuery;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiWarningCommentQuery;
    import cloud.wconnect.workperformance.service.hotelkpi.*;
    import cloud.wconnect.workperformance.service.hotelkpi.str.HotelKpiStrWeekParseService;
    import com.fasterxml.jackson.annotation.JsonView;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.ImmutableMap;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.web.bind.annotation.*;

    import java.text.MessageFormat;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.YearMonth;
    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;

    /**
     * <p>
     * Hotel KPI 前端控制器
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @RestController
    @RequestMapping("/hotel-kpi")
    public class HotelKpiController {

        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiMetricsSettingService metricsSettingService;
        @Resource
        private IHotelKpiWeeklyService weeklyService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiQuarterlyService quarterlyService;
        @Resource
        private IHotelKpiYearlyService yearlyService;
        @Resource
        private IHotelKpiStrWeekService strWeekService;
        @Resource
        private IHotelKpiCommentService commentService;
        @Resource
        private IHotelKpiWarningCommentService warningCommentService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private I18nMessageUtils i18nMessageUtils;
        @Resource
        private SessionContext sessionContext;
        @Resource
        private HotelKpiStrWeekParseService strWeekParseService;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 获取 Hotel KPI 数据
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2024/6/24
         */
        @ApiId(1206)
        @GetMapping
        public List<HotelKpiDTO> list(@RequestParam String property, @Valid HotelKpiQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            if (query.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                // 校验查询参数
                checkQueryParamForWeekly(propertyId, query);
                return weeklyService.listHotelKpi(propertyDTO, query);
            } else if (query.getDateType() == HotelKpiDateTypeCons.MONTHLY) {
                // 校验查询参数
                checkQueryParamForMonthly(query);
                return monthlyService.listHotelKpi(propertyDTO, query);
            } else if (query.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                // 校验查询参数
                checkQueryParamForQuarterly(query);
                return quarterlyService.listHotelKpi(propertyDTO, query);
            } else {
                return yearlyService.listHotelKpi(propertyDTO, query);
            }
        }

        /**
         * 获取 Hotel KPI V2 数据
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2025/8/18
         */
        @ApiId(1570)
        @GetMapping("/v2")
        public List<HotelKpiV2DTO> listV2(@RequestParam String property, @Valid HotelKpiQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            if (query.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                // 校验查询参数
                checkQueryParamForWeekly(propertyId, query);
                return weeklyService.listHotelKpiV2(propertyDTO, query);
            } else if (query.getDateType() == HotelKpiDateTypeCons.MONTHLY) {
                // 校验查询参数
                checkQueryParamForMonthly(query);
                return monthlyService.listHotelKpiV2(propertyDTO, query);
            } else if (query.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                // 校验查询参数
                checkQueryParamForQuarterly(query);
                return quarterlyService.listHotelKpiV2(propertyDTO, query);
            } else {
                return yearlyService.listHotelKpiV2(propertyDTO, query);
            }
        }

        /**
         * 校验查询参数 for Weekly
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @author zc
         * @date 2025/8/18
         */
        private void checkQueryParamForWeekly(int propertyId, HotelKpiQuery query) {
            // 查询日期不能跨年；由于 Weekly 视图的查询日期是数据基于日期，当前周生成上一周的数据，所以这里的查询日期都需要减去 7 天
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).minusDays(7);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).minusDays(7);

            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

            if (startLocalDate.getYear() != endLocalDate.getYear()) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.START_END_DATE_MUST_IN_SAME_YEAR,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The startDate and endDate must be in the same year; startDate:{0}, endDate:{1}",
                                startDate, endDate));
            }
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            // 校验查询日期是否符合设置的周起始日
            if (startLocalDate.getDayOfWeek().compareTo(dayOfWeek) != 0 || endLocalDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The startDate and endDate must be the first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
            }
            // 重新设置查询日期，查询指定周期的数据
            query.setStartDate(startDate);
            query.setEndDate(endDate);
        }

        /**
         * 校验查询参数 for Monthly
         *
         * @param query 查询条件
         * @author zc
         * @date 2025/8/18
         */
        private void checkQueryParamForMonthly(HotelKpiQuery query) {
            // 查询日期不能跨年
            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getStartDate()));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getEndDate()));
            if (startYearMonth.getYear() != endYearMonth.getYear()) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.START_END_DATE_MUST_IN_SAME_YEAR,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The startDate and endDate must be in the same year; startDate:{0}, endDate:{1}",
                                query.getStartDate(), query.getEndDate()));
            }
        }

        /**
         * 校验查询参数 for Quarterly
         *
         * @param query 查询条件
         * @author zc
         * @date 2025/8/18
         */
        private void checkQueryParamForQuarterly(HotelKpiQuery query) {
            // 查询日期不能跨年
            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getStartDate()));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getEndDate()));
            if (startYearMonth.getYear() != endYearMonth.getYear()) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.START_END_DATE_MUST_IN_SAME_YEAR,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The startDate and endDate must be in the same year; startDate:{0}, endDate:{1}",
                                query.getStartDate(), query.getEndDate()));
            }
        }

        /**
         * 保存指标数据
         *
         * @param property   资产标识
         * @param metricsDTO 保存的指标数据DTO
         * @return emptyMap
         * @author zc
         * @date 2024/6/27
         */
        @ApiId(1207)
        @PostMapping
        public Map<Object, Object> save(@RequestParam String property, @Valid @RequestBody HotelKpiMetricsSaveDTO metricsDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // 校验指标配置是否存在
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.checkExistByDateAndMetricsType(propertyDTO.getId(),
                    metricsDTO.getDateType(), metricsDTO.getDate(), metricsDTO.getMetricsType(), Boolean.TRUE, HotelKpiCons.V1);

            // 校验保存的数据是否符合预期
            checkMetricsSaveValue(metricsDTO, metricsConfig);

            int loginUserId = sessionContext.getLoginUserId();
            if (metricsDTO.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
                LocalDate localDate = DateTimeUtils.strToLocalDate(metricsDTO.getDate().toString(),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);
                if (localDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                    throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            MessageFormat.format("The date must be first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
                }
                weeklyService.save(propertyDTO, metricsDTO, loginUserId);
            } else if (metricsDTO.getDateType() == HotelKpiDateTypeCons.MONTHLY) {
                monthlyService.save(propertyDTO, metricsDTO, loginUserId);
            } else if (metricsDTO.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                quarterlyService.save(propertyDTO, metricsDTO, loginUserId);
            } else {
                yearlyService.save(propertyDTO, metricsDTO, loginUserId);
            }
            return Collections.emptyMap();
        }

        /**
         * 校验指标保存数据
         *
         * @param metricsDTO    保存的指标数据DTO
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/4/3
         */
        private void checkMetricsSaveValue(HotelKpiMetricsSaveDTO metricsDTO, HotelKpiMetricsConfig metricsConfig) {
            // 日期转换为 year
            long year = metricsDTO.getDate();
            if (metricsDTO.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                year = DateTimeUtils.strToLocalDate(metricsDTO.getDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).getYear();
            } else if (metricsDTO.getDateType() == HotelKpiDateTypeCons.MONTHLY ||
                    metricsDTO.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                year = YearMonthUtils.getYearMonth(metricsDTO.getDate().toString()).getYear();
            }

            HotelKpiCalculationFactorConfigDTO configDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getCalculationFactorConfig(), HotelKpiCalculationFactorConfigDTO.class);
            if (CollectionUtils.isNotEmpty(metricsDTO.getActual())) {
                checkSaveFactorSize(configDTO.getActual(), metricsDTO.getActual(), "ACTUAL");
                HotelKpiSettingCalculationDTO settingRule = metricsSettingService.getSettingRule(metricsConfig,
                        year, HotelKpiValueCategoryCons.ACTUAL);
                checkMetricsCanSave(settingRule, metricsDTO.getActual());
            }

            if (CollectionUtils.isNotEmpty(metricsDTO.getTarget()) || CollectionUtils.isNotEmpty(metricsDTO.getCure())) {
                HotelKpiSettingCalculationDTO settingRule = metricsSettingService.getSettingRule(metricsConfig,
                        year, HotelKpiValueCategoryCons.TARGET);
                if (CollectionUtils.isNotEmpty(metricsDTO.getTarget())) {
                    checkSaveFactorSize(configDTO.getTarget(), metricsDTO.getTarget(), "TARGET");
                    checkMetricsCanSave(settingRule, metricsDTO.getTarget());
                }
                if (CollectionUtils.isNotEmpty(metricsDTO.getCure())) {
                    checkSaveFactorSize(configDTO.getTarget(), metricsDTO.getCure(), "CURE");
                    checkMetricsCanSave(settingRule, metricsDTO.getCure());
                }
            }
        }

        /**
         * 校验保存的指标计算因子数量是否与指标配置一致
         *
         * @param configItemDTO    指标计算因子配置
         * @param saveValueDTOList 保存的指标数据
         * @param valueCategory    指标数值分类
         * @author zc
         * @date 2025/4/9
         */
        private void checkSaveFactorSize(HotelKpiCalculationFactorConfigItemDTO configItemDTO,
                                         List<HotelKpiMetricsSaveValueDTO> saveValueDTOList, String valueCategory) {
            // 提取指标配置中的计算因子数量
            long factorCountByConfigDTO = configItemDTO.getCalculationFactor().stream()
                    .map(HotelKpiCalculationFactorConfigIndexDTO::getIndex).distinct().count();
            if (factorCountByConfigDTO != saveValueDTOList.stream().map(HotelKpiMetricsSaveValueDTO::getIndex).distinct().count()) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.FACTOR_COUNT_NOT_MATCH_THE_CONFIG,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The number of saved " + valueCategory + " values does not match the number of values configured for the metrics");
            }
        }

        /**
         * 校验指标计算因子是否支持保存
         *
         * <p>
         * 1、指标计算因子中只要包含【递减剩余】规则，Target、Actual、Cure 都不可编辑<br>
         * 2、指标计算因子只有【动态】规则的可以编辑
         * </P>
         *
         * @param settingRule      指标设置规则
         * @param saveValueDTOList 要保存的指标计算因子集合
         * @author zc
         * @date 2025/4/9
         */
        private void checkMetricsCanSave(HotelKpiSettingCalculationDTO settingRule, List<HotelKpiMetricsSaveValueDTO> saveValueDTOList) {
            saveValueDTOList.forEach(saveValueDTO -> {
                if (saveValueDTO.getUseSystem()) {
                    return;
                }
                boolean matchDiminishingSurplus = settingRule.getCalculationFactor().stream().map(item ->
                        item.getValueSetting().getCategory()).filter(Objects::nonNull).anyMatch(item ->
                        item == HotelKpiSettingCategoryEnum.DIMINISHING_SURPLUS.getCategory());
                if (matchDiminishingSurplus) {
                    throw new BadRequestException(HotelKpiErrorI18nConstant.DIMINISHING_SURPLUS_CANNOT_SAVE,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "Calculation factors with Diminishing Surplus category cannot be saved");
                }

                boolean match = settingRule.getCalculationFactor().stream().filter(item ->
                        Objects.equals(item.getIndex(), saveValueDTO.getIndex())).anyMatch(item ->
                        Objects.isNull(item.getValueSetting().getCategory()) ||
                                item.getValueSetting().getCategory() != HotelKpiSettingCategoryEnum.DYNAMIC.getCategory());
                if (match) {
                    throw new BadRequestException(HotelKpiErrorI18nConstant.ONLY_DYNAMIC_CAN_SAVE,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "Only the calculated factor values of Dynamic category can be saved");
                }
            });
        }

        /**
         * 保存指标数据 V2
         *
         * @param property 资产标识
         * @param saveDTO  保存的指标数据DTO
         * @return emptyMap
         * @author zc
         * @date 2025/8/19
         */
        @ApiId(1571)
        @PostMapping("/v2")
        public Map<Object, Object> saveV2(@RequestParam String property, @Valid @RequestBody HotelKpiV2MetricsSaveDTO saveDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // 校验指标配置是否存在
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.checkExistByDateAndMetricsType(propertyDTO.getId(),
                    saveDTO.getDateType(), saveDTO.getDate(), saveDTO.getMetricsType(), Boolean.TRUE, HotelKpiCons.V2);

            // 校验保存数据参数是否有效
            checkMetricsSaveValueV2(saveDTO, metricsConfig);

            int loginUserId = sessionContext.getLoginUserId();
            if (saveDTO.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                weeklyService.saveV2(propertyDTO, saveDTO, metricsConfig, loginUserId);
            } else if (saveDTO.getDateType() == HotelKpiDateTypeCons.MONTHLY) {
                monthlyService.saveV2(propertyDTO, saveDTO, metricsConfig, loginUserId);
            } else if (saveDTO.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                quarterlyService.saveV2(propertyDTO, saveDTO, metricsConfig, loginUserId);
            } else {
                yearlyService.saveV2(propertyDTO, saveDTO, metricsConfig, loginUserId);
            }
            return Collections.emptyMap();
        }

        /**
         * 校验保存数据参数是否有效 V2
         *
         * @param saveDTO       保存的指标数据DTO
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/8/19
         */
        private void checkMetricsSaveValueV2(HotelKpiV2MetricsSaveDTO saveDTO, HotelKpiMetricsConfig metricsConfig) {
            HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
            HotelKpiV2EditConfigDTO editConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getEditConfig(), HotelKpiV2EditConfigDTO.class);

            List<HotelKpiV2FactorConfigDetailDTO> factorConfigDTOList;
            List<HotelKpiV2EditConfigDetailDTO> editConfigDTOList;
            if (saveDTO.getDateType() == HotelKpiDateTypeCons.WEEKLY) {
                factorConfigDTOList = factorConfigDTO.getWeekly();
                editConfigDTOList = editConfigDTO.getWeekly();
            } else if (saveDTO.getDateType() == HotelKpiDateTypeCons.MONTHLY) {
                factorConfigDTOList = factorConfigDTO.getMonthly();
                editConfigDTOList = editConfigDTO.getMonthly();
            } else if (saveDTO.getDateType() == HotelKpiDateTypeCons.QUARTERLY) {
                factorConfigDTOList = factorConfigDTO.getQuarterly();
                editConfigDTOList = editConfigDTO.getQuarterly();
            } else {
                factorConfigDTOList = factorConfigDTO.getYearly();
                editConfigDTOList = editConfigDTO.getYearly();
            }
            int size = factorConfigDTOList.size();
            if (saveDTO.getList().size() != size) {
                throw new BadRequestException(CommonErrorI18nConstant.LIST_SIZE_MUST_BE,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("list", size),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The list size must be {0}; list:{1}",
                                size, saveDTO.getList()));
            }
            Map<Integer, HotelKpiV2FactorConfigDetailDTO> category2FactorConfig = factorConfigDTOList.stream()
                    .collect(Collectors.toMap(HotelKpiV2FactorConfigDetailDTO::getCategory, Function.identity()));
            Map<Integer, HotelKpiV2EditConfigDetailDTO> category2EditConfig = editConfigDTOList.stream()
                    .collect(Collectors.toMap(HotelKpiV2EditConfigDetailDTO::getCategory, Function.identity()));

            List<Integer> categoryList = factorConfigDTOList.stream().map(HotelKpiV2FactorConfigDetailDTO::getCategory).toList();
            saveDTO.getList().forEach(categoryDTO -> {
                int category = categoryDTO.getCategory();
                HotelKpiV2FactorConfigDetailDTO factorConfigDetailDTO = category2FactorConfig.get(category);
                if (Objects.isNull(factorConfigDetailDTO)) {
                    throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                            i18nMessageUtils.getErrorParamMapNotNeedTranslate("category", categoryList),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            MessageFormat.format("The category must be in {0}", categoryList));
                }
                // 校验保存数据的索引与配置是否匹配
                checkMetricsSaveValueIndexV2(categoryDTO.getActual(), factorConfigDetailDTO.getActual());
                checkMetricsSaveValueIndexV2(categoryDTO.getBudget(), factorConfigDetailDTO.getBudget());
                checkMetricsSaveValueIndexV2(categoryDTO.getLy(), factorConfigDetailDTO.getLy());

                // 校验指标可编辑
                HotelKpiV2EditConfigDetailDTO editConfigDetailDTO = category2EditConfig.get(category);
                checkMetricsEditV2(categoryDTO.getActual(), editConfigDetailDTO.getActual());
                checkMetricsEditV2(categoryDTO.getBudget(), editConfigDetailDTO.getBudget());
                checkMetricsEditV2(categoryDTO.getLy(), editConfigDetailDTO.getLy());
            });
        }

        /**
         * 校验指标可编辑 V2
         *
         * @param saveValueDTOList 保存的指标数据DTO列表
         * @param canEdit          是否可编辑
         * @author zc
         * @date 2025/8/19
         */
        private void checkMetricsEditV2(List<HotelKpiMetricsSaveValueDTO> saveValueDTOList, boolean canEdit) {
            if (canEdit) {
                return;
            }
            if (CollectionUtils.isEmpty(saveValueDTOList)) {
                return;
            }
            saveValueDTOList.forEach(valueDTO -> {
                if (valueDTO.getUseSystem()) {
                    return;
                }
                throw new BadRequestException(HotelKpiErrorI18nConstant.METRICS_VALUE_CANNOT_EDIT,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The metrics value cannot be edited");
            });
        }

        /**
         * 校验保存数据的索引与配置是否匹配 V2
         *
         * @param saveValueDTOList 保存的指标数据DTO列表
         * @param configItemDTO    指标配置项
         * @author zc
         * @date 2025/8/19
         */
        private void checkMetricsSaveValueIndexV2(List<HotelKpiMetricsSaveValueDTO> saveValueDTOList,
                                                  HotelKpiCalculationFactorConfigItemDTO configItemDTO) {
            if (CollectionUtils.isEmpty(saveValueDTOList)) {
                return;
            }
            List<Integer> indexList = configItemDTO.getCalculationFactor().stream()
                    .map(HotelKpiCalculationFactorConfigIndexDTO::getIndex).toList();
            if (saveValueDTOList.stream().allMatch(item ->
                    indexList.contains(item.getIndex()))) {
                return;
            }
            throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                    i18nMessageUtils.getErrorParamMapNotNeedTranslate("index", indexList),
                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                    MessageFormat.format("The index must be in {0}", indexList));
        }

        /**
         * 获取酒店KPI评论
         *
         * @param property 资产标识
         * @param date     日期(yyyyMMdd)
         * @param query    查询条件
         * @return HotelKpiCommentDTO 集合
         * @author zc
         * @date 2024/6/27
         */
        @ApiId(1208)
        @GetMapping("/{date:\\d+}/comment")
        public ListPagedDTO<HotelKpiCommentDTO> listComment(@RequestParam String property, @PathVariable long date,
                                                            @Valid HotelKpiCommentQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            return commentService.list(propertyDTO, date, query);
        }

        /**
         * 保存酒店KPI评论
         *
         * @param property   资产标识
         * @param date       日期(yyyyMMdd)
         * @param commentDTO 评论数据DTO
         * @return 评论 id
         * @author zc
         * @date 2024/6/27
         */
        @ApiId(1209)
        @PostMapping("/{date:\\d+}/comment")
        public Map<String, Long> saveComment(@RequestParam String property, @PathVariable long date,
                                             @Valid @RequestBody HotelKpiCommentDTO commentDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();
            if (Objects.nonNull(commentDTO.getId())) {
                // 校验是否存在
                HotelKpiComment originComment = commentService.checkExist(commentDTO.getId());
                // 校验归属
                commentService.checkBelong(propertyId, date, loginUserId, originComment);
            }
            // 保存数据
            return ImmutableMap.of("id", commentService.save(propertyId, date, commentDTO, loginUserId));
        }

        /**
         * 删除评论
         *
         * @param property 资产标识
         * @param date     日期(yyyyMMdd)
         * @param cid      评论 id
         * @return emptyMap
         * @author zc
         * @date 2024/6/27
         */
        @ApiId(1210)
        @DeleteMapping("/{date:\\d+}/comment/{cid:\\d+}")
        public Map<Object, Object> deleteComment(@RequestParam String property, @PathVariable long date,
                                                 @PathVariable long cid) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();
            // 校验是否存在
            HotelKpiComment originComment = commentService.checkExist(cid);
            // 校验归属
            commentService.checkBelong(propertyId, date, loginUserId, originComment);
            // 删除
            commentService.removeById(cid);
            return Collections.emptyMap();
        }

        /**
         * 获取指标项
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return HotelKpiMetricsItemDTO 集合
         * @author zc
         * @date 2024/6/27
         */
        @ApiId(1225)
        @GetMapping("/metrics")
        public List<HotelKpiMetricsItemDTO> listMetricsItem(@RequestParam String property, @Valid HotelKpiMetricsItemQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            return metricsConfigService.listMetricsItem(propertyId, query);
        }

        /**
         * 获取酒店 KPI 警示评论
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return HotelKpiWarningCommentDTO 集合
         * @author zc
         * @date 2025/1/11
         */
        @ApiId(1327)
        @GetMapping("/warning-comment")
        @JsonView({Views.ListView.class})
        public ListPagedDTO<HotelKpiWarningCommentDTO> listWarningComment(@RequestParam String property, @Valid HotelKpiWarningCommentQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            return warningCommentService.list(propertyDTO, query);
        }

        /**
         * 保存酒店 KPI 警示评论
         *
         * @param property   资产标识
         * @param commentDTO 评论数据DTO
         * @return 评论 id
         * @author zc
         * @date 2025/1/11
         */
        @ApiId(1328)
        @PostMapping("/warning-comment")
        public Map<String, Long> saveWarningComment(@RequestParam String property, @Valid @RequestBody HotelKpiWarningCommentDTO commentDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();
            // 校验指标配置是否存在
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.checkExistByDateAndMetricsType(propertyId, commentDTO.getDateType(),
                    commentDTO.getDate(), commentDTO.getMetricsType(), Boolean.TRUE, null);

            if (HotelKpiDateTypeCons.WEEKLY == commentDTO.getDateType()) {
                LocalDate localDate = DateTimeUtils.strToLocalDate(commentDTO.getDate().toString(),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);
                // 校验日期
                DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
                // 校验查询日期是否符合设置的周起始日
                if (localDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                    throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            MessageFormat.format("The date must be the first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
                }
            }

            // 校验备注的 Category
            checkCommentCategory(metricsConfig, commentDTO);

            if (Objects.nonNull(commentDTO.getId())) {
                // 校验是否存在
                HotelKpiWarningComment originComment = warningCommentService.checkExist(commentDTO.getId());
                // 校验归属
                warningCommentService.checkBelong(propertyId, loginUserId, originComment);
            }
            // 保存数据
            return ImmutableMap.of("id", warningCommentService.save(propertyId, commentDTO, loginUserId));
        }

        /**
         * 删除警示评论
         *
         * @param property 资产标识
         * @param id       评论 id
         * @return emptyMap
         * @author zc
         * @date 2025/1/11
         */
        @ApiId(1329)
        @DeleteMapping("/warning-comment/{id:\\d+}")
        public Map<Object, Object> deleteComment(@RequestParam String property, @PathVariable long id) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();
            // 校验是否存在
            HotelKpiWarningComment originComment = warningCommentService.checkExist(id);
            // 校验归属
            warningCommentService.checkBelong(propertyId, loginUserId, originComment);
            // 删除
            warningCommentService.removeById(id);
            return Collections.emptyMap();
        }

        /**
         * 校验备注的 Category
         *
         * @param metricsConfig 指标配置
         * @param commentDTO    评论数据DTO
         * @author zc
         * @date 2025/1/11
         */
        private void checkCommentCategory(HotelKpiMetricsConfig metricsConfig, HotelKpiWarningCommentDTO commentDTO) {
            int size;
            if (Objects.equals(metricsConfig.getVersion(), HotelKpiCons.V1)) {
                size = 1;
            } else {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
                if (HotelKpiDateTypeCons.WEEKLY == commentDTO.getDateType()) {
                    size = factorConfigDTO.getWeekly().size();
                } else {
                    size = factorConfigDTO.getMonthly().size();
                }
            }
            if (commentDTO.getCategory() > size) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_GREATER_THAN,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("category", size),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The category cannot greater than {0}", size));
            }
        }

        /**
         * 获取周起始日
         *
         * @param property 资产标识
         * @return 周起始日 1 ~ 7
         * @author zc
         * @date 2025/2/25
         */
        @ApiId(1346)
        @GetMapping("/week-start-day")
        public Map<String, Integer> getWeekStartDay(@RequestParam String property) {
            int propertyId = sessionContext.getPropertyId(property);
            return ImmutableMap.of("weekStartDay", weekStartDayService.getWeekStartDay(propertyId).getValue());
        }

        /**
         * 获取 STR Data
         *
         * @param property 资产标识
         * @param date     日期(yyyyMMdd)
         * @return HotelKpiStrDataDTO
         * @author zc
         * @date 2025/8/21
         */
        @ApiId(1574)
        @GetMapping("/str-data")
        public HotelKpiStrWeekDTO getStrData(@RequestParam String property, @RequestParam long date) {
            int tenantId = sessionContext.getTenantSymbolBO().getId();
            int propertyId = sessionContext.getPropertyId(property);

            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(date),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).minusDays(7);
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            // 校验查询日期是否符合设置的周起始日
            if (localDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The date must be the first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
            }
            long effectiveDate = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            return strWeekService.get(tenantId, propertyId, effectiveDate);
        }

        /**
         * 保存 STR Data
         *
         * @param property 资产标识
         * @param saveDTO  保存数据DTO
         * @return empty map
         * @author zc
         * @date 2025/8/21
         */
        @ApiId(1575)
        @PostMapping("/str-data")
        public Map<Objects, Objects> saveStrData(@RequestParam String property, @Valid @RequestBody HotelKpiStrWeekSaveDTO saveDTO) {
            int propertyId = sessionContext.getPropertyId(property);

            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(saveDTO.getDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD);
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            // 校验查询日期是否符合设置的周起始日
            if (localDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The date must be the first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
            }
            strWeekService.save(propertyId, sessionContext.getLoginUserId(), saveDTO);
            return Collections.emptyMap();
        }

        /**
         * 导入 Str 报告
         *
         * @param property  资产
         * @param importDTO 导入报告 DTO
         * @return empty map
         * @author zc
         * @date 2025/8/22
         */
        @ApiId(1576)
        @PostMapping("/str-data/report")
        public Map<Object, Object> importStrReport(@RequestParam String property, @Valid HotelKpiStrImportReportDTO importDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(importDTO.getDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD);
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            // 校验查询日期是否符合设置的周起始日
            if (localDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                throw new BadRequestException(HotelKpiErrorI18nConstant.DATE_MUST_FIRST_DAY_OF_WEEK,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The date must be the first dayOfWeek; dayOfWeek:{0}", dayOfWeek));
            }
            // 校验文件是否合法
            strWeekParseService.checkFile(localDate, importDTO.getFile());
            // 保存数据为处理中
            int strWeekId = strWeekService.saveInProgress(propertyId, importDTO.getDate());
            // 处理数据
            strWeekParseService.handleData(propertyDTO, importDTO.getFile(), strWeekId, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

    }