Преглед изворни кода

Merge remote-tracking branch 'origin/saas-api' into saas-api

xgb пре 5 дана
родитељ
комит
71e2c2ef6b
27 измењених фајлова са 304 додато и 73 уклоњено
  1. 31 0
      docs/fs-task模块说明.md
  2. 9 3
      fs-admin/src/main/resources/application-common.yml
  3. 91 16
      fs-quartz/src/main/java/com/fs/quartz/controller/SysJobLogController.java
  4. 31 7
      fs-task/src/main/java/com/fs/task/TaskPackages.java
  5. 10 3
      fs-task/src/main/java/com/fs/task/config/TaskModuleConfiguration.java
  6. 3 3
      fs-task/src/main/java/com/fs/task/controller/TaskManualController.java
  7. 13 3
      fs-task/src/main/java/com/fs/task/jobs/CourseWatchLogScheduler.java
  8. 17 7
      fs-task/src/main/java/com/fs/task/jobs/QwTask.java
  9. 1 1
      fs-task/src/main/java/com/fs/task/jobs/QwUserAsyncTask.java
  10. 1 1
      fs-task/src/main/java/com/fs/task/support/QwExternalContactRatingMoreSevenDaysService.java
  11. 1 1
      fs-task/src/main/java/com/fs/task/support/QwExternalContactRatingService.java
  12. 1 1
      fs-task/src/main/java/com/fs/task/support/SopLogsChatTaskService.java
  13. 1 1
      fs-task/src/main/java/com/fs/task/support/SopLogsTaskService.java
  14. 1 1
      fs-task/src/main/java/com/fs/task/support/SopLogsTestService.java
  15. 1 1
      fs-task/src/main/java/com/fs/task/support/SopUserLogsInfoByIsDaysNotStudy.java
  16. 1 1
      fs-task/src/main/java/com/fs/task/support/SopWxLogsService.java
  17. 1 1
      fs-task/src/main/java/com/fs/task/support/SyncQwExternalContactService.java
  18. 9 1
      fs-task/src/main/java/com/fs/task/support/impl/AsyncCourseWatchFinishService.java
  19. 18 3
      fs-task/src/main/java/com/fs/task/support/impl/QwExternalContactRatingMoreSevenDaysServiceImpl.java
  20. 18 3
      fs-task/src/main/java/com/fs/task/support/impl/QwExternalContactRatingServiceImpl.java
  21. 2 2
      fs-task/src/main/java/com/fs/task/support/impl/SopLogsChatTaskServiceImpl.java
  22. 15 3
      fs-task/src/main/java/com/fs/task/support/impl/SopLogsTaskServiceImpl.java
  23. 3 3
      fs-task/src/main/java/com/fs/task/support/impl/SopLogsTestServiceImpl.java
  24. 14 3
      fs-task/src/main/java/com/fs/task/support/impl/SopUserLogsInfoByIsDaysNotStudyImpl.java
  25. 2 2
      fs-task/src/main/java/com/fs/task/support/impl/SopWxLogsServiceImpl.java
  26. 2 2
      fs-task/src/main/java/com/fs/task/support/impl/SyncQwExternalContactServiceImpl.java
  27. 7 0
      fs-task/src/main/resources/application-dev.yml

+ 31 - 0
docs/fs-task模块说明.md

@@ -75,3 +75,34 @@ fs-admin-saas 已移除对 fs-task 的直接依赖(原 SgTestController 等测
 - TaskPackages.java + TaskModuleConfiguration + TaskRegistryService (统一任务包扫描列表,避免不一致)
 - 标准配置文件(application*.yml, logback.xml 等,参考其他模块完善)
 - META-INF/spring/.../AutoConfiguration.imports (自动加载配置)
+
+## 当前包结构(重新整理后)
+
+fs-task/src/main/java/com/fs/
+├── FsTaskApplication.java
+├── task/
+│   ├── TaskPackages.java                  // 所有任务包前缀列表(供扫描 + 注册表过滤)
+│   ├── config/
+│   │   └── TaskModuleConfiguration.java   // @ComponentScan 声明
+│   ├── controller/
+│   │   ├── TaskRegistryController.java    // /monitor/taskRegistry Bean 注册表
+│   │   └── TaskManualController.java      // /app/common/* 手动触发调试接口
+│   ├── service/
+│   │   └── TaskRegistryService.java       // 反射扫描可调用方法的实现
+│   ├── jobs/                              // 任务入口(可被 sys_job 直接 invoke 的 @Component)
+│   │   ├── QwTask.java                    // 企微 SOP / 群 / 客户相关主入口(原 qwTask)
+│   │   ├── CourseWatchLogScheduler.java
+│   │   └── QwUserAsyncTask.java
+│   └── support/                           // 复杂任务的支撑服务与实现(原 app.taskService)
+│       ├── *.java (接口)
+│       └── impl/*.java
+├── his/task/ ...                          // HIS 领域任务(BillTask, Task.java 大类等)
+├── course/task/ ...
+├── tenant/task/ ...
+├── admin/sync/ ...
+└── ad/controller/task/ ...
+
+说明:
+- 历史遗留的 com.fs.app.task / com.fs.app.taskService 已迁移到 com.fs.task.jobs / com.fs.task.support
+- 迁移后保持 bean 名称不变(类名未改或大小写调整后首字母小写结果一致),sys_job 中的 invoke_target 无需修改。
+- 新增任务时,把入口类放在 jobs/ 下(或对应 his/course 子包),支撑逻辑放在 support/ 下,并在 TaskPackages 中登记包前缀。

+ 9 - 3
fs-admin/src/main/resources/application-common.yml

@@ -1,4 +1,4 @@
-# 项目相关配置
+# 项目相关配置
 fs:
   # 名称
   name: fs
@@ -14,6 +14,13 @@ fs:
   addressEnabled: false
   # 验证码类型 math 数组计算 char 字符验证
   captchaType: math
+  # fs-task 模块提供的 worker/消费者线程初始化控制
+  # fs-admin 仅作为 Quartz 调度中心 + 提供 Task facade bean 给调度使用时,关闭 worker 初始化,
+  # 避免 @PostConstruct 里无条件加载 "course.config"/"qwRating:config" 失败产生的 ERROR 日志,
+  # 以及不必要地启动多达几十个本地消费者线程(这些 worker 主要在 fs-task 独立进程或租户侧执行任务时需要)。
+  task:
+    workers:
+      enabled: false
 #  jwt:
 #    # 加密秘钥
 #    secret: f4e2e52034348f86b67cde581c0f9eb5
@@ -153,5 +160,4 @@ hsy:
   region: cn-north-1
   role_access_key: AKLTNmMwNjJkNDFhYTVjNDIzYzhhNzEyZmZmZTlmYzBhNGM
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
-  role_trn: trn:iam::2114522511:role/hylj
-
+  role_trn: trn:iam::2114522511:role/hylj

+ 91 - 16
fs-quartz/src/main/java/com/fs/quartz/controller/SysJobLogController.java

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
@@ -14,6 +15,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.quartz.domain.SysJobLog;
 import com.fs.quartz.service.ISysJobLogService;
 
@@ -31,60 +33,133 @@ public class SysJobLogController extends BaseController
 
     /**
      * 查询定时任务调度日志列表
+     * 支持 SaaS 平台后台跨租户查询:传入 tenantId 时自动切换到对应租户数据源执行查询,查询完成后自动还原。
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:list')")
     @GetMapping("/list")
-    public TableDataInfo list(SysJobLog sysJobLog)
+    public TableDataInfo list(SysJobLog sysJobLog, @RequestParam(value = "tenantId", required = false) Long tenantId)
     {
-        startPage();
-        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
-        return getDataTable(list);
+        try {
+            switchIfTenant(tenantId);
+            startPage();
+            List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+            return getDataTable(list);
+        } finally {
+            clearIfSwitched(tenantId);
+        }
     }
 
     /**
      * 导出定时任务调度日志列表
+     * 支持传入 tenantId 切换租户库导出。
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:export')")
     @Log(title = "任务调度日志", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(SysJobLog sysJobLog)
+    public AjaxResult export(SysJobLog sysJobLog, @RequestParam(value = "tenantId", required = false) Long tenantId)
     {
-        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
-        ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
-        return util.exportExcel(list, "调度日志");
+        try {
+            switchIfTenant(tenantId);
+            List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+            ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
+            return util.exportExcel(list, "调度日志");
+        } finally {
+            clearIfSwitched(tenantId);
+        }
     }
     
     /**
      * 根据调度编号获取详细信息
+     * 支持 tenantId 切换到对应租户库查询。
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:query')")
-    @GetMapping(value = "/{configId}")
-    public AjaxResult getInfo(@PathVariable Long jobLogId)
+    @GetMapping(value = "/{jobLogId}")
+    public AjaxResult getInfo(@PathVariable Long jobLogId, @RequestParam(value = "tenantId", required = false) Long tenantId)
     {
-        return AjaxResult.success(jobLogService.selectJobLogById(jobLogId));
+        try {
+            switchIfTenant(tenantId);
+            return AjaxResult.success(jobLogService.selectJobLogById(jobLogId));
+        } finally {
+            clearIfSwitched(tenantId);
+        }
     }
 
 
     /**
      * 删除定时任务调度日志
+     * 支持 tenantId,删除操作会在对应租户库执行(避免跨库误删)。
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
     @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE)
     @DeleteMapping("/{jobLogIds}")
-    public AjaxResult remove(@PathVariable Long[] jobLogIds)
+    public AjaxResult remove(@PathVariable Long[] jobLogIds, @RequestParam(value = "tenantId", required = false) Long tenantId)
     {
-        return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
+        try {
+            switchIfTenant(tenantId);
+            return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
+        } finally {
+            clearIfSwitched(tenantId);
+        }
     }
 
     /**
      * 清空定时任务调度日志
+     * 支持 tenantId:若指定则仅清空该租户库的日志;未指定则清空当前数据源(通常为主库平台日志)。
      */
     @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
     @Log(title = "调度日志", businessType = BusinessType.CLEAN)
     @DeleteMapping("/clean")
-    public AjaxResult clean()
+    public AjaxResult clean(@RequestParam(value = "tenantId", required = false) Long tenantId)
     {
-        jobLogService.cleanJobLog();
-        return AjaxResult.success();
+        try {
+            switchIfTenant(tenantId);
+            jobLogService.cleanJobLog();
+            return AjaxResult.success();
+        } finally {
+            clearIfSwitched(tenantId);
+        }
+    }
+
+    // ==================== 内部辅助:SaaS 租户数据源临时切换(运行时通过 SpringUtils 获取,避免 fs-quartz 模块直接依赖 framework 导致循环) ====================
+
+    private Object getTenantDataSourceManager() {
+        try {
+            return SpringUtils.getBean("tenantDataSourceManager");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private void switchIfTenant(Long tenantId) {
+        if (tenantId == null) return;
+        Object mgr = getTenantDataSourceManager();
+        if (mgr == null) return;
+        try {
+            // 调用 ensureSwitchByTenantId(Long)
+            java.lang.reflect.Method m = mgr.getClass().getMethod("ensureSwitchByTenantId", Long.class);
+            m.invoke(mgr, tenantId);
+        } catch (Exception ignore) {
+            // 切换失败则继续用当前 ds(不影响主流程)
+        }
+    }
+
+    private void clearIfSwitched(Long tenantId) {
+        if (tenantId == null) return;
+        Object mgr = getTenantDataSourceManager();
+        if (mgr == null) return;
+        try {
+            // 优先用 mgr.clear() 如果有,否则直接清 holder
+            try {
+                java.lang.reflect.Method clearM = mgr.getClass().getMethod("clear");
+                clearM.invoke(mgr);
+                return;
+            } catch (NoSuchMethodException nsme) {
+                // fallback
+            }
+            // fallback: 直接清 ThreadLocal
+            Class<?> holder = Class.forName("com.fs.framework.datasource.DynamicDataSourceContextHolder");
+            java.lang.reflect.Method clearHolder = holder.getMethod("clearDataSourceType");
+            clearHolder.invoke(null);
+        } catch (Exception ignore) {}
     }
 }

+ 31 - 7
fs-task/src/main/java/com/fs/task/TaskPackages.java

@@ -1,22 +1,46 @@
 package com.fs.task;
 
 /**
- * Central list of base packages containing @Component task beans for fs-task.
- * Used by TaskModuleConfiguration (ComponentScan) and TaskRegistryService (filter).
- * When adding new task packages, update here.
+ * Central list of base packages containing @Component / task beans for fs-task.
+ *
+ * Used by:
+ *  - TaskModuleConfiguration for @ComponentScan (so beans are discovered when this module or dependents start).
+ *  - TaskRegistryService (reflection scan) to list invocable methods for the admin "Bean registry" UI.
+ *
+ * When you introduce new schedulable task classes in a new package, ADD the package here.
+ *
+ * Note on history:
+ *  - hisStore.task and his.task were moved out of fs-task into fs-service (to make them visible to fs-admin-saas which depends on fs-service but not fs-task directly).
+ *  - Old com.fs.app.task* packages were reorganized under com.fs.task.jobs and com.fs.task.support for clarity.
  */
 public final class TaskPackages {
 
     private TaskPackages() {}
 
+    /**
+     * The packages that contain the actual @Component task entry points and supporting services.
+     * These are scanned by TaskRegistryService to build the list of classes/methods available
+     * for configuration as sys_job.invoke_target (e.g. "qWTask.��ʱ��ȡȺ��").
+     *
+     * Current layout after reorganization:
+     *   com.fs.task.jobs          -- top-level entry schedulers (QwTask, CourseWatchLogScheduler, ...)
+     *   com.fs.task.support       -- the actual business logic implementations (was taskService)
+     *   com.fs.his.task           -- his-domain specific tasks (now lives in fs-service)
+     *   com.fs.hisStore.task      -- hisStore specific tasks (now lives in fs-service)
+     *   com.fs.course.task etc.   -- other vertical domain tasks
+     *   com.fs.tenant.task        -- tenant-level orchestration
+     *   com.fs.task               -- shared config / registry / controller inside fs-task
+     *   com.fs.admin.sync         -- sync jobs
+     *   com.fs.ad.controller.task -- ad related scheduled tasks
+     */
     public static final String[] TASK_BASE_PACKAGES = {
-            "com.fs.app.task",
-            "com.fs.app.taskService",
+            "com.fs.task.jobs",         // entry points / facade schedulers
+            "com.fs.task.support",      // impls (SOP, rating, watch log, external contact sync, etc.)
             "com.fs.his.task",
-            "com.fs.hisStore.task",
+            "com.fs.hisStore.task",     // NOTE: actually provided by fs-service now
             "com.fs.course.task",
             "com.fs.tenant.task",
-            "com.fs.task",
+            "com.fs.task",              // TaskPackages + config + manual controller + registry
             "com.fs.admin.sync",
             "com.fs.ad.controller.task"
     };

+ 10 - 3
fs-task/src/main/java/com/fs/task/config/TaskModuleConfiguration.java

@@ -5,12 +5,19 @@ import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 /**
- * Registers fs-task beans when fs-task module is on the classpath (Quartz sys_job dispatch).
+ * fs-task 模块的 Spring 配置类。
+ *
+ * 作用:
+ * - 当 fs-task 出现在 classpath 上时(fs-admin 依赖 fs-task,或独立运行 fs-task),自动触发 @ComponentScan,
+ *   将 com.fs.*.task / support 下的 @Component / @Service 等任务 Bean 注册到容器。
+ * - 配合 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 实现自动装配。
+ *
+ * 注意:hisStore 相关任务类位于 fs-service 模块,由 fs-admin 自身的组件扫描或 fs-service 引入时加载。
  */
 @Configuration
 @ComponentScan(basePackages = {
-        "com.fs.app.task",
-        "com.fs.app.taskService",
+        "com.fs.task.jobs",     // 主要任务入口(QwTask、CourseWatchLogScheduler 等)
+        "com.fs.task.support",  // 复杂任务的支撑服务与实现
         "com.fs.his.task",
         "com.fs.course.task",
         "com.fs.tenant.task",

+ 3 - 3
fs-task/src/main/java/com/fs/task/controller/TaskManualController.java

@@ -3,8 +3,8 @@ package com.fs.task.controller;
 
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.ObjectUtil;
-import com.fs.app.task.qwTask;
-import com.fs.app.taskService.*;
+import com.fs.task.jobs.QwTask;
+import com.fs.task.support.*;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
@@ -73,7 +73,7 @@ public class TaskManualController {
     @Autowired
     private IQwExternalContactService qwExternalContactService;
     @Autowired
-    private qwTask qwTask1;
+    private QwTask qwTask1;
     @Autowired
     private IFsUserVideoService fsUserVideoService;
     @Autowired

+ 13 - 3
fs-task/src/main/java/com/fs/app/task/CourseWatchLogScheduler.java → fs-task/src/main/java/com/fs/task/jobs/CourseWatchLogScheduler.java

@@ -1,6 +1,6 @@
-package com.fs.app.task;
+package com.fs.task.jobs;
 
-import com.fs.app.taskService.SopLogsTaskService;
+import com.fs.task.support.SopLogsTaskService;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
@@ -22,7 +22,17 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * 看课/完课/短链等定时任务。SaaS 模式下按租户执行。
+ * 看课、完课消息生成、短链清理等定时任务调度器。
+ *
+ * 提供的方法(可被 sys_job 配置):
+ *   - checkWatch() / createFinishMessage() / deleteExpireShortUrl() 等
+ *
+ * SaaS 行为:
+ * - 受 saas.task.enabled 控制;当 RedisTenantContext 无当前租户时,会通过 TenantTaskRunner 遍历所有活跃租户分别执行。
+ * - 内部使用 AtomicBoolean 防止任务重叠执行。
+ *
+ * 典型 sys_job.invoke_target:
+ *   courseWatchLogScheduler.checkWatch()
  */
 @Component
 public class CourseWatchLogScheduler {

+ 17 - 7
fs-task/src/main/java/com/fs/app/task/qwTask.java → fs-task/src/main/java/com/fs/task/jobs/QwTask.java

@@ -1,6 +1,6 @@
-package com.fs.app.task;
+package com.fs.task.jobs;
 
-import com.fs.app.taskService.*;
+import com.fs.task.support.*;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.common.utils.PubFun;
 import com.fs.framework.task.TenantTaskRunner;
@@ -39,16 +39,26 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * 企业微信SOP定时任务管理类
- * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
+ * 企业微信(QW)相关定时任务入口类。
+ *
+ * 职责:
+ * - 作为 sys_job 中可直接配置的 bean(beanName 通常为 "qwTask"),提供多个无参 public 方法供 Quartz 通过 invokeTarget 调用。
+ * - 涵盖 SOP 规则执行、标签/群聊/客户同步、外部联系人变更处理、AI 分析触发等。
+ *
+ * SaaS 注意:
+ * - 部分方法内部会根据 saas.task.enabled 和 RedisTenantContext 决定是否跨租户执行(由 TenantTaskRunner 包装)。
+ * - 作为 fs-task 提供的 Task Bean,由 fs-admin 的中央调度器在每个租户上下文中按时触发。
+ *
+ * 典型调用示例(sys_job.invoke_target):
+ *   qwTask.sopTask()
+ *   qwTask.autoPullGroup()
  *
  * @author 系统
- * @version 1.0
  */
 @Component
-public class qwTask {
+public class QwTask {
 
-    private static final Logger log = LoggerFactory.getLogger(qwTask.class);
+    private static final Logger log = LoggerFactory.getLogger(QwTask.class);
 
     @Autowired
     private QwSopMapper qwSopMapper;

+ 1 - 1
fs-task/src/main/java/com/fs/app/task/QwUserAsyncTask.java → fs-task/src/main/java/com/fs/task/jobs/QwUserAsyncTask.java

@@ -1,4 +1,4 @@
-package com.fs.app.task;
+package com.fs.task.jobs;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.config.RedisTenantContext;

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingMoreSevenDaysService.java → fs-task/src/main/java/com/fs/task/support/QwExternalContactRatingMoreSevenDaysService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import com.fs.common.core.domain.R;
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/QwExternalContactRatingService.java → fs-task/src/main/java/com/fs/task/support/QwExternalContactRatingService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import com.fs.common.core.domain.R;
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SopLogsChatTaskService.java → fs-task/src/main/java/com/fs/task/support/SopLogsChatTaskService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import java.time.LocalDateTime;
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java → fs-task/src/main/java/com/fs/task/support/SopLogsTaskService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import java.time.LocalDateTime;
 import java.util.List;

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SopLogsTestService.java → fs-task/src/main/java/com/fs/task/support/SopLogsTestService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 public interface SopLogsTestService {
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SopUserLogsInfoByIsDaysNotStudy.java → fs-task/src/main/java/com/fs/task/support/SopUserLogsInfoByIsDaysNotStudy.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 public interface SopUserLogsInfoByIsDaysNotStudy {
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SopWxLogsService.java → fs-task/src/main/java/com/fs/task/support/SopWxLogsService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import java.time.LocalDateTime;
 

+ 1 - 1
fs-task/src/main/java/com/fs/app/taskService/SyncQwExternalContactService.java → fs-task/src/main/java/com/fs/task/support/SyncQwExternalContactService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService;
+package com.fs.task.support;
 
 import com.fs.common.core.domain.R;
 

+ 9 - 1
fs-task/src/main/java/com/fs/app/taskService/impl/AsyncCourseWatchFinishService.java → fs-task/src/main/java/com/fs/task/support/impl/AsyncCourseWatchFinishService.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 
 import com.alibaba.fastjson.JSON;
@@ -18,6 +18,7 @@ import org.apache.rocketmq.common.message.MessageConst;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.apache.rocketmq.spring.support.RocketMQHeaders;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.messaging.support.MessageBuilder;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -44,6 +45,9 @@ public class AsyncCourseWatchFinishService {
     @Autowired
     RedisCache redisCache;
 
+    @Value("${fs.task.workers.enabled:true}")
+    private boolean workersEnabled;
+
     // 重试队列和调度器
     private final BlockingQueue<RetryMessage> retryQueue = new LinkedBlockingQueue<>(10000);
     private final ScheduledExecutorService retryExecutor = Executors.newSingleThreadScheduledExecutor();
@@ -53,6 +57,10 @@ public class AsyncCourseWatchFinishService {
 
     @PostConstruct
     public void init() {
+        if (!workersEnabled) {
+            log.info("[AsyncCourseWatchFinishService] fs.task.workers.enabled=false,跳过重试队列初始化");
+            return;
+        }
         // 启动重试任务,每5秒处理一次重试队列
         retryExecutor.scheduleWithFixedDelay(this::processRetryQueue, 10, 5, TimeUnit.SECONDS);
         log.info("AsyncCourseWatchFinishService 重试队列处理器已启动");

+ 18 - 3
fs-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingMoreSevenDaysServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/QwExternalContactRatingMoreSevenDaysServiceImpl.java

@@ -1,9 +1,9 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.framework.task.TenantTaskRunner;
-import com.fs.app.taskService.QwExternalContactRatingMoreSevenDaysService;
+import com.fs.task.support.QwExternalContactRatingMoreSevenDaysService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
@@ -76,6 +76,13 @@ public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExtern
     @Value("${saas.task.enabled:false}")
     private boolean saasTaskEnabled;
 
+    /**
+     * 是否启用本地 worker 消费者线程和配置加载。
+     * fs-admin 仅依赖 facade 时设 false,避免启动时大量无意义初始化和 "Failed to load course.config" ERROR。
+     */
+    @Value("${fs.task.workers.enabled:true}")
+    private boolean workersEnabled;
+
     // 任务队列
     private final BlockingQueue<SopUserLogs> taskQueue = new LinkedBlockingQueue<>(10000);
 
@@ -88,6 +95,10 @@ public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExtern
     // 启动时初始化消费者线程
     @PostConstruct
     public void init() {
+        if (!workersEnabled) {
+            log.info("[QwExternalContactRatingMoreSevenDaysServiceImpl] fs.task.workers.enabled=false,跳过 worker 初始化和配置加载");
+            return;
+        }
 
         loadCourseConfig();
 
@@ -306,6 +317,9 @@ public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExtern
 
     /** 每6小时刷新评级配置。SaaS 开启时按租户执行。 */
     public void refreshRatingConfig() {
+        if (!workersEnabled) {
+            return;
+        }
         if (saasTaskEnabled && RedisTenantContext.getTenantId() == null) {
             tenantTaskRunner.runForEachTenant(this::doRefreshRatingConfig);
         } else {
@@ -314,6 +328,7 @@ public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExtern
     }
 
     private void doRefreshRatingConfig() {
+        if (!workersEnabled) return;
         synchronized (configLock) {
             try {
                 String json = configService.selectConfigByKey("qwRating:config");
@@ -322,7 +337,7 @@ public class QwExternalContactRatingMoreSevenDaysServiceImpl implements QwExtern
                     qwRatingConfig = config;
                     log.info("LoadedTime qwRating.config successfully.");
                 } else {
-                    log.error("Failed to load course.config from configService.");
+                    log.warn("qwRating:config not found in current datasource (normal when not in tenant context or config not yet set).");
                 }
             } catch (Exception e) {
                 log.error("Exception while refreshing course.config: {}", e.getMessage(), e);

+ 18 - 3
fs-task/src/main/java/com/fs/app/taskService/impl/QwExternalContactRatingServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/QwExternalContactRatingServiceImpl.java

@@ -1,9 +1,9 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.framework.task.TenantTaskRunner;
-import com.fs.app.taskService.QwExternalContactRatingService;
+import com.fs.task.support.QwExternalContactRatingService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
@@ -77,6 +77,13 @@ public class QwExternalContactRatingServiceImpl implements QwExternalContactRati
     @Value("${saas.task.enabled:false}")
     private boolean saasTaskEnabled;
 
+    /**
+     * 是否启用本地 worker 消费者线程和配置加载。
+     * fs-admin 仅依赖 facade 时设 false,避免启动时大量无意义初始化和 "Failed to load course.config" ERROR。
+     */
+    @Value("${fs.task.workers.enabled:true}")
+    private boolean workersEnabled;
+
     // 任务队列
     private final BlockingQueue<SopUserLogs> taskQueue = new LinkedBlockingQueue<>(10000);
     private volatile boolean running = true;
@@ -88,6 +95,10 @@ public class QwExternalContactRatingServiceImpl implements QwExternalContactRati
     // 启动时初始化消费者线程
     @PostConstruct
     public void init() {
+        if (!workersEnabled) {
+            log.info("[QwExternalContactRatingServiceImpl] fs.task.workers.enabled=false,跳过 worker 初始化和配置加载");
+            return;
+        }
 
         loadCourseConfig();
 
@@ -307,6 +318,9 @@ public class QwExternalContactRatingServiceImpl implements QwExternalContactRati
 
     /** 每6小时刷新评级配置。SaaS 开启时按租户执行。 */
     public void refreshRatingConfig() {
+        if (!workersEnabled) {
+            return; // 在 admin 等非 worker 模式下不刷新
+        }
         if (saasTaskEnabled && RedisTenantContext.getTenantId() == null) {
             tenantTaskRunner.runForEachTenant(this::doRefreshRatingConfig);
         } else {
@@ -315,6 +329,7 @@ public class QwExternalContactRatingServiceImpl implements QwExternalContactRati
     }
 
     private void doRefreshRatingConfig() {
+        if (!workersEnabled) return;
         synchronized (configLock) {
             try {
                 String json = configService.selectConfigByKey("qwRating:config");
@@ -323,7 +338,7 @@ public class QwExternalContactRatingServiceImpl implements QwExternalContactRati
                     qwRatingConfig = config;
                     log.info("LoadedTime qwRating.config successfully.");
                 } else {
-                    log.error("Failed to load course.config from configService.");
+                    log.warn("qwRating:config not found in current datasource (normal when not in tenant context or config not yet set).");
                 }
             } catch (Exception e) {
                 log.error("Exception while refreshing course.config: {}", e.getMessage(), e);

+ 2 - 2
fs-task/src/main/java/com/fs/app/taskService/impl/SopLogsChatTaskServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SopLogsChatTaskServiceImpl.java

@@ -1,8 +1,8 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.SopLogsChatTaskService;
+import com.fs.task.support.SopLogsChatTaskService;
 import com.fs.fastGpt.param.SendHookAIParam;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;

+ 15 - 3
fs-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SopLogsTaskServiceImpl.java

@@ -1,4 +1,4 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -6,7 +6,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.framework.task.TenantTaskRunner;
-import com.fs.app.taskService.SopLogsTaskService;
+import com.fs.task.support.SopLogsTaskService;
 import com.fs.common.config.FSSysConfig;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
@@ -193,6 +193,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Value("${saas.task.enabled:false}")
     private boolean saasTaskEnabled;
 
+    /**
+     * 控制是否在启动时初始化本地消费者线程和加载任务配置。
+     * 在 fs-admin 等仅作为调度中心、依赖 fs-task 提供 facade bean 的场景下,建议设为 false,
+     * 避免不必要的线程池创建和配置加载失败日志(配置通常在租户库或 fs-task 独立运行时存在)。
+     */
+    @Value("${fs.task.workers.enabled:true}")
+    private boolean workersEnabled;
+
 
     @Autowired
     private IQwSopTempVoiceService sopTempVoiceService;
@@ -202,6 +210,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     @PostConstruct
     public void init() {
+        if (!workersEnabled) {
+            log.info("[SopLogsTaskServiceImpl] fs.task.workers.enabled=false,跳过本地消费者初始化(fs-admin 等调度中心场景下无需启动)");
+            return;
+        }
         loadCourseConfig();
         startConsumers();
     }
@@ -214,7 +226,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 cachedCourseConfig = config;
                 log.info("Loaded course.config successfully.");
             } else {
-                log.error("Failed to load course.config from configService.");
+                log.warn("course.config not found in current datasource (normal in master db for fs-admin or before per-tenant config is synced).");
             }
         } catch (Exception e) {
             log.error("Exception while loading course.config: {}", e.getMessage(), e);

+ 3 - 3
fs-task/src/main/java/com/fs/app/taskService/impl/SopLogsTestServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SopLogsTestServiceImpl.java

@@ -1,9 +1,9 @@
-package com.fs.app.taskService.impl;//package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 //
 //import com.alibaba.fastjson.JSON;
 //import com.alibaba.fastjson.JSONArray;
-//import com.fs.app.taskService.SopLogsTaskService;
-//import com.fs.app.taskService.SopLogsTestService;
+//import com.fs.task.support.SopLogsTaskService;
+//import com.fs.task.support.SopLogsTestService;
 //import com.fs.common.utils.StringUtils;
 //import com.fs.course.config.CourseConfig;
 //import com.fs.course.domain.*;

+ 14 - 3
fs-task/src/main/java/com/fs/app/taskService/impl/SopUserLogsInfoByIsDaysNotStudyImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SopUserLogsInfoByIsDaysNotStudyImpl.java

@@ -1,7 +1,7 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import com.alibaba.fastjson.JSON;
-import com.fs.app.taskService.SopUserLogsInfoByIsDaysNotStudy;
+import com.fs.task.support.SopUserLogsInfoByIsDaysNotStudy;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -13,6 +13,7 @@ import com.fs.sop.service.ISopUserLogsService;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
@@ -61,9 +62,19 @@ public class SopUserLogsInfoByIsDaysNotStudyImpl implements SopUserLogsInfoByIsD
 
     private  volatile QwRatingConfig qwRatingConfig;
 
+    /**
+     * 控制是否启动本地消费者和加载评级配置。fs-admin 作为纯调度中心时设为 false 可避免无意义的初始化和 ERROR 日志。
+     */
+    @Value("${fs.task.workers.enabled:true}")
+    private boolean workersEnabled;
+
     // 启动时初始化消费者线程
     @PostConstruct
     public void init() {
+        if (!workersEnabled) {
+            log.info("[SopUserLogsInfoByIsDaysNotStudyImpl] fs.task.workers.enabled=false,跳过本地消费者/配置初始化");
+            return;
+        }
 
         loadCourseConfig();
 
@@ -82,7 +93,7 @@ public class SopUserLogsInfoByIsDaysNotStudyImpl implements SopUserLogsInfoByIsD
                 qwRatingConfig = config;
                 log.info("Loaded qwRating.config successfully.");
             } else {
-                log.error("Failed to load course.config from configService.");
+                log.warn("qwRating:config not found in current datasource (normal when not in tenant context or config not yet set).");
             }
         } catch (Exception e) {
             log.error("Exception while loading qwRating.config: {}", e.getMessage(), e);

+ 2 - 2
fs-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SopWxLogsServiceImpl.java

@@ -1,8 +1,8 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.app.taskService.SopWxLogsService;
+import com.fs.task.support.SopWxLogsService;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;

+ 2 - 2
fs-task/src/main/java/com/fs/app/taskService/impl/SyncQwExternalContactServiceImpl.java → fs-task/src/main/java/com/fs/task/support/impl/SyncQwExternalContactServiceImpl.java

@@ -1,6 +1,6 @@
-package com.fs.app.taskService.impl;
+package com.fs.task.support.impl;
 
-import com.fs.app.taskService.SyncQwExternalContactService;
+import com.fs.task.support.SyncQwExternalContactService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;

+ 7 - 0
fs-task/src/main/resources/application-dev.yml

@@ -101,3 +101,10 @@ spring:
 task:
   registry:
     scan-packages: com.fs.task,com.fs.his.task,com.fs.hisStore.task,com.fs.app.task,com.fs.course.task
+
+# fs-task 独立运行或作为 worker 进程时,启用本地消费者/配置初始化(默认 true)
+# fs-admin 等仅调度中心依赖此模块提供 facade 时,通过其 config 覆盖为 false
+fs:
+  task:
+    workers:
+      enabled: true