Explorar o código

回调客户信息

xw hai 4 semanas
pai
achega
1a09581cd8

+ 461 - 0
fs-qw-task/src/main/java/com/fs/app/task/EasyCallCallbackTask.java

@@ -0,0 +1,461 @@
+package com.fs.app.task;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
+import com.fs.company.service.impl.CompanyVoiceRoboticCallLogCallphoneServiceImpl;
+import com.fs.company.service.impl.CompanyVoiceRoboticWxServiceImpl;
+import com.fs.company.service.impl.CompanyWxClientServiceImpl;
+import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.impl.SysDictTypeServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * EasyCall外呼回调定时任务
+ * 定期查询待回调的记录,通过callback_uuid去easycallcenter365库查询回调信息
+ *
+ * @author system
+ * @date 2026-04-30
+ */
+@Component
+@Slf4j
+public class EasyCallCallbackTask {
+
+    @Autowired
+    private CompanyVoiceRoboticCallLogCallphoneMapper callLogMapper;
+
+    @Autowired
+    private EasyCallMapper easyCallMapper;
+
+    @Autowired
+    private CompanyVoiceRoboticCallLogCallphoneServiceImpl callLogService;
+
+    @Autowired
+    private ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+
+    @Autowired
+    private SysDictTypeServiceImpl sysDictTypeService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private CompanyWxClientServiceImpl companyWxClientService;
+
+    @Autowired
+    private CompanyVoiceRoboticWxServiceImpl companyVoiceRoboticWxService;
+
+    @Autowired
+    private CompanyWxAccountMapper companyWxAccountMapper;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+
+    @Autowired
+    private ICompanyVoiceRoboticCalleesService companyVoiceRoboticCalleesService;
+
+    private final AtomicBoolean isRunning = new AtomicBoolean(false);
+
+    /** 最大重试次数 */
+    private static final int MAX_RETRY_COUNT = 3;
+
+    /** 默认通话费用(元/分钟) */
+    private static final BigDecimal DEFAULT_CALL_CHARGE = new BigDecimal("0.12");
+
+    /** 一分钟的秒数 */
+    private static final BigDecimal ONE_MINUTES_SECOND = new BigDecimal("60");
+
+    /**
+     * 定时任务:处理EasyCall外呼回调
+     * 执行时间:每5分钟执行一次
+     * 功能:查询待回调记录,通过callback_uuid查询回调信息并更新
+     */
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void processEasyCallCallback() {
+        // 尝试设置标志为 true,表示任务开始执行
+        if (!isRunning.compareAndSet(false, true)) {
+            log.info("【EasyCall回调任务】上一个任务尚未完成,跳过此次执行");
+            return;
+        }
+
+        try {
+            long startTime = System.currentTimeMillis();
+            log.info("【EasyCall回调任务】开始执行");
+
+            // 查询待回调的记录(status=1且重试次数小于3)
+            List<CompanyVoiceRoboticCallLogCallphone> pendingRecords = callLogMapper.selectPendingCallbackRecords();
+
+            if (pendingRecords == null || pendingRecords.isEmpty()) {
+                log.info("【EasyCall回调任务】暂无待处理记录");
+                return;
+            }
+
+            log.info("【EasyCall回调任务】查询到待处理记录数量: {}", pendingRecords.size());
+
+            int successCount = 0;
+            int failCount = 0;
+            int maxRetryCount = 0;
+
+            for (CompanyVoiceRoboticCallLogCallphone record : pendingRecords) {
+                try {
+                    boolean success = processCallbackRecord(record);
+                    if (success) {
+                        successCount++;
+                    } else {
+                        failCount++;
+                    }
+                } catch (Exception e) {
+                    log.error("【EasyCall回调任务】处理记录失败,logId={}, callbackUuid={}",
+                            record.getLogId(), record.getCallbackUuid(), e);
+                    failCount++;
+                    // 更新重试次数
+                    updateRetryStatus(record, false);
+                }
+            }
+
+            long endTime = System.currentTimeMillis();
+            log.info("【EasyCall回调任务】执行完成,总数={}, 成功={}, 失败={}, 达到最大重试={}, 耗时={}ms",
+                    pendingRecords.size(), successCount, failCount, maxRetryCount, (endTime - startTime));
+
+        } catch (Exception e) {
+            log.error("【EasyCall回调任务】执行异常", e);
+        } finally {
+            // 重置标志为 false,表示任务已完成
+            isRunning.set(false);
+        }
+    }
+
+    /**
+     * 处理单条回调记录
+     *
+     * @param record 待处理记录
+     * @return true-处理成功,false-处理失败
+     */
+    private boolean processCallbackRecord(CompanyVoiceRoboticCallLogCallphone record) {
+        String callbackUuid = record.getCallbackUuid();
+        Long logId = record.getLogId();
+        Integer currentRetryCount = record.getRetryCount() == null ? 0 : record.getRetryCount();
+
+        log.info("【EasyCall回调任务】开始处理记录,logId={}, callbackUuid={}, 当前重试次数={}",
+                logId, callbackUuid, currentRetryCount);
+
+        // 通过callback_uuid去easycallcenter365库的cc_call_phone表查询
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByCallBackUuid(callbackUuid);
+
+        if (callPhoneRes == null) {
+            log.warn("【EasyCall回调任务】未查询到回调信息,logId={}, callbackUuid={}", logId, callbackUuid);
+            return updateRetryStatus(record, false);
+        }
+
+        log.info("【EasyCall回调任务】查询到回调信息,logId={}, uuid={}, telephone={}",
+                logId, callPhoneRes.getUuid(), callPhoneRes.getTelephone());
+
+        try {
+            // 更新回调数据到company_voice_robotic_call_log_callphone表
+            updateCallbackData(record, callPhoneRes);
+
+            // 回调成功,状态改为2(回调字段须走完整 update,仅 updateRetryCountAndStatus 不会落库 result 等)
+            record.setStatus(2);
+            record.setRetryCount(currentRetryCount + 1);
+            callLogMapper.updateCompanyVoiceRoboticCallLogCallphone(record);
+
+            // 更新用户标签
+            try {
+                crmCustomerPropertyService.addPropertyByCallLog(record);
+                log.info("【EasyCall回调任务】【isSendMsg=标签更新】用户标签更新成功,logId={}", logId);
+            } catch (Exception e) {
+                log.error("【EasyCall回调任务】更新用户标签失败,logId={}", logId, e);
+            }
+
+            log.info("【EasyCall回调任务】【isSendMsg=回调成功】回调数据更新成功,logId={}, callbackUuid={}, 状态=2",
+                    logId, callbackUuid);
+
+            return true;
+        } catch (Exception e) {
+            log.error("【EasyCall回调任务】更新回调数据失败,logId={}, callbackUuid={}", logId, callbackUuid, e);
+            return updateRetryStatus(record, false);
+        }
+    }
+
+    /**
+     * 更新重试状态
+     *
+     * @param record 记录
+     * @param success 是否成功
+     * @return true-更新成功,false-更新失败
+     */
+    private boolean updateRetryStatus(CompanyVoiceRoboticCallLogCallphone record, boolean success) {
+        Integer currentRetryCount = record.getRetryCount() == null ? 0 : record.getRetryCount();
+        int newRetryCount = currentRetryCount + 1;
+
+        record.setRetryCount(newRetryCount);
+
+        // 判断是否达到最大重试次数
+        if (newRetryCount >= MAX_RETRY_COUNT) {
+            // 达到最大重试次数,状态改为3
+            record.setStatus(3);
+            log.warn("【EasyCall回调任务】【isSendMsg=3】达到最大重试次数,logId={}, callbackUuid={}, 重试次数={}, 状态=3",
+                    record.getLogId(), record.getCallbackUuid(), newRetryCount);
+        } else {
+            log.info("【EasyCall回调任务】【isSendMsg=1】更新重试次数,logId={}, callbackUuid={}, 重试次数={}, 状态=1",
+                    record.getLogId(), record.getCallbackUuid(), newRetryCount);
+        }
+
+        try {
+            callLogMapper.updateRetryCountAndStatus(record);
+            return true;
+        } catch (Exception e) {
+            log.error("【EasyCall回调任务】更新重试状态失败,logId={}", record.getLogId(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 更新回调数据到company_voice_robotic_call_log_callphone表
+     *
+     * @param record 原记录
+     * @param callPhoneRes 回调数据
+     */
+    private void updateCallbackData(CompanyVoiceRoboticCallLogCallphone record, EasyCallCallPhoneVO callPhoneRes) {
+        log.info("【EasyCall回调任务】开始更新回调数据,logId={}, uuid={}", record.getLogId(), callPhoneRes.getUuid());
+
+        // 设置回调结果
+        record.setResult(JSON.toJSONString(callPhoneRes));
+
+        // 设置录音地址
+        record.setRecordPath(callPhoneRes.getWavfile());
+
+        // 设置对话内容
+        record.setContentList(callPhoneRes.getDialogue());
+
+        // 设置主叫号码
+        record.setCallerNum(callPhoneRes.getTelephone());
+
+        // 设置被叫号码
+        record.setCalleeNum(callPhoneRes.getCallerNumber());
+
+        // 设置uuid
+        record.setUuid(callPhoneRes.getUuid());
+
+        // 设置外呼时间
+        record.setCallCreateTime(callPhoneRes.getCalloutTime());
+
+        // 设置呼叫结束时间
+        record.setCallAnswerTime(callPhoneRes.getCallEndTime());
+
+        // 设置通话时长(毫秒转秒)
+        if (callPhoneRes.getTimeLen() != null) {
+            record.setCallTime(Long.valueOf(callPhoneRes.getTimeLen() / 1000));
+        }
+
+        // 意向度:优先使用 EasyCall cc_call_phone 的 intention/intent;无则再走 AI(对话)
+        String intentRaw = resolveIntentFromCcCallPhone(callPhoneRes);
+        String intentf = convertIntention(intentRaw);
+        if (StringUtils.isBlank(intentf)) {
+            intentf = convertIntention(calculateIntention(record, callPhoneRes));
+        }
+        if (StringUtils.isBlank(intentf)) {
+            intentf = "0";
+        }
+        record.setIntention(intentf);
+
+        // 计算通话费用
+        BigDecimal cost = calculateCallCost(record.getCallTime());
+        record.setCost(cost);
+
+        Long companyUserId = resolveCompanyUserId(record, callPhoneRes);
+        if (companyUserId != null) {
+            record.setCompanyUserId(companyUserId);
+        }
+
+        log.info("【EasyCall回调任务】【isSendMsg=数据更新】回调数据准备完成,logId={}, intention={}, callTime={}, cost={}, companyUserId={}, recordPath={}",
+                record.getLogId(), intentf, record.getCallTime(), cost, companyUserId,
+                StringUtils.isNotBlank(record.getRecordPath()) ? "有录音" : "无录音");
+    }
+
+    /**
+     * EasyCall cc_call_phone 表中的意向(intention 多为数字字符串,intent 多为等级字母)
+     */
+    private String resolveIntentFromCcCallPhone(EasyCallCallPhoneVO callPhoneRes) {
+        if (StringUtils.isNotBlank(callPhoneRes.getIntention())) {
+            return callPhoneRes.getIntention().trim();
+        }
+        if (StringUtils.isNotBlank(callPhoneRes.getIntent())) {
+            return callPhoneRes.getIntent().trim();
+        }
+        return null;
+    }
+
+    /**
+     * 计算意向度
+     *
+     * @param record 记录
+     * @param callPhoneRes 回调数据
+     * @return 意向度标签
+     */
+    private String calculateIntention(CompanyVoiceRoboticCallLogCallphone record, EasyCallCallPhoneVO callPhoneRes) {
+        String intention = null;
+        if (StringUtils.isNotBlank(callPhoneRes.getDialogue())) {
+            try {
+                log.info("【EasyCall回调任务】开始计算意向度,logId={}, dialogue长度={}",
+                        record.getLogId(), callPhoneRes.getDialogue().length());
+
+                intention = crmCustomerAnalyzeService.aiIntentionDegree(
+                        callPhoneRes.getDialogue(),
+                        java.time.LocalTime.now().getLong(java.time.temporal.ChronoField.MILLI_OF_SECOND)
+                );
+
+                log.info("【EasyCall回调任务】意向度计算完成,logId={}, intention={}",
+                        record.getLogId(), intention);
+            } catch (Exception e) {
+                log.error("【EasyCall回调任务】意向度AI解析失败,logId={}, uuid={}",
+                        record.getLogId(), callPhoneRes.getUuid(), e);
+            }
+        } else {
+            log.warn("【EasyCall回调任务】对话内容为空,无法计算意向度,logId={}", record.getLogId());
+        }
+        return intention;
+    }
+
+    /**
+     * 计算通话费用
+     *
+     * @param callTime 通话时长(秒)
+     * @return 费用(元)
+     */
+    private BigDecimal calculateCallCost(Long callTime) {
+        if (callTime == null || callTime == 0) {
+            log.info("【EasyCall回调任务】通话时长为0,费用为0");
+            return BigDecimal.ZERO;
+        }
+
+        // 获取费率配置
+        BigDecimal callCharge = DEFAULT_CALL_CHARGE;
+        try {
+            String json = configService.selectConfigByKey("cid.config");
+            if (StringUtils.isNotBlank(json)) {
+                CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+                if (cidConfigVO.getCallCharge() != null) {
+                    callCharge = cidConfigVO.getCallCharge();
+                }
+            }
+        } catch (Exception e) {
+            log.warn("【EasyCall回调任务】获取费率配置失败,使用默认费率{}", DEFAULT_CALL_CHARGE, e);
+        }
+
+        // 向上取整分钟数
+        BigDecimal minutes = new BigDecimal(callTime).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+        BigDecimal cost = minutes.multiply(callCharge);
+
+        log.info("【EasyCall回调任务】费用计算完成,通话时长={}s, 分钟数={}, 费率={}, 总费用={}",
+                callTime, minutes, callCharge, cost);
+
+        return cost;
+    }
+
+
+    private Long resolveCompanyUserId(CompanyVoiceRoboticCallLogCallphone record, EasyCallCallPhoneVO callPhoneRes) {
+        if (callPhoneRes.getCompanyUserId() != null) {
+            return callPhoneRes.getCompanyUserId();
+        }
+        return resolveCompanyUserIdFromWxBind(record);
+    }
+
+    private Long resolveCompanyUserIdFromWxBind(CompanyVoiceRoboticCallLogCallphone record) {
+        if (record.getRoboticId() == null || record.getCallerId() == null) {
+            log.warn("【EasyCall回调任务】roboticId或callerId为空,无法解析company_user_id,logId={}", record.getLogId());
+            return null;
+        }
+        try {
+            CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesById(record.getCallerId());
+            if (callees == null || callees.getUserId() == null) {
+                log.error("【EasyCall回调任务】未找到被叫记录或userId为空,logId={}, callerId={}",
+                        record.getLogId(), record.getCallerId());
+                return null;
+            }
+            CompanyWxClient companyWxClient = companyWxClientService.getOne(
+                    new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
+            if (companyWxClient == null || companyWxClient.getRoboticWxId() == null) {
+                log.error("【EasyCall回调任务】未找到CompanyWxClient或roboticWxId为空,logId={}, roboticId={}, customerId={}",
+                        record.getLogId(), callees.getRoboticId(), callees.getUserId());
+                return null;
+            }
+            CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxService.getById(companyWxClient.getRoboticWxId());
+            if (roboticWx == null) {
+                log.error("【EasyCall回调任务】未找到CompanyVoiceRoboticWx,logId={}, roboticWxId={}",
+                        record.getLogId(), companyWxClient.getRoboticWxId());
+                return null;
+            }
+            if (Integer.valueOf(1).equals(companyWxClient.getIsWeCom())) {
+                CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+                return companyWxAccount != null ? companyWxAccount.getCompanyUserId() : null;
+            }
+            if (Integer.valueOf(2).equals(companyWxClient.getIsWeCom())) {
+                QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+                return qwUser != null ? qwUser.getCompanyUserId() : null;
+            }
+            log.error("【EasyCall回调任务】CompanyWxClient.isWeCom非1/2,无法解析company_user_id,logId={}", record.getLogId());
+            return null;
+        } catch (Exception e) {
+            log.error("【EasyCall回调任务】按企微绑定解析company_user_id失败,logId={}", record.getLogId(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 转换意向度值
+     *
+     * @param intention 意向度标签
+     * @return 意向度数值
+     */
+    private String convertIntention(String intention) {
+        if (StringUtils.isBlank(intention)) {
+            return null;
+        }
+
+        List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+
+        String t = intention.trim();
+        // 已是非负整数(含 0)则直接入库
+        if (t.matches("^\\d+$")) {
+            return t;
+        }
+
+        // 根据标签查找对应的数值
+        Optional<SysDictData> firstDict = customerIntentionLevel.stream()
+                .filter(e -> e.getDictLabel().equals(t))
+                .findFirst();
+
+        if (firstDict.isPresent()) {
+            return firstDict.get().getDictValue();
+        }
+
+        return null;
+    }
+}

+ 22 - 1
fs-qw-task/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -34,13 +34,34 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.wx")
+    public DataSource wxDataSource() {
+        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) {
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource,
+                                        @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.name(), wxDataSource);
+        targetDataSources.put(DataSourceType.EASYCALL.name(), easyCallSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }
 

+ 4 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -106,6 +106,10 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @Excel(name = "外呼类型")
     private Integer callType;
 
+    /** 回调重试次数 */
+    @Excel(name = "回调重试次数")
+    private Integer retryCount;
+
     @TableField(exist = false)
     private String companyName;
 

+ 10 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -107,4 +107,14 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
     List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(@Param("calleeIds") List<Long> calleeIds,
                                                                      @Param("roboticId") Long roboticId,
                                                                      @Param("companyId") Long companyId);
+
+    /**
+     * 查询待回调的记录(status=1且重试次数小于3)
+     */
+    List<CompanyVoiceRoboticCallLogCallphone> selectPendingCallbackRecords();
+
+    /**
+     * 更新重试次数和状态
+     */
+    int updateRetryCountAndStatus(CompanyVoiceRoboticCallLogCallphone record);
 }

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

@@ -26,4 +26,7 @@ public interface EasyCallMapper {
     @DataSource(DataSourceType.EASYCALL)
     InboundCallInfo selectInboundCallbackInfoByUuid(@Param("uuid") String uuid);
 
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallCallPhoneVO getCallPhoneInfoByCallBackUuid(@Param("callBackUuid") String callBackUuid);
+
 }

+ 16 - 1
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCallPhoneVO.java

@@ -155,10 +155,15 @@ public class EasyCallCallPhoneVO {
     private String emptyNumberDetectionText;
 
     /**
-     * 客户意向
+     * 客户意向(等级字母等,对应 cc_call_phone.intent)
      */
     private String intent;
 
+    /**
+     * 意向度数值(对应 cc_call_phone.intention,多为 0~5;与 intent 并存时优先本字段)
+     */
+    private String intention;
+
     /**
      * asr时长(秒)
      */
@@ -199,6 +204,16 @@ public class EasyCallCallPhoneVO {
      */
     private String callerNumber;
 
+    /**
+     * EasyCall 公司维度(对应 cc_call_phone.company_id)
+     */
+    private Integer companyId;
+
+    /**
+     * 销售员/坐席在用户体系中的 id(对应 cc_call_phone.company_user_id)
+     */
+    private Long companyUserId;
+
     /**
      * customer dtmf input digits
      */

+ 23 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -25,6 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="companyUserId"    column="company_user_id"    />
         <result property="callTime"    column="call_time"    />
         <result property="cost"    column="cost"    />
+        <result property="retryCount"    column="retry_count"    />
     </resultMap>
 
     <sql id="selectCompanyVoiceRoboticCallLogCallphoneVo">
@@ -83,6 +84,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">company_user_id,</if>
             <if test="callTime != null">call_time,</if>
             <if test="cost != null">cost,</if>
+            <if test="retryCount != null">retry_count,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="callbackUuid != null">#{callbackUuid},</if>
@@ -105,6 +107,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="callTime != null">#{callTime},</if>
             <if test="cost != null">#{cost},</if>
+            <if test="retryCount != null">#{retryCount},</if>
          </trim>
     </insert>
 
@@ -130,6 +133,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="callTime != null">call_time = #{callTime},</if>
             <if test="cost != null">cost = #{cost},</if>
+            <if test="retryCount != null">retry_count = #{retryCount},</if>
         </trim>
         where log_id = #{logId}
     </update>
@@ -285,5 +289,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         GROUP BY caller_id
     </select>
 
+    <select id="selectPendingCallbackRecords" resultType="CompanyVoiceRoboticCallLogCallphone">
+        select * from company_voice_robotic_call_log_callphone
+        where status = 1
+          and callback_uuid is not null
+          and callback_uuid != ''
+          and (retry_count is null or retry_count &lt; 3)
+        order by create_time asc
+            limit 100
+    </select>
+
+    <update id="updateRetryCountAndStatus" parameterType="CompanyVoiceRoboticCallLogCallphone">
+        update company_voice_robotic_call_log_callphone
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="retryCount != null">retry_count = #{retryCount},</if>
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+
 
 </mapper>