# M3PlLayoutServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.finance.cons.pl.*;
    import cloud.wconnect.finance.entity.pl.M3PlLayout;
    import cloud.wconnect.finance.entity.pl.M3Row;
    import cloud.wconnect.finance.mapper.pl.M3PlLayoutMapper;
    import cloud.wconnect.finance.pojo.dto.pl.*;
    import cloud.wconnect.finance.service.pl.IM3PlLayoutService;
    import cloud.wconnect.finance.service.pl.IM3RowService;
    import cloud.wconnect.finance.service.pl.PlCommonService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import org.apache.commons.collections.CollectionUtils;
    import org.checkerframework.checker.nullness.qual.Nullable;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Service;

    import java.text.MessageFormat;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Layout配置版本表 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-09-25
     */
    @Service
    @DS("finance")
    public class M3PlLayoutServiceImpl extends ServiceImpl<M3PlLayoutMapper, M3PlLayout> implements IM3PlLayoutService {

        @Autowired
        private PlCommonService plCommonService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        @Lazy
        private IM3RowService m3RowService;

        /**
         * 根据资产id正序查询Layout配置
         *
         * @param propertyId 资产id
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3PlLayout>
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public List<M3PlLayout> listOrderByIdAsc(int propertyId) {
            return list(new LambdaQueryWrapper<M3PlLayout>()
                    .eq(M3PlLayout::getPropertyId, propertyId)
                    .orderByAsc(M3PlLayout::getId));
        }

        /**
         * 查询Layout配置并携带异常信息
         *
         * @param propertyId 资产id
         * @param category   分类
         * @return java.util.List<cloud.wconnect.finance.pojo.dto.pl.M3PlLayoutWithErrorDTO>
         * @author lichen
         * @date 2025/9/30
         */
        @Override
        public List<M3PlLayoutAbnormalDTO> listLayoutAbnormal(int propertyId, Integer category) {
            //条件查询Layout配置
            List<M3PlLayout> m3PlLayouts = list(new LambdaQueryWrapper<M3PlLayout>()
                    .eq(M3PlLayout::getPropertyId, propertyId)
                    .eq(Objects.nonNull(category), M3PlLayout::getCategory, category)
                    .orderByAsc(M3PlLayout::getId));
            if (CollectionUtils.isEmpty(m3PlLayouts)) {
                return Collections.emptyList();
            }

            //查询资产下的M3 Row信息
            List<M3Row> m3RowList = m3RowService.listByPropertyIdOrderById(propertyId);
            Map<String, String> rowNumber2Name = m3RowList.stream().collect(Collectors.toMap(M3Row::getRowNumber, M3Row::getRowName));
            Map<String, Boolean> rowNumber2CanCalculation = m3RowList.stream().collect(Collectors.toMap(M3Row::getRowNumber,
                    m3Row -> RowCategoryEnum.needCalculation(m3Row.getRowCategory())));
            Set<String> rowNumbers = rowNumber2Name.keySet();

            ArrayList<M3PlLayoutAbnormalDTO> list = Lists.newArrayListWithCapacity(m3PlLayouts.size());

            for (M3PlLayout entity : m3PlLayouts) {
                M3PlLayoutAbnormalDTO dto = beanMapper.map(entity, M3PlLayoutAbnormalDTO.class);

                //当前Layout中引用的RowNumber
                ArrayList<String> usedRowNumberByLayout = Lists.newArrayListWithCapacity(4);
                if (entity.getCategory() == LayoutCategoryEnum.ROW.getCategory()) {
                    //考虑%Rev公式中引用的Row
                    List<String> rowNumbersByRevFormula = plCommonService.parseRevUsedRowNumber(entity.getRevFormula());
                    //添加Layout中引用的Row
                    usedRowNumberByLayout.add(entity.getRowNumber());
                    usedRowNumberByLayout.add(entity.getPorFormulaDenominator());
                    usedRowNumberByLayout.addAll(rowNumbersByRevFormula);
                }

                ArrayList<M3RowAbnormalDTO> m3RowAbnormalDTOS = Lists.newArrayListWithCapacity(usedRowNumberByLayout.size());
                for (String rowNumber : usedRowNumberByLayout) {
                    M3RowAbnormalDTO m3RowAbnormalDTO = new M3RowAbnormalDTO();
                    m3RowAbnormalDTO.setRowNumber(rowNumber);
                    if(rowNumbers.contains(rowNumber)){
                        m3RowAbnormalDTO.setRowName(rowNumber2Name.get(rowNumber));
                        m3RowAbnormalDTO.setRowExist(true);

                        // RowNumber是否可以计算
                        Boolean canCalculation = rowNumber2CanCalculation.get(rowNumber);
                        // 可计算表示正常，不可以计算表示异常
                        m3RowAbnormalDTO.setRowTypeNormal(canCalculation);
                    }else{
                        m3RowAbnormalDTO.setRowExist(false);
                        m3RowAbnormalDTO.setRowTypeNormal(false);
                    }
                    m3RowAbnormalDTOS.add(m3RowAbnormalDTO);
                }

                dto.setM3RowList(m3RowAbnormalDTOS);
                list.add(dto);
            }

            return list;
        }

        /**
         * 检查layout配置是否存在
         *
         * @param id layout配置id
         * @return cloud.wconnect.finance.entity.pl.M3PlLayout
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public M3PlLayout checkExist(Integer id) {
            M3PlLayout m3PlLayout = getById(id);
            if (Objects.isNull(m3PlLayout)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "m3PlLayout not found, myPlLayoutId:" + id);
            }
            return m3PlLayout;
        }

        /**
         * 检查layout配置是否属于资产
         *
         * @param m3PlLayout layout配置
         * @param propertyId 资产id
         * @return void
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public void checkBelong(M3PlLayout m3PlLayout, int propertyId) {
            if (m3PlLayout.getPropertyId().compareTo(propertyId) != 0) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("m3PlLayout not belong currentPropertyId, m3PlLayoutId:{0},belongPropertyId:{1},currentPropertyId:{2}",
                                m3PlLayout.getId(), m3PlLayout.getPropertyId(), propertyId));
            }
        }

        /**
         * 检查layout配置的rowNumber是否重复
         *
         * @param propertyId 资产id
         * @param rowNumber  Row编号
         * @param ignoreId   忽略的layout配置id
         * @return void
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public void checkRowNumberRepeat(int propertyId, String rowNumber, Integer ignoreId) {
            long count = count(new LambdaQueryWrapper<M3PlLayout>().eq(M3PlLayout::getPropertyId, propertyId)
                    .eq(M3PlLayout::getRowNumber, rowNumber)
                    .eq(M3PlLayout::getCategory, LayoutCategoryEnum.ROW.getCategory())
                    .ne(M3PlLayout::getId, ignoreId));
            if (count > 0) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_CANNOT_REPEAT,
                        i18nMessageUtils.getErrorParamMapNeedTranslate(PlFieldI18nConstant.ROW_NUMBER),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "rowNumber cannot repeat");
            }
        }

        /**
         * 保存layout配置
         *
         * @param tenantId      租户id
         * @param propertyId    资产id
         * @param m3PlLayoutDTO layout配置DTO
         * @return java.lang.Integer
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public Integer save(Integer tenantId, int propertyId, M3PlLayoutDTO m3PlLayoutDTO) {
            String revFormula = null;
            if (Objects.nonNull(m3PlLayoutDTO.getRevFormula())) {
                revFormula = JsonUtils.convertObjToString(objectMapper, m3PlLayoutDTO.getRevFormula());
            }

            //构建layout配置
            M3PlLayout m3PlLayout = new M3PlLayout();
            m3PlLayout.setTenantId(tenantId).setPropertyId(propertyId)
                    .setCategory(m3PlLayoutDTO.getCategory()).setRowNumber(m3PlLayoutDTO.getRowNumber())
                    .setStyleType(m3PlLayoutDTO.getStyleType()).setStyleValue(m3PlLayoutDTO.getStyleValue())
                    .setRowType(m3PlLayoutDTO.getRowType()).setRevFormula(revFormula)
                    .setPorFormulaDenominator(m3PlLayoutDTO.getPorFormulaDenominator());
            //新增
            save(m3PlLayout);

            return m3PlLayout.getId();
        }

        /**
         * 更新layout配置
         *
         * @param m3PlLayout    修改前layout配置
         * @param m3PlLayoutDTO 修改后的layout配置DTO
         * @return void
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public void update(M3PlLayout m3PlLayout, M3PlLayoutDTO m3PlLayoutDTO) {
            String revFormula = null;
            if (Objects.nonNull(m3PlLayoutDTO.getRevFormula())) {
                revFormula = JsonUtils.convertObjToString(objectMapper, m3PlLayoutDTO.getRevFormula());
            }

            //修改
            update(new LambdaUpdateWrapper<M3PlLayout>().eq(M3PlLayout::getId, m3PlLayout.getId())
                    .set(M3PlLayout::getCategory, m3PlLayoutDTO.getCategory())
                    .set(M3PlLayout::getRowNumber, m3PlLayoutDTO.getRowNumber())
                    .set(M3PlLayout::getStyleType, m3PlLayoutDTO.getStyleType())
                    .set(M3PlLayout::getStyleValue, m3PlLayoutDTO.getStyleValue())
                    .set(M3PlLayout::getRowType, m3PlLayoutDTO.getRowType())
                    .set(M3PlLayout::getRevFormula, revFormula)
                    .set(M3PlLayout::getPorFormulaDenominator, m3PlLayoutDTO.getPorFormulaDenominator()));
        }

        /**
         * 校验删除的Layout配置是否被其他%Rev减法公式依赖
         *
         * @param m3PlLayout layout配置
         * @return void
         * @author lichen
         * @date 2025/9/29
         */
        @Override
        public void checkUsed(M3PlLayout m3PlLayout) {
            Integer propertyId = m3PlLayout.getPropertyId();
            //查询layout配置
            List<M3PlLayout> m3PlLayouts = listOrderByIdAsc(propertyId);
            if (CollectionUtils.isEmpty(m3PlLayouts)) return;

            HashSet<String> allUsedRowNumber = Sets.newHashSet();
            for (M3PlLayout item : m3PlLayouts) {
                //跳过删除的数据
                if (Objects.equals(item.getId(), m3PlLayout.getId())) {
                    continue;
                }
                if (item.getCategory() == LayoutCategoryEnum.ROW.getCategory()) {
                    //这里只考虑减法公式：原因只有减法公式才依赖其他Layout
                    RevFormulaDTO revFormulaDTO = JsonUtils.convertStringToObject(objectMapper, item.getRevFormula(), RevFormulaDTO.class);
                    if (revFormulaDTO.getType() == RevFormulaTypeEnum.SUBTRACTION.getType()) {
                        List<String> usedRowNumber = plCommonService.parseFormulaUsedRowNumber(revFormulaDTO.getValue());
                        allUsedRowNumber.addAll(usedRowNumber);
                    }
                }
            }
            if (allUsedRowNumber.contains(m3PlLayout.getRowNumber())) {
                //删除的Layout配置被其他%Rev减法公式依赖
                throw new BadRequestException(PlErrorI18nConstant.LAYOUT_CONFIG_USED_BY_REV_FORMULA,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The deleted Layout configuration is dependent on other %Rev subtraction formulas");
            }
        }

        /**
         * 获取Layout配置中引用的RowNumber集合
         *
         * @param propertyId  资产id
         * @return java.util.List<java.lang.String>
         * @author lichen
         * @date 2026/3/21
         */
        @Override
        public Set<String> findReferenceRowNumber(int propertyId) {
            List<M3PlLayout> m3PlLayouts = listOrderByIdAsc(propertyId);
            if (CollectionUtils.isEmpty(m3PlLayouts)) return Collections.emptySet();
            // 引用的RowNumber集合
            HashSet<String> referenceRowNumbers = Sets.newHashSetWithExpectedSize(m3PlLayouts.size() * 2);

            for (M3PlLayout m3PlLayout : m3PlLayouts) {
                if (LayoutCategoryEnum.ROW.getCategory() == m3PlLayout.getCategory()) {
                    referenceRowNumbers.add(m3PlLayout.getRowNumber());
                    //por公式使用的 Row
                    referenceRowNumbers.add(m3PlLayout.getPorFormulaDenominator());
                    //解析rev 公式使用的 Row
                    List<String> revUsedRowNumbers = plCommonService.parseRevUsedRowNumber(m3PlLayout.getRevFormula());
                    if (CollectionUtils.isNotEmpty(revUsedRowNumbers)) {
                        referenceRowNumbers.addAll(revUsedRowNumbers);
                    }
                }
            }
            return referenceRowNumbers;
        }
    }