Procházet zdrojové kódy

定时生成飞书docId缓存

wjj před 2 dny
rodič
revize
13f557ced4

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

@@ -11,11 +11,11 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.app.domain.FsAppRole;
 import com.fs.app.mapper.FsAppRoleMapper;
 import com.fs.common.exception.CustomException;
-import com.fs.company.domain.CompanyMiniapp;
 import com.fs.company.service.ICompanyMiniappService;
 import com.fs.company.service.ICompanySmsService;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
@@ -24,6 +24,13 @@ import com.fs.fastGpt.domain.FastgptChatVoiceHomo;
 import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
+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.service.FeishuClientPool;
+import com.fs.feishu.service.FeishuDocApiService;
+import com.fs.feishu.util.FeishuErrorCode;
 import com.fs.hisStore.param.FsStoreOrderExpressEditParam;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
@@ -114,6 +121,7 @@ import com.fs.wx.service.ShippingService;
 import com.fs.xiaoshouyi.dto.TokenResponse;
 import com.fs.xiaoshouyi.service.XiaoShouYiOAuthService;
 import com.google.gson.Gson;
+import com.lark.oapi.Client;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
 import org.apache.commons.lang3.StringUtils;
@@ -387,6 +395,10 @@ public class Task {
 
     @Autowired
     private IFsCourseReissueConfigService fsCourseReissueConfigService;
+    @Autowired
+    private FsUserCourseMapper fsUserCourseMapper;
+    @Autowired
+    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
 
     public void syncExpressToWx() {
         List<FsWxExpressTask> fsWxExpressTasks = fsWxExpressTaskMapper.selectPendingData();
@@ -3184,4 +3196,209 @@ public class Task {
         return link.getRealLink();
     }
 
+    private static final String FEISHU_COMPANY_USER_KEY = "feishu:account_company_user:";
+    @Autowired
+    private FeishuAccountMapper feishuAccountMapper;
+    @Autowired
+    private FeishuDocApiService docApiService;
+    @Autowired
+    private FeishuClientPool clientPool;
+    @Autowired
+    private FeishuLinkErrorMapper feishuLinkErrorMapper;
+    public void generateFeiShuRedisCache() {
+
+        //课程id数据
+        List<Long> courseIds = fsUserCourseMapper.selectPrivateFsUserCourseIds();
+        if (CollectionUtils.isEmpty(courseIds)) {
+            logger.error("未查询到私教课程");
+            return;
+        }
+
+        //小节数据
+        List<FsUserCourseVideo> fsUserCourseVideos = fsUserCourseVideoMapper.selectVideoByCourseIds(courseIds);
+        if (CollectionUtils.isEmpty(fsUserCourseVideos)) {
+            logger.error("未查询到小节数据");
+            return;
+        }
+
+        //营期中企业外部联系人
+        List<Long> externalIds = sopUserLogsInfoMapper.selectExternalIds();
+        if (CollectionUtils.isEmpty(externalIds)) {
+            logger.error("未查询到营期中企业外部联系人");
+            return;
+        }
+
+        //外部联系人数据
+        List<QwExternalContact> contacts = qwExternalContactMapper.selectQwExternalContactByIds(externalIds);
+        if (CollectionUtils.isEmpty(contacts)) {
+            logger.error("未查询到外部联系人数据");
+            return;
+        }
+
+        CourseConfig config = getCourseConfig();
+        if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
+            logger.error("未开启飞书看课");
+            return;
+        }
+
+        for (QwExternalContact contact : contacts) {
+            for (FsUserCourseVideo video : fsUserCourseVideos) {
+                try {
+                    String key = "fei_shu_doc_id:" + contact.getId() + "_" + video.getVideoId();
+                    String feiShuDocId = redisCache.getCacheObject(key);
+                    if (StringUtils.isNotBlank(feiShuDocId)) {
+                        continue;
+                    }
+                    feiShuDocId = getFeiShuDocId(video, contact.getCompanyId(), contact.getCompanyUserId());
+                    redisCache.setCacheObject("fei_shu_doc_id:" + contact.getId() + "_" + video.getVideoId(), feiShuDocId);
+                } catch (Exception e) {
+                    logger.error("生成飞书文档失败, 课程id: {}, 外部联系人id: {}, 错误信息: {}", video.getCourseId(), contact.getExternalUserId(), e.getMessage());
+                }
+            }
+        }
+    }
+
+    public List<FeishuAccount> getFeiShuAccountCompanyUser(Long companyId, Long companyUserId) {
+        String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
+        String cached = redisCache.getCacheObject(cacheKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                return JSONUtil.parseArray(cached).toList(FeishuAccount.class);
+            } catch (Exception e) {
+                log.warn("缓存数据解析失败,key: {}, 将清除缓存", cacheKey, e);
+                redisCache.deleteObject(cacheKey);
+            }
+        }
+        FeishuAccount account = new FeishuAccount();
+        account.setCompanyId(companyId);
+        account.setCompanyUserId(companyUserId);
+        account.setStatus(1);
+        List<FeishuAccount> feiShuAccounts = feishuAccountMapper.selectFeishuAccountList(account);
+        if (feiShuAccounts.isEmpty()) {
+            redisCache.setCacheObject(cacheKey, "[]", 1, TimeUnit.MINUTES);
+        } else {
+            redisCache.setCacheObject(cacheKey, JSONUtil.toJsonStr(feiShuAccounts), 1, TimeUnit.DAYS);
+        }
+        return feiShuAccounts;
+    }
+
+    private void disablePersonalAccount(FeishuAccount account, String errorMsg) {
+        feishuAccountMapper.updateStatusAndErrorMsg(account.getId(), 0, errorMsg);
+        clearCompanyUserCache(account.getCompanyId(), account.getCompanyUserId());
+    }
+
+    private void clearCompanyUserCache(Long companyId, Long companyUserId) {
+        if (companyId != null && companyUserId != null) {
+            String cacheKey = FEISHU_COMPANY_USER_KEY + companyId + ":" + companyUserId;
+            redisCache.deleteObject(cacheKey);
+        }
+    }
+
+    private CourseConfig getCourseConfig() {
+        String json = sysConfigService.selectConfigByKey("course.config");
+        if (StringUtils.isBlank(json)) {
+            throw new CustomException("飞书看课配置为空");
+        }
+        return JSONUtil.toBean(json, CourseConfig.class);
+    }
+
+    private Client buildClient(FeishuAccount account) {
+        return Client.newBuilder(account.getAppId(), account.getAppSecret())
+                .logReqAtDebug(true)
+                .build();
+    }
+
+    public String getFeiShuDocId(FsUserCourseVideo video, Long companyId,
+                                        Long companyUserId) {
+
+
+        // 1. 尝试销售个人账号创建文档
+        List<FeishuAccount> personalAccounts = getFeiShuAccountCompanyUser(companyId, companyUserId);
+        if (!personalAccounts.isEmpty()) {
+            for (FeishuAccount account : personalAccounts) {
+                if (account.getStatus() != 1) continue;
+                Client client = buildClient(account);
+                try {
+                    String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
+                    redisCache.setCacheObject("fei_shu_client:" + docId, client);
+                    return docId;
+
+                } catch (CustomException e) {
+                    Integer code = e.getCode();
+                    if (code == null) {
+                        recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                        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(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                        return null;
+                    }
+                } catch (Exception e) {
+                    recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                    return null;
+                }
+            }
+            log.warn("所有个人账号失败,降级到公共账号池");
+        }
+
+        // 2. 公共账号池
+        int retries = clientPool.getAvailableCount();
+        for (int i = 0; i < retries; i++) {
+            Client client = clientPool.getClient();
+            try {
+                String docId = docApiService.createDocument(client, video.getTitle() + "-注册");
+                redisCache.setCacheObject("fei_shu_client:" + docId, client);
+                return docId;
+
+            } catch (CustomException e) {
+                Integer code = e.getCode();
+                if (code == null) {
+                    recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                    return null;
+                }
+                if (FeishuErrorCode.isAccountDisable(code)) {
+                    clientPool.disableClient(client, e.getMessage());
+                    log.warn("公共账号被禁用,尝试下一个");
+                } else if (FeishuErrorCode.isRateLimit(code) || FeishuErrorCode.isInternalRetryable(code)) {
+                    log.warn("公共账号可重试错误,切换下一个");
+                } else {
+                    recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                    return null;
+                }
+            } catch (Exception e) {
+                recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", e.getMessage(), 1);
+                return null;
+            }
+        }
+        recordLinkError(video.getVideoId(), companyId, video.getCourseId(), companyUserId, "定时生成的缓存docId", "所有公共账号不可用", 1);
+        return null;
+    }
+
+    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);
+    }
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java

@@ -315,4 +315,7 @@ public interface FsUserCourseMapper
 
     @Select("SELECT course_name,course_id,is_private FROM fs_user_course WHERE is_del = 0 AND is_show = 1 ORDER BY is_private DESC ")
     List<FsUserCourse> selectAllFsUserCourseListOptions();
+
+    @Select("SELECT course_id FROM fs_user_course WHERE is_private = 1 AND is_del = 0")
+    List<Long> selectPrivateFsUserCourseIds();
 }

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

@@ -239,4 +239,6 @@ public interface FsUserCourseVideoMapper
     @Select("select * from fs_video_resource where hsy_vid is not null")
     List<FsVideoResource> selectVideoByVid();
 
+    List<FsUserCourseVideo> selectVideoByCourseIds(@Param("courseIds") List<Long> courseIds);
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsInfoMapper.java

@@ -291,4 +291,8 @@ public interface SopUserLogsInfoMapper {
      */
     @DataSource(DataSourceType.SOP)
     List<SopUserLogsInfo> querySopUserLogsRecord(@Param("sopId") String sopId, @Param("isBuy") Integer isBuy);
+
+    @DataSource(DataSourceType.SOP)
+    @Select("SELECT external_id FROM sop_user_logs_info GROUP BY external_id")
+    List<Long> selectExternalIds();
 }

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

@@ -399,4 +399,13 @@
         </where>
         limit 1
     </select>
+
+    <select id="selectVideoByCourseIds" resultMap="FsUserCourseVideoResult">
+        <include refid="selectFsUserCourseVideoVo"/>
+        WHERE is_del = 0
+        AND course_id IN
+        <foreach collection="courseIds" item="courseId" open="(" close=")" separator=",">
+            #{courseId}
+        </foreach>
+    </select>
 </mapper>