Sfoglia il codice sorgente

1、一键群发对于ap看课/直播跳转短链

yys 3 settimane fa
parent
commit
c6c992314e
19 ha cambiato i file con 807 aggiunte e 30 eliminazioni
  1. 20 7
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  2. 109 0
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  3. 327 11
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  4. 1 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  5. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java
  6. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  7. 8 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  8. 44 0
      fs-service/src/main/java/com/fs/course/utils/LinkUtil.java
  9. 84 0
      fs-service/src/main/java/com/fs/his/utils/LinkUtil.java
  10. 5 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  11. 10 0
      fs-service/src/main/java/com/fs/live/param/FsLiveEncryptLinkParam.java
  12. 8 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  13. 2 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java
  14. 128 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  15. 5 0
      fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java
  16. 2 0
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  17. 7 0
      fs-user-app/src/main/java/com/fs/app/controller/CourseController.java
  18. 17 10
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveController.java
  19. 23 0
      fs-user-app/src/main/java/com/fs/app/controller/store/CompanyUserScrmController.java

+ 20 - 7
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -5,14 +5,15 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
-import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.his.domain.FsPayConfig;
@@ -195,12 +196,15 @@ public class LiveController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody Live live)
     {
-        return AjaxResult.success();
-//        CompanyUser user = SecurityUtils.getLoginUser().getUser();
-//        live.setCompanyUserId(user.getUserId());
-//        live.setCompanyId(user.getCompanyId());
-//
-//        return toAjax(liveService.updateLive(live));
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        live.setCompanyUserId(user.getUserId());
+        live.setCompanyId(user.getCompanyId());
+        Live db = liveService.selectLiveByLiveIdAndCompanyIdAndCompanyUserId(
+                live.getLiveId(), user.getCompanyId(), user.getUserId());
+        if (db != null && db.getTrainingPeriodId() != null) {
+            live.setIsAudit(0);
+        }
+        return toAjax(liveService.updateLive(live));
     }
 
     /**
@@ -402,4 +406,13 @@ public class LiveController extends BaseController
 
         return R.ok().put("data", exist);
     }
+
+    @ApiOperation("创建App跳转通用链接")
+    @GetMapping("/createAppLink")
+    @PreAuthorize("@ss.hasPermi('live:live:createAppLink')")
+    public R createAppLink(@RequestParam("liveId") Long liveId,@RequestParam("corpId")String corpId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return liveService.createAppLink(user,liveId,corpId);
+    }
+
 }

+ 109 - 0
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -21,11 +21,13 @@ import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.fastGpt.service.AiHookService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.utils.LinkUtil;
 import com.fs.ipad.IpadSendUtils;
 import com.fs.ipad.vo.*;
 import com.fs.live.domain.LiveWatchLog;
@@ -105,6 +107,7 @@ public class IpadSendServer {
     private final FsUserCourseVideoMapper fsUserCourseVideoMapper;
     private final LuckyBagCollectRecordMapper luckyBagCollectRecordMapper;
 
+    private final IFsCourseLinkService linkService;
 
     private static final List<String> PROJECT_NAMES = Arrays.asList("济南联志健康", "北京存在文化","宽益堂");
     private final LiveWatchLogMapper liveWatchLogMapper;
@@ -778,6 +781,24 @@ public class IpadSendServer {
                     // 小程序H5看课
                     sendMiniProgram(vo, content, miniMap);
                     break;
+                case "18":
+                    // 发送直播卡片
+                    sendLiveMiniProgram(vo, content, miniMap);
+                    break;
+                case "19":
+                    // 发送直播短链
+                    sendLiveShortLink(vo, content, miniMap);
+                    break;
+                case "21":
+                    content.setSendStatus(0);
+                    content.setSendRemarks("短信待发送");
+                    break;
+                    //跳转APP看课链接
+                case "23":
+                    //跳转APP直播链接
+                case "24":
+                    sendAppShortLink(vo, content, miniMap);
+                    break;
                 case "99":
                     // 群发
                     sendTxtAtMsg(vo);
@@ -792,6 +813,94 @@ public class IpadSendServer {
             content.setSendRemarks("发送失败:" + e.getMessage());
         }
     }
+
+
+    private void sendAppShortLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap) {
+//        //发送短链前,先发送一个介绍语
+//        TxtVo introduction = TxtVo.builder().content("请复制以下短链内容").build();
+//        introduction.setBase(vo);
+//        ipadSendUtils.sendTxt(introduction);
+        //发送短链内容
+        String miniProgramPage = content.getMiniprogramPage();
+        String sendShortLink = null;
+        //解析直播短链信息
+        String livePrefix = "/pages_live/livingList?link=";
+        if (miniProgramPage.startsWith(livePrefix)) {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSONObject.parseObject(miniProgramPage.substring(livePrefix.length()));
+            sendShortLink = livePrefix + obj.getString("link");
+        }
+        //解析课程短链信息
+        String coursePrefix = "/courseH5/pages/course/learning?course=";
+        if (miniProgramPage.startsWith(coursePrefix)) {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSONObject.parseObject(miniProgramPage.substring(coursePrefix.length()));
+            sendShortLink = coursePrefix + obj.getString("link");
+        }
+        if (null == sendShortLink) {
+            log.warn("发送链接为空");
+            return;
+        }
+        sendShortLink = sendShortLink.replace(".html","");
+        String InvitationCode = LinkUtil.encryptLink(sendShortLink);
+        TxtVo txtVo = TxtVo.builder().content("康好健康"+InvitationCode).build();
+        txtVo.setBase(vo);
+        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> resp = ipadSendUtils.sendTxt(txtVo);
+        if (resp.getErrcode() != 0) {
+            log.debug("ID:{}-ipad接口请求返回异常:{}", vo.getId(), resp.getErrmsg());
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + resp.getErrmsg());
+        }
+    }
+
+    /**
+     * 发送直播短链
+     * @param vo
+     * @param content
+     * @param miniMap
+     */
+    private void sendLiveShortLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap) {
+        FsCoursePlaySourceConfig courseMaConfig = miniMap.get(content.getMiniprogramAppid());
+        String miniProgramPage = content.getMiniprogramPage();
+        miniProgramPage = miniProgramPage.replace(".html","");
+        String link = linkService.getGotoWxAppLink(miniProgramPage, courseMaConfig.getAppid());
+
+        TxtVo txtVo = TxtVo.builder().content(link).build();
+        txtVo.setBase(vo);
+        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> resp = ipadSendUtils.sendTxt(txtVo);
+        if (resp.getErrcode() != 0) {
+            log.debug("ID:{}-ipad接口请求返回异常:{}", vo.getId(), resp.getErrmsg());
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + resp.getErrmsg());
+        }
+    }
+
+    /**
+     * 发送直播卡片
+     * @param vo
+     * @param content
+     * @param miniMap
+     */
+    private void sendLiveMiniProgram(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap) {
+        FsCoursePlaySourceConfig courseMaConfig = miniMap.get(content.getMiniprogramAppid());
+        // 小程序
+        MiniProgramVo miniProgramVo = MiniProgramVo.builder()
+                .desc(content.getMiniprogramTitle().replaceFirst("-.*", ""))
+                .title(courseMaConfig.getName())
+                .weappIconUrl(courseMaConfig.getImg())
+                .imgUrl(content.getMiniprogramPicUrl())
+                .username(courseMaConfig.getOriginalId() + "@app")
+                .pagepath(content.getMiniprogramPage())
+                .appid(content.getMiniprogramAppid())
+                .build();
+        miniProgramVo.setBase(vo);
+        WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> resp = ipadSendUtils.sendMiniProgram(miniProgramVo);
+        if (resp.getErrcode() != 0) {
+            log.debug("ID:{}-ipad接口请求返回异常:{}", vo.getId(), resp.getErrmsg());
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + resp.getErrmsg());
+        }
+
+    }
+
     private void addLuckyBagCollectRecord(QwUser qwUser, QwSopCourseFinishTempSetting.Setting content, QwSopLogs qwSopLogs) {
 
         try {

+ 327 - 11
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -71,6 +71,7 @@ import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
+import static com.fs.course.utils.LinkUtil.generateRandomNumberWithLock;
 
 import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
 
@@ -87,6 +88,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private static final String h5miniappLink = "/pages_course/shortLink.html?s=";
     private static final String appActivitlLink = "/pages_course/activity.html?link=";
     private static final String registeredRealLink = "/pages_course/register.html?link=";
+    private static final String h5LiveShortLink = "/pages_course/livingInvite.html?s=";
+
 
 //    private static final String miniappRealLink = "/pages/index/index?course=";
 
@@ -765,6 +768,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
         if (StringUtils.isNotEmpty(logVo.getChatId())) {
             QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
+            if (groupChat == null) {
+                log.warn("群聊 {} 不存在,跳过处理。", logVo.getChatId());
+                return;
+            }
             if (groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()) {
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null,null);
                 ruleTimeVO.setSendType(6);
@@ -971,7 +978,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         String json = configService.selectConfigByKey("his.config");
-        FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
         for (QwSopTempSetting.Content.Setting setting : settings) {
             switch (setting.getContentType()) {
                 //直播小程序单独
@@ -980,8 +986,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     sopLogs.setSendType(20);
                     clonedContent.setLiveId(setting.getLiveId());
                     String sortLiveLink;
-                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
-                    if (isGroupChat) {
+                    sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId() + "&qwUserId=" + qwUserId;
+                    FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
+                    if (isGroupChat && groupChat != null) {
                         try {
                             groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                                 Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
@@ -1111,11 +1118,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         // 8. 获取小程序ID
                         String finalAppId = null;
                         Company company = companyMapper.selectCompanyById(Long.valueOf(companyId));
-                        List<String> miniAppMaster = company.getMiniAppMaster();
+                        List<String> miniAppMaster = company != null ? company.getMiniAppMaster() : null;
                         if (ObjectUtil.isNotEmpty(miniAppMaster)) {
                             finalAppId = miniAppMaster.get(0);
                         } else {
-                            finalAppId = sysConfig.getAppId();
+                            FSSysConfig luckyBagSysConfig = JSON.parseObject(json, FSSysConfig.class);
+                            finalAppId = luckyBagSysConfig != null ? luckyBagSysConfig.getAppId() : null;
                         }
 //                        try {
 //                            finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
@@ -1148,6 +1156,69 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         }
                     }
                     break;
+                //直播h5跳转卡片
+                case "18":
+                    //直播h5跳转短链
+                case "19": {
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+                    shortH5Link = shortH5Link.substring(0, shortH5Link.length() - 1);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    FSSysConfig h5LiveSysConfig = JSON.parseObject(configService.selectConfigByKey("his.config"), FSSysConfig.class);
+
+                    if (isGroupChat && groupChat != null) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), h5LiveSysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                                }
+                            });
+                            shortH5Link += ",\"chatId\":\"" + groupChat.getChatId() + "\"";
+                        } catch (Exception e) {
+                            log.error("直播H5群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), h5LiveSysConfig.getAppId(), 1, qwUserId, logVo.getCorpId());
+                            shortH5Link += ",\"externalId\":\"" + externalId + "\"";
+                        } catch (Exception e) {
+                            log.error("直播H5个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    shortH5Link += "}";
+                    String h5LiveTitle = setting.getMiniprogramTitle();
+                    int h5LiveTitleMaxLength = 17;
+                    setting.setMiniprogramTitle(h5LiveTitle.length() > h5LiveTitleMaxLength ? h5LiveTitle.substring(0, h5LiveTitleMaxLength) + "..." : h5LiveTitle);
+                    setting.setMiniprogramAppid(h5LiveSysConfig.getAppId());
+                    setting.setMiniprogramPage(shortH5Link);
+
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+                    break;
+                }
+                //录播
+                case "24": {
+                    String recordCorpId = logVo.getCorpId();
+                    String recordShortH5Link = createH5LiveShortLink(setting, recordCorpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(recordShortH5Link);
+                    break;
+                }
                 default:
                     break;
             }
@@ -1157,6 +1228,126 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         enqueueQwSopLogs(sopLogs);
     }
 
+    private String createRegisteredLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                                 String qwUserId,
+                                                 String companyUserId, String companyId, String externalId, Long fsUserId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+//        if (StringUtils.isEmpty(config.getMiniprogramPage())){
+//            log.error("miniprogramPage is not loaded.");
+//            return "";
+//        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId!=null?Long.parseLong(qwUserId):null);
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setCorpId(logVo.getCorpId());
+        link.setQwExternalId(Long.parseLong(externalId));
+        link.setUNo(UUID.randomUUID().toString());
+        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 = registeredRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
+
+    public String createRegisteredGroupLinkByMiniApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                                     String qwUserId,
+                                                     Long companyUserId, String companyId, String chatId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+//            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(qwUserId!=null?Long.parseLong(qwUserId): null);
+        link.setCompanyUserId(companyUserId);
+//        link.setVideoId(null);
+        link.setCorpId(logVo.getCorpId());
+//        link.setCourseId(null);
+        link.setChatId(chatId);
+        link.setIsRoom(1);
+        link.setLinkType(3);
+        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);
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = registeredRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+    }
+
     /**
      * 处理直播消息
      */
@@ -1188,7 +1379,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId() + "&corpId=" + logVo.getCorpId()+"&qwUserId=" + qwUserId;
                     String json = configService.selectConfigByKey("his.config");
                     FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
-                    if (isGroupChat) {
+                    if (isGroupChat && groupChat != null) {
                         try {
                             groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                                 Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
@@ -1225,6 +1416,67 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     }
 
                     break;
+                //直播h5跳转卡片
+                case "18":
+                    //直播h5跳转短链
+                case "19": {
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+                    FSSysConfig h5LiveSysConfig = JSON.parseObject(configService.selectConfigByKey("his.config"), FSSysConfig.class);
+
+                    if (isGroupChat && groupChat != null) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    //写入直播待看课记录
+                                    createLiveWatchLogAndEnQueue(companyId, companyUserId, vo.getId().toString(), setting.getLiveId(), h5LiveSysConfig.getAppId(), 2, qwUserId, logVo.getCorpId());
+                                }
+                            });
+                            shortH5Link += "&chatId=" + groupChat.getChatId();
+                        } catch (Exception e) {
+                            log.error("直播H5群聊新增报错,{}", e.getMessage(), e);
+                        }
+                    } else {
+                        try {
+                            createLiveWatchLogAndEnQueue(companyId, companyUserId, externalId, setting.getLiveId(), h5LiveSysConfig.getAppId(), 1, qwUserId, logVo.getCorpId());
+                            shortH5Link += "&externalId=" + externalId;
+                        } catch (Exception e) {
+                            log.error("直播H5个人新增报错,{}", e.getMessage(), e);
+                        }
+                    }
+
+                    String h5LiveTitle = setting.getMiniprogramTitle();
+                    int h5LiveTitleMaxLength = 17;
+                    setting.setMiniprogramTitle(h5LiveTitle.length() > h5LiveTitleMaxLength ? h5LiveTitle.substring(0, h5LiveTitleMaxLength) + "..." : h5LiveTitle);
+                    setting.setMiniprogramAppid(h5LiveSysConfig.getAppId());
+                    setting.setMiniprogramPage(shortH5Link);
+
+                    try {
+                        setting.setMiniprogramPicUrl(StringUtil.strIsNullOrEmpty(setting.getMiniprogramPicUrl()) ? "https://cos.his.cdwjyyh.com/fs/20250331/ec2b4e73be8048afbd526124a655ad56.png" : setting.getMiniprogramPicUrl());
+                    } catch (Exception e) {
+                        log.error("赋值-小程序封面地址失败-" + e);
+                    }
+                    break;
+                }
+                //跳转app直播
+                case "24": {
+                    String liveCorpId = logVo.getCorpId();
+                    String liveShortH5Link = createH5LiveShortLink(setting, liveCorpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(liveShortH5Link);
+                    break;
+                }
                 default:
                     break;
             }
@@ -1257,11 +1509,16 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             return;
         }
 
-//
-//        Integer courseType = clonedContent.getCourseType();
 
         String isOfficial = clonedContent.getIsOfficial();
 
+
+        Long msgNum = Long.valueOf(generateRandomNumberWithLock());
+        sopLogs.setSmsLogsId(msgNum);
+
+
+        AtomicInteger index = new AtomicInteger(0);
+
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         if (settings == null || settings.isEmpty()) {
             log.error("Cloned content settings are empty, skipping.");
@@ -1275,6 +1532,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
         for (QwSopTempSetting.Content.Setting setting : settings) {
+
+            Integer currentIndex = index.getAndIncrement();
+
             switch (setting.getContentType()) {
                 //文字和短链一起
                 case "1":
@@ -1347,7 +1607,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //小程序单独
                 case "4":
-                    if (isGroupChat) {
+                    if (isGroupChat && groupChat != null) {
                         try {
                             groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                                 Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
@@ -1441,7 +1701,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     sortLiveLink = "/pages_course/living.html?companyId=" + companyId + "&companyUserId=" + companyUserId + "&liveId=" + setting.getLiveId()+"&corpId=" +logVo.getCorpId()+"&qwUserId=" + qwUserId;
                     String json = configService.selectConfigByKey("his.config");
                     FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
-                    if(isGroupChat){
+                    if (isGroupChat && groupChat != null) {
                         try{
                             groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
                                 Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
@@ -1550,6 +1810,33 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         log.error("浏览器看课模板解析失败:" + e);
                     }
 
+                    break;
+                //跳转app看课
+                case "23":
+                    try {
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo,2);
+
+                        String shortH5link = createH5LinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
+                                qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId());
+
+                        setting.setMiniprogramTitle("邀请链接");
+                        setting.setMiniprogramPage(shortH5link);
+
+                    } catch (Exception e) {
+                        log.error("app看课模板解析失败:" + e);
+                    }
+                    break;
+                //app直播跳转
+                case "24":
+                    String corpId = logVo.getCorpId();
+                    String shortH5Link = createH5LiveShortLink(setting, corpId,
+                            qwUserId, companyUserId, companyId);
+
+                    sopLogs.setSendType(Integer.valueOf(setting.getContentType()));
+                    clonedContent.setLiveId(setting.getLiveId());
+
+                    setting.setMiniprogramPage(shortH5Link);
+
                     break;
                 default:
                     break;
@@ -1566,7 +1853,36 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         log.info("sopLogs加入队列,ExternalId:{}",externalId);
         enqueueQwSopLogs(sopLogs);
     }
+    private String createH5LiveShortLink(QwSopTempSetting.Content.Setting setting, String corpId, String qwUserId, String companyUserId, String companyId) {
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setLiveId(setting.getLiveId());
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
 
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        /*FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);*/
+
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = h5LiveShortLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        //存短链-
+        enqueueCourseLink(link);
+        return link.getRealLink();
+
+    }
     private String getAppIdFromMiniMap(Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                        String companyId,
                                        int sendMsgType,
@@ -1943,7 +2259,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
      * @param qwUserId
      * @param corpId
      */
-    public void createLiveWatchLogAndEnQueue(String companyId,String companyUserId,String externalId,Long liveId,String appId,Integer logSource,String qwUserId,String corpId){
+    public void createLiveWatchLogAndEnQueue(String companyId, String companyUserId, String externalId, Long liveId, String appId, Integer logSource, String qwUserId, String corpId) {
         // 写入对应数据源的记录表
         LiveWatchLog itemLiveWatchLog = new LiveWatchLog();
         itemLiveWatchLog.setLiveId(liveId);

+ 1 - 0
fs-service/src/main/java/com/fs/course/config/CourseConfig.java

@@ -20,6 +20,7 @@ public class CourseConfig implements Serializable {
     private Integer defaultLine;//默认看课线路
     private String realLinkDomainName;//真链域名
     private String realLinkH5DomainName;//H5通用看课域名
+    private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java

@@ -69,4 +69,9 @@ public class FsCourseLink extends BaseEntity
     @ApiModelProperty(value = "项目唯一标识(PS:MYHK)")
     private String projectCode;
 
+    //@ApiModelProperty(value = "直播id")
+    private Long liveId;
+
+    private Long userId;
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -166,4 +166,6 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
     R encryptLink(String url);
 
     R decryptLink(String url);
+
+    R decryptLinkV2(String url);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -32,6 +32,7 @@ import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.ConfigUtil;
+import com.fs.his.utils.LinkUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.qw.Bean.MsgBean;
 import com.fs.qw.cache.IQwExternalContactCacheService;
@@ -1351,4 +1352,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return R.ok().put("data", data);
     }
 
+    @Override
+    public R decryptLinkV2(String url) {
+        String decryptLink = LinkUtil.decryptLink(url);
+        Map<String, Object> data = new HashMap<>();
+        data.put("decryptLink", decryptLink);
+        return R.ok().put("data", data);
+    }
 }

+ 44 - 0
fs-service/src/main/java/com/fs/course/utils/LinkUtil.java

@@ -51,4 +51,48 @@ public class LinkUtil {
         // 示例:使用UUID(牺牲有序性,但保证唯一性)
         return UUID.randomUUID().toString().replace("-", "");
     }
+
+    public static String generateRandomNumberWithLock() {
+
+        RedissonClient redissonClient = SpringUtils.getBean(RedissonClient.class);
+        RLock lock = redissonClient.getLock("generateNumLinkLock");
+
+        int retryCount = 3;  // 设置重试次数
+        int waitTime = 100;  // 等待锁的最大时间(单位:毫秒)
+        int leaseTime = 10;  // 锁的持有时间(单位:秒)
+
+        while (retryCount > 0) {
+            try {
+                // 尝试获取锁,最多等待waitTime毫秒,锁定之后最多持有锁leaseTime秒
+                boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
+                if (isLocked) {
+                    // 成功获取锁,生成唯一的随机字符串
+                    return IdUtil.getSnowflake(0, 0).nextIdStr();
+                } else {
+                    // 如果锁未能获取到,重试
+                    retryCount--;
+                    if (retryCount == 0) {
+                        log.warn("Failed to acquire lock after multiple retries.");
+                        return generateFallbackIdByNum(); // 返回失败标识
+                    }
+                    // 可以在这里设置重试等待时间
+                    Thread.sleep(200); // 等待一段时间后重试
+                }
+            } catch (Exception e) {
+                log.error("Error while acquiring lock", e);
+                Thread.currentThread().interrupt(); // 恢复中断状态
+                return generateFallbackIdByNum();  // 异常情况下返回失败
+            }
+        }
+        return generateFallbackIdByNum();  // 如果所有重试都失败,返回失败
+    }
+
+    private static String generateFallbackIdByNum() {
+        // 使用纳秒级时间戳(但实际精度可能不高)
+        long nanoTime = System.nanoTime();
+        long milliTime = System.currentTimeMillis();
+
+        // 组合毫秒和纳秒,增加唯一性
+        return String.format("%d%d", milliTime, Math.abs(nanoTime % 1000000));
+    }
 }

+ 84 - 0
fs-service/src/main/java/com/fs/his/utils/LinkUtil.java

@@ -0,0 +1,84 @@
+package com.fs.his.utils;
+
+import org.springframework.util.Base64Utils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+public class LinkUtil {
+
+    // 保持密钥不变(16位 AES 密钥)
+    private static final String AES_KEY = "abcedfghijklmnop";
+    // 改用 CBC 模式(比 ECB 安全),搭配 PKCS5Padding
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
+    // CBC 模式需要初始化向量(IV),固定 16 位(和 AES 分组长度一致)
+    private static final byte[] IV = AES_KEY.getBytes(StandardCharsets.UTF_8);
+
+    // 优化后:仅 AES + Base64URL 编码(去掉 GZIP,适配短字符串)
+    public static String encryptLink(String plainText){
+        try {
+            if (plainText == null || plainText.isEmpty()) {
+                return plainText;
+            }
+            // 1. 初始化 AES CBC 模式(比 ECB 安全,长度和 ECB 一致)
+            SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
+            javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(IV);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+
+            // 2. AES 加密
+            byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
+            byte[] encryptBytes = cipher.doFinal(plainBytes);
+
+            // 3. Base64URL 编码(优化:替换 +/ 为 -_,去掉 =,比标准 Base64 短)
+            String base64Str = Base64Utils.encodeToString(encryptBytes);
+            return base64Str.replace("+", "-").replace("/", "_").replace("=", "");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    // 解密方法(可逆)
+    public static String decryptLink(String cipherText){
+        try {
+            if (cipherText == null || cipherText.isEmpty()) {
+                return cipherText;
+            }
+            // 1. 还原 Base64URL 为标准 Base64
+            String base64Str = cipherText.replace("-", "+").replace("_", "/");
+            int padding = 4 - (base64Str.length() % 4);
+            if (padding != 4) {
+                base64Str += "====".substring(0, padding);
+            }
+            byte[] encryptBytes = Base64Utils.decodeFromString(base64Str);
+
+            // 2. AES CBC 解密
+            SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
+            javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(IV);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+            byte[] plainBytes = cipher.doFinal(encryptBytes);
+
+            return new String(plainBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    // 测试:你的原字符串加密后长度对比
+    /*public static void main(String[] args) throws Exception {
+        String original = "/pages_live/livingList?link=2034103257541902336";
+        System.out.println("原字符串长度:" + original.length()); // 约 40 字符
+
+        // 优化后加密
+        String encrypted = encryptShortStr(original);
+        System.out.println("优化后加密长度:" + encrypted.length()); // 约 50 字符(从 150 大幅缩短)
+        System.out.println("加密后内容:" + encrypted);
+
+        // 解密验证可逆性
+        String decrypted = decryptShortStr(encrypted);
+        System.out.println("解密后是否一致:" + original.equals(decrypted)); // true
+    }*/
+
+}

+ 5 - 0
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -39,6 +39,11 @@ public class   Live extends BaseEntity {
      */
     private Long companyUserId;
 
+    /**
+     * 营期ID(非空表示归属「训练营-营期」直播间,与普通直播列表区分)
+     */
+    private Long trainingPeriodId;
+
     /**
      * 达人ID
      */

+ 10 - 0
fs-service/src/main/java/com/fs/live/param/FsLiveEncryptLinkParam.java

@@ -0,0 +1,10 @@
+package com.fs.live.param;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+@Data
+@ApiModel(value = "加解密")
+public class FsLiveEncryptLinkParam {
+    private String url;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/service/ILiveService.java

@@ -2,6 +2,7 @@ package com.fs.live.service;
 
 
 import com.fs.common.core.page.PageRequest;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.vo.CompanyVO;
 import com.fs.live.param.LiveNotifyParam;
 import com.fs.live.vo.LiveVo;
@@ -219,4 +220,11 @@ public interface ILiveService
     List<CompanyVO> getCompanyDropList();
 
     R checkLiveRoomPassword(Live live);
+
+
+    R createAppLink(CompanyUser user, Long liveId, String corpId);
+
+
+    R liveDecryptLinkV2(String url,String userId);
+
 }

+ 2 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java

@@ -4,6 +4,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.constant.LiveKeysConstant;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -292,7 +293,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
     @Transactional
     public R claimCoupon(CouponPO coupon) {
         LiveCouponIssue issue = liveCouponMapper.selectLiveCouponIssueByLiveIdAndCouponId(coupon.getLiveId(), coupon.getCouponIssueId());
-        if (coupon == null || issue.getStatus() != 1) {
+        if (coupon == null || ObjectUtils.isEmpty(issue)||issue.getStatus() != 1) {
             return R.error("优惠券不存在或者已下架!");
         }
 

+ 128 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -13,14 +13,24 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.page.PageRequest;
 import com.fs.common.exception.base.BaseException;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.CompanyUserUser;
 import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.ICompanyUserUserService;
 import com.fs.company.vo.CompanyVO;
 import com.fs.core.config.WxMaConfiguration;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsCourseLink;
+import com.fs.course.domain.FsCourseRealLink;
+import com.fs.course.mapper.FsCourseLinkMapper;
 import com.fs.his.domain.FsStoreProduct;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.mapper.FsUserWxMapper;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.LinkUtil;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import com.fs.live.dto.TemplateMessageSendRequestDTO;
@@ -39,8 +49,11 @@ import com.fs.live.param.LiveReplayParam;
 import com.fs.live.service.*;
 import com.fs.live.utils.ProcessManager;
 import com.fs.live.vo.*;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.service.IQwExternalContactService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageInfo;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -74,6 +87,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
+import static com.fs.course.utils.LinkUtil.generateRandomStringWithLock;
+
 /**
  * 直播Service业务层处理
  *
@@ -91,6 +107,16 @@ public class LiveServiceImpl implements ILiveService
 
     @Autowired
     private ISysConfigService sysConfigService;
+    @Autowired
+    private ICompanyUserUserService companyUserUserService;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+    @Autowired
+    private IFsUserService userService;
+    @Autowired
+    private FsCourseLinkMapper fsCourseLinkMapper;
 
 
     @Autowired
@@ -199,7 +225,46 @@ public class LiveServiceImpl implements ILiveService
             return R.error("密码错误");
         }
     }
-
+    @Override
+    public R createAppLink(CompanyUser user, Long liveId, String corpId) {
+        if(user == null || user.getCompanyId() == null){
+            return R.error("销售账号请绑定销售公司");
+        }
+
+        if(corpId == null){
+            return R.error("请选择企微公司,方便判定客户归属!");
+        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(user.getCompanyId());
+        //link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(user.getUserId());
+        link.setLiveId(liveId);
+        link.setCorpId(corpId);
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+        link.setCreateTime(new Date());
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        org.springframework.beans.BeanUtils.copyProperties(link, courseMap);
+        String courseJson = JSON.toJSONString(link);
+        String realLinkFull = "/pages_live/livingList?link=" + courseJson;
+        link.setRealLink(realLinkFull);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+
+        String sendShortLink = "/pages_live/livingList?link=" + link.getLink();
+
+        sendShortLink = sendShortLink.replace(".html","");
+        String InvitationCode = LinkUtil.encryptLink(sendShortLink);
+        log.info("真实链接为:{},发送的短链为:{}",realLinkFull, InvitationCode);
+        return R.ok().put("realLink","太乙云养"+InvitationCode);
+    }
 
     /**
      * 查询直播
@@ -1640,4 +1705,66 @@ public class LiveServiceImpl implements ILiveService
         String hexString = Long.toHexString(aLong);
         return hexString.toUpperCase();
     }
+
+
+    /**
+     * 解密链接
+     * @param url
+     * @return
+     */
+    @Override
+    public R liveDecryptLinkV2(String url,String userId) {
+        String json = sysConfigService.selectConfigByKey("course.config");
+        CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+        Long fsUserId = Long.valueOf(userId);
+        String decryptLink = LinkUtil.decryptLink(url);
+        Map<String, Object> data = new HashMap<>();
+        data.put("decryptLink", config.getRealLinkH5LiveName()+decryptLink);
+        String[] split = decryptLink.split("=");
+        if(split[1] != null){
+            FsCourseLink courseLink = fsCourseLinkMapper.selectFsCourseLinkByLink(split[1]);
+            String realLink = courseLink.getRealLink();
+            data.put("realLink", config.getRealLinkH5LiveName() + realLink);
+            String[] split1 = realLink.split("=");
+            if(split1[1] != null){
+                try {
+                    JSONObject jsonObject = JSONObject.parseObject(split1[1]);
+                    Long companyUserId = jsonObject.getLong("companyUserId");
+                    Long qwExternalId = jsonObject.getLong("qwExternalId");
+
+                    if(companyUserId == null){
+                        return R.error("销售不存在,链接无效!");
+                    }
+
+                    CompanyUserUser map = new CompanyUserUser();
+                    map.setCompanyUserId(companyUserId);
+                    map.setUserId(fsUserId);
+                    //将小程序客户和销售做绑定
+                    List<CompanyUserUser> list = companyUserUserService.selectCompanyUserUserList(map);
+                    if (list == null || list.isEmpty()) {
+                        CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
+                        if (companyUser != null && companyUser.getStatus().equals("0")) {
+                            map.setCompanyId(companyUser.getCompanyId());
+                            companyUserUserService.insertCompanyUserUser(map);
+                        }
+                    }
+
+                    FsUser user=userService.selectFsUserByUserId(fsUserId);
+                    if(user != null && qwExternalId != null){
+                        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactById(qwExternalId);
+                        if(qwExternalContact != null && qwExternalContact.getFsUserId() == null){
+                            QwExternalContact qwExtContact = new QwExternalContact();
+                            qwExtContact.setId(qwExternalContact.getId());
+                            qwExtContact.setFsUserId(fsUserId);
+                            qwExternalContactService.updateQwExternalContactBindUserId(qwExtContact);
+                        }
+                    }
+                } catch (Exception e) {
+                    return R.error("链接无效,请联系销售重新发送!");
+                }
+            }
+            return R.ok().put("data", data);
+        }
+        return R.error("链接无效,请联系销售重新发送!");
+    }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/sop/domain/QwSopLogs.java

@@ -113,6 +113,11 @@ public class QwSopLogs implements Serializable {
     private int appSendStatus;
     private String appSendRemark;
 
+    /**
+     * 短信执行记录表
+     */
+    private Long smsLogsId;
+
     // 构造函数
 //    public QwSopLogs() {
 //        this.id = UUID.randomUUID().toString();

+ 2 - 0
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -98,6 +98,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
     private static final String appActivitlLink = "/pages_course/activity.html?link=";
     private static final String appLink = "https://jump.ylrztop.com/jumpapp/pages/index/index?link=";
     private static final String registeredRealLink = "/pages_course/register.html?link=";
+    private static final String appLiveShortLink = "/pages_live/livingList?link=";
+
 //    private static final String miniappRealLink = "/pages/index/index?course=";
 
     @Autowired

+ 7 - 0
fs-user-app/src/main/java/com/fs/app/controller/CourseController.java

@@ -437,4 +437,11 @@ public class CourseController extends  AppBaseController{
         // 查询看课记录
         return linkService.getLinkInfo(param.getLogId());
     }
+
+    @ApiOperation("解密链接参数2.0")
+    @PostMapping("/decryptLink/v2")
+    public R decryptLinkV2(@RequestBody FsCourseEncryptLinkParam param) {
+        return courseWatchLogService.decryptLinkV2(param.getUrl());
+    }
+
 }

+ 17 - 10
fs-user-app/src/main/java/com/fs/app/controller/live/LiveController.java

@@ -29,6 +29,7 @@ import com.fs.his.service.IFsUserService;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveVideoMapper;
 import com.fs.live.mapper.LiveWatchUserMapper;
+import com.fs.live.param.FsLiveEncryptLinkParam;
 import com.fs.live.param.LiveNotifyParam;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveVo;
@@ -313,10 +314,10 @@ public class LiveController extends AppBaseController {
 
 	@Autowired
 	private WxMaProperties properties;
-	
+
 	@Autowired
 	private RedisCache redisCache;
-	
+
 	@ApiOperation("微信直播间urlScheme")
 	@GetMapping("/getAppletScheme")
 	public R getAppletScheme(@RequestParam(value = "liveId") Long liveId,@RequestParam(value = "companyUserId") Long companyUserId) {
@@ -337,37 +338,37 @@ public class LiveController extends AppBaseController {
 			String param = "liveId=" + liveId + "&companyUserId=" + companyUser.getUserId() + "&companyId=" + companyUser.getCompanyId() ;
 			String appId = properties.getConfigs().get(0).getAppid();
 			String secret = properties.getConfigs().get(0).getSecret();
-			
+
 			// 从 Redis 缓存中获取 access_token
 			String cacheKey = "wx:access_token:" + appId;
 			String access_token = redisCache.getCacheObject(cacheKey);
-			
+
 			// 如果缓存中没有或已过期,则重新获取
 			if (StringUtils.isEmpty(access_token)) {
 				String rspStr = HttpUtils.sendGet("https://api.weixin.qq.com/cgi-bin/token", "grant_type=client_credential&" + "appid=" + appId + "&secret=" + secret);
 				JSONObject obj = JSONObject.parseObject(rspStr);
 				access_token = obj.getString("access_token");
-				
+
 				// 检查是否获取成功
 				if (StringUtils.isEmpty(access_token)) {
 					log.error("获取微信 access_token 失败: {}", obj);
 					return R.error("获取微信 access_token 失败");
 				}
-				
+
 				// 将 access_token 存入 Redis,缓存时间为 7200 秒
 				redisCache.setCacheObject(cacheKey, access_token, 7200, java.util.concurrent.TimeUnit.SECONDS);
 				log.info("微信 access_token 已刷新并缓存,appId: {}", appId);
 			} else {
 				log.debug("从 Redis 缓存中获取 access_token,appId: {}", appId);
 			}
-			
+
 			JSONObject jump_wxaObj = new JSONObject();
 			// 跳转直播间
 			jump_wxaObj.put("page_url", "pages_course/living.html?" + param);
 			String paramStr = jump_wxaObj.toJSONString();
 			String postStr = HttpUtils.sendPost("https://api.weixin.qq.com/wxa/genwxashortlink?access_token=" + access_token, paramStr);
 			JSONObject obj = JSONObject.parseObject(postStr);
-			
+
 			// 如果 access_token 失效,清除缓存并重新获取
 			if (obj != null && (obj.getInteger("errcode") != null && obj.getInteger("errcode") == 40001)) {
 				log.warn("access_token 已失效,清除缓存并重新获取,appId: {}", appId);
@@ -383,7 +384,7 @@ public class LiveController extends AppBaseController {
 					obj = JSONObject.parseObject(postStr);
 				}
 			}
-			
+
 			//response.addHeader("Access-Control-Allow-Origin", "*");
 			return R.ok().put("result", obj);
 		} catch (Exception e) {
@@ -391,6 +392,12 @@ public class LiveController extends AppBaseController {
 			return R.error("操作失败");
 		}
 	}
-
+	@Login
+	@ApiOperation("解密链接参数")
+	@PostMapping("/decryptLink/v2")
+	public R decryptLinkV2(@RequestBody FsLiveEncryptLinkParam param) {
+		String userId = getUserId();
+		return liveService.liveDecryptLinkV2(param.getUrl(),userId);
+	}
 
 }

+ 23 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/CompanyUserScrmController.java

@@ -34,6 +34,7 @@ import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.sop.domain.QwSopTempVoice;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.system.oss.CloudStorageService;
@@ -57,7 +58,9 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -438,5 +441,25 @@ public class CompanyUserScrmController extends AppBaseController {
         return R.ok().put("data", audioVO);
     }
 
+    /**
+     * 济世百康功能,将companyUserId加密后发送给客户,后续添加绑定使用
+     * @param companyUserId
+     * @return
+     */
+    @ApiOperation("获取销售邀请码")
+    @GetMapping("/getCompanyUserInvitationCode")
+    public R getCompanyUserInvitationCode(@RequestParam("companyUserId")Long companyUserId){
+        if(companyUserId == null){
+            return R.error("请登陆账号后再试!");
+        }
+        CompanyUser companyUser = companyUserMapper.selectCompanyUserByUserId(companyUserId);
+        if (companyUser == null) {
+            return R.error("销售不存在,邀请码不合法");
+        }
+        String InvitationCode = PhoneUtil.encryptPhone(String.valueOf(companyUserId));
+        Map<String, Object> data = new HashMap<>();
+        data.put("InvitationCode", InvitationCode);
+        return R.ok().put("data",data);
+    }
 
 }