Browse Source

cid新需求改动

lmx 2 tuần trước cách đây
mục cha
commit
bf76353d34

+ 1 - 1
fs-cid-workflow/src/main/resources/application.yml

@@ -5,7 +5,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
+    active: dev-test
 #    active: druid-hcl
 #    active: druid-sxjz
 #    active: druid-hdt

+ 35 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -12,6 +12,7 @@ import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.framework.security.LoginUser;
@@ -85,6 +86,10 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     public TableDataInfo groupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
     {
         startPage();
+        if(null == companyVoiceRoboticCallLogCallphone.getCompanyId()){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            companyVoiceRoboticCallLogCallphone.setCompanyId(loginUser.getUser().getCompanyId());
+        }
         List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
         return getDataTable(list);
     }
@@ -96,7 +101,9 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     @GetMapping("/count")
     public AjaxResult selectCompanyVoiceRoboticCallPhoneLogCount()
     {
-        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount(companyId);
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
@@ -166,6 +173,33 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
         return util.exportExcel(list, "调用日志_ai打电话数据");
     }
 
+    /**
+     * 导出详情外呼记录(任务名称、客户名称、解密手机号)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:exportPhone')")
+    @Log(title = "外呼记录详情手机号导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportDetailPhone")
+    public AjaxResult exportDetailPhone(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> list =
+                companyVoiceRoboticCallLogCallphoneService.listDecryptPhoneExport(companyVoiceRoboticCallLogCallphone);
+        ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> util =
+                new ExcelUtil<>(CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO.class);
+        return util.exportExcel(list, "外呼记录详情手机号");
+    }
+
+    /**
+     * 查看外呼记录解密手机号(无CRM客户时按记录解密)
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看外呼记录手机号", businessType = BusinessType.GRANT)
+    @GetMapping("/queryPhone/{logId}")
+    public AjaxResult queryCallLogPhone(@PathVariable("logId") Long logId)
+    {
+        String mobile = companyVoiceRoboticCallLogCallphoneService.getDecryptPhoneByLogId(logId);
+        return AjaxResult.success().put("mobile", mobile);
+    }
+
 //    /**
 //     * 导出调用日志_ai打电话列表
 //     */

+ 5 - 3
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -288,10 +288,12 @@ public class CrmCustomerController extends BaseController
     ){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
-        customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
         Boolean isReceive=false;
-        if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
-            isReceive=true;
+        if(customer !=null){
+            customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+            if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
+                isReceive=true;
+            }
         }
         return R.ok().put("customer",customer).put("isReceive",isReceive);
 

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

@@ -159,6 +159,14 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private Long maxCallTime;
 
+    /** 详情筛选-手机号(明文) */
+    @TableField(exist = false)
+    private String phone;
+
+    /** 详情筛选-加密手机号(匹配 callees.phone) */
+    @TableField(exist = false)
+    private String encryptedPhone;
+
     @TableField(exist = false)
     private String roboticName;
 

+ 4 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.crm.vo.CustomerCallStatVO;
@@ -84,11 +85,13 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(@Param("companyId") Long companyId);
 
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
     /**
      * 根据业务ID查询公司ID
      * @param businessId 业务ID (bes.id)

+ 9 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java

@@ -2,6 +2,7 @@ package com.fs.company.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
@@ -79,10 +80,12 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(Long companyId);
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
     List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
     /**
@@ -92,4 +95,9 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      * @return 影响行数
      */
     int markHandleFlag(Long logId);
+
+    /**
+     * 根据外呼记录ID获取解密后的手机号
+     */
+    String getDecryptPhoneByLogId(Long logId);
 }

+ 123 - 5
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -24,6 +24,8 @@ import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
@@ -365,17 +367,67 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                     companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                     Long answerTime = result.getCallEndTime();
                     companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                    String intention = result.getIntent();
+                    // 【当前启用】读取 cc_call_phone.intent,再转换为系统字典 intention 数值
+                    String intentRaw = StringUtils.isNotBlank(result.getIntent()) ? result.getIntent().trim() : null;
                     String intentf = null;
+                    final String intentionLabel = intentRaw;
                     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);
+
+                    // ========== 【历史保留】回滚时可注释上方 EasyCall 逻辑并取消下方对应注释 ==========
+                    // 方案A:仅使用 EasyCall intent 字段(改动前的本文件写法)
+//                    String intention = result.getIntent();
+//                    String intentf = null;
+//                    List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+//                    if (!isPositiveInteger(intention)) {
+//                        Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+//                        if (firstDict.isPresent()) {
+//                            SysDictData sysDictData = firstDict.get();
+//                            intentf = sysDictData.getDictValue();
+//                        }
+//                    }
+//                    companyVoiceRoboticCallLog.setIntention(intentf);
+
+                    // 方案B:自家 AI 根据 dialogue 计算意向度
+//                    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 intentf = null;
+//                    final String intentionLabel = intention;
+//                    List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+//                    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);
                     if(null != result.getValidTimeLen() && Integer.valueOf(0).compareTo(result.getValidTimeLen()) < 0){
                         BigDecimal divide = new BigDecimal(result.getValidTimeLen()).divide(new BigDecimal(1000), 0, RoundingMode.CEILING);
                         companyVoiceRoboticCallLog.setCallTime(divide.longValue());
@@ -521,14 +573,62 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     }
 
     @Override
-    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount() {
-        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount();
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(Long companyId) {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount(companyId);
     }
 
     @Override
     public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
         return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
     }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        prepareDetailPhoneQuery(companyVoiceRoboticCallLogCallphone);
+        List<CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO> rows = baseMapper.listDecryptPhoneExport(companyVoiceRoboticCallLogCallphone);
+        List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> exportList = new ArrayList<>();
+        if (rows == null || rows.isEmpty()) {
+            return exportList;
+        }
+        Map<String, String> intentionLabelMap = buildIntentionLabelMap();
+        for (CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO row : rows) {
+            CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO vo = new CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO();
+            vo.setRoboticName(row.getRoboticName());
+            vo.setUserName(row.getUserName());
+            vo.setIntention(resolveIntentionLabel(row.getIntention(), intentionLabelMap));
+            vo.setPhone(PhoneUtil.decryptPhone(row.getPhone()));
+            exportList.add(vo);
+        }
+        return exportList;
+    }
+
+    private Map<String, String> buildIntentionLabelMap() {
+        Map<String, String> map = new HashMap<>();
+        List<SysDictData> dictList = sysDictTypeService.selectDictDataByType("customer_intention_level");
+        if (dictList != null) {
+            for (SysDictData dict : dictList) {
+                map.put(dict.getDictValue(), dict.getDictLabel());
+            }
+        }
+        return map;
+    }
+
+    private String resolveIntentionLabel(String intention, Map<String, String> intentionLabelMap) {
+        if (StringUtils.isEmpty(intention)) {
+            return "";
+        }
+        String label = intentionLabelMap.get(intention);
+        if (StringUtils.isNotEmpty(label)) {
+            return label;
+        }
+        return intention;
+    }
+
+    private void prepareDetailPhoneQuery(CompanyVoiceRoboticCallLogCallphone query) {
+        if (StringUtils.isNotEmpty(query.getPhone())) {
+            query.setEncryptedPhone(PhoneUtil.encryptPhone(query.getPhone()));
+        }
+    }
     /**
      * 判断整数
      *
@@ -551,4 +651,22 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
         updateObj.setHandleFlag(1);
         return baseMapper.updateCompanyVoiceRoboticCallLogCallphone(updateObj);
     }
+
+    @Override
+    public String getDecryptPhoneByLogId(Long logId) {
+        CompanyVoiceRoboticCallLogCallphone log = selectCompanyVoiceRoboticCallLogCallphoneByLogId(logId);
+        if (log == null) {
+            return null;
+        }
+        if (log.getCallerId() != null) {
+            CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(log.getCallerId());
+            if (callees != null && StringUtils.isNotEmpty(callees.getPhone())) {
+                return PhoneUtil.decryptPhone(callees.getPhone());
+            }
+        }
+        if (StringUtils.isNotEmpty(log.getCallerNum())) {
+            return PhoneUtil.decryptPhone(log.getCallerNum());
+        }
+        return null;
+    }
 }

+ 85 - 55
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -57,12 +57,10 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 
 import java.lang.reflect.Method;
-import java.time.temporal.ChronoField;
 import java.util.*;
 import java.util.stream.Collectors;
 
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
-import static java.time.LocalTime.now;
 
 
 /**
@@ -898,50 +896,48 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 } catch (Exception e) {
                     log.error("callerResult4EasyCall 切换租户数据源失败: tenantId={}", TenantHelper.getTenantId(), e);
                 }
-//                // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
-//                if (StringUtils.isBlank(callPhoneRes.getIntent())) {
-//                    String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
-//                    Integer retryCount = redisCache2.getCacheObject(retryKey);
-//                    if (retryCount == null) {
-//                        retryCount = 0;
-//                    }
-//                    if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
-//                        redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
-//                        log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
-//                        doRetryCallerResult4EasyCall(result, retryCount + 1);
-//                    } else {
-//                        // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
-//                        log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
-//                        redisCache2.deleteObject(retryKey);
-//                        doHandleEasyCallResult(callPhoneRes);
-//                    }
-//                    return;
-//                }
-//                // intent 已有值,直接正常处理
-//                redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
-//                doHandleEasyCallResult(callPhoneRes);
-                // dialogue(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
-                if (isDialogueEmpty(callPhoneRes.getDialogue()) && !"未接通".equals(callPhoneRes.getIntent())) {
-                    String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+                // 【当前启用】cc_call_phone.intent 由 EasyCall 异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+                if (StringUtils.isBlank(resolveCcCallPhoneIntent(callPhoneRes))) {
+                    String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
                     Integer retryCount = redisCache2.getCacheObject(retryKey);
                     if (retryCount == null) {
                         retryCount = 0;
                     }
-                    if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                    if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
                         redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
-                        log.info("easyCall外呼回调dialogue对话内容暂未写入,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
-                        doRetryDialogue4EasyCall(result, retryCount + 1);
+                        log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                        doRetryCallerResult4EasyCall(result, retryCount + 1);
                     } else {
-                        // 超过最大重试次数,以 dialogue 为空兜底继续处理
-                        log.warn("easyCall外呼回调dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                        log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
                         redisCache2.deleteObject(retryKey);
                         doHandleEasyCallResult(callPhoneRes);
                     }
                     return;
                 }
-                // dialogue 已有值,直接正常处理
-                redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+                redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
                 doHandleEasyCallResult(callPhoneRes);
+
+                // ========== 【历史保留-自家AI】根据 dialogue 等待后走 AI 意向度,回滚时注释上方 intent 重试并取消下方注释 ==========
+//                // 当前:根据对话内容同步调用自家 AI 计算意向度,不依赖第三方 intent
+//                if (isDialogueEmpty(callPhoneRes.getDialogue()) && !"未接通".equals(callPhoneRes.getIntent())) {
+//                    String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+//                    Integer retryCount = redisCache2.getCacheObject(retryKey);
+//                    if (retryCount == null) {
+//                        retryCount = 0;
+//                    }
+//                    if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+//                        redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+//                        log.info("easyCall外呼回调dialogue对话内容暂未写入,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+//                        doRetryDialogue4EasyCall(result, retryCount + 1);
+//                    } else {
+//                        log.warn("easyCall外呼回调dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+//                        redisCache2.deleteObject(retryKey);
+//                        doHandleEasyCallResult(callPhoneRes);
+//                    }
+//                    return;
+//                }
+//                redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+//                doHandleEasyCallResult(callPhoneRes);
             } catch (Exception e) {
                 throw new RuntimeException(e);
             } finally {
@@ -1033,8 +1029,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             log.error("easyCall intent重试时仍未查询到外呼结果, uuid={}", result.getUuid());
             return;
         }
-        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
-            // intent 仍为空,继续判断是否还有剩余重试次数
+        if (StringUtils.isBlank(resolveCcCallPhoneIntent(callPhoneRes))) {
+            // cc_call_phone.intent 仍为空,继续判断是否还有剩余重试次数
             String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
             Integer retryCount = redisCache2.getCacheObject(retryKey);
             if (retryCount == null) {
@@ -1052,7 +1048,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         // intent 已评估完成,正常处理
-        log.info("easyCall intent重试第{}次成功获取到意向度={},uuid={}", currentRetry, callPhoneRes.getIntent(), result.getUuid());
+        log.info("easyCall intent重试第{}次成功获取到意向度={},uuid={}", currentRetry, resolveCcCallPhoneIntent(callPhoneRes), result.getUuid());
         redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
         doHandleEasyCallResult(callPhoneRes);
     }
@@ -1151,27 +1147,37 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 //                    log.error("pushDialogContent4EasyCall 切换租户数据源失败: tenantId={}", TenantHelper.getTenantId(), e);
 //                }
 //            }
-            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)
-                    );
-                    log.info("【验证】意向度结果={}, uuid={}", intentionDegree, callPhoneRes.getUuid());
-                    intention = getIntention(intentionDegree);
-                } catch (Exception e) {
-                    log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
-                }
-            }
-            // 2) 最终兜底:意向未知
-//            String intention = getIntention(callPhoneRes.getIntent());
+            // 【当前启用】读取 cc_call_phone.intent,再转换为系统字典 intention 数值
+            String intentRaw = resolveCcCallPhoneIntent(callPhoneRes);
+            String intention = convertEasyCallIntent(intentRaw);
+            log.info("easyCall意向度来源=EasyCall平台, uuid={}, intent(raw)={}, intention(converted)={}",
+                    callPhoneRes.getUuid(), intentRaw, intention);
             if (StringUtils.isEmpty(intention)) {
                 intention = "0";
             }
+
+            // ========== 【历史保留-自家AI】回滚时注释上方 EasyCall 逻辑并取消下方注释 ==========
+//            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(),
+//                            java.time.LocalTime.now().getLong(java.time.temporal.ChronoField.MILLI_OF_SECOND)
+//                    );
+//                    log.info("【验证】意向度结果={}, uuid={}", intentionDegree, callPhoneRes.getUuid());
+//                    intention = getIntention(intentionDegree);
+//                } catch (Exception e) {
+//                    log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
+//                }
+//            }
+//            if (StringUtils.isEmpty(intention)) {
+//                intention = "0";
+//            }
+//            // 历史第三方值(仅 intent 字段,未走 AI 时的写法)
+//            // String intention = getIntention(callPhoneRes.getIntent());
             CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
             callee.setUuid(callPhoneRes.getUuid());
             callee.setIntention(intention);
@@ -1231,6 +1237,30 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         return collect.isEmpty() ? null : collect.get(0).getDictValue();
     }
 
+    /**
+     * 读取 EasyCall cc_call_phone 表原始字段 intent(非本系统 intention 字典值)
+     */
+    private String resolveCcCallPhoneIntent(EasyCallCallPhoneVO callPhoneRes) {
+        if (callPhoneRes == null || StringUtils.isBlank(callPhoneRes.getIntent())) {
+            return null;
+        }
+        return callPhoneRes.getIntent().trim();
+    }
+
+    /**
+     * 将 cc_call_phone.intent 原始值转为系统字典数值 intention(customer_intention_level.dict_value)
+     */
+    private String convertEasyCallIntent(String intentRaw) {
+        if (StringUtils.isBlank(intentRaw)) {
+            return null;
+        }
+        String t = intentRaw.trim();
+        if (t.matches("^\\d+$")) {
+            return t;
+        }
+        return getIntention(t);
+    }
+
     public void pushBilling(PushIIntentionResult result) {
         Notify notify = result.getNotify();
         CompanyVoiceRoboticCallees callee = getResultCalleeInfo(notify);

+ 23 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO.java

@@ -0,0 +1,23 @@
+package com.fs.company.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+/**
+ * 外呼记录详情导出(解密手机号)
+ */
+@Data
+public class CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO {
+
+    @Excel(name = "任务名称")
+    private String roboticName;
+
+    @Excel(name = "客户名称")
+    private String userName;
+
+    @Excel(name = "客户类型")
+    private String intention;
+
+    @Excel(name = "手机号")
+    private String phone;
+}

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

@@ -0,0 +1,19 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * 外呼记录详情导出查询中间对象
+ */
+@Data
+public class CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO {
+
+    private String roboticName;
+
+    private String userName;
+
+    /** 客户类型字典值(存库为数字) */
+    private String intention;
+
+    private String phone;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneVO.java

@@ -24,6 +24,9 @@ public class CompanyVoiceRoboticCallLogCallPhoneVO {
     @Excel(name = "caller_id")
     private Long callerId;
 
+    /** 客户ID(callees.user_id) */
+    private Long customerId;
+
     /** 记录调用时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @Excel(name = "记录调用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")

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

@@ -155,7 +155,7 @@ public class EasyCallCallPhoneVO {
     private String emptyNumberDetectionText;
 
     /**
-     * 客户意向
+     * 客户意向(EasyCall cc_call_phone 表原始字段 intent,多为等级字母或文案,需经字典转换为系统 intention)
      */
     private String intent;
 

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

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

+ 47 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -234,6 +234,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_voice_robotic cvr on cvr.id = t1.robotic_id
         <where>
             <if test="roboticId != null">and robotic_id = #{roboticId}</if>
+            <if test="companyId != null"> and cvr.company_id = #{companyId}</if>
         </where>
         group by robotic_id
         order by t1.run_time desc
@@ -246,6 +247,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             sum(case when status = 2 and run_time &gt;= CURDATE() and run_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY) then 1 else 0 end) as todaySuccessCount
         from company_voice_robotic_call_log_callphone cp
             inner join company_voice_robotic cvr on cvr.id = cp.robotic_id
+        where cvr.company_id = #{companyId}
     </select>
 
 
@@ -254,8 +256,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         t1.*,
         t2.company_name,
         t3.nick_name as companyUserName,
-        cvr.name as robotic_name
+        cvr.name as robotic_name,
+        ce.user_id as customer_id
         FROM company_voice_robotic_call_log_callphone t1
+        left join company_voice_robotic_callees ce on ce.id = t1.caller_id
         left join company t2 on t1.company_id = t2.company_id
         left join company_user t3 on t3.user_id = t1.company_user_id
         left join company_voice_robotic cvr on cvr.id = t1.robotic_id
@@ -289,6 +293,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     </select>
 
+    <select id="listDecryptPhoneExport" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone">
+        SELECT
+        cvr.name as robotic_name,
+        ce.user_name as user_name,
+        t1.intention as intention,
+        ce.phone as phone
+        FROM company_voice_robotic_call_log_callphone t1
+        left join company_voice_robotic_callees ce on ce.id = t1.caller_id
+        left join company_voice_robotic cvr on cvr.id = t1.robotic_id
+        where 1=1
+        <if test="roboticId != null">and t1.robotic_id = #{roboticId}</if>
+        <if test="callerId != null">and t1.caller_id = #{callerId}</if>
+        <if test="callerIds != null and callerIds.size() > 0">
+            AND t1.caller_id IN
+            <foreach collection='callerIds' item='item' open='(' separator=',' close=')'>
+                #{item}
+            </foreach>
+        </if>
+        <if test="callerNum != null and callerNum != ''">
+            and t1.caller_num like concat('%', #{callerNum}, '%')
+        </if>
+        <if test="encryptedPhone != null and encryptedPhone != ''">
+            and ce.phone = #{encryptedPhone}
+        </if>
+        <if test="intention != null and intention != ''">
+            and t1.intention = #{intention}
+        </if>
+        <if test="isConnected != null and isConnected == 1">
+            and t1.call_time &gt; 0
+        </if>
+        <if test="isConnected != null and isConnected == 0">
+            and (t1.call_time is null or t1.call_time = 0)
+        </if>
+        <if test="minCallTime != null">
+            and t1.call_time &gt;= #{minCallTime}
+        </if>
+        <if test="maxCallTime != null">
+            and t1.call_time &lt;= #{maxCallTime}
+        </if>
+        order by t1.run_time desc
+    </select>
+
     <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>