Ver Fonte

一键群发和企微task分片处理

xw há 1 dia atrás
pai
commit
0666e7ff9c

+ 28 - 12
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -26,6 +26,7 @@ import com.fs.course.mapper.*;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
 import com.fs.feishu.service.FeiShuService;
+import com.fs.feishu.util.FeishuDirectDocShardAllocator;
 import com.fs.live.domain.LiveWatchLog;
 import com.fs.live.mapper.LiveWatchLogMapper;
 import com.fs.qw.domain.*;
@@ -461,9 +462,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
 
         CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
+        FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forAutoSop(config);
         for (SopUserLogsVo logVo : userLogsVos) {
             processUserLogAsync(logVo, ruleTimeVO, rulesList, userLogsLatch, currentTime, groupChatMap,qwCompany.getMiniAppId(),
-                    config,miniMap,companies, deptMiniAppMap, companyUserDeptMap);
+                    config,miniMap,companies, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);
         }
 
         // 等待所有用户日志处理完成
@@ -485,9 +487,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     public void processUserLogAsync(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
                                     CountDownLatch latch, LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,
                                     String miniAppId,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                    List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
+                                    List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                    FeishuDirectDocShardAllocator feishuShardAllocator) {
         try {
-            processUserLog(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap, miniAppId, config,miniMap,companies, deptMiniAppMap, companyUserDeptMap);
+            processUserLog(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap, miniAppId, config,miniMap,companies, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);
         } catch (Exception e) {
             log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
         } finally {
@@ -499,7 +502,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private void processUserLog(SopUserLogsVo logVo, QwSopRuleTimeVO ruleTimeVO, List<QwSopTempRules> tempSettings,
                                 LocalDateTime currentTime, Map<String, QwGroupChat> groupChatMap,String miniAppId,
                                 CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
-                                List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
+                                List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                FeishuDirectDocShardAllocator feishuShardAllocator) {
         try {
 
             LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
@@ -756,7 +760,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                         insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
                                 companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName(),
-                                groupChatMap, miniAppId,config,miniMap, sendMsgType,companies, deptMiniAppMap, companyUserDeptMap);
+                                groupChatMap, miniAppId,config,miniMap, sendMsgType,companies, deptMiniAppMap, companyUserDeptMap,
+                                feishuShardAllocator);
 
                     }
                 } catch (Exception e) {
@@ -802,7 +807,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                    String qwUserId,String companyUserId,String companyId,String welcomeText,String qwUserName,
                                    Map<String, QwGroupChat> groupChatMap,String miniAppId,CourseConfig config,
                                    Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
-                                   List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
+                                   List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                   FeishuDirectDocShardAllocator feishuShardAllocator) {
         String formattedSendTime = sendTime.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .format(DATE_TIME_FORMATTER);
@@ -882,7 +888,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null,null);
             handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                     type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
-                    null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies,liveId, deptMiniAppMap, companyUserDeptMap);
+                    null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies,liveId, deptMiniAppMap, companyUserDeptMap,
+                    feishuShardAllocator);
             }
 //            if (content.getIndex() == 0) {
 //                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
@@ -931,7 +938,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, contactId.getExternalContactId(), externalUserName, fsUserId, isOfficial, contactId.getExternalId(),contactId.getIsDaysNotStudy());
                     handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                             type, qwUserId, companyUserId, companyId, externalId, welcomeText, qwUserName, fsUserId, false, miniAppId,
-                            null,config, miniMap, grade, sendMsgType,companies,liveId, deptMiniAppMap, companyUserDeptMap);
+                            null,config, miniMap, grade, sendMsgType,companies,liveId, deptMiniAppMap, companyUserDeptMap,
+                            feishuShardAllocator);
                 } catch (Exception e) {
                     e.printStackTrace();
                     log.error("处理 externalContactId {} 时发生异常", contactId, e);
@@ -1091,7 +1099,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                       QwGroupChat groupChat,CourseConfig config,
                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                       Integer grade, Integer sendMsgType ,List<Company> companies ,Long liveId,
-                                      Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
+                                      Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                      FeishuDirectDocShardAllocator feishuShardAllocator) {
         switch (type) {
             case 1:
                 handleNormalMessage(sopLogs, content,companyUserId,companyId,isGroupChat,qwUserId,groupChat,externalId,logVo);
@@ -1099,7 +1108,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             case 2:
                 handleCourseMessage(sopLogs, content, logVo, sendTime, courseId, videoId,
                         qwUserId, companyUserId, companyId, externalId, welcomeText,qwUserName, fsUserId,
-                        isGroupChat, miniAppId, groupChat,config,miniMap, grade, sendMsgType,companies, deptMiniAppMap, companyUserDeptMap);
+                        isGroupChat, miniAppId, groupChat,config,miniMap, grade, sendMsgType,companies, deptMiniAppMap, companyUserDeptMap,
+                        feishuShardAllocator);
                 break;
             case 3:
                 handleOrderMessage(sopLogs, content);
@@ -1317,7 +1327,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                      String companyId, String externalId, String welcomeText, String qwUserName,
                                      Long fsUserId, boolean isGroupChat, String miniAppId, QwGroupChat groupChat,CourseConfig config,Map<Long,
                     Map<Integer, List<CompanyMiniapp>>> miniMap,Integer grade, Integer sendMsgType,
-                                     List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
+                                     List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                     FeishuDirectDocShardAllocator feishuShardAllocator) {
         QwExternalContact contact = null;
         if(logVo.getExternalId() != null){
             contact = qwExternalContactMapper.selectById(logVo.getExternalId());
@@ -1613,8 +1624,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                 qwUserId, companyUserId, companyId, externalIdLong, chatIdForFeishu, cachedCourseConfig);
                         String shortLink = feiShuLinkMap.get("link");
 
+                        Integer directDocShard = null;
+                        if (feishuShardAllocator != null && !FeiShuService.isAuthRequired(setting.getFeishuNeedAuth())) {
+                            directDocShard = feishuShardAllocator.nextShard(companyUserIdLong, videoId, courseId,
+                                    setting.getFeishuAccountId());
+                        }
                         String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyIdLong, courseId, companyUserIdLong,
-                                shortLink, setting.getFeishuAccountId(), setting.getFeishuNeedAuth());
+                                shortLink, setting.getFeishuAccountId(), setting.getFeishuNeedAuth(), directDocShard);
                         if (StringUtils.isEmpty(feishuLink)) {
                             log.error("生成飞书注册链接失败,跳过本条: sopId={}, videoId={}, externalId={}",
                                     logVo.getSopId(), videoId, externalId);

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

@@ -123,6 +123,17 @@ public class CourseConfig implements Serializable {
      */
     private Boolean pilotCourseSkipRepeatLimit;
 
+    /**
+     * 飞书免授权 SOP 一键群发:每个飞书文档承载的最大客户数,超出后轮换新文档。默认 30。
+     */
+    private Integer feishuDirectDocMaxCustomersPerShard;
+
+    /**
+     * 飞书免授权 SOP 自动定时发课:每个飞书文档承载的最大客户数,超出后轮换新文档。默认 50。
+     * 未配置时回退 feishuDirectDocMaxCustomersPerShard,仍无则使用 50。
+     */
+    private Integer feishuDirectDocMaxCustomersPerShardAuto;
+
 
     @Data
     public static class DisabledTimeVo{

+ 33 - 7
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -74,10 +74,21 @@ public class FeiShuService {
     public String resolveFeishuSendLink(Long videoId, Long companyId, Long courseId,
                                         Long companyUserId, String shortLink, Long feishuAccountId,
                                         Integer feishuNeedAuth) {
+        return resolveFeishuSendLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId,
+                feishuNeedAuth, null);
+    }
+
+    /**
+     * @param directDocShard SOP 群发/自动发课分片序号,null 表示非分片场景(沿用按天复用逻辑)
+     */
+    public String resolveFeishuSendLink(Long videoId, Long companyId, Long courseId,
+                                        Long companyUserId, String shortLink, Long feishuAccountId,
+                                        Integer feishuNeedAuth, Integer directDocShard) {
         if (isAuthRequired(feishuNeedAuth)) {
             return getFeishuRegisterLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId);
         }
-        return getFeishuDirectCourseLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId);
+        return getFeishuDirectCourseLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId,
+                directDocShard);
     }
 
     /**
@@ -145,6 +156,15 @@ public class FeiShuService {
      */
     public String getFeishuDirectCourseLink(Long videoId, Long companyId, Long courseId,
                                             Long companyUserId, String shortLink, Long feishuAccountId) {
+        return getFeishuDirectCourseLink(videoId, companyId, courseId, companyUserId, shortLink, feishuAccountId, null);
+    }
+
+    /**
+     * @param directDocShard SOP 分片序号,null 时按销售+课程+账号+日期复用同一文档
+     */
+    public String getFeishuDirectCourseLink(Long videoId, Long companyId, Long courseId,
+                                            Long companyUserId, String shortLink, Long feishuAccountId,
+                                            Integer directDocShard) {
         CourseConfig config = getCourseConfig();
         if (!Boolean.TRUE.equals(config.getEnableFeishuNewLink())) {
             throw new CustomException("未开启飞书看课");
@@ -166,7 +186,7 @@ public class FeiShuService {
             FeishuClientHolder holder = clientPool.getClient(companyUserId, account.getId());
             try {
                 String docUrl = getOrCreateDirectCourseDocUrl(holder, account.getId(), companyUserId, companyId,
-                        courseId, videoId, shortLink, questionFlag, userCourseVideo.getTitle());
+                        courseId, videoId, shortLink, questionFlag, userCourseVideo.getTitle(), directDocShard);
                 updateCourseLinkFeishuAccount(shortLink, account.getId());
                 return docUrl;
             } catch (CustomException e) {
@@ -297,8 +317,8 @@ public class FeiShuService {
     private String getOrCreateDirectCourseDocUrl(FeishuClientHolder holder, Long feishuAccountId,
                                                  Long companyUserId, Long companyId, Long courseId,
                                                  Long videoId, String shortLink, int questionFlag,
-                                                 String documentTitle) throws Exception {
-        String cacheKey = buildDirectDocCacheKey(companyUserId, videoId, courseId, feishuAccountId);
+                                                 String documentTitle, Integer directDocShard) throws Exception {
+        String cacheKey = buildDirectDocCacheKey(companyUserId, videoId, courseId, feishuAccountId, directDocShard);
         String cached = redisCache.getCacheObject(cacheKey);
         if (StringUtils.isNotBlank(cached)) {
             log.debug("复用飞书免授权文档: companyUserId={}, videoId={}, accountId={}",
@@ -354,10 +374,16 @@ public class FeiShuService {
         }
     }
 
-    private String buildDirectDocCacheKey(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
+    private String buildDirectDocCacheKey(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId,
+                                           Integer directDocShard) {
         String dateKey = LocalDate.now(ZoneId.systemDefault()).format(DIRECT_DOC_DATE_FORMAT);
-        return String.format("%s%d:%d:%d:%d:%s",
-                FEISHU_DIRECT_DOC_CACHE_PREFIX, companyUserId, videoId, courseId, feishuAccountId, dateKey);
+        if (directDocShard == null) {
+            return String.format("%s%d:%d:%d:%d:%s",
+                    FEISHU_DIRECT_DOC_CACHE_PREFIX, companyUserId, videoId, courseId, feishuAccountId, dateKey);
+        }
+        return String.format("%s%d:%d:%d:%d:%s:%d",
+                FEISHU_DIRECT_DOC_CACHE_PREFIX, companyUserId, videoId, courseId, feishuAccountId, dateKey,
+                directDocShard);
     }
 
     private int getDirectDocCacheTtlSeconds() {

+ 66 - 0
fs-service/src/main/java/com/fs/feishu/util/FeishuDirectDocShardAllocator.java

@@ -0,0 +1,66 @@
+package com.fs.feishu.util;
+
+import com.fs.course.config.CourseConfig;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * SOP群发/自动执行场景,飞书直链文档分片分配器,控制单个分片承载的客户数量,实现租户维度文档分片负载均衡
+ */
+public class FeishuDirectDocShardAllocator {
+
+    private static final int DEFAULT_MAX_CUSTOMERS_PER_SHARD_MASS_SEND = 30;
+    private static final int DEFAULT_MAX_CUSTOMERS_PER_SHARD_AUTO = 50;
+
+    private final int maxCustomersPerShard;
+    private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
+
+    private FeishuDirectDocShardAllocator(int maxCustomersPerShard) {
+        this.maxCustomersPerShard = maxCustomersPerShard > 0 ? maxCustomersPerShard : DEFAULT_MAX_CUSTOMERS_PER_SHARD_MASS_SEND;
+    }
+
+    /** 群发场景分配器 */
+    public static FeishuDirectDocShardAllocator forMassSend(CourseConfig config) {
+        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForMassSend(config));
+    }
+
+    /** SOP自动执行场景分配器 */
+    public static FeishuDirectDocShardAllocator forAutoSop(CourseConfig config) {
+        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForAuto(config));
+    }
+
+    public static int resolveMaxCustomersPerShardForMassSend(CourseConfig config) {
+        if (config == null || config.getFeishuDirectDocMaxCustomersPerShard() == null
+                || config.getFeishuDirectDocMaxCustomersPerShard() <= 0) {
+            return DEFAULT_MAX_CUSTOMERS_PER_SHARD_MASS_SEND;
+        }
+        return config.getFeishuDirectDocMaxCustomersPerShard();
+    }
+
+    public static int resolveMaxCustomersPerShardForAuto(CourseConfig config) {
+        if (config != null && config.getFeishuDirectDocMaxCustomersPerShardAuto() != null
+                && config.getFeishuDirectDocMaxCustomersPerShardAuto() > 0) {
+            return config.getFeishuDirectDocMaxCustomersPerShardAuto();
+        }
+        if (config != null && config.getFeishuDirectDocMaxCustomersPerShard() != null
+                && config.getFeishuDirectDocMaxCustomersPerShard() > 0) {
+            return config.getFeishuDirectDocMaxCustomersPerShard();
+        }
+        return DEFAULT_MAX_CUSTOMERS_PER_SHARD_AUTO;
+    }
+
+    /**
+     * 获取当前客户应当使用的分片编号,从 0 开始
+     */
+    public int nextShard(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
+        String key = buildKey(companyUserId, videoId, courseId, feishuAccountId);
+        int customerIndex = counters.computeIfAbsent(key, ignored -> new AtomicInteger(0)).getAndIncrement();
+        return customerIndex / maxCustomersPerShard;
+    }
+
+    private static String buildKey(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
+        return companyUserId + ":" + videoId + ":" + courseId + ":" + (feishuAccountId != null ? feishuAccountId : 0);
+    }
+}

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

@@ -33,6 +33,7 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
 import com.fs.feishu.service.FeiShuService;
+import com.fs.feishu.util.FeishuDirectDocShardAllocator;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.live.domain.LiveWatchLog;
@@ -540,6 +541,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         if (config == null) {
             return R.error().put("msg","课程默认配置为空,请联系管理员");
         }
+        FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forMassSend(config);
 
         if (StringUtil.strIsNullOrEmpty(param.getCorpId())){
             return R.error().put("msg","企业编号为空,不能创建一键群发");
@@ -777,7 +779,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                         companyUserIdLong, shortCode, st.getFeishuAccountId(), st.getFeishuNeedAuth(),
                                         param.getSopId(), null, externalUserId, vo.getFsUserId(),
                                         qwUser.getQwUserId(), qwUser.getQwUserName(), companyUserId, companyId,
-                                        param.getCorpId(), createTime, param.getStartTime());
+                                        param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                                 if (StringUtils.isNotEmpty(feishuLink)) {
                                     st.setLinkUrl(feishuLink);
                                 }
@@ -1006,7 +1008,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                         companyUserIdLongGroup, shortCodeGroup, st.getFeishuAccountId(), st.getFeishuNeedAuth(),
                                         param.getSopId(), null, null, null,
                                         qwUser.getQwUserId(), qwUser.getQwUserName(), String.valueOf(companyUserIdLongGroup), String.valueOf(companyIdLongGroup),
-                                        param.getCorpId(), createTime, param.getStartTime());
+                                        param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                                 if (StringUtils.isNotEmpty(feishuLinkGroup)) {
                                     st.setLinkUrl(feishuLinkGroup);
                                 }
@@ -1351,7 +1353,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                     companyUserIdLong, shortCode, st.getFeishuAccountId(), st.getFeishuNeedAuth(),
                                     param.getSopId(), item.getUserLogsId(), item.getExternalId(), item.getFsUserId(),
                                     qwUserId, qwUser.getQwUserName(), companyUserId, companyId,
-                                    param.getCorpId(), createTime, param.getStartTime());
+                                    param.getCorpId(), createTime, param.getStartTime(), feishuShardAllocator);
                             if (StringUtils.isNotEmpty(feishuLink)) {
                                 st.setLinkUrl(feishuLink);
                             }
@@ -1480,6 +1482,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         if (config == null) {
             return R.error("课程默认配置为空,请联系管理员");
         }
+        FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forMassSend(config);
 
         if (StringUtil.strIsNullOrEmpty(param.getCorpId())){
             return R.error("企业编号为空,不能创建一键群发");
@@ -1547,7 +1550,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
                 if (qwUser.getCompanyUserId()!=null && qwUser.getCompanyId()!=null){
                     List<QwSopLogs> sopLogsList = processInsertSopUserLogsInfo(logs, qwUser, param, words, config, qwCompany, finalSort,
-                            finalSendType,miniMap,companies, deptMiniAppMap, companyUserDeptMap);
+                            finalSendType,miniMap,companies, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);
 
                     //批量插入 发送记录
                     if (!sopLogsList.isEmpty()) {
@@ -1571,7 +1574,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                                          SendUserLogsInfoMsgParam param,List<FastGptChatReplaceWords> words,
                                                          CourseConfig config,QwCompany qwCompany,int finalSort,int finalSendType,
                                                          Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,List<Company> companies,
-                                                         Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap ){
+                                                         Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                                         FeishuDirectDocShardAllocator feishuShardAllocator ){
 
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
@@ -1627,7 +1631,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 case 5:
                 case 20:
                     List<QwSopCourseFinishTempSetting.Setting> list = processSetting(item,qwUser, param, words, config, qwCompany,companyUserId,companyId,
-                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs, deptMiniAppMap, companyUserDeptMap);
+                            contact,dataTime, finalDomainName,miniMap,companies,sopLogs, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator);
                     setting.setSetting(list);
                     break;
                 case 9:
@@ -1693,7 +1697,8 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                                                       QwExternalContact contact,Date dataTime,String domainName,
                                                                       Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
                                                                       List<Company> companies,QwSopLogs sopLogs,
-                                                                      Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap ){
+                                                                      Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
+                                                                      FeishuDirectDocShardAllocator feishuShardAllocator ){
         List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(),QwSopCourseFinishTempSetting.Setting.class);
 
         for (QwSopCourseFinishTempSetting.Setting st : list) {
@@ -1977,7 +1982,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             companyUserIdLong, shortLink, st.getFeishuAccountId(), st.getFeishuNeedAuth(),
                             item.getSopId(), item.getUserLogsId(), externalId, item.getFsUserId(),
                             String.valueOf(qwUser.getId()), qwUser.getQwUserName(), companyUserId, companyId,
-                            param.getCorpId(), dataTime, item.getStartTime());
+                            param.getCorpId(), dataTime, item.getStartTime(), feishuShardAllocator);
                     if (StringUtils.isNotEmpty(feishuLink)) {
                         String txt18 = StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText();
                         String customerTitle = contact == null ? "同学" :
@@ -2516,10 +2521,15 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                                Integer feishuNeedAuth, String sopId, String userLogsId,
                                                Long externalId, Long fsUserId, String qwUserId, String qwUserName,
                                                String companyUserIdStr, String companyIdStr, String corpId,
-                                               Date sendTime, String elementTime) {
+                                               Date sendTime, String elementTime,
+                                               FeishuDirectDocShardAllocator feishuShardAllocator) {
         try {
+            Integer directDocShard = null;
+            if (feishuShardAllocator != null && !FeiShuService.isAuthRequired(feishuNeedAuth)) {
+                directDocShard = feishuShardAllocator.nextShard(companyUserId, videoId, courseId, feishuAccountId);
+            }
             String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyId, courseId, companyUserId,
-                    shortLink, feishuAccountId, feishuNeedAuth);
+                    shortLink, feishuAccountId, feishuNeedAuth, directDocShard);
             if (StringUtils.isEmpty(feishuLink)) {
                 log.error("生成飞书注册链接失败,跳过本条: sopId={}, videoId={}, externalId={}",
                         sopId, videoId, externalId);