Selaa lähdekoodia

cid更改回调逻辑

lmx 2 päivää sitten
vanhempi
commit
29eb09036a
21 muutettua tiedostoa jossa 1513 lisäystä ja 82 poistoa
  1. 222 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  2. 43 0
      fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java
  3. 64 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java
  4. 162 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  5. 7 0
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  6. 69 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  7. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanySiptaskInfoService.java
  8. 99 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  9. 91 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySiptaskInfoServiceImpl.java
  10. 105 21
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  11. 73 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  12. 21 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  13. 73 37
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  14. 15 14
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  15. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBizGroupVO.java
  16. 75 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  17. 14 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallIvrVO.java
  18. 10 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  19. 2 1
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml
  20. 79 0
      fs-service/src/main/resources/mapper/company/CompanySiptaskInfoMapper.xml
  21. 214 0
      fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml

+ 222 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java

@@ -0,0 +1,222 @@
+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.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.EasyCallBizGroupVO;
+import com.fs.company.vo.easycall.EasyCallGatewayVO;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.company.vo.easycall.EasyCallIvrVO;
+import com.fs.company.vo.easycall.EasyCallLlmAccountVO;
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入大模型配置 Controller
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/company/inboundCallManage")
+public class CompanyInboundCallManageController extends BaseController {
+
+    @Autowired
+    private ICompanyInboundCallManageService inboundCallManageService;
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    /**
+     * 查询呼入大模型配置列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(EasyCallInboundLlmVO vo) {
+        startPage();
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(vo);
+        TableDataInfo rspData = getDataTable(list);
+        // 填充关联数据
+        @SuppressWarnings("unchecked")
+        List<EasyCallInboundLlmVO> records = (List<EasyCallInboundLlmVO>) rspData.getRows();
+        for (EasyCallInboundLlmVO data : records) {
+            fillRelationData(data);
+        }
+        rspData.setRows(records);
+        return rspData;
+    }
+
+    /**
+     * 获取大模型账户下拉列表
+     */
+    @GetMapping("/llmAccountList")
+    public AjaxResult getLlmAccountList() {
+        List<EasyCallLlmAccountVO> list = inboundLlmMapper.selectLlmAccountList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取呼入大模型配置详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Integer id) {
+        return AjaxResult.success(inboundCallManageService.selectInboundLlmById(id));
+    }
+
+    /**
+     * 新增呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody EasyCallInboundLlmVO vo) {
+        return toAjax(inboundCallManageService.insertInboundLlm(vo));
+    }
+
+    /**
+     * 修改呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody EasyCallInboundLlmVO vo) {
+        return toAjax(inboundCallManageService.updateInboundLlm(vo));
+    }
+
+    /**
+     * 删除呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String ids) {
+        return toAjax(inboundCallManageService.deleteInboundLlmByIds(ids));
+    }
+
+    /**
+     * 校验被叫号码是否唯一
+     */
+    @GetMapping("/checkCallee")
+    public AjaxResult checkCallee(@RequestParam(value = "id", required = false) Integer id,
+                                  @RequestParam("callee") String callee) {
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmByCallee(callee);
+        if (list.size() <= 0) {
+            return AjaxResult.success(true);
+        }
+        if (null != id && list.get(0).getId().equals(id)) {
+            return AjaxResult.success(true);
+        }
+        return AjaxResult.success(false);
+    }
+
+    /**
+     * 获取所有AI配置列表
+     */
+    @GetMapping("/ai/all")
+    public AjaxResult getAllAi() {
+        EasyCallInboundLlmVO query = new EasyCallInboundLlmVO();
+        query.setServiceType("ai");
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取ASR提供商列表
+     */
+    @GetMapping("/asrProviderList")
+    public AjaxResult getAsrProviderList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectAsrProviderList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取TTS音色来源列表
+     */
+    @GetMapping("/voiceSourceList")
+    public AjaxResult getVoiceSourceList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectVoiceSourceList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 根据音色来源获取音色列表
+     */
+    @GetMapping("/voiceList")
+    public AjaxResult getVoiceList(@RequestParam("voiceSource") String voiceSource) {
+        List<EasyCallVoiceCodeVO> list = inboundLlmMapper.selectVoiceListBySource(voiceSource);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取业务组列表
+     */
+    @GetMapping("/bizGroupList")
+    public AjaxResult getBizGroupList() {
+        List<EasyCallBizGroupVO> list = inboundLlmMapper.selectBizGroupList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取出局网关列表
+     */
+    @GetMapping("/gatewayList")
+    public AjaxResult getGatewayList() {
+        List<EasyCallGatewayVO> list = inboundLlmMapper.selectOutboundGatewayList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取IVR列表
+     */
+    @GetMapping("/ivrList")
+    public AjaxResult getIvrList() {
+        List<EasyCallIvrVO> list = inboundLlmMapper.selectIvrList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 填充关联数据
+     */
+    private void fillRelationData(EasyCallInboundLlmVO data) {
+        // 填充大模型账户名称
+        if (data.getLlmAccountId() != null && data.getLlmAccountId() > 0) {
+            EasyCallLlmAccountVO llmAccount = inboundLlmMapper.selectLlmAccountById(data.getLlmAccountId());
+            if (llmAccount != null) {
+                data.setLlmAccountName(llmAccount.getName());
+            } else {
+                data.setLlmAccountName("");
+            }
+        }
+        // 填充音色名称
+        if (StringUtils.isNotEmpty(data.getVoiceCode())) {
+            EasyCallVoiceCodeVO voiceCode = inboundLlmMapper.selectVoiceCodeByCode(data.getVoiceCode());
+            if (voiceCode != null) {
+                data.setVoiceSource(voiceCode.getVoiceSource());
+                data.setVoiceName(voiceCode.getVoiceName());
+            }
+        }
+    }
+}

+ 43 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java

@@ -0,0 +1,43 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 任务与外呼sip任务关联关系对象 company_siptask_info
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanySiptaskInfo extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 工作流id */
+    @Excel(name = "工作流id")
+    private Long workflowId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long taskId;
+
+    /** 节点key */
+    @Excel(name = "节点key")
+    private String nodeKey;
+
+    /** 对应sip任务id */
+    @Excel(name = "对应sip任务id")
+    private Long batchId;
+
+    /** sip外呼任务 */
+    @Excel(name = "sip外呼任务")
+    private String taskJson;
+
+
+}

+ 64 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java

@@ -0,0 +1,64 @@
+package com.fs.company.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanySiptaskInfo;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 任务与外呼sip任务关联关系Mapper接口
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface CompanySiptaskInfoMapper extends BaseMapper<CompanySiptaskInfo>{
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    CompanySiptaskInfo selectCompanySiptaskInfoById(Long id);
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系集合
+     */
+    List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 删除任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoById(Long id);
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoByIds(Long[] ids);
+
+    CompanySiptaskInfo selectSipTaskInfoByTaskIdAndNodeKey(@Param("taskId") Long taskId, @Param("nodeKey") String nodeKey);
+}

+ 162 - 0
fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java

@@ -0,0 +1,162 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.vo.easycall.EasyCallBizGroupVO;
+import com.fs.company.vo.easycall.EasyCallGatewayVO;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.company.vo.easycall.EasyCallIvrVO;
+import com.fs.company.vo.easycall.EasyCallLlmAccountVO;
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入大模型配置 Mapper接口
+ *
+ * @author fs
+ */
+@Repository
+public interface EasyCallInboundLlmMapper {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID数组
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmByIds(String[] ids);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 查询大模型账户列表
+     *
+     * @return 大模型账户列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallLlmAccountVO> selectLlmAccountList();
+
+    /**
+     * 根据ID查询大模型账户
+     *
+     * @param id 大模型账户ID
+     * @return 大模型账户信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallLlmAccountVO selectLlmAccountById(Integer id);
+
+    /**
+     * 根据voiceCode查询音色信息
+     *
+     * @param voiceCode 音色编号
+     * @return 音色信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallVoiceCodeVO selectVoiceCodeByCode(String voiceCode);
+
+    /**
+     * 查询ASR提供商列表
+     *
+     * @return ASR提供商列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectAsrProviderList();
+
+    /**
+     * 查询TTS音色来源列表
+     *
+     * @return TTS音色来源列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectVoiceSourceList();
+
+    /**
+     * 根据音色来源查询音色列表
+     *
+     * @param voiceSource 音色来源
+     * @return 音色列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallVoiceCodeVO> selectVoiceListBySource(String voiceSource);
+
+    /**
+     * 查询业务组列表
+     *
+     * @return 业务组列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallBizGroupVO> selectBizGroupList();
+
+    /**
+     * 查询出局网关列表
+     *
+     * @return 网关列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallGatewayVO> selectOutboundGatewayList();
+
+    /**
+     * 查询IVR列表
+     *
+     * @return IVR列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallIvrVO> selectIvrList();
+}

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -62,4 +62,11 @@ public interface CompanyWorkflowEngine {
      * @param inputData
      */
     void timeDoExecute(String workflowInstanceId, String nodeKey, Map<String, Object> inputData);
+
+    /**
+     * 创建sip任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    Long createSipTask(Long roboticId,Long workFlowId);
 }

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

@@ -0,0 +1,69 @@
+package com.fs.company.service;
+
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+
+import java.util.List;
+
+/**
+ * 呼入大模型配置 Service接口
+ *
+ * @author fs
+ */
+public interface ICompanyInboundCallManageService {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    int deleteInboundLlmByIds(String ids);
+}

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

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

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

@@ -0,0 +1,99 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.text.Convert;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 呼入大模型配置 Service业务层处理
+ *
+ * @author fs
+ */
+@Service
+public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallManageService {
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @Override
+    public EasyCallInboundLlmVO selectInboundLlmById(Integer id) {
+        return inboundLlmMapper.selectInboundLlmById(id);
+    }
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.selectInboundLlmList(vo);
+    }
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee) {
+        return inboundLlmMapper.selectInboundLlmByCallee(callee);
+    }
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int insertInboundLlm(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.insertInboundLlm(vo);
+    }
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int updateInboundLlm(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.updateInboundLlm(vo);
+    }
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmById(Integer id) {
+        return inboundLlmMapper.deleteInboundLlmById(id);
+    }
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmByIds(String ids) {
+        return inboundLlmMapper.deleteInboundLlmByIds(Convert.toStrArray(ids));
+    }
+}

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

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

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

@@ -130,6 +130,24 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /** 每次重试等待时长(毫秒) */
     private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
 
+    /** EasyCall dialogue 对话内容重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String EASYCALL_DIALOGUE_RETRY_KEY = "easycall:dialogue:retry:";
+    /** dialogue 对话内容等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_DIALOGUE_MAX_RETRY = 5;
+    /** dialogue 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_DIALOGUE_RETRY_INTERVAL_MS = 30000L;
+
+    /**
+     * 判断 dialogue 对话内容是否为空(null、空字符串、空数组 "[]" 均视为无对话内容)
+     */
+    private boolean isDialogueEmpty(String dialogue) {
+        if (StringUtils.isBlank(dialogue)) {
+            return true;
+        }
+        String trimmed = dialogue.trim();
+        return "[]".equals(trimmed);
+    }
+
     /**
      * 查询机器人外呼任务
      *
@@ -828,7 +846,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     @Async("cidWorkFlowExecutor")
     public void callerResult4EasyCall(CdrDetailVo result) {
 //        try {
-//            Thread.sleep(20000L);
+//            Thread.sleep(3000L);
 //        } catch (InterruptedException e) {
 //            throw new RuntimeException(e);
 //        }
@@ -873,7 +891,72 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         */
 
         // 当前:根据对话内容同步调用自家 AI 计算意向度,不依赖第三方 intent
-        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+//        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+
+        // dialogue(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (isDialogueEmpty(callPhoneRes.getDialogue())) {
+            String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall外呼回调dialogue对话内容暂未写入,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                doRetryDialogue4EasyCall(result, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 dialogue 为空兜底继续处理
+                log.warn("easyCall外呼回调dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // dialogue 已有值,直接正常处理
+        redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 dialogue 对话内容异步写入完成)
+     * 每次重试前等待 {@link #EASYCALL_DIALOGUE_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryDialogue4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_DIALOGUE_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall dialogue重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall dialogue重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall dialogue重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (isDialogueEmpty(callPhoneRes.getDialogue())) {
+            // dialogue 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall dialogue对话内容仍未写入,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryDialogue4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // dialogue 已写入完成,正常处理
+        log.info("easyCall dialogue重试第{}次成功获取到对话内容,uuid={}", currentRetry, result.getUuid());
+        redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
         doHandleEasyCallResult(callPhoneRes);
     }
 
@@ -1003,6 +1086,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                         callPhoneRes.getDialogue(),
                         now().getLong(ChronoField.MILLI_OF_SECOND)
                 );
+                log.info("【验证】意向度结果={}, uuid={}", intentionDegree, callPhoneRes.getUuid());
                 intention = getIntention(intentionDegree);
             } catch (Exception e) {
                 log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
@@ -1581,25 +1665,25 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
 
-        if (!businessIds.isEmpty()) {
-            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
-                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
-            if (ObjectUtil.isNotEmpty(businesses)) {
-                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
-                records.forEach(record -> {
-                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
-                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
-                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
-                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
-                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
-                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
-                            record.setContentList(callLogCallphone.getContentList());
-                            record.setIntention(callLogCallphone.getIntention());
-                        }
-                    }
-                });
-            }
-        }
+//        if (!businessIds.isEmpty()) {
+//            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()
+//                    .in(CompanyVoiceRoboticBusiness::getId, businessIds));
+//            if (ObjectUtil.isNotEmpty(businesses)) {
+//                Map<Long, CompanyVoiceRoboticBusiness> businessMap = businesses.stream().collect(Collectors.toMap(CompanyVoiceRoboticBusiness::getId, Function.identity()));
+//                records.forEach(record -> {
+//                    if (record.getBusinessId() != null && businessMap.containsKey(record.getBusinessId())) {
+//                        CompanyVoiceRoboticBusiness business = businessMap.get(record.getBusinessId());
+//                        CompanyVoiceRoboticCallLogCallphone callLogCallphone = companyVoiceRoboticCallLogCallphoneMapper.selectOne(new LambdaQueryWrapper<CompanyVoiceRoboticCallLogCallphone>()
+//                                .eq(CompanyVoiceRoboticCallLogCallphone::getRoboticId, business.getRoboticId())
+//                                .eq(CompanyVoiceRoboticCallLogCallphone::getCallerId, business.getCalleeId()));
+//                        if (ObjectUtil.isNotEmpty(callLogCallphone)) {
+//                            record.setContentList(callLogCallphone.getContentList());
+//                            record.setIntention(callLogCallphone.getIntention());
+//                        }
+//                    }
+//                });
+//            }
+//        }
 
         if (!instanceIds.isEmpty()) {
             List<CompanyAiWorkflowExecLog> allLogs = companyAiWorkflowExecLogMapper.selectByInstanceIds(instanceIds);

+ 73 - 9
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -1,23 +1,22 @@
 package com.fs.company.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
-import com.fs.company.domain.CompanyAiWorkflowExec;
-import com.fs.company.domain.CompanyAiWorkflowExecLog;
-import com.fs.company.domain.CompanyWorkflow;
-import com.fs.company.domain.CompanyWorkflowNode;
-import com.fs.company.mapper.CompanyAiWorkflowExecLogMapper;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyWorkflowMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.IWorkflowNode;
+import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
+import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.ExecutionResult;
+import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
+import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
@@ -60,6 +59,15 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     @Autowired
     private CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
+    @Autowired
+    CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+
+    @Autowired
+    IEasyCallService easyCallService;
+
+    @Autowired
+    CompanySiptaskInfoMapper companySiptaskInfoMapper;
+
     /**
      * 初始化工作流
      * 创建工作流实例并保存初始状态
@@ -84,7 +92,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
                     definition.getStartNodeKey(), context, definition);
 
             log.info("工作流初始化成功: {} -> {}", workflowInstanceId, workflowDefinitionId);
-
+            //为任务创建sip任务并存入表数据
+            createSipTask(Long.parseLong(inputVariables.get("roboticId").toString()),workflowDefinitionId);
             return ExecutionResult.success()
                     .nextNodeKey(definition.getStartNodeKey())
                     .workflowInstanceId(workflowInstanceId).build();
@@ -545,4 +554,59 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
 
     }
 
+    /**
+     * 创建SIP任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    public Long createSipTask(Long roboticId, Long workFlowId) {
+        try {
+            List<String> nodeTypes = Arrays.asList(NodeTypeEnum.AI_CALL_TASK.getCode());
+            CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticId);
+            List<CompanyWorkflowNode> companyWorkflowNodes = companyWorkflowNodeMapper.selectNodesByWorkflowIdAndTypes(workFlowId, nodeTypes);
+            //为所有外呼节点创建任务的对应sip外呼任务
+            for (CompanyWorkflowNode callNode : companyWorkflowNodes) {
+                String nodeConfig = callNode.getNodeConfig();
+                AiCallConfigVO callConfigVo = JSONObject.parseObject(nodeConfig, AiCallConfigVO.class);
+                EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+                // 任务名称:使用任务名称_工作流id_节点key
+                createParam.setBatchName(robotic.getName() + "_" + workFlowId + "_" + callNode.getNodeKey());
+                if (null != callConfigVo.getMaxConcurrency()) {
+                    createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+                } else {
+                    createParam.setThreadNum(3L);
+                }
+                // AI 外呼模式
+                createParam.setTaskType(1);
+                // 外呼线路(网关)
+                createParam.setGatewayId(callConfigVo.getGatewayId());
+                // 大模型底座
+                createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
+                // 音色编号
+                createParam.setVoiceCode(callConfigVo.getVoiceCode());
+                // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
+                createParam.setVoiceSource(callConfigVo.getVoiceSource());
+                // 技能组(转人工客服分组,可选)
+                createParam.setGroupId(callConfigVo.getBusiGroupId());
+
+                EasyCallTaskVO task = easyCallService.createTask(createParam, null);
+                if (task == null || task.getBatchId() == null) {
+                    log.error("createSipTask: 创建 EasyCall 任务失败 - workflowInstanceId: {}", workFlowId);
+                    throw new RuntimeException("EasyCallCenter365 创建任务失败");
+                }
+                CompanySiptaskInfo sipTaskInfo = new CompanySiptaskInfo();
+                sipTaskInfo.setTaskId(roboticId);
+                sipTaskInfo.setWorkflowId(workFlowId);
+                sipTaskInfo.setNodeKey(callNode.getNodeKey());
+                sipTaskInfo.setBatchId(task.getBatchId());
+                sipTaskInfo.setTaskJson(JSONObject.toJSONString(task));
+                companySiptaskInfoMapper.insertCompanySiptaskInfo(sipTaskInfo);
+                return task.getBatchId();
+            }
+        } catch (Exception ex) {
+            log.error("创建SIP任务失败:{}", ex);
+        }
+        return null;
+    }
+
 }

+ 21 - 0
fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java

@@ -13,6 +13,7 @@ import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
@@ -426,6 +427,26 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         companyAiWorkflowExecMapper.updateByWorkflowInstanceId(update);
     }
 
+    /**
+     * 任务完成判定
+     * @param context
+     */
+    public void taskFinish(ExecutionContext context){
+        //判定是否任务完成了更新任务的状态为执行完成
+        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+        if(null != roboticBusiness){
+            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+            if(Integer.valueOf(0).equals(i)){
+                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+                    robotic.setId(roboticBusiness.getRoboticId());
+                    robotic.setTaskStatus(3);
+                    companyVoiceRoboticMapper.updateById(robotic);
+                }
+            }
+        }
+    }
     public CompanyAiWorkflowExec getWorkflowExec(String workflowInstanceId) {
         CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
         if (null == companyAiWorkflowExec) {

+ 73 - 37
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -6,11 +6,10 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.enums.BusinessTypeEnum;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyVoiceRoboticCallBlacklistCheckParam;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.easycall.IEasyCallService;
@@ -27,6 +26,7 @@ import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 import com.fs.his.config.CidPhoneConfig;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
@@ -45,6 +45,9 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
     /** EasyCallCenter365 外呼服务 */
     private static final IEasyCallService easyCallService = SpringUtils.getBean(IEasyCallService.class);
+    private static final CompanyConfigMapper companyConfigMapper = SpringUtils.getBean(CompanyConfigMapper.class);
+    private static final CompanySiptaskInfoMapper companySiptaskInfoMapper = SpringUtils.getBean(CompanySiptaskInfoMapper.class);
+    private static final CompanyWorkflowEngine companyWorkflowEngine = SpringUtils.getBean(CompanyWorkflowEngine.class);
     /** 被叫人表 Mapper,用于获取手机号 */
     private static final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper = SpringUtils.getBean(CompanyVoiceRoboticCalleesMapper.class);
     private static final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneServiceImpl.class);
@@ -156,6 +159,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         //如果没有节点可执行通路 条件均不满足 则标记流程为中断
         if(runnableCount < 1){
             super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
+            super.taskFinish(context);
         }
 
         return null;
@@ -260,6 +264,16 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private void workflowCallPhoneOne4EasyCall(Long roboticId,Long calleeId, ExecutionContext context, AiCallConfigVO callConfigVo) {
 
         CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(roboticId);
+        String callBackUrl = "";
+        try{
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(robotic.getCompanyId(), "cId.config");
+            CidPhoneConfig cidConf = JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
+            if(null != cidConf && StringUtils.isNotBlank(cidConf.getCallbackUrl())){
+                callBackUrl = cidConf.getCallbackUrl();
+            }
+        } catch (Exception ex){
+            log.error("获取公司Cid配置失败:{}", ex);
+        }
         // 1. 生成回调唯一标识符,后续回调时通过此 uuid 匹配对应的流程实例
         String callBackUuid = UUID.randomUUID().toString();
         // 将回调信息写入 Redis,保存 1 天
@@ -293,42 +307,44 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         }
 
         // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
-        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
-        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
-        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
-        if (null != callConfigVo.getMaxConcurrency())
-            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
-        else {
-            createParam.setThreadNum(3L);
-        }
-        // AI 外呼模式
-        createParam.setTaskType(1);
-        // 外呼线路(网关)
-        createParam.setGatewayId(callConfigVo.getGatewayId());
-        // 大模型底座
-        createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
-        // 音色编号
-        createParam.setVoiceCode(callConfigVo.getVoiceCode());
-        // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
-        createParam.setVoiceSource(callConfigVo.getVoiceSource());
-        // 技能组(转人工客服分组,可选)
-        createParam.setGroupId(callConfigVo.getBusiGroupId());
-
-        JSONObject runParam = (JSONObject) JSON.toJSON(createParam);
-        runParam.put("companyId", robotic.getCompanyId());
-        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
-                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
+//        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+//        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
+//        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
+//        if (null != callConfigVo.getMaxConcurrency())
+//            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+//        else {
+//            createParam.setThreadNum(3L);
+//        }
+//        // AI 外呼模式
+//        createParam.setTaskType(1);
+//        // 外呼线路(网关)
+//        createParam.setGatewayId(callConfigVo.getGatewayId());
+//        // 大模型底座
+//        createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
+//        // 音色编号
+//        createParam.setVoiceCode(callConfigVo.getVoiceCode());
+//        // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
+//        createParam.setVoiceSource(callConfigVo.getVoiceSource());
+//        // 技能组(转人工客服分组,可选)
+//        createParam.setGroupId(callConfigVo.getBusiGroupId());
+
+
         // 4. 调用 EasyCallCenter365 创建任务接口
         // companyId 传 null 是因为 EasyCallCenter365 是全局地址,不需要按公司隔离
-        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
-                context.getWorkflowInstanceId(), calleeId);
-        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
-        if (task == null || task.getBatchId() == null) {
-            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
-                    context.getWorkflowInstanceId());
-            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
+//                context.getWorkflowInstanceId(), calleeId);
+//        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
+//        if (task == null || task.getBatchId() == null) {
+//            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
+//                    context.getWorkflowInstanceId());
+//            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        }
+        Long batchId = getTaskBatchId(robotic.getId(), context.getCurrentNodeKey(), context.getWorkflowInstanceId());
+        if(null == batchId ){
+            log.error("workflowCallPhoneOne4EasyCall: 获取 EasyCall 任务批次ID失败 - workflowInstanceId: {}",context.getWorkflowInstanceId());
+            throw new RuntimeException("任务批次ID失败");
         }
-        Long batchId = task.getBatchId();
+
         log.info("workflowCallPhoneOne4EasyCall: EasyCall 任务创建成功 - batchId: {}", batchId);
 
         // 5. 将被叫号码加入任务名单(使用通用追加接口,支持传入业务数据)
@@ -338,7 +354,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
         phoneItem.setPhoneNum(callees.getPhone());
         // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
-        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid));
+        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid).fluentPut("callBackUrl",callBackUrl));
         addListParam.setPhoneList(Collections.singletonList(phoneItem));
         easyCallService.addCommonCallList(addListParam, null);
         log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
@@ -347,6 +363,10 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 6. 启动外呼任务
         easyCallService.startTask(batchId, null);
         log.info("workflowCallPhoneOne4EasyCall: 任务启动成功 - batchId: {}", batchId);
+        JSONObject runParam = (JSONObject) JSON.toJSON(addListParam);
+        runParam.put("companyId", robotic.getCompanyId());
+        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
+                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
         addLog.setStatus(1);
         addLog.setCallbackUuid(callBackUuid);
         companyVoiceRoboticCallLogCallphoneService.asyncInsertCompanyVoiceRoboticCallLog(addLog);
@@ -374,6 +394,22 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         return false;
     }
 
+    /**
+     * 获取siptaskid
+     * @param roboticId
+     * @param nodeKey
+     * @param workflowInstanceId
+     * @return
+     */
+    public Long getTaskBatchId(Long roboticId, String nodeKey, String workflowInstanceId){
+        CompanySiptaskInfo sipTaskInfo = companySiptaskInfoMapper.selectSipTaskInfoByTaskIdAndNodeKey(roboticId, nodeKey);
+        if(null != sipTaskInfo && null != sipTaskInfo.getBatchId()){
+            return  sipTaskInfo.getBatchId();
+        }
+        //没有的情况下创建任务并返回
+        CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
+        return companyWorkflowEngine.createSipTask(roboticId, companyAiWorkflowExec.getWorkflowId());
+    }
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {
 //        super.postExecute(context, result);

+ 15 - 14
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -46,19 +46,20 @@ public class EndNode extends AbstractWorkflowNode {
     protected void postExecute(ExecutionContext context, ExecutionResult result) {
       super.postExecute(context, result);
       super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
-      //判定是否任务完成了更新任务的状态为执行完成
-        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
-        if(null != roboticBusiness){
-            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
-            if(Integer.valueOf(0).equals(i)){
-                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
-                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
-                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
-                    robotic.setId(roboticBusiness.getRoboticId());
-                    robotic.setTaskStatus(3);
-                    companyVoiceRoboticMapper.updateById(robotic);
-                }
-            }
-        }
+      super.taskFinish(context);
+//      //判定是否任务完成了更新任务的状态为执行完成
+//        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+//        if(null != roboticBusiness){
+//            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+//            if(Integer.valueOf(0).equals(i)){
+//                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+//                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+//                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+//                    robotic.setId(roboticBusiness.getRoboticId());
+//                    robotic.setTaskStatus(3);
+//                    companyVoiceRoboticMapper.updateById(robotic);
+//                }
+//            }
+//        }
     }
 }

+ 14 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBizGroupVO.java

@@ -0,0 +1,14 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * EasyCallCenter365 业务组VO
+ */
+@Data
+public class EasyCallBizGroupVO {
+    /** 业务组ID */
+    private Long groupId;
+    /** 业务组名称 */
+    private String bizGroupName;
+}

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

@@ -0,0 +1,75 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 呼入大模型配置对象 cc_inbound_llm_account
+ *
+ * @author fs
+ */
+@Data
+@Accessors(chain = true)
+public class EasyCallInboundLlmVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** 呼入别名/名称 */
+    private String inboundAlias;
+
+    /** 大模型底座id */
+    private Integer llmAccountId;
+
+    /** 被叫号码 */
+    private String callee;
+
+    /** TTS voice code */
+    private String voiceCode;
+
+    /** TTS voice source */
+    private String voiceSource;
+
+    /** 服务类型:ai/acd/ivr */
+    private String serviceType;
+
+    /** ASR提供商 */
+    private String asrProvider;
+
+    /** AI转接类型:acd/extension/gateway */
+    private String aiTransferType;
+
+    /** AI转接数据 */
+    private String aiTransferData;
+
+    /** IVR ID */
+    private String ivrId;
+
+    /** 满意度调查IVR ID */
+    private String satisfSurveyIvrId;
+
+    /************ 以下不是表结构字段 ************/
+    /** 大模型底座名称 */
+    private String llmAccountName;
+
+    /** 音色名称 */
+    private String voiceName;
+
+    /** 分组ID */
+    private Integer groupId;
+
+    /** AI转接技能组ID */
+    private String aiTransferGroupId;
+
+    /** AI转接网关ID */
+    private String aiTransferGatewayId;
+
+    /** AI转接网关目标号码 */
+    private String aiTransferGatewayDestNumber;
+
+    /** AI转接分机号 */
+    private String aiTransferExtNumber;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallIvrVO.java

@@ -0,0 +1,14 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * EasyCallCenter365 IVR VO
+ */
+@Data
+public class EasyCallIvrVO {
+    /** IVR ID */
+    private Long id;
+    /** IVR节点名称 */
+    private String ivrNodeName;
+}

+ 10 - 0
fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java

@@ -43,4 +43,14 @@ public class CidPhoneConfig implements Serializable {
      * 拨打次数
      * **/
     private Long numberCalls;
+
+    /**
+     * 是否允许重复客户
+     */
+    private Boolean allowRepeatCustomer;
+
+    /**
+     * 配置回调地址
+     */
+    private String callbackUrl;
 }

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

@@ -194,7 +194,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         e.last_update_time AS lastUpdateTime,
         b.add_wx_done AS addWxDone,
         b.call_phone_done AS callPhoneDone,
-        b.send_msg_done AS sendMsgDone
+        b.send_msg_done AS sendMsgDone,
+        c.intention
         FROM company_voice_robotic_business b
         LEFT JOIN company_ai_workflow_exec e ON b.id = e.business_key
         LEFT JOIN company_voice_robotic_callees c ON b.callee_id = c.id

+ 79 - 0
fs-service/src/main/resources/mapper/company/CompanySiptaskInfoMapper.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanySiptaskInfoMapper">
+    
+    <resultMap type="CompanySiptaskInfo" id="CompanySiptaskInfoResult">
+        <result property="id"    column="id"    />
+        <result property="workflowId"    column="workflow_id"    />
+        <result property="taskId"    column="task_id"    />
+        <result property="nodeKey"    column="node_key"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="taskJson"    column="task_json"    />
+    </resultMap>
+
+    <sql id="selectCompanySiptaskInfoVo">
+        select id, workflow_id, task_id, node_key, batch_id, task_json from company_siptask_info
+    </sql>
+
+    <select id="selectCompanySiptaskInfoList" parameterType="CompanySiptaskInfo" resultMap="CompanySiptaskInfoResult">
+        <include refid="selectCompanySiptaskInfoVo"/>
+        <where>  
+            <if test="workflowId != null "> and workflow_id = #{workflowId}</if>
+            <if test="taskId != null "> and task_id = #{taskId}</if>
+            <if test="nodeKey != null  and nodeKey != ''"> and node_key = #{nodeKey}</if>
+            <if test="batchId != null "> and batch_id = #{batchId}</if>
+            <if test="taskJson != null  and taskJson != ''"> and task_json = #{taskJson}</if>
+        </where>
+    </select>
+    
+    <select id="selectCompanySiptaskInfoById" parameterType="Long" resultMap="CompanySiptaskInfoResult">
+        <include refid="selectCompanySiptaskInfoVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanySiptaskInfo" parameterType="CompanySiptaskInfo" useGeneratedKeys="true" keyProperty="id">
+        insert into company_siptask_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="workflowId != null">workflow_id,</if>
+            <if test="taskId != null">task_id,</if>
+            <if test="nodeKey != null">node_key,</if>
+            <if test="batchId != null">batch_id,</if>
+            <if test="taskJson != null">task_json,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="workflowId != null">#{workflowId},</if>
+            <if test="taskId != null">#{taskId},</if>
+            <if test="nodeKey != null">#{nodeKey},</if>
+            <if test="batchId != null">#{batchId},</if>
+            <if test="taskJson != null">#{taskJson},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanySiptaskInfo" parameterType="CompanySiptaskInfo">
+        update company_siptask_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="workflowId != null">workflow_id = #{workflowId},</if>
+            <if test="taskId != null">task_id = #{taskId},</if>
+            <if test="nodeKey != null">node_key = #{nodeKey},</if>
+            <if test="batchId != null">batch_id = #{batchId},</if>
+            <if test="taskJson != null">task_json = #{taskJson},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanySiptaskInfoById" parameterType="Long">
+        delete from company_siptask_info where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanySiptaskInfoByIds" parameterType="String">
+        delete from company_siptask_info where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+    <select id="selectSipTaskInfoByTaskIdAndNodeKey" resultType="com.fs.company.domain.CompanySiptaskInfo">
+        select * from company_siptask_info where task_id=#{taskId} and node_key=#{nodeKey} limit 1
+    </select>
+</mapper>

+ 214 - 0
fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml

@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.EasyCallInboundLlmMapper">
+
+    <resultMap id="InboundLlmResult" type="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+        <id property="id" column="id"/>
+        <result property="llmAccountId" column="llm_account_id"/>
+        <result property="inboundAlias" column="inbound_alias"/>
+        <result property="callee" column="callee"/>
+        <result property="voiceCode" column="voice_code"/>
+        <result property="voiceSource" column="voice_source"/>
+        <result property="serviceType" column="service_type"/>
+        <result property="asrProvider" column="asr_provider"/>
+        <result property="aiTransferType" column="ai_transfer_type"/>
+        <result property="aiTransferData" column="ai_transfer_data"/>
+        <result property="ivrId" column="ivr_id"/>
+        <result property="satisfSurveyIvrId" column="satisf_survey_ivr_id"/>
+    </resultMap>
+
+    <resultMap id="LlmAccountResult" type="com.fs.company.vo.easycall.EasyCallLlmAccountVO">
+        <id property="id" column="id"/>
+        <result property="name" column="name"/>
+        <result property="accountJson" column="account_json"/>
+        <result property="accountEntity" column="account_entity"/>
+        <result property="providerClassName" column="provider_class_name"/>
+    </resultMap>
+
+    <resultMap id="VoiceCodeResult" type="com.fs.company.vo.easycall.EasyCallVoiceCodeVO">
+        <result property="voiceCode" column="voice_code"/>
+        <result property="voiceName" column="voice_name"/>
+        <result property="voiceSource" column="voice_source"/>
+    </resultMap>
+
+    <resultMap id="BizGroupResult" type="com.fs.company.vo.easycall.EasyCallBizGroupVO">
+        <result property="groupId" column="group_id"/>
+        <result property="bizGroupName" column="biz_group_name"/>
+    </resultMap>
+
+    <resultMap id="GatewayResult" type="com.fs.company.vo.easycall.EasyCallGatewayVO">
+        <result property="id" column="id"/>
+        <result property="gwName" column="gw_name"/>
+        <result property="profileName" column="profile_name"/>
+        <result property="caller" column="caller"/>
+        <result property="calleePrefix" column="callee_prefix"/>
+        <result property="gwAddr" column="gw_addr"/>
+        <result property="codec" column="codec"/>
+        <result property="gwDesc" column="gw_desc"/>
+        <result property="purpose" column="purpose"/>
+    </resultMap>
+
+    <resultMap id="IvrResult" type="com.fs.company.vo.easycall.EasyCallIvrVO">
+        <result property="id" column="id"/>
+        <result property="ivrNodeName" column="node_name"/>
+    </resultMap>
+
+    <sql id="selectInboundLlmVo">
+        select id, llm_account_id, callee, voice_code, voice_source, service_type, asr_provider, ai_transfer_type, ai_transfer_data, ivr_id, satisf_survey_ivr_id, inbound_alias from cc_inbound_llm_account
+    </sql>
+
+    <select id="selectInboundLlmList" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        <where>
+            <if test="llmAccountId != null">and llm_account_id = #{llmAccountId}</if>
+            <if test="callee != null and callee != ''">and callee = #{callee}</if>
+            <if test="inboundAlias != null and inboundAlias != ''">and inbound_alias = #{inboundAlias}</if>
+            <if test="voiceCode != null and voiceCode != ''">and voice_code = #{voiceCode}</if>
+            <if test="voiceSource != null and voiceSource != ''">and voice_source = #{voiceSource}</if>
+            <if test="serviceType != null and serviceType != ''">and service_type = #{serviceType}</if>
+            <if test="asrProvider != null and asrProvider != ''">and asr_provider = #{asrProvider}</if>
+            <if test="aiTransferType != null and aiTransferType != ''">and ai_transfer_type = #{aiTransferType}</if>
+            <if test="aiTransferData != null and aiTransferData != ''">and ai_transfer_data = #{aiTransferData}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectInboundLlmById" parameterType="Integer" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectInboundLlmByCallee" parameterType="String" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        where callee = #{callee}
+    </select>
+
+    <insert id="insertInboundLlm" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+        insert into cc_inbound_llm_account
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="llmAccountId != null">llm_account_id,</if>
+            <if test="callee != null and callee != ''">callee,</if>
+            <if test="inboundAlias != null and inboundAlias != ''">inbound_alias,</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code,</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source,</if>
+            <if test="serviceType != null and serviceType != ''">service_type,</if>
+            <if test="asrProvider != null and asrProvider != ''">asr_provider,</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type,</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data,</if>
+            <if test="ivrId != null">ivr_id,</if>
+            <if test="satisfSurveyIvrId != null">satisf_survey_ivr_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="llmAccountId != null">#{llmAccountId},</if>
+            <if test="callee != null and callee != ''">#{callee},</if>
+            <if test="inboundAlias != null and inboundAlias != ''">#{inboundAlias},</if>
+            <if test="voiceCode != null and voiceCode != ''">#{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">#{voiceSource},</if>
+            <if test="serviceType != null and serviceType != ''">#{serviceType},</if>
+            <if test="asrProvider != null and asrProvider != ''">#{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">#{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">#{aiTransferData},</if>
+            <if test="ivrId != null">#{ivrId},</if>
+            <if test="satisfSurveyIvrId != null">#{satisfSurveyIvrId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateInboundLlm" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+        update cc_inbound_llm_account
+        <set>
+            <if test="llmAccountId != null">llm_account_id = #{llmAccountId},</if>
+            <if test="callee != null and callee != ''">callee = #{callee},</if>
+            <if test="inboundAlias != null and inboundAlias != ''">inbound_alias = #{inboundAlias},</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code = #{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source = #{voiceSource},</if>
+            <if test="serviceType != null and serviceType != ''">service_type = #{serviceType},</if>
+            <if test="asrProvider != null and asrProvider != ''">asr_provider = #{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type = #{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data = #{aiTransferData},</if>
+            <if test="ivrId != null">ivr_id = #{ivrId},</if>
+            <if test="satisfSurveyIvrId != null">satisf_survey_ivr_id = #{satisfSurveyIvrId},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteInboundLlmById" parameterType="Integer">
+        delete from cc_inbound_llm_account where id = #{id}
+    </delete>
+
+    <delete id="deleteInboundLlmByIds" parameterType="String">
+        delete from cc_inbound_llm_account where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectLlmAccountList" resultMap="LlmAccountResult">
+        select id, name, account_json, account_entity, provider_class_name
+        from cc_llm_agent_account
+        order by id desc
+    </select>
+
+    <select id="selectLlmAccountById" parameterType="Integer" resultMap="LlmAccountResult">
+        select id, name, account_json, account_entity, provider_class_name
+        from cc_llm_agent_account
+        where id = #{id}
+    </select>
+
+    <select id="selectVoiceCodeByCode" parameterType="String" resultMap="VoiceCodeResult">
+        select voice_code, voice_name, voice_source
+        from cc_tts_aliyun
+        where voice_code = #{voiceCode}
+    </select>
+
+    <!-- 查询ASR提供商列表 -->
+    <select id="selectAsrProviderList" resultType="java.util.Map">
+        select distinct provider as 'key', provider as 'value'
+        from cc_tts_aliyun
+        where provider is not null and provider != ''
+        order by provider
+    </select>
+
+    <!-- 查询TTS音色来源列表 -->
+    <select id="selectVoiceSourceList" resultType="java.util.Map">
+        select distinct voice_source as 'key', voice_source as 'value'
+        from cc_tts_aliyun
+        where voice_source is not null and voice_source != ''
+        order by voice_source
+    </select>
+
+    <!-- 根据音色来源查询音色列表 -->
+    <select id="selectVoiceListBySource" parameterType="String" resultMap="VoiceCodeResult">
+        select voice_code, voice_name, voice_source
+        from cc_tts_aliyun
+        where voice_source = #{voiceSource}
+        and voice_enabled = 1
+        order by priority, id
+    </select>
+
+    <!-- 查询业务组列表 -->
+    <select id="selectBizGroupList" resultMap="BizGroupResult">
+        select group_id, biz_group_name
+        from cc_biz_group
+        order by sort_no, group_id
+    </select>
+
+    <!-- 查询出局网关列表 -->
+    <select id="selectOutboundGatewayList" resultMap="GatewayResult">
+        select id, gw_name, profile_name, caller, callee_prefix, gw_addr, codec, gw_desc, purpose
+        from cc_gateways
+        where purpose in (2, 3)
+        order by id
+    </select>
+
+    <!-- 查询IVR列表 -->
+    <select id="selectIvrList" resultMap="IvrResult">
+        select id, node_name
+        from cc_ivr
+        order by id
+    </select>
+
+</mapper>