Просмотр исходного кода

1、直播拉去最新的代码
2、企微api定时任务迁到企微api

yys 3 месяцев назад
Родитель
Сommit
04775b2cb7

+ 1 - 1
fs-company-app/src/main/resources/application.yml

@@ -6,4 +6,4 @@ server:
 spring:
   profiles:
 #    active: druid-fcky-test
-    active: druid-jnmy-test
+    active: dev

+ 5 - 3
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -72,15 +72,17 @@ public class LiveCompletionPointsTask {
                     Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
 
                     if (userDurations == null || userDurations.isEmpty()) {
+
                         continue;
                     }
-
-                    // 逐个用户处理
+                    // 3. 逐个用户处理
                     for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
                         try {
                             Long userId = Long.parseLong(entry.getKey().toString());
-                            Long duration = Long.parseLong(entry.getValue().toString());
+                            Long duration = Long.parseLong(entry.getValue().toString());  // 从 Redis 直接获取观看时长
+
                             completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
+
                         } catch (Exception e) {
                             log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
                         }

+ 6 - 15
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -301,16 +301,13 @@ public class Task {
             redisCache.redisTemplate.opsForZSet()
                     .removeRangeByScore(liveKey, 0, currentTime);
             try {
-                // 从 key(如 live:red_task:129)中提取 liveId
-                String liveIdStr = liveKey.substring(liveKey.lastIndexOf(':') + 1);
-                Long liveId = Long.valueOf(liveIdStr);
                 // 广播红包关闭消息
                 SendMsgVo sendMsgVo = new SendMsgVo();
-                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setLiveId(Long.valueOf(liveKey));
                 sendMsgVo.setCmd("red");
                 sendMsgVo.setStatus(-1);
-                liveService.asyncToCacheLiveConfig(liveId);
-                webSocketServer.broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+                liveService.asyncToCacheLiveConfig(Long.parseLong(liveKey));
+                webSocketServer.broadcastMessage(Long.valueOf(liveKey), JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             } catch (Exception e) {
                 log.error("更新红包状态异常", e);
             }
@@ -529,10 +526,6 @@ public class Task {
 
     @PostConstruct
     public void initLiveDatasOnStartup() {
-        if (saasTaskEnabled) {
-            log.info("SaaS模式跳过启动时点赞数据初始化,由租户定时任务 syncLiveDataToDB 处理");
-            return;
-        }
         log.info("项目启动,开始初始化直播点赞数据...");
         try {
             List<LiveData> liveDatas = liveDataService.getAllLiveDatas();
@@ -662,12 +655,10 @@ public class Task {
         Set<String> keys = redisCache.redisTemplate.keys(String.format(LIVE_COUPON_NUM, "*"));
         if (keys != null && !keys.isEmpty()) {
             for (String key : keys) {
-                Object o = redisCache.getCacheObject(key);
+                Object o = redisCache.redisTemplate.opsForValue().get(String.format(LIVE_COUPON_NUM, key));
                 if (o != null) {
-                    // key 格式: live:coupon:num:5,提取末尾的优惠券ID
-                    String couponIdStr = key.substring(key.lastIndexOf(':') + 1);
                     LiveCouponIssue updateEntity = new LiveCouponIssue();
-                    updateEntity.setId(Long.valueOf(couponIdStr));
+                    updateEntity.setId(Long.valueOf(key));
                     updateEntity.setRemainCount(Long.parseLong(o.toString()));
                     liveCouponIssueService.updateLiveCouponIssue(updateEntity);
                 }
@@ -1019,7 +1010,7 @@ public class Task {
 //                    newLogType = 4;
 //                    needUpdate = true;
 //                } else
-                    // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
                 if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
                     newLogType = 2;
                     log.setFinishTime(now);

+ 2 - 0
fs-qw-api/src/main/java/com/fs/FsQwAppApplication.java

@@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
@@ -12,6 +13,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
+@EnableScheduling
 public class FsQwAppApplication
 {
     public static void main(String[] args)

+ 10 - 0
fs-qw-api/src/main/java/com/fs/app/qwTask/TenantPrincipal.java

@@ -0,0 +1,10 @@
+package com.fs.app.qwTask;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class TenantPrincipal {
+    private final Long tenantId;
+}

+ 212 - 0
fs-qw-api/src/main/java/com/fs/app/qwTask/TenantTaskRunner.java

@@ -0,0 +1,212 @@
+package com.fs.app.qwTask;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.core.config.TenantConfigContext;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * SaaS 模式下按租户执行定时任务:从主库查启用租户,逐租户切库并设置租户配置后执行传入的逻辑。
+ * 支持顺序执行与可配置的按租户并行执行,租户多、任务多时可通过并行提升整体执行效率。
+ */
+@Slf4j
+@Component
+public class TenantTaskRunner {
+
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+    @Resource
+    private TenantInfoService tenantInfoService;
+    @Resource
+    private SysConfigMapper sysConfigMapper;
+
+    /** 为 true 时按租户并行执行(线程池);为 false 时顺序执行。默认 true 提升效率。 */
+    @Value("${saas.task.parallel:true}")
+    private boolean parallelEnabled;
+    /** 并行时线程池大小,建议不大于租户数量且考虑数据源连接池与机器负载。 */
+    @Value("${saas.task.parallel.threads:4}")
+    private int parallelThreads;
+    /** 单个租户任务超时时间(分钟) */
+    @Value("${saas.task.timeout.minutes:10}")
+    private int timeoutMinutes;
+
+    private ExecutorService tenantExecutor;
+
+    /** ThreadLocal 标记当前是否在租户执行上下文中,防止递归分发 */
+    private static final ThreadLocal<Boolean> IN_TENANT_EXECUTION = ThreadLocal.withInitial(() -> false);
+
+    public static boolean isInTenantExecution() {
+        return Boolean.TRUE.equals(IN_TENANT_EXECUTION.get());
+    }
+
+    @PostConstruct
+    public void initExecutor() {
+        if (parallelEnabled && parallelThreads > 0) {
+            tenantExecutor = new ThreadPoolExecutor(
+                    Math.min(4, parallelThreads),
+                    parallelThreads,
+                    60L, TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(512),
+                    r -> {
+                        Thread t = new Thread(r, "saas-tenant-task-" + System.identityHashCode(r));
+                        t.setDaemon(false);
+                        return t;
+                    },
+                    new ThreadPoolExecutor.CallerRunsPolicy()
+            );
+            log.info("[SaaS Task] 按租户并行已开启,线程池大小: {}", parallelThreads);
+        }
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (tenantExecutor != null) {
+            tenantExecutor.shutdown();
+            try {
+                if (!tenantExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    tenantExecutor.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                tenantExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * 对每个启用且未过期的租户执行一次 action(已切到该租户库并设置 TenantConfigContext)。
+     * 根据配置为顺序执行或按租户并行执行。
+     */
+    public void runForEachTenant(Consumer<TenantInfo> action) {
+        runForEachTenant(null, action);
+    }
+
+    /**
+     * 对每个启用且未过期的租户执行一次 action,日志中输出任务名。
+     * @param taskName 定时任务名称,会输出在「定时任务切换数据源...」后面
+     */
+    public void runForEachTenant(String taskName, Consumer<TenantInfo> action) {
+        List<TenantInfo> validTenants = getValidTenants();
+        if (validTenants == null || validTenants.isEmpty()) {
+            return;
+        }
+        if (parallelEnabled && tenantExecutor != null) {
+            runForEachTenantParallel(validTenants, taskName, action);
+        } else {
+            runForEachTenantSequential(validTenants, taskName, action);
+        }
+    }
+
+    /**
+     * 对每个租户执行无参逻辑(不需要 TenantInfo 时使用)。
+     */
+    public void runForEachTenant(Runnable action) {
+        runForEachTenant(null, t -> action.run());
+    }
+
+    /**
+     * 对每个租户执行无参逻辑,日志中输出任务名。
+     * @param taskName 定时任务名称,会输出在「定时任务切换数据源...」后面
+     */
+    public void runForEachTenant(String taskName, Runnable action) {
+        runForEachTenant(taskName, t -> action.run());
+    }
+
+    private List<TenantInfo> getValidTenants() {
+        try {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            TenantInfo query = new TenantInfo();
+            query.setStatus(1);
+            List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(query);
+            if (tenants == null || tenants.isEmpty()) {
+                log.debug("[SaaS Task] 无启用租户,跳过");
+                return null;
+            }
+            Date now = new Date();
+            return tenants.stream()
+                    .filter(t -> t.getExpireTime() == null || !t.getExpireTime().before(now))
+                    .collect(Collectors.toList());
+        } finally {
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    private void runForEachTenantSequential(List<TenantInfo> validTenants, String taskName, Consumer<TenantInfo> action) {
+        for (TenantInfo tenant : validTenants) {
+            runForOneTenant(tenant, taskName, action);
+        }
+    }
+
+    private void runForEachTenantParallel(List<TenantInfo> validTenants, String taskName, Consumer<TenantInfo> action) {
+        CountDownLatch latch = new CountDownLatch(validTenants.size());
+        for (TenantInfo tenant : validTenants) {
+            tenantExecutor.submit(() -> {
+                try {
+                    runForOneTenant(tenant, taskName, action);
+                } finally {
+                    latch.countDown();
+                }
+            });
+        }
+        try {
+            if (!latch.await(timeoutMinutes, TimeUnit.MINUTES)) {
+                log.warn("[SaaS Task] 按租户并行执行超时({}分钟), task={}", timeoutMinutes, taskName);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("[SaaS Task] 按租户并行执行被中断, task={}", taskName, e);
+        }
+    }
+
+    private void runForOneTenant(TenantInfo tenant, String taskName, Consumer<TenantInfo> action) {
+        String dataSourceKey = "tenant:" + tenant.getId();
+        try {
+            // 标记当前线程正在租户执行上下文中
+            IN_TENANT_EXECUTION.set(true);
+
+            tenantDataSourceManager.switchTenant(tenant);
+            log.info("[SaaS Task] 定时任务切换数据源 dataSource={}, tenantId={}, tenantCode={}, task={}",
+                    dataSourceKey, tenant.getId(), tenant.getTenantCode(), taskName != null ? taskName : "");
+
+            // 加载租户项目配置
+            SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
+            if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
+                TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
+            } else {
+                TenantConfigContext.set(null);
+            }
+            ProjectConfig.loadTenantConfigsFromContext();
+
+            // 执行租户任务
+            action.accept(tenant);
+
+        } catch (Exception e) {
+            log.error("[SaaS Task] 租户 tenantId={}, tenantCode={} 执行异常, task={}",
+                    tenant.getId(), tenant.getTenantCode(), taskName, e);
+        } finally {
+            IN_TENANT_EXECUTION.remove();
+            ProjectConfig.clearTenantConfigs();
+            TenantConfigContext.clear();
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+}

+ 94 - 47
fs-admin/src/main/java/com/fs/qw/qwTask/qwTask.java → fs-qw-api/src/main/java/com/fs/app/qwTask/qwTask.java

@@ -1,4 +1,4 @@
-package com.fs.qw.qwTask;
+package com.fs.app.qwTask;
 
 import com.fs.course.service.IFinishCourseStatisticsSyncService;
 import com.fs.course.service.IFsUserCourseService;
@@ -14,7 +14,10 @@ import com.fs.statis.IFsStatisQwWatchService;
 import com.fs.statis.service.FsStatisSalerWatchService;
 import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
 import com.fs.wxwork.service.WxWorkService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import java.time.Duration;
@@ -26,7 +29,8 @@ import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 
-@Component("qwTask")
+@Component
+@Slf4j
 public class qwTask {
 
     @Autowired
@@ -85,175 +89,213 @@ public class qwTask {
     @Autowired
     private IFinishCourseStatisticsSyncService finishCourseStatisticsSyncService;
 
+    @Autowired
+    private TenantTaskRunner tenantTaskRunner;
+
+    @Value("${saas.task.enabled:true}")
+    private boolean saasTaskEnabled;
+
+    /**
+     * SaaS 多租户定时任务执行模板方法。
+     * 如果启用了 SaaS 且非租户执行上下文中,则按租户逐个执行;
+     * 否则直接执行(兼容单库模式或被 TenantTaskRunner 调用时)。
+     */
+    private void runWithSaaSTenant(String taskName, Runnable action) {
+        if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
+            tenantTaskRunner.runForEachTenant(taskName, action);
+            return;
+        }
+        action.run();
+    }
 
     //正在使用
+    @Scheduled(cron = "0 0 1 * * ?")
     public void qwExternalContact()
     {
-        qwExternalContactService.qwExternalContactSync();
+        runWithSaaSTenant("qwExternalContact", () -> qwExternalContactService.qwExternalContactSync());
     }
     //正在使用
+    @Scheduled(cron = "1/1 * * * * ?")
     public void qwExternalContactAddAndDel()
     {
-        qwExternalContactService.qwExternalContactSyncAddAndDel();
+        runWithSaaSTenant("qwExternalContactAddAndDel", () -> qwExternalContactService.qwExternalContactSyncAddAndDel());
     }
     //正在使用
+    @Scheduled(cron = "0 0/5 * * * ?")
     public void qwExternalContactSyncAddRedis()
     {
-        qwExternalContactService.qwExternalContactSyncAddRedis();
+        runWithSaaSTenant("qwExternalContactSyncAddRedis", () -> qwExternalContactService.qwExternalContactSyncAddRedis());
     }
     //正在使用
+    @Scheduled(cron = "0 0/10 * * * ?")
     public void qwExternalContactAddRedis()
     {
-        qwExternalContactService.qwExternalContactAddRedis();
+        runWithSaaSTenant("qwExternalContactAddRedis", () -> qwExternalContactService.qwExternalContactAddRedis());
     }
     //正在使用
+    @Scheduled(cron = "0 5/10 * * * ?")
     public void qwExternalContactAddAndDelByRedis()
     {
-        qwExternalContactService.qwExternalContactAddAndDelByRedis();
+        runWithSaaSTenant("qwExternalContactAddAndDelByRedis", () -> qwExternalContactService.qwExternalContactAddAndDelByRedis());
     }
     //正在使用
+//    @Scheduled(cron = "0 0 3 * * ?")
     public void qwExternalContactAddTag()
     {
-        qwExternalContactService.qwExternalContactAddCourseTag();
+        runWithSaaSTenant("qwExternalContactAddTag", () -> qwExternalContactService.qwExternalContactAddCourseTag());
     }
 
     /**
     * 定时任务 将 qw_sop任务 符合条件的录入到sop_user_Logs(clickHouse)
     */
+//    @Scheduled(cron = "0 10 1 * * ?")
     public void qwCheckSopRuleTime()
     {
-        qwSopService.checkSopRuleTime();
+        runWithSaaSTenant("qwCheckSopRuleTime", () -> qwSopService.checkSopRuleTime());
     }
 
     /**
     * 定时任务 将 clickHouse的sopUserLogs(营期表)按每小时的巡回 录入clickHouse的qw_sop_logs(消息发送表)
     */
+//    @Scheduled(cron = "0 5 * * * ?")
     public void selectSopUserLogsListByTime(){
-        sopUserLogsService.selectSopUserLogsListByTime();
+        runWithSaaSTenant("selectSopUserLogsListByTime", () -> sopUserLogsService.selectSopUserLogsListByTime());
     }
 
     /**
     * 定时 发送 通过调用 企业微信接口 发送的 SOP 群发消息
     */
+//    @Scheduled(cron = "0 20 1 * * ?")
     public void SendQwApiSopLogTimer(){
-        qwSopLogsService.checkQwSopLogs();
+        runWithSaaSTenant("SendQwApiSopLogTimer", () -> qwSopLogsService.checkQwSopLogs());
     }
 
     /**
     * 定时获取 通过调用 企业微信接口 发送的 SOP 客户群发消息 的反馈结果
     */
+//    @Scheduled(cron = "0 0 8 * * ?")
     public void GetQwApiSopLogResultTimer(){
-        qwSopLogsService.qwSopLogsResult();
+        runWithSaaSTenant("GetQwApiSopLogResultTimer", () -> qwSopLogsService.qwSopLogsResult());
     }
 
     /**
     * 定时群发API接口的 客户/群 群发
     */
+//    @Scheduled(cron = "0 0/5 * * * ?")
     public void sendQwGroupMsgTask(){
-        qwGroupMsgService.qwGroupMsgTask();
+        runWithSaaSTenant("sendQwGroupMsgTask", () -> qwGroupMsgService.qwGroupMsgTask());
     }
 
     /**
     *  定时更新 待发送中已经算过期了的不能发的消息
     */
+//    @Scheduled(cron = "0 0/10 * * * ?")
     public void updateQwSopLogsBySendStatusTask(){
-        qwSopLogsService.updateQwSopLogsBySendStatus();
+        runWithSaaSTenant("updateQwSopLogsBySendStatusTask", () -> qwSopLogsService.updateQwSopLogsBySendStatus());
     }
 
     /**
      * 2天跑一次 将 课程的封面 上传企业微信 并上传到redis
      */
+    @Scheduled(cron = "0 * * * * ?")
     public void processQwSopCourseMaterialTimer() {
-
-        iFsUserCourseService.processQwSopCourseMaterialTimer();
-
+        runWithSaaSTenant("processQwSopCourseMaterialTimer", () -> iFsUserCourseService.processQwSopCourseMaterialTimer());
     }
     /**
     *  同步待同步客户
     */
+    @Scheduled(cron = "0 0/15 * * * ?")
     public void synchronizeQwExternalContactTask(){
-        qwExternalContactService.synchronizeQwExternalContactTask();
+        runWithSaaSTenant("synchronizeQwExternalContactTask", () -> qwExternalContactService.synchronizeQwExternalContactTask());
     }
 
 
     /**
      * 定时解除 已经删除了的销售 绑定的企业微信云主机。或者是没有绑定销售 却绑定了云主机的
      */
+//    @Scheduled(cron = "0 0 2 * * ?")
     public void unlockQwUserLoginCodeUrlTask(){
-        qwUserService.unlockQwUserLoginCodeUrlTask();
+        runWithSaaSTenant("unlockQwUserLoginCodeUrlTask", () -> qwUserService.unlockQwUserLoginCodeUrlTask());
     }
 
 
     /**
      * 看课统计
      */
-    public void watchCourseStatis(String date){
-        if("1".equals(date)){
-            fsStatisSalerWatchService.writeData(null);
-        } else {
-            fsStatisSalerWatchService.writeData(date);
-        }
+    @Scheduled(cron = "30 0 0 * * ?")
+    public void watchCourseStatis(){
+        runWithSaaSTenant("watchCourseStatis", () -> fsStatisSalerWatchService.writeData(null));
     }
 
     /**
      * 看课统计昨天
      */
+    @Scheduled(cron = "30 0 0 * * ?")
     public void watchCourseStatisPrevious(){
-        LocalDate localDate = LocalDate.now().minusDays(1);
-        fsStatisSalerWatchService.writeData(localDate.toString());
+        runWithSaaSTenant("watchCourseStatisPrevious", () -> {
+            LocalDate localDate = LocalDate.now().minusDays(1);
+            fsStatisSalerWatchService.writeData(localDate.toString());
+        });
     }
     /**
      * 看课统计-统计当天的-每15分钟统计一次
      */
+    @Scheduled(cron = "0 0/10 * * * ?")
     public void watchCourseStatisToday(){
-        fsStatisSalerWatchService.writeDataToday();
+        runWithSaaSTenant("watchCourseStatisToday", () -> fsStatisSalerWatchService.writeDataToday());
     }
 
     /**
      * 进线转化统计
      */
-    public void qwWatchCourseStatis(String date){
-        if("1".equals(date)){
-            fsStatisQwWatchService.writeData(null);
-        } else {
-            fsStatisQwWatchService.writeData(date);
-        }
+    @Scheduled(cron = "30 0 0 * * ?")
+    public void qwWatchCourseStatis(){
+        runWithSaaSTenant("qwWatchCourseStatis", () -> fsStatisQwWatchService.writeData(null));
     }
 
     /**
      * 进线转化统计
      */
+    @Scheduled(cron = "0 0/10 * * * ?")
     public void qwWatchCourseStatisToday(){
-        LocalDate today = LocalDate.now();
-        fsStatisQwWatchService.writeData(today.toString());
+        runWithSaaSTenant("qwWatchCourseStatisToday", () -> {
+            LocalDate today = LocalDate.now();
+            fsStatisQwWatchService.writeData(today.toString());
+        });
     }
 
     /**
     * 每俩天 更新一次 素材库
     */
+//    @Scheduled(cron = "0 0 3 */2 * ?")
     public void updateMaterialByTwoDays(){
-        iQwMaterialService.updateQwMaterialByQw();
+        runWithSaaSTenant("updateMaterialByTwoDays", () -> iQwMaterialService.updateQwMaterialByQw());
     }
 
     /**
      * 统计 每天的官方群发统计
      */
+//    @Scheduled(cron = "0 0 1 * * ?")
     public void countQwApiAopLogToken(){
-        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-        // 获取当前日期(只包含年月日)
-        LocalDate currentDate = LocalDate.now();
-
-        String todayStr = currentDate.format(dateFormatter);
-         qwSopLogsService.countQwApiAopLogToken(todayStr);
-
-
+        runWithSaaSTenant("countQwApiAopLogToken", () -> {
+            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            // 获取当前日期(只包含年月日)
+            LocalDate currentDate = LocalDate.now();
+
+            String todayStr = currentDate.format(dateFormatter);
+            qwSopLogsService.countQwApiAopLogToken(todayStr);
+        });
     }
 
     /**
      * 定时清除 占着茅坑不拉屎的ipad 账号
      */
+//    @Scheduled(cron = "0 0 1 * * ?")
     public void selectQwUserByUnbindIpad(){
+        runWithSaaSTenant("selectQwUserByUnbindIpad", this::doSelectQwUserByUnbindIpad);
+    }
 
+    private void doSelectQwUserByUnbindIpad(){
         //查询所有状态为 绑定了AI主机的
         List<QwUser> list = qwUserMapper.selectQwUserByTest();
         for (QwUser qwUser : list) {
@@ -342,7 +384,12 @@ public class qwTask {
     /**
      * 强制注册-2025-11-12 之后的 更新
      */
+//    @Scheduled(cron = "0 0 5 * * ?")
     public void QwExternalContactMandatoryRegistration(){
+        runWithSaaSTenant("QwExternalContactMandatoryRegistration", this::doQwExternalContactMandatoryRegistration);
+    }
+
+    private void doQwExternalContactMandatoryRegistration(){
         try {
 //            List<String> longs = qwExternalContactService.selectQwExternalContactMandatoryRegistration();
             List<String> longs = iQwCompanyService.selectQwCompanyListFormCorpId();
@@ -355,7 +402,6 @@ public class qwTask {
         }catch (Exception e){
 
         }
-
     }
 
 
@@ -377,7 +423,8 @@ public class qwTask {
     /**
      * 特殊处理(木易完课统计数据)
      */
+//    @Scheduled(cron = "0 0 6 * * ?")
     public  void  syncMultiDimensionStatistics(){
-        finishCourseStatisticsSyncService.syncMultiDimensionStatistics();
+        runWithSaaSTenant("syncMultiDimensionStatistics", () -> finishCourseStatisticsSyncService.syncMultiDimensionStatistics());
     }
 }

+ 14 - 2
fs-qw-api/src/main/resources/application.yml

@@ -5,9 +5,21 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: dev
+    active: dev
 #    active: druid-hdt
 #    active: druid-sft
 #    active: druid-myhk
-    active: config-druid-hzyy
+#    active: config-druid-hzyy
+
+# SaaS 多租户定时任务配置
+#saas:
+#  task:
+#    # 是否启用多租户模式(true: 按租户逐个执行定时任务,false: 单库模式直接执行)
+#    enabled: false
+#    # 是否按租户并行执行(true: 并行,false: 顺序执行)
+#    parallel: true
+#    # 并行线程池大小
+#    parallel.threads: 4
+#    # 单个租户任务超时时间(分钟)
+#    timeout.minutes: 10
 

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -502,6 +502,9 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         // 遍历每个企业微信配置
         for (QwCompany company : companies) {
             String corpId = company.getCorpId();
+            if(!corpId.equals("wwa7a371f1026575c9")){
+                continue;
+            }
             if (corpId == null) {
                 continue;
             }