Kaynağa Gözat

Merge remote-tracking branch 'origin/master'

yjwang 2 hafta önce
ebeveyn
işleme
b4028df7db
24 değiştirilmiş dosya ile 1007 ekleme ve 106 silme
  1. 79 26
      fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java
  2. 1 1
      fs-ai-call-task/src/main/java/com/fs/app/task/Task.java
  3. 59 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java
  4. 0 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java
  5. 38 0
      fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java
  6. 57 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcTtsAliyunMapper.java
  7. 48 0
      fs-service/src/main/java/com/fs/aicall/service/ICcTtsAliyunService.java
  8. 56 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcTtsAliyunServiceImpl.java
  9. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  10. 2 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  11. 36 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java
  12. 1 1
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  13. 391 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java
  14. 6 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  15. 30 11
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  16. 1 2
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java
  17. 96 56
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  18. 6 5
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  19. 1 1
      fs-service/src/main/java/com/fs/wxcid/threadExecutor/generalCustomerExecutor.java
  20. 87 0
      fs-service/src/main/resources/mapper/aicall/CcTtsAliyunMapper.xml
  21. 4 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  22. 1 1
      fs-service/src/main/resources/mapper/company/CompanyWorkflowMapper.xml
  23. 3 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerAnalyzeMapper.xml
  24. 2 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

+ 79 - 26
fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.*;
@@ -94,41 +95,93 @@ public class CrmCustomerAiProcessingTask {
         String threadName = Thread.currentThread().getName();
         long batchStartTime = System.currentTimeMillis();
 
-//        try {
+        try {
             log.info("线程 {} 开始处理批次, 数据量: {}", threadName, batch.size());
 
-            // 示例:处理每条数据
-            for (Map<String, String> data : batch) {
-                // 获取数据
-                Long customerId = Long.valueOf(data.get("customerId"));
-                String dataJson = data.get("data");
-                Long logId = Long.valueOf(data.get("logId"));
-                //todo 业务!!!!!!2.流失风险等级8.9.客户意向度 //都要异步处理
-                //客户画像
-                crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId,dataJson,logId);
-                //沟通总结
-                crmCustomerAnalyzeService.aiCommunicationSummary(customerId,dataJson,logId);
-                //沟通摘要
-                crmCustomerAnalyzeService.aiCommunicationAbstract(customerId,dataJson,logId);
-//                //流失风险等级 //ai暂时未提供,等ai提供
-                crmCustomerAnalyzeService.aiAttritionLevel(customerId,dataJson,logId);
-                //客户关注点
-                crmCustomerAnalyzeService.aiCustomerFocus(customerId,dataJson,logId);
-                //客户意向度
-                crmCustomerAnalyzeService.aiIntentionDegree(customerId,dataJson,logId);
+            List<CompletableFuture<Void>> customerFutures = batch.stream()
+                    .map(data -> CompletableFuture.runAsync(() -> {
+                        processSingleCustomer(data, successCount, failCount);
+                    }, executorService))
+                    .collect(Collectors.toList());
 
-            }
+            // 等待该批次内所有客户处理完成
+            CompletableFuture.allOf(customerFutures.toArray(new CompletableFuture[0]))
+                    .join();
+            // 示例:处理每条数据
+//            for (Map<String, String> data : batch) {
+//                // 获取数据
+//                Long customerId = Long.valueOf(data.get("customerId"));
+//                String dataJson = data.get("data");
+//                Long logId = Long.valueOf(data.get("logId"));
+//                //客户画像1
+//                crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId,dataJson,logId);
+//                //沟通总结
+//                crmCustomerAnalyzeService.aiCommunicationSummary(customerId,dataJson,logId);
+//                //沟通摘要
+//                crmCustomerAnalyzeService.aiCommunicationAbstract(customerId,dataJson,logId);
+//                //流失风险等级
+//                crmCustomerAnalyzeService.aiAttritionLevel(customerId,dataJson,logId);
+//                //客户关注点
+//                crmCustomerAnalyzeService.aiCustomerFocus(customerId,dataJson,logId);
+//                //客户意向度
+//                crmCustomerAnalyzeService.aiIntentionDegree(customerId,dataJson,logId);
+//
+//            }
 
             long costTime = System.currentTimeMillis() - batchStartTime;
             successCount.addAndGet(batch.size());
             log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
                     threadName, batch.size(), costTime);
 
-//        } catch (Exception e) {
-//            failCount.addAndGet(batch.size());
-//            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, batch.size(), e);
-//            throw new RuntimeException("批次处理失败", e);
-//        }
+        } catch (Exception e) {
+            failCount.addAndGet(batch.size());
+            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, batch.size(), e);
+            throw new RuntimeException("批次处理失败", e);
+        }
+    }
+    /**
+     * 处理单个客户的AI分析(6个接口并行)
+     */
+    private void processSingleCustomer(Map<String, String> data,
+                                       AtomicInteger successCount,
+                                       AtomicInteger failCount) {
+        try {
+            Long customerId = Long.valueOf(data.get("customerId"));
+            String dataJson = data.get("data");
+            Long logId = Long.valueOf(data.get("logId"));
+
+            long startTime = System.currentTimeMillis();
+
+            // 并行调用6个AI分析接口
+            List<CompletableFuture<Void>> aiFutures = Arrays.asList(
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId, dataJson, logId), executorService),
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiCommunicationSummary(customerId, dataJson, logId), executorService),
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiCommunicationAbstract(customerId, dataJson, logId), executorService),
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiAttritionLevel(customerId, dataJson, logId), executorService),
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiCustomerFocus(customerId, dataJson, logId), executorService),
+                    CompletableFuture.runAsync(() ->
+                            crmCustomerAnalyzeService.aiIntentionDegree(customerId, dataJson, logId), executorService)
+            );
+
+            // 等待该客户的所有AI分析完成
+            CompletableFuture<Void> allAiFutures = CompletableFuture.allOf(
+                    aiFutures.toArray(new CompletableFuture[aiFutures.size()])
+            );
+            allAiFutures.get(30, TimeUnit.SECONDS);
+            long costTime = System.currentTimeMillis() - startTime;
+            successCount.incrementAndGet();
+            log.info("客户 {} 的AI分析完成, 耗时: {}ms", customerId, costTime);
+
+        } catch (Exception e) {
+            failCount.incrementAndGet();
+            log.error("处理客户数据失败, customerId: {}, logId: {}",
+                    data.get("customerId"), data.get("logId"), e);
+        }
     }
     /**
      * 处理失败的数据

+ 1 - 1
fs-ai-call-task/src/main/java/com/fs/app/task/Task.java

@@ -38,7 +38,7 @@ public class Task {
 //    }
 
 
-    @Scheduled(cron = "0 0/1 * * * ?")
+    @Scheduled(cron = "0/30 * * * * ?")
     public void cidWorkflowRun(){
         taskService.cidWorkflowCallRun();
     }

+ 59 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java

@@ -0,0 +1,59 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.service.ICompanyVoiceCloneService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 豆包声音克隆 Controller
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/company/voiceClone")
+public class CompanyVoiceCloneController extends BaseController {
+
+    @Autowired
+    private ICompanyVoiceCloneService companyVoiceCloneService;
+
+    /**
+     * 上传音频文件并训练声音克隆音色
+     *
+     * @param file       音频文件(wav/mp3/ogg/m4a/aac)
+     * @param voiceName  音色名称
+     * @param speakerId  声音ID
+     * @param language   语种(0-中文, 1-英文, 2-日语, 3-西班牙语, 4-印尼语, 5-葡萄牙语, 6-德语, 7-法语)
+     * @param modelType  模型类型(1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0)
+     */
+    @Log(title = "声音克隆-上传训练", businessType = BusinessType.IMPORT)
+    @PostMapping("/uploadAndTrain")
+    public AjaxResult uploadAndTrain(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("voice_name") String voiceName,
+            @RequestParam("speaker_id") String speakerId,
+            @RequestParam(value = "language", defaultValue = "0") Integer language,
+            @RequestParam(value = "model_type", defaultValue = "2") Integer modelType) {
+        return companyVoiceCloneService.uploadAndTrain(voiceName, speakerId, language, modelType, file);
+    }
+
+    /**
+     * TTS 语音合成测试
+     *
+     * @param speakerId  声音ID
+     * @param language   语种
+     * @param text       要合成的文本
+     */
+    @Log(title = "声音克隆-TTS测试", businessType = BusinessType.OTHER)
+    @PostMapping("/doubaoTtsTest")
+    public AjaxResult doubaoTtsTest(
+            @RequestParam("speakerId") String speakerId,
+            @RequestParam(value = "language", defaultValue = "0") Integer language,
+            @RequestParam("text") String text) {
+        return companyVoiceCloneService.doubaoTtsTest(speakerId, language, text);
+    }
+}

+ 0 - 1
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java

@@ -120,7 +120,6 @@ public class CrmCustomerAnalyzeController extends BaseController
      * 话术润色
      */
     @PreAuthorize("@ss.hasPermi('crm:analyze:polishingScript')")
-    @Log(title = "话术润色", businessType = BusinessType.INSERT)
     @PostMapping("/polishingScript")
     public R polishingScript(@RequestBody PolishingScriptParam param)
     {

+ 38 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java

@@ -0,0 +1,38 @@
+package com.fs.aicall.domain;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 音色表对象 cc_tts_aliyun(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+@Data
+@Accessors(chain = true)
+public class CcTtsAliyun implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** tts发音人名称 */
+    private String voiceName;
+
+    /** tts发音人代码(speaker_id) */
+    private String voiceCode;
+
+    /** 是否启用 */
+    private Integer voiceEnabled;
+
+    /** 声音源: aliyun_tts、doubao_vcl_tts 等 */
+    private String voiceSource;
+
+    /** 优先级 */
+    private Integer priority;
+
+    /** 供应商: aliyun、doubao 等 */
+    private String provider;
+}

+ 57 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcTtsAliyunMapper.java

@@ -0,0 +1,57 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 音色表 Mapper 接口(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+public interface CcTtsAliyunMapper {
+
+    /**
+     * 根据主键查询音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    CcTtsAliyun selectCcTtsAliyunById(Integer id);
+
+    /**
+     * 根据音色代码查询
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode);
+
+    /**
+     * 查询音色列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 新增音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 修改音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 删除音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteCcTtsAliyunById(Integer id);
+
+    /**
+     * 批量删除音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteCcTtsAliyunByIds(String ids);
+}

+ 48 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcTtsAliyunService.java

@@ -0,0 +1,48 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+
+import java.util.List;
+
+/**
+ * 音色表 Service 接口(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+public interface ICcTtsAliyunService {
+
+    /**
+     * 根据主键查询音色
+     */
+    CcTtsAliyun selectCcTtsAliyunById(Integer id);
+
+    /**
+     * 根据音色代码查询
+     */
+    CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode);
+
+    /**
+     * 查询音色列表
+     */
+    List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 新增音色
+     */
+    int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 修改音色
+     */
+    int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 删除音色
+     */
+    int deleteCcTtsAliyunById(Integer id);
+
+    /**
+     * 批量删除音色
+     */
+    int deleteCcTtsAliyunByIds(String ids);
+}

+ 56 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcTtsAliyunServiceImpl.java

@@ -0,0 +1,56 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.aicall.mapper.CcTtsAliyunMapper;
+import com.fs.aicall.service.ICcTtsAliyunService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 音色表 Service 实现(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+@Service
+public class CcTtsAliyunServiceImpl implements ICcTtsAliyunService {
+
+    @Autowired
+    private CcTtsAliyunMapper ccTtsAliyunMapper;
+
+    @Override
+    public CcTtsAliyun selectCcTtsAliyunById(Integer id) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunById(id);
+    }
+
+    @Override
+    public CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunByVoiceCode(voiceCode);
+    }
+
+    @Override
+    public List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunList(ccTtsAliyun);
+    }
+
+    @Override
+    public int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.insertCcTtsAliyun(ccTtsAliyun);
+    }
+
+    @Override
+    public int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.updateCcTtsAliyun(ccTtsAliyun);
+    }
+
+    @Override
+    public int deleteCcTtsAliyunById(Integer id) {
+        return ccTtsAliyunMapper.deleteCcTtsAliyunById(id);
+    }
+
+    @Override
+    public int deleteCcTtsAliyunByIds(String ids) {
+        return ccTtsAliyunMapper.deleteCcTtsAliyunByIds(ids);
+    }
+}

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

@@ -82,5 +82,7 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
 
     List<DictVO> getDictDataList(@Param("dictType") String dictType);
 
+    DictVO getDictDataByTypeAndValue(@Param("value") String val, @Param("dictType") String dictType);
+
     List<CompanyVoiceRobotic> selectSceneTaskByCompanyIdAndType(@Param("companyId") Long companyId, @Param("sceneType") Integer sceneType);
 }

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

@@ -165,4 +165,6 @@ public class EntryCustomerParam {
     //投流id
     private String traceId;
 
+    private String remark;
+
 }

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

@@ -0,0 +1,36 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 豆包声音克隆 Service 接口
+ *
+ * @author fs
+ */
+public interface ICompanyVoiceCloneService {
+
+    /**
+     * 上传音频并训练声音克隆音色
+     *
+     * @param voiceName 音色名称
+     * @param speakerId 声音ID
+     * @param language  语种 (0-中文, 1-英文...)
+     * @param modelType 模型类型 (1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0)
+     * @param file      音频文件
+     * @return 操作结果
+     */
+    AjaxResult uploadAndTrain(String voiceName, String speakerId,
+                              Integer language, Integer modelType,
+                              MultipartFile file);
+
+    /**
+     * TTS 语音合成测试
+     *
+     * @param speakerId 声音ID
+     * @param language  语种
+     * @param text      要合成的文本
+     * @return 操作结果(含 base64 音频数据)
+     */
+    AjaxResult doubaoTtsTest(String speakerId, Integer language, String text);
+}

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

@@ -12,5 +12,5 @@ public interface IGeneralCustomerEntryService {
 
 //    R entryCustomer(String param);
 
-    String entryCustomer(EntryCustomerParam param);
+    void entryCustomer(EntryCustomerParam param);
 }

+ 391 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java

@@ -0,0 +1,391 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.aicall.service.ICcParamsService;
+import com.fs.aicall.service.ICcTtsAliyunService;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.service.ICompanyVoiceCloneService;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 豆包声音克隆 Service 实现
+ * <p>
+ * 参照 DoubaoVclController 实现:
+ * 1. uploadAndTrain: 从 EASYCALL 读取账号,HTTP 调用豆包训练接口,成功后写入音色表
+ * 2. doubaoTtsTest: HTTP 调用豆包 TTS 接口,返回 base64 音频数据
+ * </p>
+ *
+ * @author fs
+ */
+@Service
+@Slf4j
+public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
+
+    private static final String HOST = "https://openspeech.bytedance.com";
+    private static final String TRAIN_URL = HOST + "/api/v1/mega_tts/audio/upload";
+    private static final String STATUS_URL = HOST + "/api/v1/mega_tts/status";
+    private static final String TTS_URL = HOST + "/api/v1/tts";
+
+    private static final String DOUBAO_ACCOUNT_PARAM_CODE = "doubao-tts-account-json";
+
+    private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
+
+    /** 错误码映射 */
+    private static final Map<Integer, String> ERROR_MAP = new HashMap<Integer, String>() {{
+        put(1001, "BadRequestError: 请求参数有误");
+        put(1101, "AudioUploadError: 音频上传失败");
+        put(1102, "ASRError: ASR(语音识别成文字)转写失败");
+        put(1103, "SIDError: SID声纹检测失败");
+        put(1104, "SIDFailError: 声纹检测未通过,声纹跟名人相似度过高");
+        put(1105, "GetAudioDataError: 获取音频数据失败");
+        put(1106, "SpeakerIDDuplicationError: SpeakerID重复");
+        put(1107, "SpeakerIDNotFoundError: SpeakerID未找到");
+        put(1108, "AudioConvertError: 音频转码失败");
+        put(1109, "WERError: wer检测错误,上传音频与请求携带文本对比字错率过高");
+        put(1111, "AEEDerror: aed检测错误,通常由于音频不包含说话声");
+        put(1112, "SNRError: SNR检测错误,通常由于信噪比过高");
+        put(1113, "DenoiseError: 降噪处理失败");
+        put(1114, "AudioQualityError: 音频质量低,降噪失败");
+        put(1122, "ASRNoSpeakerError: 未检测到人声");
+        put(1123, "MaxTrainNumLimitReached: 上传接口已经达到次数限制,目前同一个音色支持10次上传");
+        put(403, "ParameterError: 参数错误:请检查 声音ID、app_id、access_token 是否正确、检查 声音ID 是否和模型类型匹配?(也可能没有开通相关服务)");
+    }};
+
+    @Autowired
+    private ICcParamsService ccParamsService;
+
+    @Autowired
+    private ICcTtsAliyunService ccTtsAliyunService;
+
+    /**
+     * 上传并训练
+     * @param voiceName 音色名称
+     * @param speakerId 声音ID
+     * @param language  语种 (0-中文, 1-英文...)
+     * @param modelType 模型类型 (1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0)
+     * @param file      音频文件
+     * @return
+     */
+    @Override
+    public AjaxResult uploadAndTrain(String voiceName, String speakerId,
+                                     Integer language, Integer modelType,
+                                     MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return AjaxResult.error("请选择音频文件");
+        }
+        if (StringUtils.isEmpty(voiceName)) {
+            return AjaxResult.error("请填写音色名称");
+        }
+        if (StringUtils.isEmpty(speakerId)) {
+            return AjaxResult.error("请填写声音ID");
+        }
+
+        // 从 EASYCALL 数据源读取豆包账号配置
+        String ttsAccountJson = ccParamsService.getParamValueByCode(DOUBAO_ACCOUNT_PARAM_CODE, "{}");
+        JSONObject paramValues = JSONObject.parseObject(ttsAccountJson);
+        String appid = paramValues.getString("app_id");
+        String token = paramValues.getString("access_token");
+
+        if (StringUtils.isEmpty(appid) || StringUtils.isEmpty(token)) {
+            return AjaxResult.error("豆包账号未配置,请在系统参数中设置 " + DOUBAO_ACCOUNT_PARAM_CODE);
+        }
+
+        try {
+            // 获取音频格式和字节数据
+            String originalFilename = file.getOriginalFilename();
+            int strLen = originalFilename != null ? originalFilename.length() : 0;
+            String audioFormat = strLen > 3 ? originalFilename.substring(strLen - 3, strLen) : "wav";
+            byte[] audioBytes = file.getBytes();
+
+            // 调用豆包训练接口
+            String response = train(appid, token, audioFormat, audioBytes, speakerId,
+                    String.valueOf(language != null ? language : 0),
+                    String.valueOf(modelType != null ? modelType : 2));
+
+            if (!StringUtils.isEmpty(response)) {
+                JSONObject jsonObject = JSON.parseObject(response);
+                JSONObject baseResp = jsonObject.getJSONObject("BaseResp");
+                if (baseResp == null) {
+                    return AjaxResult.error("响应格式异常:" + response);
+                }
+                int statusCode = baseResp.getInteger("StatusCode");
+
+                if (statusCode == 0) {
+                    // 训练成功,写入音色表
+                    addSpeakerId(voiceName, speakerId);
+
+                    // 查询训练状态
+                    boolean modelReady = getStatus(appid, token, speakerId);
+                    String tips = modelReady ? "模型已就绪" : "模型训练中,请稍后查询状态";
+                    return AjaxResult.success("上传训练成功!\n" + tips);
+                } else {
+                    String errDetails = baseResp.getString("StatusMessage");
+                    String errMsg = ERROR_MAP.get(statusCode);
+                    if (!StringUtils.isEmpty(errMsg)) {
+                        return AjaxResult.error(errMsg + "\n" + errDetails);
+                    } else {
+                        return AjaxResult.error("未知错误!\n" + response);
+                    }
+                }
+            } else {
+                return AjaxResult.error("响应为空!");
+            }
+        } catch (IOException e) {
+            log.error("声音克隆上传训练异常,speakerId={}", speakerId, e);
+            return AjaxResult.error("服务器内部错误:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 调用豆包训练接口
+     * @param appid
+     * @param token
+     * @param audioFormat
+     * @param audioBytes
+     * @param spkId
+     * @param language
+     * @param modelType
+     * @return
+     * @throws IOException
+     */
+    private String train(String appid, String token, String audioFormat, byte[] audioBytes,
+                         String spkId, String language, String modelType) throws IOException {
+        String result;
+        OkHttpClient client = createHttpClient();
+
+        // 编码音频文件
+        String encodedData = Base64.getEncoder().encodeToString(audioBytes);
+
+        JSONArray audios = new JSONArray();
+        JSONObject audioObj = new JSONObject();
+        audioObj.put("audio_bytes", encodedData);
+        audioObj.put("audio_format", audioFormat);
+        audios.add(audioObj);
+
+        // 构建请求体
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("appid", appid);
+        requestBody.put("speaker_id", spkId);
+        requestBody.put("audios", audios);
+        requestBody.put("source", 2);
+        requestBody.put("language", Integer.parseInt(language));
+        requestBody.put("model_type", Integer.parseInt(modelType));
+
+        RequestBody body = RequestBody.create(JSON_MEDIA_TYPE, requestBody.toString());
+
+        // 根据 modelType 决定 Resource-Id
+        String resourceId = "seed-icl-1.0";
+        if (Integer.parseInt(modelType) == 4) {
+            resourceId = "seed-icl-2.0";
+        }
+
+        Request request = new Request.Builder()
+                .url(TRAIN_URL)
+                .post(body)
+                .addHeader("Content-Type", "application/json")
+                .addHeader("Authorization", "Bearer;" + token)
+                .addHeader("Resource-Id", resourceId)
+                .build();
+
+        try (Response response = client.newCall(request).execute()) {
+            log.info("doubaovcl train http response status code {}", response.code());
+            String responseStr = response.body().string();
+            log.info("doubaovcl train response: {}", responseStr);
+
+            if (response.code() == 403) {
+                result = "{\"BaseResp\":{\"StatusCode\": 403 }}";
+            } else {
+                result = responseStr;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 查询训练状态
+     * @param appid
+     * @param token
+     * @param spkId
+     * @return
+     */
+    private boolean getStatus(String appid, String token, String spkId) {
+        try {
+            OkHttpClient client = createHttpClient();
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("appid", appid);
+            requestBody.put("speaker_id", spkId);
+
+            RequestBody body = RequestBody.create(JSON_MEDIA_TYPE, requestBody.toString());
+
+            Request request = new Request.Builder()
+                    .url(STATUS_URL)
+                    .post(body)
+                    .addHeader("Content-Type", "application/json")
+                    .addHeader("Authorization", "Bearer;" + token)
+                    .addHeader("Resource-Id", "volc.megatts.voiceclone")
+                    .build();
+
+            try (Response response = client.newCall(request).execute()) {
+                if (response.isSuccessful()) {
+                    String json = response.body().string();
+                    log.info("doubaovcl getStatus response: {}", json);
+                    JSONObject jsonObject = JSON.parseObject(json);
+                    Integer status = jsonObject.getInteger("status");
+                    return status != null && (status == 2 || status == 4);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("查询训练状态异常:{}", e.getMessage());
+        }
+        return false;
+    }
+
+    /**
+     * 新增/更新音色到 EASYCALL 数据库
+     * @param nameParam
+     * @param speakerId
+     */
+    private synchronized void addSpeakerId(String nameParam, String speakerId) {
+        CcTtsAliyun ttsSpeaker = ccTtsAliyunService.selectCcTtsAliyunByVoiceCode(speakerId);
+        String name = nameParam.replace("'", "").replace(" ", "");
+        if (name.length() > 20) {
+            name = name.substring(0, 20);
+        }
+
+        boolean update = false;
+        if (ttsSpeaker == null) {
+            ttsSpeaker = new CcTtsAliyun();
+        } else {
+            update = true;
+        }
+
+        ttsSpeaker.setVoiceCode(speakerId);
+        ttsSpeaker.setVoiceEnabled(1);
+        ttsSpeaker.setVoiceName(String.format("[%s] - %s", speakerId, name));
+        ttsSpeaker.setVoiceSource("doubao_vcl_tts");
+        ttsSpeaker.setPriority(0);
+        ttsSpeaker.setProvider("doubao");
+
+        try {
+            if (update) {
+                ccTtsAliyunService.updateCcTtsAliyun(ttsSpeaker);
+            } else {
+                ccTtsAliyunService.insertCcTtsAliyun(ttsSpeaker);
+            }
+            log.info("save doubaovcl speakerId succeed. {} {}", name, speakerId);
+        } catch (Exception e) {
+            log.error("save doubaovcl speakerId error: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * TTS 测试
+     * @param speakerId 声音ID
+     * @param language  语种
+     * @param text      要合成的文本
+     * @return
+     */
+
+    @Override
+    public AjaxResult doubaoTtsTest(String speakerId, Integer language, String text) {
+        if (StringUtils.isEmpty(speakerId)) {
+            return AjaxResult.error("请填写声音ID");
+        }
+        if (StringUtils.isEmpty(text)) {
+            return AjaxResult.error("请输入测试文本");
+        }
+
+        // 从 EASYCALL 数据源读取豆包账号配置
+        String ttsAccountJson = ccParamsService.getParamValueByCode(DOUBAO_ACCOUNT_PARAM_CODE, "{}");
+        JSONObject paramValues = JSONObject.parseObject(ttsAccountJson);
+        String appid = paramValues.getString("app_id");
+        String token = paramValues.getString("access_token");
+
+        if (StringUtils.isEmpty(appid) || StringUtils.isEmpty(token)) {
+            return AjaxResult.error("豆包账号未配置,请在系统参数中设置 " + DOUBAO_ACCOUNT_PARAM_CODE);
+        }
+
+        try {
+            // 构建请求体(与 DoubaoVclController 保持一致)
+            Map<String, Object> requestBody = buildTtsRequestBody(appid, token, speakerId, language, text);
+            String reqBodyJson = JSON.toJSONString(requestBody);
+            log.info("doubaovcl tts request: {}", reqBodyJson);
+
+            OkHttpClient client = createHttpClient();
+            RequestBody body = RequestBody.create(JSON_MEDIA_TYPE, reqBodyJson);
+            Request ttsRequest = new Request.Builder()
+                    .url(TTS_URL)
+                    .header("Authorization", "Bearer;" + token)
+                    .post(body)
+                    .build();
+
+            try (Response response = client.newCall(ttsRequest).execute()) {
+                String respStr = response.body().string();
+                if (!response.isSuccessful()) {
+                    log.info("doubaovcl tts response: {}", respStr);
+                    return AjaxResult.error("语音合成失败!\n网络繁忙,请稍后重试");
+                }
+                return AjaxResult.success("tts request success.", respStr);
+            }
+        } catch (IOException e) {
+            log.error("TTS 测试异常,speakerId={}", speakerId, e);
+            return AjaxResult.error("TTS 合成失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 构建 TTS 请求体
+     */
+    private Map<String, Object> buildTtsRequestBody(String appid, String token,
+                                                     String speakerId, Integer language, String text) {
+        Map<String, Object> app = new HashMap<>();
+        app.put("appid", appid);
+        app.put("cluster", "volcano_icl");
+
+        Map<String, Object> user = new HashMap<>();
+        user.put("uid", "uid");
+
+        Map<String, Object> audio = new HashMap<>();
+        audio.put("encoding", "mp3");
+        audio.put("voice_type", speakerId);
+        audio.put("language", String.valueOf(language != null ? language : 0));
+
+        Map<String, Object> request = new HashMap<>();
+        request.put("reqid", UUID.randomUUID().toString());
+        request.put("operation", "query");
+        request.put("text", text);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("app", app);
+        body.put("user", user);
+        body.put("audio", audio);
+        body.put("request", request);
+        return body;
+    }
+
+    /**
+     * 创建带超时配置的 OkHttpClient
+     */
+    private OkHttpClient createHttpClient() {
+        return new OkHttpClient.Builder()
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(60, TimeUnit.SECONDS)
+                .writeTimeout(60, TimeUnit.SECONDS)
+                .build();
+    }
+}

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

@@ -832,6 +832,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         log.info("进入easyCall外呼结果查询结果callPhoneRes:{}", JSON.toJSONString(callPhoneRes));
+        //不处理非ai外呼回调请求
+        if(StringUtils.isBlank(callPhoneRes.getBizJson())){
+            log.error("easyCall外呼回调信息非调用:{}", JSON.toJSONString(result));
+            return;
+        }
         // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
         if (StringUtils.isBlank(callPhoneRes.getIntent())) {
             String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
@@ -1260,8 +1265,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         } else if (Integer.valueOf(1).equals(companyVoiceRobotic.getAddType())) {
             String intention = crmCustomer.getIntention();
             String queryIntention = intention;
-            List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
             if (!isPositiveInteger(intention)) {
+                List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
                 Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
                 if (firstDict.isPresent()) {
                     SysDictData sysDictData = firstDict.get();

Dosya farkı çok büyük olduğundan ihmal edildi
+ 30 - 11
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java


+ 1 - 2
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java

@@ -2,7 +2,6 @@ package com.fs.crm.service;
 
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.crm.domain.CrmCustomerAnalyze;
 import com.fs.crm.param.PolishingScriptParam;
 
@@ -77,5 +76,5 @@ public interface ICrmCustomerAnalyzeService extends IService<CrmCustomerAnalyze>
 
     List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeListAll(CrmCustomerAnalyze crmCustomerAnalyze);
 
-    String aiIntentionDegree(String content , Long chatId) throws JsonProcessingException;
+    String aiIntentionDegree(String content , Long chatId) ;
 }

+ 96 - 56
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.crm.service.impl;
 
 import cn.hutool.core.lang.TypeReference;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -21,7 +22,7 @@ import com.fs.system.mapper.SysDictDataMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Async;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
@@ -113,15 +114,16 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
     }
     private static final String AI_PORTRAIT = "crm_ai_portrait";
     private static final ObjectMapper mapper = new ObjectMapper();
+    @Value("${crm.customer.ai.Key:mygpt-qZllJOSsaKw51T8D1Ug20aGJWcEPlC9OoR5kOKLQ8G9dsCeW3xa2m}")
+    private String OTHER_KEY;
     //ai获取客户画像
     @Override
-    @Async
     public void aiGeneratedCustomerPortrait(Long customerId, String dataJson,Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","客户画像");
         log.info("请求参数:{}", stringObjectMap);
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
-
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
+//        System.out.println(aiResponse);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
 
 // 获取 data.responseData
@@ -159,18 +161,17 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
     }
 
     @Override
-    @Async
     public void aiCommunicationSummary(Long customerId, String dataJson, Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","沟通总结");
         log.info("请求参数:{}", stringObjectMap);
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
-        System.out.println(aiResponse);
+//        System.out.println(aiResponse);
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
-        JSONArray summary = null;
+        JSONObject summary = null;
 
 // 遍历 responseData
         for (int i = 0; i < responseData.size(); i++) {
@@ -185,40 +186,40 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     if ("AI".equals(historyItem.getString("obj"))) {
                         String valueStr = historyItem.getString("value");
                         JSONObject valueObj = JSON.parseObject(valueStr);
-                        summary = valueObj.getJSONArray("tagInfos");
+                        summary = valueObj.getJSONObject("userInfo");
                         break;
                     }
                 }
             }
             if (summary != null) break;
         }
-        StringBuilder summaryText = new StringBuilder();
-        if (summary!= null && !summary.isEmpty()) {
-            for (int i = 0; i < summary.size(); i++) {
-                summaryText.append(summary.get(i)).append(",");
-            }
-            summaryText.delete(summaryText.length()-1,summaryText.length());
+
+        if (!summary.isEmpty() ) {
+            String summaryText = summary.getString("沟通总结");
             CrmCustomerAnalyze crmCustomerAnalyze = new CrmCustomerAnalyze();
             crmCustomerAnalyze.setCustomerId(customerId);
-            crmCustomerAnalyze.setCommunicationSummary(summaryText.toString());
+            crmCustomerAnalyze.setCommunicationSummary(summaryText);
             baseMapper.updateCustomerPortrait(crmCustomerAnalyze);
         }
     }
 
     @Override
-    @Async
     public void aiCommunicationAbstract(Long customerId, String dataJson, Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","沟通摘要");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("沟通摘要", "");
+        stringObjectMap.put("userInfo", map);
         log.info("请求参数:{}", stringObjectMap);
 
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
+//        System.out.println(aiResponse);
 
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
-        String summary = null;
+        JSONObject summary = null;
 
 // 遍历 responseData
         for (int i = 0; i < responseData.size(); i++) {
@@ -233,7 +234,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     if ("AI".equals(historyItem.getString("obj"))) {
                         String valueStr = historyItem.getString("value");
                         JSONObject valueObj = JSON.parseObject(valueStr);
-                        summary = valueObj.getString("userContent");
+                        summary = valueObj.getJSONObject("userInfo");
                         break;
                     }
                 }
@@ -243,25 +244,28 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         if (summary != null){
             CrmCustomerAnalyze c = new CrmCustomerAnalyze();
             c.setCustomerId(customerId);
-            c.setCommunicationAbstract(summary);
+            c.setCommunicationAbstract(summary.getString("沟通摘要"));
             baseMapper.updateCustomerPortrait(c);
         }
     }
 
     @Override
-    @Async
     public void aiAttritionLevel(Long customerId, String dataJson, Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","流失风险等级");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("流失风险等级", "");
+        stringObjectMap.put("userInfo", map);
         log.info("请求参数:{}", stringObjectMap);
 
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
+//        System.out.println(aiResponse);
 
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
-        String summary = null;
+        JSONObject summary = null;
 
 // 遍历 responseData
         for (int i = 0; i < responseData.size(); i++) {
@@ -276,33 +280,50 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     if ("AI".equals(historyItem.getString("obj"))) {
                         String valueStr = historyItem.getString("value");
                         JSONObject valueObj = JSON.parseObject(valueStr);
-                        summary = valueObj.getString("tagInfos");
+                        summary = valueObj.getJSONObject("userInfo");
                         break;
                     }
                 }
             }
-            if (summary != null) break;
+            if (summary != null && !summary.isEmpty()) break;
         }
-        if (summary != null){
-            //todo 响应处理
+        if (summary != null && !summary.isEmpty()){
+            String level = summary.getString("流失风险等级");
+
+            CrmCustomerAnalyze c = new CrmCustomerAnalyze();
+            c.setCustomerId(customerId);
+            c.setAttritionLevel(getScore(level));
+            baseMapper.updateCustomerPortrait(c);
         }
     }
 
+
+        private static Long getScore(String level) {
+            if ("十分满意".equals(level) || "满意".equals(level) || "A".equals(level) || "B".equals(level)) return 1L;
+            if ("基本满意".equals(level) || "C".equals(level)) return 2L;
+            if ("不满意".equals(level) || "D".equals(level)) return 3L;
+            if ("十分不满意".equals(level) || "E".equals(level)) return 4L;
+            return 0L; // 未知
+        }
+
+
     @Override
-    @Async
     public void aiCustomerFocus(Long customerId, String dataJson, Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","客户关注点");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("客户关注点", "");
+        stringObjectMap.put("userInfo", map);
         log.info("请求参数:{}", stringObjectMap);
 
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
-        System.out.println(aiResponse);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
+//        System.out.println(aiResponse);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
 
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
-        JSONArray summary = null;
+        JSONObject summary = null;
 
 // 遍历 responseData
         for (int i = 0; i < responseData.size(); i++) {
@@ -317,22 +338,17 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     if ("AI".equals(historyItem.getString("obj"))) {
                         String valueStr = historyItem.getString("value");
                         JSONObject valueObj = JSON.parseObject(valueStr);
-                        summary = valueObj.getJSONArray("userContent");
+                        summary = valueObj.getJSONObject("userInfo");
                         break;
                     }
                 }
             }
-            if (summary != null) break;
+            if (summary != null && !summary.isEmpty()) break;
         }
-        if (summary != null){
-            List<String> list = new ArrayList<String>();
-            summary.forEach(o->{
-                JSONObject jsonObject = (JSONObject) o;
-                list.add(jsonObject.getString("tagName"));
-            });
+        if (summary != null && !summary.isEmpty()){
             CrmCustomerAnalyze crmCustomerAnalyze = new CrmCustomerAnalyze();
             crmCustomerAnalyze.setCustomerId(customerId);
-            crmCustomerAnalyze.setCustomerFocusJson(list.toString());
+            crmCustomerAnalyze.setCustomerFocusJson(summary.getString("客户关注点"));
             baseMapper.updateCustomerPortrait(crmCustomerAnalyze);
         }
     }
@@ -342,7 +358,8 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         Map<String, Object> requestParam = new HashMap<>();
         requestParam.put("history", param.getContent());
         requestParam.put("modelType","话术润色");
-        requestParam.putAll(param.getPortrait());
+        HashMap<String, Map<String, String>> userInfo = MapUtil.of("userInfo", param.getPortrait());
+        requestParam.putAll(userInfo);
 
         // 设置其他参数
         requestParam.put("tagInfos", Collections.emptyList());
@@ -350,26 +367,49 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
         requestParam.put("likeRatio", "");
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, Long.valueOf(param.getChatId()));
-        System.out.println(aiResponse);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, Long.valueOf(param.getChatId()),OTHER_KEY);
+//        System.out.println(aiResponse);
+        JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
+        JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
+
+// 遍历 responseData
+        for (int i = 0; i < responseData.size(); i++) {
+            JSONObject node = responseData.getJSONObject(i);
+            JSONArray historyPreview = node.getJSONArray("historyPreview");
+
+            if (historyPreview != null) {
+                for (int j = 0; j < historyPreview.size(); j++) {
+                    JSONObject historyItem = historyPreview.getJSONObject(j);
+
+                    // 找到 obj 为 "AI" 的项
+                    if ("AI".equals(historyItem.getString("obj"))) {
+                        String valueStr = historyItem.getString("value");
+                        JSONObject valueObj = JSON.parseObject(valueStr);
+                        return valueObj.getString("aiContent");
+                    }
+                }
+            }
+        }
         return "";
     }
 
     @Override
-    @Async
     public void aiIntentionDegree(Long customerId, String dataJson, Long logId) {
         Map<String, Object> stringObjectMap = buildRequestParam(customerId, dataJson);
         stringObjectMap.put("modelType","客户意向度");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("客户意向度", "");
+        stringObjectMap.put("userInfo", map);
         log.info("请求参数:{}", stringObjectMap);
 
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId);
-        System.out.println(aiResponse);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId,OTHER_KEY);
+//        System.out.println(aiResponse);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
 
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
-        String summary = null;
+        JSONObject summary = null;
 
 // 遍历 responseData
         for (int i = 0; i < responseData.size(); i++) {
@@ -384,17 +424,17 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     if ("AI".equals(historyItem.getString("obj"))) {
                         String valueStr = historyItem.getString("value");
                         JSONObject valueObj = JSON.parseObject(valueStr);
-                        summary = valueObj.getString("userIntent");
+                        summary = valueObj.getJSONObject("userInfo");
                         break;
                     }
                 }
             }
-            if (summary != null) break;
+            if (summary != null && !summary.isEmpty()) break;
         }
-        if (summary != null) {
+        if (summary != null && !summary.isEmpty()) {
             CrmCustomerAnalyze crmCustomerAnalyze = new CrmCustomerAnalyze();
             crmCustomerAnalyze.setCustomerId(customerId);
-            crmCustomerAnalyze.setIntentionDegree(summary);
+            crmCustomerAnalyze.setIntentionDegree(summary.getString("客户意向度"));
             baseMapper.updateCustomerPortrait(crmCustomerAnalyze);
         }
     }
@@ -437,9 +477,9 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("modelType","客户意向度");
         log.info("请求参数:{}", requestParam);
 
-        R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, chatId);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, chatId,OTHER_KEY);
         JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
-
+//        System.out.println(aiResponse);
 // 获取 data.responseData
         JSONArray responseData = root.getJSONObject("data").getJSONArray("responseData");
 
@@ -463,9 +503,9 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
                     }
                 }
             }
-            if (summary != null) break;
+            if (summary != null && !summary.isEmpty()) break;
         }
-        if (summary != null) {
+        if (summary != null && !summary.isEmpty()) {
             return summary;
         }
         return null;

+ 6 - 5
fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java

@@ -48,7 +48,7 @@ public class CrmCustomerAiTagUtil {
 
     //行业字典名称
     private static final String TRADE_TYPE = "trade_type";
-    @Value("${crm.customer.ai.key:mygpt-xZeWD9u6tTebmfn3huS8bTQ63WL2bldp56LpXnB4FWhTnIQMqW4t}")
+    @Value("${crm.customer.ai.key:mygpt-qZllJOSsaKw51T8D1Ug20aGJWcEPlC9OoR5kOKLQ8G9dsCeW3xa2m}")
     private String appKey;
     private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
 
@@ -81,7 +81,8 @@ public class CrmCustomerAiTagUtil {
         Map<String, Object> requestParam = buildRequestParam(customerId, tradeType, communication);
 
         // 3. 调用AI服务
-        R aiResponse = callAiService(requestParam, logId);
+        R aiResponse = callAiService(requestParam, logId,APP_KEY);
+//        System.out.println(aiResponse);
         // 4. 解析响应并保存
         List<CrmCustomerAiTagVo> results = parseAiResponse(aiResponse, customerId);
 
@@ -129,7 +130,7 @@ public class CrmCustomerAiTagUtil {
                 .map(tag -> buildTagVo(tag, customerId))
                 .collect(Collectors.toList());
     }
-    public static R callAiService(Map<String, Object> requestParam, Long logId) {
+    public static R callAiService(Map<String, Object> requestParam, Long logId,String appKey) {
         try {
             String requestJson = mapper.writeValueAsString(requestParam);
 
@@ -146,7 +147,7 @@ public class CrmCustomerAiTagUtil {
             ChatService chatService = SpringUtils.getBean(ChatService.class);
             AiHostProper aiHost = SpringUtils.getBean(AiHostProper.class);
 
-            return chatService.initiatingTakeChat(param, aiHost.getAiApi(), APP_KEY);
+            return chatService.initiatingTakeChat(param, aiHost.getAiApi(), appKey);
         } catch (Exception e) {
             throw new RuntimeException("AI服务调用失败", e);
         }
@@ -226,7 +227,7 @@ public class CrmCustomerAiTagUtil {
                             // 如果 value 是字符串,需要再次解析
                             if (valueNode.isTextual()) {
                                 String valueStr = valueNode.asText();
-                                JsonNode tagInfosNode = mapper.readTree(valueStr).path("tagsInfo");
+                                JsonNode tagInfosNode = mapper.readTree(valueStr).path("tagInfos");
 
                                 if (tagInfosNode.isArray()) {
                                     return mapper.convertValue(tagInfosNode,

+ 1 - 1
fs-service/src/main/java/com/fs/wxcid/threadExecutor/generalCustomerExecutor.java

@@ -20,7 +20,7 @@ public class generalCustomerExecutor {
         executor.setQueueCapacity(10000);
         executor.setThreadNamePrefix("CustomerExec-");
         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
-        executor.setKeepAliveSeconds(600);
+        executor.setKeepAliveSeconds(3600);
         executor.initialize();
         return executor;
     }

+ 87 - 0
fs-service/src/main/resources/mapper/aicall/CcTtsAliyunMapper.xml

@@ -0,0 +1,87 @@
+<?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.aicall.mapper.CcTtsAliyunMapper">
+    
+    <resultMap type="com.fs.aicall.domain.CcTtsAliyun" id="CcTtsAliyunResult">
+        <result property="id"           column="id" />
+        <result property="voiceName"    column="voice_name" />
+        <result property="voiceCode"    column="voice_code" />
+        <result property="voiceEnabled" column="voice_enabled" />
+        <result property="voiceSource"  column="voice_source" />
+        <result property="priority"     column="priority" />
+        <result property="provider"     column="provider" />
+    </resultMap>
+
+    <sql id="selectCcTtsAliyunVo">
+        select id, voice_name, voice_code, voice_enabled, voice_source, priority, provider
+        from cc_tts_aliyun
+    </sql>
+
+    <select id="selectCcTtsAliyunById" parameterType="Integer" resultMap="CcTtsAliyunResult">
+        <include refid="selectCcTtsAliyunVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectCcTtsAliyunByVoiceCode" parameterType="String" resultMap="CcTtsAliyunResult">
+        <include refid="selectCcTtsAliyunVo"/>
+        where voice_code = #{voiceCode}
+    </select>
+
+    <select id="selectCcTtsAliyunList" parameterType="com.fs.aicall.domain.CcTtsAliyun" resultMap="CcTtsAliyunResult">
+        <include refid="selectCcTtsAliyunVo"/>
+        <where>
+            <if test="voiceName != null and voiceName != ''">and voice_name like concat('%', #{voiceName}, '%')</if>
+            <if test="voiceCode != null and voiceCode != ''">and voice_code = #{voiceCode}</if>
+            <if test="voiceEnabled != null">and voice_enabled = #{voiceEnabled}</if>
+            <if test="voiceSource != null and voiceSource != ''">and voice_source = #{voiceSource}</if>
+            <if test="provider != null and provider != ''">and provider = #{provider}</if>
+        </where>
+    </select>
+
+    <insert id="insertCcTtsAliyun" parameterType="com.fs.aicall.domain.CcTtsAliyun" useGeneratedKeys="true" keyProperty="id">
+        insert into cc_tts_aliyun
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">voice_name,</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code,</if>
+            <if test="voiceEnabled != null">voice_enabled,</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source,</if>
+            <if test="priority != null">priority,</if>
+            <if test="provider != null and provider != ''">provider,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">#{voiceName},</if>
+            <if test="voiceCode != null and voiceCode != ''">#{voiceCode},</if>
+            <if test="voiceEnabled != null">#{voiceEnabled},</if>
+            <if test="voiceSource != null and voiceSource != ''">#{voiceSource},</if>
+            <if test="priority != null">#{priority},</if>
+            <if test="provider != null and provider != ''">#{provider},</if>
+        </trim>
+    </insert>
+
+    <update id="updateCcTtsAliyun" parameterType="com.fs.aicall.domain.CcTtsAliyun">
+        update cc_tts_aliyun
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">voice_name = #{voiceName},</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code = #{voiceCode},</if>
+            <if test="voiceEnabled != null">voice_enabled = #{voiceEnabled},</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source = #{voiceSource},</if>
+            <if test="priority != null">priority = #{priority},</if>
+            <if test="provider != null and provider != ''">provider = #{provider},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCcTtsAliyunById" parameterType="Integer">
+        delete from cc_tts_aliyun where id = #{id}
+    </delete>
+
+    <delete id="deleteCcTtsAliyunByIds" parameterType="String">
+        delete from cc_tts_aliyun where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

@@ -222,6 +222,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         SELECT dict_type,dict_label,dict_value FROM `sys_dict_data` where  dict_type = #{dictType}
     </select>
 
+    <select id="getDictDataByTypeAndValue" resultType="com.fs.company.vo.DictVO">
+        SELECT dict_type,dict_label,dict_value FROM `sys_dict_data` where  dict_value = #{value} and dict_type = #{dictType}
+    </select>
+
     <select id="selectSceneTaskByCompanyIdAndType" resultType="CompanyVoiceRobotic">
         select * from company_voice_robotic where company_id = #{companyId}
                                               and scene_type = #{sceneType}

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

@@ -114,7 +114,7 @@
           and aw.del_flag = 0
     </select>
     <select id="optionList" resultType="com.fs.company.vo.OptionVO">
-        select workflow_id value,workflow_name label from company_ai_workflow where company_id = #{companyId}
+        select workflow_id value,workflow_name label from company_ai_workflow where company_id = #{companyId} and del_flag = 0
     </select>
 
     <insert id="insertCompanyWorkflow" parameterType="CompanyWorkflow" useGeneratedKeys="true" keyProperty="workflowId">

+ 3 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerAnalyzeMapper.xml

@@ -130,6 +130,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="intentionDegree != null and intentionDegree != ''">
                 intention_degree = #{intentionDegree},
             </if>
+            <if test="attritionLevel != null and attritionLevel != ''">
+                attrition_level = #{attritionLevel},
+            </if>
 
         </set>
         where customer_id = #{customerId}

+ 2 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -172,6 +172,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="goodsSpecification != null">goods_specification,</if>
             <if test="shopName != null">shop_name,</if>
             <if test="traceId != null">trace_id,</if>
+            <if test="intention != null">intention,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="customerCode != null">#{customerCode},</if>
@@ -227,6 +228,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="goodsSpecification != null">#{goodsSpecification},</if>
             <if test="shopName != null">#{shopName},</if>
             <if test="traceId != null">#{traceId},</if>
+            <if test="intention != null">#{intention},</if>
          </trim>
     </insert>
     <insert id="insertCrmCustomerInfo">

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor