|  | @@ -3,6 +3,7 @@ package com.fs.course.service.impl;
 | 
	
		
			
				|  |  |  import cn.hutool.core.util.NumberUtil;
 | 
	
		
			
				|  |  |  import cn.hutool.json.JSONUtil;
 | 
	
		
			
				|  |  |  import com.alibaba.fastjson.JSON;
 | 
	
		
			
				|  |  | +import com.alibaba.fastjson.JSONObject;
 | 
	
		
			
				|  |  |  import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 | 
	
		
			
				|  |  |  import com.fs.common.BeanCopyUtils;
 | 
	
		
			
				|  |  |  import com.fs.common.core.domain.R;
 | 
	
	
		
			
				|  | @@ -21,6 +22,7 @@ import com.fs.course.param.*;
 | 
	
		
			
				|  |  |  import com.fs.course.param.newfs.*;
 | 
	
		
			
				|  |  |  import com.fs.course.service.IFsUserCourseVideoService;
 | 
	
		
			
				|  |  |  import com.fs.course.service.IFsVideoResourceService;
 | 
	
		
			
				|  |  | +import com.fs.course.vo.FsCourseVideoListBySidebarVO;
 | 
	
		
			
				|  |  |  import com.fs.course.vo.FsUserCourseVideoListUVO;
 | 
	
		
			
				|  |  |  import com.fs.course.vo.FsUserCourseVideoQVO;
 | 
	
		
			
				|  |  |  import com.fs.course.vo.FsUserCourseVideoVO;
 | 
	
	
		
			
				|  | @@ -33,10 +35,13 @@ import com.fs.his.param.WxSendRedPacketParam;
 | 
	
		
			
				|  |  |  import com.fs.his.service.IFsStorePaymentService;
 | 
	
		
			
				|  |  |  import com.fs.his.service.IFsUserService;
 | 
	
		
			
				|  |  |  import com.fs.his.vo.OptionsVO;
 | 
	
		
			
				|  |  | +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.QwUserMapper;
 | 
	
		
			
				|  |  | +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;
 | 
	
	
		
			
				|  | @@ -44,6 +49,7 @@ import com.fs.sop.mapper.QwSopLogsMapper;
 | 
	
		
			
				|  |  |  import com.fs.sop.mapper.SopUserLogsInfoMapper;
 | 
	
		
			
				|  |  |  import com.fs.sop.service.ISopUserLogsInfoService;
 | 
	
		
			
				|  |  |  import com.fs.system.service.ISysConfigService;
 | 
	
		
			
				|  |  | +import com.fs.voice.utils.StringUtil;
 | 
	
		
			
				|  |  |  import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
 | 
	
		
			
				|  |  |  import lombok.extern.slf4j.Slf4j;
 | 
	
		
			
				|  |  |  import org.slf4j.Logger;
 | 
	
	
		
			
				|  | @@ -57,11 +63,14 @@ import java.math.BigDecimal;
 | 
	
		
			
				|  |  |  import java.math.RoundingMode;
 | 
	
		
			
				|  |  |  import java.time.LocalDateTime;
 | 
	
		
			
				|  |  |  import java.time.LocalTime;
 | 
	
		
			
				|  |  | +import java.time.ZoneId;
 | 
	
		
			
				|  |  |  import java.util.*;
 | 
	
		
			
				|  |  |  import java.util.concurrent.TimeUnit;
 | 
	
		
			
				|  |  |  import java.util.concurrent.atomic.AtomicLong;
 | 
	
		
			
				|  |  |  import java.util.stream.Collectors;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  /**
 | 
	
		
			
				|  |  |   * 课堂视频Service业务层处理
 | 
	
		
			
				|  |  |   *
 | 
	
	
		
			
				|  | @@ -73,6 +82,12 @@ import java.util.stream.Collectors;
 | 
	
		
			
				|  |  |  public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 | 
	
		
			
				|  |  |  {
 | 
	
		
			
				|  |  |      private static final Logger logger = LoggerFactory.getLogger(FsUserCourseVideoServiceImpl.class);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static final String miniappRealLink = "/pages_course/video.html?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=";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      @Autowired
 | 
	
		
			
				|  |  |      private FsUserCourseVideoMapper fsUserCourseVideoMapper;
 | 
	
		
			
				|  |  |      @Autowired
 | 
	
	
		
			
				|  | @@ -149,6 +164,12 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 | 
	
		
			
				|  |  |      @Autowired
 | 
	
		
			
				|  |  |      private FsUserCompanyUserMapper fsUserCompanyUserMapper;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @Autowired
 | 
	
		
			
				|  |  | +    private IQwExternalContactService qwExternalContactService;
 | 
	
		
			
				|  |  | +    @Autowired
 | 
	
		
			
				|  |  | +    private IQwCompanyService iQwCompanyService;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      /**
 | 
	
		
			
				|  |  |       * 查询课堂视频
 | 
	
		
			
				|  |  |       *
 | 
	
	
		
			
				|  | @@ -1207,6 +1228,179 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 | 
	
		
			
				|  |  |          return fsUserCourseVideoMapper.selectVideoListByMap(params);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    public List<FsCourseVideoListBySidebarVO> getFsCourseVideoListBySidebar(FsCourseListBySidebarParam param) {
 | 
	
		
			
				|  |  | +        return fsUserCourseVideoMapper.getFsCourseVideoListBySidebar(param);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    public R createMiniLink(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","企业不存在,请联系管理员");
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        //看课记录
 | 
	
		
			
				|  |  | +        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(),param.getFsUserId(),qwUser , param.getExternalUserId());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        //生成小程序链接
 | 
	
		
			
				|  |  | +        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(),2,null);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        JSONObject news = new JSONObject(true);
 | 
	
		
			
				|  |  | +        news.put("miniprogramAppid", qwCompany.getMiniAppId());
 | 
	
		
			
				|  |  | +        news.put("miniprogramTitle", param.getTitle());
 | 
	
		
			
				|  |  | +        news.put("miniprogramPicUrl", "https://cos.his.cdwjyyh.com/fs/20250523/9c8af5735d784847818cada7fa776a7b.jpg");
 | 
	
		
			
				|  |  | +        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();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(),param.getFsUserId(),qwUser , param.getExternalUserId());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        String linkByCartLink = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(),1,domainName);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        //生成卡片链接
 | 
	
		
			
				|  |  | +        JSONObject news = new JSONObject(true); // true 表示保持字段顺序
 | 
	
		
			
				|  |  | +        news.put("linkTitle", param.getTitle());
 | 
	
		
			
				|  |  | +        news.put("linkDescribe", param.getTitle());
 | 
	
		
			
				|  |  | +        news.put("linkImageUrl", "https://cos.his.cdwjyyh.com/fs/20250523/9c8af5735d784847818cada7fa776a7b.jpg");
 | 
	
		
			
				|  |  | +        news.put("linkUrl", linkByCartLink);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return R.ok().put("data",news);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    //插入观看记录
 | 
	
		
			
				|  |  | +    private void addWatchLogIfNeeded(Long videoId, Long courseId,
 | 
	
		
			
				|  |  | +                                     Long fsUserId, QwUser qwUser,Long externalId) {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            FsCourseWatchLog watchLog = new FsCourseWatchLog();
 | 
	
		
			
				|  |  | +            watchLog.setVideoId(videoId);
 | 
	
		
			
				|  |  | +            watchLog.setQwExternalContactId(externalId);
 | 
	
		
			
				|  |  | +            watchLog.setSendType(2);
 | 
	
		
			
				|  |  | +            watchLog.setQwUserId(String.valueOf(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);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (fsUserId == null) {
 | 
	
		
			
				|  |  | +                fsUserId=0L;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            watchLog.setUserId(fsUserId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            //存看课记录
 | 
	
		
			
				|  |  | +            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) {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        FsCourseLink link = new FsCourseLink();
 | 
	
		
			
				|  |  | +        link.setCompanyId(qwUser.getCompanyId());
 | 
	
		
			
				|  |  | +        link.setQwUserId(String.valueOf(qwUser.getId()));
 | 
	
		
			
				|  |  | +        link.setCompanyUserId(qwUser.getCompanyUserId());
 | 
	
		
			
				|  |  | +        link.setVideoId(videoId);
 | 
	
		
			
				|  |  | +        link.setCorpId(qwUser.getCorpId());
 | 
	
		
			
				|  |  | +        link.setCourseId(courseId);
 | 
	
		
			
				|  |  | +        link.setQwExternalId(externalId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        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);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        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) {
 | 
	
		
			
				|  |  |          String redisKey = "h5wxuser:watch:heartbeat:" + param.getUserId() + ":" + param.getVideoId() + ":" + param.getCompanyUserId();
 |