package com.fs.course.service.impl;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fs.common.BeanCopyUtils;
import com.fs.common.constant.FsConstants;
import com.fs.common.core.domain.AjaxResult;
import com.fs.common.core.domain.R;
import com.fs.common.core.domain.ResponseResult;
import com.fs.common.core.domain.entity.SysDictData;
import com.fs.common.core.redis.RedisCache;
import com.fs.common.enums.BizResponseEnum;
import com.fs.common.exception.CustomException;
import com.fs.common.utils.CloudHostUtils;
import com.fs.common.utils.DateUtils;
import com.fs.common.utils.StringUtils;
import com.fs.common.utils.date.DateUtil;
import com.fs.company.constant.CompanyTrafficConstants;
import com.fs.company.domain.Company;
import com.fs.company.domain.CompanyCompanyFsuser;
import com.fs.company.domain.CompanyUser;
import com.fs.company.mapper.CompanyCompanyFsuserMapper;
import com.fs.company.mapper.CompanyMapper;
import com.fs.company.mapper.CompanyUserMapper;
import com.fs.company.service.ICompanyService;
import com.fs.config.cloud.CloudHostProper;
import com.fs.course.config.CourseConfig;
import com.fs.course.constant.CourseConstant;
import com.fs.course.domain.*;
import com.fs.course.dto.CoursePeriodSyncResultDTO;
import com.fs.course.dto.CoursePackageDTO;
import com.fs.course.mapper.*;
import com.fs.course.param.*;
import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
import com.fs.course.param.newfs.FsUserCourseVideoUParam;
import com.fs.course.param.newfs.UserCourseVideoPageParam;
import com.fs.course.service.IFsCourseLinkService;
import com.fs.course.service.IFsUserCompanyBindService;
import com.fs.course.service.IFsUserCompanyUserService;
import com.fs.course.service.ICourseProjectSalesBindService;
import com.fs.course.service.IFsUserCourseVideoService;
import com.fs.course.utils.FsCourseWatchLogProjectFiller;
import com.fs.course.utils.H5WxUserWatchRedisUtil;
import com.fs.course.param.newfs.*;
import com.fs.course.service.*;
import com.fs.course.vo.*;
import com.fs.course.vo.newfs.*;
import com.fs.feishu.service.FeiShuService;
import com.fs.feishu.service.IFeishuAccountService;
import com.fs.enums.ExceptionCodeEnum;
import com.fs.his.config.AppConfig;
import com.fs.his.domain.FsUser;
import com.fs.his.domain.FsUserIntegralLogs;
import com.fs.his.domain.FsUserWx;
import com.fs.his.mapper.FsUserIntegralLogsMapper;
import com.fs.his.mapper.FsUserMapper;
import com.fs.his.param.WxSendRedPacketParam;
import com.fs.his.service.IFsStorePaymentService;
import com.fs.his.service.IFsUserIntegralLogsService;
import com.fs.his.service.IFsUserService;
import com.fs.his.service.IFsUserWxService;
import com.fs.his.utils.ConfigUtil;
import com.fs.his.vo.OptionsVO;
import com.fs.im.service.OpenIMService;
import com.fs.qw.domain.*;
import com.fs.qw.domain.QwCompany;
import com.fs.qw.domain.QwExternalContact;
import com.fs.qw.domain.QwUser;
import com.fs.qw.mapper.QwExternalContactMapper;
import com.fs.qw.mapper.QwGroupChatMapper;
import com.fs.qw.mapper.QwGroupChatUserMapper;
import com.fs.qw.mapper.QwUserMapper;
import com.fs.qw.param.FsUserCourseRedPageParam;
import com.fs.qw.service.IQwCompanyService;
import com.fs.qw.service.IQwExternalContactService;
import com.fs.qwApi.Result.QwAddContactWayResult;
import com.fs.qwApi.param.QwAddContactWayParam;
import com.fs.qwApi.service.QwApiService;
import com.fs.sop.domain.SopUserLogsInfo;
import com.fs.sop.mapper.SopUserLogsInfoMapper;
import com.fs.sop.service.ISopUserLogsInfoService;
import com.fs.system.mapper.SysDictDataMapper;
import com.fs.system.service.ISysConfigService;
import com.fs.voice.utils.StringUtil;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.volcengine.service.vod.IVodService;
import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
import com.volcengine.service.vod.model.request.*;
import com.volcengine.service.vod.model.response.*;
import com.volcengine.helper.VodUploadProgressListener;
import com.volcengine.model.beans.Functions;
import com.volcengine.service.vod.IVodService;
import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
import com.volcengine.service.vod.model.request.*;
import com.volcengine.service.vod.model.response.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

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

/**
 * 课堂视频Service业务层处理
 *
 * @author fs
 * @date 2024-05-17
 */
@Slf4j
@Service
public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoMapper, FsUserCourseVideo> implements IFsUserCourseVideoService {
    @Value("${cloud_host.company_name}")
    private String signProjectName;
    @Value("${isNewWxMerchant}")
    private Boolean isNewWxMerchant;

    @Autowired
    private OpenIMService openIMService;
    @Autowired
    private CompanyCompanyFsuserMapper companyCompanyFsuserMapper;
    private static final Logger logger = LoggerFactory.getLogger(FsUserCourseVideoServiceImpl.class);

    private static final String miniappRealLink = "/pages_course/video.html?course=";
    private static final String feishuMiniappLink = "/pages_course/video?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 Set<String> EXCLUDE_PROJECTS = new HashSet<>(Arrays.asList(
            "福本源", "宽益堂", "叮当国医", "易行健","河山医院"
    ));
    @Autowired
    ICompanyService companyService;
    @Autowired
    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
    @Autowired
    private QwGroupChatMapper qwGroupChatMapper;
    @Autowired
    private QwGroupChatUserMapper qwGroupChatUserMapper;
    @Autowired
    private IFsUserService fsUserService;
    @Autowired
    private FsUserCourseCompanyUserTimeMapper companyUserTimeMapper;
    @Autowired
    private FsUserCourseStudyLogMapper courseStudyLogMapper;

    @Autowired
    private FsUserCourseOrderMapper fsUserCourseOrderMapper;

    @Autowired
    private FsCourseQuestionBankMapper courseQuestionBankMapper;
    @Autowired
    private FsCourseWatchLogMapper courseWatchLogMapper;
    @Autowired
    private FsCourseWatchLogProjectFiller fsCourseWatchLogProjectFiller;
    @Autowired
    private FeiShuCourseWatchLogMapper feishuCourseWatchLogMapper;
    @Autowired
    private IFsCourseWatchLogService courseWatchLogService;
    @Autowired
    private ISopUserLogsInfoService iSopUserLogsInfoService;
    @Autowired
    private FsCourseLinkMapper fsCourseLinkMapper;


    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private QwExternalContactMapper qwExternalContactMapper;
    @Autowired
    private ISysConfigService configService;
    @Autowired
    private ICourseRandomRedPacketService courseRandomRedPacketService;
    @Autowired
    private QwApiService qwApiService;
    @Autowired
    private QwUserMapper qwUserMapper;
    @Autowired
    private FsUserCourseVideoRedPackageMapper fsUserCourseVideoRedPackageMapper;
    @Autowired
    private FsCourseTrafficLogMapper fsCourseTrafficLogMapper;
    @Autowired
    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
    @Autowired
    private FsUserMapper fsUserMapper;
    @Autowired
    private IFsStorePaymentService paymentService;
    @Autowired
    private FsCourseRedPacketLogMapper redPacketLogMapper;

    @Autowired
    private FsCourseLinkMapper courseLinkMapper;
    @Autowired
    RedisCache redisCache;
    @Autowired
    private H5WxUserWatchRedisUtil h5WxUserWatchRedisUtil;
    @Autowired
    private ISysConfigService sysConfigService;

    @Autowired
    private CompanyUserMapper companyUserMapper;

    @Autowired
    private FsVideoResourceMapper fsVideoResourceMapper;

    @Autowired
    private FsUserCourseMapper fsUserCourseMapper;

    @Autowired
    private FsUserCoursePeriodMapper fsUserCoursePeriodMapper;

    @Autowired
    private FsUserCoursePeriodDaysMapper fsUserCoursePeriodDaysMapper;


    @Autowired
    private IQwExternalContactService qwExternalContactService;
    @Autowired
    private IQwCompanyService iQwCompanyService;
    @Autowired
    private CompanyMapper companyMapper;
    @Autowired
    private IFsUserWxService fsUserWxService;
    @Autowired
    private FsCourseProductMapper courseProductMapper;
    @Autowired
    private IFsUserCompanyUserService userCompanyUserService;
    @Autowired
    private SysDictDataMapper dictDataMapper;

    @Autowired
    private FsCourseAnswerLogsMapper courseAnswerLogsMapper;

    @Autowired
    private CloudHostProper cloudHostProper;

    @Autowired
    ConfigUtil configUtil;

    @Autowired
    private IFsUserCompanyBindService fsUserCompanyBindService;

    @Autowired
    private FsUserCompanyQwMapper fsUserCompanyQwMapper;

    @Autowired
    private CourseRepeatByProjectMqProducer courseRepeatByProjectMqProducer;

    @Autowired
    private ICourseProjectSalesBindService courseProjectSalesBindService;

    @Autowired
    private SysDictDataMapper sysDictDataMapper;

    @Autowired
    private BalanceRollbackErrorMapper balanceRollbackErrorMapper;

    @Autowired
    private IFsUserCoursePeriodService fsUserCoursePeriodService;

    @Autowired
    private IFsUserCoursePeriodDaysService fsUserCoursePeriodDaysService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private FeiShuService feiShuService;

    @Autowired
    private IFeishuAccountService feishuAccountService;


    @Autowired
    private IFsCourseLinkService linkService;
    @Autowired
    private IFsUserIntegralLogsService iFsUserIntegralLogsService;



    /**
     * 查询课堂视频
     *
     * @param videoId 课堂视频主键
     * @return 课堂视频
     */
    @Override
    public FsUserCourseVideo selectFsUserCourseVideoByVideoId(Long videoId) {
        return fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
    }

    /**
     * 查询课堂视频列表
     *
     * @param fsUserCourseVideo 课堂视频
     * @return 课堂视频
     */
    @Override
    public List<FsUserCourseVideo> selectFsUserCourseVideoList(FsUserCourseVideo fsUserCourseVideo) {
        return fsUserCourseVideoMapper.selectFsUserCourseVideoList(fsUserCourseVideo);
    }

    /**
     * 新增课堂视频
     *
     * @param fsUserCourseVideo 课堂视频
     * @return 结果
     */
    @Override
    public int insertFsUserCourseVideo(FsUserCourseVideo fsUserCourseVideo) {
        fsUserCourseVideo.setCreateTime(DateUtils.getNowDate());
        return fsUserCourseVideoMapper.insertFsUserCourseVideo(fsUserCourseVideo);
    }

    /**
     * 修改课堂视频
     *
     * @param fsUserCourseVideo 课堂视频
     * @return 结果
     */
    @Override
    public int updateFsUserCourseVideo(FsUserCourseVideo fsUserCourseVideo) {
        fsUserCourseVideo.setUpdateTime(DateUtils.getNowDate());
        //无关联商品的课程 清除脏数据
        if (fsUserCourseVideo.getIsProduct() != null && fsUserCourseVideo.getIsProduct() == 0 && fsUserCourseVideo.getProductId() != null) {
            fsUserCourseVideo.setProductId(null);
            fsUserCourseVideo.setListingStartTime(null);
            fsUserCourseVideo.setListingEndTime(null);
        }

        // 如果修改了排序号，需要处理排序冲突
        if (fsUserCourseVideo.getCourseSort() != null) {
            // 获取原视频信息
            FsUserCourseVideo oldVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(fsUserCourseVideo.getVideoId());
            if (oldVideo != null && oldVideo.getCourseSort() != null) {
                Long newSort = fsUserCourseVideo.getCourseSort();
                Long oldSort = oldVideo.getCourseSort();

                // 如果排序号发生了变化
                if (!newSort.equals(oldSort)) {
                    // 检查新排序号是否已存在（排除当前视频）
                    Long count = fsUserCourseVideoMapper.selectFsUserCourseVideoByCourseSort(oldVideo.getCourseId(), newSort);
                    if (count > 0) {
                        // 新排序号已存在，需要将该排序号及之后的所有视频排序号+1
                        fsUserCourseVideoMapper.adjustCourseSortForUpdate(oldVideo.getCourseId(), newSort, fsUserCourseVideo.getVideoId());
                    }
                }
            }
        }

        syncVideoDurationCache(fsUserCourseVideo.getVideoId(), fsUserCourseVideo.getDuration());
        return fsUserCourseVideoMapper.updateFsUserCourseVideo(fsUserCourseVideo);
    }

    @Override
    public void syncVideoDurationCache(Long videoId, Long duration) {
        if (videoId == null) {
            return;
        }
        String videoRedisKey1 = "h5user:video:duration:" + videoId;
        String videoRedisKey2 = "h5wxuser:video:duration:" + videoId;
        if (duration == null) {
            redisCache.deleteObject(videoRedisKey1);
            redisCache.deleteObject(videoRedisKey2);
            return;
        }
        redisCache.setCacheObject(videoRedisKey1, duration);
        redisCache.setCacheObject(videoRedisKey2, duration);
    }

    @Override
    public int updateFsUserCourseRedPage(FsUserCourseRedPageParam userCourseRedPageParam) {

        return fsUserCourseVideoMapper.updateFsUserCourseRedPage(userCourseRedPageParam);
    }

    @Override
    public void sortCourseVideo(List<FsUserCourseVideo> list) {
        if (list.isEmpty()) {
            return;
        }
        // 直接构建更新参数
        List<Map<String, Object>> updateParams = list.stream()
                .map(item -> {
                    Map<String, Object> param = new HashMap<>();
                    param.put("videoId", item.getVideoId());
                    param.put("courseSort", item.getCourseSort());
                    return param;
                })
                .collect(Collectors.toList());

        // 批量更新
        fsUserCourseVideoMapper.batchUpdateByVideoId(updateParams);
    }

    /**
     * 批量删除课堂视频
     *
     * @param videoIds 需要删除的课堂视频主键
     * @return 结果
     */
    @Override
    public int deleteFsUserCourseVideoByVideoIds(String[] videoIds) {
        // 先获取要删除的视频信息，用于后续调整排序
        if (videoIds != null && videoIds.length > 0) {
            for (String videoIdStr : videoIds) {
                Long videoId = Long.valueOf(videoIdStr);
                FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
                if (video != null && video.getCourseId() != null && video.getCourseSort() != null) {
                    // 删除视频后，将该排序号之后的所有视频排序号-1，保持排序连续
                    fsUserCourseVideoMapper.adjustCourseSortAfterDelete(video.getCourseId(), video.getCourseSort());
                }
            }
        }
        return fsUserCourseVideoMapper.deleteFsUserCourseVideoByVideoIds(videoIds);
    }

    /**
     * 删除课堂视频信息
     *
     * @param videoId 课堂视频主键
     * @return 结果
     */
    @Override
    public int deleteFsUserCourseVideoByVideoId(String videoId) {
        return fsUserCourseVideoMapper.deleteFsUserCourseVideoByVideoId(videoId);
    }

    @Override
    public List<FsUserCourseVideo> selectFsUserCourseVideoListByCourseId(FsUserCourseVideo fsUserCourseVideo) {
        return fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseId(fsUserCourseVideo);
    }

    @Override
    public List<FsUserCourseVideoListUVO> selectFsUserCourseVideoListUVOByCourseId(FsUserCourseVideoListUParam param) {
        List<FsUserCourseVideoListUVO> list;
        if (param == null || param.getUserId() == null) {
            list = fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAll(param);

            // 判断是否对前两条记录设置 isVip
            if (!list.isEmpty()) {
                list.stream()
                        .filter(video -> video.getCourseSort() == 1 || video.getCourseSort() == 2)
                        .forEach(video -> video.setIsVip(0));
            }
        } else {
            list = fsUserCourseVideoMapper.selectFsUserCourseVideoListUVOByCourseId(param);
            FsUserCourseOrder order = fsUserCourseOrderMapper.selectFsUserCourseOrderByCourse(param.getUserId(), param.getCourseId());
            if (order != null && order.getCreateType() == 1) {
                list.forEach(item -> item.setIsBuy(1));
            }
            for (FsUserCourseVideoListUVO videoListUVO : list) {
                FsUserCourseStudyLog studyLog = courseStudyLogMapper.selectFsUserCourseStudyLogByUser(param.getUserId(), videoListUVO.getVideoId());
                if (studyLog != null) {
                    videoListUVO.setStudyTime(studyLog.getCurrentTimes());
                    videoListUVO.setTotalTimes(studyLog.getTotalTimes());
                }
            }
            if (!list.isEmpty()) {
                list.stream()
                        .filter(video -> video.getCourseSort() == 1 || video.getCourseSort() == 2)
                        .forEach(video -> {
                            video.setIsVip(0);
                            video.setIsBuy(1);
                        });
            }

        }
        for (FsUserCourseVideoListUVO listUVO : list) {
            if (StringUtils.isNotEmpty(listUVO.getPackageJson())) {
                List<CoursePackageDTO> packageDTOS = JSON.parseArray(listUVO.getPackageJson(), CoursePackageDTO.class);
                for (CoursePackageDTO packageDTO : packageDTOS) {
                    // 将时间按 ":" 分割
                    String[] parts = packageDTO.getDuration().split(":");
                    // 提取小时和分钟
                    int hours = Integer.parseInt(parts[0]);
                    int minutes = Integer.parseInt(parts[1]);
                    // 转换为秒
                    int seconds = hours * 3600 + minutes * 60;
                    packageDTO.setDuration(String.valueOf(seconds));
                }
                listUVO.setPackageJson(JSON.toJSONString(packageDTOS));
            }

        }
        return list;

    }

    @Override
    public Long selectCourseVideoSort(Long courseId) {
        if (fsUserCourseVideoMapper.selectCourseVideoSort(courseId) != null) {
            Long lastSort = fsUserCourseVideoMapper.selectCourseVideoSort(courseId) + 1;
            return lastSort;
        }
        return 1L;
    }

    @Override
    public R updateWatchDuration(FsUserCourseVideoFinishUParam param) {

        if (param.getIsRoom() != null && param.getIsRoom() == 1 && param.getQwExternalId() == null) {
            return R.ok();
        }

        Long videoDuration = 0L;
        try {
            videoDuration = getVideoDuration(param.getVideoId());
        } catch (Exception e) {
            log.error("视频时长识别错误:{}", param);
            return R.error();
        }
        // 判断param.getDuration()是否大于videoDuration（允许10的误差区间）
        if (param.getDuration() > videoDuration + 10) {
            return R.error();
        }

        // 从Redis中获取观看时长
        String redisKey = "h5user:watch:duration:" + param.getQwUserId() + ":" + param.getQwExternalId() + ":" + param.getVideoId();
//        log.info("看课redis-key：{}", redisKey);
        try {
            String durationStr = redisCache.getCacheObject(redisKey);
            Long duration = durationStr != null ? Long.parseLong(durationStr) : 0L;

            // 更新Redis中的观看时长
            if (param.getDuration() != null && param.getDuration() > duration) {
                //24小时过期
                redisCache.setCacheObject(redisKey, param.getDuration().toString(), 2, TimeUnit.HOURS);
            }
            updateHeartbeat(param);
            if (param.getDuration() != null && param.getQwUserId() != null
                    && param.getQwExternalId() != null && param.getVideoId() != null) {
                try {
                    Long qwUserId = Long.parseLong(param.getQwUserId());
                    courseWatchLogService.syncQwUserWatchProgressOnFinish(
                            qwUserId, param.getQwExternalId(), param.getVideoId(), param.getDuration());
                } catch (NumberFormatException e) {
                    logger.warn("企微看课完课同步跳过，qwUserId: {}", param.getQwUserId());
                }
            }
            return R.ok();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("更新时长失败：{}", redisKey, e.getMessage());
            return R.error();
        }

    }

    //更新心跳时间
    public void updateHeartbeat(FsUserCourseVideoFinishUParam param) {
        String redisKey = "h5user:watch:heartbeat:" + param.getQwUserId() + ":" + param.getQwExternalId() + ":" + param.getVideoId();
        redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
        // 设置 Redis 记录的过期时间（例如 5 分钟）
        redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
    }
    /*@Override
    public R isAddKf(FsUserCourseVideoAddKfUParam param) {

        logger.info("zyp \n【判断添加客服】：{}",param);
        //查询用户
        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());

        //用户不存在唤起重新授权
        if (fsUser==null){
            return R.error(401,"会员不存在");
        }
        if (fsUser.getStatus()==0){
            return R.error("会员被停用，无权限，请联系客服！");
        }

        if (param.getIsRoom()!=null&& param.getIsRoom()== 1) {
            // 当 isRoom 为 1 时走 handleRoom
            return handleRoom(param,fsUser);
        }


        if (param.getLinkType()!=null&&param.getLinkType()==1){
            if (param.getLinkId()!=null){
                // 从数据库中查找短链对应的真实链接
                FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLinkId(param.getLinkId());
                if (courseLink != null) {
                    // 获取当前时间
                    Date currentTime = new Date();
                    // 获取链接的更新时间（假设这个字段代表过期时间）
                    Date updateTime = courseLink.getUpdateTime();

                    // 判断是否过期
                    if (currentTime.after(updateTime)) {
                        // 链接已过期
                        return R.error("链接已过期");
                    }else {
                        return R.ok();
                    }
                }
            }
            return R.error("链接过期");
        }
        FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());
//        if(StringUtils.isNotEmpty(courseLink.getUNo())){
//            if(!courseLink.getUNo().equals(param.getUNo())){
//                log.error("看课链接uNO不一致,linkUNo:{}， param-uNo：{}", courseLink.getUNo(), param.getUNo());
//            }else{
//                log.info("看课链接uNO一致,uno:{}", courseLink.getUNo());
//            }
//        }

        //未注册提示
        String noRegisterMsg = "由于您还未完成注册，请联系伴学助手完成注册即可观看!";
        //非独属链接提示
        String noMemberMsg = "此链接已被绑定，请联系伴学助手领取您的专属链接，专属链接请勿分享哦!";

        // 群聊逻辑
        if(courseLink != null && StringUtils.isNotEmpty(courseLink.getChatId())){
            QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(courseLink.getChatId());
            if(qwGroupChat == null){
                return R.error("群参数异常！");
            }
            SopUserLogsInfo sopUserLogsInfo =  new SopUserLogsInfo();
            sopUserLogsInfo.setChatId(courseLink.getChatId());
            List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);
            if(qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()){
                return R.error("群参数异常！");
            }
            QwExternalContact qwExternalContact =
                    qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
                            .eq("user_id", qwGroupChat.getOwner())
                            .eq("fs_user_id", param.getUserId())
                            .eq("corp_id", param.getCorpId())
                            .eq("status",0));
            if(qwExternalContact==null){
                return R.error(noRegisterMsg);
            }
            if(qwGroupChatUsers.stream().noneMatch(e -> e.getUserId().equals(qwExternalContact.getExternalUserId()))){
                log.error("客户不在群：{}，里面：{}", qwGroupChat.getChatId(), qwExternalContact.getExternalUserId());
                return R.error(noRegisterMsg);
            }
            Long qwExternalId = qwExternalContact.getId();
            log.info("外部联系人数据：{}", qwExternalContact);
            addCompanyCompanyFsUser(param);
            FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByExt(qwExternalId, param.getVideoId(),param.getQwUserId());
            if (log==null ){
                return R.error(noRegisterMsg);
            }
            boolean isDebug = false;
            //判断外部联系人有没有绑定userId
            if (qwExternalContact.getFsUserId()!=null && isDebug){
                //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
                if (!qwExternalContact.getFsUserId().equals(param.getUserId())) {
                    return R.error(noMemberMsg);
                }
                List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());
                //匹配客户公司id
                if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
                    return R.error(noRegisterMsg);
                }

                //看课记录中userId为0绑定userId
                if (log.getUserId()==null||log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())){
                    log.setUserId(param.getUserId());
                }

                log.setUpdateTime(new Date());
                fsUserCompanyBindService.bindFsUser(fsUser.getUserId(), qwExternalId, log.getLogId());
                courseWatchLogMapper.updateFsCourseWatchLog(log);

            }else {
                //没绑定fsUser直接绑定fsUser
                QwExternalContact contact = new QwExternalContact();
                contact.setId(qwExternalId);
                contact.setFsUserId(param.getUserId());
                qwExternalContactMapper.updateQwExternalContact(contact);
                FsUser user = new FsUser();
                user.setUserId(fsUser.getUserId());
                user.setIsAddQw(1);
                fsUserMapper.updateFsUser(user);
                //绑定上之后 更新观看记录
                //看课记录中userId为0绑定userId
                log.setUserId(param.getUserId());
                log.setUpdateTime(new Date());
                courseWatchLogMapper.updateFsCourseWatchLog(log);
            }
            fsUserCompanyBindService.bindFsUser(fsUser.getUserId(), qwExternalId, log.getLogId());
            return R.error(567,"群聊通用链接").put("qwExternalId", qwExternalContact.getId());
        }
        else{
            Long qwExternalId = param.getQwExternalId();

            FsCourseWatchLog log=new FsCourseWatchLog();

            //如果是官方的则查真正的外部联系人id
            if (param.getLinkType()!=null&&param.getLinkType()==5) {

                log = courseWatchLogMapper.selectFsCourseWatchLogByCourseSopIdAndVideoId(param.getUserId(), param.getVideoId(), param.getQwUserId());
                if (log == null) {
                    return R.error(noRegisterMsg);
                }else {
                    qwExternalId=log.getQwExternalContactId();
                }

            }else {
                log = courseWatchLogMapper.getWatchCourseVideoByExt(qwExternalId, param.getVideoId(),param.getQwUserId());
                if (log==null ){
                    return R.error(noRegisterMsg);
                }
            }


            //查询是否有添加客服
            QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(qwExternalId);

            //用小程序id查询外部联系人
            List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());


            //如果查不出来客户信息，加好友
            if(externalContact==null){
                return R.error(noRegisterMsg);
            }

            //2025.04.10 绑定公司码
            addCompanyCompanyFsUser(param);

            //判断外部联系人有没有绑定userId
            if (externalContact.getFsUserId()!=null){
                //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
                if (!externalContact.getFsUserId().equals(param.getUserId())) {
                    return R.error(noMemberMsg);
                }
                //匹配客户公司id
                if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
                    return R.error(noMemberMsg);
                }

                //看课记录中userId为0绑定userId
                if (log.getUserId()==null||log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())){
                    log.setUserId(param.getUserId());
                }

                log.setUpdateTime(new Date());
                courseWatchLogMapper.updateFsCourseWatchLog(log);

                iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId,param.getUserId());

                fsUserCompanyBindService.bindFsUser(fsUser.getUserId(), qwExternalId, log.getLogId());
                if (param.getLinkType()!=null&&param.getLinkType()==5){
                    FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                    return R.error(566,"官方群发通用链接").put("courseLink",fsCourseLink);
                }
                return R.ok();

            }else {
                //没绑定fsUser直接绑定fsUser
                QwExternalContact contact = new QwExternalContact();
                contact.setId(qwExternalId);
                contact.setFsUserId(param.getUserId());
//            adHtmlClickLogService.upload(contact.getState(), AdUploadType.REGISTERED, e -> contact.setUploadRegisterStatus(1));
                qwExternalContactMapper.updateQwExternalContact(contact);

                iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId,param.getUserId());

                FsUser user = new FsUser();
                user.setUserId(fsUser.getUserId());
                user.setIsAddQw(1);
                fsUserMapper.updateFsUser(user);

                //绑定上之后 更新观看记录
                //看课记录中userId为0绑定userId
                if (log.getUserId()==null||log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())){
                    log.setUserId(param.getUserId());
                }

                log.setUpdateTime(new Date());
                courseWatchLogMapper.updateFsCourseWatchLog(log);
                fsUserCompanyBindService.bindFsUser(fsUser.getUserId(), qwExternalId, log.getLogId());
                if (param.getLinkType()!=null&&param.getLinkType()==5){
                    FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                    return R.error(566,"官方群发通用链接").put("courseLink",fsCourseLink);
                }
                return R.ok();
            }
        }
    }*/
    @Override
    public R isAddKf(FsUserCourseVideoAddKfUParam param) {
        fillLinkMetaFromDb(param);
        logger.info("【判断添加客服】：{}", param);
        //查询用户
        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
        String dateString = "2026-02-11 20:00:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date targetDate = null;
        try {
            targetDate = sdf.parse(dateString);
        }catch (Exception e){
            e.printStackTrace();
        }

        //用户不存在唤起重新授权
        if (fsUser == null  ||  fsUser.getCreateTime().before(targetDate) ) {
            return R.error(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
        }
//        if (param.getIsOpenCourse()!=null&&param.getIsOpenCourse()==1){
//            FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByUserId(param.getUserId(), param.getVideoId());
//            if (log==null){
//                FsCourseWatchLog userLog = new FsCourseWatchLog();
//                BeanUtils.copyProperties(param,log);
//                userLog.setSendType(0);
//                userLog.setUserId(param.getUserId());
//                userLog.setVideoId(param.getVideoId());
//                userLog.setDuration(0L);
//                userLog.setCreateTime(new Date());
//                userLog.setLogType(3);
//                courseWatchLogMapper.insertFsCourseWatchLog(log);
//            }
//            return  R.ok();
//        }
        //未注册提示
        String noRegisterMsg = "由于您还未完成注册，请联系伴学助手完成注册即可观看!";
        //非独属链接提示
        String noMemberMsg = "此链接已被绑定，请联系伴学助手领取您的专属链接，专属链接请勿分享哦!";

        R bindCheck = courseProjectSalesBindService.checkAndBind(param);
        if (bindCheck != null) {
            return bindCheck;
        }

        try {
            courseRepeatByProjectMqProducer.submitCheck(param);
        } catch (Exception e) {
            log.error("看课重粉 MQ 投递失败", e);
        }

        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);


//        //服务号授权的，缺mpOpenId的重新登录 linkType = 4为app看课 不需要mp
//        if (param.getLinkType() != 4 && config.getMiniAppAuthType()==2 && StringUtil.strIsNullOrEmpty(fsUser.getMpOpenId())){
//            return R.error(401,"授权后可继续！");
//        }

        boolean oneCompanyCourse = config.isOneCompanyCourse();
        if (oneCompanyCourse && fsUser.getQwExtId() != null) {
            QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(fsUser.getQwExtId());
            if (qwExternalContact.getCompanyUserId() != null && !qwExternalContact.getCompanyUserId().equals(param.getCompanyUserId())) {
                return R.error(ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getCode(), ExceptionCodeEnum.USER_ALREADY_OTHER_SALES_MEMBER.getFormattedDescription(fsUser.getUserId()));
            }
        }

        // 重粉问题，关闭单销售重粉检测 打开公司销售重粉检测 允许不同公司销售重粉
        if (!oneCompanyCourse && config.isMoreCompanyCourse()) {
            QwExternalContact query = new QwExternalContact();
            query.setFsUserId(param.getUserId());
            query.setCompanyId(param.getCompanyId());
            List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactList(query);
            if (qwExternalContacts != null && !qwExternalContacts.isEmpty() && !Objects.equals(qwExternalContacts.get(0).getCompanyUserId(), param.getCompanyUserId())) {
                String msgInfo = "该用户(" + fsUser.getUserId() + ")已在公司" + param.getCompanyId() + "成为其他销售会员";
                return R.error(ExceptionCodeEnum.USER_COMPANY_OTHER_SALES_MEMBER.getCode(), ExceptionCodeEnum.USER_COMPANY_OTHER_SALES_MEMBER.getFormattedDescription(fsUser.getUserId(),param.getCompanyId()));
            }
        }

//        // ========== 【新增】企微自动发课（send_type=2）重粉判断逻辑 - 在所有场景处理之前判断 ==========
//        // 获取当前视频所属的课程和项目
//        if (param.getVideoId() != null && param.getCompanyId() != null) {
//            FsUserCourseVideo currentVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
//            if (currentVideo != null && currentVideo.getCourseId() != null) {
//                FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(currentVideo.getCourseId());
//                Long projectId = course != null && course.getProject() != null ? course.getProject() : 0L;
//
//                // 只对有项目ID的课程进行重粉判断
//                if (projectId != 0L) {
//                        // 查询该用户在这个项目下是否已经绑定了其他销售
//                        FsUserCompanyQw existBind = fsUserCompanyQwMapper.selectByUserAndProject(
//                            param.getUserId(), projectId
//                        );
//
//                        // 如果存在绑定记录，且绑定的销售不是当前销售，说明是重粉
//                        if (existBind != null && param.getQwUserId() != null
//                            && !existBind.getQwUserId().equals(Long.parseLong(param.getQwUserId()))) {
//                            // 查询项目名称
//                            String projectName = "未知项目";
//                            List<SysDictData> courseProjects = sysDictDataMapper.selectDictDataByType("sys_course_project");
//                            for (SysDictData dictData : courseProjects) {
//                                if (dictData.getDictValue().equals(String.valueOf(projectId))) {
//                                    projectName = dictData.getDictLabel();
//                                    break;
//                                }
//                            }
//
//                            // 查询销售名称
//                            String salesName = "其他销售";
//                            QwUser existQwUser = qwUserMapper.selectById(existBind.getQwUserId());
//                            if (existQwUser != null && existQwUser.getCompanyUserId() != null) {
//                                CompanyUser companyUser = companyUserMapper.selectCompanyUserById(existQwUser.getCompanyUserId());
//                                if (companyUser != null && StringUtils.isNotEmpty(companyUser.getUserName())) {
//                                    salesName = companyUser.getUserName();
//                                }
//                            }
//
//                            // 返回重粉提示
//                            String errorMsg = String.format("您已在销售【%s】处观看过【%s】项目的课程", salesName, projectName);
//                            logger.info("【重粉检测-提前拦截】用户ID: {}, 项目ID: {}, 原销售: {}, 当前销售: {}",
//                                param.getUserId(), projectId, existBind.getQwUserId(), param.getQwUserId());
//                            return R.error(errorMsg);
//                        }
//                }
//            }
//        }
        // ========== 【重粉判断结束】 ==========

        Integer isRoom = param.getIsRoom();

        // 处理逻辑
        if(StringUtils.isNotEmpty(param.getChatId())){
            return handleQwRoom(param, fsUser,noRegisterMsg);
        }
        if (isRoom == null || isRoom == 0) {
            // 当 isRoom 为 null 或 0 时走 handleExt
            return handleExt(param, fsUser, noMemberMsg, oneCompanyCourse);
        } else if (isRoom == 1) {
            // 当 isRoom 为 1 时走 handleRoom
            return handleRoom(param,fsUser,noRegisterMsg);
        } else {
            // 非法参数
            logger.warn("非法参数 isRoom: {}", isRoom);
            return R.error(ExceptionCodeEnum.PARAM_ERROR.getCode(), ExceptionCodeEnum.PARAM_ERROR.getDescription());
        }

    }
    private R handleQwRoom(FsUserCourseVideoAddKfUParam param,FsUser user,String noRegisterMsg) {
        FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());
//        String msg = "<div style=\"color: red;margin-bottom: 15px;font-weight: bold;\">本课程为群会员独享<br>请长按二维码</div>\n" +
//                "\t\t\t\t\t<div style=\"color: #999;font-size: 14px;font-weight: bold;\">添加伴学助手免费领取会员权限</div>";
        QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(param.getChatId());
        if (qwGroupChat == null) {
            return R.error(ExceptionCodeEnum.GROUP_PARAM_ERROR.getCode(), ExceptionCodeEnum.GROUP_PARAM_ERROR.getDescription());
        }
        SopUserLogsInfo sopUserLogsInfo =  new SopUserLogsInfo();
        sopUserLogsInfo.setChatId(param.getChatId());
        List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);
        if (qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()) {
            return R.error(ExceptionCodeEnum.GROUP_PARAM_ERROR.getCode(), ExceptionCodeEnum.GROUP_PARAM_ERROR.getDescription());
        }
        QwExternalContact qwExternalContact = selectValidQwExternalContact(new QueryWrapper<QwExternalContact>()
                .eq("user_id", qwGroupChat.getOwner())
                .eq("fs_user_id", param.getUserId())
                .eq("corp_id", param.getCorpId()));

        if (qwExternalContact == null) {
            // 通过昵称匹配
            qwExternalContact = selectValidQwExternalContact(new QueryWrapper<QwExternalContact>()
                    .eq("user_id", qwGroupChat.getOwner())
                    .eq("name", user.getNickName())
                    .eq("corp_id", param.getCorpId()));
        }
        if(qwExternalContact==null){
            return R.error(noRegisterMsg);
        }
        Integer contactStatus = qwExternalContact.getStatus();
        if (contactStatus == null || !Arrays.asList(0, 1, 2).contains(contactStatus)) {
            return R.error("当前学习通道访问限流，暂时无法观看！");
        }
        QwExternalContact finalQwExternalContact = qwExternalContact;
        if (qwGroupChatUsers.stream().noneMatch(e -> e.getUserId().equals(finalQwExternalContact.getExternalUserId()))) {
            log.error("客户不在群：{}，里面：{}", qwGroupChat.getChatId(), qwExternalContact.getExternalUserId());
            return R.error(noRegisterMsg);
        }
        Long qwExternalId = qwExternalContact.getId();
//        addCompanyCompanyFsUser(param);
        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByExt(qwExternalId, param.getVideoId(),param.getQwUserId());
        if (log==null ){
            return R.error(noRegisterMsg);
        }
        //判断外部联系人有没有绑定userId
        if (qwExternalContact.getFsUserId() != null) {
            //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
            if (!qwExternalContact.getFsUserId().equals(param.getUserId())) {
                return R.error(noRegisterMsg);
            }
            List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());
            //匹配客户公司id
            if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
                return R.error(noRegisterMsg);
            }

            //看课记录中userId为0绑定userId
            if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                log.setUserId(param.getUserId());
            }
            log.setAppId(param.getAppId());
            log.setUpdateTime(new Date());
            //重粉逻辑
            //
            courseWatchLogMapper.updateFsCourseWatchLog(log);
            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());


        } else {
            //没绑定fsUser直接绑定fsUser
            QwExternalContact contact = new QwExternalContact();
            contact.setId(qwExternalId);
            contact.setFsUserId(param.getUserId());
            qwExternalContactMapper.updateQwExternalContact(contact);
            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

            FsUser fsUser = new FsUser();
            fsUser.setUserId(user.getUserId());
            fsUser.setIsAddQw(1);
            fsUserMapper.updateFsUser(fsUser);
            //绑定上之后 更新观看记录
            //看课记录中userId为0绑定userId
            log.setUserId(param.getUserId());
            log.setUpdateTime(new Date());
            log.setAppId(param.getAppId());
            courseWatchLogMapper.updateFsCourseWatchLog(log);
        }
        //重粉逻辑
        fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());

//        // 记录用户-项目-销售绑定关系
//        recordUserProjectBind(param, qwExternalId, "handleQwRoom");
        R disabledResult = checkMemberDisabledAfterBind(user);
        if (disabledResult != null) {
            return disabledResult;
        }
        return R.error(567, "群聊通用链接").put("qwExternalId", qwExternalContact.getId());
    }

    /**
     * 查询有效状态的企微外部联系人（0正常 1待接替 2接替中），排除删除/流失；多条时优先正常状态、最新记录
     */
    private QwExternalContact selectValidQwExternalContact(QueryWrapper<QwExternalContact> queryWrapper) {
        List<QwExternalContact> list = qwExternalContactMapper.selectList(
                queryWrapper.in("status", Arrays.asList(0, 1, 2))
                        .orderByAsc("status")
                        .orderByDesc("create_time"));
        return CollectionUtils.isEmpty(list) ? null : list.get(0);
    }

    /**
     * 解析当前销售对应的外部联系人，避免重粉场景 externalId 与 qwUser 错配。
     */
    private QwExternalContact resolveExternalContactForQwUser(Long externalId, QwUser qwUser, Long fsUserId) {
        if (qwUser == null || qwUser.getId() == null) {
            return externalId != null ? qwExternalContactMapper.selectQwExternalContactById(externalId) : null;
        }
        if (externalId != null) {
            QwExternalContact contact = qwExternalContactMapper.selectQwExternalContactById(externalId);
            if (contact != null && qwUser.getId().equals(contact.getQwUserId())
                    && isValidExternalContactStatus(contact.getStatus())) {
                return contact;
            }
            if (contact != null && !qwUser.getId().equals(contact.getQwUserId())) {
                log.warn("外部联系人与当前销售不匹配，尝试纠正: externalId={}, qwUserId={}, contactQwUserId={}",
                        externalId, qwUser.getId(), contact.getQwUserId());
            }
        }
        QueryWrapper<QwExternalContact> wrapper = new QueryWrapper<QwExternalContact>()
                .eq("qw_user_id", qwUser.getId());
        if (StringUtils.isNotEmpty(qwUser.getCorpId())) {
            wrapper.eq("corp_id", qwUser.getCorpId());
        }
        if (fsUserId != null && fsUserId > 0) {
            wrapper.eq("fs_user_id", fsUserId);
        } else if (externalId != null) {
            QwExternalContact wrongContact = qwExternalContactMapper.selectQwExternalContactById(externalId);
            if (wrongContact != null && wrongContact.getFsUserId() != null && wrongContact.getFsUserId() > 0) {
                wrapper.eq("fs_user_id", wrongContact.getFsUserId());
            }
        }
        return selectValidQwExternalContact(wrapper);
    }

    private boolean isValidExternalContactStatus(Integer status) {
        return status != null && Arrays.asList(0, 1, 2).contains(status);
    }

    private QwExternalContact resolveExternalContactForRoom(FsUser user, FsUserCourseVideoAddKfUParam param) {
        if (user == null || StringUtils.isEmpty(param.getQwUserId())) {
            return null;
        }
        try {
            Long qwUserId = Long.parseLong(param.getQwUserId());
            QueryWrapper<QwExternalContact> boundWrapper = new QueryWrapper<QwExternalContact>()
                    .eq("qw_user_id", qwUserId)
                    .eq("fs_user_id", user.getUserId());
            if (StringUtils.isNotEmpty(param.getCorpId())) {
                boundWrapper.eq("corp_id", param.getCorpId());
            }
            QwExternalContact matched = selectValidQwExternalContact(boundWrapper);
            if (matched != null) {
                return matched;
            }
            QueryWrapper<QwExternalContact> unboundWrapper = new QueryWrapper<QwExternalContact>()
                    .eq("qw_user_id", qwUserId);
            if (StringUtils.isNotEmpty(param.getCorpId())) {
                unboundWrapper.eq("corp_id", param.getCorpId());
            }
            if (StringUtils.isNotEmpty(user.getUnionId())) {
                unboundWrapper.eq("unionid", user.getUnionId());
            }
            return selectValidQwExternalContact(unboundWrapper);
        } catch (NumberFormatException e) {
            log.error("群聊通用链接匹配销售失败，qwUserId 非法: {}", param.getQwUserId());
            return null;
        }
    }

    private QwExternalContact resolveExternalContactByUnionId(FsUser user, FsUserCourseVideoAddKfUParam param) {
        if (user == null || StringUtils.isEmpty(user.getUnionId()) || StringUtils.isEmpty(param.getQwUserId())) {
            return null;
        }
        List<QwExternalContact> contactList = qwExternalContactMapper.selectQwExternalByUnionID(user.getUnionId());
        if (CollectionUtils.isEmpty(contactList)) {
            return null;
        }
        Long qwUserId;
        try {
            qwUserId = Long.parseLong(param.getQwUserId());
        } catch (NumberFormatException e) {
            log.error("群聊链接匹配销售失败，qwUserId 非法: {}", param.getQwUserId());
            return null;
        }
        QwExternalContact matched = null;
        for (QwExternalContact contact : contactList) {
            if (contact == null) {
                continue;
            }
            if (contact.getFsUserId() == null || contact.getFsUserId() == 0L) {
                contact.setFsUserId(user.getUserId());
                qwExternalContactMapper.updateQwExternalContact(contact);
            }
            if (qwUserId.equals(contact.getQwUserId()) && isValidExternalContactStatus(contact.getStatus())) {
                matched = contact;
            }
        }
        return matched;
    }

    private R processRoomExternalContact(FsUserCourseVideoAddKfUParam param, FsUser user, QwExternalContact matchedContact) {
        log.info("群聊通用链接匹配外部联系人: extId={}, qwUserId={}, videoId={}",
                matchedContact.getId(), param.getQwUserId(), param.getVideoId());
        param.setQwExternalId(matchedContact.getId());
        if (param.getUserId() == null) {
            param.setUserId(user.getUserId());
        }
        FsCourseWatchLog watchLog = courseWatchLogMapper.getWatchCourseVideoByExt(
                matchedContact.getId(), param.getVideoId(), param.getQwUserId());
        if (watchLog == null) {
            createWatchLog(param);
        } else if (watchLog.getUserId() == null || watchLog.getUserId().equals(0L)
                || !watchLog.getUserId().equals(user.getUserId())) {
            watchLog.setUserId(user.getUserId());
            watchLog.setUpdateTime(new Date());
            watchLog.setAppId(param.getAppId());
            courseWatchLogMapper.updateFsCourseWatchLog(watchLog);
        }
        return R.error(567, "群聊通用链接").put("qwExternalId", matchedContact.getId());
    }

    private R handleRoom(FsUserCourseVideoAddKfUParam param,FsUser user,String noRegisterMsg) {
        QwExternalContact matchedContact = resolveExternalContactForRoom(user, param);
        if (matchedContact == null) {
            boolean bool1 = "今正科技".equals(cloudHostProper.getCompanyName());
            boolean bool2 = "弘珍医药".equals(cloudHostProper.getCompanyName());
            if (bool1 || bool2) {
                matchedContact = resolveExternalContactByUnionId(user, param);
            }
        }
        if (matchedContact == null) {
            log.warn("群聊通用链接未匹配到当前销售的外部联系人: userId={}, qwUserId={}, corpId={}",
                    user.getUserId(), param.getQwUserId(), param.getCorpId());
            return R.error(noRegisterMsg);
        }
        return processRoomExternalContact(param, user, matchedContact);
    }

    private void createWatchLog(FsUserCourseVideoAddKfUParam param) {
        FsCourseWatchLog log = new FsCourseWatchLog();
        BeanUtils.copyProperties(param, log);
        log.setQwExternalContactId(param.getQwExternalId());
        log.setSendType(2);
        log.setUserId(param.getUserId());
        log.setVideoId(param.getVideoId());
        log.setDuration(0L);
        log.setQwUserId(Long.valueOf(param.getQwUserId()));
        log.setCreateTime(new Date());
        log.setLogType(3);
        log.setWatchType(2);
        log.setAppId(param.getAppId());
        // 企微看课补齐 project，避免按项目完播统计丢失
        fsCourseWatchLogProjectFiller.fillIfAbsent(log);
        logger.info("【群聊生成看课记录】:{}", param);
        courseWatchLogMapper.insertFsCourseWatchLog(log);
    }

    private R checkMemberDisabledAfterBind(FsUser fsUser) {
        if (fsUser != null && fsUser.getStatus() != null && fsUser.getStatus() == 0) {
            return R.error(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
        }
        return null;
    }

    /**
     * 从短链记录补全 corpId / qwExternalId / qwUserId 等参数，避免 URL 缺字段导致校验失败。
     */
    private void fillLinkMetaFromDb(FsUserCourseVideoAddKfUParam param) {
        if (param == null || StringUtils.isBlank(param.getLink())) {
            return;
        }
        if (StringUtils.isNotBlank(param.getCorpId()) && param.getQwExternalId() != null
                && StringUtils.isNotBlank(param.getQwUserId()) && param.getLinkType() != null) {
            return;
        }
        FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());
        if (courseLink == null) {
            return;
        }
        if (StringUtils.isBlank(param.getCorpId()) && StringUtils.isNotBlank(courseLink.getCorpId())) {
            param.setCorpId(courseLink.getCorpId());
        }
        if (param.getQwExternalId() == null && courseLink.getQwExternalId() != null) {
            param.setQwExternalId(courseLink.getQwExternalId());
        }
        if (StringUtils.isBlank(param.getQwUserId()) && courseLink.getQwUserId() != null) {
            param.setQwUserId(String.valueOf(courseLink.getQwUserId()));
        }
        if (param.getLinkType() == null && courseLink.getLinkType() != null) {
            param.setLinkType(courseLink.getLinkType());
        }
        if (param.getIsRoom() == null && courseLink.getIsRoom() != null) {
            param.setIsRoom(courseLink.getIsRoom());
        }
        if (StringUtils.isBlank(param.getChatId()) && StringUtils.isNotBlank(courseLink.getChatId())) {
            param.setChatId(courseLink.getChatId());
        }
    }

    private R handleExt(FsUserCourseVideoAddKfUParam param, FsUser fsUser, String msg, boolean oneCompanyCourse) {
        //暂时注释应急短链逻辑
//        if (param.getLinkType()!=null&&param.getLinkType()==1){
//            if (param.getLinkId()!=null){
//                // 从数据库中查找短链对应的真实链接
//                FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLinkId(param.getLinkId());
//                if (courseLink != null) {
//                    // 获取当前时间
//                    Date currentTime = new Date();
//                    // 获取链接的更新时间（假设这个字段代表过期时间）
//                    Date updateTime = courseLink.getUpdateTime();
//
//                    // 判断是否过期
//                    if (currentTime.after(updateTime)) {
//                        // 链接已过期
//                        return R.error("链接已过期");
//                    }else {
//                        return R.ok();
//                    }
//                }
//            }
//            return R.error("链接过期");
//        }

        Long qwExternalId = param.getQwExternalId();

        FsCourseWatchLog log = new FsCourseWatchLog();

        //如果是官方的则查真正的外部联系人id
        if (param.getLinkType() != null && param.getLinkType() == 5) {
            log = courseWatchLogMapper.selectFsCourseWatchLogByCourseSopIdAndVideoId(param.getUserId(), param.getVideoId(), param.getQwUserId());
            if (log == null) {
                return R.error(msg);
            }else {
                qwExternalId=log.getQwExternalContactId();
            }

        }else {
            log = courseWatchLogMapper.getWatchCourseVideoByExt(qwExternalId, param.getVideoId(),param.getQwUserId());
            if (log==null ){
                return R.error(msg);
            }
        }

//        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByExt(param.getQwExternalId(), param.getVideoId(),param.getQwUserId());
//        if (log==null ){
//            return addCustomerService(param.getQwUserId(),msg);
//        }

        //查询是否有添加客服
        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(qwExternalId);

        //用小程序id查询外部联系人
        List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByMiniUserId(param.getUserId());


        //如果查不出来客户信息，加好友
        if(externalContact==null){
            return R.error(msg);
        }

        //判断外部联系人有没有绑定userId
        if (externalContact.getFsUserId() != null) {
            //有客户有小程序id  但 登录的小程序id和根据外部联系人id查出来的小程序id不一致
            if (!externalContact.getFsUserId().equals(param.getUserId())) {
                return R.error(msg);
            }
            //匹配客户公司id
            if (qwExternalContacts.stream().noneMatch(contact -> contact.getCorpId().equals(param.getCorpId()))){
                return R.error(msg);
            }

            //看课记录中userId为0绑定userId
            if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                log.setUserId(param.getUserId());
            }
            //跟新所属小程序ID
            log.setAppId(param.getAppId());
            log.setUpdateTime(new Date());
            courseWatchLogMapper.updateFsCourseWatchLog(log);


            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

            //重粉逻辑
            fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());

            // 记录用户-项目-销售绑定关系
//            recordUserProjectBind(param, qwExternalId, "handleExt");

            R disabledResult = checkMemberDisabledAfterBind(fsUser);
            if (disabledResult != null) {
                return disabledResult;
            }
            if (param.getLinkType() != null && param.getLinkType() == 5) {
                FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                return R.error(566, "官方群发通用链接").put("courseLink", fsCourseLink);
            }

            return R.ok();

        } else {
            //没绑定fsUser直接绑定fsUser
            QwExternalContact contact = new QwExternalContact();
            contact.setId(param.getQwExternalId());
            contact.setFsUserId(param.getUserId());
            contact.setRegisterTime(new Date());
            qwExternalContactMapper.updateQwExternalContact(contact);

            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

            FsUser user = new FsUser();
            user.setUserId(param.getUserId());
            user.setIsAddQw(1);
            user.setQwExtId(param.getQwExternalId());
            fsUserMapper.updateFsUser(user);

            iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

            //绑定上之后 更新观看记录
            //看课记录中userId为0绑定userId
            if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                log.setUserId(param.getUserId());
            }
            log.setAppId(param.getAppId());
            log.setUpdateTime(new Date());
            courseWatchLogMapper.updateFsCourseWatchLog(log);

            //重粉逻辑
            fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());

            // 记录用户-项目-销售绑定关系
//            recordUserProjectBind(param, qwExternalId, "handleExt");

            R disabledResult = checkMemberDisabledAfterBind(fsUser);
            if (disabledResult != null) {
                return disabledResult;
            }
            if (param.getLinkType() != null && param.getLinkType() == 5) {
                FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                return R.error(566, "官方群发通用链接").put("courseLink", fsCourseLink);
            }

            return R.ok();
        }
    }

    private R addCustomerService(String qwUserById,String msg){
        if ("济南联志健康".equals(signProjectName)){
            return R.error(400,msg).put("qrcode", "");
        }
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        String contactWay = "";
        if (ObjectUtils.isNotEmpty(config.getShowQwCode()) && !config.getShowQwCode()) {
            return R.error(400, msg).put("qrcode", contactWay);
        }
        QwUser qwUser = qwUserMapper.selectQwUserById(Long.parseLong(qwUserById));
        if (qwUser == null) {
            return R.error("客服不存在");
        }

        // 如果是"北京存在文化"项目,直接返回固定的二维码
        if ("北京存在文化".equals(signProjectName)) {
            contactWay = "https://czwh.obs.cn-southwest-2.myhuaweicloud.com/fs/20251121/1763690039856.jpg";
            return R.error(400, msg).put("qrcode", contactWay);
        }

        // 其他项目使用原来的逻辑
        if (StringUtils.isNotEmpty(qwUser.getContactWay())){
            contactWay = qwUser.getContactWay();
        } else {
            String qwUserId = "[\"" + qwUser.getQwUserId() + "\"]";
            List<String> users = JSON.parseArray(qwUserId, String.class);
            QwAddContactWayParam qwAddContactWayParam = new QwAddContactWayParam();
            qwAddContactWayParam.setType(1);
            qwAddContactWayParam.setScene(2);
            qwAddContactWayParam.setUser(users);
            qwAddContactWayParam.setSkip_verify(false);
            QwAddContactWayResult qwAddContactWayResult = qwApiService.addContactWay(qwAddContactWayParam, qwUser.getCorpId());
            if (qwAddContactWayResult.getErrcode() == 0) {
                qwUser.setContactWay(qwAddContactWayResult.getQr_code());
                qwUser.setConfigId(qwAddContactWayResult.getConfig_id());
                qwUserMapper.updateQwUser(qwUser);
                contactWay = qwUser.getContactWay();
            } else {
                return R.error("请联系管理员>>>" + qwAddContactWayResult.getErrmsg());
            }
        }
        return R.error(400, msg).put("qrcode", contactWay);
    }


    @Override
    public List<FsUserCourseVideoVO> selectFsUserCourseVideoListByCourseIdAndCompany(FsUserCourseVideoParam fsUserCourseVideo) {
        return fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAndCompany(fsUserCourseVideo);
    }

    @Override
    public R updateFsUserCourseVideoCompanyRedPacket(FsUserCourseVideoParam fsUserCourseVideo) {
        fsUserCourseVideoRedPackageMapper.insertOrUpdateFsUserCourseVideoRedPackage(fsUserCourseVideo);
        return R.ok();
    }


    @Transactional
    @Override
    public R getInternetTraffic(FsUserCourseVideoFinishUParam param) {
        try {
            if (param.getBufferRate() == null) {
                logger.error("【缓冲值空】参数: {}", param);
                return R.error("缓冲值空");
            }
            FsCourseTrafficLog trafficLog = new FsCourseTrafficLog();
            trafficLog.setQwExternalContactId(param.getQwExternalId());
            trafficLog.setCreateTime(new Date());
            BeanUtils.copyProperties(param, trafficLog);

            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
            if (video == null) {
                return R.error("视频不存在");
            }
            Company company = companyMapper.selectCompanyById(param.getCompanyId());
            if (company == null) {
                return R.error("公司不存在");
            }

            // 实际流量传输消耗是大于文件的，添加倍数计算流量消耗 配置 course.data.usage.multiple
            BigDecimal multiple = new BigDecimal("1"); // 默认一倍
//            String config=configService.selectConfigByKey("course.data.usage.multiple");
//            if(StringUtils.isNotEmpty(config)){
//                multiple=new BigDecimal(config);
//            }
            // 计算流量
            BigDecimal result = param.getBufferRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
            BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
            long roundedResult = result.multiply(longAsBigDecimal).multiply(multiple).setScale(0, RoundingMode.HALF_UP).longValue();
            trafficLog.setInternetTraffic(roundedResult);
            // 获取课程所属项目id
            FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
            if (fsUserCourse != null) {
                trafficLog.setProject(fsUserCourse.getProject());
            }

            // 处理 UUID 为空的情况
            if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
                // 插入或更新
                fsCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
                asyncDeductTraffic(company, trafficLog);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 打印参数param和异常信息
            logger.error("【插入或更新流量失败】参数: {}, 错误信息：{}", param, e.getMessage(), e);
            return R.error();
        }
        return R.ok();
    }

    public void asyncDeductTraffic(Company company, FsCourseTrafficLog trafficLog) {
        try {
            //根据uuid查询
            FsCourseTrafficLog existingLog = fsCourseTrafficLogMapper.selectFsCourseTrafficLogByuuId(trafficLog.getUuId());
            long recordedTraffic;
            if (existingLog != null) {
                recordedTraffic = trafficLog.getInternetTraffic() - existingLog.getInternetTraffic();
            } else {
                recordedTraffic = trafficLog.getInternetTraffic();
            }
            if (recordedTraffic <= 0) {
                return;
            }
            // 扣除流量
            Long remainingTraffic = updateRedisCache(company, recordedTraffic / 1024);

            if ("1".equals(configUtil.generateConfigByKey("watch.course.config").getString("doNotPlay")) && remainingTraffic <= 0) {
                logger.warn("公司ID: {} 流量不足，当前剩余: {}", company.getCompanyId(), remainingTraffic);
                throw new Exception("流量不足");
            }

            /*logger.info("异步扣除流量成功 - 公司ID: {}, 扣除流量: {}, 剩余流量: {}",
                    company.getCompanyId(), traffic, remainingTraffic);*/
        } catch (Exception e) {
            logger.error("异步扣除流量失败 - 公司ID: {}, 错误信息: {}",
                    company.getCompanyId(), e.getMessage(), e);
        }
    }

    //
    private Long updateRedisCache(Company company, Long traffic) throws Exception {
        // 分布式扣除流量
        String companyKey = CompanyTrafficConstants.CACHE_KEY + ":" + company.getDeptId() + ":" + company.getCompanyId();
        String companyDayKey = companyKey + ":" + LocalDate.now();
        String companyMonthKey = companyKey + ":" + YearMonth.now();

        String deptKey = CompanyTrafficConstants.CACHE_KEY + ":" + company.getDeptId();
        String deptDayKey = deptKey + ":" + LocalDate.now();
        String deptMonthKey = deptKey + ":" + YearMonth.now();

        String lockKey = companyKey + ":lock";
        //销售公司剩余流量
        Object companyTraffic = redisCache.getCacheObject(companyKey);
        Long balance;
        if (companyTraffic instanceof Long) {
            balance = (Long) companyTraffic;
        } else if (companyTraffic instanceof Integer) {
            balance = ((Integer) companyTraffic).longValue();
        } else if (companyTraffic instanceof String) {
            balance = Long.parseLong(companyTraffic.toString());
        } else {
            throw new IllegalArgumentException("流量转换异常");
        }
        try {
            if (redisCache.setIfAbsent(lockKey, "1", 2, TimeUnit.SECONDS)) {
                try {
                    balance = redisCache.decr(companyKey, traffic);
                    redisCache.decr(deptKey, traffic);
                    redisCache.incr(companyDayKey, traffic);
                    redisCache.incr(deptDayKey, traffic);
                    redisCache.incr(companyMonthKey, traffic);
                    redisCache.incr(deptMonthKey, traffic);
                } finally {
                    redisCache.deleteObject(lockKey); // 释放锁
                }
            }
        } catch (Exception e) {
            logger.error("【更新Redis缓存失败】企业ID: {}, 流量: {}, 错误信息：{}", company.getCompanyId(), traffic, e.getMessage(), e);
            throw e;
        }
        return balance;
    }


    @Override
    public R getIntegralByH5Video(FsUserCourseVideoFinishUParam param) {
//        //临时短链不做处理
//        if (param.getLinkType()!=null&&param.getLinkType()==1){
//            return R.ok();
//        }
//        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(),param.getVideoId(),param.getQwUserId(), param.getQwExternalId());
//        if (log==null){
//            return R.error("无记录");
//        }
//        if (log.getLogType()==2){
//            return R.error("已存在完课记录");
//        }
//        if (log.getLogType()==1){
//            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
//            if (video==null){
//                return R.error("课程视频不存在！");
//            }
//            FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
//            if (user!=null){
//                //判断该视频可增加积分的此时 （每十分钟增加积分）
//                Long count = video.getDuration()/600;
//                Long getIntegralCount = fsUserIntegralLogsMapper.selectH5VideoIntegralCount(param.getUserId(), param.getVideoId());
//                if (getIntegralCount.equals(count)){
//                    return R.error("该课堂或积分已达限制！");
//                }
//                String json = configService.selectConfigByKey("course.config");
//                CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
//                //增加积分
//                FsUser userMap=new FsUser();
//                userMap.setUserId(user.getUserId());
//                userMap.setIntegral(user.getIntegral()+config.getVideoIntegral());
//                fsUserMapper.updateFsUser(userMap);
//                FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
//                integralLogs.setIntegral(config.getVideoIntegral().longValue());
//                integralLogs.setUserId(user.getUserId());
//                integralLogs.setBalance(userMap.getIntegral());
//                integralLogs.setLogType(16);
//                integralLogs.setBusinessId(log.getLogId().toString());
//                integralLogs.setCreateTime(new Date());
//                fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
//
//                FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
//                redPacketLog.setCourseId(log.getCourseId());
//                redPacketLog.setOutBatchNo(integralLogs.getId().toString());
//                redPacketLog.setCompanyId(log.getCompanyId());
//                redPacketLog.setUserId(log.getUserId());
//                redPacketLog.setVideoId(log.getVideoId());
//                redPacketLog.setStatus(1);
//                redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId() : null );
//                redPacketLog.setCompanyUserId(log.getCompanyUserId());
//                redPacketLog.setCreateTime(new Date());
//                redPacketLog.setAmount(BigDecimal.valueOf(config.getVideoIntegral()).divide(BigDecimal.valueOf(1000)));
//                redPacketLog.setRemark("点播看课获得积分转");
//                redPacketLog.setWatchLogId(log.getLogId() !=null ? log.getLogId() : null);
//                redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
//                return R.ok();
//            }
//        }
        return R.error();
    }


    @Override
    @Transactional
    public R sendReward(FsCourseSendRewardUParam param) {
//        boolean isWithin10Minutes = isWithin10Minutes(param.getUserId());
//        if (isWithin10Minutes){
//            return R.error("非有效期内，不允许领取！");
//        }
        // 生成锁的key，基于用户ID和视频ID确保同一用户同一视频的请求被锁定
        String lockKey = "reward_lock:user:" + param.getUserId() + ":video:" + param.getVideoId();
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试获取锁，等待时间5秒，锁过期时间30秒
            boolean isLocked = lock.tryLock(5, 60, TimeUnit.SECONDS);
            if (!isLocked) {
                logger.warn("获取锁失败，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
                return R.error("操作频繁，请稍后再试！");
            }

            logger.info("成功获取锁，开始处理奖励发放，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
            // 获取用户信息
            FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
//        if (StringUtils.isEmpty(user.getMpOpenId())){
//            return R.error("未识别到领取信息");
//        }
            log.info("查询会员信息:{}", user);
            if (user.getStatus() == 0) {
                return R.error("当前学习通道访问限流，暂时无法观看！");
            }

            if (StringUtil.strIsNullOrEmpty(user.getMpOpenId()) && !"北京存在文化".equals(signProjectName)){
                return R.error(401,"授权后可继续！");
            }

            if (!isNewWxMerchant && StringUtils.isEmpty(user.getUnionId())){
                return R.error("会员信息有误,无法领取红包,请联系客服!");
            }
            FsCourseWatchLog watchLog = new FsCourseWatchLog();

            // 根据链接类型判断是否已发放奖励
            watchLog = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
            log.info("看课记录：{}", watchLog);
            if (watchLog == null) {
                return R.error("无记录");
            }
            if (watchLog.getLogType() != 2) {
                return R.error("未完课");
            }
            if (watchLog.getRewardType() != null) {
                FsCourseRedPacketLog packetLog = redPacketLogMapper.selectFsCourseRedPacketLogByTemporary(param.getVideoId(), param.getUserId());
                log.info("课程红包：{}", packetLog);
                if (packetLog != null && packetLog.getStatus() == 1) {
                    return R.error("已领取该课程奖励，不可重复领取！");
                }
                if (packetLog != null && packetLog.getStatus() == 0) {
                    log.info("判断领取记录");
                    if (StringUtils.isNotEmpty(packetLog.getResult())) {
                        log.info("是否有结果");
                        R r = JSON.parseObject(packetLog.getResult(), R.class);
                        return r;
                    } else {
                        return R.error("操作频繁，请稍后再试！");
                    }
                }
                if (packetLog != null && packetLog.getStatus() == 2) {
                    return R.error("请联系客服补发");
                }
                return R.error("奖励已发放");
            }
            // 获取视频信息
            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());

            // 获取配置信息
            String json = configService.selectConfigByKey("course.config");
            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

            //服务号授权的，缺mpOpenId的重新登录
//            if (config.getMiniAppAuthType()==2 && StringUtil.strIsNullOrEmpty(user.getMpOpenId())){
//                return R.error(401,"授权后可继续！");
//            }

            log.info("奖励类型：{}", config.getRewardType());
            // 根据奖励类型发放不同奖励
            switch (config.getRewardType()) {
                // 红包奖励
                case 1:
                    if (param.getSource() == 3){
                        param.setWatchLogId(watchLog.getLogId());
                        return withdrawal(param);
                    } else {
                        return sendRedPacketReward(param, user, watchLog, video, config);
                    }
                // 积分奖励
                case 2:
                    return sendIntegralReward(param, user, watchLog, config);
                // 红包+积分
                case 3:
                    if (isUserRedPacketReceiveEnabled(user)) {
                        R sendRed = sendRedPacketReward(param, user, watchLog, video, config);
                        if (!Objects.equals(sendRed.get("code"), 200)) {
                            return sendRed;
                        }
                    }
                    return sendIntegralReward(param, user, watchLog, config);
                default:
                    return R.error("参数错误！");
            }
//            return executeSendRewardBusiness(param);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error("获取锁被中断，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId(), e);
            return R.error("系统繁忙，请重试！");
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                logger.info("释放锁成功，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
            }
        }

    }


    /**
     * 检查是否在10分钟有效期内
     */
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    private static final int EXPIRE_MINUTES = 10;

    private boolean isWithin10Minutes(Long userId) {
        String key = "h5user:redPacket:" + userId;

        String storedTimeStr = redisCache.getCacheObject(key);
        if (storedTimeStr == null) {
            return false;
        }

        try {
            LocalDateTime storedTime = LocalDateTime.parse(storedTimeStr, FORMATTER);
            long minutes = Duration.between(storedTime, LocalDateTime.now()).toMinutes();
            return minutes <= EXPIRE_MINUTES;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public R sendRewardByFsUser(FsCourseSendRewardUParam param) {
        // 生成锁的key，基于用户ID和视频ID确保同一用户同一视频的请求被锁定
        String lockKey = "reward_lock:user:" + param.getUserId() + ":video:" + param.getVideoId();
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试获取锁，等待时间5秒，锁过期时间30秒
            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
            if (!isLocked) {
                logger.warn("获取锁失败，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
                return R.error("操作频繁，请稍后再试！");
            }

            logger.info("成功获取锁，开始处理奖励发放，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
            return executeSendRewardBusiness(param);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error("获取锁被中断，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId(), e);
            return R.error("系统繁忙，请重试！");
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                logger.info("释放锁成功，用户ID：{}，视频ID：{}", param.getUserId(), param.getVideoId());
            }
        }
    }


    /**
     * 实际的奖励发放业务逻辑
     */
    private R executeSendRewardBusiness(FsCourseSendRewardUParam param) {
        log.info("进入用户判断");
        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
        if (user == null) {
            return R.error("未识别到用户信息");
        }

        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
        if (log == null) {
            return R.error("无记录");
        }

        if (log.getLogType() != 2) {
            return R.error("未完课");
        }

        FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
        if (rightLog == null) {
            logger.error("未答题：{}", param.getUserId());
            return R.error("未答题");
        }

        if (log.getRewardType() != null) {
            if (log.getRewardType() == 1) {
                FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
                    return R.error("已领取该课程奖励，不可重复领取！").put("data",fsCourseRedPacketLog.getResult());
                }
                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                    if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
                        R r = JSON.parseObject(fsCourseRedPacketLog.getResult(), R.class);
                        return r;
                    } else {
                        return R.error("操作频繁，请稍后再试！");
                    }
                }
            } else if (log.getRewardType() == 2) {
                return R.error("已领取该课程奖励，不可重复领取！");
            }
        }

        // 获取视频信息
        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());

        // 获取配置信息
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

//        // 判断来源是否是app，如是app，则发放积分奖励
//        int sourceApp = 3;
//        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
//            return sendIntegralReward(param, user, log, config);
//        }

        // 根据奖励类型发放不同奖励
        switch (config.getRewardType()) {
            // 红包奖励
            case 1:
                if (param.getSource()==3){
                    WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
                    String openId = getOpenId(param, user);
                    if (StringUtils.isBlank(openId)) {
                        return R.error("请重新使用微信登录");
                    }
                    packetParam.setOpenId(openId);
                    BeanUtils.copyProperties(param, packetParam);
                    packetParam.setUser(user);
                    return sendAppRedPacket(packetParam, log,video, config);
                } else {
                    return sendRedPacketRewardFsUser(param, user, log, video, config);
                }
            // 积分奖励
            case 2:
                return sendIntegralReward(param, user, log, config);
            // 红包+积分
            case 3:
                if (isUserRedPacketReceiveEnabled(user)) {
                    R sendRed = sendRedPacketRewardFsUser(param, user, log, video, config);
                    if (!Objects.equals(sendRed.get("code"), 200)) {
                        return sendRed;
                    }
                }
                return sendIntegralReward(param, user, log, config);
            default:
                return R.error("参数错误！");
        }
    }

    /**
     * 发放红包奖励
     *
     * @param param  请求参数
     * @param user   用户信息
     * @param log    观看日志
     * @param video  视频信息
     * @param config 配置信息
     * @return 处理结果
     */
    private R sendRedPacketReward(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog log, FsUserCourseVideo video, CourseConfig config) {
        logger.info("进入发放红包");
        if (!isUserRedPacketReceiveEnabled(user)) {
            return R.ok("恭喜您，答题成功啦 ！");
        }
        // 确定红包金额
        BigDecimal amount = BigDecimal.ZERO;
        FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyIdZd(param.getVideoId(), param.getCompanyId());

        if (redPackage != null) {
            amount = redPackage.getRedPacketMoney();
        } else if (video != null) {
            amount = video.getRedPacketMoney();
            amount = courseRandomRedPacketService.resolveAmount(amount, param.getCourseId(), param.getCompanyId(), video, true);
        }

        // 准备发送红包参数
        WxSendRedPacketParam packetParam = new WxSendRedPacketParam();

        //判断是否走服务号openId发红包
        if (config.getMiniAppAuthType() == 2 && user.getMpOpenId() != null && !isNewWxMerchant) {
            packetParam.setOpenId(user.getMpOpenId());
        } else {
            //查询是否绑定小程序
            FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(), user.getUserId(), 1);
            if (fsUserWx == null) {
                return R.error(401, "openId参数错误，请清理缓存重新授权");
            } else {
                packetParam.setOpenId(fsUserWx.getOpenId());
            }
        }

        //判断服务号配置是否存在
        if (config.getMiniAppAuthType() == 2 && StringUtils.isNotEmpty(config.getMpAppId())) {
            packetParam.setMpAppId(config.getMpAppId());
        }
        packetParam.setAmount(amount);
        packetParam.setSource(param.getSource());
        packetParam.setRedPacketMode(config.getRedPacketMode());
        packetParam.setCompanyId(param.getCompanyId());
        packetParam.setAppId(param.getAppId());
        packetParam.setUser(user);

        logger.info("红包金额 {},红包商户号 {}", amount, packetParam);
        //2025.6.19 红包金额为0的时候
        if (amount.compareTo(BigDecimal.ZERO) > 0) {
            // 发送红包
            return sendRedPacketRewardToUser(param, log, config, packetParam, amount);
        } else {
            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
            // 添加红包记录
            redPacketLog.setCourseId(param.getCourseId());

            redPacketLog.setCompanyId(param.getCompanyId());
            redPacketLog.setUserId(param.getUserId());
            redPacketLog.setVideoId(param.getVideoId());
            redPacketLog.setStatus(0);
            redPacketLog.setQwUserId(param.getQwUserId());
            redPacketLog.setCompanyUserId(param.getCompanyUserId());
            redPacketLog.setCreateTime(new Date());
            redPacketLog.setAmount(BigDecimal.ZERO);
            redPacketLog.setWatchLogId(log.getLogId());
            redPacketLog.setPeriodId(param.getPeriodId());
            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

            // 更新观看记录的奖励类型
            log.setRewardType(config.getRewardType());
            courseWatchLogMapper.updateFsCourseWatchLog(log);
            return R.ok("答题成功！");
        }

    }

    /** 发红包接口返回非成功时，优先透传支付层返回的 msg */
    private static String resolveSendRedPacketFailMsg(R sendRedPacket) {
        if (sendRedPacket == null) {
            return "奖励发送失败，请联系客服";
        }
        Object msgObj = sendRedPacket.get("msg");
        if (msgObj != null) {
            String m = String.valueOf(msgObj).trim();
            if (StringUtils.isNotEmpty(m) && !"success".equalsIgnoreCase(m)) {
                return m;
            }
        }
        return "奖励发送失败，请联系客服";
    }

    private static boolean isWxAppIdMchIdNotMatch(Throwable t) {
        while (t != null) {
            if (t instanceof WxPayException) {
                WxPayException wx = (WxPayException) t;
                if ("APPID_MCHID_NOT_MATCH".equals(wx.getErrCode())) {
                    return true;
                }
                String des = wx.getErrCodeDes();
                if (des != null && des.contains("商户号和appid没有绑定关系")) {
                    return true;
                }
            }
            // 兜底：有些场景下异常类型/字段未必能完全取到，只要 message 中包含关键字也认为是该错误
            String msg = t.getMessage();
            if (msg != null && (msg.contains("APPID_MCHID_NOT_MATCH") || msg.contains("商户号和appid没有绑定关系"))) {
                return true;
            }
            String str = t.toString();
            if (str != null && (str.contains("APPID_MCHID_NOT_MATCH") || str.contains("商户号和appid没有绑定关系"))) {
                return true;
            }
            t = t.getCause();
        }
        return false;
    }

    private R sendRedPacketRewardToUser(FsCourseSendRewardUParam param, FsCourseWatchLog log, CourseConfig config, WxSendRedPacketParam packetParam, BigDecimal amount) {


        // 自动看课红包余额字段实时监控  xgb 1111
        if("1".equals(config.getIsRedPackageBalanceDeduction())) {
            // 新增的有设计文档 可以找 xgb 要
            // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
            // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
            // 2 另起定时任务 同步缓存余额到redis中
            // 3 注意！！！！！ 启动系统时查询公司账户余额（这个时候要保证余额正确）启动会自动保存到redis缓存中
            // 注意！！！！！ 打开这个开关前记得检测redis缓存余额是否正确 若不正确 修改数据库字段red_package_money，删除redis缓存，重启系统，


            // 预设值异常对象
            BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
            balanceRollbackError.setCompanyId(packetParam.getCompanyId());
            balanceRollbackError.setUserId(param.getUserId());
            balanceRollbackError.setLogId(log.getLogId());
            balanceRollbackError.setVideoId(log.getVideoId());
            balanceRollbackError.setStatus(0);
            balanceRollbackError.setMoney(amount);

            if (packetParam.getCompanyId() == null) {
                logger.error("发送红包参数错误，公司不能为空，异常请求参数{}", packetParam);
                return R.error("发送红包失败，请联系管理员");
            }
            String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();

            // 第一次加锁：预扣减余额
            RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
            boolean lockAcquired = false;
            BigDecimal newMoney;
            try {
                if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
                    lockAcquired = true;
                    BigDecimal originalMoney;
                    // 获取当前余额
                    String moneyStr = redisCache.getCacheObject(companyMoneyKey);
                    if (StringUtils.isNotEmpty(moneyStr)) {
                        originalMoney = new BigDecimal(moneyStr);
                    } else {
                        // 缓存没有值,重启系统恢复redis数据 保证数据正确性
                        logger.error("发送红包获取redis余额缓存异常，异常请求参数{}", packetParam);
                        return R.error("系统异常，请稍后重试");
                    }

                    if (originalMoney.compareTo(BigDecimal.ZERO) <0.3) {
                        logger.error("服务商余额不足，当前余额：{}，异常请求参数{}", originalMoney, packetParam);
                        return R.error("服务商余额不足，请联系群主服务器充值!");
                    }

//                    // 检查余额是否足够扣减红包金额
//                    if (originalMoney.compareTo(amount) < 0) {
//                        logger.error("服务商余额不足，当前余额：{}，需要扣减：{}，异常请求参数{}", originalMoney, amount, packetParam);
//                        return R.error("服务商余额不足，请联系群主服务器充值!");
//                    }

                    // 预扣减金额
                    newMoney = originalMoney.subtract(amount);
                    redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
                } else {
                    logger.error("获取redis锁失败，异常请求参数{}", packetParam);
                    return R.error("系统繁忙，请稍后重试");
                }
            } catch (Exception e) {
                logger.error("预扣减余额失败: 异常请求参数{}，异常信息{}", packetParam, e.getMessage(), e);
                return R.error("系统异常，请稍后重试");
            } finally {
                // 只有在成功获取锁的情况下才释放锁
                if (lockAcquired && lock1.isHeldByCurrentThread()) {
                    try {
                        lock1.unlock();
                    } catch (IllegalMonitorStateException e) {
                        logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
                    }
                }
            }

            // 调用第三方接口（锁外操作）
            R sendRedPacket;
            try {
                sendRedPacket= paymentService.sendRedPacket(packetParam);
            } catch (Exception e) {
                logger.error("红包发送异常: 异常请求参数{}",packetParam, e);
                // 异常时回滚余额

                rollbackBalance(balanceRollbackError);
                if (isWxAppIdMchIdNotMatch(e)) {
                    return R.error("未绑定该小程序，请联系群主");
                }
                return R.error("奖励发送失败，请联系客服");
            }

            if (sendRedPacket.get("code").equals(200)) {
                FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                TransferBillsResult transferBillsResult;
                if (sendRedPacket.get("isNew").equals(1)){
                    transferBillsResult = (TransferBillsResult)sendRedPacket.get("data");
                    redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                    redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                }else {
                    redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                }
                // 添加红包记录
                redPacketLog.setCourseId(param.getCourseId());
                redPacketLog.setCompanyId(param.getCompanyId());
                redPacketLog.setUserId(param.getUserId());
                redPacketLog.setVideoId(param.getVideoId());
                redPacketLog.setStatus(0);
                redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
                redPacketLog.setCompanyUserId(param.getCompanyUserId());
                redPacketLog.setCreateTime(new Date());
                redPacketLog.setAmount(amount);
                redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                redPacketLog.setPeriodId(param.getPeriodId());

                redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
                // 更新观看记录的奖励类型
                log.setRewardType(config.getRewardType());
                courseWatchLogMapper.updateFsCourseWatchLog(log);

                // 异步登记余额扣减日志
                BigDecimal money=amount.multiply(BigDecimal.valueOf(-1));
                companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包", param.getUserId());
//            redisCache.setCacheObject("h5user:redPacket:"+param.getUserId(),LocalDateTime.now().toString());

                return sendRedPacket;
            } else {
                // 登记回滚流水表
                rollbackBalance(balanceRollbackError);
                String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
                if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
                    return R.error("商户号异常，请联系群主");
                }
                return R.error(failMsg);
            }



        }else {
            // 发送红包
            R sendRedPacket = paymentService.sendRedPacket(packetParam);
            if (sendRedPacket.get("code").equals(200)) {
                FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                TransferBillsResult transferBillsResult;
                if (sendRedPacket.get("isNew").equals(1)){
                    transferBillsResult = (TransferBillsResult)sendRedPacket.get("data");
                    redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                    redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                }else {
                    redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                }
                //存储商户ID
                if (StringUtils.isNotEmpty(sendRedPacket.get("mchId").toString())){
                    redPacketLog.setMchId(sendRedPacket.get("mchId").toString());
                }
                // 添加红包记录
                redPacketLog.setCourseId(param.getCourseId());
                redPacketLog.setCompanyId(param.getCompanyId());
                redPacketLog.setUserId(param.getUserId());
                redPacketLog.setVideoId(param.getVideoId());
                redPacketLog.setStatus(0);
                redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
                redPacketLog.setCompanyUserId(param.getCompanyUserId());
                redPacketLog.setCreateTime(new Date());
                redPacketLog.setAmount(amount);
                redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                redPacketLog.setPeriodId(param.getPeriodId());

                redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
                // 更新观看记录的奖励类型
                log.setRewardType(config.getRewardType());
                courseWatchLogMapper.updateFsCourseWatchLog(log);

//            redisCache.setCacheObject("h5user:redPacket:"+param.getUserId(),LocalDateTime.now().toString());

                return sendRedPacket;
            } else {
                String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
                if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
                    return R.error("商户号异常，请联系群主");
                }
                return R.error(failMsg);
            }
        }
    }


    private void handleFsUserWx(FsUser user, String appId) {
        FsUserWx fsUserWx = new FsUserWx();
        fsUserWx.setType(1);
        fsUserWx.setFsUserId(user.getUserId());
        fsUserWx.setAppId(appId);
        fsUserWx.setOpenId(user.getCourseMaOpenId());
        fsUserWx.setUnionId(user.getUnionId());
        fsUserWx.setCreateTime(new Date());
        fsUserWx.setUpdateTime(new Date());
        fsUserWxService.saveOrUpdateByUniqueKey(fsUserWx);

        logger.info("【更新或插入用户与小程序{}的绑定关系】：{}", appId, user.getUserId());
    }

    /**
     * 发放红包奖励
     *
     * @param param  请求参数
     * @param user   用户信息
     * @param log    观看日志
     * @param video  视频信息
     * @param config 配置信息
     * @return 处理结果
     */
    private R sendRedPacketRewardFsUser(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog log, FsUserCourseVideo video, CourseConfig config) {

        FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
        periodDays.setVideoId(param.getVideoId());
        periodDays.setPeriodId(param.getPeriodId());
        //正常情况是只能查询到一条，之前可能存在重复的脏数据，暂使用查询list的方式
        List<FsUserCoursePeriodDays> fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(periodDays);
        if (fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()) {
            periodDays = fsUserCoursePeriodDays.get(0);
        }

        // 检查是否开启自由模式
        FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
        boolean isFreeMode = (period != null && period.getFreeMode() != null && period.getFreeMode() == 1);

        // 只有在非自由模式下才检查红包领取时间限制
        if(!isFreeMode && periodDays != null && periodDays.getLastJoinTime() !=null && LocalDateTime.now().isAfter(periodDays.getLastJoinTime())) {
            return R.error(403,"已超过领取红包时间");
        }

        if (!isUserRedPacketReceiveEnabled(user)) {
            return R.ok("恭喜您，答题成功啦 ！");
        }

        // 确定红包金额
        BigDecimal amount = BigDecimal.ZERO;
        FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyId(param.getVideoId(), param.getCompanyId(), param.getPeriodId());

        if (redPackage != null && redPackage.getRedPacketMoney() != null) {
            amount = redPackage.getRedPacketMoney();
        } else if (video != null && video.getRedPacketMoney() != null) {
            amount = video.getRedPacketMoney();
        }
        amount = courseRandomRedPacketService.resolveAmount(amount, param.getCourseId(), param.getCompanyId(), video, false);

        // 准备发送红包参数
        WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
        packetParam.setOpenId(user.getMpOpenId());
        // 来源是小程序切换openId
        if (param.getSource() == 2) {
            //处理多小程序问题
//            Company company = companyMapper.selectCompanyById(param.getCompanyId());
//            if (company.getCourseMiniAppId()==null){
//                return R.error("销售公司参数错误，未绑定小程序");
//            }
            if (user.getMpOpenId() != null && !isNewWxMerchant) {
                packetParam.setOpenId(user.getMpOpenId());
            } else {
                //查询是否绑定小程序
                FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(), user.getUserId(), 1);
                if (fsUserWx == null) {
                    return R.error("openId参数错误，请清理缓存重新授权");
                } else {
                    packetParam.setOpenId(fsUserWx.getOpenId());
                }
            }
        }
        //判断服务号配置是否存在
        if (StringUtils.isNotEmpty(config.getMpAppId())) {
            packetParam.setMpAppId(config.getMpAppId());
        }
        //组装发红包数据
        packetParam.setAmount(amount);
        packetParam.setSource(param.getSource());
        packetParam.setRedPacketMode(config.getRedPacketMode());
        packetParam.setCompanyId(param.getCompanyId());
        packetParam.setAppId(param.getAppId());
        packetParam.setUser(user);

        logger.info("红包金额 {},红包商户号 {}", amount, packetParam);

        //2025.6.19 红包金额为0的时候
        if (amount.compareTo(BigDecimal.ZERO) > 0) {

            // 打开红包扣减功能
            if ("1".equals(config.getIsRedPackageBalanceDeduction())) {
                // 先注释 20251024 redis 余额 充值没有考虑 其余扣减没有考虑
                // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
                // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
                // 2 另起定时任务 同步缓存余额到redis中
                // 3 注意！！！！！ 启动系统时查询公司账户余额（这个时候要保证余额正确）启动会自动保存到redis缓存中
                // 注意！！！！！ 打开这个开关前记得检测redis缓存余额是否正确 若不正确 修改数据库字段red_package_money，删除redis缓存，重启系统，


                // 预设值异常对象

                BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
                balanceRollbackError.setCompanyId(packetParam.getCompanyId());
                balanceRollbackError.setUserId(user.getUserId());
                balanceRollbackError.setLogId(log.getLogId());
                balanceRollbackError.setVideoId(log.getVideoId());
                balanceRollbackError.setStatus(0);
                balanceRollbackError.setMoney(amount);

                if (packetParam.getCompanyId() == null) {
                    logger.error("发送红包参数错误，公司不能为空，异常请求参数{}", packetParam);
                    return R.error("发送红包失败，请联系管理员");
                }
                String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();

                // 第一次加锁：预扣减余额
                RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
                boolean lockAcquired = false;
                BigDecimal newMoney;
                try {
                    if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
                        lockAcquired = true;
                        BigDecimal originalMoney;
                        // 获取当前余额
                        String moneyStr = redisCache.getCacheObject(companyMoneyKey);
                        if (StringUtils.isNotEmpty(moneyStr)) {
                            originalMoney = new BigDecimal(moneyStr);
                        } else {
                            // 缓存没有值,重启系统恢复redis数据 保证数据正确性
                            logger.error("发送红包获取redis余额缓存异常，异常请求参数{}", packetParam);
                            return R.error("系统异常，请稍后重试");
                        }

                        if (originalMoney.compareTo(BigDecimal.ZERO) < 0.3) {
                            logger.error("服务商余额不足，当前余额：{}，异常请求参数{}", originalMoney, packetParam);
                            return R.error("服务商余额不足，请联系群主服务器充值!");
                        }

                        // 预扣减金额
                        newMoney = originalMoney.subtract(amount);
                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
                    } else {
                        logger.error("获取redis锁失败，异常请求参数{}", packetParam);
                        return R.error("系统繁忙，请稍后重试");
                    }
                } catch (Exception e) {
                    logger.error("预扣减余额失败: 异常请求参数{}，异常信息{}", packetParam, e.getMessage(), e);
                    return R.error("系统异常，请稍后重试");
                } finally {
                    // 只有在成功获取锁的情况下才释放锁
                    if (lockAcquired && lock1.isHeldByCurrentThread()) {
                        try {
                            lock1.unlock();
                        } catch (IllegalMonitorStateException e) {
                            logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
                        }
                    }
                }


                // 调用第三方接口（锁外操作）
                R sendRedPacket;
                try {
                    sendRedPacket = paymentService.sendRedPacket(packetParam);
                } catch (Exception e) {
                    logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
                    // 异常时回滚余额

                    rollbackBalance(balanceRollbackError);
                    if (isWxAppIdMchIdNotMatch(e)) {
                        return R.error("未绑定该小程序，请联系群主");
                    }
                    return R.error("奖励发送失败，请联系客服");
                }

                // 红包发送成功处理
                if (sendRedPacket.get("code").equals(200)) {
                    FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                    TransferBillsResult transferBillsResult;
                    if (sendRedPacket.get("isNew").equals(1)) {
                        transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
                        redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                        redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                        redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
                    } else {
                        redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                        redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
                    }
                    // 添加红包记录
                    redPacketLog.setCourseId(param.getCourseId());
                    redPacketLog.setCompanyId(param.getCompanyId());
                    redPacketLog.setUserId(param.getUserId());
                    redPacketLog.setVideoId(param.getVideoId());
                    redPacketLog.setStatus(0);
                    redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
                    redPacketLog.setCompanyUserId(param.getCompanyUserId());
                    redPacketLog.setCreateTime(new Date());
                    redPacketLog.setAmount(amount);
                    redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                    redPacketLog.setPeriodId(param.getPeriodId());
                    redPacketLog.setAppId(param.getAppId());

                    redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

                    // 更新观看记录的奖励类型
                    log.setRewardType(config.getRewardType());
                    courseWatchLogMapper.updateFsCourseWatchLog(log);

                    // 异步登记余额扣减日志
                    BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
                    companyService.asyncRecordBalanceLog(param.getCompanyId(), money, 15, newMoney, "发放红包", redPacketLog.getLogId());

                    return sendRedPacket;


                } else {
                    // 发送失败，回滚余额
                    rollbackBalance(balanceRollbackError);
                    String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
                    if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定关系"))) {
                        return R.error("小程序未绑定，请联系群主");
                    }
                    return R.error(failMsg);
                }

                // ===================== 本次修改目的为了实时扣减公司余额=====================
            } else {
                Company company = companyMapper.selectCompanyById(param.getCompanyId());
                BigDecimal money = company.getMoney();
                if (money.compareTo(BigDecimal.ZERO) <= 0) {
                    return R.error("服务商余额不足，请联系群主服务器充值!");
                }

             try{
                 // 发送红包
                 R sendRedPacket = paymentService.sendRedPacket(packetParam);
                 if (sendRedPacket.get("code").equals(200)) {
                     FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                     TransferBillsResult transferBillsResult;
                     if (sendRedPacket.get("isNew").equals(1)) {
                         transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
                         redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                         redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                         redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
                     } else {
                         redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                         redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
                     }
                     // 添加红包记录
                     redPacketLog.setCourseId(param.getCourseId());
                     redPacketLog.setCompanyId(param.getCompanyId());
                     redPacketLog.setUserId(param.getUserId());
                     redPacketLog.setVideoId(param.getVideoId());
                     redPacketLog.setStatus(0);
                     redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
                     redPacketLog.setCompanyUserId(param.getCompanyUserId());
                     redPacketLog.setCreateTime(new Date());
                     redPacketLog.setAmount(amount);
                     redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                     redPacketLog.setPeriodId(param.getPeriodId());
                     redPacketLog.setAppId(param.getAppId());

                     redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

                     // 更新观看记录的奖励类型
                     log.setRewardType(config.getRewardType());
                     courseWatchLogMapper.updateFsCourseWatchLog(log);

                     return sendRedPacket;
                 } else {
                    String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
                    if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
                        return R.error("商户号异常，请联系群主");
                    }
                    return R.error(failMsg);
                 }
             }catch (Exception e){
                 return R.error(e.getMessage());
             }

            }
        } else {
            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
            // 添加红包记录
            redPacketLog.setCourseId(param.getCourseId());
//            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
            redPacketLog.setCompanyId(param.getCompanyId());
            redPacketLog.setUserId(param.getUserId());
            redPacketLog.setVideoId(param.getVideoId());
            redPacketLog.setStatus(1);
            redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
            redPacketLog.setCompanyUserId(param.getCompanyUserId());
            redPacketLog.setCreateTime(new Date());
            redPacketLog.setAmount(BigDecimal.ZERO);
            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
            redPacketLog.setPeriodId(param.getPeriodId());
            redPacketLog.setAppId(param.getAppId());
            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

            // 更新观看记录的奖励类
            log.setRewardType(config.getRewardType());
            courseWatchLogMapper.updateFsCourseWatchLog(log);
            return R.ok("答题成功！");
        }

    }

    /**
     * @Description: 回滚redis缓存中的余额 异常登记回滚异常表，定时重新回滚
     * @Param:
     * @Return:
     * @Author xgb
     * @Date 2025/10/22 10:37
     */
    private void rollbackBalance(BalanceRollbackError balanceRollbackError) {
        String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + balanceRollbackError.getCompanyId();
        RLock lock2 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + balanceRollbackError.getCompanyId());
        boolean lockAcquired = false;
        boolean backError = true;
        try {
            if (lock2.tryLock(3, 10, TimeUnit.SECONDS)) {
                lockAcquired = true;
                // 获取当前余额
                String currentMoneyStr = redisCache.getCacheObject(companyMoneyKey);
                if (StringUtils.isEmpty(currentMoneyStr)) {
                    throw new RuntimeException("回滚余额异常");
                }

                // 回滚金额（加回之前扣减的金额）
                BigDecimal rollbackMoney = new BigDecimal(currentMoneyStr).add(balanceRollbackError.getMoney());
                redisCache.setCacheObject(companyMoneyKey, rollbackMoney.toString());
                backError = false;
                logger.info("余额回滚成功: companyId={}, amount={}", balanceRollbackError.getCompanyId(), balanceRollbackError.getMoney());

            } else {
                logger.warn("回滚余额时获取锁失败: companyId={}", balanceRollbackError.getCompanyId());
                // 登记回滚余额异常表
                balanceRollbackErrorMapper.insert(balanceRollbackError);
            }
        } catch (Exception e) {
            logger.error("回滚余额时发生异常: companyId={}", balanceRollbackError.getCompanyId(), e);
            // 登记回滚余额异常表
            if (backError) {
                balanceRollbackErrorMapper.insert(balanceRollbackError);
            }
        } finally {
            // 只有在成功获取锁的情况下才释放锁
            if (lockAcquired && lock2.isHeldByCurrentThread()) {
                try {
                    lock2.unlock();
                } catch (IllegalMonitorStateException e) {
                    logger.warn("尝试释放非当前线程持有的锁: balanceRollbackError={}", balanceRollbackError);
                }
            }
        }
    }


    /**
     * 直接发送奖励
     *
     * @param config
     * @param packetParam
     * @param param
     * @param amount
     * @param log
     * @return
     */
    private R processRedPacket(CourseConfig config, WxSendRedPacketParam packetParam, FsCourseSendRewardUParam param, BigDecimal amount, FsCourseWatchLog log) {
        R sendRedPacket = paymentService.sendRedPacket(packetParam);

        if (!sendRedPacket.get("code").equals(200)) {
            String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
            if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
                return R.error("商户号异常，请联系群主");
            }
            return R.error(failMsg);
        }

        createRedPacketLog(sendRedPacket, param, amount, log);
        updateWatchLogRewardType(log, config);

        return sendRedPacket;
    }

    private void createRedPacketLog(R sendRedPacket, FsCourseSendRewardUParam param, BigDecimal amount, FsCourseWatchLog log) {
        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();

        // Set common fields
        redPacketLog.setCourseId(param.getCourseId());
        redPacketLog.setCompanyId(param.getCompanyId());
        redPacketLog.setUserId(param.getUserId());
        redPacketLog.setVideoId(param.getVideoId());
        redPacketLog.setStatus(0);
        redPacketLog.setQwUserId(param.getQwUserId());
        redPacketLog.setCompanyUserId(param.getCompanyUserId());
        redPacketLog.setCreateTime(new Date());
        redPacketLog.setAmount(amount);
        redPacketLog.setWatchLogId(log != null ? log.getLogId() : null);
        redPacketLog.setPeriodId(param.getPeriodId());
        redPacketLog.setAppId(param.getAppId());

        // Set batch number based on isNew flag
        if (sendRedPacket.get("isNew").equals(1)) {
            TransferBillsResult transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
            redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
            redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
        } else {
            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
        }

        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
    }

    private void updateWatchLogRewardType(FsCourseWatchLog log, CourseConfig config) {
        if (log != null) {
            log.setRewardType(config.getRewardType());
            courseWatchLogMapper.updateFsCourseWatchLog(log);
        }
    }

    /**
     * 获取用户openId
     *
     * @param userId    用户ID
     * @param companyId 公司ID
     * @param source    来源 1公众号 2小程序
     * @return openId
     */
    private String getOpenId(FsCourseSendRewardUParam param, FsUser user) {
        Integer source = param.getSource();
        Long userId = param.getUserId();
        switch (source) {
            case 1:
                Company company = companyMapper.selectCompanyById(param.getCompanyId());
                String appId = company.getCourseMaAppId();

                // 公司配置为空时获取默认配置
                if (StringUtils.isBlank(appId)) {
                    String json = configService.selectConfigByKey("course.config");
                    CourseConfig config = JSON.parseObject(json, CourseConfig.class);
                    appId = config.getMpAppId();
                }

                // 查询openId
                Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery().eq(FsUserWx::getFsUserId, userId).eq(FsUserWx::getAppId, appId);
                FsUserWx fsUserWx = fsUserWxService.getOne(queryWrapper);
                if (Objects.isNull(fsUserWx)) {
                    throw new CustomException("获取openId失败");
                }

                return fsUserWx.getOpenId();
            case 2:
                FsUserWx userWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),userId,1);
                if (Objects.nonNull(userWx) && StringUtils.isNotBlank(userWx.getOpenId())) {
                    return userWx.getOpenId();
                }

                if (StringUtils.isNotBlank(user.getCourseMaOpenId())) {
                    try {
                        handleFsUserWx(user,param.getAppId());
                    } catch (Exception e){
                        log.error("【更新或插入用户与小程序的绑定关系失败】：{}", userId, e);
                    }
                    return user.getCourseMaOpenId();
                }
                break;
            case 3:
                return user.getAppOpenId();
        }
        return null;
    }

    /**
     * 发放积分奖励
     *
     * @param user   用户信息
     * @param log    观看日志
     * @param config 配置信息
     * @return 处理结果
     */
    private R sendIntegralReward(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog log, CourseConfig config) {
        // 更新用户积分
        FsUser userMap = new FsUser();
        userMap.setUserId(user.getUserId());
        userMap.setIntegral(user.getIntegral() + config.getAnswerIntegral());
        fsUserMapper.updateFsUser(userMap);

        // 记录积分日志
        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
        integralLogs.setIntegral(config.getAnswerIntegral().longValue());
        integralLogs.setUserId(user.getUserId());
        integralLogs.setBalance(userMap.getIntegral());
        integralLogs.setLogType(17);
        integralLogs.setBusinessId(StringUtils.isNotEmpty(log.getLogId().toString()) ? log.getLogId().toString() : null);
        integralLogs.setCreateTime(new Date());
        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);

        //更新看课记录的奖励类型
        log.setRewardType(config.getRewardType());
        courseWatchLogMapper.updateFsCourseWatchLog(log);
        logger.info("发放奖励====================》看课记录,{}", log);

        // 红德堂不要积分转红包
        if (CloudHostUtils.hasCloudHostName("弘德堂")) {
            return R.ok("奖励发放成功").put("rewardType", config.getRewardType());
        }

        //转换红包
        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
        redPacketLog.setCourseId(param.getCourseId());
        redPacketLog.setOutBatchNo(integralLogs.getId().toString());
        redPacketLog.setCompanyId(param.getCompanyId());
        redPacketLog.setUserId(param.getUserId());
        redPacketLog.setVideoId(param.getVideoId());
        redPacketLog.setStatus(1);
        redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
        redPacketLog.setCompanyUserId(param.getCompanyUserId());
        redPacketLog.setCreateTime(new Date());
        redPacketLog.setAmount(BigDecimal.valueOf(config.getAnswerIntegral()).divide(BigDecimal.valueOf(1000)));
        redPacketLog.setRemark("点播答题领取积分转");
        redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
        redPacketLog.setPeriodId(param.getPeriodId());
        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
        return R.ok("积分奖励发放成功").put("rewardType", config.getRewardType());
    }

    @Override
    public List<FsUserCourseVideoPageListVO> pageListCourseVideo(UserCourseVideoPageParam param) {
        List<FsUserCourseVideoPageListVO> courseVideoPageListVOList = fsUserCourseVideoMapper.selectFsUserCourseVideoPageList(param);
        if (courseVideoPageListVOList.isEmpty()) {
            return new LinkedList<>();
        }

        List<SysDictData> courseProject = dictDataMapper.selectDictDataByType("sys_course_project");
        Map<String, String> projectMap = courseProject.stream()
                .collect(Collectors.toMap(SysDictData::getDictValue, SysDictData::getDictLabel));

        courseVideoPageListVOList.forEach(vo -> {
            if (vo.getProjectId() != null) {
                String projectName = projectMap.get(vo.getProjectId().toString());
                vo.setProjectName(projectName);
            }
        });

        return courseVideoPageListVOList;
    }

    @Override
    public ResponseResult<FsUserCourseVideoDetailsVO> getVideoDetails(Long videoId) {
        FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
        FsUserCourseVideoDetailsVO fsUserCourseVideoDetailsVO = new FsUserCourseVideoDetailsVO();
        BeanUtils.copyProperties(fsUserCourseVideo, fsUserCourseVideoDetailsVO);

        //从配置中读取默认线路
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        if (Integer.valueOf(1).equals(config.getDefaultLine())) {
            fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineTwo());
        } else {
            fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());
        }
        //这里 改成取线路一值，返回给前端。VideoUrl 是原视频（用来算流量的），不要去改，lineOne是转码后的视频
//        fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());


        // 获取课程相关的题库
        String questionBankId = fsUserCourseVideo.getQuestionBankId();
        List<FsUserVideoQuestionVO> questionList = Collections.emptyList();
        if (StringUtils.isNotEmpty(questionBankId)) {
            String[] questionBankIds = questionBankId.split(",");
            List<FsCourseQuestionBank> fsCourseQuestionBanks = courseQuestionBankMapper.selectFsCourseQuestionBankByIdVO(questionBankIds);
            questionList = fsCourseQuestionBanks.stream().map(v -> {
                FsUserVideoQuestionVO fsUserVideoQuestionVO = new FsUserVideoQuestionVO();
                BeanUtils.copyProperties(v, fsUserVideoQuestionVO);
                return fsUserVideoQuestionVO;
            }).collect(Collectors.toList());
        }

        fsUserCourseVideoDetailsVO.setQuestionBankList(questionList);
        return ResponseResult.ok(fsUserCourseVideoDetailsVO);
    }

    @Autowired
    SopUserLogsInfoMapper sopUserLogsInfoMapper;

    @Override
    public R registerCourse(FsUserCourseRegisterParam param) {


        FsUser fsUser = fsUserMapper.selectFsUserById(param.getUserId());
        if (fsUser == null) {
            return R.error("未登录成功");
        }
        logger.info("" + fsUser);
        if (fsUser.getIsAddQw() == 1) {
            return R.error("已经注册");
        }
        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(param.getQwExternalId());
        if (qwExternalContact == null) {
            return R.error("注册信息错误");
        }
        FsUser u = new FsUser();
        u.setUserId(param.getUserId());
        u.setIsAddQw(1);
        u.setQwExtId(param.getQwExternalId());
        fsUserMapper.updateFsUser(u);
        QwExternalContact qw = new QwExternalContact();
        qw.setFsUserId(fsUser.getUserId());
        qw.setId(qwExternalContact.getId());
        qwExternalContactMapper.updateById(qw);
        List<String> list = sopUserLogsInfoMapper.selectSopUserLogsInfoByExtId(qwExternalContact.getId());
        if (list != null && !list.isEmpty()) {
            sopUserLogsInfoMapper.updateSopUserLogsInfoFsUserIdById(list, param.getUserId());
        }
        return R.ok();
    }

    @Override
    public List<FsUserVideoListVO> getListCourseVideo(UserCourseVideoPageParam param) {
        List<FsUserCourseVideoPageListVO> list = fsUserCourseVideoMapper.selectFsUserCourseVideoPageList(param);
        return list.stream().map(v -> {
            FsUserVideoListVO videoListVO = new FsUserVideoListVO();
            BeanUtils.copyProperties(v, videoListVO);
            return videoListVO;
        }).collect(Collectors.toList());
    }

    @Override
    @Transactional
    public ResponseResult<FsUser> isAddCompanyUser(FsUserCourseAddCompanyUserParam param) {
        logger.info("\n 【进入个微-判断是否添加客服】，入参：{}", param);
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        boolean oneCompanyCourse = config.isOneCompanyCourse();
        //查询用户
        FsUser fsUser = fsUserMapper.selectFsUserById(param.getUserId());
        String dateString = "2026-02-11 20:00:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date targetDate = null;
        try {
            targetDate = sdf.parse(dateString);
        }catch (Exception e){
            e.printStackTrace();
        }
        if (fsUser == null  || fsUser.getCreateTime().before(targetDate) ) {
            return ResponseResult.fail(ExceptionCodeEnum.USER_NOT_FOUND.getCode(), ExceptionCodeEnum.USER_NOT_FOUND.getDescription());
        }
        if (fsUser.getStatus() == 0) {
            return ResponseResult.fail(ExceptionCodeEnum.MEMBER_DISABLED.getCode(), ExceptionCodeEnum.MEMBER_DISABLED.getDescription());
        }
        //公开课
        if (param.getIsOpenCourse() != null && param.getIsOpenCourse() == 1) {
            FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(), null);
            //添加判断：该用户是否已经存在此课程的看课记录，并且看课记录的销售id不是传入的销售id
            if (watchCourseVideo != null) {
                FsCourseWatchLog updateLog = new FsCourseWatchLog();
                updateLog.setLogId(watchCourseVideo.getLogId());
                updateLog.setAppId(param.getAppId());
                updateLog.setUpdateTime(new Date());
                courseWatchLogMapper.updateFsCourseWatchLog(updateLog);
            } else {
                FsCourseWatchLog fsCourseWatchLog = new FsCourseWatchLog();
                BeanUtils.copyProperties(param, fsCourseWatchLog);
                fsCourseWatchLog.setSendType(1);
                fsCourseWatchLog.setDuration(0L);
                fsCourseWatchLog.setCreateTime(new Date());
                fsCourseWatchLog.setLogType(1);
                fsCourseWatchLog.setAppId(param.getAppId());
                courseWatchLogMapper.insertFsCourseWatchLog(fsCourseWatchLog);
                String redisKey = H5WxUserWatchRedisUtil.heartbeatKey(param.getUserId(), param.getVideoId(), 0L);
                redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
                redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
                h5WxUserWatchRedisUtil.trackHeartbeat(redisKey);
            }
            return ResponseResult.ok(fsUser);
        }
        //判断该销售是否存在
        CompanyUser companyUser = companyUserMapper.selectCompanyUserById(param.getCompanyUserId());
        if (companyUser == null) {
            return ResponseResult.fail(ExceptionCodeEnum.SALES_NOT_FOUND.getCode(), ExceptionCodeEnum.SALES_NOT_FOUND.getDescription());
        }

        // 获取课程所属项目id
        FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
        Long courseProject = fsUserCourse.getProject();
        if (Objects.isNull(courseProject)) {
            return ResponseResult.fail(ExceptionCodeEnum.COURSE_CONFIG_ERROR.getCode(),  ExceptionCodeEnum.COURSE_CONFIG_ERROR.getFormattedDescription(param.getCourseId()));
        }
        FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
        FsUserCompanyUser userCompanyUser = userCompanyUserService.selectByUserIdAndProjectId(fsUser.getUserId(), courseProject);

        // 添加逻辑：如果存在fs_user表数据，但是又不存在fs_user_company_user表，则表示是以前企微看课的，需要手动绑定
        if (Objects.isNull(userCompanyUser)) {
            if (fsUser.getCompanyUserId() == null && fsUser.getIsAddQw() == 1) {
                userCompanyUser = userCompanyUserService.bindRelationship(fsUser.getUserId(), courseProject, param.getCompanyId(), param.getCompanyUserId(), 1);
            } else {
                Company company = companyService.selectCompanyById(param.getCompanyId());

                if((companyUser.getIsAllowedAllRegister() !=null && companyUser.getIsAllowedAllRegister() != 1)){
                    return ResponseResult.fail(504,"当前学习助手禁止绑定会员，请联系学习助手！");
                }
                // 使用 Stream API 检查是否包含 companyId
                // 修正类型转换问题
                if (companyUser.getIsNeedRegisterMember() == null || companyUser.getIsNeedRegisterMember() == 1
                        || (ObjectUtils.isNotEmpty(fsUserCoursePeriod)
                        && ObjectUtils.isNotEmpty(fsUserCoursePeriod.getIsNeedRegisterMember())
                        && Arrays.stream(fsUserCoursePeriod.getIsNeedRegisterMember().split(","))
                        .map(String::trim)
                        .anyMatch(id -> id.equals(String.valueOf(company.getCompanyId()))))) {
                    return ResponseResult.fail(504, "请联系学习助手发送邀请链接成为会员！");
                }
                int defaultStatus = (company != null ? company.getFsUserIsDefaultBlack() : 0) == 1 ? 0 : 1;
                userCompanyUser = userCompanyUserService.bindRelationship(param.getUserId(), courseProject, companyUser.getCompanyId(), companyUser.getUserId(), defaultStatus);
            }
        }

        // 逻辑调整：如果会员已经绑定了销售，直接提示，不添加重粉数据了-2025年6月16日14点53分
        if (!param.getCompanyUserId().equals(userCompanyUser.getCompanyUserId())) {
            log.error("进入：：isAddCompanyUser 该用户fsUser={},companyUser={},param={}", fsUser, companyUser, param);

            return ResponseResult.fail(500,"该用户("+fsUser.getUserId() + ")已成为其他学习助手会员");
        }

        // 如果开启了黑名单审核，需要提示
        if (userCompanyUser.getStatus() == 0) {
//            return ResponseResult.fail(505, "管理开启了会员审核，请等待审核");
            return ResponseResult.fail(BizResponseEnum.WAIT_APPROVAL, getCompanyUserQRCode(companyUser));
        }

        if (userCompanyUser.getStatus() == 2) {
            if ("福本源".equals(signProjectName)) {
                return ResponseResult.fail(ExceptionCodeEnum.SERVICE_UNAVAILABLE.getCode(), ExceptionCodeEnum.SERVICE_UNAVAILABLE.getDescription());
            } else {
                return ResponseResult.fail(ExceptionCodeEnum.USER_BLACKLISTED.getCode(), ExceptionCodeEnum.USER_BLACKLISTED.getDescription());
            }
        }

        //查询看课记录
//        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(), param.getPeriodId());

        // 检查是否为自由模式
        FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
        boolean isFreeMode = period != null && period.getFreeMode() != null && period.getFreeMode() == 1;

        // ========== 重要：所有限制检查必须在创建看课记录之前进行 ==========

        // 1. 检查是否已经存在看课记录，如果存在则更新，如果不存在则进行后续限制检查
        if(watchCourseVideo != null){
            // 存在看课记录，检查销售是否匹配
            if(!watchCourseVideo.getCompanyUserId().equals(param.getCompanyUserId())) {
                //提示
                log.error("数据库存在销售信息：{}，分享得销售信息：{}",watchCourseVideo.getCompanyUserId(),param.getCompanyUserId());
                return ResponseResult.fail(504, "已看过其他课程，不能重复观看");
            }

            // 更新现有看课记录
            FsCourseWatchLog updateLog = new FsCourseWatchLog();
            updateLog.setLogId(watchCourseVideo.getLogId());
            updateLog.setPeriodId(param.getPeriodId());
            updateLog.setProject(courseProject);
            updateLog.setUpdateTime(new Date());
            updateLog.setAppId(param.getAppId());
            courseWatchLogMapper.updateFsCourseWatchLog(updateLog);
        } else {
            // ========== 看课记录不存在，先进行各项限制检查，通过后才能创建看课记录 ==========

            // 2. 项目看课数量限制检查（自由模式下跳过此限制）
            // 注意：此限制仅针对手动发课(send_type=1)，企微自动发课(send_type=2)不受此限制
            // 逻辑：如果用户今天已经看过其他手动发课的课程，则不能再看新的手动发课课程
            if(!isFreeMode && !EXCLUDE_PROJECTS.contains(signProjectName) && !CloudHostUtils.hasCloudHostName("弘德堂")) {
                Integer logCount = fsUserCourseMapper.selectTodayCourseWatchLogCountByUserIdAndProjectId(param.getUserId(), courseProject);
                if (logCount > 0) {
                    // 根据项目ID查询项目名称
                    String projectName = "";
                    List<SysDictData> courseProjects = dictDataMapper.selectDictDataByType("sys_course_project");
                    for (SysDictData dictData : courseProjects) {
                        if (dictData.getDictValue().equals(String.valueOf(courseProject))) {
                            projectName = dictData.getDictLabel();
                            break;
                        }
                    }
                    log.info("【手动发课限制】用户今日已观看{}门手动发课课程，超过限制，periodId={}, userId={}, videoId={}",
                            logCount, param.getPeriodId(), param.getUserId(), param.getVideoId());
                    return ResponseResult.fail(504, String.format("您今日已观看%s项目的%d门课程", projectName, logCount));
                }
            } else if(isFreeMode) {
                log.info("【自由模式检查】✅ 自由模式，跳过项目看课数量限制检查，periodId={}", param.getPeriodId());
            }

            // 3. 观看时间限制检查
            if (!isUserCoursePeriodValid(param)) {
                log.info("【观看时间限制】用户不在观看时间范围内，periodId={}, userId={}, videoId={}",
                        param.getPeriodId(), param.getUserId(), param.getVideoId());
                return ResponseResult.fail(504, "不在观看时间范围内");
            }

            // ========== 所有检查通过，创建新的看课记录 ==========
            log.info("【创建看课记录】所有检查通过，创建新的看课记录，userId={}, videoId={}, periodId={}",
                    param.getUserId(), param.getVideoId(), param.getPeriodId());

            FsCourseWatchLog fsCourseWatchLog = new FsCourseWatchLog();
            BeanUtils.copyProperties(param, fsCourseWatchLog);
            fsCourseWatchLog.setSendType(1);  // 1=手动发课（个微/后台）
            fsCourseWatchLog.setDuration(0L);
            fsCourseWatchLog.setCreateTime(new Date());
            fsCourseWatchLog.setLogType(1);
            fsCourseWatchLog.setProject(courseProject);
            fsCourseWatchLog.setAppId(param.getAppId());
            courseWatchLogMapper.insertFsCourseWatchLog(fsCourseWatchLog);

            String redisKey = H5WxUserWatchRedisUtil.heartbeatKey(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
            redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
            redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
            h5WxUserWatchRedisUtil.trackHeartbeat(redisKey);
        }

        // 添加会员销售关系表数据
        // 逻辑调整：如果会员已经绑定了销售，直接提示，不添加重粉数据了-2025年6月16日14点58分
//        FsUserCompanyUser fsUserCompanyUser = getFsUserCompanyUser(param, fsUser);
//        QueryWrapper<FsUserCompanyUser> queryWrapper = new QueryWrapper<FsUserCompanyUser>().eq("user_id", param.getUserId()).eq("company_user_id", param.getCompanyUserId());
//        Integer i = fsUserCompanyUserMapper.selectCount(queryWrapper);
//        if(i == 0) {
//            fsUserCompanyUserMapper.insertFsUserCompanyUser(fsUserCompanyUser);
//        }

//        // 如果重粉需要打上重粉标签
//        if(1 == fsUserCompanyUser.getIsRepeatFans()){
//            FsUserCourseBeMemberParam fsUserCourseBeMemberParam = new FsUserCourseBeMemberParam();
//            fsUserCourseBeMemberParam.setUserId(param.getUserId());
//            fsUserCourseBeMemberParam.setCompanyId(param.getCompanyId());
//            fsUserCourseBeMemberParam.setCompanyUserId(param.getCompanyUserId());
//            fsUserService.setRepeatFansTag(fsUserCourseBeMemberParam);
//        }
        return ResponseResult.ok(fsUser);
    }

    /**
     * 检查用户课程时间段是否有效
     *
     * @param param 查询参数
     * @return 如果当前时间在有效范围内且状态为1，返回true，否则返回false
     */
    public boolean isUserCoursePeriodValid(FsUserCourseAddCompanyUserParam param) {
        // 查询课程周期信息
        FsUserCoursePeriodDays periodDays = getPeriodDaysInfo(param);

        // 检查是否开启自由模式
        FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
        if (period != null && period.getFreeMode() != null && period.getFreeMode() == 1) {
            // 自由模式：不检查时间和状态，直接返回有效
            return true;
        }

        // 固定模式：检查时间范围和状态
        // 获取公司用户时间段信息
        LocalDateTime[] companyUserTimeRange = getCompanyUserTimeRange(param);
        LocalDateTime companyUserStartDateTime = companyUserTimeRange[0];
        LocalDateTime companyUserEndDateTime = companyUserTimeRange[1];

        // 确定实际使用的开始和结束时间
        LocalDateTime effectiveStartTime = companyUserStartDateTime != null ?
                companyUserStartDateTime : periodDays.getStartDateTime();
        LocalDateTime effectiveEndTime = companyUserEndDateTime != null ?
                companyUserEndDateTime : periodDays.getEndDateTime();
        // 检查时间范围和状态
        log.error("传入参数：开始时间：{}，结束时间：{}，periodDays={}", effectiveStartTime, effectiveEndTime, periodDays);
        if (ObjectUtils.isEmpty(effectiveStartTime) || ObjectUtils.isEmpty(effectiveEndTime)) {
            log.error("请检查营期时间！");
            return false;
        }
        return DateUtil.isWithinRangeSafe(LocalDateTime.now(), effectiveStartTime, effectiveEndTime) && periodDays.getStatus() == 1;
    }

    // 其他辅助方法保持不变
    private FsUserCoursePeriodDays getPeriodDaysInfo(FsUserCourseAddCompanyUserParam param) {
        FsUserCoursePeriodDays query = new FsUserCoursePeriodDays();
        query.setVideoId(param.getVideoId());
        query.setPeriodId(param.getPeriodId());

        List<FsUserCoursePeriodDays> periodDaysList = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(query);
        return CollectionUtils.isNotEmpty(periodDaysList) ? periodDaysList.get(0) : query;
    }

    private LocalDateTime[] getCompanyUserTimeRange(FsUserCourseAddCompanyUserParam param) {
        CompanyUserTimeQueryParam query = new CompanyUserTimeQueryParam();
        query.setPeriodId(param.getPeriodId());
        query.setCourseId(param.getCourseId());
        query.setVideoId(param.getVideoId());
        query.setCompanyUserId(param.getCompanyUserId());

        List<FsUserCourseCompanyUserTime> companyUserTimes =
                companyUserTimeMapper.batchSelectByParams(Collections.singletonList(query));

        if (CollectionUtils.isEmpty(companyUserTimes)) {
            return new LocalDateTime[]{null, null};
        }

        FsUserCourseCompanyUserTime companyUserTime = companyUserTimes.get(0);
        return new LocalDateTime[]{
                convertDateToLocalDateTime(companyUserTime.getStartDateTime()),
                convertDateToLocalDateTime(companyUserTime.getEndDateTime())
        };
    }

    private LocalDateTime convertDateToLocalDateTime(Date date) {
        if (date == null) {
            return null;
        }
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }

    // 添加关系表数据
    public static FsUserCompanyUser getFsUserCompanyUser(FsUserCourseAddCompanyUserParam param, FsUser fsUser) {
        FsUserCompanyUser fsUserCompanyUser = new FsUserCompanyUser();
        // 判断是否绑定了销售，如果已绑定，则需要标识为重粉，且放黑名单
        if (fsUser.getCompanyUserId() != null && !fsUser.getCompanyUserId().equals(param.getCompanyUserId())) {
            fsUserCompanyUser.setIsRepeatFans(1);
        } else {
            fsUserCompanyUser.setIsRepeatFans(0);
        }
        fsUserCompanyUser.setUserId(param.getUserId());
        fsUserCompanyUser.setCompanyId(param.getCompanyId());
        fsUserCompanyUser.setCompanyUserId(param.getCompanyUserId());
        return fsUserCompanyUser;
    }

    private String getCompanyUserQRCode(CompanyUser companyUser) {
        String companyUserQRCode;
        if (StringUtils.isNotEmpty(companyUser.getQrCodeWeixin())) {
            companyUserQRCode = companyUser.getQrCodeWeixin();
        } else {
            String json = configService.selectConfigByKey("course.config");
            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
            companyUserQRCode = config.getCompanyUserQRCode();
        }
        return companyUserQRCode;
    }

    @Override
    public ResponseResult<FsUserCourseVideoLinkDetailsVO> getLinkCourseVideoDetails(FsUserCourseVideoLinkParam param) {
//        FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
//        periodDays.setVideoId(param.getVideoId());
//        periodDays.setPeriodId(param.getPeriodId());
//        //正常情况是只能查询到一条，之前可能存在重复的脏数据，暂使用查询list的方式
//        List<FsUserCoursePeriodDays> fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(periodDays);
//        if(fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()){
//            periodDays = fsUserCoursePeriodDays.get(0);
//        }
//        if(periodDays.getStatus() != 1){
//            return ResponseResult.fail(403, "当前课程未开始或已结束，暂不能看课");
//        }

        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

        // 1、获取视频详情、问题详情
        ResponseResult<FsUserCourseVideoDetailsVO> videoDetails = this.getVideoDetails(param.getVideoId());
        FsUserCourseVideoDetailsVO courseVideoDetails = videoDetails.getData() != null ? videoDetails.getData() : null;

        //课程logo
        if (param.getPeriodId() != null) {
            FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
            if (fsUserCoursePeriod != null) {
                config.setCourseLogo(fsUserCoursePeriod.getCourseLogo());
            }
        }

        long duration = 0L;
        long tipsTime = 0L;
        long tipsTime2 = 0L;
        int isFinish = 0;
        FsUserCourseVideoLinkDetailsVO vo = new FsUserCourseVideoLinkDetailsVO();
        vo.setCourseVideoDetails(courseVideoDetails);
        vo.setCourseConfig(config);
        vo.setIsFinish(isFinish);
        vo.setPlayDuration(duration);

        //2、判断链接类型
        if (param.getLinkType() != null && param.getLinkType() == 1) {
            ResponseResult.ok(vo);
        }
        // 从Redis中获取用户目前的观看时长
        String redisKey = "h5wxuser:watch:duration:" + param.getFsUserId() + ":" + param.getVideoId() + ":" + param.getCompanyUserId();
        String durationCurrent = redisCache.getCacheObject(redisKey);

        //3、获取看课记录
        FsCourseWatchLog watchLog = courseWatchLogMapper.getWatchLogByFsUser(param.getVideoId(), param.getFsUserId(), param.getCompanyUserId());
        if (durationCurrent != null) {
            duration = Long.parseLong(durationCurrent);
        } else {
            duration = Objects.isNull(watchLog) ? 0 : watchLog.getDuration();
        }
        int isRedPackage = 0;
        CompanyUser companyUser = companyUserMapper.selectCompanyUserByCompanyUserId(param.getCompanyUserId());
        // 查询视频是否设置了红包，没有就不提示  //根据公司ID 视频ID 营期ID  默认没有红包  手动看课视频
        List<FsUserCourseVideoRedPackage> fsUserCourseVideoRedPackageList = fsUserCourseVideoRedPackageMapper.selectRedPacketList(param.getVideoId(), companyUser == null || companyUser.getCompanyId() == null  ? null : companyUser.getCompanyId(), param.getPeriodId());
        if (fsUserCourseVideoRedPackageList.size() != 1) {
            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
            if (video != null && video.getRedPacketMoney() != null && BigDecimal.ZERO.compareTo(video.getRedPacketMoney()) < 0) {
                isRedPackage = 1;
            } else {
                isRedPackage = 0;
            }
        } else {
            if (fsUserCourseVideoRedPackageList.get(0).getRedPacketMoney() != null && BigDecimal.ZERO.compareTo(fsUserCourseVideoRedPackageList.get(0).getRedPacketMoney()) != 0) {
                isRedPackage = 1;
            }
        }
        vo.setIsRedPackage(isRedPackage);

        // 2025-11-16 鹤颜堂 xgb 添加配置控制休息提示是否打开要暂停 默认打开 0-关闭 1-打开
        if(isOpenRestReminder(config,watchLog)){
            if (courseVideoDetails != null && courseVideoDetails.getDuration() != null) {
                // 查询视频是否设置了红包，没有就不提示
                Integer fsUserCourseVideoRedPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyCount(param.getVideoId(), null, param.getPeriodId());
                if (fsUserCourseVideoRedPackage > 0) {
                    // tipsTime = courseVideoDetails.getDuration() / 3;
                    tipsTime2 = (courseVideoDetails.getDuration() * 2) / 3;
                }
            }
        }


        vo.setTipsTime(tipsTime);
        vo.setTipsTime2(tipsTime2);
        //判断是否完课
        if (watchLog != null && watchLog.getLogType() == 2) {
            isFinish = 1;
        }

        //将视频时长也存到redis
        String videoRedisKey = "h5wxuser:video:duration:" + param.getVideoId();

        Long videoDuration = 0L;
        try {
            videoDuration = redisCache.getCacheObject(videoRedisKey);
        } catch (Exception e) {
            String string = redisCache.getCacheObject(videoRedisKey);
            videoDuration = Long.parseLong(string);
            log.error("key中id为String:{}", videoDuration);
        }

        if (videoDuration == null) {
            redisCache.setCacheObject(videoRedisKey, courseVideoDetails != null ? courseVideoDetails.getDuration() != null ? courseVideoDetails.getDuration() : 0 : 0);
        }
        vo.setIsFinish(isFinish);
        vo.setPlayDuration(duration);
        //判断营期的课程状态是否是进行中
        if (param.getId() != null) {

            FsUserCoursePeriodDays days = fsUserCoursePeriodDaysMapper.selectById(param.getId());

            // 查询销售设置的看课时间
            LocalDateTime companyUserStartDateTime = null;
            LocalDateTime companyUserEndDateTime = null;

            List<CompanyUserTimeQueryParam> queryList = new ArrayList<>();
            CompanyUserTimeQueryParam query = new CompanyUserTimeQueryParam();
            query.setPeriodId(param.getPeriodId());
            query.setCourseId(param.getCourseId());
            query.setVideoId(param.getVideoId());
            query.setCompanyUserId(param.getCompanyUserId());
            queryList.add(query);
            List<FsUserCourseCompanyUserTime> fsUserCourseCompanyUserTimes = companyUserTimeMapper.batchSelectByParams(queryList);

            if (CollectionUtils.isNotEmpty(fsUserCourseCompanyUserTimes)) {
                FsUserCourseCompanyUserTime fsUserCourseCompanyUserTime = fsUserCourseCompanyUserTimes.get(0);
                Date cuStartDateTime = fsUserCourseCompanyUserTime.getStartDateTime();
                Date cuEndDateTime = fsUserCourseCompanyUserTime.getEndDateTime();

                if (cuStartDateTime != null) {
                    Instant instant = cuStartDateTime.toInstant();
                    ZoneId zoneId = ZoneId.systemDefault();
                    companyUserStartDateTime = instant.atZone(zoneId).toLocalDateTime();
                }

                if (cuEndDateTime != null) {
                    Instant instant = cuEndDateTime.toInstant();
                    ZoneId zoneId = ZoneId.systemDefault();
                    companyUserEndDateTime = instant.atZone(zoneId).toLocalDateTime();
                }
            }
            vo.setStartDateTime(companyUserStartDateTime != null ? companyUserStartDateTime : days.getStartDateTime());
            vo.setEndDateTime(companyUserEndDateTime != null ? companyUserEndDateTime : days.getEndDateTime());

            // 检查是否开启自由模式
            FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(param.getPeriodId());
            boolean canWatch;

            if (period != null && period.getFreeMode() != null && period.getFreeMode() == 1) {
                // 自由模式：不检查时间和状态，直接允许观看
                canWatch = true;
            } else {
                // 固定模式：检查时间范围和状态
                canWatch = DateUtil.isWithinRangeSafe(LocalDateTime.now(),
                        companyUserStartDateTime != null ? companyUserStartDateTime : days.getStartDateTime(),
                        companyUserEndDateTime != null ? companyUserEndDateTime : days.getEndDateTime())
                        && days.getStatus() == 1;
            }

            vo.setRang(canWatch);
        }
        return ResponseResult.ok(vo);
    }

    /**
     * @Description: 是否看课中断
     * @Param:
     * @Return:
     * @Author xgb 默认 打开看课中断
     * @Date 2025/12/18 17:33
     */

    private boolean isOpenRestReminder(CourseConfig config ,FsCourseWatchLog watchLog){
        // 查询公司看课中断是否打开
        boolean result= true;

        if (config.getIsOpenRestReminder() != null && config.getIsOpenRestReminder() == 0) {
            result=false;
        }

        if(watchLog!=null){
            if(watchLog.getCompanyId()!=null){
                Company company=companyMapper.selectCompanyById(watchLog.getCompanyId());
                if(company!=null && company.getIsOpenRestReminder()!=null){
                    if(company.getIsOpenRestReminder()==0){
                        result=false;
                    }else {
                        result=true;
                    }
                }
            }

            if(watchLog.getPeriodId()!=null){
                FsUserCoursePeriod period= fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(watchLog.getPeriodId());
                if(period!=null && watchLog.getCompanyId()!=null && StringUtils.isNotBlank(period.getIsOpenRestFlag()) ){
                    //  Json 字符串  key值公司id value值  0-关闭 1-打开 没有获取到不管
                    JSONObject jsonObject= JSON.parseObject(period.getIsOpenRestFlag());
                    Integer flag=(Integer) jsonObject.get(watchLog.getCompanyId().toString());
                    if(flag==0){
                        result=false;
                    }else {
                        result=true;
                    }

                }
            }
        }

        return result;
    }

    @Override
    public R addWatchLogByLink(FsUserCourseAddCompanyUserParam param) {

        return null;
    }

    @Override
    public R updateWatchDurationWx(FsUserCourseVideoUParam param) {
        //临时短链不做记录
        if (param.getLinkType() != null && param.getLinkType() == 1) {
            return R.ok();
        }
        if (param.getCompanyUserId() == null) {
            param.setCompanyUserId(0L);
        }
        Long videoDuration = 0L;
        try {
            videoDuration = getAutoLookVideoDuration(param.getVideoId());
        } catch (Exception e) {
            log.error("视频时长识别错误:{}", param);
            return R.error();
        }
        // 判断param.getDuration()是否大于videoDuration（允许10的误差区间）
        if (param.getDuration() > videoDuration + 10) {
            return R.error();
        }
        // 从Redis中获取观看时长
        String redisKey = H5WxUserWatchRedisUtil.durationKey(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
//        log.info("看课redis缓存key：{}", redisKey);
        try {
            String durationStr = redisCache.getCacheObject(redisKey);
//            log.info("看课记录：{}", durationStr);
            long duration = durationStr != null ? Long.parseLong(durationStr) : 0L;

            // 更新Redis中的观看时长
            if (param.getDuration() != null && param.getDuration() > duration) {
                //24小时过期
                redisCache.setCacheObject(redisKey, param.getDuration().toString(), 2, TimeUnit.HOURS);
                h5WxUserWatchRedisUtil.trackDuration(redisKey);
            }

            //更新缓存中的心跳时间
            updateHeartbeatWx(param);
            // 已达完课阈值时立即写库，避免仅依赖定时任务产生十几秒延迟
            if (param.getDuration() != null) {
                courseWatchLogService.syncH5WxUserWatchProgressOnFinish(
                        param.getUserId(), param.getVideoId(), param.getCompanyUserId(), param.getDuration());
            }
            return R.ok();
        } catch (Exception e) {
            logger.error("更新看课时长失败：{}", redisKey, e.getMessage());
            return R.error("更新看课时长失败");
        }
    }

    @Override
    public void updates(CourseVideoUpdates vo) {
        fsUserCourseVideoMapper.updates(vo);
    }

    @Override
    public void batchSaveVideo(BatchVideoSvae vo) {
        List<FsVideoResource> videoResourceList = fsVideoResourceMapper.selectBatchIds(vo.getIds());
        videoResourceList = videoResourceList.stream()
                .sorted(Comparator.comparing(FsVideoResource::getSort).thenComparing(FsVideoResource::getId))
                .collect(Collectors.toList());
        FsUserCourseVideo queryParam = new FsUserCourseVideo();
        queryParam.setCourseId(vo.getCourseId());
        List<FsUserCourseVideo> videoList = selectFsUserCourseVideoList(queryParam);
        AtomicLong i = new AtomicLong(videoList.size() + 1);
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

        List<FsUserCourseVideo> collect = videoResourceList.stream().map(e -> {
            FsUserCourseVideo entity = new FsUserCourseVideo();
            entity.setUserId(vo.getUserId());
            entity.setTitle(e.getResourceName());
            entity.setVideoUrl(e.getVideoUrl());
            entity.setThumbnail(e.getThumbnail());
            entity.setDuration(e.getDuration().longValue());
            entity.setCourseId(vo.getCourseId());
            entity.setStatus(3L);
            entity.setCourseSort(i.getAndIncrement());
            entity.setFileName(e.getFileName());
            entity.setQuestionBankId(e.getProjectIds());
            entity.setLineOne(e.getLine1());
            entity.setLineTwo(e.getLine2());
            entity.setLineThree(e.getLine3());
            entity.setRedPacketMoney(config.getRedPackageMoney());
            entity.setFileSize(e.getFileSize());
            entity.setFileKey(e.getFileKey());
            entity.setIsTranscode(0);
            entity.setProjectId(vo.getProjectId());
            return entity;
        }).collect(Collectors.toList());
        fsUserCourseVideoMapper.insertBatchFsUserCourseVideo(collect);

        //通过课程ID：查询使用课程id创建的营期来进进行关联(批量新增视频时关联到对应通过课程ID创建的营期)
        FsUserCoursePeriod coursePeriod = new FsUserCoursePeriod();
        coursePeriod.setCourseId(vo.getCourseId());
        List<FsUserCoursePeriod> coursePeriodsList = fsUserCoursePeriodService.selectFsUserCoursePeriodList(coursePeriod);//获取当前课程创建的会员营期
        //  处理对应课程安排
        if (!coursePeriodsList.isEmpty()) {
            LocalTime startTime = LocalTime.MIDNIGHT;
            LocalTime endTime = LocalTime.of(23, 59, 59);
            //获取营期数据
            coursePeriodsList.stream().filter(e -> e.getPeriodStatus() != 3).forEach(period -> {
                // 修复：改用批量添加方法，避免课次号倒序问题
                // 提取新增视频的ID列表
                List<Long> newVideoIds = collect.stream()
                        .map(FsUserCourseVideo::getVideoId)
                        .collect(Collectors.toList());

                // 构建批量添加参数
                BatchAddCourseSectionParam batchParam = new BatchAddCourseSectionParam();
                batchParam.setPeriodId(period.getPeriodId());
                batchParam.setCourseId(period.getCourseId());
                batchParam.setVideoIds(newVideoIds);
                batchParam.setStartTime(startTime);
                batchParam.setEndDateTime(endTime);
                batchParam.setJoinTime(endTime);
                batchParam.setAutoSort(true); // 自动按courseSort排序
                batchParam.setSkipDuplicate(true); // 跳过重复视频

                // 调用批量添加方法
                fsUserCoursePeriodDaysService.batchAddCourseSections(batchParam);
            });
        }
    }

    @Override
    public void batchUpdateRed(List<BatchRedUpdate> list) {
        list.forEach(e -> {
            fsUserCourseVideoMapper.updateRedPacketMoney(e.getVideoId(), e.getRedPacketMoney());
        });
    }
    @Override
    public void batchUpdateTitle(List<BatchTitleUpdate> list) {
        list.forEach(e -> {
            fsUserCourseVideoMapper.batchUpdateTitle(e.getVideoId(), e.getTitle());
        });
    }

    /**
     * 获取选项列表
     *
     * @param params 参数
     * @return list
     */
    @Override
    public List<OptionsVO> selectVideoListByMap(Map<String, Object> params) {
        return fsUserCourseVideoMapper.selectVideoListByMap(params);
    }

    @Override
    public List<FsCourseVideoListBySidebarVO> getFsCourseVideoListBySidebar(FsCourseListBySidebarParam param) {
        return fsUserCourseVideoMapper.getFsCourseVideoListBySidebar(param);
    }

    /**
     * 只查询用户当天课程章节数据
     *
     * @param param
     * @return
     */
    @Override
    public List<FsCourseVideoListBySidebarVO> getFsCourseVideoListBySidebarToday(FsCourseListBySidebarParam param) {

        //得到当前能看的videoIds
        List<SidebarTodayCourseVO> list = fsUserCourseMapper.getFsCourseListBySidebarTodayList(param.getExternalUserId());
        List<Long> videoIds = list.stream().map(SidebarTodayCourseVO::getVideoId).collect(Collectors.toList());
        param.setVideoIds(videoIds);
        List<FsCourseVideoListBySidebarVO> fsCourseVideoListBySidebarFilter = fsUserCourseVideoMapper.getFsCourseVideoListBySidebarFilter(param);
        return fsCourseVideoListBySidebarFilter;
    }

    @Override
    public R createMiniLink(FsCourseLinkMiniParam param) {


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

        QwUser qwUser = qwExternalContactService.getQwUserByRedis(param.getCorpId().trim(), param.getQwUserId().trim());

        if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
        }

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

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

        QwExternalContact externalContact = resolveExternalContactForQwUser(param.getExternalUserId(), qwUser, param.getFsUserId());
        if (externalContact == null) {
            return R.error("客户与当前销售关系未建立，请确认客户已添加后再发课");
        }
        Long externalContactId = externalContact.getId();

        //看课记录
        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, externalContactId,2);

        //生成小程序链接
        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, externalContactId, 2, null, 0);

        JSONObject news = new JSONObject(true);
        news.put("miniprogramAppid", qwCompany.getMiniAppId());
        news.put("miniprogramTitle", param.getTitle());
        news.put("miniprogramPicUrl", config.getSidebarImageUrl());
        news.put("miniprogramPage", linkByMiniApp);

        return R.ok().put("data", news);

    }

    @Override
    public R createCartLink(FsCourseLinkMiniParam param) {

        QwUser qwUser = qwExternalContactService.getQwUserByRedis(param.getCorpId().trim(), param.getQwUserId().trim());

        if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
        }

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

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

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

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

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

        QwExternalContact externalContact = resolveExternalContactForQwUser(param.getExternalUserId(), qwUser, param.getFsUserId());
        if (externalContact == null) {
            return R.error("客户与当前销售关系未建立，请确认客户已添加后再发课");
        }
        Long externalContactId = externalContact.getId();

        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, externalContactId,2);

        String linkByCartLink = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, externalContactId, 1, domainName, 0);


        //生成卡片链接
        JSONObject news = new JSONObject(true); // true 表示保持字段顺序
        news.put("linkTitle", param.getTitle());
        news.put("linkDescribe", param.getTitle());
        news.put("linkImageUrl", config.getSidebarImageUrl());
        news.put("linkUrl", linkByCartLink);

        return R.ok().put("data", news);

    }

    @Override
    public R createCartLinkByFeiShu(FsCourseLinkMiniParam param) {

        QwUser qwUser = qwExternalContactService.getQwUserByRedis(param.getCorpId().trim(), param.getQwUserId().trim());

        if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
        }

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

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

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

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

        Long courseId = param.getCourseId();
        Long qwUserId = qwUser.getId();
        Date createTime = DateUtils.getNowDate();
        Long videoId = param.getVideoId();
        Long companyId = qwUser.getCompanyId();
        Long companyUserId = qwUser.getCompanyUserId();

        QwExternalContact externalContact = resolveExternalContactForQwUser(param.getExternalUserId(), qwUser, param.getFsUserId());
        if (externalContact == null) {
            return R.error("客户与当前销售关系未建立，请确认客户已添加后再发课");
        }
        Long externalUserId = externalContact.getId();

        // 飞书发课：写入/更新看课记录；已存在则把 watch_type 改为飞书(3)
        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, externalUserId, 3);

        Map<String, String> feishuH5Link = createFeishuH5Link(param.getCorpId(), createTime, courseId.intValue(), videoId.intValue(), String.valueOf(qwUserId), companyUserId, companyId, externalUserId, config, param.getFeishuAccountId(), param.getFeishuNeedAuth());
        String shortCode = feishuH5Link.get("link");

        String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyId, courseId, companyUserId, shortCode,
                param.getFeishuAccountId(), param.getFeishuNeedAuth(), null, param.getFsUserId());

        JSONObject news = new JSONObject(true);
        news.put("linkTitle", param.getTitle());
        news.put("linkDescribe", param.getTitle());
        news.put("linkImageUrl", config.getSidebarImageUrl());
        news.put("linkUrl", feishuLink);

        return R.ok().put("data", news);

    }

    //插入观看记录
    private void addWatchLogIfNeeded(Long videoId, Long courseId,
                                     Long fsUserId, QwUser qwUser, Long externalId,Integer watchType) {

        try {
            QwExternalContact resolvedContact = resolveExternalContactForQwUser(externalId, qwUser, fsUserId);
            if (resolvedContact == null) {
                logger.error("插入观看记录失败，未找到当前销售对应的外部联系人: externalId={}, qwUserId={}, fsUserId={}",
                        externalId, qwUser != null ? qwUser.getId() : null, fsUserId);
                return;
            }
            externalId = resolvedContact.getId();

            FsCourseWatchLog watchLog = new FsCourseWatchLog();
            watchLog.setVideoId(videoId);
            watchLog.setQwExternalContactId(externalId);
            watchLog.setSendType(2);
            watchLog.setQwUserId(qwUser.getId());
            watchLog.setDuration(0L);
            watchLog.setCourseId(courseId);
            watchLog.setCompanyUserId(qwUser.getCompanyUserId());
            watchLog.setCompanyId(qwUser.getCompanyId());
            watchLog.setCreateTime(new Date());
            watchLog.setUpdateTime(new Date());
            watchLog.setLogType(3);
            watchLog.setWatchType(watchType);
            if (fsUserId == null) {
                fsUserId = 0L;
            }

            watchLog.setUserId(fsUserId);

            // 企微看课补齐 project，避免按项目完播统计丢失
            fsCourseWatchLogProjectFiller.fillIfAbsent(watchLog);
            //存看课记录
            courseWatchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);

        } catch (Exception e) {
            logger.error("一键群发失败-插入观看记录失败：" + e.getMessage());
        }


    }

    private String createLinkByMiniApp(Date sendTime, Long courseId, Long videoId,
                                       QwUser qwUser, Long externalId, int type, String domainName, Integer isRoom) {

        FsCourseLink link = new FsCourseLink();
        link.setCompanyId(qwUser.getCompanyId());
        link.setQwUserId(qwUser.getId());
        link.setCompanyUserId(qwUser.getCompanyUserId());
        link.setVideoId(videoId);
        link.setCorpId(qwUser.getCorpId());
        link.setCourseId(courseId);
        link.setQwExternalId(externalId);
        link.setIsRoom(isRoom);
        if (type == 1) {
            link.setLinkType(0);
        } else {
            link.setLinkType(3);
        }
        String randomString = generateRandomStringWithLock();
        if (StringUtil.strIsNullOrEmpty(randomString)) {
            link.setLink(UUID.randomUUID().toString().replace("-", ""));
        } else {
            link.setLink(randomString);
        }

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

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

        String courseJson = JSON.toJSONString(courseMap);

        String realLinkFull = null;

        if (type == 1) {
            realLinkFull = REAL_LINK_PREFIX + courseJson;
        } else {
            realLinkFull = miniappRealLink + courseJson;
        }

        link.setRealLink(realLinkFull);


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

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

        if (type == 1) {
            return domainName + SHORT_LINK_PREFIX + link.getLink();
        } else {
            return link.getRealLink();
        }

    }

    //会员-更新心跳时间
    public void updateHeartbeatWx(FsUserCourseVideoUParam param) {
        Long companyUserId = param.getCompanyUserId() == null ? 0L : param.getCompanyUserId();
        String redisKey = H5WxUserWatchRedisUtil.heartbeatKey(param.getUserId(), param.getVideoId(), companyUserId);
        redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
        redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
        h5WxUserWatchRedisUtil.trackHeartbeat(redisKey);
    }


    @Override
    public R isAllowAnswer(FsUserCourseVideoFinishUParam param) {
        try {
            // 参数校验
            if (param.getVideoId() == null || param.getDuration() == null) {
                return R.error("参数不完整，缺少videoId或duration");
            }

            // 获取视频时长（优先从Redis缓存获取）
            Long videoDuration = getVideoDuration(param.getVideoId());

            // 校验总时长
            return checkTotalDuration(param.getDuration(), videoDuration);
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        } catch (Exception e) {
            log.error("检查总时长异常, 参数: {}", param, e);
            return R.error("系统异常，请稍后重试");
        }
    }

    @Override
    public List<FsUserCourseVideoPageListVO> selectCourseVideoListByMap(Map<String, Object> params) {
        List<FsUserCourseVideoPageListVO> vos = fsUserCourseVideoMapper.selectFsUserCourseVideoListByMap(params);
        // 项目
        List<SysDictData> courseProject = dictDataMapper.selectDictDataByType("sys_course_project");
        Map<String, String> projectMap = courseProject.stream()
                .collect(Collectors.toMap(SysDictData::getDictValue, SysDictData::getDictLabel));
        vos.forEach(vo -> {
            if (vo.getProjectId() != null) {
                String projectName = projectMap.get(vo.getProjectId().toString());
                if (projectName != null) {
                    vo.setProjectName(projectName);
                }
            }
        });

        return vos;
    }

    /**
     * 生成唯一键
     */
    private String getKey(Long periodId, Long courseId, Long videoId, Long companyUserId) {
        return String.format("%d_%d_%d_%d", periodId, courseId, videoId, companyUserId);
    }

    @Override
    public ResponseResult<Boolean> setWatchCourseTime(List<FsWatchCourseTimeParam> paramList) {
        if (CollectionUtils.isEmpty(paramList)) {
            return ResponseResult.fail(400, "参数不能为空");
        }

        // 1. 查询参数
        List<CompanyUserTimeQueryParam> queryList = paramList.stream()
                .map(param -> {
                    CompanyUserTimeQueryParam query = new CompanyUserTimeQueryParam();
                    query.setPeriodId(param.getPeriodId());
                    query.setCourseId(param.getCourseId());
                    query.setVideoId(param.getVideoId());
                    query.setCompanyUserId(param.getCompanyUserId());
                    return query;
                })
                .collect(Collectors.toList());

        // 2. 批量查询现有的记录
        List<FsUserCourseCompanyUserTime> existingRecords = companyUserTimeMapper.batchSelectByParams(queryList);
        Map<String, FsUserCourseCompanyUserTime> existingMap = existingRecords.stream()
                .collect(Collectors.toMap(record -> getKey(record.getPeriodId(), record.getCourseId(), record.getVideoId(), record.getCompanyUserId()),
                        record -> record));

        // 3. 分离需要更新和插入的记录
        List<FsUserCourseCompanyUserTime> toUpdate = new ArrayList<>();
        List<FsUserCourseCompanyUserTime> toInsert = new ArrayList<>();
        Date now = DateUtils.getNowDate();

        for (FsWatchCourseTimeParam param : paramList) {
            String key = getKey(param.getPeriodId(), param.getCourseId(), param.getVideoId(), param.getCompanyUserId());
            FsUserCourseCompanyUserTime record = existingMap.get(key);

            if (record != null) {
                // 需要更新
                record.setStartDateTime(param.getStartDateTime());
                record.setEndDateTime(param.getEndDateTime());
                record.setUpdateTime(now);
                toUpdate.add(record);
            } else {
                // 需要插入
                FsUserCourseCompanyUserTime newRecord = new FsUserCourseCompanyUserTime();
                newRecord.setPeriodId(param.getPeriodId());
                newRecord.setCourseId(param.getCourseId());
                newRecord.setVideoId(param.getVideoId());
                newRecord.setCompanyUserId(param.getCompanyUserId());
                newRecord.setCompanyId(param.getCompanyId());
                newRecord.setStartDateTime(param.getStartDateTime());
                newRecord.setEndDateTime(param.getEndDateTime());
                newRecord.setCreateTime(now);
                toInsert.add(newRecord);
            }
        }

        // 4. 执行批量更新和插入
        if (!toUpdate.isEmpty()) {
            companyUserTimeMapper.batchUpdate(toUpdate);
        }
        if (!toInsert.isEmpty()) {
            companyUserTimeMapper.batchInsert(toInsert);
        }

        return ResponseResult.ok();
    }

    @Override
    public R createRoomMiniLink(FsCourseLinkMiniParam param) {

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

        QwUser qwUser = qwExternalContactService.getQwUserByRedis(param.getCorpId().trim(), param.getQwUserId().trim());

        if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
        }

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

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

        //生成小程序链接
        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(), 2, null, 1);

        JSONObject news = new JSONObject(true);
        news.put("miniprogramAppid", qwCompany.getMiniAppId());
        news.put("miniprogramTitle", param.getTitle());
        news.put("miniprogramPicUrl", config.getSidebarImageUrl());
        news.put("miniprogramPage", linkByMiniApp);

        return R.ok().put("data", news);
    }

    @Override
    public FsUserCourseVideo selectByFileKey(String fileKey) {
        FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectByFileKey(fileKey);
        return fsUserCourseVideo;
    }

    @Override
    public String selectFsUserCourseVideoByVideoForTitle(Long videoId) {
        return fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoForTitle(videoId);
    }

    @Override
    public R checkUserInfo(long userId) {
        FsUser user = fsUserService.selectFsUserById(userId);
        if (user == null) {
            return R.error(404, "用户不存在！");
        }
        //读取配置给前端返回
        String json = sysConfigService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        if (StringUtils.isEmpty(user.getNickName())) {
            Map<String, Object> map = new HashMap<>();
            map.put("authType", config.getMiniAppAuthType());
            if (config.getMiniAppAuthType() == 2) {
                map.put("domain", config.getUserCourseAuthDomain());
            }
            map.put("code", 500);
            return R.ok(map);
        }
        return R.ok().put("user", user);
    }

    /**
     * 获取视频时长（优先从Redis获取，不存在则查数据库）
     */
    private Long getVideoDuration(Long videoId) {
        String redisKey = "h5user:video:duration:" + videoId;
        Long duration = redisCache.getCacheObject(redisKey);

        if (duration == null) {
            FsUserCourseVideoQVO videoInfo = selectFsUserCourseVideoByVideoIdVO(videoId, null);
            if (videoInfo == null || videoInfo.getDuration() == null) {
                throw new IllegalArgumentException("视频时长信息不存在");
            }
            duration = videoInfo.getDuration();

            // 将查询结果缓存到Redis，设置适当过期时间
            redisCache.setCacheObject(redisKey, duration);
        }

        return duration;
    }

    /**
     * 获取视频时长（优先从Redis获取，不存在则查数据库）
     */
    private Long getAutoLookVideoDuration(Long videoId) {
        //将视频时长也存到redis
        String videoRedisKey = "h5wxuser:video:duration:" + videoId;
        Long videoDuration = 0L;
        try {
            videoDuration = redisCache.getCacheObject(videoRedisKey);
        } catch (Exception e) {
            String string = redisCache.getCacheObject(videoRedisKey);
            videoDuration = Long.parseLong(string);
            log.error("key中id为S:{}", videoDuration);
        }


        if (videoDuration == null) {
            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
            videoDuration = video.getDuration();
            redisCache.setCacheObject(videoRedisKey, video.getDuration());
        }
        return videoDuration;
    }

    /**
     * 时长校验服务实现
     *
     * @param duration1
     * @param duration2
     * @return
     */
    public R checkTotalDuration(Long duration1, Long duration2) {
        // 参数校验
        if (duration1 == null || duration2 == null) {
            throw new IllegalArgumentException("时长参数不能为null");
        }

        if (duration1 < 0 || duration2 < 0) {
            throw new IllegalArgumentException("时长不能为负数");
        }
        // 获取并验证配置
        CourseConfig config = getValidatedConfig();

        // 根据配置模式进行校验
        switch (config.getCompletionMode()) {
            case 1: // 百分比模式
                return checkPercentageMode(duration1, duration2, config);
            case 2: // 分钟数模式
                return checkMinutesMode(duration1, config);
            default:
                return R.error("无效的完成模式配置");
        }
    }

    /**
     * 获取并验证课程配置
     */
    private CourseConfig getValidatedConfig() {
        String json = configService.selectConfigByKey("course.config");
        if (StringUtils.isBlank(json)) {
            throw new IllegalStateException("课程配置不存在");
        }

        try {
            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
            if (config == null || config.getCompletionMode() == null) {
                throw new IllegalStateException("课程配置不完整");
            }
            return config;
        } catch (Exception e) {
            throw new IllegalStateException("解析课程配置失败");
        }
    }

    /**
     * 百分比模式校验
     */
    private R checkPercentageMode(long duration1, long duration2, CourseConfig config) {
        if (config.getAnswerRate() == null) {
            return R.error("百分比模式未配置达标比例");
        }
        long percentage = (duration1 * 100 / duration2);

        boolean isPassed = percentage >= config.getAnswerRate();
        return isPassed ? R.ok() : R.error(String.format("观看进度%d%%未达标(需%d%%)",
                percentage, config.getAnswerRate()));
    }

    /**
     * 分钟数模式校验
     */
    private R checkMinutesMode(long videoDuration, CourseConfig config) {
        if (config.getMinutesNum() == null) {
            return R.error("分钟数模式未配置达标分钟数");
        }

        int i = config.getMinutesNum() * 60;
        boolean isPassed = videoDuration > i;

        return isPassed ? R.ok() : R.error(String.format("观看时长%d分钟未达标(需大于%d分钟)",
                videoDuration / 60, config.getMinutesNum()));
    }

    @Override
    public AjaxResult updateVideoByVideoUrl(String videoUrl, String oldVideoUrl, String thumbnail, String fileName, Integer duration, String lineOne, String lineTwo) {
        List<FsUserCourseVideo> videoList = fsUserCourseVideoMapper.selectByVideoUrl(oldVideoUrl);
        if (CollectionUtils.isEmpty(videoList)){
            log.warn("根据videoUrl:{} 未查询到fs_user_course_video表数据",oldVideoUrl);
            return AjaxResult.success();
        }
        List<Long> idList = videoList.stream().map(FsUserCourseVideo::getVideoId).collect(Collectors.toList());
        fsUserCourseVideoMapper.updateVideoByVideoUrl(videoUrl, thumbnail, idList, fileName, duration, lineOne, lineTwo);
        if (duration != null) {
            Long durationValue = duration.longValue();
            for (Long videoId : idList) {
                syncVideoDurationCache(videoId, durationValue);
            }
        }
        return AjaxResult.success();
    }
    @Override
    public R updateVideo() {
        FsUserCourseVideo param = new FsUserCourseVideo();
        param.setCourseId(144L);
        List<FsUserCourseVideo> videos = selectFsUserCourseVideoListByCourseId(param);
        for (FsUserCourseVideo courseVideo : videos) {
            FsVideoResource resource = fsVideoResourceMapper.selectByFileKey(courseVideo.getFileKey());
            if (resource == null) {
                continue;
            }
            courseVideo.setLineOne(resource.getLine1());
            fsUserCourseVideoMapper.updateFsUserCourseVideo(courseVideo);
        }
        return R.ok();
    }

    @Override
    public R checkUserInfo(Long userId) {
        FsUser user = fsUserService.selectFsUserById(userId);
        if (user == null) {
            return R.error(404, "用户不存在！");
        }
        //读取配置给前端返回
        String json = sysConfigService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        if (StringUtils.isEmpty(user.getNickName())) {
            Map<String, Object> map = new HashMap<>();
            map.put("authType", config.getMiniAppAuthType());
            if (config.getMiniAppAuthType() == 2) {
                map.put("domain", config.getUserCourseAuthDomain());
            }
            map.put("code", 500);
            return R.ok(map);
        }
        return R.ok().put("user", user);
    }

    @Override
    public FsUserCourseVideoQVO selectFsUserCourseVideoByVideoIdVO(Long videoId, Long userId) {
        FsUserCourseVideoQVO fsUserCourseVideoQVO = new FsUserCourseVideoQVO();
        FsUserCourseVideo courseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoIdAndUserId(videoId, userId);

        BeanCopyUtils.copy(courseVideo, fsUserCourseVideoQVO);
        if (courseVideo != null && courseVideo.getRedPacketMoney() != null) {
            fsUserCourseVideoQVO.setRedPacketMoney(courseVideo.getRedPacketMoney().toString());
        }

        if (StringUtils.isNotEmpty(courseVideo.getQuestionBankId())) {
            List<FsCourseQuestionBank> fsCourseQuestionBanks = courseQuestionBankMapper.selectFsCourseQuestionBankByIdVO(courseVideo.getQuestionBankId().split(","));
            fsUserCourseVideoQVO.setQuestionBankList(fsCourseQuestionBanks);
        }

        //返回课程关联的拍商品
        if (courseVideo.getIsProduct() != null && courseVideo.getIsProduct() == 1 && courseVideo.getProductId() != null) {
            FsCourseProduct courseProduct = courseProductMapper.selectFsCourseProductById(courseVideo.getProductId());
            List<FsCourseProduct> courseProducts = Arrays.asList(courseProduct);
            fsUserCourseVideoQVO.setCourseProducts(courseProducts);
        }
        return fsUserCourseVideoQVO;
    }

    @Override
    public R updateWatchDurationIsOpen(FsUserCourseVideoFinishUParam param) {
        //临时短链不做记录
        if (param.getLinkType() != null && param.getLinkType() == 1) {
            return R.ok();
        }

        if (param.getIsRoom() != null && param.getIsRoom() == 1) {
            return R.ok();
        }

        // 从Redis中获取观看时长
        String redisKey = "h5OpenUser:watch:duration:" + param.getUserId() + ":" + param.getVideoId();
        //        log.info("看课redis-key：{}", redisKey);
        try {
            String durationStr = redisCache.getCacheObject(redisKey);
            Long duration = durationStr != null ? Long.parseLong(durationStr) : 0L;

            // 更新Redis中的观看时长
            if (param.getDuration() != null && param.getDuration() > duration) {
                //24小时过期
                redisCache.setCacheObject(redisKey, param.getDuration().toString(), 2, TimeUnit.HOURS);
            }
            updateHeartbeatIsOpen(param);
            return R.ok();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("更新时长失败：{}", redisKey, e.getMessage());
            return R.error();
        }
    }

    //更新心跳时间
    public void updateHeartbeatIsOpen(FsUserCourseVideoFinishUParam param) {
        String redisKey = "h5OpenUser:watch:heartbeat:" + param.getUserId() + ":" + param.getVideoId();
        redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
        // 设置 Redis 记录的过期时间（例如 5 分钟）
        redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
    }

    @Override
    public R isAddKfIsOpen(FsUserCourseVideoAddKfUParam param) {
        logger.info("【判断添加客服】：{}", param);
        //查询用户
        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());
        //用户不存在唤起重新授权
        if (fsUser == null) {
            return R.error(504, "未授权");
        }

        if (fsUser.getStatus() == 0) {
            return R.error("当前学习通道访问限流，暂时无法观看！");
        }
        return createWatchIsOpen(param);
    }

    @Override
    public R getInternetTrafficIsOpen(FsUserCourseVideoFinishUParam param) {
        try {
            if (param.getBufferRate() == null) {
                logger.error("【缓冲值空】参数: {}", param);
                return R.error("缓冲值空");
            }
            FsCourseTrafficLog trafficLog = new FsCourseTrafficLog();
            trafficLog.setCreateTime(new Date());
            BeanUtils.copyProperties(param, trafficLog);

            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
            if (video == null) {
                return R.error("视频不存在");
            }

            // 实际流量传输消耗是大于文件的，添加倍数计算流量消耗 配置 course.data.usage.multiple
            BigDecimal multiple = new BigDecimal("1"); // 默认一倍
//            String config=configService.selectConfigByKey("course.data.usage.multiple");
//            if(StringUtils.isNotEmpty(config)){
//                multiple=new BigDecimal(config);
//            }
            // 计算流量
            BigDecimal result = param.getBufferRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
            BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
            long roundedResult = result.multiply(longAsBigDecimal).setScale(0, RoundingMode.HALF_UP).longValue();
            trafficLog.setInternetTraffic(roundedResult);

            //扣除流量
            Company company = companyMapper.selectCompanyById(param.getCompanyId());
            // 处理 UUID 为空的情况
            if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
                // 插入或更新
                fsCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
                asyncDeductTraffic(company, trafficLog);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 打印参数param和异常信息
            logger.error("【插入或更新流量失败】参数: {}, 错误信息：{}", param, e.getMessage(), e);
            return R.error();
        }
        return R.ok();
    }

    private R createWatchIsOpen(FsUserCourseVideoAddKfUParam param) {
        try {
            FsCourseWatchLog log = new FsCourseWatchLog();
            BeanUtils.copyProperties(param, log);
            log.setSendType(2);
            log.setUserId(param.getUserId());
            log.setVideoId(param.getVideoId());
            log.setDuration(0L);
            log.setCreateTime(new Date());
            log.setLogType(3);
            // 企微看课补齐 project，避免按项目完播统计丢失
            fsCourseWatchLogProjectFiller.fillIfAbsent(log);
            logger.info("【群聊生成看课记录】:{}", param);
            courseWatchLogMapper.insertFsCourseWatchLog(log);
        } catch (BeansException e) {
            return R.error("群聊生成看课记录失败！");
        }
        return R.ok("生成看课记录成功！");
    }

    /**
     * 查询选择使用的视频列表
     */
    @Override
    public List<FsUserCourseVideoChooseVO> getChooseCourseVideoListByMap(Map<String, Object> params) {
        return fsUserCourseVideoMapper.getChooseCourseVideoListByMap(params);
    }

    @Override
    public R sendAppReward(FsCourseSendRewardUParam param) {
        // 获取用户信息
        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
        if (user == null) {
            return R.error("当前学习通道访问限流，暂时无法观看！");
        }
        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
        if (log == null) {
            return R.error("无记录");
        }
        if (log.getRewardType() != null) {
            return R.error("奖励已发放");
        }

        // 获取配置信息
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

        // 更新用户积分
        FsUser userMap = new FsUser();
        userMap.setUserId(user.getUserId());
        Integer appAnswerIntegral = config.getAppAnswerIntegral();
        if (appAnswerIntegral == null) {
            appAnswerIntegral = config.getAnswerIntegral();
        }
        userMap.setIntegral(user.getIntegral() + (appAnswerIntegral == null ? 0 : appAnswerIntegral));
        fsUserMapper.updateFsUser(userMap);
        CompletableFuture.runAsync(() -> {
            FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
            integralLogs.setIntegral(config.getAppAnswerIntegral().longValue());
            integralLogs.setUserId(user.getUserId());
            integralLogs.setBalance(userMap.getIntegral());
            integralLogs.setLogType(17);
            integralLogs.setBusinessId(StringUtils.isNotEmpty(log.getLogId().toString()) ? log.getLogId().toString() : null);
            integralLogs.setCreateTime(new Date());
//            integralLogs.setNickName(user.getNickName());
//            integralLogs.setPhone(user.getPhone());
            //integralLogs.setId(integralLogsService.getFsUserIntegralLogsInsertId());
//        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
//            iFsUserIntegralLogsService.insertFsUserIntegralLogsMySql(integralLogs);
            iFsUserIntegralLogsService.insertFsUserIntegralLogs(integralLogs);
            //asyncAddIntegralLogs.saveLogAsync(integralLogs);

            // 更新观看记录的奖励类型
            log.setRewardType(2);
            courseWatchLogMapper.updateFsCourseWatchLog(log);


            //转换红包
            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
            redPacketLog.setCourseId(param.getCourseId());
            redPacketLog.setOutBatchNo(integralLogs.getId().toString());
            redPacketLog.setCompanyId(param.getCompanyId());
            redPacketLog.setUserId(param.getUserId());
            redPacketLog.setVideoId(param.getVideoId());
            redPacketLog.setStatus(1);
            redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
            redPacketLog.setCompanyUserId(param.getCompanyUserId());
            redPacketLog.setCreateTime(new Date());
            redPacketLog.setAmount(BigDecimal.valueOf(config.getAppAnswerIntegral()).divide(BigDecimal.valueOf(1000)));
            redPacketLog.setRemark("点播答题领取积分转");
            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
        });
        return R.ok("奖励发放成功");
    }


    @Override
    public R isSaveKf(FsUserCourseVideoAddKfUParam param) {
        logger.info("zyp \n【判断添加客服】：{}", param);

        // 参数校验
        if (param == null) {
            return R.error("参数不能为空");
        }

        // 查询用户
        FsUser fsUser = fsUserMapper.selectFsUserByUserId(param.getUserId());

        // 用户不存在唤起重新授权
        if (fsUser == null) {
            return R.error(504, "用户不存在");
        }

        if (fsUser.getStatus() != null && fsUser.getStatus() == 0) {
            return R.error("当前学习通道访问限流，暂时无法观看！");
        }

        // 处理群聊逻辑
        if (param.getIsRoom() != null && param.getIsRoom() == 1) {
//            return handleGroupChatLogic(param,fsUser);
            return R.ok();
        }

        // 处理普通外部联系人逻辑
        return handleExternalContactLogic(param, fsUser);
    }

    /**
     * 处理群聊逻辑
     */
    private R handleGroupChatLogic(FsUserCourseVideoAddKfUParam param, FsUser fsUser) {
        try {
            FsCourseLink courseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());

            if (courseLink == null || StringUtils.isEmpty(courseLink.getChatId())) {
                return R.error("参数有误");
            }

            QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(courseLink.getChatId());
            if (qwGroupChat == null) {
                logger.error("群聊不存在，chatId: {}", courseLink.getChatId());
                return R.error("群聊不存在!");
            }

            SopUserLogsInfo sopUserLogsInfo = new SopUserLogsInfo();
            sopUserLogsInfo.setChatId(courseLink.getChatId());
            List<QwGroupChatUser> qwGroupChatUsers = qwGroupChatUserMapper.selectByChatId(sopUserLogsInfo);

            if (qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()) {
                logger.error("群聊用户为空，chatId: {}", courseLink.getChatId());
                return R.error("群聊用户为空!");
            }

            // 构建查询条件
            QueryWrapper<QwExternalContact> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("user_id", qwGroupChat.getOwner());

            if (param.getUserId() != null) {
                queryWrapper.eq("name", fsUser.getNickName());
            }

            if (param.getCorpId() != null) {
                queryWrapper.eq("corp_id", param.getCorpId());
            }

            queryWrapper.eq("status", 0);

            QwExternalContact qwExternalContact = qwExternalContactMapper.selectOne(queryWrapper);

            if (qwExternalContact == null) {
                return R.error("未查询到客户！");
            }

            // 检查客户是否在群中
            boolean isInGroup = qwGroupChatUsers.stream()
                    .anyMatch(e -> e.getUserId() != null && e.getUserId().equals(qwExternalContact.getExternalUserId()));

            if (!isInGroup) {
                logger.error("客户不在群：{}，里面：{}", qwGroupChat.getChatId(), qwExternalContact.getExternalUserId());
                return R.error("客户不在群！");
            }

            logger.info("外部联系人数据：{}", qwExternalContact);

            // 判断外部联系人是否绑定userId
            if (qwExternalContact.getFsUserId() != null) {
                return handleBoundExternalContact(qwExternalContact, param, param.getQwExternalId());
            } else {
                return handleUnboundExternalContact(qwExternalContact, param, fsUser, param.getQwExternalId());
            }

        } catch (Exception e) {
            logger.error("处理群聊逻辑异常", e);
            return R.error("处理群聊逻辑异常！");
        }
    }

    /**
     * 处理外部联系人逻辑
     */
    private R handleExternalContactLogic(FsUserCourseVideoAddKfUParam param, FsUser fsUser) {
        try {
            Long qwExternalId = param.getQwExternalId();

            if (qwExternalId == null) {
                return R.error("外部联系人ID不能为空");
            }

            // 查询外部联系人
            QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(qwExternalId);

            // 如果查不出来客户信息，加好友
            if (externalContact == null) {
                return R.error("未查询到企微客户信息！");
            }

            // 绑定公司码
            addCompanyCompanyFsUser(param);

            // 判断外部联系人是否绑定userId
            if (externalContact.getFsUserId() != null) {
                return handleBoundExternalContact(externalContact, param, qwExternalId);
            } else {
                return handleUnboundExternalContact(externalContact, param, fsUser, qwExternalId);
            }

        } catch (Exception e) {
            logger.error("处理外部联系人逻辑异常", e);
            return R.error("系统异常，请稍后重试");
        }
    }

    private void addCompanyCompanyFsUser(FsUserCourseVideoAddKfUParam param) {
        //查询是否绑定
        CompanyCompanyFsuser companyCompanyFsuser = companyCompanyFsuserMapper.getInfoByUserId(param.getUserId().toString());

        if (companyCompanyFsuser != null) {
            if (companyCompanyFsuser.getQwContactId() == null) {
                //添加企业微信外部联系人
                companyCompanyFsuser.setQwContactId(param.getQwExternalId());
                companyCompanyFsuser.setUpdateTime(DateUtils.getNowDate());
                companyCompanyFsuserMapper.updateCompanyCompanyUser(companyCompanyFsuser);
            }
        } else {
            //是邀请用户的课程才绑定
            String json = configService.selectConfigByKey("app.config");
            if (StringUtils.isNotBlank(json)) {
                AppConfig config = JSONUtil.toBean(json, AppConfig.class);
                if (config != null) {
                    List<FsUserCourseVideoVO> fsCourse = config.getFsCourse();
                    if (fsCourse != null && !fsCourse.isEmpty()) {
                        if (Objects.equals(fsCourse.get(0).getVideoId(), param.getVideoId())) {
                            QwUser qwUser = qwUserMapper.selectQwUserById(Long.parseLong(param.getQwUserId()));
                            if (qwUser != null) {
                                //新增
                                CompanyCompanyFsuser insetCompanyFsUser = new CompanyCompanyFsuser();
                                insetCompanyFsUser.setUserId(param.getUserId());
                                insetCompanyFsUser.setCompanyId(qwUser.getCompanyId());
                                insetCompanyFsUser.setCompanyUserId(qwUser.getCompanyUserId());
                                insetCompanyFsUser.setQwContactId(param.getQwExternalId());
                                insetCompanyFsUser.setStatus(1);
                                insetCompanyFsUser.setCreateTime(DateUtils.getNowDate());
                                insetCompanyFsUser.setBindType(1); //看课绑定的
                                try {
                                    companyCompanyFsuserMapper.insertCompanyCompanyUser(insetCompanyFsUser);
                                    openIMService.checkAndImportFriendByDianBo(insetCompanyFsUser.getCompanyUserId(), insetCompanyFsUser.getUserId().toString(), StringUtils.isNotEmpty(param.getCorpId()) ? param.getCorpId() : "", true);
                                } catch (Exception e) {
                                    logger.info("新增客服绑定报错:{}", e.getMessage());
                                }
                            }
                        }
                    }
                }
            }
        }

    }

    /**
     * 处理已绑定外部联系人
     */
    private R handleBoundExternalContact(QwExternalContact externalContact, FsUserCourseVideoAddKfUParam param,
                                         Long qwExternalId) {
        // 有客户有小程序id，但登录的小程序id和根据外部联系人id查出来的小程序id不一致
        if (!externalContact.getFsUserId().equals(param.getUserId())) {
            return R.error("已注册！");
        }

        iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

        // 处理链接类型5
        if (param.getLinkType() != null && param.getLinkType() == 5) {
            FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(
                    param.getQwUserId(), param.getVideoId(), qwExternalId);
            return R.error(566, "官方群发通用链接").put("courseLink", fsCourseLink);
        }

        return R.ok();
    }

    /**
     * 处理未绑定外部联系人
     */
    private R handleUnboundExternalContact(QwExternalContact externalContact, FsUserCourseVideoAddKfUParam param,
                                           FsUser fsUser, Long qwExternalId) {
        // 没绑定fsUser直接绑定fsUser
        QwExternalContact contact = new QwExternalContact();
        contact.setId(qwExternalId);
        contact.setFsUserId(param.getUserId());

        qwExternalContactMapper.updateQwExternalContact(contact);
        iSopUserLogsInfoService.updateSopUserInfoByExternalId(qwExternalId, param.getUserId());

        // 更新用户信息
        if (fsUser != null) {
            FsUser user = new FsUser();
            if (fsUser.getNickName() != null && fsUser.getNickName().contains("微信用户")) {
                // 处理包含"微信用户"字样的逻辑
                user.setNickName(externalContact.getName());
                user.setAvatar(externalContact.getAvatar());
            }
            user.setUserId(fsUser.getUserId());
            user.setIsAddQw(1);
            fsUserMapper.updateFsUser(user);
        }

        // 处理链接类型5
        if (param.getLinkType() != null && param.getLinkType() == 5) {
            FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(
                    param.getQwUserId(), param.getVideoId(), qwExternalId);
            return R.error(566, "官方群发通用链接").put("courseLink", fsCourseLink);
        }

        return R.ok();
    }

    @Override
    public R uploadVideoToHuoShanByUrl() {
        List <FsVideoResource> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
        for (FsVideoResource video : videos){
            uploadVideoByUrl(video);
        }
        return R.ok();
    }

    @Autowired
    private IVodService vodService;

    //通过Url上传视频
    public void uploadVideoByUrl(FsVideoResource videoResource) {

        try {
            VodUrlUploadRequest.Builder reqBuilder = VodUrlUploadRequest.newBuilder();
            //空间名称
            reqBuilder.setSpaceName(cloudHostProper.getSpaceName());
            VodUrlUploadURLSet.Builder uRLSetsBuilder = VodUrlUploadURLSet.newBuilder();
            //源文件 URL
            uRLSetsBuilder.setSourceUrl(videoResource.getLine2());//华为云
            //存储类型。默认为 1。取值如下：
            //1：标准存储。
            //2：归档存储。
            //3：低频存储。
            uRLSetsBuilder.setStorageClass(1);
            //文件后缀，即点播存储中文件的类型。
            uRLSetsBuilder.setFileExtension(".mp4");
            //用户额外信息，最大长度 512 字节。
            uRLSetsBuilder.setCallbackArgs("");
            // 火山云存储路径（文件上传后在火山云的路径）
//            String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
//            String fileName = System.currentTimeMillis() + ".mp4";
//            String remoteFileName = "fs/" + datePath + "/" + fileName;

            uRLSetsBuilder.setFileName(videoResource.getFileKey());
            reqBuilder.addURLSets(uRLSetsBuilder);

            VodUrlUploadResponse resp = vodService.uploadMediaByUrl(reqBuilder.build());

            if (resp.getResponseMetadata().hasError()) {
                log.info("上传返回异常：{}",resp.getResponseMetadata().getError());
                System.exit(-1);
            }else {
                FsVideoResource video = new FsVideoResource();
                video.setId(videoResource.getId());
                video.setJobId(resp.getResult().getData(0).getJobId());
                //更新JobId
                fsVideoResourceMapper.updateById(video);
            }
            log.info("上传返回参数：{}",resp);
        } catch (Exception e) {
            throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
        }
    }

    @Override
    public R getVidByJob() {
        // 查询有JobId的视频
        List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByJobId();
        if (list.isEmpty()) {
            log.info("没有待上传的视频任务");
            return R.error();
        }
        // 按五百一批切割
        List<List<FsVideoResource>> batches = splitList(list, 500);
        log.info("总任务 {} 条，分成 {} 批", list.size(), batches.size());

        int batchIndex = 1;

        // 批次顺序执行，每批内部多线程并发执行
        for (List<FsVideoResource> batch : batches) {

            log.info("开始执行批次 {}/{}，本批任务 {} 条", batchIndex, batches.size(), batch.size());

            CountDownLatch latch = new CountDownLatch(batch.size());

            for (FsVideoResource video : batch) {
                uploadExecutor.submit(() -> {
                    try {
                        uploadSingleTaskWithRetry(video,1);
                    } finally {
                        latch.countDown();
                    }
                });
            }

            // 等待这一批全部完成
            try {
                latch.await();
            } catch (InterruptedException e) {
                log.error("批次等待异常", e);
            }

            log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
            batchIndex++;
        }

        log.info("全部批次执行完成");
        return R.ok();
    }

    @Override
    public R getVideoInfoByVid() {
        // 查询有JobId的视频
        List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByVid();
        if (list.isEmpty()) {
            log.info("没有包含vid的视频");
            return R.error();
        }
        // 按五百一批切割
        List<List<FsVideoResource>> batches = splitList(list, 500);
        log.info("总任务 {} 条，分成 {} 批", list.size(), batches.size());

        int batchIndex = 1;

        // 批次顺序执行，每批内部多线程并发执行
        for (List<FsVideoResource> batch : batches) {

            log.info("开始执行批次 {}/{}，本批任务 {} 条", batchIndex, batches.size(), batch.size());

            CountDownLatch latch = new CountDownLatch(batch.size());

            for (FsVideoResource video : batch) {
                uploadExecutor.submit(() -> {
                    try {
                        uploadSingleTaskWithRetry(video,2);
                    } finally {
                        latch.countDown();
                    }
                });
            }

            // 等待这一批全部完成
            try {
                latch.await();
            } catch (InterruptedException e) {
                log.error("批次等待异常", e);
            }

            log.info("批次 {}/{} 执行完成", batchIndex, batches.size());
            batchIndex++;
        }

        log.info("全部批次执行完成");
        return R.ok();
    }

    @Override
    public R createRoomMiniLinkByCourse(FsCourseLinkRoomNewParam param) {

        QwUser qwUser = qwUserMapper.selectQwUserById(param.getQwUserId());

        if (qwUser == null || qwUser.getCompanyId() == null || qwUser.getCompanyUserId() == null) {
            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
        }

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

        if (qwCompany == null) {
            return R.error().put("msg", "企业不存在，请联系管理员");
        }
        if (qwCompany.getMiniAppId() == null) {
            return R.error().put("msg", "当前企业未绑定小程序，请联系管理员");
        }

        //生成小程序链接
        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, null, 2, null, 1);

        if(StringUtils.isNotEmpty(linkByMiniApp)){
            linkByMiniApp = linkByMiniApp.replaceAll(".html", "");
        }
        String link = linkService.getGotoWxAppLink(linkByMiniApp, qwCompany.getMiniAppId());

        System.out.println("生成课程通链： " + link + " :" + param);

        return R.ok().put("link", link);
    }

    @Override
    public R createFeishuLinkByCourse(FsCourseLinkRoomNewParam param) {
        if (param.getCompanyUserId() == null || param.getCompanyId() == null) {
            return R.error("销售信息不存在");
        }

        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
        if (config == null) {
            return R.error().put("msg", "课程默认配置为空，请联系管理员");
        }

        Date createTime = DateUtils.getNowDate();
        FsCourseLink link = createFsCourseLink(null, createTime,
                param.getCourseId().intValue(), param.getVideoId().intValue(),
                null, param.getCompanyUserId(), param.getCompanyId(), null, 3, null);
        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);
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        LocalDateTime expireDateTime = LocalDateTime.of(tomorrow, LocalTime.of(23, 59, 59));
        Date expireDate = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
        link.setUpdateTime(expireDate);
        fsCourseLinkMapper.insertFsCourseLink(link);

        String feishuLink = feiShuService.createManualFeishuDirectCourseLink(param.getVideoId(), param.getCompanyId(),
                param.getCourseId(), param.getCompanyUserId(), link.getLink());
        if (StringUtils.isEmpty(feishuLink)) {
            return R.error("生成飞书链接失败");
        }
        return R.ok().put("link", feishuLink);
    }

    private final ExecutorService uploadExecutor = new ThreadPoolExecutor(
            8,  // core
            16, // max
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000),
            new ThreadFactory() {
                private final AtomicInteger index = new AtomicInteger(1);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "video-upload-" + index.getAndIncrement());
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public <T> List<List<T>> splitList(List<T> list, int batchSize) {
        List<List<T>> result = new ArrayList<>();
        int total = list.size();
        for (int i = 0; i < total; i += batchSize) {
            result.add(list.subList(i, Math.min(total, i + batchSize)));
        }
        return result;
    }

    //根据jobid查询上传视频的vid
    public void getVidByJobId(FsVideoResource videoResource){
        try {
            VodQueryUploadTaskInfoRequest.Builder reqBuilder = VodQueryUploadTaskInfoRequest.newBuilder();
            reqBuilder.setJobIds(videoResource.getJobId());

            VodQueryUploadTaskInfoResponse resp = vodService.queryUploadTaskInfo(reqBuilder.build());
            if (resp.getResponseMetadata().hasError()) {
                System.out.println(resp.getResponseMetadata().getError());
                System.exit(-1);
            }else {
                FsVideoResource video = new FsVideoResource();
                video.setId(videoResource.getId());
                //视频上传失败，清空jobid
                if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
                    video.setJobId("");
                }else {
                    video.setHsyVid(resp.getResult().getData().getMediaInfoList(0).getVid());
                }
                fsVideoResourceMapper.updateById(video);
            }
            System.out.println(resp);
        } catch (Exception e) {
            throw new RuntimeException("查询 URL 批量上传任务状态: " + e.getMessage(), e);
        }
    }


    //根据vid获取视频详情
    public void getVideoInfoByVid(FsVideoResource videoResource) {
        try {
            VodGetMediaInfosRequest.Builder reqBuilder = VodGetMediaInfosRequest.newBuilder();
            reqBuilder.setVids(videoResource.getHsyVid());

            VodGetMediaInfosResponse resp = vodService.getMediaInfos20230701(reqBuilder.build());
            if (resp.getResponseMetadata().hasError()) {
                System.out.println(resp.getResponseMetadata().getError());
                System.exit(-1);
            }else {
                //如果路径是空的直接返回
                if (StringUtils.isEmpty(resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri())){
                    return;
                }
                //如果是未发布状态修改发布状态
                if (resp.getResult().getMediaInfoList(0).getBasicInfo().getPublishStatus().equals("Unpublished")){
                    updateMediaPublishStatus(videoResource.getHsyVid());
                }
                //更新视频资源
//                String storeUri = resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
//                String uri = storeUri.substring(storeUri.indexOf("/") + 1);
                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getFileName();

                FsVideoResource fsVideoResource = new FsVideoResource();
                fsVideoResource.setId(videoResource.getId());
                fsVideoResource.setLine2(url);
                fsVideoResourceMapper.updateById(fsVideoResource);

                //更新小节
                FsUserCourseVideo courseVideo = new FsUserCourseVideo();
                courseVideo.setFileKey(videoResource.getFileKey());
                courseVideo.setLineTwo(url);
                fsUserCourseVideoMapper.updateFsUserCourseVideoByFileKeyForHsy(courseVideo);

            }
            System.out.println(resp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //修改发布状态
    public void updateMediaPublishStatus(String vid){
        String statusPublished = "Published";

        try {
            // publish
            VodUpdateMediaPublishStatusRequest.Builder req = VodUpdateMediaPublishStatusRequest.newBuilder();
            req.setVid(vid);
            req.setStatus(statusPublished);

            VodUpdateMediaPublishStatusResponse resp = vodService.updateMediaPublishStatus(req.build());
            System.out.println(resp);

            Thread.sleep(1000);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public void uploadSingleTaskWithRetry(FsVideoResource videoResource,Integer type) {
        int maxRetry = 3;
        for (int i = 1; i <= maxRetry; i++) {
            try {
                if (type == 1){
                    //获取上传成功的视频vid，同步到数据库
                    getVidByJobId(videoResource);
                }else if (type == 2){
                    //获取视频地址同步到线路二
                    getVideoInfoByVid(videoResource);
                }
                return;
            } catch (Exception e) {
                log.error("视频 {} 上传失败，第 {} 次重试，原因：{}",
                        videoResource.getId(), i, e.getMessage());
                if (i == maxRetry) {
                    log.error("视频 {} 上传最终失败！", videoResource.getId());
                }
            }
        }
    }

    /**
     * 同步会员营期
     * 将课程的视频排序同步到绑定的营期中（更新lesson和dayDate）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CoursePeriodSyncResultDTO syncCoursePeriod(Long courseId) {
        log.info("开始同步课程 {} 的会员营期", courseId);

        CoursePeriodSyncResultDTO result = new CoursePeriodSyncResultDTO();

        // 1. 查询课程视频，按courseSort排序
        FsUserCourseVideo queryVideo = new FsUserCourseVideo();
        queryVideo.setCourseId(courseId);
        List<FsUserCourseVideo> courseVideos = fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseId(queryVideo);

        if (courseVideos == null || courseVideos.isEmpty()) {
            log.warn("课程 {} 没有视频", courseId);
            return result;
        }

        // 按courseSort排序
        courseVideos.sort(Comparator.comparing(v -> v.getCourseSort() != null ? v.getCourseSort() : Long.MAX_VALUE));
        log.info("课程 {} 共有 {} 个视频", courseId, courseVideos.size());

        // 2. 查询该课程绑定的营期
        FsUserCoursePeriod queryPeriod = new FsUserCoursePeriod();
        queryPeriod.setCourseId(courseId);
        List<FsUserCoursePeriod> periods = fsUserCoursePeriodMapper.selectFsUserCoursePeriodList(queryPeriod);

        if (periods == null || periods.isEmpty()) {
            log.warn("课程 {} 没有绑定营期", courseId);
            return result;
        }

        // 过滤已结束的营期
        List<FsUserCoursePeriod> activePeriods = periods.stream()
                .filter(p -> p.getPeriodStatus() != null && p.getPeriodStatus() != 3)
                .collect(Collectors.toList());

        if (activePeriods.isEmpty()) {
            log.warn("课程 {} 的所有营期已结束", courseId);
            return result;
        }

        log.info("课程 {} 有 {} 个未结束营期", courseId, activePeriods.size());

        // 3. 遍历营期进行同步
        for (FsUserCoursePeriod period : activePeriods) {
            try {
                // 查询营期中的课程
                FsUserCoursePeriodDays queryDays = new FsUserCoursePeriodDays();
                queryDays.setPeriodId(period.getPeriodId());
                queryDays.setCourseId(courseId);
                List<FsUserCoursePeriodDays> existingDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(queryDays);

                if (existingDays.isEmpty()) {
                    continue;
                }

                // 构建 videoId -> day 的映射
                Map<Long, FsUserCoursePeriodDays> videoToDayMap = existingDays.stream()
                        .collect(Collectors.toMap(FsUserCoursePeriodDays::getVideoId, d -> d, (a, b) -> a));

                // 获取营期开始日期
                LocalDate periodStartDate = period.getPeriodStartingTime();
                if (periodStartDate == null) {
                    periodStartDate = LocalDate.now();
                }

                // 更新lesson和dayDate
                List<FsUserCoursePeriodDays> daysToUpdate = new ArrayList<>();
                for (int i = 0; i < courseVideos.size(); i++) {
                    FsUserCourseVideo video = courseVideos.get(i);
                    if (videoToDayMap.containsKey(video.getVideoId())) {
                        FsUserCoursePeriodDays day = videoToDayMap.get(video.getVideoId());
                        int newLesson = i;
                        LocalDate newDayDate = periodStartDate.plusDays(newLesson);

                        // 判断是否需要更新
                        if (day.getLesson() == null || day.getLesson() != newLesson || !newDayDate.equals(day.getDayDate())) {
                            day.setLesson(newLesson);
                            day.setDayDate(newDayDate);

                            // 更新时间字段
                            if (day.getStartDateTime() != null) {
                                day.setStartDateTime(LocalDateTime.of(newDayDate, day.getStartDateTime().toLocalTime()));
                            }
                            if (day.getEndDateTime() != null) {
                                day.setEndDateTime(LocalDateTime.of(newDayDate, day.getEndDateTime().toLocalTime()));
                            }
                            if (day.getLastJoinTime() != null) {
                                day.setLastJoinTime(LocalDateTime.of(newDayDate, day.getLastJoinTime().toLocalTime()));
                            }

                            daysToUpdate.add(day);
                        }
                    }
                }

                // 批量更新
                if (!daysToUpdate.isEmpty()) {
                    fsUserCoursePeriodDaysMapper.batchUpdateCoursePeriodDays(daysToUpdate);
                    result.addPeriodDetail(period.getPeriodId(), period.getPeriodName(), daysToUpdate.size());
                    log.info("营期 {} ({}) 同步完成，更新 {} 条", period.getPeriodId(), period.getPeriodName(), daysToUpdate.size());
                }
            } catch (Exception e) {
                log.error("同步营期 {} 失败", period.getPeriodId(), e);
            }
        }

        log.info("课程 {} 同步完成，共更新 {} 条记录", courseId, result.getTotalUpdated());
        return result;
    }

    /**
     * 视频下架
     */
    @Override
    public int batchDown(String[] videoIds) {
        return baseMapper.batchDown(videoIds);
    }

    /**
     * 视频上架
     */
    @Override
    public int batchUp(String[] videoIds) {
        return baseMapper.batchUp(videoIds);
    }

    /**
     * 批量修改视频封面
     */
    @Override
    public int batchEditCover(BatchEditCoverParam param) {
        return baseMapper.batchEditCover(param);
    }

    @Override
    public List<OptionsVO> selectVideoOptionsByCourseId(Long courseId) {
        return fsUserCourseVideoMapper.selectVideoOptionsByCourseId(courseId);
    }

    /**
     * 用户提现
     * @param param
     */
    @Override
    @Transactional
    public R withdrawal(FsCourseSendRewardUParam param) {
        Long userId = param.getUserId();
        // 生成锁的key，基于用户ID和视频ID确保同一用户同一视频的请求被锁定
        String lockKey = "reward_red_lock:user:" + userId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试获取锁，等待时间5秒，锁过期时间30秒
            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
            if (!isLocked) {
                logger.warn("获取锁失败，用户ID：{}", userId);
                return R.error("操作频繁，请稍后再试！");
            }

            logger.info("成功获取锁，开始处理奖励发放，用户ID：{}", userId);
            return executeWithdrawal(param);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error("获取锁被中断，用户ID：{}", userId, e);
            return R.error("系统繁忙，请重试！");
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                logger.info("释放锁成功，用户ID：{}", userId);
            }
        }
    }

    private R executeWithdrawal(FsCourseSendRewardUParam param){
        log.info("进入用户判断");
        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
        if (user == null) {
            return R.error("未识别到用户信息");
        }

        FsCourseWatchLog watchLog = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
        if (watchLog == null) {
            watchLog = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
        }
        if (watchLog == null) {
            return R.error("无记录");
        }

        if (watchLog.getLogType() != 2) {
            return R.error("未完课");
        }

        FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
        if (rightLog == null) {
            logger.error("未答题：{}", param.getUserId());
            return R.error("未答题");
        }

        FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());

        if (watchLog.getRewardType() != null) {
            if (watchLog.getRewardType() == 1) {
                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
                    return R.error("已领取该课程奖励，不可重复领取！");
                }
                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                    if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
                        R r = JSON.parseObject(fsCourseRedPacketLog.getResult(), R.class);
                        return r;
                    } else {
                        return R.error("操作频繁，请稍后再试！");
                    }
                }
            } else if (watchLog.getRewardType() == 2) {
                return R.error("已领取该课程奖励，不可重复领取！");
            }
        }

        // 获取视频信息
        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());

        // 获取配置信息
        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

        // 判断来源是否是app，如是app，则发放积分奖励
//        int sourceApp = 3;
//        if (sourceApp == param.getSource() /*&& !CloudHostUtils.hasCloudHostName("中康")*/) {
//            return sendIntegralReward(param, user, log, config);
//        }
        if (ObjectUtils.isEmpty(param.getRewardType())){
            param.setRewardType(config.getRewardType());
        }
        // 根据奖励类型发放不同奖励
        switch (param.getRewardType()) {
            // 红包奖励
            case 1:
                //来源是小程序切换openId
                WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
                String openId = getOpenId(param, user);
                if (StringUtils.isBlank(openId)) {
                    return R.error("请重新使用微信登录");
                }
                packetParam.setOpenId(openId);
                BeanUtils.copyProperties(param, packetParam);
                packetParam.setUser(user);

                return sendAppRedPacket(packetParam, watchLog,video, config);
            // 积分奖励
            case 2:
                return sendIntegralReward(param, user, watchLog, config);
//            // 转盘
//            case 3:
//                return drawTurntable(param, user, log);
//            // 保底转盘
//            case 4:
//                return drawTurntableGuarantee(param, user, log);
            default:
                return R.error("参数错误！");
        }
    }

    private R sendAppRedPacket(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
        // 仅手动发课（有营期）校验红包领取截止时间；自动发课不做该限制
        if (log.getPeriodId() != null) {
            FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
            periodDays.setVideoId(log.getVideoId());
            periodDays.setPeriodId(log.getPeriodId());
            //正常情况是只能查询到一条，之前可能存在重复的脏数据，暂使用查询list的方式
            List<FsUserCoursePeriodDays> fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(periodDays);
            if (fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()) {
                periodDays = fsUserCoursePeriodDays.get(0);
            }
            if (periodDays.getLastJoinTime() != null && LocalDateTime.now().isAfter(periodDays.getLastJoinTime())) {
                return R.error(403, "已超过领取红包时间");
            }
        }

        if (packetParam.getUser() != null && !isUserRedPacketReceiveEnabled(packetParam.getUser())) {
            return R.ok("恭喜您，答题成功啦 ！");
        }

        // 确定红包金额
        BigDecimal amount = BigDecimal.ZERO;
        FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyId(log.getVideoId(), log.getCompanyId(), log.getPeriodId());

        if (redPackage != null && redPackage.getRedPacketMoney() != null) {
            amount = redPackage.getRedPacketMoney();
        } else if (video != null && video.getRedPacketMoney() != null) {
            amount = video.getRedPacketMoney();
        }
        amount = courseRandomRedPacketService.resolveAmount(amount, log.getCourseId(), log.getCompanyId(), video, false);
        packetParam.setAmount(amount);

        if (amount.compareTo(BigDecimal.ZERO) > 0) {

            // 打开红包扣减功能
            if ("1".equals(config.getIsRedPackageBalanceDeduction())) {
                // 先注释 20251024 redis 余额 充值没有考虑 其余扣减没有考虑
                // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
                // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
                // 2 另起定时任务 同步缓存余额到redis中
                // 3 注意！！！！！ 启动系统时查询公司账户余额（这个时候要保证余额正确）启动会自动保存到redis缓存中
                // 注意！！！！！ 打开这个开关前记得检测redis缓存余额是否正确 若不正确 修改数据库字段red_package_money，删除redis缓存，重启系统，


                // 预设值异常对象

                BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
                balanceRollbackError.setCompanyId(packetParam.getCompanyId());
                balanceRollbackError.setUserId(log.getUserId());
                balanceRollbackError.setLogId(log.getLogId());
                balanceRollbackError.setVideoId(log.getVideoId());
                balanceRollbackError.setStatus(0);
                balanceRollbackError.setMoney(amount);

                if (packetParam.getCompanyId() == null) {
                    logger.error("发送红包参数错误，公司不能为空，异常请求参数{}", packetParam);
                    return R.error("发送红包失败，请联系管理员");
                }
                String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();

                // 第一次加锁：预扣减余额
                RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
                boolean lockAcquired = false;
                BigDecimal newMoney;
                try {
                    if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
                        lockAcquired = true;
                        BigDecimal originalMoney;
                        // 获取当前余额
                        String moneyStr = redisCache.getCacheObject(companyMoneyKey);
                        if (StringUtils.isNotEmpty(moneyStr)) {
                            originalMoney = new BigDecimal(moneyStr);
                        } else {
                            // 缓存没有值,重启系统恢复redis数据 保证数据正确性
                            logger.error("发送红包获取redis余额缓存异常，异常请求参数{}", packetParam);
                            return R.error("系统异常，请稍后重试");
                        }

                        if (originalMoney.compareTo(BigDecimal.ZERO) < 0) {
                            logger.error("服务商余额不足，异常请求参数{}", packetParam);
                            return R.error("服务商余额不足，请联系群主服务器充值!");
                        }

                        // 预扣减金额
                        newMoney = originalMoney.subtract(amount);
                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
                    } else {
                        logger.error("获取redis锁失败，异常请求参数{}", packetParam);
                        return R.error("系统繁忙，请稍后重试");
                    }
                } catch (Exception e) {
                    logger.error("预扣减余额失败: 异常请求参数{}，异常信息{}", packetParam, e.getMessage(), e);
                    return R.error("系统异常，请稍后重试");
                } finally {
                    // 只有在成功获取锁的情况下才释放锁
                    if (lockAcquired && lock1.isHeldByCurrentThread()) {
                        try {
                            lock1.unlock();
                        } catch (IllegalMonitorStateException e) {
                            logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
                        }
                    }
                }


                // 调用第三方接口（锁外操作）
                R sendRedPacket;
                try {
                    sendRedPacket = paymentService.sendAppRedPacket(packetParam);
                } catch (Exception e) {
                    logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
                    // 异常时回滚余额

                    rollbackBalance(balanceRollbackError);
                    return R.error("奖励发送失败，请联系客服");
                }

                // 红包发送成功处理
                if (sendRedPacket.get("code").equals(200)) {
                    FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                    TransferBillsResult transferBillsResult;
                    if (sendRedPacket.get("isNew").equals(1)) {
                        transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
                        redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                        redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                        redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
                    } else {
                        redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                        redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
                    }
                    // 添加红包记录
                    redPacketLog.setCourseId(log.getCourseId());
                    redPacketLog.setCompanyId(log.getCompanyId());
                    redPacketLog.setUserId(log.getUserId());
                    redPacketLog.setVideoId(log.getVideoId());
                    redPacketLog.setStatus(0);
                    redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
                    redPacketLog.setCompanyUserId(log.getCompanyUserId());
                    redPacketLog.setCreateTime(new Date());
                    redPacketLog.setAmount(amount);
                    redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                    redPacketLog.setPeriodId(log.getPeriodId());
                    redPacketLog.setAppId(packetParam.getAppId());

                    redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

                    // 更新观看记录的奖励类型
                    log.setRewardType(config.getRewardType());
                    courseWatchLogMapper.updateFsCourseWatchLog(log);

                    // 异步登记余额扣减日志
                    BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
                    companyService.asyncRecordBalanceLog(log.getCompanyId(), money, 15, newMoney, "发放红包", redPacketLog.getLogId());

                    return sendRedPacket;


                } else {
                    // 发送失败，回滚余额
                    rollbackBalance(balanceRollbackError);
                    return R.error("奖励发送失败，请联系客服");
                }

                // ===================== 本次修改目的为了实时扣减公司余额=====================
            } else {
                Company company = companyMapper.selectCompanyById(log.getCompanyId());
                BigDecimal money = company.getMoney();
                if (money.compareTo(BigDecimal.ZERO) <= 0) {
                    return R.error("服务商余额不足，请联系群主服务器充值!");
                }

                try{
                    // 发送红包
                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam);
                    if (sendRedPacket.get("code").equals(200)) {
                        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                        TransferBillsResult transferBillsResult;
                        if (sendRedPacket.get("isNew").equals(1)) {
                            transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
                            redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
                            redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
                            redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
                        } else {
                            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
                            redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
                        }
                        // 添加红包记录
                        redPacketLog.setCourseId(log.getCourseId());
                        redPacketLog.setCompanyId(log.getCompanyId());
                        redPacketLog.setUserId(log.getUserId());
                        redPacketLog.setVideoId(log.getVideoId());
                        redPacketLog.setStatus(0);
                        redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
                        redPacketLog.setCompanyUserId(log.getCompanyUserId());
                        redPacketLog.setCreateTime(new Date());
                        redPacketLog.setAmount(amount);
                        redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                        redPacketLog.setPeriodId(log.getPeriodId());
                        redPacketLog.setAppId( packetParam.getAppId());

                        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

                        // 更新观看记录的奖励类型
                        log.setRewardType(config.getRewardType());
                        courseWatchLogMapper.updateFsCourseWatchLog(log);

                        return sendRedPacket;
                    } else {
                        return R.error("奖励发送失败，请联系客服");
                    }
                }catch (Exception e){
                    return R.error(e.getMessage());
                }

            }
        } else {
            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
            // 添加红包记录
            redPacketLog.setCourseId(log.getCourseId());
//            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
            redPacketLog.setCompanyId(log.getCompanyId());
            redPacketLog.setUserId(log.getUserId());
            redPacketLog.setVideoId(log.getVideoId());
            redPacketLog.setStatus(1);
            redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
            redPacketLog.setCompanyUserId(log.getCompanyUserId());
            redPacketLog.setCreateTime(new Date());
            redPacketLog.setAmount(BigDecimal.ZERO);
            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
            redPacketLog.setPeriodId(log.getPeriodId());
            redPacketLog.setAppId( packetParam.getAppId());
            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);

            // 更新观看记录的奖励类
            log.setRewardType(config.getRewardType());
            courseWatchLogMapper.updateFsCourseWatchLog(log);
            return R.ok("答题成功！");
        }
    }

    /** 1 可领取红包；0 关闭；null 视为可领取（兼容历史数据） */
    private boolean isUserRedPacketReceiveEnabled(FsUser user) {
        if (user == null) {
            return false;
        }
        if (user.getRedStatus() == null) {
            return true;
        }
        return Objects.equals(user.getRedStatus(), 1);
    }

    @Override
    public R getFeiShuLinkCourseVideoDetails(Long companyUserId, Long videoId, Long identifier) {
        CompanyUser companyUser = companyUserMapper.selectCompanyUserById(companyUserId);
        if (companyUser == null) {
            return R.error(405, "当前销售不存在");
        }

        FsUserCourseVideoDetailsVO courseVideoDetails = getFeiShuVideoDetails(videoId);

        FsUserCourseAddCompanyUserParam param = new FsUserCourseAddCompanyUserParam();
        param.setCourseId(courseVideoDetails.getCourseId());
        param.setVideoId(videoId);
        param.setCompanyUserId(companyUserId);
        if (!isUserCoursePeriodValid(param)) {
            return R.error(504, "请观看最新的课程项目");
        }

        FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
        Long courseProject = fsUserCourse.getProject();
        if (Objects.isNull(courseProject)) {
            return R.error(504, "课程配置错误,项目归属为空,课程ID: " + param.getCourseId());
        }

        String json = configService.selectConfigByKey("course.config");
        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
        config.setCourseLogo(fsUserCourse.getImgUrl());

        long duration = 0L;
        long tipsTime = 0L;
        long tipsTime2 = 0L;
        int isFinish = 0;
        FsUserCourseVideoLinkDetailsVO vo = new FsUserCourseVideoLinkDetailsVO();
        vo.setCourseVideoDetails(courseVideoDetails);
        vo.setCourseConfig(config);
        vo.setIsFinish(isFinish);
        vo.setPlayDuration(duration);

        FeiShuCourseWatchLog watchLog = feishuCourseWatchLogMapper.getWatchLogByFsUser(videoId, identifier, companyUserId);
        if (watchLog == null) {
            watchLog = new FeiShuCourseWatchLog();
            watchLog.setUserId(identifier);
            watchLog.setCourseId(courseVideoDetails.getCourseId());
            watchLog.setVideoId(videoId);
            watchLog.setCompanyId(companyUser.getCompanyId());
            watchLog.setCompanyUserId(companyUserId);
            watchLog.setSendType(1);
            watchLog.setDuration(0);
            watchLog.setCreateTime(LocalDateTime.now());
            watchLog.setLogType(1);
            watchLog.setProject(courseProject);
            watchLog.setPeriodId(courseProject);
            feishuCourseWatchLogMapper.insert(watchLog);

            String heartRedisKey = CourseConstant.getFeiShuWatchHeartKey(identifier, videoId, companyUserId);
            redisCache.setCacheObject(heartRedisKey, LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), 5, TimeUnit.MINUTES);
        }

        String redisKey = CourseConstant.getFeiShuWatchDurationKey(identifier, videoId, companyUserId);
        Long durationCurrent = redisCache.getCacheObject(redisKey);
        if (durationCurrent != null) {
            duration = durationCurrent;
        } else {
            duration = watchLog.getDuration();
            redisCache.setCacheObject(redisKey, duration, 2, TimeUnit.HOURS);
        }

        if (watchLog.getLogType() == 2) {
            isFinish = 1;
        }

        vo.setTipsTime(tipsTime);
        vo.setTipsTime2(tipsTime2);
        vo.setIsFinish(isFinish);
        vo.setPlayDuration(duration);

        return R.ok().put("data", vo);
    }

    private FsUserCourseVideoDetailsVO getFeiShuVideoDetails(Long videoId) {
        FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
        FsUserCourseVideoDetailsVO fsUserCourseVideoDetailsVO = new FsUserCourseVideoDetailsVO();
        BeanUtils.copyProperties(fsUserCourseVideo, fsUserCourseVideoDetailsVO);
        fsUserCourseVideoDetailsVO.setVideoUrl(fsUserCourseVideo.getLineOne());

        String questionBankId = fsUserCourseVideo.getQuestionBankId();
        List<FsUserVideoQuestionVO> questionList = Collections.emptyList();
        if (StringUtils.isNotEmpty(questionBankId)) {
            String[] questionBankIds = questionBankId.split(",");
            List<FsCourseQuestionBank> fsCourseQuestionBanks = courseQuestionBankMapper.selectFsCourseQuestionBankByIdVO(questionBankIds);
            questionList = fsCourseQuestionBanks.stream().map(v -> {
                FsUserVideoQuestionVO fsUserVideoQuestionVO = new FsUserVideoQuestionVO();
                BeanUtils.copyProperties(v, fsUserVideoQuestionVO);
                return fsUserVideoQuestionVO;
            }).collect(Collectors.toList());
        }

        fsUserCourseVideoDetailsVO.setQuestionBankList(questionList);
        return fsUserCourseVideoDetailsVO;
    }

    @Override
    public void feiShuUpdateWatchDurationWx(Long companyUserId, Long videoId, Long identifier, Long duration) {
        Long videoDuration = getAutoLookVideoDuration(videoId);
        if (duration > videoDuration + 10) {
            return;
        }

        String redisKey = CourseConstant.getFeiShuWatchDurationKey(identifier, videoId, companyUserId);
        Long userLookDuration = redisCache.getCacheObject(redisKey);

        if (duration > (userLookDuration != null ? userLookDuration : 0) + 90) {
            return;
        }
        redisCache.setCacheObject(redisKey, duration, 2, TimeUnit.HOURS);

        String heartRedisKey = CourseConstant.getFeiShuWatchHeartKey(identifier, videoId, companyUserId);
        redisCache.setCacheObject(heartRedisKey, LocalDateTime.now().toString(), 5, TimeUnit.MINUTES);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void getFeiShuInternetTraffic(Long identifier, Long companyUserId, Long videoId, String uuid, BigDecimal bufferRate) {
        if (StringUtils.isBlank(uuid)) {
            return;
        }

        FsCourseTrafficLog trafficLog = new FsCourseTrafficLog();
        trafficLog.setCreateTime(new Date());
        trafficLog.setTypeFlag(2);
        trafficLog.setUuId(uuid);
        trafficLog.setUserId(identifier);
        trafficLog.setVideoId(videoId);
        trafficLog.setCompanyUserId(companyUserId);

        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
        if (video == null) {
            return;
        }

        CompanyUser companyUser = companyUserMapper.selectCompanyUserById(companyUserId);
        if (companyUser == null) {
            return;
        }

        Company company = companyMapper.selectCompanyById(companyUser.getCompanyId());
        if (company == null) {
            return;
        }

        trafficLog.setCompanyId(company.getCompanyId());
        trafficLog.setCourseId(video.getCourseId());

        BigDecimal result = bufferRate.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP);
        BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
        long roundedResult = result.multiply(longAsBigDecimal).setScale(0, RoundingMode.HALF_UP).longValue();
        trafficLog.setInternetTraffic(roundedResult);

        FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(video.getCourseId());
        if (fsUserCourse != null) {
            trafficLog.setProject(fsUserCourse.getProject());
        }

        fsCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
        asyncDeductTraffic(company, trafficLog);
    }

    private Map<String, String> createFeishuH5Link(String corpId, Date sendTime,
                                                   Integer courseId, Integer videoId, String qwUserId,
                                                   Long companyUserId, Long companyId, Long externalId, CourseConfig config,
                                                   Long feishuAccountId, Integer feishuNeedAuth) {
        FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, Long.valueOf(qwUserId),
                companyUserId, companyId, externalId, 3, null);
        link.setFeishuAccountId(feishuAccountId);
        link.setFeishuNeedAuth(FeiShuService.isAuthRequired(feishuNeedAuth) ? 1 : 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);
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        LocalDateTime expireDateTime = LocalDateTime.of(tomorrow, LocalTime.of(23, 59, 59));
        Date expireDate = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
        link.setUpdateTime(expireDate);
        fsCourseLinkMapper.insertFsCourseLink(link);
        Map<String, String> result = new HashMap<>();
        result.put("url", link.getRealLink());
        result.put("link", link.getLink());
        return result;
    }

    public FsCourseLink createFsCourseLink(String corpId, Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                           Long companyUserId, Long companyId, Long externalId, Integer type, String chatId) {
        FsCourseLink link = new FsCourseLink();
        link.setCompanyId(companyId);
        link.setQwUserId(qwUserId);
        link.setCompanyUserId(companyUserId);
        link.setVideoId(videoId.longValue());
        link.setCorpId(corpId);
        link.setCourseId(courseId.longValue());
        link.setChatId(chatId);
        link.setQwExternalId(externalId);
        link.setLinkType(type);
        link.setIsRoom(1);
        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;
    }


}

