浏览代码

新增sop电话外呼接口

cgp 3 天之前
父节点
当前提交
f56a5e6c6a
共有 37 个文件被更改,包括 2058 次插入0 次删除
  1. 89 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiOutboundCallController.java
  2. 83 0
      fs-service/src/main/java/com/fs/company/domain/CompanyAiRobotic.java
  3. 72 0
      fs-service/src/main/java/com/fs/company/domain/QwSopTtDialog.java
  4. 48 0
      fs-service/src/main/java/com/fs/company/domain/QwSopTtTask.java
  5. 143 0
      fs-service/src/main/java/com/fs/company/mapper/QwSopTtDialogMapper.java
  6. 115 0
      fs-service/src/main/java/com/fs/company/mapper/QwSopTtTaskMapper.java
  7. 20 0
      fs-service/src/main/java/com/fs/company/service/IQwSopTtTaskService.java
  8. 203 0
      fs-service/src/main/java/com/fs/company/service/impl/QwSopTtTaskServiceImpl.java
  9. 11 0
      fs-service/src/main/java/com/fs/cpaicall/CpAiCallController.java
  10. 11 0
      fs-service/src/main/java/com/fs/cpaicall/config/CpAiCallConfig.java
  11. 13 0
      fs-service/src/main/java/com/fs/cpaicall/domain/CpBaseDomain.java
  12. 15 0
      fs-service/src/main/java/com/fs/cpaicall/domain/CpCIDGroupInfo.java
  13. 13 0
      fs-service/src/main/java/com/fs/cpaicall/domain/CpCIDGroupInfoDataItem.java
  14. 18 0
      fs-service/src/main/java/com/fs/cpaicall/domain/CpTaskInfo.java
  15. 11 0
      fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpAuthentication.java
  16. 37 0
      fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpContent.java
  17. 34 0
      fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpNotify.java
  18. 11 0
      fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpPushIIntentionResult.java
  19. 17 0
      fs-service/src/main/java/com/fs/cpaicall/domain/param/CpCalleeDomain.java
  20. 118 0
      fs-service/src/main/java/com/fs/cpaicall/domain/param/CpCalltaskcreateaiCustomizeDomain.java
  21. 17 0
      fs-service/src/main/java/com/fs/cpaicall/domain/param/CpEditDialogDomain.java
  22. 13 0
      fs-service/src/main/java/com/fs/cpaicall/domain/param/CpgetDialogMapDomain.java
  23. 20 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpCIDGroupListResult.java
  24. 17 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpCalltaskcreateaiCustomizeResult.java
  25. 16 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpEditDialogResult.java
  26. 65 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpGetCdrListResult.java
  27. 17 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpGetairobotResult.java
  28. 28 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpQueryCallTaskInfoResult.java
  29. 31 0
      fs-service/src/main/java/com/fs/cpaicall/domain/result/CpgetDialogMapResult.java
  30. 180 0
      fs-service/src/main/java/com/fs/cpaicall/service/CpAiCallService.java
  31. 98 0
      fs-service/src/main/java/com/fs/cpaicall/utils/CpAiCallUtils.java
  32. 66 0
      fs-service/src/main/java/com/fs/cpaicall/utils/CpMd5Utils.java
  33. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  34. 5 0
      fs-service/src/main/java/com/fs/sop/vo/QwSopLogsListCVO.java
  35. 250 0
      fs-service/src/main/resources/mapper/company/QwSopTtDialogMapper.xml
  36. 136 0
      fs-service/src/main/resources/mapper/company/QwSopTtTaskMapper.xml
  37. 11 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml

+ 89 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiOutboundCallController.java

@@ -0,0 +1,89 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.CompanyAiRobotic;
+import com.fs.company.service.IQwSopTtTaskService;
+import com.fs.cpaicall.domain.CpTaskInfo;
+import com.fs.cpaicall.domain.result.CpEditDialogResult;
+import com.fs.cpaicall.domain.result.CpGetCdrListResult;
+import com.fs.cpaicall.domain.result.CpGetairobotResult;
+import com.fs.cpaicall.domain.result.CpQueryCallTaskInfoResult;
+import com.fs.cpaicall.service.CpAiCallService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * AI外呼任务Controller
+ *
+ * @author fs
+ * @date 2026-02-09
+ */
+@RestController
+@RequestMapping("/company/companyAiOutboundCall")
+public class CompanyAiOutboundCallController extends BaseController {
+    @Autowired
+    private CpAiCallService cpAiCallService;
+
+    @Autowired
+    private IQwSopTtTaskService qwSopTtTaskService;
+
+    private final  Long companyId=300L;
+
+    /**
+     * 获取所属公司的机器人以及话术列表
+     */
+    @GetMapping("/getTypes")
+    public R getTypes(){
+        List<CpGetairobotResult> getairobotlist = cpAiCallService.getairobotlist(companyId);
+        List<CpEditDialogResult> editDialogResults = cpAiCallService.queryDialog(companyId);
+        return R.ok().put("robot", getairobotlist).put("dialog", editDialogResults);
+    }
+    //创建机器人外呼任务
+    @Log(title = "创建外呼任务", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    //@Transactional
+    public AjaxResult add(@RequestBody CompanyAiRobotic companyAiRobotic)
+    {
+        qwSopTtTaskService.createAiOutboundCallTask(companyAiRobotic);
+        return AjaxResult.success();
+    }
+    /**
+     * 启动任务
+     */
+    @GetMapping("/startAiOutboundTask")
+    public R startRobotic(String taskId){
+        CpTaskInfo cpTaskInfo = qwSopTtTaskService.startAiOutboundCallTask(taskId, companyId);
+        return R.ok().put("data", cpTaskInfo);
+    }
+    /**
+     * 停止任务
+     */
+    @GetMapping("/stopAiOutboundTask")
+    public R stopRobotic(String taskId){
+        CpTaskInfo cpTaskInfo = qwSopTtTaskService.stopAiOutboundCallTask(taskId, companyId);
+        return R.ok().put("cpTaskInfo", cpTaskInfo);
+    }
+    /**
+     * 查询任务执行详情
+     */
+    @GetMapping("/queryCallTaskInfo")
+    public R  queryCallTaskInfo(String taskId){
+        CpQueryCallTaskInfoResult cpQueryCallTaskInfoResult = cpAiCallService.queryCallTaskInfo(CpTaskInfo.builder().taskID(taskId).build(), companyId);
+        return R.ok().put("data", cpQueryCallTaskInfoResult);
+    }
+
+    /**
+     * 请求获取话单
+     */
+    @GetMapping("/getCdrList")
+    public R  getCdrList(@RequestBody CpTaskInfo param){
+        CpGetCdrListResult cdrList = cpAiCallService.getCdrList(param, companyId);
+        return R.ok().put("data", cdrList);
+    }
+}

+ 83 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyAiRobotic.java

@@ -0,0 +1,83 @@
+package com.fs.company.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+@Data
+public class CompanyAiRobotic {
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long id;
+
+    /** 任务名称 */
+    @Excel(name = "任务名称")
+    private String name;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 三方任务名称 */
+    @Excel(name = "三方任务名称")
+    private String taskName;
+
+    /** 三方任务ID */
+    @Excel(name = "三方任务ID")
+    private Long taskId;
+
+    /** 机器人ID */
+    @Excel(name = "机器人ID")
+    private Long robot;
+
+    /** 话术ID */
+    @Excel(name = "话术ID")
+    private Long dialogId;
+
+    /** 模式 */
+    @Excel(name = "模式")
+    private Long mode;
+
+    /** 呼叫倍率;1-5 */
+    @Excel(name = "呼叫倍率;1-5")
+    private Long multiplier;
+
+    /** 是否开启自动重呼;0否 1是 */
+    @Excel(name = "是否开启自动重呼;0否 1是")
+    private Long autoRecall;
+
+    /** 重呼次数;最大5 0表示不自动重呼 */
+    @Excel(name = "重呼次数;最大5 0表示不自动重呼")
+    private Long recallTimes;
+
+    /** 主叫分组ID */
+    @Excel(name = "主叫分组ID")
+    private Long cidGroupId;
+
+    /** 时间段星期;0表示周天 1-6表示周一到周六 */
+    @Excel(name = "时间段星期;0表示周天 1-6表示周一到周六")
+    private String weekDay1;
+
+    /** 开始时间 */
+    @Excel(name = "开始时间")
+    private String startTime1;
+
+    /** 结束时间 */
+    @Excel(name = "结束时间")
+    private String endTime1;
+
+    /** 时间段星期;0表示周天 1-6表示周一到周六 */
+    @Excel(name = "时间段星期;0表示周天 1-6表示周一到周六")
+    private String weekDay2;
+
+    /** 开始时间 */
+    @Excel(name = "开始时间")
+    private String startTime2;
+
+    /** 结束时间 */
+    @Excel(name = "结束时间")
+    private String endTime2;
+
+    /** 营期id */
+    private String sopId;
+}

+ 72 - 0
fs-service/src/main/java/com/fs/company/domain/QwSopTtDialog.java

@@ -0,0 +1,72 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 天天外呼详情对象 qw_sop_tt_call_detail
+ * 
+ * @author yourName
+ * @date 2026-02-24
+ */
+@Data
+public class QwSopTtDialog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 天天外呼内部任务id */
+    private Long sopTtTaskId;
+
+    /** 电话号码 */
+    private String phone;
+
+    /** 营期 */
+    private String userLogsId;
+
+    /** 小程序账号id */
+    private Long fsUserId;
+
+    /** 外部联系人id */
+    private Long externalId;
+
+    /** 企微销售主键 */
+    private Long qwUserKey;
+
+    /** 话术id */
+    private Long dialogId;
+
+    /** 话术名称 */
+    private String dialogName;
+
+    /** 第三方返回的接通状态 */
+    private Integer callStatus;
+
+    /** 0未接听,1已接听 */
+    private Integer isAnswered;
+
+    /** 通话时长(秒) */
+    private Integer callDuration;
+
+    /** 挂断原因 */
+    private Integer hangupReason;
+
+    /** 实际外呼时间 */
+    private String callTime;
+
+    /** 0未发送,1已发送(指已调用第三方接口) */
+    private Integer sendStatus;
+
+    /** 发送时间 */
+    private String sendTime;
+
+    /**
+     * 用于批量更新的ID列表
+     */
+    @TableField(exist = false)
+    private List<Long> ids;
+}

+ 48 - 0
fs-service/src/main/java/com/fs/company/domain/QwSopTtTask.java

@@ -0,0 +1,48 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 天天外呼任务对象 qw_sop_tt_task
+ * 
+ * @author yourName
+ * @date 2026-02-24
+ */
+@Data
+public class QwSopTtTask extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 第三方返回的任务ID */
+    private Long taskId;
+
+    /** 任务名称 */
+    private String taskName;
+
+    /** 营期id */
+    private String sopId;
+
+    /** 开始时间 */
+    private String runTime;
+
+    /** 外呼机器人组ID */
+    private Long robotGroupId;
+
+    /** 总号码数 */
+    private Integer totalCount;
+
+    /** 任务状态:0待启动,1进行中,2已停止,3已完成 */
+    private Integer status;
+
+    /**
+     * 用于批量更新的ID列表
+     */
+    @TableField(exist = false)
+    private List<Long> ids;
+}

+ 143 - 0
fs-service/src/main/java/com/fs/company/mapper/QwSopTtDialogMapper.java

@@ -0,0 +1,143 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.QwSopTtDialog;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 天天外呼详情Mapper接口
+ *
+ * @author yourName
+ * @date 2026-02-24
+ */
+public interface QwSopTtDialogMapper
+{
+    /**
+     * 查询天天外呼详情
+     *
+     * @param id 天天外呼详情主键
+     * @return 天天外呼详情
+     */
+    @DataSource(DataSourceType.SOP)
+    public QwSopTtDialog selectQwSopTtDialogById(Long id);
+
+    /**
+     * 查询天天外呼详情列表
+     *
+     * @param qwSopTtDialog 天天外呼详情
+     * @return 天天外呼详情集合
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTtDialog> selectQwSopTtDialogList(QwSopTtDialog qwSopTtDialog);
+
+    /**
+     * 根据内部任务ID查询天天外呼详情列表
+     *
+     * @param sopTtTaskId 天天外呼内部任务id
+     * @return 天天外呼详情集合
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTtDialog> selectQwSopTtDialogByTaskId(@Param("sopTtTaskId") Long sopTtTaskId);
+
+    /**
+     * 查询待发送的详情列表(send_status=0)
+     *
+     * @param limit 限制数量
+     * @return 天天外呼详情集合
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTtDialog> selectPendingSendDetails(@Param("limit") Integer limit);
+
+    /**
+     * 新增天天外呼详情
+     *
+     * @param qwSopTtDialog 天天外呼详情
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int insertQwSopTtDialog(QwSopTtDialog qwSopTtDialog);
+
+    /**
+     * 批量新增天天外呼详情
+     *
+     * @param list 天天外呼详情列表
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int batchInsertQwSopTtDialog(@Param("list") List<QwSopTtDialog> list);
+
+    /**
+     * 修改天天外呼详情
+     *
+     * @param qwSopTtDialog 天天外呼详情
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int updateQwSopTtDialog(QwSopTtDialog qwSopTtDialog);
+
+    /**
+     * 根据内部任务ID和电话号码更新天天外呼详情
+     *
+     * @param qwSopTtDialog 天天外呼详情
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int updateQwSopTtDialogByTaskIdAndPhone(QwSopTtDialog qwSopTtDialog);
+
+    /**
+     * 批量更新天天外呼详情的通话结果
+     *
+     * @param list 天天外呼详情列表
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int batchUpdateCallResult(@Param("list") List<QwSopTtDialog> list);
+
+    /**
+     * 删除天天外呼详情
+     *
+     * @param id 天天外呼详情主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtDialogById(Long id);
+
+    /**
+     * 批量删除天天外呼详情
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtDialogByIds(@Param("ids") Long[] ids);
+
+    /**
+     * 根据内部任务ID删除天天外呼详情
+     *
+     * @param sopTtTaskId 天天外呼内部任务id
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtDialogByTaskId(@Param("sopTtTaskId") Long sopTtTaskId);
+
+    /**
+     * 统计任务下未回执的详情数量
+     *
+     * @param sopTtTaskId 天天外呼内部任务id
+     * @return 数量
+     */
+    @DataSource(DataSourceType.SOP)
+    public int countUnfinishedByTaskId(@Param("sopTtTaskId") Long sopTtTaskId);
+
+    /**
+     * 批量更新天天外呼详情的指定字段
+     *
+     * @param qwSopTtDialog 包含要更新字段和ID列表的对象
+     * @return 结果 (影响的行数)
+     */
+    @DataSource(DataSourceType.SOP)
+    public int batchUpdateQwSopTtDialogByIds(QwSopTtDialog qwSopTtDialog);
+}

+ 115 - 0
fs-service/src/main/java/com/fs/company/mapper/QwSopTtTaskMapper.java

@@ -0,0 +1,115 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.QwSopTtTask;
+
+import java.util.List;
+
+/**
+ * 天天外呼任务Mapper接口
+ * 
+ * @author yourName
+ * @date 2026-02-24
+ */
+public interface QwSopTtTaskMapper 
+{
+    /**
+     * 查询天天外呼任务
+     * 
+     * @param id 天天外呼任务主键
+     * @return 天天外呼任务
+     */
+    @DataSource(DataSourceType.SOP)
+    public QwSopTtTask selectQwSopTtTaskById(Long id);
+
+    /**
+     * 根据第三方任务ID查询天天外呼任务
+     * 
+     * @param taskId 第三方任务ID
+     * @return 天天外呼任务
+     */
+    @DataSource(DataSourceType.SOP)
+    public QwSopTtTask selectQwSopTtTaskByTaskId(String taskId);
+
+    /**
+     * 查询天天外呼任务列表
+     * 
+     * @param qwSopTtTask 天天外呼任务
+     * @return 天天外呼任务集合
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTtTask> selectQwSopTtTaskList(QwSopTtTask qwSopTtTask);
+
+    /**
+     * 查询待启动的任务列表
+     * 
+     * @param status 任务状态
+     * @return 天天外呼任务集合
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTtTask> selectQwSopTtTaskByStatus(Integer status);
+
+    /**
+     * 新增天天外呼任务
+     * 
+     * @param qwSopTtTask 天天外呼任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int insertQwSopTtTask(QwSopTtTask qwSopTtTask);
+
+    /**
+     * 修改天天外呼任务
+     * 
+     * @param qwSopTtTask 天天外呼任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int updateQwSopTtTask(QwSopTtTask qwSopTtTask);
+
+    /**
+     * 根据第三方任务ID修改天天外呼任务
+     * 
+     * @param qwSopTtTask 天天外呼任务
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int updateQwSopTtTaskByTaskId(QwSopTtTask qwSopTtTask);
+
+    /**
+     * 删除天天外呼任务
+     * 
+     * @param id 天天外呼任务主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtTaskById(Long id);
+
+    /**
+     * 批量删除天天外呼任务
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtTaskByIds(Long[] ids);
+
+    /**
+     * 根据第三方任务ID删除天天外呼任务
+     * 
+     * @param taskId 第三方任务ID
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    public int deleteQwSopTtTaskByTaskId(String taskId);
+
+    /**
+     * 批量更新天天外呼任务的指定字段
+     * 
+     * @param qwSopTtTask 包含要更新字段和ID列表的对象
+     * @return 结果 (影响的行数)
+     */
+    @DataSource(DataSourceType.SOP)
+    public int batchUpdateQwSopTtTaskByIds(QwSopTtTask qwSopTtTask);
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/service/IQwSopTtTaskService.java

@@ -0,0 +1,20 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CompanyAiRobotic;
+import com.fs.company.domain.QwSopTtTask;
+import com.fs.cpaicall.domain.CpTaskInfo;
+import com.fs.cpaicall.domain.result.CpGetCdrListResult;
+
+public interface IQwSopTtTaskService {
+    //创建Ai外呼任务
+    void createAiOutboundCallTask(CompanyAiRobotic companyAiRobotic);
+
+    //启动Ai外呼任务
+    CpTaskInfo startAiOutboundCallTask(String taskId, Long companyId);
+
+    //停止Ai外呼任务
+    CpTaskInfo stopAiOutboundCallTask(String taskId, Long companyId);
+
+    //请求获取话单
+    CpGetCdrListResult getCdrList(CpTaskInfo param, Long companyId);
+}

+ 203 - 0
fs-service/src/main/java/com/fs/company/service/impl/QwSopTtTaskServiceImpl.java

@@ -0,0 +1,203 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.exception.CustomException;
+import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyAiRobotic;
+import com.fs.company.domain.QwSopTtDialog;
+import com.fs.company.domain.QwSopTtTask;
+import com.fs.company.mapper.QwSopTtDialogMapper;
+import com.fs.company.mapper.QwSopTtTaskMapper;
+import com.fs.company.service.IQwSopTtTaskService;
+import com.fs.cpaicall.domain.CpTaskInfo;
+import com.fs.cpaicall.domain.param.CpCalleeDomain;
+import com.fs.cpaicall.domain.param.CpCalltaskcreateaiCustomizeDomain;
+import com.fs.cpaicall.domain.result.CpCalltaskcreateaiCustomizeResult;
+import com.fs.cpaicall.domain.result.CpGetCdrListResult;
+import com.fs.cpaicall.service.CpAiCallService;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.params.QwSopLogsParam;
+import com.fs.sop.vo.QwSopLogsListCVO;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class QwSopTtTaskServiceImpl implements IQwSopTtTaskService {
+    @Autowired
+    private QwSopTtDialogMapper ttDialogMapper;
+
+    @Autowired
+    private QwSopTtTaskMapper ttTaskMapper;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+    @Autowired
+    private CpAiCallService aiCallService;
+
+    @Override
+    public void createAiOutboundCallTask(CompanyAiRobotic companyAiRobotic) {
+        // 构建三方接口请求数据
+        CpCalltaskcreateaiCustomizeDomain param = new CpCalltaskcreateaiCustomizeDomain();
+        param.setRobot(companyAiRobotic.getRobot());//机器人组ID
+        param.setDialogID(companyAiRobotic.getDialogId());//话术ID
+        param.setMode(companyAiRobotic.getMode());//模式 7 是 呼叫机器人后挂断,8 是 呼叫语音机器人之后转接坐席
+        // 外呼电话列表 根据营期id查询用户列表
+        QwSopLogsParam sopLogsParam=new QwSopLogsParam();
+        sopLogsParam.setSopId(companyAiRobotic.getSopId());
+        List<QwSopLogsListCVO> qwSopLogsListCVOS = qwSopLogsMapper.selectQwSopLogsListByQwSopId(sopLogsParam);
+        List<Long> collectExternalIds = qwSopLogsListCVOS.stream().map(QwSopLogsListCVO::getExternalId).collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(collectExternalIds)){
+            log.error("sopId:{}不存在外部联系人信息",companyAiRobotic.getSopId());
+            return;
+        }
+        // 根据外部联系人ID查询外部联系人信息
+        List<QwExternalContact> qwExternalContList = qwExternalContactMapper.selectQwExternalContactByIds(collectExternalIds);
+        // 获取有效的电话号码和记录ID
+        QwSopTtTaskServiceImpl.PhoneAndIdResult phoneAndIdResult = getCalleeDomainListAndIds(qwExternalContList);
+        List<CpCalleeDomain> mobileList = phoneAndIdResult.getMobileList();
+        if (CollectionUtils.isEmpty(mobileList)){
+            throw new BaseException("AI外呼拨打电话不能为空");
+        }
+        param.setCallees(mobileList);//被叫电话列表
+        // 请求第三方外呼接口
+        CpCalltaskcreateaiCustomizeResult result = aiCallService.calltaskcreateaiCustomize(param, companyAiRobotic.getCompanyId());
+        log.info("创建Ai外呼任务结果任务id: {},任务名称:{}", result.getTaskID(), result.getTaskName());
+        //增加外呼任务的本地处理记录
+        //qw_sop_tt_task
+        QwSopTtTask ttTask=new QwSopTtTask();
+        ttTask.setTaskId(result.getTaskID());
+        ttTask.setTaskName(result.getTaskName());
+        ttTask.setRobotGroupId(companyAiRobotic.getRobot());
+        ttTask.setTotalCount(mobileList.size());
+        ttTask.setStatus(0);//待启动的状态
+        ttTask.setSopId(companyAiRobotic.getSopId());
+        ttTask.setRunTime(companyAiRobotic.getStartTime1());
+        ttTaskMapper.insertQwSopTtTask(ttTask);
+        //qw_sop_tt_call_detail
+        List<QwSopTtDialog> addList=new ArrayList<>();
+        for (CpCalleeDomain mobile : mobileList) {
+            QwSopTtDialog qwSopTtDialog =new QwSopTtDialog();
+            qwSopTtDialog.setSopTtTaskId(ttTask.getId());
+            qwSopTtDialog.setPhone(mobile.getNumber());
+            qwSopTtDialog.setFsUserId(Long.valueOf(mobile.getUserData()));
+        }
+        ttDialogMapper.batchInsertQwSopTtDialog(addList);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CpTaskInfo startAiOutboundCallTask(String taskId, Long companyId) {
+        //调用第三方接口
+        CpTaskInfo cpTaskInfo = aiCallService.startCallTask(CpTaskInfo.builder().taskID(taskId).build(), companyId);
+        //维护任务状态
+        QwSopTtTask ttTask=new QwSopTtTask();
+        ttTask.setTaskId(Long.valueOf(taskId));
+        ttTask.setStatus(0);//待启动的状态
+        List<QwSopTtTask> qwSopTtTasks = ttTaskMapper.selectQwSopTtTaskList(ttTask);
+        if (CollectionUtils.isEmpty(qwSopTtTasks)){
+            log.error("taskId:{}不存在AI外呼任务",taskId);
+            throw new CustomException("不存在AI外呼任务");
+        }
+        QwSopTtTask qwSopTtTask = qwSopTtTasks.get(0);
+        QwSopTtTask conditionSopTtTask=new QwSopTtTask();
+        conditionSopTtTask.setStatus(1);//进行中的状态
+        conditionSopTtTask.setId(qwSopTtTask.getId());
+        conditionSopTtTask.setUpdateTime(DateUtils.getNowDate());
+        ttTaskMapper.updateQwSopTtTask(conditionSopTtTask);
+        return cpTaskInfo;
+    }
+
+    @Override
+    public CpTaskInfo stopAiOutboundCallTask(String taskId, Long companyId) {
+        return aiCallService.stopCallTask(CpTaskInfo.builder().taskID(taskId).build(),companyId);
+    }
+
+    @Override
+    public CpGetCdrListResult getCdrList(CpTaskInfo param, Long companyId) {
+        return aiCallService.getCdrList(param, companyId);
+    }
+
+    /**
+     * 根据用户ID列表查询用户手机号,并返回电话号码列表和对应的数据库记录ID列表
+     */
+    private QwSopTtTaskServiceImpl.PhoneAndIdResult getCalleeDomainListAndIds(List<QwExternalContact> qwExternalContList) {
+        // 1. 提取用户ID和原始记录映射
+        List<Long> userIds = qwExternalContList.stream().map(QwExternalContact::getFsUserId).collect(Collectors.toList());
+        Map<Long, String> userPhoneMap = fsUserMapper.selectFsUserPhoneMap(userIds);
+        if (userPhoneMap.isEmpty()) {
+            return new QwSopTtTaskServiceImpl.PhoneAndIdResult(Collections.emptyList());
+        }
+
+        // 2. 遍历原始记录,处理电话号码,同时收集有效的ID
+        List<CpCalleeDomain> processedPhoneList = new ArrayList<>();
+        for (QwExternalContact externalContact : qwExternalContList) { // 遍历原始的完整记录列表
+            Long userId = externalContact.getFsUserId();
+            String phone = userPhoneMap.get(userId);
+
+            if (phone == null || phone.trim().isEmpty()) {
+                log.warn("用户手机号为空, userId: {}", userId);
+                continue;
+            }
+
+            String phoneNum = phone.trim();
+            String processedPhone = null;
+
+            // 根据长度处理
+            if (phoneNum.length() > 11) {
+                // 解密
+                try {
+                    processedPhone = PhoneUtil.decryptPhone(phoneNum);
+                } catch (CustomException e) {
+                    log.error("手机号解密失败: userId={}, phone={}", userId, phoneNum);
+                    continue;
+                }
+            } else {
+                processedPhone = phoneNum;
+            }
+
+            // 如果处理成功,创建callee对象,并记录其fsUserId
+            CpCalleeDomain calleeDomain = CpCalleeDomain.builder()
+                    .number(processedPhone)
+                    .userData(userId.toString())
+                    .build();
+            processedPhoneList.add(calleeDomain);
+        }
+
+        return new QwSopTtTaskServiceImpl.PhoneAndIdResult(processedPhoneList);
+    }
+    /**
+     * 内部类,用于封装电话号码列表
+     */
+    private static class PhoneAndIdResult {
+        private final List<CpCalleeDomain> mobileList;
+
+        public PhoneAndIdResult(List<CpCalleeDomain> mobileList) {
+            this.mobileList = mobileList;
+        }
+
+        public List<CpCalleeDomain> getMobileList() {
+            return mobileList;
+        }
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/cpaicall/CpAiCallController.java

@@ -0,0 +1,11 @@
+package com.fs.cpaicall;
+
+
+import com.fs.cpaicall.domain.apiresult.CpPushIIntentionResult;
+
+public class CpAiCallController {
+
+    public void callNotify(CpPushIIntentionResult result){
+
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/cpaicall/config/CpAiCallConfig.java

@@ -0,0 +1,11 @@
+package com.fs.cpaicall.config;
+
+import lombok.Data;
+
+@Data
+public class CpAiCallConfig {
+    private String customer;
+    private String password;
+    private String url;
+    private String dialogUrl;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/CpBaseDomain.java

@@ -0,0 +1,13 @@
+package com.fs.cpaicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@Data
+public class CpBaseDomain {
+    private String seq;
+    private String userData;
+
+    private JSONObject telData;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/CpCIDGroupInfo.java

@@ -0,0 +1,15 @@
+package com.fs.cpaicall.domain;
+
+import lombok.*;
+
+import java.util.List;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpCIDGroupInfo extends CpBaseDomain {
+
+    public List<CpCIDGroupInfoDataItem> data;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/CpCIDGroupInfoDataItem.java

@@ -0,0 +1,13 @@
+package com.fs.cpaicall.domain;
+
+import com.google.gson.JsonArray;
+import lombok.Data;
+
+@Data
+public class CpCIDGroupInfoDataItem {
+    private String id;
+    private String name;
+    private JsonArray did;
+    private JsonArray cid;
+    private JsonArray channel;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/CpTaskInfo.java

@@ -0,0 +1,18 @@
+package com.fs.cpaicall.domain;
+
+import lombok.*;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpTaskInfo extends CpBaseDomain {
+
+    private String taskID;
+    private String taskName;
+    private String callee;//被叫号码
+    private String startTime;//开始时间 不传默认为当天
+    private String endTime;//被叫号码 不传默认为当天
+
+}

+ 11 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpAuthentication.java

@@ -0,0 +1,11 @@
+package com.fs.cpaicall.domain.apiresult;
+
+import lombok.Data;
+
+@Data
+public class CpAuthentication {
+    private String customer;
+    private String timestamp;
+    private String seq;
+    private String digest;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpContent.java

@@ -0,0 +1,37 @@
+package com.fs.cpaicall.domain.apiresult;
+
+import lombok.Data;
+
+@Data
+public class CpContent {
+
+    //	对话对象	Int	0-机器人 1-被叫客户
+    private Integer side;
+    //	录音地址	String	-
+    private String recordURL;
+    //	匹配回答	String	-
+    private String matched_answer;
+    //	话术子场景ID	Int	-
+    private Integer scenceID;
+    //	创建时间	String	微秒级时间戳
+    private String createTime;
+    //	匹配场景关键词	String	-
+    private String matched_word;
+    //	话术子场景唯一标识ID	String	-
+    private String action_id;
+    //	回答内容2	String	-
+    private String content2;
+    //	回答内容1	String	-
+    private String content1;
+    //	话术ID	Int	-
+    private Integer dialog_id;
+    //	ID	Int	-
+    private Integer id;
+    //	回答内容	String	-
+    private String text;
+    //	错误状态	String	-
+    private String error_status;
+    //	状态	String	-
+    private String status;
+
+}

+ 34 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpNotify.java

@@ -0,0 +1,34 @@
+package com.fs.cpaicall.domain.apiresult;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CpNotify {
+    // 标识
+    private String type;
+    //主叫号码
+    private String callerNum;
+    //被叫号码
+    private String calleeNum;
+    //创建时间
+    private String createTime;
+    //呼叫接通时间
+    private String answerTime;
+    //对话图uuid
+    private String uuid;
+    //对画图详情
+//    private String contentList;
+    private List<CpContent> cpContentList;
+    //客户分类
+    private String intention;
+    //任务ID
+    private String taskID;
+    //录音地址
+    private String recordFile;
+    //客户资料自定义字段(可选参数)
+//    private Map<String, Object> field;
+    //用户数据
+    private String userData;
+}

+ 11 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/apiresult/CpPushIIntentionResult.java

@@ -0,0 +1,11 @@
+package com.fs.cpaicall.domain.apiresult;
+
+import lombok.Data;
+
+@Data
+public class CpPushIIntentionResult {
+
+    private CpAuthentication cpAuthentication;
+    private CpNotify cpNotify;
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/param/CpCalleeDomain.java

@@ -0,0 +1,17 @@
+package com.fs.cpaicall.domain.param;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.*;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpCalleeDomain extends CpBaseDomain {
+
+    private String number;
+    private String userData;
+    private String param;
+
+}

+ 118 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/param/CpCalltaskcreateaiCustomizeDomain.java

@@ -0,0 +1,118 @@
+package com.fs.cpaicall.domain.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.*;
+
+import java.util.List;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpCalltaskcreateaiCustomizeDomain extends CpBaseDomain {
+    /*
+    机器人组ID	Int	Y	-	-
+     */
+    private Long robot;
+    /*
+    话术ID	Int	Y	-	-
+     */
+    private Long dialogID;
+    /*
+    模式	Int	Y	-	只有 7 ,8
+     */
+    private Long mode;
+    /*
+    短信模板名称	String	N	-	通过【querySmsTemplates=查询短信模板信息】接口可获得
+     */
+    private String templateID;
+    /*
+    班组ID	Int	N	-	只有mode 为8 的情况下,必填
+     */
+    private String agentGroup;
+    /*
+    被叫号码	Array	Y	1-1000	Array 数组
+     */
+    private List<CpCalleeDomain> callees;
+    /*
+    主叫号码	Array	N	1-1000	主叫号码 和 主叫分组ID 必传其中一个,都传情况下,以 主叫分组ID为主
+     */
+    private List<CpCalleeDomain> callers;
+    /*
+    主叫分组ID	Int	N	-	主叫号码 和 主叫分组ID 必传其中一个,都传情况下,以 主叫分组ID为主
+     */
+    @JsonProperty("CIDGroupID")
+    private String CIDGroupID;
+    /*
+    任务名称	String	N	-	-
+     */
+    private String taskName = "测试任务1";
+    /*
+    呼叫倍率	String	N	-	1-5
+     */
+    private Long multiplier;
+    /*
+    呼叫基准模式	String	N	-	0 机器人 1 班组
+     */
+    private String robot_concurrence_mode;
+    /*
+    是否开启自动重呼	Int	N	-	0否 1是
+     */
+    private Long autoRecall;
+    /*
+    是否使用TTS将变量转换为语音	Int	N	-	0否 1是,不传默认为1
+     */
+    private String ttsTransfer;
+    /*
+    重呼次数	Int	N	-	最大5 0表示不自动重呼
+     */
+    private Long recallTimes;
+    /*
+    自动启动	Int	N	-	0否 1是 只在被叫名单列表中有TTS变量的情况下生效
+     */
+    private String autoStart;
+    /*
+    时间段1	Array	N	0,1,2,3,4,5,6	0表示周天 1-6表示周一到周六
+     */
+    private List<String> weekday1;
+    /*
+    时间段1的开始时间	String	N	08:00	如果传入weekday1后,必填 格式为HH:ii
+     */
+    private String startTime1;
+    /*
+    时间段1的结束时间	String	N	18:00	如果传入weekday1后,必填 格式为HH:ii
+     */
+    private String endTime1;
+    /*
+    时间段2	Array	N	0,1,2,3,4,5,6	0表示周天 1-6表示周一到周六
+     */
+    private String weekday2;
+    /*
+    时间段2的开始时间	String	N	08:00	如果传入weekday2后,必填 格式为HH:ii
+     */
+    private String startTime2;
+    /*
+    时间段2的结束时间	String	N	18:00	如果传入weekday2后,必填 格式为HH:ii
+     */
+    private String endTime2;
+    /*
+    时间段3	Array	N	0,1,2,3,4,5,6	0表示周天 1-6表示周一到周六
+     */
+    private String weekday3;
+    /*
+    时间段2的开始时间	String	N	08:00	如果传入weekday3后,必填 格式为HH:ii
+     */
+    private String startTime3;
+    /*
+    时间段2的结束时间	String	N	18:00	如果传入weekday3后,必填 格式为HH:ii
+     */
+    private String endTime3;
+    /*
+    是否为多个联系人名单任务,主要用于重呼时检查数据	Int	N	-	0否 1是,不传默认为0
+     */
+    private String multipleContacts;
+
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/param/CpEditDialogDomain.java

@@ -0,0 +1,17 @@
+package com.fs.cpaicall.domain.param;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.*;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpEditDialogDomain extends CpBaseDomain {
+
+    private Long id;
+    private String name;
+    private String remark;
+
+}

+ 13 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/param/CpgetDialogMapDomain.java

@@ -0,0 +1,13 @@
+package com.fs.cpaicall.domain.param;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.*;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class CpgetDialogMapDomain extends CpBaseDomain {
+    private String uuid;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpCIDGroupListResult.java

@@ -0,0 +1,20 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpCIDGroupListResult extends CpBaseDomain {
+    /** 机器人ID */
+    private String id;
+    /** 名称 */
+    private String name;
+    private List<String> did;
+    private List<String> cid;
+    private List<String> channel;
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpCalltaskcreateaiCustomizeResult.java

@@ -0,0 +1,17 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpCalltaskcreateaiCustomizeResult extends CpBaseDomain {
+    /** 创建ai外呼任务呼叫名单 */
+    private String report;
+    /** 外呼任务名称 */
+    private String taskName;
+    /** 外呼任务ID */
+    private Long taskID;
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpEditDialogResult.java

@@ -0,0 +1,16 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpEditDialogResult extends CpBaseDomain {
+
+    private Long id;
+    private String name;
+    private String remark;
+    private String callNum;
+
+}

+ 65 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpGetCdrListResult.java

@@ -0,0 +1,65 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 获取通话详单 (CDR) 接口的 Result 类
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpGetCdrListResult extends CpBaseDomain {
+
+    // 分页信息
+    private String total;
+    private Integer page;
+    private Integer size;
+
+    // 通话详单列表
+    private List<CdrRecord> cdr;
+
+    /**
+     * 通话详单记录内部类
+     */
+    @Data
+    public static class CdrRecord {
+        private String key;
+        private String agent;
+        private String serviceType;
+        private String startTime;
+        private String ringTime;
+        private String answerTime;
+        private String caller;
+        private String numberX;
+        private String callee;
+        private String timeLength;
+        private BigDecimal fee; // 使用BigDecimal处理金额
+        private Integer releaseCause;
+        private String filename;
+        private String session;
+        private String userData; // 注意:这里cdr记录里也有userData
+        private String taskID;
+        private String byeTime;
+        private String result; // 这个result是通话结果,不同于顶层的result
+        private String area;
+        private String city;
+        private String spName;
+        private String keyPress;
+        private String callResult;
+        private String typeResult;
+        private String customer;
+        private List<String> label; // "label": ["测试","测试2"]
+        private String trunkType;
+        private String trunkIndex;
+        private String trunkName;
+        private String trunkPeerIP;
+        private String trunkUsername;
+
+        private String errMsg;
+        private String errMsgCN;
+    }
+}

+ 17 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpGetairobotResult.java

@@ -0,0 +1,17 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpGetairobotResult extends CpBaseDomain {
+    /** 机器人ID */
+    private String id;
+    /** 数量 */
+    private Integer num;
+    /** 鸣潮 */
+    private String name;
+
+}

+ 28 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpQueryCallTaskInfoResult.java

@@ -0,0 +1,28 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpQueryCallTaskInfoResult extends CpBaseDomain {
+
+    private String taskID;
+    private String taskName;
+    /** 呼叫任务预计呼叫总数 */
+    private String calleeAmount;
+    /** 呼叫任务已呼叫数 */
+    private String calledAmount;
+    /** 呼叫任务剩余呼叫数 */
+    private String calleeResidue;
+    /**
+     * 呼叫任务状态
+     * 0-未启动
+     * 1-运行中
+     * 2-已暂停
+     * 3-已停止
+    */
+    private String runningStatus;
+
+}

+ 31 - 0
fs-service/src/main/java/com/fs/cpaicall/domain/result/CpgetDialogMapResult.java

@@ -0,0 +1,31 @@
+package com.fs.cpaicall.domain.result;
+
+import com.fs.cpaicall.domain.CpBaseDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CpgetDialogMapResult extends CpBaseDomain {
+
+    private String intention;
+    private String createTime;
+    private String answerTime;
+    private String uuid;
+    private String callerNum;
+    private String calleeNum;
+    private List<ContentList> contentList;
+    private String recordPath;
+
+
+    @Data
+    static class ContentList{
+        private String side;
+        private String id;
+        private String recordURL;
+        private String createTime;
+        private String text;
+    }
+}

+ 180 - 0
fs-service/src/main/java/com/fs/cpaicall/service/CpAiCallService.java

@@ -0,0 +1,180 @@
+package com.fs.cpaicall.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.cpaicall.domain.CpBaseDomain;
+import com.fs.cpaicall.domain.CpTaskInfo;
+import com.fs.cpaicall.domain.param.CpCalltaskcreateaiCustomizeDomain;
+import com.fs.cpaicall.domain.param.CpEditDialogDomain;
+import com.fs.cpaicall.domain.param.CpgetDialogMapDomain;
+import com.fs.cpaicall.domain.result.*;
+import com.fs.cpaicall.utils.CpAiCallUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.function.Function;
+@Slf4j
+@Component
+public class CpAiCallService {
+
+
+    private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public String getDialogUrl(Long companyId){
+        return CpAiCallUtils.getConfig(companyId).getDialogUrl();
+    }
+
+
+    private <T extends CpBaseDomain> Function<JSONObject, T> getObj(Class<T> clazz){
+        return getObj("data", clazz);
+    }
+    private <T extends CpBaseDomain> Function<JSONObject, List<T>> getList(Class<T> clazz){
+        return getList("list", clazz);
+    }
+    private <T extends CpBaseDomain> Function<JSONObject, T> getObj(String attr, Class<T> clazz){
+        return e -> {
+            T t = e.getJSONObject(attr).toJavaObject(clazz);
+            t.setUserData(e.getString("userData"));
+            t.setSeq(e.getString("seq"));
+            return t;
+        };
+    }
+    private <T extends CpBaseDomain> Function<JSONObject, T> getObjWithTelData(String attr, Class<T> clazz){
+        return e -> {
+            T t = e.getJSONObject(attr).toJavaObject(clazz);
+            t.setUserData(e.getString("userData"));
+            t.setSeq(e.getString("seq"));
+            t.setTelData(e.getJSONObject("telData"));
+            return t;
+        };
+    }
+    private <T extends CpBaseDomain> Function<JSONObject, List<T>> getArr(String attr, Class<T> clazz){
+        return e -> {
+            List<T> t = e.getJSONArray(attr).toJavaList(clazz);
+            t.forEach(i->{
+                i.setUserData(e.getString("userData"));
+                i.setSeq(e.getString("seq"));
+            });
+            return t;
+        };
+    }
+
+    private <T extends CpBaseDomain> Function<JSONObject, List<T>> getList(String attr, Class<T> clazz){
+        return e -> {
+            List<T> list = e.getJSONArray(attr).toJavaList(clazz);
+            String userData = e.getString("userData");
+            String seq = e.getString("seq");
+            list.forEach(t -> {
+                t.setUserData(userData);
+                t.setSeq(seq);
+            });
+            return list;
+        };
+    }
+
+    /**
+     * 登录
+     */
+    public String getToken(Long companyId){
+        return CpAiCallUtils.send("login", e -> e.getString("token"), companyId);
+    }
+
+    /**
+     * 获取机器人列表
+     */
+    public List<CpGetairobotResult> getairobotlist(Long companyId){
+        return CpAiCallUtils.send("getairobotlist", getList(CpGetairobotResult.class), companyId);
+    }
+
+    /**
+     * 获取话术列表
+     */
+    public List<CpEditDialogResult> queryDialog(Long companyId){
+        return CpAiCallUtils.send("queryDialog", getList("data", CpEditDialogResult.class), companyId);
+    }
+    /**
+     * 获取话术详情
+     */
+    public CpEditDialogResult getDialog(Long companyId){
+        return CpAiCallUtils.send("getDialog", getObj(CpEditDialogResult.class), companyId);
+    }
+
+    /**
+     * 修改/添加话术
+     * @param param 话术参数
+     */
+    public CpEditDialogResult editDialog(CpEditDialogDomain param, Long companyId){
+        return CpAiCallUtils.send("editDialog", OBJECT_MAPPER.valueToTree(param), getObj(CpEditDialogResult.class), companyId);
+    }
+
+    /**
+     * 创建机器人外呼任务
+     * @param param 参数
+     */
+    public CpCalltaskcreateaiCustomizeResult calltaskcreateaiCustomize(CpCalltaskcreateaiCustomizeDomain param, Long companyId){
+        return CpAiCallUtils.send("calltaskcreateaiCustomize", OBJECT_MAPPER.valueToTree(param), getObj(CpCalltaskcreateaiCustomizeResult.class), companyId);
+    }
+    /**
+     * 查询外呼任务状态信息
+     * @param param 参数
+     */
+    public CpQueryCallTaskInfoResult queryCallTaskInfo(CpTaskInfo param, Long companyId){
+        return CpAiCallUtils.send("queryCallTaskInfo", OBJECT_MAPPER.valueToTree(param), getObj(CpQueryCallTaskInfoResult.class), companyId);
+    }
+    /**
+     * 启动外呼任务
+     * @param param 参数
+     */
+    public CpTaskInfo startCallTask(CpTaskInfo param, Long companyId){
+        return CpAiCallUtils.send("startCallTask", OBJECT_MAPPER.valueToTree(param), getObj(CpTaskInfo.class), companyId);
+    }
+    /**
+     * 停止外呼任务
+     * @param param 参数
+     */
+    public CpTaskInfo stopCallTask(CpTaskInfo param, Long companyId){
+        return CpAiCallUtils.send("stopCallTask", OBJECT_MAPPER.valueToTree(param), getObj(CpTaskInfo.class), companyId);
+    }
+    /**
+     * 对话图查询(uuid)
+     * @param param 参数
+     */
+    public CpTaskInfo getDialogMap(CpgetDialogMapDomain param, Long companyId){
+        return CpAiCallUtils.send("getDialogMap", OBJECT_MAPPER.valueToTree(param), getObj("telData", CpTaskInfo.class), companyId);
+    }
+
+    public CpTaskInfo getDialogMapNew(CpgetDialogMapDomain param, Long companyId){
+        return CpAiCallUtils.send("getDialogMap", OBJECT_MAPPER.valueToTree(param), getObjWithTelData("telData", CpTaskInfo.class), companyId);
+    }
+    /**
+     * 获取主叫分组
+     * @param param 参数
+     */
+    public List<CpCIDGroupListResult> getCIDGroupList(CpBaseDomain param, Long companyId){
+        return CpAiCallUtils.send("getCIDGroupList",null, getArr("data", CpCIDGroupListResult.class), companyId);
+    }
+
+    public CpGetCdrListResult getCdrList(CpTaskInfo param, Long companyId) {
+        // 创建一个仅用于 getCdrList 的专用处理器
+        Function<JSONObject, CpGetCdrListResult> cdrProcessor = dataJson -> {
+            JSONObject responseObj = dataJson.getJSONObject("response");
+            if (responseObj == null) {
+                responseObj = dataJson;
+            }
+            CpGetCdrListResult result = responseObj.toJavaObject(CpGetCdrListResult.class);
+            String userData = dataJson.getString("userData");
+            String seq = dataJson.getString("seq");
+            if (userData != null && seq != null) {
+                result.setUserData(userData);
+                result.setSeq(seq);
+            } else {
+                log.warn("getCdrList 接口返回的 'data' 部分中未找到 'userData' 或 'seq' 字段,将使用默认值或保持 null。userData={}, seq={}", userData, seq);
+            }
+            return result;
+        };
+        // 调用工具类,传入专用的处理器
+        return CpAiCallUtils.send("getCdrList", OBJECT_MAPPER.valueToTree(param), cdrProcessor, companyId);
+    }
+}

+ 98 - 0
fs-service/src/main/java/com/fs/cpaicall/utils/CpAiCallUtils.java

@@ -0,0 +1,98 @@
+package com.fs.cpaicall.utils;
+
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fs.cpaicall.config.CpAiCallConfig;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.company.domain.CompanyVoiceApiTiantian;
+import com.fs.company.mapper.CompanyVoiceApiTiantianMapper;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Instant;
+import java.util.function.Function;
+
+@Slf4j
+public class CpAiCallUtils {
+
+    private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    public static String encrypt(String customer, long timestamp, int seq, String password) {
+
+        // 获取输入字符串的字节数组
+        byte[] messageDigest = CpMd5Utils.md5(customer + "@" + timestamp + "@" + seq + "@" + password);
+        // 将字节数组转为十六进制字符串
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : messageDigest) {
+            // 将每个字节转换为两位的十六进制表示
+            String hex = Integer.toHexString(0xFF & b);
+            if (hex.length() == 1) {
+                hexString.append("0");
+            }
+            hexString.append(hex);
+        }
+        // 返回 MD5 加密后的字符串
+        return hexString.toString();
+    }
+
+    public static String getParams(ObjectNode params, Long companyId){
+        try {
+            CpAiCallConfig config = getConfig(companyId);
+            long timestamp = Instant.now().getEpochSecond();
+            int seq = RandomUtil.randomInt(1000, 1000000);
+            ObjectNode rootNode = OBJECT_MAPPER.createObjectNode();
+            rootNode.putObject("authentication")
+                    .put("customer", config.getCustomer())
+                    .put("timestamp", timestamp)
+                    .put("seq", seq)
+                    .put("digest", encrypt(config.getCustomer(), timestamp, seq, config.getPassword()));
+            if(params == null){
+                rootNode.putObject("request").put("seq", seq);
+            }else{
+                rootNode.set("request", params);
+            }
+            return OBJECT_MAPPER.writeValueAsString(rootNode);
+        }catch (Exception e){
+            e.printStackTrace();
+            throw new RuntimeException(e);
+        }
+    }
+    public static <T> T send(String addr, Function<JSONObject, T> dataFun, Long companyId){
+        return send(addr, null, dataFun, companyId);
+    }
+    public static <T> T send(String addr, ObjectNode params, Function<JSONObject, T> dataFun, Long companyId){
+        String url = getConfig(companyId).getUrl() + addr;
+        String body = getParams(params, companyId);
+        HttpRequest request = HttpUtil.createPost(url).body(body);
+        request.header("Accept", "application/json");
+        request.header("Content-Type", "application/json");
+        HttpResponse execute = request.execute();
+        JSONObject json = JSON.parseObject(execute.body());
+        JSONObject result = json.getJSONObject("result");
+        if(!"0".equals(result.getString("error"))){
+            log.error("AI外呼接口异常数据:{}", json);
+            throw new RuntimeException(result.getString("msg"));
+        }
+        return dataFun.apply(json.getJSONObject("data").getJSONObject("response"));
+    }
+
+    public static CpAiCallConfig getConfig(Long companyId) {
+        CompanyVoiceApiTiantianMapper mapper = SpringUtils.getBean(CompanyVoiceApiTiantianMapper.class);
+        CompanyVoiceApiTiantian config = mapper.selectOne(new QueryWrapper<CompanyVoiceApiTiantian>().eq("company_id", companyId));
+        CpAiCallConfig cpAiCallConfig = new CpAiCallConfig();
+        if(null !=  config){
+            cpAiCallConfig.setCustomer(config.getAccount());
+            cpAiCallConfig.setPassword(config.getPassword());
+            cpAiCallConfig.setUrl(config.getUrl());
+            cpAiCallConfig.setDialogUrl(config.getDialogUrl());
+        }
+        return cpAiCallConfig;
+    }
+}

+ 66 - 0
fs-service/src/main/java/com/fs/cpaicall/utils/CpMd5Utils.java

@@ -0,0 +1,66 @@
+package com.fs.cpaicall.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.MessageDigest;
+
+/**
+ * Md5加密方法
+ * 
+ 
+ */
+public class CpMd5Utils
+{
+    private static final Logger log = LoggerFactory.getLogger(CpMd5Utils.class);
+
+    public static byte[] md5(String s){
+        MessageDigest algorithm;
+        try
+        {
+            algorithm = MessageDigest.getInstance("MD5");
+            algorithm.reset();
+            algorithm.update(s.getBytes("UTF-8"));
+            byte[] messageDigest = algorithm.digest();
+            return messageDigest;
+        }
+        catch (Exception e)
+        {
+            log.error("MD5 Error...", e);
+        }
+        return null;
+    }
+
+    private static final String toHex(byte hash[])
+    {
+        if (hash == null)
+        {
+            return null;
+        }
+        StringBuffer buf = new StringBuffer(hash.length * 2);
+        int i;
+
+        for (i = 0; i < hash.length; i++)
+        {
+            if ((hash[i] & 0xff) < 0x10)
+            {
+                buf.append("0");
+            }
+            buf.append(Long.toString(hash[i] & 0xff, 16));
+        }
+        return buf.toString();
+    }
+
+    public static String hash(String s)
+    {
+        try
+        {
+            return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8");
+        }
+        catch (Exception e)
+        {
+            log.error("not supported charset...{}", e);
+            return s;
+        }
+    }
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -481,4 +481,10 @@ public interface FsUserMapper
 
     @Select("select * from fs_user where apple_key = #{appleKey}")
     FsUser findUserByAppleKey(String appleKey);
+
+    /**
+     * 根据用户ID查询用户手机号
+     * @return key:用户ID, value:手机号
+     */
+    Map<Long, String> selectFsUserPhoneMap(List<Long> userIds);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/sop/vo/QwSopLogsListCVO.java

@@ -104,4 +104,9 @@ public class QwSopLogsListCVO {
      * app发送备注
      */
     private String appSendRemark;
+
+    /**
+     * 外部联系人id
+     */
+    private Long externalId;
 }

+ 250 - 0
fs-service/src/main/resources/mapper/company/QwSopTtDialogMapper.xml

@@ -0,0 +1,250 @@
+<?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.QwSopTtDialogMapper">
+
+    <resultMap type="QwSopTtDialog" id="QwSopTtDialogResult">
+        <result property="id" column="id"/>
+        <result property="sopTtTaskId" column="sop_tt_task_id"/>
+        <result property="phone" column="phone"/>
+        <result property="userLogsId" column="user_logs_id"/>
+        <result property="fsUserId" column="fs_user_id"/>
+        <result property="externalId" column="external_id"/>
+        <result property="qwUserKey" column="qw_user_key"/>
+        <result property="dialogId" column="dialog_id"/>
+        <result property="dialogName" column="dialog_name"/>
+        <result property="callStatus" column="call_status"/>
+        <result property="isAnswered" column="is_answered"/>
+        <result property="callDuration" column="call_duration"/>
+        <result property="hangupReason" column="hangup_reason"/>
+        <result property="callTime" column="call_time"/>
+        <result property="sendStatus" column="send_status"/>
+        <result property="sendTime" column="send_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectQwSopTtDialogVo">
+        select id, sop_tt_task_id, phone, user_logs_id, fs_user_id, external_id, qw_user_key,
+               dialog_id, dialog_name, call_status, is_answered, call_duration, hangup_reason,
+               call_time, send_status, send_time, create_time, update_time
+        from qw_sop_tt_call_detail
+    </sql>
+
+    <select id="selectQwSopTtDialogList" parameterType="QwSopTtDialog" resultMap="QwSopTtDialogResult">
+        <include refid="selectQwSopTtDialogVo"/>
+        <where>
+            <if test="sopTtTaskId != null"> and sop_tt_task_id = #{sopTtTaskId}</if>
+            <if test="phone != null and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
+            <if test="userLogsId != null and userLogsId != ''"> and user_logs_id like concat('%', #{userLogsId}, '%')</if>
+            <if test="fsUserId != null"> and fs_user_id = #{fsUserId}</if>
+            <if test="externalId != null"> and external_id = #{externalId}</if>
+            <if test="qwUserKey != null"> and qw_user_key = #{qwUserKey}</if>
+            <if test="dialogId != null"> and dialog_id = #{dialogId}</if>
+            <if test="dialogName != null and dialogName != ''"> and dialog_name like concat('%', #{dialogName}, '%')</if>
+            <if test="callStatus != null and callStatus != ''"> and call_status = #{callStatus}</if>
+            <if test="isAnswered != null"> and is_answered = #{isAnswered}</if>
+            <if test="sendStatus != null"> and send_status = #{sendStatus}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwSopTtDialogById" parameterType="Long" resultMap="QwSopTtDialogResult">
+        <include refid="selectQwSopTtDialogVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectQwSopTtDialogByTaskId" parameterType="Long" resultMap="QwSopTtDialogResult">
+        <include refid="selectQwSopTtDialogVo"/>
+        where sop_tt_task_id = #{sopTtTaskId}
+        order by id
+    </select>
+
+    <select id="selectPendingSendDetails" parameterType="Integer" resultMap="QwSopTtDialogResult">
+        <include refid="selectQwSopTtDialogVo"/>
+        where send_status = 0
+        order by create_time
+        limit #{limit}
+    </select>
+
+    <select id="countUnfinishedByTaskId" parameterType="Long" resultType="Integer">
+        select count(1)
+        from qw_sop_tt_call_detail
+        where sop_tt_task_id = #{sopTtTaskId}
+          and (call_status is null or call_status = '')
+    </select>
+
+    <insert id="insertQwSopTtDialog" parameterType="QwSopTtDialog" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_sop_tt_call_detail
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="sopTtTaskId != null">sop_tt_task_id,</if>
+            <if test="phone != null">phone,</if>
+            <if test="userLogsId != null">user_logs_id,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="externalId != null">external_id,</if>
+            <if test="qwUserKey != null">qw_user_key,</if>
+            <if test="dialogId != null">dialog_id,</if>
+            <if test="dialogName != null">dialog_name,</if>
+            <if test="callStatus != null">call_status,</if>
+            <if test="isAnswered != null">is_answered,</if>
+            <if test="callDuration != null">call_duration,</if>
+            <if test="hangupReason != null">hangup_reason,</if>
+            <if test="callTime != null">call_time,</if>
+            <if test="sendStatus != null">send_status,</if>
+            <if test="sendTime != null">send_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="sopTtTaskId != null">#{sopTtTaskId},</if>
+            <if test="phone != null">#{phone},</if>
+            <if test="userLogsId != null">#{userLogsId},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="externalId != null">#{externalId},</if>
+            <if test="qwUserKey != null">#{qwUserKey},</if>
+            <if test="dialogId != null">#{dialogId},</if>
+            <if test="dialogName != null">#{dialogName},</if>
+            <if test="callStatus != null">#{callStatus},</if>
+            <if test="isAnswered != null">#{isAnswered},</if>
+            <if test="callDuration != null">#{callDuration},</if>
+            <if test="hangupReason != null">#{hangupReason},</if>
+            <if test="callTime != null">#{callTime},</if>
+            <if test="sendStatus != null">#{sendStatus},</if>
+            <if test="sendTime != null">#{sendTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <insert id="batchInsertQwSopTtDialog" parameterType="list">
+        insert into qw_sop_tt_call_detail
+        (sop_tt_task_id, phone,
+        user_logs_id, fs_user_id, external_id, qw_user_key, dialog_id, dialog_name,
+        send_status, create_time, update_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.sopTtTaskId},
+            #{item.phone},
+            <choose>
+                <when test="item.userLogsId != null">#{item.userLogsId}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            <choose>
+                <when test="item.fsUserId != null">#{item.fsUserId}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            <choose>
+                <when test="item.externalId != null">#{item.externalId}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            <choose>
+                <when test="item.qwUserKey != null">#{item.qwUserKey}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            <choose>
+                <when test="item.dialogId != null">#{item.dialogId}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            <choose>
+                <when test="item.dialogName != null and item.dialogName != ''">#{item.dialogName}</when>
+                <otherwise>null</otherwise>
+            </choose>,
+            0, now(), now()
+            )
+        </foreach>
+    </insert>
+
+    <update id="updateQwSopTtDialog" parameterType="QwSopTtDialog">
+        update qw_sop_tt_call_detail
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="sopTtTaskId != null">sop_tt_task_id = #{sopTtTaskId},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="userLogsId != null">user_logs_id = #{userLogsId},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="externalId != null">external_id = #{externalId},</if>
+            <if test="qwUserKey != null">qw_user_key = #{qwUserKey},</if>
+            <if test="dialogId != null">dialog_id = #{dialogId},</if>
+            <if test="dialogName != null">dialog_name = #{dialogName},</if>
+            <if test="callStatus != null">call_status = #{callStatus},</if>
+            <if test="isAnswered != null">is_answered = #{isAnswered},</if>
+            <if test="callDuration != null">call_duration = #{callDuration},</if>
+            <if test="hangupReason != null">hangup_reason = #{hangupReason},</if>
+            <if test="callTime != null">call_time = #{callTime},</if>
+            <if test="sendStatus != null">send_status = #{sendStatus},</if>
+            <if test="sendTime != null">send_time = #{sendTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="updateQwSopTtDialogByTaskIdAndPhone" parameterType="QwSopTtDialog">
+        update qw_sop_tt_call_detail
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="callStatus != null">call_status = #{callStatus},</if>
+            <if test="isAnswered != null">is_answered = #{isAnswered},</if>
+            <if test="callDuration != null">call_duration = #{callDuration},</if>
+            <if test="hangupReason != null">hangup_reason = #{hangupReason},</if>
+            <if test="callTime != null">call_time = #{callTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where sop_tt_task_id = #{sopTtTaskId} and phone = #{phone}
+    </update>
+
+    <update id="batchUpdateCallResult" parameterType="list">
+        <foreach collection="list" item="item" separator=";">
+            update qw_sop_tt_call_detail
+            <set>
+                call_status = #{item.callStatus},
+                is_answered = #{item.isAnswered},
+                call_duration = #{item.callDuration},
+                hangup_reason = #{item.hangupReason},
+                call_time = #{item.callTime},
+                update_time = now()
+            </set>
+            where sop_tt_task_id = #{item.sopTtTaskId} and phone = #{item.phone}
+        </foreach>
+    </update>
+
+    <delete id="deleteQwSopTtDialogById" parameterType="Long">
+        delete from qw_sop_tt_call_detail where id = #{id}
+    </delete>
+
+    <delete id="deleteQwSopTtDialogByIds" parameterType="Long">
+        delete from qw_sop_tt_call_detail where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteQwSopTtDialogByTaskId" parameterType="Long">
+        delete from qw_sop_tt_call_detail where sop_tt_task_id = #{sopTtTaskId}
+    </delete>
+
+    <update id="batchUpdateQwSopTtDialogByIds" parameterType="QwSopTtDialog">
+        UPDATE qw_sop_tt_call_detail
+        <set>
+            <if test="sopTtTaskId != null">sop_tt_task_id = #{sopTtTaskId},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="userLogsId != null">user_logs_id = #{userLogsId},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="externalId != null">external_id = #{externalId},</if>
+            <if test="qwUserKey != null">qw_user_key = #{qwUserKey},</if>
+            <if test="dialogId != null">dialog_id = #{dialogId},</if>
+            <if test="dialogName != null and dialogName != ''">dialog_name = #{dialogName},</if>
+            <if test="callStatus != null">call_status = #{callStatus},</if>
+            <if test="isAnswered != null">is_answered = #{isAnswered},</if>
+            <if test="callDuration != null">call_duration = #{callDuration},</if>
+            <if test="hangupReason != null">hangup_reason = #{hangupReason},</if>
+            <if test="callTime != null">call_time = #{callTime},</if>
+            <if test="sendStatus != null">send_status = #{sendStatus},</if>
+            <if test="sendTime != null">send_time = #{sendTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </set>
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+</mapper>

+ 136 - 0
fs-service/src/main/resources/mapper/company/QwSopTtTaskMapper.xml

@@ -0,0 +1,136 @@
+<?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.QwSopTtTaskMapper">
+
+    <resultMap type="QwSopTtTask" id="QwSopTtTaskResult">
+        <result property="id" column="id"/>
+        <result property="taskId" column="task_id"/>
+        <result property="taskName" column="task_name"/>
+        <result property="robotGroupId" column="robot_group_id"/>
+        <result property="totalCount" column="total_count"/>
+        <result property="status" column="status"/>
+        <result property="runTime" column="run_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectQwSopTtTaskVo">
+        select id, task_id, task_name, robot_group_id, total_count, status, create_time, update_time,run_time
+        from qw_sop_tt_task
+    </sql>
+
+    <select id="selectQwSopTtTaskList" parameterType="QwSopTtTask" resultMap="QwSopTtTaskResult">
+        <include refid="selectQwSopTtTaskVo"/>
+        <where>
+            <if test="id != null and id != ''"> and id = #{id}</if>
+            <if test="taskId != null and taskId != ''"> and task_id = #{taskId}</if>
+            <if test="runTime != null and runTime != ''"> and run_time = #{runTime}</if>
+            <if test="taskName != null and taskName != ''"> and task_name like concat('%', #{taskName}, '%')</if>
+            <if test="robotGroupId != null and robotGroupId != ''"> and robot_group_id = #{robotGroupId}</if>
+            <if test="totalCount != null"> and total_count = #{totalCount}</if>
+            <if test="status != null"> and status = #{status}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwSopTtTaskById" parameterType="Long" resultMap="QwSopTtTaskResult">
+        <include refid="selectQwSopTtTaskVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectQwSopTtTaskByTaskId" parameterType="String" resultMap="QwSopTtTaskResult">
+        <include refid="selectQwSopTtTaskVo"/>
+        where task_id = #{taskId}
+    </select>
+
+    <select id="selectQwSopTtTaskByStatus" parameterType="Integer" resultMap="QwSopTtTaskResult">
+        <include refid="selectQwSopTtTaskVo"/>
+        where status = #{status}
+        order by create_time
+    </select>
+
+    <insert id="insertQwSopTtTask" parameterType="QwSopTtTask" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_sop_tt_task
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="taskId != null">task_id,</if>
+            <if test="taskName != null">task_name,</if>
+            <if test="runTime != null">run_time,</if>
+            <if test="robotGroupId != null">robot_group_id,</if>
+            <if test="totalCount != null">total_count,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="taskId != null">#{taskId},</if>
+            <if test="taskName != null">#{taskName},</if>
+            <if test="runTime != null">#{runTime},</if>
+            <if test="robotGroupId != null">#{robotGroupId},</if>
+            <if test="totalCount != null">#{totalCount},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateQwSopTtTask" parameterType="QwSopTtTask">
+        update qw_sop_tt_task
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="taskId != null">task_id = #{taskId},</if>
+            <if test="taskName != null">task_name = #{taskName},</if>
+            <if test="runTime != null">run_time = #{runTime},</if>
+            <if test="robotGroupId != null">robot_group_id = #{robotGroupId},</if>
+            <if test="totalCount != null">total_count = #{totalCount},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="updateQwSopTtTaskByTaskId" parameterType="QwSopTtTask">
+        update qw_sop_tt_task
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="taskName != null">task_name = #{taskName},</if>
+            <if test="runTime != null">run_time = #{runTime},</if>
+            <if test="robotGroupId != null">robot_group_id = #{robotGroupId},</if>
+            <if test="totalCount != null">total_count = #{totalCount},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where task_id = #{taskId}
+    </update>
+
+    <delete id="deleteQwSopTtTaskById" parameterType="Long">
+        delete from qw_sop_tt_task where id = #{id}
+    </delete>
+
+    <delete id="deleteQwSopTtTaskByIds" parameterType="String">
+        delete from qw_sop_tt_task where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteQwSopTtTaskByTaskId" parameterType="String">
+        delete from qw_sop_tt_task where task_id = #{taskId}
+    </delete>
+
+    <update id="batchUpdateQwSopTtTaskByIds" parameterType="QwSopTtTask">
+        UPDATE qw_sop_tt_task
+        <set>
+            <if test="taskId != null">task_id = #{taskId},</if>
+            <if test="taskName != null and taskName != ''">task_name = #{taskName},</if>
+            <if test="robotGroupId != null and robotGroupId != ''">robot_group_id = #{robotGroupId},</if>
+            <if test="totalCount != null">total_count = #{totalCount},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="runTime != null">run_time = #{runTime},</if>
+        </set>
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+</mapper>

+ 11 - 0
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -2548,4 +2548,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY f1.user_id DESC
     </select>
 
+
+    <select id="selectFsUserPhoneMap" resultType="java.util.Map">
+        select user_id as "key", phone as "value"
+        from fs_user
+        where user_id in
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+        and is_del = 0
+    </select>
+
 </mapper>