# ScoreCardGmServiceImpl.java

    package cloud.wconnect.workperformance.service.scorecardgm.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.CalculatorUtil;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.scorecardgm.PointsCons;
    import cloud.wconnect.workperformance.cons.scorecardgm.ScoreCons;
    import cloud.wconnect.workperformance.cons.scorecardgm.WeighedScoreCons;
    import cloud.wconnect.workperformance.entity.scorecardgm.ScoreCardGm;
    import cloud.wconnect.workperformance.entity.scorecardgm.ScoreCardGmConfig;
    import cloud.wconnect.workperformance.mapper.scorecardgm.ScoreCardGmMapper;
    import cloud.wconnect.workperformance.pojo.dto.scorecardgm.*;
    import cloud.wconnect.workperformance.service.scorecardgm.IScoreCardGmConfigService;
    import cloud.wconnect.workperformance.service.scorecardgm.IScoreCardGmService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    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.YearMonth;
    import java.time.ZoneId;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-10-04
     */
    @Service
    @DS("monthly_report")
    public class ScoreCardGmServiceImpl extends ServiceImpl<ScoreCardGmMapper, ScoreCardGm> implements IScoreCardGmService {

        @Autowired
        private IScoreCardGmConfigService scoreCardGmConfigService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private BeanMapper beanMapper;

        /**
         * 查看 Score Card 详情
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param month      月
         * @return ScoreCard 详情
         * @author yyt
         * @date 2023/10/4
         */
        @Override
        public ScoreCardGmDTO getDetail(Integer propertyId, int year, int month) {
            ScoreCardGm scoreCardGm = getOne(new QueryWrapper<ScoreCardGm>().lambda()
                    .eq(ScoreCardGm::getPropertyId, propertyId).eq(ScoreCardGm::getYear, year).eq(ScoreCardGm::getMonth, month));
            if (Objects.isNull(scoreCardGm)) {
                scoreCardGm = saveInintData(propertyId, year, month);
            }

            Map<Integer, ScoreCardGmConfig> configById = scoreCardGmConfigService.listByStatus(propertyId, null).stream()
                    .collect(Collectors.toMap(ScoreCardGmConfig::getId, v -> v));

            ScoreCardGmDTO scoreCardGmDTO = new ScoreCardGmDTO();
            List<ConfigBasicDTO> configBasicDTOList = JsonUtils.convertStringToList(objectMapper, scoreCardGm.getConfigJson(), ConfigBasicDTO.class);
            scoreCardGmDTO.setConfigList(beanMapper.mapList(configBasicDTOList, ConfigDTO.class, (configBasic, config) -> {
                ScoreCardGmConfig parentConfig = configById.get(config.getId());
                config.setName(parentConfig.getTitle());
                config.setUnit(parentConfig.getUnit());
                config.getItems().forEach(item -> {
                    ScoreCardGmConfig childConfig = configById.get(item.getId());
                    item.setName(childConfig.getTitle());
                    item.setUnit(childConfig.getUnit());
                });
            }));
            scoreCardGmDTO.setHistoricalMonthTotalScore(baseMapper.getHistoricalMonthTotalScore(propertyId, year, month));
            return scoreCardGmDTO;
        }

        /**
         * 插入初始数据
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param month      月
         * @return ScoreCard
         */
        @Override
        public ScoreCardGm saveInintData(Integer propertyId, int year, int month) {
            Map<Integer, List<ScoreCardGmConfig>> configsByParentId = scoreCardGmConfigService.listByStatus(propertyId, false)
                    .stream().collect(Collectors.groupingBy(ScoreCardGmConfig::getParentId));
            if (MapUtils.isEmpty(configsByParentId)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the config is not found;propertyId:" + propertyId);
            }

            ScoreCardGm scoreCardGm = new ScoreCardGm();
            scoreCardGm.setPropertyId(propertyId);
            scoreCardGm.setYear(year);
            scoreCardGm.setMonth(month);
            List<ScoreCardGmConfig> parentConfigs = configsByParentId.get(BaseConstant.ZERO);
            scoreCardGm.setConfigJson(JsonUtils.convertObjListToString(objectMapper, parentConfigs.stream().map(parentConfig -> {
                ConfigBasicDTO configBasicDTO = new ConfigBasicDTO();
                configBasicDTO.setId(parentConfig.getId());
                configBasicDTO.setName(parentConfig.getTitle());
                configBasicDTO.setWeight(parentConfig.getWeight());

                configBasicDTO.setItems(configsByParentId.get(parentConfig.getId()).stream().map(item -> new ConfigItemBasicDTO(item.getId(),
                        item.getTitle(), item.getWeight())).collect(Collectors.toList()));
                return configBasicDTO;
            }).collect(Collectors.toList())));
            scoreCardGm.setAssessorId(BaseConstant.ZERO);
            scoreCardGm.setUpdateTime(BaseConstant.LONG_ZERO);
            scoreCardGm.setUpdateId(BaseConstant.ZERO);
            save(scoreCardGm);

            return scoreCardGm;
        }

        /**
         * 计算 Score、Points、Weighed Score
         *
         * @param actualTargetDTO Actual 和 Target
         * @param config          目标指标
         * @return 计算结果
         * @author yyt
         * @date 2023/10/5
         */
        @Override
        public CalculationResultDTO calculationResult(ActualTargetDTO actualTargetDTO, ScoreCardGmConfig config) {
            CalculationResultDTO scoreCardResultDTO = new CalculationResultDTO();

            UpdateItemDTO updateItemDTO = actualTargetDTO.getConfigList().stream()
                    .filter(item -> item.getId().equals(config.getId())).findFirst().orElseGet(UpdateItemDTO::new);
            scoreCardResultDTO.setScore(calculationScore(config, updateItemDTO.getActual(), updateItemDTO.getTarget(),
                    actualTargetDTO.getConfigList()));
            scoreCardResultDTO.setPoints(calculationPoints(config, scoreCardResultDTO.getScore()));
            scoreCardResultDTO.setWeighedScore(calculationWeighedScore(config, scoreCardResultDTO.getPoints()));
            return scoreCardResultDTO;
        }

        /**
         * 计算 Score
         *
         * @param config     指标
         * @param actual     Actual
         * @param target     Target
         * @param configList 指标全量内容
         * @return 计算结果
         * @author yyt
         * @date 2023/10/5
         */
        @Override
        public BigDecimal calculationScore(ScoreCardGmConfig config, BigDecimal actual, BigDecimal target, List<UpdateItemDTO> configList) {
            String scoreSetting = config.getScoreSetting();
            if (StringUtils.isEmpty(scoreSetting)) {
                return null;
            }
            if (Objects.isNull(actual) || Objects.isNull(target) || actual.compareTo(BigDecimal.ZERO) == 0
                    || target.compareTo(BigDecimal.ZERO) == 0) {
                return null;
            }

            InternationalizationNumberDTO number = propertyI18nRedisService.get(config.getPropertyId()).getInternationalization().getNumber();
            int calculatePrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            ScoreCardSettingDTO setting = JsonUtils.convertStringToObject(objectMapper, scoreSetting, ScoreCardSettingDTO.class);
            return switch (setting.getType()) {
                case ScoreCons.ACTUAL_DIVIDE_TARGET ->
                        actual.divide(target, calculatePrecision, roundingMode).multiply(BigDecimal.valueOf(100));
                case ScoreCons.ACTUAL_DIVIDE_TARGET_MINUS_ONE_NEGATIVE ->
                        actual.divide(target, calculatePrecision, roundingMode).subtract(BigDecimal.ONE)
                                .multiply(BigDecimal.valueOf(100)).negate();
                case ScoreCons.ACTUAL_DIVIDE_TARGET_MINUS_ONE ->
                        actual.divide(target, calculatePrecision, roundingMode).subtract(BigDecimal.ONE)
                                .multiply(BigDecimal.valueOf(100));
                case ScoreCons.ACTUAL_MINUS_TARGET_DIVIDE_CUSTOM ->
                    // (Actual-Target)/ 自定义，例：(_id_*30*413)
                        calculationAcrossTargetScore(actual, target, setting.getCalculationFactor(), configList,
                                calculatePrecision, roundingMode);
                default -> null;
            };

        }

        /**
         * 计算跨指标 Score
         *
         * @param actual            Actual
         * @param target            Target
         * @param calculationFactor 计算因子
         * @param configList        指标全量内容
         * @return Score
         * @author yyt
         * @date 2023/10/5
         */
        private BigDecimal calculationAcrossTargetScore(BigDecimal actual, BigDecimal target, String calculationFactor,
                                                        List<UpdateItemDTO> configList, Integer calculatePrecision,
                                                        RoundingMode roundingMode) {
            if (StringUtils.isEmpty(calculationFactor)) {
                return null;
            }

            int start = StringUtils.indexOf(calculationFactor, "_");
            int end = StringUtils.lastIndexOf(calculationFactor, "_");
            int targetId = Integer.parseInt(StringUtils.substring(calculationFactor, start + 1, end));
            ScoreCardGmConfig scoreCardGmConfig = scoreCardGmConfigService.checkExist(targetId);
            UpdateItemDTO updateItemDTO = configList.stream().filter(item -> item.getId().equals(scoreCardGmConfig.getId()))
                    .findFirst().orElseGet(UpdateItemDTO::new);
            if (Objects.isNull(updateItemDTO.getTarget())) {
                return null;
            }

            // (Actual-Target) / 自定义，例：(_id_*30*413)
            return actual.subtract(target).divide(CalculatorUtil.executeExpression(updateItemDTO.getTarget() +
                            StringUtils.substring(calculationFactor, end + 1)), calculatePrecision, roundingMode)
                    .multiply(BigDecimal.valueOf(100));
        }

        /**
         * 计算 Points
         *
         * @param config 指标
         * @param score  Score
         * @return 计算结果
         * @author yyt
         * @date 2023/10/5
         */
        @Override
        public BigDecimal calculationPoints(ScoreCardGmConfig config, BigDecimal score) {
            String pointsSetting = config.getPointsSetting();
            if (StringUtils.isEmpty(pointsSetting)) {
                return null;
            }
            if (Objects.isNull(score)) {
                return null;
            }

            InternationalizationNumberDTO number = propertyI18nRedisService.get(config.getPropertyId()).getInternationalization().getNumber();
            int calculatePrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            ScoreCardSettingDTO setting = JsonUtils.convertStringToObject(objectMapper, pointsSetting, ScoreCardSettingDTO.class);
            return switch (setting.getType()) {
                case PointsCons.SCORE_MULTIPLY_ONE_HUNDRED ->
                        score.divide(BigDecimal.valueOf(100), calculatePrecision, roundingMode).multiply(BigDecimal.valueOf(100));
                case PointsCons.SCORE_MULTIPLY_ONE_HUNDRED_MULTIPLY_THREE ->
                        score.divide(BigDecimal.valueOf(100), calculatePrecision, roundingMode).multiply(BigDecimal.valueOf(3))
                                .multiply(BigDecimal.valueOf(100));
                case PointsCons.SCORE_MINUS_ONE_MULTIPLY_ONE_HUNDRED ->
                        score.divide(BigDecimal.valueOf(100), calculatePrecision, roundingMode).subtract(BigDecimal.ONE)
                                .multiply(BigDecimal.valueOf(100));
                case PointsCons.SCORE_MINUS_ONE_MULTIPLY_THREE_MULTIPLY_ONE_HUNDRED ->
                        score.divide(BigDecimal.valueOf(100), calculatePrecision, roundingMode).subtract(BigDecimal.ONE)
                                .multiply(BigDecimal.valueOf(3)).multiply(BigDecimal.valueOf(100));
                default -> null;
            };

        }

        /**
         * 计算 Weighed Score
         *
         * @param config 指标
         * @param points Points
         * @return 计算结果
         * @author yyt
         * @date 2023/10/5
         */
        @Override
        public BigDecimal calculationWeighedScore(ScoreCardGmConfig config, BigDecimal points) {
            String weighedScoreSetting = config.getWeighedScoreSetting();
            if (StringUtils.isEmpty(weighedScoreSetting)) {
                return null;
            }
            if (Objects.isNull(points)) {
                return null;
            }

            InternationalizationNumberDTO number = propertyI18nRedisService.get(config.getPropertyId()).getInternationalization().getNumber();
            int calculatePrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            ScoreCardSettingDTO setting = JsonUtils.convertStringToObject(objectMapper, weighedScoreSetting, ScoreCardSettingDTO.class);
            if (setting.getType() == WeighedScoreCons.POINTS_MULTIPLY_WEIGHT) {
                return points.multiply(config.getWeight()).divide(BigDecimal.valueOf(100), calculatePrecision, roundingMode);
            }

            return null;
        }

        /**
         * 保存 ScoreCard
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param month      月
         * @param configList 指标内容
         * @param userId     操作人
         * @author yyt
         * @date 2023/10/6
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(Integer propertyId, int year, int month, List<UpdateItemDTO> configList, int userId) {
            ScoreCardGm scoreCardGm = getOne(new QueryWrapper<ScoreCardGm>().lambda().eq(ScoreCardGm::getPropertyId, propertyId)
                    .eq(ScoreCardGm::getYear, year).eq(ScoreCardGm::getMonth, month).last("for update"));
            String configJson = scoreCardGm.getConfigJson();
            List<ConfigBasicDTO> configBasics = JsonUtils.convertStringToList(objectMapper, configJson, ConfigBasicDTO.class);
            // 重置指标修改内容
            resetConfigData(configBasics, configList);
            // 计算
            List<ScoreCardGmConfig> configs = scoreCardGmConfigService.listByStatus(propertyId, null);
            BigDecimal totalScore = calculateScoreCard(configBasics, configs);
            ScoreCardGm updateContent = new ScoreCardGm();
            updateContent.setId(scoreCardGm.getId());
            updateContent.setConfigJson(JsonUtils.convertObjListToString(objectMapper, configBasics));
            updateContent.setTotalScore(totalScore);
            updateContent.setUpdateId(userId);
            updateContent.setUpdateTime(DateTimeUtils.nowTimestamp());
            updateById(updateContent);
        }

        /**
         * 查看 Score Card 详情(月报功能调用)
         *
         * @param propertyId 资产 id
         * @param year       年
         * @param month      月
         * @return ScoreCard 详情
         * @author lichen
         * @date 2024/02/28
         */
        @Override
        public ScoreCardGmDTO getDetail4MonthlyReport(Integer propertyId, int year, int month) {
            ScoreCardGmDTO scoreCardGmDTO = new ScoreCardGmDTO();

            ScoreCardGm scoreCardGm = getOne(new QueryWrapper<ScoreCardGm>().lambda()
                    .eq(ScoreCardGm::getPropertyId, propertyId).eq(ScoreCardGm::getYear, year).eq(ScoreCardGm::getMonth, month));
            if (Objects.isNull(scoreCardGm)) {
                scoreCardGmDTO.setConfigList(Collections.emptyList());
            }else{
                Map<Integer, ScoreCardGmConfig> configById = scoreCardGmConfigService.listByStatus(propertyId, null).stream()
                        .collect(Collectors.toMap(ScoreCardGmConfig::getId, v -> v));

                List<ConfigBasicDTO> configBasicDTOList = JsonUtils.convertStringToList(objectMapper, scoreCardGm.getConfigJson(), ConfigBasicDTO.class);
                scoreCardGmDTO.setConfigList(beanMapper.mapList(configBasicDTOList, ConfigDTO.class, (configBasic, config) -> {
                    ScoreCardGmConfig parentConfig = configById.get(config.getId());
                    config.setName(parentConfig.getTitle());
                    config.setUnit(parentConfig.getUnit());
                    config.getItems().forEach(item -> {
                        ScoreCardGmConfig childConfig = configById.get(item.getId());
                        item.setName(childConfig.getTitle());
                        item.setUnit(childConfig.getUnit());
                    });
                }));
            }

            //设置历史月总分
            scoreCardGmDTO.setHistoricalMonthTotalScore(baseMapper.getHistoricalMonthTotalScore(propertyId, year, month));
            return scoreCardGmDTO;
        }

        /**
         * 重置指标修改内容
         *
         * @param configBasics 指标信息
         * @param configList   指标修改内容
         * @author yyt
         * @date 2023/10/6
         */
        private void resetConfigData(List<ConfigBasicDTO> configBasics, List<UpdateItemDTO> configList) {
            Map<Integer, ConfigItemBasicDTO> configById = configBasics.stream().flatMap(item -> item.getItems().stream())
                    .collect(Collectors.toMap(ConfigItemBasicDTO::getId, v -> v));

            configList.forEach(item -> {
                ConfigItemBasicDTO configItemBasicDTO = configById.get(item.getId());
                configItemBasicDTO.setActual(item.getActual());
                configItemBasicDTO.setTarget(item.getTarget());
            });
        }

        /**
         * 计算 ScoreCard
         *
         * @param configBasics 修改后的指标内容
         * @param configs      指标配置
         * @return total score
         * @author yyt
         * @date 2023/10/6
         */
        private BigDecimal calculateScoreCard(List<ConfigBasicDTO> configBasics, List<ScoreCardGmConfig> configs) {
            /*
             * 1、计算子指标的计算内容（Score、Points、Weight Score）
             * 2、计算 total（子指标 weight score 指标之和）
             * 3、计算 total score（父指标 total * 父指标 weight，然后累计）
             */
            List<UpdateItemDTO> updateItemList = configBasics.stream().flatMap(item -> item.getItems().stream())
                    .map(item -> new UpdateItemDTO(item.getId(), item.getTarget(), item.getActual()))
                    .collect(Collectors.toList());

            InternationalizationNumberDTO number = propertyI18nRedisService.get(configs.get(0).getPropertyId()).getInternationalization().getNumber();
            int calculatePrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            Map<Integer, ScoreCardGmConfig> configById = configs.stream().collect(Collectors.toMap(ScoreCardGmConfig::getId, v -> v));
            BigDecimal totalScore = null;
            for (ConfigBasicDTO configBasic : configBasics) {

                BigDecimal total = null;
                for (ConfigItemBasicDTO item : configBasic.getItems()) {
                    BigDecimal actual = item.getActual();
                    BigDecimal target = item.getTarget();
                    if (Objects.isNull(actual) || Objects.isNull(target)) {
                        // 清空计算结果
                        if (Objects.nonNull(item.getScore())) {
                            item.setScore(null);
                        }
                        if (Objects.nonNull(item.getPoints())) {
                            item.setPoints(null);
                        }
                        if (Objects.nonNull(item.getWeighedScore())) {
                            item.setWeighedScore(null);
                        }
                        continue;
                    }

                    ScoreCardGmConfig config = configById.get(item.getId());
                    item.setScore(calculationScore(config, actual, target, updateItemList));
                    item.setPoints(calculationPoints(config, item.getScore()));
                    item.setWeighedScore(calculationWeighedScore(config, item.getPoints()));
                    if (Objects.nonNull(item.getWeighedScore())) {
                        if (Objects.isNull(total)) {
                            total = BigDecimal.ZERO;
                        }
                        total = total.add(item.getWeighedScore());
                    }
                }

                if (Objects.nonNull(total)) {
                    if (Objects.isNull(totalScore)) {
                        totalScore = BigDecimal.ZERO;
                    }
                    totalScore = totalScore.add(total.multiply(configBasic.getWeight()).divide(BigDecimal.valueOf(100),
                            calculatePrecision, roundingMode));
                }
            }
            return totalScore;
        }

        /**
         * Total Score 列表
         *
         * @param propertyId 资产 id
         * @param year       年
         * @return Total Score 列表
         * @author yyt
         * @date 2023/10/6
         */
        @Override
        public List<TotalScoreDTO> listTotalScore(int propertyId, Integer year) {
            Map<Integer, ScoreCardGm> scoreCardGmByMonth = list(new QueryWrapper<ScoreCardGm>().lambda()
                    .eq(ScoreCardGm::getPropertyId, propertyId).eq(ScoreCardGm::getYear, year))
                    .stream().collect(Collectors.toMap(ScoreCardGm::getMonth, v -> v));
            List<ScoreCardGm> totalScoreNotNullScoredCards = scoreCardGmByMonth.values()
                    .stream().filter(item -> Objects.nonNull(item.getTotalScore())).toList();

            List<TotalScoreDTO> totalScoreDTOList = Lists.newArrayListWithExpectedSize(12);
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            YearMonth currentMonth = YearMonth.now(ZoneId.of(timezone));
            YearMonth startMonth = YearMonth.of(year, 1);
            // 查看历史年，返回历史年全年数据，查看当前年，只能看当前月+历史月的数据
            YearMonth endMonth = YearMonth.of(year, year < currentMonth.getYear() ? 12 : currentMonth.getMonthValue());
            for (YearMonth yearMonth = endMonth; !yearMonth.isBefore(startMonth); yearMonth = yearMonth.minusMonths(1)) {
                int month = yearMonth.getMonthValue();

                ScoreCardGm currentMonthScoreCard = scoreCardGmByMonth.get(month);
                BigDecimal ytdTotalScore = null;
                for (ScoreCardGm scoreCard : totalScoreNotNullScoredCards) {
                    if (scoreCard.getMonth() <= month) {
                        if (Objects.isNull(ytdTotalScore)) {
                            ytdTotalScore = BigDecimal.ZERO;
                        }
                        ytdTotalScore = ytdTotalScore.add(scoreCard.getTotalScore());
                    }
                }

                BigDecimal weightedTotalScore = Objects.isNull(currentMonthScoreCard) ? null : currentMonthScoreCard.getTotalScore();
                totalScoreDTOList.add(new TotalScoreDTO(month, weightedTotalScore, Objects.isNull(weightedTotalScore) ? null : ytdTotalScore));
            }
            return totalScoreDTOList;
        }
    }