# OccForecastServiceImpl.java

    package cloud.wconnect.rateshop.service.revenue.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.rateshop.entity.revenue.OccForecast;
    import cloud.wconnect.rateshop.mapper.revenue.OccForecastMapper;
    import cloud.wconnect.rateshop.pojo.bo.revenue.HistoryAndForecastOccJumpAlertBO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.HistoryAndForecastOccJumpAlertDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.OccCompareDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.OccForecastValueDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.OccForecastValuesDTO;
    import cloud.wconnect.rateshop.pojo.query.revenue.OccCompareQuery;
    import cloud.wconnect.rateshop.service.revenue.IOccForecastService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    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>
     * 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-04-16
     */
    @Service
    @DS("rate_shop")
    public class OccForecastServiceImpl extends ServiceImpl<OccForecastMapper, OccForecast> implements IOccForecastService {

        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;

        /**
         * 保存 Occ Forecast 数据
         *
         * @param propertyDTO 资产
         * @param operatorId  操作人 id
         * @param boList      historyAndForecastBOS 数据
         * @author zc
         * @date 2024/4/17
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveOccForecast(PropertyDTO propertyDTO, int operatorId, List<HistoryAndForecastOccJumpAlertBO> boList) {
            if (CollectionUtils.isEmpty(boList)) {
                return;
            }

            int propertyId = propertyDTO.getId();
            String timezone = propertyDTO.getTimezone();

            // 转换数据
            List<HistoryAndForecastOccJumpAlertDTO> historyAndForecastDTOS = beanMapper.mapList(boList, HistoryAndForecastOccJumpAlertDTO.class,
                    (bo, dto) ->
                            dto.setTheDate(Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.strToLocalDate(bo.getDate(),
                                    BaseConstant.DATE_FORMAT_YYYY_MM_DD), BaseConstant.DATE_FORMAT_YYYYMMDD))));

            List<Long> theDateList = historyAndForecastDTOS.stream().map(HistoryAndForecastOccJumpAlertDTO::getTheDate).toList();
            long startDateQuery = Collections.min(theDateList);
            long endDateQuery = Collections.max(theDateList);
            // 查找指定时间范围内的数据
            List<OccForecast> occForecastList = list(new LambdaQueryWrapper<OccForecast>()
                    .between(OccForecast::getTheDate, startDateQuery, endDateQuery)
                    .eq(OccForecast::getPropertyId, propertyId)
                    .last("for update"));
            Map<Long, OccForecast> theDate2OccForecast = occForecastList.stream()
                    .collect(Collectors.toMap(OccForecast::getTheDate, Function.identity()));

            // 获取当前时间的 key
            String currentKey = DateTimeUtils.parseToLong(DateTimeUtils.dateTimeNow(timezone),
                    BaseConstant.DATE_FORMAT_YYYYMMDDHH).toString();
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            for (HistoryAndForecastOccJumpAlertDTO historyAndForecastDTO : historyAndForecastDTOS) {
                // 构造当前时刻的 OccForecastValuesDTO
                OccForecastValuesDTO currentValuesDTO = OccForecastValuesDTO.builder()
                        .key(currentKey)
                        .value(beanMapper.map(historyAndForecastDTO, OccForecastValueDTO.class))
                        .updatedBy(operatorId)
                        .updatedTime(nowTimestamp)
                        .build();

                long theDate = historyAndForecastDTO.getTheDate();
                OccForecast originOccForecast = theDate2OccForecast.get(theDate);
                if (Objects.isNull(originOccForecast)) {
                    occForecastList.add(new OccForecast()
                            .setPropertyId(propertyId)
                            .setTheDate(theDate)
                            .setValues(JsonUtils.convertObjListToString(objectMapper, Lists.newArrayList(currentValuesDTO))));
                } else {
                    List<OccForecastValuesDTO> originValuesDTOList = JsonUtils.convertStringToList(objectMapper,
                            originOccForecast.getValues(), OccForecastValuesDTO.class);
                    Map<String, OccForecastValuesDTO> key2ValuesDTO = originValuesDTOList.stream()
                            .collect(Collectors.toMap(OccForecastValuesDTO::getKey, Function.identity()));
                    OccForecastValuesDTO valuesDTO = key2ValuesDTO.get(currentKey);
                    // 如果没有指定 currentKey 的数据，则新增，否则更新
                    if (Objects.isNull(valuesDTO)) {
                        originValuesDTOList.add(currentValuesDTO);
                    } else {
                        valuesDTO.setValue(currentValuesDTO.getValue());
                        valuesDTO.setUpdatedBy(currentValuesDTO.getUpdatedBy());
                        valuesDTO.setUpdatedTime(currentValuesDTO.getUpdatedTime());
                    }
                    originOccForecast.setValues(JsonUtils.convertObjListToString(objectMapper, originValuesDTOList));
                }
            }
            // 批量保存
            saveOrUpdateBatch(occForecastList);
        }

        /**
         * 获取 occ 比对数据
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @return OccCompareDTO
         * @author zc
         * @date 2024/4/17
         */
        @Override
        public OccCompareDTO getCompare(int propertyId, OccCompareQuery query) {
            // 获取指定日期的 occ forecast 数据
            OccForecast originOccForecast = getOne(new LambdaQueryWrapper<OccForecast>()
                    .eq(OccForecast::getTheDate, query.getTheDate())
                    .eq(OccForecast::getPropertyId, propertyId));
            if (Objects.isNull(originOccForecast)) {
                return new OccCompareDTO();
            }

            Map<String, OccForecastValuesDTO> key2ValuesDTO = JsonUtils.convertStringToList(objectMapper,
                            originOccForecast.getValues(), OccForecastValuesDTO.class).stream()
                    .collect(Collectors.toMap(OccForecastValuesDTO::getKey, Function.identity()));

            String keyFirst = query.getCompareFirstDate() + StringUtils.leftPad(query.getCompareFirstHour().toString(), 2, '0');
            OccForecastValuesDTO occForecastFirst = key2ValuesDTO.get(keyFirst);
            BigDecimal pcOccFirst = Objects.isNull(occForecastFirst) ? null : occForecastFirst.getValue().getPcOcc();
            BigDecimal scOccFirst = Objects.isNull(occForecastFirst) ? null : occForecastFirst.getValue().getScOcc();

            String keySecond = query.getCompareSecondDate() + StringUtils.leftPad(query.getCompareSecondHour().toString(), 2, '0');
            OccForecastValuesDTO occForecastSecond = key2ValuesDTO.get(keySecond);
            BigDecimal pcOccSecond = Objects.isNull(occForecastSecond) ? null : occForecastSecond.getValue().getPcOcc();
            BigDecimal scOccSecond = Objects.isNull(occForecastSecond) ? null : occForecastSecond.getValue().getScOcc();

            return OccCompareDTO.builder()
                    .pcOccFirst(pcOccFirst)
                    .pcOccSecond(pcOccSecond)
                    .pcOccVariances((Objects.nonNull(pcOccSecond) && Objects.nonNull(pcOccFirst)) ?
                            pcOccSecond.subtract(pcOccFirst) : null)
                    .scOccFirst(scOccFirst)
                    .scOccSecond(scOccSecond)
                    .scOccVariances((Objects.nonNull(scOccSecond) && Objects.nonNull(scOccFirst)) ?
                            scOccSecond.subtract(scOccFirst) : null)
                    .build();
        }

        /**
         * 根据数据日期范围获取 OccForecast 数据
         *
         * @param propertyId 资产 id
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return OccForecast 集合
         * @author zc
         * @date 2024/4/18
         */
        @Override
        public List<OccForecast> listByDateRange(int propertyId, long startDate, long endDate) {
            return list(new LambdaQueryWrapper<OccForecast>()
                    .between(OccForecast::getTheDate, startDate, endDate)
                    .eq(OccForecast::getPropertyId, propertyId));
        }

        /**
         * 根据数据日期范围获取 OccForecast 数据
         *
         * @param propertyId 资产 id
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return OccForecast 集合
         * @author yyt
         * @date 2026/3/4
         */
        @Override
        public List<OccForecast> listByDateRangeForUpdate(Integer propertyId, long startDate, long endDate) {
            return  list(new LambdaQueryWrapper<OccForecast>()
                    .between(OccForecast::getTheDate, startDate, endDate)
                    .eq(OccForecast::getPropertyId, propertyId)
                    .last("for update"));
        }

        /**
         * 计算 OOO 房间数
         *
         * @param occJumpAlertDTO occ jump alert dto
         * @return OOO 房间数
         * @author yyt
         * @date 2026/3/4
         */
        @Override
        public Integer calculateOooRooms(HistoryAndForecastOccJumpAlertDTO occJumpAlertDTO) {
            // OOO 房间数 = total rooms - sold rooms - available rooms - comp rooms
            Integer totalRooms = occJumpAlertDTO.getTotalRooms();
            if (Objects.isNull(totalRooms)) {
                return 0;
            }
            int oooRooms = totalRooms;
            Integer roomsSold = occJumpAlertDTO.getRoomsSold();
            if (Objects.nonNull(roomsSold)) {
                oooRooms -= roomsSold;
            }
            Integer availableRooms = occJumpAlertDTO.getAvailableRooms();
            if (Objects.nonNull(availableRooms)) {
                oooRooms -= availableRooms;
            }
            Integer compRooms = occJumpAlertDTO.getCompRooms();
            if (Objects.nonNull(compRooms)) {
                oooRooms -= compRooms;
            }
            return Math.max(oooRooms, 0);
        }

        /**
         * 计算 SC OCC
         *
         * @param historyAndForecastDTO history and forecast dto
         * @param roundingMode          舍入模式
         * @return SC OCC
         * @author yyt
         * @date 2026/3/4
         */
        @Override
        public BigDecimal calculateScOcc(HistoryAndForecastOccJumpAlertDTO historyAndForecastDTO, RoundingMode roundingMode) {
            // sc occ = sold rooms / (total rooms - ooo rooms)
            Integer roomsSold = historyAndForecastDTO.getRoomsSold();
            Integer totalRooms = historyAndForecastDTO.getTotalRooms();
            Integer oooRooms = historyAndForecastDTO.getOooRooms();
            if (Objects.nonNull(roomsSold) && Objects.nonNull(totalRooms) && Objects.nonNull(oooRooms)) {
                return BigDecimal.valueOf(roomsSold).divide(new BigDecimal(totalRooms - oooRooms), BaseConstant.DATA_PRECISION, roundingMode);
            }
            return null;
        }

        /**
         * 包装 OccForecastValuesDTO
         *
         * @param currentKey            当前 key
         * @param historyAndForecastDTO history and forecast dto
         * @param roundingMode          舍入模式
         * @param operatorId            操作人 id
         * @param nowTimestamp          当前时间戳
         * @return OccForecastValuesDTO
         * @author yyt
         * @date 2026/3/4
         */
        @Override
        public OccForecastValuesDTO packageOccForecastValuesDTO(String currentKey, HistoryAndForecastOccJumpAlertDTO historyAndForecastDTO,
                                                                RoundingMode roundingMode, Integer operatorId, long nowTimestamp) {
            // 重新计算 ooo rooms
            historyAndForecastDTO.setOooRooms(calculateOooRooms(historyAndForecastDTO));
            // 重新计算 sc occ
            historyAndForecastDTO.setScOcc(calculateScOcc(historyAndForecastDTO, roundingMode));

            // 构造当前时刻的 OccForecastValuesDTO
            return OccForecastValuesDTO.builder()
                    .key(currentKey)
                    .value(beanMapper.map(historyAndForecastDTO, OccForecastValueDTO.class))
                    .updatedBy(operatorId)
                    .updatedTime(nowTimestamp)
                    .build();
        }

    }