Procházet zdrojové kódy

cid场景任务客户来源接口对接ai分析,新增语音克隆

lmx před 2 týdny
rodič
revize
d422f780ca
18 změnil soubory, kde provedl 821 přidání a 15 odebrání
  1. 1 1
      fs-ai-call-task/src/main/java/com/fs/app/task/Task.java
  2. 59 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java
  3. 38 0
      fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java
  4. 57 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcTtsAliyunMapper.java
  5. 48 0
      fs-service/src/main/java/com/fs/aicall/service/ICcTtsAliyunService.java
  6. 56 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcTtsAliyunServiceImpl.java
  7. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  8. 2 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  9. 36 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java
  10. 1 1
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  11. 391 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java
  12. 6 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  13. 29 10
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  14. 1 1
      fs-service/src/main/java/com/fs/wxcid/threadExecutor/generalCustomerExecutor.java
  15. 87 0
      fs-service/src/main/resources/mapper/aicall/CcTtsAliyunMapper.xml
  16. 4 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  17. 1 1
      fs-service/src/main/resources/mapper/company/CompanyWorkflowMapper.xml
  18. 2 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

+ 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);
+    }
+}

+ 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();

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 29 - 10
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java


+ 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">

+ 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">

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů