package com.fs.sop.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fs.common.config.FSSysConfig;
import com.fs.common.core.domain.R;
import com.fs.common.exception.base.BaseException;
import com.fs.common.utils.CloudHostUtils;
import com.fs.common.utils.PubFun;
import com.fs.common.utils.StringUtils;
import com.fs.common.utils.date.DateUtil;
import com.fs.company.domain.Company;
import com.fs.company.domain.CompanyDept;
import com.fs.company.domain.CompanyMiniapp;
import com.fs.company.mapper.CompanyMapper;
import com.fs.company.mapper.CompanyDeptMapper;
import com.fs.company.mapper.CompanyUserMapper;
import com.fs.company.service.ICompanyMiniappService;
import com.fs.company.util.MiniAppIdResolver;
import com.fs.config.cloud.CloudHostProper;
import com.fs.course.config.CourseConfig;
import com.fs.course.domain.*;
import com.fs.course.mapper.FsCourseDomainNameMapper;
import com.fs.course.mapper.FsCourseLinkMapper;
import com.fs.course.mapper.FsCourseWatchLogMapper;
import com.fs.course.mapper.FsUserCourseMapper;
import com.fs.course.param.FsCourseLinkCreateParam;
import com.fs.course.service.IFsCourseLinkService;
import com.fs.course.service.IFsUserCourseVideoService;
import com.fs.course.utils.FsCourseWatchLogProjectFiller;
import com.fs.fastGpt.domain.FastGptChatReplaceWords;
import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
import com.fs.feishu.service.FeiShuService;
import com.fs.feishu.util.FeishuDirectDocShardAllocator;
import com.fs.his.domain.FsUser;
import com.fs.his.mapper.FsUserMapper;
import com.fs.live.domain.LiveWatchLog;
import com.fs.live.mapper.LiveWatchLogMapper;
import com.fs.qw.domain.*;
import com.fs.qw.mapper.*;
import com.fs.qw.cache.QwSopCacheService;
import com.fs.qw.param.QwExtCourseSopWatchLog;
import com.fs.qw.param.QwExternalContactVOTime;
import com.fs.qw.param.QwTagSearchParam;
import com.fs.qw.service.IQwCompanyService;
import com.fs.qw.service.impl.AsyncSopTestService;
import com.fs.qw.service.impl.QwExternalContactServiceImpl;
import com.fs.qw.vo.GroupUserExternalVo;
import com.fs.qw.vo.QwSopCourseFinishTempSetting;
import com.fs.qw.vo.QwSopRuleTimeVO;
import com.fs.qw.vo.QwTagGroupListVO;
import com.fs.sop.domain.*;
import com.fs.sop.mapper.QwSopLogsMapper;
import com.fs.sop.mapper.QwSopMapper;
import com.fs.sop.mapper.SopUserLogsInfoMapper;
import com.fs.sop.mapper.SopUserLogsMapper;
import com.fs.sop.params.BatchSopUserLogsInfoParam;
import com.fs.sop.params.BatchSopUserLogsInfoParamU;
import com.fs.sop.params.SendUserLogsInfoMsgParam;
import com.fs.sop.params.SopUserLogsParamByDate;
import com.fs.sop.service.IQwMassSendLogService;
import com.fs.sop.service.IQwSopLogsService;
import com.fs.sop.service.IQwSopService;
import com.fs.sop.service.IQwSopTempVoiceService;
import com.fs.sop.service.ISopGenerationFailedLogService;
import com.fs.sop.service.ISopUserLogsInfoService;
import com.fs.sop.service.ISopUserLogsService;
import com.fs.sop.vo.ExtCourseSopWatchLogVO;
import com.fs.sop.vo.QwCreateLinkByAppVO;
import com.fs.sop.vo.SopUserLogsInfoVOE;
import com.fs.sop.vo.SopUserLogsVo;
import com.fs.system.domain.SysConfig;
import com.fs.system.mapper.SysConfigMapper;
import com.fs.system.service.ISysConfigService;
import com.fs.voice.utils.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.validation.ConstraintViolationException;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;

@Service
public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {

    private static final String APP_LINK_PREFIX = "/appcourse/pages/course/learning?course=";
    private static final String REAL_LINK_PREFIX = "/courseH5/pages/course/learning?course=";
    private static final String SHORT_LINK_PREFIX = "/courseH5/pages/course/learning?s=";
    private static final String miniappRealLink = "/pages_course/video.html?course=";
    private static final String appRealLink = "/pages/courseAnswer/index?link=";
    private static final String appActivitlLink = "/pages_course/activity.html?link=";
    private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
    private static final String registeredRealLink = "/pages_course/register.html?link=";
    private static final String feishuMiniappLink = "/pages_course/video?course=";
    private static final DateTimeFormatter FEISHU_FAIL_SEND_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//    private static final String miniappRealLink = "/pages/index/index?course=";

    @Autowired
    private ISysConfigService configService;

    @Autowired
    private FsCourseDomainNameMapper fsCourseDomainNameMapper;

    private static final Logger log = LoggerFactory.getLogger(SopUserLogsInfoServiceImpl.class);
    @Autowired
    private SopUserLogsInfoMapper sopUserLogsInfoMapper;

    @Autowired
    private FsCourseLinkMapper fsCourseLinkMapper;
    @Autowired
    private QwGroupChatMapper qwGroupChatMapper;

    @Autowired
    private FastGptChatReplaceWordsMapper fastGptChatReplaceWordsMapper;
    @Autowired
    private IFsCourseLinkService courseLinkService;

    @Autowired
    private SopUserLogsMapper sopUserLogsMapper;

    @Autowired
    private IQwSopService qwSopService;

    @Autowired
    private QwUserMapper qwUserMapper;

    @Autowired
    private QwSopMapper qwSopMapper;

    /** SOP 名称缓存（内部走 QwSopMapper @DataSource(SOP)） */
    @Autowired
    private QwSopCacheService qwSopCacheService;

    @Autowired
    private QwSopLogsMapper qwSopLogsMapper;

    @Autowired
    private ISopUserLogsService sopUserLogsService;

    @Autowired
    private FsCourseWatchLogMapper fsCourseWatchLogMapper;

    @Autowired
    private FsCourseWatchLogProjectFiller fsCourseWatchLogProjectFiller;

    @Autowired
    private AsyncSopTestService asyncSopTestService;

    @Autowired
    private IFsCourseLinkService fsCourseLinkService;
    @Autowired
    private QwTagMapper qwTagMapper;

    @Autowired
    private QwExternalContactMapper qwExternalContactMapper;

    @Autowired
    private QwExternalContactServiceImpl qwExternalContactService;

    @Autowired
    private IQwCompanyService iQwCompanyService;

    @Autowired
    private IFsUserCourseVideoService iFsUserCourseVideoService;

    @Autowired
    private FsUserCourseMapper fsUserCourseMapper;


    @Autowired
    private CompanyUserMapper companyUserMapper;
    @Autowired
    private QwGroupChatUserMapper qwGroupChatUserMapper;

    @Autowired
    private ICompanyMiniappService companyMiniappService;
    @Autowired
    private CloudHostProper cloudHostProper;

    @Autowired
    private CompanyMapper companyMapper;

    @Autowired
    private IQwMassSendLogService qwMassSendLogService;

    @Autowired
    private CompanyDeptMapper companyDeptMapper;

    @Autowired
    private IQwSopTempVoiceService sopTempVoiceService;

    @Autowired
    LiveWatchLogMapper liveWatchLogMapper;

    @Autowired
    private LuckyBagMapper luckyBagMapper;

    @Autowired
    private LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;

    @Autowired
    private FsUserMapper fsUserMapper;

    @Autowired
    private IQwSopLogsService qwSopLogsService;

    @Autowired
    private FeiShuService feiShuService;

    @Autowired
    private ISopGenerationFailedLogService sopGenerationFailedLogService;


    @Override
    public void save(SopUserLogsInfo sopUserLogsInfo) {
        sopUserLogsInfoMapper.insertSopUserLogsInfo(sopUserLogsInfo);
    }

    @Override
    public void update(SopUserLogsInfo info) {
        sopUserLogsInfoMapper.updateById(info);
    }

    @Override
    public void updateSopUserInfoByExternalId(Long qwExternalId, Long userId) {
        sopUserLogsInfoMapper.updateQwExternalContactChangeUserId(qwExternalId,userId);
    }

    @Override
    public SopUserLogsInfo getById(String id) {
        return sopUserLogsInfoMapper.selectById(id);
    }

    @Override
    public List<SopUserLogsInfo> getAll() {
        return sopUserLogsInfoMapper.selectAll();
    }

    @Override
    public void deleteById(String id) {
        sopUserLogsInfoMapper.deleteById(id);
    }

    @Override
    public int deleteSopUserLogsInfoByIds(String[] ids) {

        List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoByIds(ids);
        List<Map<String, Object>> result = sopUserLogsInfos.stream()
                .map(info -> {
                    Map<String, Object> fields = new HashMap<>();
                    fields.put("sopId", info.getSopId());
                    fields.put("exterId", info.getExternalContactId());
                    fields.put("qwUserId", info.getQwUserId());
                    fields.put("cropId", info.getCorpId());
                    return fields;
                })
                .collect(Collectors.toList());
        log.info("删除客户营期-"+result);

        return sopUserLogsInfoMapper.deleteSopUserLogsInfoByIds(ids);
    }

    @Override
    public List<SopUserLogsInfo> selectSopUserLogsInfoList(SopUserLogsInfo info) {

        return sopUserLogsInfoMapper.selectSopUserLogsInfoList(info);
    }
    @Override
    public List<SopUserLogsInfoVOE> selectSopUserLogsInfoListVO(SopUserLogsInfo info) {
        return  sopUserLogsInfoMapper.selectSopUserLogsInfoListVO(info);
    }


    @Override
    public void batchInsertSopUserLogsInfo(List<SopUserLogsInfo> logsToInsert) {
        sopUserLogsInfoMapper.batchInsertSopUserLogsInfo(logsToInsert);
    }

    @Override
    public void batchUpdateSopUserLogsInfoByMoreStudy(List<QwExternalContact> contactList) {
        List<QwExternalContact> validList = contactList.stream()
                .filter(c -> c.getSopUserLogsInfoId() != null)
                .collect(Collectors.toList());
        if (!validList.isEmpty()) {
            sopUserLogsInfoMapper.batchUpdateSopUserLogsInfoByMoreStudy(validList);
        }
    }

    @Override
    public void batchUpdateSopUserLogsInfoByIsDaysNotStudy(List<QwExternalContact> contactList) {
        List<QwExternalContact> validList = contactList.stream()
                .filter(c -> c.getSopUserLogsInfoId() != null)
                .collect(Collectors.toList());
        if (!validList.isEmpty()) {
            sopUserLogsInfoMapper.batchUpdateSopUserLogsInfoByIsDaysNotStudy(validList);
        }
    }

    @Override
    public void batchUpdateSopUserLogsInfoByLevel(List<QwExternalContact> contactList) {
        List<QwExternalContact> validList = contactList.stream()
                .filter(c -> c.getSopUserLogsInfoId() != null)
                .collect(Collectors.toList());
        if (!validList.isEmpty()) {
            sopUserLogsInfoMapper.batchUpdateSopUserLogsInfoByLevel(validList);
        }
    }

    @Override
    public void insertSopUserLogsInfo(SopUserLogsInfo logsToInsert) {
        sopUserLogsInfoMapper.insertSopUserLogsInfo(logsToInsert);
    }

    /**
     * 查询sopUserLogsInfo
     *
     * @param id sopUserLogsInfo主键
     * @return sopUserLogsInfo
     */
    @Override
    public SopUserLogsInfo selectSopUserLogsInfoById(String id)
    {
        return sopUserLogsInfoMapper.selectSopUserLogsInfoById(id);
    }

    /**
     * 修改sopUserLogsInfo
     *
     * @param logsInfo sopUserLogsInfo
     * @return 结果
     */
    @Override
    public R updateSopUserLogsInfoToTime(SopUserLogsInfo logsInfo)
    {


        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            logsInfo.setUpdateTime(sdf.format(new Date()));

            //用于查询
            SopUserLogs userLogs=new SopUserLogs();
            userLogs.setSopId(logsInfo.getSopId());
            userLogs.setQwUserId(logsInfo.getQwUserId());
            userLogs.setCorpId(logsInfo.getCorpId());
            userLogs.setStatus(1);
            userLogs.setStartTime(logsInfo.getCreateTime());

            String  unionSopStartId = sopUserLogsService.selectSopUserLogsByUpdate(userLogs);


            SopUserLogsInfo sopUserLogsInfo=new SopUserLogsInfo();
            sopUserLogsInfo.setId(logsInfo.getId());
            sopUserLogsInfo.setQwUserId(logsInfo.getQwUserId());
            sopUserLogsInfo.setCorpId(logsInfo.getCorpId());
            sopUserLogsInfo.setExternalContactId(logsInfo.getExternalContactId());
            sopUserLogsInfo.setExternalId(logsInfo.getExternalId());
            sopUserLogsInfo.setSopId(logsInfo.getSopId());
            sopUserLogsInfo.setExternalUserName(logsInfo.getExternalUserName());
            sopUserLogsInfo.setCreateTime(logsInfo.getCreateTime());
            sopUserLogsInfo.setUpdateTime(sdf.format(new Date()));
            sopUserLogsInfo.setCrtTime(logsInfo.getCrtTime());

            //查询sop任务开始时间营期表 是否有这个营期
            if (!StringUtil.strIsNullOrEmpty(unionSopStartId)){
                //有这个营期，则更新
                sopUserLogsInfo.setUserLogsId(unionSopStartId);

                int i = sopUserLogsInfoMapper.updateSopUserLogsInfoToTime(sopUserLogsInfo);


            } else {
                //如果查不出来营期 ，则新建营期
                //查询sop的模板id
                QwSop qwSop = qwSopService.selectQwSopById(logsInfo.getSopId());

                //查询这个sop任务的员工的信息
                QwUser qwUser = qwUserMapper.selectQwUserByQwUseridAndCorpId(logsInfo.getQwUserId(), logsInfo.getCorpId());

                SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
                userLogsParamByDate.setSopId(logsInfo.getSopId());
                userLogsParamByDate.setSopTempId(qwSop.getTempId());
                userLogsParamByDate.setQwUserId(logsInfo.getQwUserId());
                userLogsParamByDate.setCorpId(logsInfo.getCorpId());
                userLogsParamByDate.setStartTime(logsInfo.getCreateTime());
                userLogsParamByDate.setStatus(1);
                userLogsParamByDate.setUserId(qwUser.getId()+"|"+qwUser.getCompanyUserId()+"|"+qwUser.getCompanyId());

                //如果没有这个营期没有就先插入
                sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);

                userLogs.setSopTempId(qwSop.getTempId());
                //获取新的
                String  unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);

                sopUserLogsInfo.setUserLogsId(unionSopStartIdNew);
                sopUserLogsInfo.setId(logsInfo.getId());
                int i = sopUserLogsInfoMapper.updateSopUserLogsInfoToTime(sopUserLogsInfo);

            }
        }catch (Exception e){
            return R.error().put("msg","修改营期失败："+e.getMessage());
        }

        return R.ok();
    }

    @Override
    public R batchUpdateSopUserLogsInfoToTime(BatchSopUserLogsInfoParam param) {

        try {

                //用于查询
                SopUserLogs userLogs=new SopUserLogs();
                userLogs.setSopId(param.getSopId());
                userLogs.setQwUserId(param.getQwUserId());
                userLogs.setCorpId(param.getCorpId());
                userLogs.setStatus(1);
                userLogs.setStartTime(param.getParamTime());

                String  unionSopStartId = sopUserLogsService.selectSopUserLogsByUpdate(userLogs);

                 BatchSopUserLogsInfoParamU infoParamU=new BatchSopUserLogsInfoParamU();
                    infoParamU.setIds(param.getIds());
                //查询sop任务开始时间营期表 是否有这个营期
                if (!StringUtil.strIsNullOrEmpty(unionSopStartId)){
                    infoParamU.setUserLogsId(unionSopStartId);
                    if (param.getIds().length>0){
                        batchUpdateSopUserLogsInfoToTime(infoParamU);
                    }

                }
                else {
                    //如果查不出来营期 ，则新建营期
                    //查询sop的模板id
                    QwSop qwSop = qwSopService.selectQwSopById(param.getSopId());

                    //查询这个sop任务的员工的信息
                    QwUser qwUser = qwUserMapper.selectQwUserByQwUseridAndCorpId(param.getQwUserId(), param.getCorpId());

                    SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
                    userLogsParamByDate.setSopId(param.getSopId());
                    userLogsParamByDate.setSopTempId(qwSop.getTempId());
                    userLogsParamByDate.setQwUserId(param.getQwUserId());
                    userLogsParamByDate.setCorpId(param.getCorpId());
                    userLogsParamByDate.setStartTime(param.getParamTime());
                    userLogsParamByDate.setStatus(1);
                    userLogsParamByDate.setUserId(qwUser.getId()+"|"+qwUser.getCompanyUserId()+"|"+qwUser.getCompanyId());

                    //如果没有这个营期没有就先插入
                    int i1 = sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
                    if (i1>0){
                        userLogs.setSopTempId(qwSop.getTempId());
                        //获取新的
                        String  unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
                        infoParamU.setUserLogsId(unionSopStartIdNew);

                        if (param.getIds().length>0){
                            batchUpdateSopUserLogsInfoToTime(infoParamU);
                        }

                    }



                }
            } catch (ConstraintViolationException e) {
                return R.error().put("msg", "修改营期失败：目标营期已经有此客户，请检查客户信息 是否重复，重复请联系超管 删除此营期数据");
            } catch (Exception e) {
                return R.error().put("msg", "修改营期失败：" + e.getMessage());
        }

        return R.ok();
    }


    public void batchUpdateSopUserLogsInfoToTime(BatchSopUserLogsInfoParamU paramU) {
        // 定义每批次的大小
        int batchSize = 200;
        String[] ids = paramU.getIds();

        for (int i = 0; i < ids.length; i += batchSize) {
            // 计算当前批次的结束索引
            int end = Math.min(i + batchSize, ids.length);

            // 拆分当前批次数据
            String[] batchIds = Arrays.copyOfRange(ids, i, end);

            // 创建新的参数对象
            BatchSopUserLogsInfoParamU batchParam = new BatchSopUserLogsInfoParamU();
            batchParam.setIds(batchIds);
            batchParam.setUserLogsId(paramU.getUserLogsId());

            // 执行批量更新
            sopUserLogsInfoMapper.batchUpdateSopUserLogsInfoToTime(batchParam);
        }
    }

    private QwSopLogs createBaseLog(String formattedSendTime, SopUserLogsVo logVo,
                                    QwSopRuleTimeVO ruleTimeVO, String externalContactId,
                                    String externalUserName, Long fsUserId, Integer isOfficial,
                                    Long externalId) {
        QwSopLogs sopLogs = new QwSopLogs();
        sopLogs.setSendTime(formattedSendTime);
        sopLogs.setQwUserid(logVo.getQwUserId());
        sopLogs.setCorpId(logVo.getCorpId());
        sopLogs.setLogType(ruleTimeVO.getType());

        sopLogs.setSendType(isOfficial == 1 ? 1 : ruleTimeVO.getSendType());


        sopLogs.setSendStatus(3L);
        sopLogs.setReceivingStatus(0L);

        String[] userKey = logVo.getUserId().split("\\|");
        sopLogs.setCompanyId(Long.valueOf(userKey[2].trim()));
        sopLogs.setSopId(logVo.getSopId());

        sopLogs.setExternalUserId(externalContactId);
        sopLogs.setExternalId(externalId);
        sopLogs.setExternalUserName(externalUserName);
        sopLogs.setFsUserId(fsUserId);

        qwSopLogsService.applyBlacklistCheck(sopLogs);
        return sopLogs;
    }
    @Override
    public R sendUserLogsInfoMsg(SendUserLogsInfoMsgParam param) {
        Boolean sendLiveMsg = Boolean.FALSE;
        if(null != param.getLiveId()){
            sendLiveMsg = Boolean.TRUE;
        }
        Boolean sendLiveMsgFinal = sendLiveMsg;
        QwSop qwSop = qwSopMapper.selectQwSopById(param.getSopId());
        List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
        Date createTime=new Date();
        if (config == null) {
            return R.error().put("msg","课程默认配置为空，请联系管理员");
        }
        FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forMassSend(config);

        if (StringUtil.strIsNullOrEmpty(param.getCorpId())){
            return R.error().put("msg","企业编号为空，不能创建一键群发");
        }


        // 查询公司关联小程序数据
        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));

        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
        Map<Long, CompanyDept> deptMiniAppMap = MiniAppIdResolver.buildDeptMiniAppMap(companyDeptMapper.queryDeptDataAll());
        Map<Long, Long> companyUserDeptMap = MiniAppIdResolver.buildCompanyUserDeptMap(companyUserMapper.selectAllCompanyUserList());
        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(param.getCorpId());
        if (qwCompany == null ) {
            return  R.error().put("msg","企业不存在，请联系管理员");
        }
        List<QwSopLogs> sopLogsList;
        // 群聊路径客户人数（外部成员数）；个人路径为 -1 表示不使用
        int groupPathCustomerCount = -1;
        if(param.getFilterMode() != null && param.getFilterMode() == 2 && param.getChatIds() != null && param.getChatIds().length > 0){
            List<QwGroupChat> groupList = qwGroupChatMapper.selectQwGroupChatByChatIds(param.getChatIds());

            Map<String, QwGroupChat> groupMap = PubFun.listToMapByGroupObject(groupList, QwGroupChat::getChatId);
            List<QwGroupChatUser> groupUserList = qwGroupChatUserMapper.selectQwGroupChatUserByChatIds(groupList.stream().map(QwGroupChat::getChatId).toArray(String[]::new));
            Map<String, List<QwGroupChatUser>> groupUserMap = PubFun.listToMapByGroupList(groupUserList, QwGroupChatUser::getChatId);
            groupList.stream().filter(e -> groupUserMap.containsKey(e.getChatId())).forEach(e -> {
                e.setChatUserList(groupUserMap.get(e.getChatId()));
            });
            List<String> groupChatUserIds = PubFun.listToNewList(groupUserList, QwGroupChatUser::getUserId);
            if (!groupChatUserIds.isEmpty()) {
                List<GroupUserExternalVo> userList = qwExternalContactMapper.selectByGroupUser(groupChatUserIds);
                Map<String, List<GroupUserExternalVo>> userMap = PubFun.listToMapByGroupList(userList, GroupUserExternalVo::getExternalUserId);
                groupUserList.forEach(e -> {
                    e.setUserList(userMap.getOrDefault(e.getUserId(), Collections.emptyList()));
                });
            }

            // 与个人路径「本次覆盖客户数」语义对齐：统计未退群的外部联系人（type=2），而非选中群数量
            groupPathCustomerCount = countGroupMassSendCustomerCount(groupUserList, groupList);

            //没有传值课程和课节 是直播的数据；群聊飞书/小程序发课统一按群成员写看课记录
            if(null != param.getCourseId() && null !=param.getVideoId()){
                try {
                    groupList.forEach(groupChat -> {
                        QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText2(groupChat.getOwner(), groupChat.getCorpId());
                        if (qwUser == null || qwUser.getId() == null
                                || qwUser.getCompanyUserId() == null || qwUser.getCompanyId() == null) {
                            log.error("群聊创建看课记录跳过：群主企微用户不存在或未绑定员工, owner={}, corpId={}, chatId={}",
                                    groupChat.getOwner(), groupChat.getCorpId(), groupChat.getChatId());
                            return;
                        }
                        if (groupChat.getChatUserList() == null) {
                            return;
                        }
                        groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                            Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
                            GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
                            if (vo != null && vo.getId() != null) {
                                addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), vo.getFsUserId(), qwUser.getId().toString(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), vo.getId(), param.getStartTime(), createTime,2);
                            }
                        });
                    });
                } catch (Exception e) {
                    log.error("群聊创建看课记录失败！", e);
                }
            }

            if (param.getSendType() != null && param.getSendType() == 2) {
                sopLogsList = groupUserList.stream().map(groupUser -> {
                    QwGroupChat qwGroupChat = groupMap.get(groupUser.getChatId());

                    QwUser qwUser = qwExternalContactService.getQwUserByRedis(qwGroupChat.getCorpId(),qwGroupChat.getOwner());
                    if (qwUser==null){
                        throw new BaseException("企业微信用户不存在:" + qwGroupChat.getOwner());
                    }

//                    QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText2(qwGroupChat.getOwner(), qwGroupChat.getCorpId());
//                    if (qwUser == null) {
//                        throw new BaseException("企业微信用户不存在:" + qwGroupChat.getOwner());
//                    }
                    Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(groupUser.getUserList(), GroupUserExternalVo::getUserId);
                    GroupUserExternalVo vo = userMap.get(qwGroupChat.getOwner());
                    if (vo == null) return null;
                    QwSopLogs sopLogs = new QwSopLogs();

                    sopLogs.setQwUserid(qwUser.getQwUserId());
                    sopLogs.setExternalUserId(vo.getExternalUserId());
                    sopLogs.setLogType(2);
                    sopLogs.setContentJson(param.getSetting());
                    sopLogs.setSendStatus(3L);
                    sopLogs.setSendTime(DateUtil.formatLocalDateTime(LocalDateTime.now()));
                    sopLogs.setCompanyId(qwSop.getCompanyId());
                    sopLogs.setReceivingStatus(0L);
                    sopLogs.setSopId(param.getSopId());
                    sopLogs.setCorpId(qwGroupChat.getCorpId());
                    sopLogs.setSort(30000001);
                    sopLogs.setSendType(2);
                    if(sendLiveMsgFinal){
                        sopLogs.setSendType(20);
                    }
                    sopLogs.setExternalUserName(groupUser.getName());
                    sopLogs.setQwUserKey(qwUser.getId());

                    // 设置实际发送人
                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),groupUser.getChatId());
                    //域名
                    String companyUserId = qwUser.getCompanyUserId().toString();
                    String companyId = String.valueOf(qwUser.getCompanyId()).trim();
                    String domainName = companyUserMapper.selectDomainByUserId(Long.parseLong(companyUserId));
                    if (StringUtils.isEmpty(domainName)) {
                        domainName = config.getRealLinkDomainName();
                    }
                    QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
                    List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(), QwSopCourseFinishTempSetting.Setting.class);
                    for (QwSopCourseFinishTempSetting.Setting st : list) {
                        st.applyRandomTextValue();

                        //过滤违禁词
                        if ("1".equals(st.getContentType())) {
                            replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                        }
                        //过滤违禁词
                        if ("3".equals(st.getContentType())) {
                            replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                            replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                        }

                        switch (st.getContentType()) {
                            //文字和短链一起
                            case "1":
                            case "3":
//                                if ("1".equals(st.getIsBindUrl())) {
//                                    String qwUserId = qwUser.getQwUserId();
//                                    String companyId = qwUser.getCompanyId().toString();
//                                    Long externalUserId = vo.getId();
////                                    addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(), null, qwUserId, companyUserId, companyId, externalUserId, param.getStartTime(), createTime);
//                                    FsCourseLinkCreateParam createParam = new FsCourseLinkCreateParam();
//                                    createParam.setCourseId(param.getCourseId().longValue());
//                                    createParam.setVideoId(param.getVideoId().longValue());
//                                    createParam.setCorpId(qwSop.getCorpId());
//                                    createParam.setCompanyUserId(Long.parseLong(companyUserId));
//                                    createParam.setCompanyId(Long.parseLong(companyId));
//                                    createParam.setChatId(groupUser.getChatId());
//                                    createParam.setQwUserId(qwUser.getId());
//                                    createParam.setDays(st.getExpiresDays());
//                                    R createLink = courseLinkService.createRoomLinkUrl(createParam);
//                                    if (createLink.get("code").equals(500)) {
//                                        throw new BaseException("链接生成失败！");
//                                    }
//                                    String sortLink = (String) createLink.get("url");
//
//                                    if (StringUtils.isNotEmpty(sortLink)) {
//                                        if ("3".equals(st.getContentType())) {
//                                            st.setLinkUrl(sortLink);
//                                        } else {
//                                            String currentValue = st.getValue();
//                                            if (currentValue == null) {
//                                                st.setValue(sortLink);
//                                            } else {
//                                                st.setValue(currentValue.replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText()) + "\n" + sortLink);
//                                            }
//                                        }
//                                    } else {
//                                        log.warn("生成短链失败，跳过设置 URL。");
//                                    }
//                                }

                                if ("1".equals(st.getContentType())) {
                                    st.setValue(st.getValue()
                                            .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText()));
                                }
                                break;
                            //小程序单独
                            case "4":
                                String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
                                        qwUser.getId(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), null, config, qwGroupChat.getChatId());

                                int listIndex = 1;
                                String miniAppId = resolveSopMiniAppId(qwUser, qwUser.getCompanyId(), listIndex, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);
                                st.setMiniType(listIndex);
                                if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                                    st.setMiniprogramAppid(miniAppId);
                                } else {
                                    log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                                }

                                st.setMiniprogramPage(linkByMiniApp);
                                break;

                            //语音
                            case "7":
                                createVoiceUrl(st, companyUserId, qwSop);
                                break;
                            //直播小程序单独
                            case "12":
                                sopLogs.setSendType(20);
                                setting.setLiveId(Long.valueOf(st.getLiveId()));
                                String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + vo.getId().toString();
                                st.setContentType("4");
                                String js = configService.selectConfigByKey("his.config");
                                FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
                                //todo 发个人看课记录处理
                                try {
                                    if (vo != null && vo.getId() != null) {
                                        createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),vo.getId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUser.getId().toString(),param.getCorpId());
                                    }
                                } catch (Exception e) {
                                    log.error("群聊创建直播看课记录失败！", e);
                                }
//                                createLiveWatchLogAndInsert();
                                st.setMiniprogramAppid(sysConfig.getAppId());
                                st.setMiniprogramPage(sortLiveLink);
                                break;
                            case "14":

                                linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, qwGroupChat.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
                                        String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),null ,config, groupUser.getChatId());

                                int listIndexTemp = 1;
                                miniAppId = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndexTemp, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);

                                if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                                    st.setMiniprogramAppid(miniAppId);
                                } else {
                                    log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                                }

                                st.setMiniprogramTitle("福袋发放");

                                st.setMiniprogramPage(linkByMiniApp);
                                break;
                            case "16":
                                createVoiceUrl(st, companyUserId, qwSop);
                                break;
                            //飞书看课（固定免授权：一批客户复用同一飞书文档，不走领取页）
                            case "17":
                                try {
                                    st.setFeishuNeedAuth(0);
                                    Integer videoId18 = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                                    Integer courseId18 = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                                    Long companyIdLong = qwUser.getCompanyId();
                                    Long externalUserId = vo.getId();
                                    Long companyUserIdLong = Long.parseLong(companyUserId);
                                    if (videoId18 == null) {
                                        log.error("飞书链接生成失败：视频ID为空，sopId={}", param.getSopId());
                                        break;
                                    }
                                    // 群成员飞书发课：发时写入看课记录（与 contentType=18 一致）；watchType=3 飞书
                                    addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(),
                                            vo.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId,
                                            String.valueOf(qwUser.getCompanyId()), vo.getId(),
                                            param.getStartTime(), createTime, 3);
                                    Map<String, String> feishuH5Link = createFeishuH5Link(st, param.getCorpId(), createTime, courseId18, videoId18, String.valueOf(qwUser.getId()), companyUserId, String.valueOf(companyIdLong), externalUserId, config);
                                    String shortCode = feishuH5Link.get("link");
                                    String feishuLink = resolveFeishuSendLinkSafely(videoId18.longValue(), companyIdLong, courseId18.longValue(),
                                            companyUserIdLong, shortCode, st.getFeishuAccountId(), 0,
                                            param.getSopId(), null, externalUserId, null,
                                            qwUser.getQwUserId(), qwUser.getQwUserName(), companyUserId, companyId,
                                            param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                                    if (StringUtils.isNotEmpty(feishuLink)) {
                                        st.setLinkUrl(feishuLink);
                                        // 链接卡片标题/描述/封面为空时兜底，避免 PAD 发出空卡片
                                        fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                                    }
                                } catch (Exception e) {
                                    log.error("飞书群成员链接生成异常，仍写入发送记录: sopId={}, err={}",
                                            param.getSopId(), e.getMessage(), e);
                                }
                                break;
                            //飞书NEW：走需授权链路（发时写看课记录 + 入库短链），发出授权页 URL
                            case "18":
                                try {
                                    Integer videoIdFeishuNew = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                                    Integer courseIdFeishuNew = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                                    // 与飞书需授权(17)一致：发送时写入看课记录；watchType=3 飞书
                                    addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(),
                                            vo.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId,
                                            String.valueOf(qwUser.getCompanyId()), vo.getId(),
                                            param.getStartTime(), createTime, 3);
                                    st.setLinkUrl(createFeishuNewSendUrl(st, param.getCorpId(), createTime, courseIdFeishuNew, videoIdFeishuNew,
                                            qwUser.getId(), companyUserId, String.valueOf(qwUser.getCompanyId()), vo.getId(), config, null));
                                    fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                                } catch (Exception e) {
                                    log.error("飞书NEW群成员链接生成异常，仍写入发送记录: sopId={}, err={}",
                                            param.getSopId(), e.getMessage(), e);
                                }
                                break;
                            //群公告
                            case "11":
                                sopLogs.setSendType(21); // 设置为群公告类型
                                break;
                            //订阅号文章（contentType=13）透传articleUrl和miniprogramPicUrl等字段
                            case "13":
                                if (StringUtil.strIsNullOrEmpty(st.getMiniprogramPage()) && !StringUtil.strIsNullOrEmpty(st.getArticleUrl())) {
                                    String articleUrlJson = "{\"articleUrl\":\"" + st.getArticleUrl() + "\"}";
                                    st.setMiniprogramPage("/pages_course/webArticle.html?url=" + articleUrlJson);
                                    log.info("订阅号文章类型，使用articleUrl生成miniprogramPage: {}", st.getMiniprogramPage());
                                }
                                log.info("处理订阅号文章类型，保留字段: articleUrl={}, miniprogramPicUrl={}, miniprogramPage={}",
                                        st.getArticleUrl(), st.getMiniprogramPicUrl(), st.getMiniprogramPage());
                                break;
                            default:
                                break;
                        }
                    }
                    setting.setSetting(list);
                    setting.setType(2);
                    setting.setVideoId(param.getVideoId());
                    setting.setCourseId(param.getCourseId());
                    setting.setCourseType(param.getCourseType());
                    setting.setLiveId(param.getLiveId());
                    sopLogs.setContentJson(JSON.toJSONString(setting));
                    return sopLogs;
                }).filter(Objects::nonNull).collect(Collectors.toList());


            }
            else {
                sopLogsList = groupList.stream().map(groupChat -> {
                    QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText2(groupChat.getOwner(), groupChat.getCorpId());
                    if (qwUser == null) {
                        log.error("企业微信用户不存在，跳过该群: owner={}, corpId={}, chatId={}",
                                groupChat.getOwner(), groupChat.getCorpId(), groupChat.getChatId());
                        return null;
                    }
                    if(qwUser.getCompanyUserId() == null){
                        throw new BaseException("当前企微没绑定员工，无法发送！");
                    }
                    QwSopLogs sopLogs = new QwSopLogs();

                    sopLogs.setQwUserid(qwUser.getQwUserId());
                    sopLogs.setExternalUserId(groupChat.getChatId());
                    sopLogs.setLogType(2);
                    sopLogs.setContentJson(param.getSetting());
                    sopLogs.setSendStatus(3L);
                    sopLogs.setSendTime(DateUtil.formatLocalDateTime(LocalDateTime.now()));
                    sopLogs.setCompanyId(qwSop.getCompanyId());
                    sopLogs.setReceivingStatus(0L);
                    sopLogs.setSopId(param.getSopId());
                    sopLogs.setCorpId(groupChat.getCorpId());
                    sopLogs.setSort(100000000);
                    sopLogs.setSendType(6);
                    if(sendLiveMsgFinal){
                        sopLogs.setSendType(20);
                    }
                    sopLogs.setExternalUserName(groupChat.getName());
                    sopLogs.setQwUserKey(qwUser.getId());
                    // 设置实际发送人
                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),groupChat.getChatId());

                    QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();

                    List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(), QwSopCourseFinishTempSetting.Setting.class);

                    // 检查是否有群公告类型，如果有则设置sendType=21
                    boolean hasGroupNotice = list.stream().anyMatch(st -> "11".equals(st.getContentType()));
                    if(hasGroupNotice){
                        sopLogs.setSendType(21);
                        log.info("检测到群公告类型，设置sendType=21, sopId:{}, chatId:{}", param.getSopId(), groupChat.getChatId());
                    }

                    for (QwSopCourseFinishTempSetting.Setting st : list) {
                        st.applyRandomTextValue();

                        //过滤违禁词
                        if ("1".equals(st.getContentType())) {
                            replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                        }
                        //过滤违禁词
                        if ("3".equals(st.getContentType())) {
                            replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                            replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                        }
                        switch (st.getContentType()) {
                            //文字和短链一起
                            case "1":
                            case "3":
//                                if ("1".equals(st.getIsBindUrl())) {
//                                    FsCourseLinkCreateParam createParam = new FsCourseLinkCreateParam();
//                                    createParam.setCourseId(param.getCourseId().longValue());
//                                    createParam.setVideoId(param.getVideoId().longValue());
//                                    createParam.setCorpId(groupChat.getCorpId());
//                                    createParam.setCompanyUserId(qwUser.getCompanyUserId());
//                                    createParam.setCompanyId(qwUser.getCompanyId());
//                                    createParam.setChatId(groupChat.getChatId());
//                                    createParam.setQwUserId(qwUser.getId());
//                                    createParam.setDays(st.getExpiresDays());
//                                    R createLink = courseLinkService.createRoomLinkUrl(createParam);
//                                    if (createLink.get("code").equals(500)) {
//                                        throw new BaseException("链接生成失败！");
//                                    }
//                                    String link = (String) createLink.get("url");
//                                    if (StringUtils.isNotEmpty(link)) {
//                                        if ("3".equals(st.getContentType())) {
//                                            st.setLinkUrl(link);
//                                        } else {
//                                            String currentValue = st.getValue();
//                                            if (currentValue == null) {
//                                                st.setValue(link);
//                                            } else {
//                                                st.setValue(currentValue
//                                                        .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText())
//                                                        + "\n" + link);
//                                            }
//                                        }
//                                    } else {
//                                        log.error("生成短链失败，跳过设置 URL。");
//                                    }
//                                }
                                if ("1".equals(st.getContentType())) {
                                    st.setValue(st.getValue()
                                            .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText()));
                                }
                                break;
                            //小程序单独
                            case "4":
                                String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
                                        qwUser.getId(), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(), null, config, groupChat.getChatId());

                                int listIndex = 1;
                                String miniAppId = resolveSopMiniAppId(qwUser, qwUser.getCompanyId(), listIndex, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);
                                st.setMiniType(listIndex);
                                if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                                    st.setMiniprogramAppid(miniAppId);
                                } else {
                                    log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                                }

                                st.setMiniprogramPage(linkByMiniApp);
                                break;
                            //语音
                            case "7":
                                if (qwUser.getCompanyUserId() != null) {
                                    createVoiceUrl(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                                }
                                break;
                            //直播小程序单独
                            case "12":
                                sopLogs.setSendType(20);
                                setting.setLiveId(setting.getLiveId());
                                String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" +param.getCorpId()+"&qwUserId=" + qwUser.getId() + "&chatId=" + groupChat.getChatId();
                                st.setContentType("4");
                                String js = configService.selectConfigByKey("his.config");
                                FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
                                //发群处理看课记录
//                                createLiveWatchLogAndInsert();
                                try {
                                    groupList.forEach(gc -> {
                                        gc.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                                            Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
                                            GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
                                            if (vo != null && vo.getId() != null) {
                                                createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),vo.getId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUser.getId().toString(),param.getCorpId());
                                            }
                                        });
                                    });
                                } catch (Exception e) {
                                    log.error("群聊创建直播看课记录失败！", e);
                                }
                                st.setMiniprogramAppid(sysConfig.getAppId());
                                st.setMiniprogramPage(sortLiveLink);
                                break;
                            case "14":
                                String companyId = String.valueOf(qwUser.getCompanyId()).trim();
                                linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, qwUser.getCorpId(), new Date(), param.getCourseId(), param.getVideoId(),
                                        String.valueOf(qwUser.getId()), qwUser.getCompanyUserId().toString(), qwUser.getCompanyId().toString(),null, config, groupChat.getChatId());
                                int listIndexTemp = 1;
                                miniAppId = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndexTemp, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);

                                if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                                    st.setMiniprogramAppid(miniAppId);
                                } else {
                                    log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                                }

                                st.setMiniprogramTitle("福袋发放");

                                st.setMiniprogramPage(linkByMiniApp);
                                break;
                            case "15":
                                String txt2 = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                                st.setValue(st.getValue().replaceAll("#客服称呼#", txt2).replaceAll("#销售称呼#", txt2));
                                try {
                                    replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                                break;
                            case "16":
                                if (qwUser.getCompanyUserId() != null) {
                                    createVoiceUrl(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                                }
                                try {
                                    if (qwUser.getCompanyUserId() != null) {
                                        createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                                    }
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                                break;
                            //飞书看课（发群：固定免授权，一条群消息共用一个飞书文档链接）
                            case "17":
                                try {
                                    st.setFeishuNeedAuth(0);
                                    Integer videoId18Group = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                                    Integer courseId18Group = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                                    Long companyIdLongGroup = Long.valueOf(qwUser.getCompanyId().toString());
                                    Long companyUserIdLongGroup = Long.valueOf(qwUser.getCompanyUserId().toString());
                                    if (videoId18Group == null) {
                                        log.error("飞书链接生成失败：视频ID为空，sopId={}", param.getSopId());
                                        break;
                                    }
                                    Map<String, String> feishuH5LinkGroup = createFeishuH5Link(st, param.getCorpId(), createTime, courseId18Group, videoId18Group, String.valueOf(qwUser.getId()), String.valueOf(companyUserIdLongGroup), String.valueOf(companyIdLongGroup), null, config, groupChat.getChatId());
                                    String shortCodeGroup = feishuH5LinkGroup.get("link");
                                    String feishuLinkGroup = resolveFeishuSendLinkSafely(videoId18Group.longValue(), companyIdLongGroup, courseId18Group.longValue(),
                                            companyUserIdLongGroup, shortCodeGroup, st.getFeishuAccountId(), 0,
                                            param.getSopId(), null, null, null,
                                            qwUser.getQwUserId(), qwUser.getQwUserName(),
                                            String.valueOf(companyUserIdLongGroup), String.valueOf(companyIdLongGroup),
                                            param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                                    if (StringUtils.isNotEmpty(feishuLinkGroup)) {
                                        st.setLinkUrl(feishuLinkGroup);
                                        fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                                    } else {
                                        log.error("飞书发群免授权链接为空，仍写入发送记录: sopId={}, chatId={}", param.getSopId(), groupChat.getChatId());
                                    }
                                } catch (Exception e) {
                                    log.error("飞书发群链接生成异常，仍写入发送记录: sopId={}, chatId={}, err={}",
                                            param.getSopId(), groupChat.getChatId(), e.getMessage(), e);
                                }
                                break;
                            //飞书NEW（发群：只发一条领取页，挂 chatId；领取时按 link+userId 建链）
                            case "18":
                                try {
                                    Integer videoIdFeishuNewGroup = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                                    Integer courseIdFeishuNewGroup = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                                    st.setLinkUrl(createFeishuNewSendUrl(st, param.getCorpId(), createTime, courseIdFeishuNewGroup, videoIdFeishuNewGroup,
                                            qwUser.getId(), String.valueOf(qwUser.getCompanyUserId()), String.valueOf(qwUser.getCompanyId()),
                                            null, config, groupChat.getChatId()));
                                    fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                                } catch (Exception e) {
                                    log.error("飞书NEW发群链接生成异常，仍写入发送记录: sopId={}, chatId={}, err={}",
                                            param.getSopId(), groupChat.getChatId(), e.getMessage(), e);
                                }
                                break;
                            //群公告
                            case "11":
                                sopLogs.setSendType(21); // 设置为群公告类型
                                break;
                            //订阅号文章（contentType=13）透传articleUrl和miniprogramPicUrl等字段
                            case "13":

                                if (StringUtil.strIsNullOrEmpty(st.getMiniprogramPage()) && !StringUtil.strIsNullOrEmpty(st.getArticleUrl())) {
                                    String articleUrlJson = "{\"articleUrl\":\"" + st.getArticleUrl() + "\"}";
                                    st.setMiniprogramPage("/pages_course/webArticle.html?url=" + articleUrlJson);
                                    log.info("订阅号文章类型（群聊群发），使用articleUrl生成miniprogramPage: {}", st.getMiniprogramPage());
                                }
                                log.info("处理订阅号文章类型（群聊群发），保留articleUrl等字段: articleUrl={}, miniprogramPicUrl={}",
                                        st.getArticleUrl(), st.getMiniprogramPicUrl());
                                break;
                        }
                    }
                    setting.setSetting(list);
                    setting.setType(2);
                    setting.setVideoId(param.getVideoId());
                    setting.setCourseId(param.getCourseId());
                    setting.setCourseType(param.getCourseType());
                    setting.setLiveId(param.getLiveId());
                    sopLogs.setContentJson(JSON.toJSONString(setting));
                    return sopLogs;
                }).filter(Objects::nonNull).collect(Collectors.toList());
            }
        }else{
            sopLogsList = new ArrayList<>();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            List<SopUserLogsInfo> sopUserLogsInfos = sopUserLogsInfoMapper.selectSopUserLogsInfoByIds(param.getIds());

            String[] userKey = param.getUserIdParam().split("\\|");

            String qwUserId = userKey[0].trim();

            QwUser qwUser = qwUserMapper.selectQwUserByIdByWeComeText(Long.valueOf(qwUserId));

            if (qwUser == null) {
                return R.error().put("msg","企业微信用户不存在:"+qwUserId);
            }

            if (qwUser.getCompanyUserId() == null) {
                return R.error().put("msg","企业微信用户不存在未绑定销售-请绑定后重试:"+qwUser.getQwUserName()+"|"+qwUser.getQwUserId());
            }
            if (qwUser.getCompanyId() == null) {
                return R.error().put("msg","企业微信用户不存在未绑定销售公司-请绑定后重试:"+qwUser.getQwUserName()+"|"+qwUser.getQwUserId());
            }

            Company company = companyMapper.selectCompanyById(qwUser.getCompanyId());

            String companyUserId = String.valueOf(qwUser.getCompanyUserId()).trim();
            String companyId = String.valueOf(qwUser.getCompanyId()).trim();

            //域名
//            String domainName = companyUserMapper.selectDomainByUserId(Long.parseLong(companyUserId));
//            if (StringUtils.isEmpty(domainName)){
//                domainName = config.getRealLinkDomainName();
//            }


            String finalDomainName = "domainName";

            sopUserLogsInfos.forEach(item->{

                QwSopLogs sopLogs=new QwSopLogs();

                sopLogs.setQwUserid(item.getQwUserId());
                sopLogs.setExternalUserId(item.getExternalContactId());
                sopLogs.setExternalId(item.getExternalId());
                sopLogs.setLogType(2);
                sopLogs.setContentJson(param.getSetting());
                sopLogs.setSendStatus(3L);
                sopLogs.setSendTime(sdf.format(new Date()));
                sopLogs.setCompanyId(qwSop.getCompanyId());
                sopLogs.setReceivingStatus(0L);
                sopLogs.setSopId(param.getSopId());
                sopLogs.setCorpId(item.getCorpId());
                sopLogs.setFsUserId(item.getFsUserId());
                sopLogs.setSort(30000000);
                sopLogs.setSendType(5);
                sopLogs.setExternalUserName(item.getExternalUserName());
                   if(sendLiveMsgFinal){
                       sopLogs.setSendType(20);
                   }

                QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());

                QwSopCourseFinishTempSetting setting=new    QwSopCourseFinishTempSetting();

                List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
                for (QwSopCourseFinishTempSetting.Setting st : list) {
                    st.applyRandomTextValue();

                    //过滤违禁词
                    if ("1".equals(st.getContentType())){
                        replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                    }
                    //过滤违禁词
                    if ("3".equals(st.getContentType())){
                        replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                        replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
                    }

                    switch (st.getContentType()){
                        //文字和短链一起
                        case "1":
                        case "3":
//                            if ("1".equals(st.getIsBindUrl())) {
//
//                                addWatchLogIfNeeded(param.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),param.getStartTime(),createTime );
//
//                                String sortLink = generateShortLink(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
//                                        Long.valueOf(qwUserId), companyUserId, companyId, finalDomainName,item.getExternalId(),config);
//
//                                if (StringUtils.isNotEmpty(sortLink)) {
//                                    if ("3".equals(st.getContentType())) {
//                                        st.setLinkUrl(sortLink);
//                                    } else {
//                                        String currentValue = st.getValue();
//                                        if (currentValue == null) {
//                                            st.setValue(sortLink);
//                                        } else {
//                                            st.setValue(currentValue
//                                                    .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
//                                                    .replaceAll("#客户称呼#",StringUtil.strIsNullOrEmpty(contact.getStageStatus())|| "0".equals(contact.getStageStatus())?"同学":contact.getStageStatus())
//                                                    + "\n" + sortLink);
//                                        }
//                                    }
//                                } else {
//                                    log.warn("生成短链失败，跳过设置 URL。");
//                                }
//                            }else {
                                if ("1".equals(st.getContentType())) {
                                    String defaultName = "同学";
                                    if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
                                        defaultName = contact.getName();
                                    }
                                    st.setValue(st.getValue()
                                            .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
                                            .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
                                }
//                            }

                            break;
                        //小程序单独
                        case "4":
                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,2 );

                            String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
                                    Long.valueOf(qwUserId), companyUserId, companyId, item.getExternalId(), config, null);

                            int effectiveGrade = (item.getGrade() == null) ? 5 : item.getGrade();
                            int listIndex = (effectiveGrade == 1 || effectiveGrade == 2) ? 0 : 1;
                            String miniAppId = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndex, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);
                            st.setMiniType(listIndex);
                            if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                                st.setMiniprogramAppid(miniAppId);
                            } else {
                                log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                            }


                            st.setMiniprogramPage(linkByMiniApp);

                            break;
                        //语音
                        case "7":
                            createVoiceUrl(st, companyUserId, qwSop);
                            break;
                        //app
                        case "9":
                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,1 );

                            QwCreateLinkByAppVO linkByApp = createLinkByApp(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
                                    Long.valueOf(qwUserId), companyUserId, companyId, item.getExternalId(), config,qwUser.getQwUserName(),contact.getFsUserId());
                            st.setLinkUrl(linkByApp.getSortLink().replaceAll("^[\\s\\u2005]+", ""));
                            st.setAppLinkUrl(linkByApp.getAppMsgLink().replaceAll("^[\\s\\u2005]+", ""));

                            break;
                        //自定义小程序
                        case "10":
                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), qwUserId, companyUserId, companyId, item.getExternalId(),item.getStartTime(),createTime,2 );
                            if (company!=null){

                                String customMiniAppId = company.getCustomMiniAppId();

                                if (customMiniAppId != null && !customMiniAppId.trim().isEmpty()) {
                                    st.setMiniprogramAppid(customMiniAppId);
                                } else {
                                    st.setMiniprogramAppid("该公司未配置自定义小程序:"+companyId);
                                }
                            }else {
                                st.setMiniprogramAppid("未找到匹配的公司的自定义小程序:"+companyId);
                            }

                            break;
                        //直播小程序单独
                        case "12":
                            sopLogs.setSendType(20);
                            setting.setLiveId(Long.valueOf(st.getLiveId()));
                            String sortLiveLink = "/pages_course/living.html?companyId=" + qwUser.getCompanyUserId() + "&companyUserId=" + qwUser.getCompanyUserId() + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUserId +"&externalId=" + item.getExternalId().toString();
                            st.setContentType("4");
                            String js = configService.selectConfigByKey("his.config");
                            FSSysConfig sysConfig= JSON.parseObject(js,FSSysConfig.class);
                            //发个人看课记录处理
                            try {
                                    createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, qwUserId,param.getCorpId());

                            } catch (Exception e) {
                                log.error("群聊创建直播看课记录失败！", e);
                            }
                            st.setMiniprogramAppid(sysConfig.getAppId());
                            st.setMiniprogramPage(sortLiveLink);
                            break;
                        case "14":
                            LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(st.getLuckyBagId());
                            if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
                                sopLogs.setSendStatus(5L);
                                sopLogs.setReceivingStatus(0L);
                                sopLogs.setRemark("福袋配置被禁用");
                            }
                            else if (ObjectUtil.isNotEmpty(sopLogs.getFsUserId())){
                                //获取配置并校验
                                SysConfig luckyBagConfig = configService.selectConfigByConfigKey("luckyBag.config");
                                if (ObjectUtil.isEmpty(luckyBagConfig)) {
                                    sopLogs.setSendStatus(5L);
                                    sopLogs.setReceivingStatus(0L);
                                    sopLogs.setRemark("福袋配置不存在");
                                }
// 2. 解析配置值
                                JSONObject jsonObject;
                                try {
                                    jsonObject = JSON.parseObject(luckyBagConfig.getConfigValue());
                                    Integer count = jsonObject.getInteger("weekLimit");

                                    // 查询用户记录并校验次数
                                    LuckyBagCollectRecord queryRecord = new LuckyBagCollectRecord();
                                    queryRecord.setUserId(sopLogs.getFsUserId());
                                    queryRecord.setCollectType("1");
                                    // 动态计算时间范围
                                    LocalDate endDate = LocalDate.now();
                                    LocalDate startDate = endDate.minusDays(6); // 包含今天

                                    Map<String, Object> params = new HashMap<>();
                                    params.put("beginSendTime", startDate.toString());
                                    params.put("endSendTime", endDate.toString());
                                    queryRecord.setParams(params);
                                    List<LuckyBagCollectRecord> luckyBagCollectRecords =
                                            luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(queryRecord);

                                    // 判断是否超过限制
                                    if (luckyBagCollectRecords.size() >= count) {
                                        sopLogs.setSendStatus(5L);
                                        sopLogs.setReceivingStatus(0L);
                                        sopLogs.setRemark("超过福袋发放次数");
                                    }

                                } catch (Exception e) {
                                    // 处理配置解析异常
                                    sopLogs.setSendStatus(5L);
                                    sopLogs.setReceivingStatus(0L);
                                    sopLogs.setRemark("福袋配置解析失败");
                                }
                            }
                            linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
                                    qwUserId, companyUserId, companyId, item.getExternalId(), config,null);

                            int effectiveGradeTemp = (item.getGrade() == null) ? 5 : item.getGrade();
                            int listIndexTemp = (effectiveGradeTemp == 1 || effectiveGradeTemp == 2) ? 0 : 1;
                            if (effectiveGradeTemp == 6) {
                                listIndexTemp = 2;
                            }
                            String miniAppId14 = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndexTemp, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);

                            if (!StringUtil.strIsNullOrEmpty(miniAppId14)) {
                                st.setMiniprogramAppid(miniAppId14);
                            } else {
                                log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                            }

                            st.setMiniprogramTitle("福袋发放");

                            st.setMiniprogramPage(linkByMiniApp);
                            break;
                        case "15":
                            String txt = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                            st.setValue(st.getValue()
                                    .replaceAll("#客服称呼#", txt)
                                    .replaceAll("#销售称呼#", txt)
                                    .replaceAll("#客户称呼#", StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus()));
                            try {
                                replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                            break;
                        case "16":
                            createVoiceUrl(st, companyUserId, qwSop);
                            try {
                                if (qwUser.getCompanyUserId() != null) {
                                    createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                                }
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                            break;
                        //飞书看课（固定免授权：一批客户复用同一飞书文档，不走领取页）
                        case "17":
                            st.setFeishuNeedAuth(0);
                            Integer videoId18 = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                            Integer courseId18 = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                            Long companyIdLong = Long.parseLong(companyId);
                            Long companyUserIdLong = Long.parseLong(companyUserId);
                            if (videoId18 == null) {
                                log.error("飞书链接生成失败：视频ID为空，sopId={}", param.getSopId());
                                break;
                            }
                            // 飞书发课：发时写入/更新看课记录，已存在则把 watch_type 改为飞书(3)
                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),
                                    item.getFsUserId(), qwUserId, companyUserId, companyId,
                                    item.getExternalId(), param.getStartTime(), createTime, 3);
                            String txt18 = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                            String customerTitle = StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus();
                            if (StringUtils.isNotEmpty(st.getLinkTitle())) {
                                st.setLinkTitle(st.getLinkTitle()
                                        .replaceAll("#销售称呼#", txt18)
                                        .replaceAll("#客户称呼#", customerTitle));
                            }
                            if (StringUtils.isNotEmpty(st.getLinkDescribe())) {
                                st.setLinkDescribe(st.getLinkDescribe()
                                        .replaceAll("#销售称呼#", txt18)
                                        .replaceAll("#客户称呼#", customerTitle));
                            }
                            Map<String, String> feishuH5Link = createFeishuH5Link(st, param.getCorpId(), createTime, courseId18, videoId18, qwUserId, companyUserId, companyId, item.getExternalId(), config);
                            String shortCode = feishuH5Link.get("link");
                            String feishuLink = resolveFeishuSendLinkSafely(videoId18.longValue(), companyIdLong, courseId18.longValue(),
                                    companyUserIdLong, shortCode, st.getFeishuAccountId(), 0,
                                    param.getSopId(), item.getUserLogsId(), item.getExternalId(), null,
                                    qwUserId, qwUser.getQwUserName(), companyUserId, companyId,
                                    param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                            if (StringUtils.isNotEmpty(feishuLink)) {
                                st.setLinkUrl(feishuLink);
                                fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                            }
                            break;
                        //飞书NEW：走需授权链路（发时写看课记录 + 入库短链），发出授权页 URL
                        case "18":
                            Integer videoIdFeishuNewPrivate = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                            Integer courseIdFeishuNewPrivate = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                            // 与飞书需授权(17)一致：发送时写入看课记录；watchType=3 飞书
                            addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),
                                    item.getFsUserId(), qwUserId, companyUserId, companyId,
                                    item.getExternalId(), param.getStartTime(), createTime, 3);
                            String txtFeishuNew = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                            String customerTitleFeishuNew = StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus();
                            if (StringUtils.isNotEmpty(st.getLinkTitle())) {
                                st.setLinkTitle(st.getLinkTitle()
                                        .replaceAll("#销售称呼#", txtFeishuNew)
                                        .replaceAll("#客户称呼#", customerTitleFeishuNew));
                            }
                            if (StringUtils.isNotEmpty(st.getLinkDescribe())) {
                                st.setLinkDescribe(st.getLinkDescribe()
                                        .replaceAll("#销售称呼#", txtFeishuNew)
                                        .replaceAll("#客户称呼#", customerTitleFeishuNew));
                            }
                            st.setLinkUrl(createFeishuNewSendUrl(st, param.getCorpId(), createTime, courseIdFeishuNewPrivate, videoIdFeishuNewPrivate,
                                    Long.valueOf(qwUserId), companyUserId, companyId, item.getExternalId(), config, null));
                            fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                            break;
                        //群公告（仅用于一键群发，个人不应该有群公告）
                        case "11":
                            log.warn("群公告不能发给个人，跳过处理，sopId:{}, externalId:{}", param.getSopId(), item.getExternalId());
                            break;
                        default:
                            break;

                    }
                }


                setting.setSetting(list);
                setting.setType(2);
                setting.setVideoId(param.getVideoId());
                setting.setCourseId(param.getCourseId());
                setting.setCourseType(param.getCourseType());
                setting.setLiveId(param.getLiveId());

                try {
                    sopLogs.setQwUserKey(Long.valueOf(qwUserId));
                }catch (Exception e){
                    log.error("设置qwUserId异常", e);
                }
                sopLogs.setContentJson(JSON.toJSONString(setting));
                // 设置实际发送人
                updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),null);

                sopLogsList.add(sopLogs);
            });
        }

        //批量插入 发送记录
        int customerCount = 0;
        if (sopLogsList != null && !sopLogsList.isEmpty()) {
            processAndInsertQwSopLogsBySendMsg(sopLogsList);
            if (groupPathCustomerCount >= 0) {
                // 群路径：sendType=2按人发用实际发送条数；发群消息时一条日志对应一个群，客户人数改用外部成员数
                if (param.getSendType() != null && param.getSendType() == 2) {
                    customerCount = sopLogsList.size();
                } else {
                    customerCount = groupPathCustomerCount > 0 ? groupPathCustomerCount : sopLogsList.size();
                }
            } else {
                // 个人路径：一条发送记录对应一个客户
                customerCount = sopLogsList.size();
            }
        }
        // 记录销售点击一键群发行为（主库），不影响主流程
        Long companyIdHint = qwSop != null ? qwSop.getCompanyId() : null;
        // 本方法开头已从 SOP 库查出 QwSop，直接带上名称；若开头未查到则再补一次
        if (qwSop != null && (param.getSopName() == null || param.getSopName().isEmpty())
                && qwSop.getName() != null && !qwSop.getName().isEmpty()) {
            param.setSopName(qwSop.getName());
        } else {
            fillParamSopNameFromSopDb(param);
        }
        qwMassSendLogService.saveMassSendClickLog(param, customerCount, 1, companyIdHint);

        return R.ok();
    }

    /**
     * 群发参数补齐 SOP 名称：已有则不覆盖；否则按 sopId 查 SOP 库 qw_sop.name
     * 优先 QwSopCacheService，回退 Mapper（类/方法上已有 @DataSource(SOP) AOP，无需手动切库）
     */
    private void fillParamSopNameFromSopDb(SendUserLogsInfoMsgParam param) {
        if (param == null) {
            return;
        }
        if (!StringUtil.strIsNullOrEmpty(param.getSopName())) {
            return;
        }
        String sopId = param.getSopId();
        if (StringUtil.strIsNullOrEmpty(sopId) && param.getSopIds() != null && param.getSopIds().length > 0) {
            sopId = param.getSopIds()[0];
            if (!StringUtil.strIsNullOrEmpty(sopId)) {
                param.setSopId(sopId);
            }
        }
        if (StringUtil.strIsNullOrEmpty(sopId)) {
            return;
        }
        try {
            String cached = qwSopCacheService.getQwSopNameBySopId(sopId);
            if (!StringUtil.strIsNullOrEmpty(cached) && !"-".equals(cached)) {
                param.setSopName(cached);
                return;
            }
        } catch (Exception e) {
            log.warn("QwSopCacheService补全群发SOP名称失败 sopId={}", sopId, e);
        }
        try {
            String name = qwSopMapper.selectQwSopNameBySopId(sopId);
            if (!StringUtil.strIsNullOrEmpty(name)) {
                param.setSopName(name);
                return;
            }
            QwSop sop = qwSopMapper.selectQwSopById(sopId);
            if (sop != null && !StringUtil.strIsNullOrEmpty(sop.getName())) {
                param.setSopName(sop.getName());
            }
        } catch (Exception e) {
            log.warn("补全群发SOP名称失败 sopId={}", sopId, e);
        }
    }

    /**
     * 群聊一键群发客户人数：优先统计未退群外部联系人（type=2）数量，
     * 与个人路径「本次覆盖客户数」语义对齐；成员明细缺失时回退 groupSize 之和，再回退选中群数。
     */
    private static int countGroupMassSendCustomerCount(List<QwGroupChatUser> groupUserList, List<QwGroupChat> groupList) {
        if (groupUserList != null && !groupUserList.isEmpty()) {
            int externalCount = (int) groupUserList.stream()
                    .filter(Objects::nonNull)
                    .filter(u -> "2".equals(String.valueOf(u.getType())))
                    .filter(u -> u.getIsOut() == null || u.getIsOut() == 0L)
                    .count();
            if (externalCount > 0) {
                return externalCount;
            }
        }
        if (groupList != null && !groupList.isEmpty()) {
            int sizeSum = groupList.stream()
                    .filter(Objects::nonNull)
                    .filter(g -> g.getGroupSize() != null && g.getGroupSize() > 0)
                    .mapToInt(g -> g.getGroupSize().intValue())
                    .sum();
            if (sizeSum > 0) {
                return sizeSum;
            }
            return groupList.size();
        }
        return 0;
    }

    private void updateQwUserKey(QwSopLogs sopLogs, String qwUserId, String sopId,String chatId) {
        // 设置实际发送人
        SopUserLogs qwSopLogs = sopUserLogsMapper.queryUserBySopId(qwUserId,sopId,chatId);
        if (qwSopLogs != null && ObjectUtil.isNotEmpty(qwSopLogs.getActualQwId())){
            // sopLogs.setQwUserid(qwUser.getQwUserId());
            sopLogs.setQwUserKey(qwSopLogs.getActualQwId());
        }
    }

    private void createVoiceUrl(QwSopCourseFinishTempSetting.Setting st, String companyUserId, QwSop qwSop) {
        QwSopTempVoice qwSopTempVoice = sopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(Long.valueOf(companyUserId), st.getValue());
        if (qwSopTempVoice != null && qwSopTempVoice.getVoiceUrl() != null && qwSopTempVoice.getRecordType() == 1) {
            st.setVoiceUrl(qwSopTempVoice.getVoiceUrl());
            st.setVoiceDuration(String.valueOf(qwSopTempVoice.getDuration()));
        } else if (qwSopTempVoice == null) {
            if(st.getValue() != null){
                qwSopTempVoice = new QwSopTempVoice();
                qwSopTempVoice.setCompanyUserId(Long.valueOf(companyUserId));
                qwSopTempVoice.setVoiceTxt(st.getValue());
                qwSopTempVoice.setRecordType(0);
                sopTempVoiceService.insertQwSopTempVoice(qwSopTempVoice);
            }
        }
    }


    @Override
    public R sendUserLogsInfoMsgType(SendUserLogsInfoMsgParam param) {
        //营期一键群发
        switch (param.getType()) {
            case 1:
                break;
            case 2:
                processQwSopLogsBySendMsg(param, param.getDraftStrategy(), 2);
                log.info("一键群发操作日志2:{}", JSON.toJSONString(param));
                break;
        }

        return null;
    }

    @Override
    public R sendUserLogsInfoMsgSop(SendUserLogsInfoMsgParam param) {
        // SOP.qw_user_ids 存 qw_user.id；营期 sop_user_logs.qw_user_id 存企微 userid。查询前统一换算
        normalizeCampSendQwUserIds(param);
        List<String> sopUserLogsIds = sopUserLogsMapper.getSopUserLogsIds(param);
        if(sopUserLogsIds == null || sopUserLogsIds.isEmpty()){
            throw new BaseException("SOP没有可发送的营期数据");
        }
        String[] ids = sopUserLogsIds.toArray(new String[0]);
        param.setIds(ids);
        log.info("一键群发操作日志1:{}", JSON.toJSONString(param));

        // 群聊营期通常无 sop_user_logs_info 客户明细，INNER JOIN 会得到空列表；有 chat_id 时改走发群路径
        List<SopUserLogsInfo> personalInfos = sopUserLogsMapper.selectSopUserLogsByIds(ids);
        if (personalInfos == null || personalInfos.isEmpty()) {
            List<SopUserLogs> campLogs = sopUserLogsMapper.selectSopUserLogsListByIds(Arrays.asList(ids));
            List<String> chatIds = campLogs == null ? Collections.emptyList()
                    : campLogs.stream()
                    .map(SopUserLogs::getChatId)
                    .filter(StringUtils::isNotEmpty)
                    .distinct()
                    .collect(Collectors.toList());
            if (!chatIds.isEmpty()) {
                Integer draftStrategy = param.getDraftStrategy();
                if (draftStrategy != null && draftStrategy != 1) {
                    throw new BaseException("群聊营期暂不支持草稿策略，请选择直接发送");
                }
                if (StringUtil.strIsNullOrEmpty(param.getSopId()) && param.getSopIds() != null && param.getSopIds().length > 0) {
                    param.setSopId(param.getSopIds()[0]);
                }
                param.setFilterMode(2);
                param.setSendType(1);
                param.setChatIds(chatIds.toArray(new String[0]));
                log.info("SOP营期一键群发检测到群聊营期，改走发群: sopId={}, chatCount={}",
                        param.getSopId(), chatIds.size());
                return sendUserLogsInfoMsg(param);
            }
            throw new BaseException("SOP没有可发送的营期数据");
        }

        processQwSopLogsBySendMsg(param, param.getDraftStrategy(), 3);
        return null;
    }

    /**
     * 将前端/SOP 传入的 qwUserIds 统一为营期表使用的企微 userid。
     * 兼容：① qw_user 主键 id（SOP 任务成员字段）；② 已是企微 userid（我的 SOP / isMine）。
     */
    private void normalizeCampSendQwUserIds(SendUserLogsInfoMsgParam param) {
        if (param == null || param.getQwUserIds() == null || param.getQwUserIds().isEmpty()) {
            return;
        }
        LinkedHashSet<String> resolved = new LinkedHashSet<>();
        List<Long> pkIds = new ArrayList<>();
        for (String raw : param.getQwUserIds()) {
            if (StringUtils.isBlank(raw)) {
                continue;
            }
            String item = raw.trim();
            resolved.add(item);
            if (item.matches("^\\d+$")) {
                try {
                    pkIds.add(Long.valueOf(item));
                } catch (NumberFormatException ignore) {
                    // 超长数字串按原样保留
                }
            }
        }
        if (!pkIds.isEmpty()) {
            try {
                List<QwUser> byPk = qwUserMapper.selectQwUserIdByPkIds(pkIds);
                if (byPk != null) {
                    for (QwUser user : byPk) {
                        if (user != null && StringUtils.isNotEmpty(user.getQwUserId())) {
                            resolved.add(user.getQwUserId());
                        }
                    }
                }
            } catch (Exception e) {
                log.warn("营期群发换算 qw_user.id -> qw_user_id 失败: {}", e.getMessage());
            }
        }
        param.setQwUserIds(new ArrayList<>(resolved));
    }

    @Override
    public List<SopUserLogsInfo> selectRestoreByIsDaysNotStudy(String sopId, String userLogsId) {
        return sopUserLogsInfoMapper.selectRestoreByIsDaysNotStudy(sopId, userLogsId);
    }

    @Override
    public List<ExtCourseSopWatchLogVO> getExtCourseSopWatchLog(QwExtCourseSopWatchLog qwExternalContactId) {


        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSON.parseObject(json, CourseConfig.class);

        List<ExtCourseSopWatchLogVO> watchLogVOList = sopUserLogsInfoMapper.getExtCourseSopWatchLog(qwExternalContactId);

        watchLogVOList.forEach(item->{
            item.setTitle(iFsUserCourseVideoService.selectFsUserCourseVideoByVideoForTitle(item.getVideoId()));
            item.setUrl(config.getSidebarImageUrl());
        });

        return watchLogVOList;
    }

    private R processQwSopLogsBySendMsg(SendUserLogsInfoMsgParam param, Integer draftStrategy, int sendSource) {


        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSON.parseObject(json, CourseConfig.class);

        if (config == null) {
            return R.error("课程默认配置为空，请联系管理员");
        }
        FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forMassSend(config);

        if (StringUtil.strIsNullOrEmpty(param.getCorpId())){
            return R.error("企业编号为空，不能创建一键群发");
        }

        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(param.getCorpId());

        if (qwCompany == null ) {
            return  R.error().put("msg","企业不存在，请联系管理员");
        }

        List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();

        List<SopUserLogsInfo> sopUserLogs = sopUserLogsMapper.selectSopUserLogsByIds(param.getIds());

        // 补全 sopId：MsgType/MsgSop 入口常只传 ids 或 sopIds，未传 sopId，导致群发日志 sop_name 为空
        if (StringUtil.strIsNullOrEmpty(param.getSopId())) {
            if (param.getSopIds() != null && param.getSopIds().length > 0
                    && !StringUtil.strIsNullOrEmpty(param.getSopIds()[0])) {
                param.setSopId(param.getSopIds()[0]);
            } else if (sopUserLogs != null && !sopUserLogs.isEmpty()
                    && !StringUtil.strIsNullOrEmpty(sopUserLogs.get(0).getSopId())) {
                param.setSopId(sopUserLogs.get(0).getSopId());
            }
        }
        // 与 sendUserLogsInfoMsg 同口径：落库前按 sopId 从 SOP 库补齐名称（前端未传 sopName 时）
        fillParamSopNameFromSopDb(param);

        // 查询公司关联小程序数据
        List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
        Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
        Map<Long, CompanyDept> deptMiniAppMap = MiniAppIdResolver.buildDeptMiniAppMap(companyDeptMapper.queryDeptDataAll());
        Map<Long, Long> companyUserDeptMap = MiniAppIdResolver.buildCompanyUserDeptMap(companyUserMapper.selectAllCompanyUserList());

        List<Company> companies = companyMapper.selectCompanyAllList();

        //排序
        int sort = 0;
        //发送类型
        int sendType = 0;

        switch (draftStrategy){
            case 1:
                sort=30000000;
                sendType=5;
                break;
            case 2:
                sort=29999999;
                sendType=9;
                break;
            case 3:
                sort=29999999;
                sendType=10;
                break;
        }

        //分组
        Map<AbstractMap.SimpleEntry<String, String>, List<SopUserLogsInfo>> groupedLogs =
                sopUserLogs.stream()
                        .collect(Collectors.groupingBy(
                                log -> new AbstractMap.SimpleEntry<>(log.getQwUserId(), log.getCorpId())
                        ));


        // 遍历分组
        int finalSort = sort;
        if(null != param.getLiveId()){
            sendType = 20;
        }
        int finalSendType = sendType;
        // 累计本次点击对应的客户发送条数
        final int[] customerCountHolder = new int[]{0};
        final Long[] companyIdHolder = new Long[]{null};
        groupedLogs.forEach((key, logs) -> {

            QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(key.getValue(), key.getKey());

            if (qwUser == null) {
                log.error("员工信息用户不存在:" + key.getKey()+",企业id:"+key.getValue());
            }else {

                if (qwUser.getCompanyUserId()!=null && qwUser.getCompanyId()!=null){
                    if (companyIdHolder[0] == null) {
                        companyIdHolder[0] = qwUser.getCompanyId();
                    }
                    List<QwSopLogs> sopLogsList = processInsertSopUserLogsInfo(logs, qwUser, param, words, config, qwCompany, finalSort,
                            finalSendType,miniMap,companies, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);

                    //批量插入 发送记录
                    if (!sopLogsList.isEmpty()) {
                        processAndInsertQwSopLogsBySendMsg(sopLogsList);
                        customerCountHolder[0] += sopLogsList.size();
                    }

                }else {
                    log.error("员工未绑定销售账号 请先绑定 :" + qwUser.getQwUserName()+",账号id:"+qwUser.getQwUserId());
                }


            }

        });

        // 落库前再补一次 SOP 名称（防止前文查询失败或前端未传）
        fillParamSopNameFromSopDb(param);
        // 记录销售点击一键群发行为（主库），不影响主流程
        qwMassSendLogService.saveMassSendClickLog(param, customerCountHolder[0], sendSource, companyIdHolder[0]);

        return  R.ok();
    }

    private List<QwSopLogs> processInsertSopUserLogsInfo(List<SopUserLogsInfo> sopUserLogsInfos,QwUser qwUser,
                                                         SendUserLogsInfoMsgParam param,List<FastGptChatReplaceWords> words,
                                                         CourseConfig config,QwCompany qwCompany,int finalSort,int finalSendType,
                                                         Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,List<Company> companies,
                                                         Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
                                                         FeishuDirectDocShardAllocator feishuShardAllocator ){

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        String companyUserId = String.valueOf(qwUser.getCompanyUserId()).trim();
        String companyId = String.valueOf(qwUser.getCompanyId()).trim();

        Date dataTime;
        if (!StringUtil.strIsNullOrEmpty(param.getSendTime())){
            dataTime = processDate(param.getSendTime());
        }else {
            dataTime=new Date();
        }
        List<QwSopLogs> sopLogsList=new ArrayList<>();

        //域名
//        String domainName = companyUserMapper.selectDomainByUserId(qwUser.getCompanyUserId());
//        if (StringUtils.isEmpty(domainName)){
//            domainName = config.getRealLinkDomainName();
//        }

        String finalDomainName = "domainName";

        sopUserLogsInfos.forEach(item->{

            QwSopLogs sopLogs=new QwSopLogs();

            sopLogs.setQwUserid(item.getQwUserId());
            sopLogs.setExternalUserId(item.getExternalContactId());
            sopLogs.setExternalId(item.getExternalId());
            sopLogs.setLogType(2);
            sopLogs.setSendStatus(3L);
            sopLogs.setSendTime(sdf.format(dataTime));
            sopLogs.setCompanyId(Long.valueOf(companyId));
            sopLogs.setReceivingStatus(0L);
            sopLogs.setSopId(item.getSopId());
            sopLogs.setCorpId(item.getCorpId());
            sopLogs.setFsUserId(item.getFsUserId());

            sopLogs.setSort(finalSort);
            sopLogs.setSendType(finalSendType);

            sopLogs.setExternalUserName(item.getExternalUserName());

            QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactByIdForStageStatus(item.getExternalId());

            QwSopCourseFinishTempSetting setting=new   QwSopCourseFinishTempSetting();

            //草稿
            List<QwSopCourseFinishTempSetting.Setting> draftList=new ArrayList<>();
            QwSopCourseFinishTempSetting.Setting draftSetting=new QwSopCourseFinishTempSetting.Setting();

            switch (finalSendType){
                case 5:
                case 20:
                    List<QwSopCourseFinishTempSetting.Setting> list = processSetting(item,qwUser, param, words, config, qwCompany,companyUserId,companyId,
                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);
                    setting.setSetting(list);
                    break;
                case 9:


                    draftSetting.setContentType("10");
                    draftSetting.setValue("");
                    draftList.add(draftSetting);

                    setting.setSetting(draftList);
                    break;
                case 10:

                    draftSetting.setContentType("11");
                    draftSetting.setValue("");
                    draftList.add(draftSetting);

                    setting.setSetting(draftList);
                    break;
            }


            setting.setType(2);
            setting.setVideoId(param.getVideoId());
            setting.setCourseId(param.getCourseId());
            setting.setCourseType(param.getCourseType());
            setting.setLiveId(param.getLiveId());
            try {
                sopLogs.setQwUserKey(qwUser.getId());
            }catch (Exception e){
                log.error("设置qwUserId异常", e);
            }
            sopLogs.setContentJson(JSON.toJSONString(setting));

            sopLogsList.add(sopLogs);
        });



        return sopLogsList;

    }

    private Date processDate(String sendTimeParam){
        // 1. 获取当前日期（年月日）
        LocalDate currentDate = LocalDate.now();

        // 2. 解析传入的时分（支持 "HH:mm" 或 "H:mm"）
        LocalTime sendTime = LocalTime.parse(sendTimeParam);

        // 3. 合并为 LocalDateTime
        LocalDateTime dateTime = LocalDateTime.of(currentDate, sendTime);

        // 4. 转换为 Date（需通过 Instant 和系统默认时区）
        Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());

        return date;
    }

    private List<QwSopCourseFinishTempSetting.Setting> processSetting(SopUserLogsInfo item, QwUser qwUser,
                                                                      SendUserLogsInfoMsgParam param,List<FastGptChatReplaceWords> words,
                                                                      CourseConfig config,QwCompany qwCompany,String companyUserId, String companyId,
                                                                      QwExternalContact contact,Date dataTime,String domainName,
                                                                      Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                                                      List<Company> companies,QwSopLogs sopLogs,
                                                                      Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
                                                                      FeishuDirectDocShardAllocator feishuShardAllocator ){
        List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);

        for (QwSopCourseFinishTempSetting.Setting st : list) {
            st.applyRandomTextValue();

            //过滤违禁词
            if ("1".equals(st.getContentType())){
                replaceContent(st.getContentType(), st.getValue(), st::setValue, words); // 替换 value
            }
            //过滤违禁词
            if ("3".equals(st.getContentType())){
                replaceContent(st.getContentType(), st.getLinkTitle(), st::setLinkTitle, words); // 替换 linkTitle
                replaceContent(st.getContentType(), st.getLinkDescribe(), st::setLinkDescribe, words); // 替换 linkTitle
            }

            switch (st.getContentType()){
                //文字和短链一起
                case "1":
                case "3":
//                    if ("1".equals(st.getIsBindUrl())) {
//
//                        addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId,
//                                companyId, item.getExternalId(),param.getStartTime(),dataTime );
//
//                        String sortLink = generateShortLink(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
//                                qwUser.getId(), companyUserId, companyId, domainName,item.getExternalId(),config);
//
//                        if (StringUtils.isNotEmpty(sortLink)) {
//                            if ("3".equals(st.getContentType())) {
//                                st.setLinkUrl(sortLink);
//                            } else {
//                                String currentValue = st.getValue();
//                                if (currentValue == null) {
//                                    st.setValue(sortLink);
//                                } else {
//                                    st.setValue(currentValue
//                                            .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
//                                            .replaceAll("#客户称呼#",StringUtil.strIsNullOrEmpty(contact.getStageStatus())|| "0".equals(contact.getStageStatus())?"同学":contact.getStageStatus())
//                                            + "\n" + sortLink);
//                                }
//                            }
//                        } else {
//                            log.warn("生成短链失败，跳过设置 URL。");
//                        }
//                    }else {
                        if ("1".equals(st.getContentType())) {
                            String defaultName = "同学";
                            if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
                                defaultName = contact.getName();
                            }
                            st.setValue(st.getValue()
                                    .replaceAll("#销售称呼#",StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText())?"":qwUser.getWelcomeText())
                                    .replaceAll("#客户称呼#",contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
                        }
//                    }

                    break;
                //小程序单独
                case "4":

                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
                            item.getExternalId(),item.getStartTime(),dataTime,2 );

                    String linkByMiniApp = createLinkByMiniApp(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
                            qwUser.getId(), companyUserId, companyId, item.getExternalId(), config, null);


                    int effectiveGrade = (item.getGrade() == null) ? 5 : item.getGrade();
                    int listIndex = (effectiveGrade == 1 || effectiveGrade == 2) ? 0 : 1;
                    String miniAppId = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndex, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);
                    st.setMiniType(listIndex);
                    if (!StringUtil.strIsNullOrEmpty(miniAppId)) {
                        st.setMiniprogramAppid(miniAppId);
                    } else {
                        log.error("企业未配置小程序" +  param.getCorpId());
                    }


                    st.setMiniprogramPage(linkByMiniApp);
                    break;
                case "7":
                    QwSop qwSop = qwSopMapper.selectQwSopById(param.getSopId());
                    createVoiceUrl(st, companyUserId, qwSop);
                    break;
                //app
                case "9":
                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(), item.getFsUserId(), String.valueOf(qwUser.getId()),
                            companyUserId, companyId, item.getExternalId(), item.getStartTime(), dataTime,1);

                    QwCreateLinkByAppVO linkByApp = createLinkByApp(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
                            qwUser.getId(), companyUserId, companyId, item.getExternalId(), config, qwUser.getQwUserName(), contact.getFsUserId());
                    st.setLinkUrl(linkByApp.getSortLink());
                    st.setAppLinkUrl(linkByApp.getAppMsgLink());
                    break;

                //自定义小程序
                case "10":

                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
                            item.getExternalId(),item.getStartTime(),dataTime,2 );

                    Optional<Company> matchedCompany = companies.stream()
                            .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
                            .findFirst();
                    if (matchedCompany.isPresent()) {
                        Company company = matchedCompany.get();

                        String customMiniAppId = company.getCustomMiniAppId();

                        if (customMiniAppId != null && !customMiniAppId.trim().isEmpty()) {
                            st.setMiniprogramAppid(customMiniAppId);
                        } else {
                            st.setMiniprogramAppid("该公司未配置自定义小程序:"+companyId);
                        }
                    } else {
                        st.setMiniprogramAppid("未找到匹配的公司的自定义小程序:"+companyId);
                    }

                    break;

                //直播小程序单独
                case "12":
                    String sortLiveLink;
                    sopLogs.setSendType(20);
                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + st.getLiveId() + "&corpId=" + param.getCorpId()+"&qwUserId=" + qwUser.getId() +"&externalId=" + item.getExternalId().toString();


                    String miniprogramLiveTitle = st.getMiniprogramTitle();
                    int maxLiveLength = 17;
                    st.setMiniprogramTitle(miniprogramLiveTitle.length() > maxLiveLength ? miniprogramLiveTitle.substring(0, maxLiveLength) + "..." : miniprogramLiveTitle);

                    String json = configService.selectConfigByKey("his.config");
                    FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
                    //todo 发个人看课记录处理
                    try {
                        createLiveWatchLogAndInsert(qwUser.getCompanyId().toString(), qwUser.getCompanyUserId().toString(),item.getExternalId().toString(),Long.valueOf(st.getLiveId()),sysConfig.getAppId(),2, String.valueOf(qwUser.getId()),param.getCorpId());
                    } catch (Exception e) {
                        log.error("群聊创建直播看课记录失败！", e);
                    }
                    st.setMiniprogramAppid(sysConfig.getAppId());
                    st.setMiniprogramPage(sortLiveLink);
                    st.setContentType("4");
                    try {
                        st.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(st.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : st.getMiniprogramPicUrl());
                    } catch (Exception e) {
                        log.error("赋值-小程序封面地址失败-" + e);
                    }

                    break;
                case "15":
                    //app文本
                    try {
                        qwSop = qwSopMapper.selectQwSopById(param.getSopId());
                        createVoiceUrl(st, companyUserId, qwSop);
                        if (qwUser.getCompanyUserId() != null) {
                            createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    break;
                case "16":
                    //app语音
                    try {
                        qwSop = qwSopMapper.selectQwSopById(param.getSopId());
                        createVoiceUrl(st, companyUserId, qwSop);
                        if (qwUser.getCompanyUserId() != null) {
                            createVoiceUrlToIm(st, String.valueOf(qwUser.getCompanyUserId()), qwSop);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    break;
                case "14":
                    LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(st.getLuckyBagId());
                    if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
                        sopLogs.setSendStatus(5L);
                        sopLogs.setReceivingStatus(0L);
                        sopLogs.setRemark("福袋配置被禁用");
                    }else
                    if (ObjectUtil.isNotEmpty(sopLogs.getFsUserId())){
                        //获取配置并校验
                        SysConfig luckBagConfig = configService.selectConfigByConfigKey("luckyBag.config");
                        if (ObjectUtil.isEmpty(luckBagConfig)) {
                            sopLogs.setSendStatus(5L);
                            sopLogs.setReceivingStatus(0L);
                            sopLogs.setRemark("福袋配置不存在");
                        }
                        // 2. 解析配置值
                        JSONObject jsonObject;
                        try {
                            jsonObject = JSON.parseObject(luckBagConfig.getConfigValue());
                            Integer count = jsonObject.getInteger("weekLimit");

                            // 查询用户记录并校验次数
                            LuckyBagCollectRecord queryRecord = new LuckyBagCollectRecord();
                            queryRecord.setUserId(sopLogs.getFsUserId());
                            queryRecord.setCollectType("1");
                            // 动态计算时间范围
                            LocalDate endDate = LocalDate.now();
                            LocalDate startDate = endDate.minusDays(6); // 包含今天

                            Map<String, Object> params = new HashMap<>();
                            params.put("beginSendTime", startDate.toString());
                            params.put("endSendTime", endDate.toString());
                            queryRecord.setParams(params);
                            List<LuckyBagCollectRecord> luckyBagCollectRecords =
                                    luckyBagCollectRecordMapper.selectLuckyBagCollectRecordList(queryRecord);

                            // 判断是否超过限制
                            if (luckyBagCollectRecords.size() >= count) {
                                sopLogs.setSendStatus(5L);
                                sopLogs.setReceivingStatus(0L);
                                sopLogs.setRemark("超过福袋发放次数");
                            }

                        } catch (Exception e) {
                            // 处理配置解析异常
                            sopLogs.setSendStatus(5L);
                            sopLogs.setReceivingStatus(0L);
                            sopLogs.setRemark("福袋配置解析失败");
                        }
                    }

                    linkByMiniApp = createActivityLinkByMiniApp(st,sopLogs, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
                            String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config,null);

                    int effectiveGrade14 = (item.getGrade() == null) ? 5 : item.getGrade();
                    int listIndex14 = (effectiveGrade14 == 1 || effectiveGrade14 == 2) ? 0 : 1;
                    if (effectiveGrade14 == 6) {
                        listIndex14 = 2;
                    }
                    String miniAppId14 = resolveSopMiniAppId(qwUser, Long.valueOf(companyId), listIndex14, miniMap, deptMiniAppMap, companyUserDeptMap, qwCompany);

                    if (!StringUtil.strIsNullOrEmpty(miniAppId14)) {
                        st.setMiniprogramAppid(miniAppId14);
                    } else {
                        log.error("公司的小程序id为空：采用了前端传的固定值" + sopLogs.getSopId());
                    }

                    st.setMiniprogramTitle("福袋发放");

                    st.setMiniprogramPage(linkByMiniApp);
                    break;
                case "13":
                    if (StringUtil.strIsNullOrEmpty(st.getMiniprogramPage()) && !StringUtil.strIsNullOrEmpty(st.getArticleUrl())) {
                        String articleUrlJson = "{\"articleUrl\":\"" + st.getArticleUrl() + "\"}";
                        st.setMiniprogramPage("/pages_course/webArticle.html?url=" + articleUrlJson);
                        log.info("订阅号文章类型（processSetting），使用articleUrl生成miniprogramPage: {}", st.getMiniprogramPage());
                    }
                    log.info("处理订阅号文章类型（processSetting），保留字段: articleUrl={}, miniprogramPicUrl={}, miniprogramPage={}",
                            st.getArticleUrl(), st.getMiniprogramPicUrl(), st.getMiniprogramPage());
                    break;
                //飞书看课（固定免授权：一批客户复用同一飞书文档，不走领取页）
                case "17":
                    st.setFeishuNeedAuth(0);
                    Integer videoId18 = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                    Integer courseId18 = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                    Long companyIdLong = Long.parseLong(companyId);
                    Long companyUserIdLong = Long.parseLong(companyUserId);
                    Long externalId = item.getExternalId();
                    if (videoId18 == null) {
                        log.error("飞书链接生成失败：视频ID为空，sopId={}", item.getSopId());
                        break;
                    }
                    if (courseId18 == null) {
                        log.error("飞书链接生成失败：课程ID为空，sopId={}", item.getSopId());
                        break;
                    }
                    if (externalId == null) {
                        log.error("飞书链接生成失败：外部ID为空，sopId={}", item.getSopId());
                        break;
                    }
                    // 飞书发课：发时写入/更新看课记录，已存在则把 watch_type 改为飞书(3)
                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),
                            item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
                            item.getExternalId(), item.getStartTime(), dataTime, 3);
                    Map<String, String> feishuH5Link = createFeishuH5Link(st, param.getCorpId(), dataTime, courseId18, videoId18, String.valueOf(qwUser.getId()), companyUserId, companyId, externalId, config);
                    String shortLink = feishuH5Link.get("link");
                    String feishuLink = resolveFeishuSendLinkSafely(videoId18.longValue(), companyIdLong, courseId18.longValue(),
                            companyUserIdLong, shortLink, st.getFeishuAccountId(), 0,
                            item.getSopId(), item.getUserLogsId(), externalId, null,
                            String.valueOf(qwUser.getId()), qwUser.getQwUserName(), companyUserId, companyId,
                            param.getCorpId(), dataTime, item.getStartTime(), feishuShardAllocator);
                    if (StringUtils.isNotEmpty(feishuLink)) {
                        String txt18 = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                        String customerTitle = contact == null ? "同学" :
                                (StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus());
                        if (StringUtils.isNotEmpty(st.getLinkTitle())) {
                            st.setLinkTitle(st.getLinkTitle()
                                    .replaceAll("#销售称呼#", txt18)
                                    .replaceAll("#客户称呼#", customerTitle));
                        }
                        if (StringUtils.isNotEmpty(st.getLinkDescribe())) {
                            st.setLinkDescribe(st.getLinkDescribe()
                                    .replaceAll("#销售称呼#", txt18)
                                    .replaceAll("#客户称呼#", customerTitle));
                        }
                        st.setLinkUrl(feishuLink);
                        fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                    }
                    break;
                //飞书NEW：走需授权链路（发时写看课记录 + 入库短链），发出授权页 URL
                case "18":
                    Integer videoIdFeishuNewPs = param.getVideoId() != null ? param.getVideoId().intValue() : null;
                    Integer courseIdFeishuNewPs = param.getCourseId() != null ? param.getCourseId().intValue() : null;
                    Long externalIdFeishuNew = item.getExternalId();
                    if (externalIdFeishuNew == null) {
                        log.error("飞书NEW链接生成失败：外部ID为空，sopId={}", item.getSopId());
                        break;
                    }
                    // 与飞书需授权(17)一致：发送时写入看课记录；watchType=3 飞书
                    addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(),
                            item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
                            item.getExternalId(), item.getStartTime(), dataTime, 3);
                    String txtFeishuNewPs = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                    String customerTitleFeishuNewPs = contact == null ? "同学" :
                            (StringUtil.strIsNullOrEmpty(contact.getStageStatus()) || "0".equals(contact.getStageStatus()) ? "同学" : contact.getStageStatus());
                    if (StringUtils.isNotEmpty(st.getLinkTitle())) {
                        st.setLinkTitle(st.getLinkTitle()
                                .replaceAll("#销售称呼#", txtFeishuNewPs)
                                .replaceAll("#客户称呼#", customerTitleFeishuNewPs));
                    }
                    if (StringUtils.isNotEmpty(st.getLinkDescribe())) {
                        st.setLinkDescribe(st.getLinkDescribe()
                                .replaceAll("#销售称呼#", txtFeishuNewPs)
                                .replaceAll("#客户称呼#", customerTitleFeishuNewPs));
                    }
                    st.setLinkUrl(createFeishuNewSendUrl(st, param.getCorpId(), dataTime, courseIdFeishuNewPs, videoIdFeishuNewPs,
                            qwUser.getId(), companyUserId, companyId, externalIdFeishuNew, config, null));
                    fillFeishuLinkCardDefaults(st, param.getVideoId().longValue(), param.getCourseId().longValue());
                    break;

                default:
                    break;

            }
        }

        return  list;
    }

    /**
     * 拼飞书领取页（仅 contentType=18 使用）。
     */
    private String buildFeishuClaimPageUrl(String shortLink, CourseConfig config) {
        if (StringUtils.isEmpty(shortLink) || config == null
                || StringUtils.isEmpty(config.getRealLinkDomainName())
                || StringUtils.isEmpty(config.getFeishuMpAppId())) {
            return null;
        }
        String domain = config.getRealLinkDomainName().trim();
        if (!domain.endsWith("/")) {
            domain = domain + "/";
        }
        return domain + "?uuid=" + shortLink + "&appid=" + config.getFeishuMpAppId().trim();
    }

    /**
     * 飞书NEW（contentType=18）：按需授权链路处理——
     * 1）发送侧须调用 addWatchLogIfNeeded 写入看课记录；
     * 2）短链入库标记 feishuNeedAuth=1，领取后走 getFeishuCourseNewLink；
     * 3）发出 URL 为 {realLinkDomainName}/?uuid={link}&appid={feishuMpAppId}（不在发课瞬间调飞书建文档）。
     */
    private String createFeishuNewSendUrl(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
                                          Integer courseId, Integer videoId, Long qwUserId,
                                          String companyUserId, String companyId, Long externalId,
                                          CourseConfig config, String chatId) {
        if (config == null || StringUtils.isEmpty(config.getRealLinkDomainName())) {
            throw new BaseException("飞书NEW链接生成失败：未配置通用看课域名");
        }
        if (StringUtils.isEmpty(config.getFeishuMpAppId())) {
            throw new BaseException("飞书NEW链接生成失败：未配置飞书服务号AppID");
        }
        if (videoId == null) {
            throw new BaseException("飞书NEW链接生成失败：视频ID为空");
        }
        if (courseId == null) {
            throw new BaseException("飞书NEW链接生成失败：课程ID为空");
        }
        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
                companyUserId, companyId, externalId, 0, chatId);
        // 固定需授权，对齐飞书授权看课链路（领取时走 getFeishuCourseNewLink）
        link.setFeishuNeedAuth(1);
        if (setting != null && setting.getFeishuAccountId() != null) {
            link.setFeishuAccountId(setting.getFeishuAccountId());
        }
        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link, courseMap);
        String courseJson = JSON.toJSONString(courseMap);
        link.setRealLink(REAL_LINK_PREFIX + courseJson);
        link.setUpdateTime(createUpdateTime(setting, sendTime, config));
        fsCourseLinkMapper.insertFsCourseLink(link);

        String domain = config.getRealLinkDomainName().trim();
        if (!domain.endsWith("/")) {
            domain = domain + "/";
        }
        return domain + "?uuid=" + link.getLink() + "&appid=" + config.getFeishuMpAppId().trim();
    }

    /**
     * 生成飞书看课课程短链（私聊：传 externalId；发群：传 chatId）
     */
    private Map<String, String> createFeishuH5Link(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
                                                   Integer courseId, Integer videoId, String qwUserId,
                                                   String companyUserId, String companyId, Long externalId, CourseConfig config) {
        return createFeishuH5Link(setting, corpId, sendTime, courseId, videoId, qwUserId, companyUserId, companyId, externalId, config, null);
    }

    private Map<String, String> createFeishuH5Link(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
                                                   Integer courseId, Integer videoId, String qwUserId,
                                                   String companyUserId, String companyId, Long externalId, CourseConfig config, String chatId) {
        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, Long.valueOf(qwUserId),
                companyUserId, companyId, externalId, 3, chatId);
        link.setFeishuAccountId(setting.getFeishuAccountId());
        // 飞书旧链接（contentType=17）固定免授权
        link.setFeishuNeedAuth(0);
        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link, courseMap);
        String courseJson = JSON.toJSONString(courseMap);
        String realLinkFull = feishuMiniappLink + courseJson;
        if (StringUtils.isNotEmpty(config.getRealLinkGjDomainName())) {
            realLinkFull = config.getRealLinkGjDomainName() + realLinkFull;
        }
        link.setRealLink(realLinkFull);
        link.setUpdateTime(createUpdateTime(setting, sendTime, config));
        fsCourseLinkMapper.insertFsCourseLink(link);
        Map<String, String> result = new HashMap<>();
        result.put("url", link.getRealLink());
        result.put("link", link.getLink());
        return result;
    }

    public String createActivityLinkByMiniApp(QwSopCourseFinishTempSetting.Setting st, QwSopLogs sopLogs, String corpId, Date sendTime, Integer courseId, Integer videoId, String qwUserId, String companyUserId, String companyId, Long externalId, CourseConfig config, String chatId) {
        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, Long.valueOf(qwUserId),
                companyUserId, companyId, null, 3, chatId);
        Date updateTime = createUpdateTime(st, sendTime, config);
        link.setUpdateTime(updateTime);
        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link, courseMap);
        Long businessId = addLuckyBagCollectRecord(st,sopLogs,updateTime,companyUserId,companyId,chatId);
        courseMap.setBusinessId(String.valueOf(businessId));
        st.setBusinessId(String.valueOf(businessId));
        st.setExternalUserId(sopLogs.getExternalUserId());
        String json = configService.selectConfigByKey("luckyBag.config");
        Map<String, Object> luckyBagConfig = JSON.parseObject(json, Map.class);
        Object miniprogramPicUrl = luckyBagConfig.get("miniprogramPicUrl");
        if(miniprogramPicUrl != null){
            st.setMiniprogramPicUrl(miniprogramPicUrl.toString());
        }
        courseMap.setQwExternalId(sopLogs.getExternalId());
        String realLinkFull = appActivitlLink + JSON.toJSONString(courseMap);
        link.setRealLink(realLinkFull);
        log.error("存入fs_course_link:" + registeredRealLink );
        log.error("QwSopCourseFinishTempSetting.Setting:{}" ,st );
        //存短链-
        fsCourseLinkMapper.insertFsCourseLink(link);
        return link.getRealLink();
    }

    /**
     * 增加福袋发放记录、领取记录
     *
     * @param content
     * @param qwSopLogs
     * @param sendTime
     * @param companyUserId
     * @param companyId
     * @param chatId
     */
    private Long addLuckyBagCollectRecord(QwSopCourseFinishTempSetting.Setting content,
                                          QwSopLogs qwSopLogs,
                                          Date sendTime,
                                          String companyUserId,
                                          String companyId,
                                          String chatId) {
        try {
            // 参数校验
            if (content == null || qwSopLogs == null || sendTime == null) {
                log.warn("添加福袋记录失败：必要参数为空 [content:{}, qwSopLogs:{}, sendTime:{}]",
                        content, qwSopLogs, sendTime);
                return null;
            }

            if (StringUtils.isEmpty(companyId) || StringUtils.isEmpty(companyUserId)) {
                log.warn("公司ID或用户ID为空 [companyId:{}, companyUserId:{}]", companyId, companyUserId);
                return null;
            }

            // 验证福袋ID
            if (content.getLuckyBagId() == null) {
                log.warn("福袋ID为空");
                return null;
            }

            // 查询福袋信息
            LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(content.getLuckyBagId());
            if (luckyBag == null) {
                log.warn("未找到对应的福袋信息 [luckyBagId:{}]", content.getLuckyBagId());
                return null;
            }

            // 检查福袋状态
            if (luckyBag.getDataStatus() != null && luckyBag.getDataStatus().equals(0)) {
                log.warn("福袋被禁用 [luckyBagId:{}]", content.getLuckyBagId());
                return null;
            }

            // 查询公司信息
            Company company = companyMapper.selectCompanyById(Long.valueOf(companyId));
            if (company == null) {
                log.warn("未找到对应的公司信息 [companyId:{}]", companyId);
                return null;
            }

            // 构建福袋记录
            LuckyBagCollectRecord luckyBagCollectRecord = buildLuckyBagRecord(content, qwSopLogs, sendTime,
                    companyUserId, companyId, chatId, company, luckyBag);

            // 插入记录并返回ID
            int result = luckyBagCollectRecordMapper.insertLuckyBagCollectRecord(luckyBagCollectRecord);
            if (result <= 0) {
                log.warn("福袋记录插入失败 [luckyBagId:{}]", content.getLuckyBagId());
                return null;
            }

            // 返回新增记录的ID
            Long recordId = luckyBagCollectRecord.getId();
            if (recordId == null) {
                log.warn("福袋记录插入成功但未返回ID [luckyBagId:{}]", content.getLuckyBagId());
                return null;
            }

            log.info("福袋记录添加成功 [recordId:{}, luckyBagId:{}]", recordId, content.getLuckyBagId());
            return recordId;

        } catch (NumberFormatException e) {
            log.error("ID转换失败 [companyId:{}, companyUserId:{}]", companyId, companyUserId, e);
            return null;
        } catch (Exception e) {
            log.error("ID:" + (content != null ? content.getLuckyBagId() : "unknown") + "-添加福袋记录失败", e);
            return null;
        }
    }

    private void createVoiceUrlToIm(QwSopCourseFinishTempSetting.Setting st, String companyUserId, QwSop qwSop) {
        QwSopTempVoice qwSopTempVoice = sopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(Long.valueOf(companyUserId), st.getValue());
        if (qwSopTempVoice != null && qwSopTempVoice.getVoiceUrl() != null && qwSopTempVoice.getRecordType() == 1) {
            st.setVoiceUrl(qwSopTempVoice.getUserVoiceUrl());
            st.setVoiceDuration(String.valueOf(qwSopTempVoice.getDuration()));
        } else if (qwSopTempVoice == null) {
            if(st.getValue() != null){
                qwSopTempVoice = new QwSopTempVoice();
                qwSopTempVoice.setCompanyUserId(Long.valueOf(companyUserId));
                qwSopTempVoice.setVoiceTxt(st.getValue());
                qwSopTempVoice.setRecordType(0);
                sopTempVoiceService.insertQwSopTempVoice(qwSopTempVoice);
            }
        }
    }

    private List<QwSopCourseFinishTempSetting.Setting> parseSettings(String jsonData) {
        try {
            if (jsonData.startsWith("[") && jsonData.endsWith("]")) {
                return JSONArray.parseArray(jsonData, QwSopCourseFinishTempSetting.Setting.class);
            } else {
                String fixedJson = JSON.parseObject(jsonData, String.class);
                return JSONArray.parseArray(fixedJson, QwSopCourseFinishTempSetting.Setting.class);
            }
        } catch (Exception e) {
            log.error("解析模板设置失败", e);
            return null;
        }
    }
    // 提取通用替换逻辑的方法
    public void replaceContent(String contentType, String content, Consumer<String> setter, List<FastGptChatReplaceWords> words) {
        if (contentType != null && content != null && !content.isEmpty()) {
            for (FastGptChatReplaceWords word : words) {
                if (content.contains(word.getContent())) {
                    // 如果包含目标字段，则替换
                    setter.accept(content.replace(word.getContent(), word.getChangeCount()));
                }
            }
        }
    }

    private void processAndInsertQwSopLogsBySendMsg(List<QwSopLogs> sopLogsList ) {
        // 定义批量插入的大小
        int batchSize = 500;

        // 循环处理外部用户 ID，每次处理批量大小的子集
        for (int i = 0; i < sopLogsList.size(); i += batchSize) {

            int endIndex = Math.min(i + batchSize, sopLogsList.size());
            List<QwSopLogs> batchList = sopLogsList.subList(i, endIndex);  // 获取当前批次的子集

            // 直接使用批次数据进行批量更新，不需要额外的 List
            try {
                qwSopLogsService.applyBlacklistCheck(batchList);
                qwSopLogsMapper.batchInsertQwSopLogsOneTouch(batchList);
            } catch (Exception e) {
                // 记录异常日志，方便后续排查问题
                log.error("批量执行一键群发时发生异常，处理的批次起始索引为: " + i, e);
            }
        }
    }

    /**
     * 飞书链接卡片（17/18）为空时按前端逻辑兜底：
     * linkTitle=课程名，linkDescribe=课节名，linkImageUrl=课程封面图
     */
    private void fillFeishuLinkCardDefaults(QwSopCourseFinishTempSetting.Setting setting, Long videoId, Long courseId) {
        if (setting == null) {
            return;
        }
        String courseName = null;
        String courseImgUrl = null;
        String videoTitle = null;
        try {
            if (courseId != null && (StringUtils.isEmpty(setting.getLinkTitle()) || StringUtils.isEmpty(setting.getLinkImageUrl()))) {
                FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(courseId);
                if (course != null) {
                    courseName = course.getCourseName();
                    courseImgUrl = course.getImgUrl();
                }
            }
            if (videoId != null && StringUtils.isEmpty(setting.getLinkDescribe())) {
                videoTitle = iFsUserCourseVideoService.selectFsUserCourseVideoByVideoForTitle(videoId);
            }
        } catch (Exception e) {
            log.warn("补齐飞书链接卡片失败 courseId={}, videoId={}", courseId, videoId, e);
        }
        if (StringUtils.isEmpty(setting.getLinkTitle()) && StringUtils.isNotEmpty(courseName)) {
            setting.setLinkTitle(courseName);
        }
        if (StringUtils.isEmpty(setting.getLinkDescribe()) && StringUtils.isNotEmpty(videoTitle)) {
            setting.setLinkDescribe(videoTitle);
        }
        if (StringUtils.isEmpty(setting.getLinkImageUrl()) && StringUtils.isNotEmpty(courseImgUrl)) {
            setting.setLinkImageUrl(courseImgUrl);
        }
    }

    //插入观看记录
    public void addWatchLogIfNeeded(String sopId, Integer videoId, Integer courseId,
                                     Long fsUserId, String qwUserId, String companyUserId,
                                     String companyId, Long externalId, String startTime,Date createTime, Integer watchType) {

        try {
            FsCourseWatchLog watchLog = new FsCourseWatchLog();
            watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
            watchLog.setQwExternalContactId(externalId);
            watchLog.setSendType(2);
            watchLog.setQwUserId(Long.valueOf(qwUserId));
            watchLog.setSopId(sopId);
            watchLog.setDuration(0L);
            watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
            watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
            watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
            watchLog.setCreateTime(createTime);
            watchLog.setUpdateTime(createTime);
            watchLog.setLogType(3);
            watchLog.setUserId(fsUserId);
            watchLog.setWatchType(watchType);
            watchLog.setCampPeriodTime(convertStringToDate(startTime,"yyyy-MM-dd"));

            // 同步落库：按 courseId 轻量取 project（有进程内缓存）
            Long project = fsCourseWatchLogProjectFiller.getProjectByCourseId(
                    courseId != null ? courseId.longValue() : null);
            if (project != null) {
                watchLog.setProject(project);
            }
            //存看课记录
            fsCourseWatchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
        }catch (Exception e){
            log.error("一键群发失败-插入观看记录失败："+e.getMessage());
        }


    }

    private String generateShortLink(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
                                     Integer courseId, Integer videoId, Long qwUserId,
                                     String companyUserId, String companyId,String domainName, Long externalId,CourseConfig config) {

        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
                companyUserId, companyId, externalId,0,null);

        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link,courseMap);

        String courseJson = JSON.toJSONString(courseMap);
        String realLinkFull = REAL_LINK_PREFIX + courseJson;
        link.setRealLink(realLinkFull);

        Date updateTime = createUpdateTime(setting, sendTime, config);

        link.setUpdateTime(updateTime);

        String sortLink = createLink(link,domainName);

        //存短链-
        fsCourseLinkMapper.insertFsCourseLink(link);

        return sortLink.replaceAll("^[\\s\\u2005]+", "");
    }

    public String createLinkByMiniApp(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
                                     Integer courseId, Integer videoId, Long qwUserId,
                                     String companyUserId, String companyId, Long externalId,CourseConfig config, String chatId) {

        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
                                                companyUserId, companyId, externalId,3,chatId);

        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link,courseMap);

        String courseJson = JSON.toJSONString(courseMap);
        String realLinkFull = miniappRealLink + courseJson;
        link.setRealLink(realLinkFull);

        Date updateTime = createUpdateTime(setting, sendTime, config);

        link.setUpdateTime(updateTime);
        //存短链-
        fsCourseLinkMapper.insertFsCourseLink(link);
        return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
    }

    private QwCreateLinkByAppVO createLinkByApp(QwSopCourseFinishTempSetting.Setting setting, String corpId,
                                                Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                                String companyUserId, String companyId, Long externalId,
                                                CourseConfig config,String qwUserName,Long fsUserId){

        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
                companyUserId, companyId, externalId,4,null);

        FsCourseRealLink courseMap = new FsCourseRealLink();
        BeanUtils.copyProperties(link,courseMap);

        String courseJson = JSON.toJSONString(courseMap);
        String realLinkFull = APP_LINK_PREFIX + courseJson;
        if (CloudHostUtils.hasCloudHostName("木易华康")) {
            realLinkFull = REAL_LINK_PREFIX + courseJson;
        }
        link.setRealLink(realLinkFull);

        Date updateTime = createUpdateTime(setting, sendTime, config);

        link.setUpdateTime(updateTime);

        String sortLink = appLink+link.getLink()+"&videoId="+videoId;

        String appMsgLink=appRealLink+link.getLink();

        QwCreateLinkByAppVO byAppVO=new QwCreateLinkByAppVO();
        byAppVO.setSortLink(sortLink);
        byAppVO.setAppMsgLink(appMsgLink);

            //异步生成app链接记录
        asyncSopTestService.createFsCourseSopAppLink(link.getLink(),sendTime,updateTime,companyId,companyUserId,String.valueOf(qwUserId),
                qwUserName,corpId,courseId,setting.getLinkTitle(),setting.getLinkImageUrl(),videoId,
                setting.getLinkDescribe(),appMsgLink,externalId);

        //异步给app用户发送消息
        asyncSopTestService.asyncSendMsgBySopAppLink(externalId,setting.getLinkTitle(),setting.getLinkDescribe(),appMsgLink);

        //存短链-
        fsCourseLinkMapper.insertFsCourseLink(link);

        return byAppVO;
    }



    private String createLink(FsCourseLink link,String domain){
        String sortLink = domain + SHORT_LINK_PREFIX + link.getLink();
        return sortLink;
    }

    private Date createUpdateTime(QwSopCourseFinishTempSetting.Setting setting,Date sendTime,CourseConfig config){

        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
                ? config.getVideoLinkExpireDate()
                : setting.getExpiresDays();

//         使用 Java 8 时间 API 计算过期时间
        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays-1);
        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());

        return updateTime;
    }

    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                           String companyUserId, String companyId, Long externalId, Integer type, String chatId) {
        // 手动创建 FsCourseLink 对象，避免使用 BeanUtils.copyProperties
        FsCourseLink link = new FsCourseLink();
        link.setCompanyId(Long.parseLong(companyId));
        link.setQwUserId(qwUserId);
        link.setCompanyUserId(Long.parseLong(companyUserId));
        link.setVideoId(videoId.longValue());
        link.setCorpId(corpId);
        link.setCourseId(courseId.longValue());
        link.setChatId(chatId);
        link.setQwExternalId(externalId);
        link.setLinkType(type); //小程序
        link.setUNo(UUID.randomUUID().toString());
        String randomString = generateRandomStringWithLock();
        if (StringUtil.strIsNullOrEmpty(randomString)) {
            link.setLink(UUID.randomUUID().toString().replace("-", ""));
        } else {
            link.setLink(randomString);
        }

        link.setCreateTime(sendTime);
        link.setProjectCode(cloudHostProper.getProjectCode());

        return link;
    }

    /**
     * 时间字符串转Date时间
     * @param dateString
     * @return
     */
    public static Date convertStringToDate(String dateString,String pattern) {
        if (dateString == null || dateString.isEmpty()) {
            return null;
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        LocalDateTime localDateTime;
        LocalDate localDate;
        // 先解析成 LocalDate（只含年月日）
        if (pattern.equals("yyyy-MM-dd")){
            // 先解析成 LocalDate（只含年月日）
            localDate = LocalDate.parse(dateString, formatter);
            // 将 LocalDate 转为当天 00:00:00 的 LocalDateTime
            localDateTime = localDate.atStartOfDay();
        }else {
            localDateTime = LocalDateTime.parse(dateString, formatter);
        }
        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }

    /**
     * 直播看课记录处理
     * @param companyId
     * @param companyUserId
     * @param externalId
     * @param liveId
     * @param appId
     * @param logSource
     * @param qwUserId
     * @param corpId
     */
    public void createLiveWatchLogAndInsert(String companyId,String companyUserId,String externalId,Long liveId,String appId,Integer logSource,String qwUserId,String corpId){
        try{
            // 写入对应数据源的记录表
            LiveWatchLog itemLiveWatchLog = new LiveWatchLog();
            itemLiveWatchLog.setLiveId(liveId);
            itemLiveWatchLog.setLogType(3);
            itemLiveWatchLog.setSopCreateTime(new Date());
            itemLiveWatchLog.setCompanyId(Long.valueOf(companyId));
            itemLiveWatchLog.setCompanyUserId(Long.valueOf(companyUserId));
            itemLiveWatchLog.setSendAppId(appId);
            itemLiveWatchLog.setLogSource(logSource);
            itemLiveWatchLog.setQwUserId(qwUserId);
            itemLiveWatchLog.setExternalContactId(Long.valueOf(externalId));
            itemLiveWatchLog.setCorpId(corpId);
            if(liveWatchLogMapper.updateLiveWatchLogCondition(itemLiveWatchLog) > 0){

            }else{
                List<LiveWatchLog> handleList= new ArrayList<>();
                handleList.add(itemLiveWatchLog);
                liveWatchLogMapper.insertLiveWatchLogBatch(handleList);
            }
        }catch(Exception e){
            log.error("创建直播看课记录失败：{}",e.getMessage());
        }


    }

    /**
     * 构建福袋记录对象
     */
    private LuckyBagCollectRecord buildLuckyBagRecord(QwSopCourseFinishTempSetting.Setting content,
                                                      QwSopLogs qwSopLogs,
                                                      Date sendTime,
                                                      String companyUserId,
                                                      String companyId,
                                                      String chatId,
                                                      Company company,
                                                      LuckyBag luckyBag) {
        LuckyBagCollectRecord record = new LuckyBagCollectRecord();
        QwUser qwUser = qwUserMapper.selectQwUserEntityByQwUserIdAndCorId(qwSopLogs.getQwUserid(),qwSopLogs.getCorpId());
        record.setQwUserId(qwUser.getQwUserId());
        record.setQwUserName(qwUser.getQwUserName());
        record.setLuckyBagId(content.getLuckyBagId());
        record.setExpiryTime(sendTime);
        record.setCollectType("3");
        record.setCompanyId(Long.valueOf(companyId));
        record.setUserId(qwSopLogs.getFsUserId());
        if (ObjectUtil.isNotEmpty(qwSopLogs.getFsUserId())){
            FsUser fsUser = fsUserMapper.selectFsUserByUserId(qwSopLogs.getFsUserId());
            record.setUserName(ObjectUtil.isNotEmpty(fsUser)?fsUser.getNickName():null);
        }
        record.setCompanyName(company.getCompanyName());
        record.setCompanyUserId(Long.valueOf(companyUserId));
        record.setSendLink(content.getMiniprogramPage());

        // 设置奖励类型和聊天信息
        if (StringUtils.isNotEmpty(chatId)) {
            record.setRewardType(1L);
            record.setChatId(chatId);
            record.setExternalUserName(qwSopLogs.getExternalUserName());
        } else {
            record.setRewardType(2L);
        }

        // 设置币种金额
        if (luckyBag.getRewardType() != null && luckyBag.getRewardType().equals("1")) {
            record.setCoinAmount(luckyBag.getAmount());
        }

        return record;
    }

    private String resolveSopMiniAppId(QwUser qwUser,
                                       Long companyId,
                                       int listIndex,
                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                       Map<Long, CompanyDept> deptMiniAppMap,
                                       Map<Long, Long> companyUserDeptMap,
                                       QwCompany qwCompany) {
        Long deptId = (listIndex == 0 || listIndex == 1)
                ? MiniAppIdResolver.getDeptId(qwUser.getCompanyUserId(), companyUserDeptMap)
                : null;
        String miniAppId = MiniAppIdResolver.resolveMiniAppId(
                deptId, companyId, listIndex, qwUser.getSendMsgType(), miniMap, deptMiniAppMap);
        if (StringUtil.strIsNullOrEmpty(miniAppId) && qwCompany != null
                && !StringUtil.strIsNullOrEmpty(qwCompany.getMiniAppId())) {
            miniAppId = qwCompany.getMiniAppId();
        }
        return miniAppId;
    }

    /**
     * contentType=17 专用：固定免授权，一批客户复用飞书文档；不走 18 的领取页。
     */
    private String resolveFeishuSendLinkSafely(Long videoId, Long companyId, Long courseId,
                                               Long companyUserId, String shortLink, Long feishuAccountId,
                                               Integer feishuNeedAuth, String sopId, String userLogsId,
                                               Long externalId, Long fsUserId, String qwUserId, String qwUserName,
                                               String companyUserIdStr, String companyIdStr, String corpId,
                                               Date sendTime, String elementTime,
                                               FeishuDirectDocShardAllocator feishuShardAllocator) {
        try {
            // 17 已取消授权模式，无论入参如何一律免授权
            Integer directDocShard = null;
            if (feishuShardAllocator != null) {
                if (feishuShardAllocator.markDirectDocCacheForRefresh(companyUserId, videoId, courseId, feishuAccountId)) {
                    feiShuService.invalidateDirectDocCache(companyUserId, videoId, courseId, feishuAccountId);
                }
                directDocShard = feishuShardAllocator.nextShard(companyUserId, videoId, courseId, feishuAccountId);
            }
            String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyId, courseId, companyUserId,
                    shortLink, feishuAccountId, 0, directDocShard, null);
            if (StringUtils.isEmpty(feishuLink)) {
                log.error("生成飞书免授权链接失败，跳过本条: sopId={}, videoId={}, externalId={}",
                        sopId, videoId, externalId);
                recordFeishuLinkGenerationFailed(sopId, userLogsId, externalId, fsUserId, qwUserId, qwUserName,
                        companyUserIdStr, companyIdStr, corpId, sendTime, elementTime, videoId,
                        "生成飞书免授权链接失败");
                return null;
            }
            return feishuLink;
        } catch (Exception e) {
            log.error("生成飞书免授权链接异常，跳过本条: sopId={}, videoId={}, externalId={}",
                    sopId, videoId, externalId, e);
            recordFeishuLinkGenerationFailed(sopId, userLogsId, externalId, fsUserId, qwUserId, qwUserName,
                    companyUserIdStr, companyIdStr, corpId, sendTime, elementTime, videoId,
                    "生成飞书免授权链接失败: " + e.getMessage());
            return null;
        }
    }

    private void recordFeishuLinkGenerationFailed(String sopId, String userLogsId, Long externalId, Long fsUserId,
                                                  String qwUserId, String qwUserName, String companyUserId,
                                                  String companyId, String corpId, Date sendTime, String elementTime,
                                                  Long videoId, String failReason) {
        try {
            SopGenerationFailedLog failedLog = new SopGenerationFailedLog();
            failedLog.setSopId(sopId);
            failedLog.setUserLogsId(userLogsId);
            failedLog.setExternalId(externalId);
            failedLog.setFsUserId(fsUserId);
            failedLog.setQwUserId(qwUserId);
            failedLog.setQwUserName(qwUserName);
            failedLog.setCompanyUserId(companyUserId);
            failedLog.setCompanyId(companyId);
            failedLog.setCorpId(corpId);
            failedLog.setFailType(SopGenerationFailedLog.FailType.OTHER.getCode());
            String reason = failReason;
            if (videoId != null) {
                reason = failReason + " (videoId=" + videoId + ")";
            }
            failedLog.setFailReason(reason);
            failedLog.setIsRetry(SopGenerationFailedLog.RetryStatus.NOT_RETRY.getCode());
            failedLog.setDayNum(0L);
            if (sendTime != null) {
                failedLog.setSendTime(sendTime.toInstant().atZone(ZoneId.systemDefault()).format(FEISHU_FAIL_SEND_TIME_FORMATTER));
            }
            failedLog.setElementTime(elementTime);
            sopGenerationFailedLogService.batchInsert(Collections.singletonList(failedLog));
        } catch (Exception e) {
            log.error("记录飞书链接生成失败日志异常: sopId={}, externalId={}, error={}",
                    sopId, externalId, e.getMessage(), e);
        }
    }

}
