소스 검색

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	fs-service/src/main/resources/db/tenant-initTable.sql
yh 1 개월 전
부모
커밋
355989adb6
83개의 변경된 파일2731개의 추가작업 그리고 278개의 파일을 삭제
  1. 33 3
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  2. 6 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  3. 234 0
      fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java
  4. 70 6
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  5. 6 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  6. 18 11
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java
  7. 38 0
      fs-company/src/main/java/com/fs/company/controller/company/CrmCustomerCallLogController.java
  8. 11 1
      fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyTagTemplateBindingController.java
  9. 9 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  10. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  11. 49 22
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  12. 18 0
      fs-service/src/main/java/com/fs/ai/param/QdrantClearPayloadParam.java
  13. 13 0
      fs-service/src/main/java/com/fs/ai/param/QdrantCountParam.java
  14. 23 0
      fs-service/src/main/java/com/fs/ai/param/QdrantCreateCollectionParam.java
  15. 20 0
      fs-service/src/main/java/com/fs/ai/param/QdrantDeletePayloadParam.java
  16. 18 0
      fs-service/src/main/java/com/fs/ai/param/QdrantDeletePointsParam.java
  17. 71 0
      fs-service/src/main/java/com/fs/ai/param/QdrantFilter.java
  18. 21 0
      fs-service/src/main/java/com/fs/ai/param/QdrantScrollParam.java
  19. 26 0
      fs-service/src/main/java/com/fs/ai/param/QdrantSearchGroupsParam.java
  20. 34 0
      fs-service/src/main/java/com/fs/ai/param/QdrantSearchParam.java
  21. 20 0
      fs-service/src/main/java/com/fs/ai/param/QdrantSetPayloadParam.java
  22. 16 0
      fs-service/src/main/java/com/fs/ai/param/QdrantUpdateCollectionParam.java
  23. 26 0
      fs-service/src/main/java/com/fs/ai/param/QdrantUpdateVectorsParam.java
  24. 28 0
      fs-service/src/main/java/com/fs/ai/param/QdrantUpsertPointsParam.java
  25. 379 0
      fs-service/src/main/java/com/fs/ai/util/QdrantClientUtil.java
  26. 15 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantBaseResponse.java
  27. 20 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantClusterInfoResult.java
  28. 25 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantCollectionInfo.java
  29. 9 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantCountResult.java
  30. 11 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantOperationResult.java
  31. 20 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantPoint.java
  32. 13 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantScrollResult.java
  33. 19 0
      fs-service/src/main/java/com/fs/ai/vo/QdrantSearchGroupsResult.java
  34. 2 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordByUuidQueryParams.java
  35. 11 8
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java
  36. 7 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  37. 78 22
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  38. 13 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  39. 22 0
      fs-service/src/main/java/com/fs/company/domain/CompanyBindGateway.java
  40. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterTask.java
  41. 153 0
      fs-service/src/main/java/com/fs/company/domain/CrmCustomerCallLog.java
  42. 19 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyBindGatewayMapper.java
  43. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyLobsterTagUserRelMapper.java
  44. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterEdgeMapper.java
  45. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterVariableMapper.java
  46. 19 0
      fs-service/src/main/java/com/fs/company/mapper/CrmCustomerCallLogMapper.java
  47. 2 1
      fs-service/src/main/java/com/fs/company/param/BatchBindLobsterTagParam.java
  48. 26 0
      fs-service/src/main/java/com/fs/company/param/PauseRoboticActiveParam.java
  49. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java
  50. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  51. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java
  52. 3 1
      fs-service/src/main/java/com/fs/company/service/ICompanyTagTemplateBindingService.java
  53. 4 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  54. 22 0
      fs-service/src/main/java/com/fs/company/service/ICrmCustomerCallLogService.java
  55. 93 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundBindServiceImpl.java
  56. 0 11
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  57. 18 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  58. 91 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java
  59. 10 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java
  60. 31 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  61. 33 0
      fs-service/src/main/java/com/fs/company/service/impl/CrmCustomerCallLogServiceImpl.java
  62. 3 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  63. 5 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  64. 3 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java
  65. 4 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  66. 10 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  67. 5 0
      fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java
  68. 5 0
      fs-service/src/main/java/com/fs/crm/param/CrmMyCustomerListQueryParam.java
  69. 2 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerService.java
  70. 40 28
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java
  71. 14 1
      fs-service/src/main/resources/db/tenant-initData.sql
  72. 23 0
      fs-service/src/main/resources/mapper/company/CompanyBindGatewayMapper.xml
  73. 12 0
      fs-service/src/main/resources/mapper/company/CompanyLobsterTagUserRelMapper.xml
  74. 1 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml
  75. 4 3
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogSendmsgMapper.xml
  76. 17 17
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterEdgeMapper.xml
  77. 7 7
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterTaskMapper.xml
  78. 15 15
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterVariableMapper.xml
  79. 97 0
      fs-service/src/main/resources/mapper/company/CrmCustomerCallLogMapper.xml
  80. 8 2
      fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml
  81. 30 1
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  82. 203 113
      fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java
  83. 113 0
      fs-wx-api/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

+ 33 - 3
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.aiSipCall;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
 import com.fs.aiSipCall.domain.CcCustInfo;
 import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
@@ -10,8 +11,13 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.vo.easycall.EasyCallOutBoundVO;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,7 +28,7 @@ import java.util.List;
 
 /**
  * aiSIP手动外呼通话记录Controller
- * 
+ *
  * @author fs
  * @date 2026-03-19
  */
@@ -33,6 +39,10 @@ public class AiSipCallOutboundCdrController extends BaseController
 {
     @Autowired
     private IAiSipCallOutboundCdrService aiSipCallOutboundCdrService;
+    @Autowired
+    private EasyCallMapper easyCallMapper;
+    @Autowired
+    TenantDataSourceManager tenantDataSourceManager;
 
     /**
      * 查询aiSIP手动外呼通话记录列表
@@ -147,6 +157,9 @@ public class AiSipCallOutboundCdrController extends BaseController
 
     /**
      * 同步aiSIP外呼通话记录
+     * 说明:
+     * 1. 如果有 workflowInstanceId 和 roboticId,走原来的机器人/工作流外呼记录
+     * 2. 如果没有 workflowInstanceId 和 roboticId,走用户手动外呼记录
      */
     @PostMapping("/syncByUuid")
     public AjaxResult syncByUuid(@RequestBody ApiCallRecordByUuidQueryParams req) {
@@ -156,8 +169,25 @@ public class AiSipCallOutboundCdrController extends BaseController
         if (StringUtils.isBlank(req.getCallType())) {
             req.setCallType("03");
         }
-
-        int rows = aiSipCallOutboundCdrService.syncByUuid(req);
+        if (req == null || org.apache.commons.lang3.StringUtils.isBlank(req.getUuid())) {
+            throw new ServiceException("uuid不能为空");
+        }
+        EasyCallOutBoundVO callPhoneRes = easyCallMapper.getOutBoundInfoByUuid(req.getUuid());
+        if (ObjectUtil.isEmpty(callPhoneRes)) {
+            return AjaxResult.error("未同步到对应通话记录");
+        }
+        tenantDataSourceManager.ensureSwitchByTenantId(SecurityUtils.getTenantId());
+        int rows;
+        if (req.getWorkflowInstanceId() != null && req.getRoboticId() != null) {
+            // 工作流外呼保存逻辑
+            rows = aiSipCallOutboundCdrService.syncByUuid(req,callPhoneRes);
+        } else {
+            // 用户手动外呼保存逻辑
+            if (req.getCustomerId() == null) {
+                return AjaxResult.error("客户ID不能为空");
+            }
+            rows = aiSipCallOutboundCdrService.syncCrmCustomerCallLogByUuid(req,callPhoneRes);
+        }
         if (rows > 0) {
             return AjaxResult.success("同步成功");
         }

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -149,6 +150,11 @@ public class AiSipCallUserController extends BaseController
     public AjaxResult getToolbarBasicParam(@RequestBody Map<String,String> param)
     {
         String extNum = param.get("extNum");
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String ids = aiSipCallUserService.getGateWayIdListByCompanyId(loginUser.getCompany().getCompanyId());
+        if(StringUtils.isNotBlank(ids)){
+            param.put("myGateway",ids);
+        }
         if(extNum == null){
             return AjaxResult.error("分机号参数缺失");
         }

+ 234 - 0
fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java

@@ -0,0 +1,234 @@
+package com.fs.company.controller.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 录音文件代理接口
+ * 解决 HTTPS 页面下无法直接访问 HTTP 录音资源的问题
+ */
+@RestController
+public class RecordingProxyController {
+
+    private static final Logger log = LoggerFactory.getLogger(RecordingProxyController.class);
+
+    /**
+     * 允许代理的录音服务器地址白名单
+     */
+    private static final List<String> ALLOWED_HOSTS = Arrays.asList(
+            "129.28.164.235:8899"
+    );
+
+    /**
+     * 允许代理的路径前缀
+     */
+    private static final String ALLOWED_PATH_PREFIX = "/recordings/";
+
+    /**
+     * 流式缓冲区大小
+     */
+    private static final int BUFFER_SIZE = 4096;
+
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private static final int CONNECT_TIMEOUT = 5000;
+
+    /**
+     * 读取超时时间(毫秒)
+     */
+    private static final int READ_TIMEOUT = 30000;
+
+    /**
+     * 录音文件代理接口
+     * 前端通过此接口请求录音文件,后端转发到录音服务器获取文件流
+     *
+     * @param url      录音文件的原始 HTTP 地址
+     * @param request  HTTP 请求
+     * @param response HTTP 响应
+     */
+    @GetMapping("/common/proxy/recording")
+    public void proxyRecording(@RequestParam("url") String url,
+                               HttpServletRequest request,
+                               HttpServletResponse response) {
+        // 1. 安全校验
+        if (!isUrlAllowed(url)) {
+            log.warn("录音代理请求被拒绝,非法地址: {}", url);
+            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+            try {
+                response.getWriter().write("Access denied: URL not allowed");
+            } catch (IOException e) {
+                log.error("写入错误响应失败", e);
+            }
+            return;
+        }
+
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 2. 建立到录音服务器的连接
+            URL targetUrl = new URL(url);
+            connection = (HttpURLConnection) targetUrl.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(CONNECT_TIMEOUT);
+            connection.setReadTimeout(READ_TIMEOUT);
+            connection.setInstanceFollowRedirects(true);
+
+            // 3. 转发 Range 请求头(支持音频播放器拖动进度条)
+            String rangeHeader = request.getHeader("Range");
+            if (rangeHeader != null && !rangeHeader.isEmpty()) {
+                connection.setRequestProperty("Range", rangeHeader);
+            }
+
+            // 4. 发起请求
+            int responseCode = connection.getResponseCode();
+
+            // 5. 处理错误响应
+            if (responseCode >= 400) {
+                log.error("录音服务器返回错误状态码: {}, url: {}", responseCode, url);
+                response.setStatus(responseCode);
+                return;
+            }
+
+            // 6. 设置响应状态码(200 或 206)
+            response.setStatus(responseCode);
+
+            // 7. 设置 Content-Type
+            String contentType = connection.getContentType();
+            if (contentType != null && !contentType.isEmpty()) {
+                response.setContentType(contentType);
+            } else {
+                // 根据文件扩展名推断 Content-Type
+                response.setContentType(guessContentType(url));
+            }
+
+            // 8. 转发关键响应头
+            String contentLength = connection.getHeaderField("Content-Length");
+            if (contentLength != null) {
+                response.setHeader("Content-Length", contentLength);
+            }
+
+            String contentRange = connection.getHeaderField("Content-Range");
+            if (contentRange != null) {
+                response.setHeader("Content-Range", contentRange);
+            }
+
+            String acceptRanges = connection.getHeaderField("Accept-Ranges");
+            if (acceptRanges != null) {
+                response.setHeader("Accept-Ranges", acceptRanges);
+            } else {
+                response.setHeader("Accept-Ranges", "bytes");
+            }
+
+            // 9. 流式传输文件内容
+            inputStream = connection.getInputStream();
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+            outputStream.flush();
+
+        } catch (IOException e) {
+            log.error("录音文件代理请求失败, url: {}", url, e);
+            if (!response.isCommitted()) {
+                response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
+            }
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输入流失败", e);
+                }
+            }
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输出流失败", e);
+                }
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 校验 URL 是否在允许的白名单范围内
+     *
+     * @param url 待校验的 URL
+     * @return 是否允许代理
+     */
+    private boolean isUrlAllowed(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+
+        // 必须是 http 协议
+        if (!url.startsWith("http://") && !url.startsWith("https://")) {
+            return false;
+        }
+
+        try {
+            URL parsedUrl = new URL(url);
+            String host = parsedUrl.getHost();
+            int port = parsedUrl.getPort();
+            String hostWithPort = port > 0 ? host + ":" + port : host;
+            String path = parsedUrl.getPath();
+
+            // 校验主机地址是否在白名单内
+            boolean hostAllowed = ALLOWED_HOSTS.contains(hostWithPort);
+
+            // 校验路径是否包含允许的前缀
+            boolean pathAllowed = path != null && path.startsWith(ALLOWED_PATH_PREFIX);
+
+            return hostAllowed && pathAllowed;
+        } catch (Exception e) {
+            log.warn("URL 解析失败: {}", url, e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据文件扩展名推断 Content-Type
+     *
+     * @param url 文件 URL
+     * @return Content-Type
+     */
+    private String guessContentType(String url) {
+        if (url == null) {
+            return "application/octet-stream";
+        }
+        String lowerUrl = url.toLowerCase();
+        if (lowerUrl.contains(".wav")) {
+            return "audio/wav";
+        } else if (lowerUrl.contains(".mp3")) {
+            return "audio/mpeg";
+        } else if (lowerUrl.contains(".ogg")) {
+            return "audio/ogg";
+        } else if (lowerUrl.contains(".flac")) {
+            return "audio/flac";
+        } else if (lowerUrl.contains(".m4a") || lowerUrl.contains(".aac")) {
+            return "audio/aac";
+        }
+        return "application/octet-stream";
+    }
+}

+ 70 - 6
fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java

@@ -1,29 +1,32 @@
 package com.fs.company.controller.company;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.aicall.service.ICompanyBindAiModelService;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.CompanyInboundBind;
 import com.fs.company.domain.EasyCallInboundCdrVO;
 import com.fs.company.mapper.CompanyInboundBindMapper;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyVoiceCloneRefMapper;
 import com.fs.company.mapper.EasyCallInboundLlmMapper;
 import com.fs.company.service.ICompanyInboundCallManageService;
 import com.fs.company.vo.easycall.*;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -49,6 +52,17 @@ public class CompanyInboundCallManageController extends BaseController {
 
     @Autowired
     private ICompanyBindAiModelService companyBindAiModelService;
+    @Autowired
+    private CompanyVoiceCloneRefMapper companyVoiceCloneRefMapper;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    CompanyMapper companyMapper;
+
+    @Autowired
+    TenantDataSourceManager tenantDataSourceManager;
 
     /**
      * 查询呼入大模型配置列表
@@ -63,6 +77,7 @@ public class CompanyInboundCallManageController extends BaseController {
         }
         List<Long> ids = companyInboundBinds.stream().map(companyInboundBind -> companyInboundBind.getInboundLlmAccountId()).collect(Collectors.toList());
         vo.setVisibleIds(ids);
+        vo.setFsTenantId(SecurityUtils.getTenantId());
         startPage();
         List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(vo);
         TableDataInfo rspData = getDataTable(list);
@@ -112,9 +127,24 @@ public class CompanyInboundCallManageController extends BaseController {
     @Log(title = "呼入大模型配置", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody EasyCallInboundLlmVO vo) {
+        Boolean b = inboundCallManageService.checkCalleeInboundLlm(vo.getCallee());
+        if(b){
+            throw new RuntimeException("被叫号码已存在,不能重复插入");
+        }
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         vo.setCompanyId(loginUser.getUser().getCompanyId());
-        return toAjax(inboundCallManageService.insertInboundLlm(vo));
+        vo.setFsTenantId(SecurityUtils.getTenantId());
+        tenantDataSourceManager.ensureSwitchByTenantId(vo.getFsTenantId());
+        int i = inboundCallManageService.insertInboundLlm(vo);
+        tenantDataSourceManager.ensureSwitchByTenantId(vo.getFsTenantId());
+        if(i >0 && vo.getId()!= null) {
+            CompanyInboundBind bind = new CompanyInboundBind();
+            bind.setInboundLlmAccountId(Long.valueOf(vo.getId()));
+            bind.setCompanyId(vo.getCompanyId());
+            bind.setCreateTime(new Date());
+            companyInboundBindMapper.insertCompanyInboundBind(bind);
+        }
+        return toAjax(true);
     }
 
     /**
@@ -207,8 +237,23 @@ public class CompanyInboundCallManageController extends BaseController {
      */
     @GetMapping("/voiceList")
     public AjaxResult getVoiceList(@RequestParam("voiceSource") String voiceSource) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<Long> ttsIds = companyVoiceCloneRefMapper.selectByCompanyIdAndCompanyUserId(companyId, loginUser.getCompany().getUserId());
         List<EasyCallVoiceCodeVO> list = inboundLlmMapper.selectVoiceListBySource(voiceSource);
-        return AjaxResult.success(list);
+        List<EasyCallVoiceCodeVO> result = list.stream()
+                .filter(item ->
+                        item.getPriority() == 1 || (item.getPriority() == 0 && ttsIds.contains(item.getId()))
+                )
+                .map(item -> {
+                    EasyCallVoiceCodeVO vo = new EasyCallVoiceCodeVO();
+                    vo.setVoiceCode(item.getVoiceCode());
+                    vo.setVoiceName(item.getVoiceName());
+                    vo.setVoiceSource(item.getVoiceSource());
+                    return vo;
+                })
+                .collect(Collectors.toList());
+        return AjaxResult.success(result);
     }
 
     /**
@@ -225,7 +270,25 @@ public class CompanyInboundCallManageController extends BaseController {
      */
     @GetMapping("/gatewayList")
     public AjaxResult getGatewayList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        String gateWayList = companyMapper.getGateWayList(companyId);
+        String json = configService.selectConfigByKey("cId.config");
         List<EasyCallGatewayVO> list = inboundLlmMapper.selectOutboundGatewayList();
+        if (null != companyId && null != list && !list.isEmpty()) {
+            if(StringUtils.isNotBlank(gateWayList)){
+                List<Long> collect = Arrays.stream(gateWayList.split(",")).map(item -> Long.valueOf(item.trim())).collect(Collectors.toList());
+                list = list.stream().filter(item -> collect.contains(item.getId())).collect(Collectors.toList());
+            }else{
+                if (StringUtils.isNotBlank(json)) {
+                    JSONObject obj = JSONObject.parseObject(json);
+                    if(null != obj && obj.containsKey("showGatewayIds")){
+                        List<Long> showGatewayIds = obj.getJSONArray("showGatewayIds").stream().map(item -> Long.valueOf(item.toString())).collect(Collectors.toList());
+                        list = list.stream().filter(item -> showGatewayIds.contains(item.getId())).collect(Collectors.toList());
+                    }
+                }
+            }
+        }
         return AjaxResult.success(list);
     }
 
@@ -261,6 +324,7 @@ public class CompanyInboundCallManageController extends BaseController {
         // 通过llmAccountIds查询对应的callee列表
         EasyCallInboundLlmVO query = new EasyCallInboundLlmVO();
         query.setVisibleIds(llmAccountIds);
+        query.setFsTenantId(SecurityUtils.getTenantId());
         List<EasyCallInboundLlmVO> llmConfigs = inboundCallManageService.selectInboundLlmList(query);
         List<String> calleeList = llmConfigs.stream()
                 .map(EasyCallInboundLlmVO::getCallee)

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -22,6 +22,7 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
@@ -351,4 +352,9 @@ public class CompanyVoiceRoboticController extends BaseController
             }
         });
     }
+
+    @PostMapping("/pauseRoboticActive")
+    public R pauseRoboticActive(@RequestBody PauseRoboticActiveParam param){
+        return companyVoiceRoboticService.pauseRoboticActive(param);
+    }
 }

+ 18 - 11
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java

@@ -6,10 +6,13 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyWxAccount;
 import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -28,11 +31,13 @@ public class CompanyWxAccountController extends BaseController
 {
     @Autowired
     private ICompanyWxAccountService companyWxAccountService;
+    @Autowired
+    private TokenService tokenService;
 
     /**
      * 查询企微账号列表
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:list')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyWxAccount companyWxEnterpriseAccount)
     {
@@ -43,7 +48,7 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 查询企微账号列表
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:list')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyWxAccount companyWxEnterpriseAccount){
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
@@ -53,7 +58,7 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 导出企微账号列表
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:export')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:export')")
     @Log(title = "企微账号", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(CompanyWxAccount companyWxEnterpriseAccount)
@@ -66,7 +71,7 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 获取企微账号详细信息
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:query')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:query')")
     @GetMapping(value = "/{id}")
     public AjaxResult getInfo(@PathVariable("id") Long id)
     {
@@ -76,18 +81,20 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 新增企微账号
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:add')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:add')")
     @Log(title = "企微账号", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody CompanyWxAccount companyWxEnterpriseAccount)
-    {
-        return toAjax(companyWxAccountService.insertCompanyWxAccount(companyWxEnterpriseAccount));
+    public AjaxResult add(@RequestBody CompanyWxAccount account){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        account.setCompanyId(loginUser.getCompany().getCompanyId());
+        account.setCompanyUserId(loginUser.getUser().getUserId());
+        return toAjax(companyWxAccountService.insertCompanyWxAccount(account));
     }
 
     /**
      * 修改企微账号
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:edit')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:edit')")
     @Log(title = "企微账号", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody CompanyWxAccount companyWxEnterpriseAccount)
@@ -98,7 +105,7 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 删除企微账号
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:remove')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:remove')")
     @Log(title = "企微账号", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
@@ -108,7 +115,7 @@ public class CompanyWxAccountController extends BaseController
     /**
      * 删除企微账号
      */
-    @PreAuthorize("@ss.hasPermi('company:companyAccount:list')")
+    @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
 	@GetMapping("/companyListAll")
     public R companyListAll(){
         return R.ok().put("data", companyWxAccountService.companyListAllCompany(new CompanyUser()));

+ 38 - 0
fs-company/src/main/java/com/fs/company/controller/company/CrmCustomerCallLogController.java

@@ -0,0 +1,38 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.domain.CrmCustomerCallLog;
+import com.fs.company.service.ICrmCustomerCallLogService;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 客户通话记录Controller
+ *
+ * @author fs
+ * @date 2026-05-10
+ */
+@RestController
+@RequestMapping("/company/crmCustomerCallLog")
+public class CrmCustomerCallLogController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerCallLogService crmCustomerCallLogService;
+
+    /**
+     * 查询客户通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customerCallLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerCallLog crmCustomerCallLog) {
+        startPage();
+        List<CrmCustomerCallLog> list = crmCustomerCallLogService.selectCrmCustomerCallLogList(crmCustomerCallLog);
+        return getDataTable(list);
+    }
+}

+ 11 - 1
fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyTagTemplateBindingController.java

@@ -123,6 +123,16 @@ public class CompanyTagTemplateBindingController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         return tagTemplateBindingService.batchBindLobsterTag(
                 loginUser.getCompany().getCompanyId(), loginUser.getUsername(),
-                param.getQwCorpId(), param.getUserIds(), param.getTagCodes(),loginUser.getUser().getUserId());
+                param.getQwCorpId(), param.getUserIds(), param.getTagCodes(),loginUser.getUser().getUserId(),
+                param.getExternalUserId());
     }
+    /**
+     * 企微客户获取龙虾标签
+     */
+    @PostMapping("/lobsterTags")
+    public AjaxResult lobsterTags(@RequestBody List<Long> userIds) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return tagTemplateBindingService.lobsterTags(userIds,loginUser.getUser().getUserId(),loginUser.getCompany().getCompanyId());
+    }
+
 }

+ 9 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -161,6 +161,15 @@ public class CrmCustomerController extends BaseController
 
     }
 
+    @ApiOperation("获取我的客户手机号")
+    @PreAuthorize("@ss.hasPermi('crm:customer:myList')")
+    @GetMapping("/getMyCustomerPhone")
+    public R getMyCustomerPhone(@RequestParam("customerId") Long customerId){
+        String phone = crmCustomerService.selectCrmCustomerPhoneByCustomerId(customerId);
+        return R.ok(phone);
+
+    }
+
     @ApiOperation("获取客户列表")
     @GetMapping("/getCustomerList")
     @PreAuthorize("@ss.hasPermi('crm:customer:list')")

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -119,6 +119,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/common/getId**").anonymous()
                 .antMatchers("/common/uploadOSS**").anonymous()
+                .antMatchers("/common/proxy/recording").anonymous()
                 .antMatchers("/company/user/common/uploadOSS").anonymous()
                 .antMatchers("/pay/wxPay/payNotify**").anonymous()
                 .antMatchers("/common/uploadWang**").anonymous()

+ 49 - 22
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -18,10 +18,7 @@ import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.framework.aspectj.SopTenantDataSourceAspect;
 import com.fs.ipad.vo.BaseVo;
-import com.fs.qw.domain.QwIpadServer;
-import com.fs.qw.domain.QwPushCount;
-import com.fs.qw.domain.QwRestrictionPushRecord;
-import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwIpadServerMapper;
 import com.fs.qw.mapper.QwPushCountMapper;
 import com.fs.qw.mapper.QwRestrictionPushRecordMapper;
@@ -29,6 +26,7 @@ import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.impl.AsyncSopTestService;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopTempSetting;
+import com.fs.qwApi.Result.QwGetMomentTaskGResult;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.service.IQwSopLogsService;
@@ -425,36 +423,65 @@ public class SendMsg {
         log.info("销售执行完成:{}, 耗时:{}", user.getQwUserName(), end3 - start3);
     }
 
-    @Scheduled(fixedDelay = 10000*60*30) // 每30min执行一次
+    private static final Integer LOBSTER_EXECUTE_STATUS = 0;
+    @Scheduled(fixedDelay = 1000*60
+            *30
+    ) // 每30mIn执行一次
     public void sendLobsterQwMsg(){
         log.info("开始龙虾发送企微消息");
+//        sopTenantDataSourceAspect.switchTenant(tenantId);
         List<CompanyWorkflowLobsterTask> companyWorkflowLobsterTasks = companyWorkflowLobsterTaskMapper.selectList(new LambdaQueryWrapper<CompanyWorkflowLobsterTask>().eq(CompanyWorkflowLobsterTask::getDelFlag, 0)
-                .between(CompanyWorkflowLobsterTask::getSendTime, LocalDateTime.now().minusMinutes(30), LocalDateTime.now())
+                .between(CompanyWorkflowLobsterTask::getSendTime, LocalDateTime.now().minusMinutes(30), LocalDateTime.now()
+                        ).eq(CompanyWorkflowLobsterTask::getExecuteStatus, LOBSTER_EXECUTE_STATUS)
         );
+        log.info("开始龙虾发送企微消息,数量:{}",companyWorkflowLobsterTasks.size());
+        if (companyWorkflowLobsterTasks.isEmpty()){
+            log.info("龙虾没有需要发送的qw消息");
+            return;
+        }
         for (CompanyWorkflowLobsterTask task : companyWorkflowLobsterTasks){
-            QwUser qwUser = qwUserMapper.selectQwUserById(task.getQwUserId());
-            WxLoginResp login = isLogin(qwUser.getUid(), qwUser.getServerId());
-            WxWorkSendTextMsgDTO dto = new WxWorkSendTextMsgDTO();
+            QwUser qwUser = qwUserMapper.selectById(task.getQwUserId());
+
+            //只拼一个发消息的逻辑
+            QwSopCourseFinishTempSetting.Setting content = new QwSopCourseFinishTempSetting.Setting();
+            content.setValue(task.getTaskContent());
+            content.setContentType("1");
+
+            QwSopLogs qwSopLogs = new QwSopLogs();
+            qwSopLogs.setId(String.valueOf(task.getId()));
+            qwSopLogs.setSendType(1);
+            qwSopLogs.setExternalUserId(task.getExternalUserId());
+
             BaseVo vo = new BaseVo();
-            vo.setUuid(qwUser.getUid());
+            vo.setCorpCode(qwUser.getCorpId());
             vo.setCorpId(qwUser.getCorpId());
-            vo.setCorpCode(login.getUser_info().getObject().getScorp_id());
-            vo.setServerId(qwUser.getServerId());
-            dto.setUuid(qwUser.getUid());
-            dto.setSend_userid(userIds(vo));//目前没接群聊
-            dto.setContent(task.getTaskContent());
-            dto.setIsRoom(false);
-            WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(dto, qwUser.getServerId());
+            // 判断这个企微是否需要发送
+            if (!sendServer.isSend(qwUser, vo)) {
+                log.info("当前这个企微不需要发送 数据{}",qwUser);
+                return;
+            }
 
-            log.info("开始发送企微消息,内容:{}", wxWorkSendTextMsgRespDTOWxWorkResponseDTO);
-            //todo 发企微,
-            if (wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getErrcode()==0){
+            sendServer.send(content, qwUser, qwSopLogs, null, vo);
+//            WxLoginResp login = isLogin(qwUser.getUid(), qwUser.getServerId());
+//            WxWorkSendTextMsgDTO dto = new WxWorkSendTextMsgDTO();
+//            vo.setUuid(qwUser.getUid());
+//            vo.setCorpId(qwUser.getCorpId());
+//            vo.setCorpCode(login.getUser_info().getObject().getScorp_id());
+//            vo.setServerId(qwUser.getServerId());
+//            dto.setUuid(qwUser.getUid());
+//            dto.setSend_userid(userIds(vo));//目前没接群聊
+//            dto.setContent(task.getTaskContent());
+//            dto.setIsRoom(false);
+//            WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(dto, qwUser.getServerId());
+            if (content.getSendStatus() != 2) {
+                //todo 发送成功记录日志 insert日志表!!!
                 task.setExecuteStatus(2);
-                log.info("企微发送成功:{}", task.getId());
             }else {
                 task.setExecuteStatus(3);
-                log.info("企微发送失败:{}", task.getId());
+
             }
+            log.info("开始发送企微消息,内容:{}",content );
+
         }
         companyWorkflowLobsterTaskMapper.updateTaskListExecuteStatus(companyWorkflowLobsterTasks);
 

+ 18 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantClearPayloadParam.java

@@ -0,0 +1,18 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantClearPayloadParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<Long> points;
+
+    private QdrantFilter filter;
+
+    private Boolean wait;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantCountParam.java

@@ -0,0 +1,13 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QdrantCountParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private QdrantFilter filter;
+}

+ 23 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantCreateCollectionParam.java

@@ -0,0 +1,23 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QdrantCreateCollectionParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private QdrantVectorsConfig vectors;
+
+    @Data
+    public static class QdrantVectorsConfig implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Integer size;
+
+        private String distance;
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantDeletePayloadParam.java

@@ -0,0 +1,20 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantDeletePayloadParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<String> keys;
+
+    private List<Long> points;
+
+    private QdrantFilter filter;
+
+    private Boolean wait;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantDeletePointsParam.java

@@ -0,0 +1,18 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantDeletePointsParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<Long> points;
+
+    private QdrantFilter filter;
+
+    private Boolean wait;
+}

+ 71 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantFilter.java

@@ -0,0 +1,71 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class QdrantFilter implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<QdrantCondition> must;
+
+    private List<QdrantCondition> should;
+
+    private List<QdrantCondition> must_not;
+
+    @Data
+    public static class QdrantCondition implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private String key;
+
+        private QdrantMatch match;
+
+        private QdrantRange range;
+
+        private List<Long> has_id;
+
+        private QdrantValuesCount values_count;
+    }
+
+    @Data
+    public static class QdrantMatch implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Object value;
+
+        private String keyword;
+
+        private String text;
+    }
+
+    @Data
+    public static class QdrantRange implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Long gte;
+
+        private Long lte;
+
+        private Long gt;
+
+        private Long lt;
+    }
+
+    @Data
+    public static class QdrantValuesCount implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Integer gte;
+
+        private Integer lte;
+    }
+}

+ 21 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantScrollParam.java

@@ -0,0 +1,21 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QdrantScrollParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer limit;
+
+    private Long offset;
+
+    private Boolean with_payload;
+
+    private Boolean with_vector;
+
+    private QdrantFilter filter;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantSearchGroupsParam.java

@@ -0,0 +1,26 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantSearchGroupsParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<Float> vector;
+
+    private Integer limit;
+
+    private String group_by;
+
+    private Integer group_size;
+
+    private Boolean with_payload;
+
+    private Boolean with_vector;
+
+    private QdrantFilter filter;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantSearchParam.java

@@ -0,0 +1,34 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantSearchParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<Float> vector;
+
+    private Integer limit;
+
+    private Boolean with_payload;
+
+    private Boolean with_vector;
+
+    private QdrantFilter filter;
+
+    private QdrantSearchParams params;
+
+    @Data
+    public static class QdrantSearchParams implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Integer hnsw_ef;
+
+        private Boolean exact;
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantSetPayloadParam.java

@@ -0,0 +1,20 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantSetPayloadParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Object payload;
+
+    private List<Long> points;
+
+    private QdrantFilter filter;
+
+    private Boolean wait;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantUpdateCollectionParam.java

@@ -0,0 +1,16 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+@Data
+public class QdrantUpdateCollectionParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Map<String, Object> optimizers_config;
+
+    private Map<String, Object> params;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantUpdateVectorsParam.java

@@ -0,0 +1,26 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantUpdateVectorsParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<QdrantVectorPoint> points;
+
+    private Boolean wait;
+
+    @Data
+    public static class QdrantVectorPoint implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Long id;
+
+        private List<Float> vector;
+    }
+}

+ 28 - 0
fs-service/src/main/java/com/fs/ai/param/QdrantUpsertPointsParam.java

@@ -0,0 +1,28 @@
+package com.fs.ai.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class QdrantUpsertPointsParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private List<QdrantPointUpsert> points;
+
+    private Boolean wait;
+
+    @Data
+    public static class QdrantPointUpsert implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private Long id;
+
+        private List<Float> vector;
+
+        private Object payload;
+    }
+}

+ 379 - 0
fs-service/src/main/java/com/fs/ai/util/QdrantClientUtil.java

@@ -0,0 +1,379 @@
+package com.fs.ai.util;
+
+import cn.hutool.core.lang.TypeReference;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONUtil;
+import com.fs.ai.param.*;
+import com.fs.ai.vo.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Qdrant 向量数据库 REST API 工具类
+ * <p>
+ * 基于 Qdrant v1.17.1 REST API,封装 Collection 管理、Point 管理、向量搜索等接口。
+ * </p>
+ *
+ * <pre>
+ * 使用示例:
+ *   QdrantClientUtil client = new QdrantClientUtil("saaschroma.ylrzcloud.com", "saasai_3");
+ *
+ *   // 获取服务状态
+ *   String status = client.getServiceStatus();
+ *
+ *   // 获取 Collection 列表
+ *   List<String> collections = client.listCollections();
+ *
+ *   // 向量搜索
+ *   List<QdrantPoint> results = client.search(List.of(0.01f, 0.02f, ...), 5, true);
+ * </pre>
+ */
+@Slf4j
+public class QdrantClientUtil {
+
+    private static final String HTTPS_PREFIX = "https://";
+
+    private final String baseUrl;
+
+    private final String collectionName;
+
+    public QdrantClientUtil(String host, String collectionName) {
+        this.baseUrl = HTTPS_PREFIX + host;
+        this.collectionName = collectionName;
+    }
+
+    public QdrantClientUtil(String host) {
+        this.baseUrl = HTTPS_PREFIX + host;
+        this.collectionName = null;
+    }
+
+    // ==================== 1. 基础信息 ====================
+
+    /**
+     * 获取服务状态
+     */
+    public String getServiceStatus() {
+        String url = baseUrl + "/";
+        return doGet(url);
+    }
+
+    // ==================== 2. Collection 管理 ====================
+
+    /**
+     * 获取所有 Collection 列表
+     */
+    @SuppressWarnings("unchecked")
+    public List<String> listCollections() {
+        String url = baseUrl + "/collections";
+        String resp = doGet(url);
+        QdrantBaseResponse<Map<String, Object>> baseResp = JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<Map<String, Object>>>() {}, false);
+        if (baseResp.getResult() != null && baseResp.getResult().get("collections") != null) {
+            List<Map<String, Object>> collections = (List<Map<String, Object>>) baseResp.getResult().get("collections");
+            return collections.stream().map(c -> (String) c.get("name")).collect(Collectors.toList());
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取 Collection 详情
+     */
+    public QdrantBaseResponse<QdrantCollectionInfo> getCollection(String collection) {
+        String url = baseUrl + "/collections/" + (collection != null ? collection : collectionName);
+        String resp = doGet(url);
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantCollectionInfo>>() {}, false);
+    }
+
+    /**
+     * 获取当前 Collection 详情
+     */
+    public QdrantBaseResponse<QdrantCollectionInfo> getCollection() {
+        return getCollection(collectionName);
+    }
+
+    /**
+     * 创建 Collection
+     */
+    public QdrantBaseResponse<Boolean> createCollection(String collection, QdrantCreateCollectionParam param) {
+        String url = baseUrl + "/collections/" + collection;
+        String resp = doPut(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp, new TypeReference<QdrantBaseResponse<Boolean>>() {}, false);
+    }
+
+    /**
+     * 删除 Collection
+     */
+    public QdrantBaseResponse<Boolean> deleteCollection(String collection) {
+        String url = baseUrl + "/collections/" + collection;
+        String resp = doDelete(url);
+        return JSONUtil.toBean(resp, new TypeReference<QdrantBaseResponse<Boolean>>() {}, false);
+    }
+
+    /**
+     * 更新 Collection 配置
+     */
+    public QdrantBaseResponse<Boolean> updateCollection(String collection, QdrantUpdateCollectionParam param) {
+        String url = baseUrl + "/collections/" + collection;
+        String resp = doPatch(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp, new TypeReference<QdrantBaseResponse<Boolean>>() {}, false);
+    }
+
+    // ==================== 3. Point 管理 ====================
+
+    /**
+     * 批量写入(Upsert)Points
+     */
+    public QdrantBaseResponse<QdrantOperationResult> upsertPoints(String collection, QdrantUpsertPointsParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points";
+        String resp = doPut(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 批量写入(Upsert)Points - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> upsertPoints(QdrantUpsertPointsParam param) {
+        return upsertPoints(collectionName, param);
+    }
+
+    /**
+     * 查询单个 Point
+     */
+    public QdrantBaseResponse<QdrantPoint> getPoint(String collection, Long id, boolean withPayload, boolean withVector) {
+        String url = baseUrl + "/collections/" + collection + "/points/" + id
+                + "?with_payload=" + withPayload + "&with_vector=" + withVector;
+        String resp = doGet(url);
+        return JSONUtil.toBean(resp, new TypeReference<QdrantBaseResponse<QdrantPoint>>() {}, false);
+    }
+
+    /**
+     * 滚动查询 Points(分页)
+     */
+    public QdrantBaseResponse<QdrantScrollResult> scrollPoints(String collection, QdrantScrollParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/scroll";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantScrollResult>>() {}, false);
+    }
+
+    /**
+     * 滚动查询 Points - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantScrollResult> scrollPoints(QdrantScrollParam param) {
+        return scrollPoints(collectionName, param);
+    }
+
+    /**
+     * 删除 Points
+     */
+    public QdrantBaseResponse<QdrantOperationResult> deletePoints(String collection, QdrantDeletePointsParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/delete";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 删除 Points - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> deletePoints(QdrantDeletePointsParam param) {
+        return deletePoints(collectionName, param);
+    }
+
+    /**
+     * 计数 Points
+     */
+    public QdrantBaseResponse<QdrantCountResult> countPoints(String collection, QdrantCountParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/count";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantCountResult>>() {}, false);
+    }
+
+    /**
+     * 计数 Points - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantCountResult> countPoints(QdrantCountParam param) {
+        return countPoints(collectionName, param);
+    }
+
+    /**
+     * 更新向量
+     */
+    public QdrantBaseResponse<QdrantOperationResult> updateVectors(String collection, QdrantUpdateVectorsParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/vectors";
+        String resp = doPut(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 更新向量 - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> updateVectors(QdrantUpdateVectorsParam param) {
+        return updateVectors(collectionName, param);
+    }
+
+    /**
+     * 设置 Payload
+     */
+    public QdrantBaseResponse<QdrantOperationResult> setPayload(String collection, QdrantSetPayloadParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/payload";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 设置 Payload - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> setPayload(QdrantSetPayloadParam param) {
+        return setPayload(collectionName, param);
+    }
+
+    /**
+     * 按 Key 删除 Payload
+     */
+    public QdrantBaseResponse<QdrantOperationResult> deletePayload(String collection, QdrantDeletePayloadParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/payload/delete";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 按 Key 删除 Payload - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> deletePayload(QdrantDeletePayloadParam param) {
+        return deletePayload(collectionName, param);
+    }
+
+    /**
+     * 清空所有 Payload
+     */
+    public QdrantBaseResponse<QdrantOperationResult> clearPayload(String collection, QdrantClearPayloadParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/payload/clear";
+        String resp = doPut(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantOperationResult>>() {}, false);
+    }
+
+    /**
+     * 清空所有 Payload - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantOperationResult> clearPayload(QdrantClearPayloadParam param) {
+        return clearPayload(collectionName, param);
+    }
+
+    // ==================== 4. 向量搜索 ====================
+
+    /**
+     * 向量相似度搜索
+     */
+    public QdrantBaseResponse<List<QdrantPoint>> search(String collection, QdrantSearchParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/search";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<List<QdrantPoint>>>() {}, false);
+    }
+
+    /**
+     * 向量相似度搜索 - 使用默认 Collection
+     */
+    public QdrantBaseResponse<List<QdrantPoint>> search(QdrantSearchParam param) {
+        return search(collectionName, param);
+    }
+
+    /**
+     * 分组搜索
+     */
+    public QdrantBaseResponse<QdrantSearchGroupsResult> searchGroups(String collection, QdrantSearchGroupsParam param) {
+        String url = baseUrl + "/collections/" + collection + "/points/search/groups";
+        String resp = doPost(url, JSONUtil.toJsonStr(param));
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantSearchGroupsResult>>() {}, false);
+    }
+
+    /**
+     * 分组搜索 - 使用默认 Collection
+     */
+    public QdrantBaseResponse<QdrantSearchGroupsResult> searchGroups(QdrantSearchGroupsParam param) {
+        return searchGroups(collectionName, param);
+    }
+
+    // ==================== 5. Cluster 管理 ====================
+
+    /**
+     * 获取 Collection 集群信息
+     */
+    public QdrantBaseResponse<QdrantClusterInfoResult> getClusterInfo(String collection) {
+        String url = baseUrl + "/collections/" + collection + "/cluster";
+        String resp = doGet(url);
+        return JSONUtil.toBean(resp,
+                new TypeReference<QdrantBaseResponse<QdrantClusterInfoResult>>() {}, false);
+    }
+
+    /**
+     * 获取当前 Collection 集群信息
+     */
+    public QdrantBaseResponse<QdrantClusterInfoResult> getClusterInfo() {
+        return getClusterInfo(collectionName);
+    }
+
+    // ==================== HTTP 请求封装 ====================
+
+    private String doGet(String url) {
+        log.debug("Qdrant GET: {}", url);
+        try (HttpResponse response = HttpRequest.get(url)
+                .contentType("application/json")
+                .execute()) {
+            return response.body();
+        }
+    }
+
+    private String doPost(String url, String body) {
+        log.debug("Qdrant POST: {}, body: {}", url, body);
+        try (HttpResponse response = HttpRequest.post(url)
+                .contentType("application/json")
+                .body(body)
+                .execute()) {
+            return response.body();
+        }
+    }
+
+    private String doPut(String url, String body) {
+        log.debug("Qdrant PUT: {}, body: {}", url, body);
+        try (HttpResponse response = HttpRequest.put(url)
+                .contentType("application/json")
+                .body(body)
+                .execute()) {
+            return response.body();
+        }
+    }
+
+    private String doDelete(String url) {
+        log.debug("Qdrant DELETE: {}", url);
+        try (HttpResponse response = HttpRequest.delete(url)
+                .contentType("application/json")
+                .execute()) {
+            return response.body();
+        }
+    }
+
+    private String doPatch(String url, String body) {
+        log.debug("Qdrant PATCH: {}, body: {}", url, body);
+        try (HttpResponse response = HttpRequest.patch(url)
+                .contentType("application/json")
+                .body(body)
+                .execute()) {
+            return response.body();
+        }
+    }
+}

+ 15 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantBaseResponse.java

@@ -0,0 +1,15 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class QdrantBaseResponse<T> {
+
+    private T result;
+
+    private String status;
+
+    private Double time;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantClusterInfoResult.java

@@ -0,0 +1,20 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class QdrantClusterInfoResult {
+
+    private Long peer_id;
+
+    private Integer shard_count;
+
+    private List<Map<String, Object>> local_shards;
+
+    private List<Map<String, Object>> remote_shards;
+
+    private List<Map<String, Object>> shard_transfers;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantCollectionInfo.java

@@ -0,0 +1,25 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class QdrantCollectionInfo {
+
+    private String status;
+
+    private String optimizer_status;
+
+    private Long indexed_vectors_count;
+
+    private Long points_count;
+
+    private Long segments_count;
+
+    private Map<String, Object> config;
+
+    private Map<String, Object> payload_schema;
+
+    private Map<String, Object> update_queue;
+}

+ 9 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantCountResult.java

@@ -0,0 +1,9 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+@Data
+public class QdrantCountResult {
+
+    private Long count;
+}

+ 11 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantOperationResult.java

@@ -0,0 +1,11 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+@Data
+public class QdrantOperationResult {
+
+    private Long operation_id;
+
+    private String status;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantPoint.java

@@ -0,0 +1,20 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class QdrantPoint {
+
+    private Long id;
+
+    private Long version;
+
+    private Double score;
+
+    private Map<String, Object> payload;
+
+    private List<Float> vector;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantScrollResult.java

@@ -0,0 +1,13 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class QdrantScrollResult {
+
+    private List<QdrantPoint> points;
+
+    private Long next_page_offset;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/ai/vo/QdrantSearchGroupsResult.java

@@ -0,0 +1,19 @@
+package com.fs.ai.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class QdrantSearchGroupsResult {
+
+    private List<QdrantSearchGroup> groups;
+}
+
+@Data
+class QdrantSearchGroup {
+
+    private Object id;
+
+    private List<QdrantPoint> hits;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordByUuidQueryParams.java

@@ -29,5 +29,7 @@ public class ApiCallRecordByUuidQueryParams implements Serializable {
 
     private String intent;
 
+    private Long customerId;
+
 
 }

+ 11 - 8
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java

@@ -5,20 +5,21 @@ import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
 import com.fs.aiSipCall.domain.CcCustInfo;
 import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
 /**
  * aiSIP手动外呼通话记录Service接口
- * 
+ *
  * @author fs
  * @date 2026-03-19
  */
 public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutboundCdr>{
     /**
      * 查询aiSIP手动外呼通话记录
-     * 
+     *
      * @param id aiSIP手动外呼通话记录主键
      * @return aiSIP手动外呼通话记录
      */
@@ -26,7 +27,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     /**
      * 查询aiSIP手动外呼通话记录列表
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return aiSIP手动外呼通话记录集合
      */
@@ -34,7 +35,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     /**
      * 新增aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -42,7 +43,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     /**
      * 修改aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -50,7 +51,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     /**
      * 批量删除aiSIP手动外呼通话记录
-     * 
+     *
      * @param ids 需要删除的aiSIP手动外呼通话记录主键集合
      * @return 结果
      */
@@ -58,7 +59,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     /**
      * 删除aiSIP手动外呼通话记录信息
-     * 
+     *
      * @param id aiSIP手动外呼通话记录主键
      * @return 结果
      */
@@ -70,5 +71,7 @@ public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutbound
 
     CompletableFuture<String> scheduledGetCallRecord();
 
-    int syncByUuid(ApiCallRecordByUuidQueryParams req);
+    int syncByUuid(ApiCallRecordByUuidQueryParams req, EasyCallOutBoundVO callPhoneRes);
+
+    int syncCrmCustomerCallLogByUuid(ApiCallRecordByUuidQueryParams req,EasyCallOutBoundVO callPhoneRes);
 }

+ 7 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java

@@ -71,4 +71,11 @@ public interface IAiSipCallUserService extends IService<AiSipCallUser>{
      * @return AjaxResult 结果
      */
     AjaxResult getToolbarBasicParam(Map<String,String> param);
+
+    /**
+     * 根据公司找到绑定网关
+     * @param companyId
+     * @return
+     */
+    String getGateWayIdListByCompanyId(Long companyId);
 }

+ 78 - 22
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java

@@ -18,12 +18,10 @@ import com.fs.aiSipCall.utils.DateUtils;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.CompanyAiWorkflowExec;
+import com.fs.company.domain.CrmCustomerCallLog;
 import com.fs.company.domain.CompanyVoiceRoboticBusiness;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
-import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
 import com.fs.company.vo.easycall.EasyCallOutBoundVO;
@@ -45,7 +43,7 @@ import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORK
 
 /**
  * aiSIP手动外呼通话记录Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-03-19
  */
@@ -74,6 +72,10 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private CrmCustomerCallLogMapper crmCustomerCallLogMapper;
+
+
     private static final BigDecimal DEFAULT_CALL_CHARGE = new BigDecimal("0.12");
     private static final BigDecimal ONE_MINUTES_SECOND = new BigDecimal("60");
 
@@ -85,7 +87,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     }
     /**
      * 查询aiSIP手动外呼通话记录列表
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return aiSIP手动外呼通话记录
      */
@@ -122,7 +124,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 新增aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -134,7 +136,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 修改aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -146,7 +148,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 批量删除aiSIP手动外呼通话记录
-     * 
+     *
      * @param ids 需要删除的aiSIP手动外呼通话记录主键
      * @return 结果
      */
@@ -158,7 +160,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 删除aiSIP手动外呼通话记录信息
-     * 
+     *
      * @param id aiSIP手动外呼通话记录主键
      * @return 结果
      */
@@ -483,15 +485,8 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     }
 
     @Override
-    public int syncByUuid(ApiCallRecordByUuidQueryParams req) {
-        if (req == null || StringUtils.isBlank(req.getUuid())) {
-//            throw new ServiceException("uuid不能为空");
-        }
-
-        String callType = StringUtils.isBlank(req.getCallType()) ? "03" : req.getCallType();
-
-//        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(req.getUuid());
-        EasyCallOutBoundVO callPhoneRes = easyCallMapper.getOutBoundInfoByUuid(req.getUuid());
+    public int syncByUuid(ApiCallRecordByUuidQueryParams req,EasyCallOutBoundVO callPhoneRes) {
+        String callType = org.apache.commons.lang3.StringUtils.isBlank(req.getCallType()) ? "03" : req.getCallType();
         String callBackUuid = UUID.randomUUID().toString();
         CompanyAiWorkflowExec record = currentExecutionMapper.selectByWorkflowInstanceId(req.getWorkflowInstanceId());
         CompanyVoiceRoboticBusiness business = companyVoiceRoboticBusinessMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
@@ -542,9 +537,14 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
             callCharge = DEFAULT_CALL_CHARGE;
         }
         //向上取整分钟数
-        BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLogCallphone.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
-        BigDecimal multiply = divide.multiply(callCharge);
-        companyVoiceRoboticCallLogCallphone.setCost(multiply);
+        Long callTime = companyVoiceRoboticCallLogCallphone.getCallTime();
+        if (callTime != null) {
+            // 毫秒转秒
+            BigDecimal callTimeSecond = new BigDecimal(companyVoiceRoboticCallLogCallphone.getCallTime()).divide(new BigDecimal(1000), 0, RoundingMode.CEILING);
+            BigDecimal divide = callTimeSecond.divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+            BigDecimal multiply = divide.multiply(callCharge);
+            companyVoiceRoboticCallLogCallphone.setCost(multiply);
+        }
 
 
         int i = companyVoiceRoboticCallLogCallphoneMapper.insertCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLogCallphone);
@@ -561,4 +561,60 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     }
 
 
+
+    @Override
+    public int syncCrmCustomerCallLogByUuid(ApiCallRecordByUuidQueryParams req,EasyCallOutBoundVO callPhoneRes) {
+        String callType = StringUtils.isBlank(req.getCallType()) ? "03" : req.getCallType();
+
+        CrmCustomerCallLog callLog = new CrmCustomerCallLog();
+        callLog.setRunTime(callPhoneRes.getStartTime() == null ? new Date() : new Date(callPhoneRes.getStartTime()));
+        callLog.setRunParam(null);
+        callLog.setResult(null);
+        callLog.setStatus(req.getStatus());
+        callLog.setCreateTime(new Date());
+        callLog.setRecordPath(callPhoneRes.getRecordFilename());
+        callLog.setContentList(callPhoneRes.getChatContent());
+        callLog.setCallerNum(callPhoneRes.getCallee());
+        callLog.setCalleeNum(callPhoneRes.getCaller());
+        callLog.setUuid(req.getUuid());
+        callLog.setCallCreateTime(callPhoneRes.getStartTime());
+        callLog.setCallAnswerTime(callPhoneRes.getAnsweredTime());
+        callLog.setIntention(req.getIntent());
+        callLog.setCompanyId(req.getCompanyId());
+        callLog.setCompanyUserId(req.getCompanyUserId());
+        callLog.setCustomerId(req.getCustomerId());
+        if (callPhoneRes.getTimeLen() != null) {
+            callLog.setCallTime(Long.valueOf(callPhoneRes.getTimeLenValid()));
+        }
+        callLog.setCallType(Integer.valueOf(callType));
+
+        BigDecimal callCharge = DEFAULT_CALL_CHARGE;
+        String json = configService.selectConfigByKey("cId.config");
+        if (StringUtils.isNotBlank(json)) {
+            try {
+                CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+
+                if (cidConfigVO != null && cidConfigVO.getCallCharge() != null) {
+                    callCharge = cidConfigVO.getCallCharge();
+                }
+            } catch (Exception e) {
+                log.error("解析 cId.config 配置失败", e);
+            }
+        }
+
+        if (callCharge == null) {
+            callCharge = DEFAULT_CALL_CHARGE;
+        }
+
+        Long callTime = callLog.getCallTime();
+        if (callTime != null) {
+            // 毫秒转秒
+            BigDecimal callTimeSecond = new BigDecimal(callTime).divide(new BigDecimal(1000), 0, RoundingMode.CEILING);
+            BigDecimal minuteCount = callTimeSecond.divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+            BigDecimal cost = minuteCount.multiply(callCharge);
+            callLog.setCost(cost);
+        }
+
+        return crmCustomerCallLogMapper.insertCrmCustomerCallLog(callLog);
+    }
 }

+ 13 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java

@@ -7,6 +7,7 @@ import com.fs.aiSipCall.domain.AiSipCallUser;
 import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
 import com.fs.aiSipCall.service.IAiSipCallUserService;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.mapper.CompanyBindGatewayMapper;
 import com.fs.company.mapper.CompanyUserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -29,6 +30,9 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
     @Autowired
     private CompanyUserMapper companyUserMapper;
 
+    @Autowired
+    CompanyBindGatewayMapper  companyBindGatewayMapper;
+
     /**
      * 查询sip用户信息
      * 
@@ -181,4 +185,13 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         return AjaxResult.error();
     }
 
+    @Override
+    public String getGateWayIdListByCompanyId(Long companyId){
+        String gateWayIdListByCompanyId = companyBindGatewayMapper.getGateWayIdListByCompanyId(companyId);
+        if(StringUtils.isNotBlank(gateWayIdListByCompanyId)){
+            return gateWayIdListByCompanyId;
+        }
+        return "";
+    }
+
 }

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

@@ -0,0 +1,22 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/1 16:22
+ * @description
+ */
+@Data
+public class CompanyBindGateway {
+    private  Integer id;
+    //网关线路id
+    private Long gatewayId;
+    //公司id
+    private Long companyId;
+    //创建时间
+    private Date createTime;
+
+}

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

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

+ 153 - 0
fs-service/src/main/java/com/fs/company/domain/CrmCustomerCallLog.java

@@ -0,0 +1,153 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 用户手动外呼记录对象 company_user_call_log
+ *
+ * @author fs
+ * @date 2026-05-10
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CrmCustomerCallLog extends BaseEntity {
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long logId;
+
+    /**
+     * uuid回调标识
+     */
+    @Excel(name = "uuid回调标识")
+    private String callbackUuid;
+
+    /**
+     * 记录调用时间
+     */
+    @Excel(name = "记录调用时间")
+    private Date runTime;
+
+    /**
+     * 调用参数
+     */
+    @Excel(name = "调用参数")
+    private String runParam;
+
+    /**
+     * 回调返回结果
+     */
+    @Excel(name = "回调返回结果")
+    private String result;
+
+    /**
+     * 执行状态
+     * 1:执行中
+     * 2:执行成功
+     * 3:执行失败
+     */
+    @Excel(name = "执行状态")
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间")
+    private Date createTime;
+
+    /**
+     * 录音地址
+     */
+    @Excel(name = "录音地址")
+    private String recordPath;
+
+    /**
+     * 通话详细内容
+     */
+    @Excel(name = "通话详细内容")
+    private String contentList;
+
+    /**
+     * 客户号码
+     */
+    @Excel(name = "客户号码")
+    private String callerNum;
+
+    /**
+     * 坐席号码
+     */
+    @Excel(name = "坐席号码")
+    private String calleeNum;
+
+    /**
+     * 通话唯一标识
+     */
+    @Excel(name = "通话唯一标识")
+    private String uuid;
+
+    /**
+     * 呼叫开始时间
+     */
+    @Excel(name = "呼叫开始时间")
+    private Long callCreateTime;
+
+    /**
+     * 接通时间
+     */
+    @Excel(name = "接通时间")
+    private Long callAnswerTime;
+
+    /**
+     * 客户意向度
+     */
+    @Excel(name = "客户意向度")
+    private String intention;
+
+    /**
+     * 公司ID
+     */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /**
+     * 销售人员ID
+     */
+    @Excel(name = "销售人员ID")
+    private Long companyUserId;
+
+    /**
+     * 客户ID
+     */
+    @Excel(name = "客户ID")
+    private Long customerId;
+
+    /**
+     * 通话时长(秒)
+     */
+    @Excel(name = "通话时长")
+    private Long callTime;
+
+    /**
+     * 通话费用
+     */
+    @Excel(name = "通话费用")
+    private BigDecimal cost;
+
+    /**
+     * 外呼类型
+     */
+    @Excel(name = "外呼类型")
+    private Integer callType;
+}

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

@@ -0,0 +1,19 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/1 16:34
+ * @description
+ */
+public interface CompanyBindGatewayMapper {
+
+    int deleteDataByCompanyId(@Param("companyId") Long companyId);
+
+    int insertData(@Param("companyId") Long companyId, @Param("gatewayIds") List<Long> gatewayIds);
+
+    String getGateWayIdListByCompanyId(@Param("companyId") Long companyId);
+}

+ 4 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyLobsterTagUserRelMapper.java

@@ -2,9 +2,11 @@ package com.fs.company.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyLobsterTagUserRel;
+import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
 public interface CompanyLobsterTagUserRelMapper extends BaseMapper<CompanyLobsterTagUserRel> {
 
@@ -12,4 +14,6 @@ public interface CompanyLobsterTagUserRelMapper extends BaseMapper<CompanyLobste
 
     void updateBatchRelBybinding(@Param("id") Long id,@Param("flag") Integer unDelFlag);
 
+    @MapKey("id")
+    Map<String,String> selectLobsterTagsByExId(@Param("userIds") List<Long> userIds, @Param("userId") Long userId, @Param("companyId") Long companyId);
 }

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

@@ -13,5 +13,5 @@ public interface CompanyWorkflowLobsterEdgeMapper extends BaseMapper<CompanyWork
 
     List<CompanyWorkflowLobsterEdge> selectByWorkflowId(@Param("workflowId") Long workflowId);
 
-    int updateById(@Param("entity") CompanyWorkflowLobsterEdge entity);
+//    int updateById(@Param("entity") CompanyWorkflowLobsterEdge entity);
 }

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

@@ -13,5 +13,5 @@ public interface CompanyWorkflowLobsterVariableMapper extends BaseMapper<Company
 
     List<CompanyWorkflowLobsterVariable> selectByWorkflowId(@Param("workflowId") Long workflowId);
 
-    int updateById(@Param("entity") CompanyWorkflowLobsterVariable entity);
+//    int updateById(@Param("entity") CompanyWorkflowLobsterVariable entity);
 }

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

@@ -0,0 +1,19 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CrmCustomerCallLog;
+
+import java.util.List;
+
+/**
+ * 调用日志人工手动打电话Mapper接口
+ *
+ * @author fs
+ * @date 2026-01-15
+ */
+public interface CrmCustomerCallLogMapper extends BaseMapper<CrmCustomerCallLog>{
+
+    int insertCrmCustomerCallLog(CrmCustomerCallLog callLog);
+
+    List<CrmCustomerCallLog> selectCrmCustomerCallLogList(CrmCustomerCallLog crmCustomerCallLog);
+}

+ 2 - 1
fs-service/src/main/java/com/fs/company/param/BatchBindLobsterTagParam.java

@@ -22,5 +22,6 @@ public class BatchBindLobsterTagParam implements Serializable {
     /** 企微公司ID */
     private String qwCorpId;
 
-
+    /** 企微外部联系人ID */
+    private String externalUserId;
 }

+ 26 - 0
fs-service/src/main/java/com/fs/company/param/PauseRoboticActiveParam.java

@@ -0,0 +1,26 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/29 14:33
+ * @description
+ */
+
+@Data
+public class PauseRoboticActiveParam {
+
+    /**
+     * 任务id
+     */
+    private Long taskId;
+
+    /**
+     * 操作类型 1、暂停 2、继续
+     */
+    private Integer activeType;
+
+
+
+}

+ 61 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java

@@ -0,0 +1,61 @@
+package com.fs.company.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyInboundBind;
+
+/**
+ * 呼入线路模型绑定关系Service接口
+ * 
+ * @author fs
+ * @date 2026-04-27
+ */
+public interface ICompanyInboundBindService extends IService<CompanyInboundBind>{
+    /**
+     * 查询呼入线路模型绑定关系
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 呼入线路模型绑定关系
+     */
+    CompanyInboundBind selectCompanyInboundBindById(Long id);
+
+    /**
+     * 查询呼入线路模型绑定关系列表
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 呼入线路模型绑定关系集合
+     */
+    List<CompanyInboundBind> selectCompanyInboundBindList(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 新增呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    int insertCompanyInboundBind(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 修改呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    int updateCompanyInboundBind(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 批量删除呼入线路模型绑定关系
+     * 
+     * @param ids 需要删除的呼入线路模型绑定关系主键集合
+     * @return 结果
+     */
+    int deleteCompanyInboundBindByIds(Long[] ids);
+
+    /**
+     * 删除呼入线路模型绑定关系信息
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    int deleteCompanyInboundBindById(Long id);
+}

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

@@ -75,4 +75,6 @@ public interface ICompanyInboundCallManageService {
      * @return 通话记录列表
      */
     List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
+
+    Boolean checkCalleeInboundLlm(String callee);
 }

+ 61 - 0
fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java

@@ -0,0 +1,61 @@
+package com.fs.company.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanySiptaskInfo;
+
+/**
+ * 任务与外呼sip任务关联关系Service接口
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface ICompanySiptaskInfoService extends IService<CompanySiptaskInfo>{
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    CompanySiptaskInfo selectCompanySiptaskInfoById(Long id);
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系集合
+     */
+    List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的任务与外呼sip任务关联关系主键集合
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoByIds(Long[] ids);
+
+    /**
+     * 删除任务与外呼sip任务关联关系信息
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoById(Long id);
+}

+ 3 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyTagTemplateBindingService.java

@@ -53,5 +53,7 @@ public interface ICompanyTagTemplateBindingService {
     /**
      * 批量添加龙虾标签给企微客户
      */
-    AjaxResult batchBindLobsterTag(Long companyId, String userName, String qwCorpId, List<Long> externalContactIds, List<String> tagCodes,Long companyUserId);
+    AjaxResult batchBindLobsterTag(Long companyId, String userName, String qwCorpId, List<Long> externalContactIds, List<String> tagCodes,Long companyUserId,String externalUserId);
+
+    AjaxResult lobsterTags(List<Long> userIds, Long userId, Long companyId);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -3,8 +3,10 @@ package com.fs.company.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CalltaskcreateaiCustomizeResult;
+import com.fs.common.core.domain.R;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.vo.*;
 
 import java.util.List;
@@ -112,4 +114,6 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      * @param traceId
      */
     void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
+
+    R pauseRoboticActive(PauseRoboticActiveParam param);
 }

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

@@ -0,0 +1,22 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CrmCustomerCallLog;
+
+import java.util.List;
+
+/**
+ * 客户通话记录Service接口
+ *
+ * @author fs
+ * @date 2026-05-10
+ */
+public interface ICrmCustomerCallLogService {
+
+    /**
+     * 查询客户通话记录列表
+     *
+     * @param crmCustomerCallLog 客户通话记录
+     * @return 客户通话记录集合
+     */
+    List<CrmCustomerCallLog> selectCrmCustomerCallLogList(CrmCustomerCallLog crmCustomerCallLog);
+}

+ 93 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundBindServiceImpl.java

@@ -0,0 +1,93 @@
+package com.fs.company.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.company.mapper.CompanyInboundBindMapper;
+import com.fs.company.domain.CompanyInboundBind;
+import com.fs.company.service.ICompanyInboundBindService;
+
+/**
+ * 呼入线路模型绑定关系Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-04-27
+ */
+@Service
+public class CompanyInboundBindServiceImpl extends ServiceImpl<CompanyInboundBindMapper, CompanyInboundBind> implements ICompanyInboundBindService {
+
+    /**
+     * 查询呼入线路模型绑定关系
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 呼入线路模型绑定关系
+     */
+    @Override
+    public CompanyInboundBind selectCompanyInboundBindById(Long id)
+    {
+        return baseMapper.selectCompanyInboundBindById(id);
+    }
+
+    /**
+     * 查询呼入线路模型绑定关系列表
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 呼入线路模型绑定关系
+     */
+    @Override
+    public List<CompanyInboundBind> selectCompanyInboundBindList(CompanyInboundBind companyInboundBind)
+    {
+        return baseMapper.selectCompanyInboundBindList(companyInboundBind);
+    }
+
+    /**
+     * 新增呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyInboundBind(CompanyInboundBind companyInboundBind)
+    {
+        companyInboundBind.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyInboundBind(companyInboundBind);
+    }
+
+    /**
+     * 修改呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyInboundBind(CompanyInboundBind companyInboundBind)
+    {
+        return baseMapper.updateCompanyInboundBind(companyInboundBind);
+    }
+
+    /**
+     * 批量删除呼入线路模型绑定关系
+     * 
+     * @param ids 需要删除的呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyInboundBindByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanyInboundBindByIds(ids);
+    }
+
+    /**
+     * 删除呼入线路模型绑定关系信息
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyInboundBindById(Long id)
+    {
+        return baseMapper.deleteCompanyInboundBindById(id);
+    }
+}

+ 0 - 11
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java

@@ -78,10 +78,6 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
      */
     @Override
     public int insertInboundLlm(EasyCallInboundLlmVO vo) {
-        Boolean b = checkCalleeInboundLlm(vo.getCallee());
-        if(b){
-          throw new RuntimeException("被叫号码已存在,不能重复插入");
-        }
         try {
             //获得总后台配置
             SysConfig cidConf = sysConfigService.selectConfigByConfigKey("cId.config");
@@ -99,13 +95,6 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
         }
         
         int i = inboundLlmMapper.insertInboundLlm(vo);
-        if(i >0 && vo.getId()!= null) {
-            CompanyInboundBind bind = new CompanyInboundBind();
-            bind.setInboundLlmAccountId(Long.valueOf(vo.getId()));
-            bind.setCompanyId(vo.getCompanyId());
-            bind.setCreateTime(new Date());
-            companyInboundBindMapper.insertCompanyInboundBind( bind);
-        }
         return i;
     }
 

+ 18 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -140,6 +140,8 @@ public class CompanyServiceImpl implements ICompanyService
 
     @Autowired
     private ICompanyConfigService companyConfigService;
+    @Autowired
+    CompanyBindGatewayMapper companyBindGatewayMapper;
 
 
     @Override
@@ -534,8 +536,24 @@ public class CompanyServiceImpl implements ICompanyService
         if(company.isUpdateMiniApp()){
             bindMiniApp(company);
         }
+        if(null != company.getShowGatewayIds() && !company.getShowGatewayIds().isEmpty()){
+            logger.info("修改公司网关");
+            handleCompanyBindGateway(company.getCompanyId(),company.getShowGatewayIds());
+        }
         return companyMapper.updateCompany(company);
     }
+
+    /**
+     * 处理公司绑定网关路线
+     * @param companyId
+     * @param gatewayIds
+     */
+    public void handleCompanyBindGateway(Long companyId,List<Long> gatewayIds){
+       companyBindGatewayMapper.deleteDataByCompanyId(companyId);
+       companyBindGatewayMapper.insertData(companyId, gatewayIds);
+        logger.info("修改公司网关成功");
+    }
+
     // 绑定小程序
     public void bindMiniApp(Company company){
         companyMiniappService.removeByCompanyId(company.getCompanyId());

+ 91 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java

@@ -0,0 +1,91 @@
+package com.fs.company.service.impl;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.company.mapper.CompanySiptaskInfoMapper;
+import com.fs.company.domain.CompanySiptaskInfo;
+import com.fs.company.service.ICompanySiptaskInfoService;
+
+/**
+ * 任务与外呼sip任务关联关系Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+@Service
+public class CompanySiptaskInfoServiceImpl extends ServiceImpl<CompanySiptaskInfoMapper, CompanySiptaskInfo> implements ICompanySiptaskInfoService {
+
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    @Override
+    public CompanySiptaskInfo selectCompanySiptaskInfoById(Long id)
+    {
+        return baseMapper.selectCompanySiptaskInfoById(id);
+    }
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系
+     */
+    @Override
+    public List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.selectCompanySiptaskInfoList(companySiptaskInfo);
+    }
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    @Override
+    public int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.insertCompanySiptaskInfo(companySiptaskInfo);
+    }
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    @Override
+    public int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo)
+    {
+        return baseMapper.updateCompanySiptaskInfo(companySiptaskInfo);
+    }
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySiptaskInfoByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanySiptaskInfoByIds(ids);
+    }
+
+    /**
+     * 删除任务与外呼sip任务关联关系信息
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanySiptaskInfoById(Long id)
+    {
+        return baseMapper.deleteCompanySiptaskInfoById(id);
+    }
+}

+ 10 - 2
fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java

@@ -321,7 +321,7 @@ public class CompanyTagTemplateBindingServiceImpl implements ICompanyTagTemplate
     @Override
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult batchBindLobsterTag(Long companyId, String userName, String qwCorpId, List<Long> externalContactIds,
-                                           List<String> tagCodes,Long companyUserId) {
+                                           List<String> tagCodes,Long companyUserId,String externalUserId) {
         if (externalContactIds == null || externalContactIds.isEmpty()) {
             return AjaxResult.error("请勾选需要添加龙虾标签的客户");
         }
@@ -394,7 +394,8 @@ public class CompanyTagTemplateBindingServiceImpl implements ICompanyTagTemplate
                         task.setCompanyId(companyId).setCompanyUserId(companyUserId).setCorpId(qwCorpId)
                                 .setTemplateId(binding.getTemplateId()).setTaskName(binding.getTemplateName())
                                 .setTaskType(2).setTaskContent(node.getGreetingConfig()).setTaskName("龙虾企微发消息")
-                                .setQwUserId(qwUserId).setBindingId(binding.getId()).setLobsterNodeId(node.getId());
+                                .setQwUserId(qwUserId).setBindingId(binding.getId()).setLobsterNodeId(node.getId())
+                                .setExternalUserId(externalUserId);
                         task.setSendTime(getSendTimeNode(node));
                         tasks.add(task);
                     });
@@ -409,6 +410,13 @@ public class CompanyTagTemplateBindingServiceImpl implements ICompanyTagTemplate
         return AjaxResult.success("已为 " + externalContactIds.size() + " 个客户添加 " + targetBindings.size() + " 个龙虾标签");
     }
 
+    @Override
+    public AjaxResult lobsterTags(List<Long> userIds, Long userId, Long companyId) {
+
+
+        return AjaxResult.success(companyLobsterTagUserRelMapper.selectLobsterTagsByExId( userIds,  userId,  companyId));
+    }
+
     private LocalDateTime getSendTimeNode(CompanyWorkflowLobsterNode node) {
         Integer days = Integer.valueOf(node.getNodeCode().substring(4));
         LocalDate date = LocalDate.now().plusDays(days);

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

@@ -15,6 +15,7 @@ import com.fs.aicall.service.AiCallService;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.config.RedisTenantContext;
 import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.domain.model.TenantPrincipal;
 import com.fs.common.core.redis.RedisCache;
@@ -27,6 +28,7 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
@@ -1942,4 +1944,33 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return new HashMap<>();
         }
     }
+
+    //暂停
+    private final Integer ACTIVE_TYPE_PAUSE = 1;
+    //继续
+    private final Integer ACTIVE_TYPE_CONTINUE = 2;
+
+    /**
+     * 任务暂停 & 恢复操作
+     *
+     * @param param
+     * @return
+     */
+    @Override
+    public R pauseRoboticActive(PauseRoboticActiveParam param) {
+        //暂停任务
+        if (ACTIVE_TYPE_PAUSE.equals(param.getActiveType())) {
+
+            // 暂停任务更新
+
+            // 暂停任务创建的三方外呼任务
+
+        }
+        //恢复任务继续进入可运行
+        else if (ACTIVE_TYPE_CONTINUE.equals(param.getActiveType())) {
+
+        }
+
+        return R.ok("操作成功");
+    }
 }

+ 33 - 0
fs-service/src/main/java/com/fs/company/service/impl/CrmCustomerCallLogServiceImpl.java

@@ -0,0 +1,33 @@
+package com.fs.company.service.impl;
+
+import com.fs.company.domain.CrmCustomerCallLog;
+import com.fs.company.mapper.CrmCustomerCallLogMapper;
+import com.fs.company.service.ICrmCustomerCallLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 客户通话记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-05-10
+ */
+@Service
+public class CrmCustomerCallLogServiceImpl implements ICrmCustomerCallLogService {
+
+    @Autowired
+    private CrmCustomerCallLogMapper crmCustomerCallLogMapper;
+
+    /**
+     * 查询客户通话记录列表
+     *
+     * @param crmCustomerCallLog 客户通话记录
+     * @return 客户通话记录
+     */
+    @Override
+    public List<CrmCustomerCallLog> selectCrmCustomerCallLogList(CrmCustomerCallLog crmCustomerCallLog) {
+        return crmCustomerCallLogMapper.selectCrmCustomerCallLogList(crmCustomerCallLog);
+    }
+}

+ 3 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java

@@ -25,6 +25,7 @@ import com.fs.system.service.ISysConfigService;
 import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.mapper.CidIpadServerMapper;
 import com.fs.wxcid.mapper.WxContactMapper;
+import com.fs.wxcid.utils.TenantHelper;
 import com.fs.wxwork.service.WxIpadService;
 import lombok.extern.slf4j.Slf4j;
 
@@ -306,7 +307,8 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             JSONObject bizJson = new JSONObject()
                     .fluentPut("instanceId",instanceId)
                     .fluentPut("nodeKey",nodeKey)
-                    .fluentPut("accountId",companyWxAccount.getId());
+                    .fluentPut("accountId",companyWxAccount.getId())
+                    .fluentPut("tenantId", TenantHelper.getTenantId());
             param.setBizJson(bizJson.toJSONString());
             wxService.addWx(param);
         } catch (Exception ex) {

+ 5 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java

@@ -93,5 +93,10 @@ public class EasyCallInboundLlmVO implements Serializable {
      */
     private Integer fsSceneType;
 
+    /**
+     * saas租户Id
+     */
+    private Long fsTenantId;
+
 
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java

@@ -7,10 +7,13 @@ import lombok.Data;
  */
 @Data
 public class EasyCallVoiceCodeVO {
+    /** 主键id */
+    private Integer id;
     /** 音色编号 */
     private String voiceCode;
     /** 音色名称 */
     private String voiceName;
     /** 声音源:aliyun_tts */
     private String voiceSource;
+    private Integer priority;
 }

+ 4 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -183,6 +183,10 @@ public class CrmCustomer extends BaseEntity
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date visitTime;
 
+    /** 历史沟通记录 */
+    @Excel(name = "历史沟通记录")
+    private String historicalCommunication;
+
 
 
 }

+ 10 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -997,4 +997,14 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
     @Select("select customer_id  from  crm_customer where company_id = #{companyId} and  mobile=#{remarkMobile} limit 1")
     Long selectCrmCustomerByCrmMobileAndCompanyId(@Param("companyId") Long companyId, @Param("remarkMobile") String remarkMobile);
 
+    /**
+     * 批量插入客户
+     *
+     * @param list 客户列表
+     * @return 结果
+     */
+    int insertBatchCrmCustomer(@Param("list") List<CrmCustomer> list);
+
+    @Select("select mobile from crm_customer where customer_id = #{customerId} limit 1")
+    String selectCrmCustomerPhoneByCustomerId(Long customerId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java

@@ -85,4 +85,9 @@ public class CrmLineCustomerListQueryParam extends BaseQueryParam
     @Excel(name = "标签" )
     private String tags;
 
+
+    private Integer attritionLevel;
+
+    private String intentionDegree;
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/crm/param/CrmMyCustomerListQueryParam.java

@@ -95,4 +95,9 @@ public class CrmMyCustomerListQueryParam extends BaseQueryParam
     private String corpId;
     /** 客户级别 */
     private Long customerLevel;
+
+
+    private Integer attritionLevel;
+
+    private String intentionDegree;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerService.java

@@ -84,6 +84,8 @@ public interface ICrmCustomerService
 
     CrmCustomerQueryVO selectCrmFullCustomerQueryByCustomerId(Long customerId);
 
+    String selectCrmCustomerPhoneByCustomerId(Long customerId);
+
     String importLineCustomer(List<CrmLineCustomerImportParam> list, String operName);
 
     List<CrmCustomerListVO> selectCrmCustomerListVO(CrmCustomer crmCustomer);

+ 40 - 28
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java

@@ -192,6 +192,12 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
         return crmCustomerMapper.selectCrmFullCustomerQueryByCustomerId(customerId);
     }
 
+    @Override
+    public String selectCrmCustomerPhoneByCustomerId(Long customerId) {
+
+        return crmCustomerMapper.selectCrmCustomerPhoneByCustomerId(customerId);
+    }
+
     @Override
     public String importLineCustomer(List<CrmLineCustomerImportParam> list, String operName) {
         if (StringUtils.isNull(list) || list.size() == 0)
@@ -551,49 +557,42 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
         StringBuilder successMsg = new StringBuilder();
         StringBuilder failureMsg = new StringBuilder();
         StringBuilder importMsg = new StringBuilder();
+
+        // 数据准备
+        List<CrmCustomer> batchList = new ArrayList<>();
+        List<CrmCompanyLineCustomerImportParam> validParams = new ArrayList<>();
+
         for (CrmCompanyLineCustomerImportParam customer : list)
         {
             try
             {
-//                String regex = "^1[3456789]\\d{9}$";
-//                if(!customer.getMobile().matches(regex)){
-//                    failureNum++;
-//                    String msg = "<br/>" + failureNum + "、客户 " + customer.getMobile() + " 导入失败:电话号码格式不正确";
-//                    failureMsg.append(msg );
-//                    continue;
-//                }
-                CrmCustomer crmCustomer=new CrmCustomer();
-                BeanUtils.copyProperties(customer,crmCustomer);
+                CrmCustomer crmCustomer = new CrmCustomer();
+                BeanUtils.copyProperties(customer, crmCustomer);
+                // 如果没有客户名称,默认为"客户"+手机号后四位
+                if (StringUtils.isEmpty(crmCustomer.getCustomerName())) {
+                    String mobile = crmCustomer.getMobile();
+                    if (StringUtils.isNotEmpty(mobile) && mobile.length() >= 4) {
+                        crmCustomer.setCustomerName("客户" + mobile.substring(mobile.length() - 4));
+                    } else {
+                        crmCustomer.setCustomerName("客户");
+                    }
+                }
                 crmCustomer.setIsDel(0);
                 crmCustomer.setIsLine(1);
                 crmCustomer.setCompanyId(companyId);
                 crmCustomer.setStatus(1);
                 crmCustomer.setIsReceive(1);
-                if(StringUtils.isNotEmpty(customer.getSource())){
+                if (StringUtils.isNotEmpty(customer.getSource())) {
                     try {
                         crmCustomer.setSource(Integer.parseInt(customer.getSource()));
-                    }
-                    catch (Exception e){
+                    } catch (Exception e) {
                     }
                 }
                 crmCustomer.setCustomerCode(OrderUtils.getOrderNo());
                 crmCustomer.setCreateTime(new Date());
                 crmCustomer.setCreateUserId(companyUserId);
-                crmCustomerMapper.insertCrmCustomer(crmCustomer);
-                successNum++;
-                successMsg.append("<br/>" + successNum + "、客户 " + customer.getCustomerName() + " 导入成功");
-                //若存在归属员工编号 就将线索客户分配给该业务员  updated by qxj 2023年05月12日16:51:37
-                if(StringUtils.isNotEmpty(customer.getOwnerCompanyUserCode())){
-                    CompanyUser companyUser=companyUserMapper.selectUserByUserName(customer.getOwnerCompanyUserCode());
-                    if(companyUser!=null){
-                       Boolean isSuccess=assignUserAfterImport(operName,crmCustomer.getCustomerId(),companyUser);
-                       if(isSuccess){
-                           successMsg.append(" <br/> "+customer.getOwnerCompanyUserCode()+"客户分配成功");
-                       }else{
-                           successMsg.append(" <br/> "+customer.getOwnerCompanyUserCode()+"客户分配失败");
-                       }
-                    }
-                }
+                batchList.add(crmCustomer);
+                validParams.add(customer);
             }
             catch (Exception e) {
                 failureNum++;
@@ -602,9 +601,22 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
             }
         }
 
+        // 批量插入
+        int batchSize = 500;
+        for (int i = 0; i < batchList.size(); i += batchSize) {
+            List<CrmCustomer> subList = batchList.subList(i, Math.min(i + batchSize, batchList.size()));
+            crmCustomerMapper.insertBatchCrmCustomer(subList);
+        }
+
+        // 构建结果消息
+        for (int i = 0; i < batchList.size(); i++) {
+            CrmCompanyLineCustomerImportParam customer = validParams.get(i);
+            successNum++;
+            successMsg.append("<br/>" + successNum + "、客户 " + customer.getCustomerName() + " 导入成功");
+        }
+
         if (failureNum > 0) {
             failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
-            //throw new CustomException(failureMsg.toString());
         }
         else {
             successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");

+ 14 - 1
fs-service/src/main/resources/db/tenant-initData.sql

@@ -1,4 +1,4 @@
-
+
 
 INSERT INTO `sys_dept` (`dept_id`, `parent_id`, `ancestors`, `dept_name`, `order_num`, `leader`, `phone`, `email`, `status`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES (1, 0, '0', '总公司', 1, 'admin', '', '', '0', '0', 'admin', '2021-11-24 23:26:40', 'admin', '2025-03-23 15:53:14');
 
@@ -5374,3 +5374,16 @@ VALUES(3, 'Ai医生', 'name,sex,age,address,disease,consultProduct,course,course
 INSERT INTO fastgpt_role_type
 (id, name, contact_info)
 VALUES(4, '医生工作室', 'name,sex,age,address,sweat,toilet,eat,constitution,coldBody,isCold,disease,course,courseStatus,study,product_talk');
+
+-- ----------------------------
+-- Records of company_ai_workflow_node_type
+-- ----------------------------
+INSERT INTO `company_ai_workflow_node_type` VALUES (1, 'START', '开始节点', 'el-icon-video-play', '#52c41a', 'basic', '{}', 1, 1, '2026-01-29 11:22:23');
+INSERT INTO `company_ai_workflow_node_type` VALUES (2, 'END', '结束节点', 'el-icon-video-pause', '#ff4d4f', 'basic', '{}', 2, 1, '2026-01-29 11:22:23');
+INSERT INTO `company_ai_workflow_node_type` VALUES (3, 'CONDITION', '条件判断', 'el-icon-question', '#faad14', 'logic', '{\"conditions\":[]}', 3, 0, '2026-04-07 18:19:35');
+INSERT INTO `company_ai_workflow_node_type` VALUES (4, 'DELAY', '延时节点', 'el-icon-time', '#13c2c2', 'logic', '{\"delayTime\":0,\"unit\":\"second\"}', 4, 0, '2026-04-07 18:19:32');
+INSERT INTO `company_ai_workflow_node_type` VALUES (6, 'AI_CALL_TASK', '外呼', 'el-icon-phone-outline\r\n', '#E6A23C', 'aiCell', '{}', 5, 1, '2026-02-05 11:31:20');
+INSERT INTO `company_ai_workflow_node_type` VALUES (7, 'AI_ADD_WX_TASK', '加微', 'el-icon-mobile-phone', '#52c41a', 'aiCell', '{}', 6, 0, '2026-04-08 09:36:00');
+INSERT INTO `company_ai_workflow_node_type` VALUES (8, 'AI_SEND_MSG_TASK', '短信', 'el-icon-chat-round', '#409EFF', 'aiCell', '{}', 7, 1, '2026-05-08 09:37:59');
+INSERT INTO `company_ai_workflow_node_type` VALUES (9, 'AI_QW_ADD_WX_TASK', '加企微', 'el-icon-service', '#800080', 'aiCell', '{}', 8, 1, '2026-04-08 09:36:58');
+INSERT INTO `company_ai_workflow_node_type` VALUES (10, 'AI_ADD_WX_TASK_NEW', '加个微', 'el-icon-mobile-phone', '#52c41a', 'aiCell', '{}', 9, 1, '2026-04-22 14:09:52');

+ 23 - 0
fs-service/src/main/resources/mapper/company/CompanyBindGatewayMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyBindGatewayMapper">
+
+    <delete id="deleteDataByCompanyId" parameterType="java.lang.Long">
+        delete from company_bind_gateway where company_id = #{companyId}
+    </delete>
+
+    <insert id="insertData">
+        <if test="gatewayIds != null and gatewayIds.size() > 0">
+            insert into company_bind_gateway(company_id, gateway_id,create_time) values
+            <foreach item="item" collection="gatewayIds" separator=",">
+                (#{companyId},#{item},now())
+            </foreach>
+        </if>
+    </insert>
+
+    <select id="getGateWayIdListByCompanyId" resultType="java.lang.String">
+        SELECT GROUP_CONCAT(gateway_id) FROM `company_bind_gateway` where company_id = #{companyId}
+    </select>
+</mapper>

+ 12 - 0
fs-service/src/main/resources/mapper/company/CompanyLobsterTagUserRelMapper.xml

@@ -25,5 +25,17 @@
     <update id="updateBatchRelBybinding">
         update company_lobster_tag_user_rel set del_flag = #{flag} where binding_id = #{id}
     </update>
+    <select id="selectLobsterTagsByExId" resultType="java.util.Map">
+        select c.id  , b.tag_name
+        from
+            qw_external_contact c
+        JOIN company_lobster_tag_user_rel r ON r.external_contact_id = c.id AND r.del_flag = 0
+        JOIN
+            company_tag_template_binding b on r.binding_id = b.id AND b.del_flag = 0 AND b.status = 1
+        WHERE c.id IN
+              <foreach collection="userIds" item="userId" separator="," open="(" close=")">
+                #{userId}
+              </foreach>
+    </select>
 
 </mapper>

+ 1 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -214,6 +214,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="roboticId != null">and robotic_id = #{roboticId}</if>
         </where>
         group by robotic_id
+        order by t1.run_time desc
     </select>
     <select id="selectCompanyVoiceRoboticCallPhoneLogCount" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCount">
         select

+ 4 - 3
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogSendmsgMapper.xml

@@ -43,7 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCompanyVoiceRoboticCallLogSendmsgGroupList" parameterType="CompanyVoiceRoboticCallLogSendmsg" resultType="com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg">
         select
-        robotic_id,
+        msg.robotic_id,
         cvr.name,
         count(1) as totalRecordCount,
         sum(case when status = 1 then 1 else 0 end) as runningCount,
@@ -54,7 +54,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_voice_robotic cvr on cvr.id = msg.robotic_id
         left join company_voice_robotic_callees cc on cc.id = msg.caller_id
         <where>
-            <if test="roboticId != null">and robotic_id = #{roboticId}</if>
+            <if test="roboticId != null">and msg.robotic_id = #{roboticId}</if>
             <if test="callerId != null">and caller_id = #{callerId}</if>
             <if test="runTime != null">and run_time = #{runTime}</if>
             <if test="runParam != null and runParam != ''">and run_param = #{runParam}</if>
@@ -66,7 +66,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="cost != null">and cost = #{cost}</if>
             <if test="contentLen != null">and content_len = #{contentLen}</if>
         </where>
-        group by robotic_id
+        group by msg.robotic_id
+        order by msg.run_time desc
     </select>
     
     <select id="selectCompanyVoiceRoboticCallLogSendmsgByLogId" parameterType="Long" resultMap="CompanyVoiceRoboticCallLogSendmsgResult">

+ 17 - 17
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterEdgeMapper.xml

@@ -52,22 +52,22 @@
         order by sort_no asc
     </select>
 
-    <update id="updateById">
-        update company_workflow_lobster_edge
-        <trim prefix="set" suffixOverrides=",">
-            <if test="entity.edgeKey != null">edge_key = #{entity.edgeKey},</if>
-            <if test="entity.sourceNodeCode != null">source_node_code = #{entity.sourceNodeCode},</if>
-            <if test="entity.targetNodeCode != null">target_node_code = #{entity.targetNodeCode},</if>
-            <if test="entity.sourcePort != null">source_port = #{entity.sourcePort},</if>
-            <if test="entity.targetPort != null">target_port = #{entity.targetPort},</if>
-            <if test="entity.edgeLabel != null">edge_label = #{entity.edgeLabel},</if>
-            <if test="entity.edgeColor != null">edge_color = #{entity.edgeColor},</if>
-            <if test="entity.conditionExpr != null">condition_expr = #{entity.conditionExpr},</if>
-            <if test="entity.sortNo != null">sort_no = #{entity.sortNo},</if>
-            <if test="entity.updateBy != null">update_by = #{entity.updateBy},</if>
-            <if test="entity.updateTime != null">update_time = #{entity.updateTime},</if>
-        </trim>
-        where id = #{entity.id}
-    </update>
+<!--    <update id="updateById">-->
+<!--        update company_workflow_lobster_edge-->
+<!--        <trim prefix="set" suffixOverrides=",">-->
+<!--            <if test="entity.edgeKey != null">edge_key = #{entity.edgeKey},</if>-->
+<!--            <if test="entity.sourceNodeCode != null">source_node_code = #{entity.sourceNodeCode},</if>-->
+<!--            <if test="entity.targetNodeCode != null">target_node_code = #{entity.targetNodeCode},</if>-->
+<!--            <if test="entity.sourcePort != null">source_port = #{entity.sourcePort},</if>-->
+<!--            <if test="entity.targetPort != null">target_port = #{entity.targetPort},</if>-->
+<!--            <if test="entity.edgeLabel != null">edge_label = #{entity.edgeLabel},</if>-->
+<!--            <if test="entity.edgeColor != null">edge_color = #{entity.edgeColor},</if>-->
+<!--            <if test="entity.conditionExpr != null">condition_expr = #{entity.conditionExpr},</if>-->
+<!--            <if test="entity.sortNo != null">sort_no = #{entity.sortNo},</if>-->
+<!--            <if test="entity.updateBy != null">update_by = #{entity.updateBy},</if>-->
+<!--            <if test="entity.updateTime != null">update_time = #{entity.updateTime},</if>-->
+<!--        </trim>-->
+<!--        where id = #{entity.id}-->
+<!--    </update>-->
 
 </mapper>

+ 7 - 7
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterTaskMapper.xml

@@ -133,25 +133,25 @@
         set del_flag = #{flag}
         where binding_id = #{id}
     </update>
+
+
     <update id="updateTaskListExecuteStatus">
-        update company_workflow_lobster_task
-        <foreach item="item" collection="list" separator="," open="(" close=")" index="index">
+        <foreach collection="list" item="item" separator=";">
+            update company_workflow_lobster_task
             set execute_status = #{item.executeStatus}
-            where
-                id =
-            #{item.id}
+            where id = #{item.id}
         </foreach>
     </update>
 
     <insert id="batchInsert">
         insert into company_workflow_lobster_task
         (company_id, template_id, task_name, task_type, task_content,
-         corp_id,create_by, create_time, company_user_id, lobster_node_id, send_time,qw_user_id,binding_id)
+         corp_id,create_by, create_time, company_user_id, lobster_node_id, send_time,qw_user_id,binding_id, external_user_id)
         values
         <foreach collection="list" item="item" separator=",">
             (#{item.companyId}, #{item.templateId}, #{item.taskName}, #{item.taskType}, #{item.taskContent},
              #{item.corpId},#{item.createBy}, #{item.createTime},
-            #{item.companyUserId}, #{item.lobsterNodeId},#{item.sendTime},#{item.qwUserId},#{item.bindingId}
+            #{item.companyUserId}, #{item.lobsterNodeId},#{item.sendTime},#{item.qwUserId},#{item.bindingId},#{item.externalUserId}
              )
         </foreach>
     </insert>

+ 15 - 15
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterVariableMapper.xml

@@ -27,20 +27,20 @@
         order by id asc
     </select>
 
-    <update id="updateById">
-        update company_workflow_lobster_variable
-        <trim prefix="set" suffixOverrides=",">
-            <if test="entity.varCode != null">var_code = #{entity.varCode},</if>
-            <if test="entity.varName != null">var_name = #{entity.varName},</if>
-            <if test="entity.varType != null">var_type = #{entity.varType},</if>
-            <if test="entity.sourceType != null">source_type = #{entity.sourceType},</if>
-            <if test="entity.required != null">required = #{entity.required},</if>
-            <if test="entity.defaultValue != null">default_value = #{entity.defaultValue},</if>
-            <if test="entity.description != null">description = #{entity.description},</if>
-            <if test="entity.updateBy != null">update_by = #{entity.updateBy},</if>
-            <if test="entity.updateTime != null">update_time = #{entity.updateTime},</if>
-        </trim>
-        where id = #{entity.id}
-    </update>
+<!--    <update id="updateById">-->
+<!--        update company_workflow_lobster_variable-->
+<!--        <trim prefix="set" suffixOverrides=",">-->
+<!--            <if test="entity.varCode != null">var_code = #{entity.varCode},</if>-->
+<!--            <if test="entity.varName != null">var_name = #{entity.varName},</if>-->
+<!--            <if test="entity.varType != null">var_type = #{entity.varType},</if>-->
+<!--            <if test="entity.sourceType != null">source_type = #{entity.sourceType},</if>-->
+<!--            <if test="entity.required != null">required = #{entity.required},</if>-->
+<!--            <if test="entity.defaultValue != null">default_value = #{entity.defaultValue},</if>-->
+<!--            <if test="entity.description != null">description = #{entity.description},</if>-->
+<!--            <if test="entity.updateBy != null">update_by = #{entity.updateBy},</if>-->
+<!--            <if test="entity.updateTime != null">update_time = #{entity.updateTime},</if>-->
+<!--        </trim>-->
+<!--        where id = #{entity.id}-->
+<!--    </update>-->
 
 </mapper>

+ 97 - 0
fs-service/src/main/resources/mapper/company/CrmCustomerCallLogMapper.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CrmCustomerCallLogMapper">
+
+    <resultMap type="CrmCustomerCallLog" id="CrmCustomerCallLogResult">
+        <result property="logId" column="log_id" />
+        <result property="uuid" column="uuid" />
+        <result property="customerId" column="customer_id" />
+        <result property="companyId" column="company_id" />
+        <result property="companyUserId" column="company_user_id" />
+        <result property="callerNum" column="caller_num" />
+        <result property="calleeNum" column="callee_num" />
+        <result property="callCreateTime" column="call_create_time" />
+        <result property="callAnswerTime" column="call_answer_time" />
+        <result property="callTime" column="call_time" />
+        <result property="recordPath" column="record_path" />
+        <result property="status" column="status" />
+        <result property="intention" column="intention" />
+        <result property="cost" column="cost" />
+        <result property="callType" column="call_type" />
+        <result property="createTime" column="create_time" />
+        <result property="contentList" column="content_list" />
+    </resultMap>
+
+    <select id="selectCrmCustomerCallLogList" parameterType="CrmCustomerCallLog" resultMap="CrmCustomerCallLogResult">
+        select log_id, uuid, customer_id, company_id, company_user_id, caller_num, callee_num,
+               call_create_time, call_answer_time, call_time, record_path, status, intention,
+               cost, call_type, create_time, content_list
+        from crm_customer_call_log
+        <where>
+            <if test="customerId != null">AND customer_id = #{customerId}</if>
+            <if test="companyId != null">AND company_id = #{companyId}</if>
+            <if test="companyUserId != null">AND company_user_id = #{companyUserId}</if>
+            <if test="status != null">AND status = #{status}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <insert id="insertCrmCustomerCallLog"
+            parameterType="com.fs.company.domain.CrmCustomerCallLog"
+            useGeneratedKeys="true"
+            keyProperty="logId">
+        insert into crm_customer_call_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="runTime != null">run_time,</if>
+            <if test="runParam != null">run_param,</if>
+            <if test="result != null">result,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="recordPath != null">record_path,</if>
+            <if test="contentList != null">content_list,</if>
+            <if test="callerNum != null">caller_num,</if>
+            <if test="calleeNum != null">callee_num,</if>
+            <if test="uuid != null">uuid,</if>
+            <if test="callCreateTime != null">call_create_time,</if>
+            <if test="callAnswerTime != null">call_answer_time,</if>
+            <if test="intention != null">intention,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="customerId != null">customer_id,</if>
+            <if test="callTime != null">call_time,</if>
+            <if test="cost != null">cost,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="callType != null">call_type,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="runTime != null">#{runTime},</if>
+            <if test="runParam != null">#{runParam},</if>
+            <if test="result != null">#{result},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="recordPath != null">#{recordPath},</if>
+            <if test="contentList != null">#{contentList},</if>
+            <if test="callerNum != null">#{callerNum},</if>
+            <if test="calleeNum != null">#{calleeNum},</if>
+            <if test="uuid != null">#{uuid},</if>
+            <if test="callCreateTime != null">#{callCreateTime},</if>
+            <if test="callAnswerTime != null">#{callAnswerTime},</if>
+            <if test="intention != null">#{intention},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="customerId != null">#{customerId},</if>
+            <if test="callTime != null">#{callTime},</if>
+            <if test="cost != null">#{cost},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="callType != null">#{callType},</if>
+        </trim>
+    </insert>
+
+
+</mapper>

+ 8 - 2
fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml

@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="aiTransferData" column="ai_transfer_data"/>
         <result property="ivrId" column="ivr_id"/>
         <result property="satisfSurveyIvrId" column="satisf_survey_ivr_id"/>
+        <result property="fsTenantId" column="fs_tenant_id"/>
     </resultMap>
 
     <resultMap id="LlmAccountResult" type="com.fs.company.vo.easycall.EasyCallLlmAccountVO">
@@ -28,9 +29,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <resultMap id="VoiceCodeResult" type="com.fs.company.vo.easycall.EasyCallVoiceCodeVO">
+        <id property="id" column="id"/>
         <result property="voiceCode" column="voice_code"/>
         <result property="voiceName" column="voice_name"/>
         <result property="voiceSource" column="voice_source"/>
+        <result property="priority" column="priority"/>
     </resultMap>
 
     <resultMap id="BizGroupResult" type="com.fs.company.vo.easycall.EasyCallBizGroupVO">
@@ -56,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectInboundLlmVo">
-        select id, llm_account_id, callee, voice_code, voice_source, service_type, asr_provider, ai_transfer_type, ai_transfer_data, ivr_id, satisf_survey_ivr_id, inbound_alias,call_back_url,fs_scene_type from cc_inbound_llm_account
+        select id, llm_account_id, callee, voice_code, voice_source, service_type, asr_provider, ai_transfer_type, ai_transfer_data, ivr_id, satisf_survey_ivr_id, inbound_alias,call_back_url,fs_scene_type,fs_tenant_id from cc_inbound_llm_account
     </sql>
 
     <select id="selectInboundLlmList" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO" resultMap="InboundLlmResult">
@@ -79,6 +82,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                     </foreach>
                 </if>
             </if>
+            <if test="fsTenantId != null">and fs_tenant_id = #{fsTenantId}</if>
         </where>
         order by id desc
     </select>
@@ -111,6 +115,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">fs_company_id,</if>
             <if test="callBackUrl != null">call_back_url,</if>
             <if test="fsSceneType != null">fs_scene_type,</if>
+            <if test="fsTenantId != null">fs_tenant_id,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="id != null">#{id},</if>
@@ -128,6 +133,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyId != null">#{companyId},</if>
             <if test="callBackUrl != null">#{callBackUrl},</if>
             <if test="fsSceneType != null">#{fsSceneType},</if>
+            <if test="fsTenantId != null">#{fsTenantId},</if>
         </trim>
     </insert>
 
@@ -205,7 +211,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <!-- 根据音色来源查询音色列表 -->
     <select id="selectVoiceListBySource" parameterType="String" resultMap="VoiceCodeResult">
-        select voice_code, voice_name, voice_source
+        select id,voice_code, voice_name, voice_source,priority
         from cc_tts_aliyun
         where voice_source = #{voiceSource}
         and voice_enabled = 1

+ 30 - 1
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -54,10 +54,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="thirdAccount"    column="third_account"    />
         <result property="clueId"    column="clue_id"    />
         <result property="qwName"    column="qw_name"    />
+        <result property="historicalCommunication"    column="historical_communication"    />
     </resultMap>
 
     <sql id="selectCrmCustomerVo">
-        select customer_id, customer_code, customer_name, mobile, sex, weixin, remark, user_id, create_user_id, receive_user_id, customer_user_id, address,city_ids, location, detail_address, lng, lat, create_time, update_time, status, is_receive, dept_id, is_del, customer_type, receive_time, pool_time, company_id, is_line, source, tags,ext_json,visit_status,register_date,register_link_url,register_desc,register_submit_time,is_pool,register_type,pay_money,buy_count,source_code,push_time,push_code,visit_time,traffic_source,import_type,third_account,clue_id,qw_name from crm_customer
+        select customer_id, customer_code, customer_name, mobile, sex, weixin, remark, user_id, create_user_id, receive_user_id, customer_user_id, address,city_ids, location, detail_address, lng, lat, create_time, update_time, status, is_receive, dept_id, is_del, customer_type, receive_time, pool_time, company_id, is_line, source, tags,ext_json,visit_status,register_date,register_link_url,register_desc,register_submit_time,is_pool,register_type,pay_money,buy_count,source_code,push_time,push_code,visit_time,traffic_source,import_type,third_account,clue_id,qw_name,historical_communication from crm_customer
     </sql>
 
     <select id="selectCrmCustomerList" parameterType="CrmCustomer" resultMap="CrmCustomerResult">
@@ -159,6 +160,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="thirdAccount != null">third_account,</if>
             <if test="clueId != null">clue_id,</if>
             <if test="qwName != null">qw_name,</if>
+            <if test="historicalCommunication != null">historical_communication,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="customerCode != null">#{customerCode},</if>
@@ -209,6 +211,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="thirdAccount != null">#{thirdAccount},</if>
             <if test="clueId != null">#{clueId},</if>
             <if test="qwName != null">#{qwName},</if>
+            <if test="historicalCommunication != null">#{historicalCommunication},</if>
          </trim>
     </insert>
 
@@ -263,6 +266,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="thirdAccount != null">third_account = #{thirdAccount},</if>
             <if test="clueId != null">clue_id = #{clueId},</if>
             <if test="qwName != null">qw_name = #{qwName},</if>
+            <if test="historicalCommunication != null">historical_communication = #{historicalCommunication},</if>
         </trim>
         where customer_id = #{customerId}
     </update>
@@ -522,6 +526,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </trim>
     </insert>
 
+    <insert id="insertBatchCrmCustomer" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="customerId">
+        insert into crm_customer
+        (
+            customer_code, customer_name, mobile, sex, weixin, remark, user_id, create_user_id,
+            receive_user_id, customer_user_id, address, city_ids, location, detail_address, lng, lat,
+            create_time, update_time, status, is_receive, dept_id, is_del, customer_type,
+            receive_time, pool_time, company_id, is_line, source, tags, ext_json, visit_status,
+            register_date, register_link_url, register_desc, register_submit_time, is_pool,
+            register_type, pay_money, buy_count, source_code, push_time, push_code,
+            visit_time, traffic_source, import_type, third_account, clue_id, qw_name, historical_communication
+        )
+        values
+        <foreach collection="list" item="item" separator=",">
+            (
+                #{item.customerCode}, #{item.customerName}, #{item.mobile}, #{item.sex}, #{item.weixin}, #{item.remark}, #{item.userId}, #{item.createUserId},
+                #{item.receiveUserId}, #{item.customerUserId}, #{item.address}, #{item.cityIds}, #{item.location}, #{item.detailAddress}, #{item.lng}, #{item.lat},
+                #{item.createTime}, #{item.updateTime}, #{item.status}, #{item.isReceive}, #{item.deptId}, #{item.isDel}, #{item.customerType},
+                #{item.receiveTime}, #{item.poolTime}, #{item.companyId}, #{item.isLine}, #{item.source}, #{item.tags}, #{item.extJson}, #{item.visitStatus},
+                #{item.registerDate}, #{item.registerLinkUrl}, #{item.registerDesc}, #{item.registerSubmitTime}, #{item.isPool},
+                #{item.registerType}, #{item.payMoney}, #{item.buyCount}, #{item.sourceCode}, #{item.pushTime}, #{item.pushCode},
+                #{item.visitTime}, #{item.trafficSource}, #{item.importType}, #{item.thirdAccount}, #{item.clueId}, #{item.qwName}, #{item.historicalCommunication}
+            )
+        </foreach>
+    </insert>
+
     <select id="selectCrmCustomerInfoById" resultType="com.fs.crm.domain.CrmCustomerInfo">
         SELECT
             id,customer_id,name,sex,age,address,habits,illness_time,body,study,course_status,course,family,family_disease,disease,is_line,

+ 203 - 113
fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java

@@ -6,31 +6,41 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.app.enums.CmdType;
 import com.fs.app.websocket.bean.ResultMsgVo;
 import com.fs.app.websocket.bean.SendMsgVo;
-import com.fs.company.domain.CompanyWxClient;
-import com.fs.company.mapper.CompanyWxClientMapper;
-import com.fs.company.service.CompanyWorkflowEngine;
-import com.fs.company.service.impl.CompanyWxServiceImpl;
-import com.fs.wxcid.domain.CidIpadServer;
-import com.fs.wxcid.mapper.CidIpadServerMapper;
-import com.fs.wxcid.vo.wxvo.*;
+import com.fs.common.config.RedisTenantContext;
+import com.fs.common.core.domain.model.TenantPrincipal;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.CompanyWxClientMapper;
+import com.fs.company.service.CompanyWorkflowEngine;
+import com.fs.company.service.impl.CompanyWxServiceImpl;
+import com.fs.core.config.TenantConfigContext;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
 import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.CidIpadServerMapper;
 import com.fs.wxcid.mapper.WxContactMapper;
 import com.fs.wxcid.service.IWxMsgLogService;
+import com.fs.wxcid.utils.TenantHelper;
+import com.fs.wxcid.vo.wxvo.ContactInfoVo;
+import com.fs.wxcid.vo.wxvo.SyncInfoVo;
+import com.fs.wxcid.vo.wxvo.WxSendResultMsgVo;
 import com.hc.openapi.tool.fastjson.JSON;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.time.LocalDateTime;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -51,6 +61,7 @@ public class WebSocketServer {
     CompanyWxServiceImpl companyWxService = SpringUtils.getBean(CompanyWxServiceImpl.class);
     CidIpadServerMapper cidIpadServerMapper = SpringUtils.getBean(CidIpadServerMapper.class);
     CompanyWorkflowEngine companyWorkflowEngine = SpringUtils.getBean(CompanyWorkflowEngine.class);
+    TenantInfoMapper tenantInfoMapper = SpringUtils.getBean(TenantInfoMapper.class);
 
     //发送消息
     public <T> void sendMessage(Session session, ResultMsgVo<T> data) {
@@ -65,133 +76,164 @@ public class WebSocketServer {
             }
         }
     }
+
     //建立连接成功调用
     @OnOpen
-    public void onOpen(Session session, @PathParam(value = "wxId") String wxId) {
-        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
-        if(companyWxAccount == null){
-            sendMessage(session, ResultMsgVo.error("未找到对应微信数据"));
-            return;
+    public void onOpen(Session session, @PathParam(value = "wxId") String wxId, @PathParam(value = "tenantCode") String tenantCode) {
+        Boolean switchBool = switchDataBaseByTenantCode(tenantCode);
+        try {
+            CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+            if (companyWxAccount == null) {
+                sendMessage(session, ResultMsgVo.error("未找到对应微信数据"));
+                return;
+            }
+            sessionPools.put(wxId, session);
+            companyWxAccount.setLoginStatus(1);
+            companyWxAccount.setLoginTime(LocalDateTime.now());
+            accountMapper.updateById(companyWxAccount);
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("remark", companyWxAccount.getRemark());
+            sendMessage(session, ResultMsgVo.<JSONObject>builder().cmd(CmdType.INIT_REMARK).data(jsonObject).build());
+            log.info("{}加入webSocket!当前人数为{}", wxId, sessionPools.size());
+        } catch (Exception e) {
+            log.error("onOpenErr:{}", e.getMessage());
+        } finally {
+            if (switchBool) {
+                finalHandle();
+            }
         }
-        sessionPools.put(wxId, session);
-        companyWxAccount.setLoginStatus(1);
-        companyWxAccount.setLoginTime(LocalDateTime.now());
-        accountMapper.updateById(companyWxAccount);
-        JSONObject jsonObject = new JSONObject();
-        jsonObject.put("remark", companyWxAccount.getRemark());
-        sendMessage(session, ResultMsgVo.<JSONObject>builder().cmd(CmdType.INIT_REMARK).data(jsonObject).build());
-        log.info("{}加入webSocket!当前人数为{}", wxId, sessionPools.size());
+
     }
 
     //关闭连接时调用
     @OnClose
-    public void onClose(@PathParam(value = "wxId") String wxId) {
-        sessionPools.remove(wxId);
-        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
-        if(companyWxAccount != null){
-            companyWxAccount.setLoginStatus(0);
-            companyWxAccount.setOutTime(LocalDateTime.now());
-            companyWxAccount.setOutRemark("连接断开");
-            accountMapper.updateById(companyWxAccount);
+    public void onClose(@PathParam(value = "wxId") String wxId, @PathParam(value = "tenantCode") String tenantCode) {
+        Boolean switchBool = switchDataBaseByTenantCode(tenantCode);
+        try {
+            sessionPools.remove(wxId);
+            CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+            if (companyWxAccount != null) {
+                companyWxAccount.setLoginStatus(0);
+                companyWxAccount.setOutTime(LocalDateTime.now());
+                companyWxAccount.setOutRemark("连接断开");
+                accountMapper.updateById(companyWxAccount);
+            }
+            log.info("{}断开webSocket连接!当前人数为{}", wxId, sessionPools.size());
+        } catch (Exception e) {
+            log.error("onCloseErr:{}", e.getMessage());
+        } finally {
+            if (switchBool) {
+                finalHandle();
+            }
         }
-        log.info("{}断开webSocket连接!当前人数为{}", wxId, sessionPools.size());
     }
 
     //收到客户端信息
     @OnMessage
-    public void onMessage(String message, @PathParam(value = "wxId") String wxId) {
-        SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
-        if(msg.getType() == 0){
-            return;
-        }
-        Session session = sessionPools.get(wxId);
-        if(session == null){
-            log.error("参数异常:{}", wxId);
-            return;
-        }
-        log.info("收到数据:{}", msg.getCmd());
-        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
-        if(companyWxAccount == null){
-            log.error("未找到对应账号:{}", wxId);
-            return;
-        }
+    public void onMessage(String message, @PathParam(value = "wxId") String wxId, @PathParam(value = "tenantCode") String tenantCode) {
+        Boolean switchBool = switchDataBaseByTenantCode(tenantCode);
         try {
-            switch (msg.getCmd()) {
-                case HEARTBEAT:
-                    log.info("接收心跳:{}", wxId);
-                    break;
-                case SYNC_CONTACT_PERSON:
-                    ContactInfoVo contactInfoVo = JSON.parseObject(msg.getDataJson(), ContactInfoVo.class);
-                    if(contactInfoVo == null || StringUtils.isEmpty(contactInfoVo.getRemark())){
-                        log.error("{}同步数据失败,数据缺失:{}", wxId, contactInfoVo);
-                        return;
-                    }
-                    WxContact contact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", contactInfoVo.getRemark()));
-                    if(contact != null){
-                        contact.setNickName(contactInfoVo.getNickName());
-                        contact.setCity(contactInfoVo.getAddress());
-                        contact.setUserName(contactInfoVo.getWxNo());
-                        contact.setUpdateTime(new Date());
-                        wxContactMapper.updateById(contact);
-                    }else{
-                        WxContact contact1 = new WxContact();
-                        contact1.setUserName(contactInfoVo.getWxNo());
-                        contact1.setNickName(contactInfoVo.getNickName());
-                        contact1.setCity(contactInfoVo.getAddress());
-                        contact1.setAccountId(companyWxAccount.getId());
-                        contact1.setCompanyId(companyWxAccount.getCompanyId());
-                        contact1.setCompanyUserId(companyWxAccount.getCompanyUserId());
-                        contact1.setRemark(contactInfoVo.getRemark());
-                        contact1.setCreateTime(new Date());
-                        contact1.setUpdateTime(new Date());
-                        wxContactMapper.insert(contact1);
-                    }
-                    break;
-                case SEND_MSG:
-                    log.info("发送返回:{}", msg);
-                    wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
-                    break;
-                case SEND_RESULT:
-                    log.info("接收消息:{}", msg);
-                    wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
-                    break;
-                case SYNC_INFO:
-                    SyncInfoVo syncInfoVo = JSON.parseObject(msg.getDataJson(), SyncInfoVo.class);
-                    companyWxAccount.setHeadImgUrl(syncInfoVo.getImg());
-                    companyWxAccount.setPhone(syncInfoVo.getPhone());
-                    accountMapper.updateById(companyWxAccount);
-                    break;
-                case ADD_WX_RESULT:
-                    com.fs.wxcid.vo.wxvo.AddResultWxVo addResultWxVo = JSON.parseObject(msg.getDataJson(), com.fs.wxcid.vo.wxvo.AddResultWxVo.class);
-                    log.info("接收到加好友回调:{}", addResultWxVo);
-                    WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", addResultWxVo.getRemark()).eq("friends", 0));
-                    log.info("更新联系人:{}", wxContact);
-                    wxContact.setFriends(1);
-                    wxContact.setAlias(addResultWxVo.getWxid());
-                    wxContactMapper.updateById(wxContact);
-                    List<CompanyWxClient> clients = companyWxClientMapper.selectWxV2(companyWxAccount.getId(), wxContact.getPhone());
-                    log.info("更新联系人2:{}", clients);
-                    if(clients != null){
-                        clients.parallelStream().forEach(e -> {
-                            e.setIsAdd(1);
-                            e.setRemark(addResultWxVo.getRemark());
-                            e.setWxName(addResultWxVo.getUserName());
-                            e.setSuccessAddTime(LocalDateTime.now());
-                            companyWxClientMapper.updateById(e);
-                            companyWxService.triggerWorkflowOnAddWxSuccess(e.getId());
-                        });
-                    }
+            SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
+            if (msg.getType() == 0) {
+                return;
+            }
+            Session session = sessionPools.get(wxId);
+            if (session == null) {
+                log.error("参数异常:{}", wxId);
+                return;
+            }
+            log.info("收到数据:{}", msg.getCmd());
+            CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+            if (companyWxAccount == null) {
+                log.error("未找到对应账号:{}", wxId);
+                return;
+            }
+            try {
+                switch (msg.getCmd()) {
+                    case HEARTBEAT:
+                        log.info("接收心跳:{}", wxId);
+                        break;
+                    case SYNC_CONTACT_PERSON:
+                        ContactInfoVo contactInfoVo = JSON.parseObject(msg.getDataJson(), ContactInfoVo.class);
+                        if (contactInfoVo == null || StringUtils.isEmpty(contactInfoVo.getRemark())) {
+                            log.error("{}同步数据失败,数据缺失:{}", wxId, contactInfoVo);
+                            return;
+                        }
+                        WxContact contact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", contactInfoVo.getRemark()));
+                        if (contact != null) {
+                            contact.setNickName(contactInfoVo.getNickName());
+                            contact.setCity(contactInfoVo.getAddress());
+                            contact.setUserName(contactInfoVo.getWxNo());
+                            contact.setUpdateTime(new Date());
+                            wxContactMapper.updateById(contact);
+                        } else {
+                            WxContact contact1 = new WxContact();
+                            contact1.setUserName(contactInfoVo.getWxNo());
+                            contact1.setNickName(contactInfoVo.getNickName());
+                            contact1.setCity(contactInfoVo.getAddress());
+                            contact1.setAccountId(companyWxAccount.getId());
+                            contact1.setCompanyId(companyWxAccount.getCompanyId());
+                            contact1.setCompanyUserId(companyWxAccount.getCompanyUserId());
+                            contact1.setRemark(contactInfoVo.getRemark());
+                            contact1.setCreateTime(new Date());
+                            contact1.setUpdateTime(new Date());
+                            wxContactMapper.insert(contact1);
+                        }
+                        break;
+                    case SEND_MSG:
+                        log.info("发送返回:{}", msg);
+                        wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
+                        break;
+                    case SEND_RESULT:
+                        log.info("接收消息:{}", msg);
+                        wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
+                        break;
+                    case SYNC_INFO:
+                        SyncInfoVo syncInfoVo = JSON.parseObject(msg.getDataJson(), SyncInfoVo.class);
+                        companyWxAccount.setHeadImgUrl(syncInfoVo.getImg());
+                        companyWxAccount.setPhone(syncInfoVo.getPhone());
+                        accountMapper.updateById(companyWxAccount);
+                        break;
+                    case ADD_WX_RESULT:
+                        com.fs.wxcid.vo.wxvo.AddResultWxVo addResultWxVo = JSON.parseObject(msg.getDataJson(), com.fs.wxcid.vo.wxvo.AddResultWxVo.class);
+                        log.info("接收到加好友回调:{}", addResultWxVo);
+                        WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", addResultWxVo.getRemark()).eq("friends", 0));
+                        log.info("更新联系人:{}", wxContact);
+                        wxContact.setFriends(1);
+                        wxContact.setAlias(addResultWxVo.getWxid());
+                        wxContactMapper.updateById(wxContact);
+                        List<CompanyWxClient> clients = companyWxClientMapper.selectWxV2(companyWxAccount.getId(), wxContact.getPhone());
+                        log.info("更新联系人2:{}", clients);
+                        if (clients != null) {
+                            clients.parallelStream().forEach(e -> {
+                                e.setIsAdd(1);
+                                e.setRemark(addResultWxVo.getRemark());
+                                e.setWxName(addResultWxVo.getUserName());
+                                e.setSuccessAddTime(LocalDateTime.now());
+                                companyWxClientMapper.updateById(e);
+                                companyWxService.triggerWorkflowOnAddWxSuccess(e.getId());
+                            });
+                        }
 //                    if(null != addResultWxVo && StringUtils.isNotBlank(addResultWxVo.getBizJson())){
 //                        JSONObject jsonObject = JSONObject.parseObject(addResultWxVo.getBizJson());
 //                        jsonObject.put("remark",addResultWxVo.getRemark());
 //                        companyWorkflowEngine.addWxSuccess(jsonObject);
 //                    }
-                    break;
+                        break;
 
+                }
+            } catch (Exception e) {
+                log.error("发生错误;{}", e.getMessage());
             }
         } catch (Exception e) {
-            log.error("发生错误;{}", e.getMessage());
+            log.error("onMessageErr:{}", e.getMessage());
         }
+        finally {
+            if (switchBool) {
+                finalHandle();
+            }
+        }
+
 
     }
 
@@ -201,4 +243,52 @@ public class WebSocketServer {
         log.error("发生错误;{}", throwable.getMessage());
         throwable.printStackTrace();
     }
+
+    /**
+     * 根据租户编码切换数据源
+     *
+     * @param tenantCode
+     */
+    public Boolean switchDataBaseByTenantCode(String tenantCode) {
+        if (StringUtils.isBlank(tenantCode)) {
+            log.error("未找到对应租户:{}", tenantCode);
+            return Boolean.FALSE;
+        }
+        try {
+            TenantInfo tenantInfo = tenantInfoMapper.getTenByCode(tenantCode);
+            Object manager = SpringUtils.getBean("tenantDataSourceManager");
+            Method method = manager.getClass().getMethod("ensureSwitchByTenantId", Long.class);
+            method.invoke(manager, tenantInfo.getId());
+            // 设置租户到 SecurityContext,供 TenantKeyRedisSerializer 自动为 Redis Key 加 tenantid 前缀
+            SecurityContextHolder.getContext().setAuthentication(
+                    new UsernamePasswordAuthenticationToken(
+                            new TenantPrincipal(TenantHelper.getTenantId()),
+                            null,
+                            Collections.emptyList()
+                    )
+            );
+            // 切换 Redis 租户上下文
+            RedisTenantContext.setTenantId(TenantHelper.getTenantId());
+            return Boolean.TRUE;
+        } catch (Exception e) {
+            log.error("callerResult4EasyCall 切换租户数据源失败: tenantId={}", TenantHelper.getTenantId(), e);
+            return Boolean.FALSE;
+        }
+    }
+
+
+    public void finalHandle() {
+        try {
+            TenantConfigContext.clear();
+            SecurityContextHolder.clearContext();
+            Object manager = SpringUtils.getBean("tenantDataSourceManager");
+            Method method = manager.getClass().getMethod("clear");
+            method.invoke(manager);
+            TenantHelper.clearTenantId();
+            RedisTenantContext.clear();
+        } catch (Exception e) {
+            log.error("SOP异步任务清理租户数据源失败", e);
+        }
+    }
+
 }

+ 113 - 0
fs-wx-api/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -0,0 +1,113 @@
+package com.fs.framework.datasource;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 租户数据源管理,SaaS 模式下定时任务按租户切库时使用。
+ */
+@Component
+@Slf4j
+public class TenantDataSourceManager {
+
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+
+    @Resource
+    private TenantInfoService tenantInfoService;
+
+    private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
+
+    public void switchTenant(TenantInfo tenantInfo) {
+        String tenantKey = buildTenantKey(tenantInfo.getId());
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            synchronized (this) {
+                if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+                    DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                }
+            }
+        }
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+        ds.setInitialSize(5);
+        ds.setMinIdle(10);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+        return ds;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<Object, DataSource> getResolvedDataSources() {
+        try {
+            Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.class
+                    .getDeclaredField("resolvedDataSources");
+            field.setAccessible(true);
+            return (Map<Object, DataSource>) field.get(dynamicDataSource);
+        } catch (Exception e) {
+            throw new IllegalStateException("获取 resolvedDataSources 失败", e);
+        }
+    }
+    /**
+     * 根据租户ID确保数据源已注册并切换(用于 Filter/拦截器等非登录场景)
+     * 解决 JVM 重启后 TENANT_DS_CACHE 被清空,导致 resolvedDataSources 中找不到租户数据源的问题
+     *
+     * @param tenantId 租户ID
+     */
+    public void ensureSwitchByTenantId(Long tenantId) {
+        String tenantKey = buildTenantKey(tenantId);
+
+        if (TENANT_DS_CACHE.containsKey(tenantKey)) {
+            DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+            log.debug("[TenantDS] 数据源已缓存,直接切换: {}", tenantKey);
+            return;
+        }
+
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            TenantInfo tenantInfo = tenantInfoService.getById(tenantId);
+            if (tenantInfo == null) {
+                log.warn("[TenantDS] 租户ID={} 在主库中不存在,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            if (!tenantInfo.getStatus().equals(1)) {
+                log.warn("[TenantDS] 租户ID={} 已禁用,回退到主库", tenantId);
+                DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+                return;
+            }
+            switchTenant(tenantInfo);
+            log.info("[TenantDS] 动态注册并切换数据源: key={}, url={}", tenantKey, tenantInfo.getDbUrl());
+        } catch (Exception e) {
+            log.error("[TenantDS] 动态注册租户数据源失败, tenantId={}, 回退到主库", tenantId, e);
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+}