# BudgetDatabaseFbCoversServiceImpl.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.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.NumberUtil;
    import cloud.wconnect.rateshop.cons.budgetdatabase.*;
    import cloud.wconnect.rateshop.entity.budgetdatabase.*;
    import cloud.wconnect.rateshop.mapper.budgetdatabase.BudgetDatabaseFbCoversMapper;
    import cloud.wconnect.rateshop.pojo.bo.budgetdatabase.OneYearRoomsBO;
    import cloud.wconnect.rateshop.pojo.dto.budgetdatabase.FbCoversDTO;
    import cloud.wconnect.rateshop.pojo.dto.budgetdatabase.FbRevenueDetailDTO;
    import cloud.wconnect.rateshop.pojo.dto.budgetdatabase.UpdateOneYearRoomsDTO;
    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 jakarta.validation.Valid;
    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.lang.reflect.Field;
    import java.math.BigDecimal;
    import java.time.Month;
    import java.time.ZonedDateTime;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author yyt
     * @since 2025-11-10
     */
    @Service
    @DS("rate_shop")
    public class BudgetDatabaseFbCoversServiceImpl extends ServiceImpl<BudgetDatabaseFbCoversMapper, BudgetDatabaseFbCovers> implements IBudgetDatabaseFbCoversService {

        @Lazy
        @Autowired
        private IBudgetDatabaseService budgetDatabaseService;
        @Lazy
        @Autowired
        private IBudgetDatabaseConfigFbTabItemService budgetDatabaseConfigFbTabItemService;
        @Autowired
        private IBudgetDatabaseCalculateService budgetDatabaseCalculateService;
        @Autowired
        private IBudgetDatabaseDataLogFbService budgetDatabaseDataLogFbService;
        @Autowired
        private IBudgetDatabaseStatusLogService budgetDatabaseStatusLogService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private BeanMapper beanMapper;

        /**
         * 查询指标数据
         *
         * @param itemId 指标 id
         * @return 指标数据
         * @author yyt
         * @date 2025/11/11
         **/
        @Override
        public BudgetDatabaseFbCovers getByItemId(Integer itemId) {
            return getOne(new QueryWrapper<BudgetDatabaseFbCovers>().lambda().eq(BudgetDatabaseFbCovers::getFbId, itemId));
        }

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

        /**
         * 获取指标数据
         *
         * @param tab        指标 tab
         * @return 指标数据
         * @author yyt
         * @date 2025/11/11
         **/
        @Override
        public FbCoversDTO listFbCoversDTO(BudgetDatabaseConfigFbTab tab) {
            int tabId = tab.getId();
            Integer propertyId = tab.getPropertyId();
            // 查询配置指标
            List<BudgetDatabaseConfigFbTabItem> items = budgetDatabaseConfigFbTabItemService.listByTabIdAndClassification(tabId, FbClassificationEnum.COVERS.getCode());
            if (CollectionUtils.isEmpty(items)) return new FbCoversDTO();

            // 查询 revenue
            Map<Integer, BudgetDatabaseFbCovers> revenueByItemId = listByItemIds(items.stream().map(BudgetDatabaseConfigFbTabItem::getId).toList())
                    .stream().collect(Collectors.toMap(BudgetDatabaseFbCovers::getFbId, v -> v));

            // 所有指标
            List<FbRevenueDetailDTO> fbRevenueDetailDTOS = Lists.newArrayListWithExpectedSize(items.size() + 2);
            // 汇总指标最下面的 total 时使用
            List<OneYearRoomsBO> totalBOs = Lists.newArrayListWithExpectedSize(2);

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

                // 汇总当前分类指标使用
                List<OneYearRoomsBO> oneYearRoomsBOS = Lists.newArrayListWithExpectedSize(currentTypeFbs.size());
                for (BudgetDatabaseConfigFbTabItem fb : currentTypeFbs) {
                    OneYearRoomsBO oneYearRoomsBO = beanMapper.map(revenueByItemId.getOrDefault(fb.getId(), new BudgetDatabaseFbCovers()), OneYearRoomsBO.class);
                    oneYearRoomsBOS.add(oneYearRoomsBO);

                    FbRevenueDetailDTO fbRevenueDetailDTO = beanMapper.map(budgetDatabaseCalculateService.calculateRoomsTotal(propertyId, Lists.newArrayList(oneYearRoomsBO)), FbRevenueDetailDTO.class);
                    fbRevenueDetailDTO.setId(fb.getId());
                    fbRevenueDetailDTO.setName(fb.getName());
                    fbRevenueDetailDTO.setType(fb.getType());
                    fbRevenueDetailDTO.setRevenueParticipateCalculate(fb.getEnableBudgetRevenue());
                    fbRevenueDetailDTO.setRoomParticipateCalculate(fb.getEnableBudgetRoom());
                    fbRevenueDetailDTOS.add(fbRevenueDetailDTO);
                }
                // 分类汇总
                FbRevenueDetailDTO fbDetailForTypeTotal = beanMapper.map(budgetDatabaseCalculateService.calculateRoomsTotal(propertyId, oneYearRoomsBOS), FbRevenueDetailDTO.class);
                fbDetailForTypeTotal.setId(FbTypeEnum.getRowId(type));
                fbDetailForTypeTotal.setName(FbTypeEnum.getDesc(type));
                fbDetailForTypeTotal.setType(type);
                fbRevenueDetailDTOS.add(fbDetailForTypeTotal);

                totalBOs.addAll(oneYearRoomsBOS);
            });

            // total
            FbRevenueDetailDTO fbDetailForTotal = beanMapper.map(budgetDatabaseCalculateService.calculateRoomsTotal(propertyId, totalBOs), FbRevenueDetailDTO.class);
            fbDetailForTotal.setId(BaseConstant.ZERO);
            fbDetailForTotal.setName(BudgetDatabaseTitleCons.TOTAL);
            fbDetailForTotal.setType(null);
            fbRevenueDetailDTOS.add(fbDetailForTotal);

            FbCoversDTO fbdto = new FbCoversDTO();
            // 最后修改人
            fbdto.setUpdatedInfo(budgetDatabaseDataLogFbService.getLastLogDTO(tabId, FbLogAreaCons.COVERS));
            // fb revenue
            fbdto.setItems(fbRevenueDetailDTOS);
            return fbdto;
        }

        /**
         * 检查是否可以更新预算
         *
         * @param tab              tab
         * @param updateRevenueDTOS 更新预算数据
         * @author yyt
         * @date 2025/8/14
         **/
        @Override
        public void checkCanUpdateBudgetForStatus(BudgetDatabaseConfigFbTab tab, List<UpdateOneYearRoomsDTO> updateRevenueDTOS) {
            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            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 数据
            Map<Integer, BudgetDatabaseFbCovers> existRevenueByFbId = listByItemIds(updateRevenueDTOS.stream()
                    .map(UpdateOneYearRoomsDTO::getItemId).distinct().collect(Collectors.toList()))
                    .stream().collect(Collectors.toMap(BudgetDatabaseFbCovers::getFbId, v -> v));
            for (UpdateOneYearRoomsDTO updateOneYearRevenueDTO : updateRevenueDTOS) {
                BudgetDatabaseFbCovers existRevenue = existRevenueByFbId.getOrDefault(updateOneYearRevenueDTO.getItemId(), new BudgetDatabaseFbCovers());
                if (!NumberUtil.equals(existRevenue.getJanuary(), updateOneYearRevenueDTO.getJanuary())
                        || !NumberUtil.equals(existRevenue.getFebruary(), updateOneYearRevenueDTO.getFebruary())
                        || !NumberUtil.equals(existRevenue.getMarch(), updateOneYearRevenueDTO.getMarch())
                        || !NumberUtil.equals(existRevenue.getApril(), updateOneYearRevenueDTO.getApril())
                        || !NumberUtil.equals(existRevenue.getMay(), updateOneYearRevenueDTO.getMay())
                        || !NumberUtil.equals(existRevenue.getJune(), updateOneYearRevenueDTO.getJune())
                        || !NumberUtil.equals(existRevenue.getJuly(), updateOneYearRevenueDTO.getJuly())
                        || !NumberUtil.equals(existRevenue.getAugust(), updateOneYearRevenueDTO.getAugust())
                        || !NumberUtil.equals(existRevenue.getSeptember(), updateOneYearRevenueDTO.getSeptember())
                        || !NumberUtil.equals(existRevenue.getOctober(), updateOneYearRevenueDTO.getOctober())
                        || !NumberUtil.equals(existRevenue.getNovember(), updateOneYearRevenueDTO.getNovember())
                        || !NumberUtil.equals(existRevenue.getDecember(), updateOneYearRevenueDTO.getDecember())) {
                    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 updateRevenueDTOS 预算数据
         * @author yyt
         * @date 2025/8/14
         **/
        @Override
        public void checkCanUpdateForReopen(BudgetDatabaseConfigFbTab tab, List<@Valid UpdateOneYearRoomsDTO> updateRevenueDTOS) {
            Integer propertyId = tab.getPropertyId();
            Integer year = tab.getBudgetYear();
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            ZonedDateTime currentDate = DateTimeUtils.dateNow(timezone);
            if (year > currentDate.getYear()) return;

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            boolean hasReopen = budgetDatabaseStatusLogService.checkHasReopen(propertyId, year);
            if (!hasReopen) return;

            // 历史年
            if (year < currentDate.getYear()) {
                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");
            }

            // 1月时，不限制，可以修改任一月份的值
            Month currentMonth = currentDate.getMonth();
            if (currentMonth == Month.JANUARY) return;

            Map<Integer, BudgetDatabaseFbCovers> existRevenueByFbId = listByItemIds(updateRevenueDTOS.stream()
                    .map(UpdateOneYearRoomsDTO::getItemId).distinct().collect(Collectors.toList()))
                    .stream().collect(Collectors.toMap(BudgetDatabaseFbCovers::getFbId, v -> v));

            for (UpdateOneYearRoomsDTO updateOneYearRevenueDTO : updateRevenueDTOS) {
                BudgetDatabaseFbCovers existRevenue = existRevenueByFbId.getOrDefault(updateOneYearRevenueDTO.getItemId(), new BudgetDatabaseFbCovers());
                // 限制不能修改历史月份数据，比如：当前月是2月，则不能修改1月（january）的数据
                validateHistoricalMonthUpdates(currentMonth, existRevenue, updateOneYearRevenueDTO);
            }
        }

        /**
         * 检查历史月份数据不能修改
         *
         * @param currentMonth 当前月
         * @param existRevenue 已存在 revenue 数据
         * @param updateDTO    修改 revenue 数据
         * @author yyt
         * @date 2025/8/15
         **/
        private void validateHistoricalMonthUpdates(Month currentMonth, BudgetDatabaseFbCovers existRevenue, UpdateOneYearRoomsDTO updateDTO) {
            // 获取当前月份的数字表示(1-12)
            int currentMonthValue = currentMonth.getValue();

            // 遍历所有月份字段
            for (int month = 1; month < currentMonthValue; month++) {
                Month monthEnum = Month.of(month);
                String fieldName = monthEnum.name().toLowerCase();

                try {
                    // 使用反射获取字段值进行比较
                    Field existField = BudgetDatabaseFbCovers.class.getDeclaredField(fieldName);
                    Field updateField = UpdateOneYearRoomsDTO.class.getDeclaredField(fieldName);

                    existField.setAccessible(true);
                    updateField.setAccessible(true);

                    BigDecimal existValue = (BigDecimal) existField.get(existRevenue);
                    BigDecimal updateValue = (BigDecimal) updateField.get(updateDTO);

                    if (!NumberUtil.equals(existValue, updateValue)) {
                        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");
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    throw new InternalServerErrorException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                            BaseErrorCodeEnum.SYSTEM_ERROR.getCode(), "Failed to verify the monthly data", e);
                }
            }
        }

        /**
         * 检查是否可以修改预算
         *
         * @param tab               tab
         * @param updateRevenueDTOS 预算信息
         * @author yyt
         * @date 2025/11/14
         **/
        @Override
        public void checkCanUpdateForParticipateCalculate(BudgetDatabaseConfigFbTab tab, List<UpdateOneYearRoomsDTO> updateRevenueDTOS) {
            // 检查若指标是参与计算的，则不能手动修改预算数据，但是能改实际数据
            List<Integer> itemIds = updateRevenueDTOS.stream().map(UpdateOneYearRoomsDTO::getItemId).toList();
            List<Integer> enableBudgetRevenueOrRoomItemIds = budgetDatabaseConfigFbTabItemService.listByIds(itemIds)
                    .stream().filter(item -> item.getEnableBudgetRevenue() || item.getEnableBudgetRoom())
                    .map(BudgetDatabaseConfigFbTabItem::getId).toList();
            Map<Integer, BudgetDatabaseFbCovers> existRoomOtherById = listByItemIds(itemIds).stream().collect(Collectors.toMap(BudgetDatabaseFbCovers::getFbId, v -> v));
            for (UpdateOneYearRoomsDTO updateRevenueDTO : updateRevenueDTOS) {
                BudgetDatabaseFbCovers existData = existRoomOtherById.getOrDefault(updateRevenueDTO.getItemId(), new BudgetDatabaseFbCovers());
                if (enableBudgetRevenueOrRoomItemIds.contains(updateRevenueDTO.getItemId())) {
                    if (!NumberUtil.equals(updateRevenueDTO.getJanuary(), existData.getJanuary()) || !NumberUtil.equals(updateRevenueDTO.getFebruary(), existData.getFebruary())
                            || !NumberUtil.equals(updateRevenueDTO.getMarch(), existData.getMarch()) || !NumberUtil.equals(updateRevenueDTO.getApril(), existData.getApril())
                            || !NumberUtil.equals(updateRevenueDTO.getMay(), existData.getMay()) || !NumberUtil.equals(updateRevenueDTO.getJune(), existData.getJune())
                            || !NumberUtil.equals(updateRevenueDTO.getJuly(), existData.getJuly()) || !NumberUtil.equals(updateRevenueDTO.getAugust(), existData.getAugust())
                            || !NumberUtil.equals(updateRevenueDTO.getSeptember(), existData.getSeptember()) || !NumberUtil.equals(updateRevenueDTO.getOctober(), existData.getOctober())
                            || !NumberUtil.equals(updateRevenueDTO.getNovember(), existData.getNovember()) || !NumberUtil.equals(updateRevenueDTO.getDecember(), existData.getDecember())) {
                        throw new BadRequestException(BudgetDatabaseErrorCons.CANNOT_EDIT_BUDGET_DATA_FOR_CALCULATED_INDICATORS,
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the budget data is not allowed to be modified");
                    }
                }
            }
        }

        /**
         * 更新 fb
         *
         * @param tab              tab
         * @param updateRevenueDTOS 预算数据
         * @param userId           用户 id
         * @author yyt
         * @date 2025/8/14
         **/
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateFbCovers(BudgetDatabaseConfigFbTab tab, List<UpdateOneYearRoomsDTO> updateRevenueDTOS, int userId) {
            Integer tabId = tab.getId();
            int propertyId = tab.getPropertyId();
            Integer year = tab.getBudgetYear();
            // 处理 budget database 状态数据
            budgetDatabaseService.handleSaveBudgetDatabaseStatus(propertyId, year);

            baseMapper.saveOrUpdateBatch(updateRevenueDTOS.stream().map(item -> {
                BudgetDatabaseFbCovers fb = beanMapper.map(item, BudgetDatabaseFbCovers.class);
                fb.setPropertyId(propertyId).setTabId(tabId).setFbId(item.getItemId());
                return fb;
            }).sorted(Comparator.comparing(BudgetDatabaseFbCovers::getFbId)).toList());

            // 记录操作日志
            budgetDatabaseDataLogFbService.saveLog(propertyId, tabId, userId, FbLogAreaCons.COVERS);
        }

        /**
         * 获取指标数据
         *
         * @param tabIds tab id
         * @return 指标数据
         * @author yyt
         * @date 2025/11/11
         **/
        @Override
        public List<BudgetDatabaseFbCovers> listByTabIds(List<Integer> tabIds) {
            return list(new QueryWrapper<BudgetDatabaseFbCovers>().lambda().in(BudgetDatabaseFbCovers::getTabId, tabIds));
        }

        /**
         * 批量更新
         *
         * @param updates 更新数据
         * @author yyt
         * @date 2025/11/11
         **/
        @Override
        public void updateBatchByIdRewrite(List<BudgetDatabaseFbCovers> updates) {
            if (CollectionUtils.isEmpty(updates)) return;

            baseMapper.updateBatchByIdRewrite(updates);
        }

    }