# MonthlyReportBudgetDatabaseServiceImpl.java

    package cloud.wconnect.workperformance.service.monthlyreport.impl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.RoomsConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.monthlyreport.MonthlyReportIndexTypeEnum;
    import cloud.wconnect.workperformance.entity.monthlyreport.MonthlyReportBudgetDatabase;
    import cloud.wconnect.workperformance.mapper.monthlyreport.MonthlyReportBudgetDatabaseMapper;
    import cloud.wconnect.workperformance.pojo.bo.modelevent.budgetdb.BudgetDatabaseBO;
    import cloud.wconnect.workperformance.pojo.dto.monthlyreport.budgetdb.*;
    import cloud.wconnect.workperformance.service.monthlyreport.IMonthlyReportBudgetDatabaseService;
    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 com.google.common.collect.Maps;
    import org.apache.commons.collections.CollectionUtils;
    import org.checkerframework.checker.nullness.qual.Nullable;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

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

    /**
     * <p>
     * Monthly Report Budget Database 数据 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-12-22
     */
    @DS("monthly_report")
    @Service
    public class MonthlyReportBudgetDatabaseServiceImpl extends ServiceImpl<MonthlyReportBudgetDatabaseMapper, MonthlyReportBudgetDatabase> implements IMonthlyReportBudgetDatabaseService {

        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private RoomsConfigInternalService roomsConfigInternalService;

        /**
         * 保存数据
         *
         * @param eventBO 数据
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public void save(BudgetDatabaseBO eventBO) {
            MonthlyReportBudgetDatabase budgetDatabase = new MonthlyReportBudgetDatabase();
            budgetDatabase.setPropertyId(eventBO.getPropertyId());
            budgetDatabase.setYear(eventBO.getYear());
            budgetDatabase.setRoom(JsonUtils.convertObjListToString(objectMapper, eventBO.getRoom()));
            budgetDatabase.setFb(JsonUtils.convertObjListToString(objectMapper, eventBO.getFb()));
            budgetDatabase.setOther(JsonUtils.convertObjListToString(objectMapper, eventBO.getOther()));
            baseMapper.insertOrUpdateByUnique(budgetDatabase);
        }

        /**
         * 获取Budget指标
         *
         * @param propertyId 资产id
         * @param yearMonths 月份集合
         * @param typeList   指标类型集合
         * @return java.util.Map<java.time.YearMonth,java.util.Map<java.lang.Integer,java.math.BigDecimal>>
         * @author lichen
         * @date 2025/12/20
         */
        @Override
        public Map<YearMonth, Map<Integer, BigDecimal>> listMultiMonthBudget(Integer propertyId, List<YearMonth> yearMonths, List<Integer> typeList) {
            if(CollectionUtils.isEmpty(typeList)){
                return Maps.newHashMap();
            }
            if(CollectionUtils.isEmpty(yearMonths)){
                return Maps.newHashMap();
            }

            Set<Integer> years = yearMonths.stream().map(YearMonth::getYear).collect(Collectors.toSet());
            // 获取Budget数据（最多2年）
            HashMap<Integer, MonthlyReportBudgetDatabaseDTO> yearToList = Maps.newHashMapWithExpectedSize(years.size());
            for (Integer year : years) {
                MonthlyReportBudgetDatabaseDTO dto = this.getBudgetByYear(propertyId, year);
                yearToList.put(year, dto);
            }

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            // 获取资产房间数配置
            RoomsConfigDTO roomsConfigDTO = roomsConfigInternalService.getRoomsConfig(propertyId);
            Integer rooms = roomsConfigDTO.getRooms();

            ArrayList<MonthlyReportMultiMonthBudgetDTO> resultList = Lists.newArrayListWithCapacity(yearMonths.size());

            for (YearMonth yearMonth : yearMonths) {
                long yearMonthToLong = YearMonthUtils.parseYearMonthToLong(yearMonth);

                ArrayList<MonthlyReportBudgetDTO> budgetDTOS = Lists.newArrayListWithCapacity(6);

                // 获取指定年份的Budget数据
                MonthlyReportBudgetDatabaseDTO budgetDatabaseDTO = yearToList.get(yearMonth.getYear());

                // 获取当月天数
                int days = yearMonth.lengthOfMonth();

                // 获取roomsSold
                Long roomsSold = this.getRoomsSoldBudget(budgetDatabaseDTO, yearMonthToLong, yearMonthToLong);
                // 获取RoomRevenue
                BigDecimal roomRevenue = this.getRoomRevenueBudget(budgetDatabaseDTO, yearMonthToLong, yearMonthToLong);

                for (Integer type : typeList) {
                    if (type.equals(MonthlyReportIndexTypeEnum.OCC_PERCENTAGE.getType())) {
                        BigDecimal occPercent = calculateOccPercent(rooms, roomsSold, days, roundingMode);
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.OCC_PERCENTAGE.getType(), occPercent));
                    } else if (type.equals(MonthlyReportIndexTypeEnum.REV_PAR.getType())) {
                        BigDecimal revPar = calculateRevPar(rooms, roomRevenue, days, roundingMode);
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.REV_PAR.getType(), revPar));
                    } else if (type.equals(MonthlyReportIndexTypeEnum.ADR.getType())) {
                        BigDecimal adr = calculateAdr(roomRevenue, roomsSold, roundingMode);
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.ADR.getType(), adr));
                    } else if (type.equals(MonthlyReportIndexTypeEnum.ROOMS_REVENUE.getType())) {
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.ROOMS_REVENUE.getType(), roomRevenue));
                    } else if (type.equals(MonthlyReportIndexTypeEnum.FOOD_BEVERAGE.getType())) {
                        BigDecimal fbRevenue = this.getFBRevenueBudget(budgetDatabaseDTO, yearMonthToLong, yearMonthToLong);
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.FOOD_BEVERAGE.getType(), fbRevenue));
                    } else if (type.equals(MonthlyReportIndexTypeEnum.OTHERS.getType())) {
                        BigDecimal otherRevenue = this.getOtherRevenueBudget(budgetDatabaseDTO, yearMonthToLong, yearMonthToLong);
                        budgetDTOS.add(new MonthlyReportBudgetDTO(MonthlyReportIndexTypeEnum.OTHERS.getType(), otherRevenue));
                    }
                }

                MonthlyReportMultiMonthBudgetDTO monthlyReportMultiMonthBudgetDTO = new MonthlyReportMultiMonthBudgetDTO();
                monthlyReportMultiMonthBudgetDTO.setYearMonth(yearMonth);
                monthlyReportMultiMonthBudgetDTO.setList(budgetDTOS);
                resultList.add(monthlyReportMultiMonthBudgetDTO);
            }
            return resultList.stream()
                    .collect(Collectors.toMap(
                            MonthlyReportMultiMonthBudgetDTO::getYearMonth,
                            dto -> dto.getList().stream()
                                    .filter(item -> Objects.nonNull(item.getValue()))
                                    .collect(Collectors.toMap(MonthlyReportBudgetDTO::getType, MonthlyReportBudgetDTO::getValue))
                    ));
        }

        /**
         * 获取指定年的 Budget Database 数据
         *
         * @param propertyId 资产 id
         * @param year       年(yyyy)
         * @return MonthlyReportBudgetDatabaseDTO
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public MonthlyReportBudgetDatabaseDTO getBudgetByYear(int propertyId, int year) {
            MonthlyReportBudgetDatabase budgetDatabase = getOne(new LambdaQueryWrapper<MonthlyReportBudgetDatabase>()
                    .eq(MonthlyReportBudgetDatabase::getPropertyId, propertyId)
                    .eq(MonthlyReportBudgetDatabase::getYear, year));

            MonthlyReportBudgetDatabaseDTO databaseDTO = new MonthlyReportBudgetDatabaseDTO();
            if (Objects.isNull(budgetDatabase)) {
                return databaseDTO;
            }
            databaseDTO.setPropertyId(budgetDatabase.getPropertyId());
            databaseDTO.setYear(budgetDatabase.getYear());
            databaseDTO.setRoom(JsonUtils.convertStringToList(objectMapper,
                    budgetDatabase.getRoom(), MonthlyReportBudgetDatabaseRoomTabDTO.class));
            databaseDTO.setFb(JsonUtils.convertStringToList(objectMapper,
                    budgetDatabase.getFb(), MonthlyReportBudgetDatabaseFbTabDTO.class));
            databaseDTO.setOther(JsonUtils.convertStringToList(objectMapper,
                    budgetDatabase.getOther(), MonthlyReportBudgetDatabaseOtherTabDTO.class));
            return databaseDTO;
        }

        /**
         * 获取房间数销售预算
         *
         * @param budgetDTO 数据
         * @param startDate 开始时间(yyyyMM)
         * @param endDate   结束时间(yyyyMM)
         * @return 房间数销售预算
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public Long getRoomsSoldBudget(MonthlyReportBudgetDatabaseDTO budgetDTO, long startDate, long endDate) {
            return budgetDTO.getRoom().stream()
                    .filter(MonthlyReportBudgetDatabaseRoomTabDTO::getOpen)  // 过滤开启的 tab
                    .flatMap(roomTabDTO -> roomTabDTO.getRoom().stream())  // 展平获取 room 列表
                    .flatMap(roomDTO -> roomDTO.getData().stream())  // 展平获取 data 列表
                    .filter(data -> data.getYearMonth() >= startDate && data.getYearMonth() <= endDate)  // 时间范围过滤
                    .map(MonthlyReportBudgetDatabaseValueRoom::getRoomsBudget)
                    .filter(Objects::nonNull).reduce(Long::sum).orElse(null);
        }

        /**
         * 获取房间收入预算
         *
         * @param budgetDTO 数据
         * @param startDate 开始时间(yyyyMM)
         * @param endDate   结束时间(yyyyMM)
         * @return 房间收入预算
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public BigDecimal getRoomRevenueBudget(MonthlyReportBudgetDatabaseDTO budgetDTO, long startDate, long endDate) {
            return budgetDTO.getRoom().stream()
                    .filter(MonthlyReportBudgetDatabaseRoomTabDTO::getOpen)  // 过滤开启的 tab
                    .flatMap(roomTabDTO -> roomTabDTO.getRoom().stream())  // 展平获取 room 列表
                    .flatMap(roomDTO -> roomDTO.getData().stream())  // 展平获取 data 列表
                    .filter(data -> data.getYearMonth() >= startDate && data.getYearMonth() <= endDate)  // 时间范围过滤
                    .map(MonthlyReportBudgetDatabaseValueRoom::getRevenueBudget)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
        }

        /**
         * 获取其他收入预算
         *
         * @param budgetDTO 数据
         * @param startDate 开始时间(yyyyMM)
         * @param endDate   结束时间(yyyyMM)
         * @return 其他收入预算
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public BigDecimal getOtherRevenueBudget(MonthlyReportBudgetDatabaseDTO budgetDTO, long startDate, long endDate) {
            return budgetDTO.getOther().stream()
                    .filter(MonthlyReportBudgetDatabaseOtherTabDTO::getOpen)  // 过滤开启的 tab
                    .flatMap(otherTabDTO -> otherTabDTO.getOther().stream())  // 展平获取 other 列表
                    .flatMap(otherDTO -> otherDTO.getData().stream())  // 展平获取 data 列表
                    .filter(data -> data.getYearMonth() >= startDate && data.getYearMonth() <= endDate)  // 时间范围过滤
                    .map(MonthlyReportBudgetDatabaseValueDTO::getBudget)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
        }

        /**
         * 获取F&B收入预算
         *
         * @param budgetDTO 数据
         * @param startDate 开始时间(yyyyMM)
         * @param endDate   结束时间(yyyyMM)
         * @return F&B收入预算
         * @author lichen
         * @date 2025/12/22
         */
        @Override
        public BigDecimal getFBRevenueBudget(MonthlyReportBudgetDatabaseDTO budgetDTO, long startDate, long endDate) {
            return budgetDTO.getFb().stream()
                    .filter(MonthlyReportBudgetDatabaseFbTabDTO::getOpen)  // 过滤开启的 tab
                    .flatMap(fbTabDTO -> fbTabDTO.getRevenue().stream())  // 展平获取 fb 列表
                    .flatMap(fbDTO -> fbDTO.getData().stream())  // 展平获取 data 列表
                    .filter(data -> data.getYearMonth() >= startDate && data.getYearMonth() <= endDate)  // 时间范围过滤
                    .map(MonthlyReportBudgetDatabaseValueDTO::getBudget)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
        }

        /**
         * 计算 OCC %
         *
         * @param rooms        房间数
         * @param roomsSold    售出房间数
         * @param days         天数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author lichen
         * @date 2025/12/20
         */
        private BigDecimal calculateOccPercent(Integer rooms, Long roomsSold, int days, RoundingMode roundingMode) {
            if (days == 0) {
                return null;
            }
            if (Objects.isNull(rooms) || rooms == 0) {
                return null;
            }
            if (Objects.isNull(roomsSold)) {
                return null;
            }
            return new BigDecimal(String.valueOf(roomsSold)).multiply(new BigDecimal("100"))
                    .divide(new BigDecimal(String.valueOf(rooms)).multiply(new BigDecimal(String.valueOf(days))),
                            BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 计算 Rev Par
         *
         * @param rooms        房间数
         * @param roomRevenue  房间收入
         * @param days         天数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author lichen
         * @date 2025/12/20
         */
        private BigDecimal calculateRevPar(Integer rooms, BigDecimal roomRevenue, int days, RoundingMode roundingMode) {
            if (days == 0) {
                return null;
            }
            if (Objects.isNull(rooms) || rooms == 0) {
                return null;
            }
            if (Objects.isNull(roomRevenue)) {
                return null;
            }
            return roomRevenue.divide(new BigDecimal(String.valueOf(rooms))
                    .multiply(new BigDecimal(String.valueOf(days))), BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 计算 ADR
         *
         * @param roomRevenue  房间收入
         * @param roomsSold    售出房间数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author lichen
         * @date 2025/12/20
         */
        private BigDecimal calculateAdr(BigDecimal roomRevenue, Long roomsSold, RoundingMode roundingMode) {
            if (Objects.isNull(roomsSold) || roomsSold == 0) {
                return null;
            }
            if (Objects.isNull(roomRevenue)) {
                return null;
            }
            return roomRevenue.divide(new BigDecimal(String.valueOf(roomsSold)), BaseConstant.DATA_PRECISION, roundingMode);
        }
    }