# WeeklyMonthlyRevenueBudgetServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.rateshop.cons.budgetdatabase.RoomTypeEnum;
    import cloud.wconnect.rateshop.entity.weeklymonthlyrevenue.WeeklyMonthlyRevenueBudget;
    import cloud.wconnect.rateshop.mapper.weeklymonthlyrevenue.WeeklyMonthlyRevenueBudgetMapper;
    import cloud.wconnect.rateshop.pojo.bo.budgetdatabase.event.BudgetDatabaseBO;
    import cloud.wconnect.rateshop.pojo.bo.budgetdatabase.event.BudgetDatabaseRoomBO;
    import cloud.wconnect.rateshop.pojo.bo.budgetdatabase.event.BudgetDatabaseRoomTabBO;
    import cloud.wconnect.rateshop.pojo.bo.budgetdatabase.event.BudgetDatabaseValueRoomBO;
    import cloud.wconnect.rateshop.pojo.bo.weeklymonthlyrevenue.WMRevenueBudgetBO;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.budget.*;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IMonthlyRevenueCommonService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IWeeklyMonthlyRevenueBudgetService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections4.CollectionUtils;
    import org.apache.commons.compress.utils.Lists;
    import org.springframework.stereotype.Service;

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

    /**
     * <p>
     * Weekly Monthly Revenue Budget 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-05-13
     */
    @Service
    @DS("rate_shop")
    public class WeeklyMonthlyRevenueBudgetServiceImpl extends ServiceImpl<WeeklyMonthlyRevenueBudgetMapper, WeeklyMonthlyRevenueBudget> implements IWeeklyMonthlyRevenueBudgetService {
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private RoomsConfigInternalService roomsConfigInternalService;
        @Resource
        private IMonthlyRevenueCommonService monthlyRevenueCommonService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;

        /**
         * 保存指定年月 Revenue Budget
         *
         * @param propertyId       资产 id
         * @param revenueBudgetDTO Revenue Budget DTO
         * @param operatorId       操作人 id
         * @author zc
         * @date 2024/5/15
         */
        @Override
        public void saveBudget(int propertyId, WeeklyMonthlyRevenueBudgetDTO revenueBudgetDTO, int operatorId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 获取原始数据
            WeeklyMonthlyRevenueBudget revenueBudget = getByDate(propertyId, revenueBudgetDTO.getTheMonth());
            if (Objects.isNull(revenueBudget)) {
                int rooms = roomsConfigInternalService.getRoomsConfig(propertyId).getRooms();
                int lengthOfMonth = YearMonthUtils.getYearMonth(String.valueOf(revenueBudgetDTO.getTheMonth())).lengthOfMonth();

                revenueBudget = new WeeklyMonthlyRevenueBudget();
                revenueBudget.setPropertyId(propertyId);
                revenueBudget.setTheMonth(revenueBudgetDTO.getTheMonth());
                revenueBudget.setPhysicalCapacity((long) rooms * lengthOfMonth);
            }

            WeeklyMonthlyRevenueBudgetOverallDTO overall = revenueBudgetDTO.getOverall();
            if (Objects.nonNull(overall)) {
                revenueBudget.setBfr(overall.getBfr());
                revenueBudget.setOoo(overall.getOoo());
                revenueBudget.setComplimentaryRooms(overall.getComplimentaryRooms());
                revenueBudget.setNoShowRooms(overall.getNoShowRooms());

                // 计算其他项
                revenueBudget.setSellableCapacity(monthlyRevenueCommonService.getSellableCapacity(revenueBudget.getPhysicalCapacity(),
                        revenueBudget.getOoo()));
                revenueBudget.setScOcc(monthlyRevenueCommonService.getOcc(revenueBudget.getRoomsSold(),
                        revenueBudget.getSellableCapacity(), roundingMode));
                revenueBudget.setRoomLeftToSell(monthlyRevenueCommonService.getRoomLeftToSell(revenueBudget.getPhysicalCapacity(),
                        revenueBudget.getRoomsSold(), revenueBudget.getOoo()));
            }

            WeeklyMonthlyRevenueBudgetGroupDTO group = revenueBudgetDTO.getGroup();
            if (Objects.nonNull(group)) {
                revenueBudget.setGroupContracted(group.getContracted());
                revenueBudget.setGroupCancelled(group.getCancelled());
            }
            revenueBudget.setUpdatedBy(operatorId);
            revenueBudget.setUpdatedTime(DateTimeUtils.nowTimestamp());

            baseMapper.insertOrUpdateByUnique(revenueBudget);
        }

        /**
         * 获取指定年月 Revenue Budget
         *
         * @param propertyId 资产 id
         * @param theMonth   指定年月（yyyyMM）
         * @return WeeklyMonthlyRevenueBudgetDTO
         * @author zc
         * @date 2024/5/15
         */
        @Override
        public WeeklyMonthlyRevenueBudgetDTO getBudget(int propertyId, long theMonth) {
            WeeklyMonthlyRevenueBudgetDTO budgetDTO = new WeeklyMonthlyRevenueBudgetDTO();
            budgetDTO.setTheMonth(theMonth);

            WeeklyMonthlyRevenueBudget revenueBudget = getByDate(propertyId, theMonth);
            if (Objects.isNull(revenueBudget)) {
                int rooms = roomsConfigInternalService.getRoomsConfig(propertyId).getRooms();
                WeeklyMonthlyRevenueBudgetOverallDTO overallDTO = new WeeklyMonthlyRevenueBudgetOverallDTO();
                int lengthOfMonth = YearMonthUtils.getYearMonth(String.valueOf(theMonth)).lengthOfMonth();
                overallDTO.setPhysicalCapacity((long) rooms * lengthOfMonth);

                budgetDTO.setOverall(overallDTO);

                budgetDTO.setTransients(new WeeklyMonthlyRevenueBudgetTransientDTO());
                budgetDTO.setGroup(new WeeklyMonthlyRevenueBudgetGroupDTO());
                budgetDTO.setContract(new WeeklyMonthlyRevenueBudgetContractDTO());

                return budgetDTO;
            }

            budgetDTO.setOverall(beanMapper.map(revenueBudget, WeeklyMonthlyRevenueBudgetOverallDTO.class));

            WeeklyMonthlyRevenueBudgetTransientDTO transientDTO = new WeeklyMonthlyRevenueBudgetTransientDTO();
            transientDTO.setSold(revenueBudget.getTransientSold());
            transientDTO.setRevenue(revenueBudget.getTransientRevenue());
            transientDTO.setAdr(revenueBudget.getTransientAdr());
            budgetDTO.setTransients(transientDTO);

            WeeklyMonthlyRevenueBudgetGroupDTO groupDTO = new WeeklyMonthlyRevenueBudgetGroupDTO();
            groupDTO.setContracted(revenueBudget.getGroupContracted());
            groupDTO.setCancelled(revenueBudget.getGroupCancelled());
            groupDTO.setSold(revenueBudget.getGroupSold());
            groupDTO.setRevenue(revenueBudget.getGroupRevenue());
            groupDTO.setAdr(revenueBudget.getGroupAdr());
            budgetDTO.setGroup(groupDTO);

            WeeklyMonthlyRevenueBudgetContractDTO contractDTO = new WeeklyMonthlyRevenueBudgetContractDTO();
            contractDTO.setSold(revenueBudget.getContractSold());
            contractDTO.setRevenue(revenueBudget.getContractRevenue());
            contractDTO.setAdr(revenueBudget.getContractAdr());
            budgetDTO.setContract(contractDTO);

            return budgetDTO;
        }

        /**
         * 获取指定年月 Revenue Budget
         *
         * @param propertyId 资产 id
         * @param theMonth   获取指定年月（yyyyMM）
         * @return WeeklyMonthlyRevenueBudget
         * @author zc
         * @date 2025/10/14
         */
        @Override
        public WeeklyMonthlyRevenueBudget getByDate(int propertyId, long theMonth) {
            return getOne(new LambdaQueryWrapper<WeeklyMonthlyRevenueBudget>()
                    .eq(WeeklyMonthlyRevenueBudget::getTheMonth, theMonth)
                    .eq(WeeklyMonthlyRevenueBudget::getPropertyId, propertyId));
        }

        /**
         * 获取指定时间段内的 Revenue Budget
         *
         * @param propertyId 资产 id
         * @param startMonth 开始时间（yyyyMM）
         * @param endMonth   结束时间（yyyyMM）
         * @return List<WeeklyMonthlyRevenueBudget>
         * @author zc
         * @date 2025/10/14
         */
        @Override
        public List<WeeklyMonthlyRevenueBudget> listByDateRange(int propertyId, long startMonth, long endMonth) {
            return list(new LambdaQueryWrapper<WeeklyMonthlyRevenueBudget>()
                    .between(WeeklyMonthlyRevenueBudget::getTheMonth, startMonth, endMonth)
                    .eq(WeeklyMonthlyRevenueBudget::getPropertyId, propertyId));
        }

        /**
         * 处理模块事件数据
         *
         * @param propertyDTO 资产信息
         * @param eventBO     Budget Database BO
         * @author zc
         * @date 2025/10/14
         */
        @Override
        public void handleData(PropertyDTO propertyDTO, BudgetDatabaseBO eventBO) {
            int year = eventBO.getYear();
            // 获取原始数据
            List<WeeklyMonthlyRevenueBudget> originBudgetList = listByDateRange(propertyDTO.getId(),
                    YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, 1)),
                    YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, 12)));
            Map<Long, WeeklyMonthlyRevenueBudget> month2Budget = originBudgetList.stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueBudget::getTheMonth, Function.identity()));

            // 预处理 eventBO 数据，按类型分组
            Map<Integer, List<BudgetDatabaseRoomBO>> typeToDataMap = eventBO.getRoom().stream()
                    .filter(BudgetDatabaseRoomTabBO::getOpen)  // 过滤开启的 tab
                    .flatMap(roomTabBO -> roomTabBO.getRoom().stream())  // 展平获取 room 列表
                    .collect(Collectors.groupingBy(BudgetDatabaseRoomBO::getType));

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

            List<WeeklyMonthlyRevenueBudget> updateList = Lists.newArrayList();
            for (int monthValue = 1; monthValue <= 12; monthValue++) {
                YearMonth yearMonth = YearMonth.of(year, monthValue);
                long month = YearMonthUtils.parseYearMonthToLong(yearMonth);

                WeeklyMonthlyRevenueBudget budget = month2Budget.get(month);
                if (Objects.isNull(budget)) {
                    int rooms = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId()).getRooms();
                    int lengthOfMonth = yearMonth.lengthOfMonth();
                    budget = new WeeklyMonthlyRevenueBudget();
                    budget.setPropertyId(propertyDTO.getId());
                    budget.setTheMonth(month);
                    budget.setPhysicalCapacity((long) rooms * lengthOfMonth);
                    budget.setUpdatedBy(0);
                    budget.setUpdatedTime(DateTimeUtils.nowTimestamp());
                }

                // Transient
                List<BudgetDatabaseRoomBO> transientBoList = typeToDataMap
                        .getOrDefault(RoomTypeEnum.TRANSIENT.getCode(), Collections.emptyList());
                Long transientRoomsSold = calculateRoomsSold(transientBoList, month);
                BigDecimal transientRoomRevenue = calculateRoomRevenue(transientBoList, month);
                budget.setTransientSold(transientRoomsSold);
                budget.setTransientRevenue(transientRoomRevenue);
                budget.setTransientAdr(monthlyRevenueCommonService.getAdr(transientRoomRevenue, transientRoomsSold, roundingMode));

                // Group
                List<BudgetDatabaseRoomBO> groupBoList = typeToDataMap
                        .getOrDefault(RoomTypeEnum.GROUP.getCode(), Collections.emptyList());
                Long groupRoomsSold = calculateRoomsSold(groupBoList, month);
                BigDecimal groupRoomRevenue = calculateRoomRevenue(groupBoList, month);
                budget.setGroupSold(groupRoomsSold);
                budget.setGroupRevenue(groupRoomRevenue);
                budget.setGroupAdr(monthlyRevenueCommonService.getAdr(groupRoomRevenue, groupRoomsSold, roundingMode));

                // Contract
                List<BudgetDatabaseRoomBO> contractBoList = typeToDataMap
                        .getOrDefault(RoomTypeEnum.CONTRACT.getCode(), Collections.emptyList());
                Long contractRoomsSold = calculateRoomsSold(contractBoList, month);
                BigDecimal contractRoomRevenue = calculateRoomRevenue(contractBoList, month);
                budget.setContractSold(contractRoomsSold);
                budget.setContractRevenue(contractRoomRevenue);
                budget.setContractAdr(monthlyRevenueCommonService.getAdr(contractRoomRevenue, contractRoomsSold, roundingMode));

                // Overall
                Long overallRoomsSold = Stream.of(transientRoomsSold, groupRoomsSold, contractRoomsSold)
                        .filter(Objects::nonNull).reduce(Long::sum).orElse(null);
                BigDecimal overallRoomRevenue = Stream.of(transientRoomRevenue, groupRoomRevenue, contractRoomRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                budget.setRoomsSold(overallRoomsSold);
                budget.setRoomRevenue(overallRoomRevenue);
                budget.setAdr(monthlyRevenueCommonService.getAdr(overallRoomRevenue, overallRoomsSold, roundingMode));
                budget.setRevPar(monthlyRevenueCommonService.getRevPar(overallRoomRevenue, budget.getPhysicalCapacity(), roundingMode));
                budget.setPcOcc(monthlyRevenueCommonService.getOcc(overallRoomsSold, budget.getPhysicalCapacity(), roundingMode));
                Long sellableCapacity = monthlyRevenueCommonService.getSellableCapacity(budget.getPhysicalCapacity(), budget.getOoo());
                budget.setSellableCapacity(sellableCapacity);
                budget.setScOcc(monthlyRevenueCommonService.getOcc(overallRoomsSold, sellableCapacity, roundingMode));
                budget.setRoomLeftToSell(monthlyRevenueCommonService.getRoomLeftToSell(budget.getPhysicalCapacity(), overallRoomsSold, budget.getOoo()));

                updateList.add(budget);
            }

            // 批量保存数据
            insertOrUpdateBatchByUnique(updateList);
        }

        /**
         * 处理 Budget
         *
         * @param budgetBOList 报告数据
         * @param year         年（yyyy）
         * @param propertyDTO  资产
         * @param operatorId   操作人 id
         * @author zc
         * @date 2024/8/22
         */
        @Override
        public void handleBudgetReport(List<WMRevenueBudgetBO> budgetBOList, long year, PropertyDTO propertyDTO, int operatorId) {
            if (CollectionUtils.isEmpty(budgetBOList)) {
                return;
            }

            // 获取原始数据
            Map<Long, WeeklyMonthlyRevenueBudget> theMonth2Budget = listByDateRange(propertyDTO.getId(),
                    YearMonthUtils.parseYearMonthToLong(YearMonth.of((int) year, 1)),
                    YearMonthUtils.parseYearMonthToLong(YearMonth.of((int) year, 12)))
                    .stream().collect(Collectors.toMap(WeeklyMonthlyRevenueBudget::getTheMonth, Function.identity()));

            int rooms = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId()).getRooms();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<WeeklyMonthlyRevenueBudget> budgetList = budgetBOList.stream().map(bo -> {
                WeeklyMonthlyRevenueBudget originBudget = theMonth2Budget.get(bo.getTheMonth());
                if (Objects.isNull(originBudget)) {
                    originBudget = new WeeklyMonthlyRevenueBudget();
                    originBudget.setPropertyId(propertyDTO.getId());
                    originBudget.setTheMonth(bo.getTheMonth());
                    int lengthOfMonth = YearMonthUtils.getYearMonth(String.valueOf(bo.getTheMonth())).lengthOfMonth();
                    originBudget.setPhysicalCapacity((long) rooms * lengthOfMonth);
                }
                originBudget.setBfr(bo.getBfr());
                originBudget.setOoo(bo.getOoo());
                originBudget.setComplimentaryRooms(bo.getComplimentaryRooms());
                originBudget.setNoShowRooms(bo.getNoShowRooms());
                originBudget.setGroupContracted(bo.getGroupContracted());
                originBudget.setGroupCancelled(bo.getGroupCancelled());
                originBudget.setSellableCapacity(monthlyRevenueCommonService.getSellableCapacity(originBudget.getPhysicalCapacity(), bo.getOoo()));
                originBudget.setScOcc(monthlyRevenueCommonService.getOcc(originBudget.getRoomsSold(), originBudget.getSellableCapacity(), roundingMode));
                originBudget.setRoomLeftToSell(monthlyRevenueCommonService.getRoomLeftToSell(originBudget.getPhysicalCapacity(),
                        originBudget.getRoomsSold(), originBudget.getOoo()));
                originBudget.setUpdatedBy(operatorId);
                originBudget.setUpdatedTime(nowTimestamp);

                return originBudget;
            }).collect(Collectors.toList());
            // 批量保存数据
            insertOrUpdateBatchByUnique(budgetList);
        }

        /**
         * 批量保存数据
         *
         * @param list 数据
         * @author zc
         * @date 2026/1/27
         */
        private void insertOrUpdateBatchByUnique(List<WeeklyMonthlyRevenueBudget> list) {
            if (CollectionUtils.isEmpty(list)) {
                return;
            }
            // 按唯一索引字段排序，避免死锁
            list.sort(Comparator.comparing(WeeklyMonthlyRevenueBudget::getPropertyId)
                    .thenComparing(WeeklyMonthlyRevenueBudget::getTheMonth));

            baseMapper.insertOrUpdateBatchByUnique(list);
        }

        /**
         * 获取指定月份的售出房间数量
         *
         * @param items 数据项
         * @param month 月份
         * @return Integer
         * @author zc
         * @date 2025/10/14
         */
        private Long calculateRoomsSold(List<BudgetDatabaseRoomBO> items, long month) {
            return items.stream().map(item ->
                            item.getData().stream().filter(data ->
                                            data.getYearMonth() == month).map(BudgetDatabaseValueRoomBO::getRoomsBudget)
                                    .filter(Objects::nonNull).reduce(Long::sum).orElse(null))
                    .filter(Objects::nonNull).reduce(Long::sum).orElse(null);
        }

        /**
         * 获取指定月份的房间收入
         *
         * @param items 数据项
         * @param month 月份
         * @return BigDecimal
         * @author zc
         * @date 2025/10/14
         */
        private BigDecimal calculateRoomRevenue(List<BudgetDatabaseRoomBO> items, long month) {
            return items.stream().map(item ->
                            item.getData().stream().filter(data ->
                                            data.getYearMonth() == month).map(BudgetDatabaseValueRoomBO::getRevenueBudget)
                                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null))
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
        }

    }