浏览代码

统计外呼呼出次数和ai外呼用户意向度研判是否启用公司的ai来进行

xw 3 天之前
父节点
当前提交
15580ff95b

+ 28 - 1
fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java

@@ -10,16 +10,21 @@ 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.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
+import com.fs.framework.web.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -27,11 +32,13 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
  * 机器人外呼任务Controller
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -47,6 +54,10 @@ public class CompanyVoiceRoboticController extends BaseController
     private AiCallService aiCallService;
     @Autowired
     private ICompanyVoiceRoboticWxService companyVoiceRoboticWxService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     /**
      * 查询机器人外呼任务列表
@@ -71,6 +82,22 @@ public class CompanyVoiceRoboticController extends BaseController
     public TableDataInfo calleesList(Long id){
         startPage();
         List<CompanyVoiceRoboticCallees> list = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
+        if (list != null && !list.isEmpty() && id != null) {
+            Long companyId = null;
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser != null && loginUser.getUser() != null) {
+                companyId = loginUser.getUser().getCompanyId();
+            }
+            List<Long> calleeIds = list.stream().map(CompanyVoiceRoboticCallees::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            if (!calleeIds.isEmpty()) {
+                Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCalleeIds(calleeIds, id, companyId).stream()
+                        .collect(Collectors.toMap(CalleeRoboticCallOutCountVO::getCalleeId, CalleeRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+                for (CompanyVoiceRoboticCallees row : list) {
+                    long n = row.getId() == null ? 0L : countMap.getOrDefault(row.getId(), 0L);
+                    row.setRoboticCallOutCount((int) n);
+                }
+            }
+        }
         return getDataTable(list);
     }
 

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

@@ -21,9 +21,11 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CdrBodyVo;
 import com.fs.company.vo.CdrDetailVo;
 import com.fs.company.vo.WorkflowExecRecordVo;
@@ -37,6 +39,8 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -60,6 +64,8 @@ public class CompanyVoiceRoboticController extends BaseController
     private ICompanyVoiceRoboticWxService companyVoiceRoboticWxService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     /**
      * 查询机器人外呼任务列表
@@ -88,6 +94,19 @@ public class CompanyVoiceRoboticController extends BaseController
     public TableDataInfo calleesList(Long id){
         startPage();
         List<CompanyVoiceRoboticCallees> list = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
+        if (list != null && !list.isEmpty() && id != null) {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            Long companyId = loginUser.getCompany().getCompanyId();
+            List<Long> calleeIds = list.stream().map(CompanyVoiceRoboticCallees::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            if (!calleeIds.isEmpty()) {
+                Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCalleeIds(calleeIds, id, companyId).stream()
+                        .collect(Collectors.toMap(CalleeRoboticCallOutCountVO::getCalleeId, CalleeRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+                for (CompanyVoiceRoboticCallees row : list) {
+                    long n = row.getId() == null ? 0L : countMap.getOrDefault(row.getId(), 0L);
+                    row.setRoboticCallOutCount((int) n);
+                }
+            }
+        }
         return getDataTable(list);
     }
 

+ 58 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -12,7 +12,9 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.util.OrderUtils;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.*;
 import com.fs.crm.service.ICrmCustomerPropertyService;
@@ -33,6 +35,9 @@ import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 客户Controller
@@ -54,6 +59,8 @@ public class CrmCustomerController extends BaseController
     ICrmCustomerUserService crmCustomerUserService;
     @Autowired
     private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
@@ -71,6 +78,7 @@ public class CrmCustomerController extends BaseController
                 }
 
             }
+            fillLineCustomerListRoboticCallOutCount(list, param.getCompanyId());
         }
         return getDataTable(list);
     }
@@ -178,6 +186,7 @@ public class CrmCustomerController extends BaseController
                     vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
             }
+            fillMyCustomerListRoboticCallOutCount(list, param.getCompanyId());
         }
         return getDataTable(list);
 
@@ -204,6 +213,7 @@ public class CrmCustomerController extends BaseController
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
+                fillLineCustomerListRoboticCallOutCount(list1, param.getCompanyId());
             }
             return getDataTable(list1);
         }else {
@@ -216,10 +226,58 @@ public class CrmCustomerController extends BaseController
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
                 }
+                fillCustomerListRoboticCallOutCount(list, param.getCompanyId());
             }
             return getDataTable(list);
         }
     }
+    private void fillCustomerListRoboticCallOutCount(List<CrmCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
+
+    private void fillLineCustomerListRoboticCallOutCount(List<CrmLineCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmLineCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmLineCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
+
+    private void fillMyCustomerListRoboticCallOutCount(List<CrmMyCustomerListQueryVO> list, Long companyId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream().map(CrmMyCustomerListQueryVO::getCustomerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(customerIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (CrmMyCustomerListQueryVO vo : list) {
+            long n = vo.getCustomerId() == null ? 0L : countMap.getOrDefault(vo.getCustomerId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
     @ApiOperation("获取客户详情")
     @GetMapping("/getCustomerDetails")
     @PreAuthorize("@ss.hasPermi('crm:customer:query')")

+ 5 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java

@@ -8,7 +8,7 @@ import lombok.Data;
 
 /**
  * 任务外呼电话对象 company_voice_robotic_callees
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -67,4 +67,8 @@ public class CompanyVoiceRoboticCallees{
 
     //是否生成数据(0否,1是)
     private Integer isGenerate;
+
+    /** 本任务下该 callee 的 AI 外呼呼出次数(非表字段,来自 call_log 统计) */
+    @TableField(exist = false)
+    private Integer roboticCallOutCount;
 }

+ 22 - 7
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,20 +4,22 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 import org.apache.ibatis.annotations.Param;
 
 /**
  * 调用日志_ai打电话Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<CompanyVoiceRoboticCallLogCallphone>{
     /**
      * 查询调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 调用日志_ai打电话
      */
@@ -25,7 +27,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 查询调用日志_ai打电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 调用日志_ai打电话集合
      */
@@ -33,7 +35,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 新增调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -41,7 +43,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 修改调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -49,7 +51,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 删除调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 结果
      */
@@ -57,7 +59,7 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     /**
      * 批量删除调用日志_ai打电话
-     * 
+     *
      * @param logIds 需要删除的数据主键集合
      * @return 结果
      */
@@ -92,4 +94,17 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * @return 公司ID
      */
     Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
+
+    /**
+     * 按客户统计 AI 外呼呼出次数(callphone.caller_id = callees.id,callees.user_id = 客户 customer_id)
+     */
+    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(@Param("customerIds") List<Long> customerIds,
+                                                                         @Param("companyId") Long companyId);
+
+    /**
+     * 按外呼任务 callee 统计呼出次数(callphone.caller_id = callees.id,且限定当前任务 robotic_id)
+     */
+    List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(@Param("calleeIds") List<Long> calleeIds,
+                                                                     @Param("roboticId") Long roboticId,
+                                                                     @Param("companyId") Long companyId);
 }

+ 13 - 7
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java

@@ -4,19 +4,21 @@ import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
 
 /**
  * 调用日志_ai打电话Service接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<CompanyVoiceRoboticCallLogCallphone>{
     /**
      * 查询调用日志_ai打电话
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 调用日志_ai打电话
      */
@@ -24,7 +26,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 查询调用日志_ai打电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 调用日志_ai打电话集合
      */
@@ -32,7 +34,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 新增调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -40,7 +42,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 修改调用日志_ai打电话
-     * 
+     *
      * @param companyVoiceRoboticCallLogCallphone 调用日志_ai打电话
      * @return 结果
      */
@@ -48,7 +50,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 批量删除调用日志_ai打电话
-     * 
+     *
      * @param logIds 需要删除的调用日志_ai打电话主键集合
      * @return 结果
      */
@@ -56,7 +58,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     /**
      * 删除调用日志_ai打电话信息
-     * 
+     *
      * @param logId 调用日志_ai打电话主键
      * @return 结果
      */
@@ -70,6 +72,10 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      */
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallLogCallphoneListData(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId);
+
+    List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(List<Long> calleeIds, Long roboticId, Long companyId);
+
     /**
      * 查询 calleesIds
      * @param customerId

+ 60 - 13
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -5,7 +5,6 @@ import java.math.RoundingMode;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -16,7 +15,6 @@ import com.fs.aicall.domain.apiresult.Notify;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.param.getDialogMapDomain;
 import com.fs.aicall.service.AiCallService;
-import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
@@ -25,18 +23,14 @@ import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
-import com.fs.company.vo.CidConfigVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
-import com.fs.company.vo.DictVO;
+import com.fs.company.vo.*;
 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.store.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.impl.SysDictTypeServiceImpl;
-import com.fs.voice.constant.Constant;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -77,6 +71,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
+    private ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+    @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
@@ -252,7 +248,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
 
                 if (StringUtils.isNotBlank(notify.getUserData())) {
-                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData()), JSONObject.class);
+                    JSONObject userData = parseRedisCacheToJsonObject(redisCache2.getCacheObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData()));
                     if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
                         Map<String, Object> param = new HashMap<>();
                         param.put("callBackUuid", userData.getString("callBackUuid"));
@@ -313,16 +309,32 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                 Long answerTime = result.getCallEndTime();
                 companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                String intention = result.getIntent();
+                String intention = null;
+                if (StringUtils.isNotBlank(result.getDialogue())) {
+                    try {
+                        intention = crmCustomerAnalyzeService.aiIntentionDegree(
+                                result.getDialogue(),
+                                java.time.LocalTime.now().getLong(java.time.temporal.ChronoField.MILLI_OF_SECOND)
+                        );
+                    } catch (Exception e) {
+                        log.error("easyCall回调日志意向度AI解析失败,uuid={}", result.getUuid(), e);
+                    }
+                }
+                // 历史第三方值(当前不启用,保留用于回滚)
+                // String intention = result.getIntent();
                 String intentf = null;
+                final String intentionLabel = intention;
                 List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
-                if (!isPositiveInteger(intention)) {
-                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                if (!isPositiveInteger(intentionLabel)) {
+                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intentionLabel)).findFirst();
                     if (firstDict.isPresent()) {
                         SysDictData sysDictData = firstDict.get();
                         intentf = sysDictData.getDictValue();
                     }
+                } else {
+                    intentf = intentionLabel;
                 }
+                if (StringUtils.isBlank(intentf)) intentf = "0";
                 companyVoiceRoboticCallLog.setIntention(intentf);
                 companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
                 BigDecimal callCharge = cidConfigVO.getCallCharge();
@@ -340,7 +352,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
 
                 if (StringUtils.isNotBlank(result.getBizJson())) {
                     JSONObject bizJson = JSONObject.parseObject(result.getBizJson());
-                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid")), JSONObject.class);
+                    JSONObject userData = parseRedisCacheToJsonObject(redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid")));
                     if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
                         Map<String, Object> param = new HashMap<>();
                         param.put("callBackUuid", userData.getString("callBackUuid"));
@@ -380,6 +392,41 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
         return result;
     }
 
+    @Override
+    public List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return baseMapper.countRoboticCallOutByCustomerIds(customerIds, companyId);
+    }
+
+    @Override
+    public List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(List<Long> calleeIds, Long roboticId, Long companyId) {
+        if (calleeIds == null || calleeIds.isEmpty() || roboticId == null) {
+            return Collections.emptyList();
+        }
+        return baseMapper.countRoboticCallOutByCalleeIds(calleeIds, roboticId, companyId);
+    }
+    /**
+     * Redis 中 workflow 回调缓存可能是 String 或已反序列化的 JSONObject,避免 ClassCastException。
+     */
+    private static JSONObject parseRedisCacheToJsonObject(Object cacheObj) {
+        if (cacheObj == null) {
+            return null;
+        }
+        if (cacheObj instanceof JSONObject) {
+            return (JSONObject) cacheObj;
+        }
+        if (cacheObj instanceof String) {
+            String s = (String) cacheObj;
+            if (StringUtils.isBlank(s)) {
+                return null;
+            }
+            return JSONObject.parseObject(s);
+        }
+        return JSONObject.parseObject(JSONObject.toJSONString(cacheObj));
+    }
+
     @Override
     public List<Long> getCallerIdsByCustomerId(Long customerId) {
         return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);

+ 41 - 7
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -30,6 +30,7 @@ import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.param.SmsSendBatchParam;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -47,12 +48,15 @@ import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+
+import java.time.temporal.ChronoField;
 import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static com.fs.company.service.impl.call.node.AbstractWorkflowNode.companyVoiceRoboticCallLogCallphoneMapper;
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+import static java.time.LocalTime.now;
 
 
 /**
@@ -84,6 +88,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyVoiceRoboticWxServiceImpl companyVoiceRoboticWxService;
 
     private final CrmCustomerMapper crmCustomerMapper;
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
 
     private final CompanyWxClientServiceImpl companyWxClientServiceImpl;
 
@@ -822,11 +827,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     @Override
     @Async("cidWorkFlowExecutor")
     public void callerResult4EasyCall(CdrDetailVo result) {
-        try {
-            Thread.sleep(20000L);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
+//        try {
+//            Thread.sleep(20000L);
+//        } catch (InterruptedException e) {
+//            throw new RuntimeException(e);
+//        }
 //        EASYCALL
         log.info("进入easyCall外呼结果回调:{}", JSON.toJSONString(result));
         if (result == null || StringUtils.isBlank(result.getUuid())) return;
@@ -843,6 +848,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+        /*
         if (StringUtils.isBlank(callPhoneRes.getIntent())) {
             String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
             Integer retryCount = redisCache2.getCacheObject(retryKey);
@@ -864,6 +870,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         // intent 已有值,直接正常处理
         redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
         doHandleEasyCallResult(callPhoneRes);
+        */
+
+        // 当前:根据对话内容同步调用自家 AI 计算意向度,不依赖第三方 intent
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
     }
 
     /**
@@ -916,7 +927,15 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
-        String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+        Object cacheObj = redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+        String cacheString;
+        if (cacheObj instanceof String) {
+            cacheString = (String) cacheObj;
+        } else if (cacheObj instanceof JSONObject) {
+            cacheString = ((JSONObject) cacheObj).toJSONString();
+        } else {
+            cacheString = cacheObj == null ? null : JSONObject.toJSONString(cacheObj);
+        }
         if (StringUtils.isBlank(cacheString)) {
             log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
@@ -974,7 +993,22 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     public void pushDialogContent4EasyCall(JSONObject cacheInfo, EasyCallCallPhoneVO callPhoneRes) {
 
-        String intention = getIntention(callPhoneRes.getIntent());
+        String intention = null;
+        String intentionDegree = null;
+        if (StringUtils.isNotBlank(callPhoneRes.getDialogue())) {
+            log.info("【验证】意向度来源=自家AI, uuid={}, dialogueLength={}", callPhoneRes.getUuid(),
+                    StringUtils.isBlank(callPhoneRes.getDialogue()) ? 0 : callPhoneRes.getDialogue().length());
+            try {
+                intentionDegree = crmCustomerAnalyzeService.aiIntentionDegree(
+                        callPhoneRes.getDialogue(),
+                        now().getLong(ChronoField.MILLI_OF_SECOND)
+                );
+                intention = getIntention(intentionDegree);
+            } catch (Exception e) {
+                log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
+            }
+        }
+        // 2) 最终兜底:意向未知
         if (StringUtils.isEmpty(intention)) {
             intention = "0";
         }

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * 外呼任务下单个 callee(company_voice_robotic_callees.id)的呼出次数统计
+ */
+@Data
+public class CalleeRoboticCallOutCountVO {
+
+    private Long calleeId;
+
+    private Long callCount;
+}

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

@@ -0,0 +1,14 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * 客户维度 AI 外呼呼出次数统计(按 crm_customer.customer_id,对应 callees.user_id)
+ */
+@Data
+public class CustomerRoboticCallOutCountVO {
+
+    private Long customerId;
+
+    private Long callCount;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java

@@ -18,6 +18,8 @@ public class CrmCustomerListQueryVO implements Serializable
 
     /** 组织机构代码 */
     private String customerCode;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 
     /** 客户名称 */
     private String customerName;

+ 2 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java

@@ -17,6 +17,8 @@ public class CrmLineCustomerListQueryVO implements Serializable
 
     /** 组织机构代码 */
     private String customerCode;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 
     /** 客户名称 */
     private String customerName;

+ 2 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmMyCustomerListQueryVO.java

@@ -145,4 +145,6 @@ public class CrmMyCustomerListQueryVO implements Serializable
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date visitTime;
+    /** AI 外呼呼出次数(company_voice_robotic_call_log_callphone,经 callees.user_id=customerId) */
+    private Integer roboticCallOutCount;
 }

+ 35 - 6
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper">
-    
+
     <resultMap type="CompanyVoiceRoboticCallLogCallphone" id="CompanyVoiceRoboticCallLogCallphoneResult">
         <result property="logId"    column="log_id"    />
         <result property="roboticId"    column="robotic_id"    />
@@ -33,7 +33,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCompanyVoiceRoboticCallLogCallphoneList" parameterType="CompanyVoiceRoboticCallLogCallphone" resultMap="CompanyVoiceRoboticCallLogCallphoneResult">
         <include refid="selectCompanyVoiceRoboticCallLogCallphoneVo"/>
-        <where>  
+        <where>
             <if test="roboticId != null "> and robotic_id = #{roboticId}</if>
             <if test="callerId != null "> and caller_id = #{callerId}</if>
             <if test="runTime != null "> and run_time = #{runTime}</if>
@@ -54,12 +54,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="cost != null "> and cost = #{cost}</if>
         </where>
     </select>
-    
+
     <select id="selectCompanyVoiceRoboticCallLogCallphoneByLogId" parameterType="Long" resultMap="CompanyVoiceRoboticCallLogCallphoneResult">
         <include refid="selectCompanyVoiceRoboticCallLogCallphoneVo"/>
         where log_id = #{logId}
     </select>
-        
+
     <insert id="insertCompanyVoiceRoboticCallLogCallphone" parameterType="CompanyVoiceRoboticCallLogCallphone" useGeneratedKeys="true" keyProperty="logId">
         insert into company_voice_robotic_call_log_callphone
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -139,7 +139,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteCompanyVoiceRoboticCallLogCallphoneByLogIds" parameterType="String">
-        delete from company_voice_robotic_call_log_callphone where log_id in 
+        delete from company_voice_robotic_call_log_callphone where log_id in
         <foreach item="logId" collection="array" open="(" separator="," close=")">
             #{logId}
         </foreach>
@@ -253,6 +253,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectCompanyIdByBusinessId" resultType="Long">
         SELECT company_id FROM company_voice_robotic vr INNER JOIN company_voice_robotic_business rb ON vr.id = rb.robotic_id WHERE rb.id = 20 LIMIT 1
     </select>
+    <select id="countRoboticCallOutByCustomerIds" resultType="com.fs.company.vo.CustomerRoboticCallOutCountVO">
+        SELECT ce.user_id AS customerId, COUNT(1) AS callCount
+        FROM company_voice_robotic_call_log_callphone log
+        INNER JOIN company_voice_robotic_callees ce ON log.caller_id = ce.id
+        WHERE ce.user_id IS NOT NULL
+        AND ce.user_id &gt; 0
+        <if test="companyId != null">
+            AND log.company_id = #{companyId}
+        </if>
+        AND ce.user_id IN
+        <foreach collection="customerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        GROUP BY ce.user_id
+    </select>
+
+    <select id="countRoboticCallOutByCalleeIds" resultType="com.fs.company.vo.CalleeRoboticCallOutCountVO">
+        SELECT caller_id AS calleeId, COUNT(1) AS callCount
+        FROM company_voice_robotic_call_log_callphone
+        WHERE robotic_id = #{roboticId}
+        <if test="companyId != null">
+            AND company_id = #{companyId}
+        </if>
+        AND caller_id IN
+        <foreach collection="calleeIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        GROUP BY caller_id
+    </select>
 
 
-</mapper>
+</mapper>