Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

yys 1 неделя назад
Родитель
Сommit
a842546d30
26 измененных файлов с 511 добавлено и 47 удалено
  1. 9 23
      fs-company-app/src/main/java/com/fs/app/controller/aiSipCall/AiSipCallController.java
  2. 14 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  3. 2 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  4. 16 0
      fs-company/src/main/java/com/fs/company/controller/crm/CustomerAllController.java
  5. 1 1
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  6. 2 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  7. 11 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  8. 12 0
      fs-service/src/main/java/com/fs/company/mapper/CrmCustomerCallLogMapper.java
  9. 25 0
      fs-service/src/main/java/com/fs/company/param/AppendCustomersParam.java
  10. 10 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  11. 191 8
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  12. 2 0
      fs-service/src/main/java/com/fs/company/vo/CallContentVO.java
  13. 5 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  14. 12 6
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  15. 12 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java
  16. 6 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerService.java
  17. 60 1
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java
  18. 24 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListVO.java
  19. 26 0
      fs-service/src/main/java/com/fs/crm/vo/CustomerCallStatVO.java
  20. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java
  21. 5 2
      fs-service/src/main/resources/db/tenant-initTable.sql
  22. 1 1
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml
  23. 2 1
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecLogMapper.xml
  24. 3 0
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml
  25. 31 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml
  26. 28 0
      fs-service/src/main/resources/mapper/company/CrmCustomerCallLogMapper.xml

+ 9 - 23
fs-company-app/src/main/java/com/fs/app/controller/aiSipCall/AiSipCallController.java

@@ -93,8 +93,6 @@ public class AiSipCallController extends AppBaseController {
     @Autowired
     private CrmCustomerMapper crmCustomerMapper;
 
-    private final String AUDIO_BASE_URL = "http://129.28.164.235:8899";
-
     /**
      * 是否使用自有线路(迁移自 his_java sip.call.myGateway,原放在 AiSipCallUserServiceImpl)。
      * <p>SaaS 老 ServiceImpl 已被精简没有兜底逻辑,故在 Controller 层注入并保持 his_java 行为一致。
@@ -102,21 +100,15 @@ public class AiSipCallController extends AppBaseController {
     @Value("${sip.call.myGateway:false}")
     private boolean isMyGateway;
 
-    /**
-     * 手动外呼网关前缀(迁移自 his_java sip.call.manualGatewayPrefix)
-     */
+    /** 手动外呼网关前缀(迁移自 his_java sip.call.manualGatewayPrefix) */
     @Value("${sip.call.manualGatewayPrefix:weizhi}")
     private String manualGatewayPrefix;
 
-    /**
-     * 公共线路网关前缀(迁移自 his_java sip.call.publicGatewayPrefix)
-     */
+    /** 公共线路网关前缀(迁移自 his_java sip.call.publicGatewayPrefix) */
     @Value("${sip.call.publicGatewayPrefix:outbound}")
     private String publicGatewayPrefix;
 
-    /**
-     * 加密手机号末尾随机串长度(与前端约定,与 his_java 的 RandomUtil.generateRandomCode 等价)
-     */
+    /** 加密手机号末尾随机串长度(与前端约定,与 his_java 的 RandomUtil.generateRandomCode 等价) */
     private static final int RANDOM_TAIL_LEN = 6;
 
     private static final char[] RANDOM_CHARS =
@@ -124,11 +116,11 @@ public class AiSipCallController extends AppBaseController {
 
     private static final SecureRandom RANDOM = new SecureRandom();
 
-    /**
-     * XOR加密公钥(与 his_java 的 PhoneUtil.PUBLIC_KEY_STR 保持一致,避免污染 SaaS 原有 PhoneUtil)
-     */
+    /** XOR加密公钥(与 his_java 的 PhoneUtil.PUBLIC_KEY_STR 保持一致,避免污染 SaaS 原有 PhoneUtil) */
     private static final String XOR_KEY = "ylrz112233";
 
+    private static final String SPLICE_ADD = "http://129.28.164.235:8899/recordings/files?filename=";
+
     /**
      * 查询当前登录销售绑定的SIP分机账号
      * <p>前端 softPhone.vue 启动时调用,用于初始化JsSIP UA配置(user/domain/extPass)。
@@ -308,13 +300,13 @@ public class AiSipCallController extends AppBaseController {
                     try {
                         String decrypted = PhoneUtil.decryptPhone(c.getMobile());
                         c.setMobile(decrypted.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-                        if (Integer.valueOf(1).equals(c.getCanDecrypt())) {
-                            c.setDecryptedMobile(decrypted);
-                        }
                     } catch (Exception e) {
                         log.warn("[aiSipCall] 解密手机号失败, customerId={}", c.getCustomerId());
                     }
                 }
+                if(StringUtils.isNotBlank(c.getEffectiveRecordPath())){
+                    c.setEffectiveRecordPath(SPLICE_ADD + c.getEffectiveRecordPath());
+                }
             }
         }
         return getDataTable(list);
@@ -334,12 +326,6 @@ public class AiSipCallController extends AppBaseController {
         query.setCustomerId(customerId);
         PageHelper.startPage(pageNum, pageSize);
         List<AiSipCallOutboundCdr> list = aiSipCallOutboundCdrService.selectAiSipCallOutboundCdrList(query);
-        //拼接音频文件
-        list.forEach(data -> {
-            if (StringUtils.isNotBlank(data.getWavfile())) {
-                data.setWavfile(AUDIO_BASE_URL + data.getWavfile());
-            }
-        });
         return getDataTable(list);
     }
 

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

@@ -23,6 +23,7 @@ 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.param.AppendCustomersParam;
 import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
@@ -346,12 +347,13 @@ public class CompanyVoiceRoboticController extends BaseController
                             @RequestParam(defaultValue = "10") Integer pageSize,
                             @RequestParam(required = false) String customerName,
                             @RequestParam(required = false) String customerPhone,
+                            @RequestParam(required = false) String encryptPhone,
                             @RequestParam Boolean onlyCallNode) {
         if (roboticId == null) {
             return R.error("任务ID不能为空");
         }
         return R.ok(companyVoiceRoboticService.getExecRecords(roboticId, pageNum, pageSize, customerName,
-                customerPhone,onlyCallNode));
+                customerPhone,onlyCallNode,encryptPhone));
     }
 
     @GetMapping("/getCurrentCompanyId")
@@ -398,4 +400,15 @@ public class CompanyVoiceRoboticController extends BaseController
         TenantHelper.setTenantId(SecurityUtils.getTenantId());
         return companyVoiceRoboticService.pauseRoboticActive(param);
     }
+
+    /**
+     * 追加客户到运行中的普通任务
+     */
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:edit')")
+    @Log(title = "追加客户到运行中任务", businessType = BusinessType.INSERT)
+    @PostMapping("/appendCustomers")
+    public R appendCustomers(@RequestBody AppendCustomersParam param){
+        TenantHelper.setTenantId(SecurityUtils.getTenantId());
+        return companyVoiceRoboticService.appendCustomersToRunningTask(param.getTaskId(), param.getCustomerIds());
+    }
 }

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

@@ -133,6 +133,8 @@ public class CrmCustomerController extends BaseController
             crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
         }
         List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
+        // 回填今日/累计 × 手动/AI 外呼次数(接通/总数),仅本接口调用
+        crmCustomerService.fillCallStats(list);
         if (list != null) {
             for (CrmCustomerListVO vo : list) {
                 if(vo.getMobile()!=null){

+ 16 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CustomerAllController.java

@@ -1,8 +1,10 @@
 package com.fs.company.controller.crm;
 
 import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.utils.ServletUtils;
+import com.fs.crm.param.CrmCustomeRecoverParam;
 import com.fs.crm.param.CrmCustomerAllListQueryParam;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.vo.CrmCustomerAllListQueryVO;
@@ -13,6 +15,8 @@ import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -50,4 +54,16 @@ public class CustomerAllController extends BaseController {
         }
         return getDataTable(list);
     }
+
+    @ApiOperation("回收公海")
+    @PreAuthorize("@ss.hasPermi('crm:customer:recover')")
+    @PostMapping("/recover")
+    public R recover(@RequestBody CrmCustomeRecoverParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String operName = loginUser.getUsername();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        return crmCustomerService.recover(param, operName);
+    }
 }

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -147,7 +147,7 @@ public class SendMsg {
     }
 
 
-//    @Scheduled(fixedRate = 50000) // 每50秒执行一次
+    @Scheduled(fixedRate = 50000) // 每50秒执行一次
     public void refulsQwUserList() {
         qwUserList.clear();
     }

+ 2 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java

@@ -109,7 +109,8 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
             @Param("roboticId") Long roboticId,
             @Param("customerName") String customerName,
             @Param("customerPhone") String customerPhone,
-            @Param(("onlyCallNode")) Boolean onlyCallNode
+            @Param(("onlyCallNode")) Boolean onlyCallNode,
+            @Param("encryptPhone") String encryptPhone
     );
 
     WxContact selectWxContectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);

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

@@ -5,6 +5,7 @@ import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.crm.vo.CustomerCallStatVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -96,4 +97,14 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
     Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
 
     List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    /**
+     * 按 customer_id(= callees.user_id)批量统计今日 AI 外呼总数与接通数
+     */
+    List<CustomerCallStatVO> selectAiCallStatToday(@Param("customerIds") List<Long> customerIds);
+
+    /**
+     * 按 customer_id(= callees.user_id)批量统计累计 AI 外呼总数与接通数
+     */
+    List<CustomerCallStatVO> selectAiCallStatTotal(@Param("customerIds") List<Long> customerIds);
 }

+ 12 - 0
fs-service/src/main/java/com/fs/company/mapper/CrmCustomerCallLogMapper.java

@@ -2,6 +2,8 @@ package com.fs.company.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CrmCustomerCallLog;
+import com.fs.crm.vo.CustomerCallStatVO;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
@@ -18,4 +20,14 @@ public interface CrmCustomerCallLogMapper extends BaseMapper<CrmCustomerCallLog>
     List<CrmCustomerCallLog> selectCrmCustomerCallLogList(CrmCustomerCallLog crmCustomerCallLog);
 
     Long selectSumBillingMinute(CrmCustomerCallLog crmCustomerCallLog);
+
+    /**
+     * 按 customer_id 批量统计今日手动外呼记录总数与接通数
+     */
+    List<CustomerCallStatVO> selectManualCallStatToday(@Param("customerIds") List<Long> customerIds);
+
+    /**
+     * 按 customer_id 批量统计累计手动外呼记录总数与接通数
+     */
+    List<CustomerCallStatVO> selectManualCallStatTotal(@Param("customerIds") List<Long> customerIds);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/company/param/AppendCustomersParam.java

@@ -0,0 +1,25 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/5/26
+ * @description 追加客户到运行中任务的请求参数
+ */
+@Data
+public class AppendCustomersParam {
+
+    /**
+     * 任务id
+     */
+    private Long taskId;
+
+    /**
+     * 要追加的CRM客户ID列表
+     */
+    private List<Long> customerIds;
+
+}

+ 10 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -103,7 +103,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      * @param roboticId 任务ID
      * @return 执行记录列表
      */
-    Map<String, Object> getExecRecords(Long roboticId, Integer pageNum, Integer pageSize,String customerName,String customerPhone, Boolean onlyCallNode);
+    Map<String, Object> getExecRecords(Long roboticId, Integer pageNum, Integer pageSize,String customerName,String customerPhone, Boolean onlyCallNode,String encryptPhone);
 
     void finishAddWxByCallees(Set<Long> roboticIds);
 
@@ -125,4 +125,13 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
      * @return true=暂停中 false=非暂停
      */
     boolean isTaskPaused(Long taskId);
+
+    /**
+     * 追加客户到运行中的普通任务
+     *
+     * @param taskId      任务ID
+     * @param customerIds 要追加的CRM客户ID列表
+     * @return 追加结果(成功数、重复客户信息)
+     */
+    R appendCustomersToRunningTask(Long taskId, List<Long> customerIds);
 }

+ 191 - 8
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -39,6 +39,7 @@ import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import com.fs.enums.TaskTypeEnum;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
@@ -1764,11 +1765,14 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                                               Integer pageSize,
                                               String customerName,
                                               String customerPhone,
-                                              Boolean onlyCallNode) {
+                                              Boolean onlyCallNode,
+                                              String encryptPhone) {
         //分页查询主数据
         PageHelper.startPage(pageNum, pageSize);
-
-        List<WorkflowExecRecordVo> records = companyAiWorkflowExecMapper.selectExecRecordsByRoboticId(roboticId, customerName, customerPhone, onlyCallNode);
+        if(StringUtils.isNotBlank(encryptPhone)){
+            encryptPhone = PhoneUtil.encryptPhone(encryptPhone);
+        }
+        List<WorkflowExecRecordVo> records = companyAiWorkflowExecMapper.selectExecRecordsByRoboticId(roboticId, customerName, customerPhone, onlyCallNode,encryptPhone);
 
         PageInfo<WorkflowExecRecordVo> pageInfo = new PageInfo<>(records);
 
@@ -1909,7 +1913,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return new ArrayList<>();
         }
         List<CompanyAiWorkflowExecLog> callLogs = logs.stream().filter(a -> "外呼".equals(a.getNodeName())).collect(Collectors.toList());
-        HashMap<Long,String> callContentMap;
+        HashMap<Long,CallContentVO> callContentMap;
         if (null != callLogs && !callLogs.isEmpty()) {
             callContentMap = selectCallContentByCallLogs(callLogs);
         } else {
@@ -1929,7 +1933,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             vo.setDuration(log.getDuration());
             vo.setErrorMessage(log.getErrorMessage());
             vo.setOutputData(log.getOutputData());
-            vo.setNodeContentList(callContentMap.get(log.getId()));
+            CallContentVO callContentVO = callContentMap.get(log.getId());
+            if (callContentVO != null) {
+                vo.setNodeContentList(callContentVO.getCallContent());
+                vo.setNodeRecordPath(callContentVO.getRecordPath());
+            }
             return vo;
         }).collect(Collectors.toList());
     }
@@ -1939,15 +1947,15 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
      * @param callLogs
      * @return
      */
-    public  HashMap<Long,String> selectCallContentByCallLogs(List<CompanyAiWorkflowExecLog> callLogs){
+    public  HashMap<Long,CallContentVO> selectCallContentByCallLogs(List<CompanyAiWorkflowExecLog> callLogs){
         List<Long> ids = callLogs.stream().map(a -> a.getId()).collect(Collectors.toList());
         if(null == ids || ids.isEmpty()){
             return new HashMap<>();
         }
         List<CallContentVO> callContentVOS = companyAiWorkflowExecLogMapper.selectCallContent(ids);
         if(null != callContentVOS && !callContentVOS.isEmpty()){
-            HashMap<Long,String> map = new HashMap<>();
-            callContentVOS.forEach(a -> map.put(a.getLogId(),a.getCallContent()));
+            HashMap<Long,CallContentVO> map = new HashMap<>();
+            callContentVOS.forEach(a -> map.put(a.getLogId(), a));
             return map;
         }
         else{
@@ -2228,4 +2236,179 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
         return false;
     }
+
+    @Override
+    public R appendCustomersToRunningTask(Long taskId, List<Long> customerIds) {
+        // 1. 校验参数
+        if (taskId == null || customerIds == null || customerIds.isEmpty()) {
+            return R.error("参数不能为空");
+        }
+
+        // 2. 校验任务
+        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(taskId);
+        if (robotic == null) {
+            return R.error("任务不存在: " + taskId);
+        }
+        if (!robotic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())) {
+            return R.error("仅普通任务支持追加客户");
+        }
+        if (!Integer.valueOf(1).equals(robotic.getTaskStatus())) {
+            return R.error("任务不在执行中状态,无法追加客户");
+        }
+        if (robotic.getCompanyAiWorkflowId() == null) {
+            return R.error("任务未配置工作流");
+        }
+
+        // 3. 查询当前任务已有的callees的userId集合,过滤重复
+        List<CompanyVoiceRoboticCallees> existingCallees = companyVoiceRoboticCalleesMapper.selectByRoboticId(taskId);
+        Set<Long> existingUserIds = existingCallees.stream()
+                .map(CompanyVoiceRoboticCallees::getUserId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        // 分离重复和非重复客户
+        List<Long> duplicateIds = customerIds.stream()
+                .filter(existingUserIds::contains)
+                .collect(Collectors.toList());
+        List<Long> newCustomerIds = customerIds.stream()
+                .filter(id -> !existingUserIds.contains(id))
+                .collect(Collectors.toList());
+
+        if (newCustomerIds.isEmpty()) {
+            String dupNames = getDuplicateCustomerNames(duplicateIds);
+            return R.error("所选客户已存在于任务中" + (dupNames.isEmpty() ? "" : ":" + dupNames));
+        }
+
+        // 4. 批量查询CRM客户信息
+        List<CrmCustomer> crmCustomers = crmCustomerService.selectCrmCustomerListByIds(
+                newCustomerIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
+        Map<Long, CrmCustomer> customerMap = crmCustomers.stream()
+                .collect(Collectors.toMap(CrmCustomer::getCustomerId, c -> c, (a, b) -> a));
+
+        // 5. 判断是否需要加微分配
+        boolean hasAddWxNode = workflowHasAddWxNode(robotic.getCompanyAiWorkflowId());
+
+        int successCount = 0;
+        List<String> errorMessages = new ArrayList<>();
+
+        for (Long customerId : newCustomerIds) {
+            CrmCustomer crmCustomer = customerMap.get(customerId);
+            if (crmCustomer == null) {
+                errorMessages.add("客户不存在: " + customerId);
+                continue;
+            }
+            try {
+                // 5.1 创建CompanyWxClient记录
+                CompanyWxClient client = new CompanyWxClient();
+                client.setRoboticId(taskId);
+                client.setCustomerId(customerId);
+                client.setIsWeCom(robotic.getIsWeCom());
+                companyWxClientServiceImpl.insertCompanyWxClient(client);
+
+                // 5.2 创建CompanyVoiceRoboticCallees记录
+                CompanyVoiceRoboticCallees callee = new CompanyVoiceRoboticCallees();
+                callee.setUserId(crmCustomer.getCustomerId());
+                callee.setUserName(crmCustomer.getCustomerName());
+                callee.setPhone(crmCustomer.getMobile());
+                callee.setRoboticId(robotic.getId());
+                callee.setResult(0);
+                callee.setTaskFlow(robotic.getTaskFlow());
+                callee.setRunTaskFlow(robotic.getRunTaskFlow());
+                callee.setIsWeCom(robotic.getIsWeCom());
+                companyVoiceRoboticCalleesService.save(callee);
+
+                // 5.3 加微分配
+                if (hasAddWxNode && Integer.valueOf(0).equals(robotic.getAddType())) {
+                    allocateWx4SceneTask(robotic, client.getId());
+                } else if (hasAddWxNode && Integer.valueOf(1).equals(robotic.getAddType())) {
+                    String intention = crmCustomer.getIntention();
+                    String queryIntention = intention;
+                    if (!isPositiveInteger(intention)) {
+                        List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+                        Optional<SysDictData> firstDict = customerIntentionLevel.stream()
+                                .filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                        if (firstDict.isPresent()) {
+                            queryIntention = firstDict.get().getDictValue();
+                        }
+                    }
+                    List<CompanyVoiceRoboticWx> roboticWxList = companyVoiceRoboticWxMapper.selectByRoboticId(taskId, queryIntention);
+                    List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(PubFun.listToNewList(roboticWxList, CompanyVoiceRoboticWx::getAccountId)));
+                    Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountList, CompanyWxAccount::getId);
+                    roboticWxList.forEach(e -> e.setAccount(accountMap.get(e.getAccountId())));
+                    CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callee.getRoboticId()).eq("customer_id", callee.getUserId()));
+                    if (companyWxClient == null) {
+                        companyWxClient = new CompanyWxClient();
+                    }
+                    companyWxClient.setRoboticId(callee.getRoboticId());
+                    companyWxClient.setNickName(callee.getUserName());
+                    companyWxClient.setPhone(callee.getPhone());
+                    companyWxClient.setCustomerId(callee.getUserId());
+                    companyWxClient.setIntention(intention);
+                    bindCompany(companyWxClient, roboticWxList);
+                    companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
+                }
+
+                // 5.4 创建CompanyVoiceRoboticBusiness记录
+                CompanyVoiceRoboticBusiness business = buildTaskBussiness4SceneTask(robotic, callee);
+
+                // 5.5 初始化工作流实例
+                Map<String, Object> inputVariables = new HashMap<>();
+                inputVariables.put("roboticId", robotic.getId());
+                inputVariables.put("businessId", business.getId());
+                inputVariables.put("cidGroupNo", robotic.getCidGroupNo());
+                inputVariables.put("runtimeRangeStart", robotic.getRuntimeRangeStart());
+                inputVariables.put("runtimeRangeEnd", robotic.getRuntimeRangeEnd());
+                ExecutionResult initResult = companyWorkflowEngine.initialize(robotic.getCompanyAiWorkflowId(), inputVariables);
+                if (!initResult.isSuccess()) {
+                    errorMessages.add("客户" + crmCustomer.getCustomerName() + "工作流初始化失败: " + initResult.getErrorMessage());
+                    continue;
+                }
+
+                successCount++;
+            } catch (Exception e) {
+                log.error("追加客户失败 - taskId: {}, customerId: {}", taskId, customerId, e);
+                errorMessages.add("客户" + crmCustomer.getCustomerName() + "追加失败: " + e.getMessage());
+            }
+        }
+
+        // 6. 为新增的callees生成AI标签信息
+        if (successCount > 0) {
+            try {
+                List<CompanyVoiceRoboticCallees> allCallees = companyVoiceRoboticCalleesMapper.selectByRoboticId(taskId);
+                List<CompanyWxClient> companyWxClients = companyWxClientMapper.selectListByRoboticId(taskId);
+                Map<String, CompanyWxClient> clientMp = companyWxClients.stream()
+                        .collect(Collectors.toMap(e -> e.getRoboticId() + "-" + e.getCustomerId(), e -> e, (a, b) -> a));
+                asyncCalleeProcessorService.generateCustomerInfo(allCallees, clientMp, robotic);
+            } catch (Exception e) {
+                log.warn("追加客户后生成AI标签信息异常 - taskId: {}", taskId, e);
+            }
+        }
+
+        // 7. 构建返回结果
+        Map<String, Object> resultData = new HashMap<>();
+        resultData.put("successCount", successCount);
+        if (!duplicateIds.isEmpty()) {
+            resultData.put("duplicateCustomerNames", getDuplicateCustomerNames(duplicateIds));
+        }
+        if (!errorMessages.isEmpty()) {
+            resultData.put("errorMessages", errorMessages);
+        }
+        return R.ok(resultData);
+    }
+
+    /**
+     * 根据客户ID列表获取重复客户名称
+     */
+    private String getDuplicateCustomerNames(List<Long> customerIds) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return "";
+        }
+        try {
+            List<CrmCustomer> customers = crmCustomerService.selectCrmCustomerListByIds(
+                    customerIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
+            return customers.stream().map(CrmCustomer::getCustomerName).filter(Objects::nonNull).collect(Collectors.joining("、"));
+        } catch (Exception e) {
+            return customerIds.stream().map(String::valueOf).collect(Collectors.joining("、"));
+        }
+    }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/vo/CallContentVO.java

@@ -13,4 +13,6 @@ public class CallContentVO {
     private Long logId;
 
     private String callContent;
+
+    private String recordPath;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java

@@ -200,5 +200,10 @@ public class WorkflowExecRecordVo {
          * 外呼节点的对话记录
          */
         private String nodeContentList;
+
+        /**
+         * 外呼节点的录音地址
+         */
+        private String nodeRecordPath;
     }
 }

+ 12 - 6
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -244,6 +244,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "<if test = 'maps.tags != null and  maps.tags !=\"\"    '> " +
             "and find_in_set(#{maps.tags}, c.tags)   " +
             "</if>" +
+            "<if test = 'maps.customTag != null and maps.customTag != \"\"   '> " +
+            "and c.tags like CONCAT('%',#{maps.customTag},'%') " +
+            "</if>" +
             "<if test = 'maps.status != null      '> " +
             "and c.status =#{maps.status} " +
             "</if>" +
@@ -283,6 +286,12 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "<if test = 'maps.deptId != null  and maps.deptId != 0 '> " +
             "AND (c.dept_id = #{maps.deptId} OR c.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors) )) " +
             "</if>" +
+            "<if test = 'maps.roboticId != null and maps.taskCustomerFilter != null and maps.taskCustomerFilter == \"notInTask\"   '> " +
+            "and c.customer_id NOT IN (SELECT ce.user_id FROM company_voice_robotic_callees ce WHERE ce.robotic_id = #{maps.roboticId}) " +
+            "</if>" +
+            "<if test = 'maps.roboticId != null and maps.taskCustomerFilter != null and maps.taskCustomerFilter == \"inTask\"   '> " +
+            "and c.customer_id IN (SELECT ce.user_id FROM company_voice_robotic_callees ce WHERE ce.robotic_id = #{maps.roboticId}) " +
+            "</if>" +
             "${maps.params.dataScope}"+
             " order by c.customer_id desc "+
             "</script>"})
@@ -1017,17 +1026,14 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
      * callStatus: 0-全部, 1-已解密(有外呼记录), 2-未解密(无外呼记录)
      */
     @Select({"<script> " +
-            "SELECT c.*, CASE WHEN cdr.customer_id IS NOT NULL THEN 1 ELSE 0 END AS canDecrypt " +
-            "FROM crm_customer c " +
-            "LEFT JOIN (SELECT DISTINCT customer_id FROM ai_sip_call_outbound_cdr WHERE status = 0) cdr ON cdr.customer_id = c.customer_id " +
-            "WHERE c.effective_customer = 1 " +
+            "SELECT c.* FROM crm_customer c WHERE c.effective_customer = 1 " +
             "<if test='receiveUserId != null'> AND c.receive_user_id = #{receiveUserId} </if>" +
             "<if test='startTime != null and startTime != \"\"'> AND c.create_time &gt; #{startTime} </if>" +
             "<if test='endTime != null and endTime != \"\"'> AND c.create_time &lt;= #{endTime} </if>" +
             "<if test='mobile != null and mobile != \"\"'> AND c.mobile LIKE CONCAT('%', #{mobile}, '%') </if>" +
             "<if test='remark != null and remark != \"\"'> AND c.remark LIKE CONCAT('%', #{remark}, '%') </if>" +
-            "<if test='callStatus != null and callStatus == 1'> AND EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr2 WHERE cdr2.customer_id = c.customer_id) </if>" +
-            "<if test='callStatus != null and callStatus == 2'> AND NOT EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr2 WHERE cdr2.customer_id = c.customer_id) </if>" +
+            "<if test='callStatus != null and callStatus == 1'> AND EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr WHERE cdr.customer_id = c.customer_id) </if>" +
+            "<if test='callStatus != null and callStatus == 2'> AND NOT EXISTS (SELECT 1 FROM ai_sip_call_outbound_cdr cdr WHERE cdr.customer_id = c.customer_id) </if>" +
             " ORDER BY c.customer_id DESC " +
             "</script>"})
     List<CrmCustomer> selectEffectiveCustomerList(

+ 12 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java

@@ -103,5 +103,17 @@ public class CrmCustomerListQueryParam extends BaseQueryParam
     /** 结束时间 */
     private String endTime;
 
+    private Integer attritionLevel;
+
+    private String intentionDegree;
+
+    /** AI外呼任务ID,用于“已在任务”筛选 */
+    private Long roboticId;
+
+    /** 任务客户筛选类型:notInTask=未在任务(可追加),inTask=已在任务(不可追加),null=全部 */
+    private String taskCustomerFilter;
+
+    /** 自定义标签模糊搜索(LIKE '%customTag%') */
+    private String customTag;
 
 }

+ 6 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerService.java

@@ -92,6 +92,12 @@ public interface ICrmCustomerService
 
     List<CrmCustomerListVO> selectCrmCustomerListQueryParam(CrmCustomerListQueryParam crmCustomer);
 
+    /**
+     * 为客户选择/追加客户列表回填今日/累计 × 手动/AI 的外呼次数(接通/总数)
+     * 采用二阶段批量聚合,请求数与页大小无关
+     */
+    void fillCallStats(List<CrmCustomerListVO> list);
+
     List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQuery(CrmMyCustomerListQueryParam param);
 
     List<CrmCustomerListQueryVO> selectCrmCustomerListQuery(CrmCustomerListQueryParam param);

+ 60 - 1
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java

@@ -19,6 +19,8 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyDeptMapper;
 import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.company.mapper.CrmCustomerCallLogMapper;
 import com.fs.crm.domain.*;
 import com.fs.crm.dto.CrmCustomerAssignCompanyDTO;
 import com.fs.crm.dto.CrmCustomerAssignUserDTO;
@@ -93,6 +95,12 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
     @Autowired
     private IWxSopExecuteService wxSopExecuteService;
 
+    @Autowired
+    private CrmCustomerCallLogMapper crmCustomerCallLogMapper;
+
+    @Autowired
+    private CompanyVoiceRoboticCallLogCallphoneMapper companyVoiceRoboticCallLogCallphoneMapper;
+
     /**
      * 查询客户
      *
@@ -303,6 +311,57 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
         return crmCustomerMapper.selectCrmCustomerListQueryParam(crmCustomer);
     }
 
+    @Override
+    public void fillCallStats(List<CrmCustomerListVO> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<Long> customerIds = list.stream()
+                .map(CrmCustomerListVO::getCustomerId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (customerIds.isEmpty()) {
+            return;
+        }
+        Map<Long, CustomerCallStatVO> manualToday = toStatMap(crmCustomerCallLogMapper.selectManualCallStatToday(customerIds));
+        Map<Long, CustomerCallStatVO> manualTotal = toStatMap(crmCustomerCallLogMapper.selectManualCallStatTotal(customerIds));
+        Map<Long, CustomerCallStatVO> aiToday = toStatMap(companyVoiceRoboticCallLogCallphoneMapper.selectAiCallStatToday(customerIds));
+        Map<Long, CustomerCallStatVO> aiTotal = toStatMap(companyVoiceRoboticCallLogCallphoneMapper.selectAiCallStatTotal(customerIds));
+
+        for (CrmCustomerListVO vo : list) {
+            Long id = vo.getCustomerId();
+            CustomerCallStatVO mt = id == null ? null : manualToday.get(id);
+            vo.setTodayManualTotalCount(mt == null || mt.getTotalCount() == null ? 0 : mt.getTotalCount());
+            vo.setTodayManualConnectCount(mt == null || mt.getConnectCount() == null ? 0 : mt.getConnectCount());
+
+            CustomerCallStatVO ml = id == null ? null : manualTotal.get(id);
+            vo.setTotalManualTotalCount(ml == null || ml.getTotalCount() == null ? 0 : ml.getTotalCount());
+            vo.setTotalManualConnectCount(ml == null || ml.getConnectCount() == null ? 0 : ml.getConnectCount());
+
+            CustomerCallStatVO at = id == null ? null : aiToday.get(id);
+            vo.setTodayAiTotalCount(at == null || at.getTotalCount() == null ? 0 : at.getTotalCount());
+            vo.setTodayAiConnectCount(at == null || at.getConnectCount() == null ? 0 : at.getConnectCount());
+
+            CustomerCallStatVO al = id == null ? null : aiTotal.get(id);
+            vo.setTotalAiTotalCount(al == null || al.getTotalCount() == null ? 0 : al.getTotalCount());
+            vo.setTotalAiConnectCount(al == null || al.getConnectCount() == null ? 0 : al.getConnectCount());
+        }
+    }
+
+    private Map<Long, CustomerCallStatVO> toStatMap(List<CustomerCallStatVO> stats) {
+        if (stats == null || stats.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<Long, CustomerCallStatVO> map = new HashMap<>(stats.size() * 2);
+        for (CustomerCallStatVO s : stats) {
+            if (s.getCustomerId() != null) {
+                map.put(s.getCustomerId(), s);
+            }
+        }
+        return map;
+    }
+
     @Override
     public List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQuery(CrmMyCustomerListQueryParam param) {
         return crmCustomerMapper.selectCrmMyCustomerListQuery(param);
@@ -587,7 +646,7 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
         List<CrmCustomer> batchList = new ArrayList<>();
         List<CrmCompanyLineCustomerImportParam> validParams = new ArrayList<>();
         List<String> phoneList = PubFun.listToNewList(list, e -> PhoneUtil.encryptPhone(e.getMobile()));
-        List<CrmCustomer> crmList = crmCustomerMapper.selectList(new QueryWrapper<CrmCustomer>().in("mobile", phoneList));
+        List<CrmCustomer> crmList = crmCustomerMapper.selectList(new QueryWrapper<CrmCustomer>().in("mobile", phoneList).eq("company_id",companyId));
 
 
         for (CrmCompanyLineCustomerImportParam customer : list) {

+ 24 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListVO.java

@@ -129,4 +129,28 @@ public class CrmCustomerListVO implements Serializable
     @Excel(name = "进线客户提交日期" )
     private String registerSubmitTime;
 
+    /** 今日手动外呼总数 */
+    private Integer todayManualTotalCount;
+
+    /** 今日手动外呼接通数(call_time>0) */
+    private Integer todayManualConnectCount;
+
+    /** 今日AI外呼总数 */
+    private Integer todayAiTotalCount;
+
+    /** 今日AI外呼接通数(call_time>0) */
+    private Integer todayAiConnectCount;
+
+    /** 累计手动外呼总数 */
+    private Integer totalManualTotalCount;
+
+    /** 累计手动外呼接通数(call_time>0) */
+    private Integer totalManualConnectCount;
+
+    /** 累计AI外呼总数 */
+    private Integer totalAiTotalCount;
+
+    /** 累计AI外呼接通数(call_time>0) */
+    private Integer totalAiConnectCount;
+
 }

+ 26 - 0
fs-service/src/main/java/com/fs/crm/vo/CustomerCallStatVO.java

@@ -0,0 +1,26 @@
+package com.fs.crm.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 客户外呼统计聚合结果(按 customer_id 分组)
+ *
+ * 用于客户选择/追加客户列表中按批量聚合方式回填
+ * 今日/累计 × 手动/AI 的外呼次数(接通/总数)。
+ */
+@Data
+public class CustomerCallStatVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 客户ID */
+    private Long customerId;
+
+    /** 总数 */
+    private Integer totalCount;
+
+    /** 接通数(call_time > 0) */
+    private Integer connectCount;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java

@@ -48,7 +48,7 @@ public interface QwGroupChatMapper
             "FROM " +
             "    qw_group_chat gc " +
             "LEFT JOIN qw_group_chat_user gcu ON gc.chat_id = gcu.chat_id  " +
-            " left join qw_user qu on gc.owner=qu.qw_user_id  and qu.is_del=0 and qu.corp_id=gc.corp_id " +
+            " left join qw_user qu on gc.owner=qu.qw_open_user_id  and qu.is_del=0 and qu.corp_id=gc.corp_id " +
             " left join company_user cu on cu.qw_user_id=qu.id " +
             "    AND gc.corp_id = gcu.corp_id  " +
             "<where> " +

+ 5 - 2
fs-service/src/main/resources/db/tenant-initTable.sql

@@ -2735,6 +2735,7 @@ CREATE TABLE `company_voice_robotic_call_log_callphone`
     `answered_ext_num` varchar(200) NULL DEFAULT NULL COMMENT '接听分机号码',
     `manual_answered_time` bigint NULL DEFAULT NULL COMMENT '人工接听时间',
     `manual_answered_time_len` bigint NULL DEFAULT NULL COMMENT '人工接听时长',
+    `hangup_type` varchar(255) NULL DEFAULT NULL COMMENT '挂断类型',
     PRIMARY KEY (`log_id`) USING BTREE,
     INDEX              `robotic_id_and_caller_id_idx`(`robotic_id`, `caller_id`) USING BTREE,
     INDEX              `company_and_company_user_idx`(`company_id`, `company_user_id`) USING BTREE,
@@ -2803,7 +2804,8 @@ CREATE TABLE `company_voice_robotic_callees`
     INDEX            `idx_phone_is_generate`(`phone`, `is_generate`) USING BTREE,
     INDEX            `idx_user_name`(`user_name`) USING BTREE,
     INDEX            `idx_phone`(`phone`) USING BTREE,
-    INDEX            `idx_robotic_id`(`robotic_id`) USING BTREE
+    INDEX            `idx_robotic_id`(`robotic_id`) USING BTREE,
+    INDEX            `idx_user_id`(`user_id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务外呼电话' ROW_FORMAT = DYNAMIC;
 
 -- ----------------------------
@@ -3083,7 +3085,8 @@ CREATE TABLE `crm_customer`
     INDEX                   `customer_user_id`(`customer_user_id`) USING BTREE,
     INDEX                   `company_id`(`company_id`) USING BTREE,
     INDEX                   `is_line`(`is_line`) USING BTREE,
-    INDEX                   `mobile`(`mobile`) USING BTREE
+    INDEX                   `mobile`(`mobile`) USING BTREE,
+    INDEX                   `company_receive_user_idx`(`company_id`, `receive_user_id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '客户表' ROW_FORMAT = DYNAMIC;
 
 -- ----------------------------

+ 1 - 1
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml

@@ -24,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectAiSipCallOutboundCdrVo">
-        select id, caller, opnum, callee, start_time, answered_time, end_time, uuid, call_type, time_len, time_len_valid, record_filename, chat_content, hangup_cause, source_type, customer_id,wavfile from ai_sip_call_outbound_cdr
+        select id, caller, opnum, callee, start_time, answered_time, end_time, uuid, call_type, time_len, time_len_valid, record_filename, chat_content, hangup_cause, source_type, customer_id from ai_sip_call_outbound_cdr
     </sql>
 
     <select id="selectAiSipCallOutboundCdrList" parameterType="AiSipCallOutboundCdr" resultMap="AiSipCallOutboundCdrResult">

+ 2 - 1
fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecLogMapper.xml

@@ -165,7 +165,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectCallContent" resultType="com.fs.company.vo.CallContentVO" >
         SELECT
             t1.id as logId,
-            t2.content_list as callContent
+            t2.content_list as callContent,
+            t2.record_path as recordPath
         FROM
             company_ai_workflow_exec_log t1
                 INNER JOIN company_voice_robotic_call_log_callphone t2

+ 3 - 0
fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml

@@ -205,6 +205,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="customerPhone != null and customerPhone != ''">
             AND c.phone LIKE CONCAT('%', #{customerPhone}, '%')
         </if>
+        <if test="encryptPhone != null and encryptPhone != ''">
+            AND c.phone = #{encryptPhone}
+        </if>
         <if test="onlyCallNode != null and onlyCallNode == true">
             AND e.status = 11
         </if>

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

@@ -312,4 +312,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by t1.handle_flag asc, t1.create_time desc
     </select>
 
+    <!-- 按 customer_id(= callees.user_id)批量统计今日 AI 外呼(总数 / 接通数) -->
+    <select id="selectAiCallStatToday" resultType="com.fs.crm.vo.CustomerCallStatVO">
+        SELECT
+            ce.user_id   AS customerId,
+            COUNT(1)     AS totalCount,
+            SUM(CASE WHEN aic.call_time &gt; 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM company_voice_robotic_call_log_callphone aic
+        INNER JOIN company_voice_robotic_callees ce ON ce.id = aic.caller_id
+        WHERE ce.user_id IN
+        <foreach collection="customerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        AND DATE(aic.create_time) = CURDATE()
+        GROUP BY ce.user_id
+    </select>
+
+    <!-- 按 customer_id(= callees.user_id)批量统计累计 AI 外呼(总数 / 接通数) -->
+    <select id="selectAiCallStatTotal" resultType="com.fs.crm.vo.CustomerCallStatVO">
+        SELECT
+            ce.user_id   AS customerId,
+            COUNT(1)     AS totalCount,
+            SUM(CASE WHEN aic.call_time &gt; 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM company_voice_robotic_call_log_callphone aic
+        INNER JOIN company_voice_robotic_callees ce ON ce.id = aic.caller_id
+        WHERE ce.user_id IN
+        <foreach collection="customerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        GROUP BY ce.user_id
+    </select>
+
 </mapper>

+ 28 - 0
fs-service/src/main/resources/mapper/company/CrmCustomerCallLogMapper.xml

@@ -121,5 +121,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
     </select>
 
+    <!-- 按 customer_id 批量统计今日手动外呼(总数 / 接通数) -->
+    <select id="selectManualCallStatToday" resultType="com.fs.crm.vo.CustomerCallStatVO">
+        SELECT
+            customer_id      AS customerId,
+            COUNT(1)         AS totalCount,
+            SUM(CASE WHEN call_time &gt; 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM crm_customer_call_log
+        WHERE customer_id IN
+        <foreach collection="customerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        AND DATE(create_time) = CURDATE()
+        GROUP BY customer_id
+    </select>
+
+    <!-- 按 customer_id 批量统计累计手动外呼(总数 / 接通数) -->
+    <select id="selectManualCallStatTotal" resultType="com.fs.crm.vo.CustomerCallStatVO">
+        SELECT
+            customer_id      AS customerId,
+            COUNT(1)         AS totalCount,
+            SUM(CASE WHEN call_time &gt; 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM crm_customer_call_log
+        WHERE customer_id IN
+        <foreach collection="customerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        GROUP BY customer_id
+    </select>
 
 </mapper>