package com.ruoyi.project.business.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.URLUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.FileBuzType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.importExcel.excel.helper.BasicTableInfoHelper;
import com.ruoyi.common.importExcel.excel.tempInfo.LeadInfoHelper;
import com.ruoyi.common.importExcel.utils.ExcelUtils;
import com.ruoyi.common.utils.LocalUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.SpecialCharacterEscaper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.project.business.domain.TbArea;
import com.ruoyi.project.business.domain.TbCountry;
import com.ruoyi.project.business.domain.TbFile;
import com.ruoyi.project.business.domain.TcAccountContact;
import com.ruoyi.project.business.domain.TcLead;
import com.ruoyi.project.business.domain.TcLeadForRelated;
import com.ruoyi.project.business.domain.TcLeadHistory;
import com.ruoyi.project.business.domain.vo.board.BarChartVo;
import com.ruoyi.project.business.domain.vo.country.AreaWithCountriesVo;
import com.ruoyi.project.business.domain.vo.lead.*;
import com.ruoyi.project.business.enums.lead.LeadHisTypeEnum;
import com.ruoyi.project.business.enums.MessageTypeEnum;
import com.ruoyi.project.business.mapper.TcLeadMapper;
import com.ruoyi.project.business.service.ITbAreaService;
import com.ruoyi.project.business.service.ITbFileService;
import com.ruoyi.project.business.service.ITcAccountContactService;
import com.ruoyi.project.business.service.ITcLeadHistoryService;
import com.ruoyi.project.business.service.ITcLeadService;
import com.ruoyi.project.common.domain.Message;
import com.ruoyi.project.common.service.ISseEmitterService;
import com.ruoyi.project.system.domain.SysDictData;
import com.ruoyi.project.system.domain.SysRole;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDictDataMapper;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.ruoyi.common.constant.Constants.*;
import static com.ruoyi.common.enums.SerialNumberPrefix.LEAF;
import static com.ruoyi.project.business.enums.lead.LeadStatusEnum.*;
import static com.ruoyi.project.system.service.impl.SysUserServiceImpl.ROLE_KEY2NEXT_KEY_MAP;

/**
 * 线索管理Service业务层处理
 *
 * @author ruoyi
 * @date 2024-04-10
 */
@Slf4j
@Service("tcLeadService")
public class TcLeadServiceImpl extends ServiceImpl<TcLeadMapper, TcLead> implements ITcLeadService {
    // 可分配状态: 待分配、 已分配、已回收 （623反馈添加 跟进中、静默）
    private static final Set<String> ASSIGNABLE_STATUS = new HashSet<>(Arrays.asList(TO_BE_ALLOCATED.getCode(), ALLOCATED.getCode(), RECYCLED.getCode(), FOLLOWING.getCode(), ON_HOLD.getCode()));
    // 自动分配的线索的可分配状态：待分配、已回收（623反馈添加 跟进中、静默）
    private static final Set<String> ASSIGNABLE_STATUS_AUTO = new HashSet<>(Arrays.asList(TO_BE_ALLOCATED.getCode(), RECYCLED.getCode(), FOLLOWING.getCode(), ON_HOLD.getCode()));
    
    // 不【静默/无效】状态: 转化客户、转化项目、静默、无效
    private static final List<String> UN_SILENT_OR_INVALID_STATUS = Arrays.asList(CONVERTED_CUSTOMER.getCode(), CONVERTED_PROJECT.getCode(), ON_HOLD.getCode(), INVALID.getCode()); 
    
    // 可回收的状态：已分配、静默、跟进中
    private static final Set<String> RECYCLABLE_STATUS = new HashSet<>(Arrays.asList(ALLOCATED.getCode(), ON_HOLD.getCode(), FOLLOWING.getCode()));


    @Resource
    private ITbFileService tbFileService;

    @Resource
    private RedisCache redisCache;

    @Resource
    private ISseEmitterService sseEmitterService;

    @Resource
    private ISysUserService sysUserService;

    @Resource
    private ITcLeadHistoryService tcLeadHistoryService;

    @Resource
    private SysDictDataMapper sysDictDataMapper;

    @Resource
    private ITbAreaService tbAreaService;

    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
    @Resource
    private ITcAccountContactService tcAccountContactService;

    /**
     * 查询线索管理
     *
     * @param leadId 线索管理主键
     * @return 线索管理
     */
    @Override
    public LeadRes selectTcLeadByLeadId(Long leadId) {
        LeadRes tcLead = baseMapper.selectTcLeadByLeadId(leadId);
        // TcLead tcLead = getById(leadId);

        if (null == tcLead) {
            throw new RuntimeException(MessageUtils.message("lead.error.notexistOrDel"));
        }
        LeadListReq param = new LeadListReq();
        List<Long> ids = new ArrayList<>();
        ids.add(leadId);
        param.setLeadIds(ids);
        param.setPermissionChar(SecurityUtils.getPermissionCharIns());
        Long userId = SecurityUtils.getUserId();
        // 代理商销售经理的场合
        if (SecurityUtils.hasRole("agent_sales")) {
            param.setLoginUserId(userId);
        }
        String lan = LocaleContextHolder.getLocale().getCountry();
        param.setLan(lan);
        List<LeadRes> res = baseMapper.selectTcLeadList(param);
        if(ObjectUtils.isEmpty(res)){
            throw new ServiceException(MessageUtils.message("common.data.noPermession"));
        }
        // 失效原因查询
        if (INVALID.getCode().equals(tcLead.getStatus())) {
            TcLeadHistory one = tcLeadHistoryService.getOne(Wrappers.lambdaQuery(TcLeadHistory.class)
                    .eq(TcLeadHistory::getLeadId, leadId)
                    .and(i ->
                            i.eq(TcLeadHistory::getActionType, LeadHisTypeEnum.RECYCLE.getCode())
                                    .eq(TcLeadHistory::getRecoveryType, 99)
                                    .or()
                                    .eq(TcLeadHistory::getActionType, LeadHisTypeEnum.INVALID.getCode()))
                    .orderByDesc(TcLeadHistory::getCreateTime)
                    .last("limit 1"));
            if (one != null) {
                tcLead.setInvalidReason(one.getRecoveryReason());
                // tcLead.setLostRemark(one.getRecoveryReason());
            }
        }

        LeadRes leadRes = new LeadRes();
        BeanUtils.copyProperties(tcLead, leadRes);
        if (0L == leadRes.getOwnerAgent()) {
            leadRes.setOwnerAgentName(MessageUtils.message("agent.headquarters"));
        }

        // 查询区域
        // if (tcLead.getAreaId() != null) {
        //    tcLead.setAreaName(tbAreaMapper.selectTbAreaByAreaId(tcLead.getAreaId()).getAreaName());
        //}

        // 查询附件
        List<TbFile> tbFiles = tbFileService.selectTbFileListByBuzIdAndBuzType(leadId, FileBuzType.LEAD.getType());
        String urlStr = tbFiles.stream().map(TbFile::getFilePath).collect(Collectors.joining(","));
        leadRes.setTbFiles(tbFiles);
        leadRes.setAnnex(urlStr);
        return leadRes;
    }

    /**
     * 查询关联线索
     *
     * @return 线索管理
     */
    @Override
    public List<TcLeadForRelated> selectLeadListByLoginId() {
        Long loginId = SecurityUtils.getUserId();
        return baseMapper.selectLeadListByLoginId(loginId);
    }

    @Override
    public List<TcLeadForRelated> selectLeadListByLeadId(LeadRelatedQueryReq tcLead) {
        return baseMapper.selectLeadListByLeadId(tcLead);
    }

    @Override
    public List<TcLeadForRelated> selectLeadListByQuery(LeadRelatedQueryReq tcLead) {
        Long loginId = SecurityUtils.getUserId();
        SpecialCharacterEscaper.escapeSpecialCharacters(tcLead, LeadRelatedQueryReq::getQuery);
        tcLead.setLoginId(loginId);
        return baseMapper.selectLeadListByQuery(tcLead);
    }

    /**
     * 查询线索管理列表
     *
     * @param tcLead 线索管理
     * @return 线索管理
     */
    @Override
    public List<LeadRes> selectTcLeadList(LeadListReq tcLead) {
        Long userId = SecurityUtils.getUserId();

        List<LeadRes> tcLeads = selectTcLeadListBase(tcLead);

        String headquarters = MessageUtils.message("agent.headquarters");

        // 获取当前登录用户的下一级角色列表
        List<String> loginUserRoleKeys = SecurityUtils.getRoleKeys();
        Assert.notEmpty(loginUserRoleKeys, "user has no role");
        List<String> nextRoleKeys = loginUserRoleKeys.stream().map(item -> ROLE_KEY2NEXT_KEY_MAP.getOrDefault(item, Collections.emptyList())).flatMap(Collection::stream).collect(Collectors.toList());
        for (LeadRes lead : tcLeads) {
            // 分配： 负责人是自己且状态是待分配、已分配、已回收、跟进中、静默    ||  负责人是空且状态是待分配、已回收、跟进中、静默
            lead.setAssignable(Objects.equals(lead.getOwner(), userId) && ASSIGNABLE_STATUS.contains(lead.getStatus()) || lead.getOwner() == null && ASSIGNABLE_STATUS_AUTO.contains(lead.getStatus()));
            // 回收： 当前用户是VP || 当前用户是线索负责人的上级角色用户 && 当前用户在线索的操作履历中
            if (SecurityUtils.isVp()) {
//                lead.setRecyclable(ALLOCATED.getCode().equals(lead.getStatus()));
                lead.setRecyclable(!INVALID.getCode().equals(lead.getStatus()) && !Objects.equals(lead.getOwner(), userId));
            } else {
                if(!INVALID.getCode().equals(lead.getStatus()) && !ObjectUtils.isEmpty(lead.getOwner())){
                    SysUser owner = sysUserService.selectUserById(lead.getOwner());
                    if(!ObjectUtils.isEmpty(owner)){
                        List<String> ownerRoleKeys = owner.getRoles().stream().map(SysRole::getRoleKey).collect(Collectors.toList());
                        // 判断当前用户是否存在于线索的操作历史中 （如果不是上级角色，不用查是否存在于操作历史中）
                        lead.setRecyclable(CollUtil.containsAny(ownerRoleKeys, nextRoleKeys) && tcLeadHistoryService.existsUser(userId, lead.getLeadId()));
                    }

                }
            }
            // 跟进/编辑 （当前用户是VP || 负责人是自己） && 状态非无效
            lead.setFollowable((SecurityUtils.isVp() || Objects.equals(lead.getOwner(), userId)) && !INVALID.getCode().equals(lead.getStatus()));
            // 删除: （（当前用户是VP || 负责人是自己） || （市场部线索 && 创建人是自己））&& 状态是待分配
            lead.setDeletable(TO_BE_ALLOCATED.getCode().equals(lead.getStatus()) && (SecurityUtils.isVp() || Objects.equals(lead.getOwner(), userId) || (null == lead.getOwner() && userId.equals(lead.getCreateBy()))));
            // 转化客户： （当前用户是VP || 负责人是自己） && 状态是跟进中
            lead.setConvertable((SecurityUtils.isVp() || Objects.equals(lead.getOwner(), userId)) && FOLLOWING.getCode().equals(lead.getStatus()));

            // 转化项目： （当前用户是VP || 负责人是自己） && 状态是转化客户
            lead.setConvertProjectable((SecurityUtils.isVp() || Objects.equals(lead.getOwner(), userId)) && CONVERTED_CUSTOMER.getCode().equals(lead.getStatus()));
            
            // 静默/无效：（当前用户是VP || 负责人是自己） && 状态非转化客户、转化项目、静默、无效
            lead.setSilentOrInvalidAble(!UN_SILENT_OR_INVALID_STATUS.contains(lead.getStatus()) && (SecurityUtils.isVp() || Objects.equals(lead.getOwner(), userId)));
            
            // 所属代理商总部赋值
            if (0L == lead.getOwnerAgent()) {
                lead.setOwnerAgentName(headquarters);
            }
        }
        return tcLeads;
    }
    public List<LeadRes> selectTcLeadListBase(LeadListReq tcLead) {
        SpecialCharacterEscaper.escapeSpecialCharacters(tcLead, LeadListReq::getLeadNo, LeadListReq::getAccountNo,
                LeadListReq::getCompany, LeadListReq::getOwnerName, LeadListReq::getContact, LeadListReq::getRequirement,
                LeadListReq::getAddress, LeadListReq::getCreateByNickName);

        tcLead.setPermissionChar(SecurityUtils.getPermissionCharIns());
        Long userId = SecurityUtils.getUserId();
        Assert.notNull(userId, "用户未登录");
        // 代理商销售经理的场合
        if (SecurityUtils.hasRole("agent_sales")) {
            tcLead.setLoginUserId(userId);
        }

        // List<TbArea> allArea = tbAreaMapper.selectTbAreaList(new AreaParam());
        // Map<Long, TbArea> areaId2AreaMap = allArea.stream().collect(Collectors.toMap(TbArea::getAreaId, Function.identity()));
        String lan = LocaleContextHolder.getLocale().getCountry();
        // TODO 但是使用这种方式显著降低了导出速度，需要优化 关于 区域和国家 数据可以内存化，使用 cacheManager
        tcLead.setLan(lan);

        List<LeadRes> leadRes = baseMapper.selectTcLeadList(tcLead);
        // LocalUtils.setLocalAreaAndCountryNameBatch(leadRes, LeadRes::getAreaId, LeadRes::getCountryId, LeadRes::setAreaName, LeadRes::setCountryName);
        return leadRes;
    }

    /**
     * 新增线索管理
     *
     * @param lead 线索管理
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean insertTcLead(LeadFormReq lead) {
        // TODO 将这个状态改成枚举
        // 线索编号：
        String leadNo = redisCache.generateSerialNumber(LEAF.getPrefix());
        lead.setLeadNo(leadNo);
        lead.setStatus(TO_BE_ALLOCATED.getCode());
        lead.setPermissionChar(SecurityUtils.getPermissionCharIns());

        // Optional.ofNullable(lead.getLeadTime()).ifPresent(leadTime -> {
        //    lead.setLeadTime(DateUtils.CNTimeDate(lead.getLeadTime()));
        //});

        // 如果创建线索用户是区域:全球，所属：总部；即表示是市场部创建的线索，负责人是空。 其他情况，负责人：自己
        if (SecurityUtils.getDeptId() != 0L || !SecurityUtils.getLoginUser().getAreaIdList().contains(0L)) {
            lead.setOwner(SecurityUtils.getUserId());
            // 如果是代理商创建的线索，所属代理商是自己，并且分配给自己
            if (SecurityUtils.getDeptId() != 0L) {
                lead.setOwnerAgent(SecurityUtils.getDeptId());
                lead.setStatus(ALLOCATED.getCode());
            }
        }

        // int rows = leadMapper.insertTcLead(lead);
        boolean r1 = save(lead);
        // 保存附件
        saveLeadFile(lead);
        if (!r1) {
            throw new RuntimeException("保存线索失败");
        }
        return r1;
    }

    /**
     * 修改线索管理
     *
     * @param tcLead 线索管理
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateTcLead(LeadFormReq tcLead) {
        // 删除附件 url
        if (CollUtil.isNotEmpty(tcLead.getTbFiles())) {
            tbFileService.deleteTbFileByIds(tcLead.getTbFiles().stream().map(TbFile::getId).toArray(Long[]::new));
        }

        // Optional.ofNullable(tcLead.getLeadTime()).ifPresent(leadTime -> {
        //     tcLead.setLeadTime(DateUtils.CNTimeDate(leadTime, SecurityUtils.getTimeZone()));
        // });
        // 保存附件
        saveLeadFile(tcLead);
        return updateById(tcLead);
    }

    private void saveLeadFile(LeadFormReq lead) {
        // 保存附件
        String annex = lead.getAnnex();
        if (StringUtils.isNotBlank(annex)) {
            for (String fileUrl : annex.split(",")) {
                // 获取文件名：
                // 处理空格
                fileUrl = fileUrl.replaceAll(" ", "%20");
                // 解析URL
                String path = URLUtil.getPath(fileUrl);
                // 从路径中获取文件名
                String fileName = FileUtil.getName(path);
                TbFile tbFile = new TbFile(lead.getLeadId(), FileBuzType.LEAD.getType(), fileName, fileName, fileUrl);
                tbFile.setCreateBy(SecurityUtils.getUserId().toString());
                tbFile.setCreateTime(new Date());
                tbFileService.insertTbFile(tbFile);
            }
        }
    }
//
//    /**
//     * 批量删除线索管理
//     *
//     * @param leadIds 需要删除的线索管理主键
//     * @return 结果
//     */
//    @Override
//    public int deleteTcLeadByLeadIds(Long[] leadIds) {
//        return baseMapper.deleteTcLeadByLeadIds(leadIds);
//    }
//
//    /**
//     * 删除线索管理信息
//     *
//     * @param leadId 线索管理主键
//     * @return 结果
//     */
//    @Override
//    public int deleteTcLeadByLeadId(Long leadId) {
//        return baseMapper.deleteTcLeadByLeadId(leadId);
//    }

    /**
     * 查询客户相关线索
     *
     * @param tcLead 线索
     * @return 结果
     */
    @Override
    public List<TcLeadForRelated> selectLeadListByAccount(LeadRelatedQueryReq tcLead) {
        return baseMapper.selectLeadListByAccount(tcLead);
    }

    /**
     * 回收
     *
     * @param recovery 线索
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean recovery(RecoveryReq recovery) {
        Long userId = SecurityUtils.getUserId();
        Long msgTo = recovery.getOwner();
        TcLead tcLead = new TcLead();
        tcLead.setLeadId(recovery.getLeadId());
        // 如果原因是失效，更新线索状态; // 99:失效; // 8:回收
        if ("99".equals(recovery.getRecoveryType())) {
            tcLead.setStatus(INVALID.getCode());
            tcLead.setUnvalidReason(recovery.getRecoveryReason());
        } else {
            tcLead.setOwner(userId);
            // 根据区域和代理商判断
            // if (SecurityUtils.getDeptId() != 0L) {
            //     // 代理商：ownerAgent: 设为自己
            //     tcLead.setOwnerAgent(SecurityUtils.getDeptId());
            // }
            tcLead.setOwnerAgent(SecurityUtils.getDeptId());
            // 回收状态
            tcLead.setStatus(RECYCLED.getCode());
        }
        tcLead.setPermissionChar(SecurityUtils.getPermissionCharIns());

        // tcLead.setUpdateBy(userId.toString());
        // tcLead.setUpdateTime(new Date());
        // int r1 = baseMapper.updateTcLead(tcLead);
        boolean r1 = updateById(tcLead);

        // 履历
        TcLeadHistory history = BeanUtil.copyProperties(recovery, TcLeadHistory.class);
        history.setOriUser(userId);
        // history.setCreateBy(userId.toString());
        // history.setCreateTime(new Date());
        // int r2 = tcLeadHistoryMapper.insertTcLeadHistory(history);

        boolean r2 = tcLeadHistoryService.save(history);

        Message message = new Message();
        // message.setMessageContent("您有一条线索被回收，请注意查看。线索编号：" + recovery.getLeadNo());
        // sseEmitterService.sendSaveMessage(Collections.singletonList(msgTo), message);
        Map<String, String> allMessage = MessageUtils.allMessage("lead.message.recovered");
        message.setMessageType(MessageTypeEnum.LEAD.getValue());
        message.setMessageContent(allMessage.get(CN) + recovery.getLeadNo());
        message.setMessageContentUS(allMessage.get(US) + recovery.getLeadNo());
        message.setMessageContentRU(allMessage.get(RU) + recovery.getLeadNo());
        message.setMessageContentES(allMessage.get(ES) + recovery.getLeadNo());
        message.setCreateBy(userId.toString());
        sseEmitterService.sendSaveMessage(Collections.singletonList(msgTo), message);
        // return r1 + r2;
        return r1 && r2;
    }

    @Override
    public List<TcLead> selectLeadsFromMarketAndUnassigned() {
        return baseMapper.selectLeadsFromMarketAndUnassigned();
    }


    /**
     * TODO 这里有大事务问题，需要处理
     *
     * @param allots 分配线索
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean allot(List<AllotReq> allots) {
        if (CollectionUtils.isEmpty(allots)) {
            throw new RuntimeException("分配线索信息不能为空");
        }

        Long userId = SecurityUtils.getUserId();

        // 校验并根据返回负责人信息
        final SysUser sysUser = checkAndHandleOwner(allots, userId);

        Date now = new Date();

        List<TcLead> tcLeads = new ArrayList<>();
        List<TcLeadHistory> histories = new ArrayList<>();

        for (AllotReq allot : allots) {
            TcLead tLead = BeanUtil.copyProperties(allot, TcLead.class);
            tLead.setUpdateBy(userId.toString());
            tLead.setUpdateTime(now);

            // 代理商用户设置所属代理商
            if (sysUser.getDeptId() != 0) {
                tLead.setOwnerAgent(sysUser.getDeptId());
                tLead.setPermissionChar(sysUser.getDeptId().toString());
            } else {
                tLead.setPermissionChar(sysUser.getAreaId());
            }
            tcLeads.add(tLead);

            // r1 += baseMapper.updateTcLead(tLead);
            TcLeadHistory history = new TcLeadHistory(allot.getLeadId(), userId, 1L, null, null, allot.getOwner(), userId, now, null);
            TcLeadHistory tHistory = BeanUtil.copyProperties(history, TcLeadHistory.class);
            histories.add(tHistory);

        }

        boolean r1 = updateBatchById(tcLeads);
        boolean r2 = tcLeadHistoryService.saveBatch(histories);

        Message message = new Message();
        // message.setMessageContent("您有一条线索待处理，请注意查看。线索编号：" + recovery.getLeadNo());
        Map<String, String> allMessage = MessageUtils.allMessage("lead.message.allot");
        message.setMessageType(MessageTypeEnum.LEAD.getValue());
        String leadNos = allots.stream().map(AllotReq::getLeadNo).collect(Collectors.joining(","));
        message.setMessageContent(allMessage.get(CN) + leadNos);
        message.setMessageContentUS(allMessage.get(US) + leadNos);
        message.setMessageContentRU(allMessage.get(RU) + leadNos);
        message.setMessageContentES(allMessage.get(ES) + leadNos);
        message.setCreateBy(userId.toString());
        sseEmitterService.sendSaveMessage(Collections.singletonList(allots.get(0).getOwner()), message);

        return r1 && r2;
    }

    /**
     * 检查并处理
     * TODO 批量分配还没处理
     */
    private SysUser checkAndHandleOwner(List<AllotReq> allots, Long userId) {

        if (CollUtil.isNotEmpty(allots)) {
            for (AllotReq allot : allots) {
                // TcLead curLead = baseMapper.selectTcLeadByLeadId(allot.getLeadId());
                TcLead curLead = getById(allot.getLeadId());
                // TODO i18n
                if (curLead == null) {
                    throw new RuntimeException("线索不存在，线索id：【" + allot.getLeadId() + "】");
                } else if (!userId.toString().equals(curLead.getCreateBy()) && !userId.equals(curLead.getOwner())) {
                    // 只有创建人或者owner才能分配
                    throw new RuntimeException("只有创建人或者owner才能分配，线索编号：" + curLead.getLeadNo());
                } else if (!ASSIGNABLE_STATUS.contains(curLead.getStatus())) {
                    // 只有未分配和已分配才能分配
                    throw new RuntimeException("只有未分配、已分配和已回收才能分配, 线索编号：" + curLead.getLeadNo());
                }

            }
        } else {
            throw new RuntimeException("线索ID不能为空");
        }

        // 分配到对方的权限字符
        SysUser sysUser = sysUserService.selectUserById(allots.get(0).getOwner());
        if (sysUser == null) {
            throw new RuntimeException("分配用户不存在");
        }

        return sysUser;
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean convertCustomer(ConvertReq convertReq) {
        List<Long> leadIds = convertReq.getLeadIds();
        if (CollectionUtils.isEmpty(leadIds)) {
            throw new RuntimeException("leadIds is empty");
        }
        List<TcLead> tcLeads = leadIds.stream().map(leadId -> TcLead.builder()
                .relativeAccount(convertReq.getRelativeAccount())
                .status(CONVERTED_CUSTOMER.getCode())
                .leadId(leadId)
                .build()).collect(Collectors.toList());
        boolean r1 = updateBatchById(tcLeads);

        // 查询线索联系人信息
        List<TcLead> leadList = list(Wrappers.lambdaQuery(TcLead.class).in(TcLead::getLeadId, leadIds));

        // 查询当前用户全部联系人
        List<TcAccountContact> currentAll = tcAccountContactService.list(Wrappers.lambdaQuery(TcAccountContact.class).eq(TcAccountContact::getAccountId, convertReq.getRelativeAccount()));

        // 根据 name 过滤并组装客户联系人
        List<TcAccountContact> candidate = leadList.stream()
                .filter(item -> currentAll.stream().noneMatch(all -> all.getContactName().equals(item.getContactPerson())))
                .map(item ->
                        TcAccountContact.builder()
                                .accountId(convertReq.getRelativeAccount())
                                .contactName(item.getContactPerson())
                                .email(item.getEmail())
                                .phone(item.getContact())
                                .jobTitle(item.getJobTitle())
                                .build()
                ).collect(Collectors.toList());
        
        // 插入联系人
        tcAccountContactService.saveBatch(candidate);

        List<Long> existLeadIds = tcLeadHistoryService.listObjs(Wrappers.<TcLeadHistory>lambdaQuery()
                        .select(TcLeadHistory::getLeadId)
                        .in(TcLeadHistory::getLeadId, leadIds)
                        .eq(TcLeadHistory::getActionType, LeadHisTypeEnum.CONVERT_CUSTOMER.getCode()),
                o -> Long.valueOf(o.toString()));
        

        if (leadIds.size() == existLeadIds.size()) {
            return r1;
        }

        // 履历 如果已经转化客户，不再记录
        List<TcLeadHistory> histories = leadIds.stream()
                .filter(leadId -> !existLeadIds.contains(leadId))
                .map(leadId -> TcLeadHistory.builder()
                        .leadId(leadId)
                        .oriUser(SecurityUtils.getUserId())
                        .actionType(3L)
                        .getUser(convertReq.getRelativeAccount())
                        .build()).collect(Collectors.toList());
        
        boolean r2 = tcLeadHistoryService.saveBatch(histories);
        
        return r1 && r2;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean convertProject(Long leadId, Long relativeProject) {
        Assert.notNull(leadId, MessageUtils.message("lead.convert.validLeadId"));
        Assert.notNull(relativeProject, MessageUtils.message("lead.convert.validProjectId"));

        TcLead byId = getById(leadId);
        if (byId == null) {
            throw new RuntimeException(MessageUtils.message("lead.convert.notExistLead"));
        } else if (!CONVERTED_CUSTOMER.getCode().equals(byId.getStatus()) && !CONVERTED_PROJECT.getCode().equals(byId.getStatus())) {
            // 只有转化客户才能转化项目
            throw new RuntimeException(MessageUtils.message("lead.convert.mustBeConvertCust"));
        }

        TcLead tcLead = new TcLead();
        tcLead.setStatus(CONVERTED_PROJECT.getCode());
        tcLead.setLeadId(leadId);
        tcLead.setRelativeProject(relativeProject);

        boolean r1 = updateById(tcLead);

        // 履历 如果已经转化项目，不再记录
        boolean exists = tcLeadHistoryService.getBaseMapper()
                .exists(Wrappers.<TcLeadHistory>lambdaQuery()
                        .eq(TcLeadHistory::getLeadId, leadId)
                        .eq(TcLeadHistory::getActionType, LeadHisTypeEnum.CONVERT_PROJECT.getCode())
                        .eq(TcLeadHistory::getGetUser, relativeProject));
        if (exists) {
            return r1;
        }
        TcLeadHistory history = new TcLeadHistory().setLeadId(leadId).setActionType(LeadHisTypeEnum.CONVERT_PROJECT.getCode()).setOriUser(SecurityUtils.getUserId()).setGetUser(relativeProject);
        boolean r2 = tcLeadHistoryService.save(history);
        return r1 && r2;
    }


    /**
     * 客户统计
     * 下面是sql版本
     * <p>
     * SELECT DATE_FORMAT(lead_time, '%Y-%m') AS month,
     * COUNT(*)                        AS lead_count
     * FROM tc_lead
     * WHERE lead_time BETWEEN DATE_SUB(NOW(), INTERVAL 6 MONTH) AND NOW()
     * GROUP BY DATE_FORMAT(lead_time, '%Y-%m');
     *
     */
    @Override
    public BarChartVo getLeadCountStat(LocalDate startDate, LocalDate endDate, String statMode) {
        LocalDateTime start;
        LocalDateTime end;



        Function<LocalDateTime, LocalDateTime> fun;
        String pattern;
        LocalTime endTime = LocalTime.of(23, 59, 59);

        switch (statMode) {
            case "day":
                start = startDate.atStartOfDay();
                end = LocalDateTime.of(endDate, endTime);
                fun = date -> date.plusDays(1);
                pattern = "yyyy-MM-dd";
                break;
            case "month":
                start = startDate.withDayOfMonth(1).atStartOfDay();
                end = LocalDateTime.of(endDate.withDayOfMonth(endDate.lengthOfMonth()), endTime);
                fun = date -> date.plusMonths(1);
                pattern = "yyyy-MM";
                break;
            case "year":
                start = startDate.withDayOfYear(1).atStartOfDay();
                end = LocalDateTime.of(endDate.withDayOfYear(endDate.lengthOfYear()), endTime);
                fun = date -> date.plusYears(1);
                pattern = "yyyy";
                break;
            default:
                throw new RuntimeException(MessageUtils.message("board.statistical.model.error"));
        }
        // endDate = fun.apply(endDate);


        // 查询数据 // TODO 这里暂时使用 stream 处理了，因为 crm 系统数据量不会太大。如果后续耗时过长使用本方法 javadoc 中的 sql修改
        List<TcLead> tcLeads = baseMapper.selectLeadCountStat(start, end, SecurityUtils.getPermissionCharIns());
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        Map<String, Long> collect = tcLeads.stream().filter(lead -> lead.getCreateTime() != null).collect(Collectors.groupingBy(lead -> sdf.format(lead.getCreateTime()), Collectors.counting()));
        // 使用有序map
        collect = new TreeMap<>(collect);

        // 补全数据; 按照年月日处理
        while (start.isBefore(end)) {
            String key = sdf.format(DateUtil.date(start));
            collect.putIfAbsent(key, 0L);
            start = fun.apply(start);
        }

        return new BarChartVo(collect.keySet(), collect.values());

    }

    @Override
    public List<LeadRes> curAgent() {
        return baseMapper.curAgent(SecurityUtils.getPermissionCharIns());
    }


    // TODO 这个方法代码结构需要重构
    @Override
    public ExcelUtils.UploadResult<LeadInfoHelper> checkLeadImport(BasicTableInfoHelper basicTableInfoHelper, List<LeadInfoHelper> leadInfoHelpers) {

        // fast fail
        if (CollectionUtils.isEmpty(leadInfoHelpers)) {
            return ExcelUtils.checkData(leadInfoHelpers, it -> new ArrayList<>());
        }


        // 获取当前i18n
        Locale locale = LocaleContextHolder.getLocale();
        Function<SysDictData, String> dictGetter;
        Function<TbArea, String> areaGetter;
        Function<TbCountry, String> countryGetter;

        switch (locale.getCountry()) {
            case US:
                dictGetter = SysDictData::getDictLabelUS;
                areaGetter = TbArea::getAreaNameUS;
                countryGetter = TbCountry::getCountryNameUS;
                break;
            case RU:
                dictGetter = SysDictData::getDictLabelRU;
                areaGetter = TbArea::getAreaNameRU;
                countryGetter = TbCountry::getCountryNameRU;
                break;
            case ES:
                dictGetter = SysDictData::getDictLabelES;
                areaGetter = TbArea::getAreaNameES;
                countryGetter = TbCountry::getCountryNameES;
                break;
            default:
                dictGetter = SysDictData::getDictLabel;
                areaGetter = TbArea::getAreaName;
                countryGetter = TbCountry::getCountryName;
        }

        // TODO 收集成 map 集合
        // TODO cache 2 redis?
        // 来源
        CompletableFuture<Map<String, SysDictData>> originTask = asyncLoadDictFuture(dictGetter, "lead_source");
        
        // 行业
        // CompletableFuture<Map<String, SysDictData>> sectorTask = asyncLoadDictFuture(dictGetter, "lead_industry");

        // 优先级
        // CompletableFuture<Map<String, SysDictData>> priorityTask = CompletableFuture.supplyAsync(() -> getLocalLabel2DictMap(dictGetter, "lead_priority"), threadPoolTaskExecutor);
        CompletableFuture<Map<String, SysDictData>> priorityTask = asyncLoadDictFuture(dictGetter, "lead_priority");

        // // 类型
        // CompletableFuture<Map<String, SysDictData>> typeTask = asyncLoadDictFuture(dictGetter, "account_type");

        // 公司类型 lead_company_type
        CompletableFuture<Map<String, SysDictData>> companyTypeTask = asyncLoadDictFuture(dictGetter, "company_type");

        // 项目类型
        CompletableFuture<Map<String, SysDictData>> projectTypeTask = asyncLoadDictFuture(dictGetter, "project_type");
        
        // 需求类型
        CompletableFuture<Map<String, SysDictData>> requirementTask = asyncLoadDictFuture(dictGetter, "lead_requirements");
        
        // 产品意向
        CompletableFuture<Map<String, SysDictData>> productIntentionTask = asyncLoadDictFuture(dictGetter, "intentional_product");

        //  区域 + 国家
        CompletableFuture<Map<String, AreaWithCountriesVo>> areaWithCountriesMap = CompletableFuture.supplyAsync(() -> {
            List<AreaWithCountriesVo> tbCountries = tbAreaService.selectTbAreaWithCountriesList(new TbArea());
            return tbCountries.stream().collect(Collectors.toMap(areaGetter, area -> area));
        }, threadPoolTaskExecutor);

        // 汇总任务，并且处理异常
        CompletableFuture<Void> allOf = CompletableFuture.allOf(originTask, priorityTask,
                companyTypeTask, projectTypeTask, requirementTask, productIntentionTask,
                areaWithCountriesMap).exceptionally(e -> {
            throw new RuntimeException("字典数据异常", e);
        });

        allOf.join();

        Map<String, SysDictData> originMap = originTask.join();
        // Map<String, SysDictData> sectorMap = sectorTask.join();
        Map<String, SysDictData> priorityMap = priorityTask.join();
        // Map<String, SysDictData> typeMap = typeTask.join();
        Map<String, AreaWithCountriesVo> areaWithCountriesVoMap = areaWithCountriesMap.join();
        Map<String, SysDictData> companyTypeMap = companyTypeTask.join();
        Map<String, SysDictData> projectTypeMap = projectTypeTask.join();
        Map<String, SysDictData> requirementMap = requirementTask.join();
        Map<String, SysDictData> productIntentionMap = productIntentionTask.join();
        

        ExcelUtils.UploadResult<LeadInfoHelper> result = ExcelUtils.checkData(leadInfoHelpers, it -> {
            List<String> businessErrors = new ArrayList<>();

            // TODO 抽取 公共校验
            it.setOrigin(Optional.ofNullable(originMap.get(it.getOrigin())).map(SysDictData::getDictValue).orElseGet(() -> {
                // TODO 来源应该可以通过 excel 获取
                if (StringUtils.isNotBlank(it.getOrigin())) {
                    // businessErrors.add("来源【" + it.getOrigin() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.origin.not.exist", it.getOrigin()));
                }
                return null;
            }));
            // 区域
            AreaWithCountriesVo vo = areaWithCountriesVoMap.get(it.getAreaName());
            if (vo != null) {
                it.setAreaId(vo.getAreaId());
                // TbCountry country = vo.getCountryList().stream().filter(c -> c.getCountryName().equals(it.getCountryName())).findFirst().orElse(null);
                TbCountry country = vo.getCountryList().stream().filter(c -> countryGetter.apply(c).equals(it.getCountryName())).findFirst().orElse(null);
                if (country != null) {
                    it.setCountryId(country.getCountryId());
                } else {
                    if (StringUtils.isNotBlank(it.getCountryName())) {
                        // businessErrors.add("区域【" + it.getAreaName() + "】下不存在国家【" + it.getCountryName() + "】");
                        businessErrors.add(MessageUtils.message("lead.export.area.country.not.exist", it.getAreaName(), it.getCountryName()));
                    }
                }
            } else {
                if (StringUtils.isNotBlank(it.getAreaName())) {
                    // businessErrors.add("区域【" + it.getAreaName() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.area.not.exist", it.getAreaName()));
                }
            }

            it.setPriority(Optional.ofNullable(priorityMap.get(it.getPriority())).map(SysDictData::getDictValue).orElseGet(() -> {
                if (StringUtils.isNotBlank(it.getPriority())) {
                    // businessErrors.add("优先级【" + it.getPriority() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.priority.not.exist", it.getPriority()));
                }
                return null;
            }));

            // it.setType(Optional.ofNullable(typeMap.get(it.getType())).map(SysDictData::getDictValue).orElseGet(() -> {
            //     if (StringUtils.isNotBlank(it.getType())) {
            //         // businessErrors.add("客户类型【" + it.getType() + "】不存在");
            //         businessErrors.add(MessageUtils.message("lead.export.customer.type.not.exist", it.getType()));
            //     }
            //     return null;
            // }));

            it.setCompanyType(Optional.ofNullable(companyTypeMap.get(it.getCompanyType())).map(SysDictData::getDictValue).orElseGet(() -> {
                if (StringUtils.isNotBlank(it.getCompanyType())) {
                    // businessErrors.add("公司类型【" + it.getCompanyType() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.company.type.not.exist", it.getCompanyType()));
                }
                return null;
            }));

            it.setSegment(Optional.ofNullable(projectTypeMap.get(it.getSegment())).map(SysDictData::getDictValue).orElseGet(() -> {
                if (StringUtils.isNotBlank(it.getSegment())) {
                    // businessErrors.add("项目类型【" + it.getSegment() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.project.type.not.exist", it.getSegment()));
                }
                return null;
            }));

            it.setRequirementType(Optional.ofNullable(requirementMap.get(it.getRequirementType())).map(SysDictData::getDictValue).orElseGet(() -> {
                if (StringUtils.isNotBlank(it.getRequirementType())) {
                    // businessErrors.add("需求类型【" + it.getRequirementType() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.requirement.type.not.exist", it.getRequirementType()));
                }
                return null;
            }));
            it.setIntentionalProduct(Optional.ofNullable(productIntentionMap.get(it.getIntentionalProduct())).map(SysDictData::getDictValue).orElseGet(() -> {
                if (StringUtils.isNotBlank(it.getIntentionalProduct())) {
                    // businessErrors.add("产品意向【" + it.getIntentionalProduct() + "】不存在");
                    businessErrors.add(MessageUtils.message("lead.export.product.intent.not.exist", it.getIntentionalProduct()));
                }
                return null;
            }));
            

            // 邮箱和联系方式二选一
            if (StringUtils.isBlank(it.getEmail()) && StringUtils.isBlank(it.getContact())) {
                // businessErrors.add("邮箱和联系方式至少填写一项");
                businessErrors.add(MessageUtils.message("lead.export.email.contact.required"));
            }

            return businessErrors;
        });
        return result;
    }

    /**
     * 异步加载字典数据任务
     *
     * @param dictGetter    字典获取器
     * @param dictType    字典类型
     */
    private CompletableFuture<Map<String, SysDictData>> asyncLoadDictFuture(Function<SysDictData, String> dictGetter, String dictType) {
        return CompletableFuture.supplyAsync(() -> getLocalLabel2DictMap(dictGetter, dictType), threadPoolTaskExecutor);
    }

    private Map<String, SysDictData> getLocalLabel2DictMap(Function<SysDictData, String> dictGetter, String type) {
        List<SysDictData> priorities = sysDictDataMapper.selectDictDataByType(type);
        return priorities.stream().collect(Collectors.toMap(dictGetter, dict -> dict));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<String> insertLeadImportExcel(BasicTableInfoHelper basicTableInfoHelper, List<LeadInfoHelper> transform) {
        // TODO 手动事务

        List<TcLead> tcLeads = transform.stream().map(helper -> {
            TcLead tcLead = BeanUtil.copyProperties(helper, TcLead.class);
            tcLead.setCreateBy(SecurityUtils.getUserId() + "");
            tcLead.setCreateTime(new Date());
            // Optional.ofNullable(tcLead.getLeadTime()).ifPresent(leadTime -> {
            //     tcLead.setLeadTime(DateUtils.CNTimeDate(leadTime, SecurityUtils.getTimeZone()));
            // });
            tcLead.setStatus(TO_BE_ALLOCATED.getCode());
            tcLead.setPermissionChar(SecurityUtils.getPermissionCharIns());

            // 如果创建线索用户是区域:全球，所属：总部；即表示是市场部创建的线索，所属是空。 其他情况，所属：自己
            if (SecurityUtils.getDeptId() != 0L || !SecurityUtils.getLoginUser().getAreaIdList().contains(0L)) {
                tcLead.setOwner(SecurityUtils.getUserId());
                // 如果是代理商创建的线索，所属代理商是自己，并且分配给自己
                if (SecurityUtils.getDeptId() != 0L) {
                    tcLead.setOwnerAgent(SecurityUtils.getDeptId());
                    tcLead.setStatus(ALLOCATED.getCode());
                }
            }
            String leadNo = redisCache.generateSerialNumber(LEAF.getPrefix());
            tcLead.setLeadNo(leadNo);
            return tcLead;
        }).collect(Collectors.toList());

        // baseMapper.batchInsert(tcLeads);
        boolean b = saveBatch(tcLeads, 100);


        return Collections.emptyList();

    }

    @Override
    public List<TcLead> listByCurUser(LeadForAccountReq forAccountReq) {
        forAccountReq.setOwner(SecurityUtils.getUserId());
        forAccountReq.setPermissionChar(SecurityUtils.getPermissionCharIns());
        return baseMapper.listByCurUser(forAccountReq);
    }
    @Override
    public List<LeadRes> curCountry() {
        List<LeadRes> leadRes = baseMapper.curCountry(SecurityUtils.getPermissionCharIns(), LocaleContextHolder.getLocale().getCountry());
        // 去重
        leadRes = leadRes.stream().distinct().collect(Collectors.toList());
        
        return leadRes;
    }

    @Override
    public List<Pair<Long, String>> selectAreasByCurUser() {
        List<Long> areaIds =  baseMapper.selectAreaIdsByCurUser(SecurityUtils.getPermissionCharIns());
        return LocalUtils.selectAreasByCurUser(areaIds);
    }

    @Override
    public List<Pair<Long, String>> selectCountriesByCurUser() {
        List<Long> countryIds = baseMapper.selectCountryIdsByCurUser(SecurityUtils.getPermissionCharIns());
        return LocalUtils.selectCountriesByCurUser(countryIds);
    }

    @Override
    public List<LeadRes> selectTcLeadListExport(LeadListReq tcLead) {
        List<LeadRes> leadRes = selectTcLeadListBase(tcLead);

        String headquarters = MessageUtils.message("agent.headquarters");
        for (LeadRes lead : leadRes) {
            // 所属代理商总部赋值
            if (0L == lead.getOwnerAgent()) {
                lead.setOwnerAgentName(headquarters);
            }
        }
        return leadRes;
    }

    @Override
    public boolean convertQuotation(Long leadId, Long quotationId) {
        convertPreCheck(leadId);

        // 转化报价
        boolean r1 = update(Wrappers.lambdaUpdate(TcLead.class)
                .eq(TcLead::getLeadId, leadId)
                .set(TcLead::getStatus, CONVERTED_QUOTE.getCode()));

        // 第一次转化添加履历
        boolean exists = tcLeadHistoryService.getBaseMapper()
                .exists(Wrappers.<TcLeadHistory>lambdaQuery()
                        .eq(TcLeadHistory::getLeadId, leadId)
                        .eq(TcLeadHistory::getActionType, LeadHisTypeEnum.CONVERT_QUOTE.getCode())
                        .eq(TcLeadHistory::getGetUser, quotationId));
        if (exists) {
            return r1;
        } else {
            TcLeadHistory history = TcLeadHistory.builder()
                    .leadId(leadId)
                    .actionType(LeadHisTypeEnum.CONVERT_QUOTE.getCode())
                    .oriUser(SecurityUtils.getUserId())
                    .getUser(quotationId)
                    .build();

            boolean r2 = tcLeadHistoryService.save(history);
            return r1 && r2;
        }

    }

    @Override
    public boolean convertQuotationFromProject(Long projectId, Long quotationId) {
        // 根据项目id查询线索id
        TcLead lead = getOne(Wrappers.<TcLead>lambdaQuery().eq(TcLead::getRelativeProject, projectId));
        if (lead == null) {
            // 当前项目没有关联线索
            log.info("转化报价：当前项目没有关联线索，项目id：{}", projectId);
            return true;
        }
        return SpringUtils.getAopProxy(this).convertQuotation(lead.getLeadId(), quotationId);
    }


    private void convertPreCheck(Long... leadIds) {
        //
        Assert.notEmpty(leadIds, MessageUtils.message("lead.convert.validLeadId"));
        // 校验是否存在
        List<TcLead> leads = listByIds(Arrays.asList(leadIds));
        if (leads.size() != leadIds.length) {
            throw new RuntimeException(MessageUtils.message("lead.convert.notExistLead"));
        }

        // 校验是否是转化项目
        if (leads.stream().anyMatch(lead -> !CONVERTED_PROJECT.getCode().equals(lead.getStatus()))) {
            throw new RuntimeException(MessageUtils.message("lead.convert.mustBeConvertProject"));
        }
    }

    @Override
    public boolean silentOrInvalid(SilentOrInvalidReq silentOrInvalidReq) {
        // 更新状态
        Wrapper<TcLead> u = Wrappers.lambdaUpdate(TcLead.class)
                .eq(TcLead::getLeadId, silentOrInvalidReq.getLeadId())
                .set(TcLead::getStatus, silentOrInvalidReq.getStatus());
        // new TcLead() 为了使自动填充生效
        boolean r1 = update(new TcLead(), u);

        // 履历
        Long actionType = INVALID.getCode().equals(silentOrInvalidReq.getStatus())
                ? LeadHisTypeEnum.INVALID.getCode()
                : LeadHisTypeEnum.SILENT.getCode();
        Long userId = SecurityUtils.getUserId();
        TcLeadHistory history = new TcLeadHistory()
                .setLeadId(silentOrInvalidReq.getLeadId())
                .setOriUser(userId)
                .setActionType(actionType)
                .setRecoveryReason(silentOrInvalidReq.getRemark());
        boolean r2 = tcLeadHistoryService.save(history);
        return r1 && r2;
    }

}
