# CalculationIndexService.java

    package cloud.wconnect.finance.service.m3;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.bo.m3.DailyOperatingStatisticsIndexBO;
    import cloud.wconnect.commonlib.pojo.bo.m3.DailyOperatingStatisticsIndexValueBO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.RoomsConfigDTO;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.RateLimitUtils;
    import cloud.wconnect.commonlib.cons.m3.DailyOperatingStatisticsIndexEnum;
    import cloud.wconnect.finance.entity.m3.M3Mapping;
    import cloud.wconnect.finance.pojo.dto.m3.CalErrorAlertEmailDTO;
    import cloud.wconnect.finance.pojo.dto.m3.CalculatedM3JournalEntryDTO;
    import cloud.wconnect.finance.pojo.dto.m3.IndexDTO;
    import cloud.wconnect.finance.service.pl.PlCommonService;
    import cloud.wconnect.sns.http.processor.Sender;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.ezylang.evalex.Expression;
    import com.ezylang.evalex.config.ExpressionConfiguration;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
    import software.amazon.awssdk.services.sns.model.PublishResponse;

    import java.math.BigDecimal;
    import java.math.MathContext;
    import java.math.RoundingMode;
    import java.text.MessageFormat;
    import java.time.LocalDate;
    import java.util.*;
    import java.util.regex.Matcher;
    import java.util.stream.Collectors;

    /**
     * 计算指标的业务层
     *
     * @author lichen
     * @date 2025/10/14
     */
    @Service
    @DS("finance")
    @Slf4j
    public class CalculationIndexService {

        @Autowired
        private IM3MappingService m3MappingService;
        @Autowired
        private IM3JournalEntryService m3JournalEntryService;
        @Autowired
        private RoomsConfigInternalService roomsConfigInternalService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private MailInternalService mailInternalService;
        @Qualifier("moduleEventSender")
        @Autowired
        private Sender snsSender;

        /**
         * 计算指标数据
         *
         * @param propertyDTO               资产DTO
         * @param accountNumber2AccountType m3 Account集合
         * @param startLocalDate            开始日期
         * @param endLocalDate              结束日期
         */
        public void calculationIndex(PropertyDTO propertyDTO, Map<String, Integer> accountNumber2AccountType, LocalDate startLocalDate, LocalDate endLocalDate) {
            //计算指定GLAccount和指定日期范围的数据
            List<CalculatedM3JournalEntryDTO> calculatedM3JournalEntryDTOS = m3JournalEntryService.calculateByAccountNumberAndDateRange(propertyDTO.getId(),
                    accountNumber2AccountType, startLocalDate, endLocalDate);

            //获取资产配置的房间数
            RoomsConfigDTO roomsConfig = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId());
            Integer rooms = roomsConfig.getRooms();

            //预警邮件集合
            List<CalErrorAlertEmailDTO> alertEmailDTOS = new ArrayList<>();

            //计算基础指标
            List<IndexDTO> baseIndexDTOS = calculationBaseIndex(propertyDTO, calculatedM3JournalEntryDTOS, startLocalDate, endLocalDate, alertEmailDTOS);

            //计算：公式因子为基础指标的指标
            List<IndexDTO> formulaIndexDTOS = calculationFormulaIndex(propertyDTO, baseIndexDTOS, rooms, startLocalDate, endLocalDate, alertEmailDTOS);

            //发送Moudle Event
            ArrayList<IndexDTO> indexDTOS = Lists.newArrayList(baseIndexDTOS);
            indexDTOS.addAll(formulaIndexDTOS);

            List<IndexDTO> validIndexDTOS = indexDTOS.stream().filter(indexDTO -> indexDTO.getIndexValue().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList());
            if(CollectionUtils.isNotEmpty(validIndexDTOS)){
                //发送Moudle Event
                sendModuleEvent(propertyDTO, validIndexDTOS);
            }

            //发送预警邮件
            if(CollectionUtils.isNotEmpty(alertEmailDTOS)){
                sendAlertEmail(propertyDTO, alertEmailDTOS);
            }
        }

        /**
         * 发送Moudle Event
         *
         * @param propertyDTO 资产DTO
         * @param indexDTOS   指标集合
         * @author lichen
         * @date 2025/10/14
         */
        private void sendModuleEvent(PropertyDTO propertyDTO, List<IndexDTO> indexDTOS) {
            //指标和对应主题的Map
            Map<String, String> indexName2Subject = Arrays.stream(DailyOperatingStatisticsIndexEnum.values()).collect(Collectors.toMap(DailyOperatingStatisticsIndexEnum::getIndexName, DailyOperatingStatisticsIndexEnum::getSubject));
            //按照指标名称分组
            Map<String, List<IndexDTO>> indexName2IndexDTOList = indexDTOS.stream().collect(Collectors.groupingBy(IndexDTO::getIndexName));

            for (Map.Entry<String, List<IndexDTO>> entry : indexName2IndexDTOList.entrySet()) {
                String indexName = entry.getKey();
                List<IndexDTO> indexDTOSByIndexName = entry.getValue();

                DailyOperatingStatisticsIndexBO dailyOperatingStatisticsIndexBO = new DailyOperatingStatisticsIndexBO();
                dailyOperatingStatisticsIndexBO.setPropertyId(propertyDTO.getId());
                dailyOperatingStatisticsIndexBO.setIndexName(indexName);
                dailyOperatingStatisticsIndexBO.setIndexValues(beanMapper.mapList(indexDTOSByIndexName, DailyOperatingStatisticsIndexValueBO.class));

                //主题
                String subject = indexName2Subject.get(indexName);
                //消息内容
                String msg = JsonUtils.convertObjToString(objectMapper, dailyOperatingStatisticsIndexBO);
                PublishResponse response = snsSender.publish(subject, msg);
                log.info("M3 Send Operating Statistics Daily index, subject:{} msgId:{}, body:{}", subject, response.messageId(), msg);
            }
        }

        /**
         * 计算：公式因子为基础指标的指标
         *
         * @param propertyDTO    资产DTO
         * @param baseIndexDTOS  基础指标集合
         * @param rooms          资产配置的房间数
         * @param startDate      开始日期
         * @param endDate        结束日期
         * @param alertEmailDTOS 预警邮件集合
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.m3.IndexDTO>
         * @author lichen
         * @date 2025/10/14
         */
        private List<IndexDTO> calculationFormulaIndex(PropertyDTO propertyDTO, List<IndexDTO> baseIndexDTOS, Integer rooms,
                                                       LocalDate startDate, LocalDate endDate, List<CalErrorAlertEmailDTO> alertEmailDTOS) {
            Integer propertyId = propertyDTO.getId();
            Map<Integer, Map<String, BigDecimal>> date2IndexValueMap = baseIndexDTOS.stream().collect(Collectors.groupingBy(IndexDTO::getDate,
                    Collectors.toMap(IndexDTO::getIndexName, IndexDTO::getIndexValue)));

            //获取公式指标的集合
            List<DailyOperatingStatisticsIndexEnum> dailyOperatingStatisticsIndexEnums = DailyOperatingStatisticsIndexEnum.listFormulaIndex();

            ArrayList<IndexDTO> list = Lists.newArrayListWithCapacity(dailyOperatingStatisticsIndexEnums.size());

            for (DailyOperatingStatisticsIndexEnum dailyOperatingStatisticsIndexEnum : dailyOperatingStatisticsIndexEnums) {
                String formula = dailyOperatingStatisticsIndexEnum.getFormula();
                String indexName = dailyOperatingStatisticsIndexEnum.getIndexName();
                //循环开始日期到结束日期的每一天
                for (LocalDate targetDate = startDate; !targetDate.isAfter(endDate); targetDate = targetDate.plusDays(1)) {
                    //目标日期
                    Integer targetDateInt = Integer.valueOf(DateTimeUtils.localDateToStr(targetDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                    //获取计算因子的值
                    Map<String, BigDecimal> factorValueMap = date2IndexValueMap.getOrDefault(targetDateInt, Collections.emptyMap());
                    factorValueMap.put(DailyOperatingStatisticsIndexEnum.TOTAL_ROOMS_AVAILABLE, new BigDecimal(rooms.toString()));
                    try {
                        //计算指标
                        BigDecimal indexValue = doCalculation(formula, factorValueMap);

                        IndexDTO indexDTO = new IndexDTO(indexName, indexValue, targetDateInt);
                        list.add(indexDTO);
                    } catch (Exception e) {
                        String errorStr = MessageFormat.format("calculation index error, propertyId:{0}, indexName:{1}, formula:{2}, factorValueMap:{3}, targetDate:{4}",
                                propertyId, indexName, formula, factorValueMap, targetDateInt);
                        log.error(errorStr, e);
                        CalErrorAlertEmailDTO calErrorAlertEmailDTO = new CalErrorAlertEmailDTO(indexName, formula, factorValueMap, targetDateInt, e);
                        alertEmailDTOS.add(calErrorAlertEmailDTO);
                    }
                }
            }

            return list;
        }

        /**
         * 计算基础指标
         *
         * @param propertyDTO                  资产DTO
         * @param calculatedM3JournalEntryDTOS 基础数据
         * @param startDate                    开始日期
         * @param endDate                      结束日期
         * @param alertEmailDTOS               预警邮件集合
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.m3.IndexDTO>
         * @author lichen
         * @date 2025/10/14
         */
        private List<IndexDTO> calculationBaseIndex(PropertyDTO propertyDTO, List<CalculatedM3JournalEntryDTO> calculatedM3JournalEntryDTOS,
                                                    LocalDate startDate, LocalDate endDate, List<CalErrorAlertEmailDTO> alertEmailDTOS) {
            Integer propertyId = propertyDTO.getId();
            //获取基础指标名称集合
            List<String> baseIndexNameList = DailyOperatingStatisticsIndexEnum.listBaseIndex();

            //获取基础指标的公式集合
            Map<String, String> fieldCode2Formula = m3MappingService.listByPropertyId(propertyId).stream()
                    .collect(Collectors.toMap(M3Mapping::getFieldCode, M3Mapping::getFormula));

            Map<Integer, Map<String, BigDecimal>> postDate2JournalEntryMap = calculatedM3JournalEntryDTOS.stream()
                    .collect(Collectors.groupingBy(CalculatedM3JournalEntryDTO::getPostDate,
                            Collectors.toMap(CalculatedM3JournalEntryDTO::getAccountNumber, CalculatedM3JournalEntryDTO::getAmount)));

            ArrayList<IndexDTO> list = Lists.newArrayListWithCapacity(baseIndexNameList.size());
            for (String indexName : baseIndexNameList) {
                //公式
                String formula = fieldCode2Formula.get(indexName);
                if (StringUtils.isBlank(formula)) {
                    log.error("formula is blank, propertyId:{}, indexName:{}", propertyId, indexName);
                    CalErrorAlertEmailDTO calErrorAlertEmailDTO = new CalErrorAlertEmailDTO(indexName, formula,
                            Collections.emptyMap(), 0, new NullPointerException("formula is blank"));
                    alertEmailDTOS.add(calErrorAlertEmailDTO);
                    continue;
                }
                //循环开始日期到结束日期的每一天
                for (LocalDate targetDate = startDate; !targetDate.isAfter(endDate); targetDate = targetDate.plusDays(1)) {
                    //目标日期
                    Integer targetDateInt = Integer.valueOf(DateTimeUtils.localDateToStr(targetDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                    //获取指定日期的基础数据
                    Map<String, BigDecimal> accountNumber2Amount = postDate2JournalEntryMap.getOrDefault(targetDateInt, Collections.emptyMap());
                    try {
                        //计算指标
                        BigDecimal indexValue = doCalculation(formula, accountNumber2Amount);

                        IndexDTO indexDTO = new IndexDTO(indexName, indexValue, targetDateInt);
                        list.add(indexDTO);
                    } catch (Exception e) {
                        String errorStr = MessageFormat.format("calculation index error, propertyId:{0}, indexName:{1}, formula:{2}, factorValueMap:{3}, targetDate:{4}",
                                propertyId, indexName, formula, accountNumber2Amount, targetDateInt);
                        log.error(errorStr, e);
                        CalErrorAlertEmailDTO calErrorAlertEmailDTO = new CalErrorAlertEmailDTO(indexName, formula, accountNumber2Amount, targetDateInt, e);
                        alertEmailDTOS.add(calErrorAlertEmailDTO);
                    }
                }
            }
            return list;
        }

        /**
         * 计算指标
         *
         * @param formula        公式
         * @param factorValueMap 公式中计算因子的值
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.m3.IndexDTO>
         * @author lichen
         * @date 2025/10/14
         */
        public BigDecimal doCalculation(String formula, Map<String, BigDecimal> factorValueMap) throws Exception {
            //4. try-catch计算公式
            try {
                StringBuilder finalFormula = new StringBuilder();

                //匹配花括号 {} 及其内部
                Matcher matcher = PlCommonService.CURLY_BRACES_PATTERN.matcher(formula);
                while (matcher.find()) {
                    //花括号内部内容
                    String dependency = matcher.group(1);
                    //获取变量对应的值
                    BigDecimal factorValue = factorValueMap.getOrDefault(dependency, BigDecimal.ZERO);
                    //替换公式中的变量
                    matcher.appendReplacement(finalFormula, factorValue.toString());
                }

                // MathContext.精度(Precision): 指的是有效数字的总位数，而不是小数点后的位数
                MathContext mathContext = new MathContext(BaseConstant.SIGNIFICANT_FIGURES_TOTAL_NUMBER, RoundingMode.HALF_UP);
                ExpressionConfiguration configuration = ExpressionConfiguration.builder()
                        .mathContext(mathContext)
                        .build();
                Expression expression = new Expression(finalFormula.toString(), configuration);
                BigDecimal numberValue = expression.evaluate().getNumberValue();
                return numberValue.setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP);
            } catch (Exception e) {
                if (e.getMessage().equals("Division by zero")) {
                    //分母是0时，认为计算成功，指标结果为0
                    return BigDecimal.ZERO;
                } else {
                    throw e;
                }
            }
        }

        /**
         * 发送Alert Email
         *
         * @param propertyDTO    资产DTO
         * @param alertEmailDTOS 预警邮件集合
         * @author lichen
         * @date 2025/10/14
         */
        private void sendAlertEmail(PropertyDTO propertyDTO, List<CalErrorAlertEmailDTO> alertEmailDTOS) {
            String abbr = propertyDTO.getAbbr();
            if (RateLimitUtils.tryAcquire(this, abbr)) {
                String alertTitle = "[Warning] - " + abbr + " - Generate Daily Operating Statistics Index Data By M3 Task - calculation index";
                StringBuilder alertMsg = new StringBuilder();
                for (CalErrorAlertEmailDTO alertEmailDTO : alertEmailDTOS) {
                    String indexName = alertEmailDTO.getIndexName();
                    String formula = alertEmailDTO.getFormula();
                    Map<String, BigDecimal> factorValueMap = alertEmailDTO.getFactorValueMap();
                    Integer targetDate = alertEmailDTO.getDate();
                    Exception e = alertEmailDTO.getE();
                    alertMsg.append("<br/>");
                    alertMsg.append("----------").append(indexName).append("----------");
                    alertMsg.append("<br/>");
                    alertMsg.append(MessageFormat.format("Formula:{0}<br/>FactorValueMap:{1}<br/>targetDate:{2}<br/>Exception:{3}",
                            formula, factorValueMap.toString(), targetDate, e));
                    alertMsg.append("<br/>");
                    alertMsg.append("----------").append("End").append("----------");
                }
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle, alertMsg.toString());
            }
        }

    }