فهرست منبع

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

xgb 1 هفته پیش
والد
کامیت
f827880f21
100فایلهای تغییر یافته به همراه5403 افزوده شده و 1225 حذف شده
  1. 2 0
      fs-admin-saas/src/main/java/com/fs/FsSaasAdminApplication.java
  2. 912 0
      fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java
  3. 43 0
      fs-admin-saas/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql
  4. 49 0
      fs-admin/src/main/java/com/fs/admin/controller/AdminCommGatewayLogController.java
  5. 69 10
      fs-admin/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java
  6. 0 637
      fs-admin/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java
  7. 126 0
      fs-admin/src/main/java/com/fs/admin/controller/AdminVoiceConfigController.java
  8. 1 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java
  9. 245 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java
  10. 93 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceController.java
  11. 2 2
      fs-admin/src/main/resources/application-dev.yml
  12. 1 1
      fs-agent/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java
  13. 43 0
      fs-agent/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql
  14. 3 0
      fs-comm-gateway/src/main/java/com/fs/comm/dto/CommCallSendRequest.java
  15. 2 0
      fs-comm-gateway/src/main/java/com/fs/comm/dto/CommSmsSendRequest.java
  16. 75 29
      fs-comm-gateway/src/main/java/com/fs/comm/service/CommCallService.java
  17. 316 0
      fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayApiLogRecorder.java
  18. 12 6
      fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayLineAuthService.java
  19. 71 22
      fs-comm-gateway/src/main/java/com/fs/comm/service/CommSmsService.java
  20. 144 0
      fs-comm-gateway/src/main/java/com/fs/comm/sms/MyCommSmsProvider.java
  21. 32 6
      fs-comm-gateway/对接文档.md
  22. 47 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyCommGatewayLogController.java
  23. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceApiController.java
  24. 0 3
      fs-company/src/main/java/com/fs/company/controller/fastGpt/FastgptChatArtificialWordsCompanyController.java
  25. 7 4
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  26. 176 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAdminController.java
  27. 131 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java
  28. 10 23
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java
  29. 8 22
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java
  30. 108 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterModelRouteController.java
  31. 30 8
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java
  32. 9 12
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java
  33. 21 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java
  34. 71 0
      fs-company/src/main/java/com/fs/company/controller/workflow/PayCallbackController.java
  35. 1 1
      fs-framework/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java
  36. 47 47
      fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java
  37. 52 0
      fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java
  38. 29 7
      fs-qw-api/src/main/java/com/fs/app/controller/QwExternalContactController.java
  39. 16 3
      fs-qw-api/src/main/java/com/fs/app/qwTask/qwTask.java
  40. 57 0
      fs-service/src/main/java/com/fs/comm/domain/CommGatewayApiLog.java
  41. 33 0
      fs-service/src/main/java/com/fs/comm/mapper/CommGatewayApiLogMapper.java
  42. 26 0
      fs-service/src/main/java/com/fs/comm/model/CommGatewayBillingQuote.java
  43. 18 0
      fs-service/src/main/java/com/fs/comm/model/CommSmsSendContext.java
  44. 51 0
      fs-service/src/main/java/com/fs/comm/service/CommGatewayApiLogServiceImpl.java
  45. 156 0
      fs-service/src/main/java/com/fs/comm/service/CommGatewayBillingService.java
  46. 114 22
      fs-service/src/main/java/com/fs/comm/service/CommSmsSendService.java
  47. 23 0
      fs-service/src/main/java/com/fs/comm/service/CommVoiceConfigMasterService.java
  48. 188 0
      fs-service/src/main/java/com/fs/comm/service/CommVoiceLimitService.java
  49. 18 0
      fs-service/src/main/java/com/fs/comm/service/ICommGatewayApiLogService.java
  50. 36 0
      fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelRequest.java
  51. 40 0
      fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelResult.java
  52. 12 0
      fs-service/src/main/java/com/fs/comm/sms/CommSmsProvider.java
  53. 22 0
      fs-service/src/main/java/com/fs/comm/support/CommTenantDataSourceHelper.java
  54. 44 3
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  55. 49 0
      fs-service/src/main/java/com/fs/company/domain/CompanyCommGatewayLog.java
  56. 53 60
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApi.java
  57. 58 133
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApiTenant.java
  58. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java
  59. 29 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChannelPluginConfig.java
  60. 22 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatMsg.java
  61. 28 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatSession.java
  62. 20 0
      fs-service/src/main/java/com/fs/company/domain/LobsterComplianceAudit.java
  63. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterConsumeRecord.java
  64. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterDedupConfig.java
  65. 11 2
      fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java
  66. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterProfileConfig.java
  67. 36 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSalesCorpus.java
  68. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSummaryConfig.java
  69. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTenantBalance.java
  70. 33 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTokenConsumption.java
  71. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyCommGatewayLogMapper.java
  72. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiMapper.java
  73. 32 58
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiTenantMapper.java
  74. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java
  75. 29 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerFactMapper.java
  76. 25 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerHabitMapper.java
  77. 29 6
      fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java
  78. 206 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java
  79. 27 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterBillingMapper.java
  80. 44 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelPluginConfigMapper.java
  81. 35 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelRegistryMapper.java
  82. 18 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatMsgMapper.java
  83. 28 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatRecordMapper.java
  84. 36 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatSessionMapper.java
  85. 14 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceAuditMapper.java
  86. 2 4
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java
  87. 25 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterConversationSummaryMapper.java
  88. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterDedupConfigMapper.java
  89. 21 28
      fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java
  90. 17 12
      fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java
  91. 88 5
      fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java
  92. 57 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterFeedbackMapper.java
  93. 11 5
      fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java
  94. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterKnowledgeUsageLogMapper.java
  95. 22 15
      fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java
  96. 65 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterMultiTurnDialogueMapper.java
  97. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterNodeExecutionLogMapper.java
  98. 49 27
      fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java
  99. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterProfileConfigMapper.java
  100. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSalesCorpusMapper.java

+ 2 - 0
fs-admin-saas/src/main/java/com/fs/FsSaasAdminApplication.java

@@ -7,6 +7,7 @@ import org.redisson.spring.starter.RedissonAutoConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.FilterType;
 import org.springframework.context.annotation.FilterType;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 
 
 /**
 /**
@@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, RedissonAutoConfiguration.class})
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, RedissonAutoConfiguration.class})
 @Transactional
 @Transactional
 @EnableAsync
 @EnableAsync
+@EnableScheduling
 public class FsSaasAdminApplication {
 public class FsSaasAdminApplication {
     public static void main(String[] args) {
     public static void main(String[] args) {
         SpringApplication.run(FsSaasAdminApplication.class, args);
         SpringApplication.run(FsSaasAdminApplication.class, args);

+ 912 - 0
fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java

@@ -0,0 +1,912 @@
+package com.fs.lobster.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.LobsterModelConfig;
+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.LobsterModelConfigService;
+import com.fs.framework.web.service.TokenService;
+import com.fs.proxy.domain.AiChatQualityRecord;
+import com.fs.proxy.service.AiChatQualityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 龙虾引擎管理端Controller(fs-admin-saas,替代原 AdminLobsterBridgeController)
+ * 全部使用 MyBatis Service,无 JdbcTemplate,无桥接镜像表
+ */
+@RestController
+public class LobsterAdminController extends BaseController {
+
+    @Autowired
+    private ILobsterSalesCorpusService salesCorpusService;
+
+    @Autowired
+    private ILobsterEventAuditService eventAuditService;
+
+    @Autowired
+    private ILobsterBillingService billingService;
+
+    @Autowired(required = false)
+    private LobsterModelConfigService modelConfigService;
+
+    @Autowired(required = false)
+    private AiChatQualityService aiChatQualityService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.config.LobsterCompanyConfigService companyConfigService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterE2eTestService e2eTestService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterTestScenarioService testScenarioService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.DynamicNodeImplService dynamicNodeImplService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
+
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatSessionMapper chatSessionMapper;
+
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatMsgMapper chatMsgMapper;
+
+    @Autowired
+    private TokenService tokenService;
+
+    // ======== 销冠语料 ========
+    @GetMapping({"/workflow/lobster/sales-corpus", "/workflow/lobster/sales-corpus/list",
+                 "/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
+    public AjaxResult lobsterCorpus(@RequestParam(defaultValue = "1") int page,
+                                     @RequestParam(defaultValue = "10") int size,
+                                     @RequestParam(required = false) String scenario,
+                                     @RequestParam(required = false) String status,
+                                     @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(salesCorpusService.listCorpus(page, size, companyId, scenario, status));
+    }
+
+    @GetMapping("/workflow/lobster/sales-corpus/scenarios")
+    public AjaxResult lobsterCorpusScenarios() {
+        return AjaxResult.success(salesCorpusService.getScenarios());
+    }
+
+    // ======== 节点审核(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/event-audit", "/workflow/lobster/event-audit/list",
+                 "/workflow/lobster/eventAudit", "/workflow/lobster/eventAudit/list"})
+    public AjaxResult lobsterEventAuditList(@RequestParam(defaultValue = "pending") String status,
+                                             @RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int size,
+                                             @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(eventAuditService.listAudits(status, page, size, companyId));
+    }
+
+    @GetMapping({"/workflow/lobster/event-audit/{id}", "/workflow/lobster/eventAudit/{id}"})
+    public AjaxResult lobsterEventAuditDetail(@PathVariable Long id) {
+        return AjaxResult.success(eventAuditService.getById(id));
+    }
+
+    @PostMapping({"/workflow/lobster/event-audit/approve/{id}", "/workflow/lobster/eventAudit/approve/{id}"})
+    public AjaxResult lobsterEventAuditApprove(@PathVariable Long id) {
+        LoginUser loginUser = (LoginUser) tokenService.getLoginUser(ServletUtils.getRequest());
+        eventAuditService.approve(id, loginUser.getUsername());
+        return AjaxResult.success("审批通过");
+    }
+
+    @PostMapping({"/workflow/lobster/event-audit/reject/{id}", "/workflow/lobster/eventAudit/reject/{id}"})
+    public AjaxResult lobsterEventAuditReject(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
+        LoginUser loginUser = (LoginUser) tokenService.getLoginUser(ServletUtils.getRequest());
+        String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
+        eventAuditService.reject(id, loginUser.getUsername(), comment);
+        return AjaxResult.success("已驳回");
+    }
+
+    // ======== Token计费(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
+    public AjaxResult lobsterBillingList(@RequestParam(defaultValue = "1") int page,
+                                          @RequestParam(defaultValue = "10") int size,
+                                          @RequestParam(required = false) Long tenantId) {
+        if (tenantId != null) {
+            return AjaxResult.success(billingService.listConsumeRecords(page, size, tenantId));
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster/billing/records")
+    public AjaxResult lobsterBillingRecords(@RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int size,
+                                             @RequestParam(required = false) Long tenantId) {
+        if (tenantId != null) {
+            return AjaxResult.success(billingService.listConsumeRecords(page, size, tenantId));
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster/billing/stats")
+    public AjaxResult lobsterBillingStats(@RequestParam(required = false) Long tenantId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalTokens", 0);
+        stats.put("totalCost", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster/billing/types")
+    public AjaxResult lobsterBillingTypes() {
+        List<Map<String, String>> types = new ArrayList<>();
+        String[] names = {"AI模型", "课程流量", "直播流量", "Token", "短信", "人工外呼", "AI外呼", "微助手"};
+        for (int i = 0; i < names.length; i++) {
+            Map<String, String> m = new LinkedHashMap<>();
+            m.put("type", String.valueOf(i + 1));
+            m.put("name", names[i]);
+            types.add(m);
+        }
+        return AjaxResult.success(types);
+    }
+
+    // ======== Token消耗统计(管理端,非桥接) ========
+    @GetMapping("/workflow/lobster/token-stats/daily")
+    public AjaxResult tokenStatsDaily(@RequestParam Long companyId,
+                                       @RequestParam String startDate,
+                                       @RequestParam String endDate) {
+        return AjaxResult.success(billingService.getTokenDailySummary(companyId, startDate, endDate));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/model")
+    public AjaxResult tokenStatsModel(@RequestParam Long companyId,
+                                       @RequestParam String startDate,
+                                       @RequestParam String endDate) {
+        return AjaxResult.success(billingService.getTokenModelSummary(companyId, startDate, endDate));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/instance")
+    public AjaxResult tokenStatsInstance(@RequestParam Long companyId) {
+        return AjaxResult.success(billingService.getTokenInstanceSummary(companyId));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/records")
+    public AjaxResult tokenStatsRecords(@RequestParam(defaultValue = "1") int page,
+                                         @RequestParam(defaultValue = "10") int size,
+                                         @RequestParam Long companyId) {
+        return AjaxResult.success(billingService.listTokenRecords(page, size, companyId));
+    }
+
+    // ======== 以下为占位端点(无 MyBatis Service 实现的,返回空数据,前端不报 404) ========
+
+    @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
+    public AjaxResult lobsterGenerate() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
+    public AjaxResult lobsterCanvas() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
+    public AjaxResult lobsterTemplate() {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        List<Map<String, Object>> list = jdbcTemplate.queryForList(
+            "SELECT id, template_code, template_name, industry_type, description, status, version, create_time, update_time " +
+            "FROM company_workflow_lobster WHERE del_flag=0 AND status=1 ORDER BY update_time DESC LIMIT 200");
+        return AjaxResult.success(list);
+    }
+
+    /** 获取工作流节点列表(含模板信息) */
+    @GetMapping("/workflow/lobster/nodes/{workflowId}")
+    public AjaxResult getWorkflowNodes(@PathVariable Long workflowId) {
+        if (jdbcTemplate == null) return AjaxResult.error("DB不可用");
+        Map<String, Object> template = jdbcTemplate.queryForMap(
+            "SELECT id, template_code, template_name, industry_type, description, status " +
+            "FROM company_workflow_lobster WHERE id=? AND del_flag=0", workflowId);
+        List<Map<String, Object>> nodes = jdbcTemplate.queryForList(
+            "SELECT id, workflow_id, node_code, node_name, node_type, sort_no, " +
+            "next_node_code, message_template, condition_expr, node_config, scene_code, model_name, send_time, max_round " +
+            "FROM company_workflow_lobster_node WHERE workflow_id=? AND del_flag=0 ORDER BY sort_no", workflowId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("template", template);
+        result.put("nodes", nodes);
+        return AjaxResult.success(result);
+    }
+
+    /** 保存工作流节点(先删后插) */
+    @PostMapping("/workflow/lobster/nodes/save")
+    public AjaxResult saveWorkflowNodes(@RequestBody Map<String, Object> body) {
+        if (jdbcTemplate == null) return AjaxResult.error("DB不可用");
+        Long workflowId = toLong(body.get("workflowId"));
+        if (workflowId == null) return AjaxResult.error("workflowId必填");
+        // 更新模板头
+        String templateName = (String) body.get("templateName");
+        String industryType = (String) body.get("industryType");
+        String description = (String) body.get("description");
+        if (templateName != null) {
+            jdbcTemplate.update(
+                "UPDATE company_workflow_lobster SET template_name=?, industry_type=?, description=?, update_time=NOW() WHERE id=?",
+                templateName, industryType, description, workflowId);
+        }
+        // 清空旧节点
+        jdbcTemplate.update("UPDATE company_workflow_lobster_node SET del_flag=1, update_time=NOW() WHERE workflow_id=?", workflowId);
+        // 插入新节点
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> nodes = (List<Map<String, Object>>) body.get("nodes");
+        if (nodes != null) {
+            for (Map<String, Object> n : nodes) {
+                jdbcTemplate.update(
+                    "INSERT INTO company_workflow_lobster_node(workflow_id, node_code, node_name, node_type, sort_no, " +
+                    "next_node_code, message_template, condition_expr, node_config, scene_code, model_name, send_time, max_round, create_time) " +
+                    "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())",
+                    workflowId,
+                    n.getOrDefault("nodeCode", ""),
+                    n.getOrDefault("nodeName", ""),
+                    toInt(n, "nodeType", 2),
+                    toInt(n, "sortNo", 0),
+                    n.getOrDefault("nextNodeCode", null),
+                    n.getOrDefault("messageTemplate", null),
+                    n.getOrDefault("conditionExpr", null),
+                    n.getOrDefault("nodeConfig", null),
+                    n.getOrDefault("sceneCode", null),
+                    n.getOrDefault("modelName", null),
+                    n.getOrDefault("sendTime", null),
+                    toInt(n, "maxRound", 0));
+            }
+        }
+        return AjaxResult.success("保存成功");
+    }
+
+    private int toInt(Map<String, Object> map, String key, int def) {
+        Object v = map.get(key);
+        return v instanceof Number ? ((Number) v).intValue() : def;
+    }
+
+    @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
+    public AjaxResult lobsterInstance() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster/instance/stats")
+    public AjaxResult lobsterInstanceStats() {
+        Map<String, Object> stats = new HashMap<>();
+        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);
+    }
+
+    @GetMapping("/workflow/lobster/instance/node-logs/{instanceId}")
+    public AjaxResult lobsterInstanceNodeLogs(@PathVariable String instanceId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @PostMapping("/workflow/lobster/instance/terminate/{instanceId}")
+    public AjaxResult lobsterInstanceTerminate(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
+    public AjaxResult lobsterOptimization() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster/optimization/pending-audit")
+    public AjaxResult lobsterOptimizationPendingAudit() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/optimization/batch-audit")
+    public AjaxResult lobsterOptimizationBatchAudit() { return AjaxResult.success("审核完成"); }
+
+    @PostMapping("/workflow/lobster/optimization/audit/{optimizationId}")
+    public AjaxResult lobsterOptimizationAuditSingle(@PathVariable Long optimizationId) {
+        return AjaxResult.success("审核完成");
+    }
+
+    @PostMapping("/workflow/lobster/optimization/analyze")
+    public AjaxResult lobsterOptimizationAnalyze() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalSuggestions", 0);
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/workflow/lobster/optimization/stats")
+    public AjaxResult lobsterOptimizationStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("total", 0); stats.put("pending", 0);
+        stats.put("approved", 0); stats.put("rejected", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster/optimization/config")
+    public AjaxResult lobsterOptimizationConfig() { return AjaxResult.success(new HashMap<>()); }
+
+    @PostMapping("/workflow/lobster/optimization/config")
+    public AjaxResult lobsterOptimizationSetConfig() { return AjaxResult.success("配置已保存"); }
+
+    @GetMapping({"/workflow/lobster/api-registry", "/workflow/lobster/api-registry/list",
+                 "/workflow/lobster/apiRegistry", "/workflow/lobster/apiRegistry/list"})
+    public AjaxResult lobsterApiRegistry() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/api-registry")
+    public AjaxResult lobsterApiRegistryAdd() { return AjaxResult.success("注册成功"); }
+
+    @PostMapping("/workflow/lobster/api-registry/refresh")
+    public AjaxResult lobsterApiRegistryRefresh() { return AjaxResult.success("缓存已刷新"); }
+
+    @GetMapping("/workflow/lobster/api-registry/categories")
+    public AjaxResult lobsterApiRegistryCategories() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/dead-letter", "/workflow/lobster/dead-letter/list",
+                 "/workflow/lobster/deadLetter", "/workflow/lobster/deadLetter/list"})
+    public AjaxResult lobsterDeadLetter() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/dead-letter/retry-all")
+    public AjaxResult lobsterDeadLetterRetryAll() { return AjaxResult.success("重试已提交"); }
+
+    @GetMapping("/workflow/lobster/dead-letter/stats")
+    public AjaxResult lobsterDeadLetterStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("total", 0); stats.put("pending", 0); stats.put("retried", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping({"/workflow/lobster/chat-aggregate", "/workflow/lobster/chat-aggregate/list",
+                 "/workflow/lobster/chatAggregate", "/workflow/lobster/chatAggregate/list"})
+    public AjaxResult lobsterChatAggregate(@RequestParam(required = false) String channelType,
+                                           @RequestParam(required = false) String keyword) {
+        if (chatSessionMapper == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            List<com.fs.company.domain.LobsterChatSession> rows =
+                chatSessionMapper.selectForAggregate(channelType, keyword);
+            return AjaxResult.success(rows != null ? rows : new ArrayList<>());
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
+    /** 渠道聚合会话消息详情 */
+    @GetMapping("/workflow/lobster/chat-aggregate/messages/{sessionId}")
+    public AjaxResult lobsterChatMessages(@PathVariable Long sessionId) {
+        if (chatMsgMapper == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            List<com.fs.company.domain.LobsterChatMsg> rows =
+                chatMsgMapper.selectBySessionId(sessionId);
+            return AjaxResult.success(rows != null ? rows : new ArrayList<>());
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
+    /** 渠道聚合账户映射信息 */
+    @GetMapping("/workflow/lobster/chat-aggregate/contact/{sessionId}")
+    public AjaxResult lobsterChatContact(@PathVariable Long sessionId) {
+        if (chatSessionMapper == null) return AjaxResult.error("服务不可用");
+        try {
+            com.fs.company.domain.LobsterChatSession s =
+                chatSessionMapper.selectBySessionId(sessionId);
+            if (s == null) return AjaxResult.error("会话不存在");
+            return AjaxResult.success(s);
+        } catch (Exception e) {
+            return AjaxResult.error("会话不存在");
+        }
+    }
+
+    // ======== 多模型路由配置(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/model-config", "/workflow/lobster/model-config/list",
+                 "/workflow/lobster/model", "/workflow/lobster/model/list"})
+    public AjaxResult lobsterModelConfig(@RequestParam(required = false) Long companyId) {
+        if (modelConfigService == null) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+        if (companyId != null) {
+            return AjaxResult.success(modelConfigService.getConfigsByCompany(companyId));
+        }
+        return AjaxResult.success(modelConfigService.getAllConfigs());
+    }
+
+    @GetMapping({"/workflow/lobster/model-config/types", "/workflow/lobster/model/types"})
+    public AjaxResult lobsterModelConfigTypes() {
+        List<Map<String, String>> types = new ArrayList<>();
+        types.add(buildType("workflow_generator", "工作流生成"));
+        types.add(buildType("quality_check", "质检评分"));
+        types.add(buildType("intent_router", "意图路由"));
+        types.add(buildType("reply_generator", "回复生成"));
+        return AjaxResult.success(types);
+    }
+
+    @PostMapping("/workflow/lobster/model-config")
+    public AjaxResult lobsterModelConfigAdd(@RequestBody LobsterModelConfig config) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        config.setEnabled(1);
+        config.setCreatedAt(LocalDateTime.now());
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.saveConfig(config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    @PutMapping("/workflow/lobster/model-config/{id}")
+    public AjaxResult lobsterModelConfigUpdate(@PathVariable Long id, @RequestBody LobsterModelConfig config) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        config.setId(id);
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.updateConfig(config);
+        return AjaxResult.success("配置已更新");
+    }
+
+    @DeleteMapping("/workflow/lobster/model-config/{id}")
+    public AjaxResult lobsterModelConfigDelete(@PathVariable Long id) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        modelConfigService.deleteConfig(id);
+        return AjaxResult.success("配置已删除");
+    }
+
+    private Map<String, String> buildType(String code, String name) {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put("code", code);
+        m.put("name", name);
+        return m;
+    }
+
+    // ======== lobster-exec 占位端点 ========
+    @GetMapping({"/workflow/lobster-exec/instance", "/workflow/lobster-exec/instance/list"})
+    public AjaxResult lobsterExecInstanceList() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster-exec/instance/{instanceId}")
+    public AjaxResult lobsterExecInstanceGet(@PathVariable String instanceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("instanceId", instanceId);
+        data.put("status", "unknown");
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster-exec/node-logs/{instanceId}")
+    public AjaxResult lobsterExecNodeLogs(@PathVariable String instanceId) {
+        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/pause/{instanceId}")
+    public AjaxResult lobsterExecPause(@PathVariable String instanceId) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.pauseWorkflow(null, Long.valueOf(instanceId));
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    @PostMapping("/workflow/lobster-exec/resume/{instanceId}")
+    public AjaxResult lobsterExecResume(@PathVariable String instanceId) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.resumeWorkflow(null, Long.valueOf(instanceId));
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    @PostMapping("/workflow/lobster-exec/terminate/{instanceId}")
+    public AjaxResult lobsterExecTerminate(@PathVariable String instanceId, @RequestParam(defaultValue = "管理员手动终止") String reason) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.terminateWorkflow(null, Long.valueOf(instanceId), reason);
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    /** 工作流模拟执行 */
+    @PostMapping("/workflow/lobster/simulate/{workflowId}")
+    public AjaxResult simulateWorkflow(@PathVariable Long workflowId, @RequestBody(required = false) Map<String, Object> body) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        Map<String, Object> profile = body != null ? (Map<String, Object>) body.getOrDefault("profile", null) : null;
+        return AjaxResult.success(workflowExecutor.simulateExecution(null, workflowId, profile));
+    }
+
+    // ======== 渠道插件管理 ========
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.channel.ChannelPluginService channelPluginService;
+
+    @GetMapping({"/workflow/lobster/channel-plugin/list", "/lobster/channel-plugins"})
+    public AjaxResult channelPluginList() {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        return AjaxResult.success(channelPluginService.listPlugins(null));
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/enable/{channelType}")
+    public AjaxResult channelPluginEnable(@PathVariable String channelType, @RequestParam(defaultValue = "true") Boolean enabled) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        channelPluginService.setEnabled(null, channelType, enabled);
+        return AjaxResult.success(enabled ? "已启用" : "已禁用");
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/config/{channelType}")
+    public AjaxResult channelPluginConfig(@PathVariable String channelType, @RequestBody Map<String, Object> config) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        channelPluginService.saveConfig(null, channelType, config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/test/{channelType}")
+    public AjaxResult channelPluginTest(@PathVariable String channelType) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        return AjaxResult.success(channelPluginService.testConnection(null, channelType));
+    }
+
+    @GetMapping("/workflow/lobster-exec/control-mode/{instanceId}")
+    public AjaxResult lobsterExecGetControlMode(@PathVariable String instanceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("instanceId", instanceId);
+        data.put("mode", "auto");
+        return AjaxResult.success(data);
+    }
+
+    @PostMapping("/workflow/lobster-exec/control-mode/{instanceId}")
+    public AjaxResult lobsterExecSetControlMode(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @PostMapping("/workflow/lobster-exec/complete-handoff/{instanceId}")
+    public AjaxResult lobsterExecCompleteHandoff(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @GetMapping("/workflow/lobster-exec/compliance-rules")
+    public AjaxResult lobsterExecComplianceRules() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster-exec/compliance-rule")
+    public AjaxResult lobsterExecAddComplianceRule() { return AjaxResult.success("操作成功"); }
+
+    @PutMapping("/workflow/lobster-exec/compliance-rule/{id}")
+    public AjaxResult lobsterExecUpdateComplianceRule(@PathVariable Long id) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @DeleteMapping("/workflow/lobster-exec/compliance-rule/{id}")
+    public AjaxResult lobsterExecDeleteComplianceRule(@PathVariable Long id) {
+        return AjaxResult.success("操作成功");
+    }
+
+    // ======== 模拟聊天测试 ========
+    @PostMapping("/workflow/simulate")
+    public AjaxResult simulateChat(@RequestBody Map<String, Object> params) {
+        String content = (String) params.getOrDefault("content", "");
+        Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
+        // 模拟AI回复:管理端跨租户模拟,返回标准结构
+        Map<String, Object> reply = new HashMap<>();
+        reply.put("reply", "[模拟回复] 针对「" + content + "」的AI响应内容(模板ID:" + (templateId != null ? templateId : "未选择") + ")");
+        reply.put("templateId", templateId);
+        reply.put("timestamp", System.currentTimeMillis());
+        reply.put("mode", "simulate");
+        return AjaxResult.success(reply);
+    }
+
+    // ======== AI 回复质量评分(接 fs-service AiChatQualityService)========
+    @GetMapping("/aiChatQuality/list")
+    public AjaxResult qualityVerifyList(@RequestParam(defaultValue = "1") int pageNum,
+                                         @RequestParam(defaultValue = "10") int pageSize,
+                                         @RequestParam(required = false) Long workflowId,
+                                         @RequestParam(required = false) String scoreSource,
+                                         @RequestParam(required = false) String qualityResult,
+                                         @RequestParam(required = false) String sessionId) {
+        Map<String, Object> result = new HashMap<>();
+        if (aiChatQualityService == null) {
+            result.put("rows", new ArrayList<>());
+            result.put("total", 0);
+            return AjaxResult.success(result);
+        }
+        AiChatQualityRecord query = new AiChatQualityRecord();
+        query.setSessionId(sessionId);
+        query.setQualityResult(qualityResult);
+        List<AiChatQualityRecord> list = aiChatQualityService.selectAiChatQualityRecordList(query);
+        // 内存分页
+        int from = Math.max(0, (pageNum - 1) * pageSize);
+        int to = Math.min(list.size(), from + pageSize);
+        List<AiChatQualityRecord> page = from >= list.size() ? new ArrayList<>() : list.subList(from, to);
+        result.put("rows", page);
+        result.put("total", list.size());
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/aiChatQuality/{id}")
+    public AjaxResult qualityVerifyDetail(@PathVariable Long id) {
+        if (aiChatQualityService == null) {
+            return AjaxResult.success(new HashMap<>());
+        }
+        return AjaxResult.success(aiChatQualityService.selectAiChatQualityRecordById(id));
+    }
+
+    @PostMapping("/aiChatQuality")
+    public AjaxResult qualityVerifyAdd(@RequestBody AiChatQualityRecord record) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.insertAiChatQualityRecord(record));
+    }
+
+    @PutMapping("/aiChatQuality")
+    public AjaxResult qualityVerifyUpdate(@RequestBody AiChatQualityRecord record) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.updateAiChatQualityRecord(record));
+    }
+
+    @DeleteMapping("/aiChatQuality/{id}")
+    public AjaxResult qualityVerifyDelete(@PathVariable Long id) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.deleteAiChatQualityRecordById(id));
+    }
+
+    @GetMapping("/aiChatQuality/stats")
+    public AjaxResult qualityVerifyStats(@RequestParam(required = false) Long workflowId) {
+        Map<String, Object> stats = new HashMap<>();
+        if (aiChatQualityService == null) {
+            stats.put("totalCount", 0);
+            stats.put("passCount", 0);
+            stats.put("failCount", 0);
+            stats.put("pendingCount", 0);
+            stats.put("avgScore", 0);
+            return AjaxResult.success(stats);
+        }
+        AiChatQualityRecord query = new AiChatQualityRecord();
+        List<AiChatQualityRecord> all = aiChatQualityService.selectAiChatQualityRecordList(query);
+        long pass = all.stream().filter(r -> "pass".equalsIgnoreCase(r.getQualityResult())).count();
+        long fail = all.stream().filter(r -> "fail".equalsIgnoreCase(r.getQualityResult())).count();
+        long pending = all.stream().filter(r -> r.getQualityResult() == null || r.getQualityResult().isEmpty()).count();
+        stats.put("totalCount", all.size());
+        stats.put("passCount", pass);
+        stats.put("failCount", fail);
+        stats.put("pendingCount", pending);
+        stats.put("avgScore", all.isEmpty() ? 0 : Math.round(pass * 100.0 / Math.max(1, all.size())));
+        return AjaxResult.success(stats);
+    }
+
+    // ======== 引擎核心占位端点 ========
+    @GetMapping("/workflow/lobster/engine/evolution/metrics")
+    public AjaxResult lobsterEngineEvolutionMetrics() {
+        Map<String, Object> data = new HashMap<>();
+        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<>()); }
+
+    @PostMapping("/workflow/lobster/engine/evolution/apply")
+    public AjaxResult lobsterEngineEvolutionApply() { return AjaxResult.success("操作成功"); }
+
+    @GetMapping("/workflow/lobster/engine/heartbeat/status")
+    public AjaxResult lobsterEngineHeartbeat() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("status", "healthy");
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster/engine/channels")
+    public AjaxResult lobsterEngineChannels() { return AjaxResult.success(new ArrayList<>()); }
+
+    // ════════════════════════════════════════════════════════════════
+    // 画像配置 / 摘要配置 / 敏感词 / 消息去重 — 走真实 LobsterCompanyConfigService
+    // ════════════════════════════════════════════════════════════════
+
+    // ─── 画像配置 ───
+    @GetMapping("/workflow/lobster/profile-config/list")
+    public AjaxResult profileConfigList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listProfile(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/profile-config/save")
+    public AjaxResult profileConfigSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveProfile(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/profile-config/{id}")
+    public AjaxResult profileConfigDelete(@PathVariable Long id,
+                                          @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteProfile(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    // ─── 摘要配置 ───
+    @GetMapping("/workflow/lobster/summary-config/list")
+    public AjaxResult summaryConfigList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listSummary(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/summary-config/save")
+    public AjaxResult summaryConfigSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveSummary(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/summary-config/{id}")
+    public AjaxResult summaryConfigDelete(@PathVariable Long id,
+                                          @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteSummary(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    // ─── 敏感词 ───
+    @GetMapping("/workflow/lobster/sensitive-words/list")
+    public AjaxResult sensitiveWordsList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listSensitive(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/sensitive-words/save")
+    public AjaxResult sensitiveWordsSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveSensitive(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/sensitive-words/{id}")
+    public AjaxResult sensitiveWordsDelete(@PathVariable Long id,
+                                            @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteSensitive(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/workflow/lobster/sensitive-words/check")
+    public AjaxResult sensitiveWordsCheck(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        Long companyId = body.get("companyId") == null ? 0L : Long.valueOf(body.get("companyId").toString());
+        String content = (String) body.get("content");
+        return AjaxResult.success(companyConfigService.checkSensitive(companyId, content));
+    }
+
+    // ─── 消息去重监控 ───
+    @GetMapping("/workflow/lobster/dedup/stats")
+    public AjaxResult dedupStats(@RequestParam(required = false) Long companyId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("exactHits", 0);
+        stats.put("semanticHits", 0);
+        stats.put("totalChecked", 0);
+        stats.put("threshold", 0.70);
+        stats.put("note", "实时统计由 MessageDedupService 在内存维护,可后续接 Redis");
+        return AjaxResult.success(stats);
+    }
+
+    // ======== E2E 端到端测试 ========
+    @PostMapping("/workflow/lobster/e2e/run")
+    public AjaxResult e2eRun(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        com.fs.company.service.workflow.LobsterE2eTestService.E2eRequest req =
+                new com.fs.company.service.workflow.LobsterE2eTestService.E2eRequest();
+        req.setCompanyId(toLong(body.get("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")));
+        Object ui = body.get("userInputs");
+        if (ui instanceof List) {
+            List<String> in = new ArrayList<>();
+            for (Object o : (List<?>) ui) if (o != null) in.add(o.toString());
+            req.setUserInputs(in);
+        }
+        return AjaxResult.success(e2eTestService.runE2e(req));
+    }
+
+    @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));
+    }
+
+    @GetMapping("/workflow/lobster/e2e/list")
+    public AjaxResult e2eList(@RequestParam(required = false) Long companyId,
+                              @RequestParam(defaultValue = "1") Integer pageNum,
+                              @RequestParam(defaultValue = "20") Integer pageSize) {
+        if (e2eTestService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(e2eTestService.listRuns(companyId, pageNum, pageSize));
+    }
+
+    @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 测试服务未启用");
+        return AjaxResult.success(e2eTestService.stepNext(
+                toLong(body.get("companyId")), instanceId, (String) body.get("userInput")));
+    }
+
+    @PostMapping("/workflow/lobster/chat/multi-turn")
+    public AjaxResult multiTurn(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        Object ui = body.get("userInputs");
+        List<String> in = new ArrayList<>();
+        if (ui instanceof List) for (Object o : (List<?>) ui) if (o != null) in.add(o.toString());
+        return AjaxResult.success(e2eTestService.multiTurn(
+                toLong(body.get("companyId")), toLong(body.get("instanceId")),
+                (String) body.get("nodeCode"), in));
+    }
+
+    // ======== 测试场景剧本 ========
+    @GetMapping("/workflow/lobster/scenario/list")
+    public AjaxResult scenarioList(@RequestParam(required = false) Long companyId,
+                                    @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(companyId, enabled, pageNum, pageSize));
+    }
+
+    @GetMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioGet(@PathVariable Long id) {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        return AjaxResult.success(testScenarioService.getScenario(id));
+    }
+
+    @PostMapping("/workflow/lobster/scenario/save")
+    public AjaxResult scenarioSave(@RequestBody Map<String, Object> body) {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        Object idObj = body.get("id");
+        if (idObj == null) {
+            return AjaxResult.success(testScenarioService.createScenario(body));
+        }
+        testScenarioService.updateScenario(toLong(idObj), body);
+        return AjaxResult.success(idObj);
+    }
+
+    @DeleteMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioDelete(@PathVariable Long id) {
+        if (testScenarioService != null) testScenarioService.deleteScenario(id);
+        return AjaxResult.success();
+    }
+
+    @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);
+    }
+
+    // ========== 动态节点学习产物审批 ==========
+
+    @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, null));
+    }
+
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/approve")
+    public AjaxResult dynamicImplApprove(@PathVariable Long id) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        dynamicNodeImplService.approve(id, getLoginUsername());
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/reject")
+    public AjaxResult dynamicImplReject(@PathVariable Long id, @RequestParam String reason) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        dynamicNodeImplService.reject(id, getLoginUsername(), reason);
+        return AjaxResult.success();
+    }
+
+    private String getLoginUsername() {
+        return "admin"; // 动态节点审批由管理员操作
+    }
+
+    @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);
+    }
+
+    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; }
+    }
+}

+ 43 - 0
fs-admin-saas/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql

@@ -0,0 +1,43 @@
+-- =====================================================
+-- 龙虾引擎 新增页面菜单
+-- 新增 7 个缺失的菜单记录(模拟聊天测试、评分准确性验证、
+--   用户画像配置、摘要生成配置、消息去重配置、敏感词库、节点详情)
+-- =====================================================
+
+-- =====================================================
+-- PART 1: 新增页面菜单 (menu_id 29920-29926)
+
+-- 15. 模拟聊天测试
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29920, '模拟聊天测试', 29900, 13, 'chat-test', 'lobster/chat-test/index', 'C', 'el-icon-chat-dot-square', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 16. 评分准确性验证
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29921, '评分准确性验证', 29900, 14, 'quality-verify', 'lobster/quality-verify/index', 'C', 'el-icon-data-analysis', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 17. 用户画像配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29922, '用户画像配置', 29900, 15, 'profile-config', 'lobster/profile-config/index', 'C', 'el-icon-user', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 18. 摘要生成配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29923, '摘要生成配置', 29900, 16, 'summary-config', 'lobster/summary-config/index', 'C', 'el-icon-notebook', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 19. 消息去重配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29924, '消息去重配置', 29900, 17, 'dedup-config', 'lobster/dedup-config/index', 'C', 'el-icon-filter', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 20. 敏感词库
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29925, '敏感词库', 29900, 18, 'sensitive-words', 'lobster/sensitive-words/index', 'C', 'el-icon-warning', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 21. 节点详情 (hidden, 从实例页跳转)
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29926, '节点详情', 29900, 19, 'node-detail', 'lobster/node-detail/index', 'C', 'el-icon-document', '1', '0', 0, 0, 'admin', NOW(), '从实例监控页跳转');
+
+-- =====================================================
+-- PART 2: 同步更新模板表 tenant_sys_menu
+-- =====================================================
+INSERT IGNORE INTO tenant_sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark
+FROM sys_menu WHERE menu_id BETWEEN 29920 AND 29926;

+ 49 - 0
fs-admin/src/main/java/com/fs/admin/controller/AdminCommGatewayLogController.java

@@ -0,0 +1,49 @@
+package com.fs.admin.controller;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.comm.service.ICommGatewayApiLogService;
+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.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RestController;
+
+import java.util.List;
+
+/**
+ * 通讯网关 API 调用日志(主库)
+ */
+@RestController
+@RequestMapping("/admin/comm-gateway-log")
+public class AdminCommGatewayLogController extends BaseController {
+
+    @Autowired(required = false)
+    private ICommGatewayApiLogService commGatewayApiLogService;
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CommGatewayApiLog query) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CommGatewayApiLog> list = commGatewayApiLogService != null
+                ? commGatewayApiLogService.selectList(query)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:list')")
+    @GetMapping("/{logId}")
+    public AjaxResult getInfo(@PathVariable Long logId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (commGatewayApiLogService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        return AjaxResult.success(commGatewayApiLogService.selectById(logId));
+    }
+}

+ 69 - 10
fs-admin/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java

@@ -117,11 +117,11 @@ public class AdminCompanyBridgeController extends BaseController {
     }
     }
 
 
     /** 查询租户已分配的接口列表(含定价信息) */
     /** 查询租户已分配的接口列表(含定价信息) */
-    @GetMapping("/admin/voice-api/apis/{companyId}")
-    public AjaxResult voiceApiApis(@PathVariable Long companyId) {
+    @GetMapping("/admin/voice-api/apis/{tenantId}")
+    public AjaxResult voiceApiApis(@PathVariable Long tenantId) {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null ?
         List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null ?
-            companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId) : new ArrayList<>();
+            companyVoiceApiTenantService.selectEnabledApisByTenantId(tenantId) : new ArrayList<>();
         return AjaxResult.success(list);
         return AjaxResult.success(list);
     }
     }
 
 
@@ -133,22 +133,81 @@ public class AdminCompanyBridgeController extends BaseController {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
         Long apiId = Long.valueOf(body.get("apiId").toString());
         Long apiId = Long.valueOf(body.get("apiId").toString());
-        @SuppressWarnings("unchecked")
-        List<Integer> companyIds = (List<Integer>) body.get("companyIds");
-        List<Long> ids = new ArrayList<>();
-        for (Integer id : companyIds) { ids.add(id.longValue()); }
-        companyVoiceApiTenantService.batchAssignTenants(apiId, ids);
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (tenantIds.isEmpty()) {
+            tenantIds = parseIdList(body.get("companyIds"));
+        }
+        if (tenantIds.isEmpty()) {
+            return AjaxResult.error("请选择租户");
+        }
+        companyVoiceApiTenantService.batchAssignTenants(apiId, parseTenantAssignList(body, apiId, tenantIds));
         return AjaxResult.success();
         return AjaxResult.success();
     }
     }
 
 
+    @SuppressWarnings("unchecked")
+    private List<CompanyVoiceApiTenant> parseTenantAssignList(Map<String, Object> body, Long apiId, List<Long> tenantIds) {
+        List<CompanyVoiceApiTenant> list = new ArrayList<>();
+        Object rawTenants = body.get("tenants");
+        if (rawTenants instanceof List) {
+            for (Object item : (List<?>) rawTenants) {
+                if (!(item instanceof Map)) {
+                    continue;
+                }
+                Map<String, Object> tenantMap = (Map<String, Object>) item;
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                if (tenantMap.get("tenantId") != null) {
+                    tenant.setTenantId(Long.valueOf(tenantMap.get("tenantId").toString()));
+                }
+                if (tenantMap.get("tenantCode") != null) {
+                    tenant.setTenantCode(tenantMap.get("tenantCode").toString());
+                }
+                if (tenantMap.get("tenantName") != null) {
+                    tenant.setTenantName(tenantMap.get("tenantName").toString());
+                }
+                if (tenant.getTenantId() != null) {
+                    list.add(tenant);
+                }
+            }
+        }
+        if (list.isEmpty()) {
+            for (Long tenantId : tenantIds) {
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                tenant.setTenantId(tenantId);
+                list.add(tenant);
+            }
+        }
+        return list;
+    }
+
     /** 取消分配 */
     /** 取消分配 */
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
     @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
     @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
     @DeleteMapping("/admin/voice-api/unassignTenant")
     @DeleteMapping("/admin/voice-api/unassignTenant")
-    public AjaxResult unassignTenant(@RequestParam Long apiId, @RequestParam Long companyId) {
+    public AjaxResult unassignTenant(@RequestParam Long apiId,
+                                     @RequestParam(required = false) Long tenantId,
+                                     @RequestParam(required = false) Long companyId) {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
-        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, companyId));
+        Long tid = tenantId != null ? tenantId : companyId;
+        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, tid));
+    }
+
+    @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 - 637
fs-admin/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java

@@ -1,637 +0,0 @@
-package com.fs.admin.controller;
-
-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.DataSourceType;
-import com.fs.framework.datasource.DynamicDataSourceContextHolder;
-import com.fs.tenant.domain.TenantInfo;
-import com.fs.tenant.mapper.TenantInfoMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.*;
-
-/**
- * fs-admin (8004) 端龙虾引擎桥接控制器
- * 原始控制器在 com.fs.company.controller.workflow.* 被 fs-admin 排除
- * 覆盖 adminui 中 8004=404 的 workflow/lobster/* 全部13个端点
- */
-@RestController
-public class AdminLobsterBridgeController extends BaseController {
-
-    @Autowired(required = false)
-    private JdbcTemplate jdbcTemplate;
-
-    @Autowired(required = false)
-    private TenantInfoMapper tenantInfoMapper;
-
-    private TableDataInfo emptyTable() {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        r.setRows(new ArrayList<>());
-        r.setTotal(0);
-        return r;
-    }
-
-    private TableDataInfo safeListFromTable(String table) {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
-                rows = enrichWithTenantName(rows);
-                r.setRows(rows);
-                r.setTotal(rows.size());
-            } else {
-                r.setRows(new ArrayList<>());
-                r.setTotal(0);
-            }
-        } catch (Exception e) {
-            r.setRows(new ArrayList<>());
-            r.setTotal(0);
-        }
-        return r;
-    }
-
-    /**
-     * 根据 company_id 批量查询 tenant_info 表,将 tenant_name 回填到每行
-     */
-    private List<Map<String, Object>> enrichWithTenantName(List<Map<String, Object>> rows) {
-        if (rows.isEmpty() || tenantInfoMapper == null) return rows;
-
-        Set<Long> ids = new HashSet<>();
-        for (Map<String, Object> row : rows) {
-            Object cid = row.get("company_id");
-            if (cid != null) ids.add(((Number) cid).longValue());
-        }
-        if (ids.isEmpty()) return rows;
-
-        // 确保在主库查询 tenant_info
-        String prev = DynamicDataSourceContextHolder.getDataSourceType();
-        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
-        try {
-            List<TenantInfo> tenants = tenantInfoMapper.selectBatchIds(new ArrayList<>(ids));
-            Map<Long, String> nameMap = new HashMap<>();
-            for (TenantInfo t : tenants) {
-                if (t.getTenantName() != null) nameMap.put(t.getId(), t.getTenantName());
-            }
-            for (Map<String, Object> row : rows) {
-                Object cid = row.get("company_id");
-                if (cid != null) {
-                    String name = nameMap.get(((Number) cid).longValue());
-                    row.put("tenant_name", name != null ? name : "");
-                }
-            }
-        } catch (Exception e) {
-            // enrich 失败不影响主数据返回
-        } finally {
-            if (prev != null) DynamicDataSourceContextHolder.setDataSourceType(prev);
-            else DynamicDataSourceContextHolder.clearDataSourceType();
-        }
-        return rows;
-    }
-
-    // ========== AI工作流生成 ==========
-    @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
-    public TableDataInfo lobsterGenerate() {
-        return safeListFromTable("ai_generation_record");
-    }
-
-    // ========== 工作流画布 ==========
-    @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
-    public TableDataInfo lobsterCanvas() {
-        return safeListFromTable("workflow_template");
-    }
-
-    // ========== 工作流模板库 ==========
-    @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
-    public TableDataInfo lobsterTemplate() {
-        return safeListFromTable("workflow_template");
-    }
-
-    // ========== 实例监控 ==========
-    @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
-    public TableDataInfo lobsterInstance() {
-        return safeListFromTable("workflow_instance");
-    }
-
-    @GetMapping("/workflow/lobster/instance/stats")
-    public AjaxResult lobsterInstanceStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("running", 0);
-        stats.put("paused", 0);
-        stats.put("deadLetters", 0);
-        stats.put("todayTokens", "0");
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM workflow_instance GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    if ("running".equals(s)) stats.put("running", cnt);
-                    else if ("paused".equals(s)) stats.put("paused", cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        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("workflowName", "");
-        data.put("status", "unknown");
-        return AjaxResult.success(data);
-    }
-
-    @GetMapping("/workflow/lobster/instance/node-logs/{instanceId}")
-    public AjaxResult lobsterInstanceNodeLogs(@PathVariable String instanceId) {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    @PostMapping("/workflow/lobster/instance/terminate/{instanceId}")
-    public AjaxResult lobsterInstanceTerminate(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== AI优化建议 ==========
-    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
-    public TableDataInfo lobsterOptimization() {
-        return safeListFromTable("lobster_optimization_suggestion");
-    }
-
-    // ========== 提示词管理 ==========
-    @GetMapping({"/workflow/lobster/prompt", "/workflow/lobster/prompt/list"})
-    public TableDataInfo lobsterPrompt() {
-        return safeListFromTable("lobster_prompt");
-    }
-
-    // ========== 销冠语料学习 ==========
-    @GetMapping({"/workflow/lobster/sales-corpus", "/workflow/lobster/sales-corpus/list",
-                 "/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
-    public TableDataInfo lobsterSalesCorpus(
-            @RequestParam(defaultValue = "1") int page,
-            @RequestParam(defaultValue = "10") int size,
-            @RequestParam(required = false) String scenario,
-            @RequestParam(required = false) String status,
-            @RequestParam(required = false) Long companyId) {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        try {
-            if (jdbcTemplate != null) {
-                StringBuilder where = new StringBuilder(" WHERE 1=1 ");
-                List<Object> params = new ArrayList<>();
-                if (scenario != null && !scenario.isEmpty()) {
-                    where.append("AND scenario=? ");
-                    params.add(scenario);
-                }
-                if (status != null && !status.isEmpty()) {
-                    where.append("AND status=? ");
-                    params.add(status);
-                }
-                if (companyId != null) {
-                    where.append("AND company_id=? ");
-                    params.add(companyId);
-                }
-                // 查总数
-                Long total = jdbcTemplate.queryForObject(
-                        "SELECT COUNT(*) FROM lobster_sales_corpus " + where, Long.class, params.toArray());
-                r.setTotal(total != null ? total.intValue() : 0);
-                // 分页查询
-                List<Object> pageParams = new ArrayList<>(params);
-                pageParams.add((page - 1) * size);
-                pageParams.add(size);
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                        "SELECT * FROM lobster_sales_corpus " + where +
-                        "ORDER BY create_time DESC LIMIT ?, ?", pageParams.toArray());
-                rows = enrichWithTenantName(rows);
-                r.setRows(rows);
-            } else {
-                r.setRows(new ArrayList<>());
-                r.setTotal(0);
-            }
-        } catch (Exception e) {
-            r.setRows(new ArrayList<>());
-            r.setTotal(0);
-        }
-        return r;
-    }
-
-    @GetMapping("/workflow/lobster/sales-corpus/scenarios")
-    public AjaxResult lobsterSalesCorpusScenarios() {
-        List<Map<String, Object>> scenarios = new ArrayList<>();
-        try {
-            if (jdbcTemplate != null) {
-                scenarios = jdbcTemplate.queryForList(
-                    "SELECT DISTINCT scenario AS code, scenario AS name FROM lobster_sales_corpus WHERE scenario IS NOT NULL");
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(scenarios);
-    }
-
-    @PostMapping("/workflow/lobster/sales-corpus/dialog")
-    public AjaxResult lobsterSalesCorpusAdd(@RequestBody(required = false) Map<String, Object> body) {
-        if (jdbcTemplate == null) return AjaxResult.error("数据库未初始化");
-        try {
-            Long companyId = body != null && body.get("companyId") != null ?
-                    Long.valueOf(body.get("companyId").toString()) : null;
-            String salespersonName = body != null ? (String) body.getOrDefault("salespersonName", "销冠") : "销冠";
-            String customerQuestion = body != null ? (String) body.get("customerQuestion") : null;
-            String salesAnswer = body != null ? (String) body.get("salesAnswer") : null;
-            String scenario = body != null ? (String) body.getOrDefault("scenario", "通用") : "通用";
-            if (customerQuestion == null || salesAnswer == null) {
-                return AjaxResult.error("客户问题和销冠回答不能为空");
-            }
-            jdbcTemplate.update(
-                "INSERT INTO lobster_sales_corpus (company_id, salesperson_name, customer_question, sales_answer, scenario, status, create_time) " +
-                "VALUES (?,?,?,?,?,'raw',NOW())",
-                companyId, salespersonName, customerQuestion, salesAnswer, scenario);
-            return AjaxResult.success("录入成功");
-        } catch (Exception e) {
-            return AjaxResult.error("录入失败: " + e.getMessage());
-        }
-    }
-
-    @PostMapping("/workflow/lobster/sales-corpus/analyze")
-    public AjaxResult lobsterSalesCorpusAnalyze() {
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalEntries", 0);
-        result.put("overallScore", "N/A");
-        return AjaxResult.success(result);
-    }
-
-    // ========== 接口注册中心 ==========
-    @GetMapping({"/workflow/lobster/api-registry", "/workflow/lobster/api-registry/list",
-                 "/workflow/lobster/apiRegistry", "/workflow/lobster/apiRegistry/list"})
-    public TableDataInfo lobsterApiRegistry() {
-        return safeListFromTable("lobster_api_registry");
-    }
-
-    // ========== 死信队列 ==========
-    @GetMapping({"/workflow/lobster/dead-letter", "/workflow/lobster/dead-letter/list",
-                 "/workflow/lobster/deadLetter", "/workflow/lobster/deadLetter/list"})
-    public TableDataInfo lobsterDeadLetter() {
-        return safeListFromTable("lobster_dead_letter");
-    }
-
-    // ========== 节点审核 ==========
-    @GetMapping({"/workflow/lobster/event-audit", "/workflow/lobster/event-audit/list",
-                 "/workflow/lobster/eventAudit", "/workflow/lobster/eventAudit/list"})
-    public TableDataInfo lobsterEventAudit() {
-        return safeListFromTable("lobster_event_node_audit");
-    }
-
-    // ========== 聚合聊天 ==========
-    @GetMapping({"/workflow/lobster/chat-aggregate", "/workflow/lobster/chat-aggregate/list",
-                 "/workflow/lobster/chatAggregate", "/workflow/lobster/chatAggregate/list"})
-    public TableDataInfo lobsterChatAggregate() {
-        return safeListFromTable("lobster_chat_aggregate");
-    }
-
-    // ========== 模型配置 ==========
-    @GetMapping({"/workflow/lobster/model-config", "/workflow/lobster/model-config/list",
-                 "/workflow/lobster/model", "/workflow/lobster/model/list"})
-    public TableDataInfo lobsterModelConfig() {
-        return safeListFromTable("lobster_model_config");
-    }
-
-    // ========== Token系数管理 ==========
-    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
-    public TableDataInfo lobsterBilling() {
-        return safeListFromTable("lobster_billing_record");
-    }
-
-    // ========== 工作流执行引擎 lobster-exec ==========
-    @GetMapping({"/workflow/lobster-exec/instance", "/workflow/lobster-exec/instance/list"})
-    public TableDataInfo lobsterExecInstanceList() {
-        return safeListFromTable("workflow_instance");
-    }
-
-    @GetMapping("/workflow/lobster-exec/instance/{instanceId}")
-    public AjaxResult lobsterExecInstanceGet(@PathVariable String instanceId) {
-        return lobsterInstanceDetail(instanceId);
-    }
-
-    @GetMapping("/workflow/lobster-exec/node-logs/{instanceId}")
-    public AjaxResult lobsterExecNodeLogs(@PathVariable String instanceId) {
-        return lobsterInstanceNodeLogs(instanceId);
-    }
-
-    @PostMapping({"/workflow/lobster-exec/start", "/workflow/lobster-exec/next-node"})
-    public AjaxResult lobsterExecAction() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/pause/{instanceId}")
-    public AjaxResult lobsterExecPause(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/resume/{instanceId}")
-    public AjaxResult lobsterExecResume(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/terminate/{instanceId}")
-    public AjaxResult lobsterExecTerminate(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster-exec/control-mode/{instanceId}")
-    public AjaxResult lobsterExecGetControlMode(@PathVariable String instanceId) {
-        Map<String, Object> data = new HashMap<>();
-        data.put("instanceId", instanceId);
-        data.put("mode", "auto");
-        return AjaxResult.success(data);
-    }
-
-    @PostMapping("/workflow/lobster-exec/control-mode/{instanceId}")
-    public AjaxResult lobsterExecSetControlMode(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/complete-handoff/{instanceId}")
-    public AjaxResult lobsterExecCompleteHandoff(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 合规规则 lobster-exec/compliance ==========
-    @GetMapping("/workflow/lobster-exec/compliance-rules")
-    public TableDataInfo lobsterExecComplianceRules() {
-        return emptyTable();
-    }
-
-    @PostMapping("/workflow/lobster-exec/compliance-rule")
-    public AjaxResult lobsterExecAddComplianceRule() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PutMapping("/workflow/lobster-exec/compliance-rule/{id}")
-    public AjaxResult lobsterExecUpdateComplianceRule(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @DeleteMapping("/workflow/lobster-exec/compliance-rule/{id}")
-    public AjaxResult lobsterExecDeleteComplianceRule(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 引擎核心 lobster/engine ==========
-    @GetMapping("/workflow/lobster/engine/evolution/metrics")
-    public AjaxResult lobsterEngineEvolutionMetrics() {
-        Map<String, Object> data = new HashMap<>();
-        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<>());
-    }
-
-    @PostMapping("/workflow/lobster/engine/evolution/apply")
-    public AjaxResult lobsterEngineEvolutionApply() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster/engine/heartbeat/status")
-    public AjaxResult lobsterEngineHeartbeat() {
-        Map<String, Object> data = new HashMap<>();
-        data.put("status", "healthy");
-        return AjaxResult.success(data);
-    }
-
-    @GetMapping("/workflow/lobster/engine/channels")
-    public AjaxResult lobsterEngineChannels() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    // ========== 提示词管理 CRUD ==========
-    @GetMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptGet(@PathVariable Long id) {
-        return AjaxResult.success();
-    }
-
-    @PostMapping("/workflow/lobster/prompt")
-    public AjaxResult lobsterPromptAdd() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PutMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptUpdate(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @DeleteMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptDelete(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster/prompt/categories")
-    public AjaxResult lobsterPromptCategories() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    @PostMapping("/workflow/lobster/prompt/refresh-cache")
-    public AjaxResult lobsterPromptRefreshCache() {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 销冠语料补充 ==========
-    @PostMapping("/workflow/lobster/sales-corpus/batch-import")
-    public AjaxResult lobsterSalesCorpusBatchImport(@RequestBody Map<String, Object> body) {
-        if (jdbcTemplate == null) return AjaxResult.error("数据库未初始化");
-        try {
-            Long companyId = body.get("companyId") != null ?
-                    Long.valueOf(body.get("companyId").toString()) : null;
-            String salespersonName = (String) body.getOrDefault("salespersonName", "销冠");
-            String scenario = (String) body.getOrDefault("scenario", "通用");
-            List<Map<String, Object>> dialogs = (List<Map<String, Object>>) body.get("dialogs");
-            if (dialogs == null || dialogs.isEmpty()) {
-                return AjaxResult.error("导入数据不能为空");
-            }
-            int count = 0;
-            for (Map<String, Object> dialog : dialogs) {
-                String customer = (String) dialog.get("customer");
-                String sales = (String) dialog.get("sales");
-                if (customer == null || customer.trim().isEmpty() || sales == null || sales.trim().isEmpty()) {
-                    continue;
-                }
-                jdbcTemplate.update(
-                    "INSERT INTO lobster_sales_corpus (company_id, salesperson_name, customer_question, sales_answer, scenario, status, create_time) " +
-                    "VALUES (?,?,?,?,?,'raw',NOW())",
-                    companyId, salespersonName, customer.trim(), sales.trim(), scenario);
-                count++;
-            }
-            Map<String, Object> result = new LinkedHashMap<>();
-            result.put("message", "批量导入完成");
-            result.put("count", count);
-            return AjaxResult.success(result);
-        } catch (Exception e) {
-            return AjaxResult.error("批量导入失败: " + e.getMessage());
-        }
-    }
-
-    // ========== 死信队列补充 ==========
-    @PostMapping("/workflow/lobster/dead-letter/retry-all")
-    public AjaxResult lobsterDeadLetterRetryAll() {
-        return AjaxResult.success("重试已提交");
-    }
-
-    @GetMapping("/workflow/lobster/dead-letter/stats")
-    public AjaxResult lobsterDeadLetterStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("total", 0);
-        stats.put("pending", 0);
-        stats.put("retried", 0);
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM lobster_dead_letter GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    stats.put(s, cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    // ========== 节点审核补充 ==========
-    @GetMapping("/workflow/lobster/event-audit/{id}")
-    public AjaxResult lobsterEventAuditGet(@PathVariable Long id) {
-        return AjaxResult.success();
-    }
-
-    @PostMapping("/workflow/lobster/event-audit/approve/{id}")
-    public AjaxResult lobsterEventAuditApprove(@PathVariable Long id) {
-        return AjaxResult.success("审批通过");
-    }
-
-    @PostMapping("/workflow/lobster/event-audit/reject/{id}")
-    public AjaxResult lobsterEventAuditReject(@PathVariable Long id) {
-        return AjaxResult.success("已驳回");
-    }
-
-    // ========== 优化建议补充 ==========
-    @GetMapping("/workflow/lobster/optimization/pending-audit")
-    public TableDataInfo lobsterOptimizationPendingAudit() {
-        return safeListFromTable("lobster_optimization_suggestion");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/batch-audit")
-    public AjaxResult lobsterOptimizationBatchAudit() {
-        return AjaxResult.success("审核完成");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/audit/{optimizationId}")
-    public AjaxResult lobsterOptimizationAuditSingle(@PathVariable Long optimizationId) {
-        return AjaxResult.success("审核完成");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/analyze")
-    public AjaxResult lobsterOptimizationAnalyze() {
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalSuggestions", 0);
-        return AjaxResult.success(result);
-    }
-
-    @GetMapping("/workflow/lobster/optimization/stats")
-    public AjaxResult lobsterOptimizationStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("total", 0);
-        stats.put("pending", 0);
-        stats.put("approved", 0);
-        stats.put("rejected", 0);
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM lobster_optimization_suggestion GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    stats.put(s, cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    @GetMapping("/workflow/lobster/optimization/config")
-    public AjaxResult lobsterOptimizationConfig() {
-        return AjaxResult.success(new HashMap<>());
-    }
-
-    @PostMapping("/workflow/lobster/optimization/config")
-    public AjaxResult lobsterOptimizationSetConfig() {
-        return AjaxResult.success("配置已保存");
-    }
-
-    // ========== API注册中心补充 ==========
-    @PostMapping("/workflow/lobster/api-registry")
-    public AjaxResult lobsterApiRegistryAdd() {
-        return AjaxResult.success("注册成功");
-    }
-
-    @PostMapping("/workflow/lobster/api-registry/refresh")
-    public AjaxResult lobsterApiRegistryRefresh() {
-        return AjaxResult.success("缓存已刷新");
-    }
-
-    @GetMapping("/workflow/lobster/api-registry/categories")
-    public AjaxResult lobsterApiRegistryCategories() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    // ========== Token计费补充 ==========
-    @GetMapping("/workflow/lobster/billing/token-coefficient")
-    public AjaxResult lobsterBillingTokenCoefficient() {
-        Map<String, Object> data = new HashMap<>();
-        data.put("coefficient", 1.0);
-        data.put("updatedAt", "");
-        return AjaxResult.success(data);
-    }
-
-    @PutMapping("/workflow/lobster/billing/token-coefficient")
-    public AjaxResult lobsterBillingUpdateTokenCoefficient() {
-        return AjaxResult.success("更新成功");
-    }
-
-    @GetMapping("/workflow/lobster/billing/records")
-    public TableDataInfo lobsterBillingRecords() {
-        return safeListFromTable("lobster_billing_record");
-    }
-
-    @GetMapping("/workflow/lobster/billing/stats")
-    public AjaxResult lobsterBillingStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("totalTokens", 0);
-        stats.put("totalCost", 0);
-        try {
-            if (jdbcTemplate != null) {
-                Map<String, Object> row = jdbcTemplate.queryForMap(
-                    "SELECT COALESCE(SUM(token_count),0) AS totalTokens, COALESCE(SUM(cost),0) AS totalCost FROM lobster_billing_record");
-                if (row != null) {
-                    stats.putAll(row);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    @GetMapping("/workflow/lobster/billing/types")
-    public AjaxResult lobsterBillingTypes() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-}

+ 126 - 0
fs-admin/src/main/java/com/fs/admin/controller/AdminVoiceConfigController.java

@@ -0,0 +1,126 @@
+package com.fs.admin.controller;
+
+import cn.hutool.json.JSONUtil;
+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.DataSourceType;
+import com.fs.company.domain.CompanyVoiceConfig;
+import com.fs.company.param.CompanyVoiceConfigParam;
+import com.fs.company.service.ICompanyVoiceConfigService;
+import com.fs.company.vo.CompanyVoiceConfigListVO;
+import com.fs.company.vo.CompanyVoiceConfigVO;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.system.config.SystemVoiceConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 呼叫频率配置(主库 company_voice_config)
+ */
+@RestController
+@RequestMapping("/company/companyVoiceConfig")
+public class AdminVoiceConfigController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceConfigService companyVoiceConfigService;
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceConfig query) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CompanyVoiceConfigListVO> list = companyVoiceConfigService != null
+                ? companyVoiceConfigService.selectCompanyVoiceConfigListVO(query)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:query')")
+    @GetMapping("/{configId}")
+    public AjaxResult getInfo(@PathVariable Long configId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        CompanyVoiceConfig config = companyVoiceConfigService.selectCompanyVoiceConfigById(configId);
+        return AjaxResult.success(toParam(config));
+    }
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:add')")
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyVoiceConfigParam param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        return toAjax(companyVoiceConfigService.insertCompanyVoiceConfig(fromParam(param)));
+    }
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:edit')")
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyVoiceConfigParam param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        return toAjax(companyVoiceConfigService.updateCompanyVoiceConfig(fromParam(param)));
+    }
+
+    @PreAuthorize("@ss.hasPermi('platform:companyVoiceConfig:remove')")
+    @DeleteMapping("/{configIds}")
+    public AjaxResult remove(@PathVariable Long[] configIds) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        return toAjax(companyVoiceConfigService.deleteCompanyVoiceConfigByIds(configIds));
+    }
+
+    private CompanyVoiceConfig fromParam(CompanyVoiceConfigParam param) {
+        CompanyVoiceConfig config = new CompanyVoiceConfig();
+        config.setConfigId(param.getConfigId());
+        config.setCompanyId(param.getCompanyId());
+        SystemVoiceConfig caller = new SystemVoiceConfig();
+        caller.setCallerMinute(param.getCallerMinute());
+        caller.setCallerHour(param.getCallerHour());
+        caller.setCallerDay(param.getCallerDay());
+        caller.setCallerWeek(param.getCallerWeek());
+        caller.setCallerMonth(param.getCallerMonth());
+        SystemVoiceConfig callee = new SystemVoiceConfig();
+        callee.setCalleeMinute(param.getCalleeMinute());
+        callee.setCalleeHour(param.getCalleeHour());
+        callee.setCalleeDay(param.getCalleeDay());
+        callee.setCalleeWeek(param.getCalleeWeek());
+        callee.setCalleeMonth(param.getCalleeMonth());
+        config.setCallerJson(JSONUtil.toJsonStr(caller));
+        config.setCalleeJson(JSONUtil.toJsonStr(callee));
+        return config;
+    }
+
+    private CompanyVoiceConfigVO toParam(CompanyVoiceConfig config) {
+        CompanyVoiceConfigVO vo = new CompanyVoiceConfigVO();
+        vo.setConfigId(config.getConfigId());
+        vo.setCompanyId(config.getCompanyId());
+        if (config.getCallerJson() != null) {
+            SystemVoiceConfig caller = JSONUtil.toBean(config.getCallerJson(), SystemVoiceConfig.class);
+            vo.setCallerMinute(caller.getCallerMinute());
+            vo.setCallerHour(caller.getCallerHour());
+            vo.setCallerDay(caller.getCallerDay());
+            vo.setCallerWeek(caller.getCallerWeek());
+            vo.setCallerMonth(caller.getCallerMonth());
+        }
+        if (config.getCalleeJson() != null) {
+            SystemVoiceConfig callee = JSONUtil.toBean(config.getCalleeJson(), SystemVoiceConfig.class);
+            vo.setCalleeMinute(callee.getCalleeMinute());
+            vo.setCalleeHour(callee.getCalleeHour());
+            vo.setCalleeDay(callee.getCalleeDay());
+            vo.setCalleeWeek(callee.getCalleeWeek());
+            vo.setCalleeMonth(callee.getCalleeMonth());
+        }
+        return vo;
+    }
+}

+ 1 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java

@@ -69,6 +69,7 @@ public class CompanyAdminController extends BaseController {
     public void export(HttpServletResponse response, TenantInfo tenantInfo) throws IOException {
     public void export(HttpServletResponse response, TenantInfo tenantInfo) throws IOException {
         List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
         List<TenantInfo> list = tenantInfoService.selectTenantInfoList(tenantInfo);
         ExcelUtil<TenantInfo> util = new ExcelUtil<>(TenantInfo.class);
         ExcelUtil<TenantInfo> util = new ExcelUtil<>(TenantInfo.class);
+        response.setHeader("Content-Disposition", "attachment;filename=租户列表数据.xlsx");
         util.exportExcel(response, list, "租户列表数据");
         util.exportExcel(response, list, "租户列表数据");
     }
     }
 
 

+ 245 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java

@@ -0,0 +1,245 @@
+package com.fs.admin.controller;
+
+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.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.ICompanyVoiceApiTenantService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+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.List;
+import java.util.Map;
+
+/**
+ * 外呼接口-租户分配与定价管理(新版 admin 专用)
+ *
+ * @author MixLiu
+ */
+@RestController
+@RequestMapping("/admin/companyVoiceApiTenant")
+public class CompanyVoiceApiTenantController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiService companyVoiceApiService;
+
+    /**
+     * 分页查询租户-接口绑定/定价列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceApiTenant param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null
+                ? companyVoiceApiTenantService.selectCompanyVoiceApiTenantList(param)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 外呼接口下拉选项(新增绑定时选择接口)
+     */
+    @GetMapping("/apiList")
+    public TableDataInfo apiList() {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        List<CompanyVoiceApi> list = companyVoiceApiService != null
+                ? companyVoiceApiService.selectCompanyVoiceApiList(new CompanyVoiceApi())
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 分配接口给租户(批量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "分配通话接口给租户", businessType = BusinessType.INSERT)
+    @PostMapping("/assign")
+    public AjaxResult assign(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        Long apiId = Long.valueOf(body.get("apiId").toString());
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (tenantIds.isEmpty()) {
+            tenantIds = parseIdList(body.get("companyIds"));
+        }
+        if (tenantIds.isEmpty()) {
+            return AjaxResult.error("请选择租户");
+        }
+        companyVoiceApiTenantService.batchAssignTenants(apiId, parseTenantAssignList(body, apiId, tenantIds));
+        return AjaxResult.success();
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<CompanyVoiceApiTenant> parseTenantAssignList(Map<String, Object> body, Long apiId, List<Long> tenantIds) {
+        List<CompanyVoiceApiTenant> list = new ArrayList<>();
+        Object rawTenants = body.get("tenants");
+        if (rawTenants instanceof List) {
+            for (Object item : (List<?>) rawTenants) {
+                if (!(item instanceof Map)) {
+                    continue;
+                }
+                Map<String, Object> tenantMap = (Map<String, Object>) item;
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                if (tenantMap.get("tenantId") != null) {
+                    tenant.setTenantId(Long.valueOf(tenantMap.get("tenantId").toString()));
+                }
+                if (tenantMap.get("tenantCode") != null) {
+                    tenant.setTenantCode(tenantMap.get("tenantCode").toString());
+                }
+                if (tenantMap.get("tenantName") != null) {
+                    tenant.setTenantName(tenantMap.get("tenantName").toString());
+                }
+                if (tenant.getTenantId() != null) {
+                    list.add(tenant);
+                }
+            }
+        }
+        if (list.isEmpty()) {
+            for (Long tenantId : tenantIds) {
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                tenant.setTenantId(tenantId);
+                list.add(tenant);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 取消分配
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
+    @DeleteMapping("/unassign")
+    public AjaxResult unassign(@RequestParam Long apiId,
+                               @RequestParam(required = false) Long tenantId,
+                               @RequestParam(required = false) Long companyId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        Long tid = tenantId != null ? tenantId : companyId;
+        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, tid));
+    }
+
+    /**
+     * 更新租户定价/绑定配置
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "更新外呼租户定价", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult update(@RequestBody CompanyVoiceApiTenant data) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        if (data.getId() == null && data.getApiId() != null && data.getTenantId() != null) {
+            CompanyVoiceApiTenant existing = companyVoiceApiTenantService.selectByApiAndTenant(
+                    data.getApiId(), data.getTenantId());
+            if (existing != null) {
+                data.setId(existing.getId());
+            }
+        }
+        return toAjax(companyVoiceApiTenantService.updateCompanyVoiceApiTenant(data));
+    }
+
+    /**
+     * 批量更新租户定价/绑定配置
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "批量更新外呼租户定价", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchPricing")
+    public AjaxResult batchPricing(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        List<Long> ids = parseIdList(body.get("ids"));
+        if (ids.isEmpty()) {
+            return AjaxResult.error("请选择要更新的记录");
+        }
+        CompanyVoiceApiTenant pricing = parsePricingFields(body);
+        if (pricing.getSalePrice() == null && pricing.getPriority() == null
+                && pricing.getIsPrimary() == null && pricing.getSelectable() == null) {
+            return AjaxResult.error("请至少填写一项要批量更新的配置");
+        }
+        return toAjax(companyVoiceApiTenantService.batchUpdatePricing(ids, pricing));
+    }
+
+    /**
+     * 批量更新状态
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "批量更新外呼租户状态", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchStatus")
+    public AjaxResult batchStatus(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        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(companyVoiceApiTenantService.batchUpdateStatus(ids, status));
+    }
+
+    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()));
+        }
+        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
+            pricing.setPriority(Integer.valueOf(body.get("priority").toString()));
+        }
+        if (body.get("isPrimary") != null && StringUtils.isNotEmpty(body.get("isPrimary").toString())) {
+            pricing.setIsPrimary(Integer.valueOf(body.get("isPrimary").toString()));
+        }
+        Object selectable = body.get("selectable");
+        if (selectable == null) {
+            selectable = body.get("allowManual");
+        }
+        if (selectable != null && StringUtils.isNotEmpty(selectable.toString())) {
+            pricing.setSelectable(selectable.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;
+    }
+}

+ 93 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceController.java

@@ -0,0 +1,93 @@
+package com.fs.admin.controller;
+
+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.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 外呼接口管理(新版 admin 专用)
+ *
+ * @author MixLiu
+ */
+@RestController
+@RequestMapping("/admin/companyVoice")
+public class CompanyVoiceController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiService companyVoiceApiService;
+
+    /**
+     * 分页查询外呼接口列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceApi param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CompanyVoiceApi> list = companyVoiceApiService != null
+                ? companyVoiceApiService.selectCompanyVoiceApiList(param)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取外呼接口详情
+     */
+    @GetMapping("/{apiId}")
+    public AjaxResult getInfo(@PathVariable Long apiId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        CompanyVoiceApi api = companyVoiceApiService.selectCompanyVoiceApiById(apiId);
+        return AjaxResult.success(api);
+    }
+
+    /**
+     * 新增外呼接口
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyVoiceApi companyVoiceApi) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.insertCompanyVoiceApi(companyVoiceApi));
+    }
+
+    /**
+     * 修改外呼接口
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyVoiceApi companyVoiceApi) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.updateCompanyVoiceApi(companyVoiceApi));
+    }
+
+    /**
+     * 删除外呼接口(逻辑删除)
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{apiId}")
+    public AjaxResult remove(@PathVariable Long apiId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.deleteCompanyVoiceApiById(apiId));
+    }
+}

+ 2 - 2
fs-admin/src/main/resources/application-dev.yml

@@ -3,8 +3,8 @@ spring:
     # redis 配置
     # redis 配置
     redis:
     redis:
         # 地址
         # 地址
-        #host: localhost
-        host: 172.27.0.7
+        host: localhost
+#        host: 172.27.0.7
         # 端口,默认为6379
         # 端口,默认为6379
         port: 6379
         port: 6379
         # 数据库索引
         # 数据库索引

+ 1 - 1
fs-agent/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java

@@ -5,7 +5,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 
 
 /**
 /**
- * Controller ʹ��ȫ���������� fs-saasadmin ͬ����ͻ��Service ������Ĭ�϶�����
+ * Controller 使用全限定类名避免与 fs-saasadmin 同名冲突,Service 则使用默认短名称
  */
  */
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 
 

+ 43 - 0
fs-agent/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql

@@ -0,0 +1,43 @@
+-- =====================================================
+-- 龙虾引擎 新增页面菜单
+-- 新增 7 个缺失的菜单记录(模拟聊天测试、评分准确性验证、
+--   用户画像配置、摘要生成配置、消息去重配置、敏感词库、节点详情)
+-- =====================================================
+
+-- =====================================================
+-- PART 1: 新增页面菜单 (menu_id 29920-29926)
+
+-- 15. 模拟聊天测试
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29920, '模拟聊天测试', 29900, 13, 'chat-test', 'lobster/chat-test/index', 'C', 'el-icon-chat-dot-square', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 16. 评分准确性验证
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29921, '评分准确性验证', 29900, 14, 'quality-verify', 'lobster/quality-verify/index', 'C', 'el-icon-data-analysis', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 17. 用户画像配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29922, '用户画像配置', 29900, 15, 'profile-config', 'lobster/profile-config/index', 'C', 'el-icon-user', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 18. 摘要生成配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29923, '摘要生成配置', 29900, 16, 'summary-config', 'lobster/summary-config/index', 'C', 'el-icon-notebook', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 19. 消息去重配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29924, '消息去重配置', 29900, 17, 'dedup-config', 'lobster/dedup-config/index', 'C', 'el-icon-filter', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 20. 敏感词库
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29925, '敏感词库', 29900, 18, 'sensitive-words', 'lobster/sensitive-words/index', 'C', 'el-icon-warning', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 21. 节点详情 (hidden, 从实例页跳转)
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29926, '节点详情', 29900, 19, 'node-detail', 'lobster/node-detail/index', 'C', 'el-icon-document', '1', '0', 0, 0, 'admin', NOW(), '从实例监控页跳转');
+
+-- =====================================================
+-- PART 2: 同步更新模板表 tenant_sys_menu
+-- =====================================================
+INSERT IGNORE INTO tenant_sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark
+FROM sys_menu WHERE menu_id BETWEEN 29920 AND 29926;

+ 3 - 0
fs-comm-gateway/src/main/java/com/fs/comm/dto/CommCallSendRequest.java

@@ -9,6 +9,9 @@ public class CommCallSendRequest {
 
 
     private String phone;
     private String phone;
 
 
+    /** 调用人(内部调用时可随请求体传入,写入主库日志) */
+    private Long companyUserId;
+
     private Long calleeId;
     private Long calleeId;
 
 
     private Long roboticId;
     private Long roboticId;

+ 2 - 0
fs-comm-gateway/src/main/java/com/fs/comm/dto/CommSmsSendRequest.java

@@ -17,6 +17,8 @@ public class CommSmsSendRequest {
 
 
     private Long calleeId;
     private Long calleeId;
 
 
+    private Long companyId;
+
     private String nodeKey;
     private String nodeKey;
 
 
     private String workflowInstanceId;
     private String workflowInstanceId;

+ 75 - 29
fs-comm-gateway/src/main/java/com/fs/comm/service/CommCallService.java

@@ -6,6 +6,7 @@ import com.fs.comm.metrics.CommMetricsService;
 import com.fs.comm.model.CommCallSendParam;
 import com.fs.comm.model.CommCallSendParam;
 import com.fs.comm.model.CommCallSendResult;
 import com.fs.comm.model.CommCallSendResult;
 import com.fs.comm.ratelimit.CommRateLimitService;
 import com.fs.comm.ratelimit.CommRateLimitService;
+import com.fs.comm.support.CommTenantDataSourceHelper;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -31,38 +32,83 @@ public class CommCallService {
     @Autowired
     @Autowired
     private CommMetricsService commMetricsService;
     private CommMetricsService commMetricsService;
 
 
+    @Autowired
+    private CommVoiceLimitService commVoiceLimitService;
+
+    @Autowired
+    private CommGatewayApiLogRecorder commGatewayApiLogRecorder;
+
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
     public Map<String, Object> sendCall(CommCallSendRequest request) {
     public Map<String, Object> sendCall(CommCallSendRequest request) {
+        long startMs = System.currentTimeMillis();
         Long companyId = CommAuthContext.getCompanyId();
         Long companyId = CommAuthContext.getCompanyId();
         Long tenantId = CommAuthContext.getTenantId();
         Long tenantId = CommAuthContext.getTenantId();
-        if (companyId == null) {
-            throw new ServiceException("未获取到公司信息");
-        }
-        commRateLimitService.checkTenantQps(tenantId);
-        commGatewayLineAuthService.validateGateway(companyId, request.getGatewayId());
-
-        CommCallSendResult result = commCallSendService.sendWorkflowCall(CommCallSendParam.builder()
-                .roboticId(request.getRoboticId())
-                .calleeId(request.getCalleeId())
-                .businessId(request.getBusinessId())
-                .gatewayId(request.getGatewayId())
-                .nodeKey(request.getNodeKey())
-                .workflowInstanceId(request.getWorkflowInstanceId())
-                .companyId(companyId)
-                .tenantId(tenantId)
-                .callbackUrl(request.getCallbackUrl())
-                .phone(request.getPhone())
-                .bizParams(request.getBizParams())
-                .build());
-
-        if (StringUtils.isBlank(result.getPhone())) {
-            throw new ServiceException("外呼发起失败:未获取到有效被叫号码");
-        }
+        Long gatewayId = request != null ? request.getGatewayId() : null;
+        String calleePhone = null;
+        String callerKey = null;
+        CommGatewayApiLogRecorder.CommApiRecordResult recordResult = null;
 
 
-        Map<String, Object> response = new HashMap<>();
-        response.put("callBackUuid", result.getCallBackUuid());
-        response.put("batchId", result.getBatchId());
-        response.put("phone", result.getPhone());
-        commMetricsService.increment("call.success");
-        return response;
+        try {
+            if (companyId == null) {
+                throw new ServiceException("未获取到公司信息");
+            }
+            calleePhone = commVoiceLimitService.resolveCallCalleePhone(request.getCalleeId(), request.getPhone());
+            callerKey = commVoiceLimitService.buildCompanyCallerKey(companyId, gatewayId);
+
+            commRateLimitService.checkTenantQps(tenantId);
+            commVoiceLimitService.checkCallLimit(companyId, tenantId, request.getCalleeId(), request.getPhone(), gatewayId);
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+            commGatewayLineAuthService.validateGateway(companyId, tenantId, gatewayId);
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+
+            CommCallSendResult result = commCallSendService.sendWorkflowCall(CommCallSendParam.builder()
+                    .roboticId(request.getRoboticId())
+                    .calleeId(request.getCalleeId())
+                    .businessId(request.getBusinessId())
+                    .gatewayId(gatewayId)
+                    .nodeKey(request.getNodeKey())
+                    .workflowInstanceId(request.getWorkflowInstanceId())
+                    .companyId(companyId)
+                    .tenantId(tenantId)
+                    .callbackUrl(request.getCallbackUrl())
+                    .phone(request.getPhone())
+                    .bizParams(request.getBizParams())
+                    .build());
+
+            if (StringUtils.isBlank(result.getPhone())) {
+                throw new ServiceException("外呼发起失败:未获取到有效被叫号码");
+            }
+
+            if (StringUtils.isBlank(calleePhone)) {
+                calleePhone = commVoiceLimitService.normalizePhone(result.getPhone());
+            }
+
+            Map<String, Object> response = new HashMap<>();
+            response.put("callBackUuid", result.getCallBackUuid());
+            response.put("batchId", result.getBatchId());
+            response.put("phone", result.getPhone());
+
+            recordResult = commGatewayApiLogRecorder.buildSuccess(response, calleePhone, callerKey, gatewayId);
+            commMetricsService.increment("call.success");
+            return response;
+        } catch (ServiceException ex) {
+            boolean limitHit = CommGatewayApiLogRecorder.isLimitFailure(ex);
+            recordResult = limitHit
+                    ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, gatewayId)
+                    : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, gatewayId);
+            throw ex;
+        } catch (Exception ex) {
+            log.error("外呼调用异常 companyId={}", companyId, ex);
+            ServiceException wrapped = ex instanceof ServiceException
+                    ? (ServiceException) ex
+                    : new ServiceException(StringUtils.defaultIfBlank(ex.getMessage(), "外呼调用异常"));
+            recordResult = commGatewayApiLogRecorder.buildFailure(wrapped, calleePhone, callerKey, gatewayId);
+            throw wrapped;
+        } finally {
+            commGatewayApiLogRecorder.recordCallAttempt(companyId, tenantId, request, recordResult,
+                    calleePhone, callerKey, gatewayId, startMs);
+        }
     }
     }
 }
 }

+ 316 - 0
fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayApiLogRecorder.java

@@ -0,0 +1,316 @@
+package com.fs.comm.service;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.comm.auth.CommSession;
+import com.fs.comm.context.CommAuthContext;
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.comm.dto.CommCallSendRequest;
+import com.fs.comm.dto.CommSmsSendRequest;
+import com.fs.comm.model.CommGatewayBillingQuote;
+import com.fs.comm.support.CommTenantDataSourceHelper;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.company.domain.CompanyCommGatewayLog;
+import com.fs.company.service.ICompanyCommGatewayLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 通讯网关 API 调用日志记录(主库 + 租户库)
+ */
+@Slf4j
+@Service
+public class CommGatewayApiLogRecorder {
+
+    @Autowired
+    private ICommGatewayApiLogService commGatewayApiLogService;
+
+    @Autowired
+    private ICompanyCommGatewayLogService companyCommGatewayLogService;
+
+    @Autowired
+    private CommGatewayBillingService commGatewayBillingService;
+
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
+    public void recordCallAttempt(Long companyId, Long tenantId, CommCallSendRequest request,
+                                  CommApiRecordResult result, String calleePhone, String callerPhone,
+                                  Long gatewayId, long startMs) {
+        record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_CALL, "/comm/call/send",
+                request, result, calleePhone, callerPhone, gatewayId, startMs));
+    }
+
+    public void recordSmsAttempt(Long companyId, Long tenantId, CommSmsSendRequest request,
+                                 CommApiRecordResult result, String calleePhone, String callerPhone,
+                                 Long gatewayId, long startMs) {
+        record(buildBaseLog(companyId, tenantId, CommGatewayApiLog.API_TYPE_SMS, "/comm/sms/send",
+                request, result, calleePhone, callerPhone, gatewayId, startMs));
+    }
+
+    private CommGatewayApiLog buildBaseLog(Long companyId, Long tenantId, String apiType, String apiPath,
+                                           Object request, CommApiRecordResult result, String calleePhone,
+                                           String callerPhone, Long gatewayId, long startMs) {
+        CommSession session = CommAuthContext.get();
+        CommGatewayApiLog logEntity = new CommGatewayApiLog();
+        logEntity.setTenantId(tenantId);
+        logEntity.setCompanyId(companyId);
+        if (session != null) {
+            logEntity.setCompanyUserId(session.getCompanyUserId());
+            logEntity.setCallerAccount(session.getAccount());
+            logEntity.setAuthScope(session.getScope());
+        }
+        fillIdentityFromRequest(logEntity, request);
+        logEntity.setApiType(apiType);
+        logEntity.setApiPath(apiPath);
+        logEntity.setRequestBody(JSON.toJSONString(request));
+        logEntity.setDurationMs((int) (System.currentTimeMillis() - startMs));
+        try {
+            logEntity.setClientIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        } catch (Exception ignored) {
+        }
+
+        if (result != null) {
+            logEntity.setResponseBody(result.getResponseBody());
+            logEntity.setResultCode(result.getResultCode());
+            logEntity.setResultMsg(result.getResultMsg());
+            logEntity.setSuccess(result.isSuccess() ? 1 : 0);
+            logEntity.setLimitHit(result.isLimitHit() ? 1 : 0);
+            logEntity.setLimitReason(result.getLimitReason());
+            logEntity.setCalleePhone(result.getCalleePhone());
+            logEntity.setCallerPhone(result.getCallerPhone());
+            logEntity.setGatewayId(result.getGatewayId());
+            applyBilling(logEntity, tenantId, apiType, result.isSuccess());
+        } else {
+            applyFallbackFields(logEntity, calleePhone, callerPhone, gatewayId, "调用未正常返回结果");
+        }
+
+        if (StringUtils.isBlank(logEntity.getCalleePhone()) && StringUtils.isNotBlank(calleePhone)) {
+            logEntity.setCalleePhone(calleePhone);
+        }
+        if (StringUtils.isBlank(logEntity.getCallerPhone()) && StringUtils.isNotBlank(callerPhone)) {
+            logEntity.setCallerPhone(callerPhone);
+        }
+        if (logEntity.getGatewayId() == null && gatewayId != null) {
+            logEntity.setGatewayId(gatewayId);
+        }
+        return logEntity;
+    }
+
+    private void applyBilling(CommGatewayApiLog logEntity, Long tenantId, String apiType, boolean success) {
+        logEntity.setBillingUnit(apiType);
+        if (!success) {
+            logEntity.setBillingAmount(BigDecimal.ZERO);
+            logEntity.setCostPrice(BigDecimal.ZERO);
+            logEntity.setCalcPrice(BigDecimal.ZERO);
+            logEntity.setBillingQuantity(0);
+            return;
+        }
+        CommGatewayBillingQuote quote = commGatewayBillingService.resolveQuote(tenantId, apiType, 1);
+        logEntity.setCostPrice(quote.getCostPrice());
+        logEntity.setCalcPrice(quote.getCalcPrice());
+        logEntity.setBillingQuantity(quote.getBillingQuantity());
+        logEntity.setBillingAmount(quote.getBillingAmount());
+    }
+
+    private void fillIdentityFromRequest(CommGatewayApiLog logEntity, Object request) {
+        if (request instanceof CommCallSendRequest) {
+            CommCallSendRequest callRequest = (CommCallSendRequest) request;
+            if (callRequest.getCompanyUserId() != null) {
+                logEntity.setCompanyUserId(callRequest.getCompanyUserId());
+            }
+        } else if (request instanceof CommSmsSendRequest) {
+            CommSmsSendRequest smsRequest = (CommSmsSendRequest) request;
+            if (smsRequest.getCompanyId() != null) {
+                logEntity.setCompanyId(smsRequest.getCompanyId());
+            }
+            if (smsRequest.getCompanyUserId() != null) {
+                logEntity.setCompanyUserId(smsRequest.getCompanyUserId());
+            }
+        }
+        if (logEntity.getCompanyUserId() == null) {
+            logEntity.setCompanyUserId(CommAuthContext.getCompanyUserId());
+        }
+    }
+
+    private void applyFallbackFields(CommGatewayApiLog logEntity, String calleePhone, String callerPhone,
+                                     Long gatewayId, String message) {
+        logEntity.setCalleePhone(calleePhone);
+        logEntity.setCallerPhone(callerPhone);
+        logEntity.setGatewayId(gatewayId);
+        logEntity.setSuccess(0);
+        logEntity.setLimitHit(0);
+        logEntity.setResultCode(500);
+        logEntity.setResultMsg(message);
+        logEntity.setBillingAmount(BigDecimal.ZERO);
+        logEntity.setCostPrice(BigDecimal.ZERO);
+        logEntity.setCalcPrice(BigDecimal.ZERO);
+        logEntity.setBillingQuantity(0);
+        logEntity.setBillingUnit(logEntity.getApiType());
+        Map<String, Object> body = new HashMap<>();
+        body.put("code", 500);
+        body.put("msg", message);
+        logEntity.setResponseBody(JSON.toJSONString(body));
+    }
+
+    private void record(CommGatewayApiLog logEntity) {
+        Long masterLogId = null;
+        try {
+            commGatewayApiLogService.saveLog(logEntity);
+            masterLogId = logEntity.getLogId();
+        } catch (Exception ex) {
+            log.error("写入主库通讯网关调用日志失败", ex);
+        }
+        saveTenantLog(logEntity, masterLogId);
+    }
+
+    private void saveTenantLog(CommGatewayApiLog logEntity, Long masterLogId) {
+        if (logEntity == null || logEntity.getTenantId() == null) {
+            return;
+        }
+        try {
+            commTenantDataSourceHelper.ensureTenant(logEntity.getTenantId());
+            companyCommGatewayLogService.saveLog(toTenantLog(logEntity, masterLogId));
+        } catch (Exception ex) {
+            log.error("写入租户通讯网关调用日志失败 tenantId={}, companyId={}",
+                    logEntity.getTenantId(), logEntity.getCompanyId(), ex);
+        }
+    }
+
+    private CompanyCommGatewayLog toTenantLog(CommGatewayApiLog source, Long masterLogId) {
+        CompanyCommGatewayLog target = new CompanyCommGatewayLog();
+        target.setMasterLogId(masterLogId);
+        target.setCompanyId(source.getCompanyId());
+        target.setCompanyUserId(source.getCompanyUserId());
+        target.setCallerAccount(source.getCallerAccount());
+        target.setApiType(source.getApiType());
+        target.setApiPath(source.getApiPath());
+        target.setRequestBody(source.getRequestBody());
+        target.setResponseBody(source.getResponseBody());
+        target.setResultCode(source.getResultCode());
+        target.setResultMsg(source.getResultMsg());
+        target.setSuccess(source.getSuccess());
+        target.setLimitHit(source.getLimitHit());
+        target.setLimitReason(source.getLimitReason());
+        target.setCalleePhone(source.getCalleePhone());
+        target.setCallerPhone(source.getCallerPhone());
+        target.setGatewayId(source.getGatewayId());
+        target.setBillingAmount(source.getBillingAmount());
+        target.setCostPrice(source.getCostPrice());
+        target.setCalcPrice(source.getCalcPrice());
+        target.setBillingQuantity(source.getBillingQuantity());
+        target.setBillingUnit(source.getBillingUnit());
+        target.setClientIp(source.getClientIp());
+        target.setAuthScope(source.getAuthScope());
+        target.setDurationMs(source.getDurationMs());
+        target.setCreateTime(source.getCreateTime());
+        return target;
+    }
+
+    /** 是否为频率/QPS 类限制失败 */
+    public static boolean isLimitFailure(ServiceException ex) {
+        if (ex == null || StringUtils.isBlank(ex.getMessage())) {
+            return false;
+        }
+        String message = ex.getMessage();
+        return message.contains("限制") || message.contains("超限") || message.contains("频率");
+    }
+
+    public CommApiRecordResult buildLimitFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
+        Map<String, Object> body = new HashMap<>();
+        body.put("code", 500);
+        body.put("msg", ex.getMessage());
+        return CommApiRecordResult.builder()
+                .success(false)
+                .limitHit(true)
+                .limitReason(ex.getMessage())
+                .resultCode(500)
+                .resultMsg(ex.getMessage())
+                .responseBody(JSON.toJSONString(body))
+                .calleePhone(calleePhone)
+                .callerPhone(callerPhone)
+                .gatewayId(gatewayId)
+                .build();
+    }
+
+    public CommApiRecordResult buildSuccess(Object data, String calleePhone, String callerPhone, Long gatewayId) {
+        Map<String, Object> body = new HashMap<>();
+        body.put("code", 200);
+        body.put("msg", "success");
+        body.put("data", data);
+        return CommApiRecordResult.builder()
+                .success(true)
+                .limitHit(false)
+                .resultCode(200)
+                .resultMsg("success")
+                .responseBody(JSON.toJSONString(body))
+                .calleePhone(calleePhone)
+                .callerPhone(callerPhone)
+                .gatewayId(gatewayId)
+                .build();
+    }
+
+    public CommApiRecordResult buildFailure(ServiceException ex, String calleePhone, String callerPhone, Long gatewayId) {
+        Map<String, Object> body = new HashMap<>();
+        body.put("code", 500);
+        body.put("msg", ex.getMessage());
+        return CommApiRecordResult.builder()
+                .success(false)
+                .limitHit(false)
+                .resultCode(500)
+                .resultMsg(ex.getMessage())
+                .responseBody(JSON.toJSONString(body))
+                .calleePhone(calleePhone)
+                .callerPhone(callerPhone)
+                .gatewayId(gatewayId)
+                .build();
+    }
+
+    public static class CommApiRecordResult {
+        private boolean success;
+        private boolean limitHit;
+        private Integer resultCode;
+        private String resultMsg;
+        private String responseBody;
+        private String limitReason;
+        private String calleePhone;
+        private String callerPhone;
+        private Long gatewayId;
+
+        public static CommApiRecordResultBuilder builder() {
+            return new CommApiRecordResultBuilder();
+        }
+
+        public boolean isSuccess() { return success; }
+        public boolean isLimitHit() { return limitHit; }
+        public Integer getResultCode() { return resultCode; }
+        public String getResultMsg() { return resultMsg; }
+        public String getResponseBody() { return responseBody; }
+        public String getLimitReason() { return limitReason; }
+        public String getCalleePhone() { return calleePhone; }
+        public String getCallerPhone() { return callerPhone; }
+        public Long getGatewayId() { return gatewayId; }
+
+        public static class CommApiRecordResultBuilder {
+            private final CommApiRecordResult target = new CommApiRecordResult();
+
+            public CommApiRecordResultBuilder success(boolean success) { target.success = success; return this; }
+            public CommApiRecordResultBuilder limitHit(boolean limitHit) { target.limitHit = limitHit; return this; }
+            public CommApiRecordResultBuilder resultCode(Integer resultCode) { target.resultCode = resultCode; return this; }
+            public CommApiRecordResultBuilder resultMsg(String resultMsg) { target.resultMsg = resultMsg; return this; }
+            public CommApiRecordResultBuilder responseBody(String responseBody) { target.responseBody = responseBody; return this; }
+            public CommApiRecordResultBuilder limitReason(String limitReason) { target.limitReason = limitReason; return this; }
+            public CommApiRecordResultBuilder calleePhone(String calleePhone) { target.calleePhone = calleePhone; return this; }
+            public CommApiRecordResultBuilder callerPhone(String callerPhone) { target.callerPhone = callerPhone; return this; }
+            public CommApiRecordResultBuilder gatewayId(Long gatewayId) { target.gatewayId = gatewayId; return this; }
+            public CommApiRecordResult build() { return target; }
+        }
+    }
+}

+ 12 - 6
fs-comm-gateway/src/main/java/com/fs/comm/service/CommGatewayLineAuthService.java

@@ -2,9 +2,10 @@ package com.fs.comm.service;
 
 
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.comm.support.CommTenantDataSourceHelper;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
-import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyBindGatewayMapper;
 import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.vo.easycall.EasyCallGatewayVO;
 import com.fs.company.vo.easycall.EasyCallGatewayVO;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
@@ -27,18 +28,22 @@ public class CommGatewayLineAuthService {
     private IEasyCallService easyCallService;
     private IEasyCallService easyCallService;
 
 
     @Autowired
     @Autowired
-    private CompanyMapper companyMapper;
+    private CompanyBindGatewayMapper companyBindGatewayMapper;
 
 
     @Autowired
     @Autowired
     private ISysConfigService configService;
     private ISysConfigService configService;
 
 
-    public void validateGateway(Long companyId, Long gatewayId) {
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
+    public void validateGateway(Long companyId, Long tenantId, Long gatewayId) {
         if (gatewayId == null) {
         if (gatewayId == null) {
             throw new ServiceException("gatewayId不能为空");
             throw new ServiceException("gatewayId不能为空");
         }
         }
+        commTenantDataSourceHelper.ensureTenant(tenantId);
         List<EasyCallGatewayVO> allowed = easyCallService.getGatewayList(companyId);
         List<EasyCallGatewayVO> allowed = easyCallService.getGatewayList(companyId);
         if (allowed == null || allowed.isEmpty()) {
         if (allowed == null || allowed.isEmpty()) {
-            validateByConfigOnly(companyId, gatewayId);
+            validateByConfigOnly(companyId, tenantId, gatewayId);
             return;
             return;
         }
         }
         boolean matched = allowed.stream().anyMatch(item -> gatewayId.equals(item.getId()));
         boolean matched = allowed.stream().anyMatch(item -> gatewayId.equals(item.getId()));
@@ -47,8 +52,9 @@ public class CommGatewayLineAuthService {
         }
         }
     }
     }
 
 
-    private void validateByConfigOnly(Long companyId, Long gatewayId) {
-        String gateWayList = companyMapper.getGateWayList(companyId);
+    private void validateByConfigOnly(Long companyId, Long tenantId, Long gatewayId) {
+        commTenantDataSourceHelper.ensureTenant(tenantId);
+        String gateWayList = companyBindGatewayMapper.getGateWayIdListByCompanyId(companyId);
         if (StringUtils.isNotBlank(gateWayList)) {
         if (StringUtils.isNotBlank(gateWayList)) {
             List<Long> ids = Arrays.stream(gateWayList.split(","))
             List<Long> ids = Arrays.stream(gateWayList.split(","))
                     .map(String::trim).filter(StringUtils::isNotBlank).map(Long::valueOf).collect(Collectors.toList());
                     .map(String::trim).filter(StringUtils::isNotBlank).map(Long::valueOf).collect(Collectors.toList());

+ 71 - 22
fs-comm-gateway/src/main/java/com/fs/comm/service/CommSmsService.java

@@ -6,7 +6,9 @@ import com.fs.comm.metrics.CommMetricsService;
 import com.fs.comm.model.CommSmsSendParam;
 import com.fs.comm.model.CommSmsSendParam;
 import com.fs.comm.model.CommSmsSendResult;
 import com.fs.comm.model.CommSmsSendResult;
 import com.fs.comm.ratelimit.CommRateLimitService;
 import com.fs.comm.ratelimit.CommRateLimitService;
+import com.fs.comm.support.CommTenantDataSourceHelper;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
@@ -27,30 +29,77 @@ public class CommSmsService {
     @Autowired
     @Autowired
     private CommMetricsService commMetricsService;
     private CommMetricsService commMetricsService;
 
 
+    @Autowired
+    private CommVoiceLimitService commVoiceLimitService;
+
+    @Autowired
+    private CommGatewayApiLogRecorder commGatewayApiLogRecorder;
+
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
     public Map<String, Object> sendSms(CommSmsSendRequest request) {
     public Map<String, Object> sendSms(CommSmsSendRequest request) {
+        long startMs = System.currentTimeMillis();
         Long companyId = CommAuthContext.getCompanyId();
         Long companyId = CommAuthContext.getCompanyId();
         Long tenantId = CommAuthContext.getTenantId();
         Long tenantId = CommAuthContext.getTenantId();
-        commRateLimitService.checkTenantQps(tenantId);
-
-        CommSmsSendResult result = commSmsSendService.sendWorkflowSms(CommSmsSendParam.builder()
-                .roboticId(request.getRoboticId())
-                .calleeId(request.getCalleeId())
-                .smsTempId(request.getSmsTempId())
-                .nodeKey(request.getNodeKey())
-                .workflowInstanceId(request.getWorkflowInstanceId())
-                .companyId(companyId)
-                .companyUserId(request.getCompanyUserId() != null ? request.getCompanyUserId() : CommAuthContext.getCompanyUserId())
-                .senderName(request.getSenderName())
-                .phone(request.getPhone())
-                .customerId(request.getCustomerId())
-                .cardUrl(request.getCardUrl())
-                .build());
-
-        Map<String, Object> response = new HashMap<>();
-        response.put("callbackUuid", result.getCallbackUuid());
-        response.put("customerId", result.getCustomerId());
-        response.put("phone", result.getPhone());
-        commMetricsService.increment("sms.success");
-        return response;
+        String callerKey = null;
+        CommGatewayApiLogRecorder.CommApiRecordResult recordResult = null;
+        String calleePhone = null;
+
+        try {
+            if (companyId == null) {
+                throw new ServiceException("未获取到公司信息");
+            }
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+            if (request != null) {
+                calleePhone = commVoiceLimitService.resolveSmsCalleePhone(
+                        request.getCalleeId(), request.getCustomerId(), request.getPhone());
+            }
+            callerKey = commVoiceLimitService.buildCompanyCallerKey(companyId, null);
+
+            commRateLimitService.checkTenantQps(tenantId);
+            commVoiceLimitService.checkSmsLimit(companyId, tenantId, request.getCalleeId(), request.getCustomerId(), request.getPhone());
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+
+            CommSmsSendResult result = commSmsSendService.sendWorkflowSms(CommSmsSendParam.builder()
+                    .roboticId(request.getRoboticId())
+                    .calleeId(request.getCalleeId())
+                    .smsTempId(request.getSmsTempId())
+                    .nodeKey(request.getNodeKey())
+                    .workflowInstanceId(request.getWorkflowInstanceId())
+                    .companyId(request.getCompanyId() != null ? request.getCompanyId() : companyId)
+                    .companyUserId(request.getCompanyUserId() != null ? request.getCompanyUserId() : CommAuthContext.getCompanyUserId())
+                    .senderName(request.getSenderName())
+                    .phone(request.getPhone())
+                    .customerId(request.getCustomerId())
+                    .cardUrl(request.getCardUrl())
+                    .build());
+
+            calleePhone = commVoiceLimitService.normalizePhone(result.getPhone());
+            Map<String, Object> response = new HashMap<>();
+            response.put("callbackUuid", result.getCallbackUuid());
+            response.put("customerId", result.getCustomerId());
+            response.put("phone", result.getPhone());
+
+            recordResult = commGatewayApiLogRecorder.buildSuccess(response, calleePhone, callerKey, null);
+            commMetricsService.increment("sms.success");
+            return response;
+        } catch (ServiceException ex) {
+            boolean limitHit = CommGatewayApiLogRecorder.isLimitFailure(ex);
+            recordResult = limitHit
+                    ? commGatewayApiLogRecorder.buildLimitFailure(ex, calleePhone, callerKey, null)
+                    : commGatewayApiLogRecorder.buildFailure(ex, calleePhone, callerKey, null);
+            throw ex;
+        } catch (Exception ex) {
+            log.error("短信调用异常 companyId={}", companyId, ex);
+            ServiceException wrapped = ex instanceof ServiceException
+                    ? (ServiceException) ex
+                    : new ServiceException(StringUtils.defaultIfBlank(ex.getMessage(), "短信调用异常"));
+            recordResult = commGatewayApiLogRecorder.buildFailure(wrapped, calleePhone, callerKey, null);
+            throw wrapped;
+        } finally {
+            commGatewayApiLogRecorder.recordSmsAttempt(companyId, tenantId, request, recordResult,
+                    calleePhone, callerKey, null, startMs);
+        }
     }
     }
 }
 }

+ 144 - 0
fs-comm-gateway/src/main/java/com/fs/comm/sms/MyCommSmsProvider.java

@@ -0,0 +1,144 @@
+package com.fs.comm.sms;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.vo.SmsSendItemVO;
+import com.fs.common.vo.SmsSendVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 迈远(my)短信 HTTP 发送实现。
+ * 对接 company_sms_api / company_sms_api_port 配置的 url、account、password、sign、extno。
+ */
+@Slf4j
+@Service
+public class MyCommSmsProvider implements CommSmsProvider {
+
+    private static final String PROVIDER = "my";
+
+    @Override
+    public String provider() {
+        return PROVIDER;
+    }
+
+    @Override
+    public CommSmsChannelResult send(CommSmsChannelRequest request) {
+        String validateError = validate(request);
+        if (validateError != null) {
+            return CommSmsChannelResult.fail(validateError, null);
+        }
+
+        String sendUrl;
+        try {
+            sendUrl = buildSendUrl(request);
+        } catch (UnsupportedEncodingException e) {
+            log.error("MyCommSmsProvider URL编码异常 phone={}", request.getPhone(), e);
+            return CommSmsChannelResult.fail("ENCODE_ERROR", null);
+        }
+
+        String responseBody;
+        try {
+            responseBody = HttpRequest.get(sendUrl).timeout(15000).execute().body();
+        } catch (Exception e) {
+            log.error("MyCommSmsProvider HTTP请求异常 phone={}, url={}", request.getPhone(), maskUrl(sendUrl), e);
+            return CommSmsChannelResult.fail("HTTP_ERROR", e.getMessage());
+        }
+
+        return parseResponse(responseBody, request.getPhone());
+    }
+
+    private String validate(CommSmsChannelRequest request) {
+        if (request == null) {
+            return "INVALID_REQUEST";
+        }
+        if (StringUtils.isBlank(request.getPhone())) {
+            return "INVALID_PHONE";
+        }
+        if (StringUtils.isBlank(request.getUrl())) {
+            return "INVALID_URL";
+        }
+        if (StringUtils.isBlank(request.getAccount()) || StringUtils.isBlank(request.getPassword())) {
+            return "INVALID_CREDENTIAL";
+        }
+        if (request.getTempType() == null) {
+            return "UNSUPPORTED_TEMP_TYPE";
+        }
+        if (!Integer.valueOf(1).equals(request.getTempType()) && !Integer.valueOf(2).equals(request.getTempType())) {
+            return "UNSUPPORTED_TEMP_TYPE";
+        }
+        return null;
+    }
+
+    /**
+     * 迈远发送 URL:{url}sms?action=send&account=...&password=...&mobile=...&content=...&extno=...&rt=json
+     */
+    String buildSendUrl(CommSmsChannelRequest request) throws UnsupportedEncodingException {
+        String sign = StringUtils.defaultString(request.getSign());
+        String body = request.getContent();
+        if (Integer.valueOf(2).equals(request.getTempType())) {
+            body = body + "拒收请回复R";
+        }
+        String encodedContent = URLEncoder.encode(sign + body, StandardCharsets.UTF_8.name());
+        String baseUrl = normalizeBaseUrl(request.getUrl());
+        return baseUrl + "sms?action=send"
+                + "&account=" + request.getAccount()
+                + "&password=" + request.getPassword()
+                + "&mobile=" + request.getPhone()
+                + "&content=" + encodedContent
+                + "&extno=" + StringUtils.defaultString(request.getExtno())
+                + "&rt=json";
+    }
+
+    private CommSmsChannelResult parseResponse(String responseBody, String phone) {
+        if (StringUtils.isBlank(responseBody)) {
+            return CommSmsChannelResult.fail("EMPTY_RESPONSE", null);
+        }
+        try {
+            SmsSendVO vo = JSONUtil.toBean(responseBody, SmsSendVO.class);
+            if (vo == null || vo.getStatus() == null || !Integer.valueOf(0).equals(vo.getStatus())) {
+                log.warn("MyCommSmsProvider 发送失败 phone={}, response={}", phone, abbreviate(responseBody));
+                return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+            }
+            if (vo.getList() == null) {
+                return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+            }
+            for (SmsSendItemVO item : vo.getList()) {
+                if (item != null && "0".equals(item.getResult())) {
+                    return CommSmsChannelResult.ok(item.getMid());
+                }
+            }
+            log.warn("MyCommSmsProvider 无成功条目 phone={}, response={}", phone, abbreviate(responseBody));
+            return CommSmsChannelResult.fail("SEND_FAILED", abbreviate(responseBody));
+        } catch (Exception e) {
+            log.error("MyCommSmsProvider 响应解析异常 phone={}, response={}", phone, abbreviate(responseBody), e);
+            return CommSmsChannelResult.fail("PARSE_ERROR", abbreviate(responseBody));
+        }
+    }
+
+    private String normalizeBaseUrl(String url) {
+        if (StringUtils.isBlank(url)) {
+            return "";
+        }
+        return url.endsWith("/") ? url : url + "/";
+    }
+
+    private String maskUrl(String url) {
+        if (StringUtils.isBlank(url)) {
+            return url;
+        }
+        return url.replaceAll("password=[^&]*", "password=***");
+    }
+
+    private String abbreviate(String text) {
+        if (text == null) {
+            return null;
+        }
+        return text.length() > 500 ? text.substring(0, 500) : text;
+    }
+}

+ 32 - 6
fs-comm-gateway/对接文档.md

@@ -302,6 +302,28 @@ Content-Type: application/json
 
 
 发送结果异步写入租户库 `company_voice_robotic_call_log_sendmsg`(status:1 进行中 / 2 成功 / 3 失败)。
 发送结果异步写入租户库 `company_voice_robotic_call_log_sendmsg`(status:1 进行中 / 2 成功 / 3 失败)。
 
 
+**迈远(provider=my)发送说明:**
+
+网关进程内由 `MyCommSmsProvider` 负责实际 HTTP 下发,不再读取旧的 `his.sms` 全局配置,而是按租户在 Admin 配置的接口表路由:
+
+| 配置表 | 字段 | 说明 |
+|--------|------|------|
+| `company_sms_api` | `provider=my` | 固定为迈远 |
+| | `url` | 接口根地址 |
+| | `account` / `password` | 账户密码 |
+| | `sign` | 短信签名 |
+| `company_sms_api_port` | `port_no` | 迈远扩展码 `extno` |
+| | `account` / `password` / `sign` | 可选,覆盖接口级配置 |
+
+请求 URL 格式(与旧版 `sendCaptcha` 一致):
+
+```
+{url}sms?action=send&account={account}&password={password}&mobile={phone}&content={URLEncode(sign+content)}&extno={extno}&rt=json
+```
+
+- 模板类型 `tempType=1`(行业通知):内容为 `sign + content`
+- 模板类型 `tempType=2`(营销):内容为 `sign + content + 拒收请回复R`
+
 ---
 ---
 
 
 ### 5.3 查询外呼记录
 ### 5.3 查询外呼记录
@@ -540,13 +562,17 @@ location /comm/ {
 
 
 ## 11. 附录:相关数据表
 ## 11. 附录:相关数据表
 
 
-| 表名 | 说明 |
-|------|------|
-| company_voice_robotic_call_log_callphone | AI 外呼执行日志 |
-| company_voice_robotic_call_log_sendmsg | 短信发送日志 |
-| company_voice_robotic_call_log_addwx | 加微执行日志(其他模块写入) |
+| 表名 | 库 | 说明 |
+|------|-----|------|
+| comm_gateway_api_log | 主库 | 网关 API 调用日志(频率计数、Admin 查询) |
+| company_comm_gateway_log | 租户库 | 网关 API 调用记录(成功/失败/限频均写入) |
+| company_voice_robotic_call_log_callphone | 租户库 | AI 外呼执行日志 |
+| company_voice_robotic_call_log_sendmsg | 租户库 | 短信发送日志 |
+| company_voice_robotic_call_log_addwx | 租户库 | 加微执行日志(其他模块写入) |
+
+外呼/短信业务日志与 `company_comm_gateway_log` 均写入**当前租户库**;`comm_gateway_api_log` 写入主库用于全平台统计与频率限制计数。
 
 
-日志写入均路由至**当前租户库**,非主库。
+存量租户需执行:`fs-service/src/main/resources/db/20250604-company-comm-gateway-log.sql`
 
 
 ---
 ---
 
 

+ 47 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyCommGatewayLogController.java

@@ -0,0 +1,47 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.domain.CompanyCommGatewayLog;
+import com.fs.company.service.ICompanyCommGatewayLogService;
+import com.fs.framework.security.SecurityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+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.RestController;
+
+import java.util.List;
+
+/**
+ * 通讯网关 API 调用记录(租户库)
+ */
+@RestController
+@RequestMapping("/company/commGatewayLog")
+public class CompanyCommGatewayLogController extends BaseController {
+
+    @Autowired
+    private ICompanyCommGatewayLogService companyCommGatewayLogService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyCommGatewayLog query) {
+        query.setCompanyId(currentCompanyId());
+        startPage();
+        List<CompanyCommGatewayLog> list = companyCommGatewayLogService.selectList(query);
+        return getDataTable(list);
+    }
+
+    @GetMapping("/{logId}")
+    public AjaxResult getInfo(@PathVariable Long logId) {
+        CompanyCommGatewayLog log = companyCommGatewayLogService.selectById(logId);
+        if (log == null || !currentCompanyId().equals(log.getCompanyId())) {
+            return AjaxResult.error("记录不存在");
+        }
+        return AjaxResult.success(log);
+    }
+
+    private Long currentCompanyId() {
+        return SecurityUtils.getLoginUser().getUser().getCompanyId();
+    }
+}

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceApiController.java

@@ -87,7 +87,7 @@ public class CompanyVoiceApiController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany() != null ? loginUser.getCompany().getCompanyId() : null;
         Long companyId = loginUser.getCompany() != null ? loginUser.getCompany().getCompanyId() : null;
         if (companyId == null) { return AjaxResult.error("请选择租户"); }
         if (companyId == null) { return AjaxResult.error("请选择租户"); }
-        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId);
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService.selectEnabledApisByTenantId(companyId);
         return AjaxResult.success(list);
         return AjaxResult.success(list);
     }
     }
 
 

+ 0 - 3
fs-company/src/main/java/com/fs/company/controller/fastGpt/FastgptChatArtificialWordsCompanyController.java

@@ -60,9 +60,6 @@ public class FastgptChatArtificialWordsCompanyController extends BaseController
     @Autowired
     @Autowired
     private TokenService tokenService;
     private TokenService tokenService;
 
 
-    @Autowired
-    private IdentityHidingService identityHidingService;
-
     /**
     /**
      * 查询转人工关键词列表(租户隔离)
      * 查询转人工关键词列表(租户隔离)
      * 自动注入当前租户的company_id,只返回该租户的关键词
      * 自动注入当前租户的company_id,只返回该租户的关键词

+ 7 - 4
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -56,6 +56,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
+import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
 import javax.validation.Valid;
 import java.io.IOException;
 import java.io.IOException;
 import java.net.SocketTimeoutException;
 import java.net.SocketTimeoutException;
@@ -203,8 +204,8 @@ public class QwExternalContactController extends BaseController
     @Log(title = "同步企业微信客户", businessType = BusinessType.INSERT)
     @Log(title = "同步企业微信客户", businessType = BusinessType.INSERT)
     @PostMapping
     @PostMapping
     public R add(@RequestBody QwExternalContact qwExternalContact) throws IOException {
     public R add(@RequestBody QwExternalContact qwExternalContact) throws IOException {
-
-        return qwExternalContactService.syncQwExternalContact(qwExternalContact.getCorpId());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return qwExternalContactService.syncQwExternalContact(qwExternalContact.getCorpId(),loginUser.getTenantId());
     }
     }
 
 
     /**
     /**
@@ -331,8 +332,10 @@ public class QwExternalContactController extends BaseController
 
 
     @Log(title = "同步我的企业微信客户", businessType = BusinessType.INSERT)
     @Log(title = "同步我的企业微信客户", businessType = BusinessType.INSERT)
     @GetMapping("/syncMyExternalContact/{id}")
     @GetMapping("/syncMyExternalContact/{id}")
-    public R  syncMyExternalContact(@PathVariable("id") Long id ) throws IOException {
-        return qwExternalContactService.syncMyQwExternalContact(id);
+    public R  syncMyExternalContact(@PathVariable("id") Long id, HttpServletRequest request ) throws IOException {
+        LoginUser loginUser = tokenService.getLoginUser(request);
+        Long tenantId = loginUser.getTenantId();
+        return qwExternalContactService.syncMyQwExternalContact(id,tenantId);
     }
     }
 
 
 
 

+ 176 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAdminController.java

@@ -0,0 +1,176 @@
+package com.fs.company.controller;
+
+import com.fs.company.service.workflow.evolution.EvolutionEngine;
+import com.fs.company.service.workflow.evolution.UserNodeOptimizer;
+import com.fs.company.service.workflow.evolution.impl.EvolutionSchedulerImpl;
+import com.fs.company.service.workflow.feedback.FeedbackDrivenEvolution;
+import com.fs.company.service.workflow.learning.TenantLearningEngine;
+import com.fs.company.service.workflow.monitor.DashboardService;
+import com.fs.company.service.workflow.pay.PayService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 龙虾引擎 Admin REST API
+ * <p>
+ * 提供前端调用入口:进化审批 / 学习结果 / A/B测试 / 监控看板 / 支付查询
+ */
+@RestController
+@RequestMapping("/api/lobster/admin")
+public class LobsterAdminController {
+
+    private static final Logger log = LoggerFactory.getLogger(LobsterAdminController.class);
+
+    @Autowired(required = false)
+    private EvolutionEngine evolutionEngine;
+
+    @Autowired(required = false)
+    private UserNodeOptimizer userNodeOptimizer;
+
+    @Autowired(required = false)
+    private FeedbackDrivenEvolution feedbackDrivenEvolution;
+
+    @Autowired(required = false)
+    private TenantLearningEngine tenantLearningEngine;
+
+    @Autowired(required = false)
+    private EvolutionSchedulerImpl evolutionScheduler;
+
+    @Autowired(required = false)
+    private DashboardService dashboardService;
+
+    @Autowired(required = false)
+    private PayService payService;
+
+    // ════════════ 进化管理 ════════════
+
+    /** 租户进化指标统计 */
+    @GetMapping("/evolution/metrics/{companyId}")
+    public Map<String, Object> evolutionMetrics(@PathVariable Long companyId) {
+        return evolutionEngine != null ? evolutionEngine.getEvolutionMetrics(companyId) : Collections.emptyMap();
+    }
+
+    /** 应用优化建议 */
+    @PostMapping("/evolution/apply/{companyId}/{suggestionId}")
+    public Map<String, Object> applySuggestion(@PathVariable Long companyId, @PathVariable Long suggestionId) {
+        evolutionEngine.applySuggestion(companyId, suggestionId);
+        return Map.of("result", "applied");
+    }
+
+    // ════════════ 用户级优化 ════════════
+
+    /** 待审核优化列表 */
+    @GetMapping("/optimization/pending/{companyId}")
+    public Map<String, Object> pendingOptimizations(@PathVariable Long companyId,
+                                                     @RequestParam(defaultValue = "1") int page,
+                                                     @RequestParam(defaultValue = "20") int pageSize,
+                                                     @RequestParam(required = false) Long workflowId) {
+        return userNodeOptimizer != null
+            ? Map.of("list", userNodeOptimizer.getPendingAuditList(companyId, workflowId, page, pageSize))
+            : Collections.emptyMap();
+    }
+
+    /** 批量审核优化 */
+    @PostMapping("/optimization/audit/{companyId}")
+    public Map<String, Object> batchAudit(@PathVariable Long companyId,
+                                           @RequestParam String auditorId,
+                                           @RequestBody List<Map<String, Object>> items) {
+        List<UserNodeOptimizer.AuditItem> auditItems = new ArrayList<>();
+        for (Map<String, Object> item : items) {
+            UserNodeOptimizer.AuditItem ai = new UserNodeOptimizer.AuditItem();
+            ai.setOptimizationId(((Number) item.get("id")).longValue());
+            ai.setApproved(Boolean.TRUE.equals(item.get("approved")));
+            auditItems.add(ai);
+        }
+        return Map.of("result", userNodeOptimizer != null
+            ? userNodeOptimizer.batchAudit(companyId, auditItems, auditorId, "").getSummary()
+            : "optimizer unavailable");
+    }
+
+    // ════════════ 用户级优化 ════════════
+
+    /** 优化开关配置 */
+    @PostMapping("/optimization/config/{companyId}")
+    public Map<String, Object> setOptimizationConfig(@PathVariable Long companyId,
+                                                      @RequestParam Long workflowId,
+                                                      @RequestParam String nodeCode,
+                                                      @RequestParam boolean enabled,
+                                                      @RequestParam boolean autoApply,
+                                                      @RequestParam(defaultValue = "admin") String configBy) {
+        userNodeOptimizer.setOptimizationConfig(companyId, workflowId, nodeCode, enabled, autoApply, configBy);
+        return Map.of("result", "ok");
+    }
+
+    // ════════════ A/B 测试 ════════════
+
+    /** 活跃 A/B 测试列表 */
+    @GetMapping("/ab-test/active/{companyId}")
+    public Map<String, Object> activeAbTests(@PathVariable Long companyId) {
+        return feedbackDrivenEvolution != null
+            ? Map.of("tests", feedbackDrivenEvolution.getActiveABTests(companyId))
+            : Collections.emptyMap();
+    }
+
+    /** 应用 A/B 测试结果 */
+    @PostMapping("/ab-test/apply/{companyId}/{testId}")
+    public Map<String, Object> applyAbTest(@PathVariable Long companyId, @PathVariable Long testId) {
+        return feedbackDrivenEvolution != null
+            ? Map.of("result", feedbackDrivenEvolution.applyABTestResult(companyId, testId).getMessage())
+            : Map.of("result", "unavailable");
+    }
+
+    // ════════════ 学习引擎 ════════════
+
+    /** 触发学习周期 */
+    @PostMapping("/learning/trigger/{companyId}")
+    public Map<String, Object> triggerLearning(@PathVariable Long companyId) {
+        return tenantLearningEngine != null
+            ? Map.of("summary", tenantLearningEngine.triggerLearningCycle(companyId).getSummary())
+            : Map.of("result", "unavailable");
+    }
+
+    /** 学习结果列表 */
+    @GetMapping("/learning/results/{companyId}")
+    public Map<String, Object> learningResults(@PathVariable Long companyId) {
+        return tenantLearningEngine != null
+            ? Map.of("results", tenantLearningEngine.getLearningResults(companyId))
+            : Collections.emptyMap();
+    }
+
+    /** 应用学习结果 */
+    @PostMapping("/learning/apply/{companyId}/{resultId}")
+    public Map<String, Object> applyLearning(@PathVariable Long companyId, @PathVariable Long resultId) {
+        return tenantLearningEngine != null
+            ? Map.of("result", tenantLearningEngine.applyLearningResult(companyId, resultId, null).getMessage())
+            : Map.of("result", "unavailable");
+    }
+
+    // ════════════ 监控看板 ════════════
+
+    /** 租户全景看板 */
+    @GetMapping("/dashboard/{companyId}")
+    public Map<String, Object> dashboard(@PathVariable Long companyId) {
+        return dashboardService != null ? dashboardService.getTenantDashboard(companyId) : Collections.emptyMap();
+    }
+
+    // ════════════ 支付 ════════════
+
+    /** 支付状态查询 */
+    @GetMapping("/pay/status")
+    public Map<String, Object> payStatus(@RequestParam String orderNo, @RequestParam(defaultValue = "wechat") String gateway) {
+        return payService != null ? payService.checkPayStatus(orderNo, gateway) : Collections.emptyMap();
+    }
+
+    // ════════════ 测试结果 ════════════
+
+    /** 手动触发进化调度 */
+    @PostMapping("/scheduler/trigger")
+    public Map<String, Object> triggerScheduler() {
+        evolutionScheduler.recordTask(0L, "manual_trigger", "ok");
+        return Map.of("result", "triggered");
+    }
+}

+ 131 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java

@@ -0,0 +1,131 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+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.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 龙虾引擎 AI 生成工作流 Controller
+ * 包装 MultiModelWorkflowGenerator,对接前端 /workflow/lobster/ai-generator/*
+ */
+@RestController
+@RequestMapping("/workflow/lobster/ai-generator")
+public class LobsterAiGeneratorController extends BaseController {
+
+    @Autowired
+    private MultiModelWorkflowGenerator generator;
+
+    /** 生成结果临时缓存:recordId → GenerationResult */
+    private final Map<Long, GenerationResult> resultCache = new ConcurrentHashMap<>();
+
+    /**
+     * 触发 AI 生成工作流
+     * 入参:requirement / industryType / companyId
+     */
+    @PostMapping("/generate")
+    public AjaxResult generate(@RequestBody Map<String, Object> body) {
+        Long companyId = body.get("companyId") != null ? Long.valueOf(body.get("companyId").toString()) : 0L;
+        String requirement = (String) body.getOrDefault("requirement", "");
+        String industryType = (String) body.getOrDefault("industryType", "general");
+
+        if (requirement.isEmpty()) {
+            return AjaxResult.error("requirement 不能为空");
+        }
+
+        // 模型配置走 admin_ai_scene 体系(workflow_generation 场景),传 null 即由 generator 内部按场景拉模型
+        ModelConfig modelConfig = null;
+        GenerationResult result = generator.generateWorkflowWithResult(companyId, requirement, industryType, modelConfig);
+
+        Long recordId = System.currentTimeMillis();
+        resultCache.put(recordId, result);
+
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("recordId", recordId);
+        resp.put("success", result.isSuccess());
+        resp.put("errorMsg", result.getErrorMsg());
+        resp.put("qualityScore", result.getQualityScore());
+        return AjaxResult.success(resp);
+    }
+
+    /**
+     * 查询生成结果详情
+     */
+    @GetMapping("/result/{recordId}/detail")
+    public AjaxResult getResult(@PathVariable Long recordId) {
+        GenerationResult result = resultCache.get(recordId);
+        if (result == null) {
+            return AjaxResult.error("生成结果不存在或已过期");
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("recordId", recordId);
+        data.put("success", result.isSuccess());
+        data.put("workflowJson", result.getWorkflowJson());
+        data.put("modelAOutput", result.getModelAOutput());
+        data.put("modelBOutput", result.getModelBOutput());
+        data.put("modelCOutput", result.getModelCOutput());
+        data.put("qualityScore", result.getQualityScore());
+        data.put("suggestions", result.getSuggestions());
+        data.put("errorMsg", result.getErrorMsg());
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 确认生成结果(保存为工作流模板)
+     * 入参:workflowName / companyId
+     */
+    @PostMapping("/confirm/{recordId}")
+    public AjaxResult confirm(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        GenerationResult result = resultCache.get(recordId);
+        if (result == null || !result.isSuccess()) {
+            return AjaxResult.error("生成结果不存在或失败");
+        }
+        // TODO:调 CompanyWorkflowLobsterService.saveAsTemplate(json, workflowName, companyId)
+        // 当前先返回 JSON 给前端,由前端走原有保存流程
+        Map<String, Object> data = new HashMap<>();
+        data.put("workflowJson", result.getWorkflowJson());
+        data.put("workflowName", body.get("workflowName"));
+        resultCache.remove(recordId);
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 确认(带前端编辑后内容)
+     */
+    @PostMapping("/confirm/{recordId}/edited")
+    public AjaxResult confirmEdited(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        String editedJson = (String) body.get("workflowJson");
+        Map<String, Object> data = new HashMap<>();
+        data.put("workflowJson", editedJson);
+        data.put("workflowName", body.get("workflowName"));
+        resultCache.remove(recordId);
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 基于现有工作流再次生成(迭代优化)
+     */
+    @PostMapping("/regenerate/{recordId}")
+    public AjaxResult regenerate(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        Long companyId = body.get("companyId") != null ? Long.valueOf(body.get("companyId").toString()) : 0L;
+        String existingJson = (String) body.getOrDefault("existingWorkflowJson", "");
+        String modifyInstruction = (String) body.getOrDefault("modifyInstruction", "");
+
+        GenerationResult result = generator.iterateOptimize(companyId, existingJson, modifyInstruction, null);
+        Long newRecordId = System.currentTimeMillis();
+        resultCache.put(newRecordId, result);
+
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("recordId", newRecordId);
+        resp.put("success", result.isSuccess());
+        resp.put("errorMsg", result.getErrorMsg());
+        return AjaxResult.success(resp);
+    }
+}

+ 10 - 23
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java

@@ -3,7 +3,8 @@ package com.fs.company.controller.workflow;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ServletUtils;
-import com.fs.company.service.billing.BillingService;
+import com.fs.company.domain.LobsterTenantBalance;
+import com.fs.company.service.workflow.ILobsterBillingService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,8 +23,8 @@ import java.util.*;
 @RequestMapping("/workflow/lobster/billing")
 @RequestMapping("/workflow/lobster/billing")
 public class LobsterBillingController extends BaseController {
 public class LobsterBillingController extends BaseController {
 
 
-    @Autowired(required = false)
-    private BillingService billingService;
+    @Autowired
+    private ILobsterBillingService billingService;
 
 
     @Autowired
     @Autowired
     private TokenService tokenService;
     private TokenService tokenService;
@@ -35,8 +36,9 @@ public class LobsterBillingController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long tenantId = loginUser.getCompany().getCompanyId();
         Long tenantId = loginUser.getCompany().getCompanyId();
 
 
-        BigDecimal coef = billingService != null ?
-                billingService.getTokenCoefficient(tenantId) : new BigDecimal("1.00");
+        LobsterTenantBalance balance = billingService.getBalance(tenantId);
+        BigDecimal coef = (balance != null && balance.getTokenCoefficient() != null) ?
+                balance.getTokenCoefficient() : new BigDecimal("1.00");
 
 
         Map<String, Object> result = new LinkedHashMap<>();
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("tenantId", tenantId);
         result.put("tenantId", tenantId);
@@ -66,12 +68,8 @@ public class LobsterBillingController extends BaseController {
             return AjaxResult.error("系数范围: 0.01 ~ 100");
             return AjaxResult.error("系数范围: 0.01 ~ 100");
         }
         }
 
 
-        if (billingService != null) {
-            boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
-            return ok ? AjaxResult.success("Token系数已更新为 " + coefficient) : AjaxResult.error("更新失败");
-        }
-
-        return AjaxResult.error("计费服务未初始化");
+        boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
+        return ok ? AjaxResult.success("Token系数已更新为 " + coefficient) : AjaxResult.error("更新失败");
     }
     }
 
 
     /** 消费记录列表 */
     /** 消费记录列表 */
@@ -82,18 +80,7 @@ public class LobsterBillingController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long tenantId = loginUser.getCompany().getCompanyId();
         Long tenantId = loginUser.getCompany().getCompanyId();
 
 
-        Map<String, Object> result = new LinkedHashMap<>();
-        if (billingService != null) {
-            List<Map<String, Object>> list = billingService.getConsumeRecords(tenantId, page, size);
-            long total = billingService.countConsumeRecords(tenantId);
-            result.put("list", list);
-            result.put("total", total);
-        } else {
-            result.put("list", Collections.emptyList());
-            result.put("total", 0);
-        }
-        result.put("page", page);
-        result.put("size", size);
+        Map<String, Object> result = billingService.listConsumeRecords(page, size, tenantId);
         return AjaxResult.success(result);
         return AjaxResult.success(result);
     }
     }
 
 

+ 8 - 22
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java

@@ -28,7 +28,7 @@ import java.util.*;
 public class LobsterEventAuditController extends BaseController {
 public class LobsterEventAuditController extends BaseController {
 
 
     @Autowired
     @Autowired
-    private ILobsterEventAuditService eventAuditService;
+    private ILobsterEventAuditService auditService;
 
 
     @Autowired
     @Autowired
     private TokenService tokenService;
     private TokenService tokenService;
@@ -47,20 +47,7 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
         Long companyId = loginUser.getCompany().getCompanyId();
 
 
-        List<LobsterEventAudit> list = eventAuditService.selectListByCompanyIdAndStatus(companyId, status, page, size);
-        long total = eventAuditService.countByCompanyIdAndStatus(companyId, status);
-
-        Map<String, Object> stats = new LinkedHashMap<>();
-        stats.put("pending", eventAuditService.countByCompanyIdAndStatus(companyId, "pending"));
-        stats.put("approved", eventAuditService.countByCompanyIdAndStatus(companyId, "approved"));
-        stats.put("rejected", eventAuditService.countByCompanyIdAndStatus(companyId, "rejected"));
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("list", list);
-        result.put("total", total);
-        result.put("page", page);
-        result.put("size", size);
-        result.put("stats", stats);
+        Map<String, Object> result = auditService.listAudits(status, page, size, companyId);
         return AjaxResult.success(result);
         return AjaxResult.success(result);
     }
     }
 
 
@@ -72,8 +59,8 @@ public class LobsterEventAuditController extends BaseController {
     public AjaxResult approve(@PathVariable Long id) {
     public AjaxResult approve(@PathVariable Long id) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
 
-        LobsterEventAudit record = eventAuditService.selectByIdAndStatus(id, "pending");
-        if (record == null) {
+        LobsterEventAudit record = auditService.getById(id);
+        if (record == null || !"pending".equals(record.getStatus())) {
             return AjaxResult.error("审核记录不存在或已处理");
             return AjaxResult.error("审核记录不存在或已处理");
         }
         }
 
 
@@ -91,8 +78,7 @@ public class LobsterEventAuditController extends BaseController {
             }
             }
         }
         }
 
 
-        String comment = patched ? "审核通过,节点已注入" : "审核通过";
-        eventAuditService.approve(id, loginUser.getUsername(), comment);
+        auditService.approve(id, loginUser.getUsername());
 
 
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
         result.put("patched", patched);
         result.put("patched", patched);
@@ -109,9 +95,9 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
 
         String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
         String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
-        eventAuditService.reject(id, loginUser.getUsername(), comment);
+        boolean ok = auditService.reject(id, loginUser.getUsername(), comment);
 
 
-        return AjaxResult.success("已驳回");
+        return ok ? AjaxResult.success("已驳回") : AjaxResult.error("审核记录不存在");
     }
     }
 
 
     /**
     /**
@@ -120,7 +106,7 @@ public class LobsterEventAuditController extends BaseController {
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     @GetMapping("/{id}")
     public AjaxResult detail(@PathVariable Long id) {
     public AjaxResult detail(@PathVariable Long id) {
-        LobsterEventAudit record = eventAuditService.selectById(id);
+        LobsterEventAudit record = auditService.getById(id);
         return AjaxResult.success(record);
         return AjaxResult.success(record);
     }
     }
 }
 }

+ 108 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterModelRouteController.java

@@ -0,0 +1,108 @@
+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.LobsterModelConfig;
+import com.fs.company.service.workflow.LobsterModelConfigService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 龙虾多模型路由配置Controller(租户端,非桥接)
+ * 管理 lobster_model_config 表的CRUD
+ */
+@RestController
+@RequestMapping("/workflow/lobster/model-route")
+public class LobsterModelRouteController extends BaseController {
+
+    @Autowired
+    private LobsterModelConfigService modelConfigService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    private Long getCurrentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getCompany().getCompanyId();
+    }
+
+    /**
+     * 获取当前租户的所有模型路由配置
+     */
+    @GetMapping("/list")
+    public AjaxResult list() {
+        Long companyId = getCurrentCompanyId();
+        List<LobsterModelConfig> configs = modelConfigService.getConfigsByCompany(companyId);
+        return AjaxResult.success(configs);
+    }
+
+    /**
+     * 根据配置类型获取配置
+     */
+    @GetMapping("/{configType}")
+    public AjaxResult getByType(@PathVariable String configType) {
+        Long companyId = getCurrentCompanyId();
+        LobsterModelConfig config = modelConfigService.getConfig(companyId, configType);
+        return AjaxResult.success(config);
+    }
+
+    /**
+     * 新增模型路由配置
+     */
+    @PostMapping
+    public AjaxResult add(@RequestBody LobsterModelConfig config) {
+        Long companyId = getCurrentCompanyId();
+        config.setCompanyId(companyId);
+        config.setEnabled(1);
+        config.setCreatedAt(LocalDateTime.now());
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.saveConfig(config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    /**
+     * 更新模型路由配置
+     */
+    @PutMapping("/{id}")
+    public AjaxResult update(@PathVariable Long id, @RequestBody LobsterModelConfig config) {
+        config.setId(id);
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.updateConfig(config);
+        return AjaxResult.success("配置已更新");
+    }
+
+    /**
+     * 删除模型路由配置
+     */
+    @DeleteMapping("/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        modelConfigService.deleteConfig(id);
+        return AjaxResult.success("配置已删除");
+    }
+
+    /**
+     * 获取支持的配置类型列表
+     */
+    @GetMapping("/types")
+    public AjaxResult types() {
+        List<Map<String, String>> types = new ArrayList<>();
+        types.add(buildType("workflow_generator", "工作流生成"));
+        types.add(buildType("quality_check", "质检评分"));
+        types.add(buildType("intent_router", "意图路由"));
+        types.add(buildType("reply_generator", "回复生成"));
+        return AjaxResult.success(types);
+    }
+
+    private Map<String, String> buildType(String code, String name) {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put("code", code);
+        m.put("name", name);
+        return m;
+    }
+}

+ 30 - 8
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java

@@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
-import java.util.Map;
+import java.util.*;
 
 
 /**
 /**
  * 龙虾系统提示词管理Controller
  * 龙虾系统提示词管理Controller
@@ -41,23 +41,44 @@ public class LobsterPromptController extends BaseController {
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     @GetMapping("/{id}")
     public AjaxResult getById(@PathVariable Long id) {
     public AjaxResult getById(@PathVariable Long id) {
-        LobsterSystemPrompt prompt = promptService.getById(id);
-        return AjaxResult.success(prompt);
+        LobsterSystemPrompt row = promptService.getById(id);
+        return AjaxResult.success(row);
     }
     }
 
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PostMapping
     @PostMapping
-    public AjaxResult create(@RequestBody LobsterSystemPrompt body) {
+    public AjaxResult create(@RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        promptService.create(body, loginUser.getUsername(), loginUser.getCompany().getCompanyId());
+        String content = (String) body.get("promptContent");
+        if (content == null || content.isEmpty()) return AjaxResult.error("提示词内容不能为空");
+
+        LobsterSystemPrompt prompt = new LobsterSystemPrompt();
+        prompt.setPromptKey((String) body.getOrDefault("promptKey", "custom_" + System.currentTimeMillis()));
+        prompt.setPromptName((String) body.getOrDefault("promptName", "自定义提示词"));
+        prompt.setPromptCategory((String) body.getOrDefault("promptCategory", "custom"));
+        prompt.setPromptContent(content);
+        prompt.setModelName((String) body.getOrDefault("modelName", "doubao-lite"));
+        prompt.setSystemRole((String) body.get("systemRole"));
+        prompt.setIndustryType((String) body.get("industryType"));
+
+        promptService.create(prompt, loginUser.getUsername(), loginUser.getCompany().getCompanyId());
         return AjaxResult.success("创建成功");
         return AjaxResult.success("创建成功");
     }
     }
 
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PutMapping("/{id}")
     @PutMapping("/{id}")
-    public AjaxResult update(@PathVariable Long id, @RequestBody LobsterSystemPrompt body) {
+    public AjaxResult update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        promptService.update(id, body, loginUser.getCompany().getCompanyId());
+
+        LobsterSystemPrompt prompt = new LobsterSystemPrompt();
+        prompt.setPromptName((String) body.getOrDefault("promptName", ""));
+        prompt.setPromptCategory((String) body.getOrDefault("promptCategory", ""));
+        prompt.setPromptContent((String) body.get("promptContent"));
+        prompt.setModelName((String) body.getOrDefault("modelName", "doubao-lite"));
+        prompt.setSystemRole((String) body.get("systemRole"));
+        prompt.setIndustryType((String) body.get("industryType"));
+
+        promptService.update(id, prompt, loginUser.getCompany().getCompanyId());
         return AjaxResult.success("更新成功");
         return AjaxResult.success("更新成功");
     }
     }
 
 
@@ -70,7 +91,8 @@ public class LobsterPromptController extends BaseController {
 
 
     @GetMapping("/categories")
     @GetMapping("/categories")
     public AjaxResult categories() {
     public AjaxResult categories() {
-        return AjaxResult.success(promptService.getCategories());
+        List<String> cats = promptService.getCategories();
+        return AjaxResult.success(cats);
     }
     }
 
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")

+ 9 - 12
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java

@@ -3,10 +3,8 @@ package com.fs.company.controller.workflow;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.ServletUtils;
-import com.fs.company.domain.LobsterLearningCorpus;
-import com.fs.company.service.workflow.ILobsterLearningCorpusService;
+import com.fs.company.service.workflow.ILobsterSalesCorpusService;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.AnalysisReport;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.AnalysisReport;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.CorpusEntry;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.CorpusEntry;
@@ -21,7 +19,7 @@ import java.util.*;
 /**
 /**
  * 龙虾销冠语料管理Controller
  * 龙虾销冠语料管理Controller
  *
  *
- * 表: lobster_learning_corpus
+ * 表: lobster_sales_corpus
  * 页面: 销冠语料 → 录入/批量导入/AI分析/话术库查询
  * 页面: 销冠语料 → 录入/批量导入/AI分析/话术库查询
  *
  *
  * 核心价值: 租户上传销冠/金牌客服聊天话术 → AI分析提取沟通模式 → 进化引擎学习 → 全租户共享
  * 核心价值: 租户上传销冠/金牌客服聊天话术 → AI分析提取沟通模式 → 进化引擎学习 → 全租户共享
@@ -34,7 +32,7 @@ public class LobsterSalesCorpusController extends BaseController {
     private SalesCorpusAnalyzer corpusAnalyzer;
     private SalesCorpusAnalyzer corpusAnalyzer;
 
 
     @Autowired
     @Autowired
-    private ILobsterLearningCorpusService corpusService;
+    private ILobsterSalesCorpusService salesCorpusService;
 
 
     @Autowired
     @Autowired
     private TokenService tokenService;
     private TokenService tokenService;
@@ -150,16 +148,15 @@ public class LobsterSalesCorpusController extends BaseController {
      */
      */
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/list")
     @GetMapping("/list")
-    public TableDataInfo list(@RequestParam(defaultValue = "1") int page,
-                              @RequestParam(defaultValue = "10") int size,
-                              @RequestParam(required = false) String scenario,
-                              @RequestParam(required = false) String status) {
+    public AjaxResult list(@RequestParam(defaultValue = "1") int page,
+                           @RequestParam(defaultValue = "10") int size,
+                           @RequestParam(required = false) String scenario,
+                           @RequestParam(required = false) String status) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
         Long companyId = loginUser.getCompany().getCompanyId();
 
 
-        startPage();
-        List<LobsterLearningCorpus> list = corpusService.selectListByCompanyId(companyId, scenario, status);
-        return getDataTable(list);
+        Map<String, Object> result = salesCorpusService.listCorpus(page, size, companyId, scenario, status);
+        return AjaxResult.success(result);
     }
     }
 
 
     /**
     /**

+ 21 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java

@@ -247,4 +247,25 @@ public class LobsterWorkflowExecController extends BaseController {
         result.put("controlUpdatedAt", instance.getControlUpdatedAt());
         result.put("controlUpdatedAt", instance.getControlUpdatedAt());
         return AjaxResult.success(result);
         return AjaxResult.success(result);
     }
     }
+
+    /**
+     * 模拟聊天测试:使用指定模板进行对话模拟
+     */
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/simulate")
+    public AjaxResult simulateChat(@RequestBody Map<String, Object> params) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        String content = (String) params.getOrDefault("content", "");
+        Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
+
+        // 模拟对话响应(后续可对接真实的 LobsterWorkflowExecutor 模拟执行能力)
+        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("companyId", companyId);
+        reply.put("timestamp", System.currentTimeMillis());
+        reply.put("mode", "simulate");
+        return AjaxResult.success(reply);
+    }
 }
 }

+ 71 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/PayCallbackController.java

@@ -0,0 +1,71 @@
+package com.fs.company.controller;
+
+import com.fs.company.service.workflow.pay.PayService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 支付回调 Webhook 接收端点
+ * <p>
+ * 接收微信/支付宝/银联的异步支付通知,自动推进工作流
+ */
+@RestController
+@RequestMapping("/api/lobster/pay")
+public class PayCallbackController {
+
+    private static final Logger log = LoggerFactory.getLogger(PayCallbackController.class);
+
+    @Autowired(required = false)
+    private PayService payService;
+
+    /**
+     * 微信支付异步通知
+     */
+    @PostMapping("/notify/wechat")
+    public Map<String, Object> wechatNotify(@RequestBody String body) {
+        log.info("[PayCallback] 微信支付通知: {}", body);
+        try {
+            String orderNo = extractField(body, "out_trade_no");
+            String transactionId = extractField(body, "transaction_id");
+            if (orderNo != null && !orderNo.isEmpty()) {
+                if (payService != null) payService.checkPayStatus(orderNo, "wechat");
+            }
+            return Map.of("code", "SUCCESS", "message", "OK");
+        } catch (Exception e) {
+            return Map.of("code", "FAIL", "message", e.getMessage());
+        }
+    }
+
+    /**
+     * 支付宝异步通知
+     */
+    @PostMapping("/notify/alipay")
+    public Map<String, Object> alipayNotify(@RequestParam Map<String, String> params) {
+        log.info("[PayCallback] 支付宝支付通知: {}", params);
+        String orderNo = params.get("out_trade_no");
+        if (orderNo != null && payService != null) {
+            payService.checkPayStatus(orderNo, "alipay");
+        }
+        return Map.of("code", "SUCCESS", "message", "OK");
+    }
+
+    private String extractField(String xmlOrJson, String key) {
+        int idx = xmlOrJson.indexOf("\"" + key + "\"");
+        if (idx < 0) {
+            idx = xmlOrJson.indexOf("<" + key + ">");
+            if (idx >= 0) {
+                int start = idx + key.length() + 2;
+                int end = xmlOrJson.indexOf("</" + key + ">", start);
+                return end > start ? xmlOrJson.substring(start, end) : "";
+            }
+            return "";
+        }
+        int start = xmlOrJson.indexOf("\"", idx + key.length() + 3);
+        int end = xmlOrJson.indexOf("\"", start + 1);
+        return start > 0 && end > start ? xmlOrJson.substring(start + 1, end) : "";
+    }
+}

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java

@@ -5,7 +5,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 
 
 /**
 /**
- * Controller ʹ��ȫ���������� fs-saasadmin ͬ����ͻ��Service ������Ĭ�϶�����
+ * Controller 使用全限定类名避免与 fs-saasadmin 同名冲突,Service 则使用默认短名称
  */
  */
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 
 

+ 47 - 47
fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java

@@ -19,58 +19,58 @@ public class Finance {
     public native static long NewSdk();
     public native static long NewSdk();
 
 
     /**
     /**
-     * ��ʼ������
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 初始化函数
+     * Return值=0表示该API调用成功
      *
      *
-     * @param [in] sdk			NewSdk���ص�sdkָ��
-     * @param [in] corpid      ������ҵ����ҵid�����磺wwd08c8exxxx5ab44d����������ҵ΢�Ź����--�ҵ���ҵ--��ҵ��Ϣ�鿴
-     * @param [in] secret		�������ݴ浵��Secret����������ҵ΢�Ź����--������--�������ݴ浵�鿴
-     * @return �����Ƿ��ʼ���ɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in] sdk			NewSdk返回的sdk指针
+     * @param [in] corpid      调用企业的企业id,例如:wwd08c8exxxx5ab44d,登录企业微信管理后台--我的企业--企业信息查看
+     * @param [in] secret		会话内容存档的Secret,登录企业微信管理后台--管理工具--会话内容存档查看
+     * @return 返回是否初始化成功
+     * 0   - 成功
+     * !=0 - 失败
      */
      */
     public native static int Init(long sdk, String corpid, String secret);
     public native static int Init(long sdk, String corpid, String secret);
 
 
     /**
     /**
-     * ��ȡ�����¼����
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 拉取聊天记录函数
+     * Return值=0表示该API调用成功
      *
      *
-     * @param [in]  sdk				NewSdk���ص�sdkָ��
-     * @param [in]  seq				��ָ����seq��ʼ��ȡ��Ϣ��ע����Ƿ��ص���Ϣ��seq+1��ʼ���أ�seqΪ֮ǰ�ӿڷ��ص����seqֵ���״�ʹ����ʹ��seq:0
-     * @param [in]  limit			һ����ȡ����Ϣ���������ֵ1000��������1000���᷵�ش���
-     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
-     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
-     * @param [out] chatDatas		���ر�����ȡ��Ϣ�����ݣ�slice�ṹ��.���ݰ���errcode/errmsg���Լ�ÿ����Ϣ���ݡ�
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in]  sdk				NewSdk返回的sdk指针
+     * @param [in]  seq				从指定的seq开始拉取消息,注意返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+     * @param [in]  limit			一次拉取的消息条数,最大值1000,超过1000会返回错误
+     * @param [in]  proxy			使用代理的请求,需要接入代理的URL。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+     * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+     * @param [out] chatDatas		返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
      */
      */
     public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
     public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
 
 
     /**
     /**
-     * ��ȡý����Ϣ����
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 获取媒体消息数据
+     * Return值=0表示该API调用成功
      *
      *
-     * @param [in]  sdk				NewSdk���ص�sdkָ��
-     * @param [in]  sdkFileid		��GetChatData���ص�������Ϣ�У�ý����Ϣ������sdkfileid
-     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
-     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
-     * @param [in]  indexbuf		ý����Ϣ��Ƭ��ȡ����Ҫ����ÿ����ȡ��������Ϣ���״β���Ҫ��д��Ĭ����ȡ512k������ÿ�ε���ֻ��Ҫ���ϴε��÷��ص�outindexbuf���뼴�ɡ�
-     * @param [out] media_data		���ر�����ȡ��ý������.MediaData�ṹ��.���ݰ���data(��������)/outindexbuf(�´�����)/is_finish(��ȡ��ɱ��)
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in]  sdk				NewSdk返回的sdk指针
+     * @param [in]  sdkFileid		从GetChatData返回的聊天消息中,媒体消息包含的sdkfileid
+     * @param [in]  proxy			使用代理的请求,需要接入代理的URL。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+     * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+     * @param [in]  indexbuf		媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf带入即可。
+     * @param [out] media_data		返回本次拉取的媒体数据.MediaData结构体.内容包括data(消息数据)/outindexbuf(下次索引)/is_finish(拉取完成标记)
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
      */
      */
     public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
     public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
 
 
     /**
     /**
-     * @param [in]  encrypt_key, getchatdata���ص�encrypt_key
-     * @param [in]  encrypt_msg, getchatdata���ص�content
-     * @param [out] msg, ���ܵ���Ϣ����
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
-     * @brief ��������
+     * @param [in]  encrypt_key, getchatdata返回的encrypt_key
+     * @param [in]  encrypt_msg, getchatdata返回的content
+     * @param [out] msg, 解密的消息明文
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
+     * @brief 数据解密
      */
      */
     public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
     public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
 
 
@@ -79,20 +79,20 @@ public class Finance {
     public native static long NewSlice();
     public native static long NewSlice();
 
 
     /**
     /**
-     * @return
-     * @brief �ͷ�slice����NewSlice�ɶ�ʹ��
+     * @return 
+     * @brief 释放slice,和NewSlice成对使用
      */
      */
     public native static void FreeSlice(long slice);
     public native static void FreeSlice(long slice);
 
 
     /**
     /**
-     * @return ����
-     * @brief ��ȡslice����
+     * @return 内容
+     * @brief 获取slice内容
      */
      */
     public native static String GetContentFromSlice(long slice);
     public native static String GetContentFromSlice(long slice);
 
 
     /**
     /**
-     * @return ����
-     * @brief ��ȡslice���ݳ���
+     * @return 长度
+     * @brief 获取slice数据长度
      */
      */
     public native static int GetSliceLen(long slice);
     public native static int GetSliceLen(long slice);
 
 
@@ -102,13 +102,13 @@ public class Finance {
 
 
     /**
     /**
      * @return outindex
      * @return outindex
-     * @brief ��ȡmediadata outindex
+     * @brief 获取mediadata outindex
      */
      */
     public native static String GetOutIndexBuf(long mediaData);
     public native static String GetOutIndexBuf(long mediaData);
 
 
     /**
     /**
      * @return data
      * @return data
-     * @brief ��ȡmediadata data����
+     * @brief 获取mediadata data数据
      */
      */
     public native static byte[] GetData(long mediaData);
     public native static byte[] GetData(long mediaData);
 
 
@@ -117,8 +117,8 @@ public class Finance {
     public native static int GetDataLen(long mediaData);
     public native static int GetDataLen(long mediaData);
 
 
     /**
     /**
-     * @return 1��ɡ�0δ���
-     * @brief �ж�mediadata�Ƿ����
+     * @return 1完成、0未完成
+     * @brief 判断mediadata是否结束
      */
      */
     public native static int IsMediaDataFinish(long mediaData);
     public native static int IsMediaDataFinish(long mediaData);
 
 

+ 52 - 0
fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java

@@ -9,6 +9,9 @@ import com.fs.framework.datasource.TenantDataSourceUtil;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwExternalContactAddTagParam;
 import com.fs.qw.param.QwExternalContactAddTagParam;
 import com.fs.qw.param.QwExternalContactUpdateNoteParam;
 import com.fs.qw.param.QwExternalContactUpdateNoteParam;
+import com.fs.qwApi.domain.QwExternalContactAllListResult;
+import com.fs.qwApi.domain.QwExternalContactListResult;
+import com.fs.qwApi.param.QwAllExternalcontactListParam;
 import com.fs.qwApi.param.QwUploadImageByCourseParam;
 import com.fs.qwApi.param.QwUploadImageByCourseParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.qwApi.service.QwApiService;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -214,5 +217,54 @@ public class OpenQwApiController extends BaseController {
             return R.error("上传图片失败: " + e.getMessage());
             return R.error("上传图片失败: " + e.getMessage());
         }
         }
     }
     }
+
+    /**
+     * 按企微用户查询外部联系人
+     */
+    @PostMapping("/getExternalcontactList")
+    public QwExternalContactListResult getExternalcontactList(@RequestParam("qwUserId") String qwUserId,
+                                                              @RequestParam("corpId") String corpId,
+                                                              @RequestParam("tenantId") Long tenantId) {
+        log.info("按企微用户查询外部联系人,tenantId={}, qwUserId={}, corpId={}", tenantId, qwUserId, corpId);
+
+        try {
+            return tenantDataSourceUtil.executeWithResult(tenantId, () ->
+                    qwApiService.getExternalcontactList(qwUserId, corpId)
+            );
+        } catch (Exception e) {
+            log.error("[QwFriendWelcome] 按企微用户查询外部联系人失败,tenantId={}, qwUserId={}, corpId={}",
+                    tenantId, qwUserId, corpId, e);
+
+            QwExternalContactListResult result = new QwExternalContactListResult();
+            result.setErrcode(1);
+            result.setErrmsg("按企微用户查询外部联系人失败: " + e.getMessage());
+            return result;
+        }
+    }
+
+    /**
+     * 批量获取客户详情
+     */
+    @PostMapping("/getAllExternalcontactList")
+    public QwExternalContactAllListResult getAllExternalcontactList(@RequestBody QwAllExternalcontactListParam param) {
+        log.info("批量获取客户详情,参数{}", param);
+        Long tenantId = param.getTenantId();
+        if (tenantId == null){
+            return null;
+        }
+        try {
+            return tenantDataSourceUtil.executeWithResult(tenantId, () ->
+                    qwApiService.getAllExternalcontactList(param, param.getCorpId(), param.getQwCompany())
+            );
+        } catch (Exception e) {
+            log.error("批量获取客户详情失败,tenantId={}, QwCompany={}, corpId={}",
+                    tenantId, param.getQwCompany(), param.getCorpId(), e);
+            QwExternalContactAllListResult result = new QwExternalContactAllListResult();
+            result.setErrcode(1);
+            result.setErrmsg("按企微用户查询外部联系人失败: " + e.getMessage());
+            return result;
+        }
+    }
+
 }
 }
 
 

+ 29 - 7
fs-qw-api/src/main/java/com/fs/app/controller/QwExternalContactController.java

@@ -1,19 +1,24 @@
 package com.fs.app.controller;
 package com.fs.app.controller;
 
 
+import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.bean.BeanUtils;
 import com.fs.framework.datasource.TenantDataSourceUtil;
 import com.fs.framework.datasource.TenantDataSourceUtil;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qwApi.config.OpenQwConfig;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
 import com.fs.qwApi.domain.QwUnassignedListResult;
 import com.fs.qwApi.domain.QwUnassignedListResult;
 import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.domain.inner.ExternalContactInfo;
 import com.fs.qwApi.domain.inner.ExternalContactInfo;
 import com.fs.qwApi.domain.inner.FollowInfo;
 import com.fs.qwApi.domain.inner.FollowInfo;
+import com.fs.qwApi.param.QwAllExternalcontactListParam;
 import com.fs.qwApi.param.QwExternalListParam;
 import com.fs.qwApi.param.QwExternalListParam;
 import com.fs.qwApi.param.QwUnassignedListParam;
 import com.fs.qwApi.param.QwUnassignedListParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.qwApi.service.QwApiService;
@@ -51,7 +56,7 @@ public class QwExternalContactController extends BaseController {
     /**
     /**
      * 同步企微外部联系人(递归分页)
      * 同步企微外部联系人(递归分页)
      *
      *
-     * @param qwUser      企微用户信息(JSON)
+     * @param qwUserJson      企微用户信息(JSON)
      * @param cursor      分页游标
      * @param cursor      分页游标
      * @param tenantId    租户ID
      * @param tenantId    租户ID
      */
      */
@@ -63,14 +68,14 @@ public class QwExternalContactController extends BaseController {
         return tenantDataSourceUtil.executeWithResult(tenantId, () -> {
         return tenantDataSourceUtil.executeWithResult(tenantId, () -> {
             // 从JSON还原QwUser对象
             // 从JSON还原QwUser对象
             QwUser qwUser = JSON.parseObject(qwUserJson, QwUser.class);
             QwUser qwUser = JSON.parseObject(qwUserJson, QwUser.class);
-            return doSyncExternalContact(qwUser, cursor);
+            return doSyncExternalContact(qwUser, cursor,tenantId);
         });
         });
     }
     }
 
 
     /**
     /**
      * 递归同步外部联系人
      * 递归同步外部联系人
      */
      */
-    private R doSyncExternalContact(QwUser qwUser, String getNextCursor) {
+    private R doSyncExternalContact(QwUser qwUser, String getNextCursor,Long tenantId) {
         String qwUserId = qwUser.getQwUserId();
         String qwUserId = qwUser.getQwUserId();
         String corpId = qwUser.getCorpId();
         String corpId = qwUser.getCorpId();
         Long companyId = qwUser.getCompanyId();
         Long companyId = qwUser.getCompanyId();
@@ -80,9 +85,26 @@ public class QwExternalContactController extends BaseController {
         param.setUserid_list(Arrays.asList(qwUserId));
         param.setUserid_list(Arrays.asList(qwUserId));
         param.setCursor(getNextCursor);
         param.setCursor(getNextCursor);
         QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
         QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
-        QwExternalContactAllListResult list = qwApiService.getAllExternalcontactList(param, corpId, qwCompany);
-
-        log.info("批量获取客户详情 {}", list);
+        QwExternalContactAllListResult list = new QwExternalContactAllListResult();
+//        QwExternalContactAllListResult list = qwApiService.getAllExternalcontactList(param, corpId, qwCompany);
+        QwAllExternalcontactListParam qwAllExternalcontactListParam = new QwAllExternalcontactListParam();
+        BeanUtils.copyProperties(param, qwAllExternalcontactListParam);
+        qwAllExternalcontactListParam.setCorpId(corpId);
+        qwAllExternalcontactListParam.setQwCompany(qwCompany);
+        qwAllExternalcontactListParam.setTenantId(tenantId);
+        String url = OpenQwConfig.baseApi + "/getAllExternalcontactList";
+        try {
+            String result = HttpUtil.createPost(url)
+                    .body(JSON.toJSONString(qwAllExternalcontactListParam))
+                    .execute()
+                    .body();
+            log.info("批量获取客户详情: result={}", result);
+            if (StringUtils.isNotBlank(result)){
+                list = JSON.parseObject(result, QwExternalContactAllListResult.class);
+            }
+        } catch (Exception e) {
+            log.error("批量获取客户详情失败: param:{}", qwAllExternalcontactListParam);
+        }
 
 
         if (list.getErrcode() == 0) {
         if (list.getErrcode() == 0) {
             List<ExternalContactInfo> externalContactList = list.getExternal_contact_list();
             List<ExternalContactInfo> externalContactList = list.getExternal_contact_list();
@@ -137,7 +159,7 @@ public class QwExternalContactController extends BaseController {
         }
         }
 
 
         if (!StringUtil.strIsNullOrEmpty(list.getNext_cursor())) {
         if (!StringUtil.strIsNullOrEmpty(list.getNext_cursor())) {
-            return doSyncExternalContact(qwUser, list.getNext_cursor());
+            return doSyncExternalContact(qwUser, list.getNext_cursor(),tenantId);
         }
         }
         return R.ok();
         return R.ok();
     }
     }

+ 16 - 3
fs-qw-api/src/main/java/com/fs/app/qwTask/qwTask.java

@@ -1,6 +1,7 @@
 package com.fs.app.qwTask;
 package com.fs.app.qwTask;
 
 
 import com.fs.app.service.OpenQwApiService;
 import com.fs.app.service.OpenQwApiService;
+import com.fs.common.config.RedisTenantContext;
 import com.fs.course.service.IFinishCourseStatisticsSyncService;
 import com.fs.course.service.IFinishCourseStatisticsSyncService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.qw.domain.QwIpadServerLog;
 import com.fs.qw.domain.QwIpadServerLog;
@@ -13,6 +14,7 @@ import com.fs.sop.service.impl.QwSopServiceImpl;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.statis.IFsStatisQwWatchService;
 import com.fs.statis.IFsStatisQwWatchService;
 import com.fs.statis.service.FsStatisSalerWatchService;
 import com.fs.statis.service.FsStatisSalerWatchService;
+import com.fs.tenant.domain.TenantInfo;
 import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
 import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
 import com.fs.wxwork.service.WxWorkService;
 import com.fs.wxwork.service.WxWorkService;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -29,6 +31,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.Optional;
 import java.util.Optional;
+import java.util.function.Consumer;
 
 
 @Component
 @Component
 @Slf4j
 @Slf4j
@@ -105,18 +108,28 @@ public class qwTask {
      * 否则直接执行(兼容单库模式或被 TenantTaskRunner 调用时)。
      * 否则直接执行(兼容单库模式或被 TenantTaskRunner 调用时)。
      */
      */
     private void runWithSaaSTenant(String taskName, Runnable action) {
     private void runWithSaaSTenant(String taskName, Runnable action) {
+        runWithSaaSTenant(taskName, tenant -> action.run());
+    }
+
+    /**
+     * SaaS 多租户定时任务:按租户切库后执行,并将当前租户信息传给业务逻辑。
+     */
+    private void runWithSaaSTenant(String taskName, Consumer<TenantInfo> tenantAction) {
         if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
         if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
-            tenantTaskRunner.runForEachTenant(taskName, action);
+            tenantTaskRunner.runForEachTenant(taskName, tenantAction);
             return;
             return;
         }
         }
-        action.run();
+        TenantInfo tenant = new TenantInfo();
+        tenant.setId(RedisTenantContext.getTenantId());
+        tenantAction.accept(tenant);
     }
     }
 
 
     //正在使用
     //正在使用
     @Scheduled(cron = "0 0 1 * * ?")
     @Scheduled(cron = "0 0 1 * * ?")
     public void qwExternalContact()
     public void qwExternalContact()
     {
     {
-        runWithSaaSTenant("qwExternalContact", () -> qwExternalContactService.qwExternalContactSync());
+        runWithSaaSTenant("qwExternalContact", tenant ->
+                qwExternalContactService.qwExternalContactSync(tenant.getId()));
     }
     }
     //正在使用
     //正在使用
 //    @Scheduled(cron = "1/1 * * * * ?")
 //    @Scheduled(cron = "1/1 * * * * ?")

+ 57 - 0
fs-service/src/main/java/com/fs/comm/domain/CommGatewayApiLog.java

@@ -0,0 +1,57 @@
+package com.fs.comm.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 通讯网关 API 调用日志(主库 comm_gateway_api_log)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CommGatewayApiLog extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String API_TYPE_CALL = "call";
+    public static final String API_TYPE_SMS = "sms";
+
+    private Long logId;
+    private Long tenantId;
+    private Long companyId;
+    private Long companyUserId;
+    private String callerAccount;
+    private String apiType;
+    private String apiPath;
+    private String requestBody;
+    private String responseBody;
+    private Integer resultCode;
+    private String resultMsg;
+    private Integer success;
+    private Integer limitHit;
+    private String limitReason;
+    private String calleePhone;
+    private String callerPhone;
+    private Long gatewayId;
+    private BigDecimal billingAmount;
+    /** 成本价(单价) */
+    private BigDecimal costPrice;
+    /** 计算价/售价(单价) */
+    private BigDecimal calcPrice;
+    /** 计费数量 */
+    private Integer billingQuantity;
+    private String billingUnit;
+    private String clientIp;
+    private String authScope;
+    private Integer durationMs;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 查询:公司名称(关联展示) */
+    private String companyName;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/comm/mapper/CommGatewayApiLogMapper.java

@@ -0,0 +1,33 @@
+package com.fs.comm.mapper;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 通讯网关 API 调用日志(主库)
+ */
+@DataSource(DataSourceType.MASTER)
+public interface CommGatewayApiLogMapper {
+
+    int insertCommGatewayApiLog(CommGatewayApiLog log);
+
+    CommGatewayApiLog selectCommGatewayApiLogById(Long logId);
+
+    List<CommGatewayApiLog> selectCommGatewayApiLogList(CommGatewayApiLog query);
+
+    Integer countByCalleePhone(@Param("companyId") Long companyId,
+                               @Param("calleePhone") String calleePhone,
+                               @Param("type") Integer type);
+
+    Integer countByCallerPhone(@Param("companyId") Long companyId,
+                               @Param("callerPhone") String callerPhone,
+                               @Param("type") Integer type);
+
+    Integer countByCompanyCall(@Param("companyId") Long companyId,
+                               @Param("apiType") String apiType,
+                               @Param("type") Integer type);
+}

+ 26 - 0
fs-service/src/main/java/com/fs/comm/model/CommGatewayBillingQuote.java

@@ -0,0 +1,26 @@
+package com.fs.comm.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 通讯网关单次调用计费报价
+ */
+@Data
+@Builder
+public class CommGatewayBillingQuote {
+
+    /** 成本价(单价,元) */
+    private BigDecimal costPrice;
+
+    /** 计算价/售价(单价,元) */
+    private BigDecimal calcPrice;
+
+    /** 计费数量 */
+    private Integer billingQuantity;
+
+    /** 计费总额 = calcPrice × billingQuantity */
+    private BigDecimal billingAmount;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/comm/model/CommSmsSendContext.java

@@ -0,0 +1,18 @@
+package com.fs.comm.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 短信发送上下文(公司、调用人、发送人展示名)
+ */
+@Data
+@Builder
+public class CommSmsSendContext {
+
+    private Long companyId;
+
+    private Long companyUserId;
+
+    private String senderName;
+}

+ 51 - 0
fs-service/src/main/java/com/fs/comm/service/CommGatewayApiLogServiceImpl.java

@@ -0,0 +1,51 @@
+package com.fs.comm.service;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.comm.mapper.CommGatewayApiLogMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CommGatewayApiLogServiceImpl implements ICommGatewayApiLogService {
+
+    @Autowired
+    private CommGatewayApiLogMapper commGatewayApiLogMapper;
+
+    @Override
+    @DataSource(DataSourceType.MASTER)
+    public void saveLog(CommGatewayApiLog log) {
+        if (log.getCreateTime() == null) {
+            log.setCreateTime(DateUtils.getNowDate());
+        }
+        commGatewayApiLogMapper.insertCommGatewayApiLog(log);
+    }
+
+    @Override
+    @DataSource(DataSourceType.MASTER)
+    public CommGatewayApiLog selectById(Long logId) {
+        return commGatewayApiLogMapper.selectCommGatewayApiLogById(logId);
+    }
+
+    @Override
+    @DataSource(DataSourceType.MASTER)
+    public List<CommGatewayApiLog> selectList(CommGatewayApiLog query) {
+        return commGatewayApiLogMapper.selectCommGatewayApiLogList(query);
+    }
+
+    @Override
+    @DataSource(DataSourceType.MASTER)
+    public int countByCalleePhone(Long companyId, String calleePhone, Integer type) {
+        return commGatewayApiLogMapper.countByCalleePhone(companyId, calleePhone, type);
+    }
+
+    @Override
+    @DataSource(DataSourceType.MASTER)
+    public int countByCallerPhone(Long companyId, String callerPhone, Integer type) {
+        return commGatewayApiLogMapper.countByCallerPhone(companyId, callerPhone, type);
+    }
+}

+ 156 - 0
fs-service/src/main/java/com/fs/comm/service/CommGatewayBillingService.java

@@ -0,0 +1,156 @@
+package com.fs.comm.service;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+import com.fs.comm.model.CommGatewayBillingQuote;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
+import com.fs.proxy.domain.CompanySmsApiTenant;
+import com.fs.proxy.domain.ServiceFeeConfig;
+import com.fs.proxy.domain.TenantTrafficPricing;
+import com.fs.proxy.enums.ConsumeTypeEnum;
+import com.fs.proxy.mapper.CompanySmsApiTenantMapper;
+import com.fs.proxy.mapper.TenantTrafficPricingMapper;
+import com.fs.proxy.service.BalanceService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 通讯网关计费报价(对齐 BalanceServiceImpl 定价规则)
+ */
+@Slf4j
+@Service
+public class CommGatewayBillingService {
+
+    @Value("${comm.gateway.call-unit-price:0.12}")
+    private BigDecimal defaultCallCalcPrice;
+
+    @Value("${comm.gateway.sms-unit-price:0.05}")
+    private BigDecimal defaultSmsCalcPrice;
+
+    @Autowired
+    private BalanceService balanceService;
+
+    @Autowired
+    private CompanySmsApiTenantMapper smsApiTenantMapper;
+
+    @Autowired
+    private CompanyVoiceApiTenantMapper voiceApiTenantMapper;
+
+    @Autowired
+    private TenantTrafficPricingMapper trafficPricingMapper;
+
+    @DataSource(DataSourceType.MASTER)
+    public CommGatewayBillingQuote resolveQuote(Long tenantId, String apiType, int quantity) {
+        if (quantity <= 0) {
+            quantity = 1;
+        }
+        if (CommGatewayApiLog.API_TYPE_SMS.equals(apiType)) {
+            return buildQuote(resolveSmsUnitPrice(tenantId), quantity);
+        }
+        if (CommGatewayApiLog.API_TYPE_CALL.equals(apiType)) {
+            return buildQuote(resolveCallUnitPrice(tenantId), quantity);
+        }
+        return zeroQuote();
+    }
+
+    private CommGatewayBillingQuote buildQuote(UnitPricePair pair, int quantity) {
+        BigDecimal calcPrice = pair.calcPrice != null ? pair.calcPrice : BigDecimal.ZERO;
+        BigDecimal costPrice = pair.costPrice != null ? pair.costPrice : BigDecimal.ZERO;
+        return CommGatewayBillingQuote.builder()
+                .costPrice(costPrice)
+                .calcPrice(calcPrice)
+                .billingQuantity(quantity)
+                .billingAmount(calcPrice.multiply(BigDecimal.valueOf(quantity)))
+                .build();
+    }
+
+    private CommGatewayBillingQuote zeroQuote() {
+        return CommGatewayBillingQuote.builder()
+                .costPrice(BigDecimal.ZERO)
+                .calcPrice(BigDecimal.ZERO)
+                .billingQuantity(0)
+                .billingAmount(BigDecimal.ZERO)
+                .build();
+    }
+
+    private UnitPricePair resolveSmsUnitPrice(Long tenantId) {
+        ServiceFeeConfig config = balanceService.getFeeConfig(ConsumeTypeEnum.SMS_SEND.getCode());
+        BigDecimal calcPrice = config != null && config.getFeeStandard() != null
+                ? config.getFeeStandard() : defaultSmsCalcPrice;
+        BigDecimal costPrice = config != null && config.getPlatformCost() != null
+                ? config.getPlatformCost() : BigDecimal.ZERO;
+
+        if (tenantId != null) {
+            List<CompanySmsApiTenant> bindings = smsApiTenantMapper.selectByCompanyId(tenantId);
+            if (bindings != null) {
+                for (CompanySmsApiTenant binding : bindings) {
+                    if (!Integer.valueOf(1).equals(binding.getStatus())) {
+                        continue;
+                    }
+                    if (binding.getPrice() != null && binding.getPrice().compareTo(BigDecimal.ZERO) > 0) {
+                        calcPrice = binding.getPrice();
+                    }
+                    if (binding.getCostPrice() != null) {
+                        costPrice = binding.getCostPrice();
+                    }
+                    break;
+                }
+            }
+        }
+        return new UnitPricePair(costPrice, calcPrice);
+    }
+
+    private UnitPricePair resolveCallUnitPrice(Long tenantId) {
+        ServiceFeeConfig config = balanceService.getFeeConfig(ConsumeTypeEnum.AI_CALL.getCode());
+        BigDecimal calcPrice = config != null && config.getFeeStandard() != null
+                ? config.getFeeStandard() : defaultCallCalcPrice;
+        BigDecimal costPrice = config != null && config.getPlatformCost() != null
+                ? config.getPlatformCost() : BigDecimal.ZERO;
+
+        if (tenantId != null) {
+            List<CompanyVoiceApiTenant> voiceBindings = voiceApiTenantMapper.selectEnabledApisByTenantId(tenantId);
+            if (voiceBindings != null && !voiceBindings.isEmpty()) {
+                CompanyVoiceApiTenant voiceBinding = voiceBindings.get(0);
+                if (voiceBinding.getSalePrice() != null && voiceBinding.getSalePrice().compareTo(BigDecimal.ZERO) > 0) {
+                    BigDecimal voicePrice = voiceBinding.getSalePrice();
+                    BigDecimal voiceCost = voiceBinding.getCostPrice() != null ? voiceBinding.getCostPrice() : BigDecimal.ZERO;
+
+                    BigDecimal aiSurcharge = config != null && config.getFeeStandard() != null
+                            ? config.getFeeStandard() : defaultCallCalcPrice;
+                    BigDecimal aiCost = config != null && config.getPlatformCost() != null
+                            ? config.getPlatformCost() : BigDecimal.ZERO;
+
+                    TenantTrafficPricing aiTrafficPricing = trafficPricingMapper
+                            .selectByTenantAndType(tenantId, ConsumeTypeEnum.AI_CALL.getCode());
+                    if (aiTrafficPricing != null && aiTrafficPricing.getPrice() != null
+                            && aiTrafficPricing.getPrice().compareTo(BigDecimal.ZERO) > 0) {
+                        aiSurcharge = aiTrafficPricing.getPrice();
+                        if (aiTrafficPricing.getCostPrice() != null) {
+                            aiCost = aiTrafficPricing.getCostPrice();
+                        }
+                    }
+                    calcPrice = voicePrice.add(aiSurcharge);
+                    costPrice = voiceCost.add(aiCost);
+                }
+            }
+        }
+        return new UnitPricePair(costPrice, calcPrice);
+    }
+
+    private static class UnitPricePair {
+        private final BigDecimal costPrice;
+        private final BigDecimal calcPrice;
+
+        private UnitPricePair(BigDecimal costPrice, BigDecimal calcPrice) {
+            this.costPrice = costPrice;
+            this.calcPrice = calcPrice;
+        }
+    }
+}

+ 114 - 22
fs-service/src/main/java/com/fs/comm/service/CommSmsSendService.java

@@ -7,6 +7,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.service.ISmsService;
 import com.fs.common.service.ISmsService;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.comm.model.CommSmsSendContext;
 import com.fs.comm.model.CommSmsSendParam;
 import com.fs.comm.model.CommSmsSendParam;
 import com.fs.comm.model.CommSmsSendResult;
 import com.fs.comm.model.CommSmsSendResult;
 import com.fs.company.domain.*;
 import com.fs.company.domain.*;
@@ -88,28 +89,10 @@ public class CommSmsSendService {
             throw new ServiceException("被叫人不存在");
             throw new ServiceException("被叫人不存在");
         }
         }
 
 
-        Long companyId = param.getCompanyId();
-        Long companyUserId = param.getCompanyUserId();
-        String senderName = param.getSenderName();
-        if (companyUserId == null || StringUtils.isBlank(senderName)) {
-            CompanyWxClient wxClient = companyWxClientMapper.selectOneByRoboticIdAndUserId(param.getRoboticId(), callees.getUserId());
-            if (wxClient != null) {
-                if (wxClient.getIsWeCom() == 2) {
-                    QwUser qwUser = qwExternalContactService.getQwUserByRedisForId(String.valueOf(wxClient.getAccountId()));
-                    companyId = qwUser.getCompanyId();
-                    companyUserId = qwUser.getCompanyUserId();
-                    senderName = qwUser.getQwUserName();
-                } else {
-                    CompanyWxAccount wxAccount = companyWxAccountService.selectCompanyWxAccountById(wxClient.getAccountId());
-                    companyId = wxAccount.getCompanyId();
-                    companyUserId = wxAccount.getCompanyUserId();
-                    senderName = wxAccount.getWxNickName();
-                }
-            }
-        }
-        if (companyId == null) {
-            companyId = robotic.getCompanyId();
-        }
+        CommSmsSendContext sendContext = resolveSendContext(param, robotic, callees);
+        Long companyId = sendContext.getCompanyId();
+        Long companyUserId = sendContext.getCompanyUserId();
+        String senderName = sendContext.getSenderName();
 
 
         CompanySmsTemp temp = smsTempService.selectCompanySmsTempById(param.getSmsTempId());
         CompanySmsTemp temp = smsTempService.selectCompanySmsTempById(param.getSmsTempId());
         if (temp == null || !Integer.valueOf(1).equals(temp.getStatus()) || !Integer.valueOf(1).equals(temp.getIsAudit())) {
         if (temp == null || !Integer.valueOf(1).equals(temp.getStatus()) || !Integer.valueOf(1).equals(temp.getIsAudit())) {
@@ -188,6 +171,115 @@ public class CommSmsSendService {
                 .build();
                 .build();
     }
     }
 
 
+    /**
+     * 解析短信发送上下文:请求参数 → 微信绑定 → 外呼任务
+     */
+    public CommSmsSendContext resolveSendContext(CommSmsSendParam param) {
+        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(param.getRoboticId());
+        if (robotic == null) {
+            throw new ServiceException("外呼任务不存在");
+        }
+        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectById(param.getCalleeId());
+        if (callees == null) {
+            throw new ServiceException("被叫人不存在");
+        }
+        return resolveSendContext(param, robotic, callees);
+    }
+
+    public CommSmsSendContext resolveSendContext(CommSmsSendParam param, CompanyVoiceRobotic robotic,
+                                                 CompanyVoiceRoboticCallees callees) {
+        CommSmsSendContext context = CommSmsSendContext.builder()
+                .companyId(param.getCompanyId())
+                .companyUserId(param.getCompanyUserId())
+                .senderName(param.getSenderName())
+                .build();
+        fillFromWxClient(param.getRoboticId(), callees.getUserId(), context);
+        fillFromRobotic(robotic, context);
+        fillSenderNameIfBlank(context);
+        if (context.getCompanyId() == null) {
+            throw new ServiceException("未获取到公司信息");
+        }
+        return context;
+    }
+
+    public void validateSmsTempAndBalance(Long smsTempId, Long companyId) {
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempById(smsTempId);
+        if (temp == null || !Integer.valueOf(1).equals(temp.getStatus()) || !Integer.valueOf(1).equals(temp.getIsAudit())) {
+            throw new ServiceException("模板未审核");
+        }
+        CompanySms sms = companySmsService.selectCompanySmsByCompanyId(companyId);
+        if (sms == null) {
+            throw new ServiceException("请充值");
+        }
+        if (sms.getRemainSmsCount() == null || sms.getRemainSmsCount() <= 0) {
+            throw new ServiceException("剩余短信数量不足,请充值");
+        }
+    }
+
+    private void fillFromWxClient(Long roboticId, Long customerUserId, CommSmsSendContext context) {
+        if (context.getCompanyUserId() != null && StringUtils.isNotBlank(context.getSenderName())) {
+            return;
+        }
+        CompanyWxClient wxClient = companyWxClientMapper.selectOneByRoboticIdAndUserId(roboticId, customerUserId);
+        if (wxClient == null) {
+            return;
+        }
+        try {
+            if (wxClient.getIsWeCom() == 2) {
+                QwUser qwUser = qwExternalContactService.getQwUserByRedisForId(String.valueOf(wxClient.getAccountId()));
+                if (qwUser != null) {
+                    if (context.getCompanyId() == null) {
+                        context.setCompanyId(qwUser.getCompanyId());
+                    }
+                    if (context.getCompanyUserId() == null) {
+                        context.setCompanyUserId(qwUser.getCompanyUserId());
+                    }
+                    if (StringUtils.isBlank(context.getSenderName())) {
+                        context.setSenderName(qwUser.getQwUserName());
+                    }
+                }
+            } else {
+                CompanyWxAccount wxAccount = companyWxAccountService.selectCompanyWxAccountById(wxClient.getAccountId());
+                if (wxAccount != null) {
+                    if (context.getCompanyId() == null) {
+                        context.setCompanyId(wxAccount.getCompanyId());
+                    }
+                    if (context.getCompanyUserId() == null) {
+                        context.setCompanyUserId(wxAccount.getCompanyUserId());
+                    }
+                    if (StringUtils.isBlank(context.getSenderName())) {
+                        context.setSenderName(wxAccount.getWxNickName());
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.warn("解析微信发送人信息失败 roboticId={}", roboticId, ex);
+        }
+    }
+
+    private void fillFromRobotic(CompanyVoiceRobotic robotic, CommSmsSendContext context) {
+        if (context.getCompanyId() == null) {
+            context.setCompanyId(robotic.getCompanyId());
+        }
+        if (context.getCompanyUserId() == null) {
+            context.setCompanyUserId(robotic.getCompanyUserId());
+        }
+        if (StringUtils.isBlank(context.getSenderName()) && StringUtils.isNotBlank(robotic.getCreateByName())) {
+            context.setSenderName(robotic.getCreateByName());
+        }
+    }
+
+    private void fillSenderNameIfBlank(CommSmsSendContext context) {
+        if (StringUtils.isNotBlank(context.getSenderName()) || context.getCompanyUserId() == null) {
+            return;
+        }
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(context.getCompanyUserId());
+        if (companyUser == null) {
+            return;
+        }
+        context.setSenderName(StringUtils.defaultIfBlank(companyUser.getNickName(), companyUser.getUserName()));
+    }
+
     /**
     /**
      * 已组装好参数的 AI 短信批量发送(WxTask 等场景),统一收口 batchSmsOp4AiSend
      * 已组装好参数的 AI 短信批量发送(WxTask 等场景),统一收口 batchSmsOp4AiSend
      */
      */

+ 23 - 0
fs-service/src/main/java/com/fs/comm/service/CommVoiceConfigMasterService.java

@@ -0,0 +1,23 @@
+package com.fs.comm.service;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceConfig;
+import com.fs.company.mapper.CompanyVoiceConfigMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 从主库读取呼叫频率配置(与 admin 平台配置一致)
+ */
+@Service
+public class CommVoiceConfigMasterService {
+
+    @Autowired
+    private CompanyVoiceConfigMapper companyVoiceConfigMapper;
+
+    @DataSource(DataSourceType.MASTER)
+    public CompanyVoiceConfig selectByCompanyId(Long companyId) {
+        return companyVoiceConfigMapper.selectCompanyVoiceConfigByCompanyId(companyId);
+    }
+}

+ 188 - 0
fs-service/src/main/java/com/fs/comm/service/CommVoiceLimitService.java

@@ -0,0 +1,188 @@
+package com.fs.comm.service;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
+import com.fs.comm.support.CommTenantDataSourceHelper;
+import com.fs.company.domain.CompanyVoiceConfig;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.system.config.SystemVoiceConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 通讯网关外呼/短信频率限制(对齐 company_voice_config 配置,计数走主库 comm_gateway_api_log)
+ */
+@Slf4j
+@Service
+public class CommVoiceLimitService {
+
+    @Autowired
+    private CommVoiceConfigMasterService commVoiceConfigMasterService;
+
+    @Autowired
+    private ICommGatewayApiLogService commGatewayApiLogService;
+
+    @Autowired
+    private CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
+
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+
+    @Autowired
+    private CommTenantDataSourceHelper commTenantDataSourceHelper;
+
+    public void checkCallLimit(Long companyId, Long tenantId, Long calleeId, String phone, Long gatewayId) {
+        if (companyId == null) {
+            return;
+        }
+        String calleePhone = resolveCallCalleePhone(calleeId, phone);
+        String callerKey = buildCompanyCallerKey(companyId, gatewayId);
+        CompanyVoiceConfig config = commVoiceConfigMasterService.selectByCompanyId(companyId);
+        if (config == null) {
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+            return;
+        }
+        applyLimits(companyId, config, callerKey, calleePhone);
+        commTenantDataSourceHelper.ensureTenant(tenantId);
+    }
+
+    public void checkSmsLimit(Long companyId, Long tenantId, Long calleeId, Long customerId, String phone) {
+        if (companyId == null) {
+            return;
+        }
+        String calleePhone = resolveSmsPhone(calleeId, customerId, phone);
+        String callerKey = buildCompanyCallerKey(companyId, null);
+        CompanyVoiceConfig config = commVoiceConfigMasterService.selectByCompanyId(companyId);
+        if (config == null) {
+            commTenantDataSourceHelper.ensureTenant(tenantId);
+            return;
+        }
+        applyLimits(companyId, config, callerKey, calleePhone);
+        commTenantDataSourceHelper.ensureTenant(tenantId);
+    }
+
+    private void applyLimits(Long companyId, CompanyVoiceConfig config, String callerKey, String calleePhone) {
+        SystemVoiceConfig callerConfig = parseConfig(config.getCallerJson());
+        if (callerConfig != null) {
+            checkCallerLimits(companyId, callerKey, callerConfig);
+        }
+        if (StringUtils.isNotBlank(calleePhone)) {
+            SystemVoiceConfig calleeConfig = parseConfig(config.getCalleeJson());
+            if (calleeConfig != null) {
+                checkCalleeLimits(companyId, calleePhone, calleeConfig);
+            }
+        }
+    }
+
+    private void checkCallerLimits(Long companyId, String callerKey, SystemVoiceConfig callerConfig) {
+        checkLimit(companyId, callerKey, true, 1, callerConfig.getCallerMinute(), "主叫分钟限制");
+        checkLimit(companyId, callerKey, true, 2, callerConfig.getCallerHour(), "主叫小时限制");
+        checkLimit(companyId, callerKey, true, 3, callerConfig.getCallerDay(), "主叫日限制");
+        checkLimit(companyId, callerKey, true, 4, callerConfig.getCallerWeek(), "主叫周限制");
+        checkLimit(companyId, callerKey, true, 5, callerConfig.getCallerMonth(), "主叫月限制");
+    }
+
+    private void checkCalleeLimits(Long companyId, String calleePhone, SystemVoiceConfig calleeConfig) {
+        checkLimit(companyId, calleePhone, false, 1, calleeConfig.getCalleeMinute(), "被叫分钟限制");
+        checkLimit(companyId, calleePhone, false, 2, calleeConfig.getCalleeHour(), "被叫小时限制");
+        checkLimit(companyId, calleePhone, false, 3, calleeConfig.getCalleeDay(), "被叫日限制");
+        checkLimit(companyId, calleePhone, false, 4, calleeConfig.getCalleeWeek(), "被叫周限制");
+        checkLimit(companyId, calleePhone, false, 5, calleeConfig.getCalleeMonth(), "被叫月限制");
+    }
+
+    private void checkLimit(Long companyId, String phoneKey, boolean callerSide, int type, Integer max, String message) {
+        if (max == null || max <= 0 || StringUtils.isBlank(phoneKey)) {
+            return;
+        }
+        int current = callerSide
+                ? commGatewayApiLogService.countByCallerPhone(companyId, phoneKey, type)
+                : commGatewayApiLogService.countByCalleePhone(companyId, phoneKey, type);
+        if (current >= max) {
+            throw new ServiceException(message);
+        }
+    }
+
+    public String resolveCallCalleePhone(Long calleeId, String phone) {
+        if (StringUtils.isNotBlank(phone)) {
+            return normalizePhone(PhoneUtil.decryptAutoPhone(phone.trim()));
+        }
+        if (calleeId == null) {
+            return null;
+        }
+        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectById(calleeId);
+        if (callees == null || StringUtils.isBlank(callees.getPhone())) {
+            return null;
+        }
+        return normalizePhone(PhoneUtil.decryptAutoPhone(callees.getPhone()));
+    }
+
+    /** 解析短信被叫号码(供日志与限频使用) */
+    public String resolveSmsCalleePhone(Long calleeId, Long customerId, String phone) {
+        return resolveSmsPhone(calleeId, customerId, phone);
+    }
+
+    private String resolveSmsPhone(Long calleeId, Long customerId, String phone) {
+        if (StringUtils.isNotBlank(phone)) {
+            return normalizePhone(PhoneUtil.decryptAutoPhone(phone.trim()));
+        }
+        if (customerId != null) {
+            CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
+            if (customer != null && StringUtils.isNotBlank(customer.getMobile())) {
+                return normalizePhone(PhoneUtil.decryptAutoPhone(customer.getMobile()));
+            }
+        }
+        if (calleeId == null) {
+            return null;
+        }
+        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectById(calleeId);
+        if (callees == null) {
+            return null;
+        }
+        if (callees.getUserId() != null) {
+            CrmCustomer customer = crmCustomerService.selectCrmCustomerById(callees.getUserId());
+            if (customer != null && StringUtils.isNotBlank(customer.getMobile())) {
+                return normalizePhone(PhoneUtil.decryptAutoPhone(customer.getMobile()));
+            }
+        }
+        return normalizePhone(PhoneUtil.decryptAutoPhone(callees.getPhone()));
+    }
+
+    public String buildCompanyCallerKey(Long companyId, Long gatewayId) {
+        if (gatewayId != null) {
+            return "gw:" + gatewayId;
+        }
+        return "company:" + companyId;
+    }
+
+    public String normalizePhone(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            return phone;
+        }
+        String text = phone.trim();
+        if (text.startsWith("+86")) {
+            return text;
+        }
+        if (text.matches("\\d+")) {
+            return "+86" + text;
+        }
+        return text;
+    }
+
+    private SystemVoiceConfig parseConfig(String json) {
+        if (StringUtils.isBlank(json)) {
+            return null;
+        }
+        try {
+            return JSONUtil.toBean(json, SystemVoiceConfig.class);
+        } catch (Exception ex) {
+            log.warn("解析呼叫频率配置失败: {}", ex.getMessage());
+            return null;
+        }
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/comm/service/ICommGatewayApiLogService.java

@@ -0,0 +1,18 @@
+package com.fs.comm.service;
+
+import com.fs.comm.domain.CommGatewayApiLog;
+
+import java.util.List;
+
+public interface ICommGatewayApiLogService {
+
+    void saveLog(CommGatewayApiLog log);
+
+    CommGatewayApiLog selectById(Long logId);
+
+    List<CommGatewayApiLog> selectList(CommGatewayApiLog query);
+
+    int countByCalleePhone(Long companyId, String calleePhone, Integer type);
+
+    int countByCallerPhone(Long companyId, String callerPhone, Integer type);
+}

+ 36 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelRequest.java

@@ -0,0 +1,36 @@
+package com.fs.comm.sms;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 短信通道发送请求(迈远等 HTTP 通道共用)
+ */
+@Data
+@Builder
+public class CommSmsChannelRequest {
+
+    /** 目标手机号 */
+    private String phone;
+
+    /** 短信正文(不含签名) */
+    private String content;
+
+    /** 模板类型: 1=行业通知 2=营销短信 */
+    private Integer tempType;
+
+    /** 账户 */
+    private String account;
+
+    /** 密码 */
+    private String password;
+
+    /** 短信签名 */
+    private String sign;
+
+    /** 接口根地址(迈远,需以 / 结尾或不含路径) */
+    private String url;
+
+    /** 扩展码 extno */
+    private String extno;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsChannelResult.java

@@ -0,0 +1,40 @@
+package com.fs.comm.sms;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 短信通道发送结果
+ */
+@Data
+@Builder
+public class CommSmsChannelResult {
+
+    /** 是否发送成功 */
+    private boolean success;
+
+    /** 平台消息 ID(迈远 mid) */
+    private String mid;
+
+    /** 失败原因码,成功时为 OK */
+    private String errorCode;
+
+    /** 原始响应摘要(便于排查) */
+    private String rawResponse;
+
+    public static CommSmsChannelResult ok(String mid) {
+        return CommSmsChannelResult.builder()
+                .success(true)
+                .mid(mid)
+                .errorCode("OK")
+                .build();
+    }
+
+    public static CommSmsChannelResult fail(String errorCode, String rawResponse) {
+        return CommSmsChannelResult.builder()
+                .success(false)
+                .errorCode(errorCode)
+                .rawResponse(rawResponse)
+                .build();
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/comm/sms/CommSmsProvider.java

@@ -0,0 +1,12 @@
+package com.fs.comm.sms;
+
+/**
+ * 短信通道发送 SPI(由 fs-comm-gateway 等模块提供具体实现)
+ */
+public interface CommSmsProvider {
+
+    /** 服务商标识,如 my / card */
+    String provider();
+
+    CommSmsChannelResult send(CommSmsChannelRequest request);
+}

+ 22 - 0
fs-service/src/main/java/com/fs/comm/support/CommTenantDataSourceHelper.java

@@ -0,0 +1,22 @@
+package com.fs.comm.support;
+
+import com.fs.framework.datasource.TenantDataSourceManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 通讯网关模块专用:主库 @DataSource 调用后会 clear 数据源,需在租户表操作前显式切回租户库
+ */
+@Component
+public class CommTenantDataSourceHelper {
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    public void ensureTenant(Long tenantId) {
+        if (tenantId == null) {
+            return;
+        }
+        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+    }
+}

+ 44 - 3
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -7,6 +7,9 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.proxy.enums.ConsumeTypeEnum;
 import com.fs.proxy.enums.ConsumeTypeEnum;
 import com.fs.proxy.service.BalanceService;
 import com.fs.proxy.service.BalanceService;
+import com.fs.comm.sms.CommSmsChannelRequest;
+import com.fs.comm.sms.CommSmsChannelResult;
+import com.fs.comm.sms.CommSmsProvider;
 import com.fs.common.service.ISmsService;
 import com.fs.common.service.ISmsService;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsNotifyVO;
@@ -52,6 +55,7 @@ import org.springframework.transaction.annotation.Transactional;
 import java.io.UnsupportedEncodingException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.text.SimpleDateFormat;
+import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
@@ -113,6 +117,9 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     @Autowired
     private com.fs.proxy.mapper.CompanySmsCardMapper smsCardMapper;
     private com.fs.proxy.mapper.CompanySmsCardMapper smsCardMapper;
 
 
+    @Autowired(required = false)
+    private List<CommSmsProvider> commSmsProviders = Collections.emptyList();
+
     /**
     /**
      * 统一发送方法 - 替代原来6处硬编码的 his.sms 配置读取
      * 统一发送方法 - 替代原来6处硬编码的 his.sms 配置读取
      * 
      * 
@@ -167,10 +174,45 @@ public class SmsServiceImpl implements ISmsService
         }
         }
     }
     }
 
 
-    /** 迈远发送 */
+    /** 迈远发送(优先走 fs-comm-gateway 注册的 CommSmsProvider) */
     private String sendByRf(String phone, String content, Integer tempType,
     private String sendByRf(String phone, String content, Integer tempType,
                              String account, String password, String sign, String url, String extno,
                              String account, String password, String sign, String url, String extno,
                              Long tenantId, Long apiId, Long portId) {
                              Long tenantId, Long apiId, Long portId) {
+        CommSmsProvider myProvider = findCommSmsProvider("my");
+        if (myProvider != null) {
+            CommSmsChannelResult result = myProvider.send(CommSmsChannelRequest.builder()
+                    .phone(phone)
+                    .content(content)
+                    .tempType(tempType)
+                    .account(account)
+                    .password(password)
+                    .sign(sign)
+                    .url(url)
+                    .extno(extno)
+                    .build());
+            if (result.isSuccess()) {
+                return "OK";
+            }
+            return StringUtils.defaultIfBlank(result.getErrorCode(), "SEND_FAILED");
+        }
+        return sendByRfFallback(phone, content, tempType, account, password, sign, url, extno);
+    }
+
+    private CommSmsProvider findCommSmsProvider(String provider) {
+        if (commSmsProviders == null || commSmsProviders.isEmpty()) {
+            return null;
+        }
+        for (CommSmsProvider commSmsProvider : commSmsProviders) {
+            if (provider.equals(commSmsProvider.provider())) {
+                return commSmsProvider;
+            }
+        }
+        return null;
+    }
+
+    /** 非网关进程兜底:本地直连迈远 HTTP */
+    private String sendByRfFallback(String phone, String content, Integer tempType,
+                                    String account, String password, String sign, String url, String extno) {
         String urls;
         String urls;
         try {
         try {
             if (tempType.equals(1)) {
             if (tempType.equals(1)) {
@@ -185,7 +227,7 @@ public class SmsServiceImpl implements ISmsService
                 return "UNSUPPORTED_TEMP_TYPE";
                 return "UNSUPPORTED_TEMP_TYPE";
             }
             }
         } catch (UnsupportedEncodingException e) {
         } catch (UnsupportedEncodingException e) {
-            log.error("sendByRf: URL编码异常", e);
+            log.error("sendByRfFallback: URL编码异常", e);
             return "ENCODE_ERROR";
             return "ENCODE_ERROR";
         }
         }
 
 
@@ -194,7 +236,6 @@ public class SmsServiceImpl implements ISmsService
         if (vo.getStatus().equals(0)) {
         if (vo.getStatus().equals(0)) {
             for (SmsSendItemVO itemVO : vo.getList()) {
             for (SmsSendItemVO itemVO : vo.getList()) {
                 if (itemVO.getResult().equals("0")) {
                 if (itemVO.getResult().equals("0")) {
-                    // 发送成功, 返回OK (调用方负责写日志)
                     return "OK";
                     return "OK";
                 }
                 }
             }
             }

+ 49 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyCommGatewayLog.java

@@ -0,0 +1,49 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 通讯网关 API 调用记录(租户库 company_comm_gateway_log)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyCommGatewayLog extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long logId;
+    /** 主库 comm_gateway_api_log.log_id */
+    private Long masterLogId;
+    private Long companyId;
+    private Long companyUserId;
+    private String callerAccount;
+    private String apiType;
+    private String apiPath;
+    private String requestBody;
+    private String responseBody;
+    private Integer resultCode;
+    private String resultMsg;
+    private Integer success;
+    private Integer limitHit;
+    private String limitReason;
+    private String calleePhone;
+    private String callerPhone;
+    private Long gatewayId;
+    private BigDecimal billingAmount;
+    private BigDecimal costPrice;
+    private BigDecimal calcPrice;
+    private Integer billingQuantity;
+    private String billingUnit;
+    private String clientIp;
+    private String authScope;
+    private Integer durationMs;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 53 - 60
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApi.java

@@ -2,79 +2,72 @@ package com.fs.company.domain;
 
 
 import com.fs.common.annotation.Excel;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 
+import java.math.BigDecimal;
+
 /**
 /**
  * 呼叫接口对象 company_voice_api
  * 呼叫接口对象 company_voice_api
- * 
+ *
  * @author fs
  * @author fs
  * @date 2021-10-04
  * @date 2021-10-04
  */
  */
-public class CompanyVoiceApi extends BaseEntity
-{
+@Data
+public class CompanyVoiceApi extends BaseEntity {
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
-    /** ID */
     private Long apiId;
     private Long apiId;
-
-    /** API接口名 */
-    @Excel(name = "API接口名")
+    /**
+     * API名称
+     */
     private String apiName;
     private String apiName;
-
-    /** KEY */
-    @Excel(name = "apiType")
-    private String apiType;
-
-    /** JSON */
-    @Excel(name = "JSON")
-    private String apiJson;
-
-    /** 状态 */
-    @Excel(name = "状态")
+    /**
+     * 接口类型:0 SIP,1 网关,2 API
+     */
+    private Integer apiType;
+
+    /**
+     * 状态: 0 禁用 ,1 启用
+     */
     private Integer status;
     private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 成本价
+     */
+    private BigDecimal costPrice;
+    /**
+     * 账户
+     */
+    private String account;
+    /**
+     * 密码
+     */
+    private String password;
+    /**
+     * api地址
+     */
+    private String apiUrl;
+    /**
+     * 话术跳转地址(API类型)
+     */
+    private String dialogUrl;
+    /**
+     * 服务商
+     */
+    private String provider;
+    /**
+     * 历史字段,保留列暂不读写
+     */
+    private String apiJson;
 
 
-    public static long getSerialVersionUID() {
-        return serialVersionUID;
-    }
-
-    public Long getApiId() {
-        return apiId;
-    }
-
-    public void setApiId(Long apiId) {
-        this.apiId = apiId;
-    }
-
-    public String getApiName() {
-        return apiName;
-    }
-
-    public void setApiName(String apiName) {
-        this.apiName = apiName;
-    }
-
-    public String getApiType() {
-        return apiType;
-    }
-
-    public void setApiType(String apiType) {
-        this.apiType = apiType;
-    }
-
-    public String getApiJson() {
-        return apiJson;
-    }
-
-    public void setApiJson(String apiJson) {
-        this.apiJson = apiJson;
-    }
-
-    public Integer getStatus() {
-        return status;
-    }
+    /**
+     * 是否删除,0否 1是
+     */
+    private Integer isDel;
 
 
-    public void setStatus(Integer status) {
-        this.status = status;
-    }
 }
 }

+ 58 - 133
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApiTenant.java

@@ -1,7 +1,9 @@
 package com.fs.company.domain;
 package com.fs.company.domain;
 
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.util.Date;
 import java.util.Date;
@@ -12,164 +14,87 @@ import java.util.Date;
  * @author fs
  * @author fs
  * @date 2026-05-21
  * @date 2026-05-21
  */
  */
-public class CompanyVoiceApiTenant extends BaseEntity
-{
+@Data
+public class CompanyVoiceApiTenant extends BaseEntity {
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
-    /** 主键 */
+    /**
+     * 主键
+     */
     private Long id;
     private Long id;
 
 
-    /** 通话接口ID */
+    /**
+     * 通话接口ID
+     */
     private Long apiId;
     private Long apiId;
 
 
-    /** 租户ID */
-    private Long companyId;
+    /**
+     * 租户ID(tenant_info.id)
+     */
+    private Long tenantId;
 
 
-    /** 售价(元/分钟) */
-    private BigDecimal price;
+    /**
+     * 售价(元/分钟)
+     */
+    private BigDecimal salePrice;
 
 
-    /** 优先级 */
+    /**
+     * 优先级
+     */
     private Integer priority;
     private Integer priority;
 
 
-    /** 是否主线路 1是 0否 */
+    /**
+     * 是否主线路 1是 0否
+     */
     private Integer isPrimary;
     private Integer isPrimary;
 
 
-    /** 是否允许手动选择 1允许 0禁止 */
-    private Integer allowManual;
-
-    /** 状态 1启用 0禁用 */
+    /**
+     * 状态 1启用 0禁用
+     */
     private Integer status;
     private Integer status;
 
 
-    /** 创建时间 */
+    /**
+     * 创建时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
     private Date createTime;
 
 
-    /** 租户名(非表字段,关联查询用) */
-    private String companyName;
-
-    /** 接口名(非表字段,关联查询用) */
+    /**
+     * 接口名(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private String apiName;
     private String apiName;
 
 
-    /** 成本价(非表字段,关联查询用) */
+    /**
+     * 成本价(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private BigDecimal costPrice;
     private BigDecimal costPrice;
 
 
-    /** 接口类型(非表字段,关联查询用) */
+    /**
+     * 接口类型(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private Integer apiType;
     private Integer apiType;
 
 
-    /** 服务商(非表字段,关联查询用) */
+    /**
+     * 服务商(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private String provider;
     private String provider;
 
 
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
-
-    public Long getApiId() {
-        return apiId;
-    }
-
-    public void setApiId(Long apiId) {
-        this.apiId = apiId;
-    }
-
-    public Long getCompanyId() {
-        return companyId;
-    }
-
-    public void setCompanyId(Long companyId) {
-        this.companyId = companyId;
-    }
-
-    public Integer getStatus() {
-        return status;
-    }
-
-    public void setStatus(Integer status) {
-        this.status = status;
-    }
-
-    @Override
-    public Date getCreateTime() {
-        return createTime;
-    }
-
-    @Override
-    public void setCreateTime(Date createTime) {
-        this.createTime = createTime;
-    }
-
-    public String getCompanyName() {
-        return companyName;
-    }
-
-    public void setCompanyName(String companyName) {
-        this.companyName = companyName;
-    }
-
-    public String getApiName() {
-        return apiName;
-    }
-
-    public void setApiName(String apiName) {
-        this.apiName = apiName;
-    }
-
-    public BigDecimal getPrice() {
-        return price;
-    }
-
-    public void setPrice(BigDecimal price) {
-        this.price = price;
-    }
-
-    public Integer getPriority() {
-        return priority;
-    }
-
-    public void setPriority(Integer priority) {
-        this.priority = priority;
-    }
-
-    public Integer getIsPrimary() {
-        return isPrimary;
-    }
-
-    public void setIsPrimary(Integer isPrimary) {
-        this.isPrimary = isPrimary;
-    }
-
-    public Integer getAllowManual() {
-        return allowManual;
-    }
-
-    public void setAllowManual(Integer allowManual) {
-        this.allowManual = allowManual;
-    }
-
-    public BigDecimal getCostPrice() {
-        return costPrice;
-    }
+    /**
+     * 租户编码(冗余字段,关联 tenant_info 补全)
+     */
+    private String tenantCode;
 
 
-    public void setCostPrice(BigDecimal costPrice) {
-        this.costPrice = costPrice;
-    }
+    /**
+     * 租户名称(冗余字段,关联 tenant_info 补全)
+     */
+    private String tenantName;
+    /**
+     * 是否可选
+     */
+    private String selectable;
 
 
-    public Integer getApiType() {
-        return apiType;
-    }
-
-    public void setApiType(Integer apiType) {
-        this.apiType = apiType;
-    }
-
-    public String getProvider() {
-        return provider;
-    }
-
-    public void setProvider(String provider) {
-        this.provider = provider;
-    }
 }
 }

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

@@ -38,6 +38,12 @@ public class CompanyWorkflowLobsterNode extends BaseEntity {
 
 
     private String nodeConfig;
     private String nodeConfig;
 
 
+    /** AI 场景编码(对应 admin_ai_scene.scene_code),节点执行时按场景挑模型 */
+    private String sceneCode;
+
+    /** 指定具体模型(对应 admin_ai_model.model_name),优先级高于 sceneCode */
+    private String modelName;
+
     private String greetingConfig;
     private String greetingConfig;
 
 
     private LocalTime sendTime;
     private LocalTime sendTime;

+ 29 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChannelPluginConfig.java

@@ -0,0 +1,29 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾渠道插件配置 Domain
+ * <p>
+ * 表: lobster_channel_plugin_config
+ * 即插即用多IM通道:每个租户可为每个渠道(QW/WX/WHATSAPP/LINE/...)独立配置启用状态和API凭证
+ */
+@Data
+public class LobsterChannelPluginConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 渠道类型: QW/WX/IM/WHATSAPP/LINE/TELEGRAM/APP_IM/OTHER */
+    private String channelType;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 渠道配置 JSON (API Key / Webhook URL / Token / phoneNumberId 等) */
+    private String configJson;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChatMsg.java

@@ -0,0 +1,22 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾聊天消息 Domain(对应 chat_msg 表 — 多渠道聚合)
+ */
+@Data
+public class LobsterChatMsg {
+
+    private Long msgId;
+    private Long sessionId;
+    private Long companyId;
+    private String channelType;
+    private String content;
+    private Integer msgType;
+    private Integer sendType;
+    private Integer status;
+    private LocalDateTime createTime;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChatSession.java

@@ -0,0 +1,28 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾聊天会话 Domain(对应 chat_session 表 — 多渠道聚合)
+ */
+@Data
+public class LobsterChatSession {
+
+    private Long sessionId;
+    private Long companyId;
+    private Long contactId;
+    private String channelType;
+    private String channelSourceId;
+    private String channelSourceType;
+    private String userId;
+    private String externalUserId;
+    private String lastMsg;
+    private LocalDateTime lastMsgTime;
+    private Integer unreadCount;
+    private Integer status;
+    private Long instanceId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterComplianceAudit.java

@@ -0,0 +1,20 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 合规审计轨迹 Domain(对应 lobster_compliance_audit 表)
+ */
+@Data
+public class LobsterComplianceAudit {
+
+    private Long id;
+    private Long companyId;
+    private String ruleName;
+    private Integer severity;
+    private String matchedKeyword;
+    private String contentSnippet;
+    private LocalDateTime createTime;
+}

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

@@ -0,0 +1,42 @@
+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;
+
+/**
+ * 租户消费记录实体 — 表: tenant_consume_record
+ */
+@Data
+@TableName("tenant_consume_record")
+public class LobsterConsumeRecord {
+
+    @TableId(type = IdType.AUTO)
+    private Long recordId;
+    private Long tenantId;
+    private String tenantName;
+    private Long proxyId;
+    private Integer consumeType;
+    private String consumeTypeName;
+    private BigDecimal amount;
+    private BigDecimal platformCost;
+    private BigDecimal tenantPrice;
+    private BigDecimal proxyRatio;
+    private BigDecimal proxyProfit;
+    private BigDecimal unitPrice;
+    private Integer quantity;
+    private BigDecimal beforeBalance;
+    private BigDecimal afterBalance;
+    private String description;
+    private String month;
+    private String remark;
+    private String orderNo;
+    private Integer status;
+    private String failReason;
+    private LocalDateTime consumeTime;
+    private LocalDateTime createTime;
+}

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

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾消息去重配置
+ */
+@Data
+public class LobsterDedupConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 去重模式: exact/semantic/hybrid */
+    private String dedupMode;
+    /** 精确去重窗口大小(消息数) */
+    private Integer exactWindowSize;
+    /** 语义去重相似度阈值(0.0-1.0) */
+    private Double semanticThreshold;
+    /** 去重窗口时长(秒) */
+    private Integer windowDurationSeconds;
+    /** 忽略前缀消息数 */
+    private Integer ignorePrefixCount;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 11 - 2
fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java

@@ -5,8 +5,12 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.Data;
 
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 
 
+/**
+ * 龙虾事件节点审核实体 — 表: lobster_event_node_audit
+ */
 @Data
 @Data
 @TableName("lobster_event_node_audit")
 @TableName("lobster_event_node_audit")
 public class LobsterEventAudit {
 public class LobsterEventAudit {
@@ -15,11 +19,16 @@ public class LobsterEventAudit {
     private Long id;
     private Long id;
     private Long companyId;
     private Long companyId;
     private Long instanceId;
     private Long instanceId;
-    private String nodeType;
+    private String workflowId;
+    private String nodeKey;
     private String nodeName;
     private String nodeName;
+    private String nodeType;
     private String nodeJson;
     private String nodeJson;
     private String insertAt;
     private String insertAt;
-    private String reason;
+    private String insertRefNode;
+    private String decisionEngine;
+    private BigDecimal decisionScore;
+    private String decisionReason;
     private String status;
     private String status;
     private String auditBy;
     private String auditBy;
     private String auditComment;
     private String auditComment;

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterProfileConfig.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾用户画像配置
+ */
+@Data
+public class LobsterProfileConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 画像字段映射规则(JSON) */
+    private String fieldMappings;
+    /** 画像来源: crm_order/crm_tag/wechat_profile/custom */
+    private String profileSource;
+    /** 刷新策略: realtime/hourly/daily/manual */
+    private String refreshStrategy;
+    /** 最大画像字段数 */
+    private Integer maxFields;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterSalesCorpus.java

@@ -0,0 +1,36 @@
+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;
+
+/**
+ * 龙虾销冠语料实体 — 表: lobster_sales_corpus
+ */
+@Data
+@TableName("lobster_sales_corpus")
+public class LobsterSalesCorpus {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String dialogText;
+    private String customerQuestion;
+    private String salesAnswer;
+    private String scenario;
+    private BigDecimal score;
+    private String sourceType;
+    private String externalUserId;
+    private Long companyId;
+    private String salespersonName;
+    private String tags;
+    private String status;
+    private Integer enabled;
+    private String createBy;
+    private LocalDateTime createTime;
+    private String updateBy;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾摘要生成配置
+ */
+@Data
+public class LobsterSummaryConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 触发间隔(消息数) */
+    private Integer triggerInterval;
+    /** 触发策略: message_count/duration/manual */
+    private String triggerStrategy;
+    /** 摘要模型标识符 */
+    private String modelIdentifier;
+    /** 最大上下文消息数 */
+    private Integer maxContextMessages;
+    /** 摘要最大长度(字符) */
+    private Integer maxSummaryLength;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterTenantBalance.java

@@ -0,0 +1,40 @@
+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.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 租户余额实体 — 表: tenant_balance
+ */
+@Data
+@TableName("tenant_balance")
+public class LobsterTenantBalance {
+
+    @TableId(type = IdType.AUTO)
+    private Long balanceId;
+    private Long tenantId;
+    private String tenantName;
+    private BigDecimal totalBalance;
+    private BigDecimal aiModelBalance;
+    private BigDecimal courseFlowBalance;
+    private BigDecimal liveFlowBalance;
+    private BigDecimal tokenBalance;
+    private BigDecimal smsBalance;
+    private BigDecimal manualCallBalance;
+    private BigDecimal aiCallBalance;
+    private BigDecimal wechatHelperBalance;
+    private BigDecimal frozenAmount;
+    private BigDecimal totalRecharge;
+    private BigDecimal totalConsume;
+    private LocalDate accountFeeExpireTime;
+    private Integer status;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    private BigDecimal tokenCoefficient;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterTokenConsumption.java

@@ -0,0 +1,33 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾Token消耗记录(存于租户DB,管理端跨租户聚合查询)
+ */
+@Data
+public class LobsterTokenConsumption {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 工作流实例ID */
+    private Long instanceId;
+    /** 节点编码 */
+    private String nodeCode;
+    /** 模型标识符 */
+    private String modelIdentifier;
+    /** 消耗类型: prompt/completion */
+    private String consumeType;
+    /** Token数量 */
+    private Integer tokenCount;
+    /** 预估费用 */
+    private BigDecimal estimatedCost;
+    /** 请求时间 */
+    private LocalDateTime requestTime;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyCommGatewayLogMapper.java

@@ -0,0 +1,14 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyCommGatewayLog;
+
+import java.util.List;
+
+public interface CompanyCommGatewayLogMapper {
+
+    int insertCompanyCommGatewayLog(CompanyCommGatewayLog log);
+
+    CompanyCommGatewayLog selectCompanyCommGatewayLogById(Long logId);
+
+    List<CompanyCommGatewayLog> selectCompanyCommGatewayLogList(CompanyCommGatewayLog query);
+}

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

@@ -59,6 +59,6 @@ public interface CompanyVoiceApiMapper
      * @return 结果
      * @return 结果
      */
      */
     public int deleteCompanyVoiceApiByIds(Long[] apiIds);
     public int deleteCompanyVoiceApiByIds(Long[] apiIds);
-    @Select("select count(1) from company_voice_api")
+    @Select("select count(1) from company_voice_api where (is_del = 0 or is_del is null)")
     Integer selectCompanyVoiceApiCount();
     Integer selectCompanyVoiceApiCount();
 }
 }

+ 32 - 58
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiTenantMapper.java

@@ -14,64 +14,38 @@ import java.util.List;
  */
  */
 public interface CompanyVoiceApiTenantMapper
 public interface CompanyVoiceApiTenantMapper
 {
 {
-    /**
-     * 查询分配关系
-     */
-    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
-
-    /**
-     * 按接口ID+租户ID查询分配关系
-     */
-    public CompanyVoiceApiTenant selectByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
-
-    /**
-     * 查询接口已分配的租户列表(含租户名)
-     */
-    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
-
-    /**
-     * 查询租户已分配的接口列表(含接口名)
-     */
-    public List<CompanyVoiceApiTenant> selectApisByCompanyId(Long companyId);
-
-    /**
-     * 查询分配关系列表
-     */
-    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
-
-    /**
-     * 新增分配关系
-     */
-    public int insertCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
-
-    /**
-     * 批量新增分配关系
-     */
-    public int batchInsertCompanyVoiceApiTenant(@Param("list") List<CompanyVoiceApiTenant> list);
-
-    /**
-     * 修改分配关系
-     */
-    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
-
-    /**
-     * 删除分配关系
-     */
-    public int deleteCompanyVoiceApiTenantById(Long id);
-
-    /**
-     * 按接口ID+租户ID删除分配关系
-     */
-    public int deleteByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
-
-    /**
-     * 按接口ID删除所有分配关系
-     */
-    public int deleteByApiId(Long apiId);
-
-    /**
-     * 查询接口已分配的租户数量
-     */
+    CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
+
+    CompanyVoiceApiTenant selectByApiAndTenant(@Param("apiId") Long apiId, @Param("tenantId") Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
+
+    List<CompanyVoiceApiTenant> selectApisByTenantId(Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectEnabledApisByTenantId(Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
+
+    int insertCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    int batchInsertCompanyVoiceApiTenant(@Param("list") List<CompanyVoiceApiTenant> list);
+
+    int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    int deleteCompanyVoiceApiTenantById(Long id);
+
+    int deleteByApiAndTenant(@Param("apiId") Long apiId, @Param("tenantId") Long tenantId);
+
+    int deleteByApiId(Long apiId);
+
+    int disableByApiId(Long apiId);
+
+    int disableByApiIds(@Param("apiIds") Long[] apiIds);
+
+    int batchUpdatePricing(@Param("ids") List<Long> ids, @Param("data") CompanyVoiceApiTenant data);
+
+    int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
+
     @Select("SELECT COUNT(1) FROM company_voice_api_tenant WHERE api_id = #{apiId} AND status = 1")
     @Select("SELECT COUNT(1) FROM company_voice_api_tenant WHERE api_id = #{apiId} AND status = 1")
     Integer selectTenantCountByApiId(Long apiId);
     Integer selectTenantCountByApiId(Long apiId);
 }
 }

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java

@@ -21,4 +21,18 @@ public interface CompanyWorkflowLobsterNodeMapper extends BaseMapper<CompanyWork
     int checkNodeGreeting(@Param("templateId") Long templateId);
     int checkNodeGreeting(@Param("templateId") Long templateId);
 
 
     List<String> selectCallWordsByTemplateId(@Param("templateId") Long templateId);
     List<String> selectCallWordsByTemplateId(@Param("templateId") Long templateId);
+
+    /** 动态节点生成:取节点当前排序 */
+    Integer selectSortNoByCode(@Param("workflowId") Long workflowId, @Param("nodeCode") String nodeCode);
+
+    /** 动态节点生成:插入新节点(INSERT IGNORE 防止重复) */
+    int insertDynamicNode(@Param("workflowId") Long workflowId,
+                          @Param("nodeCode") String nodeCode,
+                          @Param("nodeName") String nodeName,
+                          @Param("nodeType") Integer nodeType,
+                          @Param("sortNo") Integer sortNo,
+                          @Param("nextNodeCode") String nextNodeCode,
+                          @Param("messageTemplate") String messageTemplate,
+                          @Param("conditionExpr") String conditionExpr,
+                          @Param("nodeConfig") String nodeConfig);
 }
 }

+ 29 - 0
fs-service/src/main/java/com/fs/company/mapper/CustomerFactMapper.java

@@ -0,0 +1,29 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 客户事实记忆 Mapper(customer_fact 表)
+ * 跨实例长期事实存储:目的地、金额、阶段、关键变量等
+ */
+public interface CustomerFactMapper {
+
+    /** 加载该客户最近20条历史事实(跨实例) */
+    List<Map<String, Object>> selectByUser(@Param("companyId") Long companyId,
+                                            @Param("externalUserId") String externalUserId);
+
+    /** 写入或更新单条事实(含 ON DUPLICATE KEY 更新) */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("instanceId") Long instanceId,
+               @Param("factKey") String factKey,
+               @Param("factValue") String factValue,
+               @Param("factType") String factType);
+
+    /** 遗忘机制:删除过期事实(超过 expireDays 天未更新且 confidence 低于阈值) */
+    int deleteExpiredFacts(@Param("expireBefore") String expireBefore,
+                           @Param("maxConfidence") double maxConfidence);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/CustomerHabitMapper.java

@@ -0,0 +1,25 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 客户沟通习惯 Mapper(customer_habit 表)
+ * 4维度:回复速度、消息长度偏好、Emoji偏好、对话风格
+ */
+public interface CustomerHabitMapper {
+
+    /** 查询客户全部习惯维度(按置信度过滤) */
+    List<Map<String, Object>> selectByUser(@Param("companyId") Long companyId,
+                                            @Param("externalUserId") String externalUserId);
+
+    /** Upsert 习惯:confidence 采用 EWMA 衰减(旧*0.7 + 新*0.3) */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("habitKey") String habitKey,
+               @Param("habitValue") String habitValue,
+               @Param("confidence") double confidence,
+               @Param("source") String source);
+}

+ 29 - 6
fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java

@@ -1,13 +1,36 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterApiRegistry;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
-@Mapper
-public interface LobsterApiRegistryMapper extends BaseMapper<LobsterApiRegistry> {
+/**
+ * 龙虾外部 API 注册 Mapper(lobster_api_registry 表)
+ */
+public interface LobsterApiRegistryMapper {
 
 
-    List<LobsterApiRegistry> selectEnabled();
+    List<Map<String, Object>> selectEnabled(@Param("category") String category);
+
+    int insert(@Param("apiKey") String apiKey,
+               @Param("apiName") String apiName,
+               @Param("category") String category,
+               @Param("provider") String provider,
+               @Param("baseUrl") String baseUrl,
+               @Param("authType") String authType,
+               @Param("authConfig") String authConfig,
+               @Param("timeout") Integer timeout,
+               @Param("priority") Integer priority,
+               @Param("isBackup") Integer isBackup,
+               @Param("description") String description);
+
+    int updateById(@Param("id") Long id,
+                   @Param("apiKey") String apiKey,
+                   @Param("baseUrl") String baseUrl,
+                   @Param("authConfig") String authConfig,
+                   @Param("enabled") Integer enabled);
+
+    int deleteById(@Param("id") Long id);
+
+    int ensureTable();
 }
 }

+ 206 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java

@@ -0,0 +1,206 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾知识版本 / 事件审计 / E2E / 死信等辅助模块 Mapper
+ */
+public interface LobsterAuxiliaryMapper {
+
+    // === lobster_knowledge_version ===
+    Integer selectMaxVersion(@Param("companyId") Long companyId,
+                             @Param("knowledgeId") Long knowledgeId);
+    Map<String, Object> selectKnowledgeById(@Param("companyId") Long companyId,
+                                             @Param("knowledgeId") Long knowledgeId);
+    int insertVersion(@Param("companyId") Long companyId,
+                      @Param("knowledgeId") Long knowledgeId,
+                      @Param("version") Integer version,
+                      @Param("title") String title,
+                      @Param("content") String content,
+                      @Param("changeLog") String changeLog);
+    List<Map<String, Object>> selectVersions(@Param("companyId") Long companyId,
+                                              @Param("knowledgeId") Long knowledgeId);
+    Map<String, Object> selectVersion(@Param("companyId") Long companyId,
+                                       @Param("knowledgeId") Long knowledgeId,
+                                       @Param("version") Integer version);
+    int rollbackVersion(@Param("companyId") Long companyId,
+                        @Param("knowledgeId") Long knowledgeId,
+                        @Param("toVersion") Integer toVersion);
+    int restoreVersion(@Param("companyId") Long companyId,
+                       @Param("knowledgeId") Long knowledgeId,
+                       @Param("fromVersion") Integer fromVersion);
+    int ensureVersionTable();
+
+    // === lobster_event_node_audit ===
+    List<Map<String, Object>> selectEventStuckNodes(@Param("companyId") Long companyId,
+                                                      @Param("sinceMinutes") int sinceMinutes);
+    List<Map<String, Object>> selectEventNodeStats(@Param("companyId") Long companyId);
+    int insertEventAudit(@Param("companyId") Long companyId,
+                         @Param("nodeCode") String nodeCode,
+                         @Param("stuckCount") int stuckCount,
+                         @Param("avgWaitMs") long avgWaitMs);
+    int ensureEventAuditTable();
+
+    // === lobster_user_events ===
+    int markEventProcessed(@Param("contactId") Long contactId);
+    Long countUserEvents(@Param("companyId") Long companyId);
+
+    // === lobster_dynamic_node_impl / company_dynamic_impl ===
+    List<Map<String, Object>> selectDynamicImpls(@Param("companyId") Long companyId,
+                                                  @Param("nodeType") Integer nodeType);
+    int insertDynamicImpl(@Param("companyId") Long companyId,
+                          @Param("nodeType") Integer nodeType,
+                          @Param("implName") String implName,
+                          @Param("implCode") String implCode,
+                          @Param("scriptContent") String scriptContent,
+                          @Param("status") String status);
+    Long selectLastInsertId();
+    Double selectAvgQualityScore(@Param("nodeType") Integer nodeType,
+                                  @Param("companyId") Long companyId);
+    String selectStatus(@Param("implId") Long implId);
+    int updateStatus(@Param("implId") Long implId, @Param("status") String status);
+    int updateDynamicImpl(@Param("implId") Long implId,
+                          @Param("scriptContent") String scriptContent,
+                          @Param("status") String status);
+    int deleteDynamicImpl(@Param("implId") Long implId);
+    int disableDynamicImpl(@Param("implId") Long implId);
+    List<Map<String, Object>> selectDynamicImplPaged(@Param("companyId") Long companyId,
+                                                      @Param("offset") int offset,
+                                                      @Param("limit") int limit);
+
+    // === lobster_distributed_patterns / practices ===
+    int upsertDistributedPattern(@Param("companyId") Long companyId,
+                                 @Param("industry") String industry,
+                                 @Param("scenario") String scenario,
+                                 @Param("patternType") String patternType,
+                                 @Param("content") String content,
+                                 @Param("score") double score);
+    List<Map<String, Object>> selectDistributedPatterns(@Param("companyId") Long companyId,
+                                                          @Param("industry") String industry,
+                                                          @Param("scenario") String scenario);
+    Map<String, Object> selectDistributedPractice(@Param("practiceId") Long practiceId);
+    int incrementPracticeUsage(@Param("practiceId") Long practiceId);
+    Integer countDistributedPatterns(@Param("companyId") Long companyId);
+    Integer countDistributedPractices(@Param("companyId") Long companyId);
+    Double avgDistributedScore(@Param("companyId") Long companyId);
+    Integer countDistributedContributors(@Param("companyId") Long companyId);
+    int ensureDistPatternTable();
+    int ensureDistPracticeTable();
+
+    // === lobster_heartbeat / scheduler ===
+    int insertHeartbeat(@Param("companyId") Long companyId,
+                        @Param("taskKey") String taskKey,
+                        @Param("status") String status);
+    List<Map<String, Object>> selectHeartbeats(@Param("companyId") Long companyId,
+                                                @Param("taskKey") String taskKey,
+                                                @Param("limit") int limit);
+    int updateHeartbeat(@Param("companyId") Long companyId,
+                        @Param("taskKey") String taskKey,
+                        @Param("status") String status);
+    int ensureHeartbeatTable();
+
+    // === lobster_dead_letter_queue ===
+    int insertDeadLetter(@Param("companyId") Long companyId,
+                         @Param("queueName") String queueName,
+                         @Param("payload") String payload,
+                         @Param("error") String error);
+    List<Map<String, Object>> selectDeadLetters(@Param("queueName") String queueName,
+                                                 @Param("limit") int limit);
+    int retryDeadLetter(@Param("id") Long id);
+    int deleteDeadLetter(@Param("id") Long id);
+    int ensureDeadLetterTable();
+
+    // === lobster_e2e_test ===
+    int insertE2eTest(@Param("companyId") Long companyId,
+                      @Param("testName") String testName,
+                      @Param("workflowId") Long workflowId,
+                      @Param("testData") String testData);
+    int insertE2eResult(@Param("testId") Long testId,
+                        @Param("companyId") Long companyId,
+                        @Param("passed") boolean passed,
+                        @Param("detail") String detail);
+    List<Map<String, Object>> selectE2eTests(@Param("companyId") Long companyId,
+                                              @Param("offset") int offset,
+                                              @Param("limit") int limit);
+    Map<String, Object> selectE2eResult(@Param("runId") String runId);
+    List<Map<String, Object>> selectE2eList(@Param("testId") Long testId);
+    int ensureE2eTestTable();
+    int ensureE2eResultTable();
+
+    // === lobster_test_scenario ===
+    List<Map<String, Object>> selectTestScenarios(@Param("companyId") Long companyId);
+    Map<String, Object> selectTestScenarioById(@Param("id") Long id,
+                                                @Param("companyId") Long companyId);
+    int insertTestScenario(@Param("companyId") Long companyId,
+                           @Param("scenarioName") String scenarioName,
+                           @Param("workflowId") Long workflowId,
+                           @Param("testData") String testData);
+    int updateTestScenario(@Param("id") Long id,
+                           @Param("scenarioName") String scenarioName,
+                           @Param("testData") String testData);
+    int deleteTestScenario(@Param("id") Long id, @Param("companyId") Long companyId);
+    int insertTestScenarioResult(@Param("companyId") Long companyId,
+                                  @Param("scenarioId") Long scenarioId,
+                                  @Param("passed") boolean passed,
+                                  @Param("detail") String detail);
+    int ensureScenarioTable();
+
+    // === lobster_workflow_patch ===
+    int insertPatch(@Param("companyId") Long companyId,
+                    @Param("targetTable") String targetTable,
+                    @Param("targetId") Long targetId,
+                    @Param("fieldName") String fieldName,
+                    @Param("oldValue") String oldValue,
+                    @Param("newValue") String newValue,
+                    @Param("reason") String reason);
+    List<Map<String, Object>> selectPatches(@Param("companyId") Long companyId);
+    int applyPatch(@Param("id") Long id);
+    int rejectPatch(@Param("id") Long id);
+    int ensurePatchTable();
+
+    // === vector tables ===
+    int insertVectorEmbedding(@Param("companyId") Long companyId,
+                              @Param("docId") String docId,
+                              @Param("docType") String docType,
+                              @Param("content") String content,
+                              @Param("embedding") String embedding);
+    List<Map<String, Object>> selectVectorEmbeddings(@Param("companyId") Long companyId,
+                                                      @Param("docType") String docType);
+    int ensureVectorTable();
+
+    // === 通用 ===
+    List<Map<String, Object>> selectPaged(@Param("table") String table,
+                                           @Param("whereClause") String whereClause,
+                                           @Param("offset") int offset,
+                                           @Param("limit") int limit);
+    int update(@Param("sql") String sql);
+
+    /** 带参数的 update/insert(占位符 ? 由 args 填充) */
+    int updateWithArgs(@Param("sql") String sql, @Param("args") List<Object> args);
+
+    /** ContactAdapter 使用:从平台用户表查原始ID */
+    Map<String, Object> selectSourceUserId(@Param("table") String sourceTable,
+                                            @Param("column") String userIdColumn,
+                                            @Param("companyId") Long companyId);
+
+    /** 配置读取(通用) */
+    String selectConfig(@Param("companyId") Long companyId,
+                        @Param("configKey") String configKey);
+
+    /** 兼容旧 jdbcTemplate.queryForList (单参数 companyId) */
+    List<Map<String, Object>> queryForList(@Param("sql") String sql,
+                                            @Param("customerId") Long customerId, @Param("companyId") Long companyId);
+    /** 兼容旧 jdbcTemplate.queryForList (单参数 companyId) */
+    List<Map<String, Object>> queryForList(@Param("sql") String sql,
+                                            @Param("companyId") Long companyId);
+
+    /** 兼容旧 jdbcTemplate.queryForList (单参数 + 返回类型) */
+    List<String> queryForStringList(@Param("sql") String sql,
+                                     @Param("companyId") Long companyId);
+
+    /** 兼容旧 jdbcTemplate.queryForObject → 表存在检查 */
+    int executeStatement(@Param("sql") String sql);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterBillingMapper.java

@@ -0,0 +1,27 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterConsumeRecord;
+import com.fs.company.domain.LobsterTenantBalance;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 龙虾计费 Mapper — 表: tenant_balance / tenant_consume_record
+ */
+public interface LobsterBillingMapper {
+
+    // ======== tenant_balance ========
+    LobsterTenantBalance selectBalanceByTenantId(@Param("tenantId") Long tenantId);
+
+    int updateTokenCoefficient(@Param("tenantId") Long tenantId,
+                               @Param("coefficient") BigDecimal coefficient);
+
+    // ======== tenant_consume_record ========
+    List<LobsterConsumeRecord> selectConsumeRecords(@Param("tenantId") Long tenantId,
+                                                     @Param("offset") int offset,
+                                                     @Param("limit") int limit);
+
+    long countConsumeRecords(@Param("tenantId") Long tenantId);
+}

+ 44 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChannelPluginConfigMapper.java

@@ -0,0 +1,44 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChannelPluginConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾渠道插件配置 Mapper
+ * <p>
+ * 表: lobster_channel_plugin_config
+ */
+public interface LobsterChannelPluginConfigMapper {
+
+    /**
+     * 查询租户指定渠道的配置
+     */
+    LobsterChannelPluginConfig selectByCompanyAndChannel(@Param("companyId") Long companyId,
+                                                          @Param("channelType") String channelType);
+
+    /**
+     * 查询租户所有已配置的渠道
+     */
+    List<LobsterChannelPluginConfig> selectByCompanyId(@Param("companyId") Long companyId);
+
+    /**
+     * 插入或更新(ON DUPLICATE KEY UPDATE)
+     */
+    int upsert(LobsterChannelPluginConfig config);
+
+    /**
+     * 更新启用状态
+     */
+    int updateEnabled(@Param("companyId") Long companyId,
+                      @Param("channelType") String channelType,
+                      @Param("enabled") Integer enabled);
+
+    /**
+     * 更新配置 JSON
+     */
+    int updateConfig(@Param("companyId") Long companyId,
+                     @Param("channelType") String channelType,
+                     @Param("configJson") String configJson);
+}

+ 35 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChannelRegistryMapper.java

@@ -0,0 +1,35 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 渠道注册 + 统一触点 Mapper
+ * 表: lobster_channel_type_registry / lobster_unified_contact
+ */
+public interface LobsterChannelRegistryMapper {
+
+    /** 从 unified_contact 查渠道用户ID */
+    String selectChannelUserId(@Param("companyId") Long companyId,
+                                @Param("contactId") Long contactId,
+                                @Param("channelType") String channelType);
+
+    /** 从平台用户表查原始ID */
+    Map<String, Object> selectSourceUserId(@Param("table") String sourceTable,
+                                            @Param("column") String userIdColumn,
+                                            @Param("companyId") Long companyId);
+
+    /** 绑定触点 */
+    int upsertUnifiedContact(@Param("companyId") Long companyId,
+                             @Param("contactId") Long contactId,
+                             @Param("channelType") String channelType,
+                             @Param("channelUserId") String channelUserId);
+
+    /** 加载DB渠道注册 */
+    List<Map<String, Object>> selectEnabledChannels();
+
+    /** 建表探针 */
+    int ensureTable();
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChatMsgMapper.java

@@ -0,0 +1,18 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChatMsg;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾聊天消息 Mapper(chat_msg 表 — 多渠道聚合)
+ */
+public interface LobsterChatMsgMapper {
+
+    /** 插入消息 */
+    int insert(LobsterChatMsg msg);
+
+    /** 按 sessionId 查询消息列表 */
+    List<LobsterChatMsg> selectBySessionId(@Param("sessionId") Long sessionId);
+}

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

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾聊天记录 Mapper(lobster_chat_record 表)
+ */
+public interface LobsterChatRecordMapper {
+
+    /** 统计最近N分钟新增的消息数 */
+    Integer countNewMessages(@Param("externalUserId") String externalUserId,
+                              @Param("instanceId") Long instanceId,
+                              @Param("minutes") int minutes);
+
+    /** 获取上次摘要后的新对话 */
+    List<Map<String, Object>> selectNewChatsSince(@Param("externalUserId") String externalUserId,
+                                                    @Param("instanceId") Long instanceId,
+                                                    @Param("lastSummaryTime") String lastSummaryTime);
+
+    /** ContextAssembler 使用:加载最近 N 条对话(按 message_time 倒序) */
+    List<Map<String, Object>> selectRecentChats(@Param("externalUserId") String externalUserId,
+                                                 @Param("instanceId") Long instanceId,
+                                                 @Param("companyId") Long companyId,
+                                                 @Param("limit") int limit);
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChatSessionMapper.java

@@ -0,0 +1,36 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChatSession;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾聊天会话 Mapper(chat_session 表 — 多渠道聚合)
+ */
+public interface LobsterChatSessionMapper {
+
+    /** 按 company_id + channel_source_id + channel_type 查询 */
+    LobsterChatSession selectBySourceAndChannel(@Param("companyId") Long companyId,
+                                                 @Param("channelSourceId") String channelSourceId,
+                                                 @Param("channelType") String channelType);
+
+    /** 按 company_id + user_id 查询(降级兼容) */
+    LobsterChatSession selectByCompanyAndUser(@Param("companyId") Long companyId,
+                                              @Param("userId") String userId);
+
+    /** 插入新会话,返回 sessionId */
+    int insert(LobsterChatSession session);
+
+    /** 更新最后消息 */
+    int updateLastMsg(@Param("sessionId") Long sessionId,
+                      @Param("lastMsg") String lastMsg);
+
+    /** 渠道聚合列表查询(带筛选) */
+    List<LobsterChatSession> selectForAggregate(@Param("channelType") String channelType,
+                                                 @Param("keyword") String keyword);
+
+    /** 按 sessionId 查询单个会话 */
+    LobsterChatSession selectBySessionId(@Param("sessionId") Long sessionId);
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceAuditMapper.java

@@ -0,0 +1,14 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterComplianceAudit;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 合规审计 Mapper(lobster_compliance_audit 表)
+ */
+public interface LobsterComplianceAuditMapper {
+
+    int insert(LobsterComplianceAudit audit);
+
+    int ensureTable();
+}

+ 2 - 4
fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java

@@ -12,18 +12,16 @@ import java.util.List;
 @Mapper
 @Mapper
 public interface LobsterComplianceRuleMapper extends BaseMapper<LobsterComplianceRule> {
 public interface LobsterComplianceRuleMapper extends BaseMapper<LobsterComplianceRule> {
 
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE company_id = #{companyId}")
     List<LobsterComplianceRule> selectByCompanyId(@Param("companyId") Long companyId);
     List<LobsterComplianceRule> selectByCompanyId(@Param("companyId") Long companyId);
 
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE company_id = #{companyId} AND enabled = 1")
     List<LobsterComplianceRule> selectEnabledByCompanyId(@Param("companyId") Long companyId);
     List<LobsterComplianceRule> selectEnabledByCompanyId(@Param("companyId") Long companyId);
 
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE id = #{id} AND company_id = #{companyId}")
     LobsterComplianceRule selectByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
     LobsterComplianceRule selectByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
 
 
     @Select("SELECT pattern FROM lobster_compliance_rule WHERE company_id = #{companyId} AND rule_type = 'sensitive' AND enabled = 1")
     @Select("SELECT pattern FROM lobster_compliance_rule WHERE company_id = #{companyId} AND rule_type = 'sensitive' AND enabled = 1")
     List<String> selectSensitivePatterns(@Param("companyId") Long companyId);
     List<String> selectSensitivePatterns(@Param("companyId") Long companyId);
 
 
-    @Update("UPDATE lobster_compliance_rule SET deleted = 1 WHERE id = #{id} AND company_id = #{companyId}")
     int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
     int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    int ensureTable();
 }
 }

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterConversationSummaryMapper.java

@@ -14,4 +14,29 @@ public interface LobsterConversationSummaryMapper {
     int insert(LobsterConversationSummary summary);
     int insert(LobsterConversationSummary summary);
 
 
     int updateById(LobsterConversationSummary summary);
     int updateById(LobsterConversationSummary summary);
+
+    /** 全局摘要:按 external_user_id 获取最近10条会话摘要 */
+    List<LobsterConversationSummary> selectByExternalUserId(@Param("companyId") Long companyId,
+                                                             @Param("externalUserId") String externalUserId);
+
+    /** 仅取最近一条 summary_text(按 user+instance) */
+    String selectLatestSummaryText(@Param("externalUserId") String externalUserId,
+                                    @Param("instanceId") Long instanceId);
+
+    /** 自动摘要写入(含 stage / nextActionHint / keyIntents / keyVariables 等扩展列) */
+    int insertAutoSummary(@Param("companyId") Long companyId,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("instanceId") Long instanceId,
+                          @Param("summaryText") String summaryText,
+                          @Param("chatMsgCount") int chatMsgCount,
+                          @Param("keyIntents") String keyIntents,
+                          @Param("keyVariables") String keyVariables,
+                          @Param("stage") String stage,
+                          @Param("nextActionHint") String nextActionHint);
+
+    /** 摘要写入后回写用户画像状态 */
+    int updateUserProfile(@Param("companyId") Long companyId,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("currentState") String currentState,
+                          @Param("variableSnapshot") String variableSnapshot);
 }
 }

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

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterDedupConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 消息去重配置 Mapper
+ */
+public interface LobsterDedupConfigMapper {
+
+    LobsterDedupConfig selectByCompanyId(@Param("companyId") Long companyId);
+
+    List<LobsterDedupConfig> selectAll(@Param("companyId") Long companyId);
+
+    int insert(LobsterDedupConfig config);
+
+    int updateById(LobsterDedupConfig config);
+
+    int deleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 21 - 28
fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java

@@ -1,31 +1,24 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterDialogueState;
-import org.apache.ibatis.annotations.*;
-
-@Mapper
-public interface LobsterDialogueStateMapper extends BaseMapper<LobsterDialogueState> {
-
-    @Select("SELECT state_json FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode} AND company_id = #{companyId}")
-    String selectStateJson(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode, @Param("companyId") Long companyId);
-
-    @Select("SELECT state_json FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode}")
-    String selectStateJsonNoCompany(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode);
-
-    @Delete("DELETE FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode} AND company_id = #{companyId}")
-    int deleteByInstanceAndNode(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode, @Param("companyId") Long companyId);
-
-    @Delete("DELETE FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode}")
-    int deleteByInstanceAndNodeNoCompany(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode);
-
-    @Insert("INSERT INTO lobster_dialogue_state (company_id, instance_id, node_code, state_json, update_time) " +
-            "VALUES (#{companyId}, #{instanceId}, #{nodeCode}, #{stateJson}, NOW()) " +
-            "ON DUPLICATE KEY UPDATE state_json = #{stateJson}, update_time = NOW()")
-    int upsert(LobsterDialogueState entity);
-
-    @Insert("INSERT INTO lobster_dialogue_state (instance_id, node_code, state_json, update_time) " +
-            "VALUES (#{instanceId}, #{nodeCode}, #{stateJson}, NOW()) " +
-            "ON DUPLICATE KEY UPDATE state_json = #{stateJson}, update_time = NOW()")
-    int upsertNoCompany(LobsterDialogueState entity);
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 龙虾对话状态 Mapper(lobster_dialogue_state 表 — 断点续聊)
+ */
+public interface LobsterDialogueStateMapper {
+
+    /** 读最新状态 */
+    Map<String, Object> selectLatest(@Param("companyId") Long companyId,
+                                      @Param("externalUserId") String externalUserId);
+
+    /** UPSERT 写入对话状态 */
+    int upsert(@Param("instanceId") Long instanceId,
+               @Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("nodeCode") String nodeCode,
+               @Param("intent") String intent,
+               @Param("sentiment") String sentiment,
+               @Param("lastReply") String lastReply);
 }
 }

+ 17 - 12
fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java

@@ -1,23 +1,28 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.LobsterEventAudit;
 import com.fs.company.domain.LobsterEventAudit;
-import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
 
 
-@Mapper
-public interface LobsterEventAuditMapper extends BaseMapper<LobsterEventAudit> {
+/**
+ * 龙虾事件节点审核 Mapper — 表: lobster_event_node_audit
+ */
+public interface LobsterEventAuditMapper {
 
 
-    List<LobsterEventAudit> selectListByCompanyIdAndStatus(@Param("companyId") Long companyId,
-                                                            @Param("status") String status,
-                                                            @Param("offset") int offset,
-                                                            @Param("limit") int limit);
+    List<LobsterEventAudit> selectList(@Param("companyId") Long companyId,
+                                        @Param("status") String status,
+                                        @Param("offset") int offset,
+                                        @Param("limit") int limit);
 
 
-    long countByCompanyIdAndStatus(@Param("companyId") Long companyId,
-                                   @Param("status") String status);
+    long countByStatus(@Param("companyId") Long companyId, @Param("status") String status);
 
 
-    LobsterEventAudit selectByIdAndStatus(@Param("id") Long id,
-                                           @Param("status") String status);
+    long countTotalByStatus(@Param("companyId") Long companyId, @Param("status") String status);
+
+    LobsterEventAudit selectById(@Param("id") Long id);
+
+    int updateStatus(@Param("id") Long id,
+                     @Param("status") String status,
+                     @Param("auditBy") String auditBy,
+                     @Param("auditComment") String auditComment);
 }
 }

+ 88 - 5
fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java

@@ -1,9 +1,92 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterEvolutionConfig;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 
-@Mapper
-public interface LobsterEvolutionConfigMapper extends BaseMapper<LobsterEvolutionConfig> {
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾进化配置 Mapper
+ * 表: lobster_node_optimization / lobster_optimization_config / lobster_node_interaction
+ */
+public interface LobsterEvolutionConfigMapper {
+
+    List<Map<String, Object>> selectOptimizationStats(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectOptimizationTypes(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectOptimizations(@Param("companyId") Long companyId,
+                                                   @Param("workflowId") Long workflowId,
+                                                   @Param("offset") int offset,
+                                                   @Param("limit") int limit);
+    List<Map<String, Object>> selectOptimizationByStatus(@Param("companyId") Long companyId,
+                                                          @Param("status") String status,
+                                                          @Param("offset") int offset,
+                                                          @Param("limit") int limit);
+    Map<String, Object> selectOptimizationById(@Param("id") Long id,
+                                                @Param("companyId") Long companyId);
+    int approveOptimization(@Param("id") Long id, @Param("auditorId") Long auditorId);
+    int rejectOptimization(@Param("id") Long id, @Param("auditorId") Long auditorId);
+    int ensureOptimizationTable();
+
+    Map<String, Object> selectConfig(@Param("companyId") Long companyId,
+                                      @Param("workflowId") Long workflowId,
+                                      @Param("nodeCode") String nodeCode);
+    Boolean selectConfigEnabled(@Param("companyId") Long companyId,
+                                 @Param("workflowId") Long workflowId,
+                                 @Param("nodeCode") String nodeCode);
+    int upsertConfig(@Param("companyId") Long companyId,
+                     @Param("workflowId") Long workflowId,
+                     @Param("nodeCode") String nodeCode,
+                     @Param("enabled") Boolean enabled,
+                     @Param("autoApply") String autoApply,
+                     @Param("configJson") String configJson);
+    int updateConfigStatus(@Param("companyId") Long companyId,
+                           @Param("workflowId") Long workflowId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("enabled") Boolean enabled);
+    int ensureConfigTable();
+
+    int insertInteraction(@Param("companyId") Long companyId,
+                          @Param("instanceId") Long instanceId,
+                          @Param("nodeCode") String nodeCode,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("interactionType") String interactionType,
+                          @Param("content") String content);
+    List<String> selectInteractionTrace(@Param("externalUserId") String externalUserId,
+                                         @Param("nodeCode") String nodeCode,
+                                         @Param("companyId") Long companyId);
+    int ensureInteractionTable();
+
+    Double selectAverageQualityScore(@Param("companyId") Long companyId,
+                                      @Param("nodeCode") String nodeCode);
+
+    // === lobster_evolution_log (EvolutionEngineImpl) ===
+    int insertEvolutionLog(@Param("companyId") Long companyId,
+                           @Param("workflowId") Long workflowId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("action") String action,
+                           @Param("detail") String detail);
+    List<Map<String, Object>> selectEvolutionLogs(@Param("companyId") Long companyId,
+                                                   @Param("workflowId") Long workflowId);
+    int ensureEvolutionLogTable();
+
+    // === lobster_evolution_suggestion (EvolutionEngineImpl) ===
+    Map<String, Object> selectSuggestionById(@Param("id") Long id,
+                                              @Param("companyId") Long companyId);
+    int insertSuggestion(@Param("companyId") Long companyId,
+                         @Param("workflowId") Long workflowId,
+                         @Param("nodeCode") String nodeCode,
+                         @Param("suggestionType") String suggestionType,
+                         @Param("currentContent") String currentContent,
+                         @Param("suggestedContent") String suggestedContent,
+                         @Param("reason") String reason,
+                         @Param("confidence") Double confidence);
+    int updateNodeMessage(@Param("suggestedContent") String suggestedContent,
+                          @Param("workflowId") Long workflowId,
+                          @Param("nodeCode") String nodeCode);
+    int acceptSuggestion(@Param("id") Long id);
+    Integer countTotal(@Param("companyId") Long companyId);
+    Integer countReplied(@Param("companyId") Long companyId);
+    Integer countPendingSuggestion(@Param("companyId") Long companyId);
+    Integer countApplied(@Param("companyId") Long companyId);
+    int ensureSuggestionTable();
 }
 }

+ 57 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterFeedbackMapper.java

@@ -0,0 +1,57 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾反馈驱动进化 Mapper
+ * 表: lobster_feedback_records / lobster_message_variants / lobster_ab_tests
+ */
+public interface LobsterFeedbackMapper {
+
+    // ===== feedback_records =====
+    int insertFeedback(@Param("companyId") Long companyId,
+                       @Param("instanceId") Long instanceId,
+                       @Param("nodeCode") String nodeCode,
+                       @Param("feedbackType") String feedbackType,
+                       @Param("comment") String comment);
+
+    List<Map<String, Object>> selectFeedbackStats(@Param("companyId") Long companyId);
+
+    String selectCurrentMessage(@Param("nodeCode") String nodeCode,
+                                @Param("companyId") Long companyId);
+
+    List<String> selectNegativeComments(@Param("companyId") Long companyId,
+                                         @Param("nodeCode") String nodeCode);
+
+    int ensureFeedbackTable();
+
+    // ===== message_variants =====
+    int insertVariant(@Param("companyId") Long companyId,
+                      @Param("nodeCode") String nodeCode,
+                      @Param("content") String content,
+                      @Param("generationReason") String generationReason);
+
+    int ensureVariantTable();
+
+    // ===== ab_tests =====
+    List<Map<String, Object>> selectActiveAbTests(@Param("companyId") Long companyId);
+
+    Map<String, Object> selectAbTest(@Param("testId") Long testId,
+                                      @Param("companyId") Long companyId);
+
+    int applyVariantToNode(@Param("variantMessage") String variantMessage,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("companyId") Long companyId);
+
+    int markAbTestApplied(@Param("testId") Long testId);
+
+    int markAbTestCompleted(@Param("testId") Long testId);
+
+    List<Map<String, Object>> selectWinAbTests(@Param("companyId") Long companyId,
+                                                @Param("winThreshold") double winThreshold);
+
+    int ensureAbTestTable();
+}

+ 11 - 5
fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java

@@ -1,9 +1,15 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterHandoffEvent;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 
-@Mapper
-public interface LobsterHandoffEventMapper extends BaseMapper<LobsterHandoffEvent> {
+/**
+ * 龙虾转人工事件 Mapper(lobster_handoff_events 表)
+ */
+public interface LobsterHandoffEventMapper {
+
+    int insertEvent(@Param("companyId") Long companyId,
+                    @Param("instanceId") Long instanceId,
+                    @Param("externalUserId") String externalUserId,
+                    @Param("triggerType") String triggerType,
+                    @Param("triggerDetail") String triggerDetail);
 }
 }

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

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 知识使用反馈 Mapper(lobster_knowledge_usage_log 表)
+ * 追踪知识被检索后在 AI 回复中的实际使用情况
+ */
+public interface LobsterKnowledgeUsageLogMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("instanceId") Long instanceId,
+               @Param("knowledgeTitle") String knowledgeTitle,
+               @Param("knowledgeText") String knowledgeText,
+               @Param("retrievalMethod") String retrievalMethod,
+               @Param("source") String source);
+
+    Map<String, Object> countUsage(@Param("companyId") Long companyId,
+                                    @Param("knowledgeTitle") String knowledgeTitle);
+}

+ 22 - 15
fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java

@@ -1,28 +1,35 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterLearningCorpus;
-import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
-@Mapper
-public interface LobsterLearningCorpusMapper extends BaseMapper<LobsterLearningCorpus> {
+/**
+ * 销冠语料库 Mapper(lobster_learning_corpus 表)
+ */
+public interface LobsterLearningCorpusMapper {
 
 
-    List<LobsterLearningCorpus> selectListByCompanyId(@Param("companyId") Long companyId,
-                                                       @Param("scenario") String scenario,
-                                                       @Param("status") String status);
+    int insert(@Param("companyId") Long companyId,
+               @Param("salespersonName") String salespersonName,
+               @Param("customerQuestion") String customerQuestion,
+               @Param("salesAnswer") String salesAnswer,
+               @Param("scenario") String scenario,
+               @Param("industryType") String industryType,
+               @Param("tags") String tags,
+               @Param("status") String status);
 
 
-    List<LobsterLearningCorpus> selectRawByCompanyId(@Param("companyId") Long companyId,
-                                                      @Param("limit") int limit);
+    List<Map<String, Object>> selectByScenario(@Param("companyId") Long companyId,
+                                                @Param("scenario") String scenario,
+                                                @Param("limit") int limit);
 
 
-    List<LobsterLearningCorpus> selectByScenario(@Param("companyId") Long companyId,
-                                                  @Param("scenario") String scenario,
-                                                  @Param("limit") int limit);
+    int incrementUsageCount(@Param("entryId") Long entryId);
+
+    List<Map<String, Object>> selectRawCorpus(@Param("companyId") Long companyId,
+                                               @Param("limit") int limit);
 
 
     int markAnalyzed(@Param("companyId") Long companyId,
     int markAnalyzed(@Param("companyId") Long companyId,
-                     @Param("limit") int limit);
+                     @Param("count") int count);
 
 
-    int incrementUsage(@Param("id") Long id);
+    int ensureTable();
 }
 }

+ 65 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterMultiTurnDialogueMapper.java

@@ -0,0 +1,65 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾多轮对话管理 Mapper(lobster_multi_turn_dialogue 表)
+ */
+public interface LobsterMultiTurnDialogueMapper {
+
+    int deleteState(@Param("instanceId") Long instanceId,
+                    @Param("nodeCode") String nodeCode,
+                    @Param("companyId") Long companyId);
+
+    int deleteStateNoCompany(@Param("instanceId") Long instanceId,
+                             @Param("nodeCode") String nodeCode);
+
+    List<Map<String, Object>> selectDialogue(@Param("instanceId") Long instanceId,
+                                              @Param("nodeCode") String nodeCode,
+                                              @Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectDialogueNoCompany(@Param("instanceId") Long instanceId,
+                                                       @Param("nodeCode") String nodeCode);
+
+    int appendTurn(@Param("companyId") Long companyId,
+                   @Param("instanceId") Long instanceId,
+                   @Param("nodeCode") String nodeCode,
+                   @Param("turnIndex") int turnIndex,
+                   @Param("direction") int direction,
+                   @Param("content") String content);
+
+    int appendTurnNoCompany(@Param("instanceId") Long instanceId,
+                            @Param("nodeCode") String nodeCode,
+                            @Param("turnIndex") int turnIndex,
+                            @Param("direction") int direction,
+                            @Param("content") String content);
+
+    int getMaxTurnIndex(@Param("instanceId") Long instanceId,
+                        @Param("nodeCode") String nodeCode,
+                        @Param("companyId") Long companyId);
+
+    int ensureTable();
+
+    /** 按 state_json 方式存取 */
+    String selectStateJson(@Param("instanceId") Long instanceId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("companyId") Long companyId);
+
+    String selectStateJsonNoCompany(@Param("instanceId") Long instanceId,
+                                    @Param("nodeCode") String nodeCode);
+
+    int upsertState(@Param("companyId") Long companyId,
+                    @Param("instanceId") Long instanceId,
+                    @Param("nodeCode") String nodeCode,
+                    @Param("stateJson") String stateJson);
+
+    int upsertStateNoCompany(@Param("instanceId") Long instanceId,
+                             @Param("nodeCode") String nodeCode,
+                             @Param("stateJson") String stateJson);
+
+    List<Map<String, Object>> selectNodeConfig(@Param("instanceId") Long instanceId,
+                                                @Param("nodeCode") String nodeCode);
+}

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

@@ -14,4 +14,7 @@ public interface LobsterNodeExecutionLogMapper {
     int insert(LobsterNodeExecutionLog log);
     int insert(LobsterNodeExecutionLog log);
 
 
     int updateById(LobsterNodeExecutionLog log);
     int updateById(LobsterNodeExecutionLog log);
+
+    /** 查询节点历史平均质量评分 */
+    Integer selectAverageQualityScore(@Param("companyId") Long companyId, @Param("nodeCode") String nodeCode);
 }
 }

+ 49 - 27
fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java

@@ -1,32 +1,54 @@
 package com.fs.company.mapper;
 package com.fs.company.mapper;
 
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterPendingKnowledge;
-import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.annotations.Param;
 
 
 import java.util.List;
 import java.util.List;
-
-@Mapper
-public interface LobsterPendingKnowledgeMapper extends BaseMapper<LobsterPendingKnowledge> {
-
-    @Select("SELECT * FROM lobster_pending_knowledge WHERE company_id = #{companyId} AND audit_status = 'pending'")
-    List<LobsterPendingKnowledge> selectPendingByCompanyId(@Param("companyId") Long companyId);
-
-    @Select("SELECT COUNT(*) FROM lobster_pending_knowledge WHERE company_id = #{companyId} AND audit_status = 'pending'")
-    int countPendingByCompanyId(@Param("companyId") Long companyId);
-
-    @Select("SELECT * FROM lobster_pending_knowledge WHERE id = #{id} AND company_id = #{companyId}")
-    LobsterPendingKnowledge selectByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
-
-    @Update("UPDATE lobster_pending_knowledge SET question = #{question}, answer = #{answer}, update_by = #{updateBy}, update_time = NOW() WHERE id = #{id} AND company_id = #{companyId}")
-    int updateContent(@Param("id") Long id, @Param("companyId") Long companyId, @Param("question") String question, @Param("answer") String answer, @Param("updateBy") String updateBy);
-
-    @Update("UPDATE lobster_pending_knowledge SET audit_status = 'approved', auditor = #{auditor}, audit_time = NOW(), update_time = NOW() WHERE id = #{id} AND company_id = #{companyId}")
-    int approve(@Param("id") Long id, @Param("companyId") Long companyId, @Param("auditor") String auditor);
-
-    @Update("UPDATE lobster_pending_knowledge SET audit_status = 'rejected', audit_comment = #{comment}, auditor = #{auditor}, audit_time = NOW(), update_time = NOW() WHERE id = #{id} AND company_id = #{companyId} AND audit_status = 'pending'")
-    int reject(@Param("id") Long id, @Param("companyId") Long companyId, @Param("auditor") String auditor, @Param("comment") String comment);
-
-    @Delete("DELETE FROM lobster_pending_knowledge WHERE id = #{id} AND company_id = #{companyId}")
-    int deleteByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+import java.util.Map;
+
+/**
+ * 龙虾待审知识库 Mapper(lobster_pending_knowledge 表)
+ */
+public interface LobsterPendingKnowledgeMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("knowledgeType") String knowledgeType,
+               @Param("content") String content,
+               @Param("contextSnapshot") String contextSnapshot,
+               @Param("sourceNodeCode") String sourceNodeCode,
+               @Param("status") String status);
+
+    List<Map<String, Object>> selectByCompany(@Param("companyId") Long companyId,
+                                               @Param("status") String status,
+                                               @Param("offset") int offset,
+                                               @Param("limit") int limit);
+
+    int countByCompany(@Param("companyId") Long companyId,
+                       @Param("status") String status);
+
+    List<Map<String, Object>> selectById(@Param("id") Long id,
+                                          @Param("companyId") Long companyId);
+
+    int auditApprove(@Param("id") Long id,
+                     @Param("companyId") Long companyId,
+                     @Param("auditorId") Long auditorId,
+                     @Param("auditComment") String auditComment);
+
+    int auditReject(@Param("id") Long id,
+                    @Param("companyId") Long companyId,
+                    @Param("auditorId") Long auditorId,
+                    @Param("auditComment") String auditComment);
+
+    int batchApprove(@Param("ids") List<Long> ids,
+                     @Param("companyId") Long companyId,
+                     @Param("auditorId") Long auditorId);
+
+    int batchReject(@Param("ids") List<Long> ids,
+                    @Param("companyId") Long companyId,
+                    @Param("auditorId") Long auditorId);
+
+    int delete(@Param("id") Long id,
+               @Param("companyId") Long companyId);
+
+    int ensureTable();
 }
 }

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

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterProfileConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 用户画像配置 Mapper
+ */
+public interface LobsterProfileConfigMapper {
+
+    LobsterProfileConfig selectByCompanyId(@Param("companyId") Long companyId);
+
+    List<LobsterProfileConfig> selectAll(@Param("companyId") Long companyId);
+
+    int insert(LobsterProfileConfig config);
+
+    int updateById(LobsterProfileConfig config);
+
+    int deleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSalesCorpusMapper.java

@@ -0,0 +1,31 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾销售语料分析 Mapper(lobster_sales_corpus 表)
+ */
+public interface LobsterSalesCorpusMapper {
+
+    List<Map<String, Object>> selectByScenario(@Param("companyId") Long companyId,
+                                                @Param("scenario") String scenario,
+                                                @Param("limit") int limit);
+
+    int incrementUsageCount(@Param("entryId") Long entryId);
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("scenario") String scenario,
+               @Param("content") String content,
+               @Param("source") String source,
+               @Param("score") double score);
+
+    List<Map<String, Object>> selectAll(@Param("companyId") Long companyId);
+
+    int updateScore(@Param("entryId") Long entryId,
+                    @Param("score") double score);
+
+    int ensureTable();
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است