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

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

yuhongqi 1 день назад
Родитель
Сommit
1fc3ad7bfc
100 измененных файлов с 7140 добавлено и 8255 удалено
  1. 12 3
      .vscode/settings.json
  2. 12 0
      compile.bat
  3. 1 1
      docs/fs-task模块说明.md
  4. 0 6936
      effective-fs-service.xml
  5. 259 0
      fs-admin-saas/src/main/java/com/fs/admin/controller/company/SysRedpacketConfigMoreController.java
  6. 18 0
      fs-admin-saas/src/main/java/com/fs/company/controller/CompanySmsTempController.java
  7. 27 10
      fs-admin-saas/src/main/java/com/fs/company/controller/CompanyVoiceApiController.java
  8. 19 1
      fs-admin-saas/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  9. 380 36
      fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java
  10. 0 1
      fs-admin-saas/src/main/java/com/fs/user/controller/FsUserIntegralController.java
  11. 0 26
      fs-admin/pom.xml
  12. 18 10
      fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java
  13. 248 0
      fs-admin/src/main/java/com/fs/admin/controller/company/controller/SysRedpacketConfigMoreController.java
  14. 87 0
      fs-admin/src/main/java/com/fs/admin/controller/monitor/SysJobLogController.java
  15. 8 0
      fs-admin/src/main/java/com/fs/admin/controller/monitor/TenantJobController.java
  16. 6 6
      fs-admin/src/main/java/com/fs/qw/controller/QwCompanyController.java
  17. 105 3
      fs-admin/src/main/java/com/fs/web/controller/system/CompanySmsApiTenantController.java
  18. 0 13
      fs-agent/pom.xml
  19. 5 0
      fs-common/pom.xml
  20. 110 0
      fs-common/src/main/java/com/fs/common/utils/CronUtils.java
  21. 1 95
      fs-company/src/main/java/com/fs/company/controller/bridge/CompanyBridgeController.java
  22. 9 0
      fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowLobsterController.java
  23. 13 2
      fs-company/src/main/java/com/fs/company/controller/qw/QwGroupChatController.java
  24. 18 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java
  25. 40 4
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java
  26. 193 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterE2eController.java
  27. 33 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEngineController.java
  28. 93 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterInboundController.java
  29. 117 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterInstanceMonitorController.java
  30. 214 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPlatformAdminController.java
  31. 41 4
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java
  32. 2 2
      fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  33. 2 2
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  34. 0 40
      fs-quartz/pom.xml
  35. 0 167
      fs-quartz/src/main/java/com/fs/quartz/controller/SysJobController.java
  36. 0 165
      fs-quartz/src/main/java/com/fs/quartz/controller/SysJobLogController.java
  37. 0 95
      fs-quartz/src/main/java/com/fs/quartz/util/CronUtils.java
  38. 1 1
      fs-qw-api/src/main/java/com/fs/app/controller/QwGroupChatController.java
  39. 18 16
      fs-service/pom.xml
  40. 25 8
      fs-service/src/main/java/com/fs/comm/service/CommSmsSendService.java
  41. 11 2
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  42. 12 0
      fs-service/src/main/java/com/fs/company/domain/CompanySmsTemp.java
  43. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterTask.java
  44. 27 7
      fs-service/src/main/java/com/fs/company/domain/LobsterConversationSummary.java
  45. 64 0
      fs-service/src/main/java/com/fs/company/domain/LobsterE2eRun.java
  46. 64 0
      fs-service/src/main/java/com/fs/company/domain/LobsterE2eRunNode.java
  47. 17 7
      fs-service/src/main/java/com/fs/company/domain/LobsterEvolutionLog.java
  48. 16 5
      fs-service/src/main/java/com/fs/company/domain/LobsterEvolutionSuggestion.java
  49. 6 0
      fs-service/src/main/java/com/fs/company/domain/LobsterNodeExecutionLog.java
  50. 58 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTestScenario.java
  51. 42 0
      fs-service/src/main/java/com/fs/company/domain/tenant/TenantCompanyVoiceApi.java
  52. 2 1
      fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java
  53. 28 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterE2eRunMapper.java
  54. 19 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterE2eRunNodeMapper.java
  55. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java
  56. 3 6
      fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionSuggestionMapper.java
  57. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTenantLearningMapper.java
  58. 23 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTestScenarioMapper.java
  59. 22 0
      fs-service/src/main/java/com/fs/company/mapper/tenant/TenantCompanyVoiceApiMapper.java
  60. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySmsTempServiceImpl.java
  61. 39 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiServiceImpl.java
  62. 0 218
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java
  63. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  64. 15 0
      fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiQueryService.java
  65. 29 0
      fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiSyncService.java
  66. 16 0
      fs-service/src/main/java/com/fs/company/service/tenant/ITenantMasterSmsApiQueryService.java
  67. 26 0
      fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiQueryServiceImpl.java
  68. 222 0
      fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiSyncServiceImpl.java
  69. 35 0
      fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantMasterSmsApiQueryServiceImpl.java
  70. 153 0
      fs-service/src/main/java/com/fs/company/service/workflow/capability/LobsterNodeCapabilityRegistry.java
  71. 13 8
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java
  72. 14 11
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java
  73. 32 0
      fs-service/src/main/java/com/fs/company/service/workflow/config/LobsterCompanyConfigService.java
  74. 0 16
      fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java
  75. 36 0
      fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionEngineImpl.java
  76. 4 0
      fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionSchedulerImpl.java
  77. 117 19
      fs-service/src/main/java/com/fs/company/service/workflow/heartbeat/impl/HeartbeatSchedulerImpl.java
  78. 171 11
      fs-service/src/main/java/com/fs/company/service/workflow/identity/impl/IdentityHidingServiceImpl.java
  79. 232 7
      fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeAdjusterImpl.java
  80. 773 29
      fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeExecutorImpl.java
  81. 556 19
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterE2eTestServiceImpl.java
  82. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterEvolutionEngineImpl.java
  83. 165 12
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterTestScenarioServiceImpl.java
  84. 254 11
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterWorkflowExecutorImpl.java
  85. 308 24
      fs-service/src/main/java/com/fs/company/service/workflow/impl/ToolCallFrameworkImpl.java
  86. 143 0
      fs-service/src/main/java/com/fs/company/service/workflow/inbound/LobsterInboundService.java
  87. 203 13
      fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/TenantLearningEngineImpl.java
  88. 22 3
      fs-service/src/main/java/com/fs/company/service/workflow/pay/PayService.java
  89. 36 2
      fs-service/src/main/java/com/fs/company/service/workflow/queue/DeadLetterQueue.java
  90. 195 88
      fs-service/src/main/java/com/fs/company/service/workflow/scheduler/WorkflowTriggerScheduler.java
  91. 191 9
      fs-service/src/main/java/com/fs/company/service/workflow/vector/impl/VectorPatternMatcherImpl.java
  92. 27 30
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  93. 82 0
      fs-service/src/main/java/com/fs/his/domain/SysRedpacketConfigMore.java
  94. 63 0
      fs-service/src/main/java/com/fs/his/mapper/SysRedpacketConfigMoreMapper.java
  95. 9 0
      fs-service/src/main/java/com/fs/his/param/UpdateChangeMchIdParam.java
  96. 70 0
      fs-service/src/main/java/com/fs/his/service/ISysRedpacketConfigMoreService.java
  97. 12 2
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  98. 220 0
      fs-service/src/main/java/com/fs/his/service/impl/SysRedpacketConfigMoreServiceImpl.java
  99. 44 10
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  100. 30 32
      fs-service/src/main/java/com/fs/hisStore/domain/FsUserScrm.java

+ 12 - 3
.vscode/settings.json

@@ -1,7 +1,16 @@
 {
-  // 使用当前用户的 Maven settings.xml(其中已配置 localRepository = D:\\Tool\\repository)
   "java.configuration.maven.userSettings": "C:\\Users\\Administrator\\.m2\\settings.xml",
-  // Maven 可执行文件路径,便于 IDE 与终端使用
   "maven.executable.path": "D:\\Tool\\apache-maven-3.6.3\\bin\\mvn.cmd",
-  "java.compile.nullAnalysis.mode": "automatic"
+  "java.compile.nullAnalysis.mode": "automatic",
+  "java.jdt.ls.java.home": "D:\\AICALL\\jdk-17.0.12+7",
+  "java.configuration.runtimes": [
+    {
+      "name": "JavaSE-17",
+      "path": "D:\\AICALL\\jdk-17.0.12+7",
+      "default": true
+    }
+  ],
+  "terminal.integrated.env.windows": {
+    "JAVA_HOME": "D:\\AICALL\\jdk-17.0.12+7"
+  }
 }

+ 12 - 0
compile.bat

@@ -0,0 +1,12 @@
+@echo off
+chcp 65001 >nul
+call "%~dp0set-java17.bat"
+cd /d "%~dp0"
+echo.
+echo [compile] fs-service, fs-company, fs-admin-saas ...
+call mvn compile -pl fs-service,fs-company,fs-admin-saas -am -DskipTests -q
+if errorlevel 1 (
+  echo [compile] FAILED
+  exit /b 1
+)
+echo [compile] SUCCESS

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

@@ -1,4 +1,4 @@
-# fs-task 定时任务模块说明
+# fs-task 定时任务模块说明
 
 ## 架构
 

+ 0 - 6936
effective-fs-service.xml

@@ -1,6936 +0,0 @@
-[INFO] Scanning for projects...
-[WARNING] 
-[WARNING] Some problems were encountered while building the effective model for com.fs:fs-service:jar:1.1.0
-[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.mapstruct:mapstruct:jar -> duplicate declaration of version ${org.mapstruct.version} @ line 313, column 21
-[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.mapstruct:mapstruct-processor:jar -> duplicate declaration of version ${org.mapstruct.version} @ line 318, column 21
-[WARNING] 
-[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
-[WARNING] 
-[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
-[WARNING] 
-[INFO] 
-[INFO] -------------------------< com.fs:fs-service >--------------------------
-[INFO] Building fs-service 1.1.0
-[INFO] --------------------------------[ jar ]---------------------------------
-[INFO] 
-[INFO] --- maven-help-plugin:3.5.1:effective-pom (default-cli) @ fs-service ---
-[INFO] 
-Effective POMs, after inheritance, interpolation, and profiles are applied:
-
-<?xml version="1.0" encoding="GBK"?>
-<!-- ====================================================================== -->
-<!--                                                                        -->
-<!-- Generated by Maven Help Plugin                                         -->
-<!-- See: https://maven.apache.org/plugins/maven-help-plugin/               -->
-<!--                                                                        -->
-<!-- ====================================================================== -->
-<!-- ====================================================================== -->
-<!--                                                                        -->
-<!-- Effective POM for project 'com.fs:fs-service:jar:1.1.0'                -->
-<!--                                                                        -->
-<!-- ====================================================================== -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <parent>
-    <groupId>com.fs</groupId>
-    <artifactId>fs</artifactId>
-    <version>1.1.0</version>
-  </parent>
-  <groupId>com.fs</groupId>
-  <artifactId>fs-service</artifactId>
-  <version>1.1.0</version>
-  <description>service模块</description>
-  <properties>
-    <bitwalker.version>1.21</bitwalker.version>
-    <commons.collections.version>3.2.2</commons.collections.version>
-    <commons.fileupload.version>1.4</commons.fileupload.version>
-    <commons.io.version>2.11.0</commons.io.version>
-    <druid.version>1.2.6</druid.version>
-    <fastjson.version>1.2.76</fastjson.version>
-    <fs.version>1.1.0</fs.version>
-    <gson-version>2.10</gson-version>
-    <ijpay-version>2.7.8</ijpay-version>
-    <java.version>17</java.version>
-    <jna.version>5.8.0</jna.version>
-    <jwt.version>0.9.1</jwt.version>
-    <kaptcha.version>2.3.2</kaptcha.version>
-    <lombok.version>1.18.32</lombok.version>
-    <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
-    <mybatis-spring-boot.version>2.3.2</mybatis-spring-boot.version>
-    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
-    <oshi.version>5.8.0</oshi.version>
-    <pagehelper.boot.version>1.4.7</pagehelper.boot.version>
-    <poi.version>4.1.2</poi.version>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-    <swagger.version>2.9.2</swagger.version>
-    <velocity.version>1.7</velocity.version>
-    <weixin-java-cp.version>4.7.0</weixin-java-cp.version>
-    <weixin-java-miniapp.version>4.7.0</weixin-java-miniapp.version>
-    <weixin-java-mp.version>4.7.0</weixin-java-mp.version>
-  </properties>
-  <dependencyManagement>
-    <dependencies>
-      <dependency>
-        <groupId>com.google.code.gson</groupId>
-        <artifactId>gson</artifactId>
-        <version>2.10</version>
-      </dependency>
-      <dependency>
-        <groupId>mysql</groupId>
-        <artifactId>mysql-connector-java</artifactId>
-        <version>8.0.33</version>
-      </dependency>
-      <dependency>
-        <groupId>com.alibaba</groupId>
-        <artifactId>druid-spring-boot-starter</artifactId>
-        <version>1.2.6</version>
-      </dependency>
-      <dependency>
-        <groupId>eu.bitwalker</groupId>
-        <artifactId>UserAgentUtils</artifactId>
-        <version>1.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mybatis.spring.boot</groupId>
-        <artifactId>mybatis-spring-boot-starter</artifactId>
-        <version>2.3.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.pagehelper</groupId>
-        <artifactId>pagehelper-spring-boot-starter</artifactId>
-        <version>1.4.7</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.oshi</groupId>
-        <artifactId>oshi-core</artifactId>
-        <version>5.8.0</version>
-      </dependency>
-      <dependency>
-        <groupId>net.java.dev.jna</groupId>
-        <artifactId>jna</artifactId>
-        <version>5.8.0</version>
-      </dependency>
-      <dependency>
-        <groupId>net.java.dev.jna</groupId>
-        <artifactId>jna-platform</artifactId>
-        <version>5.8.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.springfox</groupId>
-        <artifactId>springfox-swagger2</artifactId>
-        <version>2.9.2</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>swagger-annotations</artifactId>
-            <groupId>io.swagger</groupId>
-          </exclusion>
-          <exclusion>
-            <artifactId>swagger-models</artifactId>
-            <groupId>io.swagger</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>io.springfox</groupId>
-        <artifactId>springfox-swagger-ui</artifactId>
-        <version>2.9.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.xiaoymin</groupId>
-        <artifactId>swagger-bootstrap-ui</artifactId>
-        <version>1.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>commons-io</groupId>
-        <artifactId>commons-io</artifactId>
-        <version>2.11.0</version>
-      </dependency>
-      <dependency>
-        <groupId>commons-fileupload</groupId>
-        <artifactId>commons-fileupload</artifactId>
-        <version>1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.poi</groupId>
-        <artifactId>poi-ooxml</artifactId>
-        <version>4.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.velocity</groupId>
-        <artifactId>velocity</artifactId>
-        <version>1.7</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-collections</artifactId>
-            <groupId>commons-collections</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>commons-collections</groupId>
-        <artifactId>commons-collections</artifactId>
-        <version>3.2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.alibaba</groupId>
-        <artifactId>fastjson</artifactId>
-        <version>1.2.76</version>
-      </dependency>
-      <dependency>
-        <groupId>io.jsonwebtoken</groupId>
-        <artifactId>jjwt</artifactId>
-        <version>0.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.penggle</groupId>
-        <artifactId>kaptcha</artifactId>
-        <version>2.3.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-quartz</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-task</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-generator</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-framework</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-service</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-wx-ipad-task</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-common</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-repeat-api</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-ipad-task</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fs</groupId>
-        <artifactId>fs-websocket</artifactId>
-        <version>1.1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.javen205</groupId>
-        <artifactId>IJPay-All</artifactId>
-        <version>2.7.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.retry</groupId>
-        <artifactId>spring-retry</artifactId>
-        <version>1.3.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-amqp</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-blueprint</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-broker</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-camel</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-client</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-console</artifactId>
-        <version>5.16.7</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-http</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-jaas</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-jdbc-store</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-jms-pool</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-kahadb-store</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-karaf</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-leveldb-store</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-log4j-appender</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-mqtt</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-openwire-generator</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-openwire-legacy</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-osgi</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-partition</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-pool</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-ra</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-run</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-runtime-config</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-shiro</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-spring</artifactId>
-        <version>5.16.7</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-stomp</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>activemq-web</artifactId>
-        <version>5.16.7</version>
-      </dependency>
-      <dependency>
-        <groupId>antlr</groupId>
-        <artifactId>antlr</artifactId>
-        <version>2.7.7</version>
-      </dependency>
-      <dependency>
-        <groupId>com.google.appengine</groupId>
-        <artifactId>appengine-api-1.0-sdk</artifactId>
-        <version>1.9.98</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-amqp-protocol</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-commons</artifactId>
-        <version>2.19.1</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-core-client</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-jdbc-store</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-jms-client</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-jms-server</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-journal</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-quorum-api</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-selector</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-server</artifactId>
-        <version>2.19.1</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.activemq</groupId>
-        <artifactId>artemis-service-extensions</artifactId>
-        <version>2.19.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.aspectj</groupId>
-        <artifactId>aspectjrt</artifactId>
-        <version>1.9.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.aspectj</groupId>
-        <artifactId>aspectjtools</artifactId>
-        <version>1.9.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.aspectj</groupId>
-        <artifactId>aspectjweaver</artifactId>
-        <version>1.9.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.assertj</groupId>
-        <artifactId>assertj-core</artifactId>
-        <version>3.22.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.atomikos</groupId>
-        <artifactId>transactions-jdbc</artifactId>
-        <version>4.0.6</version>
-      </dependency>
-      <dependency>
-        <groupId>com.atomikos</groupId>
-        <artifactId>transactions-jms</artifactId>
-        <version>4.0.6</version>
-      </dependency>
-      <dependency>
-        <groupId>com.atomikos</groupId>
-        <artifactId>transactions-jta</artifactId>
-        <version>4.0.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.awaitility</groupId>
-        <artifactId>awaitility</artifactId>
-        <version>4.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.awaitility</groupId>
-        <artifactId>awaitility-groovy</artifactId>
-        <version>4.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.awaitility</groupId>
-        <artifactId>awaitility-kotlin</artifactId>
-        <version>4.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.awaitility</groupId>
-        <artifactId>awaitility-scala</artifactId>
-        <version>4.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>net.bytebuddy</groupId>
-        <artifactId>byte-buddy</artifactId>
-        <version>1.12.23</version>
-      </dependency>
-      <dependency>
-        <groupId>net.bytebuddy</groupId>
-        <artifactId>byte-buddy-agent</artifactId>
-        <version>1.12.23</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-api</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-config</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-core</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-jcache</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-micrometer</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.cache2k</groupId>
-        <artifactId>cache2k-spring</artifactId>
-        <version>2.6.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.ben-manes.caffeine</groupId>
-        <artifactId>caffeine</artifactId>
-        <version>2.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.ben-manes.caffeine</groupId>
-        <artifactId>guava</artifactId>
-        <version>2.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.ben-manes.caffeine</groupId>
-        <artifactId>jcache</artifactId>
-        <version>2.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.ben-manes.caffeine</groupId>
-        <artifactId>simulator</artifactId>
-        <version>2.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-core</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml</groupId>
-        <artifactId>classmate</artifactId>
-        <version>1.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>commons-codec</groupId>
-        <artifactId>commons-codec</artifactId>
-        <version>1.15</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.commons</groupId>
-        <artifactId>commons-dbcp2</artifactId>
-        <version>2.9.0</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.commons</groupId>
-        <artifactId>commons-lang3</artifactId>
-        <version>3.12.0</version>
-      </dependency>
-      <dependency>
-        <groupId>commons-pool</groupId>
-        <artifactId>commons-pool</artifactId>
-        <version>1.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.commons</groupId>
-        <artifactId>commons-pool2</artifactId>
-        <version>2.11.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.couchbase.client</groupId>
-        <artifactId>java-client</artifactId>
-        <version>3.3.4</version>
-      </dependency>
-      <dependency>
-        <groupId>com.ibm.db2</groupId>
-        <artifactId>jcc</artifactId>
-        <version>11.5.9.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.spring.gradle</groupId>
-        <artifactId>dependency-management-plugin</artifactId>
-        <version>1.0.15.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.derby</groupId>
-        <artifactId>derby</artifactId>
-        <version>10.14.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.derby</groupId>
-        <artifactId>derbyclient</artifactId>
-        <version>10.14.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.derby</groupId>
-        <artifactId>derbynet</artifactId>
-        <version>10.14.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.derby</groupId>
-        <artifactId>derbyoptionaltools</artifactId>
-        <version>10.14.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.derby</groupId>
-        <artifactId>derbytools</artifactId>
-        <version>10.14.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>net.sf.ehcache</groupId>
-        <artifactId>ehcache</artifactId>
-        <version>2.10.9.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.ehcache</groupId>
-        <artifactId>ehcache</artifactId>
-        <version>3.10.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.ehcache</groupId>
-        <artifactId>ehcache-clustered</artifactId>
-        <version>3.10.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.ehcache</groupId>
-        <artifactId>ehcache-transactions</artifactId>
-        <version>3.10.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch</groupId>
-        <artifactId>elasticsearch</artifactId>
-        <version>7.17.15</version>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.client</groupId>
-        <artifactId>transport</artifactId>
-        <version>7.17.15</version>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.client</groupId>
-        <artifactId>elasticsearch-rest-client</artifactId>
-        <version>7.17.15</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.client</groupId>
-        <artifactId>elasticsearch-rest-client-sniffer</artifactId>
-        <version>7.17.15</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.client</groupId>
-        <artifactId>elasticsearch-rest-high-level-client</artifactId>
-        <version>7.17.15</version>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.distribution.integ-test-zip</groupId>
-        <artifactId>elasticsearch</artifactId>
-        <version>7.17.15</version>
-        <type>zip</type>
-      </dependency>
-      <dependency>
-        <groupId>org.elasticsearch.plugin</groupId>
-        <artifactId>transport-netty4-client</artifactId>
-        <version>7.17.15</version>
-      </dependency>
-      <dependency>
-        <groupId>de.flapdoodle.embed</groupId>
-        <artifactId>de.flapdoodle.embed.mongo</artifactId>
-        <version>3.4.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.flywaydb</groupId>
-        <artifactId>flyway-core</artifactId>
-        <version>8.5.13</version>
-      </dependency>
-      <dependency>
-        <groupId>org.flywaydb</groupId>
-        <artifactId>flyway-firebird</artifactId>
-        <version>8.5.13</version>
-      </dependency>
-      <dependency>
-        <groupId>org.flywaydb</groupId>
-        <artifactId>flyway-mysql</artifactId>
-        <version>8.5.13</version>
-      </dependency>
-      <dependency>
-        <groupId>org.flywaydb</groupId>
-        <artifactId>flyway-sqlserver</artifactId>
-        <version>8.5.13</version>
-      </dependency>
-      <dependency>
-        <groupId>org.freemarker</groupId>
-        <artifactId>freemarker</artifactId>
-        <version>2.3.32</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish</groupId>
-        <artifactId>jakarta.el</artifactId>
-        <version>3.0.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>codemodel</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>codemodel-annotation-compiler</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>jaxb-jxc</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>jaxb-runtime</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>jaxb-xjc</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>txw2</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>txwc2</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jaxb</groupId>
-        <artifactId>xsom</artifactId>
-        <version>2.3.9</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.web</groupId>
-        <artifactId>jakarta.servlet.jsp.jstl</artifactId>
-        <version>1.2.6</version>
-      </dependency>
-      <dependency>
-        <groupId>com.graphql-java</groupId>
-        <artifactId>graphql-java</artifactId>
-        <version>18.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.h2database</groupId>
-        <artifactId>h2</artifactId>
-        <version>2.1.214</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hamcrest</groupId>
-        <artifactId>hamcrest</artifactId>
-        <version>2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hamcrest</groupId>
-        <artifactId>hamcrest-core</artifactId>
-        <version>2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hamcrest</groupId>
-        <artifactId>hamcrest-library</artifactId>
-        <version>2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.hazelcast</groupId>
-        <artifactId>hazelcast</artifactId>
-        <version>5.1.7</version>
-      </dependency>
-      <dependency>
-        <groupId>com.hazelcast</groupId>
-        <artifactId>hazelcast-spring</artifactId>
-        <version>5.1.7</version>
-      </dependency>
-      <dependency>
-        <groupId>com.hazelcast</groupId>
-        <artifactId>hazelcast-hibernate52</artifactId>
-        <version>2.2.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.hazelcast</groupId>
-        <artifactId>hazelcast-hibernate53</artifactId>
-        <version>2.2.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-c3p0</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-core</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-ehcache</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-entitymanager</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-envers</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-hikaricp</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-java8</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-jcache</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-jpamodelgen</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-micrometer</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-proxool</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-spatial</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-testing</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate</groupId>
-        <artifactId>hibernate-vibur</artifactId>
-        <version>5.6.15.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate.validator</groupId>
-        <artifactId>hibernate-validator</artifactId>
-        <version>6.2.5.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hibernate.validator</groupId>
-        <artifactId>hibernate-validator-annotation-processor</artifactId>
-        <version>6.2.5.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>com.zaxxer</groupId>
-        <artifactId>HikariCP</artifactId>
-        <version>4.0.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.hsqldb</groupId>
-        <artifactId>hsqldb</artifactId>
-        <version>2.5.2</version>
-      </dependency>
-      <dependency>
-        <groupId>net.sourceforge.htmlunit</groupId>
-        <artifactId>htmlunit</artifactId>
-        <version>2.60.0</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpasyncclient</artifactId>
-        <version>4.1.5</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>fluent-hc</artifactId>
-        <version>4.5.14</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpclient</artifactId>
-        <version>4.5.14</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>commons-logging</artifactId>
-            <groupId>commons-logging</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpclient-cache</artifactId>
-        <version>4.5.14</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpclient-osgi</artifactId>
-        <version>4.5.14</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpclient-win</artifactId>
-        <version>4.5.14</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpmime</artifactId>
-        <version>4.5.14</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.client5</groupId>
-        <artifactId>httpclient5</artifactId>
-        <version>5.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.client5</groupId>
-        <artifactId>httpclient5-cache</artifactId>
-        <version>5.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.client5</groupId>
-        <artifactId>httpclient5-fluent</artifactId>
-        <version>5.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.client5</groupId>
-        <artifactId>httpclient5-win</artifactId>
-        <version>5.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpcore</artifactId>
-        <version>4.4.16</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents</groupId>
-        <artifactId>httpcore-nio</artifactId>
-        <version>4.4.16</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.core5</groupId>
-        <artifactId>httpcore5</artifactId>
-        <version>5.1.5</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.core5</groupId>
-        <artifactId>httpcore5-h2</artifactId>
-        <version>5.1.5</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.httpcomponents.core5</groupId>
-        <artifactId>httpcore5-reactive</artifactId>
-        <version>5.1.5</version>
-      </dependency>
-      <dependency>
-        <groupId>org.influxdb</groupId>
-        <artifactId>influxdb-java</artifactId>
-        <version>2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>com.sun.activation</groupId>
-        <artifactId>jakarta.activation</artifactId>
-        <version>1.2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.activation</groupId>
-        <artifactId>jakarta.activation-api</artifactId>
-        <version>1.2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.annotation</groupId>
-        <artifactId>jakarta.annotation-api</artifactId>
-        <version>1.3.5</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.jms</groupId>
-        <artifactId>jakarta.jms-api</artifactId>
-        <version>2.0.3</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.json</groupId>
-        <artifactId>jakarta.json-api</artifactId>
-        <version>1.1.6</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.json.bind</groupId>
-        <artifactId>jakarta.json.bind-api</artifactId>
-        <version>1.0.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.mail</groupId>
-        <artifactId>jakarta.mail-api</artifactId>
-        <version>1.6.7</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.management.j2ee</groupId>
-        <artifactId>jakarta.management.j2ee-api</artifactId>
-        <version>1.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.persistence</groupId>
-        <artifactId>jakarta.persistence-api</artifactId>
-        <version>2.2.3</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.servlet</groupId>
-        <artifactId>jakarta.servlet-api</artifactId>
-        <version>4.0.4</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.servlet.jsp.jstl</groupId>
-        <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
-        <version>1.2.7</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.transaction</groupId>
-        <artifactId>jakarta.transaction-api</artifactId>
-        <version>1.3.3</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.validation</groupId>
-        <artifactId>jakarta.validation-api</artifactId>
-        <version>2.0.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.websocket</groupId>
-        <artifactId>jakarta.websocket-api</artifactId>
-        <version>1.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.ws.rs</groupId>
-        <artifactId>jakarta.ws.rs-api</artifactId>
-        <version>2.1.6</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.xml.bind</groupId>
-        <artifactId>jakarta.xml.bind-api</artifactId>
-        <version>2.3.3</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.xml.soap</groupId>
-        <artifactId>jakarta.xml.soap-api</artifactId>
-        <version>1.4.2</version>
-      </dependency>
-      <dependency>
-        <groupId>jakarta.xml.ws</groupId>
-        <artifactId>jakarta.xml.ws-api</artifactId>
-        <version>2.3.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.janino</groupId>
-        <artifactId>commons-compiler</artifactId>
-        <version>3.1.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.janino</groupId>
-        <artifactId>commons-compiler-jdk</artifactId>
-        <version>3.1.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.janino</groupId>
-        <artifactId>janino</artifactId>
-        <version>3.1.10</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.activation</groupId>
-        <artifactId>javax.activation-api</artifactId>
-        <version>1.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.annotation</groupId>
-        <artifactId>javax.annotation-api</artifactId>
-        <version>1.3.2</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.cache</groupId>
-        <artifactId>cache-api</artifactId>
-        <version>1.1.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.xml.bind</groupId>
-        <artifactId>jaxb-api</artifactId>
-        <version>2.3.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.xml.ws</groupId>
-        <artifactId>jaxws-api</artifactId>
-        <version>2.3.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.jms</groupId>
-        <artifactId>javax.jms-api</artifactId>
-        <version>2.0.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.json</groupId>
-        <artifactId>javax.json-api</artifactId>
-        <version>1.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.json.bind</groupId>
-        <artifactId>javax.json.bind-api</artifactId>
-        <version>1.0</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.mail</groupId>
-        <artifactId>javax.mail-api</artifactId>
-        <version>1.6.2</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.money</groupId>
-        <artifactId>money-api</artifactId>
-        <version>1.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.persistence</groupId>
-        <artifactId>javax.persistence-api</artifactId>
-        <version>2.2</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.transaction</groupId>
-        <artifactId>javax.transaction-api</artifactId>
-        <version>1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.validation</groupId>
-        <artifactId>validation-api</artifactId>
-        <version>2.0.1.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.websocket</groupId>
-        <artifactId>javax.websocket-api</artifactId>
-        <version>1.1</version>
-      </dependency>
-      <dependency>
-        <groupId>jaxen</groupId>
-        <artifactId>jaxen</artifactId>
-        <version>1.2.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.firebirdsql.jdbc</groupId>
-        <artifactId>jaybird</artifactId>
-        <version>4.0.9.java8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.firebirdsql.jdbc</groupId>
-        <artifactId>jaybird-jdk18</artifactId>
-        <version>4.0.9.java8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jboss.logging</groupId>
-        <artifactId>jboss-logging</artifactId>
-        <version>3.4.3.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jdom</groupId>
-        <artifactId>jdom2</artifactId>
-        <version>2.0.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>redis.clients</groupId>
-        <artifactId>jedis</artifactId>
-        <version>3.8.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mortbay.jasper</groupId>
-        <artifactId>apache-el</artifactId>
-        <version>9.0.52</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.orbit</groupId>
-        <artifactId>javax.servlet.jsp</artifactId>
-        <version>2.2.0.v201112011158</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-reactive-httpclient</artifactId>
-        <version>1.1.15</version>
-      </dependency>
-      <dependency>
-        <groupId>com.samskivert</groupId>
-        <artifactId>jmustache</artifactId>
-        <version>1.15</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-core</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-jaxrs</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-jsonb</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-jsonb-extras</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-jsonschema</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-mapper</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.johnzon</groupId>
-        <artifactId>johnzon-websocket</artifactId>
-        <version>1.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jolokia</groupId>
-        <artifactId>jolokia-core</artifactId>
-        <version>1.7.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jooq</groupId>
-        <artifactId>jooq</artifactId>
-        <version>3.14.16</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jooq</groupId>
-        <artifactId>jooq-codegen</artifactId>
-        <version>3.14.16</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jooq</groupId>
-        <artifactId>jooq-kotlin</artifactId>
-        <version>3.14.16</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jooq</groupId>
-        <artifactId>jooq-meta</artifactId>
-        <version>3.14.16</version>
-      </dependency>
-      <dependency>
-        <groupId>com.jayway.jsonpath</groupId>
-        <artifactId>json-path</artifactId>
-        <version>2.7.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.jayway.jsonpath</groupId>
-        <artifactId>json-path-assert</artifactId>
-        <version>2.7.0</version>
-      </dependency>
-      <dependency>
-        <groupId>net.minidev</groupId>
-        <artifactId>json-smart</artifactId>
-        <version>2.4.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.skyscreamer</groupId>
-        <artifactId>jsonassert</artifactId>
-        <version>1.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.servlet</groupId>
-        <artifactId>jstl</artifactId>
-        <version>1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>net.sourceforge.jtds</groupId>
-        <artifactId>jtds</artifactId>
-        <version>1.3.1</version>
-      </dependency>
-      <dependency>
-        <groupId>junit</groupId>
-        <artifactId>junit</artifactId>
-        <version>4.13.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-api</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-basic-auth-extension</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-file</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-json</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-mirror</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-mirror-client</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-runtime</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>connect-transforms</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>generator</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-clients</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-clients</artifactId>
-        <version>3.1.2</version>
-        <classifier>test</classifier>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-log4j-appender</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-metadata</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-raft</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-server-common</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-server-common</artifactId>
-        <version>3.1.2</version>
-        <classifier>test</classifier>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-shell</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-storage</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-storage-api</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-streams</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-streams-scala_2.12</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-streams-scala_2.13</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-streams-test-utils</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka-tools</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka_2.12</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka_2.12</artifactId>
-        <version>3.1.2</version>
-        <classifier>test</classifier>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka_2.13</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>kafka_2.13</artifactId>
-        <version>3.1.2</version>
-        <classifier>test</classifier>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.kafka</groupId>
-        <artifactId>trogdor</artifactId>
-        <version>3.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>io.lettuce</groupId>
-        <artifactId>lettuce-core</artifactId>
-        <version>6.1.10.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.liquibase</groupId>
-        <artifactId>liquibase-cdi</artifactId>
-        <version>4.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.liquibase</groupId>
-        <artifactId>liquibase-core</artifactId>
-        <version>4.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>ch.qos.logback</groupId>
-        <artifactId>logback-access</artifactId>
-        <version>1.2.12</version>
-      </dependency>
-      <dependency>
-        <groupId>ch.qos.logback</groupId>
-        <artifactId>logback-classic</artifactId>
-        <version>1.2.12</version>
-      </dependency>
-      <dependency>
-        <groupId>ch.qos.logback</groupId>
-        <artifactId>logback-core</artifactId>
-        <version>1.2.12</version>
-      </dependency>
-      <dependency>
-        <groupId>org.projectlombok</groupId>
-        <artifactId>lombok</artifactId>
-        <version>1.18.30</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mariadb.jdbc</groupId>
-        <artifactId>mariadb-java-client</artifactId>
-        <version>3.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-stackdriver</artifactId>
-        <version>1.9.17</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>javax.annotation-api</artifactId>
-            <groupId>javax.annotation</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>bson</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>bson-record-codec</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>mongodb-driver-core</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>mongodb-driver-legacy</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>mongodb-driver-reactivestreams</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mongodb</groupId>
-        <artifactId>mongodb-driver-sync</artifactId>
-        <version>4.6.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.microsoft.sqlserver</groupId>
-        <artifactId>mssql-jdbc</artifactId>
-        <version>10.2.3.jre8</version>
-      </dependency>
-      <dependency>
-        <groupId>com.mysql</groupId>
-        <artifactId>mysql-connector-j</artifactId>
-        <version>8.0.33</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>protobuf-java</artifactId>
-            <groupId>com.google.protobuf</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>net.sourceforge.nekohtml</groupId>
-        <artifactId>nekohtml</artifactId>
-        <version>1.9.22</version>
-      </dependency>
-      <dependency>
-        <groupId>org.neo4j.driver</groupId>
-        <artifactId>neo4j-java-driver</artifactId>
-        <version>4.4.12</version>
-      </dependency>
-      <dependency>
-        <groupId>org.messaginghub</groupId>
-        <artifactId>pooled-jms</artifactId>
-        <version>1.2.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.postgresql</groupId>
-        <artifactId>postgresql</artifactId>
-        <version>42.3.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.quartz-scheduler</groupId>
-        <artifactId>quartz</artifactId>
-        <version>2.3.2</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>c3p0</artifactId>
-            <groupId>com.mchange</groupId>
-          </exclusion>
-          <exclusion>
-            <artifactId>*</artifactId>
-            <groupId>com.zaxxer</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.quartz-scheduler</groupId>
-        <artifactId>quartz-jobs</artifactId>
-        <version>2.3.2</version>
-      </dependency>
-      <dependency>
-        <groupId>com.rabbitmq</groupId>
-        <artifactId>amqp-client</artifactId>
-        <version>5.14.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.rabbitmq</groupId>
-        <artifactId>stream-client</artifactId>
-        <version>0.5.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.reactivestreams</groupId>
-        <artifactId>reactive-streams</artifactId>
-        <version>1.0.4</version>
-      </dependency>
-      <dependency>
-        <groupId>io.reactivex</groupId>
-        <artifactId>rxjava</artifactId>
-        <version>1.3.8</version>
-      </dependency>
-      <dependency>
-        <groupId>io.reactivex</groupId>
-        <artifactId>rxjava-reactive-streams</artifactId>
-        <version>1.2.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.reactivex.rxjava2</groupId>
-        <artifactId>rxjava</artifactId>
-        <version>2.2.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-test</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-test-autoconfigure</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-actuator</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-actuator-autoconfigure</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-autoconfigure</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-autoconfigure-processor</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-buildpack-platform</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-configuration-metadata</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-configuration-processor</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-devtools</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-jarmode-layertools</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-loader</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-loader-tools</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-properties-migrator</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-activemq</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-actuator</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-amqp</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-aop</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-artemis</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-batch</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-cache</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-cassandra</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-cassandra-reactive</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-couchbase</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-couchbase-reactive</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-jdbc</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-jpa</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-ldap</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-mongodb</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-redis</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-neo4j</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-data-rest</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-freemarker</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-graphql</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-groovy-templates</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-hateoas</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-integration</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-jdbc</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-jersey</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-jetty</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-jooq</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-json</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-jta-atomikos</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-log4j2</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-logging</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-mail</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-mustache</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-oauth2-client</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-quartz</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-reactor-netty</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-rsocket</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-security</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-test</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-thymeleaf</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-tomcat</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-undertow</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-validation</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-web</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-webflux</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-websocket</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-starter-web-services</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>com.sun.xml.messaging.saaj</groupId>
-        <artifactId>saaj-impl</artifactId>
-        <version>1.5.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>lift</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-api</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-chrome-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-chromium-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-devtools-v100</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-devtools-v101</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-devtools-v85</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-devtools-v99</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-edge-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-firefox-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-grid</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-http</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-ie-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-java</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-json</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-opera-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-remote-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-safari-driver</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-session-map-jdbc</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-session-map-redis</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>selenium-support</artifactId>
-        <version>4.1.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.seleniumhq.selenium</groupId>
-        <artifactId>htmlunit-driver</artifactId>
-        <version>3.61.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.sendgrid</groupId>
-        <artifactId>sendgrid-java</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>javax.servlet</groupId>
-        <artifactId>javax.servlet-api</artifactId>
-        <version>4.0.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>jcl-over-slf4j</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>jul-to-slf4j</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>log4j-over-slf4j</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-api</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-ext</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-jcl</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-jdk14</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-log4j12</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-nop</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.slf4j</groupId>
-        <artifactId>slf4j-simple</artifactId>
-        <version>1.7.36</version>
-      </dependency>
-      <dependency>
-        <groupId>org.yaml</groupId>
-        <artifactId>snakeyaml</artifactId>
-        <version>1.30</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-analysis-extras</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-analytics</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-cell</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-core</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-dataimporthandler</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-dataimporthandler-extras</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-gcs-repository</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-jaegertracer-configurator</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-langid</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-ltr</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-prometheus-exporter</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-s3-repository</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-solrj</artifactId>
-        <version>8.11.2</version>
-        <exclusions>
-          <exclusion>
-            <artifactId>jcl-over-slf4j</artifactId>
-            <groupId>org.slf4j</groupId>
-          </exclusion>
-        </exclusions>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-test-framework</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.solr</groupId>
-        <artifactId>solr-velocity</artifactId>
-        <version>8.11.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.amqp</groupId>
-        <artifactId>spring-amqp</artifactId>
-        <version>2.4.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.amqp</groupId>
-        <artifactId>spring-rabbit</artifactId>
-        <version>2.4.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.amqp</groupId>
-        <artifactId>spring-rabbit-stream</artifactId>
-        <version>2.4.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.amqp</groupId>
-        <artifactId>spring-rabbit-junit</artifactId>
-        <version>2.4.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.amqp</groupId>
-        <artifactId>spring-rabbit-test</artifactId>
-        <version>2.4.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.batch</groupId>
-        <artifactId>spring-batch-core</artifactId>
-        <version>4.3.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.batch</groupId>
-        <artifactId>spring-batch-infrastructure</artifactId>
-        <version>4.3.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.batch</groupId>
-        <artifactId>spring-batch-integration</artifactId>
-        <version>4.3.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.batch</groupId>
-        <artifactId>spring-batch-test</artifactId>
-        <version>4.3.10</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.graphql</groupId>
-        <artifactId>spring-graphql</artifactId>
-        <version>1.0.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.graphql</groupId>
-        <artifactId>spring-graphql-test</artifactId>
-        <version>1.0.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.hateoas</groupId>
-        <artifactId>spring-hateoas</artifactId>
-        <version>1.5.6</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.kafka</groupId>
-        <artifactId>spring-kafka</artifactId>
-        <version>2.8.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.kafka</groupId>
-        <artifactId>spring-kafka-test</artifactId>
-        <version>2.8.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ldap</groupId>
-        <artifactId>spring-ldap-core</artifactId>
-        <version>2.4.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ldap</groupId>
-        <artifactId>spring-ldap-core-tiger</artifactId>
-        <version>2.4.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ldap</groupId>
-        <artifactId>spring-ldap-ldif-core</artifactId>
-        <version>2.4.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ldap</groupId>
-        <artifactId>spring-ldap-odm</artifactId>
-        <version>2.4.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ldap</groupId>
-        <artifactId>spring-ldap-test</artifactId>
-        <version>2.4.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.restdocs</groupId>
-        <artifactId>spring-restdocs-asciidoctor</artifactId>
-        <version>2.0.8.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.restdocs</groupId>
-        <artifactId>spring-restdocs-core</artifactId>
-        <version>2.0.8.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.restdocs</groupId>
-        <artifactId>spring-restdocs-mockmvc</artifactId>
-        <version>2.0.8.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.restdocs</groupId>
-        <artifactId>spring-restdocs-restassured</artifactId>
-        <version>2.0.8.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.restdocs</groupId>
-        <artifactId>spring-restdocs-webtestclient</artifactId>
-        <version>2.0.8.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ws</groupId>
-        <artifactId>spring-ws-core</artifactId>
-        <version>3.1.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ws</groupId>
-        <artifactId>spring-ws-security</artifactId>
-        <version>3.1.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ws</groupId>
-        <artifactId>spring-ws-support</artifactId>
-        <version>3.1.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ws</groupId>
-        <artifactId>spring-ws-test</artifactId>
-        <version>3.1.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.ws</groupId>
-        <artifactId>spring-xml</artifactId>
-        <version>3.1.8</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xerial</groupId>
-        <artifactId>sqlite-jdbc</artifactId>
-        <version>3.36.0.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.sun.mail</groupId>
-        <artifactId>jakarta.mail</artifactId>
-        <version>1.6.7</version>
-      </dependency>
-      <dependency>
-        <groupId>org.thymeleaf</groupId>
-        <artifactId>thymeleaf</artifactId>
-        <version>3.0.15.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.thymeleaf</groupId>
-        <artifactId>thymeleaf-spring5</artifactId>
-        <version>3.0.15.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>com.github.mxab.thymeleaf.extras</groupId>
-        <artifactId>thymeleaf-extras-data-attribute</artifactId>
-        <version>2.0.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.thymeleaf.extras</groupId>
-        <artifactId>thymeleaf-extras-java8time</artifactId>
-        <version>3.0.4.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.thymeleaf.extras</groupId>
-        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
-        <version>3.0.5.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>nz.net.ultraq.thymeleaf</groupId>
-        <artifactId>thymeleaf-layout-dialect</artifactId>
-        <version>3.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>tomcat-annotations-api</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>tomcat-jdbc</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat</groupId>
-        <artifactId>tomcat-jsp-api</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat.embed</groupId>
-        <artifactId>tomcat-embed-core</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat.embed</groupId>
-        <artifactId>tomcat-embed-el</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat.embed</groupId>
-        <artifactId>tomcat-embed-jasper</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.tomcat.embed</groupId>
-        <artifactId>tomcat-embed-websocket</artifactId>
-        <version>9.0.83</version>
-      </dependency>
-      <dependency>
-        <groupId>com.unboundid</groupId>
-        <artifactId>unboundid-ldapsdk</artifactId>
-        <version>6.0.10</version>
-      </dependency>
-      <dependency>
-        <groupId>io.undertow</groupId>
-        <artifactId>undertow-core</artifactId>
-        <version>2.2.28.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.undertow</groupId>
-        <artifactId>undertow-servlet</artifactId>
-        <version>2.2.28.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.undertow</groupId>
-        <artifactId>undertow-websockets-jsr</artifactId>
-        <version>2.2.28.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.webjars</groupId>
-        <artifactId>webjars-locator-core</artifactId>
-        <version>0.50</version>
-      </dependency>
-      <dependency>
-        <groupId>wsdl4j</groupId>
-        <artifactId>wsdl4j</artifactId>
-        <version>1.6.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-assertj</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-assertj3</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-core</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-legacy</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-matchers</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.xmlunit</groupId>
-        <artifactId>xmlunit-placeholders</artifactId>
-        <version>2.9.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-core-shaded</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-mapper-processor</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-mapper-runtime</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-query-builder</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-test-infra</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-metrics-micrometer</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-metrics-microprofile</artifactId>
-        <version>4.14.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>native-protocol</artifactId>
-        <version>1.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>com.datastax.oss</groupId>
-        <artifactId>java-driver-shaded-guava</artifactId>
-        <version>25.1-jre-graal-sub-1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-annotation</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-caffeine</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-caffeine3</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-core</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-collectd</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-ehcache</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-graphite</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-healthchecks</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-httpclient</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-httpclient5</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-httpasyncclient</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jakarta-servlet</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jakarta-servlet6</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jakarta-servlets</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jcache</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jdbi</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jdbi3</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jersey2</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jersey3</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jersey31</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jetty9</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jetty10</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jetty11</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jetty12</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jetty12-ee10</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jmx</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-json</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-jvm</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-log4j2</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-logback</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-logback13</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-logback14</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-servlet</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.dropwizard.metrics</groupId>
-        <artifactId>metrics-servlets</artifactId>
-        <version>4.2.22</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-ant</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-astbuilder</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-bsf</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-cli-commons</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-cli-picocli</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-console</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-datetime</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-dateutil</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-docgenerator</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-groovydoc</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-groovysh</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-jaxb</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-jmx</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-json</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-jsr223</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-macro</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-nio</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-servlet</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-sql</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-swing</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-templates</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-test</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-test-junit5</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-testng</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-xml</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.codehaus.groovy</groupId>
-        <artifactId>groovy-yaml</artifactId>
-        <version>3.0.19</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-api</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cachestore-jdbc</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cachestore-sql</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cachestore-jpa</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cachestore-remote</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cachestore-rocksdb</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cdi-common</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cdi-embedded</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cdi-remote</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-checkstyle</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cli-client</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-client-hotrod</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-client-rest</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-key-value-store-client</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-clustered-counter</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-clustered-lock</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-commons</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-commons-test</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-component-annotations</artifactId>
-        <version>13.0.20.Final</version>
-        <scope>provided</scope>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-component-processor</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-core</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-jboss-marshalling</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-extended-statistics</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-hibernate-cache-commons</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-hibernate-cache-spi</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-hibernate-cache-v53</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-jcache-commons</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-jcache</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-jcache-remote</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-console</artifactId>
-        <version>0.15.5.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-multimap</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-objectfilter</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-query-core</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-query</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-query-dsl</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-remote-query-client</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-remote-query-server</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-scripting</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-core</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-hotrod</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-memcached</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-rest</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-router</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-runtime</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-testdriver-core</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-testdriver-junit4</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-server-testdriver-junit5</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-spring5-common</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-spring5-embedded</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-spring5-remote</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-spring-boot-starter-embedded</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-spring-boot-starter-remote</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-tasks</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-tasks-api</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-tools</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-anchored-keys</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan.protostream</groupId>
-        <artifactId>protostream</artifactId>
-        <version>4.4.4.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan.protostream</groupId>
-        <artifactId>protostream-types</artifactId>
-        <version>4.4.4.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan.protostream</groupId>
-        <artifactId>protostream-processor</artifactId>
-        <version>4.4.4.Final</version>
-        <scope>provided</scope>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-cloudevents-integration</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-marshaller-kryo</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-marshaller-kryo-bundle</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-marshaller-protostuff</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>org.infinispan</groupId>
-        <artifactId>infinispan-marshaller-protostuff-bundle</artifactId>
-        <version>13.0.20.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.core</groupId>
-        <artifactId>jackson-annotations</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.core</groupId>
-        <artifactId>jackson-core</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.core</groupId>
-        <artifactId>jackson-databind</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-avro</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-cbor</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-csv</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-ion</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-properties</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-protobuf</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-smile</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-toml</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-xml</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.dataformat</groupId>
-        <artifactId>jackson-dataformat-yaml</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-eclipse-collections</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-guava</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-hibernate4</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-hibernate5</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-hibernate5-jakarta</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-hppc</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-jakarta-jsonp</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-jaxrs</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-joda</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-joda-money</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-jdk8</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-json-org</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-jsr310</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-jsr353</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.datatype</groupId>
-        <artifactId>jackson-datatype-pcollections</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-base</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-cbor-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-json-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-smile-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-xml-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jaxrs</groupId>
-        <artifactId>jackson-jaxrs-yaml-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-base</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-cbor-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-json-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-smile-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-xml-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
-        <artifactId>jackson-jakarta-rs-yaml-provider</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jr</groupId>
-        <artifactId>jackson-jr-all</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jr</groupId>
-        <artifactId>jackson-jr-annotation-support</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jr</groupId>
-        <artifactId>jackson-jr-objects</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jr</groupId>
-        <artifactId>jackson-jr-retrofit2</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.jr</groupId>
-        <artifactId>jackson-jr-stree</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-afterburner</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-blackbird</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-guice</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-jaxb-annotations</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-jsonSchema</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-kotlin</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-mrbean</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-no-ctor-deser</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-osgi</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-parameter-names</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-paranamer</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-scala_2.11</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-scala_2.12</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-scala_2.13</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>com.fasterxml.jackson.module</groupId>
-        <artifactId>jackson-module-scala_3</artifactId>
-        <version>2.13.5</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.core</groupId>
-        <artifactId>jersey-common</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.core</groupId>
-        <artifactId>jersey-client</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.core</groupId>
-        <artifactId>jersey-server</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.bundles</groupId>
-        <artifactId>jaxrs-ri</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-apache-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-helidon-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-grizzly-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-jetty-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-jdk-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.connectors</groupId>
-        <artifactId>jersey-netty-connector</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-jetty-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-grizzly2-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-grizzly2-servlet</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-jetty-servlet</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-jdk-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-netty-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-servlet</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-servlet-core</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers</groupId>
-        <artifactId>jersey-container-simple-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.containers.glassfish</groupId>
-        <artifactId>jersey-gf-ejb</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-bean-validation</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-entity-filtering</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-metainf-services</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.microprofile</groupId>
-        <artifactId>jersey-mp-config</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-mvc</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-mvc-bean-validation</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-mvc-freemarker</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-mvc-jsp</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-mvc-mustache</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-proxy-client</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-servlet-portability</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-spring4</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-spring5</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-declarative-linking</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext</groupId>
-        <artifactId>jersey-wadl-doclet</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-weld2-se</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi1x</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi1x-transaction</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi1x-validation</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi1x-servlet</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi1x-ban-custom-hk2-binding</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.cdi</groupId>
-        <artifactId>jersey-cdi-rs-inject</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.rx</groupId>
-        <artifactId>jersey-rx-client-guava</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.rx</groupId>
-        <artifactId>jersey-rx-client-rxjava</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.rx</groupId>
-        <artifactId>jersey-rx-client-rxjava2</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.ext.microprofile</groupId>
-        <artifactId>jersey-mp-rest-client</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-jaxb</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-json-jackson</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-json-jettison</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-json-processing</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-json-binding</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-kryo</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-moxy</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-multipart</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.media</groupId>
-        <artifactId>jersey-media-sse</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.security</groupId>
-        <artifactId>oauth1-client</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.security</groupId>
-        <artifactId>oauth1-server</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.security</groupId>
-        <artifactId>oauth1-signature</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.security</groupId>
-        <artifactId>oauth2-client</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.inject</groupId>
-        <artifactId>jersey-hk2</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.inject</groupId>
-        <artifactId>jersey-cdi2-se</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework</groupId>
-        <artifactId>jersey-test-framework-core</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-bundle</artifactId>
-        <version>2.35</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-external</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-inmemory</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-jdk-http</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-simple</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
-        <artifactId>jersey-test-framework-provider-jetty</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.glassfish.jersey.test-framework</groupId>
-        <artifactId>jersey-test-framework-util</artifactId>
-        <version>2.35</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>apache-jsp</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>apache-jstl</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-java-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-java-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-openjdk8-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-openjdk8-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-conscrypt-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-conscrypt-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-alpn-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-annotations</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-ant</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-continuation</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-deploy</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-distribution</artifactId>
-        <version>9.4.53.v20231009</version>
-        <type>zip</type>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-distribution</artifactId>
-        <version>9.4.53.v20231009</version>
-        <type>tar.gz</type>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.fcgi</groupId>
-        <artifactId>fcgi-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.fcgi</groupId>
-        <artifactId>fcgi-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.gcloud</groupId>
-        <artifactId>jetty-gcloud-session-manager</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-home</artifactId>
-        <version>9.4.53.v20231009</version>
-        <type>zip</type>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-home</artifactId>
-        <version>9.4.53.v20231009</version>
-        <type>tar.gz</type>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-http</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.http2</groupId>
-        <artifactId>http2-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.http2</groupId>
-        <artifactId>http2-common</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.http2</groupId>
-        <artifactId>http2-hpack</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.http2</groupId>
-        <artifactId>http2-http-client-transport</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.http2</groupId>
-        <artifactId>http2-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-http-spi</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>infinispan-common</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>infinispan-remote-query</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>infinispan-embedded-query</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-hazelcast</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-io</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-jaas</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-jaspi</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-jmx</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-jndi</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.memcached</groupId>
-        <artifactId>jetty-memcached-sessions</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-nosql</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.osgi</groupId>
-        <artifactId>jetty-osgi-boot</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.osgi</groupId>
-        <artifactId>jetty-osgi-boot-jsp</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.osgi</groupId>
-        <artifactId>jetty-osgi-boot-warurl</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.osgi</groupId>
-        <artifactId>jetty-httpservice</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-plus</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-proxy</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-quickstart</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-rewrite</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-security</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-openid</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-servlet</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-servlets</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-spring</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-unixsocket</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-util</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-util-ajax</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-webapp</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>javax-websocket-client-impl</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>javax-websocket-server-impl</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>websocket-api</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>websocket-client</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>websocket-common</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>websocket-server</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty.websocket</groupId>
-        <artifactId>websocket-servlet</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.eclipse.jetty</groupId>
-        <artifactId>jetty-xml</artifactId>
-        <version>9.4.53.v20231009</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.jupiter</groupId>
-        <artifactId>junit-jupiter</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.jupiter</groupId>
-        <artifactId>junit-jupiter-api</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.jupiter</groupId>
-        <artifactId>junit-jupiter-engine</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.jupiter</groupId>
-        <artifactId>junit-jupiter-migrationsupport</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.jupiter</groupId>
-        <artifactId>junit-jupiter-params</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-commons</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-console</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-engine</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-jfr</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-launcher</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-reporting</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-runner</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-suite</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-suite-api</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-suite-commons</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-suite-engine</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.platform</groupId>
-        <artifactId>junit-platform-testkit</artifactId>
-        <version>1.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.junit.vintage</groupId>
-        <artifactId>junit-vintage-engine</artifactId>
-        <version>5.8.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-stdlib</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-stdlib-jdk7</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-stdlib-jdk8</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-stdlib-js</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-stdlib-common</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-reflect</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-osgi-bundle</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-junit</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-junit5</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-testng</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-js</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-common</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-test-annotations-common</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-main-kts</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-script-runtime</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-script-util</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-scripting-common</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-scripting-jvm</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-scripting-jvm-host</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-scripting-ide-services</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-compiler</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-compiler-embeddable</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlin</groupId>
-        <artifactId>kotlin-daemon-client</artifactId>
-        <version>1.6.21</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-android</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-core-jvm</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-core</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-debug</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-guava</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-javafx</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-jdk8</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-jdk9</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-play-services</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-reactive</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-reactor</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-rx2</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-rx3</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-slf4j</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-swing</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-test-jvm</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.jetbrains.kotlinx</groupId>
-        <artifactId>kotlinx-coroutines-test</artifactId>
-        <version>1.6.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-api</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-core</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-layout-template-json</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-1.2-api</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-jcl</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-flume-ng</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-taglib</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-jmx-gui</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-slf4j-impl</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-slf4j18-impl</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-to-slf4j</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-appserver</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-web</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-couchdb</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-mongodb4</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-mongodb3</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-cassandra</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-jpa</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-iostreams</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-jul</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-jpl</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-liquibase</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-docker</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-kubernetes</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-spring-boot</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-spring-cloud-config-client</artifactId>
-        <version>2.17.2</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-core</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-appoptics</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-atlas</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-azure-monitor</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-cloudwatch</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-cloudwatch2</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-datadog</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-dynatrace</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-elastic</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-ganglia</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-graphite</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-health</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-humio</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-influx</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-jmx</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-kairos</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-new-relic</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-opentsdb</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-otlp</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-prometheus</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-signalfx</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-statsd</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-registry-wavefront</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>io.micrometer</groupId>
-        <artifactId>micrometer-test</artifactId>
-        <version>1.9.17</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-core</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-android</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-errorprone</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-inline</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-junit-jupiter</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mockito</groupId>
-        <artifactId>mockito-proxy</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-buffer</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-dns</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-haproxy</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-http</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-http2</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-memcache</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-mqtt</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-redis</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-smtp</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-socks</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-stomp</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-codec-xml</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-common</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-dev-tools</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-handler</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-handler-proxy</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-handler-ssl-ocsp</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver-dns</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-rxtx</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-sctp</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-udt</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-example</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-all</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver-dns-classes-macos</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver-dns-native-macos</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver-dns-native-macos</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-resolver-dns-native-macos</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>linux-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>linux-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-classes-epoll</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-epoll</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-epoll</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>linux-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-epoll</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>linux-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-classes-kqueue</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-kqueue</artifactId>
-        <version>4.1.101.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-kqueue</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-transport-native-kqueue</artifactId>
-        <version>4.1.101.Final</version>
-        <classifier>osx-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-classes</artifactId>
-        <version>2.0.61.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>linux-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>linux-x86_64-fedora</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>linux-aarch_64-fedora</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>osx-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>linux-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>linux-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>osx-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>osx-aarch_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>io.netty</groupId>
-        <artifactId>netty-tcnative-boringssl-static</artifactId>
-        <version>2.0.61.Final</version>
-        <classifier>windows-x86_64</classifier>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>mockwebserver</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okcurl</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp-brotli</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp-dnsoverhttps</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>logging-interceptor</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp-sse</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp-tls</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.squareup.okhttp3</groupId>
-        <artifactId>okhttp-urlconnection</artifactId>
-        <version>4.9.3</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ojdbc11</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ojdbc8</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ucp</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ucp11</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>rsi</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.security</groupId>
-        <artifactId>oraclepki</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.security</groupId>
-        <artifactId>osdt_core</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.security</groupId>
-        <artifactId>osdt_cert</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.ha</groupId>
-        <artifactId>simplefan</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.ha</groupId>
-        <artifactId>ons</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.nls</groupId>
-        <artifactId>orai18n</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.xml</groupId>
-        <artifactId>xdb</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.xml</groupId>
-        <artifactId>xmlparserv2</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc11_g</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc8_g</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc8dms_g</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc11dms_g</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.observability</groupId>
-        <artifactId>dms</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.observability</groupId>
-        <artifactId>ojdbc11dms</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.observability</groupId>
-        <artifactId>ojdbc8dms</artifactId>
-        <version>21.5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ojdbc11-production</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc</groupId>
-        <artifactId>ojdbc8-production</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.observability</groupId>
-        <artifactId>ojdbc8-observability</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.observability</groupId>
-        <artifactId>ojdbc11-observability</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc8-debug</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc11-debug</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc8-observability-debug</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.jdbc.debug</groupId>
-        <artifactId>ojdbc11-observability-debug</artifactId>
-        <version>21.5.0.0</version>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_caffeine</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_common</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_dropwizard</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_graphite_bridge</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_guava</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_hibernate</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_hotspot</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_httpserver</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_jetty</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_jetty_jdk8</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_log4j</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_log4j2</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_logback</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_pushgateway</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_servlet</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_servlet_jakarta</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_spring_boot</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_spring_web</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_tracer_otel</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_tracer_otel_agent</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.prometheus</groupId>
-        <artifactId>simpleclient_vertx</artifactId>
-        <version>0.15.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-core</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-codegen</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-codegen-utils</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-spatial</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-apt</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-collections</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-guava</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-sql</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-sql-spatial</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-sql-codegen</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-sql-spring</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-jpa</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-jpa-codegen</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-jdo</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-kotlin-codegen</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-lucene3</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-lucene4</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-lucene5</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-hibernate-search</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-mongodb</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-scala</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.querydsl</groupId>
-        <artifactId>querydsl-kotlin</artifactId>
-        <version>5.0.0</version>
-      </dependency>
-      <dependency>
-        <groupId>com.oracle.database.r2dbc</groupId>
-        <artifactId>oracle-r2dbc</artifactId>
-        <version>0.4.0</version>
-      </dependency>
-      <dependency>
-        <groupId>io.r2dbc</groupId>
-        <artifactId>r2dbc-h2</artifactId>
-        <version>0.9.1.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.mariadb</groupId>
-        <artifactId>r2dbc-mariadb</artifactId>
-        <version>1.1.2</version>
-      </dependency>
-      <dependency>
-        <groupId>io.r2dbc</groupId>
-        <artifactId>r2dbc-mssql</artifactId>
-        <version>0.9.0.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>org.postgresql</groupId>
-        <artifactId>r2dbc-postgresql</artifactId>
-        <version>0.9.2.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>io.r2dbc</groupId>
-        <artifactId>r2dbc-pool</artifactId>
-        <version>0.9.2.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>io.r2dbc</groupId>
-        <artifactId>r2dbc-proxy</artifactId>
-        <version>0.9.1.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>io.r2dbc</groupId>
-        <artifactId>r2dbc-spi</artifactId>
-        <version>0.9.1.RELEASE</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor</groupId>
-        <artifactId>reactor-core</artifactId>
-        <version>3.4.34</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor</groupId>
-        <artifactId>reactor-test</artifactId>
-        <version>3.4.34</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor</groupId>
-        <artifactId>reactor-tools</artifactId>
-        <version>3.4.34</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.addons</groupId>
-        <artifactId>reactor-extra</artifactId>
-        <version>3.4.10</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.addons</groupId>
-        <artifactId>reactor-adapter</artifactId>
-        <version>3.4.10</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.netty</groupId>
-        <artifactId>reactor-netty</artifactId>
-        <version>1.0.39</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.netty</groupId>
-        <artifactId>reactor-netty-core</artifactId>
-        <version>1.0.39</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.netty</groupId>
-        <artifactId>reactor-netty-http</artifactId>
-        <version>1.0.39</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.netty</groupId>
-        <artifactId>reactor-netty-http-brave</artifactId>
-        <version>1.0.39</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.addons</groupId>
-        <artifactId>reactor-pool</artifactId>
-        <version>0.2.12</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.kafka</groupId>
-        <artifactId>reactor-kafka</artifactId>
-        <version>1.3.22</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.rabbitmq</groupId>
-        <artifactId>reactor-rabbitmq</artifactId>
-        <version>1.5.6</version>
-      </dependency>
-      <dependency>
-        <groupId>io.projectreactor.kotlin</groupId>
-        <artifactId>reactor-kotlin-extensions</artifactId>
-        <version>1.1.10</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>json-schema-validator</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>rest-assured-common</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>json-path</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>xml-path</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>rest-assured</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>spring-commons</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>spring-mock-mvc</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>scala-support</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>spring-web-test-client</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>kotlin-extensions</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>spring-mock-mvc-kotlin-extensions</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured</groupId>
-        <artifactId>rest-assured-all</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>scalatra-example</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>scalatra-webapp</artifactId>
-        <version>4.5.1</version>
-        <type>war</type>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>rest-assured-itest-java</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>spring-mvc-webapp</artifactId>
-        <version>4.5.1</version>
-        <type>war</type>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>scala-example</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>scala-mock-mvc-example</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rest-assured.examples</groupId>
-        <artifactId>kotlin-example</artifactId>
-        <version>4.5.1</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-core</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-load-balancer</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-micrometer</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-test</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-transport-local</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>io.rsocket</groupId>
-        <artifactId>rsocket-transport-netty</artifactId>
-        <version>1.1.3</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-cassandra</artifactId>
-        <version>3.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-commons</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-couchbase</artifactId>
-        <version>4.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-elasticsearch</artifactId>
-        <version>4.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-geode</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-jdbc</artifactId>
-        <version>2.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-relational</artifactId>
-        <version>2.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-jpa</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-mongodb</artifactId>
-        <version>3.4.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-neo4j</artifactId>
-        <version>6.3.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-r2dbc</artifactId>
-        <version>1.5.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-redis</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-rest-webmvc</artifactId>
-        <version>3.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-rest-core</artifactId>
-        <version>3.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-rest-hal-explorer</artifactId>
-        <version>3.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-keyvalue</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-envers</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.data</groupId>
-        <artifactId>spring-data-ldap</artifactId>
-        <version>2.7.18</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-aop</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-aspects</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-beans</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-context</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-context-indexer</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-context-support</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-core</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-expression</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-instrument</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-jcl</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-jdbc</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-jms</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-messaging</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-orm</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-oxm</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-r2dbc</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-test</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-tx</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-web</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-webflux</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-webmvc</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-websocket</artifactId>
-        <version>5.3.31</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-amqp</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-core</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-event</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-feed</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-file</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-ftp</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-gemfire</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-groovy</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-http</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-ip</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-jdbc</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-jms</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-jmx</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-jpa</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-kafka</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-mail</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-mongodb</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-mqtt</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-r2dbc</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-redis</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-rmi</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-rsocket</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-scripting</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-security</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-sftp</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-stomp</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-stream</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-syslog</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-test</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-test-support</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-webflux</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-websocket</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-ws</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-xml</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-xmpp</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-zeromq</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.integration</groupId>
-        <artifactId>spring-integration-zookeeper</artifactId>
-        <version>5.5.20</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-acl</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-aspects</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-cas</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-config</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-core</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-crypto</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-data</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-ldap</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-messaging</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-oauth2-client</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-oauth2-core</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-oauth2-jose</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-oauth2-resource-server</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-openid</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-remoting</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-rsocket</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-saml2-service-provider</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-taglibs</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-test</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.security</groupId>
-        <artifactId>spring-security-web</artifactId>
-        <version>5.7.11</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-core</artifactId>
-        <version>2.7.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-data-geode</artifactId>
-        <version>2.7.1</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-data-mongodb</artifactId>
-        <version>2.7.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-data-redis</artifactId>
-        <version>2.7.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-hazelcast</artifactId>
-        <version>2.7.4</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework.session</groupId>
-        <artifactId>spring-session-jdbc</artifactId>
-        <version>2.7.4</version>
-      </dependency>
-    </dependencies>
-  </dependencyManagement>
-  <dependencies>
-    <dependency>
-      <groupId>io.springfox</groupId>
-      <artifactId>springfox-swagger2</artifactId>
-      <version>2.9.2</version>
-      <scope>compile</scope>
-      <exclusions>
-        <exclusion>
-          <artifactId>swagger-annotations</artifactId>
-          <groupId>io.swagger</groupId>
-        </exclusion>
-        <exclusion>
-          <artifactId>swagger-models</artifactId>
-          <groupId>io.swagger</groupId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-    <dependency>
-      <groupId>io.springfox</groupId>
-      <artifactId>springfox-swagger-ui</artifactId>
-      <version>2.9.2</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.xiaoymin</groupId>
-      <artifactId>swagger-bootstrap-ui</artifactId>
-      <version>1.9.3</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.javen205</groupId>
-      <artifactId>IJPay-All</artifactId>
-      <version>2.7.8</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.fs</groupId>
-      <artifactId>fs-quartz</artifactId>
-      <version>1.1.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>cn.jpush.api</groupId>
-      <artifactId>jpush-client</artifactId>
-      <version>3.4.3</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.alipay.sdk</groupId>
-      <artifactId>alipay-sdk-java</artifactId>
-      <version>4.8.62.ALL</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>mysql</groupId>
-      <artifactId>mysql-connector-java</artifactId>
-      <version>8.0.33</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.squareup.okhttp3</groupId>
-      <artifactId>okhttp</artifactId>
-      <version>4.9.1</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>cn.jpush.api</groupId>
-      <artifactId>jiguang-common</artifactId>
-      <version>1.1.7</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.fs</groupId>
-      <artifactId>fs-common</artifactId>
-      <version>1.1.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.projectlombok</groupId>
-      <artifactId>lombok</artifactId>
-      <version>1.18.30</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.baidu.dev2</groupId>
-      <artifactId>baiduads-sdk</artifactId>
-      <version>2023.1.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.tzbank</groupId>
-      <artifactId>tzbClient</artifactId>
-      <version>1.0-SNAPSHOT</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.google.zxing</groupId>
-      <artifactId>core</artifactId>
-      <version>3.3.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.google.zxing</groupId>
-      <artifactId>javase</artifactId>
-      <version>3.3.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>net.coobird</groupId>
-      <artifactId>thumbnailator</artifactId>
-      <version>0.4.8</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.qcloud</groupId>
-      <artifactId>cos-sts_api</artifactId>
-      <version>3.1.1</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.qiniu</groupId>
-      <artifactId>qiniu-java-sdk</artifactId>
-      <version>[7.2.0, 7.2.99]</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.aliyun.oss</groupId>
-      <artifactId>aliyun-sdk-oss</artifactId>
-      <version>3.5.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.qcloud</groupId>
-      <artifactId>cos_api</artifactId>
-      <version>5.6.3</version>
-      <scope>compile</scope>
-      <exclusions>
-        <exclusion>
-          <artifactId>slf4j-log4j12</artifactId>
-          <groupId>org.slf4j</groupId>
-        </exclusion>
-      </exclusions>
-    </dependency>
-    <dependency>
-      <groupId>com.tencentcloudapi</groupId>
-      <artifactId>tencentcloud-sdk-java</artifactId>
-      <version>3.1.322</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.hibernate.validator</groupId>
-      <artifactId>hibernate-validator</artifactId>
-      <version>6.2.5.Final</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.binarywang</groupId>
-      <artifactId>weixin-java-miniapp</artifactId>
-      <version>4.7.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.binarywang</groupId>
-      <artifactId>weixin-java-pay</artifactId>
-      <version>4.7.2.B</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.binarywang</groupId>
-      <artifactId>weixin-java-mp</artifactId>
-      <version>4.7.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.binarywang</groupId>
-      <artifactId>weixin-java-cp</artifactId>
-      <version>4.7.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.swagger</groupId>
-      <artifactId>swagger-annotations</artifactId>
-      <version>1.5.21</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.swagger</groupId>
-      <artifactId>swagger-models</artifactId>
-      <version>1.5.21</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.vdurmont</groupId>
-      <artifactId>emoji-java</artifactId>
-      <version>4.0.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.tencentyun</groupId>
-      <artifactId>tls-sig-api-v2</artifactId>
-      <version>2.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.httpcomponents.client5</groupId>
-      <artifactId>httpclient5</artifactId>
-      <version>5.2.1</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.baidubce</groupId>
-      <artifactId>appbuilder</artifactId>
-      <version>0.6.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-test</artifactId>
-      <version>5.2.12.RELEASE</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.huaweicloud.sdk</groupId>
-      <artifactId>huaweicloud-sdk-vod</artifactId>
-      <version>3.1.103</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.huaweicloud.sdk</groupId>
-      <artifactId>huaweicloud-sdk-mpc</artifactId>
-      <version>3.1.88</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.huaweicloud.sdk</groupId>
-      <artifactId>huaweicloud-sdk-cdn</artifactId>
-      <version>3.1.54</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.huaweicloud</groupId>
-      <artifactId>esdk-obs-java-bundle</artifactId>
-      <version>3.23.9</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>ws.schild</groupId>
-      <artifactId>jave-core</artifactId>
-      <version>3.0.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.google.protobuf</groupId>
-      <artifactId>protobuf-java</artifactId>
-      <version>3.11.4</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.redisson</groupId>
-      <artifactId>redisson-spring-boot-starter</artifactId>
-      <version>3.13.6</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.rocketmq</groupId>
-      <artifactId>rocketmq-spring-boot-starter</artifactId>
-      <version>2.2.3</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.baomidou</groupId>
-      <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
-      <version>3.1.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-text</artifactId>
-      <version>1.10.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.wechatpay-apiv3</groupId>
-      <artifactId>wechatpay-java</artifactId>
-      <version>0.2.16</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.ben-manes.caffeine</groupId>
-      <artifactId>caffeine</artifactId>
-      <version>2.9.3</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.mapstruct</groupId>
-      <artifactId>mapstruct</artifactId>
-      <version>1.5.5.Final</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.mapstruct</groupId>
-      <artifactId>mapstruct-processor</artifactId>
-      <version>1.5.5.Final</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.hc</groupId>
-      <artifactId>openapi</artifactId>
-      <version>1.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.volcengine</groupId>
-      <artifactId>volc-sdk-java</artifactId>
-      <version>1.0.250</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.retry</groupId>
-      <artifactId>spring-retry</artifactId>
-      <version>1.3.1</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.alibaba</groupId>
-      <artifactId>druid-spring-boot-starter</artifactId>
-      <version>1.2.6</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.penggle</groupId>
-      <artifactId>kaptcha</artifactId>
-      <version>2.3.2</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.github.oshi</groupId>
-      <artifactId>oshi-core</artifactId>
-      <version>5.8.0</version>
-      <scope>compile</scope>
-    </dependency>
-    <dependency>
-      <groupId>com.hankcs</groupId>
-      <artifactId>aho-corasick-double-array-trie</artifactId>
-      <version>1.2.3</version>
-      <scope>compile</scope>
-    </dependency>
-  </dependencies>
-  <repositories>
-    <repository>
-      <releases>
-        <enabled>true</enabled>
-      </releases>
-      <id>public</id>
-      <name>aliyun nexus</name>
-      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
-    </repository>
-    <repository>
-      <id>OceanengineOpenApi</id>
-      <name>ad_open_sdk_java</name>
-      <url>https://artifact.bytedance.com/repository/releases/</url>
-    </repository>
-    <repository>
-      <snapshots>
-        <enabled>false</enabled>
-      </snapshots>
-      <id>central</id>
-      <name>Central Repository</name>
-      <url>https://repo.maven.apache.org/maven2</url>
-    </repository>
-  </repositories>
-  <pluginRepositories>
-    <pluginRepository>
-      <releases>
-        <enabled>true</enabled>
-      </releases>
-      <snapshots>
-        <enabled>false</enabled>
-      </snapshots>
-      <id>public</id>
-      <name>aliyun nexus</name>
-      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
-    </pluginRepository>
-    <pluginRepository>
-      <releases>
-        <updatePolicy>never</updatePolicy>
-      </releases>
-      <snapshots>
-        <enabled>false</enabled>
-      </snapshots>
-      <id>central</id>
-      <name>Central Repository</name>
-      <url>https://repo.maven.apache.org/maven2</url>
-    </pluginRepository>
-  </pluginRepositories>
-  <build>
-    <sourceDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\src\main\java</sourceDirectory>
-    <scriptSourceDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\src\main\scripts</scriptSourceDirectory>
-    <testSourceDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\src\test\java</testSourceDirectory>
-    <outputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\classes</outputDirectory>
-    <testOutputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\test-classes</testOutputDirectory>
-    <resources>
-      <resource>
-        <directory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\src\main\resources</directory>
-      </resource>
-    </resources>
-    <testResources>
-      <testResource>
-        <directory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\src\test\resources</directory>
-      </testResource>
-    </testResources>
-    <directory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target</directory>
-    <finalName>fs-service-1.1.0</finalName>
-    <pluginManagement>
-      <plugins>
-        <plugin>
-          <artifactId>maven-antrun-plugin</artifactId>
-          <version>1.3</version>
-        </plugin>
-        <plugin>
-          <artifactId>maven-assembly-plugin</artifactId>
-          <version>2.2-beta-5</version>
-        </plugin>
-        <plugin>
-          <artifactId>maven-dependency-plugin</artifactId>
-          <version>2.8</version>
-        </plugin>
-        <plugin>
-          <artifactId>maven-release-plugin</artifactId>
-          <version>2.5.3</version>
-        </plugin>
-      </plugins>
-    </pluginManagement>
-    <plugins>
-      <plugin>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <version>3.11.0</version>
-        <executions>
-          <execution>
-            <id>default-compile</id>
-            <phase>compile</phase>
-            <goals>
-              <goal>compile</goal>
-            </goals>
-            <configuration>
-              <source>17</source>
-              <target>17</target>
-              <encoding>UTF-8</encoding>
-              <annotationProcessorPaths>
-                <path>
-                  <groupId>org.projectlombok</groupId>
-                  <artifactId>lombok</artifactId>
-                  <version>1.18.32</version>
-                </path>
-                <path>
-                  <groupId>org.mapstruct</groupId>
-                  <artifactId>mapstruct-processor</artifactId>
-                  <version>1.5.5.Final</version>
-                </path>
-                <path>
-                  <groupId>org.projectlombok</groupId>
-                  <artifactId>lombok-mapstruct-binding</artifactId>
-                  <version>0.2.0</version>
-                </path>
-              </annotationProcessorPaths>
-            </configuration>
-          </execution>
-          <execution>
-            <id>default-testCompile</id>
-            <phase>test-compile</phase>
-            <goals>
-              <goal>testCompile</goal>
-            </goals>
-            <configuration>
-              <source>17</source>
-              <target>17</target>
-              <encoding>UTF-8</encoding>
-              <annotationProcessorPaths>
-                <path>
-                  <groupId>org.projectlombok</groupId>
-                  <artifactId>lombok</artifactId>
-                  <version>1.18.32</version>
-                </path>
-                <path>
-                  <groupId>org.mapstruct</groupId>
-                  <artifactId>mapstruct-processor</artifactId>
-                  <version>1.5.5.Final</version>
-                </path>
-                <path>
-                  <groupId>org.projectlombok</groupId>
-                  <artifactId>lombok-mapstruct-binding</artifactId>
-                  <version>0.2.0</version>
-                </path>
-              </annotationProcessorPaths>
-            </configuration>
-          </execution>
-        </executions>
-        <configuration>
-          <source>17</source>
-          <target>17</target>
-          <encoding>UTF-8</encoding>
-          <annotationProcessorPaths>
-            <path>
-              <groupId>org.projectlombok</groupId>
-              <artifactId>lombok</artifactId>
-              <version>1.18.32</version>
-            </path>
-            <path>
-              <groupId>org.mapstruct</groupId>
-              <artifactId>mapstruct-processor</artifactId>
-              <version>1.5.5.Final</version>
-            </path>
-            <path>
-              <groupId>org.projectlombok</groupId>
-              <artifactId>lombok-mapstruct-binding</artifactId>
-              <version>0.2.0</version>
-            </path>
-          </annotationProcessorPaths>
-        </configuration>
-      </plugin>
-      <plugin>
-        <artifactId>maven-clean-plugin</artifactId>
-        <version>2.5</version>
-        <executions>
-          <execution>
-            <id>default-clean</id>
-            <phase>clean</phase>
-            <goals>
-              <goal>clean</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-resources-plugin</artifactId>
-        <version>2.6</version>
-        <executions>
-          <execution>
-            <id>default-testResources</id>
-            <phase>process-test-resources</phase>
-            <goals>
-              <goal>testResources</goal>
-            </goals>
-          </execution>
-          <execution>
-            <id>default-resources</id>
-            <phase>process-resources</phase>
-            <goals>
-              <goal>resources</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-jar-plugin</artifactId>
-        <version>2.4</version>
-        <executions>
-          <execution>
-            <id>default-jar</id>
-            <phase>package</phase>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-surefire-plugin</artifactId>
-        <version>2.12.4</version>
-        <executions>
-          <execution>
-            <id>default-test</id>
-            <phase>test</phase>
-            <goals>
-              <goal>test</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-install-plugin</artifactId>
-        <version>2.4</version>
-        <executions>
-          <execution>
-            <id>default-install</id>
-            <phase>install</phase>
-            <goals>
-              <goal>install</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-deploy-plugin</artifactId>
-        <version>2.7</version>
-        <executions>
-          <execution>
-            <id>default-deploy</id>
-            <phase>deploy</phase>
-            <goals>
-              <goal>deploy</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <plugin>
-        <artifactId>maven-site-plugin</artifactId>
-        <version>3.3</version>
-        <executions>
-          <execution>
-            <id>default-site</id>
-            <phase>site</phase>
-            <goals>
-              <goal>site</goal>
-            </goals>
-            <configuration>
-              <outputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\site</outputDirectory>
-              <reportPlugins>
-                <reportPlugin>
-                  <groupId>org.apache.maven.plugins</groupId>
-                  <artifactId>maven-project-info-reports-plugin</artifactId>
-                </reportPlugin>
-              </reportPlugins>
-            </configuration>
-          </execution>
-          <execution>
-            <id>default-deploy</id>
-            <phase>site-deploy</phase>
-            <goals>
-              <goal>deploy</goal>
-            </goals>
-            <configuration>
-              <outputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\site</outputDirectory>
-              <reportPlugins>
-                <reportPlugin>
-                  <groupId>org.apache.maven.plugins</groupId>
-                  <artifactId>maven-project-info-reports-plugin</artifactId>
-                </reportPlugin>
-              </reportPlugins>
-            </configuration>
-          </execution>
-        </executions>
-        <configuration>
-          <outputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\site</outputDirectory>
-          <reportPlugins>
-            <reportPlugin>
-              <groupId>org.apache.maven.plugins</groupId>
-              <artifactId>maven-project-info-reports-plugin</artifactId>
-            </reportPlugin>
-          </reportPlugins>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-  <reporting>
-    <outputDirectory>F:\project\Saas\ylrz_saas_his_scrm\fs-service\target\site</outputDirectory>
-  </reporting>
-</project>
-
-
-[INFO] ------------------------------------------------------------------------
-[INFO] BUILD SUCCESS
-[INFO] ------------------------------------------------------------------------
-[INFO] Total time:  0.920 s
-[INFO] Finished at: 2026-06-01T15:07:35+08:00
-[INFO] ------------------------------------------------------------------------

+ 259 - 0
fs-admin-saas/src/main/java/com/fs/admin/controller/company/SysRedpacketConfigMoreController.java

@@ -0,0 +1,259 @@
+package com.fs.admin.controller.company;
+
+
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.framework.datasource.TenantDataSourceContextHelper;
+import com.fs.his.domain.SysRedpacketConfigMore;
+import com.fs.his.param.UpdateChangeMchIdParam;
+import com.fs.his.service.ISysRedpacketConfigMoreService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Slf4j
+
+/**
+ * 多商户配置Controller
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+@RestController
+@RequestMapping("/redPacket/more")
+public class SysRedpacketConfigMoreController extends BaseController
+{
+    @Autowired
+    private ISysRedpacketConfigMoreService sysRedpacketConfigMoreService;
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private TenantDataSourceContextHelper tenantContextHelper;
+
+    /**
+     * 查询多商户配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        startPage();
+        List<SysRedpacketConfigMore> list = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreList(sysRedpacketConfigMore);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取多商户配置详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(id));
+    }
+
+    /**
+     * 获取当前发红包的商户号
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:editConfig')")
+    @GetMapping(value = "/getRedPacketConfig")
+    public AjaxResult getRedPacketConFig()
+    {
+        return AjaxResult.success(sysRedpacketConfigMoreService.getRedPacketConFig());
+    }
+
+
+
+
+    /**
+     * 新增多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:add')")
+    @Log(title = "多商户配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        int result = sysRedpacketConfigMoreService.insertSysRedpacketConfigMore(sysRedpacketConfigMore);
+
+        // 如果指定了租户,同步到租户库
+        if (result > 0 && sysRedpacketConfigMore.getTenantId() != null) {
+            try {
+                syncToTenant(sysRedpacketConfigMore, sysRedpacketConfigMore.getTenantId());
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库失败, tenantId={}, mchId={}",
+                    sysRedpacketConfigMore.getTenantId(), sysRedpacketConfigMore.getMchId(), e);
+                // 不影响主流程,记录日志即可
+            }
+        }
+
+        return toAjax(result);
+    }
+
+    /**
+     * 修改发商户号的商户
+     */
+    @Log(title = "修改发商户号的商户", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateChangeMchId")
+    public R updateChangeMchId(@RequestBody UpdateChangeMchIdParam param)
+    {
+        return sysRedpacketConfigMoreService.updateChangeMchId(param);
+    }
+
+    /**
+     * 清空当前红包商户号配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:editConfig')")
+    @Log(title = "清空红包商户号配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/clearRedPacketMchId")
+    public R clearRedPacketMchId()
+    {
+        return sysRedpacketConfigMoreService.clearRedPacketMchId();
+    }
+
+    /**
+     * 修改多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:edit')")
+    @Log(title = "多商户配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        // 查询原数据,获取原租户ID
+        SysRedpacketConfigMore oldConfig = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(sysRedpacketConfigMore.getId());
+        Long oldTenantId = oldConfig != null ? oldConfig.getTenantId() : null;
+        Long newTenantId = sysRedpacketConfigMore.getTenantId();
+        String mchId = sysRedpacketConfigMore.getMchId();
+
+        int result = sysRedpacketConfigMoreService.updateSysRedpacketConfigMore(sysRedpacketConfigMore);
+
+        if (result > 0) {
+            try {
+                // 如果原租户存在且与新租户不同,从原租户库删除
+                if (oldTenantId != null && !oldTenantId.equals(newTenantId)) {
+                    deleteFromTenant(mchId, oldTenantId);
+                }
+                // 如果指定了新租户,同步到新租户库
+                if (newTenantId != null) {
+                    syncToTenant(sysRedpacketConfigMore, newTenantId);
+                }
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库失败, mchId={}, oldTenantId={}, newTenantId={}",
+                    mchId, oldTenantId, newTenantId, e);
+            }
+        }
+
+        return toAjax(result);
+    }
+
+    /**
+     * 删除多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:remove')")
+    @Log(title = "多商户配置", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        // 先查询要删除的数据,获取租户信息
+        for (Long id : ids) {
+            SysRedpacketConfigMore config = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(id);
+            if (config != null && config.getTenantId() != null) {
+                try {
+                    deleteFromTenant(config.getMchId(), config.getTenantId());
+                } catch (Exception e) {
+                    log.error("从租户库删除商户配置失败, tenantId={}, mchId={}",
+                        config.getTenantId(), config.getMchId(), e);
+                }
+            }
+        }
+
+        return toAjax(sysRedpacketConfigMoreService.deleteSysRedpacketConfigMoreByIds(ids));
+    }
+
+    /**
+     * 获取租户列表(用于下拉选择)
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:list')")
+    @GetMapping("/tenantList")
+    public AjaxResult tenantList()
+    {
+        TenantInfo query = new TenantInfo();
+        query.setStatus(1); // 只查询启用的租户
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 同步商户配置到租户库
+     */
+    private void syncToTenant(SysRedpacketConfigMore config, Long tenantId) {
+        if (tenantId == null) {
+            return;
+        }
+        tenantContextHelper.runInTenant(tenantId, () -> {
+            try {
+                // 查询租户库是否已存在该商户号的配置
+                SysRedpacketConfigMore existConfig = sysRedpacketConfigMoreService.getOne(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysRedpacketConfigMore>()
+                        .eq("mch_id", config.getMchId())
+                );
+
+                // 复制一份数据用于租户库
+                SysRedpacketConfigMore tenantConfig = new SysRedpacketConfigMore();
+                BeanCopyUtils.copy(config, tenantConfig);
+
+                if (existConfig != null) {
+                    // 更新现有记录
+                    tenantConfig.setId(existConfig.getId());
+                    sysRedpacketConfigMoreService.updateSysRedpacketConfigMore(tenantConfig);
+                    log.info("同步商户配置到租户库-更新, tenantId={}, mchId={}", tenantId, config.getMchId());
+                } else {
+                    // 插入新记录,清空ID让数据库自增
+                    tenantConfig.setId(null);
+                    sysRedpacketConfigMoreService.insertSysRedpacketConfigMore(tenantConfig);
+                    log.info("同步商户配置到租户库-新增, tenantId={}, mchId={}", tenantId, config.getMchId());
+                }
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库异常, tenantId={}, mchId={}", tenantId, config.getMchId(), e);
+                throw new RuntimeException("同步到租户库失败: " + e.getMessage(), e);
+            }
+        });
+    }
+
+    /**
+     * 从租户库删除商户配置
+     */
+    private void deleteFromTenant(String mchId, Long tenantId) {
+        if (tenantId == null || mchId == null) {
+            return;
+        }
+        tenantContextHelper.runInTenant(tenantId, () -> {
+            try {
+                // 查询租户库中的记录
+                SysRedpacketConfigMore config = sysRedpacketConfigMoreService.getOne(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysRedpacketConfigMore>()
+                        .eq("mch_id", mchId)
+                );
+                if (config != null) {
+                    sysRedpacketConfigMoreService.deleteSysRedpacketConfigMoreById(config.getId());
+                    log.info("从租户库删除商户配置, tenantId={}, mchId={}", tenantId, mchId);
+                }
+            } catch (Exception e) {
+                log.error("从租户库删除商户配置异常, tenantId={}, mchId={}", tenantId, mchId, e);
+                throw new RuntimeException("从租户库删除失败: " + e.getMessage(), e);
+            }
+        });
+    }
+}

+ 18 - 0
fs-admin-saas/src/main/java/com/fs/company/controller/CompanySmsTempController.java

@@ -7,8 +7,10 @@ import com.fs.common.core.domain.R;
 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.SecurityUtils;
 import com.fs.company.domain.CompanySmsTemp;
 import com.fs.company.service.ICompanySmsTempService;
+import com.fs.company.service.tenant.ITenantMasterSmsApiQueryService;
 import com.fs.company.vo.CompanySmsTempListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -29,6 +31,22 @@ public class CompanySmsTempController extends BaseController
     @Autowired
     private ICompanySmsTempService companySmsTempService;
 
+    @Autowired
+    private ITenantMasterSmsApiQueryService tenantMasterSmsApiQueryService;
+
+    /**
+     * 查询当前租户已绑定的短信接口(主库 company_sms_api_tenant,供模板绑定下拉)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companySmsTemp:list')")
+    @GetMapping("/smsApiOptions")
+    public AjaxResult smsApiOptions() {
+        Long tenantId = SecurityUtils.getTenantId();
+        if (tenantId == null) {
+            return AjaxResult.error("无法获取当前租户信息");
+        }
+        return AjaxResult.success(tenantMasterSmsApiQueryService.selectBoundSmsApisByTenantId(tenantId));
+    }
+
     /**
      * 查询短信模板列表
      */

+ 27 - 10
fs-admin-saas/src/main/java/com/fs/company/controller/CompanyVoiceApiController.java

@@ -8,7 +8,9 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
 import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiQueryService;
 import com.fs.voice.service.IVoiceService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -17,7 +19,7 @@ import org.springframework.web.bind.annotation.*;
 import java.util.List;
 
 /**
- * 呼叫接口Controller
+ * 呼叫接口Controller(租户总后台:读租户库 company_voice_api)
  *
  * @author fs
  * @date 2021-10-04
@@ -28,16 +30,19 @@ public class CompanyVoiceApiController extends BaseController {
     @Autowired
     private ICompanyVoiceApiService companyVoiceApiService;
     @Autowired
+    private ITenantCompanyVoiceApiQueryService tenantCompanyVoiceApiQueryService;
+    @Autowired
     private IVoiceService voiceService;
 
     /**
-     * 查询呼叫接口列表
+     * 查询呼叫接口列表(租户库,含售价/优先级等同步字段)
      */
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceApi companyVoiceApi) {
         startPage();
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(companyVoiceApi);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(
+                toTenantQuery(companyVoiceApi));
         return getDataTable(list);
     }
 
@@ -48,8 +53,9 @@ public class CompanyVoiceApiController extends BaseController {
     @Log(title = "呼叫接口", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(CompanyVoiceApi companyVoiceApi) {
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(companyVoiceApi);
-        ExcelUtil<CompanyVoiceApi> util = new ExcelUtil<CompanyVoiceApi>(CompanyVoiceApi.class);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(
+                toTenantQuery(companyVoiceApi));
+        ExcelUtil<TenantCompanyVoiceApi> util = new ExcelUtil<>(TenantCompanyVoiceApi.class);
         return util.exportExcel(list, "companyVoiceApi");
     }
 
@@ -59,7 +65,7 @@ public class CompanyVoiceApiController extends BaseController {
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:query')")
     @GetMapping(value = "/{apiId}")
     public AjaxResult getInfo(@PathVariable("apiId") Long apiId) {
-        return AjaxResult.success(companyVoiceApiService.selectCompanyVoiceApiById(apiId));
+        return AjaxResult.success(tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiById(apiId));
     }
 
     /**
@@ -94,11 +100,22 @@ public class CompanyVoiceApiController extends BaseController {
 
     @GetMapping("/getVoiceApiList")
     public R getVoiceApiList() {
-        CompanyVoiceApi map = new CompanyVoiceApi();
-        map.setStatus(1);
-        List<CompanyVoiceApi> list = companyVoiceApiService.selectCompanyVoiceApiList(map);
+        TenantCompanyVoiceApi query = new TenantCompanyVoiceApi();
+        query.setStatus(1);
+        List<TenantCompanyVoiceApi> list = tenantCompanyVoiceApiQueryService.selectTenantCompanyVoiceApiList(query);
         return R.ok().put("data", list);
     }
 
-
+    private TenantCompanyVoiceApi toTenantQuery(CompanyVoiceApi companyVoiceApi) {
+        TenantCompanyVoiceApi query = new TenantCompanyVoiceApi();
+        if (companyVoiceApi == null) {
+            return query;
+        }
+        query.setApiName(companyVoiceApi.getApiName());
+        if (companyVoiceApi.getApiType() != null) {
+            query.setApiType(String.valueOf(companyVoiceApi.getApiType()));
+        }
+        query.setStatus(companyVoiceApi.getStatus());
+        return query;
+    }
 }

+ 19 - 1
fs-admin-saas/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -12,6 +12,8 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.web.service.TokenService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
 import com.fs.his.domain.FsStoreAfterSalesLogs;
 import com.fs.his.domain.FsUser;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
@@ -67,6 +69,9 @@ public class LiveAfterSalesController extends BaseController
     @Autowired
     private ISysConfigService sysConfigService;
 
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
     /**
      * 获取售后记录详细信息
      */
@@ -104,6 +109,19 @@ public class LiveAfterSalesController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 获取当前租户的公司名称
+     * @return 租户名称,如果未获取到则返回null
+     */
+    private String getCurrentTenantCompanyName() {
+        Long tenantId = com.fs.wxcid.utils.TenantHelper.getTenantId();
+        if (tenantId == null) {
+            return null;
+        }
+        TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+        return tenantInfo != null ? tenantInfo.getTenantName() : null;
+    }
+
     /**
      * 导出售后记录列表
      */
@@ -115,7 +133,7 @@ public class LiveAfterSalesController extends BaseController
         PageHelper.clearPage();
         PageHelper.startPage(1, 10000, "");
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoListExport(liveAfterSales);
-        if("北京卓美".equals(sysConfigService.getProjectConfig().getCloudHost().getCompanyName())){
+        if("北京卓美".equals(getCurrentTenantCompanyName())){
             List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                     .map(vo -> {
                         FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();

+ 380 - 36
fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java

@@ -19,8 +19,8 @@ import java.time.LocalDateTime;
 import java.util.*;
 
 /**
- * 龙虾引擎管理端Controller(fs-admin-saas,替代原 AdminLobsterBridgeController
- * 全部使用 MyBatis Service,无 JdbcTemplate,无桥接镜像表
+ * 龙虾引擎管理端 Controller(fs-admin-saas)
+ * 直连 MyBatis Service / JdbcTemplate 租户库,无桥接镜像表
  */
 @RestController
 public class LobsterAdminController extends BaseController {
@@ -64,6 +64,15 @@ public class LobsterAdminController extends BaseController {
     @Autowired(required = false)
     private com.fs.company.mapper.LobsterChatMsgMapper chatMsgMapper;
 
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.evolution.EvolutionEngine evolutionEngine;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.heartbeat.HeartbeatScheduler heartbeatScheduler;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.channel.MessageChannelRouter messageChannelRouter;
+
     @Autowired
     private TokenService tokenService;
 
@@ -185,10 +194,40 @@ public class LobsterAdminController extends BaseController {
     // ======== 以下为占位端点(无 MyBatis Service 实现的,返回空数据,前端不报 404) ========
 
     @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
-    public AjaxResult lobsterGenerate() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterGenerate(@RequestParam(required = false) Long companyId) {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            String sql = "SELECT id, company_id, workflow_id, node_code, suggestion_type, reason, confidence, status, create_time " +
+                    "FROM lobster_evolution_suggestion WHERE 1=1";
+            List<Object> params = new ArrayList<>();
+            if (companyId != null) {
+                sql += " AND company_id=?";
+                params.add(companyId);
+            }
+            sql += " ORDER BY create_time DESC LIMIT 100";
+            return AjaxResult.success(jdbcTemplate.queryForList(sql, params.toArray()));
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
 
     @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
-    public AjaxResult lobsterCanvas() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterCanvas(@RequestParam(required = false) Long companyId) {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            String sql = "SELECT id, company_id, template_code, template_name, industry_type, status, version, " +
+                    "canvas_data, update_time, create_time FROM company_workflow_lobster WHERE del_flag=0";
+            List<Object> params = new ArrayList<>();
+            if (companyId != null) {
+                sql += " AND company_id=?";
+                params.add(companyId);
+            }
+            sql += " ORDER BY update_time DESC LIMIT 200";
+            return AjaxResult.success(jdbcTemplate.queryForList(sql, params.toArray()));
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
 
     @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
     public AjaxResult lobsterTemplate() {
@@ -266,39 +305,94 @@ public class LobsterAdminController extends BaseController {
     }
 
     @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
-    public AjaxResult lobsterInstance() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterInstance(@RequestParam(required = false) Long companyId,
+                                       @RequestParam(required = false) String status) {
+        return lobsterExecInstanceList(companyId, null, status);
+    }
 
     @GetMapping("/workflow/lobster/instance/stats")
-    public AjaxResult lobsterInstanceStats() {
+    public AjaxResult lobsterInstanceStats(@RequestParam(required = false) Long companyId) {
         Map<String, Object> stats = new HashMap<>();
-        stats.put("running", 0); stats.put("paused", 0);
-        stats.put("deadLetters", 0); stats.put("todayTokens", "0");
+        if (jdbcTemplate != null) {
+            try {
+                String base = " FROM lobster_workflow_instance WHERE del_flag=0";
+                List<Object> params = new ArrayList<>();
+                if (companyId != null) { base += " AND company_id=?"; params.add(companyId); }
+                stats.put("running", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*)" + base + " AND status='running'", params.toArray(), Integer.class));
+                stats.put("paused", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*)" + base + " AND status='paused'", params.toArray(), Integer.class));
+                stats.put("completed", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*)" + base + " AND status='completed'", params.toArray(), Integer.class));
+                stats.put("deadLetters", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_dead_letter_queue WHERE status='pending'"
+                                + (companyId != null ? " AND company_id=?" : ""),
+                        companyId != null ? new Object[]{companyId} : new Object[]{}, Integer.class));
+                Object tokens = jdbcTemplate.queryForObject(
+                        "SELECT COALESCE(SUM(token_count),0) FROM lobster_token_consume_log WHERE DATE(create_time)=CURDATE()"
+                                + (companyId != null ? " AND company_id=?" : ""),
+                        companyId != null ? new Object[]{companyId} : new Object[]{}, Object.class);
+                stats.put("todayTokens", tokens != null ? tokens.toString() : "0");
+            } catch (Exception e) {
+                stats.put("running", 0); stats.put("paused", 0);
+                stats.put("deadLetters", 0); stats.put("todayTokens", "0");
+            }
+        } else {
+            stats.put("running", 0); stats.put("paused", 0);
+            stats.put("deadLetters", 0); stats.put("todayTokens", "0");
+        }
         return AjaxResult.success(stats);
     }
 
     @GetMapping("/workflow/lobster/instance/{instanceId}")
-    public AjaxResult lobsterInstanceDetail(@PathVariable String instanceId) {
-        Map<String, Object> data = new HashMap<>();
-        data.put("instanceId", instanceId);
-        data.put("status", "unknown");
-        return AjaxResult.success(data);
+    public AjaxResult lobsterInstanceDetail(@PathVariable Long instanceId,
+                                             @RequestParam(required = false) Long companyId) {
+        return lobsterExecInstanceGet(instanceId, companyId);
     }
 
     @GetMapping("/workflow/lobster/instance/node-logs/{instanceId}")
-    public AjaxResult lobsterInstanceNodeLogs(@PathVariable String instanceId) {
-        return AjaxResult.success(new ArrayList<>());
+    public AjaxResult lobsterInstanceNodeLogs(@PathVariable Long instanceId,
+                                               @RequestParam(required = false) Long companyId) {
+        return lobsterExecNodeLogs(instanceId, companyId);
     }
 
     @PostMapping("/workflow/lobster/instance/terminate/{instanceId}")
-    public AjaxResult lobsterInstanceTerminate(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
+    public AjaxResult lobsterInstanceTerminate(@PathVariable Long instanceId,
+                                                @RequestParam(defaultValue = "管理员手动终止") String reason) {
+        return lobsterExecTerminate(String.valueOf(instanceId), reason);
     }
 
     @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
-    public AjaxResult lobsterOptimization() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterOptimization(@RequestParam(required = false) Long companyId) {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            if (companyId != null) {
+                return AjaxResult.success(jdbcTemplate.queryForList(
+                        "SELECT * FROM lobster_evolution_suggestion WHERE company_id=? ORDER BY create_time DESC LIMIT 200",
+                        companyId));
+            }
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                    "SELECT * FROM lobster_evolution_suggestion ORDER BY create_time DESC LIMIT 200"));
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
 
     @GetMapping("/workflow/lobster/optimization/pending-audit")
-    public AjaxResult lobsterOptimizationPendingAudit() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterOptimizationPendingAudit(@RequestParam(required = false) Long companyId) {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            if (companyId != null) {
+                return AjaxResult.success(jdbcTemplate.queryForList(
+                        "SELECT * FROM lobster_evolution_suggestion WHERE company_id=? AND status=0 ORDER BY create_time DESC LIMIT 100",
+                        companyId));
+            }
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                    "SELECT * FROM lobster_evolution_suggestion WHERE status=0 ORDER BY create_time DESC LIMIT 100"));
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
 
     @PostMapping("/workflow/lobster/optimization/batch-audit")
     public AjaxResult lobsterOptimizationBatchAudit() { return AjaxResult.success("审核完成"); }
@@ -309,17 +403,43 @@ public class LobsterAdminController extends BaseController {
     }
 
     @PostMapping("/workflow/lobster/optimization/analyze")
-    public AjaxResult lobsterOptimizationAnalyze() {
+    public AjaxResult lobsterOptimizationAnalyze(@RequestParam(required = false) Long companyId,
+                                                  @RequestParam(required = false) Long workflowId) {
+        if (evolutionEngine != null && companyId != null && workflowId != null) {
+            return AjaxResult.success(evolutionEngine.analyzeAndSuggest(companyId, workflowId));
+        }
         Map<String, Object> result = new HashMap<>();
-        result.put("totalSuggestions", 0);
+        if (jdbcTemplate != null && companyId != null) {
+            try {
+                Integer total = jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=?", Integer.class, companyId);
+                result.put("totalSuggestions", total != null ? total : 0);
+            } catch (Exception e) {
+                result.put("totalSuggestions", 0);
+            }
+        } else {
+            result.put("totalSuggestions", 0);
+        }
         return AjaxResult.success(result);
     }
 
     @GetMapping("/workflow/lobster/optimization/stats")
-    public AjaxResult lobsterOptimizationStats() {
+    public AjaxResult lobsterOptimizationStats(@RequestParam(required = false) Long companyId) {
         Map<String, Object> stats = new HashMap<>();
         stats.put("total", 0); stats.put("pending", 0);
         stats.put("approved", 0); stats.put("rejected", 0);
+        if (jdbcTemplate != null && companyId != null) {
+            try {
+                stats.put("total", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=?", Integer.class, companyId));
+                stats.put("pending", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=? AND status=0", Integer.class, companyId));
+                stats.put("approved", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=? AND status=1", Integer.class, companyId));
+                stats.put("rejected", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=? AND status=2", Integer.class, companyId));
+            } catch (Exception ignored) { }
+        }
         return AjaxResult.success(stats);
     }
 
@@ -459,12 +579,49 @@ public class LobsterAdminController extends BaseController {
         return m;
     }
 
-    // ======== lobster-exec 占位端点 ========
+    // ======== lobster-exec(管理端实例监控,JDBC 直查 + 执行器) ========
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterWorkflowInstanceMapper workflowInstanceMapper;
+
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterNodeExecutionLogMapper nodeExecutionLogMapper;
+
     @GetMapping({"/workflow/lobster-exec/instance", "/workflow/lobster-exec/instance/list"})
-    public AjaxResult lobsterExecInstanceList() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterExecInstanceList(@RequestParam(required = false) Long companyId,
+                                               @RequestParam(required = false) Long workflowId,
+                                               @RequestParam(required = false) String status) {
+        if (jdbcTemplate != null) {
+            StringBuilder sql = new StringBuilder(
+                "SELECT id, company_id, workflow_id, instance_name, status, contact_id, control_mode, " +
+                "current_node_index, current_node_name, total_nodes, completed_nodes, create_time, update_time " +
+                "FROM lobster_workflow_instance WHERE del_flag=0");
+            List<Object> params = new ArrayList<>();
+            if (companyId != null) { sql.append(" AND company_id=?"); params.add(companyId); }
+            if (workflowId != null) { sql.append(" AND workflow_id=?"); params.add(workflowId); }
+            if (status != null && !status.isEmpty()) { sql.append(" AND status=?"); params.add(status); }
+            sql.append(" ORDER BY create_time DESC LIMIT 500");
+            return AjaxResult.success(jdbcTemplate.queryForList(sql.toString(), params.toArray()));
+        }
+        if (workflowInstanceMapper == null) return AjaxResult.success(new ArrayList<>());
+        if (companyId == null) return AjaxResult.success(new ArrayList<>());
+        List<com.fs.company.domain.LobsterWorkflowInstance> list = workflowInstanceMapper.selectByCompanyId(companyId);
+        return AjaxResult.success(list != null ? list : new ArrayList<>());
+    }
 
     @GetMapping("/workflow/lobster-exec/instance/{instanceId}")
-    public AjaxResult lobsterExecInstanceGet(@PathVariable String instanceId) {
+    public AjaxResult lobsterExecInstanceGet(@PathVariable Long instanceId,
+                                                @RequestParam(required = false) Long companyId) {
+        if (workflowExecutor != null && companyId != null) {
+            return AjaxResult.success(workflowExecutor.getInstanceState(companyId, instanceId));
+        }
+        if (jdbcTemplate != null) {
+            try {
+                return AjaxResult.success(jdbcTemplate.queryForMap(
+                    "SELECT * FROM lobster_workflow_instance WHERE id=? AND del_flag=0", instanceId));
+            } catch (Exception e) {
+                return AjaxResult.error("实例不存在");
+            }
+        }
         Map<String, Object> data = new HashMap<>();
         data.put("instanceId", instanceId);
         data.put("status", "unknown");
@@ -472,12 +629,35 @@ public class LobsterAdminController extends BaseController {
     }
 
     @GetMapping("/workflow/lobster-exec/node-logs/{instanceId}")
-    public AjaxResult lobsterExecNodeLogs(@PathVariable String instanceId) {
+    public AjaxResult lobsterExecNodeLogs(@PathVariable Long instanceId,
+                                           @RequestParam(required = false) Long companyId) {
+        if (jdbcTemplate != null) {
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                "SELECT * FROM lobster_node_execution_log WHERE instance_id=? ORDER BY create_time DESC LIMIT 200",
+                instanceId));
+        }
+        if (nodeExecutionLogMapper != null && companyId != null) {
+            return AjaxResult.success(nodeExecutionLogMapper.selectByInstanceId(instanceId, companyId));
+        }
         return AjaxResult.success(new ArrayList<>());
     }
 
-    @PostMapping({"/workflow/lobster-exec/start", "/workflow/lobster-exec/next-node"})
-    public AjaxResult lobsterExecAction() { return AjaxResult.success("操作成功"); }
+    @PostMapping("/workflow/lobster-exec/start")
+    public AjaxResult lobsterExecStart(@RequestParam Long workflowId,
+                                        @RequestParam(required = false, defaultValue = "0") Long contactId,
+                                        @RequestParam(required = false) Long companyId,
+                                        @RequestBody(required = false) Map<String, Object> initVariables) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        return workflowExecutor.startWorkflow(companyId, workflowId, contactId, initVariables);
+    }
+
+    @PostMapping("/workflow/lobster-exec/next-node")
+    public AjaxResult lobsterExecNext(@RequestParam Long instanceId,
+                                       @RequestParam(required = false) String customerReply,
+                                       @RequestParam(required = false) Long companyId) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        return workflowExecutor.executeNextNode(companyId, instanceId, customerReply);
+    }
 
     @PostMapping("/workflow/lobster-exec/pause/{instanceId}")
     public AjaxResult lobsterExecPause(@PathVariable String instanceId) {
@@ -667,29 +847,74 @@ public class LobsterAdminController extends BaseController {
         return AjaxResult.success(stats);
     }
 
-    // ======== 引擎核心占位端点 ========
+    // ======== 引擎核心端点(对接 EvolutionEngine / Heartbeat / Channels) ========
     @GetMapping("/workflow/lobster/engine/evolution/metrics")
-    public AjaxResult lobsterEngineEvolutionMetrics() {
+    public AjaxResult lobsterEngineEvolutionMetrics(@RequestParam(required = false) Long companyId) {
+        if (evolutionEngine != null && companyId != null) {
+            return AjaxResult.success(evolutionEngine.getEvolutionMetrics(companyId));
+        }
         Map<String, Object> data = new HashMap<>();
-        data.put("totalEvolutions", 0); data.put("appliedCount", 0); data.put("pendingCount", 0);
+        if (jdbcTemplate != null && companyId != null) {
+            try {
+                data.put("totalEvolutions", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_log WHERE company_id=?", Integer.class, companyId));
+                data.put("appliedCount", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=? AND status=1", Integer.class, companyId));
+                data.put("pendingCount", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id=? AND status=0", Integer.class, companyId));
+            } catch (Exception e) {
+                data.put("totalEvolutions", 0); data.put("appliedCount", 0); data.put("pendingCount", 0);
+            }
+        } else {
+            data.put("totalEvolutions", 0); data.put("appliedCount", 0); data.put("pendingCount", 0);
+        }
         return AjaxResult.success(data);
     }
 
     @GetMapping("/workflow/lobster/engine/evolution/analyze")
-    public AjaxResult lobsterEngineEvolutionAnalyze() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterEngineEvolutionAnalyze(@RequestParam Long companyId,
+                                                     @RequestParam Long workflowId) {
+        if (evolutionEngine != null) {
+            return AjaxResult.success(evolutionEngine.analyzeAndSuggest(companyId, workflowId));
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
 
     @PostMapping("/workflow/lobster/engine/evolution/apply")
-    public AjaxResult lobsterEngineEvolutionApply() { return AjaxResult.success("操作成功"); }
+    public AjaxResult lobsterEngineEvolutionApply(@RequestParam Long companyId,
+                                                   @RequestParam Long suggestionId) {
+        if (evolutionEngine != null) {
+            evolutionEngine.applySuggestion(companyId, suggestionId);
+            return AjaxResult.success("优化建议已应用");
+        }
+        return AjaxResult.error("进化引擎不可用");
+    }
 
     @GetMapping("/workflow/lobster/engine/heartbeat/status")
-    public AjaxResult lobsterEngineHeartbeat() {
+    public AjaxResult lobsterEngineHeartbeat(@RequestParam Long instanceId) {
+        if (heartbeatScheduler != null) {
+            return AjaxResult.success(heartbeatScheduler.getHeartbeatStatus(instanceId));
+        }
         Map<String, Object> data = new HashMap<>();
-        data.put("status", "healthy");
+        data.put("status", "unknown");
+        data.put("instanceId", instanceId);
         return AjaxResult.success(data);
     }
 
     @GetMapping("/workflow/lobster/engine/channels")
-    public AjaxResult lobsterEngineChannels() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult lobsterEngineChannels() {
+        if (messageChannelRouter != null) {
+            Map<String, Object> channels = new LinkedHashMap<>();
+            messageChannelRouter.getAllChannels().forEach((type, channel) -> {
+                Map<String, Object> info = new HashMap<>();
+                info.put("type", type);
+                info.put("available", true);
+                channels.put(type, info);
+            });
+            return AjaxResult.success(channels);
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
 
     // ════════════════════════════════════════════════════════════════
     // 画像配置 / 摘要配置 / 敏感词 / 消息去重 — 走真实 LobsterCompanyConfigService
@@ -764,6 +989,25 @@ public class LobsterAdminController extends BaseController {
     }
 
     // ─── 消息去重监控 ───
+    @GetMapping("/workflow/lobster/dedup-config/list")
+    public AjaxResult dedupConfigList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listDedup(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/dedup-config/save")
+    public AjaxResult dedupConfigSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveDedup(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/dedup-config/{id}")
+    public AjaxResult dedupConfigDelete(@PathVariable Long id,
+                                        @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteDedup(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
     @GetMapping("/workflow/lobster/dedup/stats")
     public AjaxResult dedupStats(@RequestParam(required = false) Long companyId) {
         Map<String, Object> stats = new HashMap<>();
@@ -896,6 +1140,106 @@ public class LobsterAdminController extends BaseController {
         return "admin"; // 动态节点审批由管理员操作
     }
 
+    // ======== /workflow/lobster-admin/* 跨租户管理聚合端点 ========
+
+    @GetMapping("/workflow/lobster-admin/companies")
+    public AjaxResult adminCompanies() {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            List<Map<String, Object>> list = jdbcTemplate.queryForList(
+                "SELECT id, company_name, domain, status FROM company_info WHERE del_flag=0 ORDER BY id");
+            return AjaxResult.success(list);
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
+    @GetMapping("/workflow/lobster-admin/company-stats/{companyId}")
+    public AjaxResult adminCompanyStats(@PathVariable Long companyId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("companyId", companyId);
+        stats.put("templateCount", 0);
+        stats.put("instanceCount", 0);
+        stats.put("totalTokens", "0");
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster-admin/platform-stats")
+    public AjaxResult adminPlatformStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalCompanies", 0);
+        stats.put("totalTemplates", 0);
+        stats.put("runningInstances", 0);
+        stats.put("todayTokens", "0");
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster-admin/instances")
+    public AjaxResult adminInstances(@RequestParam(defaultValue = "1") Integer pageNum,
+                                      @RequestParam(defaultValue = "10") Integer pageSize,
+                                      @RequestParam(required = false) Long companyId,
+                                      @RequestParam(required = false) Long workflowId,
+                                      @RequestParam(required = false) String status) {
+        return lobsterExecInstanceList(companyId, workflowId, status);
+    }
+
+    @GetMapping("/workflow/lobster-admin/prompts")
+    public AjaxResult adminPrompts(@RequestParam(defaultValue = "1") Integer pageNum,
+                                    @RequestParam(defaultValue = "10") Integer pageSize,
+                                    @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster-admin/dead-letters")
+    public AjaxResult adminDeadLetters(@RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster-admin/event-audits")
+    public AjaxResult adminEventAudits(@RequestParam(defaultValue = "pending") String status,
+                                        @RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(eventAuditService.listAudits(status, pageNum, pageSize, companyId));
+    }
+
+    @GetMapping("/workflow/lobster-admin/optimizations")
+    public AjaxResult adminOptimizations(@RequestParam(defaultValue = "1") Integer pageNum,
+                                          @RequestParam(defaultValue = "10") Integer pageSize,
+                                          @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster-admin/sales-corpus")
+    public AjaxResult adminSalesCorpus(@RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) String scenario,
+                                        @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(salesCorpusService.listCorpus(pageNum, pageSize, companyId, scenario, null));
+    }
+
+    @GetMapping("/workflow/lobster-admin/api-registry")
+    public AjaxResult adminApiRegistry(@RequestParam(defaultValue = "1") Integer pageNum,
+                                        @RequestParam(defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster-admin/chat-aggregate")
+    public AjaxResult adminChatAggregate(@RequestParam(defaultValue = "1") Integer pageNum,
+                                          @RequestParam(defaultValue = "10") Integer pageSize,
+                                          @RequestParam(required = false) String channelType,
+                                          @RequestParam(required = false) String keyword) {
+        if (chatSessionMapper == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            return AjaxResult.success(chatSessionMapper.selectForAggregate(channelType, keyword));
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
     @PostMapping("/workflow/lobster/scenario/run-all")
     public AjaxResult scenarioRunAll() {
         if (testScenarioService == null) return AjaxResult.error("场景服务未启用");

+ 0 - 1
fs-admin-saas/src/main/java/com/fs/user/controller/FsUserIntegralController.java

@@ -98,7 +98,6 @@ public class FsUserIntegralController {
         }
         if (StringUtils.isNotEmpty(nickName)) {
             queryUser.setNickName(nickName);
-            queryUser.setNickname(nickName);
         }
         
         // 查询用户列表(只返回需要的字段)

+ 0 - 26
fs-admin/pom.xml

@@ -93,32 +93,6 @@
             </exclusions>
         </dependency>
 
-
-
-        <!-- 定时任务框架 -->
-        <dependency>
-            <groupId>com.fs</groupId>
-            <artifactId>fs-quartz</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.tomcat</groupId>
-                    <artifactId>annotations-api</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-
-        <!-- 统一定时任务 Bean 模块(包含所有 Task 实现 + TaskRegistry + 手动触发接口) -->
-        <dependency>
-            <groupId>com.fs</groupId>
-            <artifactId>fs-task</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.tomcat</groupId>
-                    <artifactId>annotations-api</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-
         <!-- lombok -->
         <dependency>
             <groupId>org.projectlombok</groupId>

+ 18 - 10
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java

@@ -104,6 +104,7 @@ public class CompanyVoiceApiTenantController extends BaseController {
                 if (tenantMap.get("tenantName") != null) {
                     tenant.setTenantName(tenantMap.get("tenantName").toString());
                 }
+                applyPricingFieldsFromMap(tenant, tenantMap);
                 if (tenant.getTenantId() != null) {
                     list.add(tenant);
                 }
@@ -208,23 +209,30 @@ public class CompanyVoiceApiTenantController extends BaseController {
 
     private CompanyVoiceApiTenant parsePricingFields(Map<String, Object> body) {
         CompanyVoiceApiTenant pricing = new CompanyVoiceApiTenant();
-        if (body.get("salePrice") != null && StringUtils.isNotEmpty(body.get("salePrice").toString())) {
-            pricing.setSalePrice(new java.math.BigDecimal(body.get("salePrice").toString()));
+        applyPricingFieldsFromMap(pricing, body);
+        return pricing;
+    }
+
+    private void applyPricingFieldsFromMap(CompanyVoiceApiTenant tenant, Map<String, Object> map) {
+        if (tenant == null || map == null) {
+            return;
         }
-        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
-            pricing.setPriority(Integer.valueOf(body.get("priority").toString()));
+        if (map.get("salePrice") != null && StringUtils.isNotEmpty(map.get("salePrice").toString())) {
+            tenant.setSalePrice(new java.math.BigDecimal(map.get("salePrice").toString()));
         }
-        if (body.get("isPrimary") != null && StringUtils.isNotEmpty(body.get("isPrimary").toString())) {
-            pricing.setIsPrimary(Integer.valueOf(body.get("isPrimary").toString()));
+        if (map.get("priority") != null && StringUtils.isNotEmpty(map.get("priority").toString())) {
+            tenant.setPriority(Integer.valueOf(map.get("priority").toString()));
         }
-        Object selectable = body.get("selectable");
+        if (map.get("isPrimary") != null && StringUtils.isNotEmpty(map.get("isPrimary").toString())) {
+            tenant.setIsPrimary(Integer.valueOf(map.get("isPrimary").toString()));
+        }
+        Object selectable = map.get("selectable");
         if (selectable == null) {
-            selectable = body.get("allowManual");
+            selectable = map.get("allowManual");
         }
         if (selectable != null && StringUtils.isNotEmpty(selectable.toString())) {
-            pricing.setSelectable(selectable.toString());
+            tenant.setSelectable(selectable.toString());
         }
-        return pricing;
     }
 
     @SuppressWarnings("unchecked")

+ 248 - 0
fs-admin/src/main/java/com/fs/admin/controller/company/controller/SysRedpacketConfigMoreController.java

@@ -0,0 +1,248 @@
+package com.fs.admin.controller.company.controller;
+
+
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.framework.datasource.TenantDataSourceContextHelper;
+import com.fs.his.domain.SysRedpacketConfigMore;
+import com.fs.his.param.UpdateChangeMchIdParam;
+import com.fs.his.service.ISysRedpacketConfigMoreService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Slf4j
+
+/**
+ * 多商户配置Controller
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+@RestController
+@RequestMapping("/redPacket/more")
+public class SysRedpacketConfigMoreController extends BaseController
+{
+    @Autowired
+    private ISysRedpacketConfigMoreService sysRedpacketConfigMoreService;
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private TenantDataSourceContextHelper tenantContextHelper;
+
+    /**
+     * 查询多商户配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        startPage();
+        List<SysRedpacketConfigMore> list = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreList(sysRedpacketConfigMore);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取多商户配置详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(id));
+    }
+
+    /**
+     * 获取当前发红包的商户号
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:editConfig')")
+    @GetMapping(value = "/getRedPacketConfig")
+    public AjaxResult getRedPacketConFig()
+    {
+        return AjaxResult.success(sysRedpacketConfigMoreService.getRedPacketConFig());
+    }
+
+
+
+
+    /**
+     * 新增多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:add')")
+    @Log(title = "多商户配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        int result = sysRedpacketConfigMoreService.insertSysRedpacketConfigMore(sysRedpacketConfigMore);
+        
+        // 如果指定了租户,同步到租户库
+        if (result > 0 && sysRedpacketConfigMore.getTenantId() != null) {
+            try {
+                syncToTenant(sysRedpacketConfigMore, sysRedpacketConfigMore.getTenantId());
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库失败, tenantId={}, mchId={}", 
+                    sysRedpacketConfigMore.getTenantId(), sysRedpacketConfigMore.getMchId(), e);
+                // 不影响主流程,记录日志即可
+            }
+        }
+        
+        return toAjax(result);
+    }
+
+    /**
+     * 修改发商户号的商户
+     */
+    @Log(title = "修改发商户号的商户", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateChangeMchId")
+    public R updateChangeMchId(@RequestBody UpdateChangeMchIdParam param)
+    {
+        return sysRedpacketConfigMoreService.updateChangeMchId(param);
+    }
+
+    /**
+     * 修改多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:edit')")
+    @Log(title = "多商户配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        // 查询原数据,获取原租户ID
+        SysRedpacketConfigMore oldConfig = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(sysRedpacketConfigMore.getId());
+        Long oldTenantId = oldConfig != null ? oldConfig.getTenantId() : null;
+        Long newTenantId = sysRedpacketConfigMore.getTenantId();
+        String mchId = sysRedpacketConfigMore.getMchId();
+        
+        int result = sysRedpacketConfigMoreService.updateSysRedpacketConfigMore(sysRedpacketConfigMore);
+        
+        if (result > 0) {
+            try {
+                // 如果原租户存在且与新租户不同,从原租户库删除
+                if (oldTenantId != null && !oldTenantId.equals(newTenantId)) {
+                    deleteFromTenant(mchId, oldTenantId);
+                }
+                // 如果指定了新租户,同步到新租户库
+                if (newTenantId != null) {
+                    syncToTenant(sysRedpacketConfigMore, newTenantId);
+                }
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库失败, mchId={}, oldTenantId={}, newTenantId={}", 
+                    mchId, oldTenantId, newTenantId, e);
+            }
+        }
+        
+        return toAjax(result);
+    }
+
+    /**
+     * 删除多商户配置
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:remove')")
+    @Log(title = "多商户配置", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        // 先查询要删除的数据,获取租户信息
+        for (Long id : ids) {
+            SysRedpacketConfigMore config = sysRedpacketConfigMoreService.selectSysRedpacketConfigMoreById(id);
+            if (config != null && config.getTenantId() != null) {
+                try {
+                    deleteFromTenant(config.getMchId(), config.getTenantId());
+                } catch (Exception e) {
+                    log.error("从租户库删除商户配置失败, tenantId={}, mchId={}", 
+                        config.getTenantId(), config.getMchId(), e);
+                }
+            }
+        }
+        
+        return toAjax(sysRedpacketConfigMoreService.deleteSysRedpacketConfigMoreByIds(ids));
+    }
+
+    /**
+     * 获取租户列表(用于下拉选择)
+     */
+    @PreAuthorize("@ss.hasPermi('redPacket:more:list')")
+    @GetMapping("/tenantList")
+    public AjaxResult tenantList()
+    {
+        TenantInfo query = new TenantInfo();
+        query.setStatus(1); // 只查询启用的租户
+        List<TenantInfo> list = tenantInfoService.selectTenantInfoList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 同步商户配置到租户库
+     */
+    private void syncToTenant(SysRedpacketConfigMore config, Long tenantId) {
+        if (tenantId == null) {
+            return;
+        }
+        tenantContextHelper.runInTenant(tenantId, () -> {
+            try {
+                // 查询租户库是否已存在该商户号的配置
+                SysRedpacketConfigMore existConfig = sysRedpacketConfigMoreService.getOne(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysRedpacketConfigMore>()
+                        .eq("mch_id", config.getMchId())
+                );
+
+                // 复制一份数据用于租户库
+                SysRedpacketConfigMore tenantConfig = new SysRedpacketConfigMore();
+                BeanCopyUtils.copy(config, tenantConfig);
+
+                if (existConfig != null) {
+                    // 更新现有记录
+                    tenantConfig.setId(existConfig.getId());
+                    sysRedpacketConfigMoreService.updateSysRedpacketConfigMore(tenantConfig);
+                    log.info("同步商户配置到租户库-更新, tenantId={}, mchId={}", tenantId, config.getMchId());
+                } else {
+                    // 插入新记录,清空ID让数据库自增
+                    tenantConfig.setId(null);
+                    sysRedpacketConfigMoreService.insertSysRedpacketConfigMore(tenantConfig);
+                    log.info("同步商户配置到租户库-新增, tenantId={}, mchId={}", tenantId, config.getMchId());
+                }
+            } catch (Exception e) {
+                log.error("同步商户配置到租户库异常, tenantId={}, mchId={}", tenantId, config.getMchId(), e);
+                throw new RuntimeException("同步到租户库失败: " + e.getMessage(), e);
+            }
+        });
+    }
+
+    /**
+     * 从租户库删除商户配置
+     */
+    private void deleteFromTenant(String mchId, Long tenantId) {
+        if (tenantId == null || mchId == null) {
+            return;
+        }
+        tenantContextHelper.runInTenant(tenantId, () -> {
+            try {
+                // 查询租户库中的记录
+                SysRedpacketConfigMore config = sysRedpacketConfigMoreService.getOne(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysRedpacketConfigMore>()
+                        .eq("mch_id", mchId)
+                );
+                if (config != null) {
+                    sysRedpacketConfigMoreService.deleteSysRedpacketConfigMoreById(config.getId());
+                    log.info("从租户库删除商户配置, tenantId={}, mchId={}", tenantId, mchId);
+                }
+            } catch (Exception e) {
+                log.error("从租户库删除商户配置异常, tenantId={}, mchId={}", tenantId, mchId, e);
+                throw new RuntimeException("从租户库删除失败: " + e.getMessage(), e);
+            }
+        });
+    }
+}

+ 87 - 0
fs-admin/src/main/java/com/fs/admin/controller/monitor/SysJobLogController.java

@@ -0,0 +1,87 @@
+package com.fs.admin.controller.monitor;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+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.quartz.domain.SysJobLog;
+import com.fs.quartz.service.ISysJobLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 调度日志操作处理
+ * 
+
+ */
+@RestController
+@RequestMapping("/monitor/jobLog")
+public class SysJobLogController extends BaseController
+{
+    @Autowired
+    private ISysJobLogService jobLogService;
+
+    /**
+     * 查询定时任务调度日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysJobLog sysJobLog)
+    {
+        startPage();
+        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出定时任务调度日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
+    @Log(title = "任务调度日志", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(SysJobLog sysJobLog)
+    {
+        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
+        ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
+        return util.exportExcel(list, "调度日志");
+    }
+    
+    /**
+     * 根据调度编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
+    @GetMapping(value = "/{configId}")
+    public AjaxResult getInfo(@PathVariable Long jobLogId)
+    {
+        return AjaxResult.success(jobLogService.selectJobLogById(jobLogId));
+    }
+
+
+    /**
+     * 删除定时任务调度日志
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
+    @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{jobLogIds}")
+    public AjaxResult remove(@PathVariable Long[] jobLogIds)
+    {
+        return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
+    }
+
+    /**
+     * 清空定时任务调度日志
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
+    @Log(title = "调度日志", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/clean")
+    public AjaxResult clean()
+    {
+        jobLogService.cleanJobLog();
+        return AjaxResult.success();
+    }
+}

+ 8 - 0
fs-admin/src/main/java/com/fs/admin/controller/monitor/TenantJobController.java

@@ -88,6 +88,14 @@ public class TenantJobController extends BaseController {
         return AjaxResult.success();
     }
 
+    /** 保存租户任务分配:精确设置某租户对应的模板列表(templateIds 为空则取消全部分配) */
+    @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
+    @PutMapping("/config")
+    public AjaxResult saveTenantConfig(@RequestBody TenantJobConfigSaveDTO dto) {
+        tenantJobConfigService.saveTenantConfig(dto, getUsername());
+        return AjaxResult.success();
+    }
+
     /** 同步全部模板到指定租户库(自动创建缺失的 config) */
     @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
     @PostMapping("/sync/{tenantId}")

+ 6 - 6
fs-admin/src/main/java/com/fs/qw/controller/QwCompanyController.java

@@ -58,12 +58,12 @@ public class QwCompanyController extends BaseController
     {
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        String json = configService.selectConfigByKey("course.config");
-        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
-            qwCompany.setCreateDeptId(loginUser.getDeptId());
-            qwCompany.setCreateUserId(loginUser.getUserId());
-        }
+//        String json = configService.selectConfigByKey("course.config");
+//        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+//        if(!loginUser.isAdmin() && config.getDept() != null && config.getDept()){
+//            qwCompany.setCreateDeptId(loginUser.getDeptId());
+//            qwCompany.setCreateUserId(loginUser.getUserId());
+//        }
         startPage();
         List<QwCompany> list = qwCompanyService.selectQwCompanyList(qwCompany);
         return getDataTable(list);

+ 105 - 3
fs-admin/src/main/java/com/fs/web/controller/system/CompanySmsApiTenantController.java

@@ -2,13 +2,19 @@ package com.fs.web.controller.system;
 
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.StringUtils;
 import com.fs.proxy.domain.CompanySmsApiTenant;
+import com.fs.proxy.domain.SmsApiTenantBatchAddResult;
 import com.fs.proxy.service.ICompanySmsApiTenantService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 短信接口-租户绑定Controller (adminUI)
@@ -20,12 +26,13 @@ public class CompanySmsApiTenantController extends BaseController {
     @Autowired
     private ICompanySmsApiTenantService smsApiTenantService;
 
-    /** 查询绑定列表 */
+    /** 分页查询绑定列表 */
     @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:list')")
     @GetMapping("/list")
-    public AjaxResult list(CompanySmsApiTenant query) {
+    public TableDataInfo list(CompanySmsApiTenant query) {
+        startPage();
         List<CompanySmsApiTenant> list = smsApiTenantService.selectSmsApiTenantList(query);
-        return AjaxResult.success(list);
+        return getDataTable(list);
     }
 
     /** 获取绑定详情 */
@@ -49,6 +56,38 @@ public class CompanySmsApiTenantController extends BaseController {
         return toAjax(smsApiTenantService.insertSmsApiTenant(tenant));
     }
 
+    /** 批量新增绑定 */
+    @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:add')")
+    @PostMapping("/batch")
+    public AjaxResult batchAdd(@RequestBody Map<String, Object> body) {
+        List<Long> apiIds = parseIdList(body.get("apiIds"));
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (apiIds.isEmpty()) {
+            apiIds = parseIdList(body.get("apiId"));
+        }
+        if (tenantIds.isEmpty()) {
+            tenantIds = parseIdList(body.get("tenantId"));
+        }
+        CompanySmsApiTenant template = new CompanySmsApiTenant();
+        if (body.get("price") != null && StringUtils.isNotEmpty(body.get("price").toString())) {
+            template.setPrice(new BigDecimal(body.get("price").toString()));
+        }
+        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
+            template.setPriority(Integer.valueOf(body.get("priority").toString()));
+        }
+        if (body.get("allowManual") != null && StringUtils.isNotEmpty(body.get("allowManual").toString())) {
+            template.setAllowManual(Integer.valueOf(body.get("allowManual").toString()));
+        }
+        if (body.get("status") != null && StringUtils.isNotEmpty(body.get("status").toString())) {
+            template.setStatus(Integer.valueOf(body.get("status").toString()));
+        } else {
+            template.setStatus(1);
+        }
+        SmsApiTenantBatchAddResult result = smsApiTenantService.batchInsertSmsApiTenant(apiIds, tenantIds, template);
+        String msg = String.format("成功 %d 条,失败 %d 条", result.getSuccessCount(), result.getFailCount());
+        return AjaxResult.success(msg, result);
+    }
+
     /** 修改绑定(调价/启停) */
     @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:edit')")
     @PutMapping
@@ -56,10 +95,73 @@ public class CompanySmsApiTenantController extends BaseController {
         return toAjax(smsApiTenantService.updateSmsApiTenant(tenant));
     }
 
+    /** 批量更新租户定价/绑定配置 */
+    @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:edit')")
+    @PutMapping("/batchPricing")
+    public AjaxResult batchPricing(@RequestBody Map<String, Object> body) {
+        List<Long> ids = parseIdList(body.get("ids"));
+        if (ids.isEmpty()) {
+            return AjaxResult.error("请选择要更新的记录");
+        }
+        CompanySmsApiTenant pricing = parsePricingFields(body);
+        if (pricing.getPrice() == null && pricing.getPriority() == null && pricing.getAllowManual() == null) {
+            return AjaxResult.error("请至少填写一项要批量更新的配置");
+        }
+        return toAjax(smsApiTenantService.batchUpdatePricing(ids, pricing));
+    }
+
+    /** 批量更新状态 */
+    @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:edit')")
+    @PutMapping("/batchStatus")
+    public AjaxResult batchStatus(@RequestBody Map<String, Object> body) {
+        List<Long> ids = parseIdList(body.get("ids"));
+        if (ids.isEmpty()) {
+            return AjaxResult.error("请选择要更新的记录");
+        }
+        if (body.get("status") == null) {
+            return AjaxResult.error("请指定状态");
+        }
+        Integer status = Integer.valueOf(body.get("status").toString());
+        if (status != 0 && status != 1) {
+            return AjaxResult.error("状态值无效");
+        }
+        return toAjax(smsApiTenantService.batchUpdateStatus(ids, status));
+    }
+
     /** 解除绑定 */
     @PreAuthorize("@ss.hasPermi('platform:smsApiTenant:remove')")
     @DeleteMapping("/{id}")
     public AjaxResult remove(@PathVariable Long id) {
         return toAjax(smsApiTenantService.deleteSmsApiTenantById(id));
     }
+
+    private CompanySmsApiTenant parsePricingFields(Map<String, Object> body) {
+        CompanySmsApiTenant pricing = new CompanySmsApiTenant();
+        if (body.get("price") != null && StringUtils.isNotEmpty(body.get("price").toString())) {
+            pricing.setPrice(new BigDecimal(body.get("price").toString()));
+        }
+        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
+            pricing.setPriority(Integer.valueOf(body.get("priority").toString()));
+        }
+        if (body.get("allowManual") != null && StringUtils.isNotEmpty(body.get("allowManual").toString())) {
+            pricing.setAllowManual(Integer.valueOf(body.get("allowManual").toString()));
+        }
+        return pricing;
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<Long> parseIdList(Object raw) {
+        List<Long> ids = new ArrayList<>();
+        if (raw == null) {
+            return ids;
+        }
+        if (raw instanceof List) {
+            for (Object item : (List<?>) raw) {
+                if (item != null) {
+                    ids.add(Long.valueOf(item.toString()));
+                }
+            }
+        }
+        return ids;
+    }
 }

+ 0 - 13
fs-agent/pom.xml

@@ -77,19 +77,6 @@
             <artifactId>fs-service</artifactId>
         </dependency>
 
-
-
-        <!-- 定时任务-->
-        <dependency>
-            <groupId>com.fs</groupId>
-            <artifactId>fs-quartz</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.tomcat</groupId>
-                    <artifactId>annotations-api</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
         <!-- lombok -->
         <dependency>
             <groupId>org.projectlombok</groupId>

+ 5 - 0
fs-common/pom.xml

@@ -156,6 +156,11 @@
             <artifactId>commons-lang</artifactId>
             <version>2.6</version> <!-- 稳定版本,兼容大部分场景 -->
         </dependency>
+
+        <dependency>
+            <groupId>com.cronutils</groupId>
+            <artifactId>cron-utils</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 110 - 0
fs-common/src/main/java/com/fs/common/utils/CronUtils.java

@@ -0,0 +1,110 @@
+package com.fs.common.utils;
+
+import com.cronutils.model.Cron;
+import com.cronutils.model.CronType;
+import com.cronutils.model.definition.CronDefinition;
+import com.cronutils.model.definition.CronDefinitionBuilder;
+import com.cronutils.model.time.ExecutionTime;
+import com.cronutils.parser.CronParser;
+
+import java.text.ParseException;
+import java.time.ZonedDateTime;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Optional;
+
+/**
+ * cron表达式工具类
+ * 
+
+ *
+ */
+public class CronUtils
+{
+    private static final CronDefinition CRON_DEFINITION = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ);
+
+    private static final CronParser CRON_PARSER = new CronParser(CRON_DEFINITION);
+
+    /**
+     * 返回一个布尔值代表一个给定的Cron表达式的有效性
+     */
+    public static boolean isValid(String cronExpression)
+    {
+        try {
+            CRON_PARSER.parse(cronExpression).validate();
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 返回一个字符串值,表示该消息无效Cron表达式给出有效性
+     */
+    public static String getInvalidMessage(String cronExpression)
+    {
+        try {
+            CRON_PARSER.parse(cronExpression).validate();
+            return null;
+        } catch (Exception e) {
+            return e.getMessage();
+        }
+    }
+
+    /**
+     * 返回下一个执行时间根据给定的Cron表达式
+     *
+     * @param cronExpression Cron表达式
+     * @return Date 下次Cron表达式执行时间
+     */
+    public static Date getNextExecution(String cronExpression)
+    {
+        try {
+            Cron cron = CRON_PARSER.parse(cronExpression);
+            cron.validate();
+
+            ExecutionTime executionTime = ExecutionTime.forCron(cron);
+
+            Optional<ZonedDateTime> next = executionTime.nextExecution(ZonedDateTime.now());
+
+            return next.map(zdt -> Date.from(zdt.toInstant())).orElse(null);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 判断 cron 表达式是否在当前这一分钟内会触发(用于 SaaS 租户任务分发器)
+     *
+     * @param cronExpression cron 表达式
+     * @return 当前分钟内会触发返回 true
+     */
+    public static boolean isDueInThisMinute(String cronExpression) {
+        if (cronExpression == null || cronExpression.isBlank()) {
+            return false;
+        }
+
+        try {
+
+            Cron cron = CRON_PARSER.parse(cronExpression);
+            cron.validate();
+
+            ExecutionTime executionTime = ExecutionTime.forCron(cron);
+
+            ZonedDateTime now = ZonedDateTime.now();
+
+            ZonedDateTime startOfMinute = now.withSecond(0).withNano(0);
+
+            ZonedDateTime startOfNextMinute = startOfMinute.plusMinutes(1);
+
+            // 与 Quartz 版本保持一致
+            ZonedDateTime base = startOfMinute.minusNanos(1);
+
+            Optional<ZonedDateTime> next = executionTime.nextExecution(base);
+
+            return next.isPresent() && !next.get().isBefore(startOfMinute) && next.get().isBefore(startOfNextMinute);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}

+ 1 - 95
fs-company/src/main/java/com/fs/company/controller/bridge/CompanyBridgeController.java

@@ -539,41 +539,7 @@ public class CompanyBridgeController extends BaseController {
     public AjaxResult trafficLogExport() { return AjaxResult.success(); }
 
     // ==========================================
-    // /workflow/lobster/* API妗ユ帴
-    // ==========================================
-
-    @GetMapping({"/workflow/lobster/api", "/workflow/lobster/api/list"})
-    public TableDataInfo lobsterApi() { return safeListFromTable("lobster_api_registry"); }
-
-    @GetMapping({"/workflow/lobster/audit", "/workflow/lobster/audit/list"})
-    public TableDataInfo lobsterAudit() { return safeListFromTable("lobster_event_audit"); }
-
-    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
-    public TableDataInfo lobsterBilling() { return safeListFromTable("lobster_billing"); }
-
-    @GetMapping({"/workflow/lobster/chat", "/workflow/lobster/chat/list"})
-    public TableDataInfo lobsterChat() { return safeListFromTable("lobster_chat_aggregate"); }
-
-    @GetMapping({"/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
-    public TableDataInfo lobsterCorpus() { return safeListFromTable("lobster_sales_corpus"); }
-
-    @GetMapping({"/workflow/lobster/deadletter", "/workflow/lobster/deadletter/list"})
-    public TableDataInfo lobsterDeadletter() { return safeListFromTable("lobster_dead_letter"); }
-
-    @GetMapping({"/workflow/lobster/edit", "/workflow/lobster/edit/list"})
-    public TableDataInfo lobsterEdit() { return safeListFromTable("lobster_api_registry"); }
-
-    @GetMapping({"/workflow/lobster/exec", "/workflow/lobster/exec/list"})
-    public TableDataInfo lobsterExec() { return safeListFromTable("lobster_instance"); }
-
-    @GetMapping({"/workflow/lobster/list", "/workflow/lobster/"})
-    public TableDataInfo lobsterList() { return safeListFromTable("lobster_canvas"); }
-
-    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
-    public TableDataInfo lobsterOptimization() { return safeListFromTable("lobster_optimization"); }
-
-    // ==========================================
-    // /store/* 鍓╀綑缂哄けAPI妗ユ帴
+    // /store/* 剩余缺失API
     // ==========================================
 
     @GetMapping("/store/shippingTemplatesFree/")
@@ -1087,12 +1053,6 @@ public class CompanyBridgeController extends BaseController {
     @GetMapping({"/workflow/ai-generator/result/1", "/workflow/ai-generator/result/"})
     public AjaxResult workflowAiGeneratorResult() { return AjaxResult.success(new HashMap<>()); }
 
-    @GetMapping("/workflow/canvas/")
-    public TableDataInfo workflowCanvasRoot() { return safeListFromTable("lobster_canvas"); }
-
-    @GetMapping("/workflow/template/")
-    public TableDataInfo workflowTemplateRoot() { return safeListFromTable("lobster_canvas"); }
-
     // --- /qw* ---
     @GetMapping("/qwAssignRule/")
     public TableDataInfo qwAssignRuleRoot() { return safeListFromTable("qw_assign_rule_user"); }
@@ -3057,45 +3017,6 @@ public class CompanyBridgeController extends BaseController {
     public TableDataInfo bridge_withdrawalManage_list() { return safeListFromTable("withdrawalManage_list"); }
 
 
-    // --- /workflow/lobster-admin/* ---
-
-    @GetMapping("/workflow/lobster-admin/api-registry")
-    public AjaxResult bridge_workflow_lobster_admin_api_registry() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/billing-records")
-    public AjaxResult bridge_workflow_lobster_admin_billing_records() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/chat-aggregate")
-    public AjaxResult bridge_workflow_lobster_admin_chat_aggregate() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/companies")
-    public AjaxResult bridge_workflow_lobster_admin_companies() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/company-stats/")
-    public AjaxResult bridge_workflow_lobster_admin_company_stats() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/dead-letters")
-    public AjaxResult bridge_workflow_lobster_admin_dead_letters() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/event-audits")
-    public AjaxResult bridge_workflow_lobster_admin_event_audits() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/instances")
-    public AjaxResult bridge_workflow_lobster_admin_instances() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/optimizations")
-    public AjaxResult bridge_workflow_lobster_admin_optimizations() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/platform-stats")
-    public AjaxResult bridge_workflow_lobster_admin_platform_stats() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/prompts")
-    public AjaxResult bridge_workflow_lobster_admin_prompts() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster-admin/sales-corpus")
-    public AjaxResult bridge_workflow_lobster_admin_sales_corpus() { return AjaxResult.success(new HashMap<>()); }
-
-
     // --- /wx/wxSop/* ---
 
     @GetMapping({"/wx/wxSop", "/wx/wxSop/"})
@@ -3177,21 +3098,6 @@ public class CompanyBridgeController extends BaseController {
     @GetMapping("/qwCustomerLink/")
     public TableDataInfo bridge_qwCustomerLink_root() { return safeListFromTable("qw_customer_link"); }
 
-    @GetMapping("/workflow/lobster/billing/stats")
-    public AjaxResult bridge_lobster_billing_stats() { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster/instance/{id}")
-    public AjaxResult bridge_lobster_instance_detail(@PathVariable("id") Long id) { return AjaxResult.success(new HashMap<>()); }
-
-    @GetMapping("/workflow/lobster/instance/node-logs/{id}")
-    public AjaxResult bridge_lobster_instance_nodeLogs(@PathVariable("id") Long id) { return AjaxResult.success(new ArrayList<>()); }
-
-    @GetMapping("/workflow/lobster/instance/stats")
-    public AjaxResult bridge_lobster_instance_stats() { return AjaxResult.success(new HashMap<>()); }
-
-    @PostMapping("/workflow/lobster/instance/terminate/{id}")
-    public AjaxResult bridge_lobster_instance_terminate(@PathVariable("id") Long id) { return AjaxResult.success(); }
-
     // === 恢复菜单的桥接端点 ===
 
     // CRM - AI话术润色 (前端 /crm/customer_ai_chat/*)

+ 9 - 0
fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowLobsterController.java

@@ -135,6 +135,15 @@ public class CompanyWorkflowLobsterController extends BaseController {
 
     // ==================== 画布编辑接口 ====================
 
+    /**
+     * 获取画布数据(与 /workflow/template/{id} 等价,供前端 canvas 路由使用)
+     */
+    @GetMapping("/canvas/{templateId}")
+    public AjaxResult getCanvas(@PathVariable Long templateId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(lobsterService.getTemplate(loginUser.getCompany().getCompanyId(), templateId));
+    }
+
     /**
      * 保存画布数据(包含节点位置、连线等可视化信息)
      */

+ 13 - 2
fs-company/src/main/java/com/fs/company/controller/qw/QwGroupChatController.java

@@ -12,7 +12,9 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.company.service.impl.CompanyDeptServiceImpl;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwCompany;
 import com.fs.qw.param.QwGroupChatParam;
+import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwGroupChatService;
 import com.fs.qw.vo.QwGroupChatOptionsVO;
 import com.fs.qw.vo.QwGroupChatVO;
@@ -46,6 +48,8 @@ public class QwGroupChatController extends BaseController
 
     @Autowired
     private CompanyDeptServiceImpl companyDeptService;
+    @Autowired
+    private IQwCompanyService qwCompanyService;
 
     /** HTTP调用超时时间(秒) */
     @Value("${qw.api.timeout:30}")
@@ -101,8 +105,15 @@ public class QwGroupChatController extends BaseController
     public TableDataInfo myList(QwGroupChatParam qwGroupChat)
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if (qwGroupChat.getCompanyId() == null && loginUser.getCompany() != null) { qwGroupChat.setCompanyId(loginUser.getCompany().getCompanyId()); };
-        qwGroupChat.setCompanyUserId(loginUser.getUser().getUserId());
+        if (qwGroupChat.getCompanyId() == null) {
+            //查询qw_company
+            QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(qwGroupChat.getCorpId());
+            qwGroupChat.setCompanyId(qwCompany.getId());
+        }
+        if (qwGroupChat.getCompanyUserId() == null && loginUser != null){
+            qwGroupChat.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+
         startPage();
         List<QwGroupChatVO> list = qwGroupChatService.selectQwGroupChatList(qwGroupChat);
         return getDataTable(list);

+ 18 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java

@@ -16,6 +16,8 @@ import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.SortDayVo;
 import com.fs.sop.domain.QwSop;
@@ -56,11 +58,26 @@ public class QwSopTempController extends BaseController
     @Autowired
     private ISysConfigService sysConfigService;
     @Autowired
+    private TenantInfoService tenantInfoService;
+    @Autowired
     private CompanyDeptServiceImpl companyDeptService;
 
     @Autowired
     private CompanyUserServiceImpl companyUserService;
 
+    /**
+     * 获取当前租户的公司名称
+     * @return 租户名称,如果未获取到则返回null
+     */
+    private String getCurrentTenantCompanyName() {
+        Long tenantId = com.fs.wxcid.utils.TenantHelper.getTenantId();
+        if (tenantId == null) {
+            return null;
+        }
+        TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+        return tenantInfo != null ? tenantInfo.getTenantName() : null;
+    }
+
     /**
      * 查询sop模板列表(GET)
      */
@@ -155,7 +172,7 @@ public class QwSopTempController extends BaseController
     public TableDataInfo deptList(QwSopTemp qwSopTemp)
     {
 
-        if (!"今正科技".equals(sysConfigService.getProjectConfig().getCloudHost().getCompanyName())) {
+        if (!"今正科技".equals(getCurrentTenantCompanyName())) {
             return list(qwSopTemp);
         }
 

+ 40 - 4
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java

@@ -6,10 +6,13 @@ import com.fs.company.service.workflow.MultiModelWorkflowGenerator;
 import com.fs.company.service.workflow.MultiModelWorkflowGenerator.GenerationResult;
 import com.fs.company.service.workflow.MultiModelWorkflowGenerator.ModelConfig;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -23,6 +26,9 @@ public class LobsterAiGeneratorController extends BaseController {
     @Autowired
     private MultiModelWorkflowGenerator generator;
 
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
     /** 生成结果临时缓存:recordId → GenerationResult */
     private final Map<Long, GenerationResult> resultCache = new ConcurrentHashMap<>();
 
@@ -87,11 +93,41 @@ public class LobsterAiGeneratorController extends BaseController {
         if (result == null || !result.isSuccess()) {
             return AjaxResult.error("生成结果不存在或失败");
         }
-        // TODO:调 CompanyWorkflowLobsterService.saveAsTemplate(json, workflowName, companyId)
-        // 当前先返回 JSON 给前端,由前端走原有保存流程
+        Long companyId = body.get("companyId") != null ? Long.valueOf(body.get("companyId").toString()) : 0L;
+        String workflowName = (String) body.getOrDefault("workflowName", "AI生成工作流");
+        String workflowJson = result.getWorkflowJson();
+
+        // 保存到 company_workflow_lobster 表
+        if (jdbcTemplate != null && workflowJson != null) {
+            try {
+                String templateCode = "AI_" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
+                String industryType = (String) body.getOrDefault("industryType", "general");
+                jdbcTemplate.update(
+                    "INSERT INTO company_workflow_lobster(company_id, template_code, template_name, " +
+                    "industry_type, description, status, version, create_time) " +
+                    "VALUES(?,?,?,?,?,1,1,NOW())",
+                    companyId, templateCode, workflowName, industryType, workflowJson);
+                Map<String, Object> data = new HashMap<>();
+                data.put("workflowJson", workflowJson);
+                data.put("workflowName", workflowName);
+                data.put("templateCode", templateCode);
+                data.put("saved", true);
+                resultCache.remove(recordId);
+                return AjaxResult.success(data);
+            } catch (Exception e) {
+                // DB保存失败时降级返回 JSON,前端可手动保存
+                Map<String, Object> data = new HashMap<>();
+                data.put("workflowJson", workflowJson);
+                data.put("workflowName", workflowName);
+                data.put("saved", false);
+                resultCache.remove(recordId);
+                return AjaxResult.success(data);
+            }
+        }
+        // 无 DB 时返回 JSON 给前端
         Map<String, Object> data = new HashMap<>();
-        data.put("workflowJson", result.getWorkflowJson());
-        data.put("workflowName", body.get("workflowName"));
+        data.put("workflowJson", workflowJson);
+        data.put("workflowName", workflowName);
         resultCache.remove(recordId);
         return AjaxResult.success(data);
     }

+ 193 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterE2eController.java

@@ -0,0 +1,193 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.workflow.DynamicNodeImplService;
+import com.fs.company.service.workflow.LobsterE2eTestService;
+import com.fs.company.service.workflow.LobsterTestScenarioService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 租户端 E2E 测试 / 测试剧本 / 动态节点审批(saasui)
+ */
+@RestController
+public class LobsterE2eController extends BaseController {
+
+    @Autowired(required = false)
+    private LobsterE2eTestService e2eTestService;
+
+    @Autowired(required = false)
+    private LobsterTestScenarioService testScenarioService;
+
+    @Autowired(required = false)
+    private DynamicNodeImplService dynamicNodeImplService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    private Long currentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getCompany().getCompanyId();
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @PostMapping("/workflow/lobster/e2e/run")
+    public AjaxResult e2eRun(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        LobsterE2eTestService.E2eRequest req = buildE2eRequest(body, currentCompanyId());
+        return AjaxResult.success(e2eTestService.runE2e(req));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/workflow/lobster/e2e/report/{runId}")
+    public AjaxResult e2eReport(@PathVariable String runId) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        return AjaxResult.success(e2eTestService.getReport(runId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/workflow/lobster/e2e/list")
+    public AjaxResult e2eList(@RequestParam(defaultValue = "1") Integer pageNum,
+                                @RequestParam(defaultValue = "20") Integer pageSize) {
+        if (e2eTestService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(e2eTestService.listRuns(currentCompanyId(), pageNum, pageSize));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/workflow/lobster-exec/step-next/{instanceId}")
+    public AjaxResult stepNext(@PathVariable Long instanceId, @RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        String userInput = body != null ? (String) body.get("userInput") : null;
+        return AjaxResult.success(e2eTestService.stepNext(currentCompanyId(), instanceId, userInput));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/workflow/lobster/chat/multi-turn")
+    public AjaxResult multiTurn(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        List<String> inputs = parseStringList(body.get("userInputs"));
+        return AjaxResult.success(e2eTestService.multiTurn(
+                currentCompanyId(),
+                toLong(body.get("instanceId")),
+                (String) body.get("nodeCode"),
+                inputs));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/workflow/lobster/scenario/list")
+    public AjaxResult scenarioList(@RequestParam(required = false) Integer enabled,
+                                    @RequestParam(defaultValue = "1") Integer pageNum,
+                                    @RequestParam(defaultValue = "20") Integer pageSize) {
+        if (testScenarioService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(testScenarioService.listScenarios(currentCompanyId(), enabled, pageNum, pageSize));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioGet(@PathVariable Long id) {
+        if (testScenarioService == null) return AjaxResult.error("剧本服务未启用");
+        return AjaxResult.success(testScenarioService.getScenario(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
+    @PostMapping("/workflow/lobster/scenario/save")
+    public AjaxResult scenarioSave(@RequestBody Map<String, Object> body) {
+        if (testScenarioService == null) return AjaxResult.error("剧本服务未启用");
+        body.putIfAbsent("companyId", currentCompanyId());
+        Object idObj = body.get("id");
+        if (idObj == null) {
+            return AjaxResult.success(testScenarioService.createScenario(body));
+        }
+        testScenarioService.updateScenario(toLong(idObj), body);
+        return AjaxResult.success(idObj);
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
+    @DeleteMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioDelete(@PathVariable Long id) {
+        if (testScenarioService != null) testScenarioService.deleteScenario(id);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/workflow/lobster/scenario/{id}/run")
+    public AjaxResult scenarioRunNow(@PathVariable Long id) {
+        if (testScenarioService == null) return AjaxResult.error("剧本服务未启用");
+        String runId = testScenarioService.runScenarioNow(id);
+        Map<String, Object> r = new HashMap<>();
+        r.put("runId", runId);
+        return AjaxResult.success(r);
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/workflow/lobster/scenario/run-all")
+    public AjaxResult scenarioRunAll() {
+        if (testScenarioService == null) return AjaxResult.error("剧本服务未启用");
+        Map<String, Object> r = new HashMap<>();
+        r.put("triggered", testScenarioService.runAllEnabledScenarios());
+        return AjaxResult.success(r);
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/workflow/lobster/dynamic-impl/list")
+    public AjaxResult dynamicImplList(@RequestParam(required = false) String status) {
+        if (dynamicNodeImplService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(dynamicNodeImplService.listByStatus(status, currentCompanyId()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/approve")
+    public AjaxResult dynamicImplApprove(@PathVariable Long id) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        dynamicNodeImplService.approve(id, loginUser.getUsername());
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/reject")
+    public AjaxResult dynamicImplReject(@PathVariable Long id, @RequestParam String reason) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        dynamicNodeImplService.reject(id, loginUser.getUsername(), reason);
+        return AjaxResult.success();
+    }
+
+    private LobsterE2eTestService.E2eRequest buildE2eRequest(Map<String, Object> body, Long companyId) {
+        LobsterE2eTestService.E2eRequest req = new LobsterE2eTestService.E2eRequest();
+        req.setCompanyId(body.get("companyId") != null ? toLong(body.get("companyId")) : companyId);
+        req.setScenarioId(toLong(body.get("scenarioId")));
+        req.setTemplateId(toLong(body.get("templateId")));
+        req.setBusinessDesc((String) body.get("businessDesc"));
+        req.setIndustryType((String) body.get("industryType"));
+        req.setTestContactId(toLong(body.get("testContactId")));
+        req.setUserInputs(parseStringList(body.get("userInputs")));
+        return req;
+    }
+
+    private List<String> parseStringList(Object ui) {
+        List<String> in = new ArrayList<>();
+        if (ui instanceof List) {
+            for (Object o : (List<?>) ui) {
+                if (o != null) in.add(o.toString());
+            }
+        }
+        return in;
+    }
+
+    private static Long toLong(Object o) {
+        if (o == null) return null;
+        if (o instanceof Number) return ((Number) o).longValue();
+        try { return Long.valueOf(o.toString()); } catch (Exception e) { return null; }
+    }
+}

+ 33 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEngineController.java

@@ -1,6 +1,8 @@
 package com.fs.company.controller.workflow;
 
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.service.workflow.capability.LobsterNodeCapabilityRegistry;
+import com.fs.company.service.workflow.DynamicNodeExecutor;
 import com.fs.company.service.workflow.evolution.EvolutionEngine;
 import com.fs.company.service.workflow.evolution.EvolutionSuggestion;
 import com.fs.company.service.workflow.heartbeat.HeartbeatScheduler;
@@ -14,7 +16,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 @RestController
@@ -36,6 +41,34 @@ public class LobsterEngineController {
     @Autowired
     private TokenService tokenService;
 
+    @Autowired(required = false)
+    private DynamicNodeExecutor dynamicNodeExecutor;
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/node-capabilities")
+    public AjaxResult getNodeCapabilities() {
+        Map<Integer, ?> handlers = dynamicNodeExecutor != null
+                ? dynamicNodeExecutor.getRegisteredHandlers() : java.util.Collections.emptyMap();
+        List<Map<String, Object>> items = new ArrayList<>();
+        List<Map<String, Object>> gaps = new ArrayList<>();
+        for (LobsterNodeCapabilityRegistry.NodeCapability cap : LobsterNodeCapabilityRegistry.all()) {
+            Map<String, Object> row = new LinkedHashMap<>(cap.toMap());
+            boolean implemented = handlers.containsKey(cap.code);
+            row.put("handlerRegistered", implemented);
+            row.put("gap", !implemented);
+            items.add(row);
+            if (!implemented) {
+                gaps.add(row);
+            }
+        }
+        Map<String, Object> payload = new LinkedHashMap<>();
+        payload.put("summary", LobsterNodeCapabilityRegistry.summary());
+        payload.put("capabilities", items);
+        payload.put("gaps", gaps);
+        payload.put("registeredHandlerCount", handlers.size());
+        return AjaxResult.success(payload);
+    }
+
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/evolution/metrics")
     public AjaxResult getEvolutionMetrics() {

+ 93 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterInboundController.java

@@ -0,0 +1,93 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.workflow.inbound.LobsterInboundService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 龙虾统一入站网关 API
+ */
+@RestController
+@RequestMapping("/workflow/lobster/inbound")
+public class LobsterInboundController extends BaseController {
+
+    @Autowired(required = false)
+    private LobsterInboundService inboundService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/message")
+    public AjaxResult inboundMessage(@RequestBody Map<String, Object> body) {
+        if (inboundService == null) return AjaxResult.error("入站服务不可用");
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        Long contactId = toLong(body.get("contactId"));
+        String channelType = body.get("channelType") != null ? body.get("channelType").toString() : "QW";
+        String message = body.get("message") != null ? body.get("message").toString() : null;
+        Long workflowId = toLong(body.get("workflowId"));
+        @SuppressWarnings("unchecked")
+        Map<String, Object> extra = body.get("extra") instanceof Map ? (Map<String, Object>) body.get("extra") : null;
+        return inboundService.handleInboundMessage(companyId, contactId, channelType, message, workflowId, extra);
+    }
+
+    /** Webhook 入口(内部集成 / 渠道回调可传 companyId) */
+    @PostMapping("/webhook/message")
+    public AjaxResult webhookMessage(@RequestBody Map<String, Object> body) {
+        if (inboundService == null) return AjaxResult.error("入站服务不可用");
+        Long companyId = toLong(body.get("companyId"));
+        if (companyId == null) {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser != null && loginUser.getCompany() != null) {
+                companyId = loginUser.getCompany().getCompanyId();
+            }
+        }
+        if (companyId == null) return AjaxResult.error("companyId 必填");
+        Long contactId = toLong(body.get("contactId"));
+        String channelType = body.get("channelType") != null ? body.get("channelType").toString() : "QW";
+        String message = body.get("message") != null ? body.get("message").toString()
+                : (body.get("content") != null ? body.get("content").toString() : null);
+        Long workflowId = toLong(body.get("workflowId"));
+        @SuppressWarnings("unchecked")
+        Map<String, Object> extra = body.get("extra") instanceof Map ? (Map<String, Object>) body.get("extra") : null;
+        return inboundService.handleInboundMessage(companyId, contactId, channelType, message, workflowId, extra);
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
+    @PostMapping("/event")
+    public AjaxResult inboundEvent(@RequestBody Map<String, Object> body) {
+        if (inboundService == null) return AjaxResult.error("入站服务不可用");
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        String eventType = body.get("eventType") != null ? body.get("eventType").toString() : null;
+        if (eventType == null || eventType.isBlank()) {
+            return AjaxResult.error("eventType 必填");
+        }
+        return inboundService.handleBusinessEvent(companyId, eventType, body);
+    }
+
+    @PostMapping("/webhook/event")
+    public AjaxResult webhookEvent(@RequestBody Map<String, Object> body) {
+        if (inboundService == null) return AjaxResult.error("入站服务不可用");
+        Long companyId = toLong(body.get("companyId"));
+        if (companyId == null) return AjaxResult.error("companyId 必填");
+        String eventType = body.get("eventType") != null ? body.get("eventType").toString() : null;
+        if (eventType == null) return AjaxResult.error("eventType 必填");
+        return inboundService.handleBusinessEvent(companyId, eventType, body);
+    }
+
+    private Long toLong(Object v) {
+        if (v == null) return null;
+        if (v instanceof Number) return ((Number) v).longValue();
+        try { return Long.parseLong(v.toString()); } catch (Exception e) { return null; }
+    }
+}

+ 117 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterInstanceMonitorController.java

@@ -0,0 +1,117 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.LobsterNodeExecutionLog;
+import com.fs.company.domain.LobsterWorkflowInstance;
+import com.fs.company.mapper.LobsterNodeExecutionLogMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
+import com.fs.company.service.workflow.LobsterWorkflowExecutor;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 工作流实例监控(直连 Mapper / Executor,替代 CompanyBridgeController 空桩)
+ */
+@RestController
+@RequestMapping("/workflow/lobster/instance")
+public class LobsterInstanceMonitorController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired(required = false)
+    private LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper instanceMapper;
+
+    @Autowired(required = false)
+    private LobsterNodeExecutionLogMapper executionLogMapper;
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private Long companyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getCompany().getCompanyId();
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:list')")
+    @GetMapping({"", "/list"})
+    public AjaxResult list(@RequestParam(required = false) String status) {
+        if (instanceMapper == null) return AjaxResult.success(Collections.emptyList());
+        List<LobsterWorkflowInstance> list = instanceMapper.selectByCompanyId(companyId());
+        return AjaxResult.success(list != null ? list : Collections.emptyList());
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/stats")
+    public AjaxResult stats() {
+        Long cid = companyId();
+        Map<String, Object> stats = new LinkedHashMap<>();
+        if (jdbcTemplate == null) {
+            stats.put("running", 0);
+            stats.put("paused", 0);
+            stats.put("completed", 0);
+            stats.put("deadLetters", 0);
+            stats.put("todayTokens", "0");
+            return AjaxResult.success(stats);
+        }
+        try {
+            String base = " FROM lobster_workflow_instance WHERE del_flag=0 AND company_id=?";
+            stats.put("running", jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*)" + base + " AND status='running'", Integer.class, cid));
+            stats.put("paused", jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*)" + base + " AND status='paused'", Integer.class, cid));
+            stats.put("completed", jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*)" + base + " AND status='completed'", Integer.class, cid));
+            stats.put("deadLetters", jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM lobster_dead_letter_queue WHERE company_id=?", Integer.class, cid));
+            Object tokens = jdbcTemplate.queryForObject(
+                    "SELECT COALESCE(SUM(token_count),0) FROM lobster_token_consume_log WHERE company_id=? AND DATE(create_time)=CURDATE()",
+                    Object.class, cid);
+            stats.put("todayTokens", tokens != null ? tokens.toString() : "0");
+        } catch (Exception e) {
+            stats.put("running", 0);
+            stats.put("paused", 0);
+            stats.put("completed", 0);
+            stats.put("deadLetters", 0);
+            stats.put("todayTokens", "0");
+        }
+        return AjaxResult.success(stats);
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/{instanceId}")
+    public AjaxResult detail(@PathVariable Long instanceId) {
+        if (workflowExecutor != null) {
+            return AjaxResult.success(workflowExecutor.getInstanceState(companyId(), instanceId));
+        }
+        LobsterWorkflowInstance inst = instanceMapper != null ? instanceMapper.selectById(instanceId) : null;
+        return inst != null ? AjaxResult.success(inst) : AjaxResult.error("实例不存在");
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/node-logs/{instanceId}")
+    public AjaxResult nodeLogs(@PathVariable Long instanceId) {
+        List<LobsterNodeExecutionLog> logs = executionLogMapper != null
+                ? executionLogMapper.selectByInstanceId(instanceId, companyId()) : Collections.emptyList();
+        return AjaxResult.success(logs != null ? logs : Collections.emptyList());
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:terminate')")
+    @PostMapping("/terminate/{instanceId}")
+    public AjaxResult terminate(@PathVariable Long instanceId,
+                                @RequestParam(required = false) String reason) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        return workflowExecutor.terminateWorkflow(companyId(), instanceId, reason);
+    }
+}

+ 214 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPlatformAdminController.java

@@ -0,0 +1,214 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.mapper.LobsterChatSessionMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
+import com.fs.company.service.workflow.ILobsterBillingService;
+import com.fs.company.service.workflow.ILobsterEventAuditService;
+import com.fs.company.service.workflow.ILobsterSalesCorpusService;
+import com.fs.company.service.workflow.evolution.EvolutionEngine;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * ????????????? API????? Service / Mapper?????????????
+ * ¡¤???? saasui / saasadminui ?? lobster-admin.js ????
+ */
+@RestController
+@RequestMapping("/workflow/lobster-admin")
+public class LobsterPlatformAdminController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired(required = false)
+    private ILobsterSalesCorpusService salesCorpusService;
+
+    @Autowired(required = false)
+    private ILobsterEventAuditService eventAuditService;
+
+    @Autowired(required = false)
+    private ILobsterBillingService billingService;
+
+    @Autowired(required = false)
+    private EvolutionEngine evolutionEngine;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper instanceMapper;
+
+    @Autowired(required = false)
+    private LobsterChatSessionMapper chatSessionMapper;
+
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    private Long currentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getCompany().getCompanyId();
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/companies")
+    public AjaxResult companies() {
+        Long companyId = currentCompanyId();
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("id", companyId);
+        row.put("companyId", companyId);
+        if (instanceMapper != null) {
+            List<?> list = instanceMapper.selectByCompanyId(companyId);
+            row.put("instanceCount", list != null ? list.size() : 0);
+        } else {
+            row.put("instanceCount", 0);
+        }
+        return AjaxResult.success(Collections.singletonList(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/company-stats/{companyId}")
+    public AjaxResult companyStats(@PathVariable Long companyId) {
+        ensureSameTenant(companyId);
+        return AjaxResult.success(buildTenantStats(companyId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/platform-stats")
+    public AjaxResult platformStats() {
+        return AjaxResult.success(buildTenantStats(currentCompanyId()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/instances")
+    public AjaxResult instances(@RequestParam(required = false) Long workflowId,
+                                @RequestParam(required = false) String status) {
+        Long companyId = currentCompanyId();
+        if (instanceMapper == null) return AjaxResult.success(Collections.emptyList());
+        List<?> list = instanceMapper.selectByCompanyId(companyId);
+        return AjaxResult.success(list != null ? list : Collections.emptyList());
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/event-audits")
+    public AjaxResult eventAudits(@RequestParam(defaultValue = "pending") String status,
+                                  @RequestParam(defaultValue = "1") int pageNum,
+                                  @RequestParam(defaultValue = "10") int pageSize) {
+        if (eventAuditService == null) return AjaxResult.success(Collections.emptyList());
+        return AjaxResult.success(eventAuditService.listAudits(status, pageNum, pageSize, currentCompanyId()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/sales-corpus")
+    public AjaxResult salesCorpus(@RequestParam(defaultValue = "1") int pageNum,
+                                  @RequestParam(defaultValue = "10") int pageSize,
+                                  @RequestParam(required = false) String scenario) {
+        if (salesCorpusService == null) return AjaxResult.success(Collections.emptyList());
+        return AjaxResult.success(salesCorpusService.listCorpus(pageNum, pageSize, currentCompanyId(), scenario, null));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/chat-aggregate")
+    public AjaxResult chatAggregate(@RequestParam(required = false) String channelType,
+                                    @RequestParam(required = false) String keyword) {
+        if (chatSessionMapper == null) return AjaxResult.success(Collections.emptyList());
+        try {
+            return AjaxResult.success(chatSessionMapper.selectForAggregate(channelType, keyword));
+        } catch (Exception e) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/dead-letters")
+    public AjaxResult deadLetters() {
+        if (jdbcTemplate == null) return AjaxResult.success(Collections.emptyList());
+        try {
+            Long companyId = currentCompanyId();
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                    "SELECT * FROM lobster_dead_letter_queue WHERE company_id=? ORDER BY create_time DESC LIMIT 200",
+                    companyId));
+        } catch (Exception e) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/optimizations")
+    public AjaxResult optimizations() {
+        if (evolutionEngine == null) return AjaxResult.success(Collections.emptyMap());
+        return AjaxResult.success(evolutionEngine.getEvolutionMetrics(currentCompanyId()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/prompts")
+    public AjaxResult prompts() {
+        if (jdbcTemplate == null) return AjaxResult.success(Collections.emptyList());
+        try {
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                    "SELECT * FROM lobster_prompt_template WHERE company_id=? ORDER BY update_time DESC LIMIT 200",
+                    currentCompanyId()));
+        } catch (Exception e) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/api-registry")
+    public AjaxResult apiRegistry() {
+        if (jdbcTemplate == null) return AjaxResult.success(Collections.emptyList());
+        try {
+            return AjaxResult.success(jdbcTemplate.queryForList(
+                    "SELECT id, api_code, api_name, api_url, api_method, status, create_time FROM lobster_smart_api WHERE company_id=? ORDER BY create_time DESC LIMIT 200",
+                    currentCompanyId()));
+        } catch (Exception e) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
+    @GetMapping("/billing-records")
+    public AjaxResult billingRecords(@RequestParam(defaultValue = "1") int page,
+                                     @RequestParam(defaultValue = "20") int size) {
+        if (billingService == null) return AjaxResult.success(Collections.emptyList());
+        return AjaxResult.success(billingService.listTokenRecords(page, size, currentCompanyId()));
+    }
+
+    private Map<String, Object> buildTenantStats(Long companyId) {
+        Map<String, Object> stats = new LinkedHashMap<>();
+        stats.put("companyId", companyId);
+        stats.put("templateCount", 0);
+        stats.put("instanceCount", 0);
+        stats.put("runningInstances", 0);
+        stats.put("totalTokens", "0");
+        if (jdbcTemplate != null) {
+            try {
+                stats.put("templateCount", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM company_workflow_lobster WHERE company_id=? AND del_flag=0",
+                        Integer.class, companyId));
+                stats.put("instanceCount", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_workflow_instance WHERE company_id=? AND del_flag=0",
+                        Integer.class, companyId));
+                stats.put("runningInstances", jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM lobster_workflow_instance WHERE company_id=? AND del_flag=0 AND status='running'",
+                        Integer.class, companyId));
+            } catch (Exception ignored) { }
+        }
+        if (evolutionEngine != null) {
+            Map<String, Object> metrics = evolutionEngine.getEvolutionMetrics(companyId);
+            stats.put("evolutionMetrics", metrics);
+        }
+        return stats;
+    }
+
+    private void ensureSameTenant(Long companyId) {
+        if (companyId != null && !companyId.equals(currentCompanyId())) {
+            throw new SecurityException("?????????????????");
+        }
+    }
+}

+ 41 - 4
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java

@@ -15,6 +15,8 @@ import com.fs.company.service.workflow.LobsterWorkflowExecutor;
 import com.fs.company.service.workflow.SemanticTakeoverDetector;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -26,6 +28,8 @@ import java.util.Map;
 @RequestMapping("/workflow/lobster-exec")
 public class LobsterWorkflowExecController extends BaseController {
 
+    private static final Logger logger = LoggerFactory.getLogger(LobsterWorkflowExecController.class);
+
     @Autowired
     private LobsterWorkflowExecutor workflowExecutor;
 
@@ -259,13 +263,46 @@ public class LobsterWorkflowExecController extends BaseController {
         String content = (String) params.getOrDefault("content", "");
         Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
 
-        // 模拟对话响应(后续可对接真实的 LobsterWorkflowExecutor 模拟执行能力)
+        // 对接真实的龙虾工作流模拟执行
+        if (templateId != null && content != null && !content.isEmpty()) {
+            try {
+                // 发起一次模拟对话:启动临时实例 → 执行首个节点 → 返回AI回复
+                Map<String, Object> initVars = new java.util.HashMap<>();
+                initVars.put("simulate_mode", true);
+                initVars.put("customer_input", content);
+                AjaxResult startResult = workflowExecutor.startWorkflow(companyId, templateId, 0L, initVars);
+                if (startResult != null && Integer.valueOf(200).equals(startResult.get("code"))) {
+                    // startWorkflow 返回的 data 可能包含首个节点的回复
+                    Map<String, Object> reply = new java.util.HashMap<>();
+                    Object data = startResult.get("data");
+                    if (data instanceof Map) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> dataMap = (Map<String, Object>) data;
+                        Long instanceId = dataMap.get("instanceId") != null
+                                ? Long.valueOf(dataMap.get("instanceId").toString()) : null;
+                        reply.put("instanceId", instanceId);
+                        reply.put("reply", dataMap.getOrDefault("reply", dataMap.getOrDefault("message",
+                                "模拟对话已启动,请查看实例执行日志")));
+                    } else {
+                        reply.put("reply", "模拟对话已启动");
+                    }
+                    reply.put("templateId", templateId);
+                    reply.put("companyId", companyId);
+                    reply.put("timestamp", System.currentTimeMillis());
+                    reply.put("mode", "simulate");
+                    return AjaxResult.success(reply);
+                }
+            } catch (Exception e) {
+                logger.error("[Simulate] 模拟对话失败: companyId={}, templateId={}", companyId, templateId, e);
+            }
+        }
+
+        // 降级:无模板时返回提示
         Map<String, Object> reply = new java.util.HashMap<>();
-        reply.put("reply", "[模拟回复] \u300c" + content + "\u300d — 模板ID:" + (templateId != null ? templateId : "N/A"));
-        reply.put("templateId", templateId);
+        reply.put("reply", "请提供有效的模板ID和对话内容");
+        reply.put("mode", "simulate");
         reply.put("companyId", companyId);
         reply.put("timestamp", System.currentTimeMillis());
-        reply.put("mode", "simulate");
         return AjaxResult.success(reply);
     }
 }

+ 2 - 2
fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -77,7 +77,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                     sendMsgVo.setCmd("entry");
                     sendMsgVo.setMsg("用户进入");
                     sendMsgVo.setData(JSONObject.toJSONString(liveWatchUser));
-                    sendMsgVo.setNickName(fsUser.getNickname());
+                    sendMsgVo.setNickName(fsUser.getNickName());
                     sendMsgVo.setAvatar(fsUser.getAvatar());
                     broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
                 } else if (userType == 1) {
@@ -163,7 +163,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                 sendMsgVo.setCmd("out");
                 sendMsgVo.setMsg("用户离开");
                 sendMsgVo.setData(JSONObject.toJSONString(close));
-                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setNickName(fsUser.getNickName());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
                 broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             } else {

+ 2 - 2
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -203,7 +203,7 @@ public class WebSocketServer {
                 sendMsgVo.setCmd("entry");
                 sendMsgVo.setMsg("用户进入");
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setNickName(fsUser.getNickName());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
                 // 广播连接消息
                 broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
@@ -361,7 +361,7 @@ public class WebSocketServer {
                 sendMsgVo.setCmd("out");
                 sendMsgVo.setMsg("用户离开");
                 sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setNickName(fsUser.getNickName());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
                 broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             }

+ 0 - 40
fs-quartz/pom.xml

@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <parent>
-        <artifactId>fs</artifactId>
-        <groupId>com.fs</groupId>
-        <version>1.1.0</version>
-    </parent>
-    <modelVersion>4.0.0</modelVersion>
-
-    <artifactId>fs-quartz</artifactId>
-
-    <description>
-        quartz定时任务
-    </description>
-
-    <dependencies>
-
-        <!-- 定时任务 -->
-        <dependency>
-            <groupId>org.quartz-scheduler</groupId>
-            <artifactId>quartz</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>com.mchange</groupId>
-                    <artifactId>c3p0</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-
-        <!-- 通用工具-->
-        <dependency>
-            <groupId>com.fs</groupId>
-            <artifactId>fs-common</artifactId>
-        </dependency>
-
-    </dependencies>
-
-</project>

+ 0 - 167
fs-quartz/src/main/java/com/fs/quartz/controller/SysJobController.java

@@ -1,167 +0,0 @@
-package com.fs.quartz.controller;
-
-import java.util.List;
-import org.quartz.SchedulerException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-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.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-import com.fs.common.annotation.Log;
-import com.fs.common.constant.Constants;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.page.TableDataInfo;
-import com.fs.common.enums.BusinessType;
-import com.fs.common.exception.job.TaskException;
-import com.fs.common.utils.StringUtils;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.quartz.domain.SysJob;
-import com.fs.quartz.service.ISysJobService;
-import com.fs.quartz.util.CronUtils;
-
-/**
- * 调度任务信息操作处理
- * 
-
- */
-@RestController
-@RequestMapping("/monitor/job")
-public class SysJobController extends BaseController
-{
-    @Autowired
-    private ISysJobService jobService;
-
-    /**
-     * 查询定时任务列表
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(SysJob sysJob)
-    {
-        startPage();
-        List<SysJob> list = jobService.selectJobList(sysJob);
-        return getDataTable(list);
-    }
-
-    /**
-     * 导出定时任务列表
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
-    @Log(title = "定时任务", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(SysJob sysJob)
-    {
-        List<SysJob> list = jobService.selectJobList(sysJob);
-        ExcelUtil<SysJob> util = new ExcelUtil<SysJob>(SysJob.class);
-        return util.exportExcel(list, "定时任务");
-    }
-
-    /**
-     * 获取定时任务详细信息
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
-    @GetMapping(value = "/{jobId}")
-    public AjaxResult getInfo(@PathVariable("jobId") Long jobId)
-    {
-        return AjaxResult.success(jobService.selectJobById(jobId));
-    }
-
-    /**
-     * 新增定时任务
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:add')")
-    @Log(title = "定时任务", businessType = BusinessType.INSERT)
-    @PostMapping
-    public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
-    {
-        if (!CronUtils.isValid(job.getCronExpression()))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap://'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
-        }
-        job.setCreateBy(getUsername());
-        return toAjax(jobService.insertJob(job));
-    }
-
-    /**
-     * 修改定时任务
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
-    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
-    @PutMapping
-    public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
-    {
-        if (!CronUtils.isValid(job.getCronExpression()))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap://'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
-        }
-        job.setUpdateBy(getUsername());
-        return toAjax(jobService.updateJob(job));
-    }
-
-    /**
-     * 定时任务状态修改
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
-    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
-    @PutMapping("/changeStatus")
-    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
-    {
-        SysJob newJob = jobService.selectJobById(job.getJobId());
-        newJob.setStatus(job.getStatus());
-        return toAjax(jobService.changeStatus(newJob));
-    }
-
-    /**
-     * 定时任务立即执行一次
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
-    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
-    @PutMapping("/run")
-    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
-    {
-        jobService.run(job);
-        return AjaxResult.success();
-    }
-
-    /**
-     * 删除定时任务
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
-    @Log(title = "定时任务", businessType = BusinessType.DELETE)
-    @DeleteMapping("/{jobIds}")
-    public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException
-    {
-        jobService.deleteJobByIds(jobIds);
-        return AjaxResult.success();
-    }
-}

+ 0 - 165
fs-quartz/src/main/java/com/fs/quartz/controller/SysJobLogController.java

@@ -1,165 +0,0 @@
-package com.fs.quartz.controller;
-
-import java.util.List;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-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;
-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;
-
-/**
- * 调度日志操作处理
- * 
-
- */
-@RestController
-@RequestMapping("/monitor/jobLog")
-public class SysJobLogController extends BaseController
-{
-    @Autowired
-    private ISysJobLogService jobLogService;
-
-    /**
-     * 查询定时任务调度日志列表
-     * 支持 SaaS 平台后台跨租户查询:传入 tenantId 时自动切换到对应租户数据源执行查询,查询完成后自动还原。
-     */
-    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(SysJobLog sysJobLog, @RequestParam(value = "tenantId", required = false) Long tenantId)
-    {
-        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, @RequestParam(value = "tenantId", required = false) Long tenantId)
-    {
-        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 = "/{jobLogId}")
-    public AjaxResult getInfo(@PathVariable Long jobLogId, @RequestParam(value = "tenantId", required = false) Long tenantId)
-    {
-        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, @RequestParam(value = "tenantId", required = false) Long tenantId)
-    {
-        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(@RequestParam(value = "tenantId", required = false) Long tenantId)
-    {
-        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) {}
-    }
-}

+ 0 - 95
fs-quartz/src/main/java/com/fs/quartz/util/CronUtils.java

@@ -1,95 +0,0 @@
-package com.fs.quartz.util;
-
-import java.text.ParseException;
-import java.util.Calendar;
-import java.util.Date;
-import org.quartz.CronExpression;
-
-/**
- * cron表达式工具类
- * 
-
- *
- */
-public class CronUtils
-{
-    /**
-     * 返回一个布尔值代表一个给定的Cron表达式的有效性
-     *
-     * @param cronExpression Cron表达式
-     * @return boolean 表达式是否有效
-     */
-    public static boolean isValid(String cronExpression)
-    {
-        return CronExpression.isValidExpression(cronExpression);
-    }
-
-    /**
-     * 返回一个字符串值,表示该消息无效Cron表达式给出有效性
-     *
-     * @param cronExpression Cron表达式
-     * @return String 无效时返回表达式错误描述,如果有效返回null
-     */
-    public static String getInvalidMessage(String cronExpression)
-    {
-        try
-        {
-            new CronExpression(cronExpression);
-            return null;
-        }
-        catch (ParseException pe)
-        {
-            return pe.getMessage();
-        }
-    }
-
-    /**
-     * 返回下一个执行时间根据给定的Cron表达式
-     *
-     * @param cronExpression Cron表达式
-     * @return Date 下次Cron表达式执行时间
-     */
-    public static Date getNextExecution(String cronExpression)
-    {
-        try
-        {
-            CronExpression cron = new CronExpression(cronExpression);
-            return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
-        }
-        catch (ParseException e)
-        {
-            throw new IllegalArgumentException(e.getMessage());
-        }
-    }
-
-    /**
-     * 判断 cron 表达式是否在当前这一分钟内会触发(用于 SaaS 租户任务分发器)
-     *
-     * @param cronExpression cron 表达式
-     * @return 当前分钟内会触发返回 true
-     */
-    public static boolean isDueInThisMinute(String cronExpression) {
-        if (cronExpression == null || cronExpression.isEmpty()) {
-            return false;
-        }
-        try {
-            CronExpression cron = new CronExpression(cronExpression);
-
-            Calendar cal = Calendar.getInstance();
-            cal.set(Calendar.SECOND, 0);
-            cal.set(Calendar.MILLISECOND, 0);
-
-            Date startOfMinute = cal.getTime();
-            cal.add(Calendar.MINUTE, 1);
-            Date startOfNextMinute = cal.getTime();
-
-            // 关键:用 startOfMinute 往前挪一点,才能把 “刚好等于 startOfMinute(00秒)” 的触发点算进来
-            Date base = new Date(startOfMinute.getTime() - 1);
-
-            Date next = cron.getNextValidTimeAfter(base);
-            return next != null && !next.before(startOfMinute) && next.before(startOfNextMinute);
-        } catch (ParseException e) {
-            return false;
-        }
-    }
-}

+ 1 - 1
fs-qw-api/src/main/java/com/fs/app/controller/QwGroupChatController.java

@@ -67,7 +67,7 @@ public class QwGroupChatController extends BaseController {
             log.info("[GroupChat] 同步我的客户群信息,tenantId={}, corpId={}, companyUserId={}", tenantId, corpId, companyUserId);
             return tenantDataSourceUtil.executeWithResult(tenantId, () -> {
                 try {
-                    List<String> qwUserIdList = iQwUserService.selectQwUserListByCompanyUserId(companyUserId, corpId);
+                    List<String> qwUserIdList = iQwUserService.selectQwOpenUserListByCompanyUserId(companyUserId, corpId);
                     return qwGroupChatService.cogradientGroupChat(corpId, qwUserIdList);
                 } catch (Exception e) {
                     log.error("[GroupChat] 同步我的客户群信息异常", e);

+ 18 - 16
fs-service/pom.xml

@@ -43,12 +43,6 @@
             <artifactId>IJPay-All</artifactId>
         </dependency>
 
-        <!-- 定时任务(统一使用 fs-quartz,避免与 fs-service 内重复拷贝的 com.fs.quartz.* 冲突) -->
-        <dependency>
-            <groupId>com.fs</groupId>
-            <artifactId>fs-quartz</artifactId>
-        </dependency>
-
         <dependency>
             <groupId>cn.jpush.api</groupId>
             <artifactId>jpush-client</artifactId>
@@ -310,16 +304,6 @@
             <version>${org.mapstruct.version}</version>
         </dependency>
 
-        <dependency>
-            <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct</artifactId>
-            <version>${org.mapstruct.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct-processor</artifactId>
-            <version>${org.mapstruct.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.hc</groupId>
             <artifactId>openapi</artifactId>
@@ -371,4 +355,22 @@
 
     </dependencies>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                    <compilerArgs>
+                        <arg>-Xlint:-processing</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
 </project>

+ 25 - 8
fs-service/src/main/java/com/fs/comm/service/CommSmsSendService.java

@@ -110,11 +110,11 @@ public class CommSmsSendService {
             throw new ServiceException("客户不存在");
         }
         String targetPhone = StringUtils.isNotBlank(param.getPhone()) ? param.getPhone() : customer.getMobile();
-        checkSmsBlacklist(companyId, targetPhone);
+        checkCustomerSmsBlacklist(companyId, targetPhone);
         if (companyUserId != null) {
             CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
             if (companyUser != null && StringUtils.isNotBlank(companyUser.getPhonenumber())) {
-                checkSmsBlacklist(companyId, companyUser.getPhonenumber());
+                checkSalesSmsBlacklist(companyId, companyUser.getPhonenumber());
             }
         }
 
@@ -298,14 +298,14 @@ public class CommSmsSendService {
             for (Long customerId : param.getCustomerIds()) {
                 CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
                 if (customer != null && StringUtils.isNotBlank(customer.getMobile())) {
-                    checkSmsBlacklist(param.getCompanyId(), customer.getMobile());
+                    checkCustomerSmsBlacklist(param.getCompanyId(), customer.getMobile());
                 }
             }
         }
         if (param.getCompanyUserId() != null) {
             CompanyUser companyUser = companyUserService.selectCompanyUserById(param.getCompanyUserId());
             if (companyUser != null && StringUtils.isNotBlank(companyUser.getPhonenumber())) {
-                checkSmsBlacklist(param.getCompanyId(), companyUser.getPhonenumber());
+                checkSalesSmsBlacklist(param.getCompanyId(), companyUser.getPhonenumber());
             }
         }
         try {
@@ -359,17 +359,34 @@ public class CommSmsSendService {
         }
     }
 
-    private void checkSmsBlacklist(Long companyId, String phone) {
+    private void checkCustomerSmsBlacklist(Long companyId, String phone) {
+        checkSmsBlacklistInternal(companyId, phone, false);
+    }
+
+    private void checkSalesSmsBlacklist(Long companyId, String phone) {
+        checkSmsBlacklistInternal(companyId, phone, true);
+    }
+
+    private void checkSmsBlacklistInternal(Long companyId, String phone, boolean salesSide) {
         if (StringUtils.isBlank(phone)) {
             return;
         }
+        String targetValue = PhoneUtil.resolvePhoneForBlacklist(phone);
+        if (StringUtils.isBlank(targetValue)) {
+            throw new ServiceException("手机号无效或解密失败");
+        }
         CompanyVoiceRoboticCallBlacklistCheckParam checkParam = new CompanyVoiceRoboticCallBlacklistCheckParam();
         checkParam.setCompanyId(companyId);
         checkParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
-        checkParam.setTargetValue(PhoneUtil.decryptPhone(phone));
+        checkParam.setTargetType(1);
+        checkParam.setTargetValue(targetValue);
         CompanyVoiceRoboticCallBlacklistCheckVO vo = companyVoiceRoboticCallBlacklistService.checkBlacklist(checkParam);
-        if (!vo.getPass()) {
-            throw new ServiceException("号码命中短信黑名单: " + phone);
+        if (Boolean.TRUE.equals(vo.getPass())) {
+            return;
+        }
+        if (Boolean.TRUE.equals(vo.getHitBlacklist())) {
+            throw new ServiceException(salesSide ? "销售号码黑名单校验未通过" : "客户号码黑名单校验未通过");
         }
+        throw new ServiceException(StringUtils.defaultIfBlank(vo.getReason(), "黑名单校验未通过"));
     }
 }

+ 11 - 2
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -33,6 +33,7 @@ import com.fs.crm.param.SmsSendParam;
 import com.fs.crm.param.SmsSendUserParam;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.his.config.FsSmsConfig;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.his.domain.FsPayConfig;
 import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.mapper.FsPackageOrderMapper;
@@ -781,7 +782,11 @@ public class SmsServiceImpl implements ISmsService
             CompanyVoiceRoboticCallBlacklistCheckParam salesBlacklistParam = new CompanyVoiceRoboticCallBlacklistCheckParam();
             salesBlacklistParam.setCompanyId(param.getCompanyId());
             salesBlacklistParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
-            salesBlacklistParam.setTargetValue(companyUser.getPhonenumber());
+            salesBlacklistParam.setTargetType(1);
+            salesBlacklistParam.setTargetValue(PhoneUtil.resolvePhoneForBlacklist(companyUser.getPhonenumber()));
+            if (StringUtils.isEmpty(salesBlacklistParam.getTargetValue())) {
+                throw new RuntimeException("销售手机号无效或解密失败");
+            }
             CompanyVoiceRoboticCallBlacklistCheckVO salesBlacklistVo = companyVoiceRoboticCallBlacklistService.checkBlacklist(salesBlacklistParam);
             if (!salesBlacklistVo.getPass()){
                 throw new RuntimeException("销售号码黑名单校验未通过");
@@ -796,7 +801,11 @@ public class SmsServiceImpl implements ISmsService
                 CompanyVoiceRoboticCallBlacklistCheckParam customerBlacklistParam = new CompanyVoiceRoboticCallBlacklistCheckParam();
                 customerBlacklistParam.setCompanyId(param.getCompanyId());
                 customerBlacklistParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
-                customerBlacklistParam.setTargetValue(crmCustomer.getMobile());
+                customerBlacklistParam.setTargetType(1);
+                customerBlacklistParam.setTargetValue(PhoneUtil.resolvePhoneForBlacklist(crmCustomer.getMobile()));
+                if (StringUtils.isEmpty(customerBlacklistParam.getTargetValue())) {
+                    throw new RuntimeException("客户手机号无效或解密失败");
+                }
                 CompanyVoiceRoboticCallBlacklistCheckVO customerBlacklistVo = companyVoiceRoboticCallBlacklistService.checkBlacklist(customerBlacklistParam);
                 if (!customerBlacklistVo.getPass()){
                     throw new RuntimeException("客户号码黑名单校验未通过");

+ 12 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySmsTemp.java

@@ -48,6 +48,17 @@ public class CompanySmsTemp extends BaseEntity
 
     private Integer isAudit;
 
+    /** 绑定的短信接口IDs(逗号分隔,存主库 api_id) */
+    private String smsApiIds;
+
+    public String getSmsApiIds() {
+        return smsApiIds;
+    }
+
+    public void setSmsApiIds(String smsApiIds) {
+        this.smsApiIds = smsApiIds;
+    }
+
     public Integer getIsAudit() {
         return isAudit;
     }
@@ -142,6 +153,7 @@ public class CompanySmsTemp extends BaseEntity
             .append("updateTime", getUpdateTime())
             .append("status", getStatus())
             .append("cateId", getCateId())
+            .append("smsApiIds", getSmsApiIds())
             .toString();
     }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterTask.java

@@ -86,4 +86,6 @@ public class CompanyWorkflowLobsterTask extends BaseEntity {
     private Long qwUserId; // 企微用户id
 
     private Long bindingId;
+
+    private String externalUserId; // 企微外部联系人ID
 }

+ 27 - 7
fs-service/src/main/java/com/fs/company/domain/LobsterConversationSummary.java

@@ -12,13 +12,33 @@ public class LobsterConversationSummary {
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long companyId;
-    private String externalUserId;
-    private String sessionId;
-    private String summaryText;
-    private String keywords;
-    private String sentiment;
-    private String intentCategory;
+    /** DDL instance_id: 工作流实例ID */
+    private Long instanceId;
+    private Long contactId;
+    private String summaryType;
+    /** DDL summary_content: 摘要内容(结构化写入) */
+    private String summaryContent;
+    private String keyPoints;
+    private String sentimentAnalysis;
+    private String nextActionSuggestion;
     private Integer messageCount;
-    private LocalDateTime summaryTime;
+    /** 自动摘要字段: summary_text(原始文本) */
+    private String summaryText;
+    /** 自动摘要字段: 外部联系人ID */
+    private String externalUserId;
+    /** 自动摘要字段: 聊天消息计数 */
+    private Integer chatMsgCount;
+    /** 自动摘要字段: 关键意图JSON */
+    private String keyIntents;
+    /** 自动摘要字段: 关键变量JSON */
+    private String keyVariables;
+    /** 自动摘要字段: 当前阶段 */
+    private String stage;
+    /** 自动摘要字段: 下一步行动提示 */
+    private String nextActionHint;
+    private Integer delFlag;
+    private String createBy;
     private LocalDateTime createTime;
+    private String updateBy;
+    private LocalDateTime updateTime;
 }

+ 64 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterE2eRun.java

@@ -0,0 +1,64 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾E2E测试运行头表
+ */
+@Data
+@TableName("lobster_e2e_run")
+public class LobsterE2eRun {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 运行ID(UUID) */
+    private String runId;
+
+    private Long companyId;
+
+    /** 工作流模板ID */
+    private Long templateId;
+
+    /** 工作流实例ID */
+    private Long instanceId;
+
+    /** 测试场景ID */
+    private Long scenarioId;
+
+    /** 业务描述(即时生成时) */
+    private String businessDesc;
+
+    /** 综合评分(0-100) */
+    private BigDecimal totalScore;
+
+    /** 通过节点数 */
+    private Integer passedNodeCnt;
+
+    /** 总节点数 */
+    private Integer totalNodeCnt;
+
+    /** 总耗时ms */
+    private Long durationMs;
+
+    /** RUNNING|SUCCESS|FAILED */
+    private String status;
+
+    /** 错误信息 */
+    private String errorMsg;
+
+    /** 生成的进化建议数 */
+    private Integer evolutionCount;
+
+    private String createBy;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 64 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterE2eRunNode.java

@@ -0,0 +1,64 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾E2E测试节点明细
+ */
+@Data
+@TableName("lobster_e2e_run_node")
+public class LobsterE2eRunNode {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关联 lobster_e2e_run.run_id */
+    private String runId;
+
+    /** 节点序号 */
+    private Integer nodeSeq;
+
+    /** 节点编码 */
+    private String nodeCode;
+
+    /** 节点类型 */
+    private String nodeType;
+
+    private String nodeName;
+
+    /** 单节点轮次(多轮对话) */
+    private Integer turnNo;
+
+    /** 用户输入 */
+    private String userInput;
+
+    /** AI输出 */
+    private String aiOutput;
+
+    /** 本节点本轮评分 */
+    private BigDecimal score;
+
+    /** 维度评分JSON */
+    private String scoreDetail;
+
+    private Long durationMs;
+
+    /** 使用的模型 */
+    private String modelUsed;
+
+    /** 进化建议草稿 */
+    private String evolutionHint;
+
+    /** 0=未达标 1=达标 */
+    private Integer passed;
+
+    private String errorMsg;
+
+    private LocalDateTime createTime;
+}

+ 17 - 7
fs-service/src/main/java/com/fs/company/domain/LobsterEvolutionLog.java

@@ -6,6 +6,9 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import java.time.LocalDateTime;
 
+/**
+ * 龙虾进化交互日志
+ */
 @Data
 @TableName("lobster_evolution_log")
 public class LobsterEvolutionLog {
@@ -13,13 +16,20 @@ public class LobsterEvolutionLog {
     private Long id;
     private Long companyId;
     private Long workflowId;
-    private String actionType;
+    private Long instanceId;
+    private Long contactId;
+    /** 通道类型: QW/WX/IM */
+    private String channelType;
     private String nodeCode;
-    private String beforeContent;
-    private String afterContent;
-    private String changeDesc;
-    private Double improvementRate;
-    private String status;
-    private LocalDateTime evolveTime;
+    /** 发送的消息 */
+    private String sentMessage;
+    /** 客户回复 */
+    private String customerReply;
+    /** 交互结果: purchase/inquiry/complaint/positive/negative/schedule/other */
+    private String outcome;
+    /** 变量快照 JSON */
+    private String variables;
+    /** 响应时长(毫秒) */
+    private Long durationMs;
     private LocalDateTime createTime;
 }

+ 16 - 5
fs-service/src/main/java/com/fs/company/domain/LobsterEvolutionSuggestion.java

@@ -6,6 +6,9 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import java.time.LocalDateTime;
 
+/**
+ * 龙虾进化优化建议
+ */
 @Data
 @TableName("lobster_evolution_suggestion")
 public class LobsterEvolutionSuggestion {
@@ -15,10 +18,18 @@ public class LobsterEvolutionSuggestion {
     private Long workflowId;
     private String nodeCode;
     private String suggestionType;
-    private String suggestionContent;
-    private String originalContent;
-    private Double confidenceScore;
-    private String status;
-    private String createdBy;
+    /** 当前内容(对应 DDL current_content) */
+    private String currentContent;
+    /** 建议内容(对应 DDL suggested_content) */
+    private String suggestedContent;
+    /** 置信度 */
+    private Double confidence;
+    /** 优化原因 */
+    private String reason;
+    /** 相关指标 JSON */
+    private String metrics;
+    /** 状态: 0待处理 1已应用 2已忽略 */
+    private Integer status;
+    private LocalDateTime applyTime;
     private LocalDateTime createTime;
 }

+ 6 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterNodeExecutionLog.java

@@ -21,6 +21,9 @@ public class LobsterNodeExecutionLog extends BaseEntity {
 
     private Long workflowId;
 
+    /** 节点编码(用于按节点维度统计) */
+    private String nodeCode;
+
     private Integer nodeIndex;
 
     private String nodeType;
@@ -43,5 +46,8 @@ public class LobsterNodeExecutionLog extends BaseEntity {
 
     private Integer retryCount;
 
+    /** 质量评分(0-100) */
+    private Integer qualityScore;
+
     private Integer delFlag;
 }

+ 58 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterTestScenario.java

@@ -0,0 +1,58 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾测试场景剧本(数据驱动回归)
+ */
+@Data
+@TableName("lobster_test_scenario")
+public class LobsterTestScenario {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    /** 场景名 */
+    private String scenarioName;
+
+    /** 业务描述(留空则用 template_id) */
+    private String businessDesc;
+
+    /** 关联工作流模板 */
+    private Long templateId;
+
+    /** 用户输入数组JSON */
+    private String userInputsJson;
+
+    /** 期望命中的节点编码JSON */
+    private String expectedNodes;
+
+    /** 最低通过分 */
+    private BigDecimal minScore;
+
+    /** 是否启用回归 */
+    private Integer enabled;
+
+    /** 自定义cron(空则跟随全局) */
+    private String cron;
+
+    private String lastRunId;
+
+    private String lastRunStatus;
+
+    private LocalDateTime lastRunTime;
+
+    private String createBy;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/tenant/TenantCompanyVoiceApi.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain.tenant;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 租户库 company_voice_api 表实体(仅用于租户数据源读写,非总后台主库)
+ */
+@Data
+public class TenantCompanyVoiceApi {
+
+    private Long apiId;
+
+    private String apiName;
+
+    /** 租户库为 varchar,与主库 int 类型区分存储 */
+    private String apiType;
+
+    private String apiJson;
+
+    private Integer status;
+
+    private String remark;
+
+    private Integer priority;
+
+    private Integer selectable;
+
+    private BigDecimal salePrice;
+
+    private Integer isPrimary;
+
+    private String apiUrl;
+
+    private String dialogUrl;
+
+    private Integer isDel;
+
+    private Date createTime;
+}

+ 2 - 1
fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java

@@ -131,7 +131,8 @@ public interface LobsterAuxiliaryMapper {
     int ensureE2eResultTable();
 
     // === lobster_test_scenario ===
-    List<Map<String, Object>> selectTestScenarios(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectTestScenarios(@Param("companyId") Long companyId,
+                                                   @Param("enabled") Integer enabled);
     Map<String, Object> selectTestScenarioById(@Param("id") Long id,
                                                 @Param("companyId") Long companyId);
     int insertTestScenario(@Param("companyId") Long companyId,

+ 28 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterE2eRunMapper.java

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.LobsterE2eRun;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾E2E测试运行Mapper
+ */
+public interface LobsterE2eRunMapper extends BaseMapper<LobsterE2eRun> {
+
+    int insertRun(LobsterE2eRun run);
+
+    LobsterE2eRun selectByRunId(@Param("runId") String runId);
+
+    List<LobsterE2eRun> selectByCompanyId(@Param("companyId") Long companyId);
+
+    int updateStatus(@Param("runId") String runId,
+                     @Param("status") String status,
+                     @Param("totalScore") java.math.BigDecimal totalScore,
+                     @Param("passedNodeCnt") Integer passedNodeCnt,
+                     @Param("totalNodeCnt") Integer totalNodeCnt,
+                     @Param("durationMs") Long durationMs,
+                     @Param("evolutionCount") Integer evolutionCount,
+                     @Param("errorMsg") String errorMsg);
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterE2eRunNodeMapper.java

@@ -0,0 +1,19 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.LobsterE2eRunNode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾E2E测试节点明细Mapper
+ */
+public interface LobsterE2eRunNodeMapper extends BaseMapper<LobsterE2eRunNode> {
+
+    int insertNode(LobsterE2eRunNode node);
+
+    int batchInsert(@Param("list") List<LobsterE2eRunNode> list);
+
+    List<LobsterE2eRunNode> selectByRunId(@Param("runId") String runId);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java

@@ -89,4 +89,7 @@ public interface LobsterEvolutionConfigMapper {
     Integer countPendingSuggestion(@Param("companyId") Long companyId);
     Integer countApplied(@Param("companyId") Long companyId);
     int ensureSuggestionTable();
+
+    List<Map<String, Object>> selectPendingSuggestions(@Param("companyId") Long companyId,
+                                                        @Param("minConfidence") Double minConfidence);
 }

+ 3 - 6
fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionSuggestionMapper.java

@@ -12,18 +12,15 @@ public interface LobsterEvolutionSuggestionMapper extends BaseMapper<LobsterEvol
     @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId}")
     Integer countByCompanyId(@Param("companyId") Long companyId);
 
-    @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND status = 'replied'")
-    Integer countRepliedByCompanyId(@Param("companyId") Long companyId);
-
-    @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND status = 'pending'")
+    @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND status = 0")
     Integer countPendingByCompanyId(@Param("companyId") Long companyId);
 
-    @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND status = 'applied'")
+    @Select("SELECT COUNT(*) FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND status = 1")
     Integer countAppliedByCompanyId(@Param("companyId") Long companyId);
 
     @Select("SELECT * FROM lobster_evolution_suggestion WHERE company_id = #{companyId} AND workflow_id = #{workflowId} ORDER BY create_time DESC")
     List<LobsterEvolutionSuggestion> selectByCompanyAndWorkflow(@Param("companyId") Long companyId, @Param("workflowId") Long workflowId);
 
-    @Update("UPDATE lobster_evolution_suggestion SET status = 'applied' WHERE id = #{id}")
+    @Update("UPDATE lobster_evolution_suggestion SET status = 1, apply_time = NOW() WHERE id = #{id}")
     int markApplied(@Param("id") Long id);
 }

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterTenantLearningMapper.java

@@ -85,6 +85,28 @@ public interface LobsterTenantLearningMapper {
                             @Param("enabled") Integer enabled);
     int deleteSensitiveWord(@Param("id") Long id, @Param("companyId") Long companyId);
 
+    /** 去重配置 CRUD */
+    List<Map<String, Object>> selectDedupConfigs(@Param("companyId") Long companyId);
+    int insertDedupConfig(@Param("companyId") Long companyId,
+                          @Param("configName") String configName,
+                          @Param("dedupMode") String dedupMode,
+                          @Param("exactWindowSize") Integer exactWindowSize,
+                          @Param("semanticThreshold") Double semanticThreshold,
+                          @Param("windowDurationSeconds") Integer windowDurationSeconds,
+                          @Param("ignorePrefixCount") Integer ignorePrefixCount,
+                          @Param("enabled") Integer enabled,
+                          @Param("remark") String remark);
+    int updateDedupConfig(@Param("id") Long id,
+                          @Param("configName") String configName,
+                          @Param("dedupMode") String dedupMode,
+                          @Param("exactWindowSize") Integer exactWindowSize,
+                          @Param("semanticThreshold") Double semanticThreshold,
+                          @Param("windowDurationSeconds") Integer windowDurationSeconds,
+                          @Param("ignorePrefixCount") Integer ignorePrefixCount,
+                          @Param("enabled") Integer enabled,
+                          @Param("remark") String remark);
+    int deleteDedupConfig(@Param("id") Long id, @Param("companyId") Long companyId);
+
     /** 分页查询 */
     List<Map<String, Object>> selectPaged(@Param("table") String table,
                                            @Param("companyId") Long companyId,

+ 23 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterTestScenarioMapper.java

@@ -0,0 +1,23 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.LobsterTestScenario;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾测试场景剧本Mapper
+ */
+public interface LobsterTestScenarioMapper extends BaseMapper<LobsterTestScenario> {
+
+    int insertScenario(LobsterTestScenario scenario);
+
+    LobsterTestScenario selectById(@Param("id") Long id);
+
+    List<LobsterTestScenario> selectEnabledByCompanyId(@Param("companyId") Long companyId);
+
+    int updateLastRun(@Param("id") Long id,
+                      @Param("lastRunId") String lastRunId,
+                      @Param("lastRunStatus") String lastRunStatus);
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/tenant/TenantCompanyVoiceApiMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper.tenant;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 租户库 company_voice_api 表 Mapper(仅在切换租户数据源后调用)
+ */
+public interface TenantCompanyVoiceApiMapper {
+
+    TenantCompanyVoiceApi selectByApiId(@Param("apiId") Long apiId);
+
+    List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query);
+
+    int upsertTenantVoiceApi(TenantCompanyVoiceApi row);
+
+    int updateStatusByApiId(@Param("apiId") Long apiId, @Param("status") Integer status);
+
+    int markDeletedByApiId(@Param("apiId") Long apiId);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySmsTempServiceImpl.java

@@ -1,7 +1,9 @@
 package com.fs.company.service.impl;
 
 import java.util.List;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.param.CompanySmsTempListQueryParam;
 import com.fs.company.vo.CompanySmsTempListQueryVO;
 import com.fs.company.vo.CompanySmsTempListVO;
@@ -56,6 +58,9 @@ public class CompanySmsTempServiceImpl implements ICompanySmsTempService
     @Override
     public int insertCompanySmsTemp(CompanySmsTemp companySmsTemp)
     {
+        if (StringUtils.isEmpty(companySmsTemp.getSmsApiIds())) {
+            throw new ServiceException("请选择绑定的短信接口");
+        }
         companySmsTemp.setCreateTime(DateUtils.getNowDate());
         return companySmsTempMapper.insertCompanySmsTemp(companySmsTemp);
     }

+ 39 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiServiceImpl.java

@@ -5,6 +5,9 @@ import com.fs.company.domain.CompanyVoiceApi;
 import com.fs.company.mapper.CompanyVoiceApiMapper;
 import com.fs.company.service.ICompanyVoiceApiService;
 import com.fs.company.service.ICompanyVoiceApiTenantService;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiSyncService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -20,12 +23,17 @@ import java.util.List;
 @Service
 public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService 
 {
+    private static final Logger log = LoggerFactory.getLogger(CompanyVoiceApiServiceImpl.class);
+
     @Autowired
     private CompanyVoiceApiMapper companyVoiceApiMapper;
 
     @Autowired
     private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
 
+    @Autowired
+    private ITenantCompanyVoiceApiSyncService tenantCompanyVoiceApiSyncService;
+
     /**
      * 查询呼叫接口
      * 
@@ -73,7 +81,9 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
     public int updateCompanyVoiceApi(CompanyVoiceApi companyVoiceApi)
     {
         prepareForSave(companyVoiceApi);
-        return companyVoiceApiMapper.updateCompanyVoiceApi(companyVoiceApi);
+        int rows = companyVoiceApiMapper.updateCompanyVoiceApi(companyVoiceApi);
+        syncMasterApiToAllTenantDbsQuietly(companyVoiceApi.getApiId());
+        return rows;
     }
 
     /** 保存前补全默认值(数据写入 account/password/api_url/dialog_url 等新字段,不使用 apiJson) */
@@ -105,7 +115,15 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
             return 0;
         }
         companyVoiceApiTenantService.disableTenantsByApiIds(apiIds);
-        return companyVoiceApiMapper.deleteCompanyVoiceApiByIds(apiIds);
+        int rows = companyVoiceApiMapper.deleteCompanyVoiceApiByIds(apiIds);
+        if (apiIds != null)
+        {
+            for (Long apiId : apiIds)
+            {
+                syncMasterApiToAllTenantDbsQuietly(apiId);
+            }
+        }
+        return rows;
     }
 
     /**
@@ -123,11 +141,29 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
             return 0;
         }
         companyVoiceApiTenantService.disableTenantsByApiId(apiId);
-        return companyVoiceApiMapper.deleteCompanyVoiceApiById(apiId);
+        int rows = companyVoiceApiMapper.deleteCompanyVoiceApiById(apiId);
+        syncMasterApiToAllTenantDbsQuietly(apiId);
+        return rows;
     }
 
     @Override
     public Integer selectCompanyVoiceApiCount() {
         return companyVoiceApiMapper.selectCompanyVoiceApiCount();
     }
+
+    private void syncMasterApiToAllTenantDbsQuietly(Long apiId)
+    {
+        if (tenantCompanyVoiceApiSyncService == null || apiId == null)
+        {
+            return;
+        }
+        try
+        {
+            tenantCompanyVoiceApiSyncService.syncMasterApiToAllTenantDbs(apiId);
+        }
+        catch (Exception e)
+        {
+            log.warn("[TenantVoiceApiSync] 主库接口变更同步租户库失败 apiId={}", apiId, e);
+        }
+    }
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 218
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java


+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -523,7 +523,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 .build();
         CommSmsSendContext sendContext = commSmsSendService.resolveSendContext(sendParam, robotic, callees);
         try {
-            commSmsSendService.validateSmsTempAndBalance(smsTempId, sendContext.getCompanyId());
+//            commSmsSendService.validateSmsTempAndBalance(smsTempId, sendContext.getCompanyId());
         } catch (ServiceException ex) {
             log.error("workflowSendSmsOneViaGateway 校验失败 roboticId={}, callerId={}, msg={}",
                     roboticId, callerId, ex.getMessage());

+ 15 - 0
fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiQueryService.java

@@ -0,0 +1,15 @@
+package com.fs.company.service.tenant;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+
+import java.util.List;
+
+/**
+ * 租户库 company_voice_api 查询(租户总后台列表/详情)
+ */
+public interface ITenantCompanyVoiceApiQueryService {
+
+    List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query);
+
+    TenantCompanyVoiceApi selectTenantCompanyVoiceApiById(Long apiId);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/company/service/tenant/ITenantCompanyVoiceApiSyncService.java

@@ -0,0 +1,29 @@
+package com.fs.company.service.tenant;
+
+import java.util.List;
+
+/**
+ * 总后台主库 → 租户库 company_voice_api 同步服务(仅操作租户数据源,非主库 CRUD)
+ */
+public interface ITenantCompanyVoiceApiSyncService {
+
+    /**
+     * 将主库接口定义 + 租户绑定关系同步到指定租户库
+     */
+    void syncBindingToTenantDb(Long tenantId, Long apiId);
+
+    /**
+     * 主库接口变更后,同步到所有已绑定该接口的租户库
+     */
+    void syncMasterApiToAllTenantDbs(Long apiId);
+
+    /**
+     * 解除绑定时,将租户库对应接口状态置为禁用
+     */
+    void disableInTenantDb(Long tenantId, Long apiId);
+
+    /**
+     * 批量同步绑定(定价/状态批量变更后)
+     */
+    void syncBindingsToTenantDb(List<Long> bindingIds);
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/service/tenant/ITenantMasterSmsApiQueryService.java

@@ -0,0 +1,16 @@
+package com.fs.company.service.tenant;
+
+import com.fs.proxy.domain.CompanySmsApiTenant;
+
+import java.util.List;
+
+/**
+ * 主库 company_sms_api_tenant 查询(租户总后台短信模板绑定下拉等)
+ */
+public interface ITenantMasterSmsApiQueryService {
+
+    /**
+     * 查询当前租户已绑定的短信接口(主库)
+     */
+    List<CompanySmsApiTenant> selectBoundSmsApisByTenantId(Long tenantId);
+}

+ 26 - 0
fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiQueryServiceImpl.java

@@ -0,0 +1,26 @@
+package com.fs.company.service.tenant.impl;
+
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import com.fs.company.mapper.tenant.TenantCompanyVoiceApiMapper;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiQueryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class TenantCompanyVoiceApiQueryServiceImpl implements ITenantCompanyVoiceApiQueryService {
+
+    @Autowired
+    private TenantCompanyVoiceApiMapper tenantCompanyVoiceApiMapper;
+
+    @Override
+    public List<TenantCompanyVoiceApi> selectTenantCompanyVoiceApiList(TenantCompanyVoiceApi query) {
+        return tenantCompanyVoiceApiMapper.selectTenantCompanyVoiceApiList(query);
+    }
+
+    @Override
+    public TenantCompanyVoiceApi selectTenantCompanyVoiceApiById(Long apiId) {
+        return tenantCompanyVoiceApiMapper.selectByApiId(apiId);
+    }
+}

+ 222 - 0
fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantCompanyVoiceApiSyncServiceImpl.java

@@ -0,0 +1,222 @@
+package com.fs.company.service.tenant.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.call.config.CallApiConfig;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.domain.tenant.TenantCompanyVoiceApi;
+import com.fs.company.mapper.CompanyVoiceApiMapper;
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
+import com.fs.company.mapper.tenant.TenantCompanyVoiceApiMapper;
+import com.fs.company.service.tenant.ITenantCompanyVoiceApiSyncService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 总后台主库外呼接口/绑定同步至租户库 company_voice_api。
+ * 内部通过 TenantDataSourceManager.ensureSwitchByTenantId 切换租户数据源。
+ */
+@Service
+public class TenantCompanyVoiceApiSyncServiceImpl implements ITenantCompanyVoiceApiSyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(TenantCompanyVoiceApiSyncServiceImpl.class);
+
+    @Autowired
+    private CompanyVoiceApiMapper companyVoiceApiMapper;
+
+    @Autowired
+    private CompanyVoiceApiTenantMapper companyVoiceApiTenantMapper;
+
+    @Autowired
+    private TenantCompanyVoiceApiMapper tenantCompanyVoiceApiMapper;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Override
+    public void syncBindingToTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        ensureMasterDataSource();
+        CompanyVoiceApi masterApi = companyVoiceApiMapper.selectCompanyVoiceApiById(apiId);
+        CompanyVoiceApiTenant binding = companyVoiceApiTenantMapper.selectByApiAndTenant(apiId, tenantId);
+        if (masterApi == null) {
+            markDeletedInTenantDb(tenantId, apiId);
+            return;
+        }
+        TenantCompanyVoiceApi tenantRow = buildTenantRow(masterApi, binding);
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.upsertTenantVoiceApi(tenantRow));
+    }
+
+    @Override
+    public void syncMasterApiToAllTenantDbs(Long apiId) {
+        if (apiId == null) {
+            return;
+        }
+        ensureMasterDataSource();
+        CompanyVoiceApi masterApi = companyVoiceApiMapper.selectCompanyVoiceApiById(apiId);
+        List<CompanyVoiceApiTenant> bindings = companyVoiceApiTenantMapper.selectTenantsByApiId(apiId);
+        if (bindings == null || bindings.isEmpty()) {
+            return;
+        }
+        if (masterApi == null) {
+            for (CompanyVoiceApiTenant binding : bindings) {
+                if (binding == null || binding.getTenantId() == null) {
+                    continue;
+                }
+                try {
+                    markDeletedInTenantDb(binding.getTenantId(), apiId);
+                } catch (Exception e) {
+                    log.error("[TenantVoiceApiSync] 主库接口已删除,同步禁用租户库失败 apiId={}, tenantId={}",
+                            apiId, binding.getTenantId(), e);
+                }
+            }
+            return;
+        }
+        for (CompanyVoiceApiTenant binding : bindings) {
+            if (binding == null || binding.getTenantId() == null) {
+                continue;
+            }
+            try {
+                TenantCompanyVoiceApi tenantRow = buildTenantRow(masterApi, binding);
+                runInTenantDb(binding.getTenantId(),
+                        () -> tenantCompanyVoiceApiMapper.upsertTenantVoiceApi(tenantRow));
+            } catch (Exception e) {
+                log.error("[TenantVoiceApiSync] 同步主库接口到租户库失败 apiId={}, tenantId={}",
+                        apiId, binding.getTenantId(), e);
+            }
+        }
+    }
+
+    @Override
+    public void disableInTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.updateStatusByApiId(apiId, 0));
+    }
+
+    /** 主库接口已删除时,租户库标记为删除(is_del=1) */
+    private void markDeletedInTenantDb(Long tenantId, Long apiId) {
+        if (tenantId == null || apiId == null) {
+            return;
+        }
+        runInTenantDb(tenantId, () -> tenantCompanyVoiceApiMapper.markDeletedByApiId(apiId));
+    }
+
+    @Override
+    public void syncBindingsToTenantDb(List<Long> bindingIds) {
+        if (bindingIds == null || bindingIds.isEmpty()) {
+            return;
+        }
+        ensureMasterDataSource();
+        Set<String> synced = new HashSet<>();
+        for (Long bindingId : bindingIds) {
+            if (bindingId == null) {
+                continue;
+            }
+            CompanyVoiceApiTenant binding = companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantById(bindingId);
+            if (binding == null || binding.getTenantId() == null || binding.getApiId() == null) {
+                continue;
+            }
+            String key = binding.getTenantId() + ":" + binding.getApiId();
+            if (!synced.add(key)) {
+                continue;
+            }
+            try {
+                syncBindingToTenantDb(binding.getTenantId(), binding.getApiId());
+            } catch (Exception e) {
+                log.error("[TenantVoiceApiSync] 批量同步绑定失败 bindingId={}", bindingId, e);
+            }
+        }
+    }
+
+    private TenantCompanyVoiceApi buildTenantRow(CompanyVoiceApi masterApi, CompanyVoiceApiTenant binding) {
+        TenantCompanyVoiceApi row = new TenantCompanyVoiceApi();
+        row.setApiId(masterApi.getApiId());
+        row.setApiName(masterApi.getApiName());
+        row.setApiType(masterApi.getApiType() != null ? String.valueOf(masterApi.getApiType()) : null);
+        row.setApiJson(buildApiJson(masterApi));
+        row.setApiUrl(masterApi.getApiUrl());
+        row.setDialogUrl(masterApi.getDialogUrl());
+        row.setRemark(masterApi.getRemark());
+        row.setIsDel(resolveIsDel(masterApi));
+        row.setStatus(resolveEffectiveStatus(masterApi, binding));
+        if (binding != null) {
+            row.setSalePrice(binding.getSalePrice());
+            row.setPriority(binding.getPriority() != null ? binding.getPriority() : 1);
+            row.setIsPrimary(binding.getIsPrimary() != null ? binding.getIsPrimary() : 0);
+            row.setSelectable(parseSelectable(binding.getSelectable()));
+        } else {
+            row.setPriority(1);
+            row.setIsPrimary(0);
+            row.setSelectable(0);
+        }
+        return row;
+    }
+
+    private int resolveIsDel(CompanyVoiceApi masterApi) {
+        if (masterApi == null || masterApi.getIsDel() == null) {
+            return 0;
+        }
+        return masterApi.getIsDel() == 1 ? 1 : 0;
+    }
+
+    private int resolveEffectiveStatus(CompanyVoiceApi masterApi, CompanyVoiceApiTenant binding) {
+        int masterStatus = masterApi.getStatus() != null && masterApi.getStatus() == 1 ? 1 : 0;
+        if (binding == null) {
+            return 0;
+        }
+        int bindingStatus = binding.getStatus() != null && binding.getStatus() == 1 ? 1 : 0;
+        return masterStatus == 1 && bindingStatus == 1 ? 1 : 0;
+    }
+
+    private String buildApiJson(CompanyVoiceApi masterApi) {
+        if (StringUtils.isNotEmpty(masterApi.getApiJson())) {
+            return masterApi.getApiJson();
+        }
+        if (StringUtils.isEmpty(masterApi.getAccount())
+                && StringUtils.isEmpty(masterApi.getPassword())
+                && StringUtils.isEmpty(masterApi.getApiUrl())) {
+            return null;
+        }
+        CallApiConfig config = new CallApiConfig();
+        config.setAccount(masterApi.getAccount());
+        config.setPassword(masterApi.getPassword());
+        config.setUrl(masterApi.getApiUrl());
+        return JSONUtil.toJsonStr(config);
+    }
+
+    private int parseSelectable(String selectable) {
+        if (selectable == null) {
+            return 0;
+        }
+        return "1".equals(selectable) || "true".equalsIgnoreCase(selectable) ? 1 : 0;
+    }
+
+    private void runInTenantDb(Long tenantId, Runnable action) {
+        ensureMasterDataSource();
+        try {
+            tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+            action.run();
+        } finally {
+            tenantDataSourceManager.clear();
+            ensureMasterDataSource();
+        }
+    }
+
+    private void ensureMasterDataSource() {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+    }
+}

+ 35 - 0
fs-service/src/main/java/com/fs/company/service/tenant/impl/TenantMasterSmsApiQueryServiceImpl.java

@@ -0,0 +1,35 @@
+package com.fs.company.service.tenant.impl;
+
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.service.tenant.ITenantMasterSmsApiQueryService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.proxy.domain.CompanySmsApiTenant;
+import com.fs.proxy.mapper.CompanySmsApiTenantMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class TenantMasterSmsApiQueryServiceImpl implements ITenantMasterSmsApiQueryService {
+
+    @Autowired
+    private CompanySmsApiTenantMapper companySmsApiTenantMapper;
+
+    @Override
+    public List<CompanySmsApiTenant> selectBoundSmsApisByTenantId(Long tenantId) {
+        if (tenantId == null) {
+            return Collections.emptyList();
+        }
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            CompanySmsApiTenant query = new CompanySmsApiTenant();
+            query.setTenantId(tenantId);
+            query.setStatus(1);
+            return companySmsApiTenantMapper.selectSmsApiTenantList(query);
+        } finally {
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+}

+ 153 - 0
fs-service/src/main/java/com/fs/company/service/workflow/capability/LobsterNodeCapabilityRegistry.java

@@ -0,0 +1,153 @@
+package com.fs.company.service.workflow.capability;
+
+import com.fs.company.enums.LobsterNodeTypeEnum;
+
+import java.util.*;
+
+/**
+ * 节点能力成熟度注册表(与 LobsterNodeTypeEnum 对齐)
+ * 星级 1-5:1=占位 2=stub 3=部分可用 4=生产可用 5=完整
+ */
+public final class LobsterNodeCapabilityRegistry {
+
+    private LobsterNodeCapabilityRegistry() {}
+
+    public enum ImplStatus { NONE, STUB, PARTIAL, FULL }
+
+    public static final class NodeCapability {
+        public final int code;
+        public final String codeName;
+        public final String name;
+        public final int maturityStars;
+        public final ImplStatus status;
+        public final String executor; // legacy | dynamic | both
+        public final String handler;
+        public final String gapNote;
+
+        public NodeCapability(int code, String codeName, String name, int maturityStars,
+                              ImplStatus status, String executor, String handler, String gapNote) {
+            this.code = code;
+            this.codeName = codeName;
+            this.name = name;
+            this.maturityStars = maturityStars;
+            this.status = status;
+            this.executor = executor;
+            this.handler = handler;
+            this.gapNote = gapNote;
+        }
+
+        public Map<String, Object> toMap() {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("code", code);
+            m.put("codeName", codeName);
+            m.put("name", name);
+            m.put("maturityStars", maturityStars);
+            m.put("status", status.name());
+            m.put("executor", executor);
+            m.put("handler", handler);
+            if (gapNote != null && !gapNote.isEmpty()) m.put("gapNote", gapNote);
+            return m;
+        }
+    }
+
+    private static final Map<Integer, NodeCapability> REGISTRY = new LinkedHashMap<>();
+
+    static {
+        reg(1, "start", 5, ImplStatus.FULL, "legacy", "handleStartNode", null);
+        reg(2, "message", 5, ImplStatus.FULL, "both", "handleMessageNode+evolve", null);
+        reg(3, "judgment", 4, ImplStatus.PARTIAL, "dynamic", "handleJudgmentNode", null);
+        reg(4, "wait", 4, ImplStatus.PARTIAL, "dynamic", "handleWaitNode", null);
+        reg(5, "end", 5, ImplStatus.FULL, "dynamic", "handleEndNode", null);
+        reg(6, "promotion_end", 4, ImplStatus.PARTIAL, "dynamic", "handlePromotionEndNode", null);
+        reg(7, "order_success", 3, ImplStatus.PARTIAL, "dynamic", "handleOrderSuccessNode", "不含真实支付闭环");
+        reg(8, "order_confirm", 4, ImplStatus.PARTIAL, "dynamic", "handleOrderConfirmNode", null);
+        reg(9, "tag_operation", 4, ImplStatus.PARTIAL, "dynamic", "handleTagOperationNode", null);
+        reg(10, "care", 4, ImplStatus.FULL, "dynamic", "handleCareNode", null);
+        reg(11, "survey", 4, ImplStatus.FULL, "dynamic", "handleSurveyNode", null);
+        reg(12, "profile_update", 4, ImplStatus.PARTIAL, "dynamic", "handleUserProfileNode", null);
+        reg(13, "repurchase", 4, ImplStatus.FULL, "dynamic", "handleRepurchaseNode", null);
+        reg(14, "smart_api", 4, ImplStatus.PARTIAL, "dynamic", "handleSmartApiNode", null);
+        reg(20, "intent_recognition", 4, ImplStatus.FULL, "dynamic", "handleIntentRecognitionNode", null);
+        reg(21, "takeover_detect", 4, ImplStatus.FULL, "dynamic", "handleTakeoverDetectNode", null);
+        reg(22, "quality_check", 5, ImplStatus.FULL, "dynamic", "handleQualityCheckNode", null);
+        reg(23, "knowledge_retrieval", 4, ImplStatus.PARTIAL, "dynamic", "handleKnowledgeRetrievalNode", null);
+        reg(24, "product_recommend", 4, ImplStatus.PARTIAL, "dynamic", "handleProductRecommendNode", null);
+        reg(25, "tag_match", 4, ImplStatus.PARTIAL, "dynamic", "handleTagMatchNode", null);
+        reg(30, "qw_message", 4, ImplStatus.FULL, "dynamic", "handleQwMessageNode", null);
+        reg(31, "im_message", 4, ImplStatus.FULL, "dynamic", "handleImMessageNode", null);
+        reg(32, "timed_delay", 4, ImplStatus.PARTIAL, "dynamic", "handleTimedDelayNode", null);
+        reg(33, "ai_chat", 5, ImplStatus.FULL, "both", "handleAiChatNode+evolve", null);
+        reg(34, "sms_message", 4, ImplStatus.PARTIAL, "dynamic", "handleSmsMessageNode", null);
+        reg(35, "email_message", 3, ImplStatus.PARTIAL, "dynamic", "handleEmailMessageNode", "邮件仅落库日志");
+        reg(40, "variable_assign", 4, ImplStatus.FULL, "dynamic", "handleVariableAssignNode", null);
+        reg(41, "add_tag", 4, ImplStatus.PARTIAL, "dynamic", "handleAddTagNode", null);
+        reg(42, "webhook", 4, ImplStatus.PARTIAL, "dynamic", "handleWebhookNode", null);
+        reg(43, "sub_workflow", 4, ImplStatus.PARTIAL, "dynamic", "handleSubWorkflowNode", null);
+        reg(44, "create_task", 4, ImplStatus.PARTIAL, "dynamic", "handleCreateTaskNode", null);
+        reg(50, "sop_execute", 4, ImplStatus.PARTIAL, "dynamic", "handleSopExecuteNode", null);
+        reg(51, "cid_task", 4, ImplStatus.PARTIAL, "dynamic", "handleCidTaskNode", null);
+        reg(52, "product_push", 4, ImplStatus.PARTIAL, "dynamic", "handleProductPushNode", null);
+        reg(53, "logistics_notify", 4, ImplStatus.PARTIAL, "dynamic", "handleLogisticsNotifyNode", null);
+        reg(100, "external_api", 4, ImplStatus.PARTIAL, "dynamic", "handleExternalApiNode", null);
+        reg(200, "custom_1", 3, ImplStatus.PARTIAL, "dynamic", "handleCustomNode", "配置驱动+AI兜底");
+        reg(201, "custom_2", 3, ImplStatus.PARTIAL, "dynamic", "handleCustomNode", "配置驱动+AI兜底");
+    }
+
+    private static void reg(int code, String codeName, int stars, ImplStatus status,
+                            String executor, String handler, String gap) {
+        LobsterNodeTypeEnum e = LobsterNodeTypeEnum.fromCode(code);
+        REGISTRY.put(code, new NodeCapability(code, codeName, e.getName(), stars, status, executor, handler, gap));
+    }
+
+    public static NodeCapability get(int code) {
+        return REGISTRY.get(code);
+    }
+
+    public static List<NodeCapability> all() {
+        return new ArrayList<>(REGISTRY.values());
+    }
+
+    public static List<NodeCapability> belowStars(int minStarsExclusive) {
+        List<NodeCapability> list = new ArrayList<>();
+        for (NodeCapability c : REGISTRY.values()) {
+            if (c.maturityStars <= minStarsExclusive) list.add(c);
+        }
+        return list;
+    }
+
+    public static Set<Integer> dynamicExecutorTypes() {
+        Set<Integer> set = new HashSet<>();
+        for (NodeCapability c : REGISTRY.values()) {
+            if ("dynamic".equals(c.executor) || "both".equals(c.executor)) {
+                if (c.code != 2 && c.code != 3 && c.code != 4 && c.code != 5 && c.code != 1) {
+                    set.add(c.code);
+                }
+            }
+        }
+        return set;
+    }
+
+    public static Map<String, Object> summary() {
+        Map<String, Object> s = new LinkedHashMap<>();
+        int total = REGISTRY.size();
+        int full = 0, partial = 0, stub = 0, none = 0;
+        int starSum = 0;
+        for (NodeCapability c : REGISTRY.values()) {
+            starSum += c.maturityStars;
+            switch (c.status) {
+                case FULL: full++; break;
+                case PARTIAL: partial++; break;
+                case STUB: stub++; break;
+                default: none++;
+            }
+        }
+        s.put("totalTypes", total);
+        s.put("avgMaturityStars", String.format("%.1f", starSum * 1.0 / total));
+        s.put("fullCount", full);
+        s.put("partialCount", partial);
+        s.put("stubCount", stub);
+        s.put("noneCount", none);
+        s.put("below4Stars", belowStars(3).size());
+        return s;
+    }
+}

+ 13 - 8
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java

@@ -33,14 +33,19 @@ public class DouyinDmMessageChannel implements MessageChannel {
     @Override
     public MessageChannelResult sendMessage(MessageChannelRequest request) {
         try {
-            if (apiRegistryService != null && apiRegistryService.get("douyin_dm") != null) {
-                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("douyin_dm");
-                HttpHeaders headers = new HttpHeaders();
-                headers.setContentType(MediaType.APPLICATION_JSON);
-                headers.set("access-token", getAccessToken(request.getCompanyId()));
-                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
-                HttpEntity<String> entity = new HttpEntity<>(body, headers);
-                restTemplate.postForEntity(ep.baseUrl + "/im/send/", entity, String.class);
+            if (apiRegistryService == null || apiRegistryService.get("douyin_dm") == null) {
+                log.warn("[DOUYIN_DM] 通道未配置 douyin_dm API,拒绝发送");
+                return MessageChannelResult.fail(CHANNEL_TYPE, "抖音私信通道未配置,请在 API 注册中心配置 douyin_dm");
+            }
+            ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("douyin_dm");
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.set("access-token", getAccessToken(request.getCompanyId()));
+            String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+            HttpEntity<String> entity = new HttpEntity<>(body, headers);
+            ResponseEntity<String> resp = restTemplate.postForEntity(ep.baseUrl + "/im/send/", entity, String.class);
+            if (!resp.getStatusCode().is2xxSuccessful()) {
+                return MessageChannelResult.fail(CHANNEL_TYPE, "抖音发送失败: HTTP " + resp.getStatusCodeValue());
             }
             log.info("[DOUYIN_DM] 消息发送: to={}", request.getChannelUserId());
             return MessageChannelResult.ok(CHANNEL_TYPE, "dy_" + System.currentTimeMillis());

+ 14 - 11
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java

@@ -35,21 +35,24 @@ public class TmallMessageChannel implements MessageChannel {
     @Override
     public MessageChannelResult sendMessage(MessageChannelRequest request) {
         try {
+            if (apiRegistryService == null || apiRegistryService.get("tmall_msg") == null) {
+                log.warn("[TMALL] 通道未配置 tmall_msg API,拒绝发送");
+                return MessageChannelResult.fail(CHANNEL_TYPE, "天猫消息通道未配置,请在 API 注册中心配置 tmall_msg");
+            }
             String cfgJson = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
             String token = extractToken(cfgJson);
 
-            if (apiRegistryService != null && apiRegistryService.get("tmall_msg") != null) {
-                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("tmall_msg");
-                HttpHeaders headers = new HttpHeaders();
-                headers.setContentType(MediaType.APPLICATION_JSON);
-                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
-                String body = "{\"toUser\":\"" + request.getChannelUserId() + "\",\"msgType\":\"text\",\"text\":{\"content\":\"" + escape(request.getContent()) + "\"}}";
-                HttpEntity<String> entity = new HttpEntity<>(body, headers);
-                ResponseEntity<String> resp = restTemplate.postForEntity(ep.baseUrl + "/message/send", entity, String.class);
-                log.info("[TMALL] 发送完成: httpStatus={}", resp.getStatusCode());
-            } else {
-                log.info("[TMALL][降级] 消息已记录: to={}, preview={}", request.getChannelUserId(), truncate(request.getContent()));
+            ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("tmall_msg");
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+            String body = "{\"toUser\":\"" + request.getChannelUserId() + "\",\"msgType\":\"text\",\"text\":{\"content\":\"" + escape(request.getContent()) + "\"}}";
+            HttpEntity<String> entity = new HttpEntity<>(body, headers);
+            ResponseEntity<String> resp = restTemplate.postForEntity(ep.baseUrl + "/message/send", entity, String.class);
+            if (!resp.getStatusCode().is2xxSuccessful()) {
+                return MessageChannelResult.fail(CHANNEL_TYPE, "天猫发送失败: HTTP " + resp.getStatusCodeValue());
             }
+            log.info("[TMALL] 发送完成: httpStatus={}", resp.getStatusCode());
             return MessageChannelResult.ok(CHANNEL_TYPE, "tmall_" + System.currentTimeMillis());
         } catch (Exception e) {
             return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());

+ 32 - 0
fs-service/src/main/java/com/fs/company/service/workflow/config/LobsterCompanyConfigService.java

@@ -85,6 +85,37 @@ public class LobsterCompanyConfigService {
         configMapper.deleteSensitiveWord(id, companyId);
     }
 
+    public List<Map<String, Object>> listDedup(Long companyId) {
+        return configMapper != null ? configMapper.selectDedupConfigs(companyId) : new ArrayList<>();
+    }
+
+    public Map<String, Object> saveDedup(Map<String, Object> data) {
+        if (configMapper == null) return data;
+        Long companyId = toLong(data.get("companyId"));
+        String configName = (String) data.getOrDefault("configName", "");
+        String dedupMode = (String) data.getOrDefault("dedupMode", "hybrid");
+        Integer exactWindowSize = toInt(data.get("exactWindowSize"), 5);
+        Double semanticThreshold = data.get("semanticThreshold") instanceof Number
+                ? ((Number) data.get("semanticThreshold")).doubleValue() : 0.85;
+        Integer windowDurationSeconds = toInt(data.get("windowDurationSeconds"), 300);
+        Integer ignorePrefixCount = toInt(data.get("ignorePrefixCount"), 0);
+        Integer enabled = data.get("enabled") != null ? Integer.valueOf(data.get("enabled").toString()) : 1;
+        String remark = (String) data.getOrDefault("remark", "");
+        if (data.get("id") == null) {
+            configMapper.insertDedupConfig(companyId, configName, dedupMode, exactWindowSize,
+                    semanticThreshold, windowDurationSeconds, ignorePrefixCount, enabled, remark);
+        } else {
+            configMapper.updateDedupConfig(toLong(data.get("id")), configName, dedupMode, exactWindowSize,
+                    semanticThreshold, windowDurationSeconds, ignorePrefixCount, enabled, remark);
+        }
+        return data;
+    }
+
+    public void deleteDedup(Long id, Long companyId) {
+        if (configMapper == null) return;
+        configMapper.deleteDedupConfig(id, companyId);
+    }
+
     public Map<String, Object> checkSensitive(Long companyId, String content) {
         Map<String, Object> result = new HashMap<>();
         result.put("hits", new ArrayList<>());
@@ -114,4 +145,5 @@ public class LobsterCompanyConfigService {
     }
 
     private Long toLong(Object v) { return v instanceof Number ? ((Number) v).longValue() : null; }
+    private int toInt(Object v, int def) { return v instanceof Number ? ((Number) v).intValue() : def; }
 }

+ 0 - 16
fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java

@@ -53,20 +53,4 @@ public class ContactInfo {
         return info;
     }
 
-    public Long getCompanyId() { return companyId; }
-    public void setCompanyId(Long companyId) { this.companyId = companyId; }
-    public Long getContactId() { return contactId; }
-    public void setContactId(Long contactId) { this.contactId = contactId; }
-    public String getChannelType() { return channelType; }
-    public void setChannelType(String channelType) { this.channelType = channelType; }
-    public String getChannelUserId() { return channelUserId; }
-    public void setChannelUserId(String channelUserId) { this.channelUserId = channelUserId; }
-    public String getName() { return name; }
-    public void setName(String name) { this.name = name; }
-    public String getAvatar() { return avatar; }
-    public void setAvatar(String avatar) { this.avatar = avatar; }
-    public String getPhone() { return phone; }
-    public void setPhone(String phone) { this.phone = phone; }
-    public Map<String, Object> getExtra() { return extra; }
-    public void setExtra(Map<String, Object> extra) { this.extra = extra; }
 }

+ 36 - 0
fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionEngineImpl.java

@@ -115,6 +115,42 @@ public class EvolutionEngineImpl implements EvolutionEngine {
         }
     }
 
+    /**
+     * 自动应用高置信度且开启 auto_apply 的待审建议
+     */
+    public int autoApplyHighConfidenceSuggestions(Long companyId, double minConfidence) {
+        if (evolutionConfigMapper == null || companyId == null) return 0;
+        int applied = 0;
+        try {
+            List<Map<String, Object>> pending = evolutionConfigMapper.selectPendingSuggestions(companyId, minConfidence);
+            if (pending == null || pending.isEmpty()) return 0;
+            for (Map<String, Object> row : pending) {
+                Object idObj = row.get("id");
+                if (!(idObj instanceof Number)) continue;
+                Long suggestionId = ((Number) idObj).longValue();
+                Long workflowId = row.get("workflow_id") instanceof Number
+                        ? ((Number) row.get("workflow_id")).longValue() : null;
+                String nodeCode = (String) row.get("node_code");
+                if (workflowId == null || nodeCode == null) continue;
+                Map<String, Object> cfg = evolutionConfigMapper.selectConfig(companyId, workflowId, nodeCode);
+                if (!isAutoApplyEnabled(cfg)) continue;
+                applySuggestion(companyId, suggestionId);
+                applied++;
+            }
+        } catch (Exception e) {
+            log.warn("自动应用优化建议失败: companyId={}", companyId, e);
+        }
+        return applied;
+    }
+
+    private boolean isAutoApplyEnabled(Map<String, Object> cfg) {
+        if (cfg == null) return false;
+        Object autoApply = cfg.get("auto_apply");
+        if (autoApply == null) return false;
+        String v = autoApply.toString();
+        return "1".equals(v) || "true".equalsIgnoreCase(v) || "yes".equalsIgnoreCase(v);
+    }
+
     @Override
     public Map<String, Object> getEvolutionMetrics(Long companyId) {
         Map<String, Object> metrics = new HashMap<>();

+ 4 - 0
fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionSchedulerImpl.java

@@ -46,6 +46,10 @@ public class EvolutionSchedulerImpl {
                     // 触发一次进化分析(生成优化建议)
                     evolutionEngine.analyzeAndSuggest(tenantId, null);
                     analyzed++;
+                    int autoApplied = evolutionEngine.autoApplyHighConfidenceSuggestions(tenantId, 75.0);
+                    if (autoApplied > 0) {
+                        log.info("[EvolutionScheduler] 租户 {} 自动应用 {} 条优化建议", tenantId, autoApplied);
+                    }
                     // 收集当前指标后触发用户级优化
                     Map<String, Object> metrics = evolutionEngine.getEvolutionMetrics(tenantId);
                     if (metrics.get("pendingSuggestions") instanceof Number

+ 117 - 19
fs-service/src/main/java/com/fs/company/service/workflow/heartbeat/impl/HeartbeatSchedulerImpl.java

@@ -10,6 +10,7 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 @Service
 public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
@@ -19,6 +20,12 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
+    /** 活跃实例心跳配置缓存: instanceId → HeartbeatConfig */
+    private final Map<Long, HeartbeatConfig> activeInstances = new ConcurrentHashMap<>();
+
+    /** 实例最后心跳时间缓存: instanceId → lastHeartbeatTime */
+    private final Map<Long, Long> lastHeartbeatTimes = new ConcurrentHashMap<>();
+
     /** 每5分钟发送心跳 */
     @Scheduled(cron = "0 */5 * * * ?")
     public void sendHeartbeat() {
@@ -28,6 +35,113 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
         } catch (Exception e) { log.warn("[Heartbeat] 心跳发送失败: {}", e.getMessage()); }
     }
 
+    @Override
+    public void registerInstance(Long companyId, Long instanceId, HeartbeatConfig config) {
+        if (instanceId == null) return;
+        activeInstances.put(instanceId, config);
+        lastHeartbeatTimes.put(instanceId, System.currentTimeMillis());
+        if (auxMapper != null) {
+            try {
+                auxMapper.insertHeartbeat(companyId, "instance_" + instanceId, "registered");
+            } catch (Exception e) { log.debug("[Heartbeat] 注册实例心跳记录失败: {}", e.getMessage()); }
+        }
+        log.info("[Heartbeat] 工作流实例已注册: instanceId={}, timeoutMs={}", instanceId,
+                config != null ? config.getTimeoutMs() : 86400000L);
+    }
+
+    @Override
+    public void unregisterInstance(Long instanceId) {
+        activeInstances.remove(instanceId);
+        lastHeartbeatTimes.remove(instanceId);
+        log.info("[Heartbeat] 工作流实例已注销: instanceId={}", instanceId);
+    }
+
+    @Override
+    public void checkAndExecute() {
+        long now = System.currentTimeMillis();
+        List<Long> timeoutInstances = new ArrayList<>();
+
+        for (Map.Entry<Long, Long> entry : lastHeartbeatTimes.entrySet()) {
+            Long instanceId = entry.getKey();
+            Long lastHb = entry.getValue();
+            HeartbeatConfig config = activeInstances.get(instanceId);
+            long timeoutMs = (config != null ? config.getTimeoutMs() : 86400000L);
+
+            if (now - lastHb > timeoutMs) {
+
+                timeoutInstances.add(instanceId);
+                log.warn("[Heartbeat] 实例心跳超时: instanceId={}, lastHb={}, timeoutMs={}",
+                        instanceId, new Date(lastHb), timeoutMs);
+            }
+        }
+
+        // 清理超时实例
+        for (Long instanceId : timeoutInstances) {
+            unregisterInstance(instanceId);
+        }
+
+        if (!timeoutInstances.isEmpty()) {
+            log.info("[Heartbeat] 清理超时实例: {} 个", timeoutInstances.size());
+        }
+    }
+
+    @Override
+    public Map<String, Object> getHeartbeatStatus(Long instanceId) {
+        Map<String, Object> status = new LinkedHashMap<>();
+        status.put("instanceId", instanceId);
+
+        if (instanceId == null) {
+            status.put("status", "unknown");
+            status.put("active", false);
+            status.put("message", "实例ID为空");
+            return status;
+        }
+
+        boolean isRegistered = activeInstances.containsKey(instanceId);
+        Long lastHb = lastHeartbeatTimes.get(instanceId);
+
+        status.put("registered", isRegistered);
+
+        if (isRegistered && lastHb != null) {
+            long now = System.currentTimeMillis();
+            HeartbeatConfig config = activeInstances.get(instanceId);
+            long timeoutMs = (config != null ? config.getTimeoutMs() : 86400000L);
+            long elapsed = now - lastHb;
+
+            status.put("lastHeartbeatTime", new Date(lastHb).toString());
+            status.put("lastHeartbeatTimestamp", lastHb);
+            status.put("elapsedMinutes", elapsed / 60000);
+            status.put("timeoutMinutes", timeoutMs / 60000);
+            status.put("active", elapsed < timeoutMs);
+            status.put("status", elapsed < timeoutMs ? "active" : "timeout");
+
+            if (elapsed >= timeoutMs) {
+                status.put("message", "实例心跳已超时 " + (elapsed / 60000) + " 分钟");
+            } else {
+                status.put("message", "实例心跳正常");
+            }
+        } else {
+            status.put("active", false);
+            status.put("status", "unregistered");
+            status.put("message", "实例未注册或已注销");
+
+            // 尝试从数据库查询历史心跳记录
+            if (auxMapper != null) {
+                try {
+                    List<Map<String, Object>> records = auxMapper.selectHeartbeats(0L,
+                            "instance_" + instanceId, 5);
+                    if (records != null && !records.isEmpty()) {
+                        status.put("status", "history_only");
+                        status.put("message", "实例未在内存中注册,但存在历史心跳记录");
+                        status.put("historyRecords", records.size());
+                    }
+                } catch (Exception e) { log.debug("[Heartbeat] 查询历史心跳失败: {}", e.getMessage()); }
+            }
+        }
+
+        return status;
+    }
+
     /** 每15分钟清理过期心跳(1小时前) */
     @Scheduled(cron = "0 */15 * * * ?")
     public void cleanupExpiredHeartbeats() {
@@ -35,6 +149,9 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
         try {
             log.debug("[Heartbeat] 清理过期心跳记录");
         } catch (Exception e) { log.warn("[Heartbeat] 清理失败: {}", e.getMessage()); }
+
+        // 同时执行超时检查
+        checkAndExecute();
     }
 
     public void recordHeartbeat(Long companyId, String taskKey, String status) {
@@ -52,23 +169,4 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
         auxMapper.updateHeartbeat(companyId, taskKey, status);
     }
 
-    @Override
-    public void registerInstance(Long companyId, Long instanceId, HeartbeatConfig config) {
-
-    }
-
-    @Override
-    public void unregisterInstance(Long instanceId) {
-
-    }
-
-    @Override
-    public void checkAndExecute() {
-
-    }
-
-    @Override
-    public Map<String, Object> getHeartbeatStatus(Long instanceId) {
-        return Map.of();
-    }
 }

+ 171 - 11
fs-service/src/main/java/com/fs/company/service/workflow/identity/impl/IdentityHidingServiceImpl.java

@@ -7,8 +7,16 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
 
+/**
+ * 机器人身份隐藏服务实现
+ *
+ * 核心能力:确保龙虾引擎在沟通过程中不暴露机器人身份,
+ * 让客户感觉是在和真人沟通
+ */
 @Service
 public class IdentityHidingServiceImpl implements IdentityHidingService {
 
@@ -17,43 +25,195 @@ public class IdentityHidingServiceImpl implements IdentityHidingService {
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
-    public String getSafeHandoffMessage(String defaultMsg) { return defaultMsg != null ? defaultMsg : "正在为您转接人工客服..."; }
-    public String getSafeSystemPrompt() { return "你是专业的销售顾问"; }
-    public String getSafeErrorMessage(Object ctx, String code) { return "系统繁忙,请稍后再试"; }
-    public String hideIdentity(String text) { return text; }
+    /** 暴露机器人身份的关键词模式 */
+    private static final List<Pattern> IDENTITY_LEAK_PATTERNS = Arrays.asList(
+        Pattern.compile("作为.{0,3}(AI|人工智能|机器人|语言模型|大模型)"),
+        Pattern.compile("我是.{0,3}(AI|人工智能|机器人|语言模型|助手|大模型)"),
+        Pattern.compile("(系统|程序|算法|模型).{0,3}(自动|生成|回复)"),
+        Pattern.compile("抱歉.{0,5}(无法理解|不能回答|无法处理)"),
+        Pattern.compile("请稍等.{0,5}(处理|查询|核实|确认)"),
+        Pattern.compile("(AI|人工智能|机器人|系统)"),
+        Pattern.compile("(自动|程序化|机械)")
+    );
+
+    /** 替换话术模板 */
+    private static final String[] HUMAN_FILLERS = {
+        "我看看", "让我想想", "这个嘛", "嗯,", "其实呢,",
+        "我帮您看一下", "稍等一下哈", "好的呢", "没问题~"
+    };
+
+    /** 租户级配置缓存 */
+    private final Map<Long, Map<String, Object>> configCache = new ConcurrentHashMap<>();
 
     @Override
     public String hideIdentity(Long companyId, String message, Map<String, Object> context) {
-        return "";
+        if (message == null || message.isEmpty()) return message;
+
+        String result = message;
+
+        // 1. 替换暴露身份的关键词
+        for (Pattern pattern : IDENTITY_LEAK_PATTERNS) {
+            result = pattern.matcher(result).replaceAll("我");
+        }
+
+        // 2. 替换AI常见套话
+        result = result.replace("根据我的理解", "依我看")
+                       .replace("根据数据分析", "了解后发现")
+                       .replace("基于以上信息", "总的来说")
+                       .replace("为您生成以下回复", "")
+                       .replace("以下是回复内容", "");
+
+        // 3. 清理残留AI标签
+        result = result.replace("[AI]", "").replace("[自动]", "").replace("[系统]", "");
+
+        // 4. 确保回复不为空
+        if (result.trim().isEmpty()) {
+            result = getSafeErrorMessage(companyId, "empty_reply");
+        }
+
+        return result.trim();
     }
 
     @Override
     public String getSafeSystemPrompt(Long companyId) {
-        return "";
+        // 优先从租户配置获取
+        Map<String, Object> config = getIdentityConfig(companyId);
+        if (config != null && config.containsKey("systemRole")) {
+            return (String) config.get("systemRole");
+        }
+
+        // 从数据库获取
+        if (auxMapper != null) {
+            try {
+                List<Map<String, Object>> prompts = auxMapper.selectDynamicImpls(companyId, 99);
+                if (prompts != null && !prompts.isEmpty()) {
+                    String role = (String) prompts.get(0).get("script_content");
+                    if (role != null && !role.isEmpty()) return role;
+                }
+            } catch (Exception e) {
+                logger.debug("[IdentityHide] 加载系统角色失败: {}", e.getMessage());
+            }
+        }
+
+        // 默认兜底角色
+        return "你是一位经验丰富的专业顾问,请用自然口语化的方式与客户沟通。"
+             + "不要使用'作为AI'、'根据算法'、'系统分析'等暴露非真人身份的措辞。"
+             + "回复要像真人一样有停顿、有思考、有情感。"
+             + "碰到不确定的问题时,说'我帮您确认一下'而不是'我无法回答'。";
     }
 
     @Override
     public String getSafeHandoffMessage(Long companyId, String urgency) {
-        return "";
+        if (urgency == null) urgency = "medium";
+
+        // 根据紧急程度选择不同话术
+        switch (urgency.toLowerCase()) {
+            case "high":
+            case "urgent":
+                return "您稍等,我马上帮您联系更专业的人来处理这个问题。";
+            case "low":
+                return "这个我帮您记下了,后续有专人跟进。";
+            case "medium":
+            default:
+                return "您这个问题很专业,我帮您安排专属顾问来对接,稍等一下~";
+        }
     }
 
     @Override
     public String getSafeErrorMessage(Long companyId, String errorType) {
-        return "";
+        if (errorType == null) errorType = "unknown";
+
+        switch (errorType.toLowerCase()) {
+            case "timeout":
+                return "刚才有点卡,麻烦您再说一遍?";
+            case "overload":
+                return "这会儿咨询的人有点多,我缓一下马上回复您~";
+            case "empty_reply":
+                return "嗯,我再确认一下具体情况。";
+            case "unknown":
+            default:
+                return "不好意思,我需要确认一下,稍等哈~";
+        }
     }
 
     @Override
     public long calculateHumanLikeDelay(String message, Map<String, Object> context) {
-        return 0;
+        if (message == null || message.isEmpty()) return 1000L;
+
+        int length = message.length();
+        // 模拟真人打字速度:中文约3字/秒,加上思考时间
+        long baseDelay = length * 300L; // 每字符300ms
+
+        // 消息越长,思考时间越多
+        if (length > 100) baseDelay += 2000L;  // 长回复,多思考
+        else if (length > 50) baseDelay += 1000L;
+        else baseDelay += 500L;
+
+        // 随机波动 ±30%,避免过于机械
+        double jitter = 0.7 + Math.random() * 0.6;
+        long result = (long) (baseDelay * jitter);
+
+        // 限制在1-10秒之间
+        return Math.max(1000L, Math.min(10000L, result));
     }
 
     @Override
     public Map<String, Object> getIdentityConfig(Long companyId) {
-        return Map.of();
+        // 先从缓存获取
+        Map<String, Object> cached = configCache.get(companyId);
+        if (cached != null) return cached;
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        // 默认配置
+        config.put("hideIdentity", true);
+        config.put("systemRole", null);
+        config.put("humanLikeDelay", true);
+        config.put("delayMinMs", 1000);
+        config.put("delayMaxMs", 8000);
+        config.put("useFillers", true);
+        config.put("strictMode", false);
+
+        // 从数据库加载
+        if (auxMapper != null) {
+            try {
+                List<Map<String, Object>> rows = auxMapper.selectDynamicImpls(companyId, 98);
+                if (rows != null && !rows.isEmpty()) {
+                    String content = (String) rows.get(0).get("script_content");
+                    if (content != null && !content.isEmpty()) {
+                        try {
+                            @SuppressWarnings("unchecked")
+                            Map<String, Object> dbConfig =
+                                com.alibaba.fastjson.JSON.parseObject(content, Map.class);
+                            if (dbConfig != null) config.putAll(dbConfig);
+                        } catch (Exception ignored) {}
+                    }
+                }
+            } catch (Exception e) {
+                logger.debug("[IdentityHide] 加载配置失败: {}", e.getMessage());
+            }
+        }
+
+        configCache.put(companyId, config);
+        return config;
     }
 
     @Override
     public void updateIdentityConfig(Long companyId, Map<String, Object> config) {
+        if (config == null) return;
+        // 更新缓存
+        Map<String, Object> existing = getIdentityConfig(companyId);
+        existing.putAll(config);
+        configCache.put(companyId, existing);
 
+        // 持久化到数据库
+        if (auxMapper != null) {
+            try {
+                String json = com.alibaba.fastjson.JSON.toJSONString(existing);
+                auxMapper.insertDynamicImpl(companyId, 98, "identity_config", "identity_config",
+                        json, "ACTIVE");
+            } catch (Exception e) {
+                logger.error("[IdentityHide] 保存配置失败: {}", e.getMessage());
+            }
+        }
     }
 }

+ 232 - 7
fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeAdjusterImpl.java

@@ -1,41 +1,266 @@
 package com.fs.company.service.workflow.impl;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import com.fs.company.domain.LobsterWorkflowInstance;
+import com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper;
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
 import com.fs.company.service.workflow.DynamicNodeAdjuster;
+import com.fs.company.service.workflow.LobsterEvolutionEngine;
+import com.fs.company.service.workflow.SemanticTakeoverDetector;
+import com.fs.company.service.workflow.semantic.SemanticAnalyzer;
+import com.fs.company.service.workflow.semantic.SemanticResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 @Service
 public class DynamicNodeAdjusterImpl implements DynamicNodeAdjuster {
 
     private static final Logger logger = LoggerFactory.getLogger(DynamicNodeAdjusterImpl.class);
 
+    private static final String[] HANDOFF_KEYWORDS = {
+            "投诉", "人工", "客服", "经理", "退款", "律师", "报警", "12315"
+    };
+
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
+    @Autowired(required = false)
+    private SemanticAnalyzer semanticAnalyzer;
+
+    @Autowired(required = false)
+    private SemanticTakeoverDetector takeoverDetector;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper instanceMapper;
+
+    @Autowired(required = false)
+    private CompanyWorkflowLobsterNodeMapper nodeMapper;
+
+    @Autowired(required = false)
+    private LobsterEvolutionEngine evolutionEngine;
+
     public List<Map<String, Object>> listAdjustable(Long companyId) {
         if (auxMapper == null) return new ArrayList<>();
         return auxMapper.selectDynamicImpls(companyId, null);
     }
 
     @Override
-    public AdjustmentResult adjustNode(Long instanceId, Long companyId, String externalUserId, String customerMessage, String currentNodeCode, Map<String, Object> variables) {
-        return null;
+    public AdjustmentResult adjustNode(Long instanceId, Long companyId, String externalUserId,
+                                       String customerMessage, String currentNodeCode,
+                                       Map<String, Object> variables) {
+        AdjustmentResult r = new AdjustmentResult();
+        Map<String, Object> vars = variables != null ? new HashMap<>(variables) : new HashMap<>();
+        r.setUpdatedVariables(vars);
+
+        if (customerMessage == null || customerMessage.isBlank()) {
+            r.setAdjustmentReason("no-message");
+            return r;
+        }
+
+        SemanticResult semantic = analyze(customerMessage, vars);
+        if (semantic != null) {
+            r.setDetectedIntent(semantic.getIntent());
+            if (semantic.getSentiment() != null) {
+                r.setDetectedSentiment(String.valueOf(semantic.getSentiment()));
+            }
+            vars.put("customerIntent", semantic.getIntent());
+            if (semantic.getSentiment() != null) vars.put("customerSentiment", semantic.getSentiment());
+            if (semantic.getKeywords() != null) vars.put("customerKeywords", semantic.getKeywords());
+        }
+
+        if (shouldTransferToHuman(customerMessage, vars)) {
+            r.setTransferToHuman(true);
+            r.setNextNodeCode("human_takeover");
+            r.setAdjustmentReason("handoff-detected");
+            return r;
+        }
+
+        String routed = resolveBranchNextNode(companyId, instanceId, currentNodeCode, semantic, vars);
+        if (routed != null) {
+            r.setNextNodeCode(routed);
+            r.setAdjustmentReason("branch-route:" + (semantic != null ? semantic.getIntent() : ""));
+            return r;
+        }
+
+        if (evolutionEngine != null && semantic != null && semantic.getIntent() != null) {
+            try {
+                String dynamicNode = evolutionEngine.enrichWithDynamicNode(
+                        instanceId, companyId, externalUserId, currentNodeCode,
+                        semantic.getIntent(),
+                        r.getDetectedSentiment(),
+                        extractProfile(vars),
+                        vars);
+                if (dynamicNode != null && !dynamicNode.isEmpty()) {
+                    r.setNextNodeCode(dynamicNode);
+                    r.setAdjustmentReason("dynamic-enrich:" + semantic.getIntent());
+                }
+            } catch (Exception e) {
+                logger.debug("[DynamicNodeAdjuster] enrich skipped: {}", e.getMessage());
+            }
+        }
+
+        if (r.getAdjustmentReason() == null) {
+            r.setAdjustmentReason("semantic-analyzed");
+        }
+        return r;
     }
 
     @Override
     public boolean shouldTransferToHuman(String customerMessage, Map<String, Object> variables) {
-        return false;
+        if (customerMessage == null || customerMessage.isBlank()) return false;
+
+        if (takeoverDetector != null) {
+            try {
+                String ctx = variables != null ? JSON.toJSONString(variables) : "";
+                SemanticTakeoverDetector.TakeoverResult tr = takeoverDetector.detectTakeover(
+                        variables != null && variables.get("companyId") instanceof Number
+                                ? ((Number) variables.get("companyId")).longValue() : null,
+                        customerMessage, ctx);
+                if (tr != null && tr.isShouldTakeover() && tr.getConfidence() >= 0.65) {
+                    return true;
+                }
+            } catch (Exception e) {
+                logger.debug("[DynamicNodeAdjuster] takeover detect failed: {}", e.getMessage());
+            }
+        }
+
+        String lower = customerMessage.toLowerCase(Locale.ROOT);
+        for (String kw : HANDOFF_KEYWORDS) {
+            if (lower.contains(kw.toLowerCase(Locale.ROOT))) return true;
+        }
+        Object explicit = variables != null ? variables.get("forceHandoff") : null;
+        return Boolean.TRUE.equals(explicit);
     }
 
     @Override
     public String detectIntent(String customerMessage) {
-        return "";
+        SemanticResult r = analyze(customerMessage, null);
+        return r != null ? r.getIntent() : null;
+    }
+
+    private SemanticResult analyze(String message, Map<String, Object> vars) {
+        if (semanticAnalyzer == null) return null;
+        try {
+            return semanticAnalyzer.analyzeIntent(message, vars != null ? vars : Collections.emptyMap());
+        } catch (Exception e) {
+            logger.debug("[DynamicNodeAdjuster] semantic analyze failed: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private String resolveBranchNextNode(Long companyId, Long instanceId, String currentNodeCode,
+                                         SemanticResult semantic, Map<String, Object> vars) {
+        if (nodeMapper == null || companyId == null) return null;
+        Long workflowId = resolveWorkflowId(instanceId, vars);
+        if (workflowId == null) return null;
+
+        List<CompanyWorkflowLobsterNode> nodes = nodeMapper.selectByWorkflowIdAndCompanyId(workflowId, companyId);
+        if (nodes == null || nodes.isEmpty()) return null;
+
+        CompanyWorkflowLobsterNode current = findNode(nodes, currentNodeCode, instanceId);
+        if (current == null) return null;
+
+        String branchNext = matchConditionBranches(current.getConditionExpr(), semantic, vars);
+        if (branchNext != null) return branchNext;
+
+        if (semantic != null && semantic.getIntent() != null) {
+            for (CompanyWorkflowLobsterNode n : nodes) {
+                if (n.getNodeName() != null && n.getNodeName().contains(semantic.getIntent())) {
+                    return n.getNodeCode();
+                }
+                if (n.getConditionExpr() != null && n.getConditionExpr().contains(semantic.getIntent())) {
+                    return n.getNextNodeCode() != null ? n.getNextNodeCode() : n.getNodeCode();
+                }
+            }
+        }
+        return null;
+    }
+
+    private CompanyWorkflowLobsterNode findNode(List<CompanyWorkflowLobsterNode> nodes,
+                                                String nodeCode, Long instanceId) {
+        if (nodeCode != null) {
+            for (CompanyWorkflowLobsterNode n : nodes) {
+                if (nodeCode.equals(n.getNodeCode())) return n;
+            }
+        }
+        if (instanceMapper != null && instanceId != null) {
+            LobsterWorkflowInstance inst = instanceMapper.selectById(instanceId);
+            if (inst != null && inst.getCurrentNodeIndex() != null) {
+                int idx = inst.getCurrentNodeIndex();
+                nodes.sort(Comparator.comparingInt(x -> x.getSortNo() != null ? x.getSortNo() : 0));
+                if (idx >= 0 && idx < nodes.size()) return nodes.get(idx);
+            }
+        }
+        return null;
+    }
+
+    private String matchConditionBranches(String conditionExpr, SemanticResult semantic, Map<String, Object> vars) {
+        if (conditionExpr == null || conditionExpr.isBlank()) return null;
+        try {
+            if (conditionExpr.trim().startsWith("{")) {
+                JSONObject json = JSON.parseObject(conditionExpr);
+                JSONArray branches = json.getJSONArray("branches");
+                if (branches != null) {
+                    for (int i = 0; i < branches.size(); i++) {
+                        JSONObject b = branches.getJSONObject(i);
+                        if (b == null) continue;
+                        String cond = b.getString("condition");
+                        if (matchesCondition(cond, semantic, vars)) {
+                            return b.getString("nextNode");
+                        }
+                    }
+                }
+            }
+            if (semantic != null && semantic.getIntent() != null && conditionExpr.contains(semantic.getIntent())) {
+                return null;
+            }
+        } catch (Exception e) {
+            logger.debug("[DynamicNodeAdjuster] branch parse failed: {}", e.getMessage());
+        }
+        return null;
+    }
+
+    private boolean matchesCondition(String cond, SemanticResult semantic, Map<String, Object> vars) {
+        if (cond == null || cond.isBlank()) return false;
+        String c = cond.toLowerCase(Locale.ROOT);
+        if (semantic != null && semantic.getIntent() != null && c.contains(semantic.getIntent().toLowerCase(Locale.ROOT))) {
+            return true;
+        }
+        if (vars != null) {
+            for (Map.Entry<String, Object> e : vars.entrySet()) {
+                if (e.getValue() != null && c.contains(String.valueOf(e.getValue()).toLowerCase(Locale.ROOT))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private Long resolveWorkflowId(Long instanceId, Map<String, Object> vars) {
+        if (instanceMapper != null && instanceId != null) {
+            LobsterWorkflowInstance inst = instanceMapper.selectById(instanceId);
+            if (inst != null) return inst.getWorkflowId();
+        }
+        if (vars != null && vars.get("workflowId") instanceof Number) {
+            return ((Number) vars.get("workflowId")).longValue();
+        }
+        return null;
+    }
+
+    private Map<String, Object> extractProfile(Map<String, Object> vars) {
+        if (vars == null) return Collections.emptyMap();
+        Map<String, Object> profile = new LinkedHashMap<>();
+        for (String k : new String[]{"contactName", "contactPhone", "customerIntent", "customerSentiment", "budget", "interest"}) {
+            if (vars.containsKey(k)) profile.put(k, vars.get(k));
+        }
+        return profile;
     }
 }

+ 773 - 29
fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeExecutorImpl.java

@@ -4,9 +4,14 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.company.service.llm.MultiModelRouter;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.service.workflow.ConditionEvaluator;
 import com.fs.company.service.workflow.DynamicNodeExecutor;
 import com.fs.company.service.workflow.LobsterNodeTypeService;
+import com.fs.company.service.workflow.LobsterWorkflowExecutor;
 import com.fs.company.service.workflow.QualityScoringService;
+import com.fs.company.service.workflow.ToolCallFramework;
+import com.fs.company.service.workflow.vector.VectorPatternMatcher;
 import com.fs.company.service.workflow.channel.MessageChannelRequest;
 import com.fs.company.service.workflow.channel.MessageChannelResult;
 import com.fs.company.service.workflow.channel.MessageChannelRouter;
@@ -15,6 +20,7 @@ import com.fs.company.domain.LobsterWorkflowNodeType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
@@ -30,12 +36,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
-import javax.annotation.PostConstruct;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
 @Service
 public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
 
@@ -65,6 +65,19 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
     @Autowired(required = false)
     private com.fs.company.service.workflow.pay.PayService payService;
 
+    @Autowired(required = false)
+    private ConditionEvaluator conditionEvaluator;
+
+    @Autowired(required = false)
+    private VectorPatternMatcher vectorPatternMatcher;
+
+    @Autowired(required = false)
+    @Lazy
+    private LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private ToolCallFramework toolCallFramework;
+
     private final RestTemplate restTemplate = new RestTemplate();
 
     private final ConcurrentHashMap<Integer, NodeHandler> handlers = new ConcurrentHashMap<>();
@@ -80,8 +93,9 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         handlers.put(3, this::handleJudgmentNode);
         handlers.put(4, this::handleWaitNode);
         handlers.put(5, this::handleEndNode);
+        handlers.put(6, this::handlePromotionEndNode);
         handlers.put(7, this::handleOrderSuccessNode);
-        handlers.put(8, this::handleCouponNode);
+        handlers.put(8, this::handleOrderConfirmNode);
         handlers.put(9, this::handleTagOperationNode);
         handlers.put(10, this::handleCareNode);
         handlers.put(11, this::handleSurveyNode);
@@ -91,15 +105,27 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         handlers.put(20, this::handleIntentRecognitionNode);
         handlers.put(21, this::handleTakeoverDetectNode);
         handlers.put(22, this::handleQualityCheckNode);
+        handlers.put(23, this::handleKnowledgeRetrievalNode);
+        handlers.put(24, this::handleProductRecommendNode);
+        handlers.put(25, this::handleTagMatchNode);
         handlers.put(30, this::handleQwMessageNode);
         handlers.put(31, this::handleImMessageNode);
+        handlers.put(32, this::handleTimedDelayNode);
+        handlers.put(33, this::handleAiChatNode);
+        handlers.put(34, this::handleSmsMessageNode);
+        handlers.put(35, this::handleEmailMessageNode);
         handlers.put(40, this::handleVariableAssignNode);
+        handlers.put(41, this::handleAddTagNode);
         handlers.put(42, this::handleWebhookNode);
+        handlers.put(43, this::handleSubWorkflowNode);
+        handlers.put(44, this::handleCreateTaskNode);
         handlers.put(50, this::handleSopExecuteNode);
         handlers.put(51, this::handleCidTaskNode);
         handlers.put(52, this::handleProductPushNode);
         handlers.put(53, this::handleLogisticsNotifyNode);
         handlers.put(100, this::handleExternalApiNode);
+        handlers.put(200, this::handleCustomNode);
+        handlers.put(201, this::handleCustomNode);
     }
 
     @Override
@@ -221,6 +247,18 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
      * 简化评分:success=基础 60,有 outputVariables +20,有 messageToSend +15,errorMessage 为空 +5
      */
     private double scoreResult(NodeExecutionResult r, ExecutionContext ctx) {
+        if (qualityScoringService != null && r.getMessageToSend() != null && ctx.getCompanyId() != null) {
+            try {
+                String userQ = ctx.getLastMessage() != null ? ctx.getLastMessage() : "";
+                QualityScoringService.DetailedScore ds = qualityScoringService.score(
+                        ctx.getCompanyId(), r.getMessageToSend(), userQ, null, null, null);
+                if (ds != null && ds.getTotalScore() > 0) {
+                    return Math.min(100, ds.getTotalScore() * 100.0 / QualityScoringService.Threshold.FULL_SCORE);
+                }
+            } catch (Exception e) {
+                logger.debug("qualityScoringService fallback: {}", e.getMessage());
+            }
+        }
         double s = r.isSuccess() ? 60 : 0;
         if (r.getOutputVariables() != null) s += 20;
         if (r.getMessageToSend() != null && !r.getMessageToSend().isEmpty()) s += 15;
@@ -384,13 +422,25 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         try {
             JSONObject config = JSON.parseObject(nodeConfig);
             String condition = config.getString("conditionExpr");
+            if (condition == null) condition = config.getString("condition");
             String trueNext = config.getString("trueNextNode");
             String falseNext = config.getString("falseNextNode");
-            
-            boolean conditionResult = evaluateCondition(condition, context.getVariables());
-            
+            String defaultNext = config.getString("defaultNextNode");
+
             NodeExecutionResult result = NodeExecutionResult.success();
-            result.setNextNodeCode(conditionResult ? trueNext : falseNext);
+            if (conditionEvaluator != null && condition != null && !condition.isEmpty()) {
+                String nextCode = conditionEvaluator.evaluate(condition, context.getVariables(), defaultNext);
+                result.setNextNodeCode(nextCode != null ? nextCode : defaultNext);
+            } else if (conditionEvaluator != null && config.containsKey("branches")) {
+                String nextCode = conditionEvaluator.evaluateNextNode(context.getVariables(), config.toJSONString());
+                result.setNextNodeCode(nextCode);
+            } else {
+                boolean conditionResult = evaluateCondition(condition, context.getVariables());
+                result.setNextNodeCode(conditionResult ? trueNext : falseNext);
+            }
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("judgmentResult", result.getNextNodeCode());
+            result.setOutputVariables(outputs);
             return result;
         } catch (Exception e) {
             return NodeExecutionResult.fail("判断节点处理失败: " + e.getMessage());
@@ -490,6 +540,22 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         return NodeExecutionResult.success();
     }
 
+    /** 节点 6:促单结束 */
+    private NodeExecutionResult handlePromotionEndNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String reason = config != null ? config.getString("promotionReason") : null;
+            if (reason == null) reason = config != null ? config.getString("reason") : "促单阶段结束";
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("promotionEndReason", reason);
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            r.setMessageToSend(reason);
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("促单结束节点处理失败: " + e.getMessage());
+        }
+    }
+
     private NodeExecutionResult handleOrderSuccessNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
@@ -523,7 +589,69 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         }
     }
 
-    // ── 节点 8:优惠券发放 ──
+    /** 节点 8:订单确认(本地状态,不含真实支付) */
+    private NodeExecutionResult handleOrderConfirmNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String orderField = config != null ? config.getString("orderField") : "orderId";
+            String confirmMessage = config != null ? config.getString("confirmMessage") : null;
+            boolean applyCoupon = config != null && config.getBooleanValue("applyCoupon");
+            Map<String, Object> outputs = new HashMap<>();
+
+            Object orderId = context.getVariables() != null ? context.getVariables().get(orderField) : null;
+            if (orderId == null && auxMapper != null && context.getCompanyId() != null) {
+                try {
+                    List<Map<String, Object>> rows = auxMapper.queryForList(
+                            "SELECT order_no, product_name, amount, status FROM lobster_order WHERE company_id="
+                                    + context.getCompanyId() + " AND customer_id='" + sqlEscape(context.getCustomerId())
+                                    + "' ORDER BY create_time DESC LIMIT 1",
+                            context.getCompanyId());
+                    if (!rows.isEmpty()) {
+                        Map<String, Object> row = rows.get(0);
+                        orderId = row.get("order_no");
+                        outputs.put("orderProduct", row.get("product_name"));
+                        outputs.put("orderAmount", row.get("amount"));
+                        outputs.put("orderStatus", row.get("status"));
+                    }
+                } catch (Exception e) { logger.debug("order lookup: {}", e.getMessage()); }
+            }
+            outputs.put("orderId", orderId);
+            outputs.put("orderConfirmed", true);
+            outputs.put("confirmTime", System.currentTimeMillis());
+
+            if (auxMapper != null && orderId != null && context.getCompanyId() != null) {
+                try {
+                    auxMapper.update(String.format(
+                            "UPDATE lobster_order SET status='confirmed', update_time=NOW() WHERE company_id=%d AND order_no='%s'",
+                            context.getCompanyId(), sqlEscape(orderId)));
+                } catch (Exception e) { logger.debug("order confirm update: {}", e.getMessage()); }
+            }
+
+            if (applyCoupon && toolCallFramework != null) {
+                Map<String, Object> couponParams = new HashMap<>();
+                couponParams.put("customerId", context.getCustomerId());
+                if (config != null) {
+                    if (config.getString("couponType") != null) couponParams.put("couponType", config.getString("couponType"));
+                    if (config.getString("amount") != null) couponParams.put("amount", config.getString("amount"));
+                }
+                ToolCallFramework.ToolCallResult couponResult = toolCallFramework.executeTool(
+                        "applyCoupon", couponParams, context.getCompanyId());
+                if (couponResult != null && couponResult.isSuccess() && couponResult.getData() != null) {
+                    outputs.put("couponApplied", couponResult.getData());
+                }
+            }
+
+            String msg = confirmMessage != null ? substituteVariables(confirmMessage, context)
+                    : (orderId != null ? "您的订单 " + orderId + " 已确认,我们会尽快为您安排。" : "订单已确认,感谢您的信任!");
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            r.setMessageToSend(msg);
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("订单确认节点处理失败: " + e.getMessage());
+        }
+    }
+
+    /** 优惠券发放(可通过订单确认节点 applyCoupon 或工具调用触发) */
     private NodeExecutionResult handleCouponNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
@@ -872,6 +1000,429 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         }
     }
 
+    /** 节点 23:知识库检索(向量 + SQL 双通道) */
+    private NodeExecutionResult handleKnowledgeRetrievalNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String query = context.getLastMessage();
+            if (query == null || query.isEmpty()) {
+                query = config != null ? config.getString("defaultQuery") : "";
+            }
+            String kbCode = config != null ? config.getString("knowledgeBaseCode") : "default";
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("query", query);
+            List<String> snippets = new java.util.ArrayList<>();
+            List<Map<String, Object>> vectorHits = new java.util.ArrayList<>();
+
+            if (vectorPatternMatcher != null && query != null && !query.isEmpty() && context.getCompanyId() != null) {
+                try {
+                    List<VectorPatternMatcher.VectorMatchResult> matches = vectorPatternMatcher.searchSimilar(
+                            context.getCompanyId(), "knowledge", query, 5, 0.55);
+                    for (VectorPatternMatcher.VectorMatchResult m : matches) {
+                        if (m.getText() != null) snippets.add(m.getText());
+                        Map<String, Object> hit = new LinkedHashMap<>();
+                        hit.put("key", m.getKey());
+                        hit.put("text", m.getText());
+                        hit.put("score", m.getScore());
+                        if (m.getMetadata() != null) hit.put("metadata", m.getMetadata());
+                        vectorHits.add(hit);
+                    }
+                } catch (Exception e) { logger.debug("vector knowledge search: {}", e.getMessage()); }
+            }
+
+            if (snippets.isEmpty() && auxMapper != null && kbCode != null && context.getCompanyId() != null && query != null && !query.isEmpty()) {
+                try {
+                    List<Map<String, Object>> rows = auxMapper.queryForList(
+                            "SELECT title, content FROM lobster_knowledge_chunk WHERE company_id="
+                                    + context.getCompanyId() + " AND kb_code='" + sqlEscape(kbCode)
+                                    + "' AND content LIKE '%" + sqlEscape(query) + "%' LIMIT 5",
+                            context.getCompanyId());
+                    for (Map<String, Object> row : rows) {
+                        Object c = row.get("content");
+                        if (c != null) snippets.add(c.toString());
+                    }
+                } catch (Exception e) { logger.debug("knowledge db lookup: {}", e.getMessage()); }
+            }
+
+            outputs.put("snippets", snippets);
+            outputs.put("vectorHits", vectorHits);
+            outputs.put("retrievedCount", snippets.size());
+
+            if (!snippets.isEmpty() && multiModelRouter != null) {
+                String contextText = String.join("\n---\n", snippets.subList(0, Math.min(3, snippets.size())));
+                String prompt = "根据以下知识片段回答客户问题,简洁准确。\n知识:\n" + contextText + "\n\n问题: " + query;
+                String answer = multiModelRouter.generateResponse(prompt, null, "knowledge_retrieval");
+                outputs.put("ragAnswer", answer);
+                NodeExecutionResult r = NodeExecutionResult.success(outputs);
+                r.setMessageToSend(answer != null ? answer.trim() : contextText);
+                return r;
+            }
+            if (snippets.isEmpty() && multiModelRouter != null && query != null && !query.isEmpty()) {
+                String prompt = "根据以下客户问题检索知识并回答,输出JSON: {\"answer\":\"...\"}\n问题: " + query;
+                String aiResp = multiModelRouter.generateResponse(prompt, null, "knowledge_retrieval");
+                outputs.put("ragAnswer", aiResp);
+                NodeExecutionResult r = NodeExecutionResult.success(outputs);
+                r.setMessageToSend(aiResp);
+                return r;
+            }
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("知识库检索失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 24:商品推荐(标签 + 品类) */
+    private NodeExecutionResult handleProductRecommendNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String category = config != null ? config.getString("productCategory") : null;
+            String tagField = config != null ? config.getString("tagField") : null;
+            Map<String, Object> outputs = new HashMap<>();
+            List<Map<String, Object>> products = new java.util.ArrayList<>();
+            List<String> matchTags = new java.util.ArrayList<>();
+
+            if (auxMapper != null && context.getCompanyId() != null) {
+                if (tagField != null && context.getVariables() != null && context.getVariables().get(tagField) != null) {
+                    Object tv = context.getVariables().get(tagField);
+                    matchTags.add(tv.toString());
+                } else {
+                    try {
+                        List<Map<String, Object>> tagRows = auxMapper.queryForList(
+                                "SELECT tag_key, tag_value FROM customer_tag WHERE company_id="
+                                        + context.getCompanyId() + " AND external_user_id='" + sqlEscape(context.getCustomerId()) + "' LIMIT 10",
+                                context.getCompanyId());
+                        for (Map<String, Object> tr : tagRows) {
+                            Object v = tr.get("tag_value");
+                            if (v != null) matchTags.add(v.toString());
+                        }
+                    } catch (Exception e) { logger.debug("tag load for recommend: {}", e.getMessage()); }
+                }
+                try {
+                    String sql = "SELECT id, product_name, product_url, price, tags FROM lobster_product WHERE company_id="
+                            + context.getCompanyId();
+                    if (category != null && !category.isEmpty()) {
+                        sql += " AND (product_name LIKE '%" + sqlEscape(category) + "%' OR tags LIKE '%" + sqlEscape(category) + "%')";
+                    }
+                    sql += " ORDER BY update_time DESC LIMIT 20";
+                    List<Map<String, Object>> candidates = auxMapper.queryForList(sql, context.getCompanyId());
+                    if (!matchTags.isEmpty()) {
+                        for (Map<String, Object> p : candidates) {
+                            String tags = p.get("tags") != null ? p.get("tags").toString() : "";
+                            String name = p.get("product_name") != null ? p.get("product_name").toString() : "";
+                            for (String mt : matchTags) {
+                                if ((!tags.isEmpty() && tags.contains(mt)) || name.contains(mt)) {
+                                    products.add(p);
+                                    break;
+                                }
+                            }
+                            if (products.size() >= 3) break;
+                        }
+                    }
+                    if (products.isEmpty()) {
+                        products = candidates.size() > 3 ? candidates.subList(0, 3) : candidates;
+                    }
+                } catch (Exception e) { logger.debug("product recommend: {}", e.getMessage()); }
+            }
+            outputs.put("products", products);
+            outputs.put("matchTags", matchTags);
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            if (!products.isEmpty()) {
+                Map<String, Object> p = products.get(0);
+                String name = p.get("product_name") != null ? p.get("product_name").toString() : "精选商品";
+                String url = p.get("product_url") != null ? p.get("product_url").toString() : "";
+                r.setMessageToSend("为您推荐:" + name + (url.isEmpty() ? "" : "\n" + url));
+            }
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("商品推荐失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 25:标签匹配分支 */
+    private NodeExecutionResult handleTagMatchNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String requiredTags = config != null ? config.getString("requiredTags") : null;
+            String matchMode = config != null ? config.getString("matchMode") : "any";
+            String trueNext = config != null ? config.getString("trueNextNode") : null;
+            String falseNext = config != null ? config.getString("falseNextNode") : null;
+            java.util.Set<String> required = new java.util.LinkedHashSet<>();
+            if (requiredTags != null && !requiredTags.isEmpty()) {
+                for (String t : requiredTags.split("[,;|]")) {
+                    if (!t.trim().isEmpty()) required.add(t.trim());
+                }
+            }
+            java.util.Set<String> owned = new java.util.LinkedHashSet<>();
+            if (auxMapper != null && context.getCompanyId() != null) {
+                try {
+                    List<Map<String, Object>> tagRows = auxMapper.queryForList(
+                            "SELECT tag_key, tag_value FROM customer_tag WHERE company_id="
+                                    + context.getCompanyId() + " AND external_user_id='" + sqlEscape(context.getCustomerId()) + "'",
+                            context.getCompanyId());
+                    for (Map<String, Object> tr : tagRows) {
+                        if (tr.get("tag_key") != null) owned.add(tr.get("tag_key").toString());
+                        if (tr.get("tag_value") != null) owned.add(tr.get("tag_value").toString());
+                    }
+                } catch (Exception e) { logger.debug("tag match load: {}", e.getMessage()); }
+            }
+            if (context.getVariables() != null) {
+                for (Map.Entry<String, Object> e : context.getVariables().entrySet()) {
+                    if (e.getKey() != null && e.getKey().startsWith("tag_") && e.getValue() != null) {
+                        owned.add(e.getValue().toString());
+                    }
+                }
+            }
+            boolean matched;
+            if (required.isEmpty()) {
+                matched = !owned.isEmpty();
+            } else if ("all".equalsIgnoreCase(matchMode)) {
+                matched = owned.containsAll(required);
+            } else {
+                matched = false;
+                for (String r : required) {
+                    if (owned.contains(r)) { matched = true; break; }
+                }
+            }
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("tagMatched", matched);
+            outputs.put("ownedTags", new java.util.ArrayList<>(owned));
+            outputs.put("requiredTags", new java.util.ArrayList<>(required));
+            NodeExecutionResult result = NodeExecutionResult.success(outputs);
+            result.setNextNodeCode(matched ? trueNext : falseNext);
+            return result;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("标签匹配节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 32:定时延迟(独立配置项) */
+    private NodeExecutionResult handleTimedDelayNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            long delaySeconds = config != null ? config.getLongValue("delaySeconds") : 0;
+            if (delaySeconds <= 0 && config != null) delaySeconds = config.getLongValue("waitSeconds");
+            if (delaySeconds <= 0) delaySeconds = 60;
+            String scheduleAt = config != null ? config.getString("scheduleAt") : null;
+            Map<String, Object> outputs = new HashMap<>();
+            long now = System.currentTimeMillis();
+            long waitUntil = scheduleAt != null ? parseScheduleAt(scheduleAt) : now + delaySeconds * 1000L;
+            outputs.put("waitType", "timed_delay");
+            outputs.put("delaySeconds", delaySeconds);
+            outputs.put("waitUntil", waitUntil);
+            outputs.put("scheduledAt", waitUntil);
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return handleWaitNode(nodeType, nodeConfig, context);
+        }
+    }
+
+    private long parseScheduleAt(String scheduleAt) {
+        try {
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            return sdf.parse(scheduleAt).getTime();
+        } catch (Exception e) {
+            return System.currentTimeMillis() + 3600_000L;
+        }
+    }
+
+    /** 节点 200/201:自定义节点(配置驱动 + AI 兜底) */
+    private NodeExecutionResult handleCustomNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String action = config != null ? config.getString("actionType") : "ai";
+            Map<String, Object> outputs = new HashMap<>();
+            if ("assign".equals(action) && config.getJSONObject("assignments") != null) {
+                JSONObject assignments = config.getJSONObject("assignments");
+                for (String k : assignments.keySet()) {
+                    outputs.put(k, substituteVariables(assignments.getString(k), context));
+                }
+                NodeExecutionResult r = NodeExecutionResult.success(outputs);
+                if (config.getString("messageTemplate") != null) {
+                    r.setMessageToSend(substituteVariables(config.getString("messageTemplate"), context));
+                }
+                return r;
+            }
+            if ("webhook".equals(action) && config.getString("url") != null) {
+                return handleWebhookNode(nodeType, nodeConfig, context);
+            }
+            String prompt = config != null && config.getString("prompt") != null
+                    ? substituteVariables(config.getString("prompt"), context)
+                    : "执行自定义节点(type=" + nodeType + "),上下文: " + JSON.toJSONString(context.getVariables());
+            String reply = multiModelRouter != null
+                    ? multiModelRouter.generateResponse(prompt, config != null ? config.getString("model") : null, "custom_node")
+                    : "自定义节点已执行";
+            outputs.put("customNodeType", nodeType);
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            r.setMessageToSend(reply != null ? reply.trim() : "");
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("自定义节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 33:AI 对话 */
+    private NodeExecutionResult handleAiChatNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String systemPrompt = config != null ? config.getString("systemPrompt") : "你是一位专业销售顾问";
+            String userMsg = context.getLastMessage() != null ? context.getLastMessage() : "";
+            String prompt = systemPrompt + "\n客户: " + userMsg + "\n请给出简洁回复(仅输出回复文本)";
+            String reply = multiModelRouter != null
+                    ? multiModelRouter.generateResponse(prompt, config != null ? config.getString("model") : null, "ai_chat")
+                    : "您好,有什么可以帮您?";
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("aiChat", true);
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            r.setMessageToSend(reply != null ? reply.trim() : "");
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("AI对话节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 34:短信 */
+    private NodeExecutionResult handleSmsMessageNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String message = config != null ? config.getString("messageTemplate") : "";
+            message = substituteVariables(message, context);
+            String phone = config != null ? config.getString("phone") : null;
+            if (phone == null && context.getVariables() != null && context.getVariables().get("phone") != null) {
+                phone = context.getVariables().get("phone").toString();
+            }
+            MessageChannelRequest request = new MessageChannelRequest();
+            request.setCompanyId(context.getCompanyId());
+            request.setContactId(context.getCustomerId());
+            request.setContent(message);
+            request.setChannelType("sms");
+            request.setExtra(context.getVariables());
+            if (phone != null) {
+                Map<String, Object> extra = request.getExtra() != null ? new HashMap<>(request.getExtra()) : new HashMap<>();
+                extra.put("phone", phone);
+                request.setExtra(extra);
+            }
+            MessageChannelResult channelResult = messageChannelRouter.route(request);
+            NodeExecutionResult result = new NodeExecutionResult();
+            result.setSuccess(channelResult.isSuccess());
+            result.setMessageToSend(message);
+            if (!channelResult.isSuccess()) result.setErrorMessage(channelResult.getErrorMsg());
+            return result;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("短信节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 35:邮件 */
+    private NodeExecutionResult handleEmailMessageNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String subject = config != null ? config.getString("subject") : "通知";
+            String body = config != null ? config.getString("bodyTemplate") : "";
+            body = substituteVariables(body, context);
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("emailSubject", subject);
+            outputs.put("emailSent", false);
+            if (auxMapper != null && context.getCompanyId() != null) {
+                try {
+                    auxMapper.update(String.format(
+                            "INSERT INTO lobster_email_log(company_id, customer_id, subject, body, create_time) " +
+                            "VALUES(%d, '%s', '%s', '%s', NOW())",
+                            context.getCompanyId(),
+                            sqlEscape(context.getCustomerId()),
+                            sqlEscape(subject), sqlEscape(body)));
+                    outputs.put("emailSent", true);
+                } catch (Exception e) { logger.debug("email log: {}", e.getMessage()); }
+            }
+            NodeExecutionResult r = NodeExecutionResult.success(outputs);
+            r.setMessageToSend(body);
+            return r;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("邮件节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 41:打标签(与 9 类似,独立入口) */
+    private NodeExecutionResult handleAddTagNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        return handleTagOperationNode(nodeType, nodeConfig, context);
+    }
+
+    /** 节点 43:子流程(启动子工作流实例) */
+    private NodeExecutionResult handleSubWorkflowNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            Long subWorkflowId = config != null && config.get("subWorkflowId") != null
+                    ? config.getLong("subWorkflowId") : null;
+            String resultVar = config != null && config.getString("resultVar") != null
+                    ? config.getString("resultVar") : "subResult";
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("subWorkflowId", subWorkflowId);
+
+            if (auxMapper != null && context.getCompanyId() != null) {
+                String subIdSql = subWorkflowId != null ? subWorkflowId.toString() : "NULL";
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_sub_workflow_exec(company_id, parent_instance_id, sub_workflow_id, status, create_time) " +
+                        "VALUES(%d, %d, %s, 'triggered', NOW())",
+                        context.getCompanyId(),
+                        context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
+                        subIdSql));
+            }
+
+            if (workflowExecutor != null && subWorkflowId != null && context.getCompanyId() != null) {
+                Map<String, Object> subVars = context.getVariables() != null
+                        ? new HashMap<>(context.getVariables()) : new HashMap<>();
+                subVars.put("parentInstanceId", context.getWorkflowInstanceId());
+                subVars.put("channelType", context.getChannelType());
+                AjaxResult subResult = workflowExecutor.startWorkflow(
+                        context.getCompanyId(), subWorkflowId, context.getCustomerId(), subVars);
+                if (subResult != null && Integer.valueOf(200).equals(subResult.get("code"))) {
+                    Object data = subResult.get("data");
+                    if (data instanceof com.fs.company.domain.LobsterWorkflowInstance) {
+                        com.fs.company.domain.LobsterWorkflowInstance subInstance =
+                                (com.fs.company.domain.LobsterWorkflowInstance) data;
+                        outputs.put("subInstanceId", subInstance.getId());
+                        outputs.put(resultVar, "sub_instance_" + subInstance.getId());
+                    } else {
+                        outputs.put(resultVar, "sub_started");
+                    }
+                    outputs.put("subWorkflowTriggered", true);
+                } else {
+                    outputs.put("subWorkflowTriggered", false);
+                    outputs.put(resultVar, "sub_failed");
+                }
+            } else {
+                outputs.put("subWorkflowTriggered", subWorkflowId != null);
+            }
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("子流程节点失败: " + e.getMessage());
+        }
+    }
+
+    /** 节点 44:创建任务 */
+    private NodeExecutionResult handleCreateTaskNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String taskTitle = config != null ? config.getString("taskTitle") : "跟进任务";
+            String taskContent = config != null ? config.getString("taskContent") : "";
+            taskContent = substituteVariables(taskContent, context);
+            Map<String, Object> outputs = new HashMap<>();
+            outputs.put("taskTitle", taskTitle);
+            if (auxMapper != null && context.getCompanyId() != null) {
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_task(company_id, instance_id, customer_id, task_title, task_content, status, create_time) " +
+                        "VALUES(%d, %d, '%s', '%s', '%s', 'pending', NOW())",
+                        context.getCompanyId(),
+                        context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
+                        sqlEscape(context.getCustomerId()),
+                        sqlEscape(taskTitle), sqlEscape(taskContent)));
+                outputs.put("taskCreated", true);
+            }
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("创建任务失败: " + e.getMessage());
+        }
+    }
+
     /** 安全解析 config,兼容 null 和空字符串 */
     private JSONObject parseConfig(String cfg) {
         if (cfg == null || cfg.isEmpty() || "{}".equals(cfg)) return new JSONObject();
@@ -898,39 +1449,232 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
     }
 
     private NodeExecutionResult handleWebhookNode(int nodeType, String nodeConfig, ExecutionContext context) {
-        Map<String, Object> outputs = new HashMap<>();
-        outputs.put("webhookCalled", true);
-        return NodeExecutionResult.success(outputs);
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            Map<String, Object> outputs = invokeHttpFromConfig(config, context, "webhookUrl", "url");
+            outputs.put("webhookCalled", true);
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("Webhook调用失败: " + e.getMessage());
+        }
     }
 
     private NodeExecutionResult handleSopExecuteNode(int nodeType, String nodeConfig, ExecutionContext context) {
-        Map<String, Object> outputs = new HashMap<>();
-        outputs.put("sopExecuted", true);
-        return NodeExecutionResult.success(outputs);
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            Map<String, Object> outputs = new HashMap<>();
+            String sopId = config != null ? config.getString("sopId") : null;
+            String sopName = config != null ? config.getString("sopName") : "default_sop";
+            outputs.put("sopId", sopId);
+            outputs.put("sopName", sopName);
+            if (config != null && (config.containsKey("webhookUrl") || config.containsKey("url"))) {
+                outputs.putAll(invokeHttpFromConfig(config, context, "webhookUrl", "url"));
+            } else if (auxMapper != null && context.getCompanyId() != null) {
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_sop_execution(company_id, instance_id, sop_id, sop_name, status, create_time) " +
+                        "VALUES(%d, %d, '%s', '%s', 'triggered', NOW())",
+                        context.getCompanyId(),
+                        context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
+                        sqlEscape(sopId != null ? sopId : ""),
+                        sqlEscape(sopName)));
+                outputs.put("sopExecuted", true);
+            }
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("SOP执行失败: " + e.getMessage());
+        }
     }
 
     private NodeExecutionResult handleCidTaskNode(int nodeType, String nodeConfig, ExecutionContext context) {
-        Map<String, Object> outputs = new HashMap<>();
-        outputs.put("cidTaskCreated", true);
-        return NodeExecutionResult.success(outputs);
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            Map<String, Object> outputs = new HashMap<>();
+            String taskTemplate = config != null ? config.getString("taskTemplate") : null;
+            String taskName = config != null ? config.getString("taskName") : "cid_task";
+            outputs.put("taskTemplate", taskTemplate);
+            outputs.put("taskName", taskName);
+            if (config != null && (config.containsKey("apiUrl") || config.containsKey("url"))) {
+                outputs.putAll(invokeHttpFromConfig(config, context, "apiUrl", "url"));
+            } else if (auxMapper != null && context.getCompanyId() != null) {
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_cid_task(company_id, instance_id, customer_id, task_template, task_name, status, create_time) " +
+                        "VALUES(%d, %d, '%s', '%s', '%s', 'created', NOW())",
+                        context.getCompanyId(),
+                        context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
+                        sqlEscape(context.getCustomerId() != null ? String.valueOf(context.getCustomerId()) : ""),
+                        sqlEscape(taskTemplate != null ? taskTemplate : ""),
+                        sqlEscape(taskName)));
+                outputs.put("cidTaskCreated", true);
+            }
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("CID任务创建失败: " + e.getMessage());
+        }
     }
 
     private NodeExecutionResult handleProductPushNode(int nodeType, String nodeConfig, ExecutionContext context) {
-        NodeExecutionResult result = NodeExecutionResult.success();
-        result.setMessageToSend("推荐商品链接");
-        return result;
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String productName = config != null ? config.getString("productName") : null;
+            String productUrl = config != null ? config.getString("productUrl") : null;
+            String productId = config != null ? config.getString("productId") : null;
+            Map<String, Object> outputs = new HashMap<>();
+            if (productId != null && auxMapper != null && context.getCompanyId() != null) {
+                try {
+                    long pid = Long.parseLong(productId.replaceAll("[^0-9]", ""));
+                    List<Map<String, Object>> products = auxMapper.queryForList(
+                            "SELECT product_name, product_url, price FROM lobster_product WHERE id="
+                                    + pid + " AND company_id=" + context.getCompanyId(),
+                            context.getCompanyId());
+                    if (!products.isEmpty()) {
+                        Map<String, Object> p = products.get(0);
+                        productName = p.get("product_name") != null ? p.get("product_name").toString() : productName;
+                        productUrl = p.get("product_url") != null ? p.get("product_url").toString() : productUrl;
+                        outputs.put("price", p.get("price"));
+                    }
+                } catch (Exception e) { logger.debug("product lookup: {}", e.getMessage()); }
+            }
+            if (productName == null) productName = "精选商品";
+            if (productUrl == null) productUrl = config != null ? config.getString("fallbackUrl") : "";
+            outputs.put("productName", productName);
+            outputs.put("productUrl", productUrl);
+            String msg = "为您推荐:" + productName;
+            if (productUrl != null && !productUrl.isEmpty()) msg += "\n" + productUrl;
+            NodeExecutionResult result = NodeExecutionResult.success(outputs);
+            result.setMessageToSend(msg);
+            return result;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("商品推送失败: " + e.getMessage());
+        }
     }
 
     private NodeExecutionResult handleLogisticsNotifyNode(int nodeType, String nodeConfig, ExecutionContext context) {
-        NodeExecutionResult result = NodeExecutionResult.success();
-        result.setMessageToSend("您的订单已发货,物流单号:1234567890");
-        return result;
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            Map<String, Object> outputs = new HashMap<>();
+            String trackingNo = config != null ? config.getString("trackingNo") : null;
+            String carrier = config != null ? config.getString("carrier") : "快递";
+            if (trackingNo == null && context.getVariables() != null) {
+                Object v = context.getVariables().get("trackingNo");
+                if (v == null) v = context.getVariables().get("logisticsNo");
+                if (v != null) trackingNo = v.toString();
+            }
+            if (trackingNo == null && auxMapper != null && context.getCustomerId() != null) {
+                try {
+                    List<Map<String, Object>> orders = auxMapper.queryForList(
+                            "SELECT tracking_no, carrier FROM customer_order WHERE customer_id='"
+                                    + sqlEscape(String.valueOf(context.getCustomerId())) + "' AND company_id="
+                                    + context.getCompanyId() + " ORDER BY order_time DESC LIMIT 1",
+                            context.getCompanyId());
+                    if (!orders.isEmpty()) {
+                        trackingNo = orders.get(0).get("tracking_no") != null
+                                ? orders.get(0).get("tracking_no").toString() : trackingNo;
+                        if (orders.get(0).get("carrier") != null) {
+                            carrier = orders.get(0).get("carrier").toString();
+                        }
+                    }
+                } catch (Exception e) { logger.debug("logistics lookup: {}", e.getMessage()); }
+            }
+            if (trackingNo == null) trackingNo = "待更新";
+            outputs.put("trackingNo", trackingNo);
+            outputs.put("carrier", carrier);
+            NodeExecutionResult result = NodeExecutionResult.success(outputs);
+            result.setMessageToSend("您的订单已由" + carrier + "发出,物流单号:" + trackingNo);
+            return result;
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("物流通知失败: " + e.getMessage());
+        }
     }
 
     private NodeExecutionResult handleExternalApiNode(int nodeType, String nodeConfig, ExecutionContext context) {
+        try {
+            JSONObject config = parseConfig(nodeConfig);
+            String apiCode = config != null ? config.getString("apiCode") : null;
+            Map<String, Object> outputs = new HashMap<>();
+            if (apiCode != null && smartApiMapper != null) {
+                Map<String, Object> api = smartApiMapper.selectByCode(apiCode);
+                if (api != null) {
+                    JSONObject apiConfig = new JSONObject();
+                    apiConfig.put("url", api.get("api_url"));
+                    apiConfig.put("method", api.get("api_method"));
+                    apiConfig.put("headers", api.get("headers_json"));
+                    apiConfig.put("body", api.get("body_template"));
+                    outputs.putAll(invokeHttpFromConfig(apiConfig, context, "url", "apiUrl"));
+                    outputs.put("apiCode", apiCode);
+                    outputs.put("apiCalled", true);
+                    return NodeExecutionResult.success(outputs);
+                }
+            }
+            outputs.putAll(invokeHttpFromConfig(config, context, "url", "apiUrl"));
+            outputs.put("apiCalled", true);
+            return NodeExecutionResult.success(outputs);
+        } catch (Exception e) {
+            return NodeExecutionResult.fail("外部API调用失败: " + e.getMessage());
+        }
+    }
+
+    /** 从节点配置发起 HTTP 调用,支持变量替换 */
+    private Map<String, Object> invokeHttpFromConfig(JSONObject config, ExecutionContext context,
+                                                      String... urlKeys) throws Exception {
         Map<String, Object> outputs = new HashMap<>();
-        outputs.put("apiCalled", true);
-        return NodeExecutionResult.success(outputs);
+        if (config == null) return outputs;
+        String url = null;
+        for (String key : urlKeys) {
+            url = config.getString(key);
+            if (url != null && !url.isEmpty()) break;
+        }
+        if (url == null || url.isEmpty()) return outputs;
+        url = substituteVariables(url, context);
+        String method = config.getString("method");
+        if (method == null) method = config.getString("apiMethod");
+        if (method == null) method = "POST";
+
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+        Object headersObj = config.get("headers");
+        if (headersObj instanceof String && !((String) headersObj).isEmpty()) {
+            JSONObject hdr = JSON.parseObject((String) headersObj);
+            for (String key : hdr.keySet()) {
+                httpHeaders.set(key, substituteVariables(hdr.getString(key), context));
+            }
+        } else if (headersObj instanceof JSONObject) {
+            JSONObject hdr = (JSONObject) headersObj;
+            for (String key : hdr.keySet()) {
+                httpHeaders.set(key, substituteVariables(hdr.getString(key), context));
+            }
+        }
+
+        String body = config.getString("body");
+        if (body == null) body = config.getString("bodyTemplate");
+        if (body == null) body = config.getString("payload");
+        if (body == null) body = "{}";
+        body = substituteVariables(body, context);
+
+        HttpEntity<String> entity = new HttpEntity<>(body, httpHeaders);
+        HttpMethod httpMethod = "GET".equalsIgnoreCase(method) ? HttpMethod.GET : HttpMethod.POST;
+        ResponseEntity<String> resp = restTemplate.exchange(url, httpMethod, entity, String.class);
+        outputs.put("httpStatus", resp.getStatusCodeValue());
+        outputs.put("responseBody", resp.getBody());
+        outputs.put("requestUrl", url);
+        return outputs;
+    }
+
+    private String substituteVariables(String text, ExecutionContext context) {
+        if (text == null) return null;
+        String result = text;
+        if (context.getVariables() != null) {
+            for (Map.Entry<String, Object> entry : context.getVariables().entrySet()) {
+                result = result.replace("${" + entry.getKey() + "}",
+                        entry.getValue() != null ? entry.getValue().toString() : "");
+            }
+        }
+        if (context.getCustomerId() != null) {
+            result = result.replace("${customerId}", String.valueOf(context.getCustomerId()));
+        }
+        if (context.getLastMessage() != null) {
+            result = result.replace("${lastMessage}", context.getLastMessage());
+        }
+        return result;
     }
 
     private boolean evaluateCondition(String condition, Map<String, Object> variables) {

+ 556 - 19
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterE2eTestServiceImpl.java

@@ -1,44 +1,581 @@
 package com.fs.company.service.workflow.impl;
 
-import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import com.fs.company.domain.LobsterE2eRun;
+import com.fs.company.domain.LobsterE2eRunNode;
+import com.fs.company.domain.LobsterWorkflowInstance;
+import com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper;
+import com.fs.company.mapper.LobsterE2eRunMapper;
+import com.fs.company.mapper.LobsterE2eRunNodeMapper;
+import com.fs.company.mapper.LobsterTestScenarioMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
+import com.fs.company.service.workflow.LobsterE2eTestService;
+import com.fs.company.service.workflow.LobsterEvolutionEngine;
+import com.fs.company.service.workflow.LobsterWorkflowExecutor;
+import com.fs.company.service.workflow.QualityScoringService;
+import com.fs.company.service.workflow.evolution.EvolutionEngine;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 @Service
-public class LobsterE2eTestServiceImpl {
+public class LobsterE2eTestServiceImpl implements LobsterE2eTestService {
 
     private static final Logger logger = LoggerFactory.getLogger(LobsterE2eTestServiceImpl.class);
+    private static final double PASS_SCORE = 60.0;
+    private static final int RAW_FULL_SCORE = QualityScoringService.Threshold.FULL_SCORE;
 
     @Autowired(required = false)
-    private LobsterAuxiliaryMapper auxMapper;
+    private LobsterE2eRunMapper e2eRunMapper;
 
-    public Long createTest(Long companyId, String testName, Long workflowId, String testData) {
-        if (auxMapper == null) return null;
-        auxMapper.insertE2eTest(companyId, testName, workflowId, testData);
-        return auxMapper.selectLastInsertId();
+    @Autowired(required = false)
+    private LobsterE2eRunNodeMapper e2eRunNodeMapper;
+
+    @Autowired(required = false)
+    private LobsterTestScenarioMapper testScenarioMapper;
+
+    @Autowired(required = false)
+    private LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private QualityScoringService qualityScoringService;
+
+    @Autowired(required = false)
+    private EvolutionEngine evolutionEngine;
+
+    @Autowired(required = false)
+    private LobsterEvolutionEngine lobsterEvolutionEngine;
+
+    @Autowired(required = false)
+    private CompanyWorkflowLobsterNodeMapper workflowNodeMapper;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper workflowInstanceMapper;
+
+    @Override
+    public E2eReport runE2e(E2eRequest req) {
+        if (req == null || req.getCompanyId() == null) {
+            return failedReport(null, "companyId 不能为空");
+        }
+        if (workflowExecutor == null) {
+            return failedReport(null, "工作流执行器不可用");
+        }
+
+        String runId = UUID.randomUUID().toString().replace("-", "");
+        long startMs = System.currentTimeMillis();
+        Long companyId = req.getCompanyId();
+
+        resolveRequestFromScenario(req);
+
+        Long templateId = req.getTemplateId();
+        if (templateId == null) {
+            return persistFailed(runId, companyId, req, null, null, "缺少工作流模板 templateId", startMs);
+        }
+
+        LobsterE2eRun run = initRun(runId, companyId, req);
+        if (e2eRunMapper != null) {
+            e2eRunMapper.insertRun(run);
+        }
+
+        List<String> userInputs = req.getUserInputs() != null ? req.getUserInputs() : Collections.emptyList();
+        Long contactId = req.getTestContactId() != null ? req.getTestContactId() : 0L;
+
+        Map<String, Object> initVars = new LinkedHashMap<>();
+        initVars.put("channelType", "TEST");
+        initVars.put("e2eRunId", runId);
+
+        AjaxResult startResult = workflowExecutor.startWorkflow(companyId, templateId, contactId, initVars);
+        if (startResult == null || !Integer.valueOf(HttpStatus.SUCCESS).equals(startResult.get(AjaxResult.CODE_TAG))) {
+            String err = startResult != null ? String.valueOf(startResult.get(AjaxResult.MSG_TAG)) : "启动失败";
+            return persistFailed(runId, companyId, req, templateId, null, err, startMs);
+        }
+
+        Long instanceId = extractInstanceId(startResult.get(AjaxResult.DATA_TAG));
+        if (instanceId == null) {
+            return persistFailed(runId, companyId, req, templateId, null, "无法获取实例ID", startMs);
+        }
+
+        run.setInstanceId(instanceId);
+        run.setTemplateId(templateId);
+
+        List<NodeTrace> traces = new ArrayList<>();
+        int passedCnt = 0;
+        double scoreSum = 0;
+        int seq = 0;
+        String status = "SUCCESS";
+        String errorMsg = null;
+
+        String externalUserId = contactId != null ? contactId.toString() : "e2e_test_user";
+
+        for (String userInput : userInputs) {
+            seq++;
+            long stepStart = System.currentTimeMillis();
+            NodeTrace trace = new NodeTrace();
+            trace.setNodeSeq(seq);
+            trace.setTurnNo(1);
+            trace.setUserInput(userInput);
+
+            String nodeCode = resolveCurrentNodeCode(companyId, instanceId);
+            trace.setNodeCode(nodeCode);
+
+            String aiOutput;
+            double normalized;
+            boolean stepPassed;
+            boolean finished = false;
+
+            if (lobsterEvolutionEngine != null) {
+                LobsterEvolutionEngine.EvolutionResult evo = lobsterEvolutionEngine.evolve(
+                        instanceId, companyId, externalUserId, userInput, nodeCode);
+                aiOutput = evo != null ? evo.getReply() : "";
+                normalized = evo != null ? evo.getQualityScore() * 100.0 / RAW_FULL_SCORE : PASS_SCORE;
+                stepPassed = evo == null || evo.isQualityPassed() || normalized >= PASS_SCORE;
+                if (evo != null && evo.isTransferredToHuman()) {
+                    trace.setEvolutionHint("transferred_to_human");
+                }
+                if (evo != null && evo.getQualityDimensions() != null) {
+                    trace.setScoreDetail(evo.getQualityDimensions());
+                }
+            } else {
+                AjaxResult nextResult = workflowExecutor.executeNextNode(companyId, instanceId, userInput);
+                if (nextResult == null) {
+                    status = "FAILED";
+                    errorMsg = "节点推进无响应";
+                    trace.setErrorMsg(errorMsg);
+                    trace.setPassed(false);
+                    traces.add(trace);
+                    break;
+                }
+                String msg = String.valueOf(nextResult.get(AjaxResult.MSG_TAG));
+                if (!Integer.valueOf(HttpStatus.SUCCESS).equals(nextResult.get(AjaxResult.CODE_TAG))) {
+                    if (msg.contains("已完成")) {
+                        aiOutput = msg;
+                        normalized = PASS_SCORE;
+                        stepPassed = true;
+                        finished = true;
+                    } else {
+                        status = "FAILED";
+                        errorMsg = msg;
+                        trace.setErrorMsg(msg);
+                        trace.setPassed(false);
+                        traces.add(trace);
+                        break;
+                    }
+                } else {
+                    aiOutput = extractMessage(nextResult.get(AjaxResult.DATA_TAG));
+                    normalized = scoreReply(companyId, aiOutput, userInput, trace);
+                    stepPassed = normalized >= PASS_SCORE;
+                }
+            }
+
+            if (!finished && workflowExecutor != null) {
+                AjaxResult advance = workflowExecutor.executeNextNode(companyId, instanceId, userInput);
+                if (advance != null && !Integer.valueOf(HttpStatus.SUCCESS).equals(advance.get(AjaxResult.CODE_TAG))) {
+                    String msg = String.valueOf(advance.get(AjaxResult.MSG_TAG));
+                    finished = msg.contains("已完成");
+                    if (!finished && !msg.contains("冷却")) {
+                        status = "FAILED";
+                        errorMsg = msg;
+                        trace.setErrorMsg(msg);
+                        trace.setPassed(false);
+                        trace.setAiOutput(aiOutput);
+                        trace.setScore(normalized);
+                        trace.setDurationMs(System.currentTimeMillis() - stepStart);
+                        traces.add(trace);
+                        break;
+                    }
+                } else if (advance != null && (aiOutput == null || aiOutput.isEmpty())) {
+                    aiOutput = extractMessage(advance.get(AjaxResult.DATA_TAG));
+                }
+            }
+
+            fillNodeMeta(companyId, instanceId, trace);
+            trace.setAiOutput(aiOutput);
+            trace.setScore(normalized);
+            trace.setDurationMs(System.currentTimeMillis() - stepStart);
+            trace.setPassed(stepPassed);
+            if (stepPassed) passedCnt++;
+            scoreSum += normalized;
+
+            persistNodeTrace(runId, trace);
+            traces.add(trace);
+
+            if (finished) break;
+        }
+
+        int totalNodes = traces.size();
+        double avgScore = totalNodes > 0 ? scoreSum / totalNodes : 0;
+        int evolutionCount = 0;
+        if (evolutionEngine != null && templateId != null) {
+            try {
+                evolutionEngine.analyzeAndSuggest(companyId, templateId);
+                evolutionCount = 1;
+            } catch (Exception e) {
+                logger.debug("[E2E] evolution analyze skipped: {}", e.getMessage());
+            }
+        }
+
+        run.setStatus(status);
+        run.setErrorMsg(errorMsg);
+        run.setTotalScore(BigDecimal.valueOf(avgScore).setScale(2, RoundingMode.HALF_UP));
+        run.setPassedNodeCnt(passedCnt);
+        run.setTotalNodeCnt(totalNodes);
+        run.setDurationMs(System.currentTimeMillis() - startMs);
+        run.setEvolutionCount(evolutionCount);
+        updateRun(run);
+
+        return toReport(run, traces);
+    }
+
+    @Override
+    public E2eReport getReport(String runId) {
+        if (e2eRunMapper == null || runId == null) return null;
+        LobsterE2eRun run = e2eRunMapper.selectByRunId(runId);
+        if (run == null) return null;
+        List<NodeTrace> traces = loadTraces(runId);
+        return toReport(run, traces);
+    }
+
+    @Override
+    public StepResult stepNext(Long companyId, Long instanceId, String userInput) {
+        StepResult sr = new StepResult();
+        if (workflowExecutor == null) {
+            sr.setFinished(true);
+            return sr;
+        }
+        String nodeCode = resolveCurrentNodeCode(companyId, instanceId);
+        if (lobsterEvolutionEngine != null && userInput != null) {
+            LobsterEvolutionEngine.EvolutionResult evo = lobsterEvolutionEngine.evolve(
+                    instanceId, companyId, "e2e_step", userInput, nodeCode);
+            if (evo != null) {
+                sr.setReply(evo.getReply());
+                sr.setScore((int) Math.round(evo.getQualityScore() * 100.0 / RAW_FULL_SCORE));
+                sr.setNextNodeCode(evo.getNextNodeCode());
+            }
+        }
+        AjaxResult next = workflowExecutor.executeNextNode(companyId, instanceId, userInput);
+        if (sr.getReply() == null || sr.getReply().isEmpty()) {
+            sr.setReply(extractMessage(next != null ? next.get(AjaxResult.DATA_TAG) : null));
+        }
+        Map<String, Object> state = workflowExecutor.getInstanceState(companyId, instanceId);
+        sr.setCurrentNodeCode(resolveCurrentNodeCode(companyId, instanceId));
+        sr.setFinished("completed".equals(state.get("status")) || "terminated".equals(state.get("status")));
+        if (next != null && !Integer.valueOf(HttpStatus.SUCCESS).equals(next.get(AjaxResult.CODE_TAG))) {
+            sr.setFinished(sr.getFinished() || String.valueOf(next.get(AjaxResult.MSG_TAG)).contains("已完成"));
+        }
+        if (sr.getScore() == null && qualityScoringService != null && userInput != null) {
+            QualityScoringService.DetailedScore ds = qualityScoringService.score(
+                    companyId, sr.getReply(), userInput, null, null, null);
+            sr.setScore(normalizeScore(ds));
+        }
+        return sr;
+    }
+
+    @Override
+    public MultiTurnResult multiTurn(Long companyId, Long instanceId, String nodeCode, List<String> userInputs) {
+        MultiTurnResult result = new MultiTurnResult();
+        result.setNodeCode(nodeCode);
+        result.setMaxTurn(userInputs != null ? userInputs.size() : 0);
+        List<NodeTrace> turns = new ArrayList<>();
+        double scoreSum = 0;
+        if (userInputs != null) {
+            int turn = 0;
+            for (String input : userInputs) {
+                turn++;
+                long t0 = System.currentTimeMillis();
+                StepResult step = stepNext(companyId, instanceId, input);
+                NodeTrace trace = new NodeTrace();
+                trace.setNodeCode(nodeCode);
+                trace.setTurnNo(turn);
+                trace.setUserInput(input);
+                trace.setAiOutput(step.getReply());
+                trace.setScore(step.getScore() != null ? step.getScore().doubleValue() : null);
+                trace.setDurationMs(System.currentTimeMillis() - t0);
+                trace.setPassed(step.getScore() == null || step.getScore() >= PASS_SCORE);
+                turns.add(trace);
+                if (step.getScore() != null) scoreSum += step.getScore();
+                if (Boolean.TRUE.equals(step.getFinished())) break;
+            }
+        }
+        result.setTurns(turns);
+        result.setAvgScore(turns.isEmpty() ? 0 : scoreSum / turns.size());
+        return result;
+    }
+
+    @Override
+    public List<E2eReport> listRuns(Long companyId, Integer pageNum, Integer pageSize) {
+        if (e2eRunMapper == null) return Collections.emptyList();
+        List<LobsterE2eRun> all;
+        if (companyId != null) {
+            all = e2eRunMapper.selectByCompanyId(companyId);
+        } else {
+            all = e2eRunMapper.selectList(new QueryWrapper<LobsterE2eRun>().orderByDesc("create_time"));
+        }
+        if (all == null) return Collections.emptyList();
+        int page = pageNum != null && pageNum > 0 ? pageNum : 1;
+        int size = pageSize != null && pageSize > 0 ? pageSize : 20;
+        int from = (page - 1) * size;
+        int to = Math.min(from + size, all.size());
+        if (from >= all.size()) return Collections.emptyList();
+        List<E2eReport> reports = new ArrayList<>();
+        for (int i = from; i < to; i++) {
+            reports.add(toReport(all.get(i), null));
+        }
+        return reports;
+    }
+
+    private void resolveRequestFromScenario(E2eRequest req) {
+        if (req.getScenarioId() == null || testScenarioMapper == null) return;
+        try {
+            com.fs.company.domain.LobsterTestScenario scenario = testScenarioMapper.selectById(req.getScenarioId());
+            if (scenario == null) return;
+            if (req.getTemplateId() == null) req.setTemplateId(scenario.getTemplateId());
+            if (req.getBusinessDesc() == null) req.setBusinessDesc(scenario.getBusinessDesc());
+            if (req.getCompanyId() == null) req.setCompanyId(scenario.getCompanyId());
+            if (req.getUserInputs() == null || req.getUserInputs().isEmpty()) {
+                req.setUserInputs(parseUserInputs(scenario.getUserInputsJson()));
+            }
+        } catch (Exception e) {
+            logger.warn("[E2E] load scenario failed: {}", e.getMessage());
+        }
+    }
+
+    private List<String> parseUserInputs(String json) {
+        if (json == null || json.isBlank()) return Collections.emptyList();
+        try {
+            List<String> list = JSON.parseArray(json, String.class);
+            return list != null ? list : Collections.emptyList();
+        } catch (Exception e) {
+            return Arrays.asList(json.split("\n"));
+        }
+    }
+
+    private LobsterE2eRun initRun(String runId, Long companyId, E2eRequest req) {
+        LobsterE2eRun run = new LobsterE2eRun();
+        run.setRunId(runId);
+        run.setCompanyId(companyId);
+        run.setScenarioId(req.getScenarioId());
+        run.setTemplateId(req.getTemplateId());
+        run.setBusinessDesc(req.getBusinessDesc());
+        run.setStatus("RUNNING");
+        run.setPassedNodeCnt(0);
+        run.setTotalNodeCnt(0);
+        run.setDurationMs(0L);
+        run.setEvolutionCount(0);
+        run.setCreateTime(LocalDateTime.now());
+        return run;
+    }
+
+    private E2eReport persistFailed(String runId, Long companyId, E2eRequest req,
+                                    Long templateId, Long instanceId, String error, long startMs) {
+        LobsterE2eRun run = initRun(runId, companyId, req);
+        run.setTemplateId(templateId);
+        run.setInstanceId(instanceId);
+        run.setStatus("FAILED");
+        run.setErrorMsg(error);
+        run.setDurationMs(System.currentTimeMillis() - startMs);
+        if (e2eRunMapper != null) e2eRunMapper.insertRun(run);
+        return toReport(run, Collections.emptyList());
+    }
+
+    private E2eReport failedReport(String runId, String error) {
+        E2eReport r = new E2eReport();
+        r.setRunId(runId);
+        r.setStatus("FAILED");
+        r.setErrorMsg(error);
+        r.setNodeTraces(Collections.emptyList());
+        return r;
+    }
+
+    private void updateRun(LobsterE2eRun run) {
+        if (e2eRunMapper == null) return;
+        e2eRunMapper.updateStatus(run.getRunId(), run.getStatus(), run.getTotalScore(),
+                run.getPassedNodeCnt(), run.getTotalNodeCnt(), run.getDurationMs(),
+                run.getEvolutionCount(), run.getErrorMsg());
+    }
+
+    private void persistNodeTrace(String runId, NodeTrace trace) {
+        if (e2eRunNodeMapper == null) return;
+        LobsterE2eRunNode node = new LobsterE2eRunNode();
+        node.setRunId(runId);
+        node.setNodeSeq(trace.getNodeSeq());
+        node.setNodeCode(trace.getNodeCode());
+        node.setNodeType(trace.getNodeType());
+        node.setNodeName(trace.getNodeName());
+        node.setTurnNo(trace.getTurnNo());
+        node.setUserInput(trace.getUserInput());
+        node.setAiOutput(trace.getAiOutput());
+        if (trace.getScore() != null) {
+            node.setScore(BigDecimal.valueOf(trace.getScore()).setScale(2, RoundingMode.HALF_UP));
+        }
+        if (trace.getScoreDetail() != null) {
+            node.setScoreDetail(JSON.toJSONString(trace.getScoreDetail()));
+        }
+        node.setDurationMs(trace.getDurationMs());
+        node.setModelUsed(trace.getModelUsed());
+        node.setEvolutionHint(trace.getEvolutionHint());
+        node.setPassed(Boolean.TRUE.equals(trace.getPassed()) ? 1 : 0);
+        node.setErrorMsg(trace.getErrorMsg());
+        node.setCreateTime(LocalDateTime.now());
+        e2eRunNodeMapper.insertNode(node);
+    }
+
+    private List<NodeTrace> loadTraces(String runId) {
+        if (e2eRunNodeMapper == null) return Collections.emptyList();
+        List<LobsterE2eRunNode> nodes = e2eRunNodeMapper.selectByRunId(runId);
+        List<NodeTrace> traces = new ArrayList<>();
+        if (nodes == null) return traces;
+        for (LobsterE2eRunNode n : nodes) {
+            NodeTrace t = new NodeTrace();
+            t.setNodeSeq(n.getNodeSeq());
+            t.setNodeCode(n.getNodeCode());
+            t.setNodeType(n.getNodeType());
+            t.setNodeName(n.getNodeName());
+            t.setTurnNo(n.getTurnNo());
+            t.setUserInput(n.getUserInput());
+            t.setAiOutput(n.getAiOutput());
+            t.setScore(n.getScore() != null ? n.getScore().doubleValue() : null);
+            if (n.getScoreDetail() != null) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Integer> detail = JSON.parseObject(n.getScoreDetail(), Map.class);
+                    t.setScoreDetail(detail);
+                } catch (Exception ignored) { }
+            }
+            t.setDurationMs(n.getDurationMs());
+            t.setModelUsed(n.getModelUsed());
+            t.setEvolutionHint(n.getEvolutionHint());
+            t.setPassed(n.getPassed() != null && n.getPassed() == 1);
+            t.setErrorMsg(n.getErrorMsg());
+            traces.add(t);
+        }
+        return traces;
+    }
+
+    private E2eReport toReport(LobsterE2eRun run, List<NodeTrace> traces) {
+        E2eReport r = new E2eReport();
+        r.setRunId(run.getRunId());
+        r.setCompanyId(run.getCompanyId());
+        r.setTemplateId(run.getTemplateId());
+        r.setInstanceId(run.getInstanceId());
+        r.setScenarioId(run.getScenarioId());
+        r.setBusinessDesc(run.getBusinessDesc());
+        r.setTotalScore(run.getTotalScore() != null ? run.getTotalScore().doubleValue() : null);
+        r.setPassedNodeCnt(run.getPassedNodeCnt());
+        r.setTotalNodeCnt(run.getTotalNodeCnt());
+        r.setDurationMs(run.getDurationMs());
+        r.setStatus(run.getStatus());
+        r.setErrorMsg(run.getErrorMsg());
+        r.setEvolutionCount(run.getEvolutionCount());
+        if (traces != null) {
+            r.setNodeTraces(traces);
+        } else if (run.getRunId() != null) {
+            r.setNodeTraces(loadTraces(run.getRunId()));
+        }
+        if (run.getCreateTime() != null) {
+            r.setCreateTime(run.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        }
+        return r;
+    }
+
+    private Long extractInstanceId(Object data) {
+        if (data instanceof LobsterWorkflowInstance) {
+            return ((LobsterWorkflowInstance) data).getId();
+        }
+        if (data instanceof Map) {
+            Object id = ((Map<?, ?>) data).get("id");
+            if (id == null) id = ((Map<?, ?>) data).get("instanceId");
+            if (id instanceof Number) return ((Number) id).longValue();
+        }
+        return null;
+    }
+
+    private String extractMessage(Object data) {
+        if (data == null) return "";
+        if (data instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) data;
+            for (String key : new String[]{"message", "reply", "content", "aiReply"}) {
+                Object v = map.get(key);
+                if (v != null && !v.toString().isEmpty()) return v.toString();
+            }
+        }
+        return data.toString();
+    }
+
+    private void fillNodeMeta(Long companyId, Long instanceId, NodeTrace trace) {
+        Map<String, Object> state = workflowExecutor.getInstanceState(companyId, instanceId);
+        trace.setNodeCode(resolveNodeCode(companyId, instanceId, state));
+        if (workflowNodeMapper == null || workflowInstanceMapper == null) return;
+        try {
+            LobsterWorkflowInstance instance = workflowInstanceMapper.selectByIdAndCompanyId(instanceId, companyId);
+            if (instance == null) return;
+            Object idxObj = state.get("currentNodeIndex");
+            List<CompanyWorkflowLobsterNode> nodes = workflowNodeMapper.selectByWorkflowIdAndCompanyId(
+                    instance.getWorkflowId(), companyId);
+            if (nodes == null || idxObj == null) return;
+            int idx = ((Number) idxObj).intValue();
+            nodes.sort(Comparator.comparingInt(n -> n.getSortNo() != null ? n.getSortNo() : 0));
+            if (idx >= 0 && idx < nodes.size()) {
+                CompanyWorkflowLobsterNode node = nodes.get(idx);
+                trace.setNodeName(node.getNodeName());
+                trace.setNodeType(node.getNodeType() != null ? node.getNodeType().toString() : null);
+                if (trace.getNodeCode() == null) trace.setNodeCode(node.getNodeCode());
+            }
+        } catch (Exception e) {
+            logger.debug("[E2E] fillNodeMeta: {}", e.getMessage());
+        }
     }
 
-    public void recordResult(Long testId, Long companyId, boolean passed, String detail) {
-        if (auxMapper == null) return;
-        auxMapper.insertE2eResult(testId, companyId, passed, detail);
+    private String resolveCurrentNodeCode(Long companyId, Long instanceId) {
+        if (workflowInstanceMapper == null || workflowNodeMapper == null) {
+            return resolveNodeCode(companyId, instanceId, workflowExecutor.getInstanceState(companyId, instanceId));
+        }
+        LobsterWorkflowInstance inst = workflowInstanceMapper.selectByIdAndCompanyId(instanceId, companyId);
+        if (inst == null) return null;
+        List<CompanyWorkflowLobsterNode> nodes = workflowNodeMapper.selectByWorkflowIdAndCompanyId(
+                inst.getWorkflowId(), companyId);
+        if (nodes == null || inst.getCurrentNodeIndex() == null) return inst.getCurrentNodeName();
+        nodes.sort(Comparator.comparingInt(n -> n.getSortNo() != null ? n.getSortNo() : 0));
+        int idx = inst.getCurrentNodeIndex();
+        if (idx >= 0 && idx < nodes.size()) return nodes.get(idx).getNodeCode();
+        return inst.getCurrentNodeName();
     }
 
-    public List<Map<String, Object>> listTests(Long companyId, int page, int pageSize) {
-        if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectE2eTests(companyId, (page - 1) * pageSize, pageSize);
+    private String resolveNodeCode(Long companyId, Long instanceId, Map<String, Object> state) {
+        if (state == null || state.isEmpty()) {
+            state = workflowExecutor.getInstanceState(companyId, instanceId);
+        }
+        Object name = state.get("currentNodeName");
+        return name != null ? name.toString() : null;
     }
 
-    public Map<String, Object> getResult(String runId) {
-        if (auxMapper == null) return null;
-        return auxMapper.selectE2eResult(runId);
+    private double scoreReply(Long companyId, String aiOutput, String userInput, NodeTrace trace) {
+        if (qualityScoringService == null || aiOutput == null) return PASS_SCORE;
+        QualityScoringService.DetailedScore ds = qualityScoringService.score(
+                companyId, aiOutput, userInput, null, null, null);
+        Map<String, Integer> dims = new LinkedHashMap<>();
+        dims.put("relevance", ds.getRelevance());
+        dims.put("professionalism", ds.getProfessionalism());
+        dims.put("completeness", ds.getCompleteness());
+        dims.put("naturalness", ds.getNaturalness());
+        dims.put("compliance", ds.getCompliance());
+        dims.put("humanLikeliness", ds.getHumanLikeliness());
+        trace.setScoreDetail(dims);
+        return normalizeScore(ds);
     }
 
-    public List<Map<String, Object>> getResultList(Long testId) {
-        if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectE2eList(testId);
+    private int normalizeScore(QualityScoringService.DetailedScore ds) {
+        if (ds == null) return 0;
+        return (int) Math.round(ds.getTotalScore() * 100.0 / RAW_FULL_SCORE);
     }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterEvolutionEngineImpl.java

@@ -219,7 +219,7 @@ public class LobsterEvolutionEngineImpl implements LobsterEvolutionEngine {
 
             // Step 6: 工具调用检测与执行
             Map<String, Object> toolCall = toolCallFramework.extractToolCall(aiReply);
-            if (toolCall != null) {
+            if (toolCall != null && !toolCall.isEmpty() && toolCall.get("toolName") != null) {
                 String toolName = (String) toolCall.get("toolName");
                 @SuppressWarnings("unchecked")
                 Map<String, Object> toolParams = (Map<String, Object>) toolCall.get("parameters");

+ 165 - 12
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterTestScenarioServiceImpl.java

@@ -1,42 +1,195 @@
 package com.fs.company.service.workflow.impl;
 
+import com.alibaba.fastjson.JSON;
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.mapper.LobsterTestScenarioMapper;
+import com.fs.company.domain.LobsterTestScenario;
+import com.fs.company.service.workflow.LobsterE2eTestService;
+import com.fs.company.service.workflow.LobsterTestScenarioService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
 import java.util.*;
 
 @Service
-public class LobsterTestScenarioServiceImpl {
+public class LobsterTestScenarioServiceImpl implements LobsterTestScenarioService {
 
     private static final Logger logger = LoggerFactory.getLogger(LobsterTestScenarioServiceImpl.class);
 
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
-    public List<Map<String, Object>> list(Long companyId) {
+    @Autowired(required = false)
+    private LobsterTestScenarioMapper testScenarioMapper;
+
+    @Autowired(required = false)
+    private LobsterE2eTestService e2eTestService;
+
+    @Override
+    public List<Map<String, Object>> listScenarios(Long companyId, Integer enabled, Integer pageNum, Integer pageSize) {
         if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectTestScenarios(companyId);
+        return auxMapper.selectTestScenarios(companyId, enabled);
     }
 
-    public Map<String, Object> getById(Long id, Long companyId) {
+    @Override
+    public Map<String, Object> getScenario(Long id) {
         if (auxMapper == null) return null;
-        return auxMapper.selectTestScenarioById(id, companyId);
+        return auxMapper.selectTestScenarioById(id, null);
     }
 
-    public Long save(Long companyId, String name, Long workflowId, String testData) {
+    @Override
+    public Long createScenario(Map<String, Object> params) {
+        if (testScenarioMapper != null) {
+            LobsterTestScenario s = mapToEntity(params);
+            testScenarioMapper.insertScenario(s);
+            return s.getId();
+        }
         if (auxMapper == null) return null;
-        auxMapper.insertTestScenario(companyId, name, workflowId, testData);
+        Long companyId = toLong(params.get("companyId"));
+        String name = stringVal(params.get("scenarioName"), params.get("name"), "");
+        Long templateId = toLong(params.get("templateId"));
+        String userJson = toUserInputsJson(params);
+        auxMapper.insertTestScenario(companyId, name, templateId, userJson);
         return auxMapper.selectLastInsertId();
     }
 
-    public void update(Long id, String name, String testData) { if (auxMapper != null) auxMapper.updateTestScenario(id, name, testData); }
-    public void delete(Long id, Long companyId) { if (auxMapper != null) auxMapper.deleteTestScenario(id, companyId); }
-
-    public void recordResult(Long companyId, Long scenarioId, boolean passed, String detail) {
+    @Override
+    public void updateScenario(Long id, Map<String, Object> params) {
+        if (testScenarioMapper != null) {
+            LobsterTestScenario existing = testScenarioMapper.selectById(id);
+            if (existing == null) return;
+            if (params.get("scenarioName") != null) existing.setScenarioName(params.get("scenarioName").toString());
+            if (params.get("templateId") != null) existing.setTemplateId(toLong(params.get("templateId")));
+            if (params.get("businessDesc") != null) existing.setBusinessDesc(params.get("businessDesc").toString());
+            if (params.get("userInputs") != null) existing.setUserInputsJson(JSON.toJSONString(params.get("userInputs")));
+            if (params.get("minScore") != null) existing.setMinScore(new java.math.BigDecimal(params.get("minScore").toString()));
+            if (params.get("enabled") != null) existing.setEnabled(Integer.valueOf(params.get("enabled").toString()));
+            testScenarioMapper.updateById(existing);
+            return;
+        }
         if (auxMapper == null) return;
-        auxMapper.insertTestScenarioResult(companyId, scenarioId, passed, detail);
+        String name = (String) params.getOrDefault("scenarioName", null);
+        String testData = params.containsKey("userInputs") ? JSON.toJSONString(params.get("userInputs")) : null;
+        auxMapper.updateTestScenario(id, name, testData);
+    }
+
+    @Override
+    public void deleteScenario(Long id) {
+        if (testScenarioMapper != null) {
+            LobsterTestScenario s = testScenarioMapper.selectById(id);
+            if (s != null) {
+                s.setEnabled(0);
+                testScenarioMapper.updateById(s);
+            }
+            return;
+        }
+        if (auxMapper != null) auxMapper.deleteTestScenario(id, 0L);
+    }
+
+    @Override
+    public String runScenarioNow(Long id) {
+        if (e2eTestService == null) {
+            String runId = UUID.randomUUID().toString().substring(0, 8);
+            logger.warn("[Scenario] E2E service unavailable, stub runId={}", runId);
+            return runId;
+        }
+        Map<String, Object> scenario = getScenario(id);
+        if (scenario == null && testScenarioMapper != null) {
+            LobsterTestScenario entity = testScenarioMapper.selectById(id);
+            if (entity != null) scenario = entityToMap(entity);
+        }
+        if (scenario == null) {
+            logger.warn("[Scenario] scenario not found id={}", id);
+            return null;
+        }
+
+        LobsterE2eTestService.E2eRequest req = new LobsterE2eTestService.E2eRequest();
+        req.setScenarioId(id);
+        req.setCompanyId(toLong(scenario.get("company_id")) != null
+                ? toLong(scenario.get("company_id")) : toLong(scenario.get("companyId")));
+        req.setTemplateId(toLong(scenario.get("template_id")) != null
+                ? toLong(scenario.get("template_id")) : toLong(scenario.get("templateId")));
+        req.setBusinessDesc(stringVal(scenario.get("business_desc"), scenario.get("businessDesc"), null));
+
+        Object json = scenario.get("user_inputs_json");
+        if (json == null) json = scenario.get("userInputsJson");
+        if (json instanceof String) {
+            try {
+                req.setUserInputs(JSON.parseArray((String) json, String.class));
+            } catch (Exception e) {
+                req.setUserInputs(Arrays.asList(((String) json).split("\n")));
+            }
+        }
+
+        LobsterE2eTestService.E2eReport report = e2eTestService.runE2e(req);
+        String runId = report != null ? report.getRunId() : null;
+        if (testScenarioMapper != null && runId != null) {
+            testScenarioMapper.updateLastRun(id, runId, report.getStatus());
+        }
+        logger.info("[Scenario] runScenarioNow id={} runId={} status={}", id, runId,
+                report != null ? report.getStatus() : "null");
+        return runId;
+    }
+
+    @Override
+    public int runAllEnabledScenarios() {
+        List<Map<String, Object>> list = listScenarios(null, 1, 1, 1000);
+        int count = 0;
+        for (Map<String, Object> s : list) {
+            Object idObj = s.get("id");
+            if (idObj != null) {
+                runScenarioNow(Long.valueOf(idObj.toString()));
+                count++;
+            }
+        }
+        return count;
+    }
+
+    private LobsterTestScenario mapToEntity(Map<String, Object> params) {
+        LobsterTestScenario s = new LobsterTestScenario();
+        s.setCompanyId(toLong(params.get("companyId")));
+        s.setScenarioName(stringVal(params.get("scenarioName"), params.get("name"), "未命名场景"));
+        s.setTemplateId(toLong(params.get("templateId")));
+        s.setBusinessDesc((String) params.get("businessDesc"));
+        s.setUserInputsJson(toUserInputsJson(params));
+        if (params.get("minScore") != null) {
+            s.setMinScore(new java.math.BigDecimal(params.get("minScore").toString()));
+        }
+        Object enabled = params.get("enabled");
+        s.setEnabled(enabled != null ? Integer.valueOf(enabled.toString()) : 1);
+        s.setCreateTime(LocalDateTime.now());
+        return s;
+    }
+
+    private Map<String, Object> entityToMap(LobsterTestScenario s) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("id", s.getId());
+        m.put("company_id", s.getCompanyId());
+        m.put("template_id", s.getTemplateId());
+        m.put("business_desc", s.getBusinessDesc());
+        m.put("user_inputs_json", s.getUserInputsJson());
+        m.put("enabled", s.getEnabled());
+        return m;
+    }
+
+    private String toUserInputsJson(Map<String, Object> params) {
+        Object ui = params.get("userInputs");
+        if (ui != null) return JSON.toJSONString(ui);
+        return "[]";
+    }
+
+    private static Long toLong(Object o) {
+        if (o == null) return null;
+        if (o instanceof Number) return ((Number) o).longValue();
+        try { return Long.valueOf(o.toString()); } catch (Exception e) { return null; }
+    }
+
+    private static String stringVal(Object a, Object b, String def) {
+        if (a != null && !a.toString().isEmpty()) return a.toString();
+        if (b != null && !b.toString().isEmpty()) return b.toString();
+        return def;
     }
 }

+ 254 - 11
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterWorkflowExecutorImpl.java

@@ -20,6 +20,8 @@ import com.fs.company.domain.LobsterChatSession;
 import com.fs.company.domain.LobsterChatMsg;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.workflow.ConditionEvaluator;
+import com.fs.company.service.workflow.DynamicNodeExecutor;
+import com.fs.company.service.workflow.LobsterEvolutionEngine;
 import com.fs.company.service.workflow.LobsterWorkflowExecutor;
 import com.fs.company.service.workflow.VariableSubstitutionEngine;
 import com.fs.company.service.workflow.channel.MessageChannelRequest;
@@ -239,6 +241,12 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     @Autowired(required = false)
     private MultiTurnDialogueManager multiTurnDialogueManager;
 
+    @Autowired(required = false)
+    private LobsterEvolutionEngine lobsterEvolutionEngine;
+
+    @Autowired(required = false)
+    private DynamicNodeExecutor dynamicNodeExecutor;
+
     @Autowired(required = false)
     private com.fs.company.service.workflow.api.SmartApiCallNodeExecutor smartApiCallNodeExecutor;
 
@@ -277,6 +285,11 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         Integer nodeType = currentNode.getNodeType();
         String nodeCode = currentNode.getNodeCode();
+        variables.put("companyId", companyId);
+        variables.put("instanceId", instanceId);
+        variables.put("contactId", instance.getContactId());
+        variables.put("externalUserId", instance.getContactId() != null ? instance.getContactId().toString() : null);
+        String evolutionNextNodeCode = null;
 
         /*
          * ===== 处理客户回复:语义分析 + 节点类型路由 =====
@@ -375,7 +388,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             /* 7. 多轮对话检查:max_rounds > 0 且未达上限,停留当前节点 */
             Integer maxRounds = currentNode.getMaxRounds();
             if (maxRounds != null && maxRounds > 0 && nodeRound < maxRounds) {
-                String repeatMessage = generateNodeMessage(currentNode, variables);
+                String repeatMessage = evolveOrGenerateMessage(companyId, instance, currentNode, customerReply, variables);
                 instance.setVariables(JSON.toJSONString(variables));
                 instance.setLastActivityTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                 instance.setUpdateBy("system");
@@ -421,6 +434,47 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 }
                 variables.putAll(dialogueResult.getCollectedVariables());
             }
+
+            /* 8.5 12步进化引擎:对交互型节点生成 AI 回复 */
+            if (isEvolutionInteractiveNode(nodeType)) {
+                LobsterEvolutionEngine.EvolutionResult evo = invokeEvolution(
+                        instanceId, companyId, instance.getContactId(), customerReply, nodeCode, variables);
+                if (evo != null) {
+                    if (evo.isTransferredToHuman()) {
+                        return handleTransferHuman(companyId, instance, currentNode, variables, customerReply);
+                    }
+                    if (evo.getUpdatedVariables() != null) {
+                        variables.putAll(evo.getUpdatedVariables());
+                    }
+                    if (evo.getNextNodeCode() != null && !evo.getNextNodeCode().isEmpty()) {
+                        evolutionNextNodeCode = evo.getNextNodeCode();
+                    }
+                    String evoReply = evo.getReply();
+                    if (evoReply != null && !evoReply.isEmpty()) {
+                        instance.setVariables(JSON.toJSONString(variables));
+                        instance.setLastActivityTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+                        instance.setUpdateTime(DateUtils.getNowDate());
+                        instanceMapper.updateById(instance);
+                        logNodeExecution(companyId, instanceId, instance.getWorkflowId(), currentIndex,
+                                currentNode, evoReply, customerReply, "evo_reply");
+                        deliverMessage(companyId, instance.getContactId(), channelType, evoReply,
+                                variables, instanceId, instance.getWorkflowId());
+                        recordEvolutionOutcome(companyId, instance, variables, customerReply, evo);
+                        /* 多轮停留:已回复则不再推进 */
+                        if (maxRounds != null && maxRounds > 0 && nodeRound < maxRounds) {
+                            Map<String, Object> result = new HashMap<>();
+                            result.put("instanceId", instanceId);
+                            result.put("nodeIndex", currentIndex);
+                            result.put("nodeName", currentNode.getNodeName());
+                            result.put("message", evoReply);
+                            result.put("evolutionEngine", true);
+                            result.put("nodeRound", nodeRound);
+                            result.put("stayOnNode", true);
+                            return AjaxResult.success("进化引擎多轮回复", result);
+                        }
+                    }
+                }
+            }
         } else {
             /* 无客户回复:推进前先记录前序节点发送 */
             logNodeExecution(companyId, instanceId, instance.getWorkflowId(), currentIndex, currentNode, null, null, "received");
@@ -429,7 +483,8 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         /*
          * ===== 推进到下一节点 =====
          */
-        String nextNodeCode = determineNextNode(currentNode, variables);
+        String nextNodeCode = evolutionNextNodeCode != null ? evolutionNextNodeCode
+                : determineNextNode(currentNode, variables);
         int nextIndex = findNodeIndex(nodes, nextNodeCode, currentIndex + 1);
 
         if (nextIndex >= nodes.size() || nextIndex < 0) {
@@ -485,19 +540,29 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             return handleTagOperation(companyId, instance, nextNode, nodes, currentIndex, nextIndex, variables, channelType);
         }
 
-        /* 未知节点类型 → AI动态生成执行逻辑 */
+        /* 枚举对齐节点 → DynamicNodeExecutor(含 6-53/100/200 等) */
+        if (shouldUseDynamicExecutor(nextNode.getNodeType())) {
+            return advanceWithDynamicExecutor(companyId, instance, nextNode, nodes, currentIndex, nextIndex,
+                    variables, channelType, customerReply);
+        }
+
+        /* 未知节点类型 → DynamicNodeExecutor AI 兜底 */
         if (nextNode.getNodeType() != null && nextNode.getNodeType() > 0 &&
             nextNode.getNodeType() != NODE_TYPE_START && nextNode.getNodeType() != NODE_TYPE_END) {
             boolean isKnown = false;
             for (int knownType : new int[]{2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}) {
                 if (nextNode.getNodeType() == knownType) { isKnown = true; break; }
             }
+            if (!isKnown && dynamicNodeExecutor != null) {
+                return advanceWithDynamicExecutor(companyId, instance, nextNode, nodes, currentIndex, nextIndex,
+                        variables, channelType, customerReply);
+            }
             if (!isKnown) {
                 return handleUnknownNodeDynamically(companyId, instance, nextNode, nodes, currentIndex, nextIndex, variables, channelType);
             }
         }
 
-        String message = generateNodeMessage(nextNode, variables);
+        String message = evolveOrGenerateMessage(companyId, instance, nextNode, null, variables);
 
         instance.setCurrentNodeIndex(nextIndex);
         instance.setCurrentNodeName(nextNode.getNodeName());
@@ -791,8 +856,8 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
             MessageChannelResult sendResult = messageChannelRouter.route(request);
 
-            /* 同步写入chat_msg打通ChatSession聚合页面 */
-            bridgeToChatMsg(companyId, contactId, channelType, message, instanceId, sendResult.isSuccess());
+            /* 同步写入 chat_msg,供 ChatSession 聚合页展示 */
+            syncOutboundChatMsg(companyId, contactId, channelType, message, instanceId, sendResult.isSuccess());
 
             return sendResult;
         } catch (Exception e) {
@@ -812,12 +877,9 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * 同步写入chat_msg表 打通ChatSession聚合页面
-     * 以lobster_unified_contact为桥梁,支持任意渠道即插即用
-     * 
-     * 架构: contact_id(channelType) → lobster_unified_contact → chat_session(contact_id+channel_source_id)
+     * 渠道发送成功后,同步写入 chat_msg / chat_session(同库直写,非跨模块桥接)
      */
-    private void bridgeToChatMsg(Long companyId, Long contactId, String channelType,
+    private void syncOutboundChatMsg(Long companyId, Long contactId, String channelType,
                                   String message, Long instanceId, boolean success) {
         if (chatSessionMapper == null || chatMsgMapper == null || message == null) return;
         if (channelType == null) channelType = "QW";
@@ -913,6 +975,82 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         }
     }
 
+    private boolean isEvolutionInteractiveNode(Integer nodeType) {
+        if (nodeType == null) return false;
+        return nodeType == NODE_TYPE_AI_PROCESS || nodeType == 33
+                || nodeType == NODE_TYPE_COLLECT_INFO || nodeType == NODE_TYPE_HTTP_CALL
+                || nodeType == NODE_TYPE_RAG_QUERY || nodeType == NODE_TYPE_LOOP;
+    }
+
+    private LobsterEvolutionEngine.EvolutionResult invokeEvolution(Long instanceId, Long companyId,
+            Long contactId, String customerMessage, String nodeCode, Map<String, Object> variables) {
+        if (lobsterEvolutionEngine == null || customerMessage == null || customerMessage.isEmpty()) {
+            return null;
+        }
+        try {
+            String externalUserId = contactId != null ? contactId.toString()
+                    : (variables.get("externalUserId") != null ? variables.get("externalUserId").toString() : "unknown");
+            return lobsterEvolutionEngine.evolve(instanceId, companyId, externalUserId, customerMessage, nodeCode);
+        } catch (Exception e) {
+            logger.warn("[LobsterWorkflow] evolve failed instanceId={}: {}", instanceId, e.getMessage());
+            return null;
+        }
+    }
+
+    private String evolveOrGenerateMessage(Long companyId, LobsterWorkflowInstance instance,
+            CompanyWorkflowLobsterNode node, String customerReply, Map<String, Object> variables) {
+        if (customerReply != null && !customerReply.isEmpty() && lobsterEvolutionEngine != null) {
+            LobsterEvolutionEngine.EvolutionResult evo = invokeEvolution(
+                    instance.getId(), companyId, instance.getContactId(), customerReply,
+                    node.getNodeCode(), variables);
+            if (evo != null && evo.getReply() != null && !evo.getReply().isEmpty()) {
+                if (evo.getUpdatedVariables() != null) variables.putAll(evo.getUpdatedVariables());
+                recordEvolutionOutcome(companyId, instance, variables, customerReply, evo);
+                return evo.getReply();
+            }
+        }
+        return generateNodeMessage(node, variables);
+    }
+
+    private void recordEvolutionOutcome(Long companyId, LobsterWorkflowInstance instance,
+            Map<String, Object> variables, String customerReply,
+            LobsterEvolutionEngine.EvolutionResult evo) {
+        if (evolutionEngine == null) return;
+        try {
+            EvolutionContext context = new EvolutionContext();
+            context.setCompanyId(companyId);
+            context.setWorkflowId(instance.getWorkflowId());
+            context.setInstanceId(instance.getId());
+            context.setContactId(instance.getContactId());
+            context.setChannelType((String) variables.getOrDefault("channelType", "QW"));
+            context.setNodeCode(instance.getCurrentNodeName());
+            context.setCustomerReply(customerReply);
+            context.setSentMessage(evo.getReply());
+            context.setOutcome(mapCommercialOutcome(evo.getDetectedIntent(), customerReply));
+            context.setVariables(variables);
+            evolutionEngine.recordInteraction(context);
+        } catch (Exception e) {
+            logger.debug("[LobsterWorkflow] recordEvolutionOutcome: {}", e.getMessage());
+        }
+    }
+
+    private String mapCommercialOutcome(String intent, String customerReply) {
+        if (intent != null) {
+            String i = intent.toLowerCase();
+            if (i.contains("购买") || i.contains("下单") || i.contains("purchase")) return "purchase";
+            if (i.contains("咨询") || i.contains("inquiry")) return "inquiry";
+            if (i.contains("投诉") || i.contains("complaint")) return "complaint";
+            if (i.contains("预约") || i.contains("schedule")) return "schedule";
+        }
+        if (customerReply != null) {
+            String m = customerReply.toLowerCase();
+            if (m.contains("买") || m.contains("下单")) return "purchase";
+            if (m.contains("退款") || m.contains("投诉")) return "complaint";
+            if (m.contains("预约")) return "schedule";
+        }
+        return intent != null ? intent : "other";
+    }
+
     /**
      * 生成节点消息(千人千面/用户级优化版本)
      * 
@@ -1047,6 +1185,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         log.setCompanyId(companyId);
         log.setInstanceId(instanceId);
         log.setWorkflowId(workflowId);
+        log.setNodeCode(node.getNodeCode());
         log.setNodeIndex(nodeIndex);
         log.setNodeType(node.getNodeType() != null ? String.valueOf(node.getNodeType()) : "unknown");
         log.setNodeName(node.getNodeName());
@@ -1712,6 +1851,106 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         return AjaxResult.success(resultMsg, result);
     }
 
+    /**
+     * 是否通过 DynamicNodeExecutor 执行(与 LobsterNodeTypeEnum 对齐的已注册 handler)
+     */
+    private boolean shouldUseDynamicExecutor(Integer nodeType) {
+        if (nodeType == null || dynamicNodeExecutor == null) return false;
+        if (nodeType == 1 || nodeType == 2) return false;
+        return dynamicNodeExecutor.getRegisteredHandlers().containsKey(nodeType);
+    }
+
+    private String buildNodeConfigJson(CompanyWorkflowLobsterNode node) {
+        if (node.getNodeConfig() != null && !node.getNodeConfig().isEmpty()) {
+            return node.getNodeConfig();
+        }
+        JSONObject o = new JSONObject();
+        if (node.getMessageTemplate() != null) o.put("messageTemplate", node.getMessageTemplate());
+        return o.toJSONString();
+    }
+
+    /**
+     * 通过 DynamicNodeExecutor 推进节点(枚举类型 handler + AI 兜底)
+     */
+    private AjaxResult advanceWithDynamicExecutor(Long companyId, LobsterWorkflowInstance instance,
+                                                   CompanyWorkflowLobsterNode node,
+                                                   List<CompanyWorkflowLobsterNode> nodes,
+                                                   int currentIndex, int nextIndex,
+                                                   Map<String, Object> variables, String channelType,
+                                                   String customerReply) {
+        Long instanceId = instance.getId();
+        DynamicNodeExecutor.ExecutionContext ctx = DynamicNodeExecutor.ExecutionContext.builder()
+                .companyId(companyId)
+                .customerId(instance.getContactId())
+                .workflowInstanceId(instanceId)
+                .variables(variables)
+                .lastMessage(customerReply)
+                .channelType(channelType)
+                .build();
+
+        DynamicNodeExecutor.NodeExecutionResult exec = dynamicNodeExecutor.execute(
+                node.getNodeType(), buildNodeConfigJson(node), ctx);
+        if (!exec.isSuccess()) {
+            return AjaxResult.error(exec.getErrorMessage() != null ? exec.getErrorMessage() : "节点执行失败");
+        }
+        if (exec.getOutputVariables() != null) {
+            variables.putAll(exec.getOutputVariables());
+        }
+
+        if (node.getNodeType() != null && node.getNodeType() == 5) {
+            completeInstance(instance);
+            heartbeatScheduler.unregisterInstance(instanceId);
+            Map<String, Object> endResult = new HashMap<>();
+            endResult.put("instanceId", instanceId);
+            endResult.put("nodeName", node.getNodeName());
+            return AjaxResult.success("工作流已完成", endResult);
+        }
+
+        int effectiveNextIndex = nextIndex;
+        if (exec.getNextNodeCode() != null && !exec.getNextNodeCode().isEmpty()) {
+            int branchIdx = findNodeIndex(nodes, exec.getNextNodeCode(), nextIndex + 1);
+            if (branchIdx >= 0) effectiveNextIndex = branchIdx;
+        }
+
+        String message = exec.getMessageToSend();
+        if (message == null || message.isEmpty()) {
+            message = evolveOrGenerateMessage(companyId, instance, node, customerReply, variables);
+        }
+
+        instance.setCurrentNodeIndex(effectiveNextIndex);
+        instance.setCurrentNodeName(node.getNodeName());
+        instance.setCompletedNodes(currentIndex + 1);
+        instance.setVariables(JSON.toJSONString(variables));
+        instance.setLastActivityTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        instance.setUpdateBy("system");
+        instance.setUpdateTime(DateUtils.getNowDate());
+        instanceMapper.updateById(instance);
+
+        logNodeExecution(companyId, instanceId, instance.getWorkflowId(), effectiveNextIndex, node,
+                message, null, "sent");
+
+        MessageChannelResult sendResult = null;
+        if (message != null && !message.isEmpty()) {
+            sendResult = deliverMessage(companyId, instance.getContactId(), channelType, message, variables,
+                    instanceId, instance.getWorkflowId());
+            if (sendResult != null && !sendResult.isSuccess()) {
+                logger.warn("动态节点消息发送失败, instanceId={}, nodeType={}, error={}",
+                        instanceId, node.getNodeType(), sendResult.getErrorMsg());
+            }
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("instanceId", instanceId);
+        result.put("nodeIndex", effectiveNextIndex);
+        result.put("nodeName", node.getNodeName());
+        result.put("nodeType", node.getNodeType());
+        result.put("message", message);
+        result.put("dynamicExecutor", true);
+        result.put("channelType", channelType);
+        result.put("sendResult", sendResult);
+        return AjaxResult.success("动态节点执行成功", result);
+    }
+
     /**
      * 动态AI兜底:未知节点类型自动通过LLM推导执行逻辑并生成结果
      * 这是"即插即用"的关键——任何新节点类型无需改代码即可运行
@@ -1720,6 +1959,10 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                                                       CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
                                                       int currentIndex, int nextIndex, Map<String, Object> variables,
                                                       String channelType) {
+        if (dynamicNodeExecutor != null) {
+            return advanceWithDynamicExecutor(companyId, instance, node, nodes, currentIndex, nextIndex,
+                    variables, channelType, null);
+        }
         String nodeConfig = node.getNodeConfig() != null ? node.getNodeConfig() : "{}";
         java.util.Map<String, String> dynVars = new java.util.HashMap<>();
         dynVars.put("nodeName", node.getNodeName());

+ 308 - 24
fs-service/src/main/java/com/fs/company/service/workflow/impl/ToolCallFrameworkImpl.java

@@ -1,22 +1,47 @@
 package com.fs.company.service.workflow.impl;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.domain.LobsterToolConfig;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import com.fs.company.mapper.LobsterToolCallMapper;
+import com.fs.company.mapper.LobsterToolConfigMapper;
 import com.fs.company.service.workflow.ToolCallFramework;
+import com.fs.company.service.workflow.api.ApiRegistryService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @Service
 public class ToolCallFrameworkImpl implements ToolCallFramework {
 
     private static final Logger logger = LoggerFactory.getLogger(ToolCallFrameworkImpl.class);
+    private static final Pattern TOOL_JSON_PATTERN = Pattern.compile(
+            "\\{\\s*\"toolName\"\\s*:\\s*\"([^\"]+)\"[^}]*\"parameters\"\\s*:\\s*(\\{[^}]*\\})", Pattern.DOTALL);
 
     @Autowired(required = false)
     private LobsterToolCallMapper toolMapper;
 
+    @Autowired(required = false)
+    private LobsterToolConfigMapper toolConfigMapper;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    @Autowired(required = false)
+    private LobsterAuxiliaryMapper auxMapper;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final ConcurrentHashMap<String, Map<String, Object>> runtimeTools = new ConcurrentHashMap<>();
+
     public Map<String, Object> queryOrder(String orderNo, Long companyId) {
         return toolMapper != null ? toolMapper.selectOrder(orderNo, companyId) : null;
     }
@@ -30,53 +55,312 @@ public class ToolCallFrameworkImpl implements ToolCallFramework {
         toolMapper.insertSmsLog(companyId, phone, content);
     }
 
-    public List<Map<String, Object>> querySmsLogs(Long companyId) {
-        return toolMapper != null ? toolMapper.selectSmsLogs(companyId) : new ArrayList<>();
+    @Override
+    public ToolCallResult executeTool(String toolName, Map<String, Object> parameters, Long companyId) {
+        if (toolName == null || toolName.isBlank()) {
+            return ToolCallResult.fail("", "toolName empty");
+        }
+        long t0 = System.currentTimeMillis();
+        try {
+            Map<String, Object> params = parameters != null ? parameters : Collections.emptyMap();
+            ToolCallResult result;
+            switch (toolName) {
+                case "query_order":
+                    result = ToolCallResult.ok(toolName,
+                            queryOrder(String.valueOf(params.getOrDefault("orderNo", "")), companyId),
+                            System.currentTimeMillis() - t0);
+                    break;
+                case "query_user_orders":
+                    result = ToolCallResult.ok(toolName, queryUserOrders(companyId), System.currentTimeMillis() - t0);
+                    break;
+                case "send_sms":
+                    sendSms(companyId,
+                            String.valueOf(params.getOrDefault("phone", "")),
+                            String.valueOf(params.getOrDefault("content", "")));
+                    result = ToolCallResult.ok(toolName, Map.of("sent", true), System.currentTimeMillis() - t0);
+                    break;
+                case "apply_coupon":
+                    result = ToolCallResult.ok(toolName, applyCoupon(companyId, params), System.currentTimeMillis() - t0);
+                    break;
+                case "query_product":
+                    result = ToolCallResult.ok(toolName, queryProduct(companyId, params), System.currentTimeMillis() - t0);
+                    break;
+                case "update_crm_followup":
+                    result = ToolCallResult.ok(toolName, updateCrmFollowup(companyId, params), System.currentTimeMillis() - t0);
+                    break;
+                default:
+                    result = executeConfiguredTool(toolName, params, companyId, t0);
+            }
+            recordExecLog(companyId, toolName, JSON.toJSONString(params),
+                    result.isSuccess() ? JSON.toJSONString(result.getData()) : result.getError());
+            return result;
+        } catch (Exception e) {
+            logger.warn("[ToolCall] {} failed: {}", toolName, e.getMessage());
+            return ToolCallResult.fail(toolName, e.getMessage());
+        }
     }
 
-    public List<Map<String, Object>> queryByParams(String sql, List<Object> params) {
-        return toolMapper != null ? toolMapper.selectByParams(sql, params) : new ArrayList<>();
-    }
+    private ToolCallResult executeConfiguredTool(String toolName, Map<String, Object> params,
+                                                  Long companyId, long t0) {
+        LobsterToolConfig cfg = toolConfigMapper != null
+                ? toolConfigMapper.selectByCompanyAndName(companyId, toolName) : null;
+        String runtimeKey = companyId + ":" + toolName;
+        Map<String, Object> runtimeCfg = runtimeTools.get(runtimeKey);
 
-    public void recordExecLog(Long companyId, String toolName, String params, String result) {
-        if (toolMapper == null) return;
-        toolMapper.insertExecLog(companyId, toolName, params, result);
-    }
+        if (cfg == null && runtimeCfg == null) {
+            return ToolCallResult.fail(toolName, "tool not registered: " + toolName);
+        }
 
-    public List<Map<String, Object>> getRecentExecLogs(Long companyId, int limit) {
-        return toolMapper != null ? toolMapper.selectRecentExecLogs(companyId, limit) : new ArrayList<>();
-    }
+        String toolType = cfg != null ? cfg.getToolType() : String.valueOf(runtimeCfg.get("toolType"));
+        String configJson = cfg != null ? cfg.getConfigJson() : JSON.toJSONString(runtimeCfg.get("config"));
 
-    public String getOrderStatusDesc(String code) {
-        switch (code) {
-            case "0": return "待支付";
-            case "1": return "已支付";
-            default: return "未知";
+        if ("http".equalsIgnoreCase(toolType) || "builtin".equalsIgnoreCase(toolType)) {
+            return invokeHttpTool(toolName, configJson, params, t0);
         }
+        return ToolCallResult.fail(toolName, "unsupported toolType: " + toolType);
     }
 
-    @Override
-    public ToolCallResult executeTool(String toolName, Map<String, Object> parameters, Long companyId) {
-        return null;
+    private ToolCallResult invokeHttpTool(String toolName, String configJson,
+                                           Map<String, Object> params, long t0) {
+        try {
+            JSONObject cfg = configJson != null ? JSON.parseObject(configJson) : new JSONObject();
+            String url = cfg.getString("url");
+            String apiKey = cfg.getString("apiKey");
+            String method = cfg.getString("method");
+
+            if (url == null && apiKey != null && apiRegistryService != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get(apiKey);
+                if (ep != null) {
+                    url = ep.baseUrl;
+                    HttpHeaders headers = new HttpHeaders();
+                    apiRegistryService.buildAuthHeaders(apiKey).forEach(headers::set);
+                    HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, headers);
+                    ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+                    return ToolCallResult.ok(toolName, resp.getBody(), System.currentTimeMillis() - t0);
+                }
+            }
+
+            if (url == null) return ToolCallResult.fail(toolName, "missing url in tool config");
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, headers);
+            HttpMethod httpMethod = "GET".equalsIgnoreCase(method) ? HttpMethod.GET : HttpMethod.POST;
+            ResponseEntity<String> resp = restTemplate.exchange(url, httpMethod, entity, String.class);
+            return ToolCallResult.ok(toolName, resp.getBody(), System.currentTimeMillis() - t0);
+        } catch (Exception e) {
+            return ToolCallResult.fail(toolName, e.getMessage());
+        }
     }
 
     @Override
     public boolean isToolAvailable(String toolName, Long companyId) {
-        return false;
+        if (toolName == null) return false;
+        if ("query_order".equals(toolName) || "query_user_orders".equals(toolName) || "send_sms".equals(toolName)
+                || "apply_coupon".equals(toolName) || "query_product".equals(toolName)
+                || "update_crm_followup".equals(toolName)) {
+            return toolMapper != null || auxMapper != null;
+        }
+        if (toolConfigMapper != null) {
+            LobsterToolConfig cfg = toolConfigMapper.selectByCompanyAndName(companyId, toolName);
+            if (cfg != null && (cfg.getEnabled() == null || cfg.getEnabled() == 1)) return true;
+        }
+        return runtimeTools.containsKey(companyId + ":" + toolName);
     }
 
     @Override
     public List<Map<String, Object>> getAvailableTools(Long companyId) {
-        return List.of();
+        List<Map<String, Object>> list = new ArrayList<>();
+        list.add(toolMeta("query_order", "builtin", "查询订单"));
+        list.add(toolMeta("query_user_orders", "builtin", "查询用户订单列表"));
+        list.add(toolMeta("send_sms", "builtin", "发送短信"));
+        list.add(toolMeta("apply_coupon", "builtin", "发放优惠券"));
+        list.add(toolMeta("query_product", "builtin", "查询商品"));
+        list.add(toolMeta("update_crm_followup", "builtin", "更新CRM跟进"));
+        if (toolConfigMapper != null) {
+            List<LobsterToolConfig> cfgs = toolConfigMapper.selectEnabled(companyId);
+            if (cfgs != null) {
+                for (LobsterToolConfig c : cfgs) {
+                    list.add(toolMeta(c.getToolName(), c.getToolType(), c.getDescription()));
+                }
+            }
+        }
+        return list;
+    }
+
+    private Map<String, Object> toolMeta(String name, String type, String desc) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("toolName", name);
+        m.put("toolType", type);
+        m.put("description", desc);
+        return m;
     }
 
     @Override
     public void registerTool(Long companyId, String toolName, String toolType, Map<String, Object> config) {
-
+        if (companyId == null || toolName == null) return;
+        if (toolConfigMapper != null) {
+            LobsterToolConfig entity = new LobsterToolConfig();
+            entity.setCompanyId(companyId);
+            entity.setToolName(toolName);
+            entity.setToolType(toolType != null ? toolType : "http");
+            entity.setConfigJson(config != null ? JSON.toJSONString(config) : "{}");
+            entity.setEnabled(1);
+            entity.setDeleted(0);
+            toolConfigMapper.upsert(entity);
+        } else {
+            Map<String, Object> rt = new HashMap<>();
+            rt.put("toolType", toolType);
+            rt.put("config", config);
+            runtimeTools.put(companyId + ":" + toolName, rt);
+        }
     }
 
     @Override
     public Map<String, Object> extractToolCall(String aiReply) {
-        return Map.of();
+        if (aiReply == null || aiReply.isBlank()) return Collections.emptyMap();
+
+        try {
+            if (aiReply.trim().startsWith("{")) {
+                JSONObject obj = JSON.parseObject(aiReply.trim());
+                if (obj.containsKey("toolName")) {
+                    Map<String, Object> call = new LinkedHashMap<>();
+                    call.put("toolName", obj.getString("toolName"));
+                    call.put("parameters", obj.getJSONObject("parameters"));
+                    return call;
+                }
+            }
+        } catch (Exception ignored) { }
+
+        Matcher m = TOOL_JSON_PATTERN.matcher(aiReply);
+        if (m.find()) {
+            Map<String, Object> call = new LinkedHashMap<>();
+            call.put("toolName", m.group(1));
+            try {
+                call.put("parameters", JSON.parseObject(m.group(2)));
+            } catch (Exception e) {
+                call.put("parameters", Collections.emptyMap());
+            }
+            return call;
+        }
+
+        if (aiReply.contains("[TOOL:")) {
+            int start = aiReply.indexOf("[TOOL:") + 6;
+            int end = aiReply.indexOf("]", start);
+            if (end > start) {
+                String toolName = aiReply.substring(start, end).trim();
+                int jsonStart = aiReply.indexOf("{", end);
+                Map<String, Object> params = Collections.emptyMap();
+                if (jsonStart >= 0) {
+                    try {
+                        params = JSON.parseObject(aiReply.substring(jsonStart));
+                    } catch (Exception ignored) { }
+                }
+                Map<String, Object> call = new LinkedHashMap<>();
+                call.put("toolName", toolName);
+                call.put("parameters", params);
+                return call;
+            }
+        }
+        return Collections.emptyMap();
+    }
+
+    public void recordExecLog(Long companyId, String toolName, String params, String result) {
+        if (toolMapper == null) return;
+        toolMapper.insertExecLog(companyId, toolName, params, result);
+    }
+
+    private Map<String, Object> applyCoupon(Long companyId, Map<String, Object> params) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        String couponCode = String.valueOf(params.getOrDefault("couponCode",
+                params.getOrDefault("couponId", "DEFAULT")));
+        Object contactObj = params.get("contactId");
+        if (contactObj == null) contactObj = params.get("customerId");
+        Long contactId = contactObj instanceof Number ? ((Number) contactObj).longValue() : null;
+        result.put("couponCode", couponCode);
+        result.put("applied", true);
+        if (auxMapper != null && companyId != null) {
+            try {
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_coupon_record(company_id, customer_id, coupon_type, amount, description, create_time) " +
+                        "VALUES(%d, %s, '%s', %f, '%s', NOW())",
+                        companyId,
+                        contactId != null ? contactId : 0,
+                        sqlEscape(couponCode),
+                        parseDouble(params.get("amount"), 0),
+                        sqlEscape(String.valueOf(params.getOrDefault("desc", "AI tool apply_coupon")))));
+            } catch (Exception e) {
+                logger.debug("[ToolCall] apply_coupon record: {}", e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    private Map<String, Object> queryProduct(Long companyId, Map<String, Object> params) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        String productId = params.get("productId") != null ? params.get("productId").toString() : null;
+        String keyword = params.get("keyword") != null ? params.get("keyword").toString() : null;
+        List<Map<String, Object>> products = new ArrayList<>();
+        if (auxMapper != null && companyId != null) {
+            try {
+                if (productId != null && !productId.isEmpty()) {
+                    long pid = Long.parseLong(productId.replaceAll("[^0-9]", ""));
+                    products = auxMapper.queryForList(
+                            "SELECT id, product_name, product_url, price FROM lobster_product WHERE id="
+                                    + pid + " AND company_id=" + companyId + " LIMIT 1",
+                            companyId);
+                } else if (keyword != null && !keyword.isEmpty()) {
+                    products = auxMapper.queryForList(
+                            "SELECT id, product_name, product_url, price FROM lobster_product WHERE company_id="
+                                    + companyId + " AND product_name LIKE '%" + sqlEscape(keyword)
+                                    + "%' ORDER BY update_time DESC LIMIT 5",
+                            companyId);
+                } else if (auxMapper != null) {
+                    products = auxMapper.queryForList(
+                            "SELECT id, product_name, product_url, price FROM lobster_product WHERE company_id="
+                                    + companyId + " ORDER BY update_time DESC LIMIT 5",
+                            companyId);
+                }
+            } catch (Exception e) {
+                logger.debug("[ToolCall] query_product: {}", e.getMessage());
+            }
+        }
+        result.put("products", products);
+        result.put("count", products.size());
+        return result;
+    }
+
+    private Map<String, Object> updateCrmFollowup(Long companyId, Map<String, Object> params) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        Object contactObj = params.get("contactId");
+        if (contactObj == null) contactObj = params.get("customerId");
+        String contactId = contactObj != null ? contactObj.toString() : "unknown";
+        String followupContent = String.valueOf(params.getOrDefault("content",
+                params.getOrDefault("followupContent", "")));
+        String followupType = String.valueOf(params.getOrDefault("followupType", "ai_tool"));
+        result.put("contactId", contactId);
+        result.put("updated", true);
+        if (auxMapper != null && companyId != null && !followupContent.isEmpty()) {
+            try {
+                auxMapper.update(String.format(
+                        "INSERT INTO lobster_crm_followup(company_id, contact_id, followup_type, content, create_time) " +
+                        "VALUES(%d, '%s', '%s', '%s', NOW())",
+                        companyId, sqlEscape(contactId), sqlEscape(followupType), sqlEscape(followupContent)));
+            } catch (Exception e) {
+                logger.debug("[ToolCall] update_crm_followup: {}", e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    private static double parseDouble(Object v, double defaultVal) {
+        if (v == null) return defaultVal;
+        if (v instanceof Number) return ((Number) v).doubleValue();
+        try { return Double.parseDouble(v.toString()); } catch (Exception e) { return defaultVal; }
+    }
+
+    private static String sqlEscape(String val) {
+        if (val == null) return "";
+        return val.replace("'", "''").replace("\\", "\\\\");
     }
 }

+ 143 - 0
fs-service/src/main/java/com/fs/company/service/workflow/inbound/LobsterInboundService.java

@@ -0,0 +1,143 @@
+package com.fs.company.service.workflow.inbound;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.LobsterWorkflowInstance;
+import com.fs.company.mapper.CompanyWorkflowLobsterMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
+import com.fs.company.service.workflow.LobsterWorkflowExecutor;
+import com.fs.company.service.workflow.scheduler.WorkflowTriggerScheduler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 全渠道统一入站网关:客户消息 / 业务事件 -> 工作流实例
+ */
+@Service
+public class LobsterInboundService {
+
+    private static final Logger log = LoggerFactory.getLogger(LobsterInboundService.class);
+
+    @Autowired(required = false)
+    private LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper instanceMapper;
+
+    @Autowired(required = false)
+    private CompanyWorkflowLobsterMapper workflowMapper;
+
+    @Autowired(required = false)
+    private WorkflowTriggerScheduler workflowTriggerScheduler;
+
+    /**
+     * 处理入站客户消息
+     */
+    public AjaxResult handleInboundMessage(Long companyId, Long contactId, String channelType,
+                                           String message, Long workflowId, Map<String, Object> extra) {
+        if (workflowExecutor == null) {
+            return AjaxResult.error("工作流执行器不可用");
+        }
+        if (companyId == null || contactId == null) {
+            return AjaxResult.error("companyId/contactId 不能为空");
+        }
+        if (message == null || message.isBlank()) {
+            return AjaxResult.error("消息内容不能为空");
+        }
+        channelType = channelType != null ? channelType : "QW";
+
+        LobsterWorkflowInstance instance = findActiveInstance(companyId, contactId, workflowId);
+        if (instance == null) {
+            Long wfId = workflowId != null ? workflowId : resolveDefaultWorkflowId(companyId, channelType);
+            if (wfId == null) {
+                return AjaxResult.error("未找到可启动的工作流模板");
+            }
+            Map<String, Object> init = extra != null ? new HashMap<>(extra) : new HashMap<>();
+            init.put("channelType", channelType);
+            init.put("inboundSource", "gateway");
+            AjaxResult start = workflowExecutor.startWorkflow(companyId, wfId, contactId, init);
+            if (!Integer.valueOf(200).equals(start.get(AjaxResult.CODE_TAG))) {
+                return start;
+            }
+            instance = extractInstance(start.get(AjaxResult.DATA_TAG));
+            if (instance == null && instanceMapper != null) {
+                List<LobsterWorkflowInstance> active = instanceMapper.selectActiveByContactId(companyId, contactId);
+                if (active != null && !active.isEmpty()) {
+                    instance = active.get(0);
+                }
+            }
+        }
+
+        if (instance == null) {
+            return AjaxResult.error("无法创建或定位工作流实例");
+        }
+
+        log.info("[Inbound] company={} contact={} channel={} instance={} msgLen={}",
+                companyId, contactId, channelType, instance.getId(), message.length());
+        return workflowExecutor.executeNextNode(companyId, instance.getId(), message);
+    }
+
+    /**
+     * 处理业务事件(加粉/成单/打标签等)
+     */
+    public AjaxResult handleBusinessEvent(Long companyId, String eventType, Map<String, Object> payload) {
+        if (workflowTriggerScheduler == null) {
+            return AjaxResult.error("事件调度器不可用");
+        }
+        workflowTriggerScheduler.fireByEvent(eventType, companyId, payload);
+        return AjaxResult.success("事件已投递", Map.of("eventType", eventType, "companyId", companyId));
+    }
+
+    private LobsterWorkflowInstance findActiveInstance(Long companyId, Long contactId, Long workflowId) {
+        if (instanceMapper == null) return null;
+        List<LobsterWorkflowInstance> list = instanceMapper.selectActiveByContactId(companyId, contactId);
+        if (list == null || list.isEmpty()) return null;
+        if (workflowId == null) return list.get(0);
+        for (LobsterWorkflowInstance inst : list) {
+            if (workflowId.equals(inst.getWorkflowId())) return inst;
+        }
+        return list.get(0);
+    }
+
+    private Long resolveDefaultWorkflowId(Long companyId, String channelType) {
+        if (workflowMapper == null) return null;
+        try {
+            var q = new com.fs.company.domain.CompanyWorkflowLobster();
+            q.setCompanyId(companyId);
+            q.setStatus(1);
+            List<com.fs.company.domain.CompanyWorkflowLobster> list = workflowMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q)
+                            .orderByDesc("update_time").last("LIMIT 20"));
+            if (list == null || list.isEmpty()) return null;
+            for (var wf : list) {
+                if (wf.getCanvasData() != null && wf.getCanvasData().contains(channelType)) {
+                    return wf.getId();
+                }
+            }
+            return list.get(0).getId();
+        } catch (Exception e) {
+            log.warn("[Inbound] resolveDefaultWorkflowId failed: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private LobsterWorkflowInstance extractInstance(Object data) {
+        if (data instanceof LobsterWorkflowInstance) {
+            return (LobsterWorkflowInstance) data;
+        }
+        if (data instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) data;
+            Object id = map.get("instanceId");
+            if (id == null) id = map.get("id");
+            if (id instanceof Number && instanceMapper != null) {
+                return instanceMapper.selectById(((Number) id).longValue());
+            }
+        }
+        return null;
+    }
+}

+ 203 - 13
fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/TenantLearningEngineImpl.java

@@ -182,38 +182,228 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
     private int analyzeMessageEffectiveness(Long companyId) {
         if (learningMapper == null) return 0;
         try {
-            learningMapper.upsertPattern(companyId, "message", "effectiveness",
-                    "analyzed auto", 0.5, "auto");
-            return 1;
+            List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
+            if (events == null || events.isEmpty()) return 0;
+            int high = 0, low = 0, discoveries = 0;
+            StringBuilder highSamples = new StringBuilder();
+            StringBuilder lowSamples = new StringBuilder();
+            for (Map<String, Object> event : events) {
+                Integer score = event.get("quality_score") instanceof Number
+                        ? ((Number) event.get("quality_score")).intValue() : null;
+                String reply = (String) event.get("ai_reply");
+                if (reply == null || reply.isEmpty()) continue;
+                String snippet = reply.length() > 40 ? reply.substring(0, 40) + "..." : reply;
+                if (score != null && score >= 120) {
+                    high++;
+                    if (highSamples.length() < 200) highSamples.append(snippet).append("; ");
+                } else if (score != null && score < 80) {
+                    low++;
+                    if (lowSamples.length() < 200) lowSamples.append(snippet).append("; ");
+                }
+            }
+            if (high + low >= 3) {
+                double rate = high * 100.0 / Math.max(1, high + low);
+                String insight = "高质量回复" + high + "条,低质量" + low + "条,有效率"
+                        + String.format("%.0f%%", rate) + "。优质样例: " + highSamples;
+                learningMapper.upsertPattern(companyId, "message", "effectiveness", insight, rate / 100.0, "ReplayAnalyzer");
+                discoveries++;
+            }
+            if (low >= 2 && !lowSamples.toString().isEmpty()) {
+                learningMapper.upsertPattern(companyId, "message", "weak_replies",
+                        "待改进回复样例: " + lowSamples, 0.4, "ReplayAnalyzer");
+                discoveries++;
+            }
+            return discoveries;
         } catch (Exception e) { return 0; }
     }
 
     private int analyzeTimingOptimization(Long companyId) {
         if (learningMapper == null) return 0;
         try {
-            learningMapper.upsertPattern(companyId, "timing", "timing",
-                    "timing analyzed", 0.5, "auto");
-            return 1;
+            List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
+            if (events == null || events.size() < 5) return 0;
+            Map<Integer, Integer> hourCounts = new LinkedHashMap<>();
+            Map<Integer, Integer> hourHighQuality = new LinkedHashMap<>();
+            for (Map<String, Object> event : events) {
+                Object ct = event.get("create_time");
+                if (ct == null) continue;
+                int hour = -1;
+                if (ct instanceof java.sql.Timestamp) {
+                    hour = ((java.sql.Timestamp) ct).toLocalDateTime().getHour();
+                } else if (ct instanceof java.util.Date) {
+                    java.util.Calendar cal = java.util.Calendar.getInstance();
+                    cal.setTime((java.util.Date) ct);
+                    hour = cal.get(java.util.Calendar.HOUR_OF_DAY);
+                } else {
+                    String s = ct.toString();
+                    if (s.length() >= 13) {
+                        try { hour = Integer.parseInt(s.substring(11, 13)); } catch (Exception ignored) { }
+                    }
+                }
+                if (hour < 0) continue;
+                hourCounts.merge(hour, 1, Integer::sum);
+                Integer score = event.get("quality_score") instanceof Number
+                        ? ((Number) event.get("quality_score")).intValue() : null;
+                if (score != null && score >= 120) {
+                    hourHighQuality.merge(hour, 1, Integer::sum);
+                }
+            }
+            int discoveries = 0;
+            for (Integer hour : hourCounts.keySet()) {
+                int total = hourCounts.get(hour);
+                if (total < 2) continue;
+                int wins = hourHighQuality.getOrDefault(hour, 0);
+                double rate = wins * 100.0 / total;
+                String insight = hour + "点时段交互" + total + "次,高质量率" + String.format("%.0f%%", rate);
+                learningMapper.upsertPattern(companyId, "timing", "hour_" + hour, insight, rate / 100.0, "TimingAnalyzer");
+                discoveries++;
+            }
+            return discoveries;
         } catch (Exception e) { return 0; }
     }
 
     private int analyzeFlowBottlenecks(Long companyId) {
         if (learningMapper == null) return 0;
         try {
-            learningMapper.upsertPattern(companyId, "flow", "bottleneck",
-                    "bottleneck analyzed", 0.5, "auto");
-            return 1;
+            int discoveries = 0;
+            if (auxMapper != null) {
+                List<Map<String, Object>> stuck = auxMapper.selectEventStuckNodes(companyId, 30);
+                if (stuck != null) {
+                    for (Map<String, Object> row : stuck) {
+                        String nodeCode = String.valueOf(row.get("node_code"));
+                        Object cnt = row.get("stuck_count");
+                        Object wait = row.get("avg_wait");
+                        String insight = "节点[" + nodeCode + "]卡住" + cnt + "次,平均等待"
+                                + (wait != null ? wait + "秒" : "未知");
+                        learningMapper.upsertPattern(companyId, "flow", "stuck_" + nodeCode,
+                                insight, 0.8, "FlowAnalyzer");
+                        discoveries++;
+                    }
+                }
+                List<Map<String, Object>> stats = auxMapper.selectEventNodeStats(companyId);
+                if (stats != null) {
+                    Map<String, Integer> failByNode = new LinkedHashMap<>();
+                    for (Map<String, Object> row : stats) {
+                        String outcome = String.valueOf(row.get("outcome"));
+                        if ("fail".equalsIgnoreCase(outcome) || "error".equalsIgnoreCase(outcome)) {
+                            String nodeCode = String.valueOf(row.get("node_code"));
+                            int cnt = row.get("cnt") instanceof Number ? ((Number) row.get("cnt")).intValue() : 0;
+                            failByNode.merge(nodeCode, cnt, Integer::sum);
+                        }
+                    }
+                    for (Map.Entry<String, Integer> e : failByNode.entrySet()) {
+                        if (e.getValue() >= 2) {
+                            learningMapper.upsertPattern(companyId, "flow", "fail_" + e.getKey(),
+                                    "节点[" + e.getKey() + "]失败" + e.getValue() + "次", 0.7, "FlowAnalyzer");
+                            discoveries++;
+                        }
+                    }
+                }
+            }
+            if (discoveries == 0) {
+                List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
+                Map<String, Integer> nodeLow = new LinkedHashMap<>();
+                if (events != null) {
+                    for (Map<String, Object> event : events) {
+                        String nodeCode = (String) event.get("node_code");
+                        Integer score = event.get("quality_score") instanceof Number
+                                ? ((Number) event.get("quality_score")).intValue() : null;
+                        if (nodeCode != null && score != null && score < 80) {
+                            nodeLow.merge(nodeCode, 1, Integer::sum);
+                        }
+                    }
+                }
+                for (Map.Entry<String, Integer> e : nodeLow.entrySet()) {
+                    if (e.getValue() >= 2) {
+                        learningMapper.upsertPattern(companyId, "flow", "low_quality_" + e.getKey(),
+                                "节点[" + e.getKey() + "]低质量交互" + e.getValue() + "次", 0.6, "FlowAnalyzer");
+                        discoveries++;
+                    }
+                }
+            }
+            return discoveries;
         } catch (Exception e) { return 0; }
     }
 
-    private int analyzeCustomerProfileCorrelations(Long companyId) { return 0; }
+    private int analyzeCustomerProfileCorrelations(Long companyId) {
+        if (learningMapper == null) return 0;
+        try {
+            // 从replay buffer中分析客户画像与对话效果的关联
+            List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
+            if (events == null || events.isEmpty()) return 0;
+
+            int discoveries = 0;
+            Map<String, Integer> profileStats = new LinkedHashMap<>();
+            Map<String, Integer> profileWins = new LinkedHashMap<>();
+
+            for (Map<String, Object> event : events) {
+                String customerMessage = (String) event.get("customer_message");
+                Integer qualityScore = event.get("quality_score") instanceof Number
+                        ? ((Number) event.get("quality_score")).intValue() : null;
+
+                if (customerMessage == null || customerMessage.isEmpty()) continue;
+
+                String profileTag = inferProfileTag(customerMessage);
+                profileStats.merge(profileTag, 1, Integer::sum);
+                if (qualityScore != null && qualityScore >= 120) {
+                    profileWins.merge(profileTag, 1, Integer::sum);
+                }
+            }
+
+            for (String tag : profileStats.keySet()) {
+                int total = profileStats.getOrDefault(tag, 0);
+                int wins = profileWins.getOrDefault(tag, 0);
+                if (total >= 3) {
+                    double rate = wins * 100.0 / total;
+                    String insight = "画像[" + tag + "]: 总交互" + total + "次,高质量率" + String.format("%.0f%%", rate);
+                    learningMapper.upsertPattern(companyId, "profile", tag, insight, rate / 100.0, "ProfileAnalyzer");
+                    discoveries++;
+                }
+            }
+
+            return discoveries;
+        } catch (Exception e) { return 0; }
+    }
+
+    /** 从消息内容中推断客户画像标签 */
+    private String inferProfileTag(String message) {
+        if (message == null || message.isEmpty()) return "unknown";
+        if (message.contains("价格") || message.contains("优惠") || message.contains("便宜") || message.contains("贵"))
+            return "price_sensitive";
+        if (message.contains("品牌") || message.contains("质量") || message.contains("正品"))
+            return "quality_seeker";
+        if (message.contains("急") || message.contains("快") || message.contains("马上") || message.contains("立刻"))
+            return "urgent";
+        if (message.contains("推荐") || message.contains("哪个好") || message.contains("帮我选"))
+            return "guidance_seeker";
+        if (message.contains("退货") || message.contains("退款") || message.contains("投诉"))
+            return "at_risk";
+        if (message.contains("可以") || message.contains("好") || message.contains("喜欢") || message.contains("不错"))
+            return "satisfied";
+        return "general";
+    }
 
     private int generateStrategyRecommendations(Long companyId) {
         if (learningMapper == null) return 0;
         try {
-            learningMapper.upsertPattern(companyId, "strategy", "recommendation",
-                    "automated strategy", 0.6, "auto");
-            return 1;
+            List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
+            if (patterns == null || patterns.isEmpty()) return 0;
+            int discoveries = 0;
+            StringBuilder sb = new StringBuilder();
+            for (Map<String, Object> p : patterns) {
+                Object conf = p.get("confidence");
+                double c = conf instanceof Number ? ((Number) conf).doubleValue() : 0;
+                if (c >= 0.6) {
+                    sb.append("[").append(p.get("pattern_key")).append("] ")
+                            .append(p.get("pattern_value")).append("; ");
+                }
+            }
+            if (sb.length() > 0) {
+                learningMapper.upsertPattern(companyId, "strategy", "recommendation",
+                        "综合策略建议: " + sb, 0.65, "StrategySynthesizer");
+                discoveries++;
+            }
+            return discoveries;
         } catch (Exception e) { return 0; }
     }
 }

+ 22 - 3
fs-service/src/main/java/com/fs/company/service/workflow/pay/PayService.java

@@ -80,14 +80,33 @@ public class PayService {
             Map<String, Object> status = payRouter.queryOrder(gateway, orderNo);
             result.putAll(status);
         } else {
-            // Mock: 本地查库
-            result.put("status", "mock_pending");
+            // 无支付网关时查本地订单表
+            result.put("status", "UNKNOWN");
+            if (auxMapper != null && orderNo != null) {
+                try {
+                    List<Map<String, Object>> rows = auxMapper.queryForList(
+                            "SELECT status, amount, product_name, pay_time FROM lobster_pay_order WHERE order_no='"
+                                    + sqlEscape(orderNo) + "' LIMIT 1", null);
+                    if (rows != null && !rows.isEmpty()) {
+                        Map<String, Object> row = rows.get(0);
+                        result.put("status", row.getOrDefault("status", "CREATED"));
+                        result.put("amount", row.get("amount"));
+                        result.put("productName", row.get("product_name"));
+                        result.put("payTime", row.get("pay_time"));
+                    } else {
+                        result.put("status", "NOT_FOUND");
+                    }
+                } catch (Exception e) {
+                    log.warn("[Pay] 查单失败 orderNo={}: {}", orderNo, e.getMessage());
+                    result.put("status", "QUERY_ERROR");
+                }
+            }
         }
         return result;
     }
 
     private String buildMockPayUrl(String orderNo) {
-        return "https://pay.example.com/cashier?orderNo=" + orderNo;
+        return "/pay/cashier?orderNo=" + orderNo;
     }
 
     private String randomSuffix() {

+ 36 - 2
fs-service/src/main/java/com/fs/company/service/workflow/queue/DeadLetterQueue.java

@@ -61,12 +61,46 @@ public class DeadLetterQueue {
         return result;
     }
 
+    /**
+     * 获取待重试死信数量(retry_count < max_retries 的消息)
+     */
     public int getPendingCount() {
-        return getDeadLetterList().size();
+        if (auxMapper == null) return 0;
+        int pending = 0;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDeadLetters(null, 500);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    Object retryCount = row.get("retry_count");
+                    int count = retryCount instanceof Number ? ((Number) retryCount).intValue() : 0;
+                    if (count < 3) pending++;
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DeadLetter] getPendingCount failed: {}", e.getMessage());
+        }
+        return pending;
     }
 
+    /**
+     * 获取已死消息数量(retry_count >= max_retries 的消息,需人工处理)
+     */
     public int getDeadCount() {
-        return getDeadLetterList().size();
+        if (auxMapper == null) return 0;
+        int dead = 0;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDeadLetters(null, 500);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    Object retryCount = row.get("retry_count");
+                    int count = retryCount instanceof Number ? ((Number) retryCount).intValue() : 0;
+                    if (count >= 3) dead++;
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DeadLetter] getDeadCount failed: {}", e.getMessage());
+        }
+        return dead;
     }
 
     public int retryAllDead(Predicate<DeadMessage> filter) {

+ 195 - 88
fs-service/src/main/java/com/fs/company/service/workflow/scheduler/WorkflowTriggerScheduler.java

@@ -1,31 +1,38 @@
 package com.fs.company.service.workflow.scheduler;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.company.domain.CompanyWorkflowLobster;
+import com.fs.company.domain.CompanyWorkflowLobsterTask;
+import com.fs.company.domain.LobsterWorkflowInstance;
 import com.fs.company.mapper.CompanyWorkflowLobsterMapper;
+import com.fs.company.mapper.CompanyWorkflowLobsterTaskMapper;
+import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
+import com.fs.company.service.workflow.LobsterTestScenarioService;
 import com.fs.company.service.workflow.LobsterWorkflowExecutor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.support.CronExpression;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.PostConstruct;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * 工作流触发调度器(skill.md 要求)
- * <p>
- * 三种触发机制:
- *   1. 时间触发 — Cron / 固定时间 → 扫描启用的工作流并触发
- *   2. 事件触发 — 客户消息到达时由消息处理器主动调用 fire(eventType)
- *   3. 条件触发 — 定时巡检触发条件(活跃天数/沉默小时数等)
- * <p>
- * 实现:使用 Spring @Scheduled(每分钟检查一次),由 @EnableScheduling 启用
+ * 工作流触发调度器
+ * 1. 时间触发 — canvas fireTime + lobster_task Cron
+ * 2. 事件触发 — fireByEvent
+ * 3. 沉默唤醒 — 超时无活动实例自动推进
  */
 @Component
 public class WorkflowTriggerScheduler {
@@ -35,23 +42,26 @@ public class WorkflowTriggerScheduler {
     @Autowired(required = false)
     private CompanyWorkflowLobsterMapper workflowMapper;
 
+    @Autowired(required = false)
+    private CompanyWorkflowLobsterTaskMapper taskMapper;
+
+    @Autowired(required = false)
+    private LobsterWorkflowInstanceMapper instanceMapper;
+
     @Autowired(required = false)
     private LobsterWorkflowExecutor workflowExecutor;
 
     @Autowired(required = false)
-    private com.fs.company.service.workflow.LobsterTestScenarioService testScenarioService;
+    private LobsterTestScenarioService testScenarioService;
 
-    /** 最近一次触发时间(防重复) — workflowId → lastFireTime */
     private final ConcurrentHashMap<Long, LocalDateTime> lastFireTime = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<Long, LocalDateTime> lastTaskFire = new ConcurrentHashMap<>();
 
     @PostConstruct
     public void init() {
-        log.info("[WorkflowTriggerScheduler] 启动,每分钟扫描一次工作流触发");
+        log.info("[WorkflowTriggerScheduler] 已启动:定时/任务/Cron/沉默唤醒/E2E回归");
     }
 
-    /**
-     * 每天凌晨 3 点跑全部启用的 E2E 测试场景(回归测试)
-     */
     @Scheduled(cron = "0 0 3 * * ?")
     public void runDailyE2eRegression() {
         if (testScenarioService == null) return;
@@ -63,145 +73,242 @@ public class WorkflowTriggerScheduler {
         }
     }
 
-    /**
-     * 每分钟扫描一次启用状态的工作流,按其触发配置决定是否触发
-     */
     @Scheduled(cron = "0 * * * * ?")
     public void scanAndTrigger() {
+        scanCanvasFireTime();
+        scanLobsterTasks();
+    }
+
+    /** 每15分钟:沉默实例唤醒 */
+    @Scheduled(cron = "0 */15 * * * ?")
+    public void scanSilenceWakeUp() {
+        if (instanceMapper == null || workflowExecutor == null) return;
+        try {
+            CompanyWorkflowLobster q = new CompanyWorkflowLobster();
+            q.setStatus(1);
+            List<CompanyWorkflowLobster> workflows = workflowMapper != null
+                    ? workflowMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q))
+                    : null;
+            if (workflows == null) return;
+            int woken = 0;
+            for (CompanyWorkflowLobster wf : workflows) {
+                int silenceHours = parseSilenceHours(wf.getCanvasData());
+                if (silenceHours <= 0) continue;
+                List<LobsterWorkflowInstance> instances = instanceMapper.selectByWorkflowId(
+                        wf.getCompanyId(), wf.getId());
+                if (instances == null) continue;
+                for (LobsterWorkflowInstance inst : instances) {
+                    if (!"running".equals(inst.getStatus())) continue;
+                    if (isSilentTooLong(inst, silenceHours)) {
+                        workflowExecutor.executeNextNode(wf.getCompanyId(), inst.getId(),
+                                "[silence_wakeup] 您好,还有什么可以帮您的吗?");
+                        woken++;
+                    }
+                }
+            }
+            if (woken > 0) log.info("[WorkflowTriggerScheduler] 沉默唤醒 {} 个实例", woken);
+        } catch (Exception e) {
+            log.warn("[WorkflowTriggerScheduler] 沉默唤醒异常: {}", e.getMessage());
+        }
+    }
+
+    private void scanCanvasFireTime() {
         if (workflowMapper == null) return;
         try {
             CompanyWorkflowLobster q = new CompanyWorkflowLobster();
-            q.setStatus(1); // 仅启用
-            List<CompanyWorkflowLobster> list = workflowMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
+            q.setStatus(1);
+            List<CompanyWorkflowLobster> list = workflowMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
             if (list == null || list.isEmpty()) return;
-
             LocalDateTime now = LocalDateTime.now();
             int triggered = 0;
             for (CompanyWorkflowLobster wf : list) {
-                if (shouldTrigger(wf, now)) {
-                    fire(wf, now);
+                if (shouldTriggerCanvas(wf, now)) {
+                    fireWorkflow(wf, now, resolveContactId(wf, null));
                     triggered++;
                 }
             }
-            if (triggered > 0) {
-                log.info("[WorkflowTriggerScheduler] 本轮触发 {} 个工作流", triggered);
+            if (triggered > 0) log.info("[WorkflowTriggerScheduler] canvas 定时触发 {} 个", triggered);
+        } catch (Exception e) {
+            log.error("[WorkflowTriggerScheduler] canvas 扫描异常: {}", e.getMessage(), e);
+        }
+    }
+
+    private void scanLobsterTasks() {
+        if (taskMapper == null || workflowExecutor == null) return;
+        try {
+            Date now = new Date();
+            List<CompanyWorkflowLobsterTask> tasks = taskMapper.selectPendingTasks(null, now, 100);
+            if (tasks == null || tasks.isEmpty()) return;
+            for (CompanyWorkflowLobsterTask task : tasks) {
+                if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) {
+                    if (!shouldFireCron(task)) continue;
+                }
+                Long contactId = parseContactFromTaskContent(task.getTaskContent());
+                Map<String, Object> vars = new HashMap<>();
+                vars.put("triggerSource", "lobster_task");
+                vars.put("taskId", task.getId());
+                vars.put("taskName", task.getTaskName());
+                workflowExecutor.startWorkflow(task.getCompanyId(), task.getTemplateId(),
+                        contactId != null ? contactId : 0L, vars);
+                taskMapper.updateExecuteStatus(task.getId(), 2, null, now, null);
+                log.info("[WorkflowTriggerScheduler] 任务触发 template={} task={}", task.getTemplateId(), task.getId());
             }
         } catch (Exception e) {
-            log.error("[WorkflowTriggerScheduler] 扫描异常: {}", e.getMessage(), e);
+            log.warn("[WorkflowTriggerScheduler] 任务扫描异常: {}", e.getMessage());
         }
     }
 
-    /** 事件触发入口(消息到达/订单创建等场景,由业务方主动调用) */
     public void fireByEvent(String eventType, Long companyId, Object payload) {
-        log.info("[WorkflowTriggerScheduler] 事件触发 type={} company={} payload={}", eventType, companyId, payload);
-        if (workflowMapper == null || workflowExecutor == null) return;
+        log.info("[WorkflowTriggerScheduler] 事件触发 type={} company={}", eventType, companyId);
+        if (workflowMapper == null || workflowExecutor == null || companyId == null) return;
         try {
             CompanyWorkflowLobster q = new CompanyWorkflowLobster();
             q.setCompanyId(companyId);
             q.setStatus(1);
-            List<CompanyWorkflowLobster> list = workflowMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
+            List<CompanyWorkflowLobster> list = workflowMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
             if (list == null) return;
             for (CompanyWorkflowLobster wf : list) {
-                String evtCfg = parseEventTypeFromCanvas(wf.getCanvasData());
-                if (eventType != null && eventType.equalsIgnoreCase(evtCfg)) {
-                    Map<String, Object> vars = new HashMap<>();
-                    vars.put("eventType", eventType);
-                    if (payload != null) vars.put("payload", payload);
-                    Long contactId = payload instanceof Map ?
-                            (Long) ((Map<?, ?>) payload).get("contactId") : null;
-                    try {
-                        workflowExecutor.startWorkflow(companyId, wf.getId(), contactId, vars);
-                        log.info("[WorkflowTriggerScheduler] 事件 {} 启动工作流 id={}", eventType, wf.getId());
-                    } catch (Exception ex) {
-                        log.warn("[WorkflowTriggerScheduler] 启动工作流 {} 失败: {}", wf.getId(), ex.getMessage());
-                    }
+                if (!matchesEvent(wf.getCanvasData(), eventType)) continue;
+                Map<String, Object> vars = new HashMap<>();
+                vars.put("eventType", eventType);
+                vars.put("triggerSource", "event");
+                if (payload instanceof Map) {
+                    vars.putAll((Map<String, Object>) payload);
+                } else if (payload != null) {
+                    vars.put("payload", payload);
                 }
+                Long contactId = payload instanceof Map
+                        ? toLong(((Map<?, ?>) payload).get("contactId")) : 0L;
+                contactId = resolveContactId(wf, contactId);
+                workflowExecutor.startWorkflow(companyId, wf.getId(), contactId, vars);
+                log.info("[WorkflowTriggerScheduler] 事件 {} 启动工作流 id={} contact={}", eventType, wf.getId(), contactId);
             }
         } catch (Exception e) {
             log.error("[WorkflowTriggerScheduler] 事件触发异常: {}", e.getMessage(), e);
         }
     }
 
-    // ════════════ 私有方法 ════════════
+    private boolean matchesEvent(String canvasData, String eventType) {
+        if (eventType == null) return false;
+        String cfg = parseStartNodeConfig(canvasData, "eventType");
+        if (eventType.equalsIgnoreCase(cfg)) return true;
+        if (canvasData != null && canvasData.toLowerCase().contains("\"eventtype\":\"" + eventType.toLowerCase() + "\"")) {
+            return true;
+        }
+        return canvasData != null && canvasData.contains(eventType);
+    }
 
-    /**
-     * 判断工作流是否应在当前时刻触发
-     * 规则:
-     *   - 优先解析 canvasData 中的 startNode.config.fireTime(HH:mm)
-     *   - 同一工作流 1 分钟内不重复触发
-     */
-    private boolean shouldTrigger(CompanyWorkflowLobster wf, LocalDateTime now) {
+    private boolean shouldTriggerCanvas(CompanyWorkflowLobster wf, LocalDateTime now) {
         if (wf == null || wf.getId() == null) return false;
         LocalDateTime last = lastFireTime.get(wf.getId());
-        if (last != null && java.time.Duration.between(last, now).getSeconds() < 60) {
-            return false;
-        }
+        if (last != null && java.time.Duration.between(last, now).getSeconds() < 60) return false;
         String fireTime = parseFireTimeFromCanvas(wf.getCanvasData());
         if (fireTime != null && fireTime.matches("\\d{2}:\\d{2}")) {
             try {
                 LocalTime target = LocalTime.parse(fireTime);
                 LocalTime curr = now.toLocalTime();
-                return target.getHour() == curr.getHour() && target.getMinute() == curr.getMinute();
-            } catch (Exception ignored) {}
+                if (target.getHour() == curr.getHour() && target.getMinute() == curr.getMinute()) {
+                    lastFireTime.put(wf.getId(), now);
+                    return true;
+                }
+            } catch (Exception ignored) { }
         }
         return false;
     }
 
-    /** 从 canvasData JSON 抓 startNode.config.fireTime(HH:mm) */
-    private String parseFireTimeFromCanvas(String canvasData) {
-        if (canvasData == null || canvasData.isEmpty()) return null;
+    private boolean shouldFireCron(CompanyWorkflowLobsterTask task) {
         try {
-            com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSON.parseObject(canvasData);
-            com.alibaba.fastjson.JSONArray nodes = json.getJSONArray("nodes");
-            if (nodes == null) return null;
-            for (int i = 0; i < nodes.size(); i++) {
-                com.alibaba.fastjson.JSONObject node = nodes.getJSONObject(i);
-                if (node == null) continue;
-                Integer type = node.getInteger("type");
-                if (type != null && type == 1) { // 1=开始节点
-                    com.alibaba.fastjson.JSONObject cfg = node.getJSONObject("config");
-                    if (cfg != null) return cfg.getString("fireTime");
-                }
+            CronExpression cron = CronExpression.parse(task.getCronExpression());
+            LocalDateTime now = LocalDateTime.now();
+            LocalDateTime last = lastTaskFire.get(task.getId());
+            LocalDateTime from = last != null ? last : now.minusMinutes(2);
+            LocalDateTime next = cron.next(from.atZone(ZoneId.systemDefault()).toLocalDateTime());
+            if (next != null && !next.isAfter(now)) {
+                lastTaskFire.put(task.getId(), now);
+                return true;
             }
-        } catch (Exception ignored) {}
-        return null;
+        } catch (Exception e) {
+            log.debug("[WorkflowTriggerScheduler] cron parse fail task={}: {}", task.getId(), e.getMessage());
+        }
+        return false;
     }
 
-    private void fire(CompanyWorkflowLobster wf, LocalDateTime now) {
-        lastFireTime.put(wf.getId(), now);
-        log.info("[WorkflowTriggerScheduler] 触发工作流 id={} template={}", wf.getId(), wf.getTemplateName());
-        if (workflowExecutor == null) {
-            log.warn("[WorkflowTriggerScheduler] workflowExecutor 未注入,跳过");
-            return;
-        }
+    private void fireWorkflow(CompanyWorkflowLobster wf, LocalDateTime now, Long contactId) {
+        if (workflowExecutor == null) return;
         try {
             Map<String, Object> vars = new HashMap<>();
             vars.put("triggerSource", "scheduler");
             vars.put("triggerTime", now.toString());
-            // 定时触发不绑定具体 contact,contactId=null
-            workflowExecutor.startWorkflow(wf.getCompanyId(), wf.getId(), null, vars);
+            workflowExecutor.startWorkflow(wf.getCompanyId(), wf.getId(), contactId != null ? contactId : 0L, vars);
         } catch (Exception e) {
             log.error("[WorkflowTriggerScheduler] 工作流 {} 执行失败: {}", wf.getId(), e.getMessage(), e);
         }
     }
 
-    /** 从 canvasData startNode.config.eventType 解析事件触发类型 */
-    private String parseEventTypeFromCanvas(String canvasData) {
+    private Long resolveContactId(CompanyWorkflowLobster wf, Long fromPayload) {
+        if (fromPayload != null && fromPayload > 0) return fromPayload;
+        String contactStr = parseStartNodeConfig(wf.getCanvasData(), "defaultContactId");
+        if (contactStr != null) {
+            Long c = toLong(contactStr);
+            if (c != null && c > 0) return c;
+        }
+        return 0L;
+    }
+
+    private int parseSilenceHours(String canvasData) {
+        String v = parseStartNodeConfig(canvasData, "silenceHours");
+        if (v == null) return 0;
+        try { return Integer.parseInt(v); } catch (Exception e) { return 0; }
+    }
+
+    private boolean isSilentTooLong(LobsterWorkflowInstance inst, int silenceHours) {
+        String last = inst.getLastActivityTime();
+        if (last == null || last.isEmpty()) return false;
+        try {
+            LocalDateTime lastAct = LocalDateTime.parse(last,
+                    java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            return lastAct.plusHours(silenceHours).isBefore(LocalDateTime.now());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private Long parseContactFromTaskContent(String taskContent) {
+        if (taskContent == null || taskContent.isEmpty()) return null;
+        try {
+            JSONObject json = JSON.parseObject(taskContent);
+            return toLong(json.get("contactId"));
+        } catch (Exception e) { return null; }
+    }
+
+    private String parseFireTimeFromCanvas(String canvasData) {
+        return parseStartNodeConfig(canvasData, "fireTime");
+    }
+
+    private String parseStartNodeConfig(String canvasData, String key) {
         if (canvasData == null || canvasData.isEmpty()) return null;
         try {
-            com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSON.parseObject(canvasData);
-            com.alibaba.fastjson.JSONArray nodes = json.getJSONArray("nodes");
+            JSONObject json = JSON.parseObject(canvasData);
+            JSONArray nodes = json.getJSONArray("nodes");
             if (nodes == null) return null;
             for (int i = 0; i < nodes.size(); i++) {
-                com.alibaba.fastjson.JSONObject node = nodes.getJSONObject(i);
+                JSONObject node = nodes.getJSONObject(i);
                 if (node == null) continue;
                 Integer type = node.getInteger("type");
                 if (type != null && type == 1) {
-                    com.alibaba.fastjson.JSONObject cfg = node.getJSONObject("config");
-                    if (cfg != null) return cfg.getString("eventType");
+                    JSONObject cfg = node.getJSONObject("config");
+                    if (cfg != null) return cfg.getString(key);
                 }
             }
-        } catch (Exception ignored) {}
+        } catch (Exception ignored) { }
         return null;
     }
+
+    private static Long toLong(Object o) {
+        if (o == null) return 0L;
+        if (o instanceof Number) return ((Number) o).longValue();
+        try { return Long.valueOf(o.toString()); } catch (Exception e) { return 0L; }
+    }
 }

+ 191 - 9
fs-service/src/main/java/com/fs/company/service/workflow/vector/impl/VectorPatternMatcherImpl.java

@@ -1,28 +1,210 @@
 package com.fs.company.service.workflow.vector.impl;
 
-import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.domain.LobsterVectorStore;
+import com.fs.company.mapper.LobsterVectorStoreMapper;
+import com.fs.company.service.llm.MultiModelRouter;
+import com.fs.company.service.vector.EmbeddingService;
+import com.fs.company.service.workflow.vector.VectorPatternMatcher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 @Service
-public class VectorPatternMatcherImpl {
+public class VectorPatternMatcherImpl implements VectorPatternMatcher {
 
     private static final Logger logger = LoggerFactory.getLogger(VectorPatternMatcherImpl.class);
 
     @Autowired(required = false)
-    private LobsterAuxiliaryMapper auxMapper;
+    private LobsterVectorStoreMapper vectorStoreMapper;
 
-    public List<Map<String, Object>> searchSimilar(Long companyId, String docType, String query, int topK, double threshold) {
-        if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectVectorEmbeddings(companyId, docType);
+    @Autowired(required = false)
+    private EmbeddingService embeddingService;
+
+    @Autowired(required = false)
+    private MultiModelRouter multiModelRouter;
+
+    @Override
+    public void storeVector(Long companyId, String category, String key, String text, Map<String, Object> metadata) {
+        if (vectorStoreMapper == null || companyId == null || text == null || text.isBlank()) return;
+        try {
+            String vectorJson = buildEmbeddingJson(text);
+            LobsterVectorStore entity = new LobsterVectorStore();
+            entity.setCompanyId(companyId);
+            entity.setCategory(category != null ? category : "knowledge");
+            entity.setVecKey(key != null ? key : UUID.randomUUID().toString());
+            entity.setTextContent(text);
+            entity.setVector(vectorJson);
+            entity.setMetadata(metadata != null ? JSON.toJSONString(metadata) : "{}");
+            vectorStoreMapper.upsert(entity);
+        } catch (Exception e) {
+            logger.warn("[VectorPatternMatcher] storeVector failed: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public List<VectorMatchResult> searchSimilar(Long companyId, String category, String queryText,
+                                                  int topK, double minScore) {
+        if (vectorStoreMapper == null || companyId == null || queryText == null || queryText.isBlank()) {
+            return Collections.emptyList();
+        }
+        int limit = topK > 0 ? topK : 5;
+        try {
+            List<LobsterVectorStore> rows = vectorStoreMapper.selectByCompanyAndCategory(companyId, category);
+            if (rows == null || rows.isEmpty()) {
+                rows = keywordFallback(companyId, category, queryText, limit);
+            }
+            if (rows == null || rows.isEmpty()) return Collections.emptyList();
+
+            float[] queryVec = parseEmbedding(buildEmbeddingJson(queryText));
+            List<VectorMatchResult> scored = new ArrayList<>();
+            for (LobsterVectorStore row : rows) {
+                double score = scoreRow(queryVec, queryText, row);
+                if (score < minScore) continue;
+                VectorMatchResult r = new VectorMatchResult();
+                r.setId(row.getId());
+                r.setCompanyId(row.getCompanyId());
+                r.setCategory(row.getCategory());
+                r.setKey(row.getVecKey());
+                r.setText(row.getTextContent());
+                r.setScore(score);
+                if (row.getMetadata() != null) {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> meta = JSON.parseObject(row.getMetadata(), Map.class);
+                        r.setMetadata(meta);
+                    } catch (Exception ignored) {
+                        r.setMetadata(Collections.emptyMap());
+                    }
+                }
+                scored.add(r);
+            }
+            scored.sort((a, b) -> Double.compare(b.getScore(), a.getScore()));
+            return scored.stream().limit(limit).collect(Collectors.toList());
+        } catch (Exception e) {
+            logger.warn("[VectorPatternMatcher] searchSimilar failed: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public void deleteVector(Long companyId, String category, String key) {
+        if (vectorStoreMapper == null || companyId == null || key == null) return;
+        try {
+            vectorStoreMapper.deleteByKey(companyId, category, key);
+        } catch (Exception e) {
+            logger.warn("[VectorPatternMatcher] deleteVector failed: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public List<StrategyMatch> recommendByVector(Long companyId, String scenario, Map<String, Object> context) {
+        String query = scenario;
+        if (context != null && context.get("customerMessage") != null) {
+            query = query + " " + context.get("customerMessage");
+        }
+        List<VectorMatchResult> matches = searchSimilar(companyId, "strategy", query, 5, 0.35);
+        List<StrategyMatch> results = new ArrayList<>();
+        for (VectorMatchResult m : matches) {
+            StrategyMatch sm = new StrategyMatch();
+            sm.setStrategyContent(m.getText());
+            sm.setSemanticScore(m.getScore());
+            double perf = 0.5;
+            if (m.getMetadata() != null && m.getMetadata().get("performanceScore") instanceof Number) {
+                perf = ((Number) m.getMetadata().get("performanceScore")).doubleValue();
+            }
+            sm.setPerformanceScore(perf);
+            sm.setCombinedScore(m.getScore() * 0.7 + perf * 0.3);
+            sm.setMatchReason("语义相似度=" + String.format("%.2f", m.getScore()));
+            results.add(sm);
+        }
+        results.sort((a, b) -> Double.compare(b.getCombinedScore(), a.getCombinedScore()));
+        return results;
+    }
+
+    private List<LobsterVectorStore> keywordFallback(Long companyId, String category, String queryText, int limit) {
+        String keyword = queryText.length() > 20 ? queryText.substring(0, 20) : queryText;
+        return vectorStoreMapper.searchByKeyword(companyId, category, keyword, limit);
+    }
+
+    private double scoreRow(float[] queryVec, String queryText, LobsterVectorStore row) {
+        float[] rowVec = parseEmbedding(row.getVector());
+        if (queryVec != null && rowVec != null && queryVec.length == rowVec.length) {
+            return cosineSimilarity(queryVec, rowVec);
+        }
+        if (row.getTextContent() != null && queryText != null) {
+            String lowerQ = queryText.toLowerCase(Locale.ROOT);
+            String lowerT = row.getTextContent().toLowerCase(Locale.ROOT);
+            if (lowerT.contains(lowerQ) || lowerQ.contains(lowerT)) return 0.72;
+            long common = commonTokenCount(lowerQ, lowerT);
+            return common > 0 ? Math.min(0.65, 0.3 + common * 0.08) : 0.0;
+        }
+        return 0.0;
+    }
+
+    private String buildEmbeddingJson(String text) {
+        if (embeddingService != null) {
+            try {
+                List<Float> vec = embeddingService.embed(text);
+                if (vec != null && !vec.isEmpty()) return JSON.toJSONString(vec);
+            } catch (Exception e) {
+                logger.debug("[VectorPatternMatcher] EmbeddingService failed: {}", e.getMessage());
+            }
+        }
+        if (multiModelRouter != null) {
+            try {
+                String json = multiModelRouter.generateEmbedding(text);
+                if (json != null && json.startsWith("[")) return json;
+            } catch (Exception e) {
+                logger.debug("[VectorPatternMatcher] MultiModelRouter embedding failed: {}", e.getMessage());
+            }
+        }
+        return JSON.toJSONString(hashEmbedding(text, 128));
+    }
+
+    private float[] parseEmbedding(String vectorJson) {
+        if (vectorJson == null || vectorJson.isBlank() || "[]".equals(vectorJson.trim())) return null;
+        try {
+            List<Float> list = JSON.parseArray(vectorJson, Float.class);
+            if (list == null || list.isEmpty()) return null;
+            float[] arr = new float[list.size()];
+            for (int i = 0; i < list.size(); i++) arr[i] = list.get(i);
+            return arr;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private double cosineSimilarity(float[] a, float[] b) {
+        double dot = 0, na = 0, nb = 0;
+        for (int i = 0; i < a.length; i++) {
+            dot += a[i] * b[i];
+            na += a[i] * a[i];
+            nb += b[i] * b[i];
+        }
+        if (na == 0 || nb == 0) return 0;
+        return dot / (Math.sqrt(na) * Math.sqrt(nb));
+    }
+
+    private List<Float> hashEmbedding(String text, int dim) {
+        float[] arr = new float[dim];
+        for (String token : text.toLowerCase(Locale.ROOT).split("\\s+")) {
+            int h = token.hashCode();
+            int idx = Math.floorMod(h, dim);
+            arr[idx] += 1.0f;
+        }
+        List<Float> out = new ArrayList<>(dim);
+        for (float v : arr) out.add(v);
+        return out;
     }
 
-    public void indexDocument(Long companyId, String docType, String content, Map<String, String> metadata) {
-        if (auxMapper == null) return;
-        auxMapper.insertVectorEmbedding(companyId, metadata.getOrDefault("source", "manual"), docType, content, "[]");
+    private long commonTokenCount(String a, String b) {
+        Set<String> ta = Arrays.stream(a.split("\\s+")).filter(s -> s.length() > 1).collect(Collectors.toSet());
+        return Arrays.stream(b.split("\\s+")).filter(ta::contains).count();
     }
 }

+ 27 - 30
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -35,6 +35,7 @@ import com.fs.core.config.TenantConfigContext;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RandomRedPacketConfig;
 import com.fs.course.config.RandomRedPacketRule;
+import com.fs.course.config.RedPacketConfig;
 import com.fs.course.domain.*;
 import com.fs.course.dto.CoursePackageDTO;
 import com.fs.course.mapper.*;
@@ -83,6 +84,8 @@ import com.fs.sop.mapper.SopUserLogsInfoMapper;
 import com.fs.sop.service.ISopUserLogsInfoService;
 import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
 import com.fs.voice.utils.StringUtil;
 import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
 import com.volcengine.service.vod.IVodService;
@@ -128,6 +131,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private ISysConfigService sysConfigService;
     @Autowired
+    private TenantInfoService tenantInfoService;
+    @Autowired
     private OpenIMService openIMService;
     @Autowired
     private CompanyCompanyFsuserMapper companyCompanyFsuserMapper;
@@ -1172,8 +1177,21 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
     }
 
+    /**
+     * 获取当前租户的公司名称
+     * @return 租户名称,如果未获取到则返回null
+     */
+    private String getCurrentTenantCompanyName() {
+        Long tenantId = com.fs.wxcid.utils.TenantHelper.getTenantId();
+        if (tenantId == null) {
+            return null;
+        }
+        TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+        return tenantInfo != null ? tenantInfo.getTenantName() : null;
+    }
+
     private R addCustomerService(String qwUserById,String msg){
-        if ("济南联志健康".equals(sysConfigService.getProjectConfig().getCloudHost().getCompanyName())){
+        if ("济南联志健康".equals(getCurrentTenantCompanyName())){
             return R.error(400,msg).put("qrcode", "");
         }
         String json = configService.selectConfigByKey("course.config");
@@ -1675,40 +1693,19 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             amount = redPackage.getRedPacketMoney();
         } else if (video != null) {
             amount = video.getRedPacketMoney();
-            //是否开启了随机红包
-            String json = configService.selectConfigByKey("randomRedpacket:config");
-            if (StringUtils.isNotBlank(json)) {
-                RandomRedPacketConfig randomRedPacket = JSONUtil.toBean(json, RandomRedPacketConfig.class);
-                //是否开启拼手气红包
-                if (null != randomRedPacket && randomRedPacket.getEnableRandomRedpacket()) {
-                    BigDecimal randomMoney = BigDecimal.ZERO;
-                    //优先读取课程配置随机红包规则
-                    String randomRedPacketRules = video.getRandomRedPacketRules();
-                    if (StringUtils.isNotBlank(randomRedPacketRules)) {
-                        JSONArray array = JSONObject.parseArray(randomRedPacketRules);
-                        List<RandomRedPacketRule> rules = new ArrayList<>();
-                        for (Object o : array) {
-                            rules.add(JSONObject.toJavaObject((JSONObject) o, RandomRedPacketRule.class));
-                        }
-                        randomMoney = getRandomMoneyByRules(rules);
-                    }
-                    //如果课程没有配置 读取后台默认随机规则
-                    else {
-                        randomMoney = getRandomMoneyByRules(randomRedPacket.getRules());
-                    }
-                    //兼容拼手气红包报错情况的发放红包情况
-                    if (BigDecimal.ZERO.compareTo(randomMoney) < 0) {
-                        amount = randomMoney;
-                    }
-                }
-            }
         }
 
         // 准备发送红包参数
         WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+        String json = redisCache.getCacheObject("sys_config:redPacket.config.new");
 
+        RedPacketConfig config1 = JSONUtil.toBean(json, RedPacketConfig.class);
+        // 判断配置是否为空,为空则提示绑定商户号
+        if (config1 == null) {
+            throw new RuntimeException("请先绑定商户号后再进行操作");
+        }
         //判断是否走服务号openId发红包
-        if (config.getMiniAppAuthType() == 2 && user.getMpOpenId() != null && !sysConfigService.getProjectConfig().getIsNewWxMerchant()) {
+        if (config.getMiniAppAuthType() == 2 && user.getMpOpenId() != null && config1.getIsNew()!=1) {
             packetParam.setOpenId(user.getMpOpenId());
         } else {
             //查询是否绑定小程序
@@ -2614,7 +2611,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
 
         if (userCompanyUser.getStatus() == 2) {
-            if ("福本源".equals(sysConfigService.getProjectConfig().getCloudHost().getCompanyName())) {
+            if ("福本源".equals(getCurrentTenantCompanyName())) {
                 return ResponseResult.fail(ExceptionCodeEnum.SERVICE_UNAVAILABLE.getCode(), ExceptionCodeEnum.SERVICE_UNAVAILABLE.getDescription());
             } else {
                 return ResponseResult.fail(ExceptionCodeEnum.USER_BLACKLISTED.getCode(), ExceptionCodeEnum.USER_BLACKLISTED.getDescription());

+ 82 - 0
fs-service/src/main/java/com/fs/his/domain/SysRedpacketConfigMore.java

@@ -0,0 +1,82 @@
+package com.fs.his.domain;
+
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 多商户配置对象 sys_redpacket_config_more
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class SysRedpacketConfigMore {
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 0:老商户 商家转账到零钱 1:新商户 商家转账 */
+    @Excel(name = "0:老商户 商家转账到零钱 1:新商户 商家转账")
+    private Integer isNew;
+
+    /** 公众号appId */
+    @Excel(name = "公众号appId")
+    private String appId;
+
+    /** 小程序appId */
+    @Excel(name = "小程序appId")
+    private String miniappId;
+
+    /** 商户号 */
+    @Excel(name = "商户号")
+    private String mchId;
+
+    /** 商户密钥 */
+    @Excel(name = "商户密钥")
+    private String mchKey;
+
+    /** p12证书文件的绝对路径或者以classpath:开头的类路径. */
+    @Excel(name = "p12证书文件的绝对路径或者以classpath:开头的类路径.")
+    private String keyPath;
+
+    /** apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径. */
+    @Excel(name = "apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.")
+    private String privateKeyPath;
+
+    /** apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径. */
+    @Excel(name = "apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.")
+    private String privateCertPath;
+
+    /** apiV3 秘钥值. */
+    @Excel(name = "apiV3 秘钥值.")
+    private String apiV3Key;
+
+    /** 公钥ID */
+    @Excel(name = "公钥ID")
+    private String publicKeyId;
+
+    /** pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. */
+    @Excel(name = "pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.")
+    private String publicKeyPath;
+
+    /** 回调地址 */
+    @Excel(name = "回调地址")
+    private String notifyUrl;
+
+    /** $column.columnComment */
+    @Excel(name = "回调地址")
+    private String notifyUrlScrm;
+
+    /** 分配的租户ID */
+    @Excel(name = "分配的租户ID")
+    private Long tenantId;
+
+    /** 租户名称(非数据库字段) */
+    @TableField(exist = false)
+    private String tenantName;
+
+}

+ 63 - 0
fs-service/src/main/java/com/fs/his/mapper/SysRedpacketConfigMoreMapper.java

@@ -0,0 +1,63 @@
+package com.fs.his.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.SysRedpacketConfigMore;
+
+import java.util.List;
+
+/**
+ * 多商户配置Mapper接口
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+public interface SysRedpacketConfigMoreMapper extends BaseMapper<SysRedpacketConfigMore> {
+    /**
+     * 查询多商户配置
+     *
+     * @param id 多商户配置主键
+     * @return 多商户配置
+     */
+    SysRedpacketConfigMore selectSysRedpacketConfigMoreById(Long id);
+
+    /**
+     * 查询多商户配置列表
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 多商户配置集合
+     */
+    List<SysRedpacketConfigMore> selectSysRedpacketConfigMoreList(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 新增多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    int insertSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 修改多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    int updateSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 删除多商户配置
+     *
+     * @param id 多商户配置主键
+     * @return 结果
+     */
+    int deleteSysRedpacketConfigMoreById(Long id);
+
+    /**
+     * 批量删除多商户配置
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteSysRedpacketConfigMoreByIds(Long[] ids);
+}

+ 9 - 0
fs-service/src/main/java/com/fs/his/param/UpdateChangeMchIdParam.java

@@ -0,0 +1,9 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+@Data
+public class UpdateChangeMchIdParam {
+    private Long oldChangeMchId;
+    private Long newChangeMchId;
+}

+ 70 - 0
fs-service/src/main/java/com/fs/his/service/ISysRedpacketConfigMoreService.java

@@ -0,0 +1,70 @@
+package com.fs.his.service;
+
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.SysRedpacketConfigMore;
+import com.fs.his.param.UpdateChangeMchIdParam;
+
+import java.util.List;
+
+/**
+ * 多商户配置Service接口
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+public interface ISysRedpacketConfigMoreService extends IService<SysRedpacketConfigMore> {
+    /**
+     * 查询多商户配置
+     *
+     * @param id 多商户配置主键
+     * @return 多商户配置
+     */
+    SysRedpacketConfigMore selectSysRedpacketConfigMoreById(Long id);
+    Long getRedPacketConFig();
+    R updateChangeMchId(UpdateChangeMchIdParam param);
+    R clearRedPacketMchId();
+    void changeRedPacketConfig();
+
+
+    /**
+     * 查询多商户配置列表
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 多商户配置集合
+     */
+    List<SysRedpacketConfigMore> selectSysRedpacketConfigMoreList(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 新增多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    int insertSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 修改多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    int updateSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore);
+
+    /**
+     * 批量删除多商户配置
+     *
+     * @param ids 需要删除的多商户配置主键集合
+     * @return 结果
+     */
+    int deleteSysRedpacketConfigMoreByIds(Long[] ids);
+
+    /**
+     * 删除多商户配置信息
+     *
+     * @param id 多商户配置主键
+     * @return 结果
+     */
+    int deleteSysRedpacketConfigMoreById(Long id);
+}

+ 12 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -97,6 +97,7 @@ import com.github.binarywang.wxpay.service.TransferService;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.hc.openapi.tool.fastjson.JSON;
+import io.netty.util.internal.StringUtil;
 import me.chanjar.weixin.common.error.WxErrorException;
 
 import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -631,7 +632,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             // 根据红包模式获取配置
             switch (param.getRedPacketMode()){
                 case 1:
-                    json = configService.selectConfigByKey("redPacket.config");
+                    json = redisCache.getCacheObject("sys_config:redPacket.config.new");
+                    if (StringUtil.isNullOrEmpty(json) || json.isEmpty()) {
+                        json = configService.selectConfigByKey("redPacket.config");
+                    }
                     config = JSONUtil.toBean(json, RedPacketConfig.class);
                     break;
                 case 2:
@@ -674,6 +678,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                 WxPayException wxPayException = (WxPayException) e;
                 String customErrorMsg = wxPayException.getCustomErrorMsg();
                 if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    redisCache.incr("sys_config:redPacket.config.newCount",1L);
                     return R.error("[红包领取] 账户余额不足,请联系管理员!");
                 }
             }
@@ -761,7 +766,10 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             // 根据红包模式获取配置
             switch (param.getRedPacketMode()){
                 case 1:
-                    json = configService.selectConfigByKey("redPacket.config");
+                    json = redisCache.getCacheObject("sys_config:redPacket.config.new");
+                    if (StringUtil.isNullOrEmpty(json) || json.isEmpty()) {
+                        json = configService.selectConfigByKey("redPacket.config");
+                    }
                     config = JSONUtil.toBean(json, RedPacketConfig.class);
                     break;
                 case 2:
@@ -794,6 +802,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             result.put("isNew",config.getIsNew());
             logger.info("红包返回:{}",result);
 
+
             // 更新账户余额
             logger.info("[更新账户余额] 当前余额{} 更新后余额{}",companyMoney.toPlainString(),companyMoney.subtract(amount).toPlainString());
 
@@ -939,6 +948,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                 WxPayException wxPayException = (WxPayException) e;
                 String customErrorMsg = wxPayException.getCustomErrorMsg();
                 if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    redisCache.incr("sys_config:redPacket.config.newCount",1L);
                     return R.error("[红包领取] 账户余额不足,请联系管理员!");
                 }
             }

+ 220 - 0
fs-service/src/main/java/com/fs/his/service/impl/SysRedpacketConfigMoreServiceImpl.java

@@ -0,0 +1,220 @@
+package com.fs.his.service.impl;
+
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.config.RedPacketConfig;
+import com.fs.his.domain.SysRedpacketConfigMore;
+import com.fs.his.mapper.SysRedpacketConfigMoreMapper;
+import com.fs.his.param.UpdateChangeMchIdParam;
+import com.fs.his.service.ISysRedpacketConfigMoreService;
+import com.fs.system.service.ISysConfigService;
+import io.netty.util.internal.StringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 多商户配置Service业务层处理
+ *
+ * @author fs
+ * @date 2025-11-27
+ */
+@Slf4j
+@Service
+public class SysRedpacketConfigMoreServiceImpl extends ServiceImpl<SysRedpacketConfigMoreMapper, SysRedpacketConfigMore> implements ISysRedpacketConfigMoreService {
+
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 查询多商户配置
+     *
+     * @param id 多商户配置主键
+     * @return 多商户配置
+     */
+    @Override
+    public SysRedpacketConfigMore selectSysRedpacketConfigMoreById(Long id)
+    {
+        return baseMapper.selectSysRedpacketConfigMoreById(id);
+    }
+
+    @Override
+    public Long getRedPacketConFig() {
+        String json = redisCache.getCacheObject("sys_config:redPacket.config.new");
+        if (StringUtil.isNullOrEmpty(json) || json.isEmpty()) {
+            json = configService.selectConfigByKey("redPacket.config");
+        }
+
+        // 如果没有配置,返回null,前端显示"未配置"
+        if (StringUtil.isNullOrEmpty(json) || json.isEmpty()) {
+            return null;
+        }
+
+        RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+        if (config == null || config.getMchId() == null || config.getMchId().isEmpty()) {
+            return null;
+        }
+
+        return Long.valueOf(config.getMchId());
+    }
+
+    @Override
+    public R updateChangeMchId(UpdateChangeMchIdParam param) {
+
+        SysRedpacketConfigMore configMore = baseMapper.selectOne(new QueryWrapper<SysRedpacketConfigMore>().eq("mch_id", param.getNewChangeMchId()));
+
+        if (configMore == null || configMore.getMchId() == null) {
+            return R.error("商户号不存在");
+        }
+
+        RedPacketConfig config = new RedPacketConfig();
+        BeanCopyUtils.copy(configMore,config);
+
+        if (config.getMchId()!=null){
+            redisCache.setCacheObject("sys_config:redPacket.config.new", JSONUtil.toJsonStr(config));
+            redisCache.deleteObject("sys_config:redPacket.config.newCount");
+        }
+
+        return R.ok();
+    }
+
+    @Override
+    public R clearRedPacketMchId() {
+        // 删除 Redis 中的红包商户号配置
+        redisCache.deleteObject("sys_config:redPacket.config.new");
+        redisCache.deleteObject("sys_config:redPacket.config.newCount");
+        log.info("清空当前红包商户号配置");
+        return R.ok("已清空当前商户号配置");
+    }
+
+    @Override
+    public void changeRedPacketConfig() {
+        Integer count = redisCache.getCacheObject("sys_config:redPacket.config.newCount");
+        if (count >= 1000) {
+            String json = redisCache.getCacheObject("sys_config:redPacket.config.new");
+            if (StringUtil.isNullOrEmpty(json) || json.isEmpty()) {
+                json = configService.selectConfigByKey("redPacket.config");
+            }
+
+            RedPacketConfig config   = JSONUtil.toBean(json, RedPacketConfig.class);
+
+            SysRedpacketConfigMore configMore = baseMapper.selectOne(new QueryWrapper<SysRedpacketConfigMore>().eq("mch_id", config.getMchId()));
+
+            if (configMore == null) {
+                log.error("获取不到商户号");
+                return;
+            }
+
+            List<SysRedpacketConfigMore> configMoreList = baseMapper.selectList(
+                    new QueryWrapper<SysRedpacketConfigMore>().orderByAsc("id")
+            );
+
+            // 空列表处理
+            if (configMoreList.isEmpty()) {
+                log.error("商户号列表为空");
+            }
+
+
+            // 查找当前元素的索引
+            int currentIndex = -1;
+            Long currentId = configMore != null ? configMore.getId() : null;
+
+            for (int i = 0; i < configMoreList.size(); i++) {
+                if (configMoreList.get(i).getId().equals(currentId)) {
+                    currentIndex = i;
+                    break;
+                }
+            }
+
+            // 计算下一个索引(使用取模实现循环)
+            int nextIndex = (currentIndex + 1) % configMoreList.size();
+
+            SysRedpacketConfigMore configMoreNew = configMoreList.get(nextIndex);
+
+
+            RedPacketConfig configNew = new RedPacketConfig();
+            BeanCopyUtils.copy(configMoreNew,configNew);
+
+            if (configNew.getMchId()!=null){
+                log.info("自动切换商户号-原:"+configMore.getMchId()+"-新:"+configNew.getMchId());
+                redisCache.setCacheObject("sys_config:redPacket.config.new", JSONUtil.toJsonStr(configNew));
+                redisCache.deleteObject("sys_config:redPacket.config.newCount");
+            }
+        }
+    }
+
+
+
+
+    /**
+     * 查询多商户配置列表
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 多商户配置
+     */
+    @Override
+    public List<SysRedpacketConfigMore> selectSysRedpacketConfigMoreList(SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        return baseMapper.selectSysRedpacketConfigMoreList(sysRedpacketConfigMore);
+    }
+
+    /**
+     * 新增多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    @Override
+    public int insertSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        return baseMapper.insertSysRedpacketConfigMore(sysRedpacketConfigMore);
+    }
+
+    /**
+     * 修改多商户配置
+     *
+     * @param sysRedpacketConfigMore 多商户配置
+     * @return 结果
+     */
+    @Override
+    public int updateSysRedpacketConfigMore(SysRedpacketConfigMore sysRedpacketConfigMore)
+    {
+        return baseMapper.updateSysRedpacketConfigMore(sysRedpacketConfigMore);
+    }
+
+    /**
+     * 批量删除多商户配置
+     *
+     * @param ids 需要删除的多商户配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteSysRedpacketConfigMoreByIds(Long[] ids)
+    {
+        return baseMapper.deleteSysRedpacketConfigMoreByIds(ids);
+    }
+
+    /**
+     * 删除多商户配置信息
+     *
+     * @param id 多商户配置主键
+     * @return 结果
+     */
+    @Override
+    public int deleteSysRedpacketConfigMoreById(Long id)
+    {
+        return baseMapper.deleteSysRedpacketConfigMoreById(id);
+    }
+}

+ 44 - 10
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -26,30 +26,31 @@ public class PhoneUtil {
     * 解密
     */
     public static String decryptPhone(String encryptedText) {
-        String text=null;
+        String text = null;
         try {
             SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
             Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
             cipher.init(Cipher.DECRYPT_MODE, secretKey);
             byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
             text = new String(decryptedBytes);
-        } catch (Exception e) {
-            e.printStackTrace();
+        } catch (Exception ignored) {
+            // 非 AES 密文时解密失败属正常,由 decryptAutoPhone 兜底
         }
         return text;
     }
+
     /**
-    * 解密加*
-    */
+     * 解密加*
+     */
     public static String decryptPhoneMk(String encryptedText) {
-        String text=null;
+        String text = null;
         try {
             SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
             Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
             cipher.init(Cipher.DECRYPT_MODE, secretKey);
             byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
             text = new String(decryptedBytes);
-            text =text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+            text = text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
         } catch (Exception e) {
             e.printStackTrace();
         }
@@ -64,10 +65,43 @@ public class PhoneUtil {
             return null;
         }
         String text = encryptedText.trim();
-        if (text.length() > 11) {
-            return decryptPhone(text);
+        if (text.length() <= 11 && text.matches("\\d+")) {
+            return text;
         }
-        return text;
+        if (looksLikePlainPhone(text)) {
+            return text;
+        }
+        String decrypted = decryptPhone(text);
+        if (decrypted != null && !decrypted.isEmpty()) {
+            return decrypted;
+        }
+        return looksLikePlainPhone(text) ? text : null;
+    }
+
+    /**
+     * 黑名单校验用手机号:自动解密并归一化(去 +86、空格等)
+     */
+    public static String resolvePhoneForBlacklist(String phone) {
+        String decrypted = decryptAutoPhone(phone);
+        if (decrypted == null || decrypted.isEmpty()) {
+            return null;
+        }
+        return decrypted.replace(" ", "")
+                .replace("-", "")
+                .replace("+86", "")
+                .replace("(", "")
+                .replace(")", "");
+    }
+
+    private static boolean looksLikePlainPhone(String text) {
+        if (text == null || text.isEmpty()) {
+            return false;
+        }
+        if (!text.matches("^[+\\d\\-\\s()]+$")) {
+            return false;
+        }
+        String digits = text.replaceAll("\\D", "");
+        return digits.length() >= 11 && digits.length() <= 15;
     }
 
     public static String decryptAutoPhoneMk(String encryptedText) {

+ 30 - 32
fs-service/src/main/java/com/fs/hisStore/domain/FsUserScrm.java

@@ -41,10 +41,7 @@ public class FsUserScrm extends BaseEntity
     @Excel(name = "会员头像",sort = 2)
     private String avatar;
 
-    /** 用户昵称 */
-    @TableField(exist = false)
-    @Excel(name = "会员昵称", sort = 1)
-    private String nickname;
+
     private String nickName;
 
     /** 用户备注 */
@@ -196,26 +193,6 @@ public class FsUserScrm extends BaseEntity
         this.appId = appId;
     }
 
-    public void setNickName(String nickname)
-    {
-        if(StringUtils.isNotEmpty(nickname)){
-            this.nickName= EmojiParser.parseToHtmlDecimal(nickname);
-        }
-        else{
-            this.nickName= nickname;
-        }
-        this.nickname = this.nickName;
-    }
-
-    public String getNickName()
-    {
-        if(StringUtils.isNotEmpty(nickName)){
-            return EmojiParser.parseToUnicode(nickName);
-        }
-        else{
-            return nickName;
-        }
-    }
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @TableField(exist = false)
@@ -802,14 +779,6 @@ public class FsUserScrm extends BaseEntity
     }
 
 
-    public String getNickname() {
-        return nickname;
-    }
-
-    public void setNickname(String nickname) {
-        this.nickname = nickname;
-    }
-
     public Integer getLevel() {
         return level;
     }
@@ -853,4 +822,33 @@ public class FsUserScrm extends BaseEntity
     public void setHistoryApp(String historyApp) {
         this.historyApp = historyApp;
     }
+
+    public String getNickName() {
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+    @Override
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public String getCompanyUserNickName() {
+        return companyUserNickName;
+    }
+
+    public void setCompanyUserNickName(String companyUserNickName) {
+        this.companyUserNickName = companyUserNickName;
+    }
+
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    public void setCompanyName(String companyName) {
+        this.companyName = companyName;
+    }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов