# PropertyOperationServiceImpl.java

    package cloud.wconnect.rateshop.service.rateshop.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.InternationalizationUtils;
    import cloud.wconnect.rateshop.cons.rateshop.OperationCategoryCons;
    import cloud.wconnect.rateshop.entity.rateshop.PropertyOperation;
    import cloud.wconnect.rateshop.mapper.rateshop.PropertyOperationMapper;
    import cloud.wconnect.rateshop.pojo.bo.rateshop.HistoryAndForecastForRateShopBO;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.internal.InternalOperationDTO;
    import cloud.wconnect.rateshop.service.rateshop.IPropertyOperationService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    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.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * property的运营数据 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2023-06-15
     */
    @Service
    @DS("rate_shop")
    public class PropertyOperationServiceImpl extends ServiceImpl<PropertyOperationMapper, PropertyOperation> implements IPropertyOperationService {

        @Autowired
        private BeanMapper beanMapper;

        /**
         * 保存 history and forecast 报告数据
         *
         * @param propertyDTO           资产DTO
         * @param operatorId            操作人 id
         * @param historyAndForecastBOS 报告数据
         * @author zc
         * @date 2024/3/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveHistoryAndForecastReportData(PropertyDTO propertyDTO, int operatorId,
                                                     List<HistoryAndForecastForRateShopBO> historyAndForecastBOS) {
            if (CollectionUtils.isEmpty(historyAndForecastBOS)) return;

            long now = DateTimeUtils.nowTimestamp();
            List<PropertyOperation> propertyOperations = beanMapper.mapList(historyAndForecastBOS,
                    PropertyOperation.class, (bo, entity) -> {
                        entity.setPropertyId(propertyDTO.getId());
                        entity.setTargetDate(DateTimeUtils.dateStrToEpochSecond(bo.getDate(),
                                BaseConstant.DATE_FORMAT_YYYY_MM_DD, propertyDTO.getTimezone()));
                        entity.setPmsUpdatedBy(operatorId);
                        entity.setPmsUpdatedTime(now);
                    });
            // 按唯一索引字段排序，避免死锁
            propertyOperations.sort(Comparator.comparing(PropertyOperation::getPropertyId)
                    .thenComparing(PropertyOperation::getTargetDate));
            // 保存 operation 信息
            baseMapper.saveOrUpdateByReport(propertyOperations);
        }

        /**
         * 保存采集的 Operation 数据
         *
         * @param propertyDTO      资产
         * @param operationDTOList Operation 数据
         * @author zc
         * @date 2024/3/23
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveCollectOperation(PropertyDTO propertyDTO, List<InternalOperationDTO> operationDTOList) {
            if (CollectionUtils.isEmpty(operationDTOList)) {
                return;
            }
            List<PropertyOperation> operationList = Lists.newArrayList();

            int propertyId = propertyDTO.getId();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            Map<Long, List<InternalOperationDTO>> theDate2OperationList = operationDTOList.stream()
                    .collect(Collectors.groupingBy(InternalOperationDTO::getTheDate));
            // 查询 Operation 数据
            Set<Long> theDates = theDate2OperationList.keySet();
            long startDateQuery = Collections.min(theDates);
            long endDateQuery = Collections.max(theDates);
            Map<Long, PropertyOperation> theDate2OperationByExist = list(new QueryWrapper<PropertyOperation>().lambda()
                    .eq(PropertyOperation::getPropertyId, propertyId)
                    .between(PropertyOperation::getTargetDate,
                            DateTimeUtils.dateStrToEpochSecond(Long.valueOf(startDateQuery).toString(),
                                    BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone()),
                            DateTimeUtils.dateStrToEpochSecond(Long.valueOf(endDateQuery).toString(),
                                    BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone()))).stream()
                    .collect(Collectors.toMap(PropertyOperation::getTargetDate, Function.identity()));

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            for (Map.Entry<Long, List<InternalOperationDTO>> entry : theDate2OperationList.entrySet()) {
                long targetDate = DateTimeUtils.dateStrToEpochSecond(entry.getKey().toString(),
                        BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone());

                PropertyOperation propertyOperation = theDate2OperationByExist.get(targetDate);
                if (Objects.isNull(propertyOperation)) {
                    propertyOperation = new PropertyOperation();
                    propertyOperation.setPropertyId(propertyId);
                    propertyOperation.setTargetDate(targetDate);
                }

                Map<Integer, InternalOperationDTO> category2Operation = entry.getValue().stream()
                        .collect(Collectors.toMap(InternalOperationDTO::getCategory, Function.identity()));

                InternalOperationDTO roomsAvailableOperation = category2Operation.get(OperationCategoryCons.ROOMS_AVAILABLE);
                if (Objects.nonNull(roomsAvailableOperation)) {
                    propertyOperation.setPmsUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setPmsUpdatedTime(nowTimestamp);
                    propertyOperation.setRoomsAvailable(StringUtils.isNotBlank(roomsAvailableOperation.getValue()) ?
                            Integer.parseInt(roomsAvailableOperation.getValue()) : null);
                }

                InternalOperationDTO roomsSoldOperation = category2Operation.get(OperationCategoryCons.ROOMS_SOLD);
                if (Objects.nonNull(roomsSoldOperation)) {
                    propertyOperation.setPmsUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setPmsUpdatedTime(nowTimestamp);
                    propertyOperation.setRoomsSold(StringUtils.isNotBlank(roomsSoldOperation.getValue()) ?
                            Integer.parseInt(roomsSoldOperation.getValue()) : null);
                }

                InternalOperationDTO oooRoomsOperation = category2Operation.get(OperationCategoryCons.OOO_ROOMS);
                if (Objects.nonNull(oooRoomsOperation)) {
                    propertyOperation.setPmsUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setPmsUpdatedTime(nowTimestamp);
                    propertyOperation.setOooRooms(StringUtils.isNotBlank(oooRoomsOperation.getValue()) ?
                            Integer.parseInt(oooRoomsOperation.getValue()) : null);
                }

                InternalOperationDTO transientSoldOperation = category2Operation.get(OperationCategoryCons.TRANSIENT_SOLD);
                if (Objects.nonNull(transientSoldOperation)) {
                    propertyOperation.setPmsUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setPmsUpdatedTime(nowTimestamp);
                    propertyOperation.setTransientSold(StringUtils.isNotBlank(transientSoldOperation.getValue()) ?
                            Integer.parseInt(transientSoldOperation.getValue()) : null);
                }

                InternalOperationDTO groupSoldOperation = category2Operation.get(OperationCategoryCons.GROUP_SOLD);
                if (Objects.nonNull(groupSoldOperation)) {
                    propertyOperation.setPmsUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setPmsUpdatedTime(nowTimestamp);
                    propertyOperation.setGroupSold(StringUtils.isNotBlank(groupSoldOperation.getValue()) ?
                            Integer.parseInt(groupSoldOperation.getValue()) : null);
                }

                InternalOperationDTO currentBfrOperation = category2Operation.get(OperationCategoryCons.CURRENT_BFR);
                if (Objects.nonNull(currentBfrOperation)) {
                    propertyOperation.setCurrentBfrUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setCurrentBfrUpdatedTime(nowTimestamp);
                    propertyOperation.setCurrentBfr(StringUtils.isNotBlank(currentBfrOperation.getValue()) ?
                            new BigDecimal(currentBfrOperation.getValue()).setScale(BaseConstant.DATA_PRECISION, roundingMode) : null);
                }

                InternalOperationDTO currentHurdlePointOperation = category2Operation.get(OperationCategoryCons.CURRENT_HURDLE_POINT);
                if (Objects.nonNull(currentHurdlePointOperation)) {
                    propertyOperation.setCurrentHurdlePointUpdatedBy(BaseConstant.ZERO);
                    propertyOperation.setCurrentHurdlePointUpdatedTime(nowTimestamp);
                    propertyOperation.setCurrentHurdlePoint(StringUtils.isNotBlank(currentHurdlePointOperation.getValue()) ?
                            new BigDecimal(currentHurdlePointOperation.getValue()).setScale(BaseConstant.DATA_PRECISION, roundingMode) : null);
                }
                operationList.add(propertyOperation);
            }
            // 按唯一索引字段排序，避免死锁
            operationList.sort(Comparator.comparing(PropertyOperation::getPropertyId)
                    .thenComparing(PropertyOperation::getTargetDate));

            baseMapper.saveOrUpdateByCollect(operationList);
        }

        /**
         * 根据时间范围查询 Operation 数据
         *
         * @param propertyId    资产id
         * @param fromTimestamp 开始时间
         * @param toTimestamp   结束时间
         * @return Operation 数据
         * @author lichen
         * @date 2025/12/30
         */
        @Override
        public List<PropertyOperation> listByDateRange(int propertyId, long fromTimestamp, long toTimestamp) {
            return list(new LambdaQueryWrapper<PropertyOperation>()
                    .eq(PropertyOperation::getPropertyId, propertyId)
                    .between(PropertyOperation::getTargetDate, fromTimestamp, toTimestamp));
        }

    }