فهرست منبع

一键群发强制刷新飞书链接

xw 1 روز پیش
والد
کامیت
66a9e269f1

+ 29 - 0
fs-service/src/main/java/com/fs/feishu/service/FeiShuService.java

@@ -29,9 +29,11 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -374,6 +376,33 @@ public class FeiShuService {
         }
     }
 
+    /**
+     * 一键群发补发前清除当天已缓存的免授权飞书文档(含各 shard 及 lock),避免复用已被风控的旧链接。
+     */
+    public void invalidateDirectDocCache(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
+        if (companyUserId == null || videoId == null) {
+            return;
+        }
+        Long resolvedAccountId = feishuAccountService.resolveSendAccountId(companyUserId, feishuAccountId);
+        String dateKey = LocalDate.now(ZoneId.systemDefault()).format(DIRECT_DOC_DATE_FORMAT);
+        String docKeyPrefix = String.format("%s%d:%d:%d:%d:%s",
+                FEISHU_DIRECT_DOC_CACHE_PREFIX, companyUserId, videoId,
+                courseId != null ? courseId : 0L, resolvedAccountId, dateKey);
+        String lockKeyPrefix = FEISHU_DIRECT_DOC_LOCK_PREFIX + docKeyPrefix;
+
+        Set<String> keysToDelete = new HashSet<>();
+        keysToDelete.addAll(redisCache.scanKeys(docKeyPrefix + "*"));
+        keysToDelete.addAll(redisCache.scanKeys(lockKeyPrefix + "*"));
+        if (keysToDelete.isEmpty()) {
+            log.info("一键群发刷新飞书文档缓存:无命中 key, companyUserId={}, videoId={}, courseId={}, accountId={}",
+                    companyUserId, videoId, courseId, resolvedAccountId);
+            return;
+        }
+        redisCache.deleteObject(keysToDelete);
+        log.info("一键群发刷新飞书文档缓存:已清除 {} 个 key, companyUserId={}, videoId={}, courseId={}, accountId={}",
+                keysToDelete.size(), companyUserId, videoId, courseId, resolvedAccountId);
+    }
+
     private String buildDirectDocCacheKey(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId,
                                            Integer directDocShard) {
         String dateKey = LocalDate.now(ZoneId.systemDefault()).format(DIRECT_DOC_DATE_FORMAT);

+ 28 - 7
fs-service/src/main/java/com/fs/feishu/util/FeishuDirectDocShardAllocator.java

@@ -3,11 +3,13 @@ package com.fs.feishu.util;
 import com.fs.course.config.CourseConfig;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * SOP群发/自动执行场景,飞书直链文档分片分配器,控制单个分片承载的客户数量,实现租户维度文档分片负载均衡
+ * SOP群发/自动执行场景,飞书直链文档分片分配器。
+ * 控制单个分片承载客户数量,实现租户维度文档分片负载均衡,规避飞书文档访问限流。
  */
 public class FeishuDirectDocShardAllocator {
 
@@ -15,20 +17,27 @@ public class FeishuDirectDocShardAllocator {
     private static final int DEFAULT_MAX_CUSTOMERS_PER_SHARD_AUTO = 50;
 
     private final int maxCustomersPerShard;
+    private final boolean refreshCacheBeforeUse;
     private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
+    private final Set<String> cacheRefreshedKeys = ConcurrentHashMap.newKeySet();
 
-    private FeishuDirectDocShardAllocator(int maxCustomersPerShard) {
+    private FeishuDirectDocShardAllocator(int maxCustomersPerShard, boolean refreshCacheBeforeUse) {
         this.maxCustomersPerShard = maxCustomersPerShard > 0 ? maxCustomersPerShard : DEFAULT_MAX_CUSTOMERS_PER_SHARD_MASS_SEND;
+        this.refreshCacheBeforeUse = refreshCacheBeforeUse;
     }
 
-    /** 群发场景分配器 */
+    /**
+     * 群发场景分片分配器:消息推送前需要刷新文档缓存
+     */
     public static FeishuDirectDocShardAllocator forMassSend(CourseConfig config) {
-        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForMassSend(config));
+        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForMassSend(config), true);
     }
 
-    /** SOP自动执行场景分配器 */
+    /**
+     * SOP自动执行场景分片分配器:平缓流量,无需前置刷新文档缓存
+     */
     public static FeishuDirectDocShardAllocator forAutoSop(CourseConfig config) {
-        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForAuto(config));
+        return new FeishuDirectDocShardAllocator(resolveMaxCustomersPerShardForAuto(config), false);
     }
 
     public static int resolveMaxCustomersPerShardForMassSend(CourseConfig config) {
@@ -52,7 +61,19 @@ public class FeishuDirectDocShardAllocator {
     }
 
     /**
-     * 获取当前客户应当使用的分片编号,从 0 开始
+     * 群发场景下,每组维度首次分配分片前刷新Redis文档缓存,避免重复扫描
+     *
+     * @return true 代表当前key首次标记,需要执行缓存刷新逻辑
+     */
+    public boolean markDirectDocCacheForRefresh(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
+        if (!refreshCacheBeforeUse) {
+            return false;
+        }
+        return cacheRefreshedKeys.add(buildKey(companyUserId, videoId, courseId, feishuAccountId));
+    }
+
+    /**
+     * 获取当前客户分配到的分片编号,分片从0开始
      */
     public int nextShard(Long companyUserId, Long videoId, Long courseId, Long feishuAccountId) {
         String key = buildKey(companyUserId, videoId, courseId, feishuAccountId);

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

@@ -2526,6 +2526,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         try {
             Integer directDocShard = null;
             if (feishuShardAllocator != null && !FeiShuService.isAuthRequired(feishuNeedAuth)) {
+                if (feishuShardAllocator.markDirectDocCacheForRefresh(companyUserId, videoId, courseId, feishuAccountId)) {
+                    feiShuService.invalidateDirectDocCache(companyUserId, videoId, courseId, feishuAccountId);
+                }
                 directDocShard = feishuShardAllocator.nextShard(companyUserId, videoId, courseId, feishuAccountId);
             }
             String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyId, courseId, companyUserId,