소스 검색

新飞书发课逻辑

cgp 1 주 전
부모
커밋
3c612b2470

+ 2 - 2
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -3419,7 +3419,7 @@ public class Task {
                 Client client = buildClient(account);
                 try {
                     String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
-                    redisCache.setCacheObject("fei_shu_client:" + docId, account);
+                    redisCache.setCacheObject("fei_shu_client:" + docId, account,30,TimeUnit.DAYS);
                     return docId;
 
                 } catch (CustomException e) {
@@ -3454,7 +3454,7 @@ public class Task {
             FeishuAccount account = wrapper.getAccount();
             try {
                 String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
-                redisCache.setCacheObject("fei_shu_client:" + docId, account);
+                redisCache.setCacheObject("fei_shu_client:" + docId, account,30,TimeUnit.DAYS);
                 return docId;
 
             } catch (CustomException e) {

+ 45 - 1
fs-ipad-task/src/main/java/com/fs/app/service/FeishuAsyncService.java

@@ -11,6 +11,9 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
 /**
  * 飞书异步任务
  * */
@@ -39,7 +42,7 @@ public class FeishuAsyncService {
             // 成功时需要更新docId的文档内容为已填充
             String amountKey = FEISHU_CLIENT+docId;
             account.setDocumentIdFillFlag(Boolean.TRUE);
-            redisCache.setCacheObject(amountKey, account);
+            redisCache.setCacheObject(amountKey, account,30, TimeUnit.DAYS);
         } catch (CustomException e) {
             // 调用原Service的方法处理异常
             Integer code = e.getCode();
@@ -68,4 +71,45 @@ public class FeishuAsyncService {
                     e.getMessage(), 1);
         }
     }
+
+    @Async // 开启填充课程异步任务
+    public void processFeishuCoursePageAsync(Client client, String docId, String authUrl,
+                                      Long videoId, Long companyId, Long courseId,
+                                      Long companyUserId, String shortLink,
+                                      FeishuAccount account) {
+        try {
+            docApiService.createTextLinkBlock(client, docId, authUrl);
+            docApiService.changeDocumentPermissions(client, docId);
+            // 成功时需要更新docId的课程内容为已填充
+            String amountKey = FEISHU_CLIENT+docId;
+            account.setCourseFillFlag(Boolean.TRUE);
+            redisCache.setCacheObject(amountKey, account,30, TimeUnit.DAYS);
+        } catch (CustomException e) {
+            // 调用原Service的方法处理异常
+            Integer code = e.getCode();
+            if (code == null) {
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 2);
+                return;
+            }
+            if (FeishuErrorCode.isAccountDisable(code)) {
+                feiShuService.disablePersonalAccount(account, e.getMessage());
+                log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
+            } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 2);
+            } else {
+                feiShuService.recordLinkError(videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        e.getMessage(), 2);
+            }
+        } catch (Exception e) {
+            feiShuService.recordLinkError(videoId, companyId, courseId,
+                    companyUserId, shortLink,
+                    e.getMessage(), 2);
+        }
+    }
 }

+ 101 - 44
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -8,7 +8,6 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.base.BaseException;
-import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.company.domain.Company;
@@ -20,11 +19,10 @@ import com.fs.course.domain.FsCourseLink;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.mapper.FsCourseLinkMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.feishu.domain.FeishuAccount;
-import com.fs.feishu.domain.FeishuLinkError;
-import com.fs.feishu.mapper.FeishuAccountMapper;
-import com.fs.feishu.mapper.FeishuLinkErrorMapper;
+import com.fs.feishu.model.FeishuLinkParam;
 import com.fs.feishu.service.FeiShuService;
 import com.fs.his.domain.FsShareMaterial;
 import com.fs.his.domain.FsSignArticle;
@@ -45,7 +43,6 @@ import com.fs.qw.service.IRedPacketService;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qwApi.param.QwExternalContactHParam;
 import com.fs.sop.domain.QwSopLogs;
-import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.impl.QwSopLogsServiceImpl;
 import com.fs.system.service.ISysConfigService;
@@ -55,7 +52,6 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
@@ -111,7 +107,7 @@ public class IpadSendServer {
     @Autowired
     private FsCourseLinkMapper courseLinkMapper;
     @Autowired
-    private FeishuLinkErrorMapper feishuLinkErrorMapper;
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
     @Autowired
     private FeiShuService feiShuService;
     @Autowired
@@ -333,10 +329,10 @@ public class IpadSendServer {
         }
     }
 
-    public void sendLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content,QwSopLogs qwSopLogs, Long externalId) {
+    public void sendLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content,QwSopLogs qwSopLogs, Long externalId,Long qwUserId) {
         if ("18".equals(content.getContentType())){
             // 完善content里面的飞书注册链接
-            completeFeishuRegisterLink(vo, content,qwSopLogs, externalId);
+            completeFeishuRegisterLink(vo, content,qwSopLogs, externalId,qwUserId);
         }
         // 文本
         LinkVo linkVo = LinkVo.builder()
@@ -354,14 +350,81 @@ public class IpadSendServer {
         }
     }
 
+    /**
+     * 完善飞书课程链接
+     * */
+    public QwSopCourseFinishTempSetting.Setting completeFeishuCourseLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content,QwSopLogs qwSopLogs,Long externalId,Long qwUserId) {
+        //获取初始的虚拟飞书链接
+        String initUrl = content.getLinkUrl();
+        try {
+            // 1. 从虚拟飞书链接提取携带的短链
+            String shortLink = extractLastPathSegment(initUrl);
+            FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(shortLink);
+            Long companyId = fsCourseLink.getCompanyId();
+            Long companyUserId = fsCourseLink.getCompanyUserId();
+            Long courseId = fsCourseLink.getCourseId();
+            Long videoId = fsCourseLink.getVideoId();
+            //查询一下待看课记录id传给前端
+            Long logId = fsCourseWatchLogMapper.selectFsCourseWatchLogIdByVideoIdAndExternalIdAndQwUserId(videoId, externalId, String.valueOf(qwUserId));
+            //从缓存取出docId;account
+            String docIdKey=FEISHU_DOC_ID + externalId+ "_" + videoId;
+            String feiShuDocId = redisCache.getCacheObject(docIdKey);
+            if (StringUtils.isBlank(feiShuDocId)) {
+                log.error("未命中飞书缓存key:{}",docIdKey);
+                //如果缓存中的docId为空,则调用飞书API直接获取完整飞书链接
+                FeishuLinkParam param=new FeishuLinkParam();
+                param.setCompanyId(companyId);
+                param.setCompanyUserId(companyUserId);
+                param.setCourseId(courseId);
+                param.setVideoId(videoId);
+                param.setLink(shortLink);
+                param.setUserId(qwSopLogs.getFsUserId());
+                String feishuRegisterLink = feiShuService.getFeishuCoursePageLink(param);
+                content.setLinkUrl(feishuRegisterLink);
+                //更新qwSopLogs
+                qwSopLogsService.updateLinkUrlInContentJson(qwSopLogs, feishuRegisterLink);
+                return content;
+            }
+            log.error("命中飞书课程链接缓存key:{}",docIdKey);
+            //根据虚拟飞书链接组装真实飞书链接
+            String completeFeiShuLinkUrl = replaceShortLinkInUrl(initUrl, feiShuDocId);
+            String amountKey = FEISHU_CLIENT+feiShuDocId;
+            //从缓存取出account
+            FeishuAccount account = redisCache.getCacheObject(amountKey);
+
+            Client client = buildClient(account);
+
+            //更新qwSopLogs
+            qwSopLogsService.updateLinkUrlInContentJson(qwSopLogs, completeFeiShuLinkUrl);
+            content.setLinkUrl(completeFeiShuLinkUrl);
+            String authUrl = buildCourseLink(companyId, companyUserId, courseId, videoId, qwSopLogs.getFsUserId(), fsCourseLink,logId);
+            // ============ 异步调用飞书API完善文档内容与文档权限 ============
+            //异步之前要做个判断,如果缓存中的课程填充标识为true,则不需要进行异步处理(因为已经填充过了)
+            if (!account.getDocumentIdFillFlag()){
+                //异步处理(同时异步更新缓存的课程内容填充标识为true)
+                asyncService.processFeishuCoursePageAsync(
+                        client, feiShuDocId, authUrl,
+                        videoId, companyId, courseId,
+                        companyUserId, shortLink,
+                        account
+                );
+            }
+
+            return content;
+        }catch (Exception e){
+            log.error("ID:{}-完善飞书课程链接异常:{}", vo.getId(), e.getMessage());
+        }
+        return content;
+    }
+
     /**
      * 完善飞书注册链接
      * */
-    public QwSopCourseFinishTempSetting.Setting completeFeishuRegisterLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content,QwSopLogs qwSopLogs,Long externalId) {
-            //初始虚拟飞书链接
+    public QwSopCourseFinishTempSetting.Setting completeFeishuRegisterLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content,QwSopLogs qwSopLogs,Long externalId,Long qwUserId) {
+            //获取初始虚拟飞书链接
             String initUrl = content.getLinkUrl();
         try {
-            // 1. 从虚拟飞书链接提取携带的短链
+            // 1. 从虚拟飞书链接提取携带的课程短链
             String shortLink = extractLastPathSegment(initUrl);
             FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(shortLink);
             Long companyId = fsCourseLink.getCompanyId();
@@ -432,6 +495,30 @@ public class IpadSendServer {
         }
     }
 
+    private String buildCourseLink(Long companyId, Long companyUserId, Long courseId,
+                                      Long videoId, Long userId, FsCourseLink fsCourseLink,Long logId) {
+        CourseConfig config = getCourseConfig();
+        Map<String, Object> courseParam = new LinkedHashMap<>();
+        courseParam.put("companyUserId", companyUserId);
+        courseParam.put("videoId", videoId);
+        courseParam.put("companyId", companyId);
+        courseParam.put("courseId", courseId);
+        courseParam.put("link", fsCourseLink.getLink());
+        courseParam.put("logId", logId);
+        courseParam.put("corpId", fsCourseLink.getCorpId());
+        courseParam.put("linkType", fsCourseLink.getLinkType());
+        courseParam.put("qwExternalId", fsCourseLink.getQwExternalId());
+        courseParam.put("qwUserId", fsCourseLink.getQwUserId());
+        try {
+            String encodedJson = URLEncoder.encode(JSONUtil.toJsonStr(courseParam), "UTF-8");
+            String fullUrl = config.getFeishuCourseDomain() +
+                    "/feishuCourse/pages_course/video?course=" + encodedJson + "&userId=" + userId;
+            return URLEncoder.encode(fullUrl, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new CustomException("看课URL拼接失败", e);
+        }
+    }
+
     private CourseConfig getCourseConfig() {
         String json = configService.selectConfigByKey("course.config");
         if (org.apache.commons.lang3.StringUtils.isBlank(json)) {
@@ -440,36 +527,6 @@ public class IpadSendServer {
         return JSONUtil.toBean(json, CourseConfig.class);
     }
 
-    private void recordLinkError(Long videoId, Long companyId, Long courseId,
-                                 Long companyUserId, String link, String errorInfo, Integer type) {
-        FeishuLinkError error = new FeishuLinkError();
-        error.setVideoId(videoId);
-        error.setCompanyId(companyId);
-        error.setCourseId(courseId);
-        error.setCompanyUserId(companyUserId);
-        error.setLink(link);
-        error.setErrorInfo(errorInfo);
-        error.setCreateTime(DateUtils.getNowDate());
-        error.setType(type);
-        FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(link);
-        if (fsCourseLink != null) {
-            error.setQwUserId(fsCourseLink.getQwUserId());
-            error.setQwExternalId(fsCourseLink.getQwExternalId());
-            error.setCorpId(fsCourseLink.getCorpId());
-        }
-        feishuLinkErrorMapper.insertFeishuLinkError(error);
-        log.info("飞书链接创建失败已记录,videoId={}, link={}, error={}", videoId, link, errorInfo);
-    }
-
-    /**
-     * 清理销售个人飞书账号缓存
-     * */
-    private void clearCompanyUserCache(Long companyId, Long companyUserId) {
-        if (companyId != null && companyUserId != null) {
-            String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
-            redisCache.deleteObject(cacheKey);
-        }
-    }
 
     // 工具方法
     private String extractLastPathSegment(String url) throws Exception {
@@ -734,7 +791,7 @@ public class IpadSendServer {
                     break;
                 case "3":
                     // 链接
-                    sendLink(vo, content, qwSopLogs,externalUserId);
+                    sendLink(vo, content, qwSopLogs,externalUserId,qwUser.getId());
                     break;
                 case "4":
                 case "10":
@@ -826,7 +883,7 @@ public class IpadSendServer {
                     break;
                 case "18":
                     // 链接
-                    sendLink(vo, content,qwSopLogs,externalUserId);
+                    sendLink(vo, content,qwSopLogs,externalUserId,qwUser.getId());
                     break;
                 case "21":
                     content.setSendStatus(0);

+ 9 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -629,4 +629,13 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "      (update_time IS NULL AND DATE(create_time) = CURDATE())  \n" +
             "  );")
     List<FsCourseWatchLog> selectTodayWatchLogs(@Param("logType") Integer logType);
+
+    /**
+     * 根据视频ID、外部联系人ID和QW用户ID查询课程观看记录(走索引)
+     * */
+    Long selectFsCourseWatchLogIdByVideoIdAndExternalIdAndQwUserId(
+            @Param("videoId") Long videoId,
+            @Param("externalContactId") Long qwExternalContactId,
+            @Param("qwUserId") String qwUserId
+    );
 }

+ 6 - 0
fs-service/src/main/java/com/fs/feishu/domain/FeishuAccount.java

@@ -43,4 +43,10 @@ public class FeishuAccount {
      * true:已填充,false:未填充
      * */
     private Boolean documentIdFillFlag=false;
+
+
+    /** 飞书文档 课程内容填写标识 redis缓存中使用,用于判断documentId是否填充过课程内容
+     * true:已填充,false:未填充
+     * */
+    private Boolean courseFillFlag=false;
 }

+ 130 - 12
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -136,10 +136,10 @@ public class FeiShuService {
                         //有视频短链+外部联系人id,需要缓存docId和account
                         if (fsCourseLink.getQwExternalId()!=null){
                             String docIdKey=FEISHU_DOC_ID + fsCourseLink.getQwExternalId()+ "_" + videoId;
-                            redisCache.setCacheObject(docIdKey, docId);
+                            redisCache.setCacheObject(docIdKey, docId,30,TimeUnit.DAYS);
                             String accountKey = FEISHU_CLIENT+docId;
                             account.setDocumentIdFillFlag(Boolean.TRUE);//这里要记录一下文档内容已填充
-                            redisCache.setCacheObject(accountKey, account);
+                            redisCache.setCacheObject(accountKey, account,30,TimeUnit.DAYS);
                         }
                     }
                     return "https://www.feishu.cn/docx/" + docId;
@@ -184,10 +184,10 @@ public class FeiShuService {
                     if (fsCourseLink.getQwExternalId()!=null){
                         String docIdKey=FEISHU_DOC_ID + fsCourseLink.getQwExternalId()+ "_" + videoId;
                         log.error("公共账号池 外部联系人id不为空,缓存飞书docId,key:{}",docIdKey);
-                        redisCache.setCacheObject(docIdKey, docId);
+                        redisCache.setCacheObject(docIdKey, docId,30,TimeUnit.DAYS);
                         String accountKey = FEISHU_CLIENT+docId;
                         account.setDocumentIdFillFlag(Boolean.TRUE);//这里要记录一下文档内容已填充
-                        redisCache.setCacheObject(accountKey, account);
+                        redisCache.setCacheObject(accountKey, account,30,TimeUnit.DAYS);
                     }
                 }
                 return "https://www.feishu.cn/docx/" + docId;
@@ -215,6 +215,127 @@ public class FeiShuService {
         return null;
     }
 
+
+
+    // ==================== 生成新版飞书课程链接(无授权流程,直接是看课界面) ====================
+
+    public String getFeishuCoursePageLink(FeishuLinkParam param) {
+        String courseLinkKey = String.format("feishu:course_link:%d:%d:%d:%d:%d:%s",
+                param.getCompanyId(), param.getCompanyUserId(),
+                param.getCourseId(), param.getVideoId(),
+                param.getUserId(), param.getLink());
+        String cached = redisCache.getCacheObject(courseLinkKey);
+        if (StringUtils.isNotBlank(cached)) {
+            return cached;
+        }
+
+        FsUserCourseVideo userCourseVideo = videoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+        if (userCourseVideo == null) {
+            throw new CustomException("视频不存在: " + param.getVideoId());
+        }
+
+        // 1. 尝试个人账号
+        List<FeishuAccount> personalAccounts = getFeishuAccountCompanyUser(param.getCompanyId(), param.getCompanyUserId());
+        if (!personalAccounts.isEmpty()) {
+            for (FeishuAccount account : personalAccounts) {
+                if (account.getStatus() != 1) continue;
+                Client client = buildClient(account);
+                try {
+                    String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
+                    String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
+                            param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink());
+                    docApiService.createIframeBlock(client, docId, courseUrl);
+                    docApiService.changeDocumentPermissions(client, docId);
+
+                    FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());
+                    //有视频短链+外部联系人id,需要缓存docId和account
+                    if (fsCourseLink.getQwExternalId()!=null){
+                        String docIdKey=FEISHU_DOC_ID + fsCourseLink.getQwExternalId()+ "_" + param.getVideoId();
+                        redisCache.setCacheObject(docIdKey, docId,30,TimeUnit.DAYS);//30天过期
+                        String accountKey = FEISHU_CLIENT+docId;
+                        account.setCourseFillFlag(Boolean.TRUE);//这里要记录一下课程内容已填充
+                        redisCache.setCacheObject(accountKey, account,30,TimeUnit.DAYS);;//30天过期
+                    }
+                    String result = "https://www.feishu.cn/docx/" + docId;
+                    redisCache.setCacheObject(courseLinkKey, result, 72, TimeUnit.HOURS);
+                    return result;
+                } catch (CustomException e) {
+                    Integer code = e.getCode();
+                    if (code == null) {
+                        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                                param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                        return null;
+                    }
+                    if (FeishuErrorCode.isAccountDisable(code)) {
+                        disablePersonalAccount(account, e.getMessage());
+                        log.warn("个人账号[{}]被禁用,尝试下一个", account.getAppId());
+                    } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                        log.warn("个人账号[{}]可重试错误,切换下一个", account.getAppId());
+                    } else {
+                        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                                param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                        return null;
+                    }
+                } catch (Exception e) {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
+                }
+            }
+            log.warn("所有个人账号失败,降级到公共账号池");
+        }
+
+        // 2. 公共账号池
+        int retries = clientPool.getAvailableCount();
+        for (int i = 0; i < retries; i++) {
+            ClientWrapper wrapper = clientPool.getClientWrapper();
+            Client client = wrapper.getClient();
+            FeishuAccount account = wrapper.getAccount();
+            try {
+                String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
+                String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
+                        param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink());
+                docApiService.createIframeBlock(client, docId, courseUrl);
+                docApiService.changeDocumentPermissions(client, docId);
+                FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(param.getLink());
+                if (fsCourseLink.getQwExternalId()!=null){
+                    String docIdKey=FEISHU_DOC_ID + fsCourseLink.getQwExternalId()+ "_" + param.getVideoId();
+                    redisCache.setCacheObject(docIdKey, docId,30,TimeUnit.DAYS);
+                    String accountKey = FEISHU_CLIENT+docId;
+                    account.setCourseFillFlag(Boolean.TRUE);//这里要记录一下课程内容已填充
+                    redisCache.setCacheObject(accountKey, account,30,TimeUnit.DAYS);
+                }
+                String result = "https://www.feishu.cn/docx/" + docId;
+                redisCache.setCacheObject(courseLinkKey, result, 72, TimeUnit.HOURS);
+                return result;
+            } catch (CustomException e) {
+                Integer code = e.getCode();
+                if (code == null) {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
+                }
+                if (FeishuErrorCode.isAccountDisable(code)) {
+                    clientPool.disableAccount(account, e.getMessage());
+                    log.warn("公共账号被禁用,尝试下一个");
+                } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                    log.warn("公共账号可重试错误,切换下一个");
+                } else {
+                    recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                            param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                    return null;
+                }
+            } catch (Exception e) {
+                recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                        param.getCompanyUserId(), param.getLink(), e.getMessage(), 2);
+                return null;
+            }
+        }
+        recordLinkError(param.getVideoId(), param.getCompanyId(), param.getCourseId(),
+                param.getCompanyUserId(), param.getLink(), "所有公共账号不可用", 2);
+        return null;
+    }
+
     // ==================== 生成飞书课程链接 ====================
 
     public String getFeishuCourseNewLink(FeishuLinkParam param) {
@@ -234,8 +355,6 @@ public class FeiShuService {
         if (userCourseVideo == null) {
             throw new CustomException("视频不存在: " + param.getVideoId());
         }
-        Integer questionFlag = (StringUtils.isNotBlank(userCourseVideo.getQuestionBankId()) &&
-                !"null".equals(userCourseVideo.getQuestionBankId())) ? 1 : 0;
 
         // 1. 尝试个人账号
         List<FeishuAccount> personalAccounts = getFeishuAccountCompanyUser(param.getCompanyId(), param.getCompanyUserId());
@@ -246,11 +365,11 @@ public class FeiShuService {
                 try {
                     String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
                     String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
-                            param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
+                            param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink());
                     docApiService.createIframeBlock(client, docId, courseUrl);
                     docApiService.changeDocumentPermissions(client, docId);
                     String result = "https://www.feishu.cn/docx/" + docId;
-                    redisCache.setCacheObject(cacheKey, result, 24, TimeUnit.HOURS);
+                    redisCache.setCacheObject(cacheKey, result, 72, TimeUnit.HOURS);
                     return result;
                 } catch (CustomException e) {
                     Integer code = e.getCode();
@@ -287,11 +406,11 @@ public class FeiShuService {
             try {
                 String docId = docApiService.createDocument(client, userCourseVideo.getTitle());
                 String courseUrl = buildCourseNewLink(param.getCompanyId(), param.getCompanyUserId(),
-                        param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink(), questionFlag);
+                        param.getCourseId(), param.getVideoId(), param.getUserId(), param.getLink());
                 docApiService.createIframeBlock(client, docId, courseUrl);
                 docApiService.changeDocumentPermissions(client, docId);
                 String result = "https://www.feishu.cn/docx/" + docId;
-                redisCache.setCacheObject(cacheKey, result, 24, TimeUnit.HOURS);
+                redisCache.setCacheObject(cacheKey, result, 72, TimeUnit.HOURS);
                 return result;
             } catch (CustomException e) {
                 Integer code = e.getCode();
@@ -369,7 +488,7 @@ public class FeiShuService {
     // ==================== URL构建 ====================
 
     private String buildCourseNewLink(Long companyId, Long companyUserId, Long courseId,
-                                      Long videoId, Long userId, String link, Integer questionFlag) {
+                                      Long videoId, Long userId, String link) {
         CourseConfig config = getCourseConfig();
         Map<String, Object> courseParam = new LinkedHashMap<>();
         courseParam.put("companyUserId", companyUserId);
@@ -377,7 +496,6 @@ public class FeiShuService {
         courseParam.put("companyId", companyId);
         courseParam.put("courseId", courseId);
         courseParam.put("link", link);
-        courseParam.put("questionFlag", questionFlag);
         FsCourseLink fsCourseLink = courseLinkMapper.selectFsCourseLinkByLink(link);
         if (fsCourseLink == null) {
             throw new CustomException("课程链接已过期");

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

@@ -1929,7 +1929,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     Map<String, String> feishuH5Link = createFeishuH5Link(st, param.getCorpId(), dataTime, courseId, videoId, qwUserId, companyUserId, companyId, externalId, config);
                     //获取生成的课程短链码
                     String shortLink = feishuH5Link.get("link");
-
+                    log.info("课程短链接生成成功:{}", shortLink);
                     //3.调用生成飞书注册授权链接
 //                    String feishuLink = feiShuService.getFeishuRegisterLink(videoId.longValue(), companyIdLong, courseId.longValue(), companyUserIdLong,shortLink);
                     String feishuLink = feiShuService.getNewFeishuRegisterLink(videoId.longValue(), companyIdLong, courseId.longValue(), companyUserIdLong,shortLink);

+ 9 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1156,6 +1156,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by log_id desc limit 1
     </select>
 
+    <select id="selectFsCourseWatchLogIdByVideoIdAndExternalIdAndQwUserId"
+            resultType="Long">
+        SELECT log_id
+        FROM fs_course_watch_log
+        WHERE video_id = #{videoId}
+          AND qw_external_contact_id = #{externalContactId}
+          AND qw_user_id = #{qwUserId}
+    </select>
+
     <update id="batchUpdateAnswerSendStatus" parameterType="java.util.List">
         UPDATE fs_course_watch_log
         SET answer_send_status = 1

+ 1 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -183,6 +183,7 @@ public class CourseFsUserController extends AppBaseController {
         if (StringUtils.isBlank(param.getLink())){
             return R.error("飞书课程短链不能为空");
         }
+        logger.info("【获取飞书课程完整链接入参】:{}",param);
         String feishuCourseNewLink = feiShuService.getFeishuCourseNewLink(param);
         return R.ok().put("data", feishuCourseNewLink);
     }