# SalesKpiConfigServiceImpl.java

    package cloud.wconnect.workperformance.service.saleskpi.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.exception.ForbiddenException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.UserDTO;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.saleskpi.SalesKpiI18nConstant;
    import cloud.wconnect.workperformance.cons.saleskpi.SalesKpiMonthlyDataSourceCons;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiConfig;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiMonthly;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiMonthlyConfig;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiWeekly;
    import cloud.wconnect.workperformance.mapper.saleskpi.SalesKpiConfigMapper;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.config.SalesConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.config.SalesConfigMonthlyTargetDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.config.SalesConfigUpdateTargetDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.config.basic.SalesConfigMonthlyTargetBasicDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.monthly.basic.MonthlyConfigItemConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.monthly.basic.MonthlyConfigItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.saleskpi.monthly.basic.MonthlyConfigTargetSettingDTO;
    import cloud.wconnect.workperformance.pojo.query.saleskpi.SalesConfigQuery;
    import cloud.wconnect.workperformance.service.saleskpi.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections4.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.text.MessageFormat;
    import java.time.ZonedDateTime;
    import java.time.temporal.ChronoField;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    /**
     * <p>
     * salesman  kpi config 服务实现类
     * </p>
     *
     * @author zc
     * @since 2023-09-05
     */
    @Service
    @DS("work_performance")
    public class SalesKpiConfigServiceImpl extends ServiceImpl<SalesKpiConfigMapper, SalesKpiConfig> implements ISalesKpiConfigService {
        @Resource
        private ISalesKpiMonthlyConfigService salesKpiMonthlyConfigService;
        @Resource
        private ISalesKpiMonthlyService salesKpiMonthlyService;
        @Resource
        private ISalesKpiWeeklyService salesKpiWeeklyService;
        @Resource
        private ISalesKpiFirstDayLogService salesKpiFirstDayLogService;
        @Resource
        private UserInternalService userInternalService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private I18nMessageUtils i18nMessageUtils;

        /**
         * 获取配置列表
         *
         * @param propertyDTO      资产
         * @param query 查询条件
         * @return SalesConfigVO 集合
         * @author zc
         * @date 2023/9/5
         */
        @Override
        public ListPagedDTO<SalesConfigDTO> list(PropertyDTO propertyDTO, SalesConfigQuery query) {
            LambdaQueryWrapper<SalesKpiConfig> queryWrapper = new QueryWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyDTO.getId())
                    .eq(Objects.nonNull(query.getSalesmanId()),
                            SalesKpiConfig::getSalesmanId, query.getSalesmanId());

            long total;
            List<SalesKpiConfig> salesKpiConfigList;
            if (Objects.nonNull(query.getPage()) && Objects.nonNull(query.getSize())) {
                queryWrapper = queryWrapper.like(StringUtils.isNotBlank(query.getSalesAccount()),
                        SalesKpiConfig::getSalesAccount, query.getSalesAccount());
                Page<SalesKpiConfig> page = page(new Page<>(query.getPage(), query.getSize()), queryWrapper);
                total = page.getTotal();
                salesKpiConfigList = page.getRecords();
            } else {
                queryWrapper = queryWrapper.apply(StringUtils.isNotBlank(query.getSalesAccount()),
                        "FIND_IN_SET({0}, sales_account)", query.getSalesAccount());
                salesKpiConfigList = list(queryWrapper);
                total = salesKpiConfigList.size();
            }

            if (CollectionUtils.isEmpty(salesKpiConfigList)) {
                return new ListPagedDTO<>();
            }

            // 获取指标名称数据
            Map<Integer, SalesKpiMonthlyConfig> monthlyConfigId2Config = salesKpiMonthlyConfigService
                    .getMonthlyConfigs(propertyDTO.getId(), null, null).stream()
                    .collect(Collectors.toMap(SalesKpiMonthlyConfig::getId, Function.identity()));

            // 提取业务员名称
            Set<Integer> userIds = salesKpiConfigList.stream().map(SalesKpiConfig::getSalesmanId)
                    .collect(Collectors.toSet());
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);

            List<Integer> salesIds = salesKpiConfigList.stream().map(SalesKpiConfig::getId).toList();

            ZonedDateTime currentDate = DateTimeUtils.dateNow(propertyDTO.getTimezone());
            // 获取上个月销售数据
            ZonedDateTime lastMonthZonedDate = currentDate.minusMonths(1).withDayOfMonth(1);
            Map<Integer, SalesKpiMonthly> salesId2KpiMonthly = salesKpiMonthlyService.listByMonthly(propertyDTO.getId(),
                            salesIds, DateTimeUtils.parseToLong(lastMonthZonedDate, BaseConstant.DATE_FORMAT_YYYYMM)).stream()
                    .collect(Collectors.toMap(SalesKpiMonthly::getSalesId, Function.identity()));

            // 获取上周的销售数据
            int firstDayOfWeek = salesKpiFirstDayLogService.getFirstDay(propertyDTO.getId());
            ZonedDateTime lastWeekZonedDate = (currentDate.getDayOfWeek().getValue() >= firstDayOfWeek) ?
                    currentDate.minusWeeks(1).with(ChronoField.DAY_OF_WEEK, firstDayOfWeek) :
                    currentDate.minusWeeks(2).with(ChronoField.DAY_OF_WEEK, firstDayOfWeek);
            Map<Integer, SalesKpiWeekly> salesId2KpiWeekly = salesKpiWeeklyService.listByWeekly(propertyDTO.getId(),
                            salesIds, DateTimeUtils.parseToLong(lastWeekZonedDate, BaseConstant.DATE_FORMAT_YYYYMMDD)).stream()
                    .collect(Collectors.toMap(SalesKpiWeekly::getSalesId, Function.identity()));

            List<SalesConfigDTO> result = salesKpiConfigList.stream().map(item ->
                    SalesConfigDTO.builder()
                            .id(item.getId())
                            .salesmanId(item.getSalesmanId())
                            .salesmanName(userId2Name.get(item.getSalesmanId()))
                            .salesAccount(Stream.of(StringUtils.split(item.getSalesAccount(), ",")).toList())
                            .marketSegments(Stream.of(StringUtils.split(item.getMarketSegments(), ","))
                                    .map(segments -> {
                                        SalesKpiMonthlyConfig salesKpiMonthlyConfig = monthlyConfigId2Config.get(Integer.parseInt(segments));
                                        return MonthlyConfigItemDTO.builder()
                                                .itemId(salesKpiMonthlyConfig.getId())
                                                .title(salesKpiMonthlyConfig.getTitle())
                                                .build();
                                    }).toList())
                            .hiddenItemIds(JsonUtils.convertStringToList(objectMapper, item.getHiddenItem(), Integer.class))
                            .monthlyTargets(JsonUtils.convertStringToList(objectMapper, item.getMonthlyTarget(),
                                    SalesConfigMonthlyTargetDTO.class).stream().peek(monthlyItem -> {
                                SalesKpiMonthlyConfig salesKpiMonthlyConfig = monthlyConfigId2Config.get(monthlyItem.getItemId());
                                monthlyItem.setTitle(salesKpiMonthlyConfig.getTitle());
                            }).toList())
                            .weeklyTarget(item.getWeeklyTarget())
                            .status(item.getStatus())
                            .monthlyEffectiveStartTime(Objects.isNull(salesId2KpiMonthly.get(item.getId())) ?
                                    lastMonthZonedDate.plusMonths(1).toEpochSecond() : lastMonthZonedDate.toEpochSecond())
                            .weeklyEffectiveStartTime(Objects.isNull(salesId2KpiWeekly.get(item.getId())) ?
                                    lastWeekZonedDate.plusWeeks(1).toEpochSecond() : lastWeekZonedDate.toEpochSecond())
                            .build()
            ).collect(Collectors.toList());

            return new ListPagedDTO<>(total, result);
        }

        /**
         * 校验业务员是否有效
         *
         * @param tenantId   租户 id
         * @param propertyId 资产 id
         * @param salesmanId 业务员 id
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public void checkSalesmanIdEffective(int tenantId, int propertyId, int salesmanId) {
            UserDTO userDTO = userInternalService.getOneByProperty(tenantId, propertyId, salesmanId);
            if (Objects.isNull(userDTO)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The user is not found by salesmanId: " + salesmanId);
            }
        }

        /**
         * 校验业务员配置数据是否重复
         *
         * @param propertyId 资产 id
         * @param salesmanId 业务员 id
         * @param id         配置数据 id
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public void checkSalesmanAlreadyExist(int propertyId, int salesmanId, @Nullable Integer id) {
            long count = count(new QueryWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyId)
                    .eq(SalesKpiConfig::getSalesmanId, salesmanId)
                    .ne(Objects.nonNull(id), SalesKpiConfig::getId, id));
            if (count > BaseConstant.LONG_ZERO) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_ALREADY_EXIST,
                        i18nMessageUtils.getErrorParamMapNeedTranslate(SalesKpiI18nConstant.SALESMAN_CONFIG),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The salesman config already exists; salesmanId:" + salesmanId);
            }
        }

        /**
         * 校验业务员账号是否重复
         *
         * @param propertyId   资产 id
         * @param salesAccount 业务员账号
         * @param id           配置数据 id
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public void checkSalesAccountAlreadyExist(int propertyId, List<String> salesAccount, @Nullable Integer id) {
            long count = count(new QueryWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyId)
                    .ne(Objects.nonNull(id), SalesKpiConfig::getId, id)
                    .apply(String.format("CONCAT( ',', sales_account, ',' ) REGEXP ',(%s),'",
                            StringUtils.join(salesAccount, "|"))));
            if (count > BaseConstant.LONG_ZERO) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_ALREADY_EXIST,
                        i18nMessageUtils.getErrorParamMapNeedTranslate(SalesKpiI18nConstant.SALESMAN_ACCOUNT),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The salesAccount already exists; salesAccount:" + salesAccount);
            }
        }

        /**
         * 校验数据是否存在
         *
         * @param id 配置数据 id
         * @return SalesKpiConfig
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public SalesKpiConfig checkExist(int id) {
            SalesKpiConfig salesKpiConfig = getById(id);
            if (Objects.isNull(salesKpiConfig)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The salesKpiConfig not found by id: " + id);
            }
            return salesKpiConfig;
        }

        /**
         * 校验配置数据归属
         *
         * @param propertyId   资产 id
         * @param originConfig 原配置数据
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public void checkBelong(int propertyId, SalesKpiConfig originConfig) {
            if (!originConfig.getPropertyId().equals(propertyId)) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("The originConfig does not belong to this property; propertyId:{0}; originConfigId:{1}",
                                propertyId, originConfig.getId()));
            }
        }

        /**
         * 新增配置数据
         *
         * @param propertyId     资产 id
         * @param operatorId     操作人 id
         * @param salesConfigDTO 配置数据
         * @return id
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public int add(int propertyId, int operatorId, SalesConfigDTO salesConfigDTO) {
            // 保存配置数据
            SalesKpiConfig salesKpiConfig = SalesKpiConfig.builder()
                    .propertyId(propertyId)
                    .salesmanId(salesConfigDTO.getSalesmanId())
                    .salesAccount(String.join(",", salesConfigDTO.getSalesAccount()))
                    .marketSegments(StringUtils.EMPTY)
                    .monthlyTarget(JsonUtils.convertObjListToString(objectMapper,
                            beanMapper.mapList(salesConfigDTO.getMonthlyTargets(), SalesConfigMonthlyTargetBasicDTO.class)))
                    .weeklyTarget(salesConfigDTO.getWeeklyTarget())
                    .hiddenItem(JsonUtils.convertObjListToString(objectMapper, salesConfigDTO.getHiddenItemIds()))
                    .status(Boolean.TRUE)
                    .updateBy(operatorId)
                    .updateTime(DateTimeUtils.nowTimestamp())
                    .build();
            save(salesKpiConfig);
            return salesKpiConfig.getId();
        }

        /**
         * 编辑配置数据
         *
         * @param id             配置数据 id
         * @param propertyId     资产 id
         * @param operatorId     操作人 id
         * @param salesConfigDTO 配置数据
         * @author zc
         * @date 2023/9/6
         */
        @Override
        public void update(int id, int propertyId, int operatorId, SalesConfigDTO salesConfigDTO) {
            // 编辑配置数据
            updateById(SalesKpiConfig.builder()
                    .id(id)
                    .salesmanId(salesConfigDTO.getSalesmanId())
                    .salesAccount(String.join(",", salesConfigDTO.getSalesAccount()))
                    .monthlyTarget(JsonUtils.convertObjListToString(objectMapper,
                            beanMapper.mapList(salesConfigDTO.getMonthlyTargets(), SalesConfigMonthlyTargetBasicDTO.class)))
                    .weeklyTarget(salesConfigDTO.getWeeklyTarget())
                    .hiddenItem(JsonUtils.convertObjListToString(objectMapper, salesConfigDTO.getHiddenItemIds()))
                    .updateBy(operatorId)
                    .updateTime(DateTimeUtils.nowTimestamp())
                    .build());
        }

        /**
         * 保存 segments
         *
         * @param propertyId   资产 id
         * @param originConfig 原销售配置数据
         * @param segment      segment 指标
         * @param operatorId   操作人 id
         * @author zc
         * @date 2023/9/6
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveSegments(int propertyId, SalesKpiConfig originConfig, List<MonthlyConfigItemDTO> segment, int operatorId) {
            // 处理 segment
            List<Integer> marketSegmentIds = handleSegment(propertyId, segment);

            // 获取基本指标
            List<Integer> generalItemIds = salesKpiMonthlyConfigService.getMonthlyConfigs(propertyId,
                            SalesKpiMonthlyConfig.TYPE_GENERAL, null).stream()
                    .map(SalesKpiMonthlyConfig::getId).toList();

            // 移除 monthlyTargets 中不再使用的 segment 指标
            List<SalesConfigMonthlyTargetDTO> salesConfigMonthlyTargets = JsonUtils.convertStringToList(objectMapper,
                            originConfig.getMonthlyTarget(), SalesConfigMonthlyTargetDTO.class).stream()
                    .filter(item -> marketSegmentIds.contains(item.getItemId())
                            || generalItemIds.contains(item.getItemId())).toList();

            // 更新关联的 segment
            updateById(SalesKpiConfig.builder()
                    .id(originConfig.getId())
                    .marketSegments(marketSegmentIds.stream().map(Objects::toString)
                            .collect(Collectors.joining(",")))
                    .monthlyTarget(JsonUtils.convertObjListToString(objectMapper, salesConfigMonthlyTargets))
                    .updateBy(operatorId)
                    .updateTime(DateTimeUtils.nowTimestamp())
                    .build());
        }

        /**
         * 处理 segment
         *
         * @param propertyId     资产 id
         * @param marketSegments segment
         * @return item id
         * @author zc
         * @date 2023/9/20
         */
        private List<Integer> handleSegment(int propertyId, List<MonthlyConfigItemDTO> marketSegments) {
            if (CollectionUtils.isEmpty(marketSegments)) {
                return Collections.emptyList();
            }

            // 获取 segment 父级配置
            SalesKpiMonthlyConfig parentMonthlyConfigForSegment = salesKpiMonthlyConfigService.getByDataSource(propertyId,
                    SalesKpiMonthlyDataSourceCons.DATA_SOURCE_BUDGET_ACHIEVEMENT);

            // 获取已存在的 segment 配置数据
            Map<String, Integer> title2MonthlyConfigId = salesKpiMonthlyConfigService.getSegmentConfigForUpdate(propertyId).stream()
                    .collect(Collectors.toMap(SalesKpiMonthlyConfig::getTitle, SalesKpiMonthlyConfig::getId));

            List<Integer> result = Lists.newArrayList();

            List<SalesKpiMonthlyConfig> needAddMonthlyConfigList = Lists.newArrayList();
            List<SalesKpiMonthlyConfig> needUpdateMonthlyConfigList = Lists.newArrayList();
            for (MonthlyConfigItemDTO segment : marketSegments) {
                Integer segmentId = title2MonthlyConfigId.get(segment.getTitle());
                if (Objects.isNull(segmentId)) {
                    if (Objects.isNull(segment.getItemId())) {
                        if (Objects.isNull(parentMonthlyConfigForSegment)) {
                            continue;
                        }
                        needAddMonthlyConfigList.add(SalesKpiMonthlyConfig.builder()
                                .propertyId(propertyId)
                                .parentId(parentMonthlyConfigForSegment.getId())
                                .title(segment.getTitle())
                                .description(StringUtils.EMPTY)
                                .dataSource(StringUtils.EMPTY)
                                .unit(SalesKpiMonthlyConfig.UNIT_DOLLAR)
                                .sort(BaseConstant.ZERO)
                                .targetSetting(JsonUtils.convertObjToString(objectMapper,
                                        new MonthlyConfigTargetSettingDTO(StringUtils.EMPTY)))
                                .itemConfig(JsonUtils.convertObjToString(objectMapper,
                                        new MonthlyConfigItemConfigDTO(StringUtils.EMPTY)))
                                .type(SalesKpiMonthlyConfig.TYPE_SEGMENT)
                                .status(Boolean.FALSE).build());
                    } else {
                        needUpdateMonthlyConfigList.add(SalesKpiMonthlyConfig.builder()
                                .id(segment.getItemId())
                                .title(segment.getTitle())
                                .build());
                    }
                } else {
                    result.add(segmentId);
                }
            }

            // 校验更新的 segment
            if (CollectionUtils.isNotEmpty(needUpdateMonthlyConfigList)) {
                List<Integer> segmentIds = needUpdateMonthlyConfigList.stream().map(SalesKpiMonthlyConfig::getId).toList();
                salesKpiMonthlyConfigService.checkMonthlyItemEffective(propertyId, segmentIds,
                        SalesKpiMonthlyConfig.TYPE_SEGMENT);
                // 更新 segment
                salesKpiMonthlyConfigService.updateBatchById(needUpdateMonthlyConfigList);

                // 添加更新的 segment id
                result.addAll(segmentIds);
            }

            // 保存 segment
            salesKpiMonthlyConfigService.saveBatch(needAddMonthlyConfigList);

            // 添加新增的 segment id
            result.addAll(needAddMonthlyConfigList.stream().map(SalesKpiMonthlyConfig::getId).toList());

            return result;
        }

        /**
         * 批量修改配置指标
         *
         * @param propertyId      资产 id
         * @param updateTargetDTO 指标数据
         * @author zc
         * @date 2023/9/7
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateWeeklyTargetBatch(int propertyId, SalesConfigUpdateTargetDTO updateTargetDTO) {
            update(new UpdateWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyId)
                    .set(SalesKpiConfig::getWeeklyTarget, updateTargetDTO.getValue()));
        }

        /**
         * 获取 Sales 配置
         *
         * @param propertyId 资产 id
         * @param status     状态（false：off，true：on）
         * @return SalesKpiConfig 集合
         * @author zc
         * @date 2023/9/7
         */
        @Override
        public List<SalesKpiConfig> listConfigs(int propertyId, @Nullable Boolean status) {
            return list(new QueryWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyId)
                    .eq(Objects.nonNull(status), SalesKpiConfig::getStatus, status));
        }

        /**
         * 根据用户 id 获取 Sales config 数据
         *
         * @param propertyId 资产 id
         * @param salesmanId 用户 id
         * @return SalesKpiConfig
         * @author zc
         * @date 2023/10/12
         */
        @Override
        public SalesKpiConfig getBySalesmanId(int propertyId, int salesmanId) {
            return getOne(new QueryWrapper<SalesKpiConfig>().lambda()
                    .eq(SalesKpiConfig::getPropertyId, propertyId)
                    .eq(SalesKpiConfig::getSalesmanId, salesmanId));
        }
    }