# M3ForecastsServiceImpl.java

    package cloud.wconnect.finance.service.pl.impl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.PropertyM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.TenantM3ConfigDTO;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.finance.cons.pl.M3DataTypeEnum;
    import cloud.wconnect.finance.cons.pl.IndexTypeEnum;
    import cloud.wconnect.finance.cons.pl.RowCategoryEnum;
    import cloud.wconnect.finance.entity.pl.M3Forecast;
    import cloud.wconnect.finance.entity.pl.M3ForecastName;
    import cloud.wconnect.finance.entity.pl.M3Row;
    import cloud.wconnect.finance.entity.pl.M3Variable;
    import cloud.wconnect.finance.mapper.pl.M3ForecastsMapper;
    import cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO;
    import cloud.wconnect.finance.pojo.dto.pl.index.RowIndexCalculationDTO;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.AccountForecastDTO;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.ForecastDTO;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.PeriodForecastDTO;
    import cloud.wconnect.finance.service.pl.*;
    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.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

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

    /**
     * <p>
     * M3系统forecasts数据 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-09-25
     */
    @Service
    @DS("finance")
    public class M3ForecastsServiceImpl extends ServiceImpl<M3ForecastsMapper, M3Forecast> implements IM3ForecastsService {

        @Autowired
        private M3ApiService m3ApiService;
        @Autowired
        private PlCommonService plCommonService;
        @Autowired
        private IM3ForecastsNameService m3ForecastsNameService;
        @Autowired
        private IM3VariableService m3VariableService;
        /**
         * forecast类型
         */
        public static final String DATA_TYPE = M3DataTypeEnum.FORECAST.getDataType();

        /**
         * 处理forecasts数据
         *
         * @param tenantM3ConfigDTO   租户M3配置
         * @param propertyM3ConfigDTO 资产M3配置
         * @param yearMonth           年月,yyyyMM
         * @param forcedRefresh       是否强制刷新m3数据
         * @author lichen
         * @date 2025/10/20
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleForecasts(TenantM3ConfigDTO tenantM3ConfigDTO, PropertyM3ConfigDTO propertyM3ConfigDTO, Integer yearMonth, Boolean forcedRefresh) {
            Integer propertyId = propertyM3ConfigDTO.getPropertyId();
            boolean m3DataHandleStatus = plCommonService.getM3DataHandleStatus(DATA_TYPE, yearMonth.toString(), propertyId);
            if (forcedRefresh || !m3DataHandleStatus) {
                //获取年和月
                YearMonth yearMonthObj = YearMonthUtils.getYearMonth(yearMonth.toString());
                int year = yearMonthObj.getYear();
                String monthValue = String.valueOf(yearMonthObj.getMonthValue());

                //查询m3 forecast name数据
                List<M3ForecastName> m3ForecastNames = m3ForecastsNameService.listByYear(propertyId, year);
                if (CollectionUtils.isEmpty(m3ForecastNames)) {
                    throw new RuntimeException("m3 forecast name is empty");
                }

                //删除旧数据
                remove(new LambdaQueryWrapper<M3Forecast>().eq(M3Forecast::getPropertyId, propertyId)
                        .eq(M3Forecast::getYearMonth, yearMonth));

                ArrayList<M3Forecast> m3ForecastList = Lists.newArrayList();

                for (M3ForecastName m3ForecastName : m3ForecastNames) {
                    //查询m3 forecast数据
                    ForecastDTO forecastDTO = m3ApiService.listForecasts(tenantM3ConfigDTO, propertyM3ConfigDTO, String.valueOf(year), m3ForecastName.getName());
                    for (AccountForecastDTO accountEntry : forecastDTO.getAccountEntries()) {
                        if (StringUtils.isNotBlank(accountEntry.getGlAccountNumber())) {
                            //获取指定月份的预测值
                            BigDecimal ptdForecast = accountEntry.getPeriodEntries().stream()
                                    .filter(periodEntry -> periodEntry.getPeriod().equals(monthValue))
                                    .map(PeriodForecastDTO::getAmount).findFirst().orElse(BigDecimal.ZERO);
                            //获取ytd预测值
                            BigDecimal ytdForecast = accountEntry.getPeriodEntries().stream()
                                    .filter(item -> {
                                        int period = Integer.parseInt(item.getPeriod());
                                        return period <= Integer.parseInt(monthValue);
                                    })
                                    .map(PeriodForecastDTO::getAmount).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                            M3Forecast m3Forecast = new M3Forecast();
                            m3Forecast.setTenantId(tenantM3ConfigDTO.getTenantId());
                            m3Forecast.setPropertyId(propertyId);
                            m3Forecast.setYearMonth(yearMonth);
                            m3Forecast.setM3ForecastId(m3ForecastName.getM3ForecastId());
                            m3Forecast.setAccountNumber(accountEntry.getGlAccountNumber());
                            m3Forecast.setPtdForecast(ptdForecast.setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                            m3Forecast.setYtdForecast(ytdForecast.setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                            m3ForecastList.add(m3Forecast);
                        }
                    }
                }

                //保存数据
                if (CollectionUtils.isNotEmpty(m3ForecastList)) {
                    // 分批处理，每300条记录一批
                    int batchSize = 300;
                    for (int i = 0; i < m3ForecastList.size(); i += batchSize) {
                        int endIndex = Math.min(i + batchSize, m3ForecastList.size());
                        List<M3Forecast> batch = m3ForecastList.subList(i, endIndex);
                        baseMapper.insertBatch(batch);
                    }
                }

                //保存处理结果
                plCommonService.setM3DataAlreadyHandled(DATA_TYPE, yearMonth.toString(), propertyM3ConfigDTO.getPropertyId());
            }
        }

        /**
         * 根据m3数据计算指标
         *
         * @param propertyId                        资产id
         * @param m3Row                             m3 Row
         * @param yearMonth                         计算的年月，yyyyMM
         * @param alreadyCalculationRowIndexDTOList 已经计算过的Row指标集合
         * @param m3DataMap                         m3数据集合
         * @param rooms                             rooms房间数
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO>
         * @author lichen
         * @date 2025/9/26
         */
        @Override
        public List<IndexCalculationDTO> calculationIndex(Integer propertyId, M3Row m3Row, Integer yearMonth,
                                                          ArrayList<RowIndexCalculationDTO> alreadyCalculationRowIndexDTOList,
                                                          Map<String, Object> m3DataMap, Integer rooms) {
            //获取m3 forecast name数据
            List<M3ForecastName> m3ForecastNames = (List<M3ForecastName>) m3DataMap.getOrDefault(M3DataTypeEnum.FORECAST_NAME.getDataType(), new ArrayList<M3ForecastName>());
            if (CollectionUtils.isEmpty(m3ForecastNames)) {
                //forecast name数据为空时，不用计算指标
                return Collections.emptyList();
            }
            List<String> m3ForecastIds = m3ForecastNames.stream().map(M3ForecastName::getM3ForecastId).toList();

            String accountNumber = m3Row.getAccountNumber();

            List<IndexCalculationDTO> indexCalculationDTOS = Lists.newArrayListWithCapacity(2 * m3ForecastNames.size());
            ArrayList<String> needIndexList = Lists.newArrayList(IndexTypeEnum.PTD_FORECAST.getIndexType(), IndexTypeEnum.YTD_FORECAST.getIndexType());

            //获取m3 forecast数据
            List<M3Forecast> m3Forecasts = (List<M3Forecast>) m3DataMap.getOrDefault(M3DataTypeEnum.FORECAST.getDataType(), new ArrayList<M3Forecast>());
            Map<String, List<M3Forecast>> m3ForecastId2List = m3Forecasts.stream().collect(Collectors.groupingBy(M3Forecast::getM3ForecastId));
            m3ForecastId2List.forEach((m3ForecastId, list) -> {
                if (m3Row.getRowCategory().equals(RowCategoryEnum.ITEM.getValue())) {//非公式计算
                    //获取row对应的Forecasts数据
                    Map<String, M3Forecast> accountNumber2M3Forecasts = list.stream().collect(Collectors.toMap(M3Forecast::getAccountNumber, Function.identity()));
                    //获取row对应的Forecasts数据
                    M3Forecast m3ForecastByAccountNumber = accountNumber2M3Forecasts.getOrDefault(accountNumber, null);
                    if (Objects.nonNull(m3ForecastByAccountNumber)) {
                        //PTD Forecast
                        IndexCalculationDTO ptdForecastIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.PTD_FORECAST.getIndexType(),
                                m3ForecastByAccountNumber.getPtdForecast(), true, Collections.emptyList(), m3ForecastId);
                        indexCalculationDTOS.add(ptdForecastIndexCalculationDTO);
                        //YTD Forecast
                        IndexCalculationDTO ytdForecastIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.YTD_FORECAST.getIndexType(),
                                m3ForecastByAccountNumber.getYtdForecast(), true, Collections.emptyList(), m3ForecastId);
                        indexCalculationDTOS.add(ytdForecastIndexCalculationDTO);
                    }
                } else {//公式计算
                    //特殊变量集合
                    List<M3Variable> specialVariables = (List<M3Variable>) m3DataMap.getOrDefault(M3DataTypeEnum.SPECIAL_VARIABLES.getDataType(), new ArrayList<M3Variable>());
                    for (String indexType : needIndexList) {
                        //替换公式中的特殊变量
                        String expandedFormula = m3VariableService.replaceSpecialVariable(m3Row.getFormula(), propertyId, indexType, yearMonth, rooms, specialVariables);

                        IndexCalculationDTO indexCalculationDTO = plCommonService.calculationIndex(m3Row.getRowNumber(), expandedFormula, indexType, alreadyCalculationRowIndexDTOList, m3ForecastId);
                        indexCalculationDTOS.add(indexCalculationDTO);
                    }
                }
            });

            for (String m3ForecastId : m3ForecastIds) {
                //没有计算的指标默认按0处理
                List<String> alreadyCalculationIndexList = indexCalculationDTOS.stream().filter(item -> item.getIndexNameId().equals(m3ForecastId))
                        .map(IndexCalculationDTO::getIndexType).toList();
                List<IndexCalculationDTO> defaultIndexCalculationDTOS = needIndexList.stream().filter(indexType -> !alreadyCalculationIndexList.contains(indexType))
                        .map(indexName -> new IndexCalculationDTO(indexName, new BigDecimal("0"), true, Collections.emptyList(), m3ForecastId))
                        .toList();
                if (CollectionUtils.isNotEmpty(defaultIndexCalculationDTOS)) {
                    indexCalculationDTOS.addAll(defaultIndexCalculationDTOS);
                }
            }
            return indexCalculationDTOS;
        }

        /**
         * 根据m3数据获取指定的forecasts数据
         *
         * @param propertyId    资产id
         * @param yearMonth     年月，yyyyMM
         * @param m3ForecastIds m3 forecasts id集合
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3Forecasts>
         * @author lichen
         * @date 2025/9/27
         */
        @Override
        public List<M3Forecast> listByYearMonthAndForecastId(Integer propertyId, Integer yearMonth, List<String> m3ForecastIds) {
            if (CollectionUtils.isEmpty(m3ForecastIds)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<M3Forecast>().eq(M3Forecast::getPropertyId, propertyId)
                    .eq(M3Forecast::getYearMonth, yearMonth)
                    .in(M3Forecast::getM3ForecastId, m3ForecastIds));
        }
    }