ct 4 месяцев назад
Родитель
Сommit
bd39476ffc

+ 1 - 1
fs-admin/src/main/java/com/fs/FSApplication.java

@@ -13,7 +13,7 @@ import org.springframework.transaction.annotation.Transactional;
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
 @Transactional
 @EnableAsync
-//@EnableScheduling
+@EnableScheduling
 public class FSApplication {
     public static void main(String[] args) {
         // System.setProperty("spring.devtools.restart.enabled", "false");

+ 19 - 4
fs-admin/src/main/java/com/fs/company/controller/CompanyTrafficController.java

@@ -11,9 +11,11 @@ import com.fs.company.domain.CompanyTrafficRecord;
 import com.fs.company.param.CompanyTrafficRecordChargeParam;
 import com.fs.company.param.CompanyTrafficRecordQueryParam;
 import com.fs.company.service.ICompanyTrafficRecordService;
+import com.fs.framework.task.TenantTaskRunner;
 import com.fs.framework.web.service.TokenService;
 import com.fs.hisStore.config.MedicalMallConfig;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -34,13 +36,26 @@ public class CompanyTrafficController extends BaseController {
     @Autowired
     private MedicalMallConfig medicalMallConfig;
 
+    @Autowired
+    private TenantTaskRunner tenantTaskRunner;
+
+    /** SaaS 模式:为 true 时定时任务按租户执行 */
+    @Value("${saas.task.enabled:true}")
+    private boolean saasTaskEnabled;
+
     /**
-     * 每天扣除流量余额
-     * */
+     * 每天扣除流量余额。SaaS 开启时按租户执行。
+     */
     @Scheduled(cron = "0 1 0 * * ?")
-    public void refreshTraffic(){
-        if(medicalMallConfig.isStatics())
+    public void refreshTraffic() {
+        if (!medicalMallConfig.isStatics()) {
+            return;
+        }
+        if (saasTaskEnabled) {
+            tenantTaskRunner.runForEachTenant("refreshTraffic", () -> companyTrafficRecordService.refreshTraffic());
+        } else {
             companyTrafficRecordService.refreshTraffic();
+        }
     }
 
     /**

+ 19 - 4
fs-admin/src/main/java/com/fs/course/controller/qw/QwFsCourseWatchLogController.java

@@ -10,6 +10,7 @@ import com.fs.common.exception.CustomException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.config.saas.ProjectConfig;
+import com.fs.core.config.TenantConfigContext;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
@@ -82,7 +83,7 @@ public class QwFsCourseWatchLogController extends BaseController
         }
         param.setSendType(2); //企微
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
-        if("济南联志健康".equals(projectConfig.getCloudHost().getCompanyName())){
+        if ("济南联志健康".equals(getCompanyName())) {
             FsCourseWatchLogStatisticsListVO totalData = fsCourseWatchLogService.getTotalDataAddItem(param);
             list.add(totalData);
         }
@@ -104,7 +105,7 @@ public class QwFsCourseWatchLogController extends BaseController
 
         param.setSendType(2); //企微
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
-        if("济南联志健康".equals(projectConfig.getCloudHost().getCompanyName())){
+        if ("济南联志健康".equals(getCompanyName())) {
             FsCourseWatchLogStatisticsListVO totalData = fsCourseWatchLogService.getTotalDataAddItem(param);
             list.add(totalData);
         }
@@ -117,8 +118,22 @@ public class QwFsCourseWatchLogController extends BaseController
 
 
     @GetMapping("/getSignProjectName")
-    public R getSignProjectName(){
-        return R.ok().put("signProjectName", projectConfig.getCloudHost().getCompanyName());
+    public R getSignProjectName() {
+        return R.ok().put("signProjectName", getCompanyName());
+    }
+
+    /**
+     * 优先从租户上下文取公司名(SaaS),否则从 ProjectConfig 取,避免 getCloudHost() 为 null 导致 NPE。
+     */
+    private String getCompanyName() {
+        String fromTenant = TenantConfigContext.getString("cloud_host.company_name");
+        if (fromTenant != null) {
+            return fromTenant;
+        }
+        if (projectConfig != null && projectConfig.getCloudHost() != null) {
+            return projectConfig.getCloudHost().getCompanyName();
+        }
+        return null;
     }
 
     @GetMapping("/statisticsListByCompany")

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

@@ -34,6 +34,7 @@ import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
 import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastGpt.service.AiHookService;
 import com.fs.fastGpt.service.IFastgptEventLogTotalService;
+import com.fs.framework.task.TenantTaskRunner;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.his.config.FsSysConfig;
@@ -77,6 +78,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -206,6 +208,13 @@ public class Task {
     @Autowired
     private FsUserOperationLogMapper fsUserOperationLogMapper;
 
+    @Autowired
+    private TenantTaskRunner tenantTaskRunner;
+
+    /** SaaS 模式:为 true 时定时任务按租户执行 */
+    @Value("${saas.task.enabled:true}")
+    private boolean saasTaskEnabled;
+
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
     @Autowired
     private IFsStorePaymentService fsStorePaymentService;
@@ -1794,13 +1803,21 @@ public class Task {
         }
     }
 
-    //定时删除行为轨迹记录 (数据量太大 默认保留一天的)
+    /** 定时删除行为轨迹记录 (数据量太大 默认保留一天的)。SaaS 开启时按租户执行。 */
     @Scheduled(cron = "0 0 1 * * ?")
-    //@Scheduled(cron = "0 * * * * ?") //测试每分钟执行一次
-    public void deleteUserOperationLog(){
+//    @Scheduled(cron = "0 * * * * ?") //测试每分钟执行一次
+    public void deleteUserOperationLog() {
+        if (saasTaskEnabled) {
+            tenantTaskRunner.runForEachTenant("deleteUserOperationLog", this::doDeleteUserOperationLog);
+        } else {
+            doDeleteUserOperationLog();
+        }
+    }
+
+    private void doDeleteUserOperationLog() {
         LambdaQueryWrapper<FsUserOperationLog> wrapper = new LambdaQueryWrapper<>();
         wrapper.lt(FsUserOperationLog::getCreateTime, DateUtils.addDays(new Date(), -1));
-        int deleteCount  = fsUserOperationLogMapper.delete(wrapper);
+        int deleteCount = fsUserOperationLogMapper.delete(wrapper);
         log.info("定时删除行为轨迹记录 {} 条", deleteCount);
     }
 

+ 4 - 0
fs-admin/src/main/java/com/fs/quartz/saas/TenantJobDispatcherJob.java

@@ -41,6 +41,7 @@ public class TenantJobDispatcherJob implements Job {
         try {
             // 1. 切到主库查启用且未过期的租户
             DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+            log.info("[SaaS Quartz] 定时任务切换数据源 dataSource=MASTER 查租户列表");
             TenantInfo query = new TenantInfo();
             query.setStatus(1);
             List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(query);
@@ -70,6 +71,7 @@ public class TenantJobDispatcherJob implements Job {
 
     private void dispatchForTenant(TenantInfo tenant, TenantDataSourceManager tenantDataSourceManager,
                                   SysJobMapper sysJobMapper, SysConfigMapper sysConfigMapper) throws Exception {
+        String dataSourceKey = "tenant:" + tenant.getId();
         tenantDataSourceManager.switchTenant(tenant);
 
         SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
@@ -90,6 +92,8 @@ public class TenantJobDispatcherJob implements Job {
                 .collect(Collectors.toList());
 
         for (SysJob job : dueJobs) {
+            String taskName = job.getJobName() != null ? job.getJobName() : job.getInvokeTarget();
+            log.info("[SaaS Quartz] 定时任务切换数据源 dataSource={}, tenantId={}, tenantCode={}, task={}", dataSourceKey, tenant.getId(), tenant.getTenantCode(), taskName != null ? taskName : "");
             try {
                 JobInvokeUtil.invokeMethod(job);
             } catch (Exception e) {

+ 28 - 36
fs-admin/src/main/resources/logback.xml

@@ -12,63 +12,55 @@
 		</encoder>
 	</appender>
 
-	<!-- 系统日志输出 -->
+	<!-- 系统日志输出:按大小+时间滚动,单文件最大100MB,避免文件过大无法打开 -->
 	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
 	    <file>${log.path}/sys-info.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 30 -->
-			<maxHistory>7</maxHistory>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>10GB</totalSizeCap>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
 		</encoder>
 		<filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
             <level>INFO</level>
-            <!-- 匹配时的操作:接收(记录) -->
             <onMatch>ACCEPT</onMatch>
-            <!-- 不匹配时的操作:拒绝(不记录) -->
             <onMismatch>DENY</onMismatch>
         </filter>
 	</appender>
 
 	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
 	    <file>${log.path}/sys-error.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 30 -->
-			<maxHistory>7</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>3GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>ERROR</level>
-			<!-- 匹配时的操作:接收(记录) -->
-            <onMatch>ACCEPT</onMatch>
-			<!-- 不匹配时的操作:拒绝(不记录) -->
-            <onMismatch>DENY</onMismatch>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
         </filter>
     </appender>
 
-	<!-- 用户访问日志输出  -->
+	<!-- 用户访问日志输出:按大小+时间滚动 -->
     <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
 		<file>${log.path}/sys-user.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 按天回滚 daily -->
-            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
-            <!-- 日志最大的历史 30 -->
-            <maxHistory>7</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>5GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
     </appender>
 
 	<!-- 系统模块日志级别控制  -->

+ 2 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java

@@ -58,6 +58,8 @@ public class FsCourseRedPacketLogController extends BaseController
 
     @Autowired
     private IFsUserCoursePeriodService fsUserCoursePeriodService;
+    @Autowired
+    private ProjectConfig  projectConfig;
 
     /**
      * 查询短链课程看课记录列表

+ 114 - 0
fs-framework/src/main/java/com/fs/framework/task/TenantTaskRunner.java

@@ -0,0 +1,114 @@
+package com.fs.framework.task;
+
+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.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * SaaS 模式下按租户执行定时任务:从主库查启用租户,逐租户切库并设置租户配置后执行传入的逻辑。
+ * 供 fs-admin 等使用 fs-framework 的模块在 @Scheduled 中按租户执行任务。
+ */
+@Slf4j
+@Component
+public class TenantTaskRunner {
+
+    @Resource
+    private TenantDataSourceManager tenantDataSourceManager;
+    @Resource
+    private TenantInfoService tenantInfoService;
+    @Resource
+    private SysConfigMapper sysConfigMapper;
+
+    /**
+     * 对每个启用且未过期的租户执行一次 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;
+        }
+        for (TenantInfo tenant : validTenants) {
+            runForOneTenant(tenant, 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 runForOneTenant(TenantInfo tenant, String taskName, Consumer<TenantInfo> action) {
+        String dataSourceKey = "tenant:" + tenant.getId();
+        try {
+            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={} 执行异常", tenant.getId(), tenant.getTenantCode(), e);
+        } finally {
+            ProjectConfig.clearTenantConfigs();
+            TenantConfigContext.clear();
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+}

+ 5 - 5
fs-qw-task/src/main/java/com/fs/app/task/CourseWatchLogScheduler.java

@@ -30,7 +30,7 @@ public class CourseWatchLogScheduler {
     private final AtomicBoolean isRunning3 = new AtomicBoolean(false);
     private final AtomicBoolean isRunning4 = new AtomicBoolean(false);
 
-    @Value("${saas.task.enabled:false}")
+    @Value("${saas.task.enabled:true}")
     private boolean saasTaskEnabled;
     @Resource
     private TenantTaskRunner tenantTaskRunner;
@@ -99,7 +99,7 @@ public class CourseWatchLogScheduler {
         }
         try {
             if (saasTaskEnabled) {
-                tenantTaskRunner.runForEachTenant(this::doCheckWatchStatus);
+                tenantTaskRunner.runForEachTenant("checkWatchStatus", this::doCheckWatchStatus);
             } else {
                 doCheckWatchStatus();
             }
@@ -141,7 +141,7 @@ public class CourseWatchLogScheduler {
         }
         try {
             if (saasTaskEnabled) {
-                tenantTaskRunner.runForEachTenant(() -> {
+                tenantTaskRunner.runForEachTenant("createCourseFinishMsg", () -> {
                     try {
                         log.info("创建完课消息 - 定时任务开始");
                         sopLogsTaskService.createCourseFinishMsg();
@@ -171,7 +171,7 @@ public class CourseWatchLogScheduler {
     public void delCourseExpireLink() {
         try {
             if (saasTaskEnabled) {
-                tenantTaskRunner.runForEachTenant(() -> {
+                tenantTaskRunner.runForEachTenant("delCourseExpireLink", () -> {
                     try {
                         log.info("删除过期短链 - 定时任务开始");
                         courseLinkService.delCourseExpireLink();
@@ -199,7 +199,7 @@ public class CourseWatchLogScheduler {
         }
         try {
             if (saasTaskEnabled) {
-                tenantTaskRunner.runForEachTenant(() -> {
+                tenantTaskRunner.runForEachTenant("checkFsUserWatchStatus", () -> {
                     try {
                         log.info("WXH5-检查会员看课中任务执行>>>>>>>>>>>>");
                         courseWatchLogService.scheduleUpdateDurationToDatabase();

+ 27 - 9
fs-qw-task/src/main/java/com/fs/app/task/TenantTaskRunner.java

@@ -40,7 +40,7 @@ public class TenantTaskRunner {
     private SysConfigMapper sysConfigMapper;
 
     /** 为 true 时按租户并行执行(线程池);为 false 时顺序执行。默认 false 保持与原有行为一致。 */
-    @Value("${saas.task.parallel:false}")
+    @Value("${saas.task.parallel:true}")
     private boolean parallelEnabled;
     /** 并行时线程池大小,建议不大于租户数量且考虑数据源连接池与机器负载。 */
     @Value("${saas.task.parallel.threads:4}")
@@ -87,14 +87,22 @@ public class TenantTaskRunner {
      * 根据配置为顺序执行或按租户并行执行。
      */
     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, action);
+            runForEachTenantParallel(validTenants, taskName, action);
         } else {
-            runForEachTenantSequential(validTenants, action);
+            runForEachTenantSequential(validTenants, taskName, action);
         }
     }
 
@@ -102,7 +110,15 @@ public class TenantTaskRunner {
      * 对每个租户执行无参逻辑(不需要 TenantInfo 时使用)。
      */
     public void runForEachTenant(Runnable action) {
-        runForEachTenant(t -> action.run());
+        runForEachTenant(null, t -> action.run());
+    }
+
+    /**
+     * 对每个租户执行无参逻辑,日志中输出任务名。
+     * @param taskName 定时任务名称,会输出在「定时任务切换数据源...」后面
+     */
+    public void runForEachTenant(String taskName, Runnable action) {
+        runForEachTenant(taskName, t -> action.run());
     }
 
     private List<TenantInfo> getValidTenants() {
@@ -124,18 +140,18 @@ public class TenantTaskRunner {
         }
     }
 
-    private void runForEachTenantSequential(List<TenantInfo> validTenants, Consumer<TenantInfo> action) {
+    private void runForEachTenantSequential(List<TenantInfo> validTenants, String taskName, Consumer<TenantInfo> action) {
         for (TenantInfo tenant : validTenants) {
-            runForOneTenant(tenant, action);
+            runForOneTenant(tenant, taskName, action);
         }
     }
 
-    private void runForEachTenantParallel(List<TenantInfo> validTenants, Consumer<TenantInfo> 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, action);
+                    runForOneTenant(tenant, taskName, action);
                 } finally {
                     latch.countDown();
                 }
@@ -151,9 +167,11 @@ public class TenantTaskRunner {
         }
     }
 
-    private void runForOneTenant(TenantInfo tenant, Consumer<TenantInfo> action) {
+    private void runForOneTenant(TenantInfo tenant, String taskName, Consumer<TenantInfo> action) {
+        String dataSourceKey = "tenant:" + tenant.getId();
         try {
             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()));

+ 2 - 2
fs-qw-task/src/main/java/com/fs/app/task/UserCourseWatchCountTask.java

@@ -22,7 +22,7 @@ public class UserCourseWatchCountTask {
     @Resource
     private TenantTaskRunner tenantTaskRunner;
 
-    @Value("${saas.task.enabled:false}")
+    @Value("${saas.task.enabled:true}")
     private boolean saasTaskEnabled;
 
     private final AtomicBoolean isRunning1 = new AtomicBoolean(false);
@@ -40,7 +40,7 @@ public class UserCourseWatchCountTask {
             log.info("==============会员看课统计任务执行===============开始");
             long startTime = System.currentTimeMillis();
             if (saasTaskEnabled) {
-                tenantTaskRunner.runForEachTenant(() -> userCourseCountService.insertFsUserCourseCountTask());
+                tenantTaskRunner.runForEachTenant("userCourseCountTask", () -> userCourseCountService.insertFsUserCourseCountTask());
             } else {
                 userCourseCountService.insertFsUserCourseCountTask();
             }

+ 21 - 21
fs-qw-task/src/main/java/com/fs/app/task/qwTask.java

@@ -113,7 +113,7 @@ public class qwTask {
     private TenantTaskRunner tenantTaskRunner;
 
     /** SaaS 模式:为 true 时各定时任务按租户遍历执行;为 false 时保持原单库执行(私有化部署) */
-    @Value("${saas.task.enabled:false}")
+    @Value("${saas.task.enabled:true}")
     private boolean saasTaskEnabled;
 
     /**
@@ -124,7 +124,7 @@ public class qwTask {
     @Scheduled(cron = "0 10 1 * * ?")
     public void qwCheckSopRuleTime() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopService.checkSopRuleTime());
+            tenantTaskRunner.runForEachTenant("qwCheckSopRuleTime", () -> qwSopService.checkSopRuleTime());
         } else {
             qwSopService.checkSopRuleTime();
         }
@@ -138,7 +138,7 @@ public class qwTask {
     @Scheduled(cron = "0 0/20 * * * ?")
     public void addTag() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopTagService.addTag());
+            tenantTaskRunner.runForEachTenant("addTag", () -> qwSopTagService.addTag());
         } else {
             qwSopTagService.addTag();
         }
@@ -157,7 +157,7 @@ public class qwTask {
         LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
         log.info("任务实际执行时间: {}", currentTime);
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> {
+            tenantTaskRunner.runForEachTenant("selectSopUserLogsListByTime", () -> {
                 try {
                     sopLogsTaskService.selectSopUserLogsListByTime(currentTime, null);
                 } catch (Exception e) {
@@ -181,7 +181,7 @@ public class qwTask {
         LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
         log.info("任务实际执行时间: {}", currentTime);
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> {
+            tenantTaskRunner.runForEachTenant("wxSop", () -> {
                 try {
                     sopWxLogsService.wxSopLogsByTime(currentTime);
                 } catch (Exception e) {
@@ -218,7 +218,7 @@ public class qwTask {
         LocalDate localDate = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0).toLocalDate();
         String date = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopLogsService.createCorpMassSending(date));
+            tenantTaskRunner.runForEachTenant("SendQwApiSopLogTimer", () -> qwSopLogsService.createCorpMassSending(date));
         } else {
             qwSopLogsService.createCorpMassSending(date);
         }
@@ -236,7 +236,7 @@ public class qwTask {
         String taskEndTime = LocalDate.now().atTime(currentHour, 59, 59)
                 .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopLogsService.createCorpMassSendingByUserLogs(taskStartTime, taskEndTime));
+            tenantTaskRunner.runForEachTenant("SendQwApiSopLogTimerNew", () -> qwSopLogsService.createCorpMassSendingByUserLogs(taskStartTime, taskEndTime));
         } else {
             qwSopLogsService.createCorpMassSendingByUserLogs(taskStartTime, taskEndTime);
         }
@@ -249,7 +249,7 @@ public class qwTask {
     @Scheduled(cron = "0 0 8 * * ?")
     public void GetQwApiSopLogResultTimerNew() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopLogsService.qwSopLogsResultNew());
+            tenantTaskRunner.runForEachTenant("GetQwApiSopLogResultTimerNew", () -> qwSopLogsService.qwSopLogsResultNew());
         } else {
             qwSopLogsService.qwSopLogsResultNew();
         }
@@ -263,7 +263,7 @@ public class qwTask {
     @Scheduled(cron = "0 0/10 * * * ?")
     public void sendQwGroupMsgTask() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwGroupMsgService.qwGroupMsgTask());
+            tenantTaskRunner.runForEachTenant("sendQwGroupMsgTask", () -> qwGroupMsgService.qwGroupMsgTask());
         } else {
             qwGroupMsgService.qwGroupMsgTask();
         }
@@ -277,7 +277,7 @@ public class qwTask {
     @Scheduled(cron = "0 0 8 * * ?")
     public void sendQwBySop() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> sopUserLogsService.sendQwBySop());
+            tenantTaskRunner.runForEachTenant("sendQwBySop", () -> sopUserLogsService.sendQwBySop());
         } else {
             sopUserLogsService.sendQwBySop();
         }
@@ -292,7 +292,7 @@ public class qwTask {
     public void qwExternalErrRetryTimer() {
         log.info("补偿机制开始");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> errRetryService.qwExternalErrRetryTimer());
+            tenantTaskRunner.runForEachTenant("qwExternalErrRetryTimer", () -> errRetryService.qwExternalErrRetryTimer());
         } else {
             errRetryService.qwExternalErrRetryTimer();
         }
@@ -307,7 +307,7 @@ public class qwTask {
     public void updateQwSopLogsByCancel() {
         log.info("补发过期完课消息 - 定时任务开始");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> {
+            tenantTaskRunner.runForEachTenant("updateQwSopLogsByCancel", () -> {
                 try {
                     sopLogsTaskService.updateSopLogsByCancel();
                 } catch (Exception e) {
@@ -333,7 +333,7 @@ public class qwTask {
     public void batchProcessingExpiredMessages() {
         log.info("批量处理sop待发送记录中已过期的消息");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(this::doBatchProcessingExpiredMessages);
+            tenantTaskRunner.runForEachTenant("batchProcessingExpiredMessages", this::doBatchProcessingExpiredMessages);
         } else {
             doBatchProcessingExpiredMessages();
         }
@@ -385,7 +385,7 @@ public class qwTask {
     @Scheduled(cron = "0 10 0 * * ?")
     public void deleteQwSopLogsByDate() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopLogsMapper.deleteQwSopLogsByDate());
+            tenantTaskRunner.runForEachTenant("deleteQwSopLogsByDate", () -> qwSopLogsMapper.deleteQwSopLogsByDate());
         } else {
             qwSopLogsMapper.deleteQwSopLogsByDate();
         }
@@ -399,7 +399,7 @@ public class qwTask {
     @Scheduled(cron = "0 30 0/3 * * ? ")
     public void processRepairQwSopLogsTimer() {
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> sopUserLogsService.repairSopUserLogsTimer());
+            tenantTaskRunner.runForEachTenant("processRepairQwSopLogsTimer", () -> sopUserLogsService.repairSopUserLogsTimer());
         } else {
             sopUserLogsService.repairSopUserLogsTimer();
         }
@@ -414,7 +414,7 @@ public class qwTask {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 开始选择和处理 是否7天未看课的(E级) 客户的 恢复一下 ======");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> logsInfoByIsDaysNotStudy.restoreByIsDaysNotStudy());
+            tenantTaskRunner.runForEachTenant("processSopUserLogsInfoByIsDaysNotStudy", () -> logsInfoByIsDaysNotStudy.restoreByIsDaysNotStudy());
         } else {
             logsInfoByIsDaysNotStudy.restoreByIsDaysNotStudy();
         }
@@ -433,7 +433,7 @@ public class qwTask {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 开始选择和处理 sop营期-用户分级 ======");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwExternalContactRatingService.ratingUserLogs());
+            tenantTaskRunner.runForEachTenant("processQwSopExternalContactRatingTimer", () -> qwExternalContactRatingService.ratingUserLogs());
         } else {
             qwExternalContactRatingService.ratingUserLogs();
         }
@@ -450,7 +450,7 @@ public class qwTask {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 开始选择和处理 sop营期-用户超7天的看课情况 ======");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwExternalContactRatingMoreSevenDaysService.ratingMoreSevenDaysUserLogs());
+            tenantTaskRunner.runForEachTenant("processQwSopExternalContactRatingMoreSevenDaysTimer", () -> qwExternalContactRatingMoreSevenDaysService.ratingMoreSevenDaysUserLogs());
         } else {
             qwExternalContactRatingMoreSevenDaysService.ratingMoreSevenDaysUserLogs();
         }
@@ -466,7 +466,7 @@ public class qwTask {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 更新掉所有前一天的所有待发送 ======");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> qwSopLogsMapper.updateQwSopLogsByDayBefore());
+            tenantTaskRunner.runForEachTenant("updateQwSopLogsDayBefore", () -> qwSopLogsMapper.updateQwSopLogsByDayBefore());
         } else {
             qwSopLogsMapper.updateQwSopLogsByDayBefore();
         }
@@ -479,7 +479,7 @@ public class qwTask {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 同步外部联系人的UnionId ======");
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(() -> syncQwExternalContactService.syncQwExternalContactUnionid());
+            tenantTaskRunner.runForEachTenant("updateQwExternalContactUnionid", () -> syncQwExternalContactService.syncQwExternalContactUnionid());
         } else {
             syncQwExternalContactService.syncQwExternalContactUnionid();
         }
@@ -491,7 +491,7 @@ public class qwTask {
     @Scheduled(cron = "0 0 16 * * ?")
     public void autoPullGroup(){
         if (saasTaskEnabled) {
-            tenantTaskRunner.runForEachTenant(this::doAutoPullGroup);
+            tenantTaskRunner.runForEachTenant("autoPullGroup", this::doAutoPullGroup);
         } else {
             doAutoPullGroup();
         }

+ 28 - 36
fs-qw-task/src/main/resources/logback.xml

@@ -12,63 +12,55 @@
 		</encoder>
 	</appender>
 
-	<!-- 系统日志输出 -->
+	<!-- 系统日志输出:按大小+时间滚动,单文件最大100MB,避免文件过大无法打开 -->
 	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
 	    <file>${log.path}/sys-info.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 30 -->
-			<maxHistory>7</maxHistory>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>10GB</totalSizeCap>
 		</rollingPolicy>
 		<encoder>
 			<pattern>${log.pattern}</pattern>
 		</encoder>
 		<filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
             <level>INFO</level>
-            <!-- 匹配时的操作:接收(记录) -->
             <onMatch>ACCEPT</onMatch>
-            <!-- 不匹配时的操作:拒绝(不记录) -->
             <onMismatch>DENY</onMismatch>
         </filter>
 	</appender>
 
 	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
 	    <file>${log.path}/sys-error.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 30 -->
-			<maxHistory>7</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>3GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>ERROR</level>
-			<!-- 匹配时的操作:接收(记录) -->
-            <onMatch>ACCEPT</onMatch>
-			<!-- 不匹配时的操作:拒绝(不记录) -->
-            <onMismatch>DENY</onMismatch>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
         </filter>
     </appender>
 
-	<!-- 用户访问日志输出  -->
+	<!-- 用户访问日志输出:按大小+时间滚动 -->
     <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
 		<file>${log.path}/sys-user.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 按天回滚 daily -->
-            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
-            <!-- 日志最大的历史 30 -->
-            <maxHistory>7</maxHistory>
-        </rollingPolicy>
-        <encoder>
-            <pattern>${log.pattern}</pattern>
-        </encoder>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+			<maxFileSize>100MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+			<totalSizeCap>5GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
     </appender>
 
 	<!-- 系统模块日志级别控制  -->