# M3TrialbalanceServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    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.DateTimeUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.finance.cons.pl.AccountTypeEnum;
    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.M3Account;
    import cloud.wconnect.finance.entity.pl.M3Row;
    import cloud.wconnect.finance.entity.pl.M3Trialbalance;
    import cloud.wconnect.finance.entity.pl.M3Variable;
    import cloud.wconnect.finance.mapper.pl.M3TrialbalanceMapper;
    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.TrialbalanceDTO;
    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.collections.MapUtils;
    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.LocalDate;
    import java.time.YearMonth;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * M3系统trialbalance数据 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-09-25
     */
    @Service
    @DS("finance")
    public class M3TrialbalanceServiceImpl extends ServiceImpl<M3TrialbalanceMapper, M3Trialbalance> implements IM3TrialbalanceService {

        @Autowired
        private M3ApiService m3ApiService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private PlCommonService plCommonService;
        @Autowired
        private IM3VariableService m3VariableService;
        /**
         * 数据类型
         */
        public static final String DATA_TYPE = M3DataTypeEnum.TRIALBALANCE.getDataType();

        /**
         * 处理trialbalance数据
         *
         * @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 handleTrialbalance(TenantM3ConfigDTO tenantM3ConfigDTO, PropertyM3ConfigDTO propertyM3ConfigDTO, Integer yearMonth, Boolean forcedRefresh) {
            Integer tenantId = tenantM3ConfigDTO.getTenantId();
            Integer propertyId = propertyM3ConfigDTO.getPropertyId();
            boolean m3DataHandleStatus = plCommonService.getM3DataHandleStatus(DATA_TYPE, yearMonth.toString(), propertyId);
            if (forcedRefresh || !m3DataHandleStatus) {
                //删除指定月份的trialbalance数据
                remove(new LambdaQueryWrapper<M3Trialbalance>().eq(M3Trialbalance::getPropertyId, propertyId)
                        .eq(M3Trialbalance::getYearMonth, yearMonth));

                YearMonth yearMonthObj = YearMonthUtils.getYearMonth(yearMonth.toString());
                LocalDate endOfMonth = yearMonthObj.atEndOfMonth();
                String asOfDate = DateTimeUtils.localDateToStr(endOfMonth, BaseConstant.DATE_FORMAT_YYYY_MM_DD);

                //查询指定月份的trialbalance数据
                List<TrialbalanceDTO> trialbalanceDTOS = m3ApiService.listTrialbalance(tenantM3ConfigDTO, propertyM3ConfigDTO, asOfDate);
                //筛选出accountNumber非空的trialbalance数据
                List<TrialbalanceDTO> effectiveList = trialbalanceDTOS.stream().filter(item -> StringUtils.isNotBlank(item.getAccountNumber())).toList();
                //保存trialbalance数据
                batchInsert(dtoToEntity(tenantId, propertyId, yearMonth, effectiveList));

                //记录步骤处理标识
                plCommonService.setM3DataAlreadyHandled(DATA_TYPE, yearMonth.toString(), propertyId);
            }
        }

        /**
         * 批量转换DTO为实体
         *
         * @param tenantId         租户id
         * @param propertyId       资产id
         * @param trialbalanceDTOS 数据集合
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3Trialbalance>
         * @author lichen
         * @date 2025/9/26
         */
        private List<M3Trialbalance> dtoToEntity(Integer tenantId, int propertyId, Integer yearMonth, List<TrialbalanceDTO> trialbalanceDTOS) {
            return beanMapper.mapList(trialbalanceDTOS, M3Trialbalance.class, (dto, entity) -> {
                entity.setTenantId(tenantId);
                entity.setPropertyId(propertyId);
                entity.setYearMonth(yearMonth);
                //小数位保留6位
                entity.setBeginningBalance(dto.getBeginningBalance().setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                entity.setNetChange(dto.getNetChange().setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                entity.setEndingBalance(dto.getEndingBalance().setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                entity.setDebitAmount(dto.getDebitAmount().setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
                entity.setCreditAmount(dto.getCreditAmount().setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP));
            });
        }

        /**
         * 批量插入
         *
         * @param effectiveTrialbalances 数据集合
         * @return void
         * @author lichen
         * @date 2025/9/26
         */
        private void batchInsert(List<M3Trialbalance> effectiveTrialbalances) {
            if (CollectionUtils.isEmpty(effectiveTrialbalances)) return;
            // 分批处理，每300条记录一批
            int batchSize = 300;
            for (int i = 0; i < effectiveTrialbalances.size(); i += batchSize) {
                int endIndex = Math.min(i + batchSize, effectiveTrialbalances.size());
                List<M3Trialbalance> batch = effectiveTrialbalances.subList(i, endIndex);
                baseMapper.insertBatch(batch);
            }
        }

        /**
         * 查询指定资产指定月份的trialbalance数据
         *
         * @param propertyId 资产id
         * @param yearMonth  年月,yyyyMM
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3Trialbalance>
         * @author lichen
         * @date 2025/9/26
         */
        @Override
        public List<M3Trialbalance> listByYearMonth(Integer propertyId, Integer yearMonth) {
            return list(new LambdaQueryWrapper<M3Trialbalance>().eq(M3Trialbalance::getPropertyId, propertyId).eq(M3Trialbalance::getYearMonth, yearMonth));
        }

        /**
         * 计算指标
         *
         * @param propertyId                        资产id
         * @param m3Row                             m3 Row
         * @param yearMonth                         计算的年月，yyyyMM
         * @param alreadyCalculationRowIndexDTOList 已经计算过的Row指标集合
         * @param m3DataMap                         m3数据
         * @param rooms                             房间数
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO>
         * @author lichen
         * @date 2025/10/1
         */
        @Override
        public List<IndexCalculationDTO> calculationIndex(Integer propertyId, M3Row m3Row, Integer yearMonth,
                                                          ArrayList<RowIndexCalculationDTO> alreadyCalculationRowIndexDTOList,
                                                          Map<String, Object> m3DataMap, Integer rooms) {
            String accountNumber = m3Row.getAccountNumber();
            List<IndexCalculationDTO> indexCalculationDTOS = Lists.newArrayListWithCapacity(4);
            ArrayList<String> needIndexList = Lists.newArrayList(IndexTypeEnum.PTD.getIndexType(), IndexTypeEnum.PTD_LY.getIndexType(),
                    IndexTypeEnum.YTD.getIndexType(), IndexTypeEnum.YTD_LY.getIndexType());

            if (m3Row.getRowCategory().equals(RowCategoryEnum.ITEM.getValue())) {//非公式计算
                //TrialbalanceService数据
                List<M3Trialbalance> m3TrialbalanceList = (List<M3Trialbalance>) m3DataMap.getOrDefault(M3DataTypeEnum.TRIALBALANCE.getDataType(),
                        new ArrayList<M3Trialbalance>());
                //Account数据
                List<M3Account> m3AccountList = (List<M3Account>) m3DataMap.getOrDefault(M3DataTypeEnum.ACCOUNT.getDataType(),
                        new ArrayList<M3Account>());

                //查询指定资产所有会计科目数据
                Map<String, Integer> accountNumber2Type = m3AccountList.stream().collect(Collectors.toMap(M3Account::getAccountNumber, M3Account::getAccountType));

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

                //查询指定资产指定月份的trialbalance数据
                Map<Integer, Map<String, M3Trialbalance>> yearMonth2AccountNumber2M3Trialbalance = m3TrialbalanceList
                        .stream().collect(Collectors.groupingBy(M3Trialbalance::getYearMonth,
                                Collectors.toMap(M3Trialbalance::getAccountNumber, Function.identity())));

                if (MapUtils.isNotEmpty(yearMonth2AccountNumber2M3Trialbalance)) {
                    //获取row对应的trialbalance数据
                    M3Trialbalance m3Trialbalance = yearMonth2AccountNumber2M3Trialbalance.getOrDefault(yearMonth, Collections.emptyMap())
                            .getOrDefault(accountNumber, null);
                    //PTD
                    IndexCalculationDTO ptdIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.PTD.getIndexType(), getPTD(accountNumber, m3Trialbalance, accountNumber2Type),
                            true);
                    indexCalculationDTOS.add(ptdIndexCalculationDTO);
                    //YTD
                    IndexCalculationDTO ytdIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.YTD.getIndexType(), getYTD(accountNumber, m3Trialbalance, accountNumber2Type),
                            true);
                    indexCalculationDTOS.add(ytdIndexCalculationDTO);

                    //获取row对应的上年同期trialbalance数据
                    M3Trialbalance m3TrialbalanceLastYear = yearMonth2AccountNumber2M3Trialbalance.getOrDefault(lastYearMonth, Collections.emptyMap())
                            .getOrDefault(accountNumber, null);
                    //PTD LY
                    IndexCalculationDTO ptdLyIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.PTD_LY.getIndexType(), getPTD(accountNumber, m3TrialbalanceLastYear, accountNumber2Type),
                            true);
                    indexCalculationDTOS.add(ptdLyIndexCalculationDTO);
                    //YTD LY
                    IndexCalculationDTO ytdLyIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.YTD_LY.getIndexType(), getYTD(accountNumber, m3TrialbalanceLastYear, accountNumber2Type),
                            true);
                    indexCalculationDTOS.add(ytdLyIndexCalculationDTO);
                }
            } 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, "");
                    indexCalculationDTOS.add(indexCalculationDTO);
                }
            }

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

            return indexCalculationDTOS;
        }

        /**
         * 计算PTD指标
         *
         * @param propertyId                        资产id
         * @param m3Row                             m3 Row
         * @param yearMonth                         计算的年月，yyyyMM
         * @param alreadyCalculationRowIndexDTOList 已经计算过的Row指标集合
         * @param m3TrialbalanceList                Trialbalance数据
         * @param m3Accounts                        Account数据
         * @param rooms                             房间数
         * @param specialM3Variables                特殊变量（这里指totalRoomsAvailable）
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO>
         * @author lichen
         * @date 2026/3/17
         */
        @Override
        public IndexCalculationDTO calculationPTD(Integer propertyId, M3Row m3Row, Integer yearMonth,
                                                  ArrayList<RowIndexCalculationDTO> alreadyCalculationRowIndexDTOList,
                                                  List<M3Trialbalance> m3TrialbalanceList, List<M3Account> m3Accounts,
                                                  Integer rooms, List<M3Variable> specialM3Variables) {
            String accountNumber = m3Row.getAccountNumber();
            IndexCalculationDTO ptdIndexCalculationDTO = null;

            if (m3Row.getRowCategory().equals(RowCategoryEnum.ITEM.getValue())) {//非公式计算
                //查询指定资产所有会计科目数据
                Map<String, Integer> accountNumber2Type = m3Accounts.stream().collect(Collectors.toMap(M3Account::getAccountNumber, M3Account::getAccountType));
                if (CollectionUtils.isNotEmpty(m3TrialbalanceList)) {
                    Map<String, M3Trialbalance> accountNumber2M3Trialbalance = m3TrialbalanceList
                            .stream().collect(Collectors.toMap(M3Trialbalance::getAccountNumber, Function.identity()));

                    //获取row对应的trialbalance数据
                    M3Trialbalance m3Trialbalance = accountNumber2M3Trialbalance.getOrDefault(accountNumber, null);
                    //PTD
                    ptdIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.PTD.getIndexType(), getPTD(accountNumber, m3Trialbalance, accountNumber2Type),
                            true);
                }
            } else {//公式计算
                //替换公式中的特殊变量
                String expandedFormula = m3VariableService.replaceSpecialVariable(m3Row.getFormula(), propertyId, IndexTypeEnum.PTD.getIndexType(), yearMonth, rooms, specialM3Variables);
                //计算PTD指标
                ptdIndexCalculationDTO = plCommonService.calculationIndex(m3Row.getRowNumber(), expandedFormula,
                        IndexTypeEnum.PTD.getIndexType(), alreadyCalculationRowIndexDTOList, "");
            }

            //没有计算，指标默认按0处理
            if (Objects.isNull(ptdIndexCalculationDTO)) {
                ptdIndexCalculationDTO = new IndexCalculationDTO(IndexTypeEnum.PTD.getIndexType(), new BigDecimal("0"), true);
            }
            return ptdIndexCalculationDTO;
        }

        /**
         * 获取PTD
         *
         * @param accountNumber  账户编号
         * @param m3Trialbalance trialbalance数据
         * @return java.lang.String
         * @author lichen
         * @date 2025/9/26
         */
        private BigDecimal getPTD(String accountNumber, M3Trialbalance m3Trialbalance, Map<String, Integer> accountNumber2Type) {
            if (Objects.isNull(m3Trialbalance)) {
                return new BigDecimal("0");
            }

            Integer accountType = accountNumber2Type.get(accountNumber);
            if (Objects.isNull(accountType)) {
                return new BigDecimal("0");
            }

            if (AccountTypeEnum.INCOME.getType() == accountType) {
                return m3Trialbalance.getNetChange().negate();
            } else {
                return m3Trialbalance.getNetChange();
            }
        }

        /**
         * 获取YTD
         *
         * @param accountNumber  账户编号
         * @param m3Trialbalance trialbalance数据
         * @return java.lang.String
         * @author lichen
         * @date 2025/9/26
         */
        private BigDecimal getYTD(String accountNumber, M3Trialbalance m3Trialbalance, Map<String, Integer> accountNumber2Type) {
            if (Objects.isNull(m3Trialbalance)) {
                return new BigDecimal("0");
            }

            Integer accountType = accountNumber2Type.get(accountNumber);
            if (Objects.isNull(accountType)) {
                return new BigDecimal("0");
            }

            if (AccountTypeEnum.INCOME.getType() == accountType) {
                return m3Trialbalance.getEndingBalance().negate();
            } else {
                return m3Trialbalance.getEndingBalance();
            }
        }

        /**
         * 查询指定资产指定月份的trialbalance数据
         *
         * @param propertyId 资产id
         * @param yearMonths 年月,yyyyMM
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3Trialbalance>
         * @author lichen
         * @date 2025/9/26
         */
        @Override
        public List<M3Trialbalance> listByYearMonths(Integer propertyId, ArrayList<Integer> yearMonths) {
            if (CollectionUtils.isEmpty(yearMonths)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<M3Trialbalance>().eq(M3Trialbalance::getPropertyId, propertyId)
                    .in(M3Trialbalance::getYearMonth, yearMonths));
        }

        /**
         * 计算一组Account PTD的和
         *
         * @param propertyId      资产id
         * @param m3Trialbalances Trialbalance数据
         * @param m3Accounts      Account的集合
         * @return java.math.BigDecimal
         * @author lichen
         * @date 2026-03-16
         */
        @Override
        public BigDecimal calculateAccountPTDSum(Integer propertyId, List<M3Trialbalance> m3Trialbalances, List<M3Account> m3Accounts) {
            if (CollectionUtils.isEmpty(m3Accounts)) {
                return BigDecimal.ZERO;
            }

            Map<String, Integer> accountNumber2Type = m3Accounts.stream().collect(Collectors.toMap(M3Account::getAccountNumber, M3Account::getAccountType));
            Set<String> accountNumbers = accountNumber2Type.keySet();

            Map<String, M3Trialbalance> acountNumber2M3Trialbalances = m3Trialbalances.stream().collect(Collectors.toMap(M3Trialbalance::getAccountNumber, Function.identity()));

            ArrayList<BigDecimal> ptdList = Lists.newArrayListWithCapacity(accountNumbers.size());
            for (String accountNumber : accountNumbers) {
                M3Trialbalance m3Trialbalance = acountNumber2M3Trialbalances.get(accountNumber);
                if (Objects.nonNull(m3Trialbalance)) {
                    BigDecimal ptd = m3Trialbalance.getNetChange();
                    ptdList.add(ptd);
                }
            }
            return ptdList.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
        }

    }