Browse Source

Merge remote-tracking branch 'origin/master'

yuhongqi 3 days ago
parent
commit
9daa71230c
43 changed files with 2166 additions and 322 deletions
  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. 124 0
      fs-service/src/main/java/com/fs/company/vo/CdrBodyVo.java
  16. 19 0
      fs-service/src/main/java/com/fs/company/vo/CdrDetailVo.java
  17. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallAddCallListParam.java
  18. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallBusiGroupVO.java
  19. 216 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java
  20. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCommonAddCallListParam.java
  21. 36 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java
  22. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java
  23. 28 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallLlmAccountVO.java
  24. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPageResult.java
  25. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallPhoneItemVO.java
  26. 34 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordQueryParam.java
  27. 39 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallRecordVO.java
  28. 22 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskQueryParam.java
  29. 54 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallTaskVO.java
  30. 16 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallVoiceCodeVO.java
  31. 82 0
      fs-service/src/main/java/com/fs/wxcid/FileToBase64Util.java
  32. 0 85
      fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java
  33. 74 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java
  34. 27 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java
  35. 20 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java
  36. 1 0
      fs-service/src/main/java/com/fs/wxcid/service/MessageService.java
  37. 28 2
      fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java
  38. 4 0
      fs-service/src/main/resources/application-common.yml
  39. 48 0
      fs-service/src/main/resources/application-dev.yml
  40. 4 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  41. 11 0
      fs-service/src/main/resources/mapper/company/EasyCallMapper.xml
  42. 19 6
      fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java
  43. 162 220
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

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

+ 124 - 0
fs-service/src/main/java/com/fs/company/vo/CdrBodyVo.java

@@ -0,0 +1,124 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CdrBodyVo {
+
+    private String uuid;
+
+    private String cdrType;
+
+    private String cdrBody;
+
+    private Long answeredTime;
+    private Long answeredTimeLen;
+    private String callee;
+    private String caller;
+    private List<ChatContent> chatContent;
+    private String extnum;
+    private String groupId;
+    private String hangupCause;
+    private Long hangupTime;
+    private String id;
+    private Long inboundTime;
+    private String ivrDtmfDigits;
+    private Long manualAnsweredTime;
+    private Long manualAnsweredTimeLen;
+    private String opnum;
+    private OutboundPhoneInfo outboundPhoneInfo;
+    private Integer remoteVideoPort;
+    private Boolean startDtmfExecuted;
+    private Long timeLen;
+    private Boolean transferredSucceed;
+    private String wavFile;
+
+    @Data
+    public static class ChatContent {
+        private String content;
+        private String content_type;
+        private String role;
+        private Integer completionTokens;
+        private Integer promptTokens;
+        private String bizJson;
+    }
+
+    @Data
+    public static class OutboundPhoneInfo {
+        private String acdOpnum;
+        private Long acdQueueTime;
+        private Long acdWaitTime;
+        private Long answeredTime;
+        private Long batchId;
+        private String bizJson;
+        private Long callEndTime;
+        private Integer callcount;
+        private Long calloutTime;
+        private Integer callstatus;
+        private Long connectedTime;
+        private Long createtime;
+        private String custName;
+        private List<Dialogue> dialogue;
+        private Integer dialogueCount;
+        private String emptyNumberDetectionText;
+        private Boolean hangup;
+        private String hangupCause;
+        private String id;
+        private String ivrDtmfDigits;
+        private Long manualAnsweredTime;
+        private Long manualAnsweredTimeLen;
+        private String recordServerUrl;
+        private TaskInfo taskInfo;
+        private String telephone;
+        private Long timeLen;
+        private Boolean transferred;
+        private String ttsText;
+        private String uuid;
+        private Long validTimeLen;
+        private String wavfile;
+    }
+
+    @Data
+    public static class Dialogue {
+        private String $ref;
+    }
+
+    @Data
+    public static class TaskInfo {
+        private String aiTransferData;
+        private String aiTransferType;
+        private String asrProvider;
+        private String authUsername;
+        private Integer autoStop;
+        private Long avgCallEndProcessTimeLen;
+        private Long avgCallTalkTimeLen;
+        private Long avgRingTimeLen;
+        private Long batchId;
+        private String batchName;
+        private String callNodeNo;
+        private String calleePrefix;
+        private String caller;
+        private String codec;
+        private Long createtime;
+        private Integer executing;
+        private Long gatewayId;
+        private String groupId;
+        private String gwAddr;
+        private String gwName;
+        private Integer ifcall;
+        private String ivrId;
+        private Long llmAccountId;
+        private Integer playTimes;
+        private String profileName;
+        private Integer rate;
+        private Integer register;
+        private Long stopTime;
+        private Integer taskType;
+        private Integer threadNum;
+        private String userid;
+        private String voiceCode;
+        private String voiceSource;
+    }
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/vo/CdrDetailVo.java

@@ -0,0 +1,19 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+@Data
+public class CdrDetailVo {
+
+    private String uuid;
+
+    /**
+     *  话单类型: inbound、outbound
+     */
+    private String cdrType;
+
+    /**
+     * 话单消息体;
+     */
+    private String cdrBody;
+}

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

+ 82 - 0
fs-service/src/main/java/com/fs/wxcid/FileToBase64Util.java

@@ -0,0 +1,82 @@
+package com.fs.wxcid;
+
+import javax.imageio.stream.FileImageInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Base64;
+
+/**
+ * 网络图片转Base64工具类
+ */
+public class FileToBase64Util {
+
+    /**
+     * 网络文件 URL 转 byte[]
+     *
+     * @param fileUrl 文件的网络URL
+     * @return 文件字节数组
+     */
+    public static byte[] downloadToBytes(String fileUrl) throws Exception {
+        if (fileUrl == null || fileUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("文件URL不能为空");
+        }
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        ByteArrayOutputStream outputStream = null;
+        try {
+            URL url = new URL(fileUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(10000);
+            connection.setInstanceFollowRedirects(true);
+
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new Exception("文件URL访问失败,响应码:" + responseCode);
+            }
+
+            inputStream = connection.getInputStream();
+            outputStream = new ByteArrayOutputStream();
+            byte[] buffer = new byte[4096];
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            if (outputStream != null) outputStream.close();
+            if (inputStream != null) inputStream.close();
+            if (connection != null) connection.disconnect();
+        }
+    }
+
+    /**
+     * 网络文件 URL 转 Base64 编码字符串
+     *
+     * @param imageUrl 文件的网络URL
+     * @return Base64编码字符串(不带data:image/xxx;base64,前缀)
+     */
+    public static String convertImageUrlToBase64(String imageUrl) throws Exception {
+        byte[] bytes = downloadToBytes(imageUrl);
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    public static void main(String[] args) throws Exception {
+        FileInputStream inputStream = new FileInputStream("F:\\google下载\\a71990bf-4d22-4e9e-9d58-05bf99b6784b.mp3");
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        byte[] buffer = new byte[4096];
+        int len;
+        while ((len = inputStream.read(buffer)) != -1) {
+            outputStream.write(buffer, 0, len);
+        }
+        byte[] bytes = outputStream.toByteArray();
+        System.out.printf(Base64.getEncoder().encodeToString(bytes));
+//        String s = convertImageUrlToBase64("https://cdn.his.cdwjyyh.com/fs/20250627/ec72b8ea378340b2b804f38983f1e5e8.wav");
+//        System.out.println(s);
+    }
+}

+ 0 - 85
fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java

@@ -1,85 +0,0 @@
-package com.fs.wxcid;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Base64;
-
-/**
- * 网络图片转Base64工具类
- */
-public class ImageToBase64Util {
-
-    /**
-     * 网络图片转Base64编码字符串
-     * @param imageUrl 图片的网络URL(如https://xxx.com/xxx.png)
-     * @return Base64编码字符串(不带data:image/xxx;base64,前缀)
-     * @throws Exception 异常(网络错误、图片读取失败等)
-     */
-    public static String convertImageUrlToBase64(String imageUrl) throws Exception {
-        // 1. 校验URL参数
-        if (imageUrl == null || imageUrl.trim().isEmpty()) {
-            throw new IllegalArgumentException("图片URL不能为空");
-        }
-
-        HttpURLConnection connection = null;
-        InputStream inputStream = null;
-        ByteArrayOutputStream outputStream = null;
-
-        try {
-            // 2. 打开URL连接
-            URL url = new URL(imageUrl);
-            connection = (HttpURLConnection) url.openConnection();
-            // 设置连接参数(防超时、防重定向问题)
-            connection.setRequestMethod("GET");
-            connection.setConnectTimeout(5000); // 连接超时5秒
-            connection.setReadTimeout(5000);    // 读取超时5秒
-            connection.setInstanceFollowRedirects(true); // 允许重定向
-
-            // 3. 校验响应码(200表示成功)
-            int responseCode = connection.getResponseCode();
-            if (responseCode != HttpURLConnection.HTTP_OK) {
-                throw new Exception("图片URL访问失败,响应码:" + responseCode);
-            }
-
-            // 4. 读取图片流到字节数组
-            inputStream = connection.getInputStream();
-            outputStream = new ByteArrayOutputStream();
-            byte[] buffer = new byte[1024]; // 缓冲区,每次读1KB
-            int len;
-            while ((len = inputStream.read(buffer)) != -1) {
-                outputStream.write(buffer, 0, len);
-            }
-
-            // 5. 将字节数组编码为Base64
-            byte[] imageBytes = outputStream.toByteArray();
-            return Base64.getEncoder().encodeToString(imageBytes);
-
-        } finally {
-            // 6. 关闭所有资源(避免内存泄漏)
-            if (outputStream != null) {
-                outputStream.close();
-            }
-            if (inputStream != null) {
-                inputStream.close();
-            }
-            if (connection != null) {
-                connection.disconnect();
-            }
-        }
-    }
-
-    // 测试示例
-    public static void main(String[] args) {
-        try {
-            // 替换为你的网络图片URL
-            String imageUrl = "https://czwh.obs.cn-southwest-2.myhuaweicloud.com/fs/20260225/1771999986358.png";
-            String base64Str = convertImageUrlToBase64(imageUrl);
-            System.out.println(base64Str);
-        } catch (Exception e) {
-            e.printStackTrace();
-            System.out.println("转换失败:" + e.getMessage());
-        }
-    }
-}

+ 74 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java

@@ -0,0 +1,74 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 接口返回的 Data 结构
+ */
+@Data
+public class CdnUploadVideoResult {
+
+    @JSONField(name = "FileKey")
+    private String fileKey;
+
+    @JSONField(name = "Ver")
+    private Integer ver;
+
+    @JSONField(name = "ThumbDataSize")
+    private Integer thumbDataSize;
+
+    @JSONField(name = "ThumbURL")
+    private String thumbURL;
+
+    @JSONField(name = "FileAesKey")
+    private String fileAesKey;
+
+    @JSONField(name = "Mp4identify")
+    private String mp4identify;
+
+    @JSONField(name = "EnableQuic")
+    private Integer enableQuic;
+
+    @JSONField(name = "IsRetry")
+    private Integer isRetry;
+
+    @JSONField(name = "IsOverLoad")
+    private Integer isOverLoad;
+
+    @JSONField(name = "RecvLen")
+    private Integer recvLen;
+
+    @JSONField(name = "IsGetCDN")
+    private Integer isGetCDN;
+
+    @JSONField(name = "RetrySec")
+    private Integer retrySec;
+
+    @JSONField(name = "XClientIP")
+    private String xClientIP;
+
+    @JSONField(name = "FileURL")
+    private String fileURL;
+
+    @JSONField(name = "VideoDataMD5")
+    private String videoDataMD5;
+
+    @JSONField(name = "RetCode")
+    private Integer retCode;
+
+    @JSONField(name = "FileID")
+    private String fileID;
+
+    @JSONField(name = "ThumbHeight")
+    private Integer thumbHeight;
+
+    @JSONField(name = "ThumbWidth")
+    private Integer thumbWidth;
+
+    @JSONField(name = "Seq")
+    private Integer seq;
+
+    @JSONField(name = "VideoDataSize")
+    private Integer videoDataSize;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java

@@ -0,0 +1,27 @@
+package com.fs.wxcid.dto.message;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发送视频消息业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SendVideoMessageParam extends BaseAccountRequest {
+    /**
+     * 视频封面文件 URL
+     */
+    private String thumbUrl;
+
+    /**
+     * 视频文件 URL
+     */
+    private String videoUrl;
+
+    /**
+     * 接收方 wxid
+     */
+    private String toUser;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java

@@ -0,0 +1,20 @@
+package com.fs.wxcid.dto.message;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 请求体
+ */
+@Data
+public class SendVideoMessageRequest {
+
+    @JsonProperty("ThumbData")
+    private byte[] thumbData;
+
+    @JsonProperty("ToUserName")
+    private String toUserName;
+
+    @JsonProperty("VideoData")
+    private byte[] videoData;
+}

+ 1 - 0
fs-service/src/main/java/com/fs/wxcid/service/MessageService.java

@@ -8,6 +8,7 @@ import java.util.List;
 public interface MessageService {
     List<SendMessageResult> sendTextMessage(SendTextMessageParam param);
     List<SendImageMessageResult> sendImageMessage(SendImageMessageParam param);
+    CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param);
     RevokeMsgResult revokeMessage(RevokeMsgRequest request);
 
 }

+ 28 - 2
fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java

@@ -2,18 +2,21 @@ package com.fs.wxcid.service.impl;
 
 import com.alibaba.fastjson.TypeReference;
 import com.fs.common.exception.CustomException;
-import com.fs.wxcid.ImageToBase64Util;
+import com.fs.wxcid.FileToBase64Util;
 import com.fs.wxcid.ServiceUtils;
 import com.fs.wxcid.dto.common.ApiResponseCommon;
 import com.fs.wxcid.dto.login.RequestBaseVo;
+import com.fs.wxcid.dto.message.CdnUploadVideoResult;
 import com.fs.wxcid.dto.message.MsgItem;
 import com.fs.wxcid.dto.message.RevokeMsgRequest;
 import com.fs.wxcid.dto.message.SendImageMessageParam;
 import com.fs.wxcid.dto.message.SendTextMessageParam;
+import com.fs.wxcid.dto.message.SendVideoMessageParam;
 import com.fs.wxcid.dto.message.RevokeMsgResult;
 import com.fs.wxcid.dto.message.SendImageMessageResult;
 import com.fs.wxcid.dto.message.SendMessageResult;
 import com.fs.wxcid.dto.message.SendTextMessageRequest;
+import com.fs.wxcid.dto.message.SendVideoMessageRequest;
 import com.fs.wxcid.service.MessageService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -59,7 +62,7 @@ public class MessageServiceImpl implements MessageService {
         MsgItem msgItem = new MsgItem();
         try {
             msgItem.setMsgType(0);
-            msgItem.setImageContent(ImageToBase64Util.convertImageUrlToBase64(param.getImgUrl()));
+            msgItem.setImageContent(FileToBase64Util.convertImageUrlToBase64(param.getImgUrl()));
             msgItem.setToUserName(param.getToUser());
         }catch (Exception e){
             log.error("发送消息时,图片转换base64错误", e);
@@ -93,4 +96,27 @@ public class MessageServiceImpl implements MessageService {
         }
         return response.getData();
     }
+
+    @Override
+    public CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param) {
+        SendVideoMessageRequest request = new SendVideoMessageRequest();
+        request.setToUserName(param.getToUser());
+        try {
+            request.setThumbData(FileToBase64Util.downloadToBytes(param.getThumbUrl()));
+            request.setVideoData(FileToBase64Util.downloadToBytes(param.getVideoUrl()));
+        } catch (Exception e) {
+            log.error("发送视频消息时,文件下载失败", e);
+            throw new CustomException("视频消息发送失败");
+        }
+        ApiResponseCommon<CdnUploadVideoResult> response = serviceUtils.sendPost(
+                BASE_URL + "CdnUploadVideo",
+                RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(),
+                new TypeReference<ApiResponseCommon<CdnUploadVideoResult>>() {}
+        );
+        CdnUploadVideoResult result = response.getData();
+        if (result != null && result.getRetCode() != null && result.getRetCode() != 0) {
+            throw new CustomException("视频上传失败,RetCode=" + result.getRetCode());
+        }
+        return result;
+    }
 }

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

+ 19 - 6
fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -2,21 +2,18 @@ package com.fs.app.controller;
 
 
 import com.fs.app.service.WxTaskService;
-import com.fs.common.core.domain.R;
 import com.fs.company.service.ICompanyWxAccountService;
 import com.fs.company.service.impl.CompanyWxServiceImpl;
+import com.fs.company.vo.CdrDetailVo;
 import com.fs.wxcid.dto.message.SendImageMessageParam;
+import com.fs.wxcid.dto.message.SendVideoMessageParam;
 import com.fs.wxcid.service.MessageService;
 import io.swagger.annotations.Api;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
-import java.util.Arrays;
 import java.util.Collections;
 
 @Slf4j
@@ -89,5 +86,21 @@ public class CommonController {
         messageService.sendImageMessage(param);
     }
 
+    @GetMapping("sendVideo")
+    public void sendVideo(Long accountId, String thumbUrl, String videoUrl, String toUser) {
+        SendVideoMessageParam param = new SendVideoMessageParam();
+        param.setAccountId(accountId);
+        param.setThumbUrl(thumbUrl);
+        param.setVideoUrl(videoUrl);
+        param.setToUser(toUser);
+        messageService.sendVideoMessage(param);
+    }
+
+    @PostMapping("/saveCdrTest")
+    public String saveCdrTest(@RequestBody CdrDetailVo cdr) throws InstantiationException, IllegalAccessException {
+        log.info("cdr=" + cdr);
+        return  "success";
+    }
+
 
 }

+ 162 - 220
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -859,73 +859,106 @@ public class WxTaskService {
      * @param accountIdList 企微成员id
      */
     public void qwAddWx(List<Long> accountIdList) {
-        log.info("==========执行企微申请加个微任务开始==========");
+        log.info("==========执行申请企微加好友任务开始==========");
         try {
-            // 获取需要添加微信的企微客户列表
-            List<CompanyWxClient> clientList = getFilteredClientList(accountIdList);
-            if (clientList.isEmpty()) {
-                log.info("没有符合条件的客户需要添加微信");
-                return;
-            }
-            
-            // 获取CompanyWxClient信息
-            Map<Long, CompanyWxClient> clientMap = PubFun.listToMapByGroupObject(clientList, CompanyWxClient::getAccountId);
+            // 需要添加微信的列表
+            List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getQwAddWxList4Workflow(accountIdList);
+            log.info("申请企微加好友任务需要添加微信的数量:{}", list.size());
+            if (list.isEmpty()) return;
+            List<CompanyWxClient> addList = new ArrayList<>();
+            Map<Long, CompanyWxClient4WorkFlowVO> clientMap = PubFun.listToMapByGroupObject(list, CompanyWxClient4WorkFlowVO::getAccountId);
             // 获取实际企微用户信息
-            List<QwUser> qwUserList = qwUserMapper.selectBatchIds(clientMap.keySet()).stream()
+            List<QwUser> addAccountList = qwUserMapper.selectBatchIds(clientMap.keySet()).stream()
                     .filter(this::isValidQwUser)
                     .collect(Collectors.toList());
-            
-            log.info("需要企微添加的账号数量:{}", qwUserList.size());
-            if (qwUserList.isEmpty()) return;
-            
-            // 处理加微逻辑
-            List<CompanyWxClient> upClientList = processQwAddWx(qwUserList, clientMap);
-            
-            // 批量更新客户状态
-            if (!upClientList.isEmpty()) {
-                companyWxClientService.updateBatchById(upClientList);
-                log.info("成功更新{}个客户的加微状态", upClientList.size());
+            log.info("企微申请加好友任务需要企微的账号数量:{}", addAccountList.size());
+            addAccountList.forEach(qwUser -> {
+                CompanyWxClient4WorkFlowVO client = clientMap.get(qwUser.getId());
+                if (client != null) {
+                    CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+                    // 开始申请加微
+                    WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(crmCustomer.getMobile(), qwUser.getUid(), qwUser.getServerId());
+                    JSONObject runParam = new JSONObject();
+                    runParam.put("qwId", qwUser.getId());
+                    runParam.put("mobile", crmCustomer.getMobile());
+                    runParam.put("qwUid", qwUser.getUid());
+                    runParam.put("clientId", client.getId());
+                    CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                            runParam.toJSONString(), client.getId(), client.getRoboticId(), qwUser.getId(), qwUser.getCompanyId());
+                    if (resp != null && resp.getErrcode() == 0) {
+                        // 加微消息已发送成功
+                        client.setIsAdd(2);
+                        client.setAddTime(LocalDateTime.now());
+                        CompanyWxClient addItem = new CompanyWxClient();
+                        BeanUtils.copyProperties(client, addItem);
+                        addList.add(addItem);
+                        addLog.setStatus(1);
+                        addLog.setResult(JSON.toJSONString(resp));
+                        addLog.setIsWeCom(2);
+                        log.info("ROBOTIC-ID:{},企微申请加好友任务申请成功", client.getRoboticId());
+                    } else {
+                        log.error("ROBOTIC-ID:{},企微申请加好友任务加微失败:{}", client.getRoboticId(), runParam);
+                        addLog.setStatus(3);
+                        addLog.setResult(JSON.toJSONString(runParam));
+                    }
+                    asyncSaveCompanyVoiceRoboticCallLog(addLog);
+                } else {
+                    log.error("企微申请加好友任务当前账号暂无需要添加微信:{}-{}", qwUser.getId(), qwUser.getQwUserName());
+                }
+            });
+            if (!addList.isEmpty()) {
+                companyWxClientService.updateBatchById(addList);
+                for (CompanyWxClient client : addList) {
+                    CompanyWxClient4WorkFlowVO vo = clientMap.get(client.getAccountId());
+                    IWorkflowNode node = workflowNodeFactory.createNode(vo.getCurrentNodeKey(),
+                            NodeTypeEnum.fromValue(vo.getCurrentNodeType()),
+                            vo.getCurrentNodeName(), null);
+                    if (node instanceof AiQwAddWxTaskNode) {
+                        CompletableFuture.runAsync(() -> {
+                            AiQwAddWxTaskNode qwAddWxNode = (AiQwAddWxTaskNode) node;
+                            qwAddWxNode.doneQwAddWx(vo.getWorkflowInstanceId());
+                        }, cidExcutor);
+                    }
+                }
             }
         } catch (Exception e) {
-            log.error("企微加微信任务执行异常", e);
+            log.error("企微申请加好友任务执行异常", e);
         }
-        log.info("==========执行企微申请加个微任务结束==========");
+        log.info("==========执行企微申请加好友任务结束==========");
     }
 
     /**
      * 企微加微结果处理
      */
     public void qwAddWxResult(List<Long> accountIdList) {
-        log.info("==========执行企微申请加个微结果查询任务开始==========");
+        log.info("==========执行企微申请加微结果查询任务开始==========");
         try {
             //is_add = 2,状态为加微中且是企微类型
             List<CompanyWxClient> clients = companyWxClientService.getQwAddWxList(accountIdList, 2);
-            log.info("需要查询企微加个微结果的数量:{}", clients.size());
-            
+            log.info("企微申请加微结果查询任务需要查询的数量:{}", clients.size());
+
             if (clients.isEmpty()) return;
-            
             // 处理每个客户的加微结果
             List<CompanyWxClient> upClientList = new ArrayList<>();
             clients.parallelStream().forEach(client -> {
                 try {
                     processSingleClientResult(client, upClientList);
                 } catch (Exception e) {
-                    log.error("处理客户{}加微结果异常", client.getId(), e);
+                    log.error("企微申请加微结果查询任务处理客户{}加微结果异常", client.getId(), e);
                 }
             });
-            
+
             // 批量更新和后续处理
             if (!upClientList.isEmpty()) {
                 batchUpdateClients(upClientList);
             }
-            
+
         } catch (Exception e) {
-            log.error("企微加微结果处理异常", e);
+            log.error("企微申请加微结果查询任务处理异常", e);
         }
-        log.info("==========执行企微申请加个微结果查询任务结束==========");
+        log.info("==========执行企微申请加微结果查询任务结束==========");
     }
 
-
     /**
      * 扫描企微加微工作流延时任务
      */
@@ -957,24 +990,7 @@ public class WxTaskService {
         log.info("===========工作流延时任务扫描结束===========");
     }
 
-    /**
-     * 获取过滤后的企微客户列表
-     */
-    private List<CompanyWxClient> getFilteredClientList(List<Long> accountIdList) {
-        List<CompanyWxClient> list = companyWxClientService.getAddWxList(accountIdList, 2);
-        
-        // 排除掉没到达加微步骤的人
-        List<CompanyVoiceRoboticCallees> excludeList = companyVoiceRoboticCalleesMapper.selectExcludeList(list,2);
-        Set<String> excludeKeys = excludeList.stream()
-                .filter(e -> !Constants.QW_ADD_WX.equals(getNextTaskOptimized(e.getTaskFlow(), e.getRunTaskFlow())))
-                .map(callee -> callee.getRoboticId() + "_" + callee.getUserId())
-                .collect(Collectors.toSet());
-        
-        return list.stream()
-                .filter(client -> !excludeKeys.contains(client.getRoboticId() + "_" + client.getCustomerId()))
-                .collect(Collectors.toList());
-    }
-    
+
     /**
      * 验证企微用户有效性
      */
@@ -985,55 +1001,7 @@ public class WxTaskService {
         }
         return true;
     }
-    
-    /**
-     * 处理企微加微逻辑
-     */
-    private List<CompanyWxClient> processQwAddWx(List<QwUser> qwUserList, Map<Long, CompanyWxClient> clientMap) {
-        List<CompanyWxClient> upClientList = Collections.synchronizedList(new ArrayList<>());
-        
-        qwUserList.parallelStream().forEach(qwUser -> {
-            try {
-                processSingleQwUser(qwUser, clientMap, upClientList);
-            } catch (Exception e) {
-                log.error("处理企微用户{}异常", qwUser.getId(), e);
-            }
-        });
-        
-        return upClientList;
-    }
-    
-    /**
-     * 处理单个企微用户
-     */
-    private void processSingleQwUser(QwUser qwUser, Map<Long, CompanyWxClient> clientMap, 
-                                   List<CompanyWxClient> upClientList) {
-        CompanyWxClient client = clientMap.get(qwUser.getId());
-        if (client == null) {
-            log.error("当前账号暂无需要添加微信:{}-{}", qwUser.getId(), qwUser.getQwUserName());
-            return;
-        }
-        
-        CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
-        if (crmCustomer == null || StringUtils.isBlank(crmCustomer.getMobile())) {
-            log.info("查询客户{}手机号为空,跳过执行", crmCustomer == null ? "" : crmCustomer.getCustomerName());
-            return;
-        }
-        
-        String task = redisCache.getCacheObject(Constants.TASK_ID + client.getRoboticId());
-        log.info("ROBOTIC-ID:{},CLIENT-ID:{},当前任务执行状态:{}", 
-                client.getRoboticId(), client.getId(), task);
-        
-        if (!StringUtils.isNotEmpty(task) || !Constants.QW_ADD_WX.equals(task)) {
-            log.error("ROBOTIC-ID:{},当前任务没有执行加微任务", client.getRoboticId());
-            return;
-        }
-        
-        // 开始申请加微
-        WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(crmCustomer.getMobile(), qwUser.getUid(), qwUser.getServerId());
-        //处理申请加微结果
-        handleAddWxResult(resp, client, qwUser, crmCustomer, upClientList);
-    }
+
 
     /**
      * 企微加个微调用ipad端
@@ -1044,7 +1012,7 @@ public class WxTaskService {
      */
     private WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId) {
         if (StringUtils.isBlank(mobile) || StringUtils.isBlank(qwUid) || serverId == null) {
-            log.warn("参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
+            log.warn("企微申请加好友任务参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
             return null;
         }
         try {
@@ -1057,69 +1025,46 @@ public class WxTaskService {
             wxAddSearchDTO.setTicket(null);
 
             WxWorkResponseDTO<String> response = wxWorkService.addSearch(wxAddSearchDTO, serverId);
-            log.debug("企微加微接口调用结果: errcode={}, errmsg={}",
+            log.debug("企微申请加好友任务调用结果: errcode={}, errmsg={}",
                     response != null ? response.getErrcode() : "null",
                     response != null ? response.getErrmsg() : "null");
 
             return response;
+
+            // 测试代码
+//            WxWorkResponseDTO<String> response = new WxWorkResponseDTO<>();
+//            response.setErrcode(0);
+//            return response;
         } catch (Exception e) {
-            log.error("企微加个微请求接口异常: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId, e);
+            log.error("企微申请加好友任务请求接口异常: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId, e);
             return null;
         }
     }
 
-    /**
-     * 处理加微结果
-     */
-    private void handleAddWxResult(WxWorkResponseDTO<String> resp, CompanyWxClient client, 
-                                 QwUser qwUser, CrmCustomer crmCustomer, 
-                                 List<CompanyWxClient> upClientList) {
-        JSONObject runParam = new JSONObject();
-        runParam.put("qwId", qwUser.getId());
-        runParam.put("mobile", crmCustomer.getMobile());
-        runParam.put("qwUid", qwUser.getUid());
-        runParam.put("clientId", client.getId());
-        
-        CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
-                runParam.toJSONString(), client.getId(), client.getRoboticId(), qwUser.getId(), qwUser.getCompanyId());
-        
-        if (resp != null && resp.getErrcode() == 0) {
-            // 加微消息已发送成功
-            client.setIsAdd(2);
-            client.setAddTime(LocalDateTime.now());
-            upClientList.add(client);
-            addLog.setStatus(1);
-            addLog.setResult(JSON.toJSONString(resp));
-            addLog.setIsWeCom(2);
-            log.info("ROBOTIC-ID:{},加微成功", client.getRoboticId());
-        } else {
-            // 加微消息发送失败,补偿重试
-            handleFailedAddWxWithRetry(client, upClientList);
-        }
-        
-        asyncSaveCompanyVoiceRoboticCallLog(addLog);
-    }
-    
     /**
      * 处理单个客户加微结果
      */
     private void processSingleClientResult(CompanyWxClient client, List<CompanyWxClient> upClientList) {
         if (StringUtils.isBlank(client.getPhone())) {
-            handleFailedAddWx(client, upClientList, "无电话号码");
+            handleFailedAddWx(client, upClientList, "无电话号码",0);
             return;
         }
-        
+
         // 查询外部联系人表是否有数据
         QwExternalContact qwExternalContact = qwExternalContactMapper.queryQwUserIdIsAddContact(
                 client.getAccountId(), client.getPhone(), 2);
-        
+
         if (qwExternalContact != null && qwExternalContact.getId() > 0) {
             handleSuccessfulAddWx(client, upClientList);
         } else {
             handleFailedAddWxWithRetry(client, upClientList);
         }
+
+        //测试代码
+//        handleSuccessfulAddWx(client, upClientList);
+//        handleFailedAddWx(client, upClientList, "无电话号码",0);
     }
-    
+
     /**
      * 处理加微成功的情况
      */
@@ -1132,24 +1077,24 @@ public class WxTaskService {
                 .eq(CompanyVoiceRoboticCallLogAddwx::getIsWeCom, 2)
                 .set(CompanyVoiceRoboticCallLogAddwx::getStatus, 2)
                 .update();
-        
+
         client.setIsAdd(1);
         client.setAddTime(LocalDateTime.now());
         upClientList.add(client);
         redisCache.deleteObject("qwAddWx_" + client.getId());
-        log.info("ROBOTIC-ID:{},加微成功:{}", client.getRoboticId(), client.getId());
+        log.info("ROBOTIC-ID:{},企微申请加微结果查询任务加微成功:{}", client.getRoboticId(), client.getId());
     }
-    
+
     /**
      * 处理加微失败的情况
      */
-    private void handleFailedAddWx(CompanyWxClient client, List<CompanyWxClient> upClientList, String reason) {
-        log.error("ROBOTIC-ID:{},{}加微失败:{}", client.getRoboticId(), reason, client.getId());
+    private void handleFailedAddWx(CompanyWxClient client, List<CompanyWxClient> upClientList, String reason,Integer isAdd) {
+        String taskName = isAdd == 1 ? "企微申请加好友任务" : "加微结果查询任务";
+        log.error("ROBOTIC-ID:{},{}:{},clientId={}", client.getRoboticId(),taskName, reason, client.getId());
         client.setIsAdd(3);
         client.setUpdateTime(new Date());
         upClientList.add(client);
-        
-        // 更新记录
+        // 更新日志记录
         companyVoiceRoboticCallLogAddwxService.lambdaUpdate()
                 .eq(CompanyVoiceRoboticCallLogAddwx::getRoboticId, client.getRoboticId())
                 .eq(CompanyVoiceRoboticCallLogAddwx::getWxClientId, client.getId())
@@ -1159,106 +1104,103 @@ public class WxTaskService {
                 .set(CompanyVoiceRoboticCallLogAddwx::getResult, reason)
                 .update();
     }
-    
+
     /**
      * 处理加微失败并重试计数
      */
     private void handleFailedAddWxWithRetry(CompanyWxClient client, List<CompanyWxClient> upClientList) {
-        log.error("ROBOTIC-ID:{},加微失败:{}", client.getRoboticId(), client.getId());
+        String taskName = 0 == 1 ? "企微申请加好友任务" : "加微结果查询任务";
+        log.error("ROBOTIC-ID:{},{}失败:{}", client.getRoboticId(),taskName, client.getId());
         String failCountStr = redisCache.getCacheObject("qwAddWx_" + client.getId());
         int failCount = 1;
-        
+
         if (StringUtils.isNotBlank(failCountStr)) {
-            failCount += Integer.parseInt(failCountStr);
-            if (failCount >= 60 * 24) { // 超过一天
-                handleFailedAddWx(client, upClientList, "超过最大重试次数");
+            if (Integer.parseInt(failCountStr) >= 60 * 24) { // 超过一天
+                handleFailedAddWx(client, upClientList, "超过最大重试次数", 0);
+                redisCache.deleteObject("qwAddWx_" + client.getId());
             } else {
-                redisCache.setCacheObject("qwAddWx_" + client.getId(), String.valueOf(failCount));
+                failCount += Integer.parseInt(failCountStr);
+                redisCache.setCacheObject("qwAddWx_" + client.getId(), String.valueOf(failCount-1));
             }
         } else {
             redisCache.setCacheObject("qwAddWx_" + client.getId(), String.valueOf(failCount), 25, TimeUnit.HOURS);
         }
     }
-    
+
     /**
      * 批量更新客户和相关数据
      */
     private void batchUpdateClients(List<CompanyWxClient> upClientList) {
         companyWxClientService.updateBatchById(upClientList);
-
-        // 从 upClientList 中筛选出 isAdd=1即加微成功的数据
+        // 从 upClientList 中筛选出 isAdd=1和3加微失败的数据
         List<CompanyWxClient> successClients = upClientList.stream()
-                .filter(client -> client.getIsAdd() != null && client.getIsAdd() == 1)
+                .filter(client -> client.getIsAdd() != null && (client.getIsAdd() == 1 || client.getIsAdd() == 3))
                 .collect(Collectors.toList());
-
-        // 根据加微成功的用户,判定是否加入延时执行下一步任务
-        Set<Long> roboticIdSet = successClients.stream()
-                .map(CompanyWxClient::getRoboticId)
-                .collect(Collectors.toSet());
-        Set<Long> userIdSet = successClients.stream()
-                .map(CompanyWxClient::getCustomerId)
-                .collect(Collectors.toSet());
-        
-        // 获取任务和callees数据
-        List<CompanyVoiceRobotic> robotList = companyVoiceRoboticMapper.selectBatchIds(roboticIdSet);
-        Map<Long, CompanyVoiceRobotic> roboticsMap = robotList.stream()
-                .collect(Collectors.toMap(CompanyVoiceRobotic::getId, Function.identity(), (a, b) -> a));
-        
-        List<CompanyVoiceRoboticCallees> calleesList = companyVoiceRoboticCalleesMapper
-                .selectCalleesListByRoboticIdsAndUserIds(userIdSet, roboticIdSet);
-        Map<String, CompanyVoiceRoboticCallees> calleesMap = calleesList.stream()
-                .collect(Collectors.toMap(e -> e.getUserId() + "-" + e.getRoboticId(), Function.identity(), (a, b) -> a));
-        
-        // 设置延时任务
-        setupDelayTasks(successClients, roboticsMap, calleesMap);
-        
-        // 更新任务流程状态
-        updateTaskFlows(calleesList, roboticIdSet);
+        if(!successClients.isEmpty()){
+            successClients.forEach(client -> {
+                triggerWorkflowOnAddWxSuccess(client.getId());
+            });
+        }
     }
-    
+
+
     /**
-     * 设置延时任务
+     * 加微结果触发工作流继续执行
+     * @param wxClientId 加微客户ID
      */
-    private void setupDelayTasks(List<CompanyWxClient> upClientList, 
-                               Map<Long, CompanyVoiceRobotic> roboticsMap,
-                               Map<String, CompanyVoiceRoboticCallees> calleesMap) {
-        upClientList.forEach(client -> {
-            CompanyVoiceRobotic robotic = roboticsMap.get(client.getRoboticId());
-            if (robotic == null) {
-                log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有找到任务", client.getRoboticId(), client.getId());
+    private void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
+        try {
+            // 查找等待中的加微工作流实例
+            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+                    wxClientId,
+                    ExecutionStatusEnum.WAITING.getValue(),
+                    NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+            if (waitingExec == null) {
+                log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
                 return;
             }
-            
-            CompanyVoiceRoboticCallees callee = calleesMap.get(client.getCustomerId() + "-" + client.getRoboticId());
-            if (callee == null) {
-                log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有找到任务", client.getRoboticId(), client.getId());
+            //查询工作流加微执行日志是否未更新状态
+            CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+            queryP.setWorkflowInstanceId(waitingExec.getWorkflowInstanceId());
+            queryP.setNodeType(NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+            queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
+            List<CompanyAiWorkflowExecLog> companyAiWorkflowExecLogs = companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(queryP);
+            companyAiWorkflowExecLogs.forEach(log -> {
+                        log.setStatus(ExecutionStatusEnum.SUCCESS.getValue());
+                        companyAiWorkflowExecLogMapper.updateById(log);
+                    }
+            );
+
+            String workflowInstanceId = waitingExec.getWorkflowInstanceId();
+            String currentNodeKey = waitingExec.getCurrentNodeKey();
+
+            log.info("加微成功回调,尝试触发工作流继续执行 - workflowInstanceId: {}, nodeKey: {}, wxClientId: {}",
+                    workflowInstanceId, currentNodeKey, wxClientId);
+
+            // 互斥检查:如果已经被执行过(超时路径或其他回调),则不再执行
+            if (!AiQwAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+                log.info("企微申请加微结果查询任务工作流已被其他路径执行,跳过 - workflowInstanceId: {}, wxClientId: {}",
+                        workflowInstanceId, wxClientId);
                 return;
             }
-            
-            Integer addWxTime = robotic.getAddWxTime();
-            if (addWxTime != null) {
-                long endTime = System.currentTimeMillis() + addWxTime * 60 * 1000;
-                String key = Constants.CID_NEXT_TASK_ID + callee.getRoboticId() + ":" + callee.getId();
-                redisCache.setCacheObject(key, String.valueOf(endTime));
-            } else {
-                log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有设置加微后置等待时间", client.getRoboticId(), client.getId());
-            }
-        });
-    }
-    
-    /**
-     * 更新任务流程状态
-     */
-    private void updateTaskFlows(List<CompanyVoiceRoboticCallees> calleesList, Set<Long> roboticIdSet) {
-        calleesList.forEach(callee -> 
-            callee.setRunTaskFlow(
-                StringUtils.isBlank(callee.getRunTaskFlow()) ?
-                    Constants.QW_ADD_WX : callee.getRunTaskFlow() + "," + Constants.QW_ADD_WX
-            )
-        );
-        
-        companyVoiceRoboticCalleesServiceImpl.updateBatchById(calleesList);
-        companyVoiceRoboticServiceImpl.finishAddWxByCallees(roboticIdSet);
+
+            // 清除超时检测Key(回调成功了,不需要超时检测了)
+            AiQwAddWxTaskNode.clearTimeoutKey(workflowInstanceId, wxClientId);
+
+            // 触发工作流继续执行
+            Map<String, Object> inputData = new HashMap<>();
+            inputData.put("addWxSuccess", true);
+            inputData.put("wxClientId", wxClientId);
+            inputData.put("triggerType", "callback"); // 回调触发
+
+            companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
+
+            log.info("企微申请加微结果查询任务加微成功回调触发工作流继续执行完成 - workflowInstanceId: {}, wxClientId: {}",
+                    workflowInstanceId, wxClientId);
+
+        } catch (Exception ex) {
+            log.error("企微申请加微结果查询任务加微成功回调触发工作流异常 - wxClientId: {}", wxClientId, ex);
+        }
     }
 
 }