소스 검색

cid流程接入新AI外呼EasyCall

lmx 5 일 전
부모
커밋
efe03f8215
32개의 변경된 파일1610개의 추가작업 그리고 9개의 파일을 삭제
  1. 1 0
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  2. 8 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  3. 209 0
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  4. 8 1
      fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java
  5. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  6. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  7. 21 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  8. 3 4
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  9. 326 0
      fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java
  10. 111 0
      fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java
  11. 79 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  12. 84 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  13. 114 4
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  14. 22 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  15. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallAddCallListParam.java
  16. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBusiGroupVO.java
  17. 216 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java
  18. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCommonAddCallListParam.java
  19. 36 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java
  20. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java
  21. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallLlmAccountVO.java
  22. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPageResult.java
  23. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPhoneItemVO.java
  24. 34 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordQueryParam.java
  25. 39 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordVO.java
  26. 22 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskQueryParam.java
  27. 54 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskVO.java
  28. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java
  29. 4 0
      fs-service/src/main/resources/application-common.yml
  30. 48 0
      fs-service/src/main/resources/application-dev.yml
  31. 4 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  32. 11 0
      fs-service/src/main/resources/mapper/company/EasyCallMapper.xml

+ 1 - 0
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -16,6 +16,7 @@ public enum DataSourceType
 
     SOP,
     WX,
+    EASYCALL,
     WX_READ,
     /**
      * 从库

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

@@ -23,6 +23,8 @@ import com.fs.company.domain.CompanyVoiceRoboticWx;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CdrBodyVo;
+import com.fs.company.vo.CdrDetailVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -221,6 +223,12 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.callerResult(result);
         return R.ok();
     }
+
+    @PostMapping("/callerResult4EasyCall")
+    public R callerResult4EasyCall(CdrDetailVo cdr) {
+        companyVoiceRoboticService.callerResult4EasyCall(cdr);
+        return R.ok();
+    }
     /**
      * 外呼回调
      */

+ 209 - 0
fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java

@@ -0,0 +1,209 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.*;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/6 14:28
+ * @description EasyCallCenter365 外呼管理控制器
+ * <p>
+ * 所有接口均需要登录态,通过 TokenService 获取当前登录用户的 companyId。
+ * 接口地址前缀:/company/easyCall
+ * 对应三方服务器:http://129.28.164.235:8899
+ */
+@Api(tags = "EasyCallCenter365外呼管理")
+@RestController
+@RequestMapping("/company/easyCall")
+public class EasyCallController extends BaseController {
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /** 用于获取当前登录用户信息,从中取出 companyId */
+    @Autowired
+    private TokenService tokenService;
+
+    // =================== 基础数据查询 ===================
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/list")
+    public R getGatewayList() {
+        // 获取当前登录用户的公司ID(当前仅用于传递,未涉及多公司隔离)
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取大模型配置列表
+     * 创建 AI 外呼任务时需要选择大模型配置,决定机器人论某能力和话术
+     */
+    @ApiOperation("获取大模型配置列表")
+    @GetMapping("/llmAccount/list")
+    public R getLlmAccountList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallLlmAccountVO> list = easyCallService.getLlmAccountList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取音色列表
+     * AI 外呼任务用于配置语音合成声音风格,如普通话男声、女声等
+     */
+    @ApiOperation("获取音色列表")
+    @GetMapping("/voiceCode/list")
+    public R getVoiceCodeList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallVoiceCodeVO> list = easyCallService.getVoiceCodeList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取技能组列表
+     * 技能组也叫业务组,AI 外呼需要转人工时用于指定转入哪个客服组
+     */
+    @ApiOperation("获取技能组列表")
+    @GetMapping("/busiGroup/list")
+    public R getBusiGroupList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallBusiGroupVO> list = easyCallService.getBusiGroupList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    // =================== 任务管理 ===================
+
+    /**
+     * 分页查询外呼任务列表
+     * 支持按任务ID、任务名称、创建时间范围进行过滤,同时返回拨打统计
+     */
+    @ApiOperation("任务列表查询")
+    @PostMapping("/task/list")
+    public R getTaskList(@RequestBody EasyCallTaskQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallTaskVO> result = easyCallService.getTaskList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 分页查询通话记录
+     * 支持按号码、拨打时间、通话时长、通话状态等多条件过滤
+     * callType:01-呼入,02-AI外呼,03-人工外呼,三种类型不支持混合查询
+     */
+    @ApiOperation("通话记录查询")
+    @PostMapping("/record/list")
+    public R getRecordList(@RequestBody EasyCallRecordQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallRecordVO> result = easyCallService.getRecordList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 创建外呼任务
+     * 创建成功后返回包含 batchId 的任务对象,后续操作(启动/追加名单)均需要该 batchId
+     */
+    @ApiOperation("创建外呼任务")
+    @PostMapping("/task/create")
+    public R createTask(@RequestBody EasyCallCreateTaskParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallTaskVO task = easyCallService.createTask(param, companyId);
+        return R.ok().put("data", task);
+    }
+
+    /**
+     * 启动外呼任务
+     * 任务创建后默认处于待机状态,调用此接口才会开始对名单中的号码拨打
+     * @param batchId 任务ID,来自创建任务或任务列表查询返回的 batchId
+     */
+    @ApiOperation("启动外呼任务")
+    @GetMapping("/task/start")
+    public R startTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.startTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 停止外呼任务
+     * 停止后任务不再拨打,可重新调用启动接口继续运行
+     * @param batchId 任务ID
+     */
+    @ApiOperation("停止外呼任务")
+    @GetMapping("/task/stop")
+    public R stopTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.stopTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 名单管理 ===================
+
+    /**
+     * AI 外呼专用追加名单
+     * 仅支持 AI 外呼任务,phoneList 为纯手机号字符串列表
+     * 任务启动后也可以继续追加,实现动态补充
+     */
+    @ApiOperation("AI外呼追加名单")
+    @PostMapping("/callList/addAi")
+    public R addAiCallList(@RequestBody EasyCallAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addAiCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 通用追加名单
+     * 同时支持 AI 外呼和通知提醒任务
+     * 通知提醒任务必须在每个号码条目中填写 noticeContent(具体提醒话术)
+     * bizJson 可传入需要传递给机器人的业务数据小弹口
+     */
+    @ApiOperation("通用追加名单")
+    @PostMapping("/callList/addCommon")
+    public R addCommonCallList(@RequestBody EasyCallCommonAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addCommonCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 录音相关 ===================
+
+    /**
+     * 将通话记录中的录音相对路径拼接为可直接访问的完整 URL
+     * 录音查询接口返回的 wavFileUrl 属于相对路径,需要拼接 baseUrl 才能下载
+     * @param wavFileUrl 通话记录中返回的 wavFileUrl 字段
+     */
+    @ApiOperation("获取录音文件完整访问url")
+    @GetMapping("/record/fileUrl")
+    public R getRecordFileUrl(@RequestParam String wavFileUrl) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        String fullUrl = easyCallService.getRecordFileUrl(wavFileUrl, companyId);
+        return R.ok().put("data", fullUrl);
+    }
+}

+ 8 - 1
fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -46,17 +46,24 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.easycall.druid.master")
+    public DataSource easyCallSource() {
+        return new DruidDataSource();
+    }
+
 
 
     @Bean
     @Primary
     public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource,
-                                        @Qualifier("slaveDataSource") DataSource slaveDataSource , @Qualifier("wxDataSource") DataSource wxDataSource) {
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource , @Qualifier("wxDataSource") DataSource wxDataSource, @Qualifier("easyCallSource") DataSource easyCallSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SLAVE.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         targetDataSources.put(DataSourceType.WX, wxDataSource);
+        targetDataSources.put(DataSourceType.EASYCALL.name(), easyCallSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }
 

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

@@ -112,6 +112,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 ).permitAll()
                 .antMatchers("/test").anonymous()
                 .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
+                .antMatchers("/company/companyVoiceRobotic/callerResult4EasyCall").anonymous()
                 .antMatchers("/qw/getJsapiTicket/**").anonymous()
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()

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

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
+import com.fs.company.vo.DictVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -69,4 +70,6 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
     void finishRobotic(@Param("id") Long id);
 
     int finishAddWxRobotic(@Param("collect") List<Long> collect);
+
+    List<DictVO> getDictDataList(@Param("dictType") String dictType);
 }

+ 21 - 0
fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java

@@ -0,0 +1,21 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/7 10:43
+ * @description
+ */
+
+@Repository
+public interface EasyCallMapper {
+
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallCallPhoneVO getCallPhoneInfoByUuid(@Param("uuid") String uuid);
+
+}

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

@@ -5,10 +5,7 @@ import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CalltaskcreateaiCustomizeResult;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.param.ExecutionContext;
-import com.fs.company.vo.AddWxClientVo;
-import com.fs.company.vo.AiCallConfigVO;
-import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
-import com.fs.company.vo.WorkflowExecRecordVo;
+import com.fs.company.vo.*;
 
 import java.util.List;
 import java.util.Set;
@@ -79,6 +76,8 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     void callerResult(PushIIntentionResult result);
 
+    void callerResult4EasyCall(CdrDetailVo result);
+
     void dispenseWx(Long id);
 
     void addCompany(AddWxClientVo vo);

+ 326 - 0
fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java

@@ -0,0 +1,326 @@
+package com.fs.company.service.easycall;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.vo.easycall.*;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * EasyCallCenter365 外呼服务实现类
+ * <p>
+ * 对接 EasyCallCenter365 外呼系统的所有 HTTP 接口,包括:
+ * 网关、大模型、音色、技能组的基础数据查询,
+ * 外呼任务的创建/启动/停止,名单追加,以及通话记录查询。
+ * <p>
+ * 服务地址通过配置项 easycall.base-url 注入,默认值:http://129.28.164.235:8899
+ */
+@Service
+@Slf4j
+public class EasyCallServiceImpl implements IEasyCallService {
+
+    /** EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取 */
+    @Value("${easycall.base-url:http://129.28.164.235:8899}")
+    private String baseUrl;
+
+    // =================== EasyCallCenter365 接口路径常量 ===================
+    /** 获取外呼网关列表 */
+    private static final String API_GATEWAY_LIST     = "/aicall/api/gateway/list";
+    /** 获取大模型配置列表 */
+    private static final String API_LLMACCOUNT_LIST  = "/aicall/api/llmacount/list";
+    /** 获取音色列表 */
+    private static final String API_VOICECODE_LIST   = "/aicall/api/voicecode/list";
+    /** 获取技能组(业务组)列表 */
+    private static final String API_BUSIGROUP_LIST   = "/aicall/api/busigroup/list";
+    /** 分页查询任务列表 */
+    private static final String API_CALLTASK_LIST    = "/aicall/api/calltask/list";
+    /** 分页查询通话记录 */
+    private static final String API_RECORDS_LIST     = "/aicall/api/records/list";
+    /** 创建外呼任务 */
+    private static final String API_CREATE_TASK      = "/aicall/api/ai/createTask";
+    /** 启动外呼任务(GET,携带 batchId 参数) */
+    private static final String API_START_TASK       = "/aicall/api/ai/startTask";
+    /** 停止外呼任务(GET,携带 batchId 参数) */
+    private static final String API_STOP_TASK        = "/aicall/api/ai/stopTask";
+    /** AI外呼专用追加名单(仅支持 AI 外呼任务,phoneList 为纯号码列表) */
+    private static final String API_AI_ADD_CALL_LIST = "/aicall/api/ai/addCallList";
+    /** 通用追加名单(同时支持 AI 外呼和通知提醒任务,支持传入提醒内容和业务数据) */
+    private static final String API_COMMON_ADD_LIST  = "/aicall/api/common/addCallList";
+
+    // =================== 基础数据查询接口 ===================
+
+    /**
+     * 查询外呼网关列表
+     * 网关用于创建任务时选择线路,purpose=2 表示仅限AI外呼,purpose=3 表示不限制
+     */
+    @Override
+    public List<EasyCallGatewayVO> getGatewayList(Long companyId) {
+        // 拼接完整请求地址并发起 GET 请求
+        String url = buildUrl(API_GATEWAY_LIST);
+        JSONObject result = doGet(url);
+        // 从响应的 data 字段中取出网关数组并转为 Java 对象列表
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallGatewayVO.class);
+    }
+
+    /**
+     * 查询大模型配置列表
+     * 大模型配置用于创建任务时绑定 AI 话术,支持 DeepSeek、Coze、MaxKB、Dify 等
+     */
+    @Override
+    public List<EasyCallLlmAccountVO> getLlmAccountList(Long companyId) {
+        String url = buildUrl(API_LLMACCOUNT_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallLlmAccountVO.class);
+    }
+
+    /**
+     * 查询音色列表
+     * 音色用于 AI 外呼任务(taskType=1)时配置语音合成的声音风格
+     */
+    @Override
+    public List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId) {
+        String url = buildUrl(API_VOICECODE_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallVoiceCodeVO.class);
+    }
+
+    /**
+     * 查询技能组(业务组)列表
+     * 技能组用于 AI 外呼需要转人工时,指定接入的人工客服分组
+     */
+    @Override
+    public List<EasyCallBusiGroupVO> getBusiGroupList(Long companyId) {
+        String url = buildUrl(API_BUSIGROUP_LIST);
+        JSONObject result = doGet(url);
+        JSONArray data = result.getJSONArray("data");
+        return data == null ? new ArrayList<>() : data.toJavaList(EasyCallBusiGroupVO.class);
+    }
+
+    // =================== 任务管理接口 ===================
+
+    /**
+     * 分页查询外呼任务列表
+     * 返回任务基本信息以及各任务的拨打统计数据(总量、接通量、接通率等)
+     */
+    @Override
+    public EasyCallPageResult<EasyCallTaskVO> getTaskList(EasyCallTaskQueryParam param, Long companyId) {
+        String url = buildUrl(API_CALLTASK_LIST);
+        // 将查询参数序列化为 JSON 后 POST 发送
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        // 响应为分页格式:{ total, rows },统一用 parsePageResult 解析
+        return parsePageResult(result, EasyCallTaskVO.class);
+    }
+
+    /**
+     * 分页查询通话记录
+     * 支持按号码、拨打时间、通话时长、通话状态等多条件过滤
+     * callType:01-呼入,02-AI外呼,03-人工外呼,三种类型不支持混合查询
+     * pageNum 和 pageSize 均为空时返回全部数据
+     */
+    @Override
+    public EasyCallPageResult<EasyCallRecordVO> getRecordList(EasyCallRecordQueryParam param, Long companyId) {
+        String url = buildUrl(API_RECORDS_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        return parsePageResult(result, EasyCallRecordVO.class);
+    }
+
+    /**
+     * 创建外呼任务
+     * taskType=1 为 AI 外呼(需传 voiceCode、voiceSource)
+     * taskType=2 为通知提醒(需传 playTimes)
+     * 创建成功后返回包含 batchId 的任务信息,后续启动/停止/追加名单均依赖 batchId
+     */
+    @Override
+    public EasyCallTaskVO createTask(EasyCallCreateTaskParam param, Long companyId) {
+        String url = buildUrl(API_CREATE_TASK);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        // 响应的 data 字段即为创建成功的任务对象
+        JSONObject data = result.getJSONObject("data");
+        return data == null ? null : data.toJavaObject(EasyCallTaskVO.class);
+    }
+
+    /**
+     * 启动外呼任务
+     * 任务创建后默认处于待机状态,调用此接口后开始拨打名单中的号码
+     */
+    @Override
+    public void startTask(Long batchId, Long companyId) {
+        // batchId 以 Query 参数形式拼接到 URL
+        String url = buildUrl(API_START_TASK) + "?batchId=" + batchId;
+        JSONObject result = doGet(url);
+        checkSuccess(result);
+    }
+
+    /**
+     * 停止外呼任务
+     * 停止后任务不再继续拨打,可重新启动
+     */
+    @Override
+    public void stopTask(Long batchId, Long companyId) {
+        String url = buildUrl(API_STOP_TASK) + "?batchId=" + batchId;
+        JSONObject result = doGet(url);
+        checkSuccess(result);
+    }
+
+    // =================== 名单管理接口 ===================
+
+    /**
+     * AI 外呼专用追加名单
+     * 仅支持 AI 外呼任务(taskType=1),phoneList 传入纯手机号列表
+     * 注意:任务启动后也可以继续追加名单
+     */
+    @Override
+    public void addAiCallList(EasyCallAddCallListParam param, Long companyId) {
+        String url = buildUrl(API_AI_ADD_CALL_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        checkSuccess(result);
+    }
+
+    /**
+     * 通用追加名单
+     * 同时支持 AI 外呼(taskType=1)和通知提醒(taskType=2)任务
+     * 通知提醒任务必须在 phoneList 的每条记录中填写 noticeContent(播报内容)
+     * bizJson 字段可传入需要传递给机器人的业务数据(如客户姓名、订单号等)
+     */
+    @Override
+    public void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId) {
+        String url = buildUrl(API_COMMON_ADD_LIST);
+        JSONObject result = doPost(url, JSON.toJSONString(param));
+        checkSuccess(result);
+    }
+
+    // =================== 录音相关接口 ===================
+
+    /**
+     * 拼接录音文件完整访问 URL
+     * 通话记录查询返回的 wavFileUrl 是相对路径(如 /recordings/files?filename=xxx.wav)
+     * 需要拼接系统 baseUrl 才能直接在浏览器访问或下载
+     */
+    @Override
+    public String getRecordFileUrl(String wavFileUrl, Long companyId) {
+        // 录音路径为空时直接返回空字符串,避免拼出错误地址
+        if (StringUtils.isEmpty(wavFileUrl)) {
+            return "";
+        }
+        String base = trimTrailingSlash(baseUrl);
+        return base + wavFileUrl;
+    }
+
+    // =================== 私有工具方法 ===================
+
+    /**
+     * 拼接完整请求地址
+     * 将 baseUrl(如 http://129.28.164.235:8899)与接口路径(如 /aicall/api/gateway/list)拼接
+     */
+    private String buildUrl(String path) {
+        return trimTrailingSlash(baseUrl) + path;
+    }
+
+    /**
+     * 去除 URL 末尾多余的斜杠
+     * 防止拼接后出现 http://xxx//path 的双斜杠问题
+     */
+    private String trimTrailingSlash(String url) {
+        if (url != null && url.endsWith("/")) {
+            return url.substring(0, url.length() - 1);
+        }
+        return url;
+    }
+
+    /**
+     * 发送 GET 请求
+     * 用于不需要请求体的查询类接口(如网关列表、启动/停止任务等)
+     */
+    private JSONObject doGet(String url) {
+        log.info("EasyCall GET 请求: {}", url);
+        try {
+            String body = HttpUtil.createGet(url)
+                    .header("Accept", "application/json")
+                    .execute()
+                    .body();
+            log.info("EasyCall GET 响应: {}", body);
+            // 解析响应 JSON 并校验 code 字段,非 0 则抛异常
+            return parseAndCheck(body);
+        } catch (RuntimeException e) {
+            // RuntimeException 直接向上抛(已经是业务异常,不重新包装)
+            throw e;
+        } catch (Exception e) {
+            log.error("EasyCall GET 请求异常, url: {}", url, e);
+            throw new RuntimeException("请求EasyCallCenter365失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 发送 POST 请求
+     * 用于需要传入 JSON 请求体的接口(如创建任务、查询列表、追加名单等)
+     */
+    private JSONObject doPost(String url, String jsonBody) {
+        log.info("EasyCall POST 请求: {}, body: {}", url, jsonBody);
+        try {
+            HttpRequest request = HttpUtil.createPost(url)
+                    .header("Accept", "application/json")
+                    .header("Content-Type", "application/json")
+                    .body(jsonBody);
+            String body = request.execute().body();
+            log.info("EasyCall POST 响应: {}", body);
+            return parseAndCheck(body);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("EasyCall POST 请求异常, url: {}", url, e);
+            throw new RuntimeException("请求EasyCallCenter365失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 解析响应 JSON 并校验业务状态码
+     * EasyCallCenter365 的接口规范:code=0 表示成功,非 0 表示失败
+     * 失败时将 msg 字段内容包装为 RuntimeException 向上抛出
+     */
+    private JSONObject parseAndCheck(String responseBody) {
+        JSONObject json = JSON.parseObject(responseBody);
+        Integer code = json.getInteger("code");
+        if (code == null || code != 0) {
+            String msg = json.getString("msg");
+            log.error("EasyCallCenter365 接口返回错误, code: {}, msg: {}", code, msg);
+            throw new RuntimeException("EasyCallCenter365 接口错误: " + msg);
+        }
+        return json;
+    }
+
+    /**
+     * 操作类接口成功校验(占位方法)
+     * 启动、停止、追加名单等操作类接口无 data 返回,
+     * 成功与否已由 parseAndCheck 中的 code 判断保证,此处无需额外处理
+     */
+    private void checkSuccess(JSONObject result) {
+        // parseAndCheck 中 code != 0 时已抛异常,走到这里即代表操作成功
+    }
+
+    /**
+     * 解析分页响应结果
+     * EasyCallCenter365 分页接口格式:{ code, msg, total, rows }
+     * 将 total(总数)和 rows(当前页数据)封装到 EasyCallPageResult 中返回
+     */
+    private <T> EasyCallPageResult<T> parsePageResult(JSONObject result, Class<T> clazz) {
+        EasyCallPageResult<T> pageResult = new EasyCallPageResult<>();
+        // total 可能为 null(如无数据时),默认为 0
+        Long total = result.getLong("total");
+        pageResult.setTotal(total == null ? 0L : total);
+        // rows 为当前页的数据数组,反序列化为指定 VO 类型列表
+        JSONArray rows = result.getJSONArray("rows");
+        pageResult.setRows(rows == null ? new ArrayList<>() : rows.toJavaList(clazz));
+        return pageResult;
+    }
+}

+ 111 - 0
fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java

@@ -0,0 +1,111 @@
+package com.fs.company.service.easycall;
+
+import com.fs.company.vo.easycall.*;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 外呼服务接口
+ */
+public interface IEasyCallService {
+
+    /**
+     * 获取网关列表
+     *
+     * @param companyId 公司id
+     * @return 网关列表
+     */
+    List<EasyCallGatewayVO> getGatewayList(Long companyId);
+
+    /**
+     * 获取大模型配置列表
+     *
+     * @param companyId 公司id
+     * @return 大模型配置列表
+     */
+    List<EasyCallLlmAccountVO> getLlmAccountList(Long companyId);
+
+    /**
+     * 获取音色列表
+     *
+     * @param companyId 公司id
+     * @return 音色列表
+     */
+    List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId);
+
+    /**
+     * 获取技能组列表
+     *
+     * @param companyId 公司id
+     * @return 技能组列表
+     */
+    List<EasyCallBusiGroupVO> getBusiGroupList(Long companyId);
+
+    /**
+     * 任务列表查询
+     *
+     * @param param     查询参数
+     * @param companyId 公司id
+     * @return 分页任务列表
+     */
+    EasyCallPageResult<EasyCallTaskVO> getTaskList(EasyCallTaskQueryParam param, Long companyId);
+
+    /**
+     * 通话记录查询
+     *
+     * @param param     查询参数
+     * @param companyId 公司id
+     * @return 分页通话记录
+     */
+    EasyCallPageResult<EasyCallRecordVO> getRecordList(EasyCallRecordQueryParam param, Long companyId);
+
+    /**
+     * 创建外呼任务
+     *
+     * @param param     任务参数
+     * @param companyId 公司id
+     * @return 创建结果
+     */
+    EasyCallTaskVO createTask(EasyCallCreateTaskParam param, Long companyId);
+
+    /**
+     * 启动外呼任务
+     *
+     * @param batchId   任务id
+     * @param companyId 公司id
+     */
+    void startTask(Long batchId, Long companyId);
+
+    /**
+     * 停止外呼任务
+     *
+     * @param batchId   任务id
+     * @param companyId 公司id
+     */
+    void stopTask(Long batchId, Long companyId);
+
+    /**
+     * AI外呼追加名单
+     *
+     * @param param     追加参数
+     * @param companyId 公司id
+     */
+    void addAiCallList(EasyCallAddCallListParam param, Long companyId);
+
+    /**
+     * 通用追加名单(支持AI外呼和通知提醒任务)
+     *
+     * @param param     追加参数
+     * @param companyId 公司id
+     */
+    void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId);
+
+    /**
+     * 获取录音文件访问URL
+     *
+     * @param wavFileUrl 录音文件相对路径
+     * @param companyId  公司id
+     * @return 完整的录音文件URL
+     */
+    String getRecordFileUrl(String wavFileUrl, Long companyId);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -29,6 +29,7 @@ import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyWxAccountMapper;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.store.config.StoreConfig;
@@ -42,6 +43,8 @@ import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
+import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+
 /**
  * 调用日志_ai打电话Service业务层处理
  *
@@ -263,6 +266,82 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
             log.error("处理回调结果异常:{}", result, ex);
         }
     }
+    @Async("callLogExcutor")
+    public void asyncHandleCalleeCallBackResult4EasyCall(EasyCallCallPhoneVO result, CompanyVoiceRoboticCallees callees) {
+        try {
+            String json = configService.selectConfigByKey("cid.config");
+            if (StringUtils.isBlank(json)) {
+                log.error("未配置cid.config");
+            }
+            CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+            if (null != result) {
+//                getDialogMapDomain getDialogMap = getDialogMapDomain.builder()
+//                        .uuid(uuid)
+//                        .build();
+                CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
+
+                companyVoiceRoboticCallLog.setStatus(2);
+                companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
+
+                CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
+                CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
+                Long setCompanyUserId = null;
+                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
+                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
+                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
+                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+                    setCompanyUserId = qwUser.getCompanyUserId();
+                }
+
+                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
+                // 调用接口查询通话其他信息
+//                TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
+                // 写入其他记录
+//                JSONObject telData = dialogMap.getTelData();
+                companyVoiceRoboticCallLog.setRecordPath(result.getWavfile());
+                companyVoiceRoboticCallLog.setContentList(result.getDialogue());
+                companyVoiceRoboticCallLog.setCallerNum(result.getTelephone());
+                companyVoiceRoboticCallLog.setCalleeNum(result.getCallerNumber());
+                companyVoiceRoboticCallLog.setUuid(result.getUuid());
+                Long createTime = result.getCalloutTime();
+                companyVoiceRoboticCallLog.setCallCreateTime(createTime);
+                Long answerTime = result.getCallEndTime();
+                companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
+                companyVoiceRoboticCallLog.setIntention(result.getIntent());
+                companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
+                BigDecimal callCharge = cidConfigVO.getCallCharge();
+                //
+                if (null == callCharge) {
+                    callCharge = DEFAULT_CALL_CHARGE;
+                }
+                //向上取整分钟数
+                BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+                BigDecimal multiply = divide.multiply(callCharge);
+                companyVoiceRoboticCallLog.setCost(multiply);
+                baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+
+                if (StringUtils.isNotBlank(result.getBizJson())) {
+                    JSONObject bizJson = JSONObject.parseObject(result.getBizJson());
+                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid")), JSONObject.class);
+                    if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
+                        Map<String, Object> param = new HashMap<>();
+                        param.put("callBackUuid", userData.getString("callBackUuid"));
+                        param.put("callSource", "callBack");
+                        CompletableFuture.runAsync(() -> {
+                            companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
+                        }, cidWorkFlowExecutor).thenRun(() -> {
+                            redisCache2.deleteObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
+                        });
+                    }
+                    redisCache2.deleteObject(bizJson.getString("callBackUuid"));
+                }
+            }
+
+        } catch (Exception ex) {
+            log.error("处理回调结果异常:{}", result, ex);
+        }
+    }
 
     @Async("callLogExcutor")
     public void asyncInsertCompanyVoiceRoboticCallLogBatch(List<CompanyVoiceRoboticCallLogCallphone> list) {

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

@@ -23,6 +23,7 @@ import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.param.SmsSendBatchParam;
@@ -44,6 +45,8 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
+import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+
 
 /**
  * 机器人外呼任务Service业务层处理
@@ -102,6 +105,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final RedisCache redisCache2;
     private final CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
     private final QwUserMapper qwUserMapper;
+    private final EasyCallMapper easyCallMapper;
     /**
      * 查询机器人外呼任务
      *
@@ -760,6 +764,34 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
     }
 
+    /**
+     * 外呼结果回调
+     * @param result
+     */
+    @Override
+    public void callerResult4EasyCall(CdrDetailVo result){
+//        EASYCALL
+        log.info("进入easyCall外呼结果回调:{}", JSON.toJSONString(result));
+        if(result == null || StringUtils.isBlank(result.getUuid())) return;
+        //调用查询查找外呼结果
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if(null == callPhoneRes){
+            log.error("easyCall外呼回调信息未查询到结果:{}", JSON.toJSONString(result));
+            return;
+        }
+        JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
+        String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
+        if(StringUtils.isBlank(cacheString)){
+            log.error("easyCall外呼回调缓存信息缺失:{}", JSON.toJSONString(result));
+            return;
+        }
+        JSONObject cacheInfo = JSONObject.parseObject(cacheString);
+        pushDialogContent4EasyCall(cacheInfo,callPhoneRes);
+        CompanyVoiceRoboticCallees callee =  companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
+        companyVoiceRoboticCallLogCallphoneService.asyncHandleCalleeCallBackResult4EasyCall(callPhoneRes,callee);
+        System.out.println(callPhoneRes);
+    }
+
     public void pushDialogContent(PushIIntentionResult result){
         Notify notify = result.getNotify();
         String intention = notify.getIntention();
@@ -803,12 +835,64 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         bindCompany(companyWxClient, roboticWxList);
         companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
     }
+    public void pushDialogContent4EasyCall(JSONObject cacheInfo,EasyCallCallPhoneVO callPhoneRes){
+
+        String intention = getIntention(callPhoneRes.getIntent());
+        if(StringUtils.isEmpty(intention)){
+            intention = "0";
+        }
+        CompanyVoiceRoboticCallees callee =  companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
+        callee.setUuid(callPhoneRes.getUuid());
+        callee.setIntention(intention);
+        callee.setJson(JSON.toJSONString(callPhoneRes.getDialogue()));
+        callee.setResult(1);
+        companyVoiceRoboticCalleesMapper.updateById(callee);
+        CrmCustomer crmCustomer = crmCustomerMapper.selectCrmCustomerById(callee.getUserId());
+        crmCustomer.setIntention(intention);
+        crmCustomerMapper.updateById(crmCustomer);
+        CompanyVoiceRobotic companyVoiceRobotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(callee.getRoboticId());
+        //平均分配时 已经完成了分配 不需要走下面的分配动作
+        if(Integer.valueOf(0).equals(companyVoiceRobotic.getAddType())){
+            return;
+        }
+        List<CompanyVoiceRoboticWx> roboticWxList = companyVoiceRoboticWxMapper.selectByRoboticId(callee.getRoboticId(), intention);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(PubFun.listToNewList(roboticWxList, CompanyVoiceRoboticWx::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountList, CompanyWxAccount::getId);
+        roboticWxList.forEach(e -> e.setAccount(accountMap.get(e.getAccountId())));
+        CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callee.getRoboticId()).eq("customer_id", callee.getUserId()));
+        if(companyWxClient == null){
+            companyWxClient = new CompanyWxClient();
+        }
+        // 任务ID
+        companyWxClient.setRoboticId(callee.getRoboticId());
+        // 客户名称
+        companyWxClient.setNickName(callee.getUserName());
+        // 手机号
+        companyWxClient.setPhone(callee.getPhone());
+        // 微信号
+        companyWxClient.setCustomerId(callee.getUserId());
+        // 意向
+        companyWxClient.setIntention(intention);
+        bindCompany(companyWxClient, roboticWxList);
+        companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
+    }
+
+    public String getIntention(String intent){
+        List<DictVO> datas =  companyVoiceRoboticMapper.getDictDataList("customer_intention_level");
+        List<DictVO> collect = datas.stream().filter(e -> e.getDictLabel().equals(intent)).collect(Collectors.toList());
+        return collect.isEmpty() ? null : collect.get(0).getDictValue();
+    }
     public void pushBilling(PushIIntentionResult result){
         Notify notify = result.getNotify();
         CompanyVoiceRoboticCallees callee = getResultCalleeInfo(notify);
         callee.setResult(1);
         companyVoiceRoboticCalleesMapper.updateById(callee);
     }
+//    public void pushBilling4EasyCall(Long callId){
+//        CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(callId);
+//        callee.setResult(1);
+//        companyVoiceRoboticCalleesMapper.updateById(callee);
+//    }
     public CompanyVoiceRoboticCallees getResultCalleeInfo(Notify notify){
         String cacheString = (String) redisCache2.getCacheObject(companyVoiceRoboticCallLogCallphoneService.WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData());
         if(StringUtils.isNotBlank(cacheString)){

+ 114 - 4
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -1,23 +1,29 @@
 package com.fs.company.service.impl.call.node;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IWorkflowNode;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.service.impl.CompanyVoiceRoboticCallLogCallphoneServiceImpl;
 import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.ExecutionResult;
+import com.fs.company.vo.easycall.EasyCallCommonAddCallListParam;
+import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
+import com.fs.company.vo.easycall.EasyCallPhoneItemVO;
+import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -29,6 +35,13 @@ import java.util.concurrent.TimeUnit;
 public class AiCallTaskNode extends AbstractWorkflowNode {
     private static final CompanyWorkflowNodeMapper companyWorkflowNodeMapper = SpringUtils.getBean(CompanyWorkflowNodeMapper.class);
     private static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
+    /** EasyCallCenter365 外呼服务 */
+    private static final IEasyCallService easyCallService = SpringUtils.getBean(IEasyCallService.class);
+    /** 被叫人表 Mapper,用于获取手机号 */
+    private static final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper = SpringUtils.getBean(CompanyVoiceRoboticCalleesMapper.class);
+    private static final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneServiceImpl.class);
+    /** EasyCall 外呼回调信息存放到 Redis 的 key 前缀 */
+    public static final String EASYCALL_WORKFLOW_REDIS_KEY = "easycall:workflow:callback:";
     public static final String DELAY_CALL_KEY = "aiCallTask:delay:%s:%s:%s:";
     private final String CALL_FROM_CALLBACK = "callBack";
     private final String CALL_FROM_TIMER = "timer";
@@ -158,7 +171,9 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 if (bus == null) {
                     return ExecutionResult.failure().errorMessage("未找到业务数据").build();
                 }
-                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
+//                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
+                // EasyCallCenter365 外呼
+                 workflowCallPhoneOne4EasyCall(bus.getRoboticId(),bus.getCalleeId(), context, callConfigVo);
                 super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.PAUSED);
                 return ExecutionResult.paused()
                         .outputData(context.getVariables())
@@ -199,6 +214,101 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         return String.format(DELAY_CALL_KEY, cidGroupNo,nowDay.getHours(), nowDay.getMinutes());
     }
 
+    /**
+     * EasyCallCenter365 外呼节点核心方法(替代旧天天外呼的 workflowCallPhoneOne)
+     * <p>
+     * 执行流程:
+     * 1. 生成 callBackUuid 存入 Redis(用于后续回调匹配)
+     * 2. 查询被叫人手机号
+     * 3. 调用 EasyCallCenter365 创建 AI 外呼任务
+     * 4. 将被叫号码加入任务名单
+     * 5. 启动外呼任务
+     *
+     * @param calleeId    被叫人记录 ID(对应 company_voice_robotic_callees 表)
+     * @param context     工作流执行上下文,含 workflowInstanceId、currentNodeKey 等
+     * @param callConfigVo 节点配置,包含外呼线路 ID、大模型底座 ID、音色、技能组等参数
+     */
+    private void workflowCallPhoneOne4EasyCall(Long roboticId,Long calleeId, ExecutionContext context, AiCallConfigVO callConfigVo) {
+
+        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(roboticId);
+        // 1. 生成回调唯一标识符,后续回调时通过此 uuid 匹配对应的流程实例
+        String callBackUuid = UUID.randomUUID().toString();
+        // 将回调信息写入 Redis,保存 1 天
+        JSONObject callbackInfo = new JSONObject();
+        callbackInfo.put("callBackUuid", callBackUuid);
+        callbackInfo.put("nodeKey", context.getCurrentNodeKey());
+        callbackInfo.put("workflowInstanceId", context.getWorkflowInstanceId());
+        callbackInfo.put("calleeId", calleeId);
+        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,
+                callbackInfo.toJSONString(), 1, TimeUnit.DAYS);
+        // 将 callBackUuid 写入 context,供后续回调时从 context 取用
+        context.setVariable("callBackUuid", callBackUuid);
+
+        // 2. 获取被叫人手机号
+        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectById(calleeId);
+        if (callees == null || StringUtils.isBlank(callees.getPhone())) {
+            log.error("workflowCallPhoneOne4EasyCall: 被叫人不存在或手机号为空 - calleeId: {}", calleeId);
+            throw new RuntimeException("被叫人信息异常,calleeId: " + calleeId);
+        }
+
+        // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
+        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
+        createParam.setBatchName("workflow_" + context.getWorkflowInstanceId() + "_" + calleeId);
+        createParam.setThreadNum(100L);
+        // 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());
+        // 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 创建任务失败");
+        }
+        Long batchId = task.getBatchId();
+        log.info("workflowCallPhoneOne4EasyCall: EasyCall 任务创建成功 - batchId: {}", batchId);
+
+        // 5. 将被叫号码加入任务名单(使用通用追加接口,支持传入业务数据)
+        EasyCallCommonAddCallListParam addListParam = new EasyCallCommonAddCallListParam();
+        addListParam.setBatchId(batchId);
+        // 构建号码条目,bizJson 传入默认客户信息占位符
+        EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
+        phoneItem.setPhoneNum(callees.getPhone());
+        // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
+        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid));
+        addListParam.setPhoneList(Collections.singletonList(phoneItem));
+        easyCallService.addCommonCallList(addListParam, null);
+        log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
+                batchId, callees.getPhone());
+
+        // 6. 启动外呼任务
+        easyCallService.startTask(batchId, null);
+        log.info("workflowCallPhoneOne4EasyCall: 任务启动成功 - batchId: {}", batchId);
+        addLog.setStatus(1);
+        addLog.setCallbackUuid(callBackUuid);
+        companyVoiceRoboticCallLogCallphoneService.asyncInsertCompanyVoiceRoboticCallLog(addLog);
+        // 7. 将 batchId 写入 context,方便后续节点使用
+        context.setVariable("easyCallBatchId", batchId);
+    }
+
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {
 //        super.postExecute(context, result);

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

@@ -38,4 +38,26 @@ public class AiCallConfigVO {
      */
     private String cidGroupId;
 
+    // ===== EasyCallCenter365 外呼新字段 =====
+    /**
+     * 外呼线路(网关列表接口返回的 id)
+     */
+    private Long gatewayId;
+    /**
+     * 大模型底座(大模型配置列表接口返回的 id)
+     */
+    private Long llmAccountId;
+    /**
+     * 音色编号(音色列表接口返回的 voiceCode)
+     */
+    private String voiceCode;
+    /**
+     * 音色来源(音色列表接口返回的 voiceSource,如 aliyun_tts)
+     */
+    private String voiceSource;
+    /**
+     * tts厂商 / 技能组 id(技能组列表接口返回的 groupId)
+     */
+    private String busiGroupId;
+
 }

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallAddCallListParam.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 AI外呼追加名单请求参数
+ */
+@Data
+public class EasyCallAddCallListParam {
+    /** 任务id */
+    private Long batchId;
+    /** 号码列表 */
+    private List<String> phoneList;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBusiGroupVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 技能组VO
+ */
+@Data
+public class EasyCallBusiGroupVO {
+    /** 技能组id */
+    private Long groupId;
+    /** 技能组名称 */
+    private String bizGroupName;
+    /** 备注 */
+    private String notes;
+}

+ 216 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java

@@ -0,0 +1,216 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/7 11:21
+ * @description
+ */
+@Data
+public class EasyCallCallPhoneVO {
+    /**
+     * 主键ID
+     */
+    private String id;
+
+    /**
+     * 任务批次ID
+     */
+    private Integer batchId;
+
+    /**
+     * 电话号码
+     */
+    private String telephone;
+
+    /**
+     * 客户称呼
+     */
+    private String custName;
+
+    /**
+     * 任务创建时间
+     */
+    private Long createtime;
+
+    /**
+     * 呼叫状态
+     * 0. Not dialed
+     * 1. Entered call queue
+     * 2. Dialing (in progress)
+     * 3. Not connected (If the empty number detection feature is turned off)
+     * 4. Connected
+     * 5. Call dropped (hung up before transferring to agent)
+     * 6. Successfully transferred to agent or AI
+     * 7. Line failure
+     * 30. Not connected
+     * 31. Customer is on another call
+     * 32. Phone is powered off
+     * 33. Invalid number
+     * 34. No answer
+     * 35. Suspended service
+     * 36. Network busy
+     * 37. Voice assistant
+     * 38. Temporarily unavailable
+     * 39. Call restriction
+     */
+    private Short callstatus;
+
+    /**
+     * 外呼时间
+     */
+    private Long calloutTime;
+
+    /**
+     * 呼叫次数
+     */
+    private Short callcount;
+
+    /**
+     * 呼叫结束时间
+     */
+    private Long callEndTime;
+
+    /**
+     * 通话时长; 秒;
+     */
+    private Integer timeLen;
+
+    /**
+     * 人工接听的通话时长; 秒
+     */
+    private Integer validTimeLen;
+
+    /**
+     * 通话唯一标志
+     */
+    private String uuid;
+
+    /**
+     * 通话接通时间
+     */
+    private Long connectedTime;
+
+    /**
+     * 挂机原因
+     */
+    private String hangupCause;
+
+    /**
+     * 人工坐席应答时间
+     */
+    private Long answeredTime;
+
+    /**
+     * 对话内容
+     */
+    private String dialogue;
+
+    /**
+     * 全程通话录音文件名
+     */
+    private String wavfile;
+
+    /**
+     * 录音文件路径前缀
+     */
+    private String recordServerUrl;
+
+    /**
+     * 业务json数据
+     */
+    private String bizJson;
+
+    /**
+     * 交互轮次(一问一答算一轮交互)
+     */
+    private Integer dialogueCount;
+
+    /**
+     * 人工坐席工号
+     */
+    private String acdOpnum;
+
+    /**
+     * 加入转人工排队的时间;
+     */
+    private Long acdQueueTime;
+
+    /**
+     * 人工排队等待时长,秒
+     */
+    private Integer acdWaitTime;
+
+    /**
+     * tts text for voice call notification.
+     */
+    private String ttsText;
+
+    /**
+     * 空号检测文本
+     */
+    private String emptyNumberDetectionText;
+
+    /**
+     * 客户意向
+     */
+    private String intent;
+
+    /**
+     * asr时长(秒)
+     */
+    private Integer asrSeconds;
+
+    /**
+     * tts调用次数(次)
+     */
+    private Integer ttsTimes;
+
+    /**
+     * 大模型tts的字符数(字符)
+     */
+    private Integer ttsFlowTokens;
+
+    /**
+     * 总输入token数
+     */
+    private Integer inputTokens;
+
+    /**
+     * 总输出token数
+     */
+    private Integer outputTokens;
+
+    /**
+     * 总调用费用(asr+tts+大模型)
+     */
+    private BigDecimal totalCost;
+
+    /**
+     * 计费状态(1:已计费、0:未计费)
+     */
+    private Integer billingStatus;
+
+    /**
+     * 主叫号码
+     */
+    private String callerNumber;
+
+    /**
+     * customer dtmf input digits
+     */
+    private String ivrDtmfDigits;
+
+    /**
+     * manual agent answered time.
+     */
+    private Long manualAnsweredTime;
+
+    /**
+     * The duration of the manual agent service time.
+     */
+    private Long manualAnsweredTimeLen;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCommonAddCallListParam.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 通用追加名单请求参数
+ */
+@Data
+public class EasyCallCommonAddCallListParam {
+    /** 任务id */
+    private Long batchId;
+    /** 号码列表 */
+    private List<EasyCallPhoneItemVO> phoneList;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java

@@ -0,0 +1,36 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 创建任务请求参数
+ */
+@Data
+public class EasyCallCreateTaskParam {
+    /** 任务名称 */
+    private String batchName;
+    /** 最大外呼并发 */
+    private Long threadNum;
+    /** 任务类型:1-AI外呼,2-通知提醒 */
+    private Integer taskType;
+    /** 线路id(来自网关列表接口) */
+    private Long gatewayId;
+    /** 大模型配置id(来自大模型配置列表接口) */
+    private Long llmAccountId;
+    /** 音色编号(taskType=1时必填) */
+    private String voiceCode;
+    /** 音色来源(taskType=1时必填) */
+    private String voiceSource;
+    /** 播放次数(taskType=2时必填) */
+    private Integer playTimes;
+    /** 业务组id(AI外呼需要转人工时必填) */
+    private String groupId;
+    /** 预估接通率(taskType=0时必填,如接通率30%则传30) */
+    private Integer conntectRate;
+    /** 平均振铃时长(taskType=0时必填,单位秒) */
+    private Double avgRingTimeLen;
+    /** 平均通话时长(taskType=0时必填,单位秒) */
+    private Double avgCallTalkTimeLen;
+    /** 平均事后处理时长(taskType=0时必填,单位秒) */
+    private Double avgCallEndProcessTimeLen;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java

@@ -0,0 +1,28 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 网关信息VO
+ */
+@Data
+public class EasyCallGatewayVO {
+    /** 网关id */
+    private Long id;
+    /** 网关名称 */
+    private String gwName;
+    /** profile名称 */
+    private String profileName;
+    /** 外呼的主叫号码 */
+    private String caller;
+    /** 被叫前缀 */
+    private String calleePrefix;
+    /** 网关地址 */
+    private String gwAddr;
+    /** 语音编码 */
+    private String codec;
+    /** 网关描述 */
+    private String gwDesc;
+    /** 网关用途:2-AI外呼,3-不限制 */
+    private Integer purpose;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallLlmAccountVO.java

@@ -0,0 +1,28 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 大模型配置VO
+ */
+@Data
+public class EasyCallLlmAccountVO {
+    /** 大模型配置id */
+    private Integer id;
+    /** 大模型配置名称 */
+    private String name;
+    /** 配置详细信息 */
+    private String accountJson;
+    /** 大模型类型:LlmAccount / CozeAccount */
+    private String accountEntity;
+    /** 实现类:DeepSeekChat / Coze / MaxKB / Dify */
+    private String providerClassName;
+    /** 是否支持打断 */
+    private Integer interruptFlag;
+    /** 打断关键词列表 */
+    private String interruptKeywords;
+    /** 打断忽略关键字列表 */
+    private String interruptIgnoreKeywords;
+    /** 客户意向提示词 */
+    private String intentionTips;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPageResult.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @description EasyCallCenter365 分页结果VO
+ */
+@Data
+public class EasyCallPageResult<T> {
+    /** 总数 */
+    private Long total;
+    /** 当前页数据 */
+    private List<T> rows;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPhoneItemVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 通用追加名单中的号码条目VO
+ */
+@Data
+public class EasyCallPhoneItemVO {
+    /** 号码 */
+    private String phoneNum;
+    /** 提醒内容(提醒类任务必填) */
+    private String noticeContent;
+    /** 业务数据(需要传给机器人的业务数据,如客户信息) */
+    private Object bizJson;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordQueryParam.java

@@ -0,0 +1,34 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 通话记录查询请求参数
+ */
+@Data
+public class EasyCallRecordQueryParam {
+    /** 页号(传空则返回全部) */
+    private Integer pageNum;
+    /** 每页数量(传空则返回全部) */
+    private Integer pageSize;
+    /** 通话唯一标识 */
+    private String uuid;
+    /** 类型:01-呼入,02-AI外呼,03-人工外呼 */
+    private String callType;
+    /** 客户号码(外呼即被叫号码) */
+    private String telephone;
+    /** 通话状态:3-未接通,6-成功转接,7-线路故障 */
+    private Integer callstatus;
+    /** 主叫号码 */
+    private String callerNumber;
+    /** 拨打时间起(格式:yyyy-MM-dd HH:mm:ss) */
+    private String calloutTimeStart;
+    /** 拨打外呼时间止(格式:yyyy-MM-dd HH:mm:ss) */
+    private String calloutTimeEnd;
+    /** 通话时长起(单位秒) */
+    private Integer timeLenStart;
+    /** 通话时长止(单位秒) */
+    private Integer timeLenEnd;
+    /** 分机号 */
+    private String extnum;
+}

+ 39 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordVO.java

@@ -0,0 +1,39 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description EasyCallCenter365 通话记录VO
+ */
+@Data
+public class EasyCallRecordVO {
+    /** 通话唯一标识 */
+    private String uuid;
+    /** 号码 */
+    private String telephone;
+    /** 通话状态:3-未接通,6-成功转接,7-线路故障 */
+    private Integer callstatus;
+    /** 外呼记录id(AI外呼) */
+    private String sessionId;
+    /** 主叫号码 */
+    private String callerNumber;
+    /** 外呼/呼入时间 */
+    private String calloutTime;
+    /** 接通时间 */
+    private String answeredTime;
+    /** 挂机时间 */
+    private String callEndTime;
+    /** 挂机原因 */
+    private String hangupCause;
+    /** 录音文件url(需拼接系统baseUrl) */
+    private String wavFileUrl;
+    /** 对话内容 */
+    private List<Map<String, String>> dialogue;
+    /** 分机号 */
+    private String extnum;
+    /** 通话时长(秒) */
+    private Integer timeLen;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskQueryParam.java

@@ -0,0 +1,22 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 任务列表查询请求参数
+ */
+@Data
+public class EasyCallTaskQueryParam {
+    /** 页号 */
+    private Integer pageNum = 1;
+    /** 每页数量 */
+    private Integer pageSize = 10;
+    /** 任务id */
+    private Long batchId;
+    /** 任务名称(支持模糊搜索) */
+    private String batchName;
+    /** 任务创建时间起(格式:yyyy-MM-dd HH:mm:ss) */
+    private String createTimeStart;
+    /** 任务创建时间止(格式:yyyy-MM-dd HH:mm:ss) */
+    private String createTimeEnd;
+}

+ 54 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskVO.java

@@ -0,0 +1,54 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 任务信息VO(查询列表/创建任务通用)
+ */
+@Data
+public class EasyCallTaskVO {
+    /** 任务id */
+    private Long batchId;
+    /** 任务名称 */
+    private String batchName;
+    /** 最大外呼并发 */
+    private Long threadNum;
+    /** 任务类型:0-人工预测外呼,1-AI外呼,2-通知提醒 */
+    private Integer taskType;
+    /** 线路id */
+    private Long gatewayId;
+    /** 大模型配置id */
+    private Long llmAccountId;
+    /** 音色编号 */
+    private String voiceCode;
+    /** 音色来源 */
+    private String voiceSource;
+    /** 播放次数(taskType=2时必填) */
+    private Integer playTimes;
+    /** 业务组id */
+    private String groupId;
+    /** 预估接通率(taskType=0时必填) */
+    private Integer conntectRate;
+    /** 平均振铃时长(taskType=0时必填,单位秒) */
+    private Double avgRingTimeLen;
+    /** 平均通话时长(taskType=0时必填,单位秒) */
+    private Double avgCallTalkTimeLen;
+    /** 平均事后处理时长(taskType=0时必填,单位秒) */
+    private Double avgCallEndProcessTimeLen;
+    /** 创建时间(时间戳) */
+    private Long createtime;
+    /** 结束时间(时间戳,0代表没有结束) */
+    private Long stopTime;
+    /** 总电话量 */
+    private Integer phoneCount;
+    /** 未拨打电话量 */
+    private Integer noCallCount;
+    /** 已拨打电话量 */
+    private Integer callCount;
+    /** 接通量 */
+    private Integer connectCount;
+    /** 未接通量 */
+    private Integer noConnectCount;
+    /** 实际接通率 */
+    private Double realConnectRate;
+}

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

@@ -0,0 +1,16 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @description EasyCallCenter365 音色VO
+ */
+@Data
+public class EasyCallVoiceCodeVO {
+    /** 音色编号 */
+    private String voiceCode;
+    /** 音色名称 */
+    private String voiceName;
+    /** 声音源:aliyun_tts */
+    private String voiceSource;
+}

+ 4 - 0
fs-service/src/main/resources/application-common.yml

@@ -151,3 +151,7 @@ hsy:
   role_secret_key: T0RaaFl6UmhZV1V4WXpKbU5EWTBNMkZpT0RNNU9UY3daak0wTjJFd09XUQ==
   role_trn: trn:iam::2114522511:role/hylj
 
+# EasyCallCenter365 外呼系统配置
+easycall:
+  base-url: http://129.28.164.235:8899
+

+ 48 - 0
fs-service/src/main/resources/application-dev.yml

@@ -139,6 +139,54 @@ spring:
                     wall:
                         config:
                             multi-statement-allow: true
+        easycall:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: easycallcenter365
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
 
 #rocketmq:
 #    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址

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

@@ -208,4 +208,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </update>
+
+    <select id="getDictDataList" resultType="com.fs.company.vo.DictVO">
+        SELECT dict_type,dict_label,dict_value FROM `sys_dict_data` where  dict_type = #{dictType}
+    </select>
 </mapper>

+ 11 - 0
fs-service/src/main/resources/mapper/company/EasyCallMapper.xml

@@ -0,0 +1,11 @@
+<?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.EasyCallMapper">
+
+    <select id="getCallPhoneInfoByUuid" resultType="com.fs.company.vo.easycall.EasyCallCallPhoneVO">
+        select * from cc_call_phone where uuid = #{uuid}
+    </select>
+
+</mapper>