# SalesKpiTask.java

    package cloud.wconnect.workperformance.businesstask.saleskpi;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionIdConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.query.OpenFunctionPropertyQuery;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiFirstDayLog;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiMonthly;
    import cloud.wconnect.workperformance.entity.saleskpi.SalesKpiWeekly;
    import cloud.wconnect.workperformance.service.saleskpi.ISalesKpiFirstDayLogService;
    import cloud.wconnect.workperformance.service.saleskpi.ISalesKpiMonthlyService;
    import cloud.wconnect.workperformance.service.saleskpi.ISalesKpiWeeklyService;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections4.CollectionUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    import jakarta.annotation.Resource;
    import java.text.MessageFormat;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.time.temporal.ChronoField;
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * 定时任务：Sales Kpi Monthly Task
     *
     * @author zc
     * @date 2023/09/08
     */
    @Slf4j
    @Component
    public class SalesKpiTask {
        @Resource
        private ISalesKpiMonthlyService salesKpiMonthlyService;
        @Resource
        private ISalesKpiWeeklyService salesKpiWeeklyService;
        @Resource
        private ISalesKpiFirstDayLogService salesKpiFirstDayLogService;
        @Resource
        private PropertyInternalService propertyInternalService;
        @Resource
        private MailInternalService mailInternalService;
        @Resource
        private RedisTemplate<String, Object> jsonRedisTemplate;

        private static final String PREFIX_KEY_MONTHLY = "wconnect:task:init-sales-kpi-monthly:";

        private static final String PREFIX_KEY_WEEKLY = "wconnect:task:init-sales-kpi-weekly:";

        /**
         * 默认时区
         */
        @Value("${default.timezone}")
        private String defaultTimezone;

        /**
         * 默认周起始日
         */
        @Value("${default.first-day.sales-kpi}")
        private Integer defaultFirstDayOfWeek;

        /**
         * 月初初始化 Monthly 数据
         *
         * <p>月初 01:10:00 检查上月数据是否存在，若不存在则插入上月初始数据，否则不插入</p>
         *
         * @author zc
         * @date 2023/9/8
         */
        @Scheduled(cron = "${schedule.cron.sales-kpi.init-monthly}", zone = "${default.timezone}")
        public void initMonthlyData() {
            log.info("SalesKpiTask.initMonthlyData start...");

            // 防止并发操作
            String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYMMDD));
            if (!preventConcurrency(PREFIX_KEY_MONTHLY + currentDate)) {
                log.warn("SalesKpiTask.initMonthlyData end... a machine has been executed.");
                return;
            }

            // 过滤出有效的资产
            List<PropertyDTO> openPropertyList = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                            .openFunctionIds(Lists.newArrayList(FunctionIdConstant.SALES_KPI)).build()).stream()
                    .filter(PropertyDTO::getStatus).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(openPropertyList)) {
                log.info("SalesKpiTask.initMonthlyData end... openPropertyList is empty");
                return;
            }

            List<Integer> propertyIds = openPropertyList.stream().map(PropertyDTO::getId).toList();

            // 获取上月的月份（yyyyMM）
            long lastMonthly = Long.parseLong(DateTimeUtils.dateTimeNow(defaultTimezone).minusMonths(1)
                    .format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMM)));

            // 获取已存在指定月份 Monthly 数据的资产 id
            Set<Integer> existMonthlyDataPropertyIds = salesKpiMonthlyService.listByMonthly(null,
                    null, lastMonthly).stream().map(SalesKpiMonthly::getPropertyId).collect(Collectors.toSet());

            try {
                // 生成初始化数据集合
                List<SalesKpiMonthly> initSalesKpiMonthlyList = propertyIds.stream()
                        .filter(item -> !existMonthlyDataPropertyIds.contains(item))
                        .map(item ->
                                salesKpiMonthlyService.generateInitData(item, lastMonthly))
                        .flatMap(Collection::stream).toList();

                // 保存数据
                salesKpiMonthlyService.saveBatch(initSalesKpiMonthlyList);
            } catch (Exception e) {
                String mailContent = MessageFormat.format("Business:{0}<br/>Content:{1}", "init-sales-kpi-monthly",
                        "There is something wrong when auto insert init data for Sales Kpi Monthly.Exception: " + e.getMessage());
                mailInternalService.sendAlertEmail("[Warning] - Insert Init Data For Sales Kpi Monthly", mailContent);
            }

            log.info("SalesKpiTask.initMonthlyData end...");
        }

        /**
         * 初始化 Weekly 数据
         *
         * <p>每天 01:15:00 检查上周数据是否存在，若不存在则插入上周初始数据，否则不插入</p>
         *
         * @author zc
         * @date 2023/9/8
         */
        @Scheduled(cron = "${schedule.cron.sales-kpi.init-weekly}", zone = "${default.timezone}")
        public void initWeeklyData() {
            log.info("SalesKpiTask.initWeeklyData start...");

            // 防止并发操作
            String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYMMDD));
            if (!preventConcurrency(PREFIX_KEY_WEEKLY + currentDate)) {
                return;
            }

            // 过滤出有效的资产
            List<PropertyDTO> openPropertyList = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                            .openFunctionIds(Lists.newArrayList(FunctionIdConstant.SALES_KPI)).build()).stream()
                    .filter(PropertyDTO::getStatus).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(openPropertyList)) {
                return;
            }

            List<Integer> propertyIds = openPropertyList.stream().map(PropertyDTO::getId).toList();

            // 获取当前时间是周几
            int dayOfWeek = DateTimeUtils.dateNow(defaultTimezone).getDayOfWeek().getValue();

            long lastWeekly = Long.parseLong(DateTimeUtils.dateTimeNow(defaultTimezone).minusWeeks(1)
                    .with(ChronoField.DAY_OF_WEEK, dayOfWeek)
                    .format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD)));

            // 获取已存在指定周 Weekly 数据的资产 id
            Set<Integer> existWeeklyDataPropertyIds = salesKpiWeeklyService.listByWeekly(null,
                    null, lastWeekly).stream().map(SalesKpiWeekly::getPropertyId).collect(Collectors.toSet());

            // 根据周几获取周起始日数据
            Map<Integer, SalesKpiFirstDayLog> propertyId2FirstDay = salesKpiFirstDayLogService.getPropertyId2FirstDay().stream()
                    .collect(Collectors.toMap(SalesKpiFirstDayLog::getPropertyId, Function.identity()));

            try {
                // 生成初始化数据集合
                List<SalesKpiWeekly> needAddSalesKpiWeeklyList = Lists.newArrayList();
                propertyIds.stream().filter(item -> !existWeeklyDataPropertyIds.contains(item))
                        .forEach(propertyId -> {
                            SalesKpiFirstDayLog salesKpiFirstDayLog = propertyId2FirstDay.get(propertyId);
                            // 判断资产最新配置的周起始日是否与当前时间的周几一致，若一致则初始化数据
                            int firstDayOfWeek = Objects.nonNull(salesKpiFirstDayLog) ? salesKpiFirstDayLog.getFirstDay() : defaultFirstDayOfWeek;
                            if (firstDayOfWeek == dayOfWeek) {
                                needAddSalesKpiWeeklyList.addAll(salesKpiWeeklyService.generateInitData(propertyId, lastWeekly));
                            }
                        });

                // 保存数据
                salesKpiWeeklyService.saveBatch(needAddSalesKpiWeeklyList);
            } catch (Exception e) {
                String mailContent = MessageFormat.format("Business:{0}<br/>Content:{1}", "init-sales-kpi-weekly",
                        "There is something wrong when auto insert init data for Sales Kpi Weekly.Exception: " + e.getMessage());
                mailInternalService.sendAlertEmail("[Warning] - Insert Init Data For Sales Kpi Weekly", mailContent);
            }

            log.info("SalesKpiTask.initWeeklyData end...");
        }

        /**
         * 校验是否并发执行
         *
         * @param key 缓存前缀
         * @return true:是；false:否
         * @author zc
         * @date 2023/9/8
         */
        private Boolean preventConcurrency(String key) {
            return jsonRedisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }
    }