# M3DataService.java

    package cloud.wconnect.finance.service.pl;

    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.PropertyM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.TenantM3ConfigDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.util.RateLimitUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.finance.cons.pl.M3DataTypeEnum;
    import cloud.wconnect.finance.entity.pl.*;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.CompanyCloseAccountDTO;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.CustomerCloseAccountDTO;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.FiscalPeriodCloseAccountDTO;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.text.MessageFormat;
    import java.time.YearMonth;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * 处理M3基础数据
     *
     * @author lichen
     * @date 2025/9/26
     */
    @Service
    @DS("finance")
    @Slf4j
    public class M3DataService {

        @Autowired
        private IM3TrialbalanceService m3TrialbalanceService;
        @Autowired
        private IM3BudgetsService m3BudgetService;
        @Autowired
        private IM3ForecastsNameService m3ForecastsNameService;
        @Autowired
        private IM3ForecastsService m3ForecastsService;
        @Autowired
        private IM3AccountService m3AccountService;
        @Autowired
        private IM3PlReportLogService m3PlReportLogService;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private IM3VariableService m3VariableService;

        /**
         * 处理M3系统基础数据
         *
         * @param tenantM3ConfigDTO   租户M3配置
         * @param propertyM3ConfigDTO 资产M3配置
         * @param yearMonth           年月,yyyyMM
         * @param userId              用户id
         * @param logId               日志id
         * @return void
         * @author lichen
         * @date 2025/9/26
         */
        public void handleM3BasicData(TenantM3ConfigDTO tenantM3ConfigDTO, PropertyM3ConfigDTO propertyM3ConfigDTO, Integer yearMonth, int userId, Integer logId) {
            boolean forcedRefresh = userId > 0;
            Integer propertyId = propertyM3ConfigDTO.getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            int errorCount = 0;
            try {
                //1.处理trialbalance数据
                m3TrialbalanceService.handleTrialbalance(tenantM3ConfigDTO, propertyM3ConfigDTO, yearMonth, forcedRefresh);
            } catch (Exception e) {
                String errorMsg = MessageFormat.format("handle trialbalance data failed, propertyId:{0},yearMonth:{1}", propertyId, yearMonth);
                log.error(errorMsg, e);
                sendAlertEmail(propertyDTO, M3DataTypeEnum.TRIALBALANCE.getDataType(), e);
                errorCount++;
            }

            try {
                //2.处理上一年的trialbalance数据
                YearMonth lastYearMonthObj = YearMonthUtils.getYearMonth(yearMonth.toString()).minusYears(1);
                Integer lastYearMonth = Integer.valueOf(YearMonthUtils.DATE_TIME_FORMATTER.format(lastYearMonthObj));
                m3TrialbalanceService.handleTrialbalance(tenantM3ConfigDTO, propertyM3ConfigDTO, lastYearMonth, forcedRefresh);
            } catch (Exception e) {
                String errorMsg = MessageFormat.format("handle last year trialbalance data failed, propertyId:{0},yearMonth:{1}", propertyId, yearMonth);
                log.error(errorMsg, e);
                sendAlertEmail(propertyDTO, "last year - " + M3DataTypeEnum.TRIALBALANCE.getDataType(), e);
                errorCount++;
            }

            try {
                //3.处理budget数据
                m3BudgetService.handleBudget(tenantM3ConfigDTO, propertyM3ConfigDTO, yearMonth, forcedRefresh);
            } catch (Exception e) {
                String errorMsg = MessageFormat.format("handle budget data failed, propertyId:{0},yearMonth:{1}", propertyId, yearMonth);
                log.error(errorMsg, e);
                sendAlertEmail(propertyDTO, M3DataTypeEnum.BUDGET.getDataType(), e);
                errorCount++;
            }

            try {
                //4.处理forecastName数据
                m3ForecastsNameService.handleForecastName(tenantM3ConfigDTO, propertyM3ConfigDTO, yearMonth, forcedRefresh);
            } catch (Exception e) {
                String errorMsg = MessageFormat.format("handle forecast name data failed, propertyId:{0},yearMonth:{1}", propertyId, yearMonth);
                log.error(errorMsg, e);
                sendAlertEmail(propertyDTO, M3DataTypeEnum.FORECAST_NAME.getDataType(), e);
                errorCount++;
            }

            try {
                //5.处理forecasts数据
                m3ForecastsService.handleForecasts(tenantM3ConfigDTO, propertyM3ConfigDTO, yearMonth, forcedRefresh);
            } catch (Exception e) {
                String errorMsg = MessageFormat.format("handle forecast data failed, propertyId:{0},yearMonth:{1}", propertyId, yearMonth);
                log.error(errorMsg, e);
                sendAlertEmail(propertyDTO, M3DataTypeEnum.FORECAST.getDataType(), e);
                errorCount++;
            }

            //处理结果都是失败时，更新日志为处理失败
            if (errorCount == 5) {
                m3PlReportLogService.updateFailed(logId, M3PlReportLog.MESSAGE_FLAG_CALL_M3_FAILED);
                throw new RuntimeException("All calls to the M3 interface failed");
            }
        }

        /**
         * 从数据库获取存在M3数据
         *
         * @param propertyId 资产id
         * @param yearMonth  年月,yyyyMM
         * @return java.util.Map<java.lang.String, java.lang.Object>
         * @author lichen
         * @date 2025/10/3
         */
        public Map<String, Object> listExistM3Data(Integer propertyId, Integer yearMonth) {
            HashMap<String, Object> m3DataMap = Maps.newHashMapWithExpectedSize(5);

            //查询指定资产的Account数据
            List<M3Account> m3Accounts = m3AccountService.listByPropertyId(propertyId);
            m3DataMap.put(M3DataTypeEnum.ACCOUNT.getDataType(), m3Accounts);

            YearMonth yearMonthObj = YearMonthUtils.getYearMonth(yearMonth.toString());
            //获取上一年
            YearMonth lastYearMonthObj = yearMonthObj.minusYears(1);
            Integer lastYearMonth = Integer.valueOf(YearMonthUtils.DATE_TIME_FORMATTER.format(lastYearMonthObj));

            //查询指定资产指定月份的trialbalance数据
            List<M3Trialbalance> m3Trialbalances = m3TrialbalanceService.listByYearMonths(propertyId, Lists.newArrayList(yearMonth, lastYearMonth));
            m3DataMap.put(M3DataTypeEnum.TRIALBALANCE.getDataType(), m3Trialbalances);

            //查询指定资产指定月份的bugdet数据
            List<M3Budget> m3Budgets = m3BudgetService.listByYearMonth(propertyId, yearMonth);
            m3DataMap.put(M3DataTypeEnum.BUDGET.getDataType(), m3Budgets);

            //查询指定资产指定年的forecastName数据
            List<M3ForecastName> m3ForecastNames = m3ForecastsNameService.listByYear(propertyId, yearMonthObj.getYear());
            m3DataMap.put(M3DataTypeEnum.FORECAST_NAME.getDataType(), m3ForecastNames);

            if (CollectionUtils.isNotEmpty(m3ForecastNames)) {
                List<String> forecastIds = m3ForecastNames.stream().map(M3ForecastName::getM3ForecastId).toList();
                //查询指定资产指定月份的forecasts数据
                List<M3Forecast> m3Forecasts = m3ForecastsService.listByYearMonthAndForecastId(propertyId, yearMonth, forecastIds);
                m3DataMap.put(M3DataTypeEnum.FORECAST.getDataType(), m3Forecasts);
            } else {
                m3DataMap.put(M3DataTypeEnum.FORECAST.getDataType(), Lists.newArrayList());
            }

            //查询特殊变量
            List<M3Variable> specialM3Variables = m3VariableService.listByTypes(propertyId, Lists.newArrayList(M3Variable.TYPE_SPECIAL));
            m3DataMap.put(M3DataTypeEnum.SPECIAL_VARIABLES.getDataType(), specialM3Variables);
            return m3DataMap;
        }

        /**
         * 判断客户是否关账
         *
         * @param customerCloseAccount 客户关账信息
         * @param propertyM3ConfigDTO  资产M3配置
         * @param yearMonth            年月,yyyyMM
         * @return boolean true:关账 false:未关账
         * @author lichen
         * @date 2025/10/6
         */
        public boolean isClosed(CustomerCloseAccountDTO customerCloseAccount, PropertyM3ConfigDTO propertyM3ConfigDTO, Integer yearMonth) {
            Map<String, List<FiscalPeriodCloseAccountDTO>> companyCode2FiscalPeriodCloseAccountDTOList = customerCloseAccount.getCompanyList().stream()
                    .collect(Collectors.toMap(CompanyCloseAccountDTO::getCompanyCode, CompanyCloseAccountDTO::getFiscalPeriods));
            List<FiscalPeriodCloseAccountDTO> fiscalPeriodCloseAccountDTOS = companyCode2FiscalPeriodCloseAccountDTOList.get(propertyM3ConfigDTO.getCompanyCode());
            if (CollectionUtils.isEmpty(fiscalPeriodCloseAccountDTOS)) {
                log.error("generatePLReport, close account info not exist, propertyId:" + propertyM3ConfigDTO.getPropertyId());
                return false;
            }
            int monthValue = YearMonthUtils.getYearMonth(yearMonth.toString()).getMonthValue();
            Map<Integer, FiscalPeriodCloseAccountDTO> month2FiscalPeriodCloseAccountDTO = fiscalPeriodCloseAccountDTOS.stream()
                    .collect(Collectors.toMap(FiscalPeriodCloseAccountDTO::getFiscalPeriod, Function.identity()));
            FiscalPeriodCloseAccountDTO fiscalPeriodCloseAccountDTO = month2FiscalPeriodCloseAccountDTO.get(monthValue);
            if (fiscalPeriodCloseAccountDTO.isGLOpen()) {
                log.warn("generatePLReport, accounts were not closed, propertyId:{}, month:{}", propertyM3ConfigDTO.getPropertyId(), monthValue);
                return false;
            }
            return true;
        }

        /**
         * 发送邮件
         *
         * @param propertyDTO 资产信息
         * @param m3ApiDesc   M3接口描述
         * @param e           异常
         * @author lichen
         * @date 2025/10/6
         */
        private void sendAlertEmail(PropertyDTO propertyDTO, String m3ApiDesc, Exception e) {
            String abbr = propertyDTO.getAbbr();
            if (RateLimitUtils.tryAcquire(this, abbr, m3ApiDesc)) {
                String alertTitle = "[Warning] - P&L Exceptions about call M3 Api of propertyAbbr:" + abbr;
                String alertContent = "<p>M3 Api:" + m3ApiDesc + "</p>"
                        + "<p>Exception:" + e + "</p>";
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle, alertContent);
            }
        }
    }