# BudgetDatabaseRoomServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.UserDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.NumberUtil;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.rateshop.cons.budgetdatabase.*;
    import cloud.wconnect.rateshop.entity.budgetdatabase.*;
    import cloud.wconnect.rateshop.mapper.budgetdatabase.BudgetDatabaseRoomMapper;
    import cloud.wconnect.rateshop.pojo.dto.budgetdatabase.*;
    import cloud.wconnect.rateshop.service.budgetdatabase.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import jakarta.annotation.Nullable;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.time.YearMonth;
    import java.time.ZonedDateTime;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author yyt
     * @since 2025-11-10
     */
    @Service
    @DS("rate_shop")
    public class BudgetDatabaseRoomServiceImpl extends ServiceImpl<BudgetDatabaseRoomMapper, BudgetDatabaseRoom> implements IBudgetDatabaseRoomService {

        @Lazy
        @Autowired
        private IBudgetDatabaseService budgetDatabaseService;
        @Autowired
        private IBudgetDatabaseConfigRoomTabItemService budgetDatabaseConfigRoomTabItemService;
        @Autowired
        private IBudgetDatabaseCommentRoomService budgetDatabaseCommentRoomService;
        @Autowired
        private IBudgetDatabaseCalculateService budgetDatabaseCalculateService;
        @Autowired
        private IBudgetDatabaseDataLogRoomMonthStatisticsService budgetDatabaseDataLogRoomMonthStatisticsService;
        @Autowired
        private IBudgetDatabaseRoomRgiService budgetDatabaseRoomRgiService;
        @Autowired
        private IBudgetDatabaseStatusLogService budgetDatabaseStatusLogService;
        @Autowired
        private IBudgetDatabaseMarketSegmentationLogService budgetDatabaseMarketSegmentationLogService;
        @Autowired
        private BudgetDatabaseCalculateRevenueAndRoomService budgetDatabaseCalculateRevenueAndRoomService;
        @Autowired
        private IBudgetDatabaseConfigVariableRoomService budgetDatabaseConfigVariableRoomService;
        @Autowired
        private IBudgetDatabaseConfigVariableService budgetDatabaseConfigVariableService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private BeanMapper beanMapper;

        /**
         * 查询指标数据
         *
         * @param itemIds 指标 id
         * @return 指标数据
         * @author yyt
         * @date 2025/11/10
         **/
        @Override
        public List<BudgetDatabaseRoom> listByItemIds(List<Integer> itemIds) {
            return list(new QueryWrapper<BudgetDatabaseRoom>().lambda()
                    .in(BudgetDatabaseRoom::getRoomId, itemIds));
        }

        /**
         * 查询指标数据
         *
         * @param itemId 指标 id
         * @return 指标数据
         * @author yyt
         * @date 2025/11/10
         **/
        @Override
        public List<BudgetDatabaseRoom> listByItemId(Integer itemId) {
            return list(new QueryWrapper<BudgetDatabaseRoom>().lambda()
                    .eq(BudgetDatabaseRoom::getRoomId, itemId));
        }

        /**
         * 批量查询指标数据
         *
         * @param tabIds tab id
         * @return 批次数据
         * @author yyt
         * @date 2025/11/10
         **/
        @Override
        public List<BudgetDatabaseRoom> listByTabIds(List<Integer> tabIds) {
            if (CollectionUtils.isEmpty(tabIds)) return Collections.emptyList();

            return list(new QueryWrapper<BudgetDatabaseRoom>().lambda()
                    .in(BudgetDatabaseRoom::getTabId, tabIds));
        }

        /**
         * 批量查询指标数据
         *
         * @param tab             tab
         * @param selectCommenter 是否查询评论人
         * @return 批次数据
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public List<RoomOperationInfoDTO> listRoom(BudgetDatabaseConfigRoomTab tab, boolean selectCommenter) {
            int tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            int year = tab.getBudgetYear();
            // 查询配置指标
            List<BudgetDatabaseConfigRoomTabItem> items = budgetDatabaseConfigRoomTabItemService.listByTabIdAndClassification(tabId, RoomClassificationEnum.ROOM.getCode());
            if (CollectionUtils.isEmpty(items)) return Collections.emptyList();

            // 查询 revenue
            Map<Integer, List<BudgetDatabaseRoom>> revenuesByItemId = listByItemIds(items.stream().map(BudgetDatabaseConfigRoomTabItem::getId).toList())
                    .stream().collect(Collectors.groupingBy(BudgetDatabaseRoom::getRoomId));

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // comment
            List<BudgetDatabaseCommentRoom> comments = budgetDatabaseCommentRoomService.list(propertyId, tabId, RoomCommentAreaCons.ROOM);
            Map<Integer, BudgetDatabaseCommentRoom> commentByRow = comments.stream().collect(Collectors.toMap(BudgetDatabaseCommentRoom::getRow, v -> v));
            Map<Integer, UserDTO> userById;
            if (selectCommenter) {
                userById = userInternalService.getUsers(propertyDTO.getTenantId(), comments.stream().map(BudgetDatabaseCommentRoom::getUpdatedBy).distinct().toList())
                        .stream().collect(Collectors.toMap(UserDTO::getId, v -> v));
            } else {
                userById = Collections.emptyMap();
            }

            // 按照类型分组，循环完一组后，汇总，然后再循环下一组，最后处理 total
            int size = items.size();
            List<RoomOperationInfoDTO> operationInfoDTOS = Lists.newArrayListWithExpectedSize(size);
            Map<Integer, List<BudgetDatabaseConfigRoomTabItem>> itemsByType = items.stream().collect(Collectors.groupingBy(BudgetDatabaseConfigRoomTabItem::getType));
            itemsByType.forEach((type, typeItems) -> {
                // 按照 sort 排序
                typeItems.sort(Comparator.comparing(BudgetDatabaseConfigRoomTabItem::getSort));

                List<RoomOperationInfoDTO> segmentOperationInfoDTOSForType = Lists.newArrayListWithExpectedSize(size);
                for (BudgetDatabaseConfigRoomTabItem item : typeItems) {
                    List<BudgetDatabaseRoom> revenues = revenuesByItemId.getOrDefault(item.getId(), Collections.emptyList());
                    RoomOperationInfoDTO infoDTO = beanMapper.map(packageOperationInfoDTO(propertyId, year, null, revenues), RoomOperationInfoDTO.class);
                    infoDTO.setRoomId(item.getId());
                    infoDTO.setName(item.getName());
                    infoDTO.setType(item.getType());
                    infoDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(item.getId()), userById));
                    segmentOperationInfoDTOSForType.add(infoDTO);

                    infoDTO.setRevenueParticipateCalculate(item.getEnableBudgetRevenue());
                    infoDTO.setRoomParticipateCalculate(item.getEnableBudgetRoom());
                    operationInfoDTOS.add(infoDTO);
                }

                // 分类汇总
                RoomOperationInfoDTO infoDTO = beanMapper.map(packageOperationInfoDTO(propertyId, year, null,
                        beanMapper.mapList(segmentOperationInfoDTOSForType, BudgetDatabaseRoom.class)), RoomOperationInfoDTO.class);
                infoDTO.setRoomId(RoomTypeEnum.getRowId(type));
                infoDTO.setName(RoomTypeEnum.getDesc(type));
                infoDTO.setType(type);
                infoDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(RoomTypeEnum.getRowId(type)), userById));
                operationInfoDTOS.add(infoDTO);
            });

            // total
            OperationInfoDTO operationInfoForTotal = packageOperationInfoDTO(propertyId, year, null, beanMapper.mapList(operationInfoDTOS.stream()
                    .filter(s -> s.getRoomId() < BaseConstant.ZERO).toList(), BudgetDatabaseRoom.class));
            RoomOperationInfoDTO segmentOperationInfoForTotal = beanMapper.map(operationInfoForTotal, RoomOperationInfoDTO.class);
            segmentOperationInfoForTotal.setRoomId(BaseConstant.ZERO);
            segmentOperationInfoForTotal.setName(BudgetDatabaseTitleCons.TOTAL);
            segmentOperationInfoForTotal.setType(BaseConstant.ZERO);
            segmentOperationInfoForTotal.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(RoomTypeEnum.TOTAL.getRowId()), userById));
            operationInfoDTOS.add(segmentOperationInfoForTotal);

            return operationInfoDTOS;
        }

        /**
         * segment statistics
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param revenues   revenue
         * @return segment statistics
         * @author yyt
         * @date 2025/11/14
         **/
        private OperationInfoDTO packageOperationInfoDTO(int propertyId, int year, @Nullable Integer month, List<BudgetDatabaseRoom> revenues) {
            OperationInfoDTO operationInfoDTO = new OperationInfoDTO();

            Long totalBudgetRooms = null;
            Integer totalActaulRooms = null;
            BigDecimal totalBudgetRevenue = null;
            BigDecimal totalActualRevenue = null;

            for (BudgetDatabaseRoom revenue : revenues) {
                Long budgetRooms = revenue.getBudgetRooms();
                if (Objects.nonNull(budgetRooms)) {
                    totalBudgetRooms = Objects.isNull(totalBudgetRooms) ? budgetRooms : totalBudgetRooms + budgetRooms;
                }
                Integer actualRooms = revenue.getActualRooms();
                if (Objects.nonNull(actualRooms)) {
                    totalActaulRooms = Objects.isNull(totalActaulRooms) ? actualRooms : totalActaulRooms + actualRooms;
                }
                BigDecimal budgetRevenue = revenue.getBudgetRevenue();
                if (Objects.nonNull(budgetRevenue)) {
                    totalBudgetRevenue = Objects.isNull(totalBudgetRevenue) ? budgetRevenue : totalBudgetRevenue.add(budgetRevenue);
                }
                BigDecimal actualRevenue = revenue.getActualRevenue();
                if (Objects.nonNull(actualRevenue)) {
                    totalActualRevenue = Objects.isNull(totalActualRevenue) ? actualRevenue : totalActualRevenue.add(actualRevenue);
                }
            }

            operationInfoDTO.setBudgetRooms(totalBudgetRooms);
            operationInfoDTO.setActualRooms(totalActaulRooms);
            operationInfoDTO.setRoomsYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, totalBudgetRooms, totalActaulRooms));

            operationInfoDTO.setBudgetAdr(budgetDatabaseCalculateService.calculateAdr(propertyId, totalBudgetRevenue, totalBudgetRooms));
            operationInfoDTO.setActualAdr(budgetDatabaseCalculateService.calculateAdr(propertyId, totalActualRevenue, totalActaulRooms));
            operationInfoDTO.setAdrYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, operationInfoDTO.getBudgetAdr(), operationInfoDTO.getActualAdr()));

            operationInfoDTO.setBudgetRevenue(totalBudgetRevenue);
            operationInfoDTO.setActualRevenue(totalActualRevenue);
            operationInfoDTO.setRevenueYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, totalBudgetRevenue, totalActualRevenue));

            operationInfoDTO.setBudgetOcc(budgetDatabaseCalculateService.calculateOcc(propertyId, year, month, totalBudgetRooms));
            operationInfoDTO.setActualOcc(budgetDatabaseCalculateService.calculateOcc(propertyId, year - BaseConstant.ONE, month, totalActaulRooms));
            operationInfoDTO.setOccChange(budgetDatabaseCalculateService.calculateOccChange(operationInfoDTO.getBudgetOcc(), operationInfoDTO.getActualOcc()));

            operationInfoDTO.setBudgetRevpar(budgetDatabaseCalculateService.calculateRevpar(propertyId, operationInfoDTO.getBudgetAdr(), operationInfoDTO.getBudgetOcc()));
            operationInfoDTO.setActualRevpar(budgetDatabaseCalculateService.calculateRevpar(propertyId, operationInfoDTO.getActualAdr(), operationInfoDTO.getActualOcc()));
            operationInfoDTO.setRevparYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, operationInfoDTO.getBudgetRevpar(), operationInfoDTO.getActualRevpar()));

            return operationInfoDTO;
        }

        /**
         * 查询 month statistics
         *
         * @param tab             tab
         * @param selectCommenter 是否查询评论人
         * @return month statistics
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public MonthStatisticsDTO listMonthStatistics(BudgetDatabaseConfigRoomTab tab, boolean selectCommenter) {
            int tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            int year = tab.getBudgetYear();
            // 查询配置指标
            List<BudgetDatabaseConfigRoomTabItem> items = budgetDatabaseConfigRoomTabItemService.listByTabIdAndClassification(tabId, RoomClassificationEnum.ROOM.getCode());
            if (CollectionUtils.isEmpty(items)) return new MonthStatisticsDTO();

            MonthStatisticsDTO monthStatisticsDTO = new MonthStatisticsDTO();

            // 最后修改人
            monthStatisticsDTO.setUpdatedInfo(budgetDatabaseDataLogRoomMonthStatisticsService.getLastLogDTO(tabId, RoomLogAreaCons.MONTH_STATISTICS));

            List<MonthOperationInfoDTO> monthOperationInfoDTOS = Lists.newArrayListWithExpectedSize(13);

            // 月统计信息
            Map<Integer, List<BudgetDatabaseRoom>> revenuesByYearMonth = listByItemIds(items.stream().map(BudgetDatabaseConfigRoomTabItem::getId).toList())
                    .stream().collect(Collectors.groupingBy(BudgetDatabaseRoom::getYearMonth));
            Map<Integer, BudgetDatabaseRoomRgi> rgiByYearMonth = budgetDatabaseRoomRgiService.list(tabId)
                    .stream().collect(Collectors.toMap(BudgetDatabaseRoomRgi::getYearMonth, v -> v));

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // 评论信息
            List<BudgetDatabaseCommentRoom> comments = budgetDatabaseCommentRoomService.list(propertyId, tabId, RoomCommentAreaCons.MONTH_STATISTICS);
            Map<Integer, BudgetDatabaseCommentRoom> commentByRow = comments.stream().collect(Collectors.toMap(BudgetDatabaseCommentRoom::getRow, v -> v));
            Map<Integer, UserDTO> userById;
            if (selectCommenter) {
                userById = userInternalService.getUsers(propertyDTO.getTenantId(), comments.stream().map(BudgetDatabaseCommentRoom::getUpdatedBy).distinct().toList())
                        .stream().collect(Collectors.toMap(UserDTO::getId, v -> v));
            } else {
                userById = Collections.emptyMap();
            }

            for (int i = 1; i <= 12; i++) {
                int yearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(year, i)));
                List<BudgetDatabaseRoom> revenues = revenuesByYearMonth.getOrDefault(yearMonth, Collections.emptyList());
                MonthOperationInfoDTO monthOperationInfoDTO = beanMapper.map(packageOperationInfoDTO(propertyId, year, i, revenues), MonthOperationInfoDTO.class);
                monthOperationInfoDTO.setYearMonth(yearMonth);

                BudgetDatabaseRoomRgi rgi = rgiByYearMonth.getOrDefault(yearMonth, new BudgetDatabaseRoomRgi());
                monthOperationInfoDTO.setBudgetRgi(rgi.getBudgetRgi());
                monthOperationInfoDTO.setActualRgi(rgi.getActualRgi());
                monthOperationInfoDTO.setRgiYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, rgi.getBudgetRgi(), rgi.getActualRgi()));

                monthOperationInfoDTO.setStrRevpar(rgi.getStrRevpar());
                monthOperationInfoDTO.setHotelRevpar(rgi.getHotelRevpar());
                monthOperationInfoDTO.setCsRevpar(rgi.getCsRevpar());

                monthOperationInfoDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(i), userById));

                monthOperationInfoDTOS.add(monthOperationInfoDTO);
            }

            // total 统计信息
            MonthOperationInfoDTO totalDTO = packageTotalMonthOperationInfoDTO(propertyId, year, monthOperationInfoDTOS);
            totalDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(BaseConstant.ZERO), userById));
            monthOperationInfoDTOS.add(totalDTO);

            monthStatisticsDTO.setMonthStatistics(monthOperationInfoDTOS);
            return monthStatisticsDTO;
        }

        /**
         * total month operation info
         *
         * @param propertyId             资产 id
         * @param year                   年
         * @param monthOperationInfoDTOS month operation info
         * @return total month operation info
         * @author yyt
         * @date 2025/11/14
         **/
        private MonthOperationInfoDTO packageTotalMonthOperationInfoDTO(int propertyId, int year, List<MonthOperationInfoDTO> monthOperationInfoDTOS) {
            BigDecimal totalBudgetRgi = null;
            int totalBudgetRgiMonth = BaseConstant.ZERO;
            BigDecimal totalActaulRgi = null;
            int totalActaulRgiMonth = BaseConstant.ZERO;
            Integer totalStrRevpar = null;
            int totalStrRevparMonth = BaseConstant.ZERO;
            BigDecimal totalHotelRevpar = null;
            int totalHotelRevparMonth = BaseConstant.ZERO;
            Integer totalCsRevpar = null;
            int totalCsRevparMonth = BaseConstant.ZERO;

            for (MonthOperationInfoDTO monthOperationInfoDTO : monthOperationInfoDTOS) {
                BigDecimal budgetRgi = monthOperationInfoDTO.getBudgetRgi();
                if (Objects.nonNull(budgetRgi)) {
                    totalBudgetRgi = Objects.isNull(totalBudgetRgi) ? budgetRgi : totalBudgetRgi.add(budgetRgi);
                    totalBudgetRgiMonth++;
                }
                BigDecimal actualRgi = monthOperationInfoDTO.getActualRgi();
                if (Objects.nonNull(actualRgi)) {
                    totalActaulRgi = Objects.isNull(totalActaulRgi) ? actualRgi : totalActaulRgi.add(actualRgi);
                    totalActaulRgiMonth++;
                }
                Object strRevpar = monthOperationInfoDTO.getStrRevpar();
                if (Objects.nonNull(strRevpar)) {
                    totalStrRevpar = Objects.isNull(totalStrRevpar) ? Integer.parseInt(strRevpar.toString()) : totalStrRevpar + Integer.parseInt(strRevpar.toString());
                    totalStrRevparMonth++;
                }
                BigDecimal hotelRevpar = monthOperationInfoDTO.getHotelRevpar();
                if (Objects.nonNull(hotelRevpar)) {
                    totalHotelRevpar = Objects.isNull(totalHotelRevpar) ? hotelRevpar : totalHotelRevpar.add(hotelRevpar);
                    totalHotelRevparMonth++;
                }
                Object csRevpar = monthOperationInfoDTO.getCsRevpar();
                if (Objects.nonNull(csRevpar)) {
                    totalCsRevpar = Objects.isNull(totalCsRevpar) ? Integer.parseInt(csRevpar.toString()) : totalCsRevpar + Integer.parseInt(csRevpar.toString());
                    totalCsRevparMonth++;
                }
            }

            OperationInfoDTO operationInfoDTOForTotal = packageOperationInfoDTO(propertyId, year, null, beanMapper.mapList(monthOperationInfoDTOS, BudgetDatabaseRoom.class));
            MonthOperationInfoDTO operationInfoForTotal = beanMapper.map(operationInfoDTOForTotal, MonthOperationInfoDTO.class);
            operationInfoForTotal.setYearMonth(-1);
            operationInfoForTotal.setBudgetRgi(budgetDatabaseCalculateService.calculateAvg(propertyId, totalBudgetRgi, totalBudgetRgiMonth));
            operationInfoForTotal.setActualRgi(budgetDatabaseCalculateService.calculateAvg(propertyId, totalActaulRgi, totalActaulRgiMonth));
            operationInfoForTotal.setRgiYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, operationInfoForTotal.getBudgetRgi(), operationInfoForTotal.getActualRgi()));

            operationInfoForTotal.setStrRevpar(budgetDatabaseCalculateService.calculateAvg(propertyId, totalStrRevpar, totalStrRevparMonth));
            operationInfoForTotal.setHotelRevpar(budgetDatabaseCalculateService.calculateAvg(propertyId, totalHotelRevpar, totalHotelRevparMonth));
            operationInfoForTotal.setCsRevpar(budgetDatabaseCalculateService.calculateAvg(propertyId, totalCsRevpar, totalCsRevparMonth));
            return operationInfoForTotal;
        }

        /**
         * 查询某个月 market segmentation
         *
         * @param propertyId 资产 id
         * @param tab        tab
         * @param month      月
         * @return market segmentation
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public MarketSegmentationDTO listMarketSegmentation(int propertyId, BudgetDatabaseConfigRoomTab tab, int month) {
            MarketSegmentationDTO marketSegmentationDTO = new MarketSegmentationDTO();
            // 最后修改人
            marketSegmentationDTO.setUpdatedInfo(budgetDatabaseMarketSegmentationLogService.getLastLogDTO(propertyId, tab.getId(), month));
            // segment statistics
            marketSegmentationDTO.setMonthStatistics(listRoom(propertyId, tab, month));
            return marketSegmentationDTO;
        }

        /**
         * 查询 room
         *
         * @param propertyId 资产 id
         * @param tab        年
         * @param month      月
         * @return room
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public List<RoomOperationInfoDTO> listRoom(int propertyId, BudgetDatabaseConfigRoomTab tab, Integer month) {
            int tabId = tab.getId();
            int year = tab.getBudgetYear();
            // 查询配置指标
            List<BudgetDatabaseConfigRoomTabItem> items = budgetDatabaseConfigRoomTabItemService.listByTabIdAndClassification(tabId, RoomClassificationEnum.ROOM.getCode());
            if (CollectionUtils.isEmpty(items)) return Collections.emptyList();

            // 查询 revenue
            int yearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(year, month)));
            Map<Integer, BudgetDatabaseRoom> revenueByItemId = listByItemIdsAndYearMonth(tabId, yearMonth, items.stream().map(BudgetDatabaseConfigRoomTabItem::getId).toList())
                    .stream().collect(Collectors.toMap(BudgetDatabaseRoom::getRoomId, v -> v));

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // comment
            List<BudgetDatabaseCommentRoom> comments = budgetDatabaseCommentRoomService.list(propertyId, tabId, Sets.newHashSet(month));
            Map<Integer, BudgetDatabaseCommentRoom> commentByRow = comments.stream().collect(Collectors.toMap(BudgetDatabaseCommentRoom::getRow, v -> v));
            Map<Integer, UserDTO> userById = userInternalService.getUsers(propertyDTO.getTenantId(), comments.stream().map(BudgetDatabaseCommentRoom::getUpdatedBy).distinct().toList())
                    .stream().collect(Collectors.toMap(UserDTO::getId, v -> v));

            return packageRoomOperationInfoDTO(propertyId, year, month, items, revenueByItemId, commentByRow, userById);
        }

        /**
         * 查询 revenue
         *
         * @param tabId     tab id
         * @param yearMonth 月
         * @param itemIds   指标 id
         * @return revenue
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public List<BudgetDatabaseRoom> listByItemIdsAndYearMonth(Integer tabId, Integer yearMonth, List<Integer> itemIds) {
            return list(new QueryWrapper<BudgetDatabaseRoom>().lambda()
                    .eq(BudgetDatabaseRoom::getTabId, tabId)
                    .eq(BudgetDatabaseRoom::getYearMonth, yearMonth)
                    .in(CollectionUtils.isNotEmpty(itemIds), BudgetDatabaseRoom::getRoomId, itemIds));
        }

        /**
         * 封装每月 room operation info
         *
         * @param propertyId         资产 id
         * @param year               年
         * @param month              月
         * @param items              指标
         * @param revenueBySegmentId revenue
         * @param commentByRow       评论
         * @param userById           评论人
         * @return RoomOperationInfoDTO
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public List<RoomOperationInfoDTO> packageRoomOperationInfoDTO(int propertyId, int year, Integer month, List<BudgetDatabaseConfigRoomTabItem> items,
                                                                      Map<Integer, BudgetDatabaseRoom> revenueBySegmentId,
                                                                      Map<Integer, BudgetDatabaseCommentRoom> commentByRow, Map<Integer, UserDTO> userById) {
            // 按照类型分组，循环完一组后，汇总，然后再循环下一组，最后处理 total
            int size = items.size();
            List<RoomOperationInfoDTO> segmentOperationInfoDTOS = Lists.newArrayListWithExpectedSize(size);
            Map<Integer, List<BudgetDatabaseConfigRoomTabItem>> segmentsByType = items.stream().collect(Collectors.groupingBy(BudgetDatabaseConfigRoomTabItem::getType));
            segmentsByType.forEach((type, typeSegments) -> {
                // 按照 sort 排序
                typeSegments.sort(Comparator.comparing(BudgetDatabaseConfigRoomTabItem::getSort));

                List<RoomOperationInfoDTO> segmentOperationInfoDTOSForType = Lists.newArrayListWithExpectedSize(size);
                for (BudgetDatabaseConfigRoomTabItem item : typeSegments) {
                    BudgetDatabaseRoom revenue = revenueBySegmentId.getOrDefault(item.getId(), new BudgetDatabaseRoom());
                    RoomOperationInfoDTO segmentInfoDTO = beanMapper.map(packageOperationInfoDTO(propertyId, year, month, revenue), RoomOperationInfoDTO.class);
                    segmentInfoDTO.setRoomId(item.getId());
                    segmentInfoDTO.setName(item.getName());
                    segmentInfoDTO.setType(item.getType());
                    segmentInfoDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(item.getId()), userById));
                    segmentInfoDTO.setRoomParticipateCalculate(item.getEnableBudgetRoom());
                    segmentInfoDTO.setRevenueParticipateCalculate(item.getEnableBudgetRevenue());
                    segmentOperationInfoDTOSForType.add(segmentInfoDTO);
                    segmentOperationInfoDTOS.add(segmentInfoDTO);
                }

                // 分类汇总
                RoomOperationInfoDTO segmentInfoDTO = beanMapper.map(packageOperationInfoDTO(propertyId, year, month,
                        beanMapper.mapList(segmentOperationInfoDTOSForType, BudgetDatabaseRoom.class)), RoomOperationInfoDTO.class);
                segmentInfoDTO.setRoomId(RoomTypeEnum.getRowId(type));
                segmentInfoDTO.setName(RoomTypeEnum.getDesc(type));
                segmentInfoDTO.setType(type);
                segmentInfoDTO.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(RoomTypeEnum.getRowId(type)), userById));
                segmentOperationInfoDTOS.add(segmentInfoDTO);
            });

            // segment 设置 % of Total
            packagePercentageOfTotal(propertyId, segmentOperationInfoDTOS);

            // total
            List<RoomOperationInfoDTO> summaryByCategoryDTOs = segmentOperationInfoDTOS.stream().filter(s -> s.getRoomId() < BaseConstant.ZERO).toList();
            OperationInfoDTO operationInfoForTotal = packageOperationInfoDTO(propertyId, year, month, beanMapper.mapList(summaryByCategoryDTOs, BudgetDatabaseRoom.class));
            List<BigDecimal> revenueTotalAll = summaryByCategoryDTOs.stream().map(OperationInfoDTO::getRevenueTotal).filter(Objects::nonNull).toList();
            operationInfoForTotal.setRevenueTotal(CollectionUtils.isEmpty(revenueTotalAll) ? null : revenueTotalAll.stream().reduce(BigDecimal.ZERO, BigDecimal::add));
            RoomOperationInfoDTO segmentOperationInfoForTotal = beanMapper.map(operationInfoForTotal, RoomOperationInfoDTO.class);
            segmentOperationInfoForTotal.setRoomId(BaseConstant.ZERO);
            segmentOperationInfoForTotal.setName(BudgetDatabaseTitleCons.TOTAL);
            segmentOperationInfoForTotal.setType(BaseConstant.ZERO);
            segmentOperationInfoForTotal.setComment(budgetDatabaseCommentRoomService.packageCommentDTO(commentByRow.get(BaseConstant.ZERO), userById));
            segmentOperationInfoDTOS.add(segmentOperationInfoForTotal);

            return segmentOperationInfoDTOS;
        }

        /**
         * room
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param revenue   revenue
         * @return room
         * @author yyt
         * @date 2025/11/14
         **/
        private OperationInfoDTO packageOperationInfoDTO(int propertyId, int year, Integer month, BudgetDatabaseRoom revenue) {
            OperationInfoDTO operationInfoDTO = new OperationInfoDTO();
            operationInfoDTO.setBudgetRooms(revenue.getBudgetRooms());
            operationInfoDTO.setActualRooms(revenue.getActualRooms());
            operationInfoDTO.setRoomsYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, revenue.getBudgetRooms(), revenue.getActualRooms()));

            operationInfoDTO.setBudgetAdr(revenue.getBudgetAdr());
            operationInfoDTO.setActualAdr(revenue.getActualAdr());
            operationInfoDTO.setAdrYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, revenue.getBudgetAdr(), revenue.getActualAdr()));

            operationInfoDTO.setBudgetRevenue(revenue.getBudgetRevenue());
            operationInfoDTO.setActualRevenue(revenue.getActualRevenue());
            operationInfoDTO.setRevenueYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, revenue.getBudgetRevenue(), revenue.getActualRevenue()));

            operationInfoDTO.setBudgetOcc(budgetDatabaseCalculateService.calculateOcc(propertyId, year, month, revenue.getBudgetRooms()));
            operationInfoDTO.setActualOcc(budgetDatabaseCalculateService.calculateOcc(propertyId, year - BaseConstant.ONE, month, revenue.getActualRooms()));
            operationInfoDTO.setOccChange(budgetDatabaseCalculateService.calculateOccChange(operationInfoDTO.getBudgetOcc(), operationInfoDTO.getActualOcc()));

            operationInfoDTO.setBudgetRevpar(budgetDatabaseCalculateService.calculateRevpar(propertyId, revenue.getBudgetAdr(), operationInfoDTO.getBudgetOcc()));
            operationInfoDTO.setActualRevpar(budgetDatabaseCalculateService.calculateRevpar(propertyId, revenue.getActualAdr(), operationInfoDTO.getActualOcc()));
            operationInfoDTO.setRevparYoyChg(budgetDatabaseCalculateService.calculateYoyChg(propertyId, operationInfoDTO.getBudgetRevpar(), operationInfoDTO.getActualRevpar()));

            return operationInfoDTO;
        }

        /**
         * 封装  % of Total
         *
         * @param propertyId               资产 id
         * @param segmentOperationInfoDTOS RoomOperationInfoDTO
         * @author yyt
         * @date 2025/11/14
         **/
        private void packagePercentageOfTotal(Integer propertyId, List<RoomOperationInfoDTO> segmentOperationInfoDTOS) {
            List<BigDecimal> totalBudgetRevenues = segmentOperationInfoDTOS.stream().filter(item -> item.getRoomId() < BaseConstant.ZERO)
                    .map(OperationInfoDTO::getBudgetRevenue).filter(Objects::nonNull).collect(Collectors.toList());
            BigDecimal totalBudgetRevenue = CollectionUtils.isEmpty(totalBudgetRevenues) ? null : totalBudgetRevenues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);

            segmentOperationInfoDTOS.stream().collect(Collectors.groupingBy(RoomOperationInfoDTO::getType))
                    .forEach((type, segmentOperationInfoDTOSForType) -> {

                        BigDecimal revenueTotalAll = null;
                        // 设置当前分类下每个 segment 的 % of Total
                        List<RoomOperationInfoDTO> notIncludeCategorySummaryDTOS = segmentOperationInfoDTOSForType.stream()
                                .filter(item -> item.getRoomId() > BaseConstant.ZERO).toList();
                        for (RoomOperationInfoDTO segmentOperationInfoDTO : notIncludeCategorySummaryDTOS) {
                            BigDecimal revenueTotal = budgetDatabaseCalculateService.calculateRevenueTotal(propertyId, segmentOperationInfoDTO.getBudgetRevenue(), totalBudgetRevenue);
                            segmentOperationInfoDTO.setRevenueTotal(revenueTotal);
                            if (Objects.nonNull(revenueTotal)) {
                                revenueTotalAll = Objects.isNull(revenueTotalAll) ? revenueTotal : revenueTotalAll.add(revenueTotal);
                            }
                        }
                        // 设置分类汇总的 % of Total
                        Optional<RoomOperationInfoDTO> first = segmentOperationInfoDTOSForType.stream()
                                .filter(item -> item.getRoomId() < BaseConstant.ZERO)
                                .findFirst();
                        if (first.isPresent()) {
                            first.get().setRevenueTotal(revenueTotalAll);
                        }
                    });
        }

        /**
         * 检查是否可以修改预测信息
         *
         * @param tab                          tab
         * @param month                        月
         * @param updateMarketSegmentationDTOS 修改信息
         * @author yyt
         * @date 2025/8/15
         **/
        public void checkCanUpdateBudgetForStatus(BudgetDatabaseConfigRoomTab tab, int month, List<UpdateMarketSegmentationDTO> updateMarketSegmentationDTOS) {
            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            Integer tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            Integer year = tab.getBudgetYear();
            BudgetDatabase budgetDatabase = budgetDatabaseService.get(propertyId, year);
            if (Objects.isNull(budgetDatabase)) return;
            if (Objects.equals(budgetDatabase.getStatus(), BudgetDatabaseStatusEnum.DRAFT.getCode())) return;

            // 检查 Budget 数据不可修改
            // 查询数据库对应的 revenue 数据
            int yearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(year, month)));
            Map<Integer, BudgetDatabaseRoom> existRevenueBySegmentId = listByItemIdsAndYearMonth(tabId, yearMonth, updateMarketSegmentationDTOS.stream()
                    .map(UpdateMarketSegmentationDTO::getRoomId).collect(Collectors.toList()))
                    .stream().collect(Collectors.toMap(BudgetDatabaseRoom::getRoomId, v -> v));

            for (UpdateMarketSegmentationDTO updateMarketSegmentationDTO : updateMarketSegmentationDTOS) {
                BudgetDatabaseRoom existRevenue = existRevenueBySegmentId.getOrDefault(updateMarketSegmentationDTO.getRoomId(), new BudgetDatabaseRoom());
                if (!NumberUtil.equals(existRevenue.getBudgetRooms(), updateMarketSegmentationDTO.getBudgetRooms())
                        || !NumberUtil.equals(existRevenue.getBudgetAdr(), updateMarketSegmentationDTO.getBudgetAdr())
                        || !NumberUtil.equals(existRevenue.getBudgetRevenue(), updateMarketSegmentationDTO.getBudgetRevenue())){
                    throw new BadRequestException(BudgetDatabaseErrorCons.EDITING_NOT_SUPPORTED_FOR_COMPLETED_STATUS,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the budget data is not allowed to be modified");
                }
            }
        }

        /**
         * 检查是否可以修改预测信息
         *
         * @param tab                          tab
         * @param month                        月
         * @param updateMarketSegmentationDTOS 修改信息
         * @author yyt
         * @date 2025/8/15
         **/
        @Override
        public void checkCanUpdateForReopen(BudgetDatabaseConfigRoomTab tab, int month, List<UpdateMarketSegmentationDTO> updateMarketSegmentationDTOS) {
            Integer tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            Integer year = tab.getBudgetYear();
            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            boolean hasReopen = budgetDatabaseStatusLogService.checkHasReopen(propertyId, year);
            if (!hasReopen) return;

            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            ZonedDateTime currentDate = DateTimeUtils.dateNow(timezone);
            int currentYearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(currentDate.getYear(), currentDate.getMonthValue())));

            // 检查 Budget 数据不可修改
            int updateYearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(year, month)));
            boolean updatePreviousMonth = updateYearMonth < currentYearMonth;
            if (!updatePreviousMonth) return;
            // 查询数据库对应的 revenue 数据
            Map<Integer, BudgetDatabaseRoom> existRevenueByRoomId = listByItemIdsAndYearMonth(tabId, updateYearMonth, updateMarketSegmentationDTOS.stream()
                    .map(UpdateMarketSegmentationDTO::getRoomId).collect(Collectors.toList()))
                    .stream().collect(Collectors.toMap(BudgetDatabaseRoom::getRoomId, v -> v));
            for (UpdateMarketSegmentationDTO updateMarketSegmentationDTO : updateMarketSegmentationDTOS) {
                BudgetDatabaseRoom existRevenue = existRevenueByRoomId.getOrDefault(updateMarketSegmentationDTO.getRoomId(), new BudgetDatabaseRoom());
                if (!NumberUtil.equals(existRevenue.getBudgetRooms(), updateMarketSegmentationDTO.getBudgetRooms())
                        || !NumberUtil.equals(existRevenue.getBudgetAdr(), updateMarketSegmentationDTO.getBudgetAdr())
                        || !NumberUtil.equals(existRevenue.getBudgetRevenue(), updateMarketSegmentationDTO.getBudgetRevenue())) {
                    throw new BadRequestException(BudgetDatabaseErrorCons.EDITING_NOT_SUPPORTED_FOR_REOPEN,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the budget data of the previous month cannot be modified after reopen");
                }
            }
        }

        /**
         * 检查是否可以修改预测信息
         *
         * @param tab                          tab
         * @param month                        月
         * @param updateMarketSegmentationDTOS 修改信息
         * @author yyt
         * @date 2025/8/15
         **/
        @Override
        public void checkCanUpdateForParticipateCalculate(BudgetDatabaseConfigRoomTab tab, int month, List<UpdateMarketSegmentationDTO> updateMarketSegmentationDTOS) {
            // 检查若指标是参与计算的，则不能手动修改预算数据，但是能改实际数据
            List<Integer> roomItemIds = updateMarketSegmentationDTOS.stream().map(UpdateMarketSegmentationDTO::getRoomId).toList();
            Map<Integer, BudgetDatabaseConfigRoomTabItem> enableBudgetRoomOrRevenueItemById = budgetDatabaseConfigRoomTabItemService.listByIds(roomItemIds)
                    .stream().filter(item -> item.getEnableBudgetRoom() || item.getEnableBudgetRevenue())
                    .collect(Collectors.toMap(BudgetDatabaseConfigRoomTabItem::getId, v -> v));
            Map<Integer, BudgetDatabaseRoom> existRoomById = listByItemIdsAndYearMonth(tab.getId(),
                    Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(tab.getBudgetYear(), month))), roomItemIds)
                    .stream().collect(Collectors.toMap(BudgetDatabaseRoom::getRoomId, v -> v));
            for (UpdateMarketSegmentationDTO updateRevenueDTO : updateMarketSegmentationDTOS) {
                BudgetDatabaseRoom existRoom = existRoomById.getOrDefault(updateRevenueDTO.getRoomId(), new BudgetDatabaseRoom());
                BudgetDatabaseConfigRoomTabItem enableBudgetRoomOrRevenueItem = enableBudgetRoomOrRevenueItemById.get(updateRevenueDTO.getRoomId());
                if (Objects.nonNull(enableBudgetRoomOrRevenueItem)) {
                    // 开启 room 计算的，则不能修改 rooms
                    // 开启 revenue 的，则不能修改 revenue
                    if ((enableBudgetRoomOrRevenueItem.getEnableBudgetRoom() && !NumberUtil.equals(existRoom.getBudgetRooms(), updateRevenueDTO.getBudgetRooms()))
                            || (enableBudgetRoomOrRevenueItem.getEnableBudgetRevenue() && !NumberUtil.equals(existRoom.getBudgetRevenue(), updateRevenueDTO.getBudgetRevenue()))) {
                        throw new BadRequestException(BudgetDatabaseErrorCons.CANNOT_EDIT_BUDGET_DATA_FOR_CALCULATED_INDICATORS,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the budget data of the previous month cannot be modified after participate calculate");
                    }
                }
            }
        }

        /**
         * 修改每月 room 信息
         *
         * @param tab                          tab
         * @param month                        月
         * @param updateMarketSegmentationDTOS 预测信息
         * @param userId                       操作人
         * @author yyt
         * @date 2025/8/15
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateMarketSegmentation(BudgetDatabaseConfigRoomTab tab, int month, List<UpdateMarketSegmentationDTO> updateMarketSegmentationDTOS, int userId) {
            Integer tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            Integer year = tab.getBudgetYear();
            // 处理 budget database 状态数据
            budgetDatabaseService.handleSaveBudgetDatabaseStatus(propertyId, year);

            int yearMonth = Integer.parseInt(YearMonthUtils.DATE_TIME_FORMATTER.format(YearMonth.of(year, month)));

            List<BudgetDatabaseConfigRoomTabItem> roomTabItems = budgetDatabaseConfigRoomTabItemService.listByIds(updateMarketSegmentationDTOS.stream().map(UpdateMarketSegmentationDTO::getRoomId).toList());
            Map<Integer, BudgetDatabaseConfigRoomTabItem> roomById = roomTabItems.stream().collect(Collectors.toMap(BudgetDatabaseConfigRoomTabItem::getId, v -> v));

            // 保存指标数据
            baseMapper.saveOrUpdateBatch(updateMarketSegmentationDTOS.stream().map(item -> {
                BudgetDatabaseRoom room = new BudgetDatabaseRoom()
                        .setPropertyId(propertyId)
                        .setTabId(tabId)
                        .setRoomId(item.getRoomId())
                        .setYearMonth(yearMonth)
                        .setBudgetRooms(item.getBudgetRooms())
                        .setBudgetAdr(item.getBudgetAdr())
                        .setBudgetRevenue(budgetDatabaseCalculateService.calculateRevenue(propertyId, item.getBudgetRooms(), item.getBudgetAdr()))
                        .setActualRooms(item.getActualRooms())
                        .setActualAdr(item.getActualAdr())
                        .setActualRevenue(budgetDatabaseCalculateService.calculateRevenue(propertyId, item.getActualRooms(), item.getActualAdr()));

                BudgetDatabaseConfigRoomTabItem roomConfig = roomById.get(item.getRoomId());
                // 若开启了 revenue 或 room，则需重新计算 budget adr
                if (roomConfig.getEnableBudgetRevenue() || roomConfig.getEnableBudgetRoom()) {
                    room.setBudgetRooms(item.getBudgetRooms());
                    room.setBudgetRevenue(item.getBudgetRevenue());
                    room.setBudgetAdr(budgetDatabaseCalculateService.calculateAdr(propertyId, item.getBudgetRevenue(), item.getBudgetRooms()));
                }

                return room;
            }).toList());

            // 记录操作日志
            budgetDatabaseMarketSegmentationLogService.saveLog(propertyId, tabId, month, userId);

            // 过滤出没有参与计算的指标，看看这些指标是否配置在变配置中，若有，则重置依赖变量配置的相关指标数据
            List<BudgetDatabaseConfigRoomTabItem> notEnableItems = roomTabItems.stream()
                    .filter(item -> !item.getEnableBudgetRevenue() && !item.getEnableBudgetRoom()).toList();

            if (CollectionUtils.isEmpty(notEnableItems)) {
                return;
            }

            // 查询引用当前指标的变量配置
            List<BudgetDatabaseConfigVariableRoom> variableRooms = budgetDatabaseConfigVariableRoomService.listByItemIds(notEnableItems.stream().map(BudgetDatabaseConfigRoomTabItem::getId).toList());
            if (CollectionUtils.isEmpty(variableRooms)) {
                return;
            }
            Map<Integer, List<Integer>> variableIdByType = budgetDatabaseConfigVariableService.listByIds(variableRooms.stream().map(BudgetDatabaseConfigVariableRoom::getVariableId).distinct().toList())
                    .stream().collect(Collectors.groupingBy(BudgetDatabaseConfigVariable::getType,
                            Collectors.mapping(BudgetDatabaseConfigVariable::getId, Collectors.toList())));
            List<Integer> handleRevenueVariableIds = variableIdByType.getOrDefault(VariableTypeEnum.REVENUE.getCode(), Collections.emptyList());
            List<Integer> handleRoomVariableIds = variableIdByType.getOrDefault(VariableTypeEnum.ROOM.getCode(), Collections.emptyList());
            Integer budgetYear = tab.getBudgetYear();
            if (CollectionUtils.isNotEmpty(handleRevenueVariableIds) && CollectionUtils.isNotEmpty(handleRoomVariableIds)) {
                budgetDatabaseCalculateRevenueAndRoomService.updateBudgetRevenueAndRoomForRoom(propertyId, budgetYear,
                        handleRevenueVariableIds, handleRoomVariableIds);
            } else {
                if (CollectionUtils.isNotEmpty(handleRevenueVariableIds)) {
                    budgetDatabaseCalculateRevenueAndRoomService.updateBudgetRevenue(propertyId, budgetYear, handleRevenueVariableIds);
                } else {
                    budgetDatabaseCalculateRevenueAndRoomService.updateBudgetRoom(propertyId, budgetYear, handleRoomVariableIds);
                }
            }
        }

        /**
         * 获取资产下的 room 数据
         *
         * @param propertyId 资产 id
         * @return room 数据
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public List<BudgetDatabaseRoom> listByPropertyId(Integer propertyId) {
            return list(new QueryWrapper<BudgetDatabaseRoom>().lambda().eq(BudgetDatabaseRoom::getPropertyId, propertyId));
        }

        /**
         * 批量更新 room
         *
         * @param updateRooms room
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public void updateBatchByIdRewrite(List<BudgetDatabaseRoom> updateRooms) {
            if (CollectionUtils.isEmpty(updateRooms)) {
                return;
            }
            baseMapper.updateBatchByIdRewrite(updateRooms);
        }

        /**
         * 清空预算数据
         *
         * @param id 预算 id
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void clearBudgetRevenue(Integer id) {
            List<BudgetDatabaseRoom> rooms = listByItemId(id);
            if (CollectionUtils.isEmpty(rooms)) return;

            List<BudgetDatabaseRoom> updateRooms = rooms.stream()
                    .filter(item -> Objects.isNull(item.getBudgetRooms()) && Objects.isNull(item.getBudgetAdr()) && Objects.nonNull(item.getBudgetRevenue()))
                    .map(item -> new BudgetDatabaseRoom().setId(item.getId()).setBudgetRevenue(null)).toList();
            if (CollectionUtils.isNotEmpty(updateRooms)) {
                baseMapper.clearBudgetRevenue(updateRooms);
            }
        }

    }