
package com.tmerclub.cloud.flow.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.tmerclub.cloud.api.coupon.dto.ReceiveCouponDTO;
import com.tmerclub.cloud.api.coupon.feign.CouponFeignClient;
import com.tmerclub.cloud.api.delivery.feign.AreaFeignClient;
import com.tmerclub.cloud.api.order.dto.CustomerRetainedDTO;
import com.tmerclub.cloud.api.order.feign.OrderFeignClient;
import com.tmerclub.cloud.api.order.vo.CustomerRetainVO;
import com.tmerclub.cloud.api.user.dto.CustomerReqDTO;
import com.tmerclub.cloud.api.user.dto.MemberReqDTO;
import com.tmerclub.cloud.api.user.feign.UserFeignClient;
import com.tmerclub.cloud.api.user.vo.MemberContributeValueVO;
import com.tmerclub.cloud.api.user.vo.MemberCountVO;
import com.tmerclub.cloud.api.user.vo.MemberTrendRespVO;
import com.tmerclub.cloud.api.user.vo.UserApiVO;
import com.tmerclub.cloud.common.constant.Constant;
import com.tmerclub.cloud.common.exception.LuckException;
import com.tmerclub.cloud.common.order.vo.AreaVO;
import com.tmerclub.cloud.common.response.ServerResponseEntity;
import com.tmerclub.cloud.flow.bo.*;
import com.tmerclub.cloud.flow.constant.FlowDateTypeEnum;
import com.tmerclub.cloud.flow.service.UserAnalyseMongoService;
import com.tmerclub.cloud.flow.util.FlowUtil;
import com.tmerclub.cloud.flow.util.IpSearchUtil;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;

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

/**
 * 流量分析—用户访问数据
 *
 * @author YXF
 * @date 2021-05-21 15:25:19
 */
@Service
public class UserAnalyseMongoServiceImpl implements UserAnalyseMongoService {

    private static final Logger LOG = LoggerFactory.getLogger(UserAnalyseMongoServiceImpl.class);

    @Autowired
    private MongoTemplate mongoTemplate;
    @DubboReference
    private UserFeignClient userFeignClient;
    @DubboReference
    private CouponFeignClient couponFeignClient;
    @DubboReference
    private AreaFeignClient areaFeignClient;
    @DubboReference
    private OrderFeignClient orderFeignClient;


    @Override
    public void statisticsAndSaveUserOfDay(Date beginDate, Date endDate) {
        // 获取 步骤数、浏览量、IP、是否访客
        List<FlowUserOperationBO> userOperationList = this.listUserAnalyseLogBO(beginDate, endDate);
        if (CollUtil.isEmpty(userOperationList)) {
            return;
        }

        List<Long> userIds = userOperationList.stream()
                .filter(flowUserOperationBO -> Objects.equals(flowUserOperationBO.getMembership(), 1))
                .map(FlowUserOperationBO::getUserId)
                .collect(Collectors.toList());

        if (CollUtil.isNotEmpty(userIds)) {
            // 是否新老会员、是否付费会员、会员领券数量
            this.loadUserMemberInfo(userIds, userOperationList, beginDate, endDate);

            // 是否下单、是否支付、下单数量、支付数量、下单金额、支付金额
            this.statisticsUserOrderLog(userIds, userOperationList, beginDate, endDate);
        }

        String date = DateUtil.format(beginDate, "yyMMdd");
        Date currentTime = new Date();

        int index = 1;

        for (FlowUserOperationBO flowUserOperationBO : userOperationList) {
            flowUserOperationBO.setCreateTime(beginDate);
            flowUserOperationBO.setUpdateTime(currentTime);
            if (Objects.isNull(flowUserOperationBO.getMembership())) {
                flowUserOperationBO.setMembership(0);
            }
            flowUserOperationBO.setId(FlowUtil.getId(date + flowUserOperationBO.getMembership(), index++));
        }

        // 批量保存
        this.batchInsertUserOperationBO(beginDate, endDate, userOperationList);
    }

    @Override
    public void statisticsAndSaveUserFootprintOfDay(int dateType, Date beginDate, Date endDate) {
        // flow_user_operation表中的数据
        List<FlowUserFootprintBO> flowUserFootprintList = this.statisticsUserFootprintOfTime(dateType, beginDate, endDate, new ArrayList<>());

        this.batchInsertFlowUserFootprintBO(dateType, beginDate, endDate, flowUserFootprintList);
    }

    @Override
    public void statisticsAndSaveUserFootprintOfTime(int dateType, Date beginDate, Date endDate) {
        // flow_user_operation表中的数据
        List<FlowUserFootprintBO> flowUserFootprintList = this.listUserFootprintOfTime(dateType, beginDate, endDate);

        this.batchInsertFlowUserFootprintBO(dateType, beginDate, endDate, flowUserFootprintList);
    }

    @Override
    public List<FlowUserFootprintBO> listUserFootprintOfTime(int dateType, Date beginDate, Date endDate) {
        List<AggregationOperation> aggregationOperationList = new ArrayList<>();

        aggregationOperationList.add(Aggregation.group("userId")
                .sum("step").as("step")
                .first("userId").as("userId"));

        List<FlowUserFootprintBO> flowUserFootprintList = statisticsUserFootprintOfTime(dateType, beginDate, endDate, aggregationOperationList);
        return flowUserFootprintList;
    }

    @Override
    public List<FlowUserFootprintBO> statisticsUserFootprintOfTime(int dateType, Date beginDate, Date endDate, List<AggregationOperation> aggregationOperationList) {
        // flow_user_operation表中的数据

        Long beginId = FlowUtil.timeToId(beginDate);
        Long endId = FlowUtil.timeToId(endDate);
        aggregationOperationList.add(0, Aggregation.match(Criteria.where("_id").gte(beginId).lt(endId)));

        aggregationOperationList.add(Aggregation.project()
                .and(ConditionalOperators.when(Criteria.where("step").gt(20))
                        .then(8).otherwise(
                                ConditionalOperators.when(Criteria.where("step").gt(10)).then(7).otherwise(
                                        ConditionalOperators.when(Criteria.where("step").gt(5)).then(6).otherwise("$step")
                                )
                        )
                ).as("step"));
        aggregationOperationList.add(Aggregation.group("step")
                .count().as("userNums")
                .first("step").as("visitId"));



        TypedAggregation<FlowUserFootprintBO> typedAggregation = Aggregation.newAggregation(
                FlowUserFootprintBO.class,
                aggregationOperationList
        );
        LOG.info("statisticsUserFootprintOfTime:aggregation:{}", typedAggregation);

        List<FlowUserFootprintBO> userFootprintAnalyseList = mongoTemplate.aggregate(typedAggregation, "flow_user_operation", FlowUserFootprintBO.class).getMappedResults();
        if (CollUtil.isEmpty(userFootprintAnalyseList)) {
            return new ArrayList<>();
        }
        return userFootprintAnalyseList;
    }

    @Override
    public void statisticsAndSaveRegionalOfTime(int dateType, Date beginDate, Date endDate) {
        List<FlowRegionalBO> regionalStatisticsList = this.statisticsRegionalOfTime(beginDate, endDate);

        this.batchInsertFlowRegionalBO(dateType, beginDate, endDate, regionalStatisticsList);
    }

    @Override
    public List<FlowRegionalBO> listFlowRegional(Date beginDate, Date endDate, int dateType) {
        Criteria criteria = Criteria.where("dateType").is(dateType)
                .and("_id")
                .gte(FlowUtil.timeToId(beginDate, FlowRegionalBO.ID_LENGTH))
                .lt(FlowUtil.timeToId(endDate, FlowRegionalBO.ID_LENGTH));

        Query query = new Query(criteria).with(Sort.by(Sort.Direction.ASC, "_id"));

        LOG.info("listFlowRegional query:{}", query);

        List<FlowRegionalBO> flowRegionalList = mongoTemplate.find(query, FlowRegionalBO.class);

        return flowRegionalList;
    }

    @Override
    public List<FlowRegionalBO> statisticsRegionalOfTime(Date beginDate, Date endDate) {
        Long beginId = FlowUtil.timeToId(beginDate);
        Long endId = FlowUtil.timeToId(endDate);

        // 浏览量、浏览人数、下单金额、支付金额
        List<FlowRegionalBO> regionalStatisticsList = this.orderRegionalStatistics(beginId, endId);

        // 商品访客数、商品浏览量、加购数量、加购人数
        List<FlowRegionalBO> userRegionalStatisticsList = this.userRegionalStatistics(beginId, endId);

        if (CollUtil.isNotEmpty(regionalStatisticsList) && CollUtil.isNotEmpty(userRegionalStatisticsList)) {
            Map<Integer, FlowRegionalBO> map = regionalStatisticsList.stream().collect(Collectors.toMap(FlowRegionalBO::getProvinceId, r -> r));

            for (FlowRegionalBO flowRegionalBO : userRegionalStatisticsList) {
                FlowRegionalBO regionalStatistics = map.get(flowRegionalBO.getProvinceId());
                if (Objects.isNull(regionalStatistics)) {
                    regionalStatisticsList.add(flowRegionalBO);
                    continue;
                }
                regionalStatistics.setSpuVisitNum(flowRegionalBO.getSpuVisitNum());
                regionalStatistics.setSpuVisitUserNum(flowRegionalBO.getSpuVisitUserNum());
                regionalStatistics.setPlusShopCartNum(flowRegionalBO.getPlusShopCartNum());
                regionalStatistics.setPlusShopCartUserNum(flowRegionalBO.getPlusShopCartUserNum());

            }
        } else if (CollUtil.isEmpty(regionalStatisticsList)) {
            regionalStatisticsList = userRegionalStatisticsList;
        }
        return regionalStatisticsList;
    }

    @Override
    public List<FlowUserBO> statisticsUserOfTime(int dateType, Date beginTime, Date endTime) {
        Long beginId = FlowUtil.timeToId(beginTime);
        Long endId = FlowUtil.timeToId(endTime);

        // 成交会员数、支付订单数、支付金额
        List<FlowUserBO> flowUserList = this.statisticsUserOrderInfo(beginTime, endTime);

        // 统计 免费会员、付费会员的领券数量
        List<FlowUserBO> couponFlowUserBOList = this.membershipUserStatisticsList(beginId, endId);

        for (FlowUserBO flowUserBO : flowUserList) {
            // 只有两条数据，直接遍历
            for (FlowUserBO userBO : couponFlowUserBOList) {
                if (Objects.equals(flowUserBO.getType(), userBO.getType())) {
                    flowUserBO.setReceiveCouponUser(userBO.getReceiveCouponUser());
                }
            }
        }

        FlowUserBO allFlowUserBO = new FlowUserBO();
        allFlowUserBO.setType(0);

        for (FlowUserBO flowUserBO : flowUserList) {
            flowUserBO.setType(flowUserBO.getType() + 1);
            allFlowUserBO.setPayUser(allFlowUserBO.getPayUser() + flowUserBO.getPayUser());
            allFlowUserBO.setPayOrder(allFlowUserBO.getPayOrder() + flowUserBO.getPayOrder());
            allFlowUserBO.setPayAmount(allFlowUserBO.getPayAmount() + flowUserBO.getPayAmount());
            allFlowUserBO.setReceiveCouponUser(allFlowUserBO.getReceiveCouponUser() + flowUserBO.getReceiveCouponUser());
        }
        flowUserList.add(allFlowUserBO);

        // 用户map
        Map<Integer, FlowUserBO> flowUserMap = flowUserList.stream().collect(Collectors.toMap(FlowUserBO::getType, f -> f));

        // 累计会员、新增会员
        MemberCountVO memberCountVO = userFeignClient.statisticsUserNum(new MemberReqDTO(beginTime, endTime));

        // 全部会员 - 累积会员、新增会员
        this.insertUserInfoToFlowUser(flowUserList, flowUserMap, 0,
                memberCountVO.getTotalMember(),
                memberCountVO.getNewMember());
        // 普通会员 - 累积会员、新增会员
        this.insertUserInfoToFlowUser(flowUserList, flowUserMap, 1,
                memberCountVO.getTotalMember() - memberCountVO.getPaidMember(),
                memberCountVO.getNewMember());
        // 付费会员 - 累积会员、新增会员
        this.insertUserInfoToFlowUser(flowUserList, flowUserMap, 2,
                memberCountVO.getPaidMember(),
                memberCountVO.getNewPaidMember());
        return flowUserList;
    }

    @Override
    public void statisticsAndSaveUserOfTime(int dateType, Date beginTime, Date endTime) {
        List<FlowUserBO> flowUserList = this.statisticsUserOfTime(dateType, beginTime, endTime);

        // 批量保存
        this.batchInsertFlowUserBO(dateType, beginTime, endTime, flowUserList);
    }

    @Override
    public List<FlowUserFootprintBO> listUserFootprint(Date startTime, Date endTime, Integer timeType) {

        Criteria criteria = Criteria.where("dateType").is(timeType)
                .and("_id").
                gte(FlowUtil.timeToId(startTime, FlowUserFootprintBO.ID_LENGTH))
                .lt(FlowUtil.timeToId(endTime, FlowUserFootprintBO.ID_LENGTH));

        Query query = new Query(criteria);

        LOG.info("listUserFootprint query:{}", query);

        return mongoTemplate.find(query, FlowUserFootprintBO.class);
    }

    @Override
    public List<MemberTrendRespVO> statisticsRealMemberTrend(Date startTime, Date endTime, Integer dateType) {
        List<MemberTrendRespVO> memberTrendRespList = new ArrayList<>();
        // 获取当天注册会员数量
        DateTime currentTime = DateUtil.beginOfDay(new Date());
        CustomerReqDTO customerReqDTO = new CustomerReqDTO();
        customerReqDTO.setStartTime(currentTime);

        ServerResponseEntity<MemberTrendRespVO> memberResponseEntity = userFeignClient.countUserNumByType(customerReqDTO);
        if (!memberResponseEntity.isSuccess()) {
            throw new LuckException(memberResponseEntity.getMsg());
        }
        MemberTrendRespVO memberTrendRespVO = memberResponseEntity.getData();
        memberTrendRespVO.setCurrentDay(String.valueOf(currentTime.getTime()));
        memberTrendRespList.add(memberTrendRespVO);

        // 查询前29天的数据
        List<MemberTrendRespVO> flowUserBOList = this.listUserNumAndTypeByTime(startTime, currentTime);
        memberTrendRespList.addAll(flowUserBOList);
        return memberTrendRespList;
    }

    @Override
    public List<MemberTrendRespVO> listUserNumAndTypeByTime(Date startTime, Date endTime) {

        Integer[] types = new Integer[]{1, 2};
        Criteria criteria = new Criteria().and("dateType").is(FlowDateTypeEnum.DAY.value())
                .and("type").in(Arrays.asList(types))
                .and("_id")
                .gte(FlowUtil.timeToId(startTime, FlowUserBO.ID_LENGTH))
                .lt(FlowUtil.timeToId(endTime, FlowUserBO.ID_LENGTH));
        Query query = new Query(criteria);

        List<FlowUserBO> flowUserList = mongoTemplate.find(query, FlowUserBO.class);
        List<MemberTrendRespVO> memberTrendRespList = new ArrayList<>();

        if (CollUtil.isEmpty(flowUserList)) {
            return memberTrendRespList;
        }


        Map<Date, List<FlowUserBO>> map = flowUserList.stream().collect(Collectors.groupingBy(FlowUserBO::getCreateTime));
        map.forEach((key, value) -> {
            MemberTrendRespVO memberTrendRespVO = new MemberTrendRespVO();
            memberTrendRespVO.setCurrentDay(String.valueOf(key.getTime()));
            for (FlowUserBO flowUserBO : value) {
                if (flowUserBO.getType() == 1) {
                    memberTrendRespVO.setMemberNum(flowUserBO.getNewUserNum());
                } else if (flowUserBO.getType() == 2) {
                    memberTrendRespVO.setVipMemberNum(flowUserBO.getNewUserNum());
                }
            }
            memberTrendRespList.add(memberTrendRespVO);
        });
        return memberTrendRespList;
    }

    @Override
    public FlowUserBO getFlowUserByTimeAndUserType(Date startTime, Date endTime, Integer dateType, Integer memberType) {
        List<FlowUserBO> flowUserList = this.listMongoFlowUserByTimeAndUserType(startTime, endTime, dateType, memberType);
        if (CollUtil.isEmpty(flowUserList)) {
            return new FlowUserBO();
        }
        return flowUserList.get(0);
    }

    @Override
    public List<FlowUserBO> listFlowUserByTimeAndUserType(Date startTime, Date endTime, Integer dateType, Integer memberType) {
        if (!FlowDateTypeEnum.weekOrMonth(dateType)) {
            dateType = FlowDateTypeEnum.DAY.value();
        }
        return this.listMongoFlowUserByTimeAndUserType(startTime, endTime, dateType, memberType);
    }

    private List<FlowUserBO> listMongoFlowUserByTimeAndUserType(Date startTime, Date endTime, Integer dateType, Integer memberType) {
        Criteria criteria = new Criteria().and("dateType").is(dateType)
                .and("_id")
                .gte(FlowUtil.timeToId(startTime, FlowUserBO.ID_LENGTH))
                .lt(FlowUtil.timeToId(endTime, FlowUserBO.ID_LENGTH));

        if (Objects.nonNull(memberType)) {
            criteria.and("type").is(memberType);
        }

        Query query = new Query(criteria);

        LOG.info("listFlowUserByTimeAndUserType query:{}", query);

        return mongoTemplate.find(query, FlowUserBO.class);
    }

    @Override
    public List<MemberContributeValueVO> statisticsRealMemberContributeValue(Date startTime, Date endTime) {
        // 成交会员数 支付订单数 支付金额
        Criteria criteria = Criteria.where("isPayed").is(1)
                .and("createTime").gte(startTime).lt(endTime);
        GroupOperation initGroupOperation = Aggregation.group("userId")
                .last("userType").as("userType")
                .sum("actualTotal").as("payAmount")
                .count().as("payOrderNum");
        GroupOperation groupOperation = Aggregation.group("userType")
                .first("userType").as("userType")
                .sum("payAmount").as("payAmount")
                .sum("payOrderNum").as("payOrderNum")
                .count().as("payMemberNum");

        TypedAggregation<MemberContributeValueVO> typedAggregation = Aggregation.newAggregation(
                MemberContributeValueVO.class,
                Aggregation.match(criteria),
                initGroupOperation,
                groupOperation
        );

        LOG.info("statisticsRealMemberContributeValue typedAggregation:{}", typedAggregation);
        List<MemberContributeValueVO> memberContributeValueList = mongoTemplate
                .aggregate(typedAggregation, "order", MemberContributeValueVO.class)
                .getMappedResults();
        memberContributeValueList = memberContributeValueList.stream().filter(memberContributeValueVO -> Objects.nonNull(memberContributeValueVO.getUserType())).collect(Collectors.toList());
        for (MemberContributeValueVO memberContributeValueVO : memberContributeValueList) {
            memberContributeValueVO.setUserType(memberContributeValueVO.getUserType() + 1);
        }
        return memberContributeValueList;
    }

    @Override
    public Date getLatestTime() {
        Criteria criteria = Criteria.where("dateType").is(FlowDateTypeEnum.DAY.value());
        Query query = new Query(criteria).with(Sort.by(Sort.Direction.DESC, "_id")).limit(1);
        List<FlowUserBO> flowUserBOList = mongoTemplate.find(query, FlowUserBO.class);
        if (CollUtil.isEmpty(flowUserBOList)) {
            return null;
        }
        return flowUserBOList.get(0).getCreateTime();
    }

    @Override
    public void statisticsAndSaveUserRetained() {

        // 统计结束的时间 -- 用于统计六月前的留存数据
        Date endTime = DateUtil.beginOfMonth(DateUtil.offsetMonth(new Date(), -6));
        // 统计开始时间
        Date startTime = this.getBeginTime(endTime);
        // 留存数据列表
        List<FlowUserRetainBO> list = this.statisticsUserRetained(startTime, endTime);

        // 根据id查询结果删除数据
        Criteria criteria = Criteria.where("createTime").gte(startTime)
                .lt(endTime);
        Query query = new Query(criteria);

        mongoTemplate.remove(query, FlowUserRetainBO.class);

        if (CollUtil.isNotEmpty(list)) {
            mongoTemplate.insertAll(list);
        }
    }

    /**
     * 交易留存/成交留存
     * 成交时间，是每一个月的时间 (例如： 2021-05, 2021-06)
     * 新成交客户数：以前从没有支付过，第一次下单支付了的用户。(余额充值，也是要生成订单信息的，所以都只需要统计订单表的信息就可以了)
     * 因为要计算用户第一次的成交时间所以一定有全表扫描的
     * 第一月留存：新成交的客户，在第一次交易后的下一个月，是否有再次交易成功的人数
     * eg：假设2021年1月份，A B C D 四位客户首次成交；2月份 A B 成交；3月份 D成交，4月份B C D有成交。求 1月份客户在后续2，3，4月份的留存率分别是多少？
     * 根据前面的算法公式，则：
     * 2月份的留存数是 2，留存率 = 2 /4 =50%
     * 3月份的留存数是 1，留存率 = 1/4 =25%
     * 4月份的留存数是 3，留存率 = 3/4 =75%
     *
     * @param startTime
     * @param endTime
     * @return
     */
    @Override
    public List<FlowUserRetainBO> statisticsUserRetained(Date startTime, Date endTime) {
        // ============================== 访问留存 ==============================
        List<FlowUserRetainBO> retainedList = new ArrayList<>();
        Date beginTime = startTime;
        while (beginTime.getTime() < endTime.getTime()) {

            Date intiEndTime = DateUtil.offsetDay(beginTime, 1);
            Date statisticsBeginTime = intiEndTime;
            // 结束时间--统计开始时间的后6个月，即统计6个月的数据
            Date statisticsEndTime = DateUtil.beginOfMonth(DateUtil.offsetMonth(statisticsBeginTime, 6));

            FlowUserRetainBO flowUserRetainBO = this.statisticsUserVisitRetained(beginTime, intiEndTime, statisticsEndTime);
            retainedList.add(flowUserRetainBO);
            beginTime = DateUtil.beginOfDay(DateUtil.offsetMonth(beginTime, 1));
        }

        // ============================== 成交留存 ==============================
        ServerResponseEntity<List<CustomerRetainVO>> orderResponseEntity = orderFeignClient.getTradeRetained(new CustomerRetainedDTO(startTime, endTime));
        if (!orderResponseEntity.isSuccess()) {
            throw new LuckException(orderResponseEntity.getMsg());
        }
        Date currentTime = new Date();
        List<FlowUserRetainBO> tradeRetainedList = new ArrayList<>();
        List<CustomerRetainVO> customerRetainList = orderResponseEntity.getData();
        for (CustomerRetainVO customerRetainVO : customerRetainList) {
            FlowUserRetainBO flowUserRetainBO = new FlowUserRetainBO();
            flowUserRetainBO.setRetainType(2);
            DateTime time = DateUtil.parse(customerRetainVO.getCurrentMonth(), "yyyyMM");
            flowUserRetainBO.setId(Integer.parseInt(customerRetainVO.getCurrentMonth() + flowUserRetainBO.getRetainType()));
            flowUserRetainBO.setCreateTime(time);
            flowUserRetainBO.setUpdateTime(currentTime);
            flowUserRetainBO.setNewCustomers(customerRetainVO.getNewCustomers());
            flowUserRetainBO.setFirstMonthRemain(customerRetainVO.getFirstMonthRemain());
            flowUserRetainBO.setSecondMonthRemain(customerRetainVO.getSecondMonthRemain());
            flowUserRetainBO.setThirdMonthRemain(customerRetainVO.getThirdMonthRemain());
            flowUserRetainBO.setFourthMonthRemain(customerRetainVO.getFourthMonthRemain());
            flowUserRetainBO.setFifthMonthRemain(customerRetainVO.getFifthMonthRemain());
            flowUserRetainBO.setSixthMonthRemain(customerRetainVO.getSixthMonthRemain());
            retainedList.add(flowUserRetainBO);
        }
        return retainedList;
    }

    @Override
    public void statisticsAndSaveUserRetainedOfCurrent() {
        // 新用户的时间、开始统计的时间、统计结束时间
        Date endTime = DateUtil.beginOfMonth(DateUtil.offsetMonth(new Date(), 1));
        Date startTime = DateUtil.beginOfMonth(DateUtil.offsetMonth(new Date(), -6));
        // 先查询列表再进行统计，一个月完全统计一次，每天统计近一个月的数据
        List<FlowUserRetainBO> list = this.statisticsUserRetained(startTime, endTime);

        if (CollUtil.isEmpty(list)) {
            return;
        }
        // 先删除旧的数据
        Criteria criteria = Criteria.where("_id")
                .gte(retainedDateToId(startTime))
                .lte(retainedDateToId(endTime));
        Query query = new Query(criteria);
        mongoTemplate.remove(query, FlowUserRetainBO.class);

        // 批量保存统计数据
        mongoTemplate.insertAll(list);
    }

    @Override
    public List<FlowUserRetainBO> listFlowUserRetain(Date startTime, Date endTime, Integer retainType) {
        Criteria criteria = Criteria.where("_id")
                .gte(retainedDateToId(startTime))
                .lt(retainedDateToId(endTime))
                .and("retainType").is(retainType);
        Query query = new Query(criteria);

        return mongoTemplate.find(query, FlowUserRetainBO.class);
    }

    private void batchInsertUserOperationBO(Date beginDate, Date endDate, List<FlowUserOperationBO> flowUserOperationBOList) {
        Long beginId = FlowUtil.timeToId(beginDate);
        Long endId = FlowUtil.timeToId(endDate);

        // 先删除再更新-mongo没有事务，如果之前保存后程序报错了，数据还是保存在数据库中
        mongoTemplate.remove(new Query(Criteria.where("_id").gte(beginId).lt(endId)), FlowUserOperationBO.class);

        // 统计的数据批量保存到flow_system_user
        mongoTemplate.insertAll(flowUserOperationBOList);
    }

    /**
     * 统计用户的成交数据
     *
     * @param beginTime
     * @param endTime
     * @return
     */
    private List<FlowUserBO> statisticsUserOrderInfo(Date beginTime, Date endTime) {

        Criteria criteria = Criteria.where("isPayed").is(1)
                .and("createTime").gte(beginTime).lt(endTime);

        GroupOperation initGroupOperation = Aggregation.group("userId")
                .last("userType").as("userType")
                .sum("actualTotal").as("payAmount")
                .count().as("payOrder");

        String[] fields = new String[]{"userType"};

        GroupOperation groupOperation = Aggregation.group(fields)
                .first("userType").as("type")
                .sum("payAmount").as("payAmount")
                .sum("payOrder").as("payOrder")
                .count().as("payUser");

        TypedAggregation<FlowUserBO> typedAggregation = Aggregation.newAggregation(
                FlowUserBO.class,
                Aggregation.match(criteria),
                initGroupOperation,
                groupOperation
        );
        LOG.info("statisticsUserOrderInfo：typedAggregation:{}", typedAggregation);
        List<FlowUserBO> orderFlowUserList = mongoTemplate
                .aggregate(typedAggregation, "order", FlowUserBO.class)
                .getMappedResults();

        return new ArrayList<>(orderFlowUserList);
    }

    private void insertUserInfoToFlowUser(List<FlowUserBO> flowUserBOList, Map<Integer, FlowUserBO> flowUserMap, int type, Integer userNum, Integer newUserNum) {
        FlowUserBO flowUserBO = flowUserMap.get(type);
        if (Objects.isNull(flowUserBO)) {
            flowUserBO = new FlowUserBO();
            flowUserBO.setType(type);
            flowUserBOList.add(flowUserBO);
        }
        flowUserBO.setUserNum(userNum);
        flowUserBO.setNewUserNum(newUserNum);
    }


    private String retainedDateToString(Date date) {
        return DateUtil.format(date, "yyyyMM");
    }

    private Integer retainedDateToId(Date date) {
        return Integer.parseInt(retainedDateToString(date) + Constant.ZERO_INTEGER);
    }

    private FlowUserRetainBO statisticsUserVisitRetained(Date startTime, Date endTime, Date statisticsEndTime) {
        // 1.先查出指定月份的用户数据
        Date current = new Date();
        FlowUserRetainBO flowUserRetainBO = new FlowUserRetainBO();
        flowUserRetainBO.setRetainType(1);
        flowUserRetainBO.setId(Integer.parseInt(DateUtil.format(startTime, "yyyyMM") + flowUserRetainBO.getRetainType()));
        flowUserRetainBO.setCreateTime(startTime);
        flowUserRetainBO.setUpdateTime(current);

        List<Long> userIds = userFeignClient.listNewUserIdByTime(new CustomerReqDTO(startTime, endTime));
        flowUserRetainBO.setNewCustomers(userIds.size());
        if (CollUtil.isEmpty(userIds)) {
            return flowUserRetainBO;
        }

        // 2.统计用户后续时间的访问情况
        MatchOperation matchOperation = Aggregation.match(Criteria.where("userId").in(userIds).and("_id")
                .gte(FlowUtil.timeToId(endTime))
                .lt(FlowUtil.timeToId(statisticsEndTime)));

        // 3.处理数据，得到年月
        ProjectionOperation projectionOperation = Aggregation.project("userId").and("createTime").dateAsFormattedString("%Y%m").as("currentMonth");

        // 4.去重时间和用户id来
        GroupOperation userGroupOperation = Aggregation.group("currentMonth", "userId")
                .first("userId").as("userId")
                .first("currentMonth").as("currentMonth");


        // 5.统计各个月份的人数
        GroupOperation groupOperation = Aggregation.group("currentMonth")
                .first("currentMonth").as("currentMonth")
                .count().as("newCustomers");

        // 查询数据
        TypedAggregation<CustomerRetainVO> typedAggregation = Aggregation.newAggregation(
                CustomerRetainVO.class,
                matchOperation,
                projectionOperation,
                userGroupOperation,
                groupOperation
        );
        List<CustomerRetainVO> customerRetainList = mongoTemplate
                .aggregate(typedAggregation, "order", CustomerRetainVO.class)
                .getMappedResults();

        if (CollUtil.isEmpty(customerRetainList)) {
            return flowUserRetainBO;
        }
        Map<String, Integer> map = customerRetainList.stream()
                .collect(Collectors.toMap(CustomerRetainVO::getCurrentMonth, CustomerRetainVO::getNewCustomers));

        // 6.处理数据
        for (int i = 1; i <= Constant.SIX; i++) {
            Date beginTime = DateUtil.beginOfMonth(DateUtil.offsetMonth(startTime, 1));
            String format = DateUtil.format(beginTime, "yyyyMM");
            if (!map.containsKey(format)) {
                continue;
            }
            Integer newCustomers = map.get(format);
            switch (i) {
                case 1:
                    flowUserRetainBO.setFirstMonthRemain(newCustomers);
                    break;
                case 2:
                    flowUserRetainBO.setSecondMonthRemain(newCustomers);
                    break;
                case 3:
                    flowUserRetainBO.setThirdMonthRemain(newCustomers);
                    break;
                case 4:
                    flowUserRetainBO.setFourthMonthRemain(newCustomers);
                    break;
                case 5:
                    flowUserRetainBO.setFifthMonthRemain(newCustomers);
                    break;
                case 6:
                    flowUserRetainBO.setSixthMonthRemain(newCustomers);
                    break;
                default:
                    break;
            }
        }
        return flowUserRetainBO;
    }

    private Date getBeginTime(Date entTime) {
        String format = DateUtil.format(entTime, "yyyyMM");

        Query query = new Query(Criteria.where("_id").lt(Integer.parseInt(format + 0)))
                .with(Sort.by(Sort.Direction.DESC, "_id")).limit(1);
        List<FlowUserRetainBO> dataList = mongoTemplate.find(query, FlowUserRetainBO.class);

        if (CollUtil.isNotEmpty(dataList)) {
            return dataList.get(0).getCreateTime();
        }

        // 如果未统计，则从flow_user_operation中获取最早的时间
        // 查询第一个订单的时间-统计成交记录
        List<FlowUserOperationBO> orderList = mongoTemplate.find(
                new Query().with(Sort.by(Sort.Direction.ASC, "_id")).limit(1),
                FlowUserOperationBO.class, "order"
        );

        Date orderTime = null;
        if (CollUtil.isNotEmpty(orderList)) {
            orderTime = orderList.get(0).getCreateTime();
        }

        // 查询第一个订单的时间-统计成交记录
        List<FlowUserOperationBO> flowUserOperationBOList = mongoTemplate.find(
                new Query().with(Sort.by(Sort.Direction.ASC, "_id")).limit(1),
                FlowUserOperationBO.class
        );

        Date operationTime = null;
        if (CollUtil.isNotEmpty(flowUserOperationBOList)) {
            operationTime = flowUserOperationBOList.get(0).getCreateTime();
        }

        // 全部都是空的，就从上一月开始计算
        if (orderTime == null && operationTime == null) {
            return DateUtil.beginOfMonth(DateUtil.offsetMonth(new Date(), -1));
        }
        // 都不为空，判断哪个时间更早
        else if (orderTime != null && operationTime != null) {
            return orderTime.before(operationTime) ? orderTime : operationTime;
        } else if (orderTime == null) {
            return operationTime;
        }

        return orderTime;
    }


    private void batchInsertFlowUserBO(Integer dateType, Date beginDate, Date endDate, List<FlowUserBO> flowUserList) {
        if (CollUtil.isEmpty(flowUserList)) {
            return;
        }

        Date currentTime = new Date();
        String date = DateUtil.format(beginDate, "yyMMdd");
        int index = 1;

        for (FlowUserBO flowUserBO : flowUserList) {
            flowUserBO.setCreateTime(beginDate);
            flowUserBO.setUpdateTime(currentTime);
            flowUserBO.setDateType(dateType);
            flowUserBO.setId(FlowUtil.getId(date + dateType, index++, FlowUserBO.ID_LENGTH));
        }

        // 先删除再更新
        mongoTemplate.remove(FlowUtil.getRemoveQuery(beginDate, endDate, FlowUserBO.ID_LENGTH, dateType), FlowUserBO.class);

        // 统计的数据批量保存到flow_system
        mongoTemplate.insertAll(flowUserList);
    }


    private void batchInsertFlowRegionalBO(Integer dateType, Date beginDate, Date endDate, List<FlowRegionalBO> flowRegionalList) {
        if (CollUtil.isEmpty(flowRegionalList)) {
            return;
        }

        String currentTime = DateUtil.format(beginDate, "yyMMdd");
        Date currentDate = new Date();

        for (FlowRegionalBO flowRegionalBO : flowRegionalList) {
            flowRegionalBO.setCreateTime(beginDate);
            flowRegionalBO.setUpdateTime(currentDate);
            flowRegionalBO.setDateType(dateType);
            if (flowRegionalBO.getProvinceId() == 0L) {
                // 未知地区的数据
                flowRegionalBO.setId(Integer.parseInt(currentTime + dateType + "00"));
                continue;
            }
            flowRegionalBO.setId(Integer.parseInt(currentTime + dateType + flowRegionalBO.getProvinceId()));
        }

        // 先删除再更新
        mongoTemplate.remove(FlowUtil.getRemoveQuery(beginDate, endDate, FlowRegionalBO.ID_LENGTH, dateType), FlowRegionalBO.class);

        // 统计的数据批量保存到flow_system
        mongoTemplate.insertAll(flowRegionalList);
    }

    private void batchInsertFlowUserFootprintBO(Integer dateType, Date beginDate, Date endDate, List<FlowUserFootprintBO> flowUserFootprintList) {
        if (CollUtil.isEmpty(flowUserFootprintList)) {
            return;
        }

        String currentTime = DateUtil.format(beginDate, "yyMMdd");
        Date currentDate = new Date();

        for (FlowUserFootprintBO flowUserFootprintBO : flowUserFootprintList) {
            flowUserFootprintBO.setCreateTime(beginDate);
            flowUserFootprintBO.setUpdateTime(currentDate);
            flowUserFootprintBO.setDateType(dateType);
            flowUserFootprintBO.setId(Integer.parseInt(currentTime + dateType + flowUserFootprintBO.getVisitId() + "0"));
        }

        // 先删除再更新
        mongoTemplate.remove(FlowUtil.getRemoveQuery(beginDate, endDate, FlowUserFootprintBO.ID_LENGTH, dateType), FlowUserFootprintBO.class);

        // 统计的数据批量保存到flow_system
        mongoTemplate.insertAll(flowUserFootprintList);
    }


    private List<FlowUserBO> membershipUserStatisticsList(Long beginId, Long endId) {

        List<AggregationOperation> aggregationOperationList = new ArrayList<>();

        aggregationOperationList.add(Aggregation.match(Criteria.where("_id").gte(beginId).lt(endId).and("membership").is(1)));

        List<FlowUserBO> flowUserBOList = this.aggragationUserReceiveCoupon(aggregationOperationList, new String[]{"vipUser"});
        flowUserBOList = flowUserBOList.stream().filter(flowUserBO -> Objects.nonNull(flowUserBO.getType())).collect(Collectors.toList());

        flowUserBOList.sort(Comparator.comparing(FlowUserBO::getType));
        for (FlowUserBO flowUserBO : flowUserBOList) {
            flowUserBO.setType(flowUserBO.getType() + 1);
        }

        return flowUserBOList;
    }

    private List<FlowUserBO> aggragationUserReceiveCoupon(List<AggregationOperation> aggregationOperationList, String[] fieIds) {
        aggregationOperationList.add(Aggregation.group(fieIds)
                .first("vipUser").as("type")
                .sum("receiveCoupon").as("receiveCouponUser"));

        TypedAggregation<FlowUserBO> typedAggregation = Aggregation.newAggregation(
                FlowUserBO.class,
                aggregationOperationList
        );

        LOG.info("aggragationUserReceiveCoupon:aggregation:{}", typedAggregation);

        List<FlowUserBO> flowUserBOList = mongoTemplate
                .aggregate(typedAggregation, "flow_user_operation", FlowUserBO.class
                ).getMappedResults();

        return new ArrayList<>(flowUserBOList);
    }

    private List<FlowRegionalBO> orderRegionalStatistics(Long beginId, Long endId) {
        GroupOperation initGroupOperation = Aggregation.group("provinceId", "userId")
                .first("provinceId").as("provinceId")
                .sum("placeOrderAmount").as("placeOrderAmount")
                .sum("payAmount").as("payAmount")
                .max("placeOrder").as("placeOrder")
                .max("pay").as("pay")
                .sum("visitNum").as("visitNum");


        GroupOperation groupOperation = Aggregation.group("provinceId")
                .first("provinceId").as("provinceId")
                .sum("placeOrderAmount").as("placeOrderAmount")
                .sum("payAmount").as("payAmount")
                .sum("placeOrder").as("placeOrderUserNum")
                .sum("pay").as("payUserNum")
                .sum("visitNum").as("visitNum")
                .count().as("visitUserNum");

        TypedAggregation<FlowRegionalBO> typedAggregation = Aggregation.newAggregation(
                FlowRegionalBO.class,
                Aggregation.match(Criteria.where("_id").gte(beginId).lt(endId)),
                initGroupOperation,
                groupOperation
        );
        LOG.info("orderRegionalStatistics:aggregation:{}", typedAggregation);
        return new ArrayList<>(mongoTemplate.aggregate(typedAggregation, "flow_user_operation", FlowRegionalBO.class).getMappedResults());
    }

    private List<FlowRegionalBO> userRegionalStatistics(Long beginId, Long endId) {
        GroupOperation initGroupOperation = Aggregation.group("provinceId", "userId")
                .first("provinceId").as("provinceId")
                .sum("visit").as("spuVisitNum")
                .sum("plusShopCartNum").as("plusShopCartNum")
                .max("plusShopCart").as("plusShopCart");


        GroupOperation groupOperation = Aggregation.group("provinceId")
                .first("provinceId").as("provinceId")
                .sum("spuVisitNum").as("spuVisitNum")
                .sum("plusShopCartNum").as("plusShopCartNum")
                .sum("plusShopCart").as("plusShopCart")
                .count().as("spuVisitUserNum");

        TypedAggregation<FlowRegionalBO> typedAggregation = Aggregation.newAggregation(
                FlowRegionalBO.class,
                Aggregation.match(Criteria.where("_id").gte(beginId).lt(endId)),
                initGroupOperation,
                groupOperation
        );
        LOG.info("userRegionalStatistics:aggregation:{}", typedAggregation);
        return mongoTemplate.aggregate(typedAggregation, "flow_spu_user", FlowRegionalBO.class).getMappedResults();
    }

    private void statisticsUserOrderLog(List<Long> userIds,
                                        List<FlowUserOperationBO> userAnalyseLogList,
                                        Date beginDate, Date endDate) {

        MatchOperation matchOperation = Aggregation.match(
                Criteria.where("createTime").gte(beginDate).lt(endDate).and("userId").in(userIds));

        ProjectionOperation projectionOperation = Aggregation.project("userId", "isPayed")
                .and("actualTotal").as("placeOrderAmount")
                .and("allCount").as("placeOrderNum")
                .and(ConditionalOperators.when(Criteria.where("isPayed").is(1)).then("$actualTotal").otherwise(0)).as("payAmount")
                .and(ConditionalOperators.when(Criteria.where("isPayed").is(1)).then("$allCount").otherwise(0)).as("payNum");


        GroupOperation groupOperation = Aggregation.group("userId")
                .first("userId").as("userId")
                .first("placeOrderAmount").as("placeOrderAmount")
                .first("placeOrderNum").as("placeOrderNum")
                .first("payAmount").as("payAmount")
                .first("payNum").as("payNum");

        TypedAggregation<FlowUserOperationBO> typedAggregation = Aggregation.newAggregation(
                FlowUserOperationBO.class,
                matchOperation,
                projectionOperation,
                groupOperation
        );

        List<FlowUserOperationBO> flowUserOperationBOList = mongoTemplate
                .aggregate(typedAggregation, "order", FlowUserOperationBO.class)
                .getMappedResults();
        if (CollUtil.isEmpty(flowUserOperationBOList)) {
            return;
        }
        Map<Long, FlowUserOperationBO> map = userAnalyseLogList.stream()
                .collect(Collectors.toMap(FlowUserOperationBO::getUserId, u -> u));
        for (FlowUserOperationBO flowUserOperationBO : flowUserOperationBOList) {
            if (!map.containsKey(flowUserOperationBO.getUserId())) {
                continue;
            }
            FlowUserOperationBO userAnalyseLog = map.get(flowUserOperationBO.getUserId());
            userAnalyseLog.setPlaceOrderAmount(flowUserOperationBO.getPlaceOrderAmount());
            userAnalyseLog.setPayAmount(flowUserOperationBO.getPayAmount());
            if (Objects.nonNull(flowUserOperationBO.getPlaceOrderNum()) && flowUserOperationBO.getPlaceOrderNum() > 0) {
                userAnalyseLog.setPlaceOrder(1);
            }
            if (Objects.nonNull(flowUserOperationBO.getPayNum()) && flowUserOperationBO.getPayNum() > 0) {
                userAnalyseLog.setPay(1);
            }
            userAnalyseLog.setPlaceOrderNum(flowUserOperationBO.getPlaceOrderNum());
            userAnalyseLog.setPayNum(flowUserOperationBO.getPayNum());
        }
    }

    private void loadUserMemberInfo(List<Long> userIds,
                                    List<FlowUserOperationBO> userAnalyseLogList,
                                    Date beginDate, Date endDate) {

        // 新老、付费免费会员
        List<UserApiVO> userList = userFeignClient.listUserByUserIds(userIds);

        // 用户领券数量
        ServerResponseEntity<List<Long>> receiveCouponResponseEntity = couponFeignClient.userReceiveCouponCount(new ReceiveCouponDTO(userIds, beginDate, endDate));
        if (!receiveCouponResponseEntity.isSuccess()) {
            throw new LuckException(receiveCouponResponseEntity.getMsg());
        }
        List<Long> couponUserList = receiveCouponResponseEntity.getData();
        Map<Long, UserApiVO> userMap = userList.stream().collect(Collectors.toMap(UserApiVO::getUserId, u -> u));

        long beginTime = beginDate.getTime();
        long endTime = endDate.getTime();
        for (FlowUserOperationBO flowUserOperationBO : userAnalyseLogList) {
            if (!Objects.equals(flowUserOperationBO.getMembership(), 1) || !userMap.containsKey(flowUserOperationBO.getUserId())) {
                continue;
            }
            // 新注册的会员
            UserApiVO user = userMap.get(flowUserOperationBO.getUserId());
            long createTime = user.getCreateTime().getTime();
            if (beginTime < createTime && createTime < endTime) {
                flowUserOperationBO.setNewUser(1);
            } else {
                flowUserOperationBO.setNewUser(0);
            }
            // 当天购买vip用户
            if (Objects.nonNull(user.getVipStartTime())) {
                long vipStartTime = user.getVipStartTime().getTime();
                if (beginTime < vipStartTime && vipStartTime < endTime) {
                    flowUserOperationBO.setNewVipUser(1);
                } else {
                    flowUserOperationBO.setNewVipUser(0);
                }
            }
            flowUserOperationBO.setVipUser(user.getLevelType());

            //优惠券
            if (couponUserList.contains(flowUserOperationBO.getUserId())) {
                flowUserOperationBO.setReceiveCoupon(1);
            }
        }
    }

    private List<FlowUserOperationBO> listUserAnalyseLogBO(Date beginDate, Date endDate) {
        GroupOperation initGroupOperation = Aggregation.group("oid")
                .last("uid").as("uid")
                .first("ip").as("provinceId")
                .max("s").as("step")
                .count().as("visit");

        ProjectionOperation projectionOperation = Aggregation.project("visit", "step", "provinceId")
                .and(ConditionalOperators.when(Criteria.where("uid").gt(0)).then("$uid").otherwise("$_id.oid")).as("uid")
                .and(ConditionalOperators.when(Criteria.where("uid").gt(0)).then(1).otherwise(0)).as("membership");

        GroupOperation groupOperation = Aggregation.group("uid")
                .first("provinceId").as("provinceId")
                .first("uid").as("userId")
                .max("step").as("step")
                .max("membership").as("membership")
                .sum("visit").as("visitNum");

        TypedAggregation<FlowUserOperationBO> typedAggregation = Aggregation.newAggregation(
                FlowUserOperationBO.class,
                Aggregation.match(Criteria.where("dt").gte(beginDate).lt(endDate)),
                initGroupOperation,
                projectionOperation,
                groupOperation
        );

        LOG.info("listUserAnalyseLogBO.typedAggregation:{}", typedAggregation);

        List<FlowUserOperationBO> userAnalyseLogList = mongoTemplate
                .aggregate(typedAggregation, "flow_user_log", FlowUserOperationBO.class)
                .getMappedResults();

        if (CollUtil.isEmpty(userAnalyseLogList)) {
            return userAnalyseLogList;
        }

        // 获取省名称列表
        List<AreaVO> areaList = areaFeignClient.listProvinceArea();
        Map<String, Long> areaMap = areaList.stream().collect(Collectors.toMap(AreaVO::getAreaName, AreaVO::getAreaId));

        // 设置省id
        for (FlowUserOperationBO flowUserOperationBO : userAnalyseLogList) {
            if (Objects.isNull(flowUserOperationBO.getProvinceId())) {
                continue;
            }
            flowUserOperationBO.setProvinceId(IpSearchUtil.getProvinceIdByUserIp(flowUserOperationBO.getProvinceId(), areaMap));
        }
        return userAnalyseLogList;
    }
}
